agentic-flow-kit 1.6.0 → 1.7.15
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 +40 -6
- package/modules/gates/README.md +1 -0
- package/modules/gates/test-protect/README.md +23 -0
- package/modules/gates/test-protect/check-test-protect.mjs +64 -0
- package/modules/gates/test-protect/local-pre-commit.line +1 -0
- package/modules/hosts/README.md +57 -0
- package/modules/hosts/claude/commands/wf-build.md +5 -24
- package/modules/hosts/claude/commands/wf-deploy.md +1 -0
- package/modules/hosts/claude/commands/wf-design.md +2 -2
- package/modules/hosts/claude/commands/wf-gate-checklist.md +1 -3
- package/modules/hosts/claude/commands/wf-plan.md +1 -1
- package/modules/hosts/claude/commands/wf-source-sync-check.md +1 -3
- package/modules/hosts/claude/commands/wf-sync-hosts.md +1 -3
- package/modules/hosts/claude/commands/wf-test.md +5 -4
- package/modules/hosts/codex/commands/wf-build.md +5 -24
- package/modules/hosts/codex/commands/wf-deploy.md +1 -0
- package/modules/hosts/codex/commands/wf-design.md +2 -2
- package/modules/hosts/codex/commands/wf-gate-checklist.md +1 -3
- package/modules/hosts/codex/commands/wf-plan.md +1 -1
- package/modules/hosts/codex/commands/wf-source-sync-check.md +1 -3
- package/modules/hosts/codex/commands/wf-sync-hosts.md +1 -3
- package/modules/hosts/codex/commands/wf-test.md +5 -4
- package/modules/hosts/cursor/commands/wf-build.md +5 -24
- package/modules/hosts/cursor/commands/wf-deploy.md +1 -0
- package/modules/hosts/cursor/commands/wf-design.md +2 -2
- package/modules/hosts/cursor/commands/wf-gate-checklist.md +1 -3
- package/modules/hosts/cursor/commands/wf-plan.md +1 -1
- package/modules/hosts/cursor/commands/wf-source-sync-check.md +1 -3
- package/modules/hosts/cursor/commands/wf-sync-hosts.md +1 -3
- package/modules/hosts/cursor/commands/wf-test.md +5 -4
- package/modules/hosts/opencode/commands/wf-build.md +5 -24
- package/modules/hosts/opencode/commands/wf-deploy.md +1 -0
- package/modules/hosts/opencode/commands/wf-design.md +2 -2
- package/modules/hosts/opencode/commands/wf-gate-checklist.md +1 -3
- package/modules/hosts/opencode/commands/wf-plan.md +1 -1
- package/modules/hosts/opencode/commands/wf-source-sync-check.md +1 -3
- package/modules/hosts/opencode/commands/wf-sync-hosts.md +1 -3
- package/modules/hosts/opencode/commands/wf-test.md +5 -4
- package/modules/hosts/trae/commands/wf-build.md +5 -24
- package/modules/hosts/trae/commands/wf-deploy.md +1 -0
- package/modules/hosts/trae/commands/wf-design.md +2 -2
- package/modules/hosts/trae/commands/wf-gate-checklist.md +1 -3
- package/modules/hosts/trae/commands/wf-plan.md +1 -1
- package/modules/hosts/trae/commands/wf-source-sync-check.md +1 -3
- package/modules/hosts/trae/commands/wf-sync-hosts.md +1 -3
- package/modules/hosts/trae/commands/wf-test.md +5 -4
- package/package.json +2 -2
- package/src/add-gate.mjs +25 -1
- package/src/add-gate.test.mjs +103 -0
- package/src/cli.mjs +17 -2
- package/src/doctor.mjs +99 -11
- package/src/gates.test.mjs +17 -0
- package/src/init.mjs +7 -1
- package/src/init.test.mjs +19 -0
- package/src/instance-ref.test.mjs +62 -0
- package/src/jscpd-drift.test.mjs +144 -0
- package/src/render.mjs +5 -0
- package/src/rule-budget.test.mjs +103 -0
- package/src/run-tests.mjs +16 -9
- package/src/sync-hosts.mjs +4 -2
- package/src/trae-hooks.test.mjs +15 -3
- package/src/validate.mjs +72 -0
- package/src/validate.test.mjs +95 -0
- package/templates/AGENTS.md +3 -1
- package/templates/_agents/commands/build.md +5 -24
- package/templates/_agents/commands/deploy.md +1 -0
- package/templates/_agents/commands/design.md +2 -2
- package/templates/_agents/commands/gate-checklist.md +1 -3
- package/templates/_agents/commands/plan.md +1 -1
- package/templates/_agents/commands/source-sync-check.md +1 -3
- package/templates/_agents/commands/sync-hosts.md +1 -3
- package/templates/_agents/commands/test.md +5 -4
- package/templates/_agents/control-surfaces.txt +53 -0
- package/templates/_agents/scripts/agg-delegations.cjs +51 -6
- package/templates/_agents/scripts/agg-delegations.test.mjs +79 -1
- package/templates/_agents/scripts/check-engine-integrity.mjs +288 -0
- package/templates/_agents/scripts/check-engine-integrity.test.mjs +227 -0
- package/templates/_agents/scripts/check-lane-surface.mjs +7 -2
- package/templates/_agents/scripts/check-ledger-invariant.mjs +18 -0
- package/templates/_agents/scripts/check-ledger-invariant.test.mjs +16 -0
- package/templates/_agents/scripts/check-ledger.test.mjs +2 -0
- package/templates/_agents/scripts/check-loop.mjs +247 -5
- package/templates/_agents/scripts/check-loop.test.mjs +367 -2
- package/templates/_agents/scripts/confirm-doc.mjs +50 -3
- package/templates/_agents/scripts/confirm-doc.test.mjs +93 -1
- package/templates/_agents/scripts/doctor.test.mjs +251 -1
- package/templates/_agents/scripts/draft-sediment.mjs +178 -0
- package/templates/_agents/scripts/draft-sediment.test.mjs +272 -0
- package/templates/_agents/scripts/gate-checklist.mjs +10 -2
- package/templates/_agents/scripts/gate-checklist.test.mjs +7 -1
- package/templates/_agents/scripts/gen-wiki-board.mjs +61 -6
- package/templates/_agents/scripts/gen-wiki-board.test.mjs +74 -1
- package/templates/_agents/scripts/gen-workflow-dashboard.mjs +2 -2
- package/templates/_agents/scripts/gen-workflow-metrics.mjs +11 -5
- package/templates/_agents/scripts/kb-search.mjs +87 -12
- package/templates/_agents/scripts/kb-search.test.mjs +83 -0
- package/templates/_agents/scripts/pipeline-run.mjs +1 -1
- package/templates/_agents/scripts/policy.mjs +49 -0
- package/templates/_agents/scripts/solidify-task.mjs +1 -1
- package/templates/_agents/scripts/source-sync-check.test.mjs +9 -4
- package/templates/_agents/scripts/stage-gates.mjs +48 -1
- package/templates/_agents/scripts/stage-gates.test.mjs +105 -0
- package/templates/_agents/scripts/verify-wiki-consistency.mjs +34 -0
- package/templates/_agents/scripts/verify-wiki-consistency.test.mjs +44 -2
- package/templates/_agents/scripts/verify.mjs +12 -4
- package/templates/_agents/scripts/verify.test.mjs +30 -3
- package/templates/_agents/scripts-test-exempt.txt +3 -1
- package/templates/_agents/skills/wiki/SKILL.md +5 -3
- package/templates/wiki/INDEX.md +44 -1
- package/templates/wiki//347/237/245/350/257/206/346/262/211/346/267/200/346/200/273/350/247/210.html +22 -1
- package/templates/workflow/delegations.md +5 -4
- package/templates/workflow/incidents/_TEMPLATE.md +2 -0
- package/templates/workflow/intents/_TEMPLATE.md +3 -0
- package/templates/workflow/plans/_TEMPLATE.md +4 -0
- package/templates/workflow/specs/_TEMPLATE.md +23 -0
|
@@ -24,6 +24,12 @@
|
|
|
24
24
|
// 6. 子智能体角色契约 + OpenCode/Trae/ZCode Adapter 一致性(含旧委派残留/钉死模型) [warning]
|
|
25
25
|
// 7. 阶段索引同步(AGENTS.md 与 new-task.md 须双向索引全部阶段指令) [warning]
|
|
26
26
|
// 8. intent 验收标准对账(新建:done 未勾验/缺节=hard,勾选缺证据=warning;存量聚合 warning,含「存量对账豁免」声明者出账)
|
|
27
|
+
// + 场景覆盖子判据(2026-10-10 scenario-coverage,灰度 warning):同名 spec 有「验收场景」节时,
|
|
28
|
+
// done intent 验收标准须逐场景 ID 引用(场景:S1…),缺引用=warning;spec 无该节则不适用(零变化)
|
|
29
|
+
// + THEN 断言子判据(2026-10-11 then-assertion,灰度 warning):声明场景缺「- THEN」行=空壳场景,
|
|
30
|
+
// 聚合一条「场景无断言」warning(THEN 是机器对账锚;节不存在=零变化)
|
|
31
|
+
// + 引用断链子判据(2026-10-11 dangling-ref,灰度 warning):验收标准引用 spec 未声明的场景 ID
|
|
32
|
+
// (含 spec 无场景节)=「场景引用断链」warning——场景未覆盖的反向对账,共用同一次解析
|
|
27
33
|
// **生效日锚=git 首次加入日期(2026-09-28 改;此前取文件名前 10 字符——命名规范强制的字段、
|
|
28
34
|
// 写早零成本,一条 hard 门可被平凡绕过;incident 2026-09-28-check8-git-anchor 实证)**:
|
|
29
35
|
// 非 git / 查不到加入记录 → 不可判定 → 走存量口径(不误报 hard)
|
|
@@ -31,6 +37,8 @@
|
|
|
31
37
|
// 9. 文件名英文 kebab-case(非 ASCII 文件名=warning,2026-09-11 规则)
|
|
32
38
|
// 10. 级别 vs 迁移文件一致性(L1/L2 入口文档加入提交触及迁移 SQL/Migrations=疑似判低,warning)
|
|
33
39
|
// 11. 生成物漂移:workflow/INDEX.md(2026-09-21 检索层) + workflow/DASHBOARD.md(2026-10-08 扩覆盖面)=warning;
|
|
40
|
+
// wiki 侧(wiki/INDEX.md + 看板 DATA)由 doctor §6b 经 gen-wiki-board --check 校验,不在此重复
|
|
41
|
+
// (2026-10-10:wiki 生成区此前零机器校验,现与 workflow 侧对称为 doctor 单路);
|
|
34
42
|
// 12. frontmatter「模块:」合法性(枚举非法 / 2026-09-22 起新建缺字段=warning;词表单源 .agents/workflow-modules.txt)
|
|
35
43
|
// 13. 常驻面体积预算(超限=warning;判定单源 rule-budget.sh——经 sh 调用,
|
|
36
44
|
// 无 sh 环境静默跳过:advisory 级且 pre-commit 侧在 git 钩子 sh 环境照常硬拦)
|
|
@@ -60,6 +68,13 @@
|
|
|
60
68
|
// 按 prev 复原跳转前文本重算 sha256 与台账全量比对,不符=hard「确认内容漂移」;状态行保分隔符换值
|
|
61
69
|
// (非规范格式不误伤);台账行缺 prev 降级 warning;关单编辑顺序新约定:勾验/回填先于关单确认,
|
|
62
70
|
// confirm-doc 是最后一次写入)
|
|
71
|
+
// + scope 子项(2026-10-09 审查 A1 干净版):受管行(ts≥policy.scopeSince,**时刻**锚,缺键 v1-v6 整体跳过
|
|
72
|
+
// →存量 406 行零新增告警)须带机器派生的改动面 scope{files,sha256}——写入侧 confirm-doc.machineScope
|
|
73
|
+
// 自动派生(被确认文档 + `git status --porcelain` 未提交改动集),**AI 无输入面**,分辨力从
|
|
74
|
+
// 「一句可复用的用户原话」换成「机器实测的这 N 个文件」。判据:缺 scope→warning(灰度第一档);
|
|
75
|
+
// scope.files 重算 sha256 不符→hard(「改了 files 没改摘要」= 同一信任边界内手改痕迹,兜住整文件
|
|
76
|
+
// 重写重建链后的二次篡改)。**边界**:scope 证「放行时盘面上哪些文件在动」,不证「这些改动语义正确」
|
|
77
|
+
// ——后者由 fingerprint(文档内容绑定)+ commit diff(事后对质)承担,不越界宣称
|
|
63
78
|
// + 并录批次审计子检查(2026-09-28 batch-ledger-audit 改判据;**读调用事实,不猜时间戳模式**):
|
|
64
79
|
// 判据 = 台账行按 batch 分组、组内 of>1(delegated 行) → warning「确认并录」;batch/seq/of 由
|
|
65
80
|
// confirm-doc 写入时记录(那才是"本次调用落几份"确定已知的时刻)。旧判据「同 quote + 相邻 ts<2s」
|
|
@@ -111,7 +126,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
111
126
|
import { createHash } from 'node:crypto';
|
|
112
127
|
import { loadEnums } from './workflow-enums.mjs';
|
|
113
128
|
import { runCheck16 } from './check-metric-claims.mjs';
|
|
114
|
-
import { auditEnabled, loadKitPolicy, hasFreshVerifyLine, ledgerChainHash } from './policy.mjs';
|
|
129
|
+
import { auditEnabled, loadKitPolicy, hasFreshVerifyLine, ledgerChainHash, scopeMatches } from './policy.mjs';
|
|
115
130
|
import { laneOfEntry } from './stage-gates.mjs';
|
|
116
131
|
// MARK_RE:incident 留痕形态的单源(第七轮复核 N3——此前 check-loop 内联复制一份同口径字面量,
|
|
117
132
|
// 两处靠注释与人工同步;改为复用 stage-gates.mjs 的导出,从结构上消除漂移可能)
|
|
@@ -149,7 +164,7 @@ function takeOpts() {
|
|
|
149
164
|
return opts;
|
|
150
165
|
}
|
|
151
166
|
|
|
152
|
-
// ---- runCheckLoop:门禁主流程可 import 化(2026-10-08 checkloop-importable;
|
|
167
|
+
// ---- runCheckLoop:门禁主流程可 import 化(2026-10-08 checkloop-importable;2026-10-04 isMain 行点名单独立项)----
|
|
153
168
|
// import 本模块零副作用(isMain 主守卫,board-kb-p1 先例);opts = { root?, rev?, hardening? }——root 显式参数
|
|
154
169
|
// 优先于 CHECK_LOOP_ROOT env 优先于 git toplevel(CLI 缺省行为逐字节不变);返回 { exitCode, blockers, warnings },
|
|
155
170
|
// 输出打印照旧在本函数内(稳定输出契约:banner 后空行、条目 `- ` 前缀、HARD-BLOCK/WARN 两段式——消费者 .githooks/pre-push)。
|
|
@@ -161,7 +176,7 @@ function takeOpts() {
|
|
|
161
176
|
|
|
162
177
|
// writeGateStats:门禁 ROI 落盘(2026-10-08 gate-roi-metrics)——append 一行到
|
|
163
178
|
// <root>/.agents/cache/gate-stats.jsonl(该目录已 gitignore,属运行时数据、不入 git、不进 kit.json
|
|
164
|
-
// managed 台账——刻意避开
|
|
179
|
+
// managed 台账——刻意避开 2026-10-05 探针临时件被 sync 收编的事故面)。
|
|
165
180
|
// IO 失败只出账不阻断(沿 verify.mjs 凭证落账同款容错——度量失败不该让门禁变成红灯)。
|
|
166
181
|
export function writeGateStats(root, segs) {
|
|
167
182
|
try {
|
|
@@ -318,6 +333,18 @@ export function runCheckLoop(opts = {}) {
|
|
|
318
333
|
return '';
|
|
319
334
|
}
|
|
320
335
|
|
|
336
|
+
// 落点空壳告警(2026-10-11 landing-reality,沉淀追踪第三级,incident/intent 两侧共用):
|
|
337
|
+
// 「沉淀: <路径>」存在 ≠ 有效——文件存在但 sedimentLandingIsShell 为真(无来源 frontmatter
|
|
338
|
+
// 且非占位正文不足 3 行)→ WARN 落点空壳(advisory,灰度第一档)。调用方须先出过「沉淀断档」
|
|
339
|
+
// 分支(文件不存在),故此处 readFileSync 异常 = 真读取故障,按空壳出账并在文案注明读取失败。
|
|
340
|
+
const warnSedimentShell = (single, sed) => {
|
|
341
|
+
let text = null;
|
|
342
|
+
try { text = fs.readFileSync(path.resolve(ROOT, sed), 'utf8'); } catch { text = null; }
|
|
343
|
+
if (!sedimentLandingIsShell(text)) return;
|
|
344
|
+
const readFail = text === null ? ';落点文件读取失败' : '';
|
|
345
|
+
warnings.push(`- [WARN 落点空壳] ${single} frontmatter「沉淀: ${sed}」指向的文件存在但无有效内容(无来源 frontmatter 且非占位正文不足 3 行${readFail})——沉淀须真实整理而非空壳占位(advisory,灰度第一档)`);
|
|
346
|
+
};
|
|
347
|
+
|
|
321
348
|
const WF = 'workflow';
|
|
322
349
|
const DOC_DIRS = ['intents', 'specs', 'plans', 'incidents'];
|
|
323
350
|
const blockers = [];
|
|
@@ -452,9 +479,25 @@ export function runCheckLoop(opts = {}) {
|
|
|
452
479
|
}
|
|
453
480
|
|
|
454
481
|
// 三件套:正文行锚定 /^1\. /^2\. /^3\. (sh awk 同款)
|
|
482
|
+
let trioOk = true;
|
|
455
483
|
for (const n of [1, 2, 3]) {
|
|
456
484
|
if (!lines.some((l) => l.startsWith(`${n}. `))) {
|
|
457
485
|
warnings.push(`- [WARN 三件套不全] ${name} 缺复盘三件套之 ${n}`);
|
|
486
|
+
trioOk = false;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// 沉淀追踪(incident 侧,与检查 8 intent 侧对称):closed 且三件套齐全 = 最高价值沉淀源。
|
|
490
|
+
// frontmatter「沉淀: <相对仓根路径>」登记去向;缺登记 → WARN 未沉淀(advisory,灰度第一档);
|
|
491
|
+
// 登记了但文件不存在 → WARN 沉淀断档;存在但空壳(无来源且非占位正文不足 3 行)→ WARN 落点空壳。
|
|
492
|
+
// 同「沉淀: 无」显式豁免口径。
|
|
493
|
+
if (incSt === 'closed' && trioOk) {
|
|
494
|
+
const sed = fmGet(inc, '沉淀');
|
|
495
|
+
if (!sed) {
|
|
496
|
+
warnings.push(`- [WARN 未沉淀] ${name} 复盘三件套齐全(closed)但未登记沉淀去向——整理到 wiki 后在 frontmatter 记「沉淀: wiki/<主题>/<文件>」,或显式写「沉淀: 无」(advisory,灰度第一档;沉淀是人工判断,机器只提醒不强制)`);
|
|
497
|
+
} else if (sed !== '无' && !fs.existsSync(path.resolve(ROOT, sed))) {
|
|
498
|
+
warnings.push(`- [WARN 沉淀断档] ${name} frontmatter「沉淀: ${sed}」指向的文件不存在——沉淀文件移动/改名后须同步更新本字段(advisory,灰度第一档)`);
|
|
499
|
+
} else if (sed !== '无') {
|
|
500
|
+
warnSedimentShell(name, sed); // 落点真实性(P1-3):存在但空壳 → WARN 落点空壳
|
|
458
501
|
}
|
|
459
502
|
}
|
|
460
503
|
const hasParent = lines.some((l) => l.includes('是否需要新 intent'));
|
|
@@ -735,7 +778,7 @@ export function runCheckLoop(opts = {}) {
|
|
|
735
778
|
// --- 8. intent 验收标准对账(done 须逐条勾验并补证据;新建 hard,存量聚合 warning)---
|
|
736
779
|
markGate('8', '验收标准对账');
|
|
737
780
|
// 生效日锚 2026-09-28 改「git 首次加入日期」(此前取文件名前 10 字符——命名规范强制的字段、
|
|
738
|
-
//
|
|
781
|
+
// 写早零成本)。非 git → 不可判定 → 走存量口径(不误报 hard)。
|
|
739
782
|
{
|
|
740
783
|
const accCutoff = '2026-09-12';
|
|
741
784
|
let legacyUnaccounted = 0;
|
|
@@ -870,6 +913,65 @@ export function runCheckLoop(opts = {}) {
|
|
|
870
913
|
else if (!ex) legacyUnaccounted++;
|
|
871
914
|
continue;
|
|
872
915
|
}
|
|
916
|
+
// 场景覆盖子判据(2026-10-10 scenario-coverage,灰度第一档 = warning):
|
|
917
|
+
// 同名 spec 写了「## 验收场景」节(GIVEN/WHEN/THEN,每条带 ID:S1、S2…)时,
|
|
918
|
+
// done intent 的验收标准须**逐场景 ID 引用**(`场景:S1`)。缺引用只出 warning——
|
|
919
|
+
// 有意不进 hard:新判据先灰度观察存量装户告警噪音,沿检查 8 自身 / 检查 15 / 检查 18
|
|
920
|
+
// 「先 WARN、装户吃过警告后升 FAIL」的既有路径;spec 无该节 = 原判据零变化。
|
|
921
|
+
// ID 提取单源 = 本块(spec 侧),引用匹配只认验收标准节内的 `场景:S<n>` 行。
|
|
922
|
+
// 引用断链子判据(2026-10-11 dangling-ref,P2-1,灰度第一档 = warning):
|
|
923
|
+
// 上判据的**反向对账**——验收标准引用了 spec 未声明的场景 ID = 悬空引用(改 ID 漏改 / 复制粘贴
|
|
924
|
+
// 残留的机器信号);spec 无场景节但引用了 S<n> 同样算断链(引用了不存在的场景)。与「场景未覆盖」
|
|
925
|
+
// 共用同一次 ID 解析与引用遍历(covered 一份两用),不重复解析。
|
|
926
|
+
// THEN 断言子判据(2026-10-11 then-assertion,P2-2,灰度第一档 = warning):
|
|
927
|
+
// 声明了场景却无 THEN 断言行 = 空壳场景——THEN 是可观测结果的机器对账锚(GIVEN/WHEN 可省),
|
|
928
|
+
// 聚合一条不逐场景刷屏;验收场景节本身可选,节不存在 → 零变化。
|
|
929
|
+
const specPath = path.join(ROOT, WF, 'specs', base);
|
|
930
|
+
if (fs.existsSync(specPath)) {
|
|
931
|
+
const sids = scenarioIdsOf(specPath);
|
|
932
|
+
// 引用遍历单次(覆盖/断链两方向共用):不锁进 sids.length 分支——spec 无场景节时引用同样悬空
|
|
933
|
+
const covered = new Set();
|
|
934
|
+
for (const line of lines) {
|
|
935
|
+
const m = line.match(/场景[::]\s*S(\d+)/);
|
|
936
|
+
if (m) covered.add(`S${m[1]}`);
|
|
937
|
+
}
|
|
938
|
+
if (sids.length) {
|
|
939
|
+
const missing = sids.filter((id) => !covered.has(id));
|
|
940
|
+
if (missing.length) {
|
|
941
|
+
warnings.push(`- [WARN 场景未覆盖] ${base} 同名 spec 有验收场景但验收标准未逐场景引用:缺 ${missing.join('、')}——在「## 验收标准」按「- [x] 场景:S<n> <判据>(证据:…)」补齐(advisory,灰度第一档;不写场景节的 spec 不适用)`);
|
|
942
|
+
}
|
|
943
|
+
// THEN 对账需 spec 全文(scenarioIdsOf 只回 ID);读取失败 = 不可判定 → 跳过不误报(fail-open)
|
|
944
|
+
let thenMap = null;
|
|
945
|
+
try { thenMap = extractThenByScenario(fs.readFileSync(specPath, 'utf8')); } catch { /* 极窄竞态,不判空壳 */ }
|
|
946
|
+
if (thenMap) {
|
|
947
|
+
const noThen = sids.filter((id) => !(thenMap.get(id) || []).length);
|
|
948
|
+
if (noThen.length) {
|
|
949
|
+
warnings.push(`- [WARN 场景无断言] ${base} 同名 spec 场景缺 THEN 断言:${noThen.join('、')}——每场景至少一行「- THEN <可观测结果>」(GIVEN/WHEN 可省;advisory,灰度第一档)`);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
const dangling = [...covered].filter((id) => !sids.includes(id));
|
|
954
|
+
if (dangling.length) {
|
|
955
|
+
warnings.push(`- [WARN 场景引用断链] ${base} 验收标准引用了 spec 未声明的场景 ID:${dangling.join('、')}——修正 ID 或在 spec「## 验收场景」补该场景(advisory,灰度第一档)`);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
// 沉淀追踪子判据(2026-10-10 sediment-tracking,灰度第一档 = warning):
|
|
959
|
+
// intent 高价值节(影响面 / 触达红线)是 wiki 沉淀的主源——frontmatter「沉淀: <相对仓根路径>」
|
|
960
|
+
// 登记去向(如 wiki/项目规范/xxx.md),「沉淀: 无」= 显式声明不沉淀。缺登记且内容高价值
|
|
961
|
+
// → WARN 未沉淀(advisory:沉淀是人工判断,机器只提醒不强制,不进 hard);登记了但文件
|
|
962
|
+
// 不存在 → WARN 沉淀断档(路径漂移);存在但空壳(无来源且非占位正文不足 3 行)→ WARN 落点
|
|
963
|
+
// 空壳(2026-10-11 landing-reality,P1-3 落点真实性)。高价值判据 = 两节之一有非占位正文内容。
|
|
964
|
+
// incident 侧对称(检查 3 内):closed 且复盘三件套齐全 = 最高价值沉淀源,同款提醒。
|
|
965
|
+
// 有意不加 policyVersion 锚:字段缺失 = 未登记,无字段语义零变化(沿场景覆盖子判据同款口径)。
|
|
966
|
+
const sed = fmGet(intent, '沉淀');
|
|
967
|
+
const highValue = sectionHasBody(lines, '影响面') || sectionHasBody(lines, '触达红线');
|
|
968
|
+
if (highValue && !sed) {
|
|
969
|
+
warnings.push(`- [WARN 未沉淀] ${base} 含高价值沉淀内容(影响面/触达红线)但未登记沉淀去向——整理到 wiki 后在 frontmatter 记「沉淀: wiki/<主题>/<文件>」,或显式写「沉淀: 无」(advisory,灰度第一档;沉淀是人工判断,机器只提醒不强制)`);
|
|
970
|
+
} else if (sed && sed !== '无' && !fs.existsSync(path.resolve(ROOT, sed))) {
|
|
971
|
+
warnings.push(`- [WARN 沉淀断档] ${base} frontmatter「沉淀: ${sed}」指向的文件不存在——沉淀文件移动/改名后须同步更新本字段(advisory,灰度第一档)`);
|
|
972
|
+
} else if (sed && sed !== '无') {
|
|
973
|
+
warnSedimentShell(base, sed); // 落点真实性(P1-3):存在但空壳 → WARN 落点空壳
|
|
974
|
+
}
|
|
873
975
|
if (!uc && !ne) continue;
|
|
874
976
|
if (isNew) {
|
|
875
977
|
if (uc) blockers.push(`- [验收未对账] done intent 验收标准有未勾验项:${base}(逐条勾验并补证据:commit/用例/冒烟输出)`);
|
|
@@ -1006,6 +1108,39 @@ export function runCheckLoop(opts = {}) {
|
|
|
1006
1108
|
blockers.push(`- [台账链断裂] confirmations.jsonl 自第 ${physical} 行(物理)起哈希链校验失败(共 ${brokenCount} 行不一致——append-only 台账出现就地改/删痕迹;prevHash 接续或内容重算不符。整文件重写不可机器防,对质走台账自身 git 历史)`);
|
|
1007
1109
|
}
|
|
1008
1110
|
}
|
|
1111
|
+
// scope 子项(2026-10-09 审查 A1 干净版):受管台账行须带**机器派生**的改动面 scope{files,sha256}。
|
|
1112
|
+
// 背景:委托代录的授权凭据原是 `--delegated` 的用户原话——自由文本机器不可校验、AI 可零成本复用
|
|
1113
|
+
// (本仓实测「可以」133 次、「确认」60 次,406 行里仅 6 行 tty),整条授权链源头不可核对。
|
|
1114
|
+
// 干净版把分辨力从「一句说过的话」换成「机器实测的这 N 个文件」:写入侧 confirm-doc.machineScope
|
|
1115
|
+
// 自动派生(被确认文档 + `git status --porcelain` 未提交改动集),AI 无输入面。
|
|
1116
|
+
// 判定(scopeSince 生效**时刻**锚,缺键 v1-v6 整体跳过——存量行零新增告警、不追溯):
|
|
1117
|
+
// ① 缺 scope → warning(灰度第一档:新机制先观察,升 hard 走后续 policyVersion 演进)
|
|
1118
|
+
// ② scope 存在但按其 files 重算 sha256 不符 → hard-block(「改了 files 没改摘要」= 同一信任
|
|
1119
|
+
// 边界内的手改痕迹;哈希链本已覆盖就地改,本项兜住「整文件重写重建链」后二次篡改的场景)
|
|
1120
|
+
// 注:scope 证明的是「放行时盘面上哪些文件在动」,**不是**「这些改动语义正确」——后者由 fingerprint
|
|
1121
|
+
// (文档内容绑定)+ commit diff(事后对质)承担。不越界宣称(沿本检查既有口径纪律)。
|
|
1122
|
+
{
|
|
1123
|
+
const since = loadKitPolicy(ROOT).scopeSince;
|
|
1124
|
+
if (typeof since === 'string' && Number.isFinite(Date.parse(since))) {
|
|
1125
|
+
const sinceT = Date.parse(since);
|
|
1126
|
+
const missing = [];
|
|
1127
|
+
const mismatched = [];
|
|
1128
|
+
for (const row of ledger) {
|
|
1129
|
+
if (!row || typeof row.ts !== 'string') continue;
|
|
1130
|
+
const t = Date.parse(row.ts);
|
|
1131
|
+
if (!Number.isFinite(t) || t < sinceT) continue; // 存量豁免(不追溯)
|
|
1132
|
+
const name = row.doc || '(未知 doc)';
|
|
1133
|
+
if (!row.scope) { missing.push(name); continue; }
|
|
1134
|
+
if (!scopeMatches(row)) mismatched.push(name);
|
|
1135
|
+
}
|
|
1136
|
+
if (missing.length) {
|
|
1137
|
+
warnings.push(`- [WARN 缺机器改动面] ${missing.length} 条受管台账行无 scope 字段(${missing.slice(0, 3).join('、')}${missing.length > 3 ? ' …' : ''})——confirm-doc 落账自动派生 scope;此形态多见于手工/旧脚本写的行,advisory 不阻断`);
|
|
1138
|
+
}
|
|
1139
|
+
if (mismatched.length) {
|
|
1140
|
+
blockers.push(`- [台账 scope 失配] ${mismatched.length} 条受管台账行的 scope.sha256 与其 files 重算不符(${mismatched.slice(0, 3).join('、')}${mismatched.length > 3 ? ' …' : ''})——「改了 files 没同步摘要」,同一信任边界内的手改痕迹;对质走台账 git 历史`);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1009
1144
|
// incidents 侧覆盖(2026-09-27 gate-coverage):fixed/closed 为已确认态(open = 起草态不加门——
|
|
1010
1145
|
// maintain.md 创建即对话确认的既有口径)
|
|
1011
1146
|
// 放弃态覆盖(2026-09-27 closing-coverage):superseded/cancelled 入配对集——confirm-doc --to 是唯一
|
|
@@ -1097,7 +1232,7 @@ export function runCheckLoop(opts = {}) {
|
|
|
1097
1232
|
if (rows.every((r) => r.brief === true)) continue;
|
|
1098
1233
|
const of = Math.max(...rows.map((r) => (typeof r.of === 'number' ? r.of : 0)));
|
|
1099
1234
|
const docs = rows.map((r) => String(r.doc).replace(/^workflow\//, '')).join('、');
|
|
1100
|
-
warnings.push(`- [WARN 确认并录] 单次调用落账 ${rows.length} 份(batch ${b},of=${of}):${docs}——同一次 --delegated 调用放行多份,塌掉「逐件确认」门(build.md
|
|
1235
|
+
warnings.push(`- [WARN 确认并录] 单次调用落账 ${rows.length} 份(batch ${b},of=${of}):${docs}——同一次 --delegated 调用放行多份,塌掉「逐件确认」门(build.md;口径见根 AGENTS.md「门禁与提交」确认门节)`);
|
|
1101
1236
|
}
|
|
1102
1237
|
}
|
|
1103
1238
|
}
|
|
@@ -1516,3 +1651,110 @@ function readdirOrNull(dir) {
|
|
|
1516
1651
|
function isDirectoryOrNull(p) {
|
|
1517
1652
|
try { return fs.statSync(p).isDirectory(); } catch { return false; }
|
|
1518
1653
|
}
|
|
1654
|
+
|
|
1655
|
+
// 场景 ID 提取(2026-10-10 scenario-coverage,检查 8 场景覆盖子判据的 spec 侧单源):
|
|
1656
|
+
// 认「## 验收场景」节内的场景声明——三级标题 `### S1 <名>` 或列表项 `- S1 <名>`,
|
|
1657
|
+
// 提取 S<n> 形 ID(按出现序去重)。节外不认(防「## 功能行为」里偶然出现的 S1 干扰)。
|
|
1658
|
+
// 无该节 / 无场景声明 → 空数组(调用方按「不适用」跳过,原判据零变化)。
|
|
1659
|
+
// 独立 export 供单元测试直测(纯函数:只吃文件路径、只读)。
|
|
1660
|
+
export function scenarioIdsOf(specPath) {
|
|
1661
|
+
let lines;
|
|
1662
|
+
try { lines = fs.readFileSync(specPath, 'utf8').split(/\r?\n/); } catch { return []; }
|
|
1663
|
+
const ids = [];
|
|
1664
|
+
let inSec = false;
|
|
1665
|
+
for (const line of lines) {
|
|
1666
|
+
if (/^\s*##\s+[^#]*验收场景/.test(line)) { inSec = true; continue; }
|
|
1667
|
+
if (inSec && /^\s*##\s/.test(line)) break; // 下一二级标题 = 节末
|
|
1668
|
+
if (!inSec) continue;
|
|
1669
|
+
const m = line.match(/^\s*(?:###|-)\s*\**S(\d+)\b/i);
|
|
1670
|
+
if (m && !ids.includes(`S${m[1]}`)) ids.push(`S${m[1]}`);
|
|
1671
|
+
}
|
|
1672
|
+
return ids;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
// THEN 断言抽取(2026-10-11 then-assertion,检查 8「场景无断言」子判据的 spec 侧单源):
|
|
1676
|
+
// 输入 spec 全文,定位「## 验收场景」节(节定位正则与 scenarioIdsOf 同款——口径单源,改写法时两处同步),
|
|
1677
|
+
// 按场景块分组(`### S1 <名>` 三级标题或 `- S1 <名>` 列表项,声明正则同 scenarioIdsOf),抽块内 THEN 断言行。
|
|
1678
|
+
// 行匹配口径:剥缩进后以 `- THEN` 开头(模板规定形式);`THEN:` / `THEN `(无连字符)也认——
|
|
1679
|
+
// 防「写了断言但少了 `- ` 前缀」的写法漂移把断言误判为空;只认行首 THEN,GIVEN/WHEN 行中段的
|
|
1680
|
+
// "THEN" 字样不误抽。返回 Map<S-ID, string[]>(声明即建键,无 THEN = 空数组——空壳判定交给调用方);
|
|
1681
|
+
// 节不存在 / 非字符串输入 → 空 Map(验收场景节本身可选,调用方零变化)。
|
|
1682
|
+
// 独立 export 供单元测试直测(纯函数:只吃文本、无 IO)。
|
|
1683
|
+
export function extractThenByScenario(text) {
|
|
1684
|
+
const byScen = new Map();
|
|
1685
|
+
if (typeof text !== 'string') return byScen; // 读取失败(调用方传 null)→ 空 Map,调用方按不可判定跳过
|
|
1686
|
+
const lines = text.split(/\r?\n/);
|
|
1687
|
+
let cur = null; // 当前场景块 ID(null = 尚未进入任何场景块,节头说明行不算断言)
|
|
1688
|
+
let inSec = false;
|
|
1689
|
+
for (const line of lines) {
|
|
1690
|
+
if (/^\s*##\s+[^#]*验收场景/.test(line)) { inSec = true; continue; }
|
|
1691
|
+
if (inSec && /^\s*##\s/.test(line)) break; // 下一二级标题 = 节末
|
|
1692
|
+
if (!inSec) continue;
|
|
1693
|
+
const decl = line.match(/^\s*(?:###|-)\s*\**S(\d+)\b/i);
|
|
1694
|
+
if (decl) {
|
|
1695
|
+
const id = `S${decl[1]}`;
|
|
1696
|
+
if (!byScen.has(id)) byScen.set(id, []);
|
|
1697
|
+
cur = id;
|
|
1698
|
+
continue;
|
|
1699
|
+
}
|
|
1700
|
+
if (cur === null) continue;
|
|
1701
|
+
const s = line.trim();
|
|
1702
|
+
if (/^-\s*THEN\b/i.test(s) || /^THEN\b/i.test(s)) byScen.get(cur).push(s);
|
|
1703
|
+
}
|
|
1704
|
+
return byScen;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
// 节体非空判定(2026-10-10 sediment-tracking,检查 8 沉淀追踪子判据的「高价值」信号):
|
|
1708
|
+
// 认 `## <标题前缀>` 节内**标题之后**存在非空非占位行(占位 = `<…>` 单行包揽 / HTML 注释 / 引用)。
|
|
1709
|
+
// 只有光标题(如模板未填的 `## 影响面`)= 未沉淀价值,不出告警——防模板未填段被误判为高价值。
|
|
1710
|
+
// 前缀匹配(startsWith):兼容「## 影响面」「## 影响面与边界」等变体写法。
|
|
1711
|
+
// 独立 export 供单元测试直测(纯函数:只吃行数组、无 IO)。
|
|
1712
|
+
export function sectionHasBody(lines, headingPrefix) {
|
|
1713
|
+
let inSec = false;
|
|
1714
|
+
for (const line of lines) {
|
|
1715
|
+
if (/^\s*##\s/.test(line)) {
|
|
1716
|
+
const t = line.replace(/^\s*##\s*/, '').trim();
|
|
1717
|
+
inSec = t.startsWith(headingPrefix);
|
|
1718
|
+
continue;
|
|
1719
|
+
}
|
|
1720
|
+
if (!inSec) continue;
|
|
1721
|
+
const s = line.trim();
|
|
1722
|
+
if (!s) continue;
|
|
1723
|
+
if (/^<.*>$/.test(s) || s.startsWith('<!--') || s.startsWith('>')) continue; // 占位 / 注释 / 引用
|
|
1724
|
+
return true;
|
|
1725
|
+
}
|
|
1726
|
+
return false;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// 沉淀落点空壳判定(2026-10-11 landing-reality,「存在 ≠ 有效」的落点真实性一环):
|
|
1730
|
+
// 落点文件可能是只有标题没有内容的空壳。满足任一即**非**空壳(有效沉淀):
|
|
1731
|
+
// ① frontmatter 含「来源:」键(人工已登记来源 = 真实整理的机器信号;frontmatter 口径同 fmGet 受限子集);
|
|
1732
|
+
// ② frontmatter / 标题行 / 占位行剔除后的非空正文 ≥ 3 行(占位口径逐字对齐 sectionHasBody:
|
|
1733
|
+
// `<…>` 单行包揽 / HTML 注释 / 引用块)——标题行不算正文,防「只有标题」凑行数绕过。
|
|
1734
|
+
// text = 落点文件全文;非字符串(调用方读取失败传 null)按空壳处理,调用方在告警文案注明读取失败。
|
|
1735
|
+
// 独立 export 供单元测试直测(纯函数:只吃文本、无 IO)。
|
|
1736
|
+
export function sedimentLandingIsShell(text) {
|
|
1737
|
+
if (typeof text !== 'string') return true; // 读取失败 → 按空壳
|
|
1738
|
+
const lines = text.split(/\r?\n/);
|
|
1739
|
+
const isDelim = (l) => /^---\s*$/.test(l); // 同 fmGet:首行 --- 进入、下一 --- 闭合
|
|
1740
|
+
let inFm = isDelim(lines[0] || '');
|
|
1741
|
+
if (inFm) {
|
|
1742
|
+
for (let i = 1; i < lines.length; i++) {
|
|
1743
|
+
if (isDelim(lines[i])) break; // 下一 --- = frontmatter 闭合
|
|
1744
|
+
if (lines[i].startsWith('来源:')) return false; // ① 有来源登记 = 有效
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
let body = 0;
|
|
1748
|
+
for (let i = inFm ? 1 : 0; i < lines.length; i++) {
|
|
1749
|
+
if (inFm) {
|
|
1750
|
+
if (isDelim(lines[i])) inFm = false; // frontmatter 闭合,其后才开始数正文
|
|
1751
|
+
continue;
|
|
1752
|
+
}
|
|
1753
|
+
const s = lines[i].trim();
|
|
1754
|
+
if (!s) continue;
|
|
1755
|
+
if (s.startsWith('#')) continue; // 标题行 = 结构不算正文(空壳 = 只有标题没有内容)
|
|
1756
|
+
if (/^<.*>$/.test(s) || s.startsWith('<!--') || s.startsWith('>')) continue; // 占位 / 注释 / 引用(同 sectionHasBody)
|
|
1757
|
+
if (++body >= 3) return false; // ② 非占位正文 ≥ 3 行 = 有效
|
|
1758
|
+
}
|
|
1759
|
+
return true;
|
|
1760
|
+
}
|