feat: convert browser-tools/core.js to TypeScript, add c8 test coverage

- Convert browser-tools/core.js (1058 lines) to native TypeScript with
  full type annotations from the existing .d.ts file. Remove the
  separate .d.ts declaration file (types are now inline).
- Add c8 test coverage reporting: `npm run test:coverage` generates
  text + lcov reports with 50% statement threshold baseline.
- Add coverage/ to .gitignore

All 712 unit tests, 63 browser-tools tests, and 11 integration tests
pass with zero regressions.
This commit is contained in:
Jeremy McSpadden 2026-03-16 13:25:52 -05:00
parent 2c926c12e3
commit 9c8a24042f
4 changed files with 971 additions and 382 deletions

640
package-lock.json generated
View file

@ -55,6 +55,7 @@
"devDependencies": {
"@types/node": "^22.0.0",
"@types/picomatch": "^4.0.2",
"c8": "^11.0.0",
"jiti": "^2.6.1",
"typescript": "^5.4.0"
},
@ -795,6 +796,16 @@
"node": ">=6.9.0"
}
},
"node_modules/@bcoe/v8-coverage": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
"integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@borewit/text-codec": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz",
@ -1407,6 +1418,44 @@
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@istanbuljs/schema": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
"integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@mariozechner/jiti": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/@mariozechner/jiti/-/jiti-2.6.5.tgz",
@ -2207,6 +2256,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
"integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/mime-types": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz",
@ -2320,6 +2376,22 @@
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/ast-types": {
"version": "0.13.4",
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
@ -2412,6 +2484,40 @@
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/c8": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/c8/-/c8-11.0.0.tgz",
"integrity": "sha512-e/uRViGHSVIJv7zsaDKM7VRn2390TgHXqUSvYwPHBQaU6L7E9L0n9JbdkwdYPvshDT0KymBmmlwSpms3yBaMNg==",
"dev": true,
"license": "ISC",
"dependencies": {
"@bcoe/v8-coverage": "^1.0.1",
"@istanbuljs/schema": "^0.1.3",
"find-up": "^5.0.0",
"foreground-child": "^3.1.1",
"istanbul-lib-coverage": "^3.2.0",
"istanbul-lib-report": "^3.0.1",
"istanbul-reports": "^3.1.6",
"test-exclude": "^8.0.0",
"v8-to-istanbul": "^9.0.0",
"yargs": "^17.7.2",
"yargs-parser": "^21.1.1"
},
"bin": {
"c8": "bin/c8.js"
},
"engines": {
"node": "20 || >=22"
},
"peerDependencies": {
"monocart-coverage-reports": "^2"
},
"peerDependenciesMeta": {
"monocart-coverage-reports": {
"optional": true
}
}
},
"node_modules/chalk": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
@ -2424,6 +2530,86 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/cliui/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true,
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
@ -2491,6 +2677,13 @@
"safe-buffer": "^5.0.1"
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
@ -2500,6 +2693,16 @@
"once": "^1.4.0"
}
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/escodegen": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
@ -2684,6 +2887,53 @@
"url": "https://github.com/sindresorhus/file-type?sponsor=1"
}
},
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
"dev": true,
"license": "MIT",
"dependencies": {
"locate-path": "^6.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
"dev": true,
"license": "ISC",
"dependencies": {
"cross-spawn": "^7.0.6",
"signal-exit": "^4.0.1"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/foreground-child/node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
@ -2738,6 +2988,16 @@
"node": ">=18"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-east-asian-width": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz",
@ -2837,6 +3097,16 @@
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/hosted-git-info": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz",
@ -2849,6 +3119,13 @@
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/html-escaper": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true,
"license": "MIT"
},
"node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
@ -2913,6 +3190,62 @@
"node": ">= 12"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC"
},
"node_modules/istanbul-lib-coverage": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
"integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=8"
}
},
"node_modules/istanbul-lib-report": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
"integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"istanbul-lib-coverage": "^3.0.0",
"make-dir": "^4.0.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/istanbul-reports": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
"integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"html-escaper": "^2.0.0",
"istanbul-lib-report": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/jiti": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
@ -2983,6 +3316,22 @@
"url": "https://liberapay.com/Koromix"
}
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-locate": "^5.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
@ -2998,6 +3347,22 @@
"node": "20 || >=22"
}
},
"node_modules/make-dir": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
"integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
"dev": true,
"license": "MIT",
"dependencies": {
"semver": "^7.5.3"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/marked": {
"version": "15.0.12",
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
@ -3142,6 +3507,38 @@
}
}
},
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"yocto-queue": "^0.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-locate": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-limit": "^3.0.2"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-retry": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
@ -3187,6 +3584,16 @@
"node": ">= 14"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/path-expression-matcher": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz",
@ -3202,6 +3609,16 @@
"node": ">=14.0.0"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/path-scurry": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
@ -3374,6 +3791,16 @@
"once": "^1.3.1"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
@ -3468,6 +3895,29 @@
"@img/sharp-win32-x64": "0.34.5"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
@ -3540,6 +3990,44 @@
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
"license": "MIT"
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string-width/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/string-width/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
@ -3583,6 +4071,34 @@
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/test-exclude": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-8.0.0.tgz",
"integrity": "sha512-ZOffsNrXYggvU1mDGHk54I96r26P8SyMjO5slMKSc7+IWmtB/MQKnEC2fP51imB3/pT6YK5cT5E8f+Dd9KdyOQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"@istanbuljs/schema": "^0.1.2",
"glob": "^13.0.6",
"minimatch": "^10.2.2"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/token-types": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz",
@ -3654,6 +4170,21 @@
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"license": "MIT"
},
"node_modules/v8-to-istanbul": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
"integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==",
"dev": true,
"license": "ISC",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.12",
"@types/istanbul-lib-coverage": "^2.0.1",
"convert-source-map": "^2.0.0"
},
"engines": {
"node": ">=10.12.0"
}
},
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
@ -3663,6 +4194,63 @@
"node": ">= 8"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@ -3690,6 +4278,16 @@
}
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/yaml": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
@ -3705,6 +4303,35 @@
"url": "https://github.com/sponsors/eemeli"
}
},
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.1.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
@ -3715,6 +4342,19 @@
"fd-slicer": "~1.1.0"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/yoctocolors": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz",

