agentic-flow-kit 0.4.0 → 0.6.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 +3 -0
- package/modules/gates/dotnet-ca/check-architecture.sh +23 -5
- package/modules/hosts/omp/agents/implementer.md +47 -6
- package/modules/hosts/omp/agents/independent-reviewer.md +44 -5
- package/modules/hosts/omp/agents/ui-verifier.md +43 -6
- package/modules/hosts/opencode/agents/implementer.md +47 -1
- package/modules/hosts/opencode/agents/independent-reviewer.md +44 -1
- package/modules/hosts/opencode/agents/ui-verifier.md +43 -1
- package/modules/hosts/opencode/commands/build.md +74 -7
- package/modules/hosts/opencode/commands/deploy.md +51 -6
- package/modules/hosts/opencode/commands/design.md +36 -6
- package/modules/hosts/opencode/commands/gate-checklist.md +77 -0
- package/modules/hosts/opencode/commands/maintain.md +70 -6
- package/modules/hosts/opencode/commands/new-task.md +59 -6
- package/modules/hosts/opencode/commands/plan.md +35 -6
- package/modules/hosts/opencode/commands/review.md +41 -6
- package/modules/hosts/opencode/commands/source-sync-check.md +89 -0
- package/modules/hosts/opencode/commands/sync-hosts.md +66 -0
- package/modules/hosts/opencode/commands/test.md +58 -8
- package/modules/hosts/trae/agents/implementer.md +46 -2
- package/modules/hosts/trae/agents/independent-reviewer.md +44 -1
- package/modules/hosts/trae/agents/ui-verifier.md +41 -3
- package/modules/hosts/trae/commands/wf-build.md +74 -7
- package/modules/hosts/trae/commands/wf-deploy.md +51 -6
- package/modules/hosts/trae/commands/wf-design.md +36 -6
- package/modules/hosts/trae/commands/wf-gate-checklist.md +78 -0
- package/modules/hosts/trae/commands/wf-maintain.md +70 -6
- package/modules/hosts/trae/commands/wf-new-task.md +59 -6
- package/modules/hosts/trae/commands/wf-plan.md +35 -6
- package/modules/hosts/trae/commands/wf-review.md +41 -6
- package/modules/hosts/trae/commands/wf-source-sync-check.md +90 -0
- package/modules/hosts/trae/commands/wf-sync-hosts.md +67 -0
- package/modules/hosts/trae/commands/wf-test.md +58 -7
- package/modules/hosts/trae/hooks/post-edit-check.cjs +3 -2
- package/modules/hosts/trae/hooks/pre-shell-check.cjs +40 -9
- package/modules/hosts/trae/hooks.json +1 -1
- package/modules/hosts/zcode/agents/implementer.md +47 -6
- package/modules/hosts/zcode/agents/independent-reviewer.md +44 -5
- package/modules/hosts/zcode/agents/ui-verifier.md +43 -5
- package/package.json +6 -3
- package/src/add-gate.mjs +5 -3
- package/src/cli.mjs +15 -2
- package/src/doctor.mjs +245 -15
- package/src/gate-dotnet-ca.test.mjs +124 -0
- package/src/init.mjs +79 -17
- package/src/init.test.mjs +84 -2
- package/src/pack.test.mjs +24 -0
- package/src/render.mjs +64 -4
- package/src/run-tests.mjs +7 -11
- package/src/sync-hosts.mjs +194 -0
- package/src/sync-hosts.test.mjs +170 -0
- package/src/sync.mjs +65 -20
- package/src/sync.test.mjs +52 -0
- package/src/trae-hooks.test.mjs +115 -0
- package/templates/AGENTS.md +2 -1
- package/templates/_agents/board/index.html +32 -3
- package/templates/_agents/commands/build.md +22 -1
- package/templates/_agents/commands/design.md +2 -0
- package/templates/_agents/commands/gate-checklist.md +91 -0
- package/templates/_agents/commands/maintain.md +2 -3
- package/templates/_agents/commands/plan.md +4 -1
- package/templates/_agents/commands/review.md +1 -0
- package/templates/_agents/commands/source-sync-check.md +103 -0
- package/templates/_agents/commands/sync-hosts.md +79 -0
- package/templates/_agents/commands/test.md +4 -2
- package/templates/_agents/hooks/check-pairing-incremental.sh +20 -7
- package/templates/_agents/notes/runtime-env.md +1 -1
- package/templates/_agents/roles/implementer.md +2 -0
- package/templates/_agents/roles/independent-reviewer.md +2 -0
- package/templates/_agents/roles/ui-verifier.md +2 -0
- package/templates/_agents/rule-budgets.txt +1 -1
- package/templates/_agents/scripts/agg-delegations.cjs +39 -30
- package/templates/_agents/scripts/agg-delegations.test.mjs +32 -0
- package/templates/_agents/scripts/check-loop.mjs +763 -0
- package/templates/_agents/scripts/check-loop.sh +5 -808
- package/templates/_agents/scripts/check-loop.test.mjs +1102 -0
- package/templates/_agents/scripts/confirm-doc.mjs +228 -0
- package/templates/_agents/scripts/confirm-doc.test.mjs +321 -0
- package/templates/_agents/scripts/doctor.test.mjs +371 -0
- package/templates/_agents/scripts/ensure-board.mjs +30 -3
- package/templates/_agents/scripts/fill-intent.mjs +59 -0
- package/templates/_agents/scripts/fill-intent.test.mjs +73 -0
- package/templates/_agents/scripts/fill-plan.mjs +60 -0
- package/templates/_agents/scripts/fill-plan.test.mjs +67 -0
- package/templates/_agents/scripts/fill-spec.mjs +52 -0
- package/templates/_agents/scripts/fill-spec.test.mjs +61 -0
- package/templates/_agents/scripts/gate-checklist.mjs +194 -0
- package/templates/_agents/scripts/gate-checklist.test.mjs +204 -0
- package/templates/_agents/scripts/gen-wiki-board.mjs +38 -24
- package/templates/_agents/scripts/gen-wiki-board.test.mjs +18 -0
- package/templates/_agents/scripts/gen-workflow-index.mjs +4 -2
- package/templates/_agents/scripts/gen-workflow-metrics.mjs +12 -3
- package/templates/_agents/scripts/kb-search.mjs +25 -9
- package/templates/_agents/scripts/kb-search.test.mjs +38 -8
- package/templates/_agents/scripts/source-sync-check.mjs +145 -0
- package/templates/_agents/scripts/source-sync-check.test.mjs +244 -0
- package/templates/_agents/scripts/verify-wiki-consistency.mjs +46 -23
- package/templates/_agents/scripts/verify.mjs +8 -10
- package/templates/_agents/scripts/verify.test.mjs +13 -1
- package/templates/_agents/scripts/wf-journal.mjs +158 -0
- package/templates/_agents/scripts/wf-journal.test.mjs +150 -0
- package/templates/_agents/scripts/workflow-board-server.mjs +73 -17
- package/templates/_agents/scripts/workflow-board-server.test.mjs +136 -0
- package/templates/_agents/scripts/workflow-enums.mjs +70 -0
- package/templates/_agents/scripts/workflow-enums.test.mjs +110 -0
- package/templates/_agents/scripts/workflows-check.mjs +260 -0
- package/templates/_agents/scripts/workflows-check.test.mjs +267 -0
- package/templates/_agents/workflow-enums.txt +19 -0
- package/templates/_agents/workflows/_TEMPLATE.md +75 -0
- package/templates/_agents/workflows/pipeline-closing.md +29 -0
- package/templates/_agents/workflows/source-sync-repair.md +30 -0
- package/templates/_agents/workflows/steps//347/244/272/344/276/213-/351/203/250/347/275/262/351/252/214/350/257/201.md +10 -0
- package/templates/_agents/workflows//347/244/272/344/276/213-/345/271/266/350/241/214/345/256/236/347/216/260/350/257/204/345/256/241.md +16 -0
- package/templates/_githooks/pre-commit +16 -1
- package/templates/workflow/README.md +1 -1
- package/templates/_agents/cache/kb-index.json +0 -1
- package/templates/_agents/scripts/check-loop.test.sh +0 -1042
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// source-sync-check.test.mjs — 装户面同步一致性检查工具测试(2026-09-25 source-sync-check)
|
|
2
|
+
// 方法:fixture mini 包源(templates/_agents/) + 装副本(.agents/),调 sourceSyncCheck() 拿三类差异
|
|
3
|
+
// + 实际仓库扫描做 baseline
|
|
4
|
+
// 判据:①fixture 三类齐全 ②cache/ 排除 ③kit.json/settings.json 排除 ④sha 一致 ⑤baseline ≥ 100 ⑥JSON 可解析
|
|
5
|
+
// 用法:node .agents/scripts/source-sync-check.test.mjs
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { sourceSyncCheck } from './source-sync-check.mjs';
|
|
12
|
+
|
|
13
|
+
let pass = 0, failCount = 0;
|
|
14
|
+
function check(name, cond, detail = '') {
|
|
15
|
+
if (cond) { pass++; console.log('PASS ' + name); }
|
|
16
|
+
else { failCount++; console.log('FAIL ' + name + (detail ? '——' + detail : '')); }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// SRC_ROOT:自测试文件位置向上探测包源(templates/_agents)——包源路径(templates/_agents/scripts/)与
|
|
20
|
+
// 装副本路径(.agents/scripts/)直跑都解析到仓库根;无包源(装户环境)返回 null,S5/S9 baseline 记 SKIP
|
|
21
|
+
// 不算失败(2026-09-27 gate-hardening-p2-batch:旧 `../../..` 硬编码层级从装副本直跑必 FAIL)
|
|
22
|
+
const findSrcRoot = () => {
|
|
23
|
+
let d = path.dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
for (let i = 0; i < 5; i++) {
|
|
25
|
+
if (fs.existsSync(path.join(d, 'templates', '_agents'))) return d;
|
|
26
|
+
d = path.dirname(d);
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
};
|
|
30
|
+
const SRC_ROOT = findSrcRoot();
|
|
31
|
+
const SCRIPT = path.join(path.dirname(fileURLToPath(import.meta.url)), 'source-sync-check.mjs');
|
|
32
|
+
const runGate = (args) => spawnSync(process.execPath, [SCRIPT, ...args], { encoding: 'utf8' });
|
|
33
|
+
|
|
34
|
+
// ---- fixture:mini 包源 + 装副本,3 类差异各造 1 个 ----
|
|
35
|
+
function mkFixture() {
|
|
36
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-ssc-'));
|
|
37
|
+
const pkgRoot = path.join(root, 'pkg');
|
|
38
|
+
const target = path.join(root, 'target');
|
|
39
|
+
const pkgBase = path.join(pkgRoot, 'templates', '_agents');
|
|
40
|
+
const tgtBase = path.join(target, '.agents');
|
|
41
|
+
fs.mkdirSync(path.join(pkgBase, 'commands'), { recursive: true });
|
|
42
|
+
fs.mkdirSync(path.join(pkgBase, 'scripts'), { recursive: true });
|
|
43
|
+
fs.mkdirSync(path.join(pkgBase, 'cache'), { recursive: true }); // 应排除
|
|
44
|
+
fs.mkdirSync(path.join(tgtBase, 'commands'), { recursive: true });
|
|
45
|
+
fs.mkdirSync(path.join(tgtBase, 'scripts'), { recursive: true });
|
|
46
|
+
fs.mkdirSync(path.join(tgtBase, 'hooks'), { recursive: true }); // 孤儿
|
|
47
|
+
fs.mkdirSync(path.join(tgtBase, 'cache'), { recursive: true }); // 装副本 cache 也应排除
|
|
48
|
+
// 一致文件(cmdA.md):包源 + 装副本 同内容
|
|
49
|
+
fs.writeFileSync(path.join(pkgBase, 'commands', 'cmdA.md'), '# cmdA v1\n');
|
|
50
|
+
fs.writeFileSync(path.join(tgtBase, 'commands', 'cmdA.md'), '# cmdA v1\n');
|
|
51
|
+
// 缺失(cmdB.md):包源有 / 装副本无
|
|
52
|
+
fs.writeFileSync(path.join(pkgBase, 'commands', 'cmdB.md'), '# cmdB v1\n');
|
|
53
|
+
// 孤儿(orphan-real.json):装副本 hooks/ 有 / 包源无(普通孤儿,应被报告)
|
|
54
|
+
fs.writeFileSync(path.join(tgtBase, 'hooks', 'orphan-real.json'), '{}\n');
|
|
55
|
+
// 白名单孤儿(commit-check.config.json):装副本 hooks/ 有 / 包源无,但 init 渲染产物不应报孤儿
|
|
56
|
+
fs.writeFileSync(path.join(tgtBase, 'hooks', 'commit-check.config.json'), '{}\n');
|
|
57
|
+
// 装副本独有(应排除):kit.json / settings.json
|
|
58
|
+
fs.writeFileSync(path.join(tgtBase, 'kit.json'), '{}\n');
|
|
59
|
+
fs.writeFileSync(path.join(tgtBase, 'settings.json'), '{}\n');
|
|
60
|
+
// 漂移(scriptX.mjs):包源 v1 / 装副本 v2
|
|
61
|
+
fs.writeFileSync(path.join(pkgBase, 'scripts', 'scriptX.mjs'), 'export const X = 1;\n');
|
|
62
|
+
fs.writeFileSync(path.join(tgtBase, 'scripts', 'scriptX.mjs'), 'export const X = 2;\n');
|
|
63
|
+
// cache/ 排除:包源 + 装副本 都有,但不应被计入
|
|
64
|
+
fs.writeFileSync(path.join(pkgBase, 'cache', 'foo.md'), 'cache-pkg\n');
|
|
65
|
+
fs.writeFileSync(path.join(tgtBase, 'cache', 'foo.md'), 'cache-tgt\n');
|
|
66
|
+
return { pkgRoot, target };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---- 场景 1:fixture 三类差异齐全(缺失 1 / 孤儿 1 / 漂移 1)----
|
|
70
|
+
{
|
|
71
|
+
const { pkgRoot, target } = mkFixture();
|
|
72
|
+
const r = sourceSyncCheck({ pkgRoot, target });
|
|
73
|
+
check('S1 fixture 缺失 1(cmdB.md)', r.missing.length === 1 && r.missing[0].rel === 'commands/cmdB.md', JSON.stringify(r.missing));
|
|
74
|
+
check('S1 fixture 孤儿 1(orphan-real.json)', r.orphan.length === 1 && r.orphan[0].rel === 'hooks/orphan-real.json', JSON.stringify(r.orphan));
|
|
75
|
+
check('S1 fixture 漂移 1(scriptX.mjs)', r.drift.length === 1 && r.drift[0].rel === 'scripts/scriptX.mjs', JSON.stringify(r.drift));
|
|
76
|
+
check('S1 fixture 白名单项(commit-check.config.json)不在孤儿', !r.orphan.some((o) => o.rel === 'hooks/commit-check.config.json'));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---- 场景 2:cache/ 目录排除 ----
|
|
80
|
+
{
|
|
81
|
+
const { pkgRoot, target } = mkFixture();
|
|
82
|
+
const r = sourceSyncCheck({ pkgRoot, target });
|
|
83
|
+
check('S2 cache/ 排除(缺失/孤儿/漂移不含 cache/foo.md)',
|
|
84
|
+
!r.missing.some((m) => m.rel.includes('cache/')) &&
|
|
85
|
+
!r.orphan.some((o) => o.rel.includes('cache/')) &&
|
|
86
|
+
!r.drift.some((d) => d.rel.includes('cache/')));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---- 场景 3:装副本独有(kit.json / settings.json)排除 ----
|
|
90
|
+
{
|
|
91
|
+
const { pkgRoot, target } = mkFixture();
|
|
92
|
+
const r = sourceSyncCheck({ pkgRoot, target });
|
|
93
|
+
check('S3 装副本独有(kit.json / settings.json)排除',
|
|
94
|
+
!r.orphan.some((o) => o.rel === 'kit.json' || o.rel === 'settings.json'));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---- 场景 4:sha 一致文件不出现在漂移列表 ----
|
|
98
|
+
{
|
|
99
|
+
const { pkgRoot, target } = mkFixture();
|
|
100
|
+
const r = sourceSyncCheck({ pkgRoot, target });
|
|
101
|
+
check('S4 一致文件 cmdA.md 不在漂移', !r.drift.some((d) => d.rel === 'commands/cmdA.md'));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---- 场景 5:实际仓库扫描 baseline(包源 ≥ 30 份 / 装副本 ≥ 30 份;装户环境无包源 → SKIP)----
|
|
105
|
+
if (!SRC_ROOT) {
|
|
106
|
+
check('S5 baseline SKIP(未探测到包源 templates/_agents——装户环境直跑,不算失败)', true);
|
|
107
|
+
} else {
|
|
108
|
+
const r = sourceSyncCheck({ pkgRoot: SRC_ROOT, target: SRC_ROOT });
|
|
109
|
+
check('S5 实际仓库 包源 ≥ 30 份', r.pkgCount >= 30, '实际 ' + r.pkgCount);
|
|
110
|
+
check('S5 实际仓库 装副本 ≥ 30 份', r.tgtCount >= 30, '实际 ' + r.tgtCount);
|
|
111
|
+
// 装副本 kit.json / settings.json 不应在孤儿
|
|
112
|
+
check('S5 实际仓库 装副本独有(kit.json / settings.json)排除',
|
|
113
|
+
!r.orphan.some((o) => o.rel === 'kit.json' || o.rel === 'settings.json'));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---- 场景 6:--json 输出可解析(CLI 子流程)----
|
|
117
|
+
{
|
|
118
|
+
const { pkgRoot, target } = mkFixture();
|
|
119
|
+
// 直接调 sourceSyncCheck + 序列化模拟 --json 输出
|
|
120
|
+
const r = sourceSyncCheck({ pkgRoot, target });
|
|
121
|
+
const jsonOut = JSON.stringify({
|
|
122
|
+
pkgCount: r.pkgCount, tgtCount: r.tgtCount,
|
|
123
|
+
missing: r.missing.map((m) => m.rel),
|
|
124
|
+
orphan: r.orphan.map((o) => o.rel),
|
|
125
|
+
drift: r.drift.map((d) => ({ rel: d.rel, pkgSha: d.pkgSha.slice(0, 8), tgtSha: d.tgtSha.slice(0, 8) })),
|
|
126
|
+
}, null, 2);
|
|
127
|
+
let parsed = null;
|
|
128
|
+
try { parsed = JSON.parse(jsonOut); } catch {}
|
|
129
|
+
check('S6 --json 输出可解析', parsed && typeof parsed === 'object' && Array.isArray(parsed.missing));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---- 场景 8:白名单 RHO 文件过滤(hooks/commit-check.config.json + kit.json + settings.json 都不报孤儿)----
|
|
133
|
+
{
|
|
134
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-ssc-rho-'));
|
|
135
|
+
const pkgRoot = path.join(root, 'pkg');
|
|
136
|
+
const target = path.join(root, 'target');
|
|
137
|
+
const pkgBase = path.join(pkgRoot, 'templates', '_agents');
|
|
138
|
+
const tgtBase = path.join(target, '.agents');
|
|
139
|
+
fs.mkdirSync(pkgBase, { recursive: true });
|
|
140
|
+
fs.mkdirSync(path.join(tgtBase, 'hooks'), { recursive: true });
|
|
141
|
+
fs.mkdirSync(tgtBase, { recursive: true });
|
|
142
|
+
// 3 个白名单项都装副本独有
|
|
143
|
+
fs.writeFileSync(path.join(tgtBase, 'hooks', 'commit-check.config.json'), '{}\n');
|
|
144
|
+
fs.writeFileSync(path.join(tgtBase, 'kit.json'), '{}\n');
|
|
145
|
+
fs.writeFileSync(path.join(tgtBase, 'settings.json'), '{}\n');
|
|
146
|
+
// 1 个非白名单孤儿作对照(应被报告)
|
|
147
|
+
fs.writeFileSync(path.join(tgtBase, 'hooks', 'real-orphan.json'), '{}\n');
|
|
148
|
+
const r = sourceSyncCheck({ pkgRoot, target });
|
|
149
|
+
check('S8 白名单 3 项(commit-check.config.json / kit.json / settings.json)不报孤儿',
|
|
150
|
+
!r.orphan.some((o) => o.rel === 'hooks/commit-check.config.json' || o.rel === 'kit.json' || o.rel === 'settings.json'));
|
|
151
|
+
check('S8 非白名单孤儿(real-orphan.json)仍报孤儿',
|
|
152
|
+
r.orphan.length === 1 && r.orphan[0].rel === 'hooks/real-orphan.json', JSON.stringify(r.orphan));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---- 场景 9:实际仓库 hooks/commit-check.config.json 不在孤儿(装户环境随 S5 一并 SKIP)----
|
|
156
|
+
if (!SRC_ROOT) {
|
|
157
|
+
check('S9 baseline SKIP(装户环境,随 S5)', true);
|
|
158
|
+
} else {
|
|
159
|
+
const r = sourceSyncCheck({ pkgRoot: SRC_ROOT, target: SRC_ROOT });
|
|
160
|
+
check('S9 实际仓库 hooks/commit-check.config.json 不在孤儿',
|
|
161
|
+
!r.orphan.some((o) => o.rel === 'hooks/commit-check.config.json'), JSON.stringify(r.orphan));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---- 场景 7:空目录 / 不存在的目录不崩 ----
|
|
165
|
+
{
|
|
166
|
+
const empty = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-ssc-empty-'));
|
|
167
|
+
const r = sourceSyncCheck({ pkgRoot: empty, target: empty });
|
|
168
|
+
check('S7 空目录 fixture 不崩(0/0)', r.pkgCount === 0 && r.tgtCount === 0);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ---- 场景 10-12:--gate 门禁模式(2026-09-27 audit-gate-hardening,pre-commit 双源门禁用)----
|
|
172
|
+
{
|
|
173
|
+
// S10 差异 fixture(缺失 1 / 漂移 1 / 孤儿 1)→ --gate exit 1(缺失/漂移阻断)
|
|
174
|
+
const { pkgRoot, target } = mkFixture();
|
|
175
|
+
const r = runGate(['--gate', '--pkg-root', pkgRoot, '--target', target]);
|
|
176
|
+
check('S10 --gate 差异 fixture(缺失+漂移)→ exit 1', r.status === 1, `exit=${r.status}\n${r.stdout || ''}${r.stderr || ''}`);
|
|
177
|
+
}
|
|
178
|
+
{
|
|
179
|
+
// S11 孤儿-only fixture → --gate exit 0(孤儿只报告不计失败——装户自持内容不入机器门)
|
|
180
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-ssc-gate-orphan-'));
|
|
181
|
+
const pkgRoot = path.join(root, 'pkg');
|
|
182
|
+
const target = path.join(root, 'target');
|
|
183
|
+
const pkgBase = path.join(pkgRoot, 'templates', '_agents');
|
|
184
|
+
const tgtBase = path.join(target, '.agents');
|
|
185
|
+
fs.mkdirSync(path.join(pkgBase, 'commands'), { recursive: true });
|
|
186
|
+
fs.mkdirSync(path.join(tgtBase, 'commands'), { recursive: true });
|
|
187
|
+
fs.writeFileSync(path.join(pkgBase, 'commands', 'same.md'), '# v1\n');
|
|
188
|
+
fs.writeFileSync(path.join(tgtBase, 'commands', 'same.md'), '# v1\n');
|
|
189
|
+
fs.writeFileSync(path.join(tgtBase, 'local-only.md'), '# 装户自持\n');
|
|
190
|
+
const r = runGate(['--gate', '--pkg-root', pkgRoot, '--target', target]);
|
|
191
|
+
check('S11 --gate 孤儿-only → exit 0(孤儿不计失败)', r.status === 0, `exit=${r.status}\n${r.stdout || ''}${r.stderr || ''}`);
|
|
192
|
+
}
|
|
193
|
+
{
|
|
194
|
+
// S12 干净 fixture --gate → exit 0;差异 fixture --diff → 仍 exit 0(B-b「只报告不修复」默认语义不变)
|
|
195
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-ssc-gate-clean-'));
|
|
196
|
+
const pkgRoot = path.join(root, 'pkg');
|
|
197
|
+
const target = path.join(root, 'target');
|
|
198
|
+
const pkgBase = path.join(pkgRoot, 'templates', '_agents');
|
|
199
|
+
const tgtBase = path.join(target, '.agents');
|
|
200
|
+
fs.mkdirSync(path.join(pkgBase, 'commands'), { recursive: true });
|
|
201
|
+
fs.mkdirSync(path.join(tgtBase, 'commands'), { recursive: true });
|
|
202
|
+
fs.writeFileSync(path.join(pkgBase, 'commands', 'same.md'), '# v1\n');
|
|
203
|
+
fs.writeFileSync(path.join(tgtBase, 'commands', 'same.md'), '# v1\n');
|
|
204
|
+
const rClean = runGate(['--gate', '--pkg-root', pkgRoot, '--target', target]);
|
|
205
|
+
check('S12 --gate 干净 fixture → exit 0', rClean.status === 0, `exit=${rClean.status}`);
|
|
206
|
+
const { pkgRoot: dp, target: dt } = mkFixture();
|
|
207
|
+
const rDiff = runGate(['--diff', '--pkg-root', dp, '--target', dt]);
|
|
208
|
+
check('S12 --diff 差异 fixture 仍 exit 0(B-b 默认语义不变)', rDiff.status === 0, `exit=${rDiff.status}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---- 场景 13-14:sha LF 归一(2026-09-27 closing-coverage)——CRLF 等价不误报 / 内容差异照常抓 ----
|
|
212
|
+
{
|
|
213
|
+
// S13 同内容不同行尾(包源 LF / 装副本 CRLF)→ 不报漂移
|
|
214
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-ssc-crlf-'));
|
|
215
|
+
const pkgRoot = path.join(root, 'pkg');
|
|
216
|
+
const target = path.join(root, 'target');
|
|
217
|
+
const pkgBase = path.join(pkgRoot, 'templates', '_agents', 'commands');
|
|
218
|
+
const tgtBase = path.join(target, '.agents', 'commands');
|
|
219
|
+
fs.mkdirSync(pkgBase, { recursive: true });
|
|
220
|
+
fs.mkdirSync(tgtBase, { recursive: true });
|
|
221
|
+
fs.writeFileSync(path.join(pkgBase, 'a.md'), '# 标题\n\n正文行\n- 项目\n');
|
|
222
|
+
fs.writeFileSync(path.join(tgtBase, 'a.md'), '# 标题\r\n\r\n正文行\r\n- 项目\r\n');
|
|
223
|
+
fs.writeFileSync(path.join(pkgBase, 'b.md'), '# B\n');
|
|
224
|
+
fs.writeFileSync(path.join(tgtBase, 'b.md'), '# B\n');
|
|
225
|
+
const r = sourceSyncCheck({ pkgRoot, target });
|
|
226
|
+
check('S13 同内容不同行尾(LF vs CRLF)→ 0 漂移(LF 归一口径)', r.drift.length === 0, JSON.stringify(r.drift));
|
|
227
|
+
}
|
|
228
|
+
{
|
|
229
|
+
// S14 内容真实差异(即使行尾也不同)→ 照常报漂移(归一不掩盖内容差异)
|
|
230
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-ssc-crlf2-'));
|
|
231
|
+
const pkgRoot = path.join(root, 'pkg');
|
|
232
|
+
const target = path.join(root, 'target');
|
|
233
|
+
const pkgBase = path.join(pkgRoot, 'templates', '_agents', 'commands');
|
|
234
|
+
const tgtBase = path.join(target, '.agents', 'commands');
|
|
235
|
+
fs.mkdirSync(pkgBase, { recursive: true });
|
|
236
|
+
fs.mkdirSync(tgtBase, { recursive: true });
|
|
237
|
+
fs.writeFileSync(path.join(pkgBase, 'a.md'), '# v1\n内容甲\n');
|
|
238
|
+
fs.writeFileSync(path.join(tgtBase, 'a.md'), '# v1\r\n内容乙\r\n');
|
|
239
|
+
const r = sourceSyncCheck({ pkgRoot, target });
|
|
240
|
+
check('S14 内容差异(甲 vs 乙)→ 报漂移(归一只认 CRLF 不吞内容)', r.drift.length === 1 && r.drift[0].rel === 'commands/a.md', JSON.stringify(r.drift));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
console.log('\n合计: PASS ' + pass + ' / FAIL ' + failCount);
|
|
244
|
+
process.exit(failCount ? 1 : 0);
|
|
@@ -31,6 +31,23 @@ const walkTree = (dir, rel = '') => {
|
|
|
31
31
|
|
|
32
32
|
// ---- 磁盘枚举(登记口径与 gen-wiki-board.mjs 一致)----
|
|
33
33
|
const isKnowledgeDoc = (f) => /\.(md|html)$/i.test(f) && !/\.visual-check\./i.test(f);
|
|
34
|
+
// gitignore 豁免(F4,incident 2026-09-23-code-review-f3-f4-closeout):drafts-archive 含
|
|
35
|
+
// .gitignore 忽略的本机文件(如 *.xlsx),计入会使生成物计数在 fresh clone 必然漂移;
|
|
36
|
+
// 口径 = 排除忽略文件(git 不可用/非仓库时退化为全量计数,fixture 环境不受影响)
|
|
37
|
+
const gitIgnoredArchive = (() => {
|
|
38
|
+
try {
|
|
39
|
+
const out = execSync('git -c core.quotepath=false status --ignored=matching --porcelain -uall -- wiki',
|
|
40
|
+
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
41
|
+
const prefix = 'wiki/';
|
|
42
|
+
return new Set(out.split(/\r?\n/)
|
|
43
|
+
.filter((l) => l.startsWith('!! '))
|
|
44
|
+
.map((l) => l.slice(3).trim().replace(/\/$/, ''))
|
|
45
|
+
.filter((p) => p.startsWith(prefix))
|
|
46
|
+
.map((p) => p.slice(prefix.length)));
|
|
47
|
+
} catch {
|
|
48
|
+
return new Set();
|
|
49
|
+
}
|
|
50
|
+
})();
|
|
34
51
|
const diskFiles = new Set(); // "主题/文件名"(子目录文件名带 子目录/ 前缀)
|
|
35
52
|
const diskTopics = new Set();
|
|
36
53
|
const activeTree = { files: [], dirs: [] }; // 递归视图(断链守卫 / 台账登记断言用)
|
|
@@ -39,15 +56,18 @@ for (const d of fs.readdirSync(WIKI)) {
|
|
|
39
56
|
if (!fs.statSync(p).isDirectory()) continue;
|
|
40
57
|
if (d === 'drafts-archive') continue;
|
|
41
58
|
diskTopics.add(d);
|
|
59
|
+
// gitignore 豁免消费(p2-batch2 复核 P1-3):gen 侧排除 ignored 本机件,verify 须同口径——
|
|
60
|
+
// 否则「gen 恒写 N、verify 恒数 N+1」门禁死锁且自愈指引(重跑 gen)无效
|
|
61
|
+
const isIgnored = (rel) => { const k = rel.replace(/\/$/, ''); return gitIgnoredArchive.has(`${d}/${k}`) || gitIgnoredArchive.has(`${d}/${k}/`); }; // 尾斜杠归一(复核 P2-a)
|
|
42
62
|
for (const f of fs.readdirSync(p)) {
|
|
43
|
-
if (fs.statSync(path.join(p, f)).isFile()) diskFiles.add(`${d}/${f}`);
|
|
63
|
+
if (fs.statSync(path.join(p, f)).isFile() && !isIgnored(f)) diskFiles.add(`${d}/${f}`);
|
|
44
64
|
}
|
|
45
65
|
if (d !== '数据维护') {
|
|
46
66
|
for (const sub of fs.readdirSync(p)) {
|
|
47
67
|
const sp = path.join(p, sub);
|
|
48
68
|
if (!fs.statSync(sp).isDirectory()) continue;
|
|
49
69
|
for (const f of fs.readdirSync(sp)) {
|
|
50
|
-
if (fs.statSync(path.join(sp, f)).isFile() && isKnowledgeDoc(f)) diskFiles.add(`${d}/${sub}/${f}`);
|
|
70
|
+
if (fs.statSync(path.join(sp, f)).isFile() && isKnowledgeDoc(f) && !isIgnored(`${sub}/${f}`)) diskFiles.add(`${d}/${sub}/${f}`);
|
|
51
71
|
}
|
|
52
72
|
}
|
|
53
73
|
}
|
|
@@ -56,27 +76,11 @@ for (const d of fs.readdirSync(WIKI)) {
|
|
|
56
76
|
activeTree.dirs.push(...sub.dirs);
|
|
57
77
|
}
|
|
58
78
|
|
|
59
|
-
|
|
60
|
-
// .gitignore 忽略的本机文件(如 *.xlsx),计入会使生成物计数在 fresh clone 必然漂移;
|
|
61
|
-
// 口径 = 排除忽略文件(git 不可用/非仓库时退化为全量计数,fixture 环境不受影响)
|
|
62
|
-
const gitIgnoredArchive = (() => {
|
|
63
|
-
try {
|
|
64
|
-
const out = execSync('git -c core.quotepath=false status --ignored=matching --porcelain -uall -- wiki/drafts-archive',
|
|
65
|
-
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
66
|
-
const prefix = 'wiki/drafts-archive/';
|
|
67
|
-
return new Set(out.split(/\r?\n/)
|
|
68
|
-
.filter((l) => l.startsWith('!! '))
|
|
69
|
-
.map((l) => l.slice(3).trim().replace(/\/$/, ''))
|
|
70
|
-
.filter((p) => p.startsWith(prefix))
|
|
71
|
-
.map((p) => p.slice(prefix.length)));
|
|
72
|
-
} catch {
|
|
73
|
-
return new Set();
|
|
74
|
-
}
|
|
75
|
-
})();
|
|
79
|
+
|
|
76
80
|
// drafts-archive 缺失:ENOENT 崩栈改明示失败(本脚本是门禁,协议目录被删应 exit 1 而非崩栈,2026-09-24)
|
|
77
81
|
const ARCHIVE_DIR = path.join(WIKI, 'drafts-archive');
|
|
78
82
|
let archiveCount = 0;
|
|
79
|
-
if (fs.existsSync(ARCHIVE_DIR)) archiveCount = walkTree(ARCHIVE_DIR).files.filter((r) => !gitIgnoredArchive.has(r)).length;
|
|
83
|
+
if (fs.existsSync(ARCHIVE_DIR)) archiveCount = walkTree(ARCHIVE_DIR).files.filter((r) => !gitIgnoredArchive.has(`drafts-archive/${r}`)).length; // 集合键带 drafts-archive/ 前缀(复核 P2-b)
|
|
80
84
|
else problems.push('wiki/drafts-archive/ 目录缺失(wiki 协议结构,模板自带)——归档一致性无从校验');
|
|
81
85
|
|
|
82
86
|
// ---- 看板 DATA 解析 ----
|
|
@@ -86,7 +90,25 @@ if (!m) {
|
|
|
86
90
|
console.error('❌ 看板 DATA 解析失败:知识沉淀总览.html 中找不到 `const DATA = {...}`');
|
|
87
91
|
process.exit(1);
|
|
88
92
|
}
|
|
89
|
-
|
|
93
|
+
// JSON.parse(p2-batch2):DATA 由 gen-wiki-board 经 JSON.stringify 生成、必为合法 JSON——
|
|
94
|
+
// eval 换 JSON.parse 消除门禁脚本对 repo 内文件执行任意 JS 的面;解析失败即 fail-loud 门禁失败
|
|
95
|
+
let DATA;
|
|
96
|
+
try {
|
|
97
|
+
DATA = JSON.parse(m[1]);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
console.error(`❌ 看板 DATA 不是合法 JSON(应跑 gen-wiki-board.mjs 重生成):${e.message}`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
// 合计行比对(p2-batch2):合计行有生成(fail-loud 锚点)无验证——手改/陈旧此前不可见
|
|
103
|
+
{
|
|
104
|
+
const totalM = fs.readFileSync(path.join(WIKI, 'INDEX.md'), 'utf8').match(/合计:\*\*(\d+) 份\*\*知识文档,\*\*(\d+) 个主题\*\*/);
|
|
105
|
+
if (totalM) {
|
|
106
|
+
if (Number(totalM[1]) !== diskFiles.size || Number(totalM[2]) !== diskTopics.size) {
|
|
107
|
+
console.error(`❌ INDEX 合计行与磁盘不一致:合计 ${totalM[1]} 份/${totalM[2]} 主题 vs 磁盘 ${diskFiles.size} 份/${diskTopics.size} 主题——跑 gen-wiki-board.mjs 重生成`);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
90
112
|
const boardFiles = new Set();
|
|
91
113
|
for (const t of DATA.topics) for (const f of t.files) boardFiles.add(`${t.name}/${f.file}`);
|
|
92
114
|
const boardTopics = new Set(DATA.topics.map((t) => t.name));
|
|
@@ -97,8 +119,9 @@ const indexTopicCounts = new Map(); // 速览表 topic → 文件数
|
|
|
97
119
|
for (const line of fs.readFileSync(path.join(WIKI, 'INDEX.md'), 'utf8').split(/\r?\n/)) {
|
|
98
120
|
const mm = line.match(/^\| ([^|]+) \| ([^|]+) \| wiki\/([^/]+)\/[^|]*\|$/);
|
|
99
121
|
if (mm) indexFiles.add(`${mm[3].trim()}/${mm[1].trim()}`);
|
|
100
|
-
|
|
101
|
-
|
|
122
|
+
// 速览行用途列容忍转义管道(p2-batch2 复核 P2-4:gen 侧转义写入后本解析须同款,否则该主题计数核对静默跳过)
|
|
123
|
+
const ov = line.match(/^\| ([^|]+) \| (\d+) \|((?:\\.|[^|])*)\|$/);
|
|
124
|
+
if (ov && ov[1].trim() !== '主题') indexTopicCounts.set(ov[1].trim().replace(/\\\|/g, '|'), Number(ov[2]));
|
|
102
125
|
}
|
|
103
126
|
|
|
104
127
|
const diff = (a, b) => [...a].filter((x) => !b.has(x));
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// verify.mjs — 关单固定编排(2026-09-24 intent closeout-verify-script)
|
|
3
|
-
// 用途:关单前一键过门——按写死顺序执行 npm test → check-loop
|
|
2
|
+
// verify.mjs — 关单固定编排(2026-09-24 intent closeout-verify-script;2026-09-26 check-loop-node 第 2 步直连 node)
|
|
3
|
+
// 用途:关单前一键过门——按写死顺序执行 npm test → check-loop,逐项输出 pass/fail。
|
|
4
4
|
// 语义:只编排不裁决——两条命令都是既有门禁原样跑,本脚本不新增校验、不读配置、无状态;
|
|
5
|
-
// 任一步非零即 fail-fast 退出 1,失败原因经 stdio inherit
|
|
6
|
-
//
|
|
5
|
+
// 任一步非零即 fail-fast 退出 1,失败原因经 stdio inherit 原样透传不吞。
|
|
6
|
+
// check-loop 为 node 实现(经 check-loop.mjs 直跑——node 即本脚本运行时必然可用,
|
|
7
|
+
// 旧「缺 sh 环境 fail-closed」分支随 sh 依赖消失退役)。
|
|
7
8
|
// 测试:node templates/_agents/scripts/verify.test.mjs(--test-cmd 注入假命令 + CHECK_LOOP_ROOT 注入夹具,不触真实 npm test 与 workflow)
|
|
8
9
|
// 用法:node .agents/scripts/verify.mjs [--test-cmd "<shell 命令>"](--test-cmd 仅供测试注入)
|
|
9
10
|
import { spawnSync } from 'node:child_process';
|
|
@@ -13,7 +14,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
13
14
|
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
14
15
|
const args = process.argv.slice(2);
|
|
15
16
|
const testCmd = args.includes('--test-cmd') ? args[args.indexOf('--test-cmd') + 1] : 'npm test';
|
|
16
|
-
const checkLoop = path.join(SCRIPT_DIR, 'check-loop.
|
|
17
|
+
const checkLoop = path.join(SCRIPT_DIR, 'check-loop.mjs');
|
|
17
18
|
|
|
18
19
|
console.log(`[verify] ▶ 1/2 ${testCmd}`);
|
|
19
20
|
const t = spawnSync(testCmd, { shell: true, stdio: 'inherit' });
|
|
@@ -24,12 +25,9 @@ if (t.status !== 0) {
|
|
|
24
25
|
console.log('[verify] ✅ 1/2 测试通过');
|
|
25
26
|
|
|
26
27
|
console.log('[verify] ▶ 2/2 check-loop');
|
|
27
|
-
const c = spawnSync(
|
|
28
|
+
const c = spawnSync(process.execPath, [checkLoop], { stdio: 'inherit' });
|
|
28
29
|
if (c.status !== 0) {
|
|
29
|
-
|
|
30
|
-
? '环境无 sh(装 Git Bash / WSL)——关单门不可跳过,不静默放行'
|
|
31
|
-
: '按上方输出修复后原路重跑';
|
|
32
|
-
console.error(`[verify] ❌ 2/2 check-loop 未通过(exit ${c.status ?? c.error?.code})——${hint}`);
|
|
30
|
+
console.error(`[verify] ❌ 2/2 check-loop 未通过(exit ${c.status ?? c.error?.code})——按上方输出修复后原路重跑`);
|
|
33
31
|
process.exit(1);
|
|
34
32
|
}
|
|
35
33
|
console.log('[verify] ✅ 2/2 闭环校验通过');
|
|
@@ -19,10 +19,22 @@ const check = (desc, cond, detail = '') => {
|
|
|
19
19
|
else { fail++; console.log(`FAIL ${desc}${detail ? `\n${detail}` : ''}`); }
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
// 空闭环夹具(四目录齐全、无文档 → check-loop
|
|
22
|
+
// 空闭环夹具(四目录齐全、无文档 → check-loop 无断档;枚举单源随 fixture 内联——check-loop 启动段 fail-loud 要求)
|
|
23
23
|
const mkfix = () => {
|
|
24
24
|
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'verify-fix-'));
|
|
25
25
|
for (const s of ['intents', 'specs', 'plans', 'incidents']) fs.mkdirSync(path.join(d, 'workflow', s), { recursive: true });
|
|
26
|
+
fs.mkdirSync(path.join(d, '.agents'), { recursive: true });
|
|
27
|
+
fs.writeFileSync(path.join(d, '.agents', 'workflow-enums.txt'), [
|
|
28
|
+
'doc.status.all=draft approved done superseded cancelled',
|
|
29
|
+
'doc.status.confirmed=approved done superseded cancelled',
|
|
30
|
+
'doc.status.active=draft approved',
|
|
31
|
+
'doc.status.terminal=done superseded cancelled',
|
|
32
|
+
'doc.status.abandoned=superseded cancelled',
|
|
33
|
+
'incident.status.all=open fixed closed',
|
|
34
|
+
'incident.status.active=open',
|
|
35
|
+
'level.all=L0 L1 L2 L3',
|
|
36
|
+
'',
|
|
37
|
+
].join('\n'), 'utf8');
|
|
26
38
|
return d;
|
|
27
39
|
};
|
|
28
40
|
// 配对断裂夹具(L1 intent 无同名 plan → hard-block)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// wf-journal — 编排执行 run journal(2026-09-26 orchestration-journal-human,审查改进方向 4)
|
|
2
|
+
// 用途:宿主 AI 按 .agents/workflows/_TEMPLATE.md 纪律,每个 stage 每次尝试出结果(pass/fail/blocked)
|
|
3
|
+
// 记一行 JSONL;中止后重跑先看 status——已 pass 不重跑,从就绪层续(断点续跑与重试计量)。
|
|
4
|
+
// 存储:.agents/cache/orchestration-runs.jsonl(本地运行态缓存,已 gitignore,可丢弃——丢失仅失去
|
|
5
|
+
// 续跑记忆,不影响任何门禁;status 对空 journal 天然全量输出不误报)。
|
|
6
|
+
// 行 schema:{ ts, run, wf, stage, form?, status, attempt, note? }
|
|
7
|
+
// run = <wf>@<YYYYMMDD-HHMM>;同 run 同 stage 多行按末次状态生效(重试后过 = pass),attempt 取最大。
|
|
8
|
+
// 语义口径(与 _TEMPLATE.md 四形态一致;stages 表解析复用 workflows-check.mjs 的 parseStages 单源)。
|
|
9
|
+
// 用法:
|
|
10
|
+
// node .agents/scripts/wf-journal.mjs add --wf <编排名> --stage <id> --status <pass|fail|blocked>
|
|
11
|
+
// [--form role|step|gate|human] [--attempt N] [--note <文本>] [--run <runId>] [--new-run]
|
|
12
|
+
// [--journal <文件>](默认 .agents/cache/orchestration-runs.jsonl,按 cwd)
|
|
13
|
+
// node .agents/scripts/wf-journal.mjs status --wf <编排名> [--run <runId>] [--journal <文件>] [--wf-root <目录>]
|
|
14
|
+
// 测试:node templates/_agents/scripts/wf-journal.test.mjs(fixture + 真实仓库场景)
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { parseStages } from './workflows-check.mjs';
|
|
18
|
+
|
|
19
|
+
function fail(msg) { console.error('❌ ' + msg); process.exit(1); }
|
|
20
|
+
|
|
21
|
+
const DEFAULT_JOURNAL = () => path.join(process.cwd(), '.agents', 'cache', 'orchestration-runs.jsonl');
|
|
22
|
+
const DEFAULT_WF_ROOT = () => path.join(process.cwd(), '.agents', 'workflows');
|
|
23
|
+
const STATUSES = ['pass', 'fail', 'blocked'];
|
|
24
|
+
const FORMS = ['role', 'step', 'gate', 'human'];
|
|
25
|
+
|
|
26
|
+
// 读 journal 原始行(文件不存在/空 → [];目录不存在自动可建由 add 处理)
|
|
27
|
+
export function readJournalLines(journalFile) {
|
|
28
|
+
try {
|
|
29
|
+
return fs.readFileSync(journalFile, 'utf8').split(/\r?\n/).filter((l) => l.trim());
|
|
30
|
+
} catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// replayRun:重放一个 run 的记录 → { state: {stage: {status, attempt, form, note, ts}}, total, bad }
|
|
36
|
+
// 末次状态生效;attempt 取出现过的最大值;坏行跳过(计数返回,调用方决定告警口径)
|
|
37
|
+
export function replayRun(rawLines, run) {
|
|
38
|
+
const state = {};
|
|
39
|
+
let total = 0;
|
|
40
|
+
let bad = 0;
|
|
41
|
+
for (const line of rawLines) {
|
|
42
|
+
let j;
|
|
43
|
+
try { j = JSON.parse(line); } catch { bad++; continue; }
|
|
44
|
+
if (!j || typeof j !== 'object' || j.run !== run) continue;
|
|
45
|
+
if (!j.stage || !STATUSES.includes(j.status)) { bad++; continue; }
|
|
46
|
+
total++;
|
|
47
|
+
const prev = state[j.stage];
|
|
48
|
+
state[j.stage] = {
|
|
49
|
+
status: j.status,
|
|
50
|
+
attempt: Math.max(prev?.attempt ?? 0, Number(j.attempt) || 1),
|
|
51
|
+
...(j.form ? { form: j.form } : {}),
|
|
52
|
+
...(j.note ? { note: j.note } : {}),
|
|
53
|
+
ts: j.ts,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return { state, total, bad };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// latestRun:journal 中该 wf 最后一条的 run id(无记录 → null)
|
|
60
|
+
export function latestRun(rawLines, wf) {
|
|
61
|
+
let run = null;
|
|
62
|
+
for (const line of rawLines) {
|
|
63
|
+
try {
|
|
64
|
+
const j = JSON.parse(line);
|
|
65
|
+
if (j && j.wf === wf && j.run) run = j.run;
|
|
66
|
+
} catch { /* 坏行不参与 run 推断 */ }
|
|
67
|
+
}
|
|
68
|
+
return run;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// planStages:stages(parseStages 产物)× 状态 → { passed, ready, pending, blocked }
|
|
72
|
+
// passed = 末次 pass;ready = 依赖全 pass 且自身未 pass(末次 fail 标注重试就绪);blocked = 自身末次 blocked;pending = 其余
|
|
73
|
+
export function planStages(stages, state) {
|
|
74
|
+
const passed = [];
|
|
75
|
+
const ready = [];
|
|
76
|
+
const pending = [];
|
|
77
|
+
const blocked = [];
|
|
78
|
+
const passedSet = new Set(stages.filter((s) => state[s.id]?.status === 'pass').map((s) => s.id));
|
|
79
|
+
for (const s of stages) {
|
|
80
|
+
const st = state[s.id];
|
|
81
|
+
if (st?.status === 'pass') { passed.push({ id: s.id }); continue; }
|
|
82
|
+
if (st?.status === 'blocked') { blocked.push({ id: s.id, note: st.note || '' }); continue; }
|
|
83
|
+
const deps = s.after ? s.after.split(/[,,、\s]+/).filter(Boolean) : [];
|
|
84
|
+
if (deps.every((d) => passedSet.has(d))) {
|
|
85
|
+
ready.push({ id: s.id, ...(st?.status === 'fail' ? { retry: st.attempt } : {}) });
|
|
86
|
+
} else {
|
|
87
|
+
pending.push({ id: s.id, ...(deps.some((d) => state[d] && state[d].status !== 'pass') && (state[deps.find((d) => state[d] && state[d].status !== 'pass')]?.status === 'blocked') ? { depBlocked: true } : {}) });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { passed, ready, pending, blocked };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
94
|
+
const runIdOf = (wf, d = new Date()) =>
|
|
95
|
+
`${wf}@${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}`;
|
|
96
|
+
|
|
97
|
+
function parseArgs(argv) {
|
|
98
|
+
const out = { cmd: argv[0] };
|
|
99
|
+
for (let i = 1; i < argv.length; i++) {
|
|
100
|
+
const a = argv[i];
|
|
101
|
+
if (a.startsWith('--')) out[a.slice(2)] = argv[++i] ?? true;
|
|
102
|
+
else fail(`未知参数:${a}(用法见文件头注释)`);
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const isMain = process.argv[1] && process.argv[1].endsWith('wf-journal.mjs');
|
|
108
|
+
if (isMain) {
|
|
109
|
+
const a = parseArgs(process.argv.slice(2));
|
|
110
|
+
const journalFile = a.journal || DEFAULT_JOURNAL();
|
|
111
|
+
if (a.cmd === 'add') {
|
|
112
|
+
if (!a.wf || !a.stage || !a.status) fail('add 必填:--wf / --stage / --status(pass|fail|blocked)');
|
|
113
|
+
if (!STATUSES.includes(a.status)) fail(`--status 须为 ${STATUSES.join('|')}(现 ${a.status})`);
|
|
114
|
+
if (a.form && !FORMS.includes(a.form)) fail(`--form 须为 ${FORMS.join('|')}(现 ${a.form})`);
|
|
115
|
+
const lines = readJournalLines(journalFile);
|
|
116
|
+
const run = a.run || (a['new-run'] ? runIdOf(a.wf) : latestRun(lines, a.wf) || runIdOf(a.wf));
|
|
117
|
+
const attempt = a.attempt
|
|
118
|
+
? Number(a.attempt)
|
|
119
|
+
: lines.filter((l) => { try { const j = JSON.parse(l); return j.run === run && j.stage === a.stage; } catch { return false; } }).length + 1;
|
|
120
|
+
const row = {
|
|
121
|
+
ts: new Date().toISOString(), run, wf: a.wf, stage: a.stage,
|
|
122
|
+
status: a.status, attempt,
|
|
123
|
+
...(a.form ? { form: a.form } : {}), ...(a.note ? { note: a.note } : {}),
|
|
124
|
+
};
|
|
125
|
+
fs.mkdirSync(path.dirname(journalFile), { recursive: true });
|
|
126
|
+
fs.appendFileSync(journalFile, JSON.stringify(row) + '\n');
|
|
127
|
+
console.log(`✅ journal += ${a.wf}/${a.stage} ${a.status}(attempt ${attempt},run ${run})`);
|
|
128
|
+
process.exit(0);
|
|
129
|
+
}
|
|
130
|
+
if (a.cmd === 'status') {
|
|
131
|
+
if (!a.wf) fail('status 必填:--wf(编排名)');
|
|
132
|
+
const wfRoot = a['wf-root'] || DEFAULT_WF_ROOT();
|
|
133
|
+
const wfFile = path.join(wfRoot, `${a.wf}.md`);
|
|
134
|
+
if (!fs.existsSync(wfFile)) fail(`找不到编排脚本:${wfFile}`);
|
|
135
|
+
const stages = parseStages(fs.readFileSync(wfFile, 'utf8'));
|
|
136
|
+
if (!stages) fail(`编排脚本缺 stages 表:${wfFile}`);
|
|
137
|
+
const lines = readJournalLines(journalFile);
|
|
138
|
+
const run = a.run || latestRun(lines, a.wf);
|
|
139
|
+
console.log(`▶ wf-journal status — ${a.wf}${run ? ` @ ${run}` : '(无 journal 记录,按首轮输出)'}`);
|
|
140
|
+
if (!run) {
|
|
141
|
+
const { ready, pending } = planStages(stages, {});
|
|
142
|
+
console.log(` ▶ 就绪(第 0 层,${ready.length}):${ready.map((s) => s.id).join('、') || '无'}`);
|
|
143
|
+
console.log(` ⏸ 待定(${pending.length}):${pending.map((s) => s.id).join('、') || '无'}`);
|
|
144
|
+
process.exit(0);
|
|
145
|
+
}
|
|
146
|
+
const { state, total, bad } = replayRun(lines, run);
|
|
147
|
+
if (bad) console.error(`⚠️ journal 坏行 ${bad} 条已跳过(缓存可丢弃,不影响门禁)`);
|
|
148
|
+
const plan = planStages(stages, state);
|
|
149
|
+
console.log(` (${total} 条记录)`);
|
|
150
|
+
console.log(` ✅ 已完成(${plan.passed.length}):${plan.passed.map((s) => s.id).join('、') || '无'}`);
|
|
151
|
+
console.log(` ▶ 就绪(${plan.ready.length}):${plan.ready.map((s) => (s.retry ? `${s.id}(重试就绪×${s.retry})` : s.id)).join('、') || '无'}`);
|
|
152
|
+
console.log(` ⏸ 待定(${plan.pending.length}):${plan.pending.map((s) => s.id + (s.depBlocked ? '(依赖中止)' : '')).join('、') || '无'}`);
|
|
153
|
+
console.log(` ⛔ 中止(${plan.blocked.length}):${plan.blocked.map((s) => `${s.id}${s.note ? `(${s.note})` : ''}`).join('、') || '无'}`);
|
|
154
|
+
process.exit(0);
|
|
155
|
+
}
|
|
156
|
+
fail(`未知命令:${a.cmd ?? ''}(add | status,用法见文件头注释)`);
|
|
157
|
+
}
|
|
158
|
+
export default { replayRun, planStages, latestRun, readJournalLines };
|