Skip to content

Commit 928b718

Browse files
committed
验收离线安装修复链路并约束 CI 测试制品预算
1 parent a688292 commit 928b718

9 files changed

Lines changed: 208 additions & 0 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ jobs:
1212
gate:
1313
# ubuntu-latest 预装稳定版 Rust;Cargo.toml 声明 rust-version = 1.85 / edition = 2024。
1414
runs-on: ubuntu-latest
15+
env:
16+
# Linux 完整 DWARF 测试制品会超出 SDK 的 256 MiB 宿主制品读取预算。
17+
# 仅去掉测试调试符号;保留 debug assertions 和产品读取上限。
18+
CARGO_PROFILE_TEST_DEBUG: "0"
1519
steps:
1620
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
1721

@@ -46,6 +50,9 @@ jobs:
4650
CODEGUARD_NO_WASM_BIN="$RUNNER_TEMP/codeguard-no-wasm" \
4751
CODEGUARD_WASM_BIN="$PWD/target/debug/codeguard" \
4852
node --test tests/npm_pack_wasm.test.mjs
53+
# 独占打包输出与 npm 缓存,不与上面的全量 grammar 包测试并发。
54+
CODEGUARD_WASM_BIN="$PWD/target/debug/codeguard" \
55+
node --test tests/npm_pack_repair.test.mjs
4956
5057
- name: Run test suite
5158
run: cargo test --workspace --all-targets

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,3 +395,5 @@ Briefs also expose the latest native report reference/digest and current diagnos
395395
`verify_zig_task_resolution` lets a protected host verify a task-specific signed policy, compare the original counterexample and current bytes with the same Zig 0.16.0, and append resolution, investigation or recurrence events. Ordinary `task verify` can record recurrence with the matching tool and reopen the same task; existing leases and attempt receipts remain in use.
396396

397397
The default plugin and public CLI still lack the trusted policy provider, and npm 0.1.3 does not include this API. Editing task state or reading a local closure file cannot close an issue or approve delivery. See the [technical design](docs/Codeguard-Technical-Design.md) for the API, execution graph and full report, and [lifecycle acceptance](tests/acceptance/task-resolution-lifecycle.md) for its current scope.
398+
399+
The current source also passes an offline npm package edit/task-recheck flow through the installed Node launcher; [acceptance evidence](tests/acceptance/npm-repair-local-package.md) distinguishes this from the published 0.1.3 package and actual host automation.

