v1.91.122.0 fix(deps): clear the 2026-11-30 OSV suppressions, replace #3228, exempt dependabot from Version Gate rule 1 (#3239)

* fix(deps): clear the OSV suppressions that expire on 2026-11-30

Delete the hono, @anthropic-ai/sdk (#2754) and sprintf-js ignores, which the
lockfiles no longer match. Override image-size to 2.0.4 (#2753) and chokidar
to 4.0.3 in lib/diagram-render (drops braces), and delete those ignores.
Re-date katex to 2027-01-31: its fix is outside mermaid 11's range
(mermaid-js/mermaid#8388).

html-to-docx 1.8.0 bundles its own image-size 1.x, which the override cannot
reach and whose ICNS parser loops forever. make-pdf --to docx now hands it
only PNG, JPEG, GIF, WebP or SVG bytes. A golden docx test proves the Word
output is unchanged.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* build(deps-dev): @types/bun 1.4.3, prettier 3.9.10; hold the Anthropic SDKs to patches

Replaces dependabot #3228 without its agent-sdk 0.3 and sdk 0.133 bumps,
which break typecheck:test and overlay-sdk-cancel-eof. Their 0.x breaking
releases are semver-minor to dependabot, so the ignore covers minor and major.
@types/bun 1.4.3 retires 18 test type diagnostics; the baseline locks that in.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(ci): exempt dependabot dependency-manifest PRs from Version Gate rule 1

Dependabot never edits VERSION, so hard rule 1 failed every package.json PR
it opened. The workflow now passes the PR author and changed-file list; the
gate skips rule 1 only for dependabot[bot] when every file is a package.json,
bun.lock(b) or .github/dependabot.yml. A failed file lookup keeps the rule.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(make-pdf): guard every html-to-docx sizing path, keep non-ICNS formats

Review found three spellings that got past the regex guard and still hung
--to docx: a data-src decoy, a percent-encoded separator, and an unquoted
encoded colon. html-to-docx sizes images on three paths, and buildImage
URI-decodes the whole src first. The guard now mirrors all three, drops ICNS
(the only input its bundled image-size routes to the looping parser) instead
of allowlisting formats, so BMP and TIFF embed as before, and re-parses the
result with html-to-docx's own html-to-vdom to fail closed on anything the
regex misreads. The golden test tolerates CRLF checkouts on Windows.

Version Gate treats a failed file lookup as an empty list, so a partial
paginated result cannot decide the exemption.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore(release): stamp v1.91.121.0

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore(release): restamp v1.91.122.0

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-10-17 16:47:50 +00:00
1 parent d258bcffb0
commit 9dea818e34
28 files changed
+554 -144

No files matched your search

+8
View File
@@ -21,6 +21,14 @@ updates:
update-types: ["version-update:semver-minor", "version-update:semver-major"]
- dependency-name: "playwright-core"
update-types: ["version-update:semver-minor", "version-update:semver-major"]
# The eval harness pins the agent SDK exactly. Its 0.x breaking releases
# are semver-minor to dependabot: agent-sdk 0.2 -> 0.3 and sdk 0.78 ->
# 0.133 (#3228) break typecheck:test and overlay-sdk-cancel-eof. Patch
# updates still flow; the SDK migration lands as its own maintainer PR.
- dependency-name: "@anthropic-ai/claude-agent-sdk"
update-types: ["version-update:semver-minor", "version-update:semver-major"]
- dependency-name: "@anthropic-ai/sdk"
update-types: ["version-update:semver-minor", "version-update:semver-major"]
open-pull-requests-limit: 5
- package-ecosystem: "github-actions"
+4
View File
@@ -73,6 +73,10 @@ jobs:
- name: Compare PR VERSION to next free slot
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_VERSION: ${{ steps.versions.outputs.pr_version }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
run: |
if ! PR_CHANGED_FILES=$(gh api --paginate "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" --jq '.[].filename'); then PR_CHANGED_FILES=""; fi
export PR_CHANGED_FILES
bun run scripts/compare-pr-version.ts next.json "${{ github.event.pull_request.number }}"
+9 -66
View File
@@ -16,73 +16,16 @@
# forces a fresh decision. Tracking issues are filed at ship time and named in
# each reason.
[[IgnoredVulns]]
id = "GHSA-frvp-7c67-39w9"
# @hono/node-server 1.19.x. Reachable only through @modelcontextprotocol/sdk,
# which is an unused transitive dependency (no source file imports it) and never
# starts a Hono HTTP server, so the advisory's request path is not exercised.
# The only fix is @hono/node-server 2.0.5, a major bump the MCP SDK pins against
# (^1.19.9); forcing it via override risks breaking the SDK at runtime for a
# vulnerability we do not expose. Re-evaluate if the MCP SDK becomes a direct,
# server-hosting dependency.
reason = "Unreachable transitive (unused @modelcontextprotocol/sdk); fix requires a risky major override on a pinned peer dep."
ignoreUntil = 2026-11-30T00:00:00Z
[[IgnoredVulns]]
id = "GHSA-5p2g-fcmc-qvqq"
# image-size 1.2.1 via html-to-docx@1.8.0. image-size 2.0.3 (2026-09-14)
# fixes both advisories, but html-to-docx pins image-size ^1.0.0 and no fixed
# 1.x exists, so taking the fix is a cross-major override. Exposure:
# image-size only parses images the user themselves embeds into their own
# generated .docx; no untrusted input path. Upgrade trigger: html-to-docx
# moving off image-size 1.x, or a deliberate override to 2.x after checking
# html-to-docx's call site against 2.x's API with a docx-export smoke test.
# Tracking: #2753.
reason = "Fixed in image-size 2.0.3, but html-to-docx pins image-size ^1.0.0 (no fixed 1.x); local-only input path (user's own docx images). Re-evaluate on expiry."
ignoreUntil = 2026-11-30T00:00:00Z
[[IgnoredVulns]]
id = "GHSA-w3rx-r6r6-pgpr"
# Same package/node as GHSA-5p2g-fcmc-qvqq above; same rationale.
reason = "Fixed in image-size 2.0.3, but html-to-docx pins image-size ^1.0.0 (no fixed 1.x); local-only input path (user's own docx images). Re-evaluate on expiry."
ignoreUntil = 2026-11-30T00:00:00Z
[[IgnoredVulns]]
id = "GHSA-p7fg-763f-g4gf"
# @anthropic-ai/sdk 0.81.0 nested under @anthropic-ai/claude-agent-sdk@0.2.117,
# which is deliberately exact-pinned (eval-harness stability; the v1.77 wave
# pinned the whole harness after repeated CLI-drift breakage) and declares
# ^0.81.0 (0.x caret = 0.81.x only), so the 0.91.1 fix cannot be reached
# without violating the harness pin. 4.8 MEDIUM. Upgrade trigger: the next
# deliberate claude-agent-sdk bump. Tracking: #2754.
reason = "Fix requires breaking the deliberate eval-harness agent-sdk pin; MEDIUM severity accepted until the next harness bump."
ignoreUntil = 2026-11-30T00:00:00Z
[[IgnoredVulns]]
id = "GHSA-vfj7-8cjw-p6xm"
# braces 3.0.3 in lib/diagram-render only: @excalidraw/excalidraw -> sass ->
# chokidar -> braces. chokidar is sass's Node file watcher; the diagram-render
# bundle is built for the browser and contains no chokidar/braces code
# (test/osv-config-wiring.test.ts checks the built page for chokidar). No fixed
# braces release exists. Re-evaluate on expiry or when excalidraw drops sass.
reason = "Build-time only (excalidraw -> sass -> chokidar); not in the shipped browser bundle; no fixed release."
ignoreUntil = 2026-11-30T00:00:00Z
[[IgnoredVulns]]
id = "GHSA-hp3w-g68c-fv3c"
# sprintf-js 1.1.3 (DoS through an unbounded precision specifier in the FORMAT
# string). Reachable only as roarr's logger dependency (global-agent -> roarr),
# which formats its own constant messages; no caller passes user-controlled
# format strings. No fixed release exists.
reason = "No fixed release; only roarr's constant-format logging reaches it."
ignoreUntil = 2026-11-30T00:00:00Z
[[IgnoredVulns]]
id = "GHSA-238p-pmpm-9mq7"
# katex 0.16.47 in lib/diagram-render (mermaid's math renderer). The advisory
# needs prototype pollution to already exist in the page; the render page is
# offline, loads only gstack's own bundle and the user's diagram source. The
# only fix is 0.18.x, outside mermaid 11.16's ^0.16 range. Low severity (2.1).
# Re-evaluate when mermaid accepts katex 0.18.
reason = "Requires pre-existing prototype pollution in an offline single-bundle page; fix is outside mermaid's katex range; low severity."
ignoreUntil = 2026-11-30T00:00:00Z
# offline, loads only gstack's own bundle and the user's diagram source. Low
# severity (2.1). The fix is katex 0.18.2, outside mermaid 11's ^0.16.47 range.
# Mermaid 12 accepts ^0.18, but @excalidraw/mermaid-to-excalidraw 2.2.2 (the
# latest) requires mermaid ^11.12.1, and katex 0.18 renames KaTeX's CSS
# classes, so forcing it means a bundle requalification, not a lockfile bump.
# Upgrade trigger: a mermaid 11.x release that allows katex 0.18
# (https://github.com/mermaid-js/mermaid/issues/8388), then rebuild the bundle.
reason = "Fix (katex 0.18.2) is outside mermaid 11's ^0.16 range; waiting on mermaid-js/mermaid#8388. Needs pre-existing prototype pollution in an offline single-bundle page; low severity."
ignoreUntil = 2027-01-31T00:00:00Z
+17
View File
@@ -1,5 +1,22 @@
# Changelog
## [1.91.122.0] - 2026-10-17
**Main no longer goes red on 2026-11-16.** All seven `.osv-scanner.toml` suppressions expired on 2026-11-30, and `test/osv-ignore-expiry.test.ts` fails 14 days early. Six are gone and the seventh is re-dated with an upstream link. Dependabot's grouped dev bump (#3228) is replaced by hand, and Version Gate stops failing every dependabot package.json PR.
### Fixed
- **Six OSV suppressions removed (#2754, #2753).** The hono, @anthropic-ai/sdk and sprintf-js ignores no longer matched either lockfile. image-size is overridden to 2.0.4. braces leaves `lib/diagram-render` because chokidar is overridden to 4.0.3; that bundle's sha256 is unchanged. katex (low) is re-dated to 2027-01-31: its fix needs a mermaid 11 release that allows katex 0.18 (mermaid-js/mermaid#8388). osv-scanner reports no issues and no unused ignores.
- **`make-pdf --to docx` no longer hangs on an ICNS image.** html-to-docx 1.8.0 sizes images with its own bundled image-size 1.x, which the override cannot reach and which loops forever on GHSA-w3rx-r6r6-pgpr's input. ICNS images are dropped with a warning before conversion. The result is re-parsed with html-to-docx's own html-to-vdom, so a tag the guard misreads fails the conversion instead of hanging it. Every other image format embeds as before; a golden test pins the Word output.
- **Version Gate passes dependabot dependency PRs.** Hard rule 1 (VERSION above base) is skipped only when the author is `dependabot[bot]` and every changed file is a `package.json`, a `bun.lock` or `.github/dependabot.yml`. A failed file lookup keeps the rule.
### Changed
- **@types/bun 1.4.3 and prettier 3.9.10.** The test type baseline drops 18 diagnostics.
- **Dependabot holds both Anthropic SDKs to patch updates.** Their breaking 0.x releases (agent-sdk 0.2 to 0.3, sdk 0.78 to 0.133) count as minor bumps to dependabot and break `typecheck:test` and `overlay-sdk-cancel-eof`.
### If you…
- **…delete an OSV ignore after adding an override:** check that the vulnerable code loads from node_modules. A dependency that bundles its own copy keeps the old code even though the lockfile looks fixed.
- **…convert untrusted markdown with `--to docx --allow-network`:** html-to-docx still downloads remote images and sizes them with its bundled copy (#2753 stays open for this).
## [1.91.121.0] - 2026-10-17
**Windows free-test shards stop running out of time on files the Windows seed didn't know.** The Windows duration seed dated from 2026-10-04 and had no entry for `test/gstack-doctor.test.ts` (about 6.5 minutes on Windows) or `test/hook-scripts.test.ts`. The packer guessed, put `cookie-import-native-job` into a 121-file shard, and that shard hit the 15-minute job limit.
+5 -6
View File
@@ -41,8 +41,8 @@ The 158 older open issues were re-checked against main 5cb5e1c (v1.91.83.0). Of
- **/cso**
- #2993: tracked under the /cso capacity follow-ups above. **Priority:** P2.
- **Dependency security ledgers** (not code defects; keep open until the trigger)
- #2753: `.osv-scanner.toml` ignores image-size (via html-to-docx) until 2026-11-30. Bump it once a fixed release exists, or re-justify at expiry. **Effort:** S. **Priority:** P3.
- #2754: `.osv-scanner.toml` ignores `@anthropic-ai/sdk` 0.81.0 under the pinned claude-agent-sdk until 2026-11-30. Drop it with the next harness bump. **Effort:** S. **Priority:** P3.
- `.osv-scanner.toml` ignores katex 0.16.47 in lib/diagram-render (GHSA-238p-pmpm-9mq7, low) until 2027-01-31. The fix (0.18.2) is outside mermaid 11's range; when mermaid-js/mermaid#8388 ships, override katex, rebuild the bundle and drop the ignore. **Effort:** S. **Priority:** P3.
- #2753 residual: html-to-docx 1.8.0 bundles its own image-size 1.x, which the package.json override cannot reach. `make-pdf --to docx` never hands it ICNS bytes (make-pdf/src/docx-images.ts), but with `--allow-network` html-to-docx downloads remote images and sizes them itself. Inline remote images before the guard for docx, or replace html-to-docx. Revisit 2026-12-15. **Effort:** M. **Priority:** P2.
- **Tests and CI**
- #2756: the periodic lane. Tracked under "Periodic-lane stabilization (#2756)". **Priority:** P2.
- #2967: `test/redact-prepush-scan-range.test.ts` (and its rebase twin) run fixture pushes with the inherited global config, so a developer's `core.hooksPath` pre-push runs inside the fixture. Set `GIT_CONFIG_GLOBAL=/dev/null` and `GIT_CONFIG_NOSYSTEM=1` in the helper. **Effort:** S. **Priority:** P3.
@@ -965,10 +965,9 @@ wave"). Each was explicitly deferred with rationale, not dropped:
- **mermaid 10→11-class major bumps in lib/diagram-render** — the wave's
dependency pass cleared 102 of 105 OSV advisories via in-range bumps +
overrides; the residual ignores (image-size no-fix, @anthropic-ai/sdk under
the harness-pinned agent-sdk) carry `ignoreUntil` expiries (~2026-11-30) and
re-justify themselves on expiry. When the agent-sdk pin next moves, drop the
GHSA-p7fg ignore. Effort S. **Priority:** P3.
overrides; the residual ignores carry `ignoreUntil` expiries and
re-justify themselves on expiry. Only the katex ignore remains (see the
dependency security ledgers above). Effort S. **Priority:** P3.
- **#2750 split absorption** — the record-scanning Codex JSONL parser (real
fix; current Codex streams interleave envelopes so sessions vanish from
/retro global) should be absorbed once the author splits it from the
+1 -1
View File
@@ -1 +1 @@
1.91.121.0
1.91.122.0
+1 -1
View File
@@ -1,4 +1,4 @@
# gstack digest v1.91.121.0 — regenerate/re-copy after upgrading gstack
# gstack digest v1.91.122.0 — regenerate/re-copy after upgrading gstack
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
for agent hosts without a full skill install. The full skills add workflows,
+8 -8
View File
@@ -10,6 +10,7 @@
"cross-spawn": "^7.0.6",
"diff": "^9.0.0",
"html-to-docx": "1.8.0",
"html-to-vdom": "0.7.0",
"marked": "^18.0.11",
"playwright": "^1.62.1",
"sharp": "^0.35.4",
@@ -18,8 +19,8 @@
"devDependencies": {
"@anthropic-ai/claude-agent-sdk": "0.2.141",
"@anthropic-ai/sdk": "^0.78.0",
"@types/bun": "1.4.2",
"prettier": "3.9.9",
"@types/bun": "1.4.3",
"prettier": "3.9.10",
"typescript": "7.0.2",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0",
@@ -34,6 +35,7 @@
"adm-zip": "^0.6.1",
"fast-uri": "3.1.8",
"hono": "4.13.13",
"image-size": "2.0.4",
"ip-address": "10.7.3",
"proxy-addr": "2.0.8",
"sharp": "0.35.5",
@@ -181,7 +183,7 @@
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.2", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="],
"@types/bun": ["@types/bun@1.4.2", "", { "dependencies": { "bun-types": "1.4.2" } }, "sha512-GimotNn7+ZV0uVArItBbriZsR1oNf0+WTzPkdcFrzShI7k2norL0uzEaJT8T33dWr7O/c9ZDuAFQrctKCi72oQ=="],
"@types/bun": ["@types/bun@1.4.3", "", { "dependencies": { "bun-types": "1.4.3" } }, "sha512-N67Pqy8JXbAY/j0tNraS3V5mubGvc6rrrNDmDCtZLlFzccbUJPDb0tvgUBC5q4m8ybkP93Jcn1QB+RYQ6PvBhw=="],
"@types/node": ["@types/node@26.4.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ=="],
@@ -237,7 +239,7 @@
"browser-split": ["browser-split@0.0.1", "", {}, "sha512-JhvgRb2ihQhsljNda3BI8/UcRHVzrVwo3Q+P8vDtSiyobXuFpuZ9mq+MbRGMnC22CjW3RrfXdg6j6ITX8M+7Ow=="],
"bun-types": ["bun-types@1.4.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-bxV1FgK7yBIzjRe5zBozIM4Bem11ZJcCXSrjWRG3YWLt8yFDePu4cLjpebO8OvPeIE9trbyPF4fuj3Cia4Fj3w=="],
"bun-types": ["bun-types@1.4.3", "", { "dependencies": { "@types/node": "*", "undici-types": "*" } }, "sha512-tinRJNGOmUN+tEAsn2y2e942n9JHRHR42iUYzDH75NmhJqVBokGSQkkfSFdNWwsANIdrK6ACH9t01M452YyTwg=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
@@ -371,7 +373,7 @@
"iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="],
"image-size": ["image-size@1.2.1", "", { "dependencies": { "queue": "6.0.2" }, "bin": { "image-size": "bin/image-size.js" } }, "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw=="],
"image-size": ["image-size@2.0.4", "", { "bin": { "image-size": "bin/image-size.js" } }, "sha512-QRUkFFsRV/6fuESxb9Vkq+a0LkSrgKXuc2NEqfikiXxxN/G3tjWt5EVUlMaImRBZRZK/jRBEbYvpPYZL8t08Zw=="],
"image-to-base64": ["image-to-base64@2.2.0", "", { "dependencies": { "node-fetch": "^2.6.0" } }, "sha512-Z+aMwm/91UOQqHhrz7Upre2ytKhWejZlWV/JxUTD1sT7GWWKFDJUEV5scVQKnkzSgPHFuQBUEWcanO+ma0PSVw=="],
@@ -469,7 +471,7 @@
"playwright-core": ["playwright-core@1.62.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw=="],
"prettier": ["prettier@3.9.9", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg=="],
"prettier": ["prettier@3.9.10", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA=="],
"process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
@@ -483,8 +485,6 @@
"qs": ["qs@6.16.0", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA=="],
"queue": ["queue@6.0.2", "", { "dependencies": { "inherits": "~2.0.3" } }, "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA=="],
"range-parser": ["range-parser@1.3.0", "", {}, "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw=="],
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
+3 -28
View File
@@ -14,6 +14,7 @@
},
},
"overrides": {
"chokidar": "4.0.3",
"dompurify": "3.4.16",
"lodash-es": "4.18.1",
"nanoid": "5.1.16",
@@ -178,14 +179,8 @@
"@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="],
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"browser-fs-access": ["browser-fs-access@0.29.1", "", {}, "sha512-LSvVX5e21LRrXqVMhqtAwj5xPgDb+fXAIH80NsnCQ9xuZPs2xWsOREi24RKgZa1XOiQRbcmVrv87+ulOKsgjxw=="],
"canvas-roundrect-polyfill": ["canvas-roundrect-polyfill@0.0.1", "", {}, "sha512-yWq+R3U3jE+coOeEb3a3GgE2j/0MMiDKM/QpLb6h9ihf5fGY9UXtvK9o4vNqjWXoZz7/3EaSVU3IX53TvFFUOw=="],
@@ -194,7 +189,7 @@
"chevrotain-allstar": ["chevrotain-allstar@0.3.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^11.0.0" } }, "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw=="],
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"clsx": ["clsx@1.1.1", "", {}, "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA=="],
@@ -292,18 +287,12 @@
"es6-promise-pool": ["es6-promise-pool@2.5.0", "", {}, "sha512-VHErXfzR/6r/+yyzPKeBvO0lgjfC5cbDCQWjWwMZWSb6YU39TGIl51OUmCfWCq4ylMdJSB8zkz2vIuIeIxXApA=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"fractional-indexing": ["fractional-indexing@3.2.0", "", {}, "sha512-PcOxmqwYCW7O2ovKRU8OoQQj2yqTfEB/yeTYk4gPid6dN5ODRfU1hXd9tTVZzax/0NkO7AxpHykvZnT1aYp/BQ=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"fuzzy": ["fuzzy@0.1.3", "", {}, "sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w=="],
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"glur": ["glur@1.1.2", "", {}, "sha512-l+8esYHTKOx2G/Aao4lEQ0bnHWg4fWtJbVoZZT9Knxi01pB8C80BR85nONLFwkkQoFRCmXY+BUcGZN3yZ2QsRA=="],
"hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="],
@@ -320,14 +309,6 @@
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jotai": ["jotai@2.11.0", "", { "peerDependencies": { "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@types/react", "react"] }, "sha512-zKfoBBD1uDw3rljwHkt0fWuja1B76R7CjznuBO+mSX6jpsO1EBeWNRKpeaQho9yPI/pvCv4recGfgOXGxwPZvQ=="],
@@ -360,8 +341,6 @@
"nanoid": ["nanoid@5.1.16", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ=="],
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"open-color": ["open-color@1.9.1", "", {}, "sha512-vCseG/EQ6/RcvxhUcGJiHViOgrtz4x0XbZepXvKik66TMGkvbmjeJrKFyBEx6daG5rNyyd14zYXhz0hZVwQFOw=="],
@@ -378,8 +357,6 @@
"pica": ["pica@7.1.1", "", { "dependencies": { "glur": "^1.1.2", "inherits": "^2.0.3", "multimath": "^2.0.0", "object-assign": "^4.1.1", "webworkify": "^1.5.0" } }, "sha512-WY73tMvNzXWEld2LicT9Y260L43isrZ85tPuqRyvtkljSDLmnNFQmZICt4xUJMVulmcc6L9O7jbBrtx3DOz/YQ=="],
"picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"png-chunk-text": ["png-chunk-text@1.0.0", "", {}, "sha512-DEROKU3SkkLGWNMzru3xPVgxyd48UGuMSZvioErCure6yhOc/pRH2ZV+SEn7nmaf7WNf3NdIpH+UTrRdKyq9Lw=="],
"png-chunks-encode": ["png-chunks-encode@1.0.0", "", { "dependencies": { "crc-32": "^0.3.0", "sliced": "^1.0.1" } }, "sha512-J1jcHgbQRsIIgx5wxW9UmCymV3wwn4qCCJl6KYgEU/yHCh/L2Mwq/nMOkRPtmV79TLxRZj5w3tH69pvygFkDqA=="],
@@ -402,7 +379,7 @@
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="],
@@ -428,8 +405,6 @@
"tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"ts-dedent": ["ts-dedent@2.3.0", "", {}, "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+1
View File
@@ -14,6 +14,7 @@
"react-dom": "18.3.1"
},
"overrides": {
"chokidar": "4.0.3",
"dompurify": "3.4.16",
"nanoid": "5.1.16",
"source-map-js": "1.2.2",
+78
View File
@@ -0,0 +1,78 @@
/**
* DOCX image guard. html-to-docx 1.8.0 sizes every data-URI <img> with its
* own bundled copy of image-size 1.x, which the package.json override cannot
* reach. That copy's detector sends a buffer to its ICNS parser exactly when
* the bytes start with "icns", and the parser loops forever on a zero-length
* entry (GHSA-w3rx-r6r6-pgpr). No other parser in the copy has a known flaw.
*
* guardDocxImages drops the <img> tags it can see that would size unsafe
* bytes, then re-parses the result with the same html-to-vdom html-to-docx
* uses and refuses the document if any <img> it would size is still unsafe,
* so a tag the regex misreads fails closed instead of hanging.
*/
import HTMLToVDOM from "html-to-vdom";
/** html-to-docx's data-URI pattern, shared by all three of its sizeOf call sites. */
const DATA_URI = /^data:([A-Za-z-+\/]+);base64,(.+)$/;
const IMG_TAG = /<img\b[^>]*>/gi;
const SRC_ATTR = /(?:^|\s)src\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i;
interface ImgNode { tagName?: string; properties?: { src?: unknown }; children?: ImgNode[] }
const toVdom = HTMLToVDOM({
VNode: function (this: ImgNode, tagName: string, properties: ImgNode["properties"], children: ImgNode[]) {
Object.assign(this, { tagName, properties, children });
},
VText: function () {},
});
function tryDecode(text: string): string | null {
try {
return decodeURIComponent(text);
} catch {
return null;
}
}
function isIcns(base64: string | null | undefined): boolean {
return base64 != null && Buffer.from(base64, "base64").toString("ascii", 0, 4) === "icns";
}
/**
* False when html-to-docx would hand ICNS bytes to its bundled sizer. Its two
* paragraph paths match the raw src and URI-decode the payload; buildImage
* URI-decodes the whole src, matches that, and sizes the payload as is.
*/
function sizesSafely(src: unknown): boolean {
if (typeof src !== "string") return true;
const raw = src.match(DATA_URI);
if (raw && isIcns(tryDecode(raw[2]))) return false;
return !isIcns(tryDecode(src)?.match(DATA_URI)?.[2]);
}
/** html-to-docx's minifyHTMLString, so the re-parse sees the string it parses. */
function minify(html: string): string {
return html.replace(/\n/g, " ").replace(/\r/g, " ").replace(/[\t]+</g, "<").replace(/>[\t ]+</g, "><").replace(/>[\t ]+$/g, ">");
}
function* imgSources(nodes: ImgNode | ImgNode[]): Generator<unknown> {
for (const node of Array.isArray(nodes) ? nodes : [nodes]) {
if (node.tagName === "img") yield node.properties?.src;
if (node.children) yield* imgSources(node.children);
}
}
export function guardDocxImages(html: string, warn: (msg: string) => void): string {
const guarded = html.replace(IMG_TAG, (tag) => {
const src = tag.match(SRC_ATTR);
if (sizesSafely(src?.[1] ?? src?.[2] ?? src?.[3])) return tag;
warn("docx: dropped an ICNS image (html-to-docx's bundled image-size hangs on it)");
return "";
});
for (const src of imgSources(toVdom(minify(guarded)))) {
if (!sizesSafely(src)) {
throw new Error("docx: an <img> tag hides an ICNS image, which html-to-docx's bundled image-size hangs on; convert or remove it");
}
}
return guarded;
}
+3
View File
@@ -0,0 +1,3 @@
declare module 'html-to-vdom' {
export default function HTMLToVDOM(dependencies: { VNode: Function; VText: Function }): (html: string) => any;
}
+2
View File
@@ -41,6 +41,7 @@ import {
substituteSlots,
} from "./diagram-prepass";
import { applyImagePolicy } from "./image-policy";
import { guardDocxImages } from "./docx-images";
/** Default output location (`$P generate letter.md` → /tmp/letter.pdf). */
export const OUTPUT_TMP_DIR = process.platform === "win32" ? os.tmpdir() : "/tmp";
@@ -174,6 +175,7 @@ export async function generate(opts: GenerateOptions): Promise<string> {
progress.end("Rasterizing diagrams for DOCX");
}
finalHtml = convertDiagnosticsForDocx(finalHtml);
finalHtml = guardDocxImages(finalHtml, warn);
}
// ─── --to html: write the self-contained document, no print round-trip ──
+113
View File
@@ -0,0 +1,113 @@
/**
* DOCX image sizing (#2753). package.json overrides image-size to 2.x, which
* fixes GHSA-5p2g-fcmc-qvqq and GHSA-w3rx-r6r6-pgpr in the lockfile, but
* html-to-docx 1.8.0 never loads it: it runs its own bundled image-size 1.x.
* The golden document.xml pins the Word output the change must not move. The
* guard tests prove ICNS bytes, which hang that bundled copy, never reach it,
* however the <img> tag is spelled.
*/
import { describe, expect, test } from "bun:test";
import { execFileSync } from "node:child_process";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { guardDocxImages } from "../src/docx-images";
import { generate } from "../src/orchestrator";
const FIXTURES = path.join(import.meta.dir, "fixtures", "docx-images");
const EMU_PER_PX = 9525;
const IMAGES = [
{ file: "wide.png", ext: "png", width: 120, height: 48 },
{ file: "tall.jpg", ext: "jpeg", width: 30, height: 90 },
{ file: "square.gif", ext: "gif", width: 64, height: 64 },
];
/** GHSA-w3rx-r6r6-pgpr: valid ICNS magic and a zero-length entry. */
function icnsLoop(): Buffer {
const buf = Buffer.alloc(24);
buf.write("icns", 0);
buf.writeUInt32BE(24, 4);
buf.write("ic08", 8);
return buf;
}
function unzip(args: string[]): Buffer {
return execFileSync("unzip", args, { timeout: 30_000, maxBuffer: 16 * 1024 * 1024 });
}
describe("make-pdf --to docx image sizing", () => {
test("embeds every image at its pixel size, matching the image-size 1.x output", async () => {
const workDir = fs.mkdtempSync(path.join(os.tmpdir(), "make-pdf-docx-images-"));
try {
const out = path.join(workDir, "out.docx");
await generate({ input: path.join(FIXTURES, "doc.md"), output: out, to: "docx", quiet: true });
const xml = unzip(["-p", out, "word/document.xml"]).toString("utf8");
const extents = [...xml.matchAll(/<wp:extent cx="(\d+)" cy="(\d+)"\/>/g)].map(m => [Number(m[1]), Number(m[2])]);
expect(extents).toEqual(IMAGES.map(img => [img.width * EMU_PER_PX, img.height * EMU_PER_PX]));
const media = unzip(["-Z1", out]).toString("utf8").split("\n").filter(name => /^word\/media\/.+/.test(name));
expect(media).toHaveLength(IMAGES.length);
for (const [i, img] of IMAGES.entries()) {
expect(media[i].endsWith(`.${img.ext}`)).toBe(true);
expect(unzip(["-p", out, media[i]]).equals(fs.readFileSync(path.join(FIXTURES, img.file)))).toBe(true);
}
const normalized = xml.replace(/image-[A-Za-z0-9_-]{21}\.(png|jpeg|gif)/g, "image-ID.$1");
const golden = fs.readFileSync(path.join(FIXTURES, "document.golden.xml"), "utf8").replace(/\r\n/g, "\n");
expect(normalized.trimEnd()).toBe(golden.trimEnd());
} finally {
fs.rmSync(workDir, { recursive: true, force: true });
}
});
test("the guard drops ICNS images however the tag is spelled and keeps every other format", () => {
const icns = "data:image/png;base64," + icnsLoop().toString("base64");
const png = "data:image/png;base64," + fs.readFileSync(path.join(FIXTURES, "wide.png")).toString("base64");
const bmp = "data:image/bmp;base64," + Buffer.from("BM" + "\0".repeat(52)).toString("base64");
const warnings: string[] = [];
const guard = (html: string) => guardDocxImages(html, msg => warnings.push(msg));
expect(guard(`<p><img src="${png}"><img src="${bmp}"></p>`)).toBe(`<p><img src="${png}"><img src="${bmp}"></p>`);
expect(warnings).toEqual([]);
for (const tag of [
`<img src="${icns}">`,
`<img src='${icns}' alt="x">`,
`<img src=${icns}>`,
`<IMG SRC="${icns}">`,
`<img data-src="${png}" src="${icns}">`,
`<img src="${icns.replace("base64,a", "base64,%61")}">`,
`<img src="${icns.replace(";", "%3B")}">`,
`<img src=${icns.replace(":", "%3A")}>`,
]) expect(guard(`<p>${tag}</p>`)).toBe("<p></p>");
expect(warnings).toHaveLength(8);
});
test("a tag the regex misreads fails closed through html-to-docx's own parser", () => {
const icns = "data:image/png;base64," + icnsLoop().toString("base64");
expect(() => guardDocxImages(`<p><img alt=">" src="${icns}"></p>`, () => {})).toThrow("hides an ICNS image");
});
test("the re-parse uses the html-to-vdom copy html-to-docx loads", () => {
const fromDocx = require.resolve("html-to-vdom/package.json", { paths: [path.dirname(require.resolve("html-to-docx/package.json"))] });
expect(fromDocx).toBe(require.resolve("html-to-vdom/package.json"));
});
test("an ICNS file named .png is dropped with a warning instead of hanging the conversion", () => {
const workDir = fs.mkdtempSync(path.join(os.tmpdir(), "make-pdf-docx-icns-"));
try {
fs.writeFileSync(path.join(workDir, "evil.png"), icnsLoop());
fs.writeFileSync(path.join(workDir, "doc.md"), "# ICNS\n\n![evil](evil.png)\n\n![inline](data:image/png;base64," + icnsLoop().toString("base64") + ")\n");
const out = path.join(workDir, "out.docx");
const script = `import { generate } from ${JSON.stringify(path.join(import.meta.dir, "..", "src", "orchestrator.ts"))};
await generate({ input: ${JSON.stringify(path.join(workDir, "doc.md"))}, output: ${JSON.stringify(out)}, to: "docx" });`;
const run = Bun.spawnSync([process.execPath, "-e", script], { timeout: 30_000, stdout: "pipe", stderr: "pipe" });
const stderr = run.stderr.toString();
expect(run.exitCode).toBe(0);
expect(stderr.match(/docx: dropped an ICNS image/g)).toHaveLength(2);
expect(unzip(["-p", out, "word/document.xml"]).toString("utf8")).not.toContain("<w:drawing>");
} finally {
fs.rmSync(workDir, { recursive: true, force: true });
}
});
});
+11
View File
@@ -0,0 +1,11 @@
# Image sizing smoke
A wide PNG:
![wide](wide.png)
A tall JPEG and a square GIF:
![tall](tall.jpg)
![square](square.gif)
+163
View File
@@ -0,0 +1,163 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:cdr="http://schemas.openxmlformats.org/drawingml/2006/chartDrawing" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:ve="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml">
<w:body>
<w:sectPr>
<w:pgSz w:w="12240" w:h="15840" w:orient="portrait"/>
<w:pgMar w:top="1440" w:right="1800" w:bottom="1440" w:left="1800" w:header="720" w:footer="720" w:gutter="0"/>
</w:sectPr>
<w:p>
<w:pPr>
<w:spacing w:lineRule="auto"/>
</w:pPr>
<w:r>
<w:rPr/>
<w:t xml:space="preserve"/>
</w:r>
</w:p>
<w:p>
<w:pPr>
<w:pStyle w:val="Heading1"/>
<w:spacing w:lineRule="auto"/>
</w:pPr>
<w:r>
<w:rPr/>
<w:t xml:space="preserve">Image sizing smoke</w:t>
</w:r>
</w:p>
<w:p>
<w:pPr>
<w:spacing w:lineRule="auto"/>
</w:pPr>
<w:r>
<w:rPr/>
<w:t xml:space="preserve">A wide PNG:</w:t>
</w:r>
</w:p>
<w:p>
<w:pPr>
<w:spacing w:lineRule="auto"/>
</w:pPr>
<w:r>
<w:rPr/>
<w:drawing>
<wp:inline distB="0" distL="0" distR="0" distT="0">
<wp:extent cx="1143000" cy="457200"/>
<wp:effectExtent b="0" l="0" r="0" t="0"/>
<wp:docPr id="1" name="image-ID.png"/>
<a:graphic>
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
<pic:pic>
<pic:nvPicPr>
<pic:cNvPr id="1" name="image-ID.png" descr="wide"/>
<pic:cNvPicPr/>
</pic:nvPicPr>
<pic:blipFill>
<a:blip r:embed="rId6" cstate="print"/>
<a:srcRect b="0" l="0" r="0" t="0"/>
<a:stretch>
<a:fillRect/>
</a:stretch>
</pic:blipFill>
<pic:spPr>
<a:xfrm>
<a:off x="0" y="0"/>
<a:ext cx="1143000" cy="457200"/>
</a:xfrm>
<a:prstGeom prst="rect"/>
</pic:spPr>
</pic:pic>
</a:graphicData>
</a:graphic>
</wp:inline>
</w:drawing>
</w:r>
</w:p>
<w:p>
<w:pPr>
<w:spacing w:lineRule="auto"/>
</w:pPr>
<w:r>
<w:rPr/>
<w:t xml:space="preserve">A tall JPEG and a square GIF:</w:t>
</w:r>
</w:p>
<w:p>
<w:pPr>
<w:spacing w:lineRule="auto"/>
</w:pPr>
<w:r>
<w:rPr/>
<w:drawing>
<wp:inline distB="0" distL="0" distR="0" distT="0">
<wp:extent cx="285750" cy="857250"/>
<wp:effectExtent b="0" l="0" r="0" t="0"/>
<wp:docPr id="2" name="image-ID.jpeg"/>
<a:graphic>
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
<pic:pic>
<pic:nvPicPr>
<pic:cNvPr id="2" name="image-ID.jpeg" descr="tall"/>
<pic:cNvPicPr/>
</pic:nvPicPr>
<pic:blipFill>
<a:blip r:embed="rId7" cstate="print"/>
<a:srcRect b="0" l="0" r="0" t="0"/>
<a:stretch>
<a:fillRect/>
</a:stretch>
</pic:blipFill>
<pic:spPr>
<a:xfrm>
<a:off x="0" y="0"/>
<a:ext cx="285750" cy="857250"/>
</a:xfrm>
<a:prstGeom prst="rect"/>
</pic:spPr>
</pic:pic>
</a:graphicData>
</a:graphic>
</wp:inline>
</w:drawing>
</w:r>
</w:p>
<w:p>
<w:pPr>
<w:spacing w:lineRule="auto"/>
</w:pPr>
<w:r>
<w:rPr/>
<w:drawing>
<wp:inline distB="0" distL="0" distR="0" distT="0">
<wp:extent cx="609600" cy="609600"/>
<wp:effectExtent b="0" l="0" r="0" t="0"/>
<wp:docPr id="3" name="image-ID.gif"/>
<a:graphic>
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
<pic:pic>
<pic:nvPicPr>
<pic:cNvPr id="3" name="image-ID.gif" descr="square"/>
<pic:cNvPicPr/>
</pic:nvPicPr>
<pic:blipFill>
<a:blip r:embed="rId8" cstate="print"/>
<a:srcRect b="0" l="0" r="0" t="0"/>
<a:stretch>
<a:fillRect/>
</a:stretch>
</pic:blipFill>
<pic:spPr>
<a:xfrm>
<a:off x="0" y="0"/>
<a:ext cx="609600" cy="609600"/>
</a:xfrm>
<a:prstGeom prst="rect"/>
</pic:spPr>
</pic:pic>
</a:graphicData>
</a:graphic>
</wp:inline>
</w:drawing>
</w:r>
</w:p>
</w:body>
</w:document>
Binary file not shown.

After

Width:  |  Height:  |  Size: 110 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

+5 -3
View File
@@ -1,6 +1,6 @@
{
"name": "gstack",
"version": "1.91.121",
"version": "1.91.122",
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT",
"type": "module",
@@ -73,6 +73,7 @@
"cross-spawn": "^7.0.6",
"diff": "^9.0.0",
"html-to-docx": "1.8.0",
"html-to-vdom": "0.7.0",
"marked": "^18.0.11",
"playwright": "^1.62.1",
"sharp": "^0.35.4",
@@ -94,8 +95,8 @@
"devDependencies": {
"@anthropic-ai/claude-agent-sdk": "0.2.141",
"@anthropic-ai/sdk": "^0.78.0",
"@types/bun": "1.4.2",
"prettier": "3.9.9",
"@types/bun": "1.4.3",
"prettier": "3.9.10",
"typescript": "7.0.2",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0"
@@ -105,6 +106,7 @@
"adm-zip": "^0.6.1",
"fast-uri": "3.1.8",
"hono": "4.13.13",
"image-size": "2.0.4",
"ip-address": "10.7.3",
"proxy-addr": "2.0.8",
"sharp": "0.35.5"
+14
View File
@@ -77,10 +77,24 @@ console.log(` Queue (${claimed.length} open PRs claiming versions):`);
if (claimedList) console.log(claimedList);
console.log("::endgroup::");
// Dependabot never edits VERSION, so hard rule 1 would fail every dependency
// PR it opens. Exempt it only when the author is dependabot and every changed
// file is a dependency manifest, a lockfile or the dependabot config. The
// workflow passes the author and the file list; an empty list (the lookup
// failed) keeps the rule.
const DEPENDENCY_FILE = /(?:^|\/)(?:package\.json|bun\.lockb?)$|^\.github\/dependabot\.yml$/;
const changedFiles = (process.env.PR_CHANGED_FILES ?? "").split("\n").map((f) => f.trim()).filter(Boolean);
const dependabotManifestOnly =
process.env.PR_AUTHOR === "dependabot[bot]" && changedFiles.length > 0 && changedFiles.every((f) => DEPENDENCY_FILE.test(f));
// Hard rule 1: this PR's VERSION must be strictly greater than the base
// version, otherwise we're not actually bumping.
const pBase = parseV((parsed.base_version ?? "").trim());
if (pBase && cmp(pPR, pBase) <= 0) {
if (dependabotManifestOnly) {
console.log(`::notice::${tag} is a dependabot dependency update (${changedFiles.join(", ")}); VERSION bump not required.`);
process.exit(0);
}
console.log(`::error::VERSION not bumped: ${tag} claims v${prVersion} but base is v${parsed.base_version}.`);
process.exit(1);
}
-18
View File
@@ -90,13 +90,7 @@
"test/ceo-count-ad-v2.test.ts\tTS2769\tNo overload matches this call. The last overload gave the following error. Argument of type '({ answered: boolean; answeredAt: string; answers: { ...; }; failed: boolean; questions: { header: string; multiSelect: boolean; options: { description: string; label: string; }[]; question: string; }[]; sessionId: string; toolUseId: string; unansweredQuestionIndices: never[]; } | { ...; } | { ...; } | { ...; } | { ...' is not assignable to parameter of type 'NativePlanQuestionCall[]'. Type '({ answered: boolean; answeredAt: string; answers: { ...; }; failed: boolean; questions: { header: string; multiSelect: boolean; options: { description: string; label: string; }[]; question: string; }[]; sessionId: string; toolUseId: string; unansweredQuestionIndices: never[]; } | { ...; } | { ...; } | { ...; } | { ...' is not assignable to type 'NativePlanQuestionCall[]'. Type '{ answered: boolean; answeredAt: string; answers: { ...; }; failed: boolean; questions: { header: string; multiSelect: boolean; options: { description: string; label: string; }[]; question: string; }[]; sessionId: string; toolUseId: string; unansweredQuestionIndices: never[]; } | { ...; } | { ...; } | { ...; } | { ....' is not assignable to type 'NativePlanQuestionCall'. Type '{ sessionId: string; toolUseId: string; questions: { header: string; multiSelect: boolean; options: { description: string; label: string; }[]; question: string; }[]; answered: boolean; failed: boolean; answers: { \"D0 \\u2014 Add gstack skill routing rules to this project's CLAUDE.md?\\nProject/branch/task: plan-count ...' is not assignable to type 'NativePlanQuestionCall'. Types of property 'answers' are incompatible. Type '{ \"D0 \\u2014 Add gstack skill routing rules to this project's CLAUDE.md?\\nProject/branch/task: plan-count fixture on main, reviewing PLAN.md (Stripe payment webhook handler) in HOLD SCOPE mode.\\nELI10: gstack works best when your project's CLAUDE.md includes skill routing rules, so future requests like \\\"review this...' is not assignable to type 'Record<string, string>'. Property '\"D1 \\u2014 Enable cross-project learnings search?\\nProject/branch/task: plan-count fixture on main, reviewing the Stripe payment webhook plan in HOLD SCOPE mode.\\nELI10: gstack can search learnings saved from your other projects on this machine to find patterns that might apply here. Everything stays local; no data leaves the machine. It helps solo developers most. Skip it if you work on multiple client codebases where mixing lessons between them would be a concern.\\nStakes if we pick wrong: Enabling on a multi-client machine could surface one client's quirks in another's review; disabling loses reusable patterns.\\nRecommendation: A because this is a local-only lookup and more prior context makes the review sharper.\\nNote: options differ in kind, not coverage \\u2014 no completeness score.\\nNet: broader recall versus strict per-project isolation.\"' is incompatible with index signature. Type 'undefined' is not assignable to type 'string'.": 1,
"test/ceo-expansion-auq.test.ts\tTS2339\tProperty 'is_error' does not exist on type '{ text: string; type: string; } | { caller: { ...; }; id: string; input: { questions: { header: string; multiSelect: boolean; options: { description: string; label: string; }[]; question: string; }[]; }; name: string; type: string; } | { ...; } | { ...; }'. Property 'is_error' does not exist on type '{ text: string; type: string; }'.": 1,
"test/ceo-expansion-auq.test.ts\tTS7053\tElement implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ toolu_014Q5SA47BqSvstMA1QpQzds: { reply: string; request: string; }; toolu_01GSNy9VjrqLscsjp6sceiV2: { reply: string; request: string; }; }'. No index signature with a parameter of type 'string' was found on type '{ toolu_014Q5SA47BqSvstMA1QpQzds: { reply: string; request: string; }; toolu_01GSNy9VjrqLscsjp6sceiV2: { reply: string; request: string; }; }'.": 5,
"test/ceo-finding-fixture.test.ts\tTS2322\tType 'string | number | string[]' is not assignable to type 'string'. Type 'number' is not assignable to type 'string'.": 1,
"test/ceo-finding-fixture.test.ts\tTS2339\tProperty 'map' does not exist on type 'string | number | string[] | readonly [\"Run /office-hours first\", \"\\\"Skip — standard review\\\"\"] | readonly [\"Run /office-hours first\", \"Skip\"] | readonly [\"Run /office-hours first\", \"Skip security review\"] | ... 4 more ... | readonly [...]'. Property 'map' does not exist on type 'string'.": 1,
"test/ceo-finding-fixture.test.ts\tTS2345\tArgument of type '(question: string | number | string[], labels: string | number | string[] | readonly [\"Run /office-hours first\", \"\\\"Skip — standard review\\\"\"] | readonly [\"Run /office-hours first\", \"Skip\"] | readonly [\"Run /office-hours first\", \"Skip security review\"] | ... 4 more ... | readonly [...], expected: string | ... 1 mo...' is not assignable to parameter of type '(...args: (string | number | string[])[] | [\"D1 — Discuss /office-hours in our documentation?\", string[], 1] | [\"D1 — No design doc found: run /office-hours before the review?\", readonly [...], 2] | ... 8 more ... | [...]) => void | Promise<...>'. Types of parameters 'question' and 'args' are incompatible. Type '(string | number | string[])[] | [\"D1 — Discuss /office-hours in our documentation?\", string[], 1] | [\"D1 — No design doc found: run /office-hours before the review?\", readonly [...], 2] | ... 8 more ... | [...]' is not assignable to type '[question: string | number | string[], labels: string | number | string[] | readonly [\"Run /office-hours first\", \"\\\"Skip — standard review\\\"\"] | readonly [\"Run /office-hours first\", \"Skip\"] | readonly [\"Run /office-hours first\", \"Skip security review\"] | ... 4 more ... | readonly [...], expected: string | ... 1 mo...'. Type '(string | number | string[])[]' is not assignable to type '[question: string | number | string[], labels: string | number | string[] | readonly [\"Run /office-hours first\", \"\\\"Skip — standard review\\\"\"] | readonly [\"Run /office-hours first\", \"Skip\"] | readonly [\"Run /office-hours first\", \"Skip security review\"] | ... 4 more ... | readonly [...], expected: string | ... 1 mo...'. Target requires 3 element(s) but source may have fewer.": 1,
"test/ceo-finding-fixture.test.ts\tTS2769\tNo overload matches this call. The last overload gave the following error. Argument of type 'string | number | string[]' is not assignable to parameter of type 'number'. Type 'string' is not assignable to type 'number'.": 1,
"test/ceo-finding-fixture.test.ts\tTS2769\tNo overload matches this call. The last overload gave the following error. Argument of type '{ kind: \"committed\" | \"failed\" | \"forbidden\" | \"unregistered-event\"; status: 200 | 403 | 503; }' is not assignable to parameter of type 'RequestOutcome'. Type '{ kind: \"committed\" | \"failed\" | \"forbidden\" | \"unregistered-event\"; status: 200 | 403 | 503; }' is not assignable to type '{ kind: \"failed\" | \"unregistered-event\"; status: 503; }'. Types of property 'status' are incompatible. Type '200 | 403 | 503' is not assignable to type '503'. Type '200' is not assignable to type '503'.": 1,
"test/ceo-finding-fixture.test.ts\tTS7006\tParameter 'i' implicitly has an 'any' type.": 1,
"test/ceo-finding-fixture.test.ts\tTS7006\tParameter 'label' implicitly has an 'any' type.": 1,
"test/ceo-hold-posture-review.test.ts\tTS2339\tProperty 'filePath' does not exist on type '{}'.": 4,
"test/ceo-hold-posture-review.test.ts\tTS2339\tProperty 'questions' does not exist on type 'AskUserQuestionFingerprint'.": 4,
"test/ceo-hold-posture-review.test.ts\tTS2339\tProperty 'selectedOptions' does not exist on type 'AskUserQuestionFingerprint'.": 1,
@@ -140,8 +134,6 @@
"test/cso-ntfs-fixture.test.ts\tTS2769\tNo overload matches this call. The last overload gave the following error. Argument of type 'bigint | undefined' is not assignable to parameter of type 'bigint'. Type 'undefined' is not assignable to type 'bigint'.": 1,
"test/cso-preparation-executor.test.ts\tTS2339\tProperty 'sidecar' does not exist on type 'PreparedDatabaseContract'. Property 'sidecar' does not exist on type '{ adapter: \"sqlite\"; connections: string[]; }'.": 1,
"test/cso-public-ghcr.test.ts\tTS2352\tConversion of type '() => Promise<Response>' to type 'typeof fetch' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. Property 'preconnect' is missing in type '() => Promise<Response>' but required in type 'typeof fetch'.": 1,
"test/cso-scanner-executor.test.ts\tTS2345\tArgument of type 'unknown' is not assignable to parameter of type '\"gitleaks\" | \"osv\" | \"schemathesis\" | \"semgrep\" | \"trivy\" | \"zizmor\"'.": 9,
"test/cso-scanner-executor.test.ts\tTS2769\tNo overload matches this call. The last overload gave the following error. The type 'readonly [\"gitleaks\", \"osv\", \"semgrep\", \"zizmor\", \"trivy\", \"schemathesis\"]' is 'readonly' and cannot be assigned to the mutable type 'unknown[]'.": 2,
"test/design-catalog.test.ts\tTS2769\tNo overload matches this call. The last overload gave the following error. Argument of type 'string' is not assignable to parameter of type '\"quality\" | \"slop\"'.": 1,
"test/design-completion-handoff-scored.test.ts\tTS2352\tConversion of type '({ answered: boolean; answeredAt: string; answers: { ...; }; failed: boolean; questions: { header: string; multiSelect: boolean; options: { description: string; label: string; }[]; question: string; }[]; sessionId: string; toolUseId: string; unansweredQuestionIndices: never[]; } | ... 10 more ... | { ...; })[]' to type 'NativePlanQuestionCall[]' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. Type '{ answered: boolean; answeredAt: string; answers: { ...; }; failed: boolean; questions: { header: string; multiSelect: boolean; options: { description: string; label: string; }[]; question: string; }[]; sessionId: string; toolUseId: string; unansweredQuestionIndices: never[]; } | ... 10 more ... | { ...; }' is not comparable to type 'NativePlanQuestionCall'. Type '{ sessionId: string; toolUseId: string; questions: { header: string; multiSelect: boolean; options: { description: string; label: string; }[]; question: string; }[]; answered: boolean; failed: boolean; answers: { \"Pass 1 (Info Architecture) \\u2014 7/10. The plan has DOM order and heading structure, but no scan path ...' is not comparable to type 'NativePlanQuestionCall'. Types of property 'answers' are incompatible. Type '{ \"Pass 1 (Info Architecture) \\u2014 7/10. The plan has DOM order and heading structure, but no scan path specification: what does the user's eye land on first, second, third? The Visual Hierarchy gap identifies that Save is indistinguishable from other buttons, but names it as a styling problem rather than an IA pr...' is not comparable to type 'Record<string, string>'. Property '\"Pass 1 (Info Architecture) \\u2014 7/10. The plan has DOM order and heading structure, but no scan path specification: what does the user's eye land on first, second, third? The Visual Hierarchy gap identifies that Save is indistinguishable from other buttons, but names it as a styling problem rather than an IA problem \\u2014 the primary action is missing from the visual hierarchy. Should I add a scan path description to the plan? <gstack-qid:plan-design-review-ia-scan-path>\"' is incompatible with index signature. Type 'undefined' is not comparable to type 'string'.": 1,
"test/design-completion-handoff-scored.test.ts\tTS2352\tConversion of type '({ answered: boolean; answeredAt: string; answers: { ...; }; failed: boolean; questions: { header: string; multiSelect: boolean; options: { description: string; label: string; }[]; question: string; }[]; sessionId: string; toolUseId: string; unansweredQuestionIndices: never[]; } | ... 4 more ... | { ...; })[]' to type 'NativePlanQuestionCall[]' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. Type '{ answered: boolean; answeredAt: string; answers: { ...; }; failed: boolean; questions: { header: string; multiSelect: boolean; options: { description: string; label: string; }[]; question: string; }[]; sessionId: string; toolUseId: string; unansweredQuestionIndices: never[]; } | ... 4 more ... | { ...; }' is not comparable to type 'NativePlanQuestionCall'. Type '{ sessionId: string; toolUseId: string; questions: { header: string; multiSelect: boolean; options: { description: string; label: string; }[]; question: string; }[]; answered: boolean; failed: boolean; answers: { \"Pass 1 \\u2014 Visual Hierarchy: The plan lists this gap but has no fix. The Save button renders with th...' is not comparable to type 'NativePlanQuestionCall'. Types of property 'answers' are incompatible. Type '{ \"Pass 1 \\u2014 Visual Hierarchy: The plan lists this gap but has no fix. The Save button renders with the same size, weight, and color as Reset, Cancel, and Export. DESIGN.md already specifies the remedy: Save gets #1d4ed8 fill with white text; the other three are ghost neutral buttons. Should I add this fix speci...' is not comparable to type 'Record<string, string>'. Property '\"Pass 1 \\u2014 Visual Hierarchy: The plan lists this gap but has no fix. The Save button renders with the same size, weight, and color as Reset, Cancel, and Export. DESIGN.md already specifies the remedy: Save gets #1d4ed8 fill with white text; the other three are ghost neutral buttons. Should I add this fix specification to the plan? <gstack-qid:plan-design-review-gap1-visual-hierarchy>\"' is incompatible with index signature. Type 'undefined' is not comparable to type 'string'.": 1,
@@ -258,10 +250,6 @@
"test/helpers/shared-libs-path-fixture.ts\tTS2352\tConversion of type '{ env: { GSTACK_HOME: string; }; repo: string; root: string; state: string; }' to type 'SharedLibsFixture' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. Type '{ env: { GSTACK_HOME: string; }; repo: string; root: string; state: string; }' is missing the following properties from type 'SharedLibsFixture': bin, trace, hookTrace, tip": 1,
"test/helpers/shared-libs-plan-actor.ts\tTS18046\t'questions' is of type 'unknown'.": 1,
"test/impeccable-fixtures.test.ts\tTS2769\tNo overload matches this call. The last overload gave the following error. Argument of type 'string | undefined' is not assignable to parameter of type 'string'. Type 'undefined' is not assignable to type 'string'.": 1,
"test/llm-judge-abort.test.ts\tTS2769\tNo overload matches this call. The last overload gave the following error. Argument of type '{ passed: boolean; }' is not assignable to parameter of type 'undefined'.": 3,
"test/llm-judge-frontier.test.ts\tTS2769\tNo overload matches this call. The last overload gave the following error. Argument of type '{ reason: string; score: number; }' is not assignable to parameter of type 'undefined'.": 1,
"test/llm-judge-frontier.test.ts\tTS2769\tNo overload matches this call. The last overload gave the following error. Argument of type '{ score: number; }' is not assignable to parameter of type 'undefined'.": 3,
"test/llm-judge-stream.test.ts\tTS2769\tNo overload matches this call. The last overload gave the following error. Argument of type '{ actionability: number; clarity: number; completeness: number; reasoning: string; }' is not assignable to parameter of type 'undefined'.": 1,
"test/model-overlays.test.ts\tTS2322\tType 'string' is not assignable to type '\"claude\" | \"fable-5\" | \"gemini\" | \"gpt\" | \"gpt-5.4\" | \"gpt-5.6-sol\" | \"gpt-6-astra\" | \"o-series\" | \"opus-4-7\" | \"opus-4-8\" | \"sonnet-5\" | undefined'.": 4,
"test/native-auto-decide-pty.test.ts\tTS2345\tArgument of type 'string | undefined' is not assignable to parameter of type 'string'. Type 'undefined' is not assignable to type 'string'.": 1,
"test/native-auto-decide.test.ts\tTS2345\tArgument of type '{ assistantMessages: { sessionId: string; text: string; timestamp: string; }[]; calls: never[]; status: string; }' is not assignable to parameter of type 'PlanCountTranscript'. Types of property 'status' are incompatible. Type 'string' is not assignable to type '\"error\" | \"missing\" | \"ready\"'.": 1,
@@ -423,8 +411,6 @@
"test/pty-workspace-trust.test.ts\tTS2345\tArgument of type '(_command: any, options: any) => any' is not assignable to parameter of type '{ <const In extends SpawnOptions.Writable = \"ignore\", const Out extends SpawnOptions.Readable = \"pipe\", const Err extends SpawnOptions.Readable = \"inherit\">(options: SpawnOptions<In, Out, Err> & { cmd: string[]; }): Subprocess<...>; <const In extends SpawnOptions.Writable = \"ignore\", const Out extends SpawnOptions.R...'. Target signature provides too few arguments. Expected 2 or more, but got 1.": 1,
"test/pty-workspace-trust.test.ts\tTS2559\tType '5000' has no properties in common with type '{ pollMs?: number | undefined; since?: number | undefined; timeoutMs?: number | undefined; }'.": 1,
"test/qa-browser-preservation.test.ts\tTS2769\tNo overload matches this call. The last overload gave the following error. Argument of type 'number | null' is not assignable to parameter of type 'number'. Type 'null' is not assignable to type 'number'.": 1,
"test/qa-exploratory-callers.test.ts\tTS2345\tArgument of type 'unknown' is not assignable to parameter of type '\"review-exploratory-small-cli\" | \"ship-exploratory-late-input\" | \"ship-exploratory-plan-checks\" | \"ship-exploratory-small-cli\" | \"ship-exploratory-unavailable\"'.": 1,
"test/qa-exploratory-callers.test.ts\tTS2769\tNo overload matches this call. The last overload gave the following error. The type 'readonly [\"review-exploratory-small-cli\", \"ship-exploratory-small-cli\", \"ship-exploratory-unavailable\", \"ship-exploratory-plan-checks\", \"ship-exploratory-late-input\"]' is 'readonly' and cannot be assigned to the mutable type 'unknown[]'.": 1,
"test/qa-functional-fixture.test.ts\tTS7006\tParameter 'request' implicitly has an 'any' type.": 2,
"test/qa-functional-prompt.test.ts\tTS18046\t'capture' is of type 'unknown'.": 4,
"test/qa-functional-prompt.test.ts\tTS7006\tParameter 'block' implicitly has an 'any' type.": 2,
@@ -437,9 +423,7 @@
"test/review-count-markdown.test.ts\tTS2352\tConversion of type '({ answered: boolean; answeredAt: string; answers: { ...; }; failed: boolean; questions: { header: string; multiSelect: boolean; options: { description: string; label: string; }[]; question: string; }[]; sessionId: string; toolUseId: string; unansweredQuestionIndices: never[]; } | { ...; } | { ...; } | { ...; } | { ...' to type 'NativePlanQuestionCall[]' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. Type '({ answered: boolean; answeredAt: string; answers: { ...; }; failed: boolean; questions: { header: string; multiSelect: boolean; options: { description: string; label: string; }[]; question: string; }[]; sessionId: string; toolUseId: string; unansweredQuestionIndices: never[]; } | ... 7 more ... | { ...; })[]' is not comparable to type 'NativePlanQuestionCall[]'. Type '{ answered: boolean; answeredAt: string; answers: { ...; }; failed: boolean; questions: { header: string; multiSelect: boolean; options: { description: string; label: string; }[]; question: string; }[]; sessionId: string; toolUseId: string; unansweredQuestionIndices: never[]; } | ... 7 more ... | { ...; }' is not comparable to type 'NativePlanQuestionCall'. Type '{ sessionId: string; toolUseId: string; questions: { header: string; multiSelect: boolean; options: { description: string; label: string; }[]; question: string; }[]; answered: boolean; failed: boolean; answers: { \"D1 \\u2014 Add gstack skill routing rules to this project's CLAUDE.md?\\nProject/branch/task: plan-count ...' is not comparable to type 'NativePlanQuestionCall'. Types of property 'answers' are incompatible. Type '{ \"D1 \\u2014 Add gstack skill routing rules to this project's CLAUDE.md?\\nProject/branch/task: plan-count fixture on main, running /plan-design-review on PLAN.md.\\nELI10: gstack skills work best when the project's CLAUDE.md tells the assistant which slash command to reach for (bugs \\u2192 /investigate, design plan \\...' is not comparable to type 'Record<string, string>'. Property '\"D1 \\u2014 Add gstack skill routing rules to this project's CLAUDE.md?\\nProject/branch/task: plan-count fixture on main, running /plan-design-review on PLAN.md.\\nELI10: gstack skills work best when the project's CLAUDE.md tells the assistant which slash command to reach for (bugs \\u2192 /investigate, design plan \\u2192 /plan-design-review, and so on). Without it you invoke skills by hand each time. This is a one-time prompt per project.\\nStakes if we pick wrong: pick A and CLAUDE.md gains a short section you may not want in a fixture repo; pick B and skills are never suggested automatically here.\\nRecommendation: A because routing rules cost one short section and save repeated manual invocations.\\nNote: options differ in kind, not coverage \\u2014 no completeness score.\\nNet: convenience of auto-routing vs keeping the fixture CLAUDE.md untouched. Note: plan mode is active, so if you pick A the CLAUDE.md edit and commit happen after we leave plan mode.\"' is incompatible with index signature. Type 'undefined' is not comparable to type 'string'.": 1,
"test/salience-allowlist.test.ts\tTS2307\tCannot find module '../bin/gstack-brain-cache' or its corresponding type declarations.": 3,
"test/schema-version-migration.test.ts\tTS2307\tCannot find module '../bin/gstack-brain-cache' or its corresponding type declarations.": 3,
"test/schema-version-migration.test.ts\tTS2353\tObject literal may only specify known properties, and 'timeout' does not exist in type '(done: (err?: unknown) => void) => void | Promise<unknown>'.": 3,
"test/section-capture-native-tools.test.ts\tTS2339\tProperty 'CI' does not exist on type '{ NODE_ENV?: string | undefined; PATH: string; TZ?: string | undefined; }'.": 1,
"test/session-runner-browse-errors.test.ts\tTS2769\tNo overload matches this call. The last overload gave the following error. Argument of type 'never[]' is not assignable to parameter of type 'undefined'.": 1,
"test/session-runner-browse-errors.test.ts\tTS7006\tParameter 'row' implicitly has an 'any' type.": 6,
"test/setup-gbrain-path4-caller.test.ts\tTS2339\tProperty 'diagnostic-secret' does not exist on type '{ 'no-init': number; 'no-install': number; 'no-registration': number; 'no-verifier': number; }'.": 1,
"test/setup-gbrain-path4-caller.test.ts\tTS2339\tProperty 'exit' does not exist on type '{ 'no-init': number; 'no-install': number; 'no-registration': number; 'no-verifier': number; }'.": 1,
@@ -453,12 +437,10 @@
"test/setup-gbrain-path4-caller.test.ts\tTS2339\tProperty 'wrong-engine' does not exist on type '{ 'no-init': number; 'no-install': number; 'no-registration': number; 'no-verifier': number; }'.": 1,
"test/setup-gbrain-remote-caller.test.ts\tTS18048\t'lateDecision' is possibly 'undefined'.": 1,
"test/shared-libs-checker-interface-evidence.test.ts\tTS18048\t'current' is possibly 'undefined'.": 1,
"test/shared-libs-checker-interface-evidence.test.ts\tTS2769\tNo overload matches this call. The last overload gave the following error. The type 'readonly [\"symlinks\", \"submodule\", \"ignored\", \"legacy\", \"assume-unchanged\", \"skip-worktree\", \"removed-filter\"]' is 'readonly' and cannot be assigned to the mutable type 'unknown[]'.": 2,
"test/shared-libs-fixture.test.ts\tTS18046\t'packet' is of type 'unknown'.": 4,
"test/shared-libs-fixture.test.ts\tTS2345\tArgument of type '(options: { label: string; } | { description: string; label: string; } | { description: string; label: string; }) => Promise<void>' is not assignable to parameter of type '(...args: [{ label: string; } | { description: string; label: string; } | { description: string; label: string; }, done: (err?: unknown) => void] | [{ label: string; } | { description: string; label: string; } | { ...; }, { ...; } | undefined, done: (err?: unknown) => void]) => void | Promise<...>'. Types of parameters 'options' and 'args' are incompatible. Type '[{ label: string; } | { description: string; label: string; } | { description: string; label: string; }, done: (err?: unknown) => void] | [{ label: string; } | { description: string; label: string; } | { description: string; label: string; }, { ...; } | undefined, done: (err?: unknown) => void]' is not assignable to type '[options: { label: string; } | { description: string; label: string; } | { description: string; label: string; }]'. Type '[{ label: string; } | { description: string; label: string; } | { description: string; label: string; }, done: (err?: unknown) => void]' is not assignable to type '[options: { label: string; } | { description: string; label: string; } | { description: string; label: string; }]'. Source has 2 element(s) but target allows only 1.": 1,
"test/shared-libs-fixture.test.ts\tTS2345\tArgument of type '(options: { label: string; } | { description: string; label: string; } | { label: string; preview: string; } | { description: string; label: string; preview: string; } | { label: string; } | { description: string; label: string; } | { ...; }) => Promise<...>' is not assignable to parameter of type '(...args: [{ label: string; } | { description: string; label: string; } | { label: string; preview: string; } | { description: string; label: string; preview: string; } | { label: string; } | { description: string; label: string; } | { ...; }, done: (err?: unknown) => void] | [...]) => void | Promise<...>'. Types of parameters 'options' and 'args' are incompatible. Type '[{ label: string; } | { description: string; label: string; } | { label: string; preview: string; } | { description: string; label: string; preview: string; } | { label: string; } | { description: string; label: string; } | { ...; }, done: (err?: unknown) => void] | [...]' is not assignable to type '[options: { label: string; } | { description: string; label: string; } | { label: string; preview: string; } | { description: string; label: string; preview: string; } | { label: string; } | { description: string; label: string; } | { ...; }]'. Type '[{ label: string; } | { description: string; label: string; } | { label: string; preview: string; } | { description: string; label: string; preview: string; } | { label: string; } | { description: string; label: string; } | { ...; }, done: (err?: unknown) => void]' is not assignable to type '[options: { label: string; } | { description: string; label: string; } | { label: string; preview: string; } | { description: string; label: string; preview: string; } | { label: string; } | { description: string; label: string; } | { ...; }]'. Source has 2 element(s) but target allows only 1.": 1,
"test/shared-libs-fixture.test.ts\tTS2345\tArgument of type '({ input }: { input: any; }) => Promise<void>' is not assignable to parameter of type '(args_0: unknown, ...args: unknown[]) => void | Promise<unknown>'. Types of parameters '__0' and 'args_0' are incompatible. Type 'unknown' is not assignable to type '{ input: any; }'.": 1,
"test/shared-libs-fixture.test.ts\tTS2769\tNo overload matches this call. The last overload gave the following error. Argument of type 'readonly [\"Skip\", \"Keep current\", \"Decline\", \"Do not change\", \"Leave as-is\"] | readonly [\"Fix it\", \"Apply remedy\", \"Approve\", \"Extract helper\", \"Reuse library\", \"Choice (recommended)\"]' is not assignable to parameter of type 'unknown[]'. The type 'readonly [\"Skip\", \"Keep current\", \"Decline\", \"Do not change\", \"Leave as-is\"]' is 'readonly' and cannot be assigned to the mutable type 'unknown[]'.": 1,
"test/shared-libs-fixture.test.ts\tTS7053\tElement implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ \"Pre-Landing Review: 0 issues (0 critical, 0 informational). 1 [ADVISORY] needs your input:\\n\\n1. [ADVISORY] src/retry-worker.ts:2 \\u2014 Duplicated `retrySeconds` parser (shared-libs, confidence 9/10, maintainability + core)\\n Both src/retry-worker.ts:2-15 (this diff) and src/retry-route.ts:2-15 are verbatim co...'. No index signature with a parameter of type 'string' was found on type '{ \"Pre-Landing Review: 0 issues (0 critical, 0 informational). 1 [ADVISORY] needs your input:\\n\\n1. [ADVISORY] src/retry-worker.ts:2 \\u2014 Duplicated `retrySeconds` parser (shared-libs, confidence 9/10, maintainability + core)\\n Both src/retry-worker.ts:2-15 (this diff) and src/retry-route.ts:2-15 are verbatim co...'.": 1,
"test/shared-libs-review-start-evidence.test.ts\tTS2345\tArgument of type 'unknown' is not assignable to parameter of type 'number | undefined'.": 1,
"test/shared-libs-stage-actor.test.ts\tTS2769\tNo overload matches this call. The last overload gave the following error. Argument of type 'string | undefined' is not assignable to parameter of type 'string'. Type 'undefined' is not assignable to type 'string'.": 1,
+1 -1
View File
@@ -262,7 +262,7 @@ test.each([
[reviewStartLead, reviewStartLabels, 2],
['D1 — No design doc found: run /office-hours before the review?', ['Run /office-hours now', 'Skip — proceed with review (Recommended)'], 2],
[reviewStartLead, ['Skip — standard review', 'Run /office-hours first'], 1],
...['Example: ', 'If approved: ', 'Do not ', '> ', ' ', '"', '`'].map(prefix => [prefix + reviewStartLead, reviewStartLabels, 1]),
...['Example: ', 'If approved: ', 'Do not ', '> ', ' ', '"', '`'].map(prefix => [prefix + reviewStartLead, reviewStartLabels, 1] as const),
['D1 — Discuss /office-hours in our documentation?', reviewStartLabels, 1],
['D1 — Run /office-hours instead of this review?', reviewStartLabels, 1],
[reviewStartLead, ['Run /office-hours first', 'Skip'], 1],
+80
View File
@@ -0,0 +1,80 @@
/**
* Version Gate (scripts/compare-pr-version.ts). Hard rule 1 requires VERSION
* above the base, which dependabot can never satisfy, so a dependabot PR that
* touches only dependency manifests, lockfiles and dependabot config is exempt.
* Every other author or file keeps the rule.
*/
import { describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
const ROOT = path.resolve(import.meta.dir, '..');
const SCRIPT = path.join(ROOT, 'scripts', 'compare-pr-version.ts');
function gate(env: { PR_VERSION: string; PR_AUTHOR?: string; PR_CHANGED_FILES?: string }) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'version-gate-'));
try {
const next = path.join(dir, 'next.json');
fs.writeFileSync(next, JSON.stringify({ version: '1.91.117.0', base_version: '1.91.116.0', claimed: [{ pr: 3236, branch: 'fix-wave-5', version: '1.91.117.0' }] }));
const run = Bun.spawnSync([process.execPath, 'run', SCRIPT, next, '3227'], {
env: { PATH: process.env.PATH ?? '', ...env },
timeout: 30_000,
stdout: 'pipe',
stderr: 'pipe',
});
return { code: run.exitCode, out: run.stdout.toString() + run.stderr.toString() };
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
}
const DEPENDABOT = 'dependabot[bot]';
describe('version gate: dependabot dependency-manifest exemption', () => {
test('an unbumped VERSION still fails for a human author', () => {
const result = gate({ PR_VERSION: '1.91.116.0', PR_AUTHOR: 'garrytan', PR_CHANGED_FILES: 'package.json\nbun.lock' });
expect(result.code).toBe(1);
expect(result.out).toContain('::error::VERSION not bumped');
});
test('a dependabot PR behind main that touches only manifests, lockfiles and dependabot config passes', () => {
const files = ['package.json', 'bun.lock', 'lib/diagram-render/package.json', 'lib/diagram-render/bun.lock', '.github/dependabot.yml'].join('\n');
const result = gate({ PR_VERSION: '1.91.106.0', PR_AUTHOR: DEPENDABOT, PR_CHANGED_FILES: files + '\n' });
expect(result.code).toBe(0);
expect(result.out).toContain('::notice::PR #3227 is a dependabot dependency update');
expect(result.out).not.toContain('::error::');
});
test('a dependabot PR with any other file keeps hard rule 1', () => {
for (const other of ['scripts/compare-pr-version.ts', 'VERSION', '.github/workflows/version-gate.yml', 'evil-package.json', 'package.json.bak']) {
const result = gate({ PR_VERSION: '1.91.116.0', PR_AUTHOR: DEPENDABOT, PR_CHANGED_FILES: `package.json\nbun.lock\n${other}` });
expect(result.code).toBe(1);
expect(result.out).toContain('::error::VERSION not bumped');
}
});
test('a missing file list or author keeps hard rule 1', () => {
expect(gate({ PR_VERSION: '1.91.116.0', PR_AUTHOR: DEPENDABOT, PR_CHANGED_FILES: '' }).code).toBe(1);
expect(gate({ PR_VERSION: '1.91.116.0', PR_AUTHOR: DEPENDABOT }).code).toBe(1);
expect(gate({ PR_VERSION: '1.91.116.0', PR_CHANGED_FILES: 'package.json' }).code).toBe(1);
});
test('a bumped dependabot VERSION goes through the collision rule as before', () => {
const result = gate({ PR_VERSION: '1.91.117.0', PR_AUTHOR: DEPENDABOT, PR_CHANGED_FILES: 'package.json' });
expect(result.code).toBe(1);
expect(result.out).toContain('::error::VERSION collision');
});
test('the workflow passes the PR author and its changed-file list to the gate', () => {
const workflow = Bun.YAML.parse(fs.readFileSync(path.join(ROOT, '.github', 'workflows', 'version-gate.yml'), 'utf8')) as any;
const step = workflow.jobs.check.steps.find((s: any) => s.name === 'Compare PR VERSION to next free slot');
expect(step.env.PR_AUTHOR).toBe('${{ github.event.pull_request.user.login }}');
expect(step.env.GH_TOKEN).toBe('${{ secrets.GITHUB_TOKEN }}');
expect(step.run).toContain('pulls/${{ github.event.pull_request.number }}/files');
expect(step.run).toContain('--paginate');
expect(step.run).toContain('then PR_CHANGED_FILES=""; fi');
expect(step.run).toContain('export PR_CHANGED_FILES');
expect(workflow.jobs.check.permissions).toEqual({ contents: 'read', 'pull-requests': 'read' });
});
});
+8
View File
@@ -50,4 +50,12 @@ describe("osv-scanner config wiring", () => {
const page = fs.readFileSync(path.join(ROOT, "lib", "diagram-render", "dist", "diagram-render.html"), "utf8");
expect(page).not.toMatch(/chokidar|FSWatcher/);
});
test("the chokidar override keeps braces (GHSA-vfj7-8cjw-p6xm, no fixed release) out of lib/diagram-render", () => {
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, "lib", "diagram-render", "package.json"), "utf8"));
expect(pkg.overrides.chokidar).toMatch(/^4\./);
const lock = fs.readFileSync(path.join(ROOT, "lib", "diagram-render", "bun.lock"), "utf8");
expect(lock).toMatch(/"chokidar": \["chokidar@4\.\d+\.\d+"/);
expect(lock).not.toMatch(/"braces": \["braces@/);
});
});
+8 -11
View File
@@ -58,18 +58,15 @@ describe('OSV suppression expiry', () => {
for (const text of ['"$SCAN_RESULT" = "failure"', 'gh issue create', 'gh issue comment', 'gh issue close']) expect(run).toContain(text);
});
test('the image-size entries name the fixed release and the pin that blocks it (#2753)', () => {
const toml = fs.readFileSync(path.join(import.meta.dir, '..', '.osv-scanner.toml'), 'utf8');
const entries = toml.split('[[IgnoredVulns]]').slice(1).filter(e => /^id = "GHSA-(?:5p2g-fcmc-qvqq|w3rx-r6r6-pgpr)"/m.test(e));
expect(entries).toHaveLength(2);
for (const entry of entries) {
expect(entry).not.toContain('No fixed version exists');
expect(entry).toMatch(/^reason = ".*image-size 2\.0\.3.*\^1\.0\.0.*"$/m);
expect(entry).toContain('ignoreUntil = 2026-11-30T00:00:00Z');
}
// The stated blocker is the lockfile's: html-to-docx asks for image-size ^1.0.0 and resolves a 1.x.
test('image-size is overridden to a fixed 2.x and its suppressions are gone (#2753)', () => {
const toml = fs.readFileSync(path.join(ROOT, CONFIG), 'utf8');
expect(toml).not.toMatch(/GHSA-(?:5p2g-fcmc-qvqq|w3rx-r6r6-pgpr)/);
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
expect(pkg.overrides['image-size']).toBe('2.0.4');
// html-to-docx still asks for ^1.0.0; the override is what moves the lockfile off the affected 1.x.
const lock = fs.readFileSync(path.join(ROOT, 'bun.lock'), 'utf8');
expect(lock).toMatch(/"html-to-docx": \["html-to-docx@[^"]+", "", \{ "dependencies": \{[^}]*"image-size": "\^1\.0\.0"/);
expect(lock).toMatch(/"image-size": \["image-size@1\.\d+\.\d+"/);
expect(lock).toMatch(/"image-size": \["image-size@2\.0\.4"/);
expect(lock).not.toMatch(/"image-size": \["image-size@1\./);
});
});
+9
View File
@@ -113,4 +113,13 @@ describe('workflow action pins', () => {
expect(ignored[name]).toEqual({ 'dependency-name': name, 'update-types': ['version-update:semver-minor', 'version-update:semver-major'] });
}
});
test('Dependabot holds the Anthropic SDKs to patch updates; their 0.x breaking releases arrive as semver-minor', () => {
const config = Bun.YAML.parse(fs.readFileSync(path.join(GITHUB_DIR, 'dependabot.yml'), 'utf-8')) as any;
const bun = (config.updates ?? []).find((update: any) => update['package-ecosystem'] === 'bun');
const ignored = Object.fromEntries((bun?.ignore ?? []).map((rule: any) => [rule['dependency-name'], rule]));
for (const name of ['@anthropic-ai/claude-agent-sdk', '@anthropic-ai/sdk']) {
expect(ignored[name]).toEqual({ 'dependency-name': name, 'update-types': ['version-update:semver-minor', 'version-update:semver-major'] });
}
});
});
+2 -1
View File
@@ -8,7 +8,8 @@
"design/test/**/*.ts",
"ios-qa/**/*.test.ts",
"ios-qa/daemon/test/**/*.ts",
"make-pdf/src/html-to-docx.d.ts"
"make-pdf/src/html-to-docx.d.ts",
"make-pdf/src/html-to-vdom.d.ts"
],
"exclude": [
"lib/diagram-render/**",