agentic-flow-kit 1.7.15 → 1.7.16
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 +41 -3
- package/modules/hosts/claude/commands/wf-design.md +5 -4
- package/modules/hosts/claude/commands/wf-new-task.md +42 -21
- package/modules/hosts/claude/commands/wf-review.md +1 -1
- package/modules/hosts/claude/commands/wf-sync-hosts.md +7 -13
- package/modules/hosts/claude/commands/wf-test.md +1 -1
- package/modules/hosts/codex/commands/wf-design.md +5 -4
- package/modules/hosts/codex/commands/wf-new-task.md +42 -21
- package/modules/hosts/codex/commands/wf-review.md +1 -1
- package/modules/hosts/codex/commands/wf-sync-hosts.md +7 -13
- package/modules/hosts/codex/commands/wf-test.md +1 -1
- package/modules/hosts/cursor/commands/wf-design.md +5 -4
- package/modules/hosts/cursor/commands/wf-new-task.md +42 -21
- package/modules/hosts/cursor/commands/wf-review.md +1 -1
- package/modules/hosts/cursor/commands/wf-sync-hosts.md +7 -13
- package/modules/hosts/cursor/commands/wf-test.md +1 -1
- package/modules/hosts/opencode/commands/wf-design.md +5 -4
- package/modules/hosts/opencode/commands/wf-new-task.md +42 -21
- package/modules/hosts/opencode/commands/wf-review.md +1 -1
- package/modules/hosts/opencode/commands/wf-sync-hosts.md +7 -13
- package/modules/hosts/opencode/commands/wf-test.md +1 -1
- package/modules/hosts/trae/commands/wf-design.md +5 -4
- package/modules/hosts/trae/commands/wf-new-task.md +42 -21
- package/modules/hosts/trae/commands/wf-review.md +1 -1
- package/modules/hosts/trae/commands/wf-sync-hosts.md +7 -13
- package/modules/hosts/trae/commands/wf-test.md +1 -1
- package/package.json +1 -1
- package/src/cli.mjs +43 -4
- package/src/doctor.mjs +37 -1
- package/src/executor.mjs +245 -0
- package/src/executor.test.mjs +292 -0
- package/src/init.mjs +7 -0
- package/src/profiles.mjs +3 -0
- package/src/render.mjs +3 -3
- package/src/sync-hosts.mjs +37 -43
- package/src/sync-hosts.test.mjs +67 -56
- package/templates/.gitattributes +8 -0
- package/templates/AGENTS.md +1 -1
- package/templates/_agents/commands/design.md +5 -4
- package/templates/_agents/commands/new-task.md +42 -21
- package/templates/_agents/commands/review.md +1 -1
- package/templates/_agents/commands/sync-hosts.md +7 -13
- package/templates/_agents/commands/test.md +1 -1
- package/templates/_agents/control-surfaces.txt +1 -1
- package/templates/_agents/engine-lock.json +246 -0
- package/templates/_agents/lane-surfaces.txt +1 -1
- package/templates/_agents/level-hints.txt +30 -0
- package/templates/_agents/scripts/agg-delegations.test.mjs +5 -3
- package/templates/_agents/scripts/check-engine-integrity.mjs +55 -126
- package/templates/_agents/scripts/check-engine-integrity.test.mjs +33 -45
- package/templates/_agents/scripts/check-lane-surface.mjs +72 -23
- package/templates/_agents/scripts/check-lane-surface.test.mjs +66 -0
- package/templates/_agents/scripts/check-ledger-invariant.mjs +112 -14
- package/templates/_agents/scripts/check-ledger-invariant.test.mjs +5 -1
- package/templates/_agents/scripts/check-loop-rev.test.mjs +6 -4
- package/templates/_agents/scripts/check-loop.mjs +49 -41
- package/templates/_agents/scripts/check-loop.test.mjs +18 -13
- package/templates/_agents/scripts/check-metric-claims.mjs +4 -2
- 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 +40 -11
- package/templates/_agents/scripts/doctor.test.mjs +44 -1
- package/templates/_agents/scripts/edit-face-check.test.mjs +5 -4
- package/templates/_agents/scripts/fill-intent.mjs +2 -2
- package/templates/_agents/scripts/fixture-scripts.mjs +153 -0
- package/templates/_agents/scripts/fixture-scripts.test.mjs +107 -0
- package/templates/_agents/scripts/gate-checklist.mjs +4 -9
- package/templates/_agents/scripts/gate-checklist.test.mjs +2 -11
- package/templates/_agents/scripts/gen-workflow-dashboard.mjs +8 -11
- package/templates/_agents/scripts/gen-workflow-metrics.mjs +9 -33
- 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/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-ts-monotonic.test.mjs +135 -0
- package/templates/_agents/scripts/level-criteria.mjs +232 -0
- package/templates/_agents/scripts/level-criteria.test.mjs +279 -0
- package/templates/_agents/scripts/pipeline-run.mjs +4 -2
- package/templates/_agents/scripts/policy.mjs +47 -0
- package/templates/_agents/scripts/stage-gates.mjs +4 -9
- package/templates/_agents/scripts/wf-journal.test.mjs +1 -1
- package/templates/_agents/scripts/wiki-search.test.mjs +5 -6
- package/templates/_agents/scripts/workflows-check.mjs +1 -1
- package/templates/_agents/workflows/pipeline-closing.md +4 -5
- package/templates/_githooks/pre-commit +3 -34
- package/templates/_githooks/pre-push +3 -3
- package/templates/workflow/README.md +11 -0
- 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/scripts/check-ledger.mjs +0 -109
- package/templates/_agents/scripts/check-ledger.test.mjs +0 -199
- 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
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: 装户面同步一致性检查(包源 templates/_agents/ vs 装副本 .agents/ 三类差异:缺失 / 孤儿 / 漂移)
|
|
3
|
-
stage: helper(横向工具,不占 6 阶段)
|
|
4
|
-
triggers:
|
|
5
|
-
- "改了包源同步了吗"
|
|
6
|
-
- "装了装副本同步了吗"
|
|
7
|
-
- "扫一下装户面漂移"
|
|
8
|
-
- "source-sync 跑一下"
|
|
9
|
-
delegation:
|
|
10
|
-
- role: implementer
|
|
11
|
-
instructions: .agents/roles/implementer.md
|
|
12
|
-
when: 用户已确认同步范围(发现漂移后修包源或装副本时)
|
|
13
|
-
required: false
|
|
14
|
-
fallback: main
|
|
15
|
-
approval_required: true
|
|
16
|
-
next: .agents/commands/build.md(漂移属 build 改包源未同步导致的,修复后回 build)
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
> **装户边界**:本文档中 `src/`、`bin/flow-kit.mjs`、`templates/_agents/`、`modules/hosts/` 为 kit 包源仓视角引用——装户仓(无 `templates/` 包源)对应命令不适用,本仓以 `.agents/` 装副本为准。
|
|
20
|
-
|
|
21
|
-
# Source-Sync-Check · 装户面同步一致性
|
|
22
|
-
|
|
23
|
-
> `templates/_agents/` 包源 vs `.agents/` 装副本——三类差异报告:缺失 / 孤儿 / 漂移。
|
|
24
|
-
> managed 文件由 `flow-kit sync` 跟踪;owned 文件由 `flow-kit doctor` §6.6 校验——本工具补"两者都没扫"的真空地带(结构级差异)。
|
|
25
|
-
|
|
26
|
-
## 1. 何时跑
|
|
27
|
-
|
|
28
|
-
- 改 `templates/_agents/*` 包源后
|
|
29
|
-
- 怀疑装副本未跟包源更新
|
|
30
|
-
- 体检装副本完整性(清理前 / 发版前)
|
|
31
|
-
- 不跑:仅改装副本 `.agents/*`(B-b 决策「只报告」+ 不反向同步)
|
|
32
|
-
|
|
33
|
-
## 2. 怎么跑
|
|
34
|
-
|
|
35
|
-
### 2.1 默认 `--diff`(仅报告)
|
|
36
|
-
|
|
37
|
-
```
|
|
38
|
-
node .agents/scripts/source-sync-check.mjs --diff
|
|
39
|
-
node .agents/scripts/source-sync-check.mjs --json # 机器可读
|
|
40
|
-
node .agents/scripts/source-sync-check.mjs --pkg-root <path> --target <path> # 自定义路径
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
输出三类:
|
|
44
|
-
- **缺失**(missing)——包源有 / 装副本无(如新增文件 init 未执行)
|
|
45
|
-
- **孤儿**(orphan)——包源无 / 装副本有(如历史残留 / 用户本地添加)
|
|
46
|
-
- **漂移**(drift)——两者都有但 sha 不一致(如包源改了装副本未跟)
|
|
47
|
-
|
|
48
|
-
**排除**:
|
|
49
|
-
- `templates/_agents/cache/`(运行时缓存,与 `flow-kit sync` 既有约定一致)
|
|
50
|
-
- `.agents/kit.json` / `.agents/settings.json`(init 渲染产物,不属双源)
|
|
51
|
-
|
|
52
|
-
### 2.2 输出示例
|
|
53
|
-
|
|
54
|
-
```
|
|
55
|
-
▶ flow-kit source-sync-check --diff
|
|
56
|
-
包源 (templates/_agents/) 49 份 | 装副本 (.agents/) 49 份
|
|
57
|
-
缺失(包源有 / 装副本 无)1:
|
|
58
|
-
- scripts/source-sync-check.mjs
|
|
59
|
-
孤儿(包源无 / 装副本 有)1:
|
|
60
|
-
- hooks/commit-check.config.json
|
|
61
|
-
漂移(包源装副本都有但 sha 不一致)3:
|
|
62
|
-
- commands/maintain.md pkg=3782b695… tgt=26203f7b…
|
|
63
|
-
- notes/runtime-env.md pkg=aedde1ba… tgt=ad08441e…
|
|
64
|
-
- scripts/ensure-board.mjs pkg=e66653b0… tgt=b6f3cc9b…
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### 2.3 自验
|
|
68
|
-
|
|
69
|
-
```
|
|
70
|
-
node .agents/scripts/source-sync-check.test.mjs # 11 场景全过
|
|
71
|
-
npm test # 含 source-sync-check 套件
|
|
72
|
-
flow-kit doctor # 10/0/0
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
## 3. 边界(不要做)
|
|
76
|
-
|
|
77
|
-
- 不要自动化同步(B-b 决策「只报告不修复」——与 `gate-checklist` 同源)
|
|
78
|
-
- 不改 managed 台账跟踪逻辑(`sync.mjs` 不动)
|
|
79
|
-
- 不改 owned 漂移校验(`doctor.mjs` §6.6 不动)
|
|
80
|
-
- 不反向同步——装副本独自有文件(如 hooks/commit-check.config.json)可能是装户配置,不应被删
|
|
81
|
-
- 不检查注册表内宿主 `modules/hosts/` 薄适配漂移(那是 `flow-kit sync-hosts` 范围)
|
|
82
|
-
|
|
83
|
-
## 4. 当前已识别的真实漂点(首次实测,2026-09-25)
|
|
84
|
-
|
|
85
|
-
- **缺失**:本工具写完后未同步装副本(预期内——随本次 commit 解决)
|
|
86
|
-
- **孤儿**:`.agents/hooks/commit-check.config.json`(装副本独有,疑似装户配置)
|
|
87
|
-
- **漂移**:`commands/maintain.md` / `notes/runtime-env.md` / `scripts/ensure-board.mjs`
|
|
88
|
-
|
|
89
|
-
修复留后续 incident / intent 跟踪(不在本次范围)。
|
|
90
|
-
|
|
91
|
-
## 5. 与既有工具的关系
|
|
92
|
-
|
|
93
|
-
| 工具 | 作用域 | 算法 |
|
|
94
|
-
|---|---|---|
|
|
95
|
-
| `flow-kit sync` | managed 文件(kit.json 台账跟踪)| 三态覆盖 + 加新 + 报错缺 |
|
|
96
|
-
| `flow-kit doctor` §6.6 | owned 文件(kit.owned 哈希比对)| 盘面 sha vs 台账哈希 |
|
|
97
|
-
| **`flow-kit source-sync-check`** | **所有文件结构(包源 ↔ 装副本全集)** | **sha 全量比对 + 缺失/孤儿分类** |
|
|
98
|
-
|
|
99
|
-
三者互补,不重叠。
|
|
100
|
-
|
|
101
|
-
## 6. 相关留痕
|
|
102
|
-
|
|
103
|
-
- 立法出处:2026-09-25 source-sync-check(intent/plan;父事件 = 包源改了装副本未同步,本工具预防性体检)——留痕在包源仓 git 历史与 CHANGELOG,装户径不引实例路径
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
// check-ledger.mjs — managed 台账快检(2026-09-28 ledger-precommit-gate):kit.json 台账 sha ↔ 盘面工作树
|
|
2
|
-
// 补 pre-commit 盲区:双源门禁(source-sync-check)查 templates↔.agents 正文,两侧成对回退时静默;
|
|
3
|
-
// doctor §4 查台账↔盘面但只在手动全量跑——「台账 sha 预 landing」(台账记了盘面尚不存在的内容 sha)
|
|
4
|
-
// 随常规提交静默落 main,三次真实发生(0272a1c 预入 / ff3c721 还原窗口 / 537df18 闭合),本脚本在
|
|
5
|
-
// 提交时刻补齐第三边。sha 口径:LF 归一 utf8(同 doctor §4 / render.mjs shaText / source-sync-check
|
|
6
|
-
// 读取侧,跨 checkout 稳定;装户自包含约束决定独立实现,三处口径改动须同批)。
|
|
7
|
-
// 入库态口径(2026-10-08 papercuts-cleanup-batch 修 5,独立复核 P1 修正取值链):快检原比「工作树」,
|
|
8
|
-
// 而分批提交时提交树(= HEAD + 暂存合成,删除同样被应用)里的 kit.json sha 可能指向「工作区已改、
|
|
9
|
-
// 未提交」内容 → clone/CI 跑 doctor 必红(e8445c5 实证 6 项不符)。无参运行自适应:**暂存区非空即
|
|
10
|
-
// 入库态**(不判「提交集含 kit.json」——diff --cached 只列与 HEAD 有差异的文件,kit.json 无变化暂存
|
|
11
|
-
// 时不出现,但其合成提交树同样可能预 landing:只 add 件不 add 台账是姊妹漏洞)。入库态取值:
|
|
12
|
-
// 台账清单 = 暂存版 kit.json(`git show :.agents/kit.json`);暂存无而 HEAD 有 = 退出性提交(kit.json
|
|
13
|
-
// 被暂存删除)放行;managed 内容 = `git show :rel` 暂存版,**暂存无即 gone**(提交树=index——
|
|
14
|
-
// 未暂存改动的文件 index 仍在,`:rel` 必命中;`:rel` 缺失唯一可达路径是暂存删除,HEAD 兜底会把它
|
|
15
|
-
// 误读为「未触碰」放行 = 提交门绕过,复核 P1 实证)。
|
|
16
|
-
// 暂存区空(amend 等罕见态)→ 维持工作树口径零变化。
|
|
17
|
-
// 边界:仅包源环境由 .githooks/pre-commit 触发(装户 managed 手改走 doctor §4 WARN 口径,不硬拦);
|
|
18
|
-
// managed 删除即拦——sync 对 removed 只报不删,删件须手动同步装副本并重跑 sync 刷台账。
|
|
19
|
-
// 用法:node .agents/scripts/check-ledger.mjs(cwd = 仓库根;非零出口即阻断提交;无参自适应两口径)
|
|
20
|
-
// 单元测试:import { ledgerDrift } from './check-ledger.mjs'({staged:true} 显式入库态)
|
|
21
|
-
import fs from 'node:fs';
|
|
22
|
-
import path from 'node:path';
|
|
23
|
-
import { createHash } from 'node:crypto';
|
|
24
|
-
import { spawnSync } from 'node:child_process';
|
|
25
|
-
|
|
26
|
-
const gitText = (target, args) => {
|
|
27
|
-
const r = spawnSync('git', args, { cwd: target, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 });
|
|
28
|
-
return r.status === 0 ? r.stdout : null;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export function ledgerDrift(target = process.cwd(), { staged = false } = {}) {
|
|
32
|
-
const kitPath = path.join(target, '.agents', 'kit.json');
|
|
33
|
-
if (!fs.existsSync(kitPath)) return { skipped: true, note: '无 .agents/kit.json(非 flow-kit 安装)' };
|
|
34
|
-
// staged 模式:台账清单取暂存版 kit.json(提交树=index 合成语义);暂存无而 HEAD 有 = 退出性提交
|
|
35
|
-
// (kit.json 被暂存删除)放行;两者皆无(新仓无 kit.json)同样放行
|
|
36
|
-
let kit = null;
|
|
37
|
-
if (staged) {
|
|
38
|
-
const stagedKit = gitText(target, ['show', ':.agents/kit.json']);
|
|
39
|
-
const headKit = stagedKit === null ? gitText(target, ['show', 'HEAD:.agents/kit.json']) : null;
|
|
40
|
-
if (stagedKit === null) {
|
|
41
|
-
return { skipped: true, note: headKit !== null ? '提交树无 .agents/kit.json(退出 flow-kit 的提交),跳过' : '提交树无 .agents/kit.json(新仓),跳过', mode: 'staged' };
|
|
42
|
-
}
|
|
43
|
-
try {
|
|
44
|
-
kit = JSON.parse(stagedKit);
|
|
45
|
-
} catch (e) {
|
|
46
|
-
return { skipped: false, fatal: `kit.json(暂存版)解析失败:${e.message}`, modified: [], gone: [], total: 0, version: '?', mode: 'staged' };
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
if (!kit) {
|
|
50
|
-
try {
|
|
51
|
-
kit = JSON.parse(fs.readFileSync(kitPath, 'utf8'));
|
|
52
|
-
} catch (e) {
|
|
53
|
-
return { skipped: false, fatal: `kit.json 解析失败:${e.message}`, modified: [], gone: [], total: 0, version: '?', mode: staged ? 'staged' : 'worktree' };
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
if (!Array.isArray(kit.managed)) {
|
|
57
|
-
// fail-loud(独立复核 P2-3,2026-09-28):JSON 合法但 managed 缺失/非数组时静默「全对齐」= 假放,
|
|
58
|
-
// 同 doctor §4 的 Array.isArray 显式判口径,台账结构坏了必须响
|
|
59
|
-
return { skipped: false, fatal: 'kit.json managed 缺失或非数组', modified: [], gone: [], total: 0, version: kit.version, mode: staged ? 'staged' : 'worktree' };
|
|
60
|
-
}
|
|
61
|
-
const modified = [];
|
|
62
|
-
const gone = [];
|
|
63
|
-
const managed = kit.managed;
|
|
64
|
-
for (const f of managed) {
|
|
65
|
-
let content = null;
|
|
66
|
-
if (staged) {
|
|
67
|
-
// 入库态:提交树 = index(暂存版);`:rel` 缺失唯一可达路径是暂存删除 → gone 拦(复核 P1:
|
|
68
|
-
// HEAD 兜底会把暂存删除误读为「未触碰」放行——提交树已无该件而台账仍跟踪,clone/CI 必红)
|
|
69
|
-
content = gitText(target, ['show', `:${f.rel}`]);
|
|
70
|
-
if (content === null) { gone.push(f.rel); continue; }
|
|
71
|
-
} else {
|
|
72
|
-
const p = path.join(target, f.rel);
|
|
73
|
-
if (!fs.existsSync(p)) { gone.push(f.rel); continue; }
|
|
74
|
-
content = fs.readFileSync(p, 'utf8');
|
|
75
|
-
}
|
|
76
|
-
const h = createHash('sha256').update(content.replace(/\r\n/g, '\n'), 'utf8').digest('hex'); // LF 归一,同 doctor §4
|
|
77
|
-
if (h !== f.sha256) modified.push(f.rel);
|
|
78
|
-
}
|
|
79
|
-
return { skipped: false, fatal: '', modified, gone, total: managed.length, version: kit.version, mode: staged ? 'staged' : 'worktree' };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const isMain = process.argv[1] && process.argv[1].endsWith('check-ledger.mjs');
|
|
83
|
-
if (isMain) {
|
|
84
|
-
// 无参自适应:暂存区非空(有提交发生)→ 入库态口径;暂存区空(amend 等罕见态/非 git 仓)→ 工作树口径
|
|
85
|
-
const cached = gitText(process.cwd(), ['diff', '--cached', '--name-only']) || '';
|
|
86
|
-
const staged = cached.split(/\r?\n/).some((l) => l.trim() !== '');
|
|
87
|
-
const r = ledgerDrift(process.cwd(), { staged });
|
|
88
|
-
console.log('▶ flow-kit check-ledger');
|
|
89
|
-
if (r.skipped) {
|
|
90
|
-
console.log(' ' + r.note + ',跳过 ✅');
|
|
91
|
-
process.exit(0);
|
|
92
|
-
}
|
|
93
|
-
console.log(' kit.json v' + r.version + ':managed ' + r.total + ' 份,台账 ↔ ' + (r.mode === 'staged' ? '提交树入库态(暂存版=index,暂存删除即拦,LF 归一)' : '盘面(工作树,LF 归一)'));
|
|
94
|
-
if (r.fatal) console.error('❌ ' + r.fatal);
|
|
95
|
-
if (r.modified.length) {
|
|
96
|
-
console.error(' 台账漂移(盘面 ≠ 台账 sha)' + r.modified.length + ':');
|
|
97
|
-
for (const rel of r.modified) console.error(' - ' + rel);
|
|
98
|
-
}
|
|
99
|
-
if (r.gone.length) {
|
|
100
|
-
console.error(' managed 缺失 ' + r.gone.length + ':');
|
|
101
|
-
for (const rel of r.gone) console.error(' - ' + rel);
|
|
102
|
-
}
|
|
103
|
-
if (r.fatal || r.modified.length || r.gone.length) {
|
|
104
|
-
console.error(' 提交被拦:kit.json 与' + (r.mode === 'staged' ? '提交树(暂存面)' : '盘面') + '不一致(预 landing / 手改未随动)——跑 node bin/flow-kit.mjs sync 成对后原路重试');
|
|
105
|
-
process.exit(1);
|
|
106
|
-
}
|
|
107
|
-
console.log(' 全对齐 ✅');
|
|
108
|
-
process.exit(0);
|
|
109
|
-
}
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
// check-ledger.test.mjs — managed 台账快检测试(2026-09-28 ledger-precommit-gate)
|
|
2
|
-
// 方法:fixture 临时目录 .agents/kit.json + managed 件,调 ledgerDrift() + spawnSync CLI 出口码;
|
|
3
|
-
// 真实仓库 baseline(包源环境全对齐;装户环境 SKIP 不计失败,沿 source-sync-check 先例)
|
|
4
|
-
// 判据:S1 全对齐 S2 内容漂移逐份列出 S3 缺失 S4 CRLF×LF 归一不误报 S5 无 kit.json skip
|
|
5
|
-
// S6 kit.json 解析失败 fatal S7/S8 CLI 出口码 0/1 S9 真实仓 baseline
|
|
6
|
-
// 用法:node templates/_agents/scripts/check-ledger.test.mjs(装副本直跑亦然)
|
|
7
|
-
import fs from 'node:fs';
|
|
8
|
-
import path from 'node:path';
|
|
9
|
-
import os from 'node:os';
|
|
10
|
-
import { spawnSync } from 'node:child_process';
|
|
11
|
-
import { fileURLToPath } from 'node:url';
|
|
12
|
-
import { createHash } from 'node:crypto';
|
|
13
|
-
import { ledgerDrift } from './check-ledger.mjs';
|
|
14
|
-
|
|
15
|
-
let pass = 0, failCount = 0;
|
|
16
|
-
function check(name, cond, detail = '') {
|
|
17
|
-
if (cond) { pass++; console.log('PASS ' + name); }
|
|
18
|
-
else { failCount++; console.log('FAIL ' + name + (detail ? '——' + detail : '')); }
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// LF 归一 sha(同 check-ledger.mjs / doctor §4 口径)
|
|
22
|
-
const lfSha = (s) => createHash('sha256').update(s.replace(/\r\n/g, '\n'), 'utf8').digest('hex');
|
|
23
|
-
|
|
24
|
-
function mkFixture(managed, entries, kitRaw) {
|
|
25
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-cl-'));
|
|
26
|
-
for (const [rel, content] of Object.entries(managed)) {
|
|
27
|
-
fs.mkdirSync(path.join(root, path.dirname(rel)), { recursive: true });
|
|
28
|
-
fs.writeFileSync(path.join(root, rel), content);
|
|
29
|
-
}
|
|
30
|
-
fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
|
|
31
|
-
fs.writeFileSync(path.join(root, '.agents', 'kit.json'),
|
|
32
|
-
kitRaw !== undefined ? kitRaw : JSON.stringify({ kit: 't', version: '0.0.0-test', managed: entries }));
|
|
33
|
-
return root;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const SCRIPT = path.join(path.dirname(fileURLToPath(import.meta.url)), 'check-ledger.mjs');
|
|
37
|
-
const runCli = (cwd) => spawnSync(process.execPath, [SCRIPT], { encoding: 'utf8', cwd });
|
|
38
|
-
|
|
39
|
-
// ---- S1 全对齐 → 无漂移无缺失 ----
|
|
40
|
-
{
|
|
41
|
-
const root = mkFixture({ '.agents/scripts/a.mjs': 'A v1\n' }, [{ rel: '.agents/scripts/a.mjs', sha256: lfSha('A v1\n') }]);
|
|
42
|
-
const r = ledgerDrift(root);
|
|
43
|
-
check('S1 全对齐 → modified=0 gone=0 total=1', !r.skipped && r.modified.length === 0 && r.gone.length === 0 && r.total === 1, JSON.stringify(r));
|
|
44
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// ---- S2 内容漂移 → 逐份列出 rel ----
|
|
48
|
-
{
|
|
49
|
-
const root = mkFixture({ '.agents/scripts/a.mjs': 'A 改了\n' }, [{ rel: '.agents/scripts/a.mjs', sha256: lfSha('A v1\n') }]);
|
|
50
|
-
const r = ledgerDrift(root);
|
|
51
|
-
check('S2 内容漂移 → modified=[a.mjs]', !r.skipped && r.modified.length === 1 && r.modified[0] === '.agents/scripts/a.mjs' && r.gone.length === 0, JSON.stringify(r));
|
|
52
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// ---- S3 managed 缺失 → gone ----
|
|
56
|
-
{
|
|
57
|
-
const root = mkFixture({}, [{ rel: '.agents/scripts/gone.mjs', sha256: lfSha('x') }]);
|
|
58
|
-
const r = ledgerDrift(root);
|
|
59
|
-
check('S3 缺失 → gone=[gone.mjs]', !r.skipped && r.gone.length === 1 && r.gone[0] === '.agents/scripts/gone.mjs' && r.modified.length === 0, JSON.stringify(r));
|
|
60
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// ---- S4 CRLF 盘面 × LF 台账 → 归一不误报 ----
|
|
64
|
-
{
|
|
65
|
-
const root = mkFixture({ '.agents/scripts/crlf.mjs': 'B v1\r\nline2\r\n' }, [{ rel: '.agents/scripts/crlf.mjs', sha256: lfSha('B v1\nline2\n') }]);
|
|
66
|
-
const r = ledgerDrift(root);
|
|
67
|
-
check('S4 CRLF 盘面 × LF 台账 → modified=0(归一口径)', !r.skipped && r.modified.length === 0 && r.gone.length === 0, JSON.stringify(r));
|
|
68
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ---- S5 无 kit.json → skipped(非 flow-kit 安装兼容) ----
|
|
72
|
-
{
|
|
73
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-cl-'));
|
|
74
|
-
const r = ledgerDrift(root);
|
|
75
|
-
check('S5 无 kit.json → skipped=true', r.skipped === true && typeof r.note === 'string', JSON.stringify(r));
|
|
76
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ---- S6 kit.json 解析失败 → fatal ----
|
|
80
|
-
{
|
|
81
|
-
const root = mkFixture({}, [], '{ kit: 坏 json');
|
|
82
|
-
const r = ledgerDrift(root);
|
|
83
|
-
check('S6 解析失败 → fatal 非空', !r.skipped && typeof r.fatal === 'string' && r.fatal.includes('解析失败'), JSON.stringify(r));
|
|
84
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// ---- S6b JSON 合法但 managed 缺失/非数组 → fatal(不静默全对齐;独立复核 P2-3) ----
|
|
88
|
-
{
|
|
89
|
-
const root = mkFixture({}, [], JSON.stringify({ kit: 't', version: '0.0.0-test' }));
|
|
90
|
-
const r = ledgerDrift(root);
|
|
91
|
-
check('S6b managed 非数组 → fatal 非空(fail-loud)', !r.skipped && typeof r.fatal === 'string' && r.fatal.includes('managed'), JSON.stringify(r));
|
|
92
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// ---- S7 CLI 干净 fixture → exit 0 + 全对齐 ----
|
|
96
|
-
{
|
|
97
|
-
const root = mkFixture({ '.agents/scripts/a.mjs': 'A v1\n' }, [{ rel: '.agents/scripts/a.mjs', sha256: lfSha('A v1\n') }]);
|
|
98
|
-
const p = runCli(root);
|
|
99
|
-
check('S7 CLI 全对齐 → exit 0 + 「全对齐」', p.status === 0 && p.stdout.includes('全对齐'), `status=${p.status} out=${p.stdout}`);
|
|
100
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ---- S8 CLI 漂移 fixture → exit 1 + 列出 rel + sync 提示 ----
|
|
104
|
-
{
|
|
105
|
-
const root = mkFixture({ '.agents/scripts/a.mjs': 'A 改了\n' }, [{ rel: '.agents/scripts/a.mjs', sha256: lfSha('A v1\n') }]);
|
|
106
|
-
const p = runCli(root);
|
|
107
|
-
check('S8 CLI 漂移 → exit 1 + rel + sync 提示', p.status === 1 && p.stderr.includes('.agents/scripts/a.mjs') && p.stderr.includes('sync'), `status=${p.status} err=${p.stderr}`);
|
|
108
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// ---- S9 真实仓 baseline:包源环境台账↔盘面全对齐;装户环境 SKIP 不计失败 ----
|
|
112
|
-
{
|
|
113
|
-
let srcRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
114
|
-
for (let i = 0; i < 5 && srcRoot; i++) {
|
|
115
|
-
if (fs.existsSync(path.join(srcRoot, 'templates', '_agents'))) break;
|
|
116
|
-
srcRoot = path.dirname(srcRoot);
|
|
117
|
-
}
|
|
118
|
-
if (!srcRoot || !fs.existsSync(path.join(srcRoot, 'templates', '_agents'))) {
|
|
119
|
-
console.log('SKIP S9(装户环境无包源,不计失败)');
|
|
120
|
-
} else if (!fs.existsSync(path.join(srcRoot, '.agents', 'kit.json'))) {
|
|
121
|
-
console.log('SKIP S9(纯包源仓无 .agents/ 装副本——装户端内容清理后形态,不计失败)');
|
|
122
|
-
} else {
|
|
123
|
-
const r = ledgerDrift(srcRoot);
|
|
124
|
-
check('S9 真实仓 baseline → 台账↔盘面全对齐', !r.skipped && r.modified.length === 0 && r.gone.length === 0, JSON.stringify({ modified: r.modified, gone: r.gone }));
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// ---- S10 入库态口径(2026-10-08 papercuts-cleanup-batch 修 5):git 仓 fixture,分批提交场景 ----
|
|
129
|
-
// 核心场景(papercuts 2026-09-30 行):sync 刷新 kit.json 后只 add 台账不 add 件 → 提交树内 kit.json
|
|
130
|
-
// 预 landing → 修前工作树口径「全对齐」放行(假绿),clone/CI doctor 必红;修后入库态模式拦。
|
|
131
|
-
{
|
|
132
|
-
const GIT = process.platform === 'win32' ? 'git.exe' : 'git';
|
|
133
|
-
const g = (cwd, ...args) => spawnSync(GIT, args, { cwd, encoding: 'utf8' });
|
|
134
|
-
const W2 = (root, rel, content) => {
|
|
135
|
-
fs.mkdirSync(path.join(root, path.dirname(rel)), { recursive: true });
|
|
136
|
-
fs.writeFileSync(path.join(root, rel), content);
|
|
137
|
-
};
|
|
138
|
-
const repo = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-cl-git-'));
|
|
139
|
-
g(repo, 'init', '-q');
|
|
140
|
-
g(repo, 'config', 'user.email', 't@t');
|
|
141
|
-
g(repo, 'config', 'user.name', 't');
|
|
142
|
-
g(repo, 'config', 'core.autocrlf', 'false');
|
|
143
|
-
const kitOf = (aSha) => JSON.stringify({
|
|
144
|
-
kit: 't', version: '0.0.0-test',
|
|
145
|
-
managed: [{ rel: '.agents/scripts/a.mjs', sha256: aSha }],
|
|
146
|
-
}) + '\n';
|
|
147
|
-
W2(repo, '.agents/scripts/a.mjs', 'A v1\n');
|
|
148
|
-
W2(repo, '.agents/kit.json', kitOf(lfSha('A v1\n')));
|
|
149
|
-
g(repo, 'add', '-A');
|
|
150
|
-
g(repo, 'commit', '-q', '-m', 'v1');
|
|
151
|
-
// sync 模拟:件与台账同刷 v2(工作区自洽,均未提交)
|
|
152
|
-
W2(repo, '.agents/scripts/a.mjs', 'A v2\n');
|
|
153
|
-
W2(repo, '.agents/kit.json', kitOf(lfSha('A v2\n')));
|
|
154
|
-
|
|
155
|
-
// S10① 只 add kit.json(分批提交)→ 暂存台账(v2) vs 入库件(HEAD v1) → 拦
|
|
156
|
-
g(repo, 'add', '.agents/kit.json');
|
|
157
|
-
const r1 = ledgerDrift(repo, { staged: true });
|
|
158
|
-
check('S10① 入库态:台账(v2 暂存) vs 件(HEAD v1) → 拦(修前工作树口径假绿放行)',
|
|
159
|
-
r1.mode === 'staged' && r1.modified.includes('.agents/scripts/a.mjs'), JSON.stringify(r1));
|
|
160
|
-
// S10② 件与台账同 add → 入库态一致 → 过
|
|
161
|
-
g(repo, 'add', '.agents/scripts/a.mjs');
|
|
162
|
-
const r2 = ledgerDrift(repo, { staged: true });
|
|
163
|
-
check('S10② 入库态:件与台账同 add → 一致过', r2.mode === 'staged' && !r2.modified.length && !r2.gone.length, JSON.stringify(r2));
|
|
164
|
-
g(repo, 'commit', '-q', '-m', 'v2 both');
|
|
165
|
-
// S10③ 工作树删件未暂存 → index 仍有该件(`:rel` 命中 HEAD 内容)→ 不误报 gone(复核 P1 修后语义:
|
|
166
|
-
// HEAD 兜底已移除,判定路径 = `:rel` 命中而非兜底)
|
|
167
|
-
fs.rmSync(path.join(repo, '.agents/scripts/a.mjs'));
|
|
168
|
-
const r3 = ledgerDrift(repo, { staged: true });
|
|
169
|
-
check('S10③ 入库态:工作树删件未暂存 → index 仍在,不误报', !r3.gone.length && !r3.modified.length, JSON.stringify(r3));
|
|
170
|
-
// S10④ 暂存删除(git rm)→ 提交树已无该件而台账仍跟踪 → gone 拦(复核 P1:HEAD 兜底曾把它误读
|
|
171
|
-
// 为「未触碰」放行 = 提交门绕过;修后与模块头「managed 删除即拦」政策一致)
|
|
172
|
-
g(repo, 'rm', '-q', '--cached', '.agents/scripts/a.mjs');
|
|
173
|
-
const r4 = ledgerDrift(repo, { staged: true });
|
|
174
|
-
check('S10④ 入库态:暂存删除 → gone 拦(提交树=index,删除被应用)', r4.gone.includes('.agents/scripts/a.mjs'), JSON.stringify(r4));
|
|
175
|
-
// S10④b 退出性提交:kit.json 被暂存删除 → 放行(skip,语义与 managed 删除拦截区分)
|
|
176
|
-
g(repo, 'rm', '-q', '--cached', '.agents/kit.json');
|
|
177
|
-
const r4b = ledgerDrift(repo, { staged: true });
|
|
178
|
-
check('S10④b 入库态:kit.json 暂存删除(退出性提交)→ skip 放行', r4b.skipped === true, JSON.stringify(r4b));
|
|
179
|
-
g(repo, 'reset', '-q', '--hard', 'HEAD'); // 复原(防脏)
|
|
180
|
-
|
|
181
|
-
// S10⑤ isMain 无参自适应(子进程):暂存区空 → 「工作树」;暂存区非空 → 自动「入库态」
|
|
182
|
-
g(repo, 'reset', '-q', '--hard', 'HEAD'); // 清掉 ④ 的暂存删除,回到干净 v2 both
|
|
183
|
-
const s1 = runCli(repo); // 暂存区空 → 工作树口径
|
|
184
|
-
check('S10⑤ 暂存区空 → 工作树口径', s1.status === 0 && (s1.stdout || '').includes('工作树'), `out=${s1.stdout}`);
|
|
185
|
-
W2(repo, 'unrelated.txt', 'x\n');
|
|
186
|
-
g(repo, 'add', 'unrelated.txt'); // 暂存非空(无关文件即可)→ 自动入库态;kit.json 未暂存走 HEAD 兜底
|
|
187
|
-
const s2 = runCli(repo);
|
|
188
|
-
check('S10⑤ 暂存区非空 → 自动切入库态且一致 exit 0',
|
|
189
|
-
s2.status === 0 && (s2.stdout || '').includes('入库态'), `status=${s2.status} out=${s2.stdout}`);
|
|
190
|
-
// S10⑥ 姊妹漏洞(完备口径价值场景):只 add 件不 add 台账 → 提交树 = HEAD 台账(v1? v2) + 暂存件 → 错位拦
|
|
191
|
-
W2(repo, '.agents/scripts/a.mjs', 'A v3\n');
|
|
192
|
-
g(repo, 'add', '.agents/scripts/a.mjs');
|
|
193
|
-
const s3 = runCli(repo);
|
|
194
|
-
check('S10⑥ 只 add 件不 add 台账 → 入库态错位拦 exit 1', s3.status === 1 && (s3.stderr || '').includes('.agents/scripts/a.mjs'), `status=${s3.status} err=${s3.stderr}`);
|
|
195
|
-
fs.rmSync(repo, { recursive: true, force: true });
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
console.log(`\n合计: PASS ${pass} / FAIL ${failCount}`);
|
|
199
|
-
process.exit(failCount ? 1 : 0);
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
// flow-kit source-sync-check:装户面同步一致性检查(包源 templates/_agents/ vs 装副本 .agents/)
|
|
2
|
-
// 目的:包源改了 templates/_agents/ 但 .agents/ 装副本没刷新——这种"真空地带"sync.mjs 不扫、doctor.mjs §6.6 不扫
|
|
3
|
-
// 本工具扫 templates/_agents/ 下所有 .md / .mjs / .json / .txt(排除 cache/)vs .agents/ 对应路径
|
|
4
|
-
// 输出三类差异:缺失(包源有装副本无)/ 孤儿(包源无装副本有)/ 漂移(两者都有 sha 不一致)
|
|
5
|
-
// 排除装副本独有文件:kit.json / settings.json / hooks/commit-check.config.json(init 渲染产物,不属双源)
|
|
6
|
-
// 用法:node .agents/scripts/source-sync-check.mjs --diff
|
|
7
|
-
// node .agents/scripts/source-sync-check.mjs --json
|
|
8
|
-
// node .agents/scripts/source-sync-check.mjs --gate(门禁模式:缺失/漂移 exit 1,孤儿只报告不计失败
|
|
9
|
-
// ——pre-commit 双源一致性门禁用,2026-09-27 audit-gate-hardening;--diff/--json 仍恒 exit 0)
|
|
10
|
-
// node .agents/scripts/source-sync-check.mjs --pkg-root <path> --target <path>
|
|
11
|
-
// 零依赖;B-b 决策「只报告不修复」(--gate 只加 exit 码裁决,不改默认行为)
|
|
12
|
-
import fs from 'node:fs';
|
|
13
|
-
import path from 'node:path';
|
|
14
|
-
import { createHash } from 'node:crypto';
|
|
15
|
-
|
|
16
|
-
function fail(msg) { console.error('❌ ' + msg); process.exit(1); }
|
|
17
|
-
|
|
18
|
-
const VALID_EXTS = new Set(['.md', '.mjs', '.json', '.txt']);
|
|
19
|
-
// 装副本独有文件(init 渲染产物,不属双源结构)
|
|
20
|
-
const RENDER_OUTPUT_FILES = new Set(['kit.json', 'settings.json', 'hooks/commit-check.config.json']);
|
|
21
|
-
// **项目自持、按设计就该与包源骨架不同**的架子文件(2026-09-28 追加):
|
|
22
|
-
// `notes/runtime-env.md` 自述「记录本机/本项目的运行时差异……**本文件是骨架:项目按实际填写,
|
|
23
|
-
// 不随 flow-kit 升级覆盖**」——即包源只发骨架、装户填实况,**两处本就应当不同**。
|
|
24
|
-
// 但本工具此前按 `.md` 全量比对,导致装户一旦按文件自述填写,`--gate` 即 exit 1(CI 红):
|
|
25
|
-
// 实测(本仓自装)填写后 --gate 报 `漂移 1: notes/runtime-env.md`,回滚即恢复 0。
|
|
26
|
-
// 这属「门禁范围与文件自述冲突」,非真漂移——故按渲染产物同类处理(排除)。
|
|
27
|
-
// 判据:该文件是**项目内容**而非**引擎件**;引擎件(.mjs 脚本等)不在排除内,仍受双源门禁约束。
|
|
28
|
-
const PROJECT_OWNED_FILES = new Set(['notes/runtime-env.md']);
|
|
29
|
-
const TARGET_EXCLUDE = new Set([...RENDER_OUTPUT_FILES, ...PROJECT_OWNED_FILES]); // 向后兼容别名(原为 RENDER_OUTPUT_FILES)
|
|
30
|
-
|
|
31
|
-
function sha256(p) {
|
|
32
|
-
try {
|
|
33
|
-
// LF 归一(2026-09-27 closing-coverage):CRLF 盘面与 LF 克隆同值,行尾不一致不误报缺失/漂移/孤儿外差异
|
|
34
|
-
return createHash('sha256').update(fs.readFileSync(p, 'utf8').replace(/\r\n/g, '\n'), 'utf8').digest('hex');
|
|
35
|
-
} catch {
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// walkMd:递归扫目录,列出所有 validExt 文件(排除 cache/)
|
|
41
|
-
function walkMd(root) {
|
|
42
|
-
const out = [];
|
|
43
|
-
function walk(dir, relBase) {
|
|
44
|
-
if (!fs.existsSync(dir)) return;
|
|
45
|
-
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
46
|
-
const rel = relBase ? relBase + '/' + e.name : e.name;
|
|
47
|
-
const abs = path.join(dir, e.name);
|
|
48
|
-
// 排除 cache/ 目录(与 sync.mjs 既有约定一致)
|
|
49
|
-
if (rel === 'cache' || rel.startsWith('cache/')) continue;
|
|
50
|
-
if (e.isDirectory()) walk(abs, rel);
|
|
51
|
-
else if (VALID_EXTS.has(path.extname(e.name))) out.push(rel);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
walk(root, '');
|
|
55
|
-
return out;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function sourceSyncCheck({ pkgRoot, target }) {
|
|
59
|
-
const authorityRoot = path.join(pkgRoot, 'templates', '_agents');
|
|
60
|
-
const adapterRoot = path.join(target, '.agents');
|
|
61
|
-
// **两侧都要按 TARGET_EXCLUDE 过滤**(2026-09-28 修正):只过滤装副本一侧会把排除件
|
|
62
|
-
// 变成「包源有 / 装副本无」的**缺失**(实测:加排除后 --gate 从「漂移 1」变成「缺失 1」,
|
|
63
|
-
// 仍 exit 1)。排除的语义是「不参与双源比对」,故包源侧同样滤掉。
|
|
64
|
-
const pkgFiles = walkMd(authorityRoot).filter((f) => !TARGET_EXCLUDE.has(f));
|
|
65
|
-
const tgtFiles = walkMd(adapterRoot).filter((f) => !TARGET_EXCLUDE.has(f));
|
|
66
|
-
const pkgSet = new Set(pkgFiles);
|
|
67
|
-
const tgtSet = new Set(tgtFiles);
|
|
68
|
-
const missing = []; // 包源有 / 装副本无
|
|
69
|
-
const orphan = []; // 包源无 / 装副本有
|
|
70
|
-
const drift = []; // 两者都有但 sha 不一致
|
|
71
|
-
for (const f of pkgFiles) {
|
|
72
|
-
if (!tgtSet.has(f)) { missing.push({ rel: f }); continue; }
|
|
73
|
-
const pkgSha = sha256(path.join(authorityRoot, f));
|
|
74
|
-
const tgtSha = sha256(path.join(adapterRoot, f));
|
|
75
|
-
if (pkgSha !== null && tgtSha !== null && pkgSha !== tgtSha) {
|
|
76
|
-
drift.push({ rel: f, pkgSha, tgtSha });
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
for (const f of tgtFiles) {
|
|
80
|
-
if (!pkgSet.has(f)) orphan.push({ rel: f });
|
|
81
|
-
}
|
|
82
|
-
return {
|
|
83
|
-
pkgRoot, target,
|
|
84
|
-
pkgCount: pkgFiles.length,
|
|
85
|
-
tgtCount: tgtFiles.length,
|
|
86
|
-
missing: missing.sort((a, b) => a.rel.localeCompare(b.rel)),
|
|
87
|
-
orphan: orphan.sort((a, b) => a.rel.localeCompare(b.rel)),
|
|
88
|
-
drift: drift.sort((a, b) => a.rel.localeCompare(b.rel)),
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function printDiff(result) {
|
|
93
|
-
console.log('▶ flow-kit source-sync-check --diff');
|
|
94
|
-
console.log(' 包源 (templates/_agents/) ' + result.pkgCount + ' 份 | 装副本 (.agents/) ' + result.tgtCount + ' 份');
|
|
95
|
-
if (!result.missing.length && !result.orphan.length && !result.drift.length) {
|
|
96
|
-
console.log(' 无差异 ✅');
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
if (result.missing.length) {
|
|
100
|
-
console.log(' 缺失(包源有 / 装副本无)' + result.missing.length + ':');
|
|
101
|
-
for (const m of result.missing) console.log(' - ' + m.rel);
|
|
102
|
-
}
|
|
103
|
-
if (result.orphan.length) {
|
|
104
|
-
console.log(' 孤儿(包源无 / 装副本有)' + result.orphan.length + ':');
|
|
105
|
-
for (const o of result.orphan) console.log(' - ' + o.rel);
|
|
106
|
-
}
|
|
107
|
-
if (result.drift.length) {
|
|
108
|
-
console.log(' 漂移(包源装副本都有但 sha 不一致)' + result.drift.length + ':');
|
|
109
|
-
for (const d of result.drift) console.log(' - ' + d.rel + ' pkg=' + d.pkgSha.slice(0, 8) + '… tgt=' + d.tgtSha.slice(0, 8) + '…');
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const isMain = process.argv[1] && process.argv[1].endsWith('source-sync-check.mjs');
|
|
114
|
-
if (isMain) {
|
|
115
|
-
let jsonMode = false, gateMode = false;
|
|
116
|
-
let pkgRoot = null, target = null;
|
|
117
|
-
for (let i = 0; i < process.argv.length - 2; i++) {
|
|
118
|
-
const a = process.argv[2 + i];
|
|
119
|
-
if (a === '--json') jsonMode = true;
|
|
120
|
-
else if (a === '--diff') { /* diff 为默认输出模式,无需置位 */ }
|
|
121
|
-
else if (a === '--gate') gateMode = true;
|
|
122
|
-
else if (a === '--pkg-root') pkgRoot = process.argv[2 + i + 1];
|
|
123
|
-
else if (a === '--target') target = process.argv[2 + i + 1];
|
|
124
|
-
}
|
|
125
|
-
if (!pkgRoot) pkgRoot = process.cwd();
|
|
126
|
-
if (!target) target = process.cwd();
|
|
127
|
-
const authorityRoot = path.join(pkgRoot, 'templates', '_agents');
|
|
128
|
-
const adapterRoot = path.join(target, '.agents');
|
|
129
|
-
if (!fs.existsSync(authorityRoot)) fail('找不到包源:' + authorityRoot);
|
|
130
|
-
if (!fs.existsSync(adapterRoot)) fail('找不到装副本:' + adapterRoot);
|
|
131
|
-
const result = sourceSyncCheck({ pkgRoot, target });
|
|
132
|
-
if (jsonMode) {
|
|
133
|
-
process.stdout.write(JSON.stringify({
|
|
134
|
-
pkgCount: result.pkgCount,
|
|
135
|
-
tgtCount: result.tgtCount,
|
|
136
|
-
missing: result.missing.map((m) => m.rel),
|
|
137
|
-
orphan: result.orphan.map((o) => o.rel),
|
|
138
|
-
drift: result.drift.map((d) => ({ rel: d.rel, pkgSha: d.pkgSha.slice(0, 8), tgtSha: d.tgtSha.slice(0, 8) })),
|
|
139
|
-
}, null, 2) + '\n');
|
|
140
|
-
} else {
|
|
141
|
-
printDiff(result);
|
|
142
|
-
}
|
|
143
|
-
if (gateMode) {
|
|
144
|
-
// 门禁模式(pre-commit 双源一致性用):缺失/漂移非零阻断——正是「包源改了装副本没跟」的故障模式
|
|
145
|
-
// (doctor §4 台账 sha 对此静默:盘面=台账即 PASS;§4.5 只抓未登记)。孤儿只报告不计失败:
|
|
146
|
-
// 孤儿 = 装副本独有(装户自持内容如本地 skills),删留由人拍板,不入机器门。
|
|
147
|
-
if (result.missing.length || result.drift.length) {
|
|
148
|
-
console.error('❌ 双源不一致(缺失 ' + result.missing.length + ' / 漂移 ' + result.drift.length + ')——包源与装副本须同提交成对(双源纪律);跑 node bin/flow-kit.mjs sync 后原路重试(孤儿只报告不计失败)');
|
|
149
|
-
process.exit(1);
|
|
150
|
-
}
|
|
151
|
-
process.exit(0);
|
|
152
|
-
}
|
|
153
|
-
// diff/json 模式总 exit 0;差异存在不视作错误(人类阅读用;B-b 决策「只报告不修复」)
|
|
154
|
-
process.exit(0);
|
|
155
|
-
}
|
|
156
|
-
export default sourceSyncCheck;
|