agentic-flow-kit 1.7.15 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -8
- package/modules/hosts/claude/commands/wf-build.md +1 -1
- package/modules/hosts/claude/commands/wf-design.md +5 -4
- package/modules/hosts/claude/commands/wf-maintain.md +1 -1
- package/modules/hosts/claude/commands/wf-new-task.md +47 -25
- package/modules/hosts/claude/commands/wf-plan.md +1 -1
- package/modules/hosts/claude/commands/wf-review.md +2 -2
- package/modules/hosts/claude/commands/wf-sync-hosts.md +7 -13
- package/modules/hosts/claude/commands/wf-test.md +2 -2
- package/modules/hosts/codex/commands/wf-build.md +1 -1
- package/modules/hosts/codex/commands/wf-design.md +5 -4
- package/modules/hosts/codex/commands/wf-maintain.md +1 -1
- package/modules/hosts/codex/commands/wf-new-task.md +47 -25
- package/modules/hosts/codex/commands/wf-plan.md +1 -1
- package/modules/hosts/codex/commands/wf-review.md +2 -2
- package/modules/hosts/codex/commands/wf-sync-hosts.md +7 -13
- package/modules/hosts/codex/commands/wf-test.md +2 -2
- package/modules/hosts/cursor/commands/wf-build.md +1 -1
- package/modules/hosts/cursor/commands/wf-design.md +5 -4
- package/modules/hosts/cursor/commands/wf-maintain.md +1 -1
- package/modules/hosts/cursor/commands/wf-new-task.md +47 -25
- package/modules/hosts/cursor/commands/wf-plan.md +1 -1
- package/modules/hosts/cursor/commands/wf-review.md +2 -2
- package/modules/hosts/cursor/commands/wf-sync-hosts.md +7 -13
- package/modules/hosts/cursor/commands/wf-test.md +2 -2
- package/modules/hosts/opencode/commands/wf-build.md +1 -1
- package/modules/hosts/opencode/commands/wf-design.md +5 -4
- package/modules/hosts/opencode/commands/wf-maintain.md +1 -1
- package/modules/hosts/opencode/commands/wf-new-task.md +47 -25
- package/modules/hosts/opencode/commands/wf-plan.md +1 -1
- package/modules/hosts/opencode/commands/wf-review.md +2 -2
- package/modules/hosts/opencode/commands/wf-sync-hosts.md +7 -13
- package/modules/hosts/opencode/commands/wf-test.md +2 -2
- package/modules/hosts/trae/commands/wf-build.md +1 -1
- package/modules/hosts/trae/commands/wf-design.md +5 -4
- package/modules/hosts/trae/commands/wf-maintain.md +1 -1
- package/modules/hosts/trae/commands/wf-new-task.md +47 -25
- package/modules/hosts/trae/commands/wf-plan.md +1 -1
- package/modules/hosts/trae/commands/wf-review.md +2 -2
- package/modules/hosts/trae/commands/wf-sync-hosts.md +7 -13
- package/modules/hosts/trae/commands/wf-test.md +2 -2
- package/package.json +1 -1
- package/src/cli.mjs +47 -4
- package/src/doctor.mjs +48 -3
- package/src/executor.mjs +262 -0
- package/src/executor.test.mjs +295 -0
- package/src/fresh-init.test.mjs +1 -1
- package/src/init.mjs +27 -3
- package/src/profiles.mjs +10 -3
- package/src/render.mjs +3 -3
- package/src/rule-budget.test.mjs +2 -2
- package/src/stack-profile.test.mjs +6 -0
- package/src/sync-hosts.mjs +37 -43
- package/src/sync-hosts.test.mjs +67 -56
- package/src/validate.mjs +6 -1
- package/src/validate.test.mjs +1 -1
- package/templates/.gitattributes +12 -1
- package/templates/AGENTS.md +3 -3
- package/templates/_agents/commands/build.md +1 -1
- package/templates/_agents/commands/design.md +5 -4
- package/templates/_agents/commands/maintain.md +1 -1
- package/templates/_agents/commands/new-task.md +47 -25
- package/templates/_agents/commands/plan.md +1 -1
- package/templates/_agents/commands/review.md +2 -2
- package/templates/_agents/commands/sync-hosts.md +7 -13
- package/templates/_agents/commands/test.md +2 -2
- package/templates/_agents/control-surfaces.txt +1 -1
- package/templates/_agents/engine-lock.json +334 -0
- package/templates/_agents/hooks/check-index-incremental.mjs +48 -0
- package/templates/_agents/hooks/check-pairing-incremental.mjs +66 -0
- package/templates/_agents/hooks/check-wiki-ledger.mjs +30 -0
- package/templates/_agents/lane-surfaces.txt +5 -2
- package/templates/_agents/level-hints.txt +30 -0
- package/templates/_agents/loop/papercut-debt/DESIGN.md +112 -0
- package/templates/_agents/mechanism-registry.md +27 -0
- package/templates/_agents/rule-budgets.txt +2 -2
- package/templates/_agents/scripts/agg-delegations.cjs +21 -3
- package/templates/_agents/scripts/agg-delegations.test.mjs +5 -3
- package/templates/_agents/scripts/agg-gate-stats.mjs +3 -3
- package/templates/_agents/scripts/agg-judgments.mjs +80 -0
- package/templates/_agents/scripts/agg-judgments.test.mjs +45 -0
- package/templates/_agents/scripts/archive-sweep.mjs +233 -0
- package/templates/_agents/scripts/archive-sweep.test.mjs +129 -0
- package/templates/_agents/scripts/check-attribution.mjs +37 -0
- package/templates/_agents/scripts/check-convergence.mjs +119 -0
- package/templates/_agents/scripts/check-engine-integrity.mjs +59 -127
- package/templates/_agents/scripts/check-engine-integrity.test.mjs +33 -45
- package/templates/_agents/scripts/check-funnel.mjs +56 -0
- package/templates/_agents/scripts/check-hygiene.mjs +5 -5
- package/templates/_agents/scripts/check-index-incremental.test.mjs +73 -0
- package/templates/_agents/scripts/check-lane-surface.mjs +74 -25
- package/templates/_agents/scripts/check-lane-surface.test.mjs +71 -5
- package/templates/_agents/scripts/check-ledger-invariant.mjs +427 -78
- package/templates/_agents/scripts/check-ledger-invariant.test.mjs +438 -4
- package/templates/_agents/scripts/check-loop-rev.test.mjs +13 -8
- package/templates/_agents/scripts/check-loop-unit.test.mjs +8 -0
- package/templates/_agents/scripts/check-loop.mjs +264 -75
- package/templates/_agents/scripts/check-loop.test.mjs +587 -61
- package/templates/_agents/scripts/check-metric-claims.mjs +4 -2
- package/templates/_agents/scripts/check-prejudgment.mjs +59 -0
- package/templates/_agents/scripts/check-registry.mjs +45 -0
- package/templates/_agents/scripts/check-scope.mjs +96 -0
- package/templates/_agents/scripts/check-scope.test.mjs +96 -0
- package/templates/_agents/scripts/check-scripts-syntax.mjs +61 -0
- package/templates/_agents/scripts/check-scripts-syntax.test.mjs +72 -0
- package/templates/_agents/scripts/check-spec-verif.mjs +47 -0
- package/templates/_agents/scripts/compact-ledgers.mjs +97 -0
- package/templates/_agents/scripts/compact-ledgers.test.mjs +162 -0
- package/templates/_agents/scripts/confirm-doc.mjs +136 -31
- package/templates/_agents/scripts/confirm-doc.test.mjs +180 -4
- package/templates/_agents/scripts/doctor.test.mjs +44 -1
- package/templates/_agents/scripts/edit-face-check.mjs +5 -3
- package/templates/_agents/scripts/edit-face-check.test.mjs +5 -4
- package/templates/_agents/scripts/fill-intent.mjs +7 -5
- package/templates/_agents/scripts/fill-intent.test.mjs +19 -1
- package/templates/_agents/scripts/fill-plan.mjs +10 -6
- package/templates/_agents/scripts/fill-plan.test.mjs +19 -3
- package/templates/_agents/scripts/fill-spec.mjs +6 -3
- package/templates/_agents/scripts/fill-spec.test.mjs +12 -4
- package/templates/_agents/scripts/fixture-scripts.mjs +153 -0
- package/templates/_agents/scripts/fixture-scripts.test.mjs +112 -0
- package/templates/_agents/scripts/gate-checklist.mjs +10 -9
- package/templates/_agents/scripts/gate-checklist.test.mjs +2 -11
- package/templates/_agents/scripts/gate-seg.mjs +1 -1
- package/templates/_agents/scripts/gen-effective-policy.mjs +107 -0
- package/templates/_agents/scripts/gen-effective-policy.test.mjs +100 -0
- package/templates/_agents/scripts/gen-verify-evidence.mjs +107 -0
- package/templates/_agents/scripts/gen-verify-evidence.test.mjs +84 -0
- package/templates/_agents/scripts/gen-workflow-dashboard.mjs +16 -14
- package/templates/_agents/scripts/gen-workflow-dashboard.test.mjs +2 -1
- package/templates/_agents/scripts/gen-workflow-metrics.mjs +12 -36
- package/templates/_agents/scripts/git-core.mjs +45 -0
- package/templates/_agents/scripts/git-merge-jsonl.mjs +63 -0
- package/templates/_agents/scripts/git-merge-jsonl.test.mjs +125 -0
- package/templates/_agents/scripts/judgment-ledger.mjs +65 -0
- package/templates/_agents/scripts/judgment-ledger.test.mjs +129 -0
- package/templates/_agents/scripts/kb-search.mjs +19 -4
- package/templates/_agents/scripts/kb-search.test.mjs +33 -0
- package/templates/_agents/scripts/lane-surface-audit.mjs +143 -0
- package/templates/_agents/scripts/lane-surface-audit.test.mjs +93 -0
- package/templates/_agents/scripts/ledger-doc-chain.test.mjs +166 -0
- package/templates/_agents/scripts/ledger-loader.mjs +108 -0
- package/templates/_agents/scripts/ledger-loader.test.mjs +131 -0
- package/templates/_agents/scripts/ledger-prune.mjs +96 -0
- package/templates/_agents/scripts/ledger-prune.test.mjs +120 -0
- package/templates/_agents/scripts/ledger-shards.mjs +307 -0
- package/templates/_agents/scripts/ledger-shards.test.mjs +339 -0
- package/templates/_agents/scripts/ledger-ts-monotonic.test.mjs +135 -0
- package/templates/_agents/scripts/level-criteria.mjs +231 -0
- package/templates/_agents/scripts/level-criteria.test.mjs +281 -0
- package/templates/_agents/scripts/papercut-debt-controller.mjs +141 -0
- package/templates/_agents/scripts/papercut-debt-controller.test.mjs +127 -0
- package/templates/_agents/scripts/papercut-debt-sensor.mjs +170 -0
- package/templates/_agents/scripts/papercut-debt-sensor.test.mjs +146 -0
- package/templates/_agents/scripts/pipeline-run.mjs +10 -24
- package/templates/_agents/scripts/pipeline-run.test.mjs +22 -42
- package/templates/_agents/scripts/policy.mjs +75 -0
- package/templates/_agents/scripts/resolve-merge-conflicts.mjs +110 -0
- package/templates/_agents/scripts/resolve-merge-conflicts.test.mjs +87 -0
- package/templates/_agents/scripts/rewrite-file.mjs +133 -0
- package/templates/_agents/scripts/rewrite-file.test.mjs +148 -0
- package/templates/_agents/scripts/rule-budget.mjs +83 -0
- package/templates/_agents/scripts/run-tests.mjs +99 -0
- package/templates/_agents/scripts/run-tests.test.mjs +105 -0
- package/templates/_agents/scripts/solidify-task.test.mjs +1 -1
- package/templates/_agents/scripts/stage-gates.mjs +38 -21
- package/templates/_agents/scripts/trust-mode.mjs +4 -4
- package/templates/_agents/scripts/trust-mode.test.mjs +1 -1
- package/templates/_agents/scripts/verify.mjs +135 -58
- package/templates/_agents/scripts/verify.test.mjs +156 -47
- package/templates/_agents/scripts/wf-journal.mjs +8 -4
- package/templates/_agents/scripts/wf-journal.test.mjs +1 -1
- package/templates/_agents/scripts/wiki-search.test.mjs +5 -6
- package/templates/_agents/scripts/workflow-board-server.test.mjs +2 -2
- package/templates/_agents/scripts/workflow-enums.test.mjs +14 -5
- package/templates/_agents/scripts/workflows-check.mjs +1 -1
- package/templates/_agents/scripts-test-exempt.txt +1 -0
- package/templates/_agents/skills/papercut-fix/SKILL.md +60 -0
- package/templates/_agents/skills/papercut-fix/references/response-template.md +81 -0
- package/templates/_agents/workflow-enums.txt +8 -1
- package/templates/_agents/workflows/pipeline-closing.md +4 -5
- package/templates/_githooks/pre-commit +109 -120
- package/templates/_githooks/pre-merge-commit +27 -23
- package/templates/_githooks/pre-push +7 -6
- package/templates/_github/workflows/kit-ci.yml +4 -3
- package/templates/workflow/README.md +23 -5
- package/templates/workflow/incidents/_TEMPLATE.md +4 -1
- package/templates/workflow/intents/_TEMPLATE.md +4 -2
- package/templates/workflow/papercuts.md +2 -0
- package/templates/workflow/plans/_TEMPLATE.md +10 -5
- package/templates/workflow/regression-checklist.md +1 -0
- package/templates/workflow/specs/_TEMPLATE.md +17 -1
- package/modules/hosts/claude/commands/wf-source-sync-check.md +0 -89
- package/modules/hosts/codex/commands/wf-source-sync-check.md +0 -89
- package/modules/hosts/cursor/commands/wf-source-sync-check.md +0 -89
- package/modules/hosts/opencode/commands/wf-source-sync-check.md +0 -89
- package/modules/hosts/trae/commands/wf-source-sync-check.md +0 -90
- package/templates/_agents/commands/source-sync-check.md +0 -103
- package/templates/_agents/hooks/check-pairing-incremental.sh +0 -43
- package/templates/_agents/hooks/check-wiki-ledger.sh +0 -18
- package/templates/_agents/scripts/check-ledger.mjs +0 -109
- package/templates/_agents/scripts/check-ledger.test.mjs +0 -199
- package/templates/_agents/scripts/rule-budget.sh +0 -74
- package/templates/_agents/scripts/source-sync-check.mjs +0 -156
- package/templates/_agents/scripts/source-sync-check.test.mjs +0 -286
- package/templates/_agents/workflows/source-sync-repair.md +0 -30
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// resolve-merge-conflicts.mjs — 台账与派生生成物合并冲突一键自愈器(2026-10-10;B9 自 Shipyard.Material
|
|
3
|
+
// 回灌——判定面逐字对齐,出处沿革留痕在包源仓 git 历史与 CHANGELOG,装户径不引实例路径)
|
|
4
|
+
// 解决什么:多人并发提同一分支,或单人多 worktree 并行后合并时,派生生成物与 append-only 台账走
|
|
5
|
+
// git 文本合并产生冲突。第一道防线是 `.gitattributes` 的 `merge=jsonl-union` 驱动(台账类冲突直接
|
|
6
|
+
// 确定性并集,见 git-merge-jsonl.mjs);本脚本是**兜底**——driver / gitattributes 行漏配退化成文本
|
|
7
|
+
// 冲突、或 INDEX.md 这类生成物仍要人工解时,一键消解派生面,把人留在业务冲突上。
|
|
8
|
+
// 逻辑:
|
|
9
|
+
// 1. 扫描当前 Git 处于 unmerged (UU / AA / DD / AU / UA 等) 状态的文件;
|
|
10
|
+
// 2. 若 workflow/INDEX.md 冲突:checkout --ours 后重新调用 gen-workflow-index.mjs 全量生成并 git add;
|
|
11
|
+
// 3. 若 .agents/ 下 *.jsonl 台账冲突且双方内容都在(UU/AA):利用 git-merge-jsonl.mjs 合并当前与对方
|
|
12
|
+
// 版本并 git add。判定面按「.agents/ 前缀 + .jsonl 后缀」匹配,**天然含 confirmations.d/ 分片**——
|
|
13
|
+
// 分片是 kit 确认行的默认写入口径(2026-10-13 ledger-shard-default),分片 UU/AA 同走并集;
|
|
14
|
+
// 删除/改删型(DD/AU/UD/UA/DU)不做任何写入——自动并集会复活被删文件或凭空建空文件,仅提示人工裁决;
|
|
15
|
+
// 4. 输出处理清单,退出码 0。
|
|
16
|
+
|
|
17
|
+
import { execFileSync } from 'node:child_process';
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import { mergeJsonlLines } from './git-merge-jsonl.mjs';
|
|
20
|
+
|
|
21
|
+
const git = (args) => {
|
|
22
|
+
try { return execFileSync('git', args, { encoding: 'utf8' }).trim(); }
|
|
23
|
+
catch (e) { return ''; }
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function getUnmergedFiles() {
|
|
27
|
+
const status = git(['status', '--porcelain']);
|
|
28
|
+
if (!status) return [];
|
|
29
|
+
|
|
30
|
+
const unmerged = [];
|
|
31
|
+
for (const line of status.split(/\r?\n/)) {
|
|
32
|
+
if (!line) continue;
|
|
33
|
+
const code = line.slice(0, 2);
|
|
34
|
+
const file = line.slice(3).trim();
|
|
35
|
+
// 冲突代码:UU (both modified), AA (both added), DU, UD 等
|
|
36
|
+
if (/^(UU|AA|DD|AU|UD|UA|DU)/.test(code)) {
|
|
37
|
+
unmerged.push({ file, code });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return unmerged;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function autoResolveConflicts() {
|
|
44
|
+
const files = getUnmergedFiles();
|
|
45
|
+
if (files.length === 0) {
|
|
46
|
+
console.log('[resolve-conflicts] 当前无未解决的合并冲突。');
|
|
47
|
+
return { resolved: [], remaining: [] };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const resolved = [];
|
|
51
|
+
const remaining = [];
|
|
52
|
+
|
|
53
|
+
for (const { file, code } of files) {
|
|
54
|
+
const normFile = file.replace(/\\/g, '/');
|
|
55
|
+
|
|
56
|
+
// 1. workflow/INDEX.md 派生索引冲突:取当前版本为底,重新全量生成
|
|
57
|
+
if (normFile === 'workflow/INDEX.md') {
|
|
58
|
+
try {
|
|
59
|
+
execFileSync('git', ['checkout', '--ours', 'workflow/INDEX.md'], { stdio: 'pipe' });
|
|
60
|
+
// 调用索引生成器重新渲染
|
|
61
|
+
execFileSync(process.execPath, ['.agents/scripts/gen-workflow-index.mjs'], { stdio: 'pipe' });
|
|
62
|
+
execFileSync('git', ['add', 'workflow/INDEX.md'], { stdio: 'pipe' });
|
|
63
|
+
resolved.push('workflow/INDEX.md (已自动全量重生成)');
|
|
64
|
+
} catch (err) {
|
|
65
|
+
remaining.push(`workflow/INDEX.md: 重生成失败 (${err.message})`);
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 2. .agents/ 下 *.jsonl 台账冲突(主台账与 confirmations.d/ 分片同判定面):仅 UU/AA(双方内容
|
|
71
|
+
// 都在)取当前版本和对方版本合并去重;删除/改删型(DD/AU/UD/UA/DU)不可并集,自动写入会
|
|
72
|
+
// 复活被删文件或凭空建空文件
|
|
73
|
+
if (normFile.startsWith('.agents/') && normFile.endsWith('.jsonl')) {
|
|
74
|
+
if (!/^(UU|AA)$/.test(code)) {
|
|
75
|
+
remaining.push(`${file}: 删除/改删型冲突不自动处理,请人工裁决`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const ours = git(['show', `:2:${normFile}`]);
|
|
80
|
+
const theirs = git(['show', `:3:${normFile}`]);
|
|
81
|
+
const merged = mergeJsonlLines(ours, theirs);
|
|
82
|
+
fs.writeFileSync(file, merged, 'utf8');
|
|
83
|
+
execFileSync('git', ['add', file], { stdio: 'pipe' });
|
|
84
|
+
resolved.push(`${file} (已自动并集去重)`);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
remaining.push(`${file}: 台账合并失败 (${err.message})`);
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 其余业务文件保持冲突,需人工审查
|
|
92
|
+
remaining.push(file);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { resolved, remaining };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (process.argv[1] && process.argv[1].endsWith('resolve-merge-conflicts.mjs')) {
|
|
99
|
+
const { resolved, remaining } = autoResolveConflicts();
|
|
100
|
+
if (resolved.length > 0) {
|
|
101
|
+
console.log('[resolve-conflicts] ✅ 已自动消解以下派生/台账冲突:');
|
|
102
|
+
resolved.forEach((f) => console.log(` - ${f}`));
|
|
103
|
+
}
|
|
104
|
+
if (remaining.length > 0) {
|
|
105
|
+
console.log('\n[resolve-conflicts] ⚠️ 剩余业务代码冲突需手动介入处理:');
|
|
106
|
+
remaining.forEach((f) => console.log(` - ${f}`));
|
|
107
|
+
} else if (resolved.length > 0) {
|
|
108
|
+
console.log('\n[resolve-conflicts] 所有台账冲突已全部清零,可继续 git commit 完成合并。');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// resolve-merge-conflicts.test.mjs — 端到端测试合并冲突自愈器的收窄行为(临时 git 仓库夹具;B9 自 Shipyard.Material 回灌):
|
|
2
|
+
// UU(双方内容都在)走自动并集并 git add;删除/改删型(UD 等)不自动写入、不 git add,仅提示人工裁决。
|
|
3
|
+
// 夹具说明:git merge 对「双方都删」会静默自动解决(不产生 DD 未合并条目,已实测),
|
|
4
|
+
// 故删除型冲突用 modify/delete 构造(ours 修改 + theirs 删除 → UD),确定性复现未合并条目,
|
|
5
|
+
// 且正是旧 bug 的复活路径(:3: 读取失败 → 吞错为 '' → 误写回 ours 内容并 git add)。
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
let pass = 0, fail = 0;
|
|
13
|
+
const check = (name, cond, detail = '') => {
|
|
14
|
+
if (cond) { pass++; console.log(`PASS ${name}`); }
|
|
15
|
+
else { fail++; console.log(`FAIL ${name}${detail ? ` —— ${detail}` : ''}`); }
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const scriptPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'resolve-merge-conflicts.mjs');
|
|
19
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'resolve-conflicts-test-'));
|
|
20
|
+
|
|
21
|
+
const git = (...args) => spawnSync('git', args, { cwd: tmp, encoding: 'utf8' });
|
|
22
|
+
const porcelain = () => git('status', '--porcelain').stdout.split(/\r?\n/).filter(Boolean);
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// 夹具:临时 git 仓库,基线含主台账 + 分片
|
|
26
|
+
git('init');
|
|
27
|
+
git('config', 'user.email', 'test@local');
|
|
28
|
+
git('config', 'user.name', 'test');
|
|
29
|
+
git('config', 'core.autocrlf', 'false');
|
|
30
|
+
const ledger = path.join(tmp, '.agents', 'confirmations.jsonl');
|
|
31
|
+
const shard = path.join(tmp, '.agents', 'confirmations.d', 'shard-x.jsonl');
|
|
32
|
+
fs.mkdirSync(path.dirname(shard), { recursive: true });
|
|
33
|
+
fs.writeFileSync(ledger, '{"id":1}\n');
|
|
34
|
+
fs.writeFileSync(shard, '{"id":2}\n');
|
|
35
|
+
git('add', '-A');
|
|
36
|
+
git('commit', '-m', 'base');
|
|
37
|
+
const defaultBranch = git('rev-parse', '--abbrev-ref', 'HEAD').stdout.trim();
|
|
38
|
+
|
|
39
|
+
// side:改主台账(追加 id:3)+ 删分片
|
|
40
|
+
git('checkout', '-b', 'side');
|
|
41
|
+
fs.writeFileSync(ledger, '{"id":1}\n{"id":3}\n');
|
|
42
|
+
fs.rmSync(shard);
|
|
43
|
+
git('add', '-A');
|
|
44
|
+
git('commit', '-m', 'side');
|
|
45
|
+
|
|
46
|
+
// main:改主台账(追加 id:4,与 side 不同行 → UU)+ 改分片(side 已删 → UD)
|
|
47
|
+
git('checkout', defaultBranch);
|
|
48
|
+
fs.writeFileSync(ledger, '{"id":1}\n{"id":4}\n');
|
|
49
|
+
fs.writeFileSync(shard, '{"id":2}\n{"id":5}\n');
|
|
50
|
+
git('add', '-A');
|
|
51
|
+
git('commit', '-m', 'main');
|
|
52
|
+
|
|
53
|
+
const merge = git('merge', 'side');
|
|
54
|
+
const statusConflicted = porcelain();
|
|
55
|
+
check('S1 夹具产生 UU+UD 冲突(merge 失败且两条未合并条目就位)',
|
|
56
|
+
merge.status !== 0 &&
|
|
57
|
+
statusConflicted.includes('UU .agents/confirmations.jsonl') &&
|
|
58
|
+
statusConflicted.includes('UD .agents/confirmations.d/shard-x.jsonl'),
|
|
59
|
+
`merge-exit=${merge.status} status=${statusConflicted.join(' | ')}`);
|
|
60
|
+
|
|
61
|
+
const shardBefore = fs.readFileSync(shard, 'utf8');
|
|
62
|
+
|
|
63
|
+
// 被测 CLI
|
|
64
|
+
const proc = spawnSync(process.execPath, [scriptPath], { cwd: tmp, encoding: 'utf8' });
|
|
65
|
+
|
|
66
|
+
check('S2 CLI exit 0(remaining 非零也 exit 0 的既有契约)', proc.status === 0, `status=${proc.status} stderr=${proc.stderr}`);
|
|
67
|
+
|
|
68
|
+
const merged = fs.readFileSync(ledger, 'utf8');
|
|
69
|
+
check('S3 UU 主台账=双方行并集', merged === '{"id":1}\n{"id":4}\n{"id":3}\n', merged);
|
|
70
|
+
check('S4 UU 主台账无冲突标记', !merged.includes('<<<<<<<'), merged);
|
|
71
|
+
const statusAfter = porcelain();
|
|
72
|
+
const ledgerLine = statusAfter.find((l) => l.endsWith('.agents/confirmations.jsonl'));
|
|
73
|
+
check('S5 UU 主台账已 git add(不再未合并)', !!ledgerLine && !/^(UU|AA|DD|AU|UD|UA|DU)/.test(ledgerLine), ledgerLine || '(不在 status 中)');
|
|
74
|
+
|
|
75
|
+
check('S6 UD 分片未被 git add(仍处未合并态)',
|
|
76
|
+
statusAfter.includes('UD .agents/confirmations.d/shard-x.jsonl'),
|
|
77
|
+
statusAfter.join(' | '));
|
|
78
|
+
check('S7 UD 分片内容未被自动改写', fs.readFileSync(shard, 'utf8') === shardBefore, fs.readFileSync(shard, 'utf8'));
|
|
79
|
+
check('S8 stdout 提示分片删除/改删型冲突需人工裁决',
|
|
80
|
+
proc.stdout.includes('.agents/confirmations.d/shard-x.jsonl') && proc.stdout.includes('删除/改删型冲突不自动处理'),
|
|
81
|
+
proc.stdout);
|
|
82
|
+
} finally {
|
|
83
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
87
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// rewrite-file.mjs — 按行字面替换 + 响亮失败(2026-10-12 silent-noop-loud-failure)
|
|
3
|
+
// 背景(papercut 2026-10-06 行,留痕在包源仓 git 历史与 CHANGELOG;两次假落账实证):
|
|
4
|
+
// `node -e "fs.writeFileSync(p, s.replace(A, B))"` 对 **CRLF 检出文件**用 `\n` 锚串定位时
|
|
5
|
+
// **静默无操作**,脚本照样打印「落账 OK」——假成功比响亮失败危险得多(台账/文档被以为改了)。
|
|
6
|
+
// 本工具把该形态的三个失效点逐一堵死:
|
|
7
|
+
// ① 按行处理并保留原行尾(`/\r?\n/` 切分 + 检出 EOL 回写)——不再有「\n 锚串匹配不上 CRLF」这一类
|
|
8
|
+
// ② 替换数必须与 --expect 相符(默认 1)——0 命中即非零退出,**文件不写**,绝不存在「以为改了」
|
|
9
|
+
// ③ 写后回读校验(新锚已在、旧锚已零、行数不变、EOL 未变)——全过才打印成功;任一不过即非零退出
|
|
10
|
+
// 另防两类自伤:find === replace(无操作却报成功)与目标文件不存在(fail-loud,不静默建文件)。
|
|
11
|
+
// 原子写:同目录临时文件 + rename——中途崩溃不留半写文件(沿 git-merge-jsonl.mjs / compact-ledgers.mjs 惯例)。
|
|
12
|
+
// 用法:node .agents/scripts/rewrite-file.mjs <文件> --find <字面串> --replace <字面串>
|
|
13
|
+
// [--expect N] [--allow-multi] [--dry-run] [--root <仓库根>]
|
|
14
|
+
// --expect N 期望命中数(默认 1);实际 ≠ N → 非零退出且不写盘
|
|
15
|
+
// --allow-multi 显式声明允许多命中(等价 --expect 的宽松形态:N ≥ 1 即过,用于批量同串替换)
|
|
16
|
+
// --dry-run 只打印将发生的替换与校验预览,不写盘
|
|
17
|
+
// 测试:node .agents/scripts/rewrite-file.test.mjs
|
|
18
|
+
// 沿革:2026-10-12 立档(优先级 3:把「静默无操作」改成响亮失败)
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
|
|
22
|
+
const argv = process.argv.slice(2);
|
|
23
|
+
const argOf = (k) => {
|
|
24
|
+
const i = argv.indexOf(k);
|
|
25
|
+
return i >= 0 && i + 1 < argv.length ? argv[i + 1] : '';
|
|
26
|
+
};
|
|
27
|
+
const DRY = argv.includes('--dry-run');
|
|
28
|
+
const MULTI = argv.includes('--allow-multi');
|
|
29
|
+
let file = '';
|
|
30
|
+
let find = null;
|
|
31
|
+
let replace = null;
|
|
32
|
+
for (let i = 0; i < argv.length; i++) {
|
|
33
|
+
const a = argv[i];
|
|
34
|
+
if (a === '--find') find = argv[++i] ?? null;
|
|
35
|
+
else if (a === '--replace') replace = argv[++i] ?? null;
|
|
36
|
+
else if (a === '--expect') { /* 下面统一解析 */ }
|
|
37
|
+
else if (a === '--root') { /* 仅为与其它脚本口令一致,路径按 cwd 解析 */ i++; }
|
|
38
|
+
else if (a === '--dry-run' || a === '--allow-multi') { /* 旗标已处理 */ }
|
|
39
|
+
else if (!a.startsWith('--') && !file) file = a;
|
|
40
|
+
}
|
|
41
|
+
const expectRaw = argOf('--expect');
|
|
42
|
+
|
|
43
|
+
const USAGE = `用法:node .agents/scripts/rewrite-file.mjs <文件> --find <字面串> --replace <字面串> [--expect N] [--allow-multi] [--dry-run]`;
|
|
44
|
+
const die = (msg) => { console.error(`❌ rewrite-file: ${msg}\n${USAGE}`); process.exit(1); };
|
|
45
|
+
if (argv.includes('--help') || argv.includes('-h')) { console.log(USAGE); process.exit(0); }
|
|
46
|
+
if (!file) die('缺目标文件');
|
|
47
|
+
if (find === null || replace === null) die('--find 与 --replace 必须成对给出');
|
|
48
|
+
if (find === '') die('--find 不能为空串(空串会命中每一行,属误用)');
|
|
49
|
+
if (find === replace) die(`--find 与 --replace 相同(「${find.slice(0, 40)}」)——无操作却会报成功,正是本工具要堵的形态`);
|
|
50
|
+
let expect = 1;
|
|
51
|
+
if (expectRaw !== '') {
|
|
52
|
+
if (!/^\d+$/.test(expectRaw)) die(`--expect 须为非负整数(现 ${expectRaw})`);
|
|
53
|
+
expect = Number(expectRaw);
|
|
54
|
+
}
|
|
55
|
+
if (MULTI && expectRaw !== '') die('--allow-multi 与 --expect 互斥(一个宽松一个严格,同给无法裁决)');
|
|
56
|
+
|
|
57
|
+
const abs = path.resolve(process.cwd(), file);
|
|
58
|
+
if (!fs.existsSync(abs)) die(`目标文件不存在:${file}(不静默创建新文件——路径写错时应响亮失败)`);
|
|
59
|
+
let raw;
|
|
60
|
+
try { raw = fs.readFileSync(abs, 'utf8'); } catch (err) { die(`读取失败:${file}(${err.message})`); }
|
|
61
|
+
|
|
62
|
+
// ---- 按行处理 + EOL 保全 ----
|
|
63
|
+
const crlf = raw.includes('\r\n');
|
|
64
|
+
const eol = crlf ? '\r\n' : '\n';
|
|
65
|
+
const hadTrailingNewline = /\r?\n$/.test(raw);
|
|
66
|
+
const lines = raw.split(/\r?\n/);
|
|
67
|
+
// 末行是「 trailing newline 产生的空串」时不计入可替换行集合(避免把 EOF 空串当一行改)
|
|
68
|
+
const bodyLines = hadTrailingNewline ? lines.slice(0, -1) : lines;
|
|
69
|
+
|
|
70
|
+
let hits = 0;
|
|
71
|
+
const changedIdx = [];
|
|
72
|
+
const newBody = bodyLines.map((l, i) => {
|
|
73
|
+
if (!l.includes(find)) return l;
|
|
74
|
+
const n = l.split(find).length - 1;
|
|
75
|
+
hits += n;
|
|
76
|
+
changedIdx.push(i + 1);
|
|
77
|
+
return l.split(find).join(replace);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (hits === 0) {
|
|
81
|
+
console.error(`❌ rewrite-file: 0 命中——未写盘(文件保持原样)`);
|
|
82
|
+
console.error(` 文件:${file}`);
|
|
83
|
+
console.error(` 行尾:${crlf ? 'CRLF(仓库 autocrlf 检出态)' : 'LF'}|总行数:${bodyLines.length}`);
|
|
84
|
+
console.error(` 锚串(前 80 字符):${JSON.stringify(find.slice(0, 80))}`);
|
|
85
|
+
console.error(` 常见成因:① 锚串与文件实际内容不一致(空格/全角/标点);② 用 \\n 跨行锚串去匹配 CRLF 文件——本工具按行匹配,跨行锚请拆成单行锚或改用 Edit 工具;③ 文件已被前一次改动写过。`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
if (MULTI) {
|
|
89
|
+
if (hits < 1) die('内部不变式违反:hits < 1');
|
|
90
|
+
} else if (hits !== expect) {
|
|
91
|
+
console.error(`❌ rewrite-file: 命中数 ${hits} ≠ 期望 ${expect}——未写盘(文件保持原样)`);
|
|
92
|
+
console.error(` 文件:${file}|命中行号:${changedIdx.join(', ')}`);
|
|
93
|
+
console.error(` 含义:锚串不是唯一命中——要么目标不唯一(应加 --expect ${hits} 或 --allow-multi 明示),要么文件已被改过。`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const out = newBody.join(eol) + (hadTrailingNewline ? eol : '');
|
|
98
|
+
if (newBody.length !== bodyLines.length) die('内部不变式违反:行数发生变化');
|
|
99
|
+
|
|
100
|
+
if (DRY) {
|
|
101
|
+
console.log(`[dry-run] ${file}:将替换 ${hits} 处(行 ${changedIdx.join(', ')}),行尾 ${crlf ? 'CRLF' : 'LF'},未写盘`);
|
|
102
|
+
process.exit(0);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---- 原子写 ----
|
|
106
|
+
const tmp = `${abs}.rewrite-tmp`;
|
|
107
|
+
try {
|
|
108
|
+
fs.writeFileSync(tmp, out, 'utf8');
|
|
109
|
+
fs.renameSync(tmp, abs);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
try { fs.unlinkSync(tmp); } catch { /* tmp 可能尚未创建 */ }
|
|
112
|
+
die(`写入失败:${file}(${err.message})——原文件应保持原样,请核对后重试`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---- 写后回读校验(③):任一不过即非零退出,绝不打印成功 ----
|
|
116
|
+
const problems = [];
|
|
117
|
+
let back;
|
|
118
|
+
try { back = fs.readFileSync(abs, 'utf8'); } catch (err) { die(`回读失败:${file}(${err.message})——写入结果不可验证,请人工核对`); }
|
|
119
|
+
if (back !== out) problems.push('回读内容与拟写内容不一致(写入被截断或并发改动)');
|
|
120
|
+
if (back.includes(find)) problems.push('旧锚串仍存在于回读内容中(替换不完整)');
|
|
121
|
+
if (replace !== '' && !back.includes(replace)) problems.push('新锚串不在回读内容中');
|
|
122
|
+
const backLines = back.split(/\r?\n/);
|
|
123
|
+
const backBody = hadTrailingNewline ? backLines.slice(0, -1) : backLines;
|
|
124
|
+
if (backBody.length !== bodyLines.length) problems.push(`行数变化:${bodyLines.length} → ${backBody.length}`);
|
|
125
|
+
if (back.includes('\r\n') !== crlf) problems.push(`行尾形态变化:${crlf ? 'CRLF' : 'LF'} → ${back.includes('\r\n') ? 'CRLF' : 'LF'}`);
|
|
126
|
+
if (problems.length) {
|
|
127
|
+
console.error(`❌ rewrite-file: 写后回读校验未过——${file}`);
|
|
128
|
+
problems.forEach((p) => console.error(` - ${p}`));
|
|
129
|
+
console.error(' 现场已保留(不要手改),请核对后用 git 恢复或重新替换');
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log(`✅ rewrite-file: ${file} 替换 ${hits} 处(行 ${changedIdx.join(', ')})|行尾 ${crlf ? 'CRLF' : 'LF'} 保全|行数 ${bodyLines.length} 不变|回读校验通过`);
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// rewrite-file.test.mjs — 按行字面替换 + 响亮失败 fixture 套件(2026-10-12 silent-noop-loud-failure)
|
|
3
|
+
// 覆盖:CRLF 保全(papercut 2026-10-06 的静默无操作场景)/ 0 命中不写盘 / 命中数不符期望 /
|
|
4
|
+
// --allow-multi / --dry-run / 缺文件 fail-loud / find===replace 自伤防护 / 跨行锚串必失败 /
|
|
5
|
+
// 无尾换行保全 / 写后回读校验成功形态
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const SCRIPT = path.join(SCRIPT_DIR, 'rewrite-file.mjs');
|
|
14
|
+
|
|
15
|
+
let pass = 0;
|
|
16
|
+
let fail = 0;
|
|
17
|
+
const check = (desc, cond, detail = '') => {
|
|
18
|
+
if (cond) { pass++; console.log(`PASS ${desc}`); }
|
|
19
|
+
else { fail++; console.log(`FAIL ${desc}${detail ? '\n ' + detail : ''}`); }
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const run = (file, args) => {
|
|
23
|
+
const r = spawnSync(process.execPath, [SCRIPT, file, ...args], { encoding: 'utf8' });
|
|
24
|
+
return { code: r.status, out: `${r.stdout || ''}${r.stderr || ''}` };
|
|
25
|
+
};
|
|
26
|
+
const mk = (name, content) => {
|
|
27
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'rewrite-file-'));
|
|
28
|
+
const p = path.join(d, name);
|
|
29
|
+
fs.writeFileSync(p, content, 'utf8');
|
|
30
|
+
return p;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// T1:基本 LF 替换 → exit 0 + 内容正确 + 成功语含回读校验
|
|
34
|
+
{
|
|
35
|
+
const p = mk('lf.md', 'a\nANCHOR\nc\n');
|
|
36
|
+
const r = run(p, ['--find', 'ANCHOR', '--replace', 'NEW']);
|
|
37
|
+
const after = fs.readFileSync(p, 'utf8');
|
|
38
|
+
check('T1 基本 LF 替换 → exit 0 且内容正确', r.code === 0 && after === 'a\nNEW\nc\n', r.out);
|
|
39
|
+
check('T1b 成功语含「回读校验通过」', /回读校验通过/.test(r.out), r.out);
|
|
40
|
+
fs.rmSync(path.dirname(p), { recursive: true, force: true });
|
|
41
|
+
}
|
|
42
|
+
// T2:CRLF 保全——papercut 2026-10-06 的静默无操作场景,本工具必须真改且不坏行尾
|
|
43
|
+
{
|
|
44
|
+
const p = mk('crlf.md', 'line1\r\nANCHOR here\r\nline3\r\n');
|
|
45
|
+
const r = run(p, ['--find', 'ANCHOR', '--replace', 'NEW']);
|
|
46
|
+
const after = fs.readFileSync(p, 'utf8');
|
|
47
|
+
check('T2 CRLF 文件替换成功且行尾保全', r.code === 0 && after === 'line1\r\nNEW here\r\nline3\r\n', r.out);
|
|
48
|
+
check('T2b 未混入裸 LF(无 CRLF→LF 漂移)', !/[^\r]\n/.test(after), JSON.stringify(after));
|
|
49
|
+
fs.rmSync(path.dirname(p), { recursive: true, force: true });
|
|
50
|
+
}
|
|
51
|
+
// T3:0 命中 → exit 1 且文件逐字节不变(核心:绝不存在「以为改了」)
|
|
52
|
+
{
|
|
53
|
+
const before = 'x\r\nZZZ not here\r\n';
|
|
54
|
+
const p = mk('nohit.md', before);
|
|
55
|
+
const r = run(p, ['--find', 'QQQ', '--replace', 'Q']);
|
|
56
|
+
const after = fs.readFileSync(p, 'utf8');
|
|
57
|
+
check('T3 0 命中 → exit 1 且文件逐字节不变', r.code === 1 && after === before, r.out);
|
|
58
|
+
check('T3b 报错点名行尾与常见成因(CRLF 检出态)', /0 命中/.test(r.out) && /CRLF/.test(r.out) && /跨行锚/.test(r.out), r.out);
|
|
59
|
+
fs.rmSync(path.dirname(p), { recursive: true, force: true });
|
|
60
|
+
}
|
|
61
|
+
// T4:命中数与期望不符 → exit 1 且不写盘(锚串非唯一时必须响亮,不可顺手全改)
|
|
62
|
+
{
|
|
63
|
+
const before = 'k\nk\n';
|
|
64
|
+
const p = mk('multi.md', before);
|
|
65
|
+
const r = run(p, ['--find', 'k', '--replace', 'K']);
|
|
66
|
+
const after = fs.readFileSync(p, 'utf8');
|
|
67
|
+
check('T4 命中 2 ≠ 期望 1 → exit 1 且未写盘', r.code === 1 && after === before && /命中数 2 ≠ 期望 1/.test(r.out), r.out);
|
|
68
|
+
fs.rmSync(path.dirname(p), { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
// T5:显式 --expect 2 → 过(同一行两处命中也按处计)
|
|
71
|
+
{
|
|
72
|
+
const p = mk('expect2.md', 'k k\n');
|
|
73
|
+
const r = run(p, ['--find', 'k', '--replace', 'K', '--expect', '2']);
|
|
74
|
+
const after = fs.readFileSync(p, 'utf8');
|
|
75
|
+
check('T5 --expect 2 且命中 2 → exit 0 且两处都改', r.code === 0 && after === 'K K\n', r.out);
|
|
76
|
+
fs.rmSync(path.dirname(p), { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
// T6:--allow-multi 多命中 → 过(宽松形态,明示批量)
|
|
79
|
+
{
|
|
80
|
+
const p = mk('allowmulti.md', 'k\nk\nk\n');
|
|
81
|
+
const r = run(p, ['--find', 'k', '--replace', 'K', '--allow-multi']);
|
|
82
|
+
const after = fs.readFileSync(p, 'utf8');
|
|
83
|
+
check('T6 --allow-multi 三命中 → exit 0 且全改', r.code === 0 && after === 'K\nK\nK\n', r.out);
|
|
84
|
+
fs.rmSync(path.dirname(p), { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
// T7:--allow-multi 与 --expect 互斥 → exit 1(一宽一严无法裁决)
|
|
87
|
+
{
|
|
88
|
+
const p = mk('conflict.md', 'k\n');
|
|
89
|
+
const r = run(p, ['--find', 'k', '--replace', 'K', '--allow-multi', '--expect', '1']);
|
|
90
|
+
check('T7 --allow-multi 与 --expect 互斥 → exit 1', r.code === 1 && /互斥/.test(r.out), r.out);
|
|
91
|
+
fs.rmSync(path.dirname(p), { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
// T8:--dry-run → 不写盘
|
|
94
|
+
{
|
|
95
|
+
const p = mk('dry.md', 'a\nANCHOR\n');
|
|
96
|
+
const r = run(p, ['--find', 'ANCHOR', '--replace', 'NEW', '--dry-run']);
|
|
97
|
+
const after = fs.readFileSync(p, 'utf8');
|
|
98
|
+
check('T8 --dry-run → exit 0 且未写盘', r.code === 0 && after === 'a\nANCHOR\n' && /dry-run/.test(r.out), r.out);
|
|
99
|
+
fs.rmSync(path.dirname(p), { recursive: true, force: true });
|
|
100
|
+
}
|
|
101
|
+
// T9:目标文件不存在 → exit 1 fail-loud(不静默创建新文件)
|
|
102
|
+
{
|
|
103
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'rewrite-file-'));
|
|
104
|
+
const r = run(path.join(d, 'nope.md'), ['--find', 'a', '--replace', 'b']);
|
|
105
|
+
check('T9 文件不存在 → exit 1 且不创建文件', r.code === 1 && /不存在/.test(r.out) && !fs.existsSync(path.join(d, 'nope.md')), r.out);
|
|
106
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
// T10:find === replace → exit 1(无操作却报成功,正是要堵的形态)
|
|
109
|
+
{
|
|
110
|
+
const p = mk('same.md', 'a\nSAME\n');
|
|
111
|
+
const r = run(p, ['--find', 'SAME', '--replace', 'SAME']);
|
|
112
|
+
check('T10 find === replace → exit 1', r.code === 1 && /相同/.test(r.out), r.out);
|
|
113
|
+
fs.rmSync(path.dirname(p), { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
// T11:跨行锚串(含 \n)→ 0 命中 exit 1——把「\n 锚串匹配 CRLF 文件静默无操作」变成响亮失败
|
|
116
|
+
{
|
|
117
|
+
const p = mk('crossline.md', 'aaa\r\nbbb\r\n');
|
|
118
|
+
const r = run(p, ['--find', 'aaa\nbbb', '--replace', 'xxx']);
|
|
119
|
+
const after = fs.readFileSync(p, 'utf8');
|
|
120
|
+
check('T11 跨行锚串 → exit 1 且文件不变(提示拆单行锚)', r.code === 1 && after === 'aaa\r\nbbb\r\n' && /跨行锚/.test(r.out), r.out);
|
|
121
|
+
fs.rmSync(path.dirname(p), { recursive: true, force: true });
|
|
122
|
+
}
|
|
123
|
+
// T12:无尾换行保全
|
|
124
|
+
{
|
|
125
|
+
const p = mk('notrailing.md', 'a\nANCHOR');
|
|
126
|
+
const r = run(p, ['--find', 'ANCHOR', '--replace', 'NEW']);
|
|
127
|
+
const after = fs.readFileSync(p, 'utf8');
|
|
128
|
+
check('T12 原文件无尾换行 → 替换后仍无尾换行', r.code === 0 && after === 'a\nNEW', JSON.stringify(after));
|
|
129
|
+
fs.rmSync(path.dirname(p), { recursive: true, force: true });
|
|
130
|
+
}
|
|
131
|
+
// T13:空 --find → exit 1(空串会命中每一行,属误用)
|
|
132
|
+
{
|
|
133
|
+
const p = mk('emptyfind.md', 'a\n');
|
|
134
|
+
const r = run(p, ['--find', '', '--replace', 'b']);
|
|
135
|
+
check('T13 空 --find → exit 1', r.code === 1 && /不能为空串/.test(r.out), r.out);
|
|
136
|
+
fs.rmSync(path.dirname(p), { recursive: true, force: true });
|
|
137
|
+
}
|
|
138
|
+
// T14:同行的两处命中按处计数,且 --expect 3 精确匹配(处计数语义显式化)
|
|
139
|
+
{
|
|
140
|
+
const p = mk('twiceinline.md', 'k-k-k\n');
|
|
141
|
+
const r = run(p, ['--find', 'k', '--replace', 'K', '--expect', '3']);
|
|
142
|
+
const after = fs.readFileSync(p, 'utf8');
|
|
143
|
+
check('T14 同行多处按处计数(3 处)→ exit 0', r.code === 0 && after === 'K-K-K\n', r.out);
|
|
144
|
+
fs.rmSync(path.dirname(p), { recursive: true, force: true });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
148
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 常驻面体积预算检查(单源:本脚本 + .agents/rule-budgets.txt;触发点:check-hygiene 检查 13(--all,WARN)、
|
|
3
|
+
// .githooks/pre-commit(--staged,硬拦))
|
|
4
|
+
// 自 sh 版移植 node(2026-10-08 ci 去 sh 化;2026-10-13 自 Shipyard.Material 参考实现回灌)——CI agent(Windows)
|
|
5
|
+
// 免 git-bash 依赖,check-loop 检查 13 从「无 sh 静默跳过」变恒可跑。语义逐条对齐 sh 版:
|
|
6
|
+
// --all 查「提交后树」全量;--staged 仅查本次暂存触及的条目 + 触及目录的合计
|
|
7
|
+
// 退出码 0 通过 / 1 超限(违例打到 stdout,调用方决定警告还是阻断);缺预算表 exit 0(fixture 沙箱不拦——既有语义)
|
|
8
|
+
// 「提交后树」= 索引(index)内容:staged 版优先于 HEAD;无 git(fixture)回退工作区文件。
|
|
9
|
+
// 字节口径 = 索引 / HEAD 内容(git cat-file,LF 归一)。禁用工作区 wc -c 口径(CRLF 膨胀系统性偏大,
|
|
10
|
+
// 2026-09-21 实测差 ~500B 一度误判超限)——本脚本所有字节数都走 cat-file,仅无 git 时才落工作区。
|
|
11
|
+
import { spawnSync } from 'node:child_process';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
const mode = process.argv[2] === '--staged' ? '--staged' : '--all';
|
|
16
|
+
|
|
17
|
+
const git = (args) => {
|
|
18
|
+
const r = spawnSync('git', args, { encoding: 'utf8' });
|
|
19
|
+
return { ok: !r.error && r.status === 0, out: (r.stdout || '').trim() };
|
|
20
|
+
};
|
|
21
|
+
const root = git(['rev-parse', '--show-toplevel']);
|
|
22
|
+
if (root.ok) process.chdir(root.out); // 非 git 环境(fixture 沙箱)留在原地
|
|
23
|
+
|
|
24
|
+
const BUDGETS = '.agents/rule-budgets.txt';
|
|
25
|
+
if (!fs.existsSync(BUDGETS)) process.exit(0);
|
|
26
|
+
|
|
27
|
+
// 「提交后」字节数:索引 → HEAD → 工作区
|
|
28
|
+
const fsize = (f) => {
|
|
29
|
+
if (git(['cat-file', '-e', `:${f}`]).ok) return Number(git(['cat-file', '-s', `:${f}`]).out);
|
|
30
|
+
if (git(['cat-file', '-e', `HEAD:${f}`]).ok) return Number(git(['cat-file', '-s', `HEAD:${f}`]).out);
|
|
31
|
+
try { return fs.readFileSync(f).length; } catch { return 0; }
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const staged = mode === '--staged'
|
|
35
|
+
? git(['-c', 'core.quotepath=off', 'diff', '--cached', '--name-only']).out.split('\n').filter(Boolean)
|
|
36
|
+
: [];
|
|
37
|
+
|
|
38
|
+
// intentional-simple: glob 仅支持预算表在用的两形态——「固定目录 + * 后缀」(.agents/commands/*.md)
|
|
39
|
+
// 与目录本身(/ 结尾,展开一层直接子文件);表出现更复杂 glob 时再升级
|
|
40
|
+
const expand = (pat) => {
|
|
41
|
+
if (pat.endsWith('/')) {
|
|
42
|
+
const dir = pat.replace(/\/$/, '');
|
|
43
|
+
if (!fs.existsSync(dir)) return [];
|
|
44
|
+
return fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile())
|
|
45
|
+
.map((e) => path.join(dir, e.name));
|
|
46
|
+
}
|
|
47
|
+
if (pat.includes('*')) {
|
|
48
|
+
const i = pat.lastIndexOf('/');
|
|
49
|
+
const dir = i < 0 ? '.' : pat.slice(0, i);
|
|
50
|
+
const name = pat.slice(i + 1);
|
|
51
|
+
const star = name.indexOf('*');
|
|
52
|
+
const pre = name.slice(0, star), suf = name.slice(star + 1);
|
|
53
|
+
if (!fs.existsSync(dir)) return [];
|
|
54
|
+
return fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile()
|
|
55
|
+
&& e.name.startsWith(pre) && e.name.endsWith(suf))
|
|
56
|
+
.map((e) => path.join(dir, e.name));
|
|
57
|
+
}
|
|
58
|
+
return fs.existsSync(pat) ? [pat] : [];
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
let viol = 0;
|
|
62
|
+
const over = (what, bytes, lim) => { console.log(`常驻面超限:${what} ${bytes}B > 上限 ${lim}B`); viol = 1; };
|
|
63
|
+
|
|
64
|
+
for (const line of fs.readFileSync(BUDGETS, 'utf8').split(/\r?\n/)) {
|
|
65
|
+
const sp = line.lastIndexOf(' ');
|
|
66
|
+
if (sp < 0) continue;
|
|
67
|
+
const pat = line.slice(0, sp), lim = line.slice(sp + 1);
|
|
68
|
+
if (pat === '' || pat.startsWith('#')) continue;
|
|
69
|
+
if (!/^\d+$/.test(lim)) continue;
|
|
70
|
+
if (pat.endsWith('/')) {
|
|
71
|
+
// 目录合计:--staged 下仅当暂存触及该目录内文件才查(字面前缀匹配,同 sh 版 2026-09-24 口径)
|
|
72
|
+
if (mode === '--staged' && !staged.some((sf) => sf.startsWith(pat))) continue;
|
|
73
|
+
const total = expand(pat).reduce((s, f) => s + fsize(f), 0);
|
|
74
|
+
if (total > Number(lim)) over(`${pat} 合计`, total, lim);
|
|
75
|
+
} else {
|
|
76
|
+
for (const f of expand(pat)) {
|
|
77
|
+
if (mode === '--staged' && !staged.includes(f.replace(/\\/g, '/'))) continue;
|
|
78
|
+
const b = fsize(f);
|
|
79
|
+
if (b > Number(lim)) over(f, b, lim);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
process.exit(viol ? 1 : 0);
|