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,307 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ledger-shards.mjs — 三个 append-only 台账的分片读写单源(2026-10-13 ledger-shard-ledgers,自源仓回灌)
|
|
3
|
+
//
|
|
4
|
+
// 背景:workflow/{regression-checklist,delegations,papercuts}.md 是 append-only 活文档,每次追加
|
|
5
|
+
// 全量重写整文件——源仓实测 blob 膨胀(regression-checklist 单文件 14.0 MiB、delegations 11.5 MiB,
|
|
6
|
+
// 膨胀/冲突实测数据出处留痕在包源仓 git 历史与 CHANGELOG),且多 worktree 并行时每次合并都可能冲突。
|
|
7
|
+
//
|
|
8
|
+
// 分片形态(沿 confirmations.d 先例,2026-10-12 ledger-shard-default):
|
|
9
|
+
// workflow/<ledger>.d/<YYYY-MM>.md —— 按月切分,每月一个文件
|
|
10
|
+
// 读侧 = 主台账 ∪ 全部分片(按文件名排序,月序即时间序);写侧 = 追加进当月分片。
|
|
11
|
+
// 主台账保留「头部说明 + 维护规则 + 表头」等人类阅读面,数据行迁入分片。
|
|
12
|
+
//
|
|
13
|
+
// 为什么按月而非按条目:三个台账的数据行天然带日期列(首列 YYYY-MM-DD),月边界即天然分片键;
|
|
14
|
+
// 同月并发追加落同一文件(冲突面从「全历史」缩到「当月」),跨月并发零冲突。
|
|
15
|
+
//
|
|
16
|
+
// 合并驱动:.gitattributes 的 merge=jsonl-union 只覆盖 .agents/*.jsonl;本模块的 .md 分片走
|
|
17
|
+
// 追加式表格行,冲突面已由月切分压到最小,不另配驱动(如需可后续加 merge=union)。
|
|
18
|
+
//
|
|
19
|
+
// kit 适配(源仓→kit,2026-10-13 B8 批):表头签名对齐 kit 模板形态——delegations 两表末列为
|
|
20
|
+
// 「合入结论」(源仓为「返工原因」)、regression-checklist 表头为「incident | 条目 | 落点」
|
|
21
|
+
// (源仓为「来源 incident | 回归条目 | 落点」)。数据行判据不变(首列日期即数据行),表头签名
|
|
22
|
+
// 只进分片渲染。分片仍带节标题:kit 侧 wf-journal.delegationSectionRows 按 `## 委派结果` 节门
|
|
23
|
+
// 读行(kit check-loop 检查 18 走表头签名、不依赖节标题——与源仓相反;节标题 + 表头都带则两边读侧全兼容)。
|
|
24
|
+
//
|
|
25
|
+
// 用法(库):
|
|
26
|
+
// import { readLedger, shardPathFor, appendRows } from './ledger-shards.mjs';
|
|
27
|
+
// const { main, shards, all } = readLedger(root, 'delegations');
|
|
28
|
+
// 用法(CLI,迁移/巡检/追加):
|
|
29
|
+
// node .agents/scripts/ledger-shards.mjs --check # 巡检:主台账残留数据行 / 分片可读性
|
|
30
|
+
// node .agents/scripts/ledger-shards.mjs --split <ledger> # 把主台账数据行按月迁入分片(幂等)
|
|
31
|
+
// node .agents/scripts/ledger-shards.mjs --append <ledger> --row "<行>" [--month YYYY-MM]
|
|
32
|
+
// 测试:node .agents/scripts/ledger-shards.test.mjs
|
|
33
|
+
import fs from 'node:fs';
|
|
34
|
+
import path from 'node:path';
|
|
35
|
+
|
|
36
|
+
// 受管台账清单(单源):键 = 台账基名,值 = 数据行判定
|
|
37
|
+
// - 表格式台账(delegations/papercuts):整文件数据行按月迁出
|
|
38
|
+
// - 分节台账(regression-checklist):只有「防复发验证」节是 append-only 追加区(incident 追加区),
|
|
39
|
+
// 其余模块节是稳定参考内容——故用 section 限定,只迁该节的数据行
|
|
40
|
+
export const LEDGERS = {
|
|
41
|
+
'regression-checklist': {
|
|
42
|
+
section: '防复发验证', // 只迁此节内的数据行(节标题前缀匹配)
|
|
43
|
+
row: /^\s*\|\s*\d{4}-\d{2}-\d{2}/, // 表格式:首列日期
|
|
44
|
+
},
|
|
45
|
+
delegations: {
|
|
46
|
+
row: /^\s*\|\s*\d{4}-\d{2}-\d{2}/,
|
|
47
|
+
},
|
|
48
|
+
papercuts: {
|
|
49
|
+
row: /^\s*\|\s*\d{4}-\d{2}-\d{2}/,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// 表格式台账的表头签名(分片须自带表头——读侧按表头识别数据边界,见 agg-delegations.splitTables)。
|
|
54
|
+
// delegations 有两张表(委派表 / 自做表),分片须两张都带且行归入正确表下。
|
|
55
|
+
// **归属判据 = 原节标题**(`## 委派结果` / `## 自做任务结果`),不用列数——末列「合入结论」可缺省、
|
|
56
|
+
// 存量行不回填,两表都可能出现 5 列行,列数无法区分两表。
|
|
57
|
+
// **分片须同时带节标题**:kit 读侧 wf-journal.delegationSectionRows 要求行落在 `## 委派结果`
|
|
58
|
+
// 节内(节门);只带表头会让 wf-journal 读到 0 行。kit check-loop 检查 18 与
|
|
59
|
+
// agg-delegations.splitTables 走表头签名(节标题只服务人类阅读)——节标题 + 表头都带则两边读侧全兼容。
|
|
60
|
+
const TABLE_HEADERS = {
|
|
61
|
+
delegations: [
|
|
62
|
+
{ section: '委派结果', header: ['## 委派结果', '', '| 日期 | 被委派方(模型) | 任务一句话 | 结果 | 备注 | 合入结论 |', '|------|----------------|------------|------|------|----------|'] },
|
|
63
|
+
{ section: '自做任务结果', header: ['## 自做任务结果', '', '| 日期 | 任务一句话 | 结果 | 备注 | 合入结论 |', '|------|------------|------|------|----------|'] },
|
|
64
|
+
],
|
|
65
|
+
papercuts: [
|
|
66
|
+
{ section: null, header: ['| 日期 | 位置 | 症状 | 拟修 |', '|------|------|------|------|'] },
|
|
67
|
+
],
|
|
68
|
+
'regression-checklist': [
|
|
69
|
+
{ section: null, header: ['| incident | 条目 | 落点 |', '|----------|------|------|'] },
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// tableIndexOf(name, line, section):该行属于哪张表。
|
|
74
|
+
// 多表台账(delegations)按**原节标题**归属(列数不可靠,见 TABLE_HEADERS 注释);
|
|
75
|
+
// 单表台账恒 0。section 为行所在节的标题(调用方按 `## ` 滚动维护)。
|
|
76
|
+
function tableIndexOf(name, line, section = '') {
|
|
77
|
+
const specs = TABLE_HEADERS[name];
|
|
78
|
+
if (!specs || specs.length === 1) return 0;
|
|
79
|
+
for (let i = 0; i < specs.length; i++) {
|
|
80
|
+
if (specs[i].section && String(section).startsWith(specs[i].section)) return i;
|
|
81
|
+
}
|
|
82
|
+
return 0; // 无节信息(旧分片/裸行)→ 归首表(委派表),沿原主台账行序语义
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function ledgerPath(root, name) {
|
|
86
|
+
return path.join(root, 'workflow', `${name}.md`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function shardDir(root, name) {
|
|
90
|
+
return path.join(root, 'workflow', `${name}.d`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function shardPathFor(root, name, month) {
|
|
94
|
+
return path.join(shardDir(root, name), `${month}.md`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// monthOf(line):从数据行提取分片月(YYYY-MM)。表格式取首列日期;清单式无日期 → 返回 ''(调用方兜底)。
|
|
98
|
+
export function monthOf(name, line) {
|
|
99
|
+
const m = /(\d{4})-(\d{2})-\d{2}/.exec(line);
|
|
100
|
+
return m ? `${m[1]}-${m[2]}` : '';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// isDataRow(name, line):该行是否属于「应迁入分片」的数据行(不含节上下文)。
|
|
104
|
+
export function isDataRow(name, line) {
|
|
105
|
+
const spec = LEDGERS[name];
|
|
106
|
+
if (!spec) return false;
|
|
107
|
+
if (!spec.row.test(line)) return false;
|
|
108
|
+
// 表头/分隔行排除
|
|
109
|
+
if (/^\s*\|/.test(line)) {
|
|
110
|
+
const cells = line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim());
|
|
111
|
+
if (!cells.length || cells.every((c) => /^[-: ]*$/.test(c))) return false;
|
|
112
|
+
if (cells[0] === '日期' || cells[0] === '来源 incident' || cells[0] === 'incident') return false;
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// inScope(name, section):当前节是否属于该台账的迁移范围(无 section 限定 → 恒 true)。
|
|
118
|
+
export function inScope(name, section) {
|
|
119
|
+
const spec = LEDGERS[name];
|
|
120
|
+
if (!spec || !spec.section) return true;
|
|
121
|
+
return String(section).startsWith(spec.section);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// readLedger(root, name):主台账 ∪ 全部分片。返回 { main, shards: [{file, text}], all }。
|
|
125
|
+
// 分片按文件名排序(月序 = 时间序);读失败 fail-loud(指标缺数会喂错门禁,沿 agg-delegations 口径)。
|
|
126
|
+
export function readLedger(root, name) {
|
|
127
|
+
const mainP = ledgerPath(root, name);
|
|
128
|
+
let main = '';
|
|
129
|
+
if (fs.existsSync(mainP)) {
|
|
130
|
+
try { main = fs.readFileSync(mainP, 'utf8'); }
|
|
131
|
+
catch (err) { throw new Error(`台账读取失败: ${mainP} (${err.message})`); }
|
|
132
|
+
}
|
|
133
|
+
const dir = shardDir(root, name);
|
|
134
|
+
const shards = [];
|
|
135
|
+
if (fs.existsSync(dir) && fs.statSync(dir).isDirectory()) {
|
|
136
|
+
let files;
|
|
137
|
+
try { files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort(); }
|
|
138
|
+
catch (err) { throw new Error(`分片目录遍历失败: ${dir} (${err.message})`); }
|
|
139
|
+
for (const f of files) {
|
|
140
|
+
const p = path.join(dir, f);
|
|
141
|
+
try { shards.push({ file: f, text: fs.readFileSync(p, 'utf8') }); }
|
|
142
|
+
catch (err) { throw new Error(`分片读取失败: ${f} (${err.message})`); }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const all = [main, ...shards.map((s) => s.text)].join('\n');
|
|
146
|
+
return { main, shards, all };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// readLedgerText(root, name):便捷形态——只要合并文本(读侧最小改动接入点)。
|
|
150
|
+
export function readLedgerText(root, name) {
|
|
151
|
+
return readLedger(root, name).all;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// readDelegationsText(root, file):delegations 合并文本(主台账 ∪ delegations.d/*.md)。
|
|
155
|
+
// 供读侧消费者共用(单源,禁各自拼路径):
|
|
156
|
+
// - agg-delegations.splitTables(表头签名识别)
|
|
157
|
+
// - check-loop.delegationResultRows(表头签名识别,检查 18)
|
|
158
|
+
// - wf-journal.delegationSectionRows(`## 委派结果` 节门)
|
|
159
|
+
// file 语义:**仅当它不是本仓默认主台账路径时**才作显式覆盖(测试注入临时文件用);
|
|
160
|
+
// 传默认路径(消费者按 `path.join(ROOT,'workflow','delegations.md')` 拼出来的)→ 走分片合并,
|
|
161
|
+
// 否则分片被静默跳过(2026-10-13 源仓实测踩中:检查 18 读到 0 行)。
|
|
162
|
+
export function readDelegationsText(root, file = null) {
|
|
163
|
+
const def = ledgerPath(root, 'delegations');
|
|
164
|
+
if (file && path.resolve(file) !== path.resolve(def)) {
|
|
165
|
+
try { return fs.readFileSync(file, 'utf8'); } catch { return ''; }
|
|
166
|
+
}
|
|
167
|
+
try { return readLedgerText(root, 'delegations'); } catch { return ''; }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// splitMain(root, name, { dryRun }):把主台账里的数据行按月迁入分片(幂等:已迁走的行不再出现)。
|
|
171
|
+
// 返回 { moved: {month: count}, kept: 主台账保留行数 }。主台账保留非数据行(说明/规则/表头/生成区)。
|
|
172
|
+
export function splitMain(root, name, { dryRun = false } = {}) {
|
|
173
|
+
const mainP = ledgerPath(root, name);
|
|
174
|
+
if (!fs.existsSync(mainP)) throw new Error(`台账不存在: ${mainP}`);
|
|
175
|
+
const raw = fs.readFileSync(mainP, 'utf8');
|
|
176
|
+
const eol = raw.includes('\r\n') ? '\r\n' : '\n';
|
|
177
|
+
const lines = raw.split(/\r?\n/);
|
|
178
|
+
const kept = [];
|
|
179
|
+
const byMonth = new Map();
|
|
180
|
+
let section = '';
|
|
181
|
+
for (const line of lines) {
|
|
182
|
+
const h = /^##\s+(.+?)\s*$/.exec(line);
|
|
183
|
+
if (h) section = h[1];
|
|
184
|
+
if (!inScope(name, section) || !isDataRow(name, line)) { kept.push(line); continue; }
|
|
185
|
+
const month = monthOf(name, line);
|
|
186
|
+
if (!month) { kept.push(line); continue; } // 无日期行不迁
|
|
187
|
+
if (!byMonth.has(month)) byMonth.set(month, []);
|
|
188
|
+
byMonth.get(month).push({ line, section }); // 带节信息(多表台账按节归属表)
|
|
189
|
+
}
|
|
190
|
+
const moved = {};
|
|
191
|
+
for (const [month, rows] of byMonth) moved[month] = rows.length;
|
|
192
|
+
if (dryRun) return { moved, kept: kept.length, byMonth };
|
|
193
|
+
// 写分片:已存在的分片保留原内容,新行追加——**按行去重**(幂等:重跑不重复追加)。
|
|
194
|
+
// 2026-10-13 源仓实测踩中:无去重时二次 split 把 58 行重复写入(120 行 vs 62 行)。
|
|
195
|
+
for (const [month, rows] of byMonth) {
|
|
196
|
+
const p = shardPathFor(root, name, month);
|
|
197
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
198
|
+
const existing = fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : '';
|
|
199
|
+
const existingRows = new Set(existing.split(/\r?\n/).filter((l) => l.trim()));
|
|
200
|
+
const fresh = rows.filter((r) => !existingRows.has(r.line));
|
|
201
|
+
if (!fresh.length) continue; // 全部已在分片 → 不写(幂等)
|
|
202
|
+
const next = existing
|
|
203
|
+
? `${existing.replace(/\s*$/, '')}${eol}${fresh.map((r) => r.line).join(eol)}${eol}`
|
|
204
|
+
: renderShard(name, fresh, eol);
|
|
205
|
+
fs.writeFileSync(p, next, 'utf8');
|
|
206
|
+
}
|
|
207
|
+
// 主台账:保留行 + 尾部空行规整
|
|
208
|
+
const out = kept.join(eol).replace(/(\r?\n)+$/, '') + eol;
|
|
209
|
+
fs.writeFileSync(mainP, out, 'utf8');
|
|
210
|
+
return { moved, kept: kept.length };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// renderShard(name, rows, eol):完整分片内容 = 表头 + 行(行归入各自表头之下)。
|
|
214
|
+
// 多表台账(delegations)必须逐表「表头 + 该表行」交错输出——否则读侧 splitTables 会把
|
|
215
|
+
// 后一张表的行按前一张表解析(2026-10-13 源仓实测:240 任务塌成 100)。
|
|
216
|
+
// rows 项 = { line, section }(section 为原节标题,用于表归属);rows 为空 → 出全部表头(空分片骨架)。
|
|
217
|
+
export function renderShard(name, rows, eol = '\n') {
|
|
218
|
+
const specs = TABLE_HEADERS[name];
|
|
219
|
+
if (!specs) return rows.length ? `${rows.map((r) => r.line ?? r).join(eol)}${eol}` : '';
|
|
220
|
+
const out = [];
|
|
221
|
+
for (let i = 0; i < specs.length; i++) {
|
|
222
|
+
const mine = rows.filter((r) => tableIndexOf(name, r.line ?? r, r.section) === i);
|
|
223
|
+
if (rows.length && !mine.length) continue; // 有行但本表无行 → 不出空表头
|
|
224
|
+
out.push(...specs[i].header, ...mine.map((r) => r.line ?? r));
|
|
225
|
+
}
|
|
226
|
+
return out.length ? `${out.join(eol)}${eol}` : '';
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// appendRows(root, name, rows, { month, section }):把数据行追加进指定月分片(缺省取行内日期月)。
|
|
230
|
+
// rows 可为字符串数组或 { line, section } 数组(多表台账须带 section 才能归对表)。
|
|
231
|
+
// 返回实际写入的分片路径列表。写失败 fail-loud(不静默吞)。
|
|
232
|
+
export function appendRows(root, name, rows, { month, section = '' } = {}) {
|
|
233
|
+
const written = [];
|
|
234
|
+
const byMonth = new Map();
|
|
235
|
+
for (const row of rows) {
|
|
236
|
+
const line = typeof row === 'string' ? row : row.line;
|
|
237
|
+
const sec = typeof row === 'string' ? section : (row.section ?? section);
|
|
238
|
+
const m = month || monthOf(name, line);
|
|
239
|
+
if (!m) throw new Error(`appendRows: 无法确定分片月(行无日期且未传 month): ${String(line).slice(0, 80)}`);
|
|
240
|
+
if (!byMonth.has(m)) byMonth.set(m, []);
|
|
241
|
+
byMonth.get(m).push({ line, section: sec });
|
|
242
|
+
}
|
|
243
|
+
for (const [m, rs] of byMonth) {
|
|
244
|
+
const p = shardPathFor(root, name, m);
|
|
245
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
246
|
+
const existing = fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : '';
|
|
247
|
+
const existingRows = new Set(existing.split(/\r?\n/).filter((l) => l.trim()));
|
|
248
|
+
const fresh = rs.filter((r) => !existingRows.has(r.line));
|
|
249
|
+
if (!fresh.length) { written.push(p); continue; } // 已在分片 → 幂等跳过
|
|
250
|
+
const next = existing
|
|
251
|
+
? `${existing.replace(/\s*$/, '')}\n${fresh.map((r) => r.line).join('\n')}\n`
|
|
252
|
+
: renderShard(name, fresh, '\n');
|
|
253
|
+
fs.writeFileSync(p, next, 'utf8');
|
|
254
|
+
written.push(p);
|
|
255
|
+
}
|
|
256
|
+
return written;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ---- CLI:巡检 / 迁移 / 追加 ----
|
|
260
|
+
function main() {
|
|
261
|
+
const argv = process.argv.slice(2);
|
|
262
|
+
const root = process.cwd();
|
|
263
|
+
if (argv.includes('--check')) {
|
|
264
|
+
let bad = 0;
|
|
265
|
+
for (const name of Object.keys(LEDGERS)) {
|
|
266
|
+
const mainP = ledgerPath(root, name);
|
|
267
|
+
if (!fs.existsSync(mainP)) { console.log(`[skip] ${name}: 主台账不存在`); continue; }
|
|
268
|
+
const lines = fs.readFileSync(mainP, 'utf8').split(/\r?\n/);
|
|
269
|
+
const residual = lines.filter((l) => isDataRow(name, l)).length;
|
|
270
|
+
let shardRows = 0;
|
|
271
|
+
try {
|
|
272
|
+
const { shards } = readLedger(root, name);
|
|
273
|
+
for (const s of shards) shardRows += s.text.split(/\r?\n/).filter((l) => isDataRow(name, l)).length;
|
|
274
|
+
} catch (err) { console.error(`[FAIL] ${name}: ${err.message}`); bad++; continue; }
|
|
275
|
+
console.log(`[${residual ? 'WARN' : 'OK'}] ${name}: 主台账残留数据行 ${residual}|分片数据行 ${shardRows}`);
|
|
276
|
+
if (residual) bad++;
|
|
277
|
+
}
|
|
278
|
+
process.exit(bad ? 1 : 0);
|
|
279
|
+
}
|
|
280
|
+
const splitIdx = argv.indexOf('--split');
|
|
281
|
+
if (splitIdx >= 0) {
|
|
282
|
+
const name = argv[splitIdx + 1];
|
|
283
|
+
if (!LEDGERS[name]) { console.error(`--split 须指定台账名(${Object.keys(LEDGERS).join('|')})`); process.exit(2); }
|
|
284
|
+
const dry = argv.includes('--dry-run');
|
|
285
|
+
const r = splitMain(root, name, { dryRun: dry });
|
|
286
|
+
console.log(`${dry ? '[dry] ' : ''}${name}: 迁出 ${Object.values(r.moved).reduce((a, b) => a + b, 0)} 行 → ${JSON.stringify(r.moved)}|主台账保留 ${r.kept} 行`);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
// --append <ledger> --row "<行>" [--month YYYY-MM]:追加一行进当月分片(写侧唯一入口)
|
|
290
|
+
const appIdx = argv.indexOf('--append');
|
|
291
|
+
if (appIdx >= 0) {
|
|
292
|
+
const name = argv[appIdx + 1];
|
|
293
|
+
if (!LEDGERS[name]) { console.error(`--append 须指定台账名(${Object.keys(LEDGERS).join('|')})`); process.exit(2); }
|
|
294
|
+
const rowIdx = argv.indexOf('--row');
|
|
295
|
+
const row = rowIdx >= 0 ? argv[rowIdx + 1] : '';
|
|
296
|
+
if (!row) { console.error('--append 须带 --row "<数据行>"'); process.exit(2); }
|
|
297
|
+
const mIdx = argv.indexOf('--month');
|
|
298
|
+
const month = mIdx >= 0 ? argv[mIdx + 1] : undefined;
|
|
299
|
+
const written = appendRows(root, name, [row], { month });
|
|
300
|
+
console.log(`✓ 追加 1 行 → ${written.map((p) => path.relative(root, p).split(path.sep).join('/')).join(', ')}`);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
console.error('用法:node .agents/scripts/ledger-shards.mjs --check | --split <ledger> [--dry-run] | --append <ledger> --row "<行>" [--month YYYY-MM]');
|
|
304
|
+
process.exit(2);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('ledger-shards.mjs')) main();
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ledger-shards.test.mjs — 分片读写单源 fixture(2026-10-13 ledger-shard-ledgers,自源仓回灌)
|
|
3
|
+
// 覆盖:数据行判定(含节限定)/ 月提取 / 主台账∪分片读 / 迁移幂等 / 追加 / 表头自带 / fail-loud
|
|
4
|
+
// kit 适配:表头签名用 kit 模板形态(delegations 末列「合入结论」、regression-checklist「incident | 条目 | 落点」);
|
|
5
|
+
// S22 钉住 kit 适配(分片渲染须出 kit 表头、不出源仓表头——防再同步漂移)。
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import {
|
|
10
|
+
LEDGERS, isDataRow, inScope, monthOf, readLedger, readLedgerText,
|
|
11
|
+
splitMain, appendRows, shardPathFor, renderShard,
|
|
12
|
+
} from './ledger-shards.mjs';
|
|
13
|
+
|
|
14
|
+
let pass = 0;
|
|
15
|
+
let fail = 0;
|
|
16
|
+
const check = (desc, cond, detail = '') => {
|
|
17
|
+
if (cond) { pass++; console.log(`PASS ${desc}`); }
|
|
18
|
+
else { fail++; console.log(`FAIL ${desc}${detail ? '\n ' + detail : ''}`); }
|
|
19
|
+
};
|
|
20
|
+
const mkroot = () => {
|
|
21
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ledger-shards-'));
|
|
22
|
+
fs.mkdirSync(path.join(root, 'workflow'), { recursive: true });
|
|
23
|
+
return root;
|
|
24
|
+
};
|
|
25
|
+
const wf = (root, ...p) => path.join(root, 'workflow', ...p);
|
|
26
|
+
|
|
27
|
+
// ---- S1 数据行判定:表格式(delegations/papercuts)----
|
|
28
|
+
{
|
|
29
|
+
check('S1a delegations 数据行命中', isDataRow('delegations', '| 2026-10-01 | ui-verifier | 任务 | 一次通过 | 备注 |'));
|
|
30
|
+
check('S1b delegations 表头排除', !isDataRow('delegations', '| 日期 | 被委派方(模型) | 任务一句话 | 结果 | 备注 | 合入结论 |'));
|
|
31
|
+
check('S1c delegations 分隔行排除', !isDataRow('delegations', '|------|----------------|------------|------|------|----------|'));
|
|
32
|
+
check('S1d papercuts 数据行命中', isDataRow('papercuts', '| 2026-10-11 | `.agents/x.mjs` | 症状 | 拟修 |'));
|
|
33
|
+
check('S1e papercuts 表头排除', !isDataRow('papercuts', '| 日期 | 位置 | 症状 | 拟修 |'));
|
|
34
|
+
check('S1f 说明行不命中', !isDataRow('papercuts', '> 说明文字,非数据行'));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---- S2 节限定:regression-checklist 只迁「防复发验证」节 ----
|
|
38
|
+
{
|
|
39
|
+
check('S2a 防复发验证节在范围内', inScope('regression-checklist', '防复发验证'));
|
|
40
|
+
check('S2b 模块节不在范围内', !inScope('regression-checklist', '回归必过'));
|
|
41
|
+
check('S2c 无 section 限定的台账恒在范围', inScope('delegations', '任意节') && inScope('papercuts', ''));
|
|
42
|
+
check('S2d 防复发数据行命中', isDataRow('regression-checklist', '| 2026-09-11 某事故 | 条目 | 落点 |'));
|
|
43
|
+
check('S2e 防复发表头排除', !isDataRow('regression-checklist', '| incident | 条目 | 落点 |'));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---- S3 月提取 ----
|
|
47
|
+
{
|
|
48
|
+
check('S3a 表格式取首列日期月', monthOf('delegations', '| 2026-10-01 | x |') === '2026-10');
|
|
49
|
+
check('S3b 无日期返回空', monthOf('delegations', '| 无日期 | x |') === '');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---- S4 读:主台账 ∪ 分片 ----
|
|
53
|
+
{
|
|
54
|
+
const root = mkroot();
|
|
55
|
+
fs.mkdirSync(wf(root, 'papercuts.d'), { recursive: true });
|
|
56
|
+
fs.writeFileSync(wf(root, 'papercuts.md'), '# 台账\n\n| 日期 | 位置 | 症状 | 拟修 |\n|------|------|------|------|\n| 2026-10-01 | 主表行 | s | f |\n', 'utf8');
|
|
57
|
+
fs.writeFileSync(wf(root, 'papercuts.d', '2026-09.md'), renderShard('papercuts', []) + '| 2026-09-01 | 分片行 | s | f |\n', 'utf8');
|
|
58
|
+
const r = readLedger(root, 'papercuts');
|
|
59
|
+
check('S4a 主台账 + 分片都读到', r.main.includes('主表行') && r.shards.length === 1 && r.all.includes('分片行'));
|
|
60
|
+
check('S4b all 含两侧', r.all.includes('主表行') && r.all.includes('分片行'));
|
|
61
|
+
check('S4c readLedgerText 等价 all', readLedgerText(root, 'papercuts') === r.all);
|
|
62
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---- S5 读:分片按文件名排序(月序 = 时间序)----
|
|
66
|
+
{
|
|
67
|
+
const root = mkroot();
|
|
68
|
+
fs.mkdirSync(wf(root, 'papercuts.d'), { recursive: true });
|
|
69
|
+
fs.writeFileSync(wf(root, 'papercuts.md'), '# 台账\n', 'utf8');
|
|
70
|
+
fs.writeFileSync(wf(root, 'papercuts.d', '2026-10.md'), '| 2026-10-01 | 十月 | s | f |\n', 'utf8');
|
|
71
|
+
fs.writeFileSync(wf(root, 'papercuts.d', '2026-09.md'), '| 2026-09-01 | 九月 | s | f |\n', 'utf8');
|
|
72
|
+
const r = readLedger(root, 'papercuts');
|
|
73
|
+
check('S5 分片按文件名排序(09 在 10 前)', r.shards[0].file === '2026-09.md' && r.shards[1].file === '2026-10.md');
|
|
74
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---- S6 读:无分片目录 → 等价原读法(向后兼容)----
|
|
78
|
+
{
|
|
79
|
+
const root = mkroot();
|
|
80
|
+
fs.writeFileSync(wf(root, 'papercuts.md'), '# 台账\n| 2026-10-01 | 行 | s | f |\n', 'utf8');
|
|
81
|
+
const r = readLedger(root, 'papercuts');
|
|
82
|
+
check('S6 无分片目录 → shards 空、all = main', r.shards.length === 0 && r.all === r.main);
|
|
83
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---- S7 迁移:主台账数据行按月迁出,非数据行保留 ----
|
|
87
|
+
{
|
|
88
|
+
const root = mkroot();
|
|
89
|
+
fs.writeFileSync(wf(root, 'papercuts.md'), [
|
|
90
|
+
'# 台账', '', '> 说明', '',
|
|
91
|
+
'| 日期 | 位置 | 症状 | 拟修 |', '|------|------|------|------|',
|
|
92
|
+
'| 2026-09-01 | 九月行 | s | f |',
|
|
93
|
+
'| 2026-10-01 | 十月行 | s | f |',
|
|
94
|
+
'| 2026-10-02 | 十月行2 | s | f |', '',
|
|
95
|
+
].join('\n'), 'utf8');
|
|
96
|
+
const r = splitMain(root, 'papercuts');
|
|
97
|
+
check('S7a 迁移计数正确', r.moved['2026-09'] === 1 && r.moved['2026-10'] === 2, JSON.stringify(r.moved));
|
|
98
|
+
const main = fs.readFileSync(wf(root, 'papercuts.md'), 'utf8');
|
|
99
|
+
check('S7b 主台账保留说明与表头、无数据行', main.includes('# 台账') && main.includes('| 日期 |') && !main.includes('九月行') && !main.includes('十月行'));
|
|
100
|
+
check('S7c 分片自带表头', fs.readFileSync(wf(root, 'papercuts.d', '2026-09.md'), 'utf8').includes('| 日期 | 位置 | 症状 | 拟修 |'));
|
|
101
|
+
check('S7d 分片含数据行', fs.readFileSync(wf(root, 'papercuts.d', '2026-10.md'), 'utf8').includes('十月行2'));
|
|
102
|
+
// 迁移后读回:数据不丢
|
|
103
|
+
const back = readLedgerText(root, 'papercuts');
|
|
104
|
+
check('S7e 迁移后读回数据完整', back.includes('九月行') && back.includes('十月行') && back.includes('十月行2'));
|
|
105
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---- S8 迁移幂等:二次 split 无数据行可迁 ----
|
|
109
|
+
{
|
|
110
|
+
const root = mkroot();
|
|
111
|
+
fs.writeFileSync(wf(root, 'papercuts.md'), '| 日期 | 位置 | 症状 | 拟修 |\n|------|------|------|------|\n| 2026-10-01 | 行 | s | f |\n', 'utf8');
|
|
112
|
+
splitMain(root, 'papercuts');
|
|
113
|
+
const r2 = splitMain(root, 'papercuts');
|
|
114
|
+
check('S8 二次迁移 0 行(幂等)', Object.keys(r2.moved).length === 0, JSON.stringify(r2.moved));
|
|
115
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---- S9 节限定迁移:regression-checklist 只迁防复发节 ----
|
|
119
|
+
{
|
|
120
|
+
const root = mkroot();
|
|
121
|
+
fs.writeFileSync(wf(root, 'regression-checklist.md'), [
|
|
122
|
+
'# 回归清单', '',
|
|
123
|
+
'## 回归必过', '',
|
|
124
|
+
'| 模块 | 条目 | 依据 |', '|------|------|------|',
|
|
125
|
+
'| 模块条目(不迁) | x | y |', '',
|
|
126
|
+
'## 防复发验证', '',
|
|
127
|
+
'| incident | 条目 | 落点 |', '|----------|------|------|',
|
|
128
|
+
'| 2026-09-11 某事故 | 条目 | 落点 |', '',
|
|
129
|
+
].join('\n'), 'utf8');
|
|
130
|
+
const r = splitMain(root, 'regression-checklist');
|
|
131
|
+
check('S9a 只迁防复发节 1 行', r.moved['2026-09'] === 1, JSON.stringify(r.moved));
|
|
132
|
+
const main = fs.readFileSync(wf(root, 'regression-checklist.md'), 'utf8');
|
|
133
|
+
check('S9b 模块节条目留守', main.includes('| 模块条目(不迁) | x | y |'));
|
|
134
|
+
check('S9c 防复发数据行已迁出', !main.includes('2026-09-11 某事故'));
|
|
135
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---- S10 appendRows:追加进指定月分片,缺表头则补 ----
|
|
139
|
+
{
|
|
140
|
+
const root = mkroot();
|
|
141
|
+
appendRows(root, 'papercuts', ['| 2026-10-13 | 新行 | s | f |']);
|
|
142
|
+
const p = shardPathFor(root, 'papercuts', '2026-10');
|
|
143
|
+
check('S10a 分片文件已建', fs.existsSync(p));
|
|
144
|
+
const txt = fs.readFileSync(p, 'utf8');
|
|
145
|
+
check('S10b 自带表头 + 数据行', txt.includes('| 日期 | 位置 | 症状 | 拟修 |') && txt.includes('新行'));
|
|
146
|
+
appendRows(root, 'papercuts', ['| 2026-10-14 | 第二行 | s | f |']);
|
|
147
|
+
const txt2 = fs.readFileSync(p, 'utf8');
|
|
148
|
+
check('S10c 二次追加不重复表头', (txt2.match(/\| 日期 \| 位置/g) || []).length === 1 && txt2.includes('第二行'));
|
|
149
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---- S11 appendRows:显式 month 覆盖(无日期行)----
|
|
153
|
+
{
|
|
154
|
+
const root = mkroot();
|
|
155
|
+
appendRows(root, 'papercuts', ['| 无日期行 | s | f |'], { month: '2026-10' });
|
|
156
|
+
check('S11 显式 month 生效', fs.existsSync(shardPathFor(root, 'papercuts', '2026-10')));
|
|
157
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---- S12 fail-loud:无法确定月 → 抛(不静默吞)----
|
|
161
|
+
{
|
|
162
|
+
const root = mkroot();
|
|
163
|
+
let thrown = null;
|
|
164
|
+
try { appendRows(root, 'papercuts', ['| 无日期行 | s | f |']); } catch (e) { thrown = e; }
|
|
165
|
+
check('S12 无日期且未传 month → 抛错', thrown !== null && /无法确定分片月/.test(thrown.message));
|
|
166
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ---- S13 fail-loud:分片目录读取异常 → 抛 ----
|
|
170
|
+
{
|
|
171
|
+
const root = mkroot();
|
|
172
|
+
fs.writeFileSync(wf(root, 'papercuts.md'), '# 台账\n', 'utf8');
|
|
173
|
+
fs.mkdirSync(wf(root, 'papercuts.d'), { recursive: true });
|
|
174
|
+
fs.mkdirSync(wf(root, 'papercuts.d', 'bad.md')); // 目录冒充分片 → readFileSync EISDIR
|
|
175
|
+
let thrown = null;
|
|
176
|
+
try { readLedger(root, 'papercuts'); } catch (e) { thrown = e; }
|
|
177
|
+
check('S13 分片读失败 → 抛(不静默吞)', thrown !== null && /分片读取失败/.test(thrown.message));
|
|
178
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---- S14 台账清单单源:三个台账都在册 ----
|
|
182
|
+
{
|
|
183
|
+
check('S14 三台账在册', Object.keys(LEDGERS).sort().join(',') === 'delegations,papercuts,regression-checklist');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ---- S15 delegations 双表分组:分片须两张表头都带且行归入正确表 ----
|
|
187
|
+
// (2026-10-13 源仓迁移实测踩中:只带一张表头 → splitTables 把另一张的行按错表解析,240 任务塌成 100)
|
|
188
|
+
{
|
|
189
|
+
const root = mkroot();
|
|
190
|
+
fs.writeFileSync(wf(root, 'delegations.md'), [
|
|
191
|
+
'# 台账', '',
|
|
192
|
+
'## 委派结果', '',
|
|
193
|
+
'| 日期 | 被委派方(模型) | 任务一句话 | 结果 | 备注 | 合入结论 |',
|
|
194
|
+
'|------|----------------|------------|------|------|----------|',
|
|
195
|
+
'| 2026-09-01 | ui-verifier | 委派任务 | 一次通过 | 备注 | |',
|
|
196
|
+
'',
|
|
197
|
+
'## 自做任务结果', '',
|
|
198
|
+
'| 日期 | 任务一句话 | 结果 | 备注 | 合入结论 |',
|
|
199
|
+
'|------|------------|------|------|----------|',
|
|
200
|
+
'| 2026-09-02 | 自做任务 | 一次通过 | 备注 | |',
|
|
201
|
+
'',
|
|
202
|
+
].join('\n'), 'utf8');
|
|
203
|
+
splitMain(root, 'delegations');
|
|
204
|
+
const shard = fs.readFileSync(wf(root, 'delegations.d', '2026-09.md'), 'utf8');
|
|
205
|
+
check('S15a 分片含委派表头(kit 形态)', shard.includes('| 日期 | 被委派方(模型) | 任务一句话 | 结果 | 备注 | 合入结论 |'));
|
|
206
|
+
check('S15b 分片含自做表头(kit 形态)', shard.includes('| 日期 | 任务一句话 | 结果 | 备注 | 合入结论 |'));
|
|
207
|
+
// 委派行须在委派表头之后、自做表头之前
|
|
208
|
+
const iDeleg = shard.indexOf('被委派方(模型)');
|
|
209
|
+
const iSelf = shard.indexOf('| 日期 | 任务一句话');
|
|
210
|
+
const iRow = shard.indexOf('委派任务');
|
|
211
|
+
check('S15c 委派行归入委派表(表头之后、自做表头之前)', iDeleg < iRow && iRow < iSelf, `deleg=${iDeleg} row=${iRow} self=${iSelf}`);
|
|
212
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ---- S16 renderShard 分组:有行时不出空表头 ----
|
|
216
|
+
{
|
|
217
|
+
const onlyDeleg = renderShard('delegations', [{ line: '| 2026-09-01 | a | b | c | d | e |', section: '委派结果' }]);
|
|
218
|
+
check('S16a 只有委派行 → 不出自做表头', onlyDeleg.includes('被委派方(模型)') && !onlyDeleg.includes('| 日期 | 任务一句话'));
|
|
219
|
+
const onlySelf = renderShard('delegations', [{ line: '| 2026-09-01 | b | c | d | e |', section: '自做任务结果' }]);
|
|
220
|
+
check('S16b 只有自做行 → 不出委派表头', !onlySelf.includes('被委派方(模型)') && onlySelf.includes('| 日期 | 任务一句话'));
|
|
221
|
+
const empty = renderShard('delegations', []);
|
|
222
|
+
check('S16c 无行 → 出全部表头(空分片骨架)', empty.includes('被委派方(模型)') && empty.includes('| 日期 | 任务一句话'));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ---- S17 幂等:分片已存在时重跑 split 不重复追加 ----
|
|
226
|
+
// (2026-10-13 源仓实测踩中:无去重时二次 split 把 58 行重复写入,120 行 vs 62 行)
|
|
227
|
+
{
|
|
228
|
+
const root = mkroot();
|
|
229
|
+
fs.writeFileSync(wf(root, 'papercuts.md'), [
|
|
230
|
+
'| 日期 | 位置 | 症状 | 拟修 |', '|------|------|------|------|',
|
|
231
|
+
'| 2026-09-01 | 行A | s | f |',
|
|
232
|
+
'| 2026-09-02 | 行B | s | f |', '',
|
|
233
|
+
].join('\n'), 'utf8');
|
|
234
|
+
splitMain(root, 'papercuts');
|
|
235
|
+
const after1 = fs.readFileSync(wf(root, 'papercuts.d', '2026-09.md'), 'utf8');
|
|
236
|
+
// 模拟「主台账被还原后重跑」(分片仍在)
|
|
237
|
+
fs.writeFileSync(wf(root, 'papercuts.md'), [
|
|
238
|
+
'| 日期 | 位置 | 症状 | 拟修 |', '|------|------|------|------|',
|
|
239
|
+
'| 2026-09-01 | 行A | s | f |',
|
|
240
|
+
'| 2026-09-02 | 行B | s | f |', '',
|
|
241
|
+
].join('\n'), 'utf8');
|
|
242
|
+
splitMain(root, 'papercuts');
|
|
243
|
+
const after2 = fs.readFileSync(wf(root, 'papercuts.d', '2026-09.md'), 'utf8');
|
|
244
|
+
check('S17 重跑 split 不重复追加(幂等)', after1 === after2, `len1=${after1.length} len2=${after2.length}`);
|
|
245
|
+
const rows = after2.split(/\r?\n/).filter((l) => /^\| 20/.test(l));
|
|
246
|
+
check('S17b 行数仍为 2', rows.length === 2, `rows=${rows.length}`);
|
|
247
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ---- S18 appendRows 幂等:同行走两次只落一次 ----
|
|
251
|
+
{
|
|
252
|
+
const root = mkroot();
|
|
253
|
+
appendRows(root, 'papercuts', ['| 2026-10-13 | 同行 | s | f |']);
|
|
254
|
+
appendRows(root, 'papercuts', ['| 2026-10-13 | 同行 | s | f |']);
|
|
255
|
+
const txt = fs.readFileSync(shardPathFor(root, 'papercuts', '2026-10'), 'utf8');
|
|
256
|
+
const rows = txt.split(/\r?\n/).filter((l) => /^\| 20/.test(l));
|
|
257
|
+
check('S18 同行走两次只落一次', rows.length === 1, `rows=${rows.length}`);
|
|
258
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ---- S19 嵌入 `|` 的行不丢(2026-10-13 源仓实测踩中:4 行因列数虚高被 renderShard 丢弃)----
|
|
262
|
+
{
|
|
263
|
+
const root = mkroot();
|
|
264
|
+
const weird = '| 2026-09-17 | `.agents/x.mjs` | 正则 `[:=]\\s*["\']?` 含字面竖线 | 拟修 |';
|
|
265
|
+
fs.writeFileSync(wf(root, 'papercuts.md'), [
|
|
266
|
+
'| 日期 | 位置 | 症状 | 拟修 |', '|------|------|------|------|',
|
|
267
|
+
'| 2026-09-01 | 正常行 | s | f |',
|
|
268
|
+
weird, '',
|
|
269
|
+
].join('\n'), 'utf8');
|
|
270
|
+
splitMain(root, 'papercuts');
|
|
271
|
+
const shard = fs.readFileSync(wf(root, 'papercuts.d', '2026-09.md'), 'utf8');
|
|
272
|
+
check('S19a 嵌入竖线的行不丢', shard.includes('含字面竖线'), shard.slice(0, 200));
|
|
273
|
+
check('S19b 正常行也在', shard.includes('正常行'));
|
|
274
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ---- S20 多表按节归属:委派行(含嵌入竖线)仍归委派表 ----
|
|
278
|
+
{
|
|
279
|
+
const root = mkroot();
|
|
280
|
+
const weirdDeleg = '| 2026-09-01 | ui-verifier | 任务含 `a|b` 竖线 | 一次通过 | 备注 | |';
|
|
281
|
+
fs.writeFileSync(wf(root, 'delegations.md'), [
|
|
282
|
+
'## 委派结果', '',
|
|
283
|
+
'| 日期 | 被委派方(模型) | 任务一句话 | 结果 | 备注 | 合入结论 |',
|
|
284
|
+
'|------|----------------|------------|------|------|----------|',
|
|
285
|
+
weirdDeleg, '',
|
|
286
|
+
'## 自做任务结果', '',
|
|
287
|
+
'| 日期 | 任务一句话 | 结果 | 备注 | 合入结论 |',
|
|
288
|
+
'|------|------------|------|------|----------|',
|
|
289
|
+
'| 2026-09-02 | 自做任务 | 一次通过 | 备注 | |', '',
|
|
290
|
+
].join('\n'), 'utf8');
|
|
291
|
+
splitMain(root, 'delegations');
|
|
292
|
+
const shard = fs.readFileSync(wf(root, 'delegations.d', '2026-09.md'), 'utf8');
|
|
293
|
+
const iDeleg = shard.indexOf('被委派方(模型)');
|
|
294
|
+
const iSelf = shard.indexOf('| 日期 | 任务一句话');
|
|
295
|
+
const iWeird = shard.indexOf('任务含');
|
|
296
|
+
check('S20 嵌入竖线的委派行仍归委派表', iDeleg < iWeird && iWeird < iSelf, `deleg=${iDeleg} weird=${iWeird} self=${iSelf}`);
|
|
297
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ---- S21 分片须带节标题(wf-journal.delegationSectionRows 的节门)----
|
|
301
|
+
// (2026-10-13 源仓实测踩中:只带表头 → 节门读侧读到 0 行 → 静默漏报)
|
|
302
|
+
{
|
|
303
|
+
const root = mkroot();
|
|
304
|
+
fs.writeFileSync(wf(root, 'delegations.md'), [
|
|
305
|
+
'## 委派结果', '',
|
|
306
|
+
'| 日期 | 被委派方(模型) | 任务一句话 | 结果 | 备注 | 合入结论 |',
|
|
307
|
+
'|------|----------------|------------|------|------|----------|',
|
|
308
|
+
'| 2026-09-01 | ui-verifier | 委派任务 | 一次通过 | 备注 | |', '',
|
|
309
|
+
].join('\n'), 'utf8');
|
|
310
|
+
splitMain(root, 'delegations');
|
|
311
|
+
const shard = fs.readFileSync(wf(root, 'delegations.d', '2026-09.md'), 'utf8');
|
|
312
|
+
check('S21a 分片带 `## 委派结果` 节标题', /^##\s+委派结果\s*$/m.test(shard), shard.slice(0, 120));
|
|
313
|
+
// 复刻 wf-journal.delegationSectionRows 的节门判据(不 break 形态)
|
|
314
|
+
let inSec = false;
|
|
315
|
+
let rows = 0;
|
|
316
|
+
for (const line of shard.split(/\r?\n/)) {
|
|
317
|
+
if (/^##\s+/.test(line)) { inSec = /^##\s+委派结果\s*$/.test(line); continue; }
|
|
318
|
+
if (!inSec || !/^\s*\|/.test(line)) continue;
|
|
319
|
+
const cells = line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim());
|
|
320
|
+
if (!cells.length || cells.every((c) => /^[-: ]*$/.test(c))) continue;
|
|
321
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(cells[0])) continue;
|
|
322
|
+
rows++;
|
|
323
|
+
}
|
|
324
|
+
check('S21b 节门口径能读到分片行', rows === 1, `rows=${rows}`);
|
|
325
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ---- S22 kit 适配钉子:分片渲染出 kit 表头、不出源仓表头 ----
|
|
329
|
+
// (kit delegations 两表末列 =「合入结论」、regression-checklist 表头 =「incident | 条目 | 落点」;
|
|
330
|
+
// 再自源仓同步本模块时若把源仓表头带回来,此组先红)
|
|
331
|
+
{
|
|
332
|
+
const deleg = renderShard('delegations', []);
|
|
333
|
+
check('S22a delegations 分片骨架出「合入结论」表头', deleg.includes('合入结论') && !deleg.includes('返工原因'));
|
|
334
|
+
const regr = renderShard('regression-checklist', []);
|
|
335
|
+
check('S22b regression-checklist 分片骨架出 kit 表头', regr.includes('| incident | 条目 | 落点 |') && !regr.includes('来源 incident'));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
339
|
+
process.exit(fail ? 1 : 0);
|