View file

@ -48,6 +48,7 @@
"copy-themes": "node -e \"const{mkdirSync,cpSync}=require('fs');const{resolve}=require('path');const src=resolve(__dirname,'packages/pi-coding-agent/dist/modes/interactive/theme');mkdirSync('pkg/dist/modes/interactive/theme',{recursive:true});cpSync(src,'pkg/dist/modes/interactive/theme',{recursive:true})\"",
"copy-export-html": "node -e \"const{mkdirSync,cpSync}=require('fs');const{resolve}=require('path');const src=resolve(__dirname,'packages/pi-coding-agent/dist/core/export-html');mkdirSync('pkg/dist/core/export-html',{recursive:true});cpSync(src,'pkg/dist/core/export-html',{recursive:true})\"",
"test:unit": "node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/*.test.ts src/resources/extensions/gsd/tests/*.test.mjs src/tests/*.test.ts",
"test:coverage": "c8 --reporter=text --reporter=lcov --exclude='src/resources/extensions/gsd/tests/**' --exclude='src/tests/**' --exclude='scripts/**' --exclude='native/**' --exclude='node_modules/**' --check-coverage --statements=40 --lines=40 --branches=0 --functions=0 node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/*.test.ts src/resources/extensions/gsd/tests/*.test.mjs src/tests/*.test.ts",
"test:integration": "node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/*integration*.test.ts src/tests/integration/*.test.ts",
"test": "npm run test:unit && npm run test:integration",
"test:browser-tools": "node --test src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs src/resources/extensions/browser-tools/tests/browser-tools-integration.test.mjs",
@ -103,14 +104,15 @@
"devDependencies": {
"@types/node": "^22.0.0",
"@types/picomatch": "^4.0.2",
"c8": "^11.0.0",
"jiti": "^2.6.1",
"typescript": "^5.4.0"
},
"optionalDependencies": {
"@gsd-build/engine-darwin-arm64": ">=2.10.2",
"@gsd-build/engine-darwin-x64": ">=2.10.2",
"@gsd-build/engine-linux-x64-gnu": ">=2.10.2",
"@gsd-build/engine-linux-arm64-gnu": ">=2.10.2",
"@gsd-build/engine-linux-x64-gnu": ">=2.10.2",
"@gsd-build/engine-win32-x64-msvc": ">=2.10.2",
"fsevents": "~2.3.3",
"koffi": "^2.9.0"

View file

@ -1,205 +0,0 @@
/**
* Type declarations for core.js runtime-neutral helper logic for browser-tools.
*/
export interface ActionTimeline {
limit: number;
nextId: number;
entries: ActionEntry[];
}
export interface ActionEntry {
id: number;
tool: string;
paramsSummary: string;
startedAt: number;
finishedAt: number | null;
status: string;
beforeUrl: string;
afterUrl: string;
verificationSummary?: string;
warningSummary?: string;
diffSummary?: string;
changed?: boolean;
error?: string;
}
export interface ActionPartial {
tool: string;
paramsSummary?: string;
startedAt?: number;
beforeUrl?: string;
afterUrl?: string;
verificationSummary?: string;
warningSummary?: string;
diffSummary?: string;
changed?: boolean;
error?: string;
}
export interface ActionUpdates {
finishedAt?: number;
status?: string;
afterUrl?: string;
verificationSummary?: string;
warningSummary?: string;
diffSummary?: string;
changed?: boolean;
error?: string;
}
export interface DiffResult {
changed: boolean;
changes: Array<{ type: string; before: unknown; after: unknown }>;
summary: string;
}
export interface Threshold {
op: string;
n: number;
}
export interface PageRegistry {
pages: PageEntry[];
activePageId: number | null;
nextId: number;
}
export interface PageEntry {
id: number;
page: any;
title: string;
url: string;
opener: number | null;
}
export interface PageListEntry {
id: number;
title: string;
url: string;
opener: number | null;
isActive: boolean;
}
export interface SnapshotModeConfig {
tags: string[];
roles: string[];
selectors: string[];
ariaAttributes: string[];
useInteractiveFilter: boolean;
visibleOnly?: boolean;
containerExpand?: boolean;
}
export interface AssertionCheckResult {
name: string;
passed: boolean;
actual: unknown;
expected: unknown;
selector?: string;
text?: string;
}
export interface AssertionEvaluation {
verified: boolean;
checks: AssertionCheckResult[];
summary: string;
agentHint: string;
}
export interface WaitValidationError {
error: string;
}
export interface BatchStepResult {
ok: boolean;
stopReason: string | null;
failedStepIndex: number | null;
stepResults: unknown[];
summary: string;
}
export interface FormattedTimeline {
entries: Array<{
id: number | null;
tool: string;
status: string;
durationMs: number | null;
beforeUrl: string;
afterUrl: string;
line: string;
}>;
retained: number;
totalRecorded: number;
bounded: boolean;
summary: string;
}
export interface FailureHypothesis {
hasFailures: boolean;
categories: string[];
summary: string;
signals: Array<{ category: string; source: string; detail: string }>;
}
export interface SessionSummary {
counts: {
pages: number;
actions: { total: number; retained: number; success: number; error: number; running: number };
waits: { total: number; success: number; error: number; running: number };
assertions: { total: number; passed: number; failed: number; running: number };
consoleErrors: number;
failedRequests: number;
dialogs: number;
};
activePage: { id: number | null; title: string; url: string } | null;
caveats: string[];
failureHypothesis: FailureHypothesis;
summary: string;
}
export function createActionTimeline(limit?: number): ActionTimeline;
export function beginAction(timeline: ActionTimeline, partial: ActionPartial): ActionEntry;
export function finishAction(timeline: ActionTimeline, actionId: number, updates?: ActionUpdates): ActionEntry | null;
export function findAction(timeline: ActionTimeline, actionId: number): ActionEntry | null;
export function toActionParamsSummary(params: unknown): string;
export function diffCompactStates(before: unknown, after: unknown): DiffResult;
export function includesNeedle(haystack: string, needle: string): boolean;
export function parseThreshold(value: string | null | undefined): Threshold | null;
export function meetsThreshold(count: number, threshold: Threshold): boolean;
export function getEntriesSince(
entries: Array<{ timestamp?: number }>,
sinceActionId: number | undefined,
timeline: ActionTimeline,
): unknown[];
export function evaluateAssertionChecks(args: { checks: unknown[]; state: unknown }): AssertionEvaluation;
export function validateWaitParams(params: { condition: string; value?: string; threshold?: string }): WaitValidationError | null;
export function createRegionStableScript(selector: string): string;
export function createPageRegistry(): PageRegistry;
export function registryAddPage(
registry: PageRegistry,
info: { page: unknown; title?: string; url?: string; opener?: number | null },
): PageEntry;
export function registryRemovePage(registry: PageRegistry, pageId: number): { removed: PageEntry; newActiveId: number | null };
export function registrySetActive(registry: PageRegistry, pageId: number): void;
export function registryGetActive(registry: PageRegistry): PageEntry;
export function registryGetPage(registry: PageRegistry, pageId: number): PageEntry | null;
export function registryListPages(registry: PageRegistry): PageListEntry[];
export function createBoundedLogPusher(maxSize: number): (array: unknown[], entry: unknown) => void;
export function runBatchSteps(args: {
steps: unknown[];
executeStep: (step: unknown, index: number) => Promise<{ ok: boolean; [key: string]: unknown }>;
stopOnFailure?: boolean;
}): Promise<BatchStepResult>;
export declare const SNAPSHOT_MODES: Record<string, SnapshotModeConfig>;
export function getSnapshotModeConfig(mode: string): SnapshotModeConfig | null;
export function computeContentHash(text: string): string;
export function computeStructuralSignature(tag: string, role: string, childTags: string[]): string;
export function matchFingerprint(
stored: { contentHash?: string; structuralSignature?: string },
candidate: { contentHash?: string; structuralSignature?: string },
): boolean;
export function formatTimelineEntries(entries?: unknown[], options?: Record<string, unknown>): FormattedTimeline;
export function buildFailureHypothesis(session?: Record<string, unknown>): FailureHypothesis;
export function summarizeBrowserSession(session?: Record<string, unknown>): SessionSummary;

View file

@ -4,7 +4,171 @@
* Kept free of pi-specific imports so it can be exercised with node:test.
*/
export function createActionTimeline(limit = 60) {
// ---------------------------------------------------------------------------
// Interfaces & Types
// ---------------------------------------------------------------------------
export interface ActionTimeline {
limit: number;
nextId: number;
entries: ActionEntry[];
}
export interface ActionEntry {
id: number;
tool: string;
paramsSummary: string;
startedAt: number;
finishedAt: number | null;
status: string;
beforeUrl: string;
afterUrl: string;
verificationSummary?: string;
warningSummary?: string;
diffSummary?: string;
changed?: boolean;
error?: string;
}
export interface ActionPartial {
tool: string;
paramsSummary?: string;
startedAt?: number;
beforeUrl?: string;
afterUrl?: string;
verificationSummary?: string;
warningSummary?: string;
diffSummary?: string;
changed?: boolean;
error?: string;
}
export interface ActionUpdates {
finishedAt?: number;
status?: string;
afterUrl?: string;
verificationSummary?: string;
warningSummary?: string;
diffSummary?: string;
changed?: boolean;
error?: string;
}
export interface DiffResult {
changed: boolean;
changes: Array<{ type: string; before: unknown; after: unknown }>;
summary: string;
}
export interface Threshold {
op: string;
n: number;
}
export interface PageRegistry {
pages: PageEntry[];
activePageId: number | null;
nextId: number;
}
export interface PageEntry {
id: number;
page: unknown;
title: string;
url: string;
opener: number | null;
}
export interface PageListEntry {
id: number;
title: string;
url: string;
opener: number | null;
isActive: boolean;
}
export interface SnapshotModeConfig {
tags: string[];
roles: string[];
selectors: string[];
ariaAttributes: string[];
useInteractiveFilter: boolean;
visibleOnly?: boolean;
containerExpand?: boolean;
}
export interface AssertionCheckResult {
name: string;
passed: boolean;
actual: unknown;
expected: unknown;
selector?: string;
text?: string;
}
export interface AssertionEvaluation {
verified: boolean;
checks: AssertionCheckResult[];
summary: string;
agentHint: string;
}
export interface WaitValidationError {
error: string;
}
export interface BatchStepResult {
ok: boolean;
stopReason: string | null;
failedStepIndex: number | null;
stepResults: unknown[];
summary: string;
}
export interface FormattedTimeline {
entries: Array<{
id: number | null;
tool: string;
status: string;
durationMs: number | null;
beforeUrl: string;
afterUrl: string;
line: string;
}>;
retained: number;
totalRecorded: number;
bounded: boolean;
summary: string;
}
export interface FailureHypothesis {
hasFailures: boolean;
categories: string[];
summary: string;
signals: Array<{ category: string; source: string; detail: string }>;
}
export interface SessionSummary {
counts: {
pages: number;
actions: { total: number; retained: number; success: number; error: number; running: number };
waits: { total: number; success: number; error: number; running: number };
assertions: { total: number; passed: number; failed: number; running: number };
consoleErrors: number;
failedRequests: number;
dialogs: number;
};
activePage: { id: number | null; title: string; url: string } | null;
caveats: string[];
failureHypothesis: FailureHypothesis;
summary: string;
}
// ---------------------------------------------------------------------------
// Action Timeline
// ---------------------------------------------------------------------------
export function createActionTimeline(limit = 60): ActionTimeline {
return {
limit,
nextId: 1,
@ -12,8 +176,8 @@ export function createActionTimeline(limit = 60) {
};
}
export function beginAction(timeline, partial) {
const entry = {
export function beginAction(timeline: ActionTimeline, partial: ActionPartial): ActionEntry {
const entry: ActionEntry = {
id: timeline.nextId++,
tool: partial.tool,
paramsSummary: partial.paramsSummary ?? "",
@ -35,7 +199,7 @@ export function beginAction(timeline, partial) {
return entry;
}
export function finishAction(timeline, actionId, updates = {}) {
export function finishAction(timeline: ActionTimeline, actionId: number, updates: ActionUpdates = {}): ActionEntry | null {
const entry = timeline.entries.find((item) => item.id === actionId);
if (!entry) return null;
Object.assign(entry, updates, {
@ -51,14 +215,14 @@ export function finishAction(timeline, actionId, updates = {}) {
return entry;
}
export function findAction(timeline, actionId) {
export function findAction(timeline: ActionTimeline, actionId: number): ActionEntry | null {
return timeline.entries.find((item) => item.id === actionId) ?? null;
}
export function toActionParamsSummary(params) {
export function toActionParamsSummary(params: unknown): string {
if (!params || typeof params !== "object") return "";
const entries = [];
for (const [key, value] of Object.entries(params)) {
const entries: string[] = [];
for (const [key, value] of Object.entries(params as Record<string, unknown>)) {
if (value === undefined || value === null) continue;
if (typeof value === "string") {
entries.push(`${key}=${JSON.stringify(value.length > 60 ? `${value.slice(0, 57)}...` : value)}`);
@ -77,8 +241,22 @@ export function toActionParamsSummary(params) {
return entries.slice(0, 6).join(", ");
}
export function diffCompactStates(before, after) {
const changes = [];
// ---------------------------------------------------------------------------
// Compact State Diffing
// ---------------------------------------------------------------------------
interface CompactStateForDiff {
url?: string;
title?: string;
focus?: string;
dialog?: { count?: number; title?: string };
counts?: Record<string, number>;
headings?: string[];
bodyText?: string;
}
export function diffCompactStates(before: CompactStateForDiff | null | undefined, after: CompactStateForDiff | null | undefined): DiffResult {
const changes: Array<{ type: string; before: unknown; after: unknown }> = [];
if (!before || !after) {
return {
changed: false,
@ -159,11 +337,15 @@ export function diffCompactStates(before, after) {
return { changed, changes, summary };
}
function normalizeString(value) {
// ---------------------------------------------------------------------------
// String helpers
// ---------------------------------------------------------------------------
function normalizeString(value: unknown): string {
return String(value ?? "").trim();
}
export function includesNeedle(haystack, needle) {
export function includesNeedle(haystack: string, needle: string): boolean {
return normalizeString(haystack).toLowerCase().includes(normalizeString(needle).toLowerCase());
}
@ -173,10 +355,8 @@ export function includesNeedle(haystack, needle) {
/**
* Parse a threshold expression like ">=3", "==0", "<5", or bare "3" (defaults to ">=").
* @param {string} value
* @returns {{ op: string, n: number } | null} null if malformed
*/
export function parseThreshold(value) {
export function parseThreshold(value: string | null | undefined): Threshold | null {
if (value == null) return null;
const str = String(value).trim();
if (str === "") return null;
@ -189,11 +369,8 @@ export function parseThreshold(value) {
/**
* Evaluate whether a count meets a parsed threshold.
* @param {number} count
* @param {{ op: string, n: number }} threshold
* @returns {boolean}
*/
export function meetsThreshold(count, threshold) {
export function meetsThreshold(count: number, threshold: Threshold): boolean {
switch (threshold.op) {
case ">=": return count >= threshold.n;
case "<=": return count <= threshold.n;
@ -207,12 +384,12 @@ export function meetsThreshold(count, threshold) {
/**
* Filter entries that occurred at or after a given action's start time.
* If sinceActionId is missing or the action isn't found, returns all entries.
* @param {Array<{ timestamp?: number }>} entries
* @param {number | undefined} sinceActionId
* @param {{ entries: Array<{ id: number, startedAt: number }> }} timeline
* @returns {Array}
*/
export function getEntriesSince(entries, sinceActionId, timeline) {
export function getEntriesSince(
entries: Array<{ timestamp?: number }>,
sinceActionId: number | undefined,
timeline: ActionTimeline,
): Array<{ timestamp?: number }> {
if (!entries || !Array.isArray(entries)) return [];
if (sinceActionId == null || !timeline) return entries;
const action = findAction(timeline, sinceActionId);
@ -221,8 +398,34 @@ export function getEntriesSince(entries, sinceActionId, timeline) {
return entries.filter((e) => (e.timestamp ?? 0) >= since);
}
export function evaluateAssertionChecks({ checks, state }) {
const results = [];
// ---------------------------------------------------------------------------
// Assertion Evaluation
// ---------------------------------------------------------------------------
interface AssertionCheckInput {
kind: string;
selector?: string;
value?: string;
text?: string;
checked?: boolean;
sinceActionId?: number;
}
interface AssertionState {
url?: string;
title?: string;
bodyText?: string;
focus?: string;
selectorStates?: Record<string, { visible?: boolean; value?: string; checked?: boolean | null }>;
consoleEntries?: Array<{ type?: string; text?: string; message?: string; timestamp?: number }>;
networkEntries?: Array<{ type?: string; url?: string; status?: number; failed?: boolean; timestamp?: number }>;
allConsoleEntries?: Array<{ type?: string; text?: string; message?: string; timestamp?: number }>;
allNetworkEntries?: Array<{ type?: string; url?: string; status?: number; failed?: boolean; timestamp?: number }>;
actionTimeline?: ActionTimeline | null;
}
export function evaluateAssertionChecks({ checks, state }: { checks: AssertionCheckInput[]; state: AssertionState }): AssertionEvaluation {
const results: AssertionCheckResult[] = [];
const selectorStates = state.selectorStates ?? {};
const consoleEntries = state.consoleEntries ?? [];
const networkEntries = state.networkEntries ?? [];
@ -233,29 +436,29 @@ export function evaluateAssertionChecks({ checks, state }) {
for (const check of checks) {
const selectorState = check.selector ? selectorStates[check.selector] ?? null : null;
let passed = false;
let actual;
let expected;
let actual: unknown;
let expected: unknown;
switch (check.kind) {
case "url_contains":
actual = state.url ?? "";
expected = check.value ?? "";
passed = includesNeedle(actual, expected);
passed = includesNeedle(actual as string, expected as string);
break;
case "title_contains":
actual = state.title ?? "";
expected = check.value ?? "";
passed = includesNeedle(actual, expected);
passed = includesNeedle(actual as string, expected as string);
break;
case "text_visible":
actual = state.bodyText ?? "";
expected = check.text ?? "";
passed = includesNeedle(actual, expected);
passed = includesNeedle(actual as string, expected as string);
break;
case "text_not_visible":
actual = state.bodyText ?? "";
expected = check.text ?? "";
passed = !includesNeedle(actual, expected);
passed = !includesNeedle(actual as string, expected as string);
break;
case "selector_visible":
actual = selectorState?.visible ?? false;
@ -275,12 +478,12 @@ export function evaluateAssertionChecks({ checks, state }) {
case "value_contains":
actual = selectorState?.value ?? "";
expected = check.value ?? "";
passed = includesNeedle(actual, expected);
passed = includesNeedle(actual as string, expected as string);
break;
case "focused_matches":
actual = state.focus ?? "";
expected = check.value ?? "";
passed = includesNeedle(actual, expected);
passed = includesNeedle(actual as string, expected as string);
break;
case "checked_equals":
actual = selectorState?.checked ?? null;
@ -301,8 +504,8 @@ export function evaluateAssertionChecks({ checks, state }) {
// --- S02: New structured network/console assertion kinds ---
case "request_url_seen": {
const filtered = getEntriesSince(allNetworkEntries, check.sinceActionId, actionTimeline);
const matches = filtered.filter((e) => includesNeedle(e.url ?? "", check.text ?? ""));
const filtered = getEntriesSince(allNetworkEntries, check.sinceActionId, actionTimeline!);
const matches = (filtered as typeof allNetworkEntries).filter((e) => includesNeedle(e.url ?? "", check.text ?? ""));
actual = matches.length > 0;
expected = true;
passed = actual === true;
@ -310,9 +513,9 @@ export function evaluateAssertionChecks({ checks, state }) {
}
case "response_status": {
const filtered = getEntriesSince(allNetworkEntries, check.sinceActionId, actionTimeline);
const statusNum = parseInt(check.value, 10);
const matches = filtered.filter(
const filtered = getEntriesSince(allNetworkEntries, check.sinceActionId, actionTimeline!);
const statusNum = parseInt(check.value!, 10);
const matches = (filtered as typeof allNetworkEntries).filter(
(e) => includesNeedle(e.url ?? "", check.text ?? "") && typeof e.status === "number" && e.status === statusNum
);
actual = matches.length > 0 ? `found (status=${matches[0].status})` : `not found`;
@ -322,8 +525,8 @@ export function evaluateAssertionChecks({ checks, state }) {
}
case "console_message_matches": {
const filtered = getEntriesSince(allConsoleEntries, check.sinceActionId, actionTimeline);
const matches = filtered.filter((e) => includesNeedle(e.text ?? "", check.text ?? ""));
const filtered = getEntriesSince(allConsoleEntries, check.sinceActionId, actionTimeline!);
const matches = (filtered as typeof allConsoleEntries).filter((e) => includesNeedle(e.text ?? "", check.text ?? ""));
actual = matches.length > 0;
expected = true;
passed = actual === true;
@ -331,8 +534,8 @@ export function evaluateAssertionChecks({ checks, state }) {
}
case "network_count": {
const filtered = getEntriesSince(allNetworkEntries, check.sinceActionId, actionTimeline);
const matches = filtered.filter((e) => includesNeedle(e.url ?? "", check.text ?? ""));
const filtered = getEntriesSince(allNetworkEntries, check.sinceActionId, actionTimeline!);
const matches = (filtered as typeof allNetworkEntries).filter((e) => includesNeedle(e.url ?? "", check.text ?? ""));
const threshold = parseThreshold(check.value);
if (!threshold) {
actual = `invalid threshold: ${check.value}`;
@ -347,8 +550,8 @@ export function evaluateAssertionChecks({ checks, state }) {
}
case "console_count": {
const filtered = getEntriesSince(allConsoleEntries, check.sinceActionId, actionTimeline);
const matches = filtered.filter((e) => includesNeedle(e.text ?? "", check.text ?? ""));
const filtered = getEntriesSince(allConsoleEntries, check.sinceActionId, actionTimeline!);
const matches = (filtered as typeof allConsoleEntries).filter((e) => includesNeedle(e.text ?? "", check.text ?? ""));
const threshold = parseThreshold(check.value);
if (!threshold) {
actual = `invalid threshold: ${check.value}`;
@ -363,8 +566,8 @@ export function evaluateAssertionChecks({ checks, state }) {
}
case "no_console_errors_since": {
const filtered = getEntriesSince(allConsoleEntries, check.sinceActionId, actionTimeline);
const errors = filtered.filter((e) => e.type === "error" || e.type === "pageerror");
const filtered = getEntriesSince(allConsoleEntries, check.sinceActionId, actionTimeline!);
const errors = (filtered as typeof allConsoleEntries).filter((e) => e.type === "error" || e.type === "pageerror");
actual = errors.length;
expected = 0;
passed = errors.length === 0;
@ -372,8 +575,8 @@ export function evaluateAssertionChecks({ checks, state }) {
}
case "no_failed_requests_since": {
const filtered = getEntriesSince(allNetworkEntries, check.sinceActionId, actionTimeline);
const failures = filtered.filter((e) => e.failed || (typeof e.status === "number" && e.status >= 400));
const filtered = getEntriesSince(allNetworkEntries, check.sinceActionId, actionTimeline!);
const failures = (filtered as typeof allNetworkEntries).filter((e) => e.failed || (typeof e.status === "number" && e.status >= 400));
actual = failures.length;
expected = 0;
passed = failures.length === 0;
@ -417,11 +620,16 @@ export function evaluateAssertionChecks({ checks, state }) {
// Wait-condition validation
// ---------------------------------------------------------------------------
interface WaitConditionSpec {
needsValue: boolean;
valueLabel: string;
needsThreshold?: boolean;
}
/**
* All recognized wait conditions with their parameter requirements.
* Each entry: { needsValue: bool, valueLabel: string, needsThreshold?: bool }
*/
const WAIT_CONDITIONS = {
const WAIT_CONDITIONS: Record<string, WaitConditionSpec> = {
// Existing 5 conditions
selector_visible: { needsValue: true, valueLabel: "CSS selector" },
selector_hidden: { needsValue: true, valueLabel: "CSS selector" },
@ -440,10 +648,8 @@ const WAIT_CONDITIONS = {
/**
* Validate parameters for a browser_wait_for condition.
* @param {{ condition: string, value?: string, threshold?: string }} params
* @returns {null | { error: string }} null if valid, structured error otherwise
*/
export function validateWaitParams(params) {
export function validateWaitParams(params: { condition: string; value?: string; threshold?: string }): WaitValidationError | null {
const { condition, value, threshold } = params ?? {};
if (!condition) {
@ -477,14 +683,8 @@ export function validateWaitParams(params) {
/**
* Generate a JS expression string for page.waitForFunction() that detects
* DOM stability by comparing snapshot hashes across polling intervals.
*
* The script stores a snapshot on a namespaced window key. When the snapshot
* matches the previous value, the region is considered stable.
*
* @param {string} selector CSS selector for the target element
* @returns {string} self-contained JS function body suitable for waitForFunction
*/
export function createRegionStableScript(selector) {
export function createRegionStableScript(selector: string): string {
// Create a stable key from the selector (simple hash to avoid special chars)
const safeKey = Array.from(selector).reduce((h, c) => ((h << 5) - h + c.charCodeAt(0)) | 0, 0) >>> 0;
const windowKey = `__pw_region_stable_${safeKey}`;
@ -504,40 +704,20 @@ export function createRegionStableScript(selector) {
// Page Registry — pure-logic operations for multi-page/tab management
// ---------------------------------------------------------------------------
/**
* Create a fresh page registry.
* @returns {{ pages: Array, activePageId: number | null, nextId: number }}
*/
export function createPageRegistry() {
export function createPageRegistry(): PageRegistry {
return { pages: [], activePageId: null, nextId: 1 };
}
/**
* @typedef {{ id: number, page: any, title: string, url: string, opener: number | null }} PageEntry
*/
/**
* Add a page to the registry. Assigns an auto-incrementing ID.
* @param {ReturnType<typeof createPageRegistry>} registry
* @param {{ page: any, title?: string, url?: string, opener?: number | null }} info
* @returns {PageEntry}
*/
export function registryAddPage(registry, { page, title = "", url = "", opener = null }) {
const entry = { id: registry.nextId++, page, title, url, opener };
export function registryAddPage(
registry: PageRegistry,
{ page, title = "", url = "", opener = null }: { page: unknown; title?: string; url?: string; opener?: number | null },
): PageEntry {
const entry: PageEntry = { id: registry.nextId++, page, title, url, opener };
registry.pages.push(entry);
return entry;
}
/**
* Remove a page from the registry by ID.
* If the removed page was active, falls back to the opener (if still present)
* or the last remaining page.
* Orphans any pages whose opener was the removed page (sets their opener to null).
* @param {ReturnType<typeof createPageRegistry>} registry
* @param {number} pageId
* @returns {{ removed: PageEntry, newActiveId: number | null }}
*/
export function registryRemovePage(registry, pageId) {
export function registryRemovePage(registry: PageRegistry, pageId: number): { removed: PageEntry; newActiveId: number | null } {
const idx = registry.pages.findIndex((p) => p.id === pageId);
if (idx === -1) {
const available = registry.pages.map((p) => p.id);
@ -571,12 +751,7 @@ export function registryRemovePage(registry, pageId) {
return { removed, newActiveId };
}
/**
* Set the active page by ID. Throws if the page is not in the registry.
* @param {ReturnType<typeof createPageRegistry>} registry
* @param {number} pageId
*/
export function registrySetActive(registry, pageId) {
export function registrySetActive(registry: PageRegistry, pageId: number): void {
const entry = registry.pages.find((p) => p.id === pageId);
if (!entry) {
const available = registry.pages.map((p) => p.id);
@ -589,12 +764,7 @@ export function registrySetActive(registry, pageId) {
registry.activePageId = pageId;
}
/**
* Get the active page entry. Throws if no active page or active page not found.
* @param {ReturnType<typeof createPageRegistry>} registry
* @returns {PageEntry}
*/
export function registryGetActive(registry) {
export function registryGetActive(registry: PageRegistry): PageEntry {
if (registry.activePageId === null) {
throw new Error(
`registryGetActive: no active page. ` +
@ -613,22 +783,11 @@ export function registryGetActive(registry) {
return entry;
}
/**
* Get a page entry by ID, or null if not found.
* @param {ReturnType<typeof createPageRegistry>} registry
* @param {number} pageId
* @returns {PageEntry | null}
*/
export function registryGetPage(registry, pageId) {
export function registryGetPage(registry: PageRegistry, pageId: number): PageEntry | null {
return registry.pages.find((p) => p.id === pageId) ?? null;
}
/**
* List all pages (without the raw `page` reference).
* @param {ReturnType<typeof createPageRegistry>} registry
* @returns {Array<{ id: number, title: string, url: string, opener: number | null, isActive: boolean }>}
*/
export function registryListPages(registry) {
export function registryListPages(registry: PageRegistry): PageListEntry[] {
return registry.pages.map((entry) => ({
id: entry.id,
title: entry.title,
@ -642,13 +801,8 @@ export function registryListPages(registry) {
// FIFO Bounded Log Pusher
// ---------------------------------------------------------------------------
/**
* Create a push function that enforces FIFO eviction at push-time.
* @param {number} maxSize maximum number of entries to retain
* @returns {(array: Array, entry: any) => void}
*/
export function createBoundedLogPusher(maxSize) {
return function push(array, entry) {
export function createBoundedLogPusher(maxSize: number): (array: unknown[], entry: unknown) => void {
return function push(array: unknown[], entry: unknown): void {
array.push(entry);
if (array.length > maxSize) {
array.splice(0, array.length - maxSize);
@ -656,10 +810,14 @@ export function createBoundedLogPusher(maxSize) {
};
}
export async function runBatchSteps({ steps, executeStep, stopOnFailure = true }) {
const results = [];
export async function runBatchSteps({ steps, executeStep, stopOnFailure = true }: {
steps: unknown[];
executeStep: (step: unknown, index: number) => Promise<{ ok: boolean; [key: string]: unknown }>;
stopOnFailure?: boolean;
}): Promise<BatchStepResult> {
const results: unknown[] = [];
for (let i = 0; i < steps.length; i += 1) {
const step = steps[i];
const step = steps[i] as { action: string };
const result = await executeStep(step, i);
results.push(result);
if (result.ok === false && stopOnFailure) {
@ -685,15 +843,7 @@ export async function runBatchSteps({ steps, executeStep, stopOnFailure = true }
// Snapshot Modes — semantic element filtering for browser_snapshot_refs
// ---------------------------------------------------------------------------
/**
* Pre-defined snapshot modes that filter elements by semantic category.
* Each mode config defines which elements should be captured.
*
* Shape: { tags: string[], roles: string[], selectors: string[],
* ariaAttributes: string[], useInteractiveFilter: boolean,
* visibleOnly?: boolean, containerExpand?: boolean }
*/
export const SNAPSHOT_MODES = {
export const SNAPSHOT_MODES: Record<string, SnapshotModeConfig> = {
interactive: {
tags: [],
roles: [],
@ -748,12 +898,7 @@ export const SNAPSHOT_MODES = {
},
};
/**
* Get the snapshot mode config by name.
* @param {string} mode mode name (e.g. "form", "dialog", "interactive")
* @returns {{ tags: string[], roles: string[], selectors: string[], ariaAttributes: string[], useInteractiveFilter: boolean, visibleOnly?: boolean, containerExpand?: boolean } | null}
*/
export function getSnapshotModeConfig(mode) {
export function getSnapshotModeConfig(mode: string): SnapshotModeConfig | null {
return SNAPSHOT_MODES[mode] ?? null;
}
@ -761,13 +906,7 @@ export function getSnapshotModeConfig(mode) {
// Fingerprint functions — structural identity for ref resolution
// ---------------------------------------------------------------------------
/**
* Compute a content hash from visible text using djb2.
* Caller is expected to pre-truncate to ~200 chars and normalize whitespace.
* @param {string} text visible text content
* @returns {string} hex string hash, or "0" for empty input
*/
export function computeContentHash(text) {
export function computeContentHash(text: string): string {
if (!text) return "0";
let h = 5381;
for (let i = 0; i < text.length; i++) {
@ -776,15 +915,7 @@ export function computeContentHash(text) {
return (h >>> 0).toString(16);
}
/**
* Compute a structural signature from tag, role, and immediate child tag names.
* Uses djb2 hash on the concatenated string `tag|role|child1,child2,...`.
* @param {string} tag element tag name (lowercase)
* @param {string} role ARIA role or empty string
* @param {string[]} childTags array of immediate child tag names (lowercase)
* @returns {string} hex string hash
*/
export function computeStructuralSignature(tag, role, childTags) {
export function computeStructuralSignature(tag: string, role: string, childTags: string[]): string {
const input = `${tag}|${role}|${childTags.join(",")}`;
let h = 5381;
for (let i = 0; i < input.length; i++) {
@ -793,14 +924,10 @@ export function computeStructuralSignature(tag, role, childTags) {
return (h >>> 0).toString(16);
}
/**
* Match two fingerprint objects by contentHash and structuralSignature.
* Returns true only when both fields are present on both objects and both match.
* @param {{ contentHash?: string, structuralSignature?: string }} stored
* @param {{ contentHash?: string, structuralSignature?: string }} candidate
* @returns {boolean}
*/
export function matchFingerprint(stored, candidate) {
export function matchFingerprint(
stored: { contentHash?: string; structuralSignature?: string },
candidate: { contentHash?: string; structuralSignature?: string },
): boolean {
if (!stored || !candidate) return false;
if (!stored.contentHash || !stored.structuralSignature) return false;
if (!candidate.contentHash || !candidate.structuralSignature) return false;
@ -808,28 +935,32 @@ export function matchFingerprint(stored, candidate) {
stored.structuralSignature === candidate.structuralSignature;
}
function formatDurationMs(entry) {
// ---------------------------------------------------------------------------
// Timeline Formatting
// ---------------------------------------------------------------------------
function formatDurationMs(entry: { startedAt?: number; finishedAt?: number | null }): number | null {
const startedAt = typeof entry?.startedAt === "number" ? entry.startedAt : null;
const finishedAt = typeof entry?.finishedAt === "number" ? entry.finishedAt : null;
if (startedAt == null || finishedAt == null || finishedAt < startedAt) return null;
return finishedAt - startedAt;
}
function summarizeActionStatus(status) {
function summarizeActionStatus(status: string | undefined): string {
if (status === "error") return "error";
if (status === "running") return "running";
return "success";
}
function looksBoundedWarning(value) {
function looksBoundedWarning(value: unknown): boolean {
return /bounded .*history/i.test(String(value ?? ""));
}
function uniqueStrings(values) {
return [...new Set(values.filter(Boolean))];
function uniqueStrings(values: (string | undefined)[]): string[] {
return [...new Set(values.filter(Boolean))] as string[];
}
export function formatTimelineEntries(entries = [], options = {}) {
export function formatTimelineEntries(entries: ActionEntry[] = [], options: { retained?: number; totalRecorded?: number } = {}): FormattedTimeline {
const retained = options.retained ?? entries.length;
const totalRecorded = options.totalRecorded ?? retained;
const bounded = totalRecorded > retained;
@ -847,7 +978,7 @@ export function formatTimelineEntries(entries = [], options = {}) {
const formattedEntries = entries.map((entry) => {
const status = summarizeActionStatus(entry.status);
const durationMs = formatDurationMs(entry);
const parts = [
const parts: string[] = [
`#${entry.id ?? "?"}`,
entry.tool ?? "unknown_tool",
status,
@ -884,12 +1015,23 @@ export function formatTimelineEntries(entries = [], options = {}) {
};
}
export function buildFailureHypothesis(session = {}) {
// ---------------------------------------------------------------------------
// Failure Hypothesis
// ---------------------------------------------------------------------------
interface SessionForHypothesis {
actionTimeline?: { entries: ActionEntry[] };
consoleEntries?: Array<{ type?: string; text?: string; message?: string }>;
networkEntries?: Array<{ url?: string; status?: number; failed?: boolean }>;
dialogEntries?: Array<{ type?: string; message?: string }>;
}
export function buildFailureHypothesis(session: SessionForHypothesis = {}): FailureHypothesis {
const timelineEntries = session.actionTimeline?.entries ?? [];
const consoleEntries = session.consoleEntries ?? [];
const networkEntries = session.networkEntries ?? [];
const dialogEntries = session.dialogEntries ?? [];
const signals = [];
const signals: Array<{ category: string; source: string; detail: string }> = [];
for (const entry of timelineEntries) {
if (entry?.status !== "error") continue;
@ -920,7 +1062,7 @@ export function buildFailureHypothesis(session = {}) {
if (entry?.type !== "error" && entry?.type !== "pageerror") continue;
signals.push({
category: "console",
source: entry.type,
source: entry.type!,
detail: entry.text || "Console error recorded",
});
}
@ -957,8 +1099,18 @@ export function buildFailureHypothesis(session = {}) {
};
}
export function summarizeBrowserSession(session = {}) {
const actionTimeline = session.actionTimeline ?? { limit: 0, entries: [] };
// ---------------------------------------------------------------------------
// Session Summary
// ---------------------------------------------------------------------------
interface SessionForSummary extends SessionForHypothesis {
retainedActionCount?: number;
totalActionCount?: number;
pages?: Array<{ id?: number; title?: string; url?: string; isActive?: boolean }>;
}
export function summarizeBrowserSession(session: SessionForSummary = {}): SessionSummary {
const actionTimeline = session.actionTimeline ?? { limit: 0, entries: [] as ActionEntry[] };
const actionEntries = actionTimeline.entries ?? [];
const retainedActionCount = session.retainedActionCount ?? actionEntries.length;
const totalActionCount = session.totalActionCount ?? retainedActionCount;
@ -973,7 +1125,7 @@ export function summarizeBrowserSession(session = {}) {
acc[status] = (acc[status] ?? 0) + 1;
return acc;
},
{ success: 0, error: 0, running: 0 },
{ success: 0, error: 0, running: 0 } as Record<string, number>,
);
const waitEntries = actionEntries.filter((entry) => entry.tool === "browser_wait_for");
@ -982,7 +1134,7 @@ export function summarizeBrowserSession(session = {}) {
const failedRequests = networkEntries.filter((entry) => entry.failed || (typeof entry.status === "number" && entry.status >= 400));
const activePage = pages.find((page) => page.isActive) ?? pages[0] ?? null;
const caveats = [];
const caveats: string[] = [];
if (totalActionCount > retainedActionCount) {
caveats.push(`Showing ${retainedActionCount} of ${totalActionCount} recorded actions; older actions were discarded due to bounded history.`);
}