‎README.zh-CN.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,3 +394,5 @@ Cargo 声明 `Apache-2.0`;当前工作树已有仓库级 `LICENSE` 与 `NOTICE
394394
源码现提供 `verify_zig_task_resolution`:受保护宿主验签限定任务策略,使用同一 Zig 0.16.0 对原始反例与当前源码做原生对照,再追加关闭、调查或复发事件。普通 `task verify` 能记录匹配原工具的复发并重开同一任务;失败尝试沿用现有租约与验证收据。
395395

396396
它尚未接入默认插件或公开 CLI 的可信策略来源,也未发布到 npm 0.1.3。手工修改任务或读取本地关闭文件仍不能关闭问题或放行交付。API、执行图与完整报告示例见[技术方案](docs/Codeguard-Technical-Design.zh_CN.md),当前验收范围见[任务生命周期记录](tests/acceptance/task-resolution-lifecycle.md)。
397+
398+
当前源码另已通过离线 npm 包的编辑与任务复检链路,实际经安装后的 Node 入口运行;[验收记录](tests/acceptance/npm-repair-local-package.md)区分本地包、公开 0.1.3 和真实宿主自动触发。

‎crates/codeguard-cli/tests/task_resolution_service.rs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ impl Drop for Project {
3535
}
3636
impl Project {
3737
fn new() -> Self {
38+
let host_size = fs::metadata(std::env::current_exe().unwrap())
39+
.unwrap()
40+
.len();
41+
assert!(
42+
host_size <= 256 * 1024 * 1024,
43+
"SDK test host exceeds the product artifact budget: {host_size} bytes; use CARGO_PROFILE_TEST_DEBUG=0 without widening the product limit"
44+
);
3845
let root = std::env::temp_dir().canonicalize().unwrap().join(format!(
3946
"cg-resolution-{}-{}",
4047
std::process::id(),

‎openspec/changes/introduce-rust-codeguard-cli/specs/binary-distribution/spec.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,7 @@ The npm package SHALL provide `npx @partme.ai/codeguard` through a minimal Node
220220
#### Scenario: A package declares all pinned grammar candidates
221221
- **WHEN** the packer is asked to prepare an artifact containing the 32 candidate grammars
222222
- **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
223+
224+
#### Scenario: An installed npm candidate carries repair feedback across commands
225+
- **WHEN** a local WASM candidate package is invoked offline through npm on an initialized workspace, an edit produces a syntax confirmation task, and the same task is rechecked through its explicit native tool
226+
- **THEN** the installed launcher preserves host stdin, stable task identity, evidence and exit status across hook/next/task verify; repeated edits update the same task, native diagnostics guide repair, changed inputs invalidate old positions, and zero native diagnostics without a protected policy retain an open task and unevaluated delivery

‎openspec/changes/introduce-rust-codeguard-cli/tasks.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1145,3 +1145,7 @@ TDD缺自动同步先RED。相关普通:build9、work sync15、next6,共30
11451145
处理器复用现有原生局部报告、验证事件、ready-to-verify 关联及租约。普通 `task verify` 再次检出匹配原工具的当前诊断可追加 `reopened`,宿主确认或重复复检不制造重复事件;首次 finding 不改写。next/task show/status 本地读者只给当前原生修复或历史待核验步骤,不把文件中的关闭声明升级为可信状态或交付 allow。独立策略/记录/对照/收据 schema 与中英文设计、README 已补充,见 [限定任务验收](../../../tests/acceptance/task-resolution-lifecycle.md)。
11461146

11471147
当前范围仅 Zig 语法确认任务及宿主 SDK;默认插件与公开 CLI 的可信策略来源、其它原生检查器、环境/依赖/目标/政策处置、白名单裁定、完整门禁、跨机器/Windows 和性能仍缺。因此这些父任务均保持未完成,不以局部关闭测试代替全工作流验收;未发布 npm 或改动插件锁。
1148+
1149+
## 2026-10-04 安装后的修复反馈验收增量
1150+
1151+
11.17 / 13.4 / 14.18 已补私有离线 npm 包真实链路测试:Node 入口将保存事件和原生任务复检贯通到稳定任务、next 和有界 Claude 形状摘要;源码变化使旧位置失效,未经受保护策略的零诊断不自闭。旧公开 0.1.3 对照明确缺此功能。该验收纳入 CI,见 [安装链路记录](../../../tests/acceptance/npm-repair-local-package.md);公开发行、默认插件、可信来源、真实宿主及完整平台验收仍缺,三个父任务不勾选。

‎openspec/changes/introduce-rust-codeguard-cli/verification.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2142,3 +2142,15 @@ Python 选中文件检查不再通过全项目元数据 walk 发现配置;只
21422142
- `/tmp/codeguard-resolution-schemas-final.log`:SHA-256 `15f1b959f89def12836da2d6cd90f0d7700edc0c2d8934e126ffcb66f48fb0d2`。
21432143

21442144
中英文架构、技术方案、README 与修复工作流已同步。SDK 仅覆盖限定 Zig 语法任务;默认插件/公开 CLI 的可信策略提供者、其它原生适配器、环境/依赖/政策处置、白名单裁定、跨机器/Windows、完整门禁和全语言精度/性能仍缺。9.7/9.10/9.11/14.10 等父任务保持未完成;没有发布 npm 或修改插件锁。此前提交 4585d8f 的 CI 37140830716 已成功,新提交 CI 须按新 SHA 独立核验。
2145+
2146+
## 2026-10-04 离线 npm 安装后的编辑与任务复检
2147+
2148+
通过新 binary-distribution 场景和独立 `npm_pack_repair.test.mjs` 验证安装后的 Node 入口保留 stdin、参数、工作目录及退出码。公开旧 0.1.3 经固定摘要核对后运行新链路测试真实 RED:编辑事件没有语法任务;当前源码的私有离线包 GREEN,1 passed/0 failed/0 skipped,62.4 秒。重复编辑、Claude 形状上下文、原生诊断指导、源码变化失效、修复零诊断不自闭、WASM 零恢复不关闭及同问题同任务均实际运行;受控原生工具与真实宿主边界见 [安装链路验收](../../../tests/acceptance/npm-repair-local-package.md)。9 份版本化实际报告通过 schema,三个伪造变体拒绝;记录保留旧包失败和测试参数修正失败,不混计成功。
2149+
2150+
CI 顺序接入该离线测试。本批仅增加验收、文档和 CI;Rust 产品源码未再修改,前段全工作区结果仍适用于同一产品源码。公开制品/插件默认接线未改变,11.17/13.4/14.18 和整体目标保持未完成。a688292 的 CI 37147462608 已失败:SDK 7 项均报 task_resolution_adapter_unavailable,npm 和全量步骤未运行;不能用本机成功替代此结果。
2151+
2152+
Linux CI 失败处理:宿主制品读取仍限制 256 MiB,不因测试失败扩大产品上限。测试夹具现先断言实际宿主程序大小,CI 设置 `CARGO_PROFILE_TEST_DEBUG=0` 去掉完整调试符号、保留 debug assertions;本机相同 profile 下宿主程序 101062368 字节,SDK 9 passed/0 failed/1 ignored(89.6 秒)。原 Linux 失败与新提交远端结果分开,当前根因指向调试制品预算,Linux 修正尚待新 CI 证明。
2153+
- `/tmp/codeguard-a688292-ci-failure.log`:SHA-256 `25faca123f505251cca14d688e774131a438503960adb20126b52ae7a5b6b670`。
2154+
- `/tmp/codeguard-resolution-ci-profile-local.log`:SHA-256 `d37c052e9fc23eeb7a3de0bba741024d42bd92a9b933fb0ae9d66b2827a20253`。
2155+
2156+
本批新增测试与 CI 配置最终通过 CLI 特性全目标 Clippy `-D warnings`、fmt、分层、OpenSpec strict、Node 语法和差异检查;CLI Clippy 日志 SHA-256 `07302979ebe5e9d95fc0a9419217173dbf32fd669f3d956c322890e9920b5318`。
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# 离线 npm 包的编辑与修复任务链路
2+
3+
对应 OpenSpec `introduce-rust-codeguard-cli` 的 `binary-distribution` 安装后修复反馈场景,关联 11.17、13.4、14.18。此记录证明本机 macOS arm64 私有候选包的实际运行,不证明公开版本升级、默认插件或已安装 Claude 自动触发。
4+
5+
## 对照与结果
6+
7+
先从 npm 注册表获取不可变的公开 0.1.3 资产,核对插件 lock 的 tarball SHA-256 `37509cdd108fee631eebb9ebcd794a6e8d63652801b3522625d0909c3da2ae12` 及程序 SHA-256 `04bdea8c489693a8e97b451df8d0c2b7a89ba82372db5a4d1fed277ba19b0f4d`。仅提取经核对的普通程序成员。新安装链路测试在旧制品上真实失败:编辑事件的 `local_feedback` 没有语法任务。该 RED 用于证明已发布旧包缺少能力,不是当前源码新增缺陷。
8+
9+
当前源码 WASM 程序由真实 packer 生成私有 `@full-stack-plugins/codeguard` tarball,并在独立缓存经 `npm exec --offline --ignore-scripts` 安装调用。测试实际执行 init、编辑 Hook、重复编辑、Claude 形状保存事件、原生 task verify、next、修改源码后的 next、原生零诊断、WASM 零恢复与问题再次出现。重复扫描与问题再次出现保持同一真实任务 ID;原生证据指导修复;源码变化后旧位置清空;没有受保护策略时原生零诊断及 WASM 零恢复均不关闭首次 finding。
10+
11+
最终测试 1 passed、0 failed、0 skipped,62.4 秒。早先当前源码试跑因测试使用 task verify 不支持的等号形式 `--timeout=30s` 失败,测试已改为该命令现有的 `--timeout 30s`;这次试跑不计成功,也不计产品缺陷。成功运行导出的 9 份版本化真实报告通过对应 JSON Schema,拒绝伪造交付 allow、复检 resolved 及简报 resolved 三种变体。Claude 格式输出含真实任务 ID 和 task show 指引,限制 1200 字符,不回显输入中的源码标记;此证据仍是宿主形状重放,不是已安装宿主验收。
12+
13+
## 复现
14+
15+
```bash
16+
cargo build --locked -p codeguard-cli --features wasm-precheck --offline
17+
CODEGUARD_WASM_BIN="$PWD/target/debug/codeguard" \
18+
node --test tests/npm_pack_repair.test.mjs
19+
```
20+
21+
可以使用 `CODEGUARD_NPM_REPAIR_ARTIFACT=/absolute/path/report.json` 导出测试实际报告,默认不导出。测试临时项目与 npm 缓存运行后清除,不执行 npm publish 或全局安装。CI 顺序执行原有 32 grammar 包测试与本测试,避免共享 tarball 路径竞争;Linux 新增用例的远端结果须按对应提交核验。
22+
23+
## 范围限制
24+
25+
原生工具是受控 Zig 0.16.0 形状夹具,只证明 Node/Rust 安装后的协议编排;真实 Zig 与签名生命周期另见 [任务生命周期验收](task-resolution-lifecycle.md)。32 grammar 的全量包内执行另见 [WASM 包验收](npm-wasm-local-package.md)。公开 `@partme.ai/codeguard@0.1.3` 仍缺本批闭环,默认插件、可信策略来源、其它宿主/平台、完整门禁和语言精度未完成。11.17、13.4、14.18 父任务不勾选。
26+
27+
日志身份:
28+
- `/tmp/codeguard-npm-repair-red.log`:SHA-256 `cfb132d2edf04d54187e44e3b81bb0ceaa30c60c3529f8f180c837662cc66134`。
29+
- `/tmp/codeguard-npm-repair-green-final.log`:SHA-256 `4b0ccaa0327a61097d5efc6fea5a47aa55c78fbdb71d1ca5283fa5cfe0e62d98`。
30+
- `/tmp/codeguard-npm-repair-schema-final.log`:SHA-256 `8e88893969dfb750249a73dcaa468d8ac478155b322b814fcbc4531d1edda045`。
31+
32+
Linux 前置 SDK 验收在提交 a688292 的 CI 37147462608 失败,npm 步骤被跳过。后续 CI 去掉测试制品调试符号并加入 256 MiB 大小断言,产品读取预算不放宽;本机同 profile SDK 9 passed/0 failed/1 ignored。新增 npm 链路及 Linux 修正都须按新提交等待远端结果,不能视为已通过。

‎tests/npm_pack_repair.test.mjs‎

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import assert from 'node:assert/strict';
2+
import { spawnSync } from 'node:child_process';
3+
import { mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
4+
import os from 'node:os';
5+
import path from 'node:path';
6+
import test from 'node:test';
7+
import { fileURLToPath } from 'node:url';
8+
9+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
10+
11+
test('离线 npm 包贯通编辑任务与原生复检,未批准时不关闭', () => {
12+
const binary = process.env.CODEGUARD_WASM_BIN;
13+
assert.ok(binary, 'CODEGUARD_WASM_BIN is required');
14+
const packed = spawnSync(process.execPath, [
15+
path.join(root, 'scripts', 'pack-npm-local.mjs'), '--require-wasm', binary,
16+
], { cwd: root, encoding: 'utf8', timeout: 120_000 });
17+
assert.equal(packed.status, 0, packed.stderr);
18+
const tarball = packed.stdout.trim();
19+
assert.ok(tarball.endsWith('.tgz'));
20+
const scratch = realpathSync(mkdtempSync(path.join(os.tmpdir(), 'codeguard-npm-repair-')));
21+
try {
22+
const file = path.join(scratch, 'app.zig');
23+
const bad = 'pub fn main( void {\n';
24+
const good = 'pub fn main() void {}\n';
25+
writeFileSync(file, bad);
26+
function invoke(args, input) {
27+
const result = spawnSync('npm', [
28+
'exec', '--offline', '--yes', '--ignore-scripts', '--cache', path.join(scratch, 'npm-cache'),
29+
'--package', tarball, '--', 'codeguard', ...args, '--format=json',
30+
], {
31+
cwd: scratch, input: input === undefined ? undefined : JSON.stringify(input),
32+
encoding: 'utf8', timeout: 120_000, maxBuffer: 1024 * 1024,
33+
env: { ...process.env, npm_config_audit: 'false', npm_config_fund: 'false' },
34+
});
35+
assert.equal(result.error, undefined, result.error?.message);
36+
assert.equal(result.signal, null);
37+
let value;
38+
try { value = JSON.parse(result.stdout); } catch {
39+
assert.fail(`npm launcher did not return JSON: ${result.stderr}`);
40+
}
41+
return { exit: result.status, value };
42+
}
43+
const initialized = invoke(['init', scratch, '--apply']);
44+
assert.equal(initialized.exit, 3);
45+
const changed = {
46+
schema_version: '1.0.0', report_type: 'hook_trigger_request',
47+
input: {
48+
event: 'file_changed', changed_paths: ['app.zig'], task_id: null,
49+
write_outcome: 'confirmed', host_claims_blocking: false,
50+
},
51+
};
52+
const edit = () => invoke(['hook', 'execute', scratch, '--timeout=30s'], changed);
53+
const first = edit();
54+
assert.equal(first.exit, 3);
55+
assert.equal(first.value.delivery_decision, 'not_evaluated');
56+
assert.ok(first.value.local_feedback?.syntax_tasks,
57+
'installed package must connect edit feedback to syntax tasks');
58+
const tasks = first.value.local_feedback.syntax_tasks;
59+
assert.equal(tasks?.status, 'synced_partial', 'installed package must persist syntax tasks');
60+
const id = tasks.tasks[0].task_id;
61+
assert.match(id, /^CG-B-[a-f0-9]+$/);
62+
const repeated = edit();
63+
assert.equal(repeated.exit, 3);
64+
assert.equal(repeated.value.local_feedback.syntax_tasks.tasks[0].task_id, id);
65+
66+
const conversation = invoke([
67+
'hook', 'claude', 'post-tool-use', scratch, '--timeout=30s',
68+
], {
69+
hook_event_name: 'PostToolUse', cwd: scratch, tool_name: 'Edit',
70+
tool_input: {
71+
file_path: file, old_string: 'old', new_string: 'HOST_SOURCE_MUST_NOT_BE_ECHOED',
72+
},
73+
tool_response: { success: true },
74+
});
75+
assert.equal(conversation.exit, 0);
76+
assert.equal(conversation.value.hookSpecificOutput.hookEventName, 'PostToolUse');
77+
const context = conversation.value.hookSpecificOutput.additionalContext;
78+
assert.ok(context.includes(id));
79+
assert.ok(context.includes('codeguard task show'));
80+
assert.ok([...context].length <= 1200);
81+
assert.ok(!context.includes('HOST_SOURCE_MUST_NOT_BE_ECHOED'));
82+
83+
// 受控工具只验证安装后编排;真实 Zig 验收另有独立用例。
84+
const tool = path.join(scratch, 'zig-tool');
85+
writeFileSync(tool, `#!/bin/sh
86+
if [ "$1" = version ]; then printf '0.16.0\\n'; exit 0; fi
87+
while IFS= read -r line; do
88+
case "$line" in *'main( void'*) printf '<stdin>:1:13: error: expected token\\n' >&2; exit 1;; esac
89+
done
90+
exit 0
91+
`, { mode: 0o700 });
92+
const verify = () => invoke([
93+
'task', 'verify', id, scratch, '--zig-tool', tool, '--timeout', '30s',
94+
]);
95+
const present = verify();
96+
assert.equal(present.exit, 3);
97+
assert.equal(present.value.event_persisted, true);
98+
assert.equal(present.value.observation, 'still_blocked');
99+
assert.equal(present.value.native_scan.native.status, 'diagnostics_observed');
100+
const next = invoke(['next', scratch]);
101+
assert.equal(next.value.repair_brief.disposition, 'actionable');
102+
assert.equal(next.value.repair_brief.native_diagnostic_positions[0].column, 13);
103+
assert.equal(next.value.repair_brief.recheck_argv.at(-1), tool);
104+
assert.equal(next.value.repair_brief.native_confirmation_ref.run_id,
105+
present.value.native_scan.run_id);
106+
107+
writeFileSync(file, good);
108+
const stale = invoke(['next', scratch]);
109+
assert.equal(stale.value.repair_brief.disposition, 'verification_required');
110+
assert.deepEqual(stale.value.repair_brief.native_diagnostic_positions, []);
111+
const repaired = verify();
112+
assert.equal(repaired.exit, 3);
113+
assert.equal(repaired.value.event_persisted, true);
114+
assert.equal(repaired.value.observation, 'candidate_absent_unverified_policy');
115+
assert.equal(repaired.value.native_scan.native.status, 'completed');
116+
const factPath = path.join(scratch, '.codeguard', 'findings', id, 'finding.json');
117+
assert.equal(JSON.parse(readFileSync(factPath)).state, 'open');
118+
const cleanCandidate = edit();
119+
assert.equal(cleanCandidate.value.local_feedback.candidate_recovery_count, 0);
120+
assert.equal(JSON.parse(readFileSync(factPath)).state, 'open');
121+
122+
writeFileSync(file, bad);
123+
const again = edit();
124+
assert.equal(again.value.local_feedback.syntax_tasks.tasks[0].task_id, id);
125+
assert.equal(verify().value.observation, 'still_blocked');
126+
assert.equal(JSON.parse(readFileSync(factPath)).state, 'open');
127+
if (process.env.CODEGUARD_NPM_REPAIR_ARTIFACT) {
128+
writeFileSync(process.env.CODEGUARD_NPM_REPAIR_ARTIFACT, JSON.stringify({
129+
initialized: initialized.value, edit: first.value, repeated: repeated.value,
130+
conversation: conversation.value, present: present.value, next: next.value,
131+
stale: stale.value, repaired: repaired.value, clean_candidate: cleanCandidate.value,
132+
repeated_problem: again.value,
133+
}, null, 2));
134+
}
135+
} finally {
136+
rmSync(scratch, { recursive: true, force: true });
137+
}
138+
});

0 commit comments

Comments
 (0)