From ad6f87a88847a4a8a72204826ef52f0937ce0a72 Mon Sep 17 00:00:00 2001 From: loong10k <20489781+loong10k@users.noreply.github.com> Date: Wed, 30 Sep 2026 02:13:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(npm):=20=E6=9C=AC=E5=9C=B0=E5=8C=85?= =?UTF-8?q?=E6=89=93=E5=8C=85=E7=BA=B3=E5=85=A5=2032=20=E4=BB=BD=E8=AF=AD?= =?UTF-8?q?=E6=B3=95=20WASM=20=E5=B9=B6=E9=99=84=E9=AA=8C=E6=94=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scripts/pack-npm-local.mjs:新增 --public / --require-wasm 选项,打包时 复制并赋权 WASM 候选产物、记录来源散列,缺失时报错 - tests/npm_pack_wasm.test.mjs:node:test 验收本地包内 WASM 的存在性、 散列与可执行性 - tests/acceptance/npm-wasm-local-package.md:验收记录 - CI、双语 README、架构/技术设计文档与 openspec binary-distribution/tasks 同步该面 --- .github/workflows/ci.yml | 10 +++ README.md | 2 + README.zh-CN.md | 2 + docs/Codeguard-Architecture.md | 9 ++- docs/Codeguard-Architecture.zh_CN.md | 9 ++- docs/Codeguard-Technical-Design.md | 2 + docs/Codeguard-Technical-Design.zh_CN.md | 2 + npm/README.md | 4 ++ .../specs/binary-distribution/spec.md | 4 ++ .../introduce-rust-codeguard-cli/tasks.md | 2 + scripts/pack-npm-local.mjs | 63 ++++++++++++++++-- tests/acceptance/npm-wasm-local-package.md | 22 +++++++ tests/npm_pack_wasm.test.mjs | 65 +++++++++++++++++++ 13 files changed, 186 insertions(+), 10 deletions(-) create mode 100644 tests/acceptance/npm-wasm-local-package.md create mode 100644 tests/npm_pack_wasm.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b1bf99b6..821087e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,12 +29,22 @@ jobs: - name: Build workspace run: cargo build --workspace --all-targets + - name: Preserve the binary without WASM for package rejection + run: cp target/debug/codeguard "$RUNNER_TEMP/codeguard-no-wasm" + - name: Verify Linux WASM worker resource boundary run: | cargo test -p codeguard-runtime --features wasm-precheck --test process_contract cargo test -p codeguard-runtime --features wasm-precheck --test wasm_grammar_load --test dart_upstream_corpus cargo test -p codeguard-cli --features wasm-precheck --test syntax_worker_candidate --test grammar_probe_cli --test grammar_route --test check_all_grammar_candidates --test zig_lint_cli --test typescript_syntax_fallback_candidate --test java_syntax_fallback_candidate + - name: Verify the local npm package contains a running WASM worker + run: | + cargo build --locked -p codeguard-cli --features wasm-precheck + CODEGUARD_NO_WASM_BIN="$RUNNER_TEMP/codeguard-no-wasm" \ + CODEGUARD_WASM_BIN="$PWD/target/debug/codeguard" \ + node --test tests/npm_pack_wasm.test.mjs + - name: Run test suite run: cargo test --workspace --all-targets diff --git a/README.md b/README.md index 61bcba32..678b8ad0 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,8 @@ npm exec --yes --package "$CODEGUARD_TARBALL" -- codeguard detect . --format jso The packer currently supports macOS and Linux on x64/arm64 when a matching native binary is available. It requires Node 18+, npm, and an already built Rust binary for the current host. It checks the binary's reported target and version before packaging, and writes a platform-tagged tarball under ignored `release/npm/`. The default package is private and local-only. `node scripts/pack-npm-local.mjs --public` prepares the public `@partme.ai/codeguard` package with a README, full license, and host restrictions. Version `0.1.2` is published; its registry-hosted `npx` entry and artifact hashes were verified on Apple Silicon macOS. The binary reports the candidate source commit, which is not a signed or reproducible-build attestation. Other platforms need their own verified binary packages. See the [distribution design](docs/Codeguard-Technical-Design.md) and [0.1.2 acceptance record](tests/acceptance/npm-0.1.2-candidate.md). +To prepare a **local** package with candidate WASM support, build with `cargo build --locked -p codeguard-cli --features wasm-precheck` and run `node scripts/pack-npm-local.mjs --require-wasm target/debug/codeguard`. The packer rejects a binary without the worker, checks all 32 pinned asset identities, and executes a bounded Zig candidate probe before writing the tarball. `--public` now requires the same check; this does not change the already published `0.1.2`, which contains no WASM. Local offline npm execution of Zig and Dart remains candidate-only and does not qualify a language or certify a release. + ## 5. Repair workflow Make the built binary available as `codeguard` on `PATH`, or use its absolute path. In a target project: diff --git a/README.zh-CN.md b/README.zh-CN.md index 8d552499..4277a98a 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -131,6 +131,8 @@ npm exec --yes --package "$CODEGUARD_TARBALL" -- codeguard detect . --format jso 打包器在已有匹配原生程序时支持 macOS 与 Linux 的 x64/arm64 平台。打包需要 Node 18+、npm 和预先构建的本机 Rust 二进制;打包前核对二进制报告的平台和版本,在已忽略的 `release/npm/` 写入带平台标识的 tarball。默认包标记为 private,仅用于本地分发。`node scripts/pack-npm-local.mjs --public` 准备带说明、完整许可证与平台限制的公开 `@partme.ai/codeguard` 包。`0.1.2` 已发布;其公开包在 Apple Silicon macOS 上通过全新缓存 `npx` 执行和制品摘要核对。二进制回报的候选源码提交不等于签名或可复现构建证明。其它平台仍需各自验收的二进制包。见[发行技术方案](docs/Codeguard-Technical-Design.zh_CN.md)及[0.1.2 验收记录](tests/acceptance/npm-0.1.2-candidate.md)。 +若要准备**本地**含 WASM 候选的包,先执行 `cargo build --locked -p codeguard-cli --features wasm-precheck`,再运行 `node scripts/pack-npm-local.mjs --require-wasm target/debug/codeguard`。打包器会拒绝缺少 worker 的二进制,核对 32 份固定资产身份,并在写包前实际运行有界 Zig 候选探针。`--public` 现在也要求同样检查;这不改变已发布且不含 WASM 的 `0.1.2`。本地离线 npm 包能运行 Zig、Dart 候选,并不等于语种精度或公开发行验收通过。 + ## 5. 修复流程 将构建出的二进制加入 `PATH`,使 `codeguard` 可用,或使用其绝对路径。在受检项目中执行: diff --git a/docs/Codeguard-Architecture.md b/docs/Codeguard-Architecture.md index 6becb12e..471b5ab1 100644 --- a/docs/Codeguard-Architecture.md +++ b/docs/Codeguard-Architecture.md @@ -478,8 +478,11 @@ Current deployment is a locally built binary plus independently prepared native ```mermaid flowchart LR - Build[Explicit Rust release build] --> Verify[Check binary version and host target] - Verify --> Pack[Local platform-tagged npm tarball] + Build[Explicit Rust build] --> Verify[Check binary version and host target] + Verify --> Wasm{WASM package requested?} + Wasm -->|yes| Probe[Match 32 pinned identities and run Zig worker] + Wasm -->|no, local private only| Pack[Local platform-tagged npm tarball] + Probe --> Pack Pack --> Node[Node bin: codeguard] Node --> Rust[Bundled Rust executable] Rust --> Native[Selected native analyzers] @@ -487,6 +490,8 @@ flowchart LR The default local package is marked private and contains no npm install hook. Its `bin` entry is [npm/codeguard.cjs](../npm/codeguard.cjs); [scripts/pack-npm-local.mjs](../scripts/pack-npm-local.mjs) builds it from an existing binary. The `--public` packaging mode produced the published `@partme.ai/codeguard@0.1.2`, restricted to Apple Silicon macOS. Fresh-cache registry execution and matching package/binary hashes are recorded in [npm 0.1.2 acceptance evidence](../tests/acceptance/npm-0.1.2-candidate.md). Broad registry distribution still needs approved platform coverage, a trusted artifact manifest, matching versions and hashes, and a release process. CodeGraph's Node entry and platform package layout informed this design; its optional network fallback is not part of Codeguard's current installation path. +For a new candidate-WASM package, `--require-wasm` and `--public` both reject a binary lacking the worker. The packer compares its inventory against the 32 pinned manifest identities and executes a Zig worker probe before writing a tarball; [local offline npm acceptance](../tests/acceptance/npm-wasm-local-package.md) additionally runs Zig and Dart from the package. This does not retroactively add WASM to published `0.1.2`, nor does it qualify all languages for lint or certify a public release. + For upgrades: record binary/schema versions, preserve workbench data, preview managed changes, rerun a bounded check, and validate report consumption before adopting the new binary. Downgrade must respect supported schemas; never rewrite historical files into an older shape merely to make parsing pass. Target plugin integration should map host invocation to the same CLI semantics, return result/next action to the conversation, and keep transport failure distinct from quality status. Host-specific fail-open policy must not erase unresolved findings. No MCP server or host binding is provided by this baseline's command dispatcher. diff --git a/docs/Codeguard-Architecture.zh_CN.md b/docs/Codeguard-Architecture.zh_CN.md index 94f409ec..9c8a489d 100644 --- a/docs/Codeguard-Architecture.zh_CN.md +++ b/docs/Codeguard-Architecture.zh_CN.md @@ -478,8 +478,11 @@ CLI 返回命令专属的版本化 JSON、可读投影及部分 SARIF。[Schema ```mermaid flowchart LR - Build[显式构建 Rust release] --> Verify[核对二进制版本和本机平台] - Verify --> Pack[本地带平台标识的 npm 包] + Build[显式构建 Rust] --> Verify[核对二进制版本和本机平台] + Verify --> Wasm{要求包含 WASM?} + Wasm -->|是| Probe[核对 32 份固定身份并运行 Zig worker] + Wasm -->|否,仅本地私有包| Pack[本地带平台标识的 npm 包] + Probe --> Pack Pack --> Node[Node bin: codeguard] Node --> Rust[包内 Rust 可执行文件] Rust --> Native[选定的原生检查器] @@ -487,6 +490,8 @@ flowchart LR 默认本地包标记为 private,不含 npm 安装钩子。`bin` 入口见 [npm/codeguard.cjs](../npm/codeguard.cjs),[scripts/pack-npm-local.mjs](../scripts/pack-npm-local.mjs)使用已构建二进制打包。`--public` 模式已产出公开的 `@partme.ai/codeguard@0.1.2`,仅覆盖 Apple Silicon macOS;全新缓存运行和包/二进制摘要核对见[npm 0.1.2 验收记录](../tests/acceptance/npm-0.1.2-candidate.md)。扩大分发范围还需平台覆盖、可信制品清单、版本与摘要绑定及发行流程。CodeGraph 的 Node 入口和平台包布局为此设计提供参考;其可选网络回退不属于 Codeguard 当前安装路径。 +新的 WASM 候选包使用 `--require-wasm` 或 `--public` 时,打包器会拒绝没有 worker 的二进制,核对固定清单的 32 份资产身份,并在写包前执行 Zig worker;[本地离线 npm 验收](../tests/acceptance/npm-wasm-local-package.md)另从包内运行 Zig、Dart。该检查不会让已发布 `0.1.2` 自动获得 WASM,也不证明全部语言的 lint 精度或公开发行已验收。 + 升级时记录二进制/schema 版本,保留工作区,预览受管变更,重跑有限检查,再核对报告消费。降级必须服从已有 schema 读取范围,不能为了通过解析而改写历史记录成旧格式。 目标插件应将宿主调用映射到同一 CLI 语义,将结果与下一步带回对话,同时区分传输失败与质量状态。宿主 fail-open 策略不能抹掉未解决发现。本基线调度器未提供 MCP 服务或宿主绑定。 diff --git a/docs/Codeguard-Technical-Design.md b/docs/Codeguard-Technical-Design.md index 665980e2..00dc6ee8 100644 --- a/docs/Codeguard-Technical-Design.md +++ b/docs/Codeguard-Technical-Design.md @@ -578,6 +578,8 @@ The Node path supports the published macOS arm64 package and local packaging. Fo The `--public` packer mode produced `@partme.ai/codeguard@0.1.2` for `darwin-arm64`; its npm `os`/`cpu` constraints reject other hosts. Registry publication, a fresh-cache registry-hosted `npx --yes @partme.ai/codeguard@0.1.2 --version --format json` run, and registry/local tarball and binary SHA-256 comparisons passed on Apple Silicon macOS. Its clean-checkout build identity reports the candidate source commit but does not prove a reproducible build, signed provenance, or S13 release completion; see [acceptance evidence](../tests/acceptance/npm-0.1.2-candidate.md). For broader distribution, adapt CodeGraph's thin entry/platform bundle pattern only after S13 release evidence is ready: publish a version-matched package for each certified target, then a public command package with exact optional platform dependencies. Missing platform support must fail clearly; network recovery must follow Codeguard's signed selection and explicit host permission contract before any download. +For a later WASM candidate package, `--require-wasm` verifies the binary's 32 reported identities against the pinned manifest, rejects a default build without the worker, and executes a real Zig grammar probe before packing. `--public` implies this guard and retains its clean-checkout/build-identity checks. A local private tarball then runs offline through `npm exec`; Zig and Dart probes demonstrate the launcher reaches the bundled Rust worker. This is [local packaging evidence](../tests/acceptance/npm-wasm-local-package.md), not a registry release or per-language syntax qualification. The published `0.1.2` package predates the guard and has no WASM fallback; a future public release needs a new version, platform and license verification, and the remaining S14 acceptance. + ## 12. Test and evaluation plan | Layer | Proves | Does not prove | diff --git a/docs/Codeguard-Technical-Design.zh_CN.md b/docs/Codeguard-Technical-Design.zh_CN.md index 0fff231d..02fec1ee 100644 --- a/docs/Codeguard-Technical-Design.zh_CN.md +++ b/docs/Codeguard-Technical-Design.zh_CN.md @@ -578,6 +578,8 @@ Node 路径支持已发布的 macOS arm64 包和本地打包。本地打包流 `--public` 打包模式已产出仅适用于 `darwin-arm64` 的 `@partme.ai/codeguard@0.1.2`;npm `os`/`cpu` 限制会拒绝其它宿主。npm 仓库发布、Apple Silicon macOS 上全新缓存运行 `npx --yes @partme.ai/codeguard@0.1.2 --version --format json`、注册表/本地 tarball 及二进制 SHA-256 对比均通过。干净 checkout 构建身份回报候选源码提交,但不证明可复现构建或签名来源,也不代表 S13 发行证据已完成,见[验收记录](../tests/acceptance/npm-0.1.2-candidate.md)。扩大分发范围可参考 CodeGraph 的精简入口加平台包模式,但须先满足 S13 发行证据:为每个通过验收的平台发布版本一致的包,再提供含精确可选平台依赖的公开命令包。缺对应平台包时应给出清晰错误;如需网络补包,下载前必须遵循 Codeguard 的签名选择与独立宿主授权。 +后续 WASM 候选包使用 `--require-wasm`,打包前将二进制报告的 32 份身份逐一与固定清单核对,拒绝无 worker 的默认构建,并实际执行 Zig grammar 探针。`--public` 自动启用该门禁,同时保留干净源码和构建身份核对。本地 private tarball 再经离线 `npm exec` 运行;Zig、Dart 探针证明 Node 入口能够调用内置 Rust worker。见[本地打包验收](../tests/acceptance/npm-wasm-local-package.md)。这不是注册表发行或逐语言语法验收。已发布的 `0.1.2` 早于该门禁,不含 WASM 兜底;未来公开版本须升级版本号,补齐平台、许可证和 S14 剩余验收。 + ## 12. 测试与评估方案 | 层次 | 能证明 | 不能证明 | diff --git a/npm/README.md b/npm/README.md index e289b9b1..6c285564 100644 --- a/npm/README.md +++ b/npm/README.md @@ -10,8 +10,12 @@ npx @partme.ai/codeguard init . --apply This initial npm package contains an **Apple Silicon macOS** executable only (`darwin-arm64`). npm rejects installation on other platforms. The CLI is an early implementation: individual checkers and end-to-end quality gates are incomplete. A successful command or an empty task list does not certify a project as clean. +The published `0.1.2` package does not include WASM syntax prechecks. Future packages that advertise the 32 pinned grammar candidates must pass a WASM worker and asset probe before packing. Candidate observations remain incomplete and do not replace native lint or certify a clean project. + The project workspace created by `init` is `.codeguard/`; existing user source under `codeguard/` stays in scope. See the [repository README](https://github.com/full-stack-plugins/codeguard#readme) for supported commands, current coverage, and limitations. ## 中文 此 npm 包只提供精简 Node 入口和 Rust 原生程序,不在安装时构建、下载或运行检测工具。首个 npm 版本仅支持 Apple Silicon macOS。`init` 创建的项目数据目录是 `.codeguard/`;部分检测器和完整质量门禁仍在开发中。具体能力和限制见[项目说明](https://github.com/full-stack-plugins/codeguard/blob/main/README.zh-CN.md)。 + +已发布的 `0.1.2` 不含 WASM 语法初检。未来声称包含 32 份固定 grammar 候选的包,须在打包前验证 worker 与资产;候选观察始终不代替原生 lint,也不证明项目质量通过。 diff --git a/openspec/changes/introduce-rust-codeguard-cli/specs/binary-distribution/spec.md b/openspec/changes/introduce-rust-codeguard-cli/specs/binary-distribution/spec.md index c28d68ef..d145b5a8 100644 --- a/openspec/changes/introduce-rust-codeguard-cli/specs/binary-distribution/spec.md +++ b/openspec/changes/introduce-rust-codeguard-cli/specs/binary-distribution/spec.md @@ -216,3 +216,7 @@ The npm package SHALL provide `npx @partme.ai/codeguard` through a minimal Node #### Scenario: Optional WASM assets join a later release - **WHEN** a release declares bundled syntax capability - **THEN** its package includes the pinned assets and notices required by syntax-precheck, and independent offline invocation is validated for each advertised host + +#### Scenario: A package declares all pinned grammar candidates +- **WHEN** the packer is asked to prepare an artifact containing the 32 candidate grammars +- **THEN** it rejects a binary without the WASM worker or with a missing, duplicated or digest-mismatched candidate before creating the artifact; an accepted local artifact runs at least one pinned grammar offline through the installed Node launcher and reports an incomplete candidate rather than a qualified lint result diff --git a/openspec/changes/introduce-rust-codeguard-cli/tasks.md b/openspec/changes/introduce-rust-codeguard-cli/tasks.md index 48dd676e..ab01fb70 100644 --- a/openspec/changes/introduce-rust-codeguard-cli/tasks.md +++ b/openspec/changes/introduce-rust-codeguard-cli/tasks.md @@ -1073,3 +1073,5 @@ TDD缺自动同步先RED。相关普通:build9、work sync15、next6,共30 14.6 / 14.7 / 14.19 全项目候选路由增量:源码可选构建中的 `check all` 现先执行原生节点,再按方言把已发现源码送入固定隔离 worker;TSX、JavaScript、CFScript 分路由,CFQuery 只从完整标签体提取,歧义 `.h` 和普通 `.sql` 不猜测。四个有界项目的真实 `check all` 样例合计调用全部 32 份资产,输出 0.32.0 封闭候选字段并保持原生阻塞、退出码 3、交付未完成。单项目 31 文件的早期样例在 macOS 45 秒预算内仅 26/32、90 秒约 54 秒可完成;Linux CI 在 90 秒内仍为 26/32,因此跨平台验收改为分批项目,预算超限范围明确保持未完成。64 文件/64 片段上限不放宽。逐语种原生选择/版本语料、误报漏报对照、任务宿主反馈、发行验收仍缺;14.6/14.7/14.17/14.19 不勾选。见[32 份路由局部验收](../../../tests/acceptance/check-all-32-grammar-candidates.md)。 14.6 / 14.7 逐文件原生优先局部增量:源码可选构建中的 `check all` 现只在 Ruff 本轮完整检查同一路径、工具/配置摘要存在且源码 SHA-256 与候选阶段重新读取的字节一致时,对该 Python 文件跳过重复 WASM;`findings` 原生诊断仍原样保留,混合项目的 Zig 文件继续跑固定 worker,整体仍为退出码 3 和 `incomplete`。封闭协议升至 0.33.0,0.32.0 单独归档,新增 `native_preferred_count`;未完成扫描、路径错配或源码变化不得跳过。见[Python 原生优先局部验收](../../../tests/acceptance/check-all-native-preferred-python.md)。尚未覆盖其它 31 种 grammar 的逐模块原生能力判定、独立语法 oracle、任务/宿主反馈及发行验收;14.6/14.7/14.19 保持未完成。 + +14.18 / 14.19 本地 npm 打包增量:打包器现可用 `--require-wasm` 核对二进制内 32 份候选身份和固定 SHA-256,实跑 Zig worker;`--public` 自动启用该门禁。无 WASM 特性的默认二进制在出包前被拒。本机 private tarball 经离线 `npm exec` 读到 32 份清单并实际运行 Zig、Dart 候选 worker,仍返回退出码 3、未验收且交付未评估;见[本地 npm 包局部验收](../../../tests/acceptance/npm-wasm-local-package.md)。Linux CI 的同一测试待本次提交运行;已发布 `0.1.2` 不含此能力,尚无新版本公开发行、所有 32 份包内逐一正反例、逐语言原生对照或宿主反馈。14.18、14.19 不勾选。 diff --git a/scripts/pack-npm-local.mjs b/scripts/pack-npm-local.mjs index 778c5e3a..1749be57 100644 --- a/scripts/pack-npm-local.mjs +++ b/scripts/pack-npm-local.mjs @@ -3,7 +3,8 @@ // Build a local, single-platform npm package from an already-built Rust binary. // This script never downloads a binary or runs project checks. import { execFileSync, spawnSync } from 'node:child_process'; -import { copyFileSync, chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'; +import { createHash } from 'node:crypto'; +import { copyFileSync, chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -21,14 +22,23 @@ if (!expectedTarget) { throw new Error(`No verified Codeguard package target for ${host}`); } -const publishable = process.argv[2] === '--public'; +const options = new Set(); +let binaryArgument; +for (const argument of process.argv.slice(2)) { + if (argument === '--public' || argument === '--require-wasm') { + if (options.has(argument)) throw new Error(`Duplicate option: ${argument}`); + options.add(argument); + } else if (argument.startsWith('--') || binaryArgument) { + throw new Error('Usage: pack-npm-local.mjs [--public] [--require-wasm] [binary-path]'); + } else { + binaryArgument = argument; + } +} +const publishable = options.has('--public'); +const requireWasm = publishable || options.has('--require-wasm'); if (publishable && host !== 'darwin-arm64') { throw new Error(`The first @partme.ai/codeguard release is certified only for darwin-arm64, not ${host}`); } -const binaryArgument = publishable ? process.argv[3] : process.argv[2]; -if (process.argv.length > (publishable ? 4 : 3)) { - throw new Error('Usage: pack-npm-local.mjs [--public] [binary-path]'); -} const binary = binaryArgument ? path.resolve(binaryArgument) : path.join(root, 'target', 'release', process.platform === 'win32' ? 'codeguard.exe' : 'codeguard'); @@ -61,6 +71,47 @@ if (publishable) { } } +if (requireWasm) { + const manifest = JSON.parse(readFileSync(path.join(root, 'grammars', 'manifest.json'), 'utf8')); + const expected = new Map(manifest.assets.map(asset => [asset.language, asset.sha256])); + if (expected.size !== 32 || manifest.assets.length !== 32) { + throw new Error('WASM capability probe failed: local manifest must contain 32 distinct assets'); + } + const inventory = spawnSync(binary, ['grammar', 'status', '--format=json'], { + cwd: root, encoding: 'utf8', timeout: 30_000, maxBuffer: 1024 * 1024, shell: false, + }); + let report; + try { report = JSON.parse(inventory.stdout); } catch { report = null; } + if (inventory.error || inventory.status !== 3 || report?.candidate_count !== 32 || + !Array.isArray(report.assets) || report.assets.length !== 32 || + new Set(report.assets.map(asset => asset.language)).size !== 32 || + report.released_count !== 0 || report.delivery_decision !== 'not_evaluated' || + report.assets.some(asset => !expected.has(asset.language) || expected.get(asset.language) !== asset.candidate_wasm_sha256)) { + throw new Error('WASM capability probe failed: 32 pinned asset identities are not present'); + } + const probeDir = realpathSync(mkdtempSync(path.join(os.tmpdir(), 'codeguard-wasm-pack-probe-'))); + try { + const source = path.join(probeDir, 'sample.zig'); + const sourceText = 'const Empty = struct {};\n'; + writeFileSync(source, sourceText); + const observed = spawnSync(binary, ['grammar', 'probe', 'zig', source, '--format=json'], { + cwd: root, encoding: 'utf8', timeout: 120_000, maxBuffer: 1024 * 1024, shell: false, + }); + let candidate; + try { candidate = JSON.parse(observed.stdout); } catch { candidate = null; } + if (observed.error || observed.status !== 3 || candidate?.report_type !== 'grammar_candidate_probe' || + candidate.language !== 'zig' || candidate.grammar_sha256 !== expected.get('zig') || + candidate.source_sha256 !== createHash('sha256').update(sourceText).digest('hex') || + candidate.grammar_qualified !== false || candidate.precheck?.status !== 'incomplete' || + candidate.precheck?.checked_files !== 1 || !Array.isArray(candidate.recoveries) || + candidate.native?.status !== 'not_run' || candidate.delivery_decision !== 'not_evaluated') { + throw new Error('WASM capability probe failed: pinned Zig worker did not produce an incomplete candidate'); + } + } finally { + rmSync(probeDir, { recursive: true, force: true }); + } +} + const stage = mkdtempSync(path.join(os.tmpdir(), 'codeguard-npm-')); const outDir = path.join(root, 'release', 'npm'); try { diff --git a/tests/acceptance/npm-wasm-local-package.md b/tests/acceptance/npm-wasm-local-package.md new file mode 100644 index 00000000..be5397fa --- /dev/null +++ b/tests/acceptance/npm-wasm-local-package.md @@ -0,0 +1,22 @@ +# 32 份 WASM 候选的本地 npm 包局部验收 + +日期:2026-09-30。对应既有 OpenSpec S14.18、S14.19 的打包增量;两项任务仍未完成。 + +## 行为与命令 + +`scripts/pack-npm-local.mjs --require-wasm` 在创建 tarball 前核对二进制的 32 份候选身份、固定清单的语言与 SHA-256,并让 Zig worker 解析真实源码。`--public` 自动执行相同门禁,同时保留已有的干净 checkout 和构建提交身份核对。默认无 `wasm-precheck` 特性的二进制被拒,不能被打成声称带有 WASM 的包。Node 入口仍只转发参数给 Rust。 + +本机 Apple Silicon macOS 使用以下指令验证: + +```bash +cargo build --locked --offline -p codeguard-cli --features wasm-precheck +CODEGUARD_NO_WASM_BIN=/tmp/codeguard-default-for-wasm-pack-af20dd8 \ +CODEGUARD_WASM_BIN="$PWD/target/debug/codeguard" \ +node --test tests/npm_pack_wasm.test.mjs +``` + +测试先要求一个单独保存的默认构建作为拒绝反例;上述 `/tmp` 路径只是本次本机临时证据,不是仓库或 CI 的固定路径。CI 先构建默认二进制并复制到 `$RUNNER_TEMP`,再构建 WASM 特性二进制,给同一测试传入两条实际路径。 + +本机结果:2/2 项通过。负例确认无 WASM worker 的二进制在打包前被拒。正例产出 private 本地 tarball,用独立缓存执行 `npm exec --offline --package -- codeguard grammar status --format=json`,得到 32 份候选、0 份已验收发行;随后同一离线安装的 Rust 程序分别解析 Zig 和 Dart 源码,返回退出码 3、固定 grammar 摘要、`grammar_qualified=false`、`native.status=not_run`、`delivery_decision=not_evaluated`。测试中的临时源码采用物理路径,避免 macOS `/var` 别名路径触发源码路径安全检查。首次按别名路径测试失败后已修正,并再次通过。 + +这是**本地单平台包可运行性**,不是 32 种语言全部通过 npm 包逐一解析、独立语法 oracle、误报/漏报、原生优先完整路由、Linux 包验收或公开 npm 发布。已发布的 `@partme.ai/codeguard@0.1.2` 不含本次 WASM 特性;同一版本不能再次发布为新能力。待 CI 实际结果和新版本发行证据到位后,再决定是否更新对应发布任务。S14.18、S14.19 不勾选。 diff --git a/tests/npm_pack_wasm.test.mjs b/tests/npm_pack_wasm.test.mjs new file mode 100644 index 00000000..114b9cb7 --- /dev/null +++ b/tests/npm_pack_wasm.test.mjs @@ -0,0 +1,65 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const packer = path.join(root, 'scripts', 'pack-npm-local.mjs'); +const wasmBinary = process.env.CODEGUARD_WASM_BIN; +const defaultBinary = process.env.CODEGUARD_NO_WASM_BIN; + +test('打包器拒绝未包含 WASM 的二进制', () => { + assert.ok(defaultBinary, 'CODEGUARD_NO_WASM_BIN is required'); + const result = spawnSync(process.execPath, [packer, '--require-wasm', defaultBinary], { + cwd: root, encoding: 'utf8', timeout: 30_000, + }); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /WASM capability probe failed/); +}); + +test('本地离线 npm 包实际运行固定 Zig 与 Dart WASM', () => { + assert.ok(wasmBinary, 'CODEGUARD_WASM_BIN is required'); + const packed = spawnSync(process.execPath, [packer, '--require-wasm', wasmBinary], { + cwd: root, encoding: 'utf8', timeout: 120_000, + }); + assert.equal(packed.status, 0, packed.stderr); + const tarball = packed.stdout.trim(); + assert.ok(tarball.endsWith('.tgz')); + const scratch = realpathSync(mkdtempSync(path.join(os.tmpdir(), 'codeguard-npm-wasm-'))); + try { + const inventory = spawnSync('npm', [ + 'exec', '--offline', '--yes', '--cache', path.join(scratch, 'cache'), + '--package', tarball, '--', 'codeguard', 'grammar', 'status', '--format=json', + ], { cwd: root, encoding: 'utf8', timeout: 120_000 }); + assert.equal(inventory.status, 3, inventory.stderr); + const assets = JSON.parse(inventory.stdout); + assert.equal(assets.candidate_count, 32); + assert.equal(assets.released_count, 0); + for (const [language, filename, source] of [ + ['zig', 'sample.zig', 'const Empty = struct {};\n'], + ['dart', 'sample.dart', "String greet(String name) => 'Hello $name';\n"], + ]) { + const file = path.join(scratch, filename); + writeFileSync(file, source); + const probe = spawnSync('npm', [ + 'exec', '--offline', '--yes', '--cache', path.join(scratch, 'cache'), + '--package', tarball, '--', 'codeguard', 'grammar', 'probe', language, file, + '--format=json', + ], { cwd: root, encoding: 'utf8', timeout: 120_000 }); + assert.equal(probe.status, 3, probe.stderr); + const report = JSON.parse(probe.stdout); + assert.equal(report.report_type, 'grammar_candidate_probe'); + assert.equal(report.language, language); + assert.equal(report.grammar_qualified, false); + assert.equal(report.native.status, 'not_run'); + assert.equal(report.delivery_decision, 'not_evaluated'); + const manifest = JSON.parse(readFileSync(path.join(root, 'grammars', 'manifest.json'))); + assert.equal(report.grammar_sha256, manifest.assets.find(asset => asset.language === language).sha256); + } + } finally { + rmSync(scratch, { recursive: true, force: true }); + } +});