From 9dea818e34d6964229bca83eed112eaedaeef507 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sat, 17 Oct 2026 09:47:50 -0700 Subject: [PATCH] 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> --- .github/dependabot.yml | 8 + .github/workflows/version-gate.yml | 4 + .osv-scanner.toml | 75 +------- CHANGELOG.md | 17 ++ TODOS.md | 11 +- VERSION | 2 +- agents-digest/gstack-AGENTS.md | 2 +- bun.lock | 16 +- lib/diagram-render/bun.lock | 31 +--- lib/diagram-render/package.json | 1 + make-pdf/src/docx-images.ts | 78 +++++++++ make-pdf/src/html-to-vdom.d.ts | 3 + make-pdf/src/orchestrator.ts | 2 + make-pdf/test/docx-image-size.test.ts | 113 ++++++++++++ make-pdf/test/fixtures/docx-images/doc.md | 11 ++ .../fixtures/docx-images/document.golden.xml | 163 ++++++++++++++++++ make-pdf/test/fixtures/docx-images/square.gif | Bin 0 -> 110 bytes make-pdf/test/fixtures/docx-images/tall.jpg | Bin 0 -> 287 bytes make-pdf/test/fixtures/docx-images/wide.png | Bin 0 -> 328 bytes package.json | 8 +- scripts/compare-pr-version.ts | 14 ++ scripts/typecheck-test-baseline.json | 18 -- test/ceo-finding-fixture.test.ts | 2 +- test/compare-pr-version.test.ts | 80 +++++++++ test/osv-config-wiring.test.ts | 8 + test/osv-ignore-expiry.test.ts | 19 +- test/workflow-action-pins.test.ts | 9 + tsconfig.test.json | 3 +- 28 files changed, 554 insertions(+), 144 deletions(-) create mode 100644 make-pdf/src/docx-images.ts create mode 100644 make-pdf/src/html-to-vdom.d.ts create mode 100644 make-pdf/test/docx-image-size.test.ts create mode 100644 make-pdf/test/fixtures/docx-images/doc.md create mode 100644 make-pdf/test/fixtures/docx-images/document.golden.xml create mode 100644 make-pdf/test/fixtures/docx-images/square.gif create mode 100644 make-pdf/test/fixtures/docx-images/tall.jpg create mode 100644 make-pdf/test/fixtures/docx-images/wide.png create mode 100644 test/compare-pr-version.test.ts diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3fdfdc9f2..7392520e5 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -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" diff --git a/.github/workflows/version-gate.yml b/.github/workflows/version-gate.yml index e034aaa3e..4943a1f6e 100644 --- a/.github/workflows/version-gate.yml +++ b/.github/workflows/version-gate.yml @@ -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 }}" diff --git a/.osv-scanner.toml b/.osv-scanner.toml index cac5e40a6..7062905f8 100644 --- a/.osv-scanner.toml +++ b/.osv-scanner.toml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 79fc1201d..7dfa0a6e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/TODOS.md b/TODOS.md index e8ab35a1b..aa26453fa 100644 --- a/TODOS.md +++ b/TODOS.md @@ -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 diff --git a/VERSION b/VERSION index f3edfab61..e0b7c3105 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.91.121.0 +1.91.122.0 diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md index f39a93c46..47072b96d 100644 --- a/agents-digest/gstack-AGENTS.md +++ b/agents-digest/gstack-AGENTS.md @@ -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, diff --git a/bun.lock b/bun.lock index db2f7ae8d..f96507bb4 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/lib/diagram-render/bun.lock b/lib/diagram-render/bun.lock index fa55c8ade..ccf979184 100644 --- a/lib/diagram-render/bun.lock +++ b/lib/diagram-render/bun.lock @@ -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=="], diff --git a/lib/diagram-render/package.json b/lib/diagram-render/package.json index 6c4bc4ef6..8a41abd38 100644 --- a/lib/diagram-render/package.json +++ b/lib/diagram-render/package.json @@ -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", diff --git a/make-pdf/src/docx-images.ts b/make-pdf/src/docx-images.ts new file mode 100644 index 000000000..1df00b6d6 --- /dev/null +++ b/make-pdf/src/docx-images.ts @@ -0,0 +1,78 @@ +/** + * DOCX image guard. html-to-docx 1.8.0 sizes every data-URI 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 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 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 = /]*>/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]+[\t ]+<").replace(/>[\t ]+$/g, ">"); +} + +function* imgSources(nodes: ImgNode | ImgNode[]): Generator { + 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 tag hides an ICNS image, which html-to-docx's bundled image-size hangs on; convert or remove it"); + } + } + return guarded; +} diff --git a/make-pdf/src/html-to-vdom.d.ts b/make-pdf/src/html-to-vdom.d.ts new file mode 100644 index 000000000..4224f4fdf --- /dev/null +++ b/make-pdf/src/html-to-vdom.d.ts @@ -0,0 +1,3 @@ +declare module 'html-to-vdom' { + export default function HTMLToVDOM(dependencies: { VNode: Function; VText: Function }): (html: string) => any; +} diff --git a/make-pdf/src/orchestrator.ts b/make-pdf/src/orchestrator.ts index b392e6393..4fe440546 100644 --- a/make-pdf/src/orchestrator.ts +++ b/make-pdf/src/orchestrator.ts @@ -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 { progress.end("Rasterizing diagrams for DOCX"); } finalHtml = convertDiagnosticsForDocx(finalHtml); + finalHtml = guardDocxImages(finalHtml, warn); } // ─── --to html: write the self-contained document, no print round-trip ── diff --git a/make-pdf/test/docx-image-size.test.ts b/make-pdf/test/docx-image-size.test.ts new file mode 100644 index 000000000..8301303c9 --- /dev/null +++ b/make-pdf/test/docx-image-size.test.ts @@ -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 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(//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(`

`)).toBe(`

`); + expect(warnings).toEqual([]); + for (const tag of [ + ``, + `x`, + ``, + ``, + ``, + ``, + ``, + ``, + ]) expect(guard(`

${tag}

`)).toBe("

"); + 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(`

>

`, () => {})).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(""); + } finally { + fs.rmSync(workDir, { recursive: true, force: true }); + } + }); +}); diff --git a/make-pdf/test/fixtures/docx-images/doc.md b/make-pdf/test/fixtures/docx-images/doc.md new file mode 100644 index 000000000..fd1fdec1a --- /dev/null +++ b/make-pdf/test/fixtures/docx-images/doc.md @@ -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) diff --git a/make-pdf/test/fixtures/docx-images/document.golden.xml b/make-pdf/test/fixtures/docx-images/document.golden.xml new file mode 100644 index 000000000..e9e4c4c04 --- /dev/null +++ b/make-pdf/test/fixtures/docx-images/document.golden.xml @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + Image sizing smoke + + + + + + + + + A wide PNG: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A tall JPEG and a square GIF: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/make-pdf/test/fixtures/docx-images/square.gif b/make-pdf/test/fixtures/docx-images/square.gif new file mode 100644 index 0000000000000000000000000000000000000000..3fcc4a7c726df05cb9e8aeb6bcb57c4669478915 GIT binary patch literal 110 zcmZ?wbhEHbbYO5`XkcLQ$t)B(5vBN(g_QvabU*}1hJneor+?+?xBQFeY`N9F`QD!2 z{B4gsrakLi_UhEOcihK6`CR|j`~26r@BjFDxY~~_`q<&4HT}%8&z!wp%df2Z+I2Q- M`<->>0SpY*00poy(EtDd literal 0 HcmV?d00001 diff --git a/make-pdf/test/fixtures/docx-images/tall.jpg b/make-pdf/test/fixtures/docx-images/tall.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7bdd8c0a98ce53a72a4981bd72b2632e582ee6ca GIT binary patch literal 287 zcmb7-OAdlC6h+^&(83E*8n9sfU?gZUlCT3!+=a_=D<-bPfs637m|BJ~^9)b!$G+?r z&cHpN`yS^$7lH>A6NMqA<4F`}X`W}PQhHexdR{J+S|8Tsc31D~S<$qOX)9BkZUlu8 z6i`Bx7_GGV8~Xqmzy($eH!zaOB-$rT0TKFer_*?7!C8flL0EKYIj{is0}(^_!z3Kx Miuc>8Uj(%8A3CWd^8f$< literal 0 HcmV?d00001 diff --git a/make-pdf/test/fixtures/docx-images/wide.png b/make-pdf/test/fixtures/docx-images/wide.png new file mode 100644 index 0000000000000000000000000000000000000000..f388b4ab726a390f111f63b50f3c572b2ea394f1 GIT binary patch literal 328 zcmeAS@N?(olHy`uVBq!ia0vp^6+mpj!3HF6o?j*RC03YNt{|7u;G)CK#ai@_k{^hRTXw5X!JWBJmu|Ba4( literal 0 HcmV?d00001 diff --git a/package.json b/package.json index aa46790a8..41bab4c69 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/scripts/compare-pr-version.ts b/scripts/compare-pr-version.ts index 27f746aaa..c2c813bf3 100644 --- a/scripts/compare-pr-version.ts +++ b/scripts/compare-pr-version.ts @@ -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); } diff --git a/scripts/typecheck-test-baseline.json b/scripts/typecheck-test-baseline.json index 5a97c3cc2..215ee7eb1 100644 --- a/scripts/typecheck-test-baseline.json +++ b/scripts/typecheck-test-baseline.json @@ -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'. 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' 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' 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'. 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? \"' 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'. 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? \"' 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 '{ (options: SpawnOptions & { cmd: string[]; }): Subprocess<...>; '. 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'.": 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' 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' is not assignable to parameter of type '(args_0: unknown, ...args: unknown[]) => void | Promise'. 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, diff --git a/test/ceo-finding-fixture.test.ts b/test/ceo-finding-fixture.test.ts index db0da204b..3c7d1b700 100644 --- a/test/ceo-finding-fixture.test.ts +++ b/test/ceo-finding-fixture.test.ts @@ -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], diff --git a/test/compare-pr-version.test.ts b/test/compare-pr-version.test.ts new file mode 100644 index 000000000..e4a4d3890 --- /dev/null +++ b/test/compare-pr-version.test.ts @@ -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' }); + }); +}); diff --git a/test/osv-config-wiring.test.ts b/test/osv-config-wiring.test.ts index 69cab8835..442924b45 100644 --- a/test/osv-config-wiring.test.ts +++ b/test/osv-config-wiring.test.ts @@ -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@/); + }); }); diff --git a/test/osv-ignore-expiry.test.ts b/test/osv-ignore-expiry.test.ts index c185d3ec3..d88b9fdb5 100644 --- a/test/osv-ignore-expiry.test.ts +++ b/test/osv-ignore-expiry.test.ts @@ -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\./); }); }); diff --git a/test/workflow-action-pins.test.ts b/test/workflow-action-pins.test.ts index a86ab0d3a..6293412ef 100644 --- a/test/workflow-action-pins.test.ts +++ b/test/workflow-action-pins.test.ts @@ -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'] }); + } + }); }); diff --git a/tsconfig.test.json b/tsconfig.test.json index 6276a3d73..da9480913 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -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/**",