agentic-flow-kit 0.8.0 → 0.9.1
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 +93 -69
- package/modules/gates/README.md +24 -0
- package/modules/gates/generated-readonly/README.md +13 -0
- package/modules/gates/generated-readonly/check-generated.mjs +61 -0
- package/modules/gates/generated-readonly/local-pre-commit.line +1 -0
- package/modules/gates/node-layer/README.md +9 -0
- package/modules/gates/node-layer/check-node-layer.mjs +29 -0
- package/modules/gates/node-layer/local-pre-commit.line +1 -0
- package/modules/gates/py-import/README.md +9 -0
- package/modules/gates/py-import/check-py-import.mjs +30 -0
- package/modules/gates/py-import/local-pre-commit.line +1 -0
- package/modules/hosts/claude/agents/implementer.md +52 -0
- package/modules/hosts/claude/agents/independent-reviewer.md +49 -0
- package/modules/hosts/claude/agents/ui-verifier.md +48 -0
- package/modules/hosts/claude/commands/wf-build.md +79 -0
- package/modules/hosts/claude/commands/wf-deploy.md +57 -0
- package/modules/hosts/claude/commands/wf-design.md +42 -0
- package/modules/hosts/claude/commands/wf-gate-checklist.md +77 -0
- package/modules/hosts/claude/commands/wf-maintain.md +76 -0
- package/modules/hosts/claude/commands/wf-new-task.md +65 -0
- package/modules/hosts/claude/commands/wf-plan.md +41 -0
- package/modules/hosts/claude/commands/wf-review.md +47 -0
- package/modules/hosts/claude/commands/wf-source-sync-check.md +89 -0
- package/modules/hosts/claude/commands/wf-sync-hosts.md +66 -0
- package/modules/hosts/claude/commands/wf-test.md +66 -0
- package/modules/hosts/codex/agents/implementer.md +52 -0
- package/modules/hosts/codex/agents/independent-reviewer.md +49 -0
- package/modules/hosts/codex/agents/ui-verifier.md +48 -0
- package/modules/hosts/codex/commands/wf-build.md +79 -0
- package/modules/hosts/codex/commands/wf-deploy.md +57 -0
- package/modules/hosts/codex/commands/wf-design.md +42 -0
- package/modules/hosts/codex/commands/wf-gate-checklist.md +77 -0
- package/modules/hosts/codex/commands/wf-maintain.md +76 -0
- package/modules/hosts/codex/commands/wf-new-task.md +65 -0
- package/modules/hosts/codex/commands/wf-plan.md +41 -0
- package/modules/hosts/codex/commands/wf-review.md +47 -0
- package/modules/hosts/codex/commands/wf-source-sync-check.md +89 -0
- package/modules/hosts/codex/commands/wf-sync-hosts.md +66 -0
- package/modules/hosts/codex/commands/wf-test.md +66 -0
- package/modules/hosts/codex/skills/flow-kit/SKILL.md +14 -0
- package/modules/hosts/cursor/agents/implementer.md +52 -0
- package/modules/hosts/cursor/agents/independent-reviewer.md +49 -0
- package/modules/hosts/cursor/agents/ui-verifier.md +48 -0
- package/modules/hosts/cursor/commands/wf-build.md +79 -0
- package/modules/hosts/cursor/commands/wf-deploy.md +57 -0
- package/modules/hosts/cursor/commands/wf-design.md +42 -0
- package/modules/hosts/cursor/commands/wf-gate-checklist.md +77 -0
- package/modules/hosts/cursor/commands/wf-maintain.md +76 -0
- package/modules/hosts/cursor/commands/wf-new-task.md +65 -0
- package/modules/hosts/cursor/commands/wf-plan.md +41 -0
- package/modules/hosts/cursor/commands/wf-review.md +47 -0
- package/modules/hosts/cursor/commands/wf-source-sync-check.md +89 -0
- package/modules/hosts/cursor/commands/wf-sync-hosts.md +66 -0
- package/modules/hosts/cursor/commands/wf-test.md +66 -0
- package/modules/hosts/omp/agents/implementer.md +1 -1
- package/modules/hosts/omp/agents/independent-reviewer.md +1 -1
- package/modules/hosts/omp/agents/ui-verifier.md +1 -1
- package/modules/hosts/opencode/agents/implementer.md +1 -1
- package/modules/hosts/opencode/agents/independent-reviewer.md +1 -1
- package/modules/hosts/opencode/agents/ui-verifier.md +1 -1
- package/modules/hosts/opencode/commands/wf-build.md +3 -4
- package/modules/hosts/opencode/commands/wf-design.md +2 -2
- package/modules/hosts/opencode/commands/wf-maintain.md +1 -1
- package/modules/hosts/opencode/commands/wf-plan.md +3 -3
- package/modules/hosts/opencode/commands/wf-review.md +1 -1
- package/modules/hosts/opencode/commands/wf-source-sync-check.md +1 -1
- package/modules/hosts/opencode/commands/wf-test.md +3 -1
- package/modules/hosts/trae/agents/implementer.md +1 -1
- package/modules/hosts/trae/agents/independent-reviewer.md +1 -1
- package/modules/hosts/trae/agents/ui-verifier.md +1 -1
- package/modules/hosts/trae/commands/wf-build.md +3 -4
- package/modules/hosts/trae/commands/wf-design.md +2 -2
- package/modules/hosts/trae/commands/wf-maintain.md +1 -1
- package/modules/hosts/trae/commands/wf-plan.md +3 -3
- package/modules/hosts/trae/commands/wf-review.md +1 -1
- package/modules/hosts/trae/commands/wf-source-sync-check.md +1 -1
- package/modules/hosts/trae/commands/wf-test.md +3 -1
- package/modules/hosts/zcode/agents/implementer.md +1 -1
- package/modules/hosts/zcode/agents/independent-reviewer.md +1 -1
- package/modules/hosts/zcode/agents/ui-verifier.md +1 -1
- package/package.json +45 -45
- package/src/cli.mjs +16 -4
- package/src/doctor.mjs +49 -1
- package/src/fresh-init.test.mjs +36 -0
- package/src/gates.test.mjs +126 -0
- package/src/init.mjs +8 -1
- package/src/init.test.mjs +7 -1
- package/src/profiles.mjs +10 -4
- package/src/run-tests.mjs +3 -0
- package/src/stack-profile.test.mjs +89 -0
- package/src/sync-hosts.test.mjs +26 -6
- package/templates/AGENTS.md +4 -4
- package/templates/_agents/board/index.html +3 -3
- package/templates/_agents/commands/build.md +4 -5
- package/templates/_agents/commands/design.md +4 -4
- package/templates/_agents/commands/maintain.md +1 -1
- package/templates/_agents/commands/new-task.md +13 -2
- package/templates/_agents/commands/plan.md +4 -4
- package/templates/_agents/commands/review.md +1 -1
- package/templates/_agents/commands/source-sync-check.md +1 -1
- package/templates/_agents/commands/test.md +4 -2
- package/templates/_agents/hooks/commit-check.cjs +45 -24
- package/templates/_agents/lane-surfaces.txt +14 -0
- package/templates/_agents/roles/implementer.md +1 -1
- package/templates/_agents/roles/independent-reviewer.md +1 -1
- package/templates/_agents/roles/ui-verifier.md +1 -1
- package/templates/_agents/scripts/check-lane-surface.mjs +164 -0
- package/templates/_agents/scripts/check-lane-surface.test.mjs +200 -0
- package/templates/_agents/scripts/check-loop-rev.test.mjs +337 -0
- package/templates/_agents/scripts/check-loop.mjs +460 -268
- package/templates/_agents/scripts/check-loop.test.mjs +677 -17
- package/templates/_agents/scripts/check-metric-claims.mjs +275 -0
- package/templates/_agents/scripts/commit-check-trigger.test.mjs +64 -1
- package/templates/_agents/scripts/confirm-doc.mjs +159 -15
- package/templates/_agents/scripts/confirm-doc.test.mjs +362 -10
- package/templates/_agents/scripts/doctor.test.mjs +102 -1
- package/templates/_agents/scripts/fill-intent.mjs +8 -7
- package/templates/_agents/scripts/fill-intent.test.mjs +12 -3
- package/templates/_agents/scripts/fill-plan.mjs +27 -11
- package/templates/_agents/scripts/fill-plan.test.mjs +148 -8
- package/templates/_agents/scripts/fill-spec.mjs +18 -3
- package/templates/_agents/scripts/fill-spec.test.mjs +198 -1
- package/templates/_agents/scripts/gate-checklist.mjs +4 -1
- package/templates/_agents/scripts/gen-workflow-index.mjs +6 -2
- package/templates/_agents/scripts/gen-workflow-index.test.mjs +12 -0
- package/templates/_agents/scripts/policy.mjs +46 -0
- package/templates/_agents/scripts/pre-push.test.mjs +82 -0
- package/templates/_agents/scripts/solidify-task.mjs +107 -0
- package/templates/_agents/scripts/solidify-task.test.mjs +126 -0
- package/templates/_agents/scripts/stage-gates.mjs +256 -0
- package/templates/_agents/scripts/trust-mode.mjs +67 -0
- package/templates/_agents/scripts/wf-journal.mjs +71 -2
- package/templates/_agents/scripts/wf-journal.test.mjs +66 -0
- package/templates/_agents/scripts/workflow-board-server.mjs +18 -15
- package/templates/_agents/scripts/workflow-board-server.test.mjs +30 -1
- package/templates/_agents/workflows/pipeline-closing.md +1 -1
- package/templates/_githooks/pre-commit +33 -2
- package/templates/_githooks/pre-push +58 -10
- package/templates/workflow/README.md +22 -3
- package/templates/workflow/incidents/_TEMPLATE.md +4 -0
- package/templates/workflow/intents/_TEMPLATE.md +1 -0
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
// 违例输出到 stderr 并 exit 1;只剩警告时 exit 0。
|
|
14
14
|
//
|
|
15
15
|
// 检查项清单(编号/标题/severity 逐条沿 sh 版头部——gate-checklist 配对登记表按 id 消费,不得增删改号):
|
|
16
|
-
// 1. 入口文档/spec/plan 同名配对(L1 必须有 plan;L2/L3 必须有 spec+plan
|
|
16
|
+
// 1. 入口文档/spec/plan 同名配对(L1 必须有 plan;L2/L3 必须有 spec+plan;L0 协作道豁免 plan;
|
|
17
|
+
// L0/L1 勾触达红线 → 「红线判低」hard——协作道不受理 STOP 级改动,就高不就低,
|
|
18
|
+
// 2026-09-30 hybrid-governance-risk-lanes) [hard-block]
|
|
17
19
|
// 2. 模板字段占位符残留(YYYY-MM-DD / <主题> 等未替换;<主题> 与 .md 同行 = 命名约定描述,豁免) [warning]
|
|
18
20
|
// 3. incidents 复盘三件套完整性 + 状态严格枚举 + 新 intent 回路(回路断档=hard,其他=warning)
|
|
19
21
|
// 4. 引用有效性(文档/指令中引用的 .agents/ 路径必须存在;支持 fill-{a,b,c}.mjs 花括号展开与 fill-*.mjs 通配;
|
|
@@ -65,6 +67,12 @@
|
|
|
65
67
|
// 等于引导伪装)/噪声不可消退(不按日期门豁免,退役前本仓 22 条 advisory 中 14 条为该告警)/
|
|
66
68
|
// quote 字段职责冲突(对质凭据 vs 并录指纹,现归还单一职责)。无 batch 的历史行静默跳过
|
|
67
69
|
// (无判定依据的行不产出不可消除噪声——沿 audit-gate-hardening P3 教训)
|
|
70
|
+
// 17. 发版提交树上仍未收口的 intent/spec/plan [hard-block]
|
|
71
|
+
// 18. 委派台账对账 [warning]
|
|
72
|
+
// 19. 逐阶段审计(起草先于入口确认 / 台账审批顺序倒置) [warning]
|
|
73
|
+
// 判据 A=在途扫描(同名 spec/plan 为 draft 且日期≥stageGateSince 时判入口确认;plan 另判 L2/L3 的 spec);
|
|
74
|
+
// 判据 B=台账 approved 行按主题校验 intents≤specs≤plans 顺序(组内最早 ts 日期≥stageGateSince 才判);
|
|
75
|
+
// stageGateSince 缺键(v1) → 本检查整体跳过;口径与 stage-gates.mjs / fill-* / confirm-doc 前置门互引
|
|
68
76
|
// 16. 量化断言指标签名对账 [warning](2026-09-28 起;登记表单源 .agents/metric-claims.txt):
|
|
69
77
|
// 判据 = 活跃态文档(draft/approved/open)中的 `{{指标名}}` 签名须替换为实时值,留签名=未回填=warning。
|
|
70
78
|
// **只查显式签名、不全文扫数字**(据实说明):本仓活跃文档「N 行/N 条/N 份」类表述数十处,绝大多数是
|
|
@@ -80,21 +88,63 @@
|
|
|
80
88
|
// 运行模式:CHECK_LOOP_ROOT 注入 fixture 根(全扫不过滤,非 git 时检查 10/14 跳过);
|
|
81
89
|
// 缺省 = git 仓库根(不在 git 仓库内 stderr 提示后 exit 0 跳过);仓库模式只扫已提交(HEAD)内容
|
|
82
90
|
// (并行会话未跟踪半成品不拦别人的 push;ls-tree 失败退化为全扫,门禁不失效)。
|
|
91
|
+
// --rev <sha>:把该提交剥到 commit 后挂到临时 detached worktree,ROOT 改到那里再扫。
|
|
92
|
+
// 原工作区的未提交改动不进入这次扫描。与 CHECK_LOOP_ROOT 同时出现则 exit 1。全 0 sha exit 1。
|
|
83
93
|
// 文档协议:机器字段一律由文件头 YAML frontmatter(受限子集:每行 `键: 值`)承载,本脚本只扫 frontmatter 取字段;
|
|
84
94
|
// 叙述性字段(独立复核/复盘三件套/验收勾验)按正文行锚定。枚举单源 .agents/workflow-enums.txt
|
|
85
95
|
// (缺文件/缺键 fail-loud exit 1;经 workflow-enums.mjs 读取,CRLF 天然容忍)。
|
|
86
96
|
// 已确认状态: approved/done=已批或闭环; superseded/cancelled=放弃留档(仍算确认,不挡 push); incident: fixed/closed
|
|
87
|
-
// 用法:node .agents/scripts/check-loop.mjs (或经 check-loop.sh shim)
|
|
97
|
+
// 用法:node .agents/scripts/check-loop.mjs [--rev <sha>] [--hardening] (或经 check-loop.sh shim)
|
|
98
|
+
// --hardening:追加「加固门」(experiment 泳道 exploring 任务入 main 的转正门槛,hard-block;
|
|
99
|
+
// .githooks/pre-push 对 remote=refs/heads/main 的推送传入;不占 1-19 编号,gate-checklist 登记表
|
|
100
|
+
// 按号配对面不变,门体说明见加固门段注释)
|
|
88
101
|
// 测试:node templates/_agents/scripts/check-loop.test.mjs(fixture 注入 CHECK_LOOP_ROOT)
|
|
89
102
|
import fs from 'node:fs';
|
|
103
|
+
import os from 'node:os';
|
|
90
104
|
import path from 'node:path';
|
|
91
105
|
import { spawnSync } from 'node:child_process';
|
|
92
106
|
import { createHash } from 'node:crypto';
|
|
93
107
|
import { createRequire } from 'node:module';
|
|
94
108
|
import { fileURLToPath } from 'node:url';
|
|
95
109
|
import { loadEnums } from './workflow-enums.mjs';
|
|
110
|
+
import { runCheck16 } from './check-metric-claims.mjs';
|
|
111
|
+
import { auditEnabled, loadKitPolicy } from './policy.mjs';
|
|
112
|
+
// MARK_RE:incident 留痕形态的单源(第七轮复核 N3——此前 check-loop 内联复制一份同口径字面量,
|
|
113
|
+
// 两处靠注释与人工同步;改为复用 stage-gates.mjs 的导出,从结构上消除漂移可能)
|
|
114
|
+
import { MARK_RE, approvedTraceHit } from './stage-gates.mjs';
|
|
96
115
|
|
|
97
116
|
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
117
|
+
const GIT = process.platform === 'win32' ? 'git.exe' : 'git';
|
|
118
|
+
|
|
119
|
+
function takeOpts() {
|
|
120
|
+
const args = process.argv.slice(2);
|
|
121
|
+
const opts = { rev: null, hardening: false };
|
|
122
|
+
for (let i = 0; i < args.length; i++) {
|
|
123
|
+
if (args[i] === '--rev') {
|
|
124
|
+
opts.rev = args[++i] ?? '';
|
|
125
|
+
if (!opts.rev) {
|
|
126
|
+
console.error('check-loop: 用法 node check-loop.mjs [--rev <sha>] [--hardening]');
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
} else if (args[i] === '--hardening') {
|
|
130
|
+
opts.hardening = true; // 加固门(2026-09-30 hybrid-governance-explore-hardening):pre-push 对 main 目标传入
|
|
131
|
+
} else {
|
|
132
|
+
console.error('check-loop: 用法 node check-loop.mjs [--rev <sha>] [--hardening]');
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return opts;
|
|
137
|
+
}
|
|
138
|
+
const { rev: REV_ARG, hardening: HARDENING } = takeOpts();
|
|
139
|
+
if (REV_ARG && process.env.CHECK_LOOP_ROOT) {
|
|
140
|
+
console.error('check-loop: CHECK_LOOP_ROOT 与 --rev 不能同时使用');
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
if (REV_ARG && /^0+$/.test(REV_ARG)) {
|
|
144
|
+
console.error(`check-loop: --rev 不是提交 ${REV_ARG}`);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
let REV = null;
|
|
98
148
|
|
|
99
149
|
// ---- 根目录与枚举单源 ----
|
|
100
150
|
let ROOT;
|
|
@@ -107,10 +157,38 @@ if (process.env.CHECK_LOOP_ROOT) {
|
|
|
107
157
|
} else {
|
|
108
158
|
const top = gitOut(['rev-parse', '--show-toplevel']);
|
|
109
159
|
if (top === null) {
|
|
160
|
+
if (REV_ARG) {
|
|
161
|
+
console.error(`check-loop: --rev 剥不到提交 ${REV_ARG}`);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
110
164
|
console.error('check-loop: 不在 git 仓库内,跳过扫描');
|
|
111
165
|
process.exit(0);
|
|
112
166
|
}
|
|
113
167
|
ROOT = top.trim();
|
|
168
|
+
if (REV_ARG) {
|
|
169
|
+
const peeled = gitOut(['rev-parse', '--verify', `${REV_ARG}^{commit}`]);
|
|
170
|
+
if (!peeled) {
|
|
171
|
+
console.error(`check-loop: --rev 剥不到提交 ${REV_ARG}`);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
REV = peeled.trim();
|
|
175
|
+
const parent = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-rev-'));
|
|
176
|
+
const wt = path.join(parent, 'wt');
|
|
177
|
+
const main = ROOT;
|
|
178
|
+
const add = spawnSync(GIT, ['worktree', 'add', '--detach', '--quiet', wt, REV], {
|
|
179
|
+
cwd: main, encoding: 'utf8',
|
|
180
|
+
});
|
|
181
|
+
if (add.status !== 0) {
|
|
182
|
+
fs.rmSync(parent, { recursive: true, force: true });
|
|
183
|
+
console.error(`check-loop: 导出提交失败 ${REV_ARG}\n${add.stderr || add.stdout || ''}`);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
process.on('exit', () => {
|
|
187
|
+
spawnSync(GIT, ['worktree', 'remove', '--force', wt], { cwd: main });
|
|
188
|
+
fs.rmSync(parent, { recursive: true, force: true });
|
|
189
|
+
});
|
|
190
|
+
ROOT = wt;
|
|
191
|
+
}
|
|
114
192
|
}
|
|
115
193
|
// 枚举单源(<root>/.agents/workflow-enums.txt——fixture 契约:缺文件/缺键 fail-loud)
|
|
116
194
|
let ENUMS;
|
|
@@ -130,7 +208,7 @@ const stOkDoc = (st) => inSet(st, ENUMS['doc.status.confirmed']);
|
|
|
130
208
|
|
|
131
209
|
// git 调用(参数数组形式不走 shell——Windows cmd 不认单引号;git.exe 显式解析沿 doctor 先例)
|
|
132
210
|
function gitOut(args, { ok = () => true } = {}) {
|
|
133
|
-
const r = spawnSync(
|
|
211
|
+
const r = spawnSync(GIT, args,
|
|
134
212
|
{ cwd: ROOT, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 });
|
|
135
213
|
if (r.error || r.status !== 0 || !ok(r)) return null;
|
|
136
214
|
return r.stdout || '';
|
|
@@ -161,6 +239,12 @@ const WF = 'workflow';
|
|
|
161
239
|
const DOC_DIRS = ['intents', 'specs', 'plans', 'incidents'];
|
|
162
240
|
const blockers = [];
|
|
163
241
|
const warnings = [];
|
|
242
|
+
// audit:false(init 新装)只保留 blockers。缺省与 audit:true 保持全量警告。
|
|
243
|
+
// 硬规则:配对 / 验收勾验的阻断 / 确认留痕 / 发版草稿。卫生项走 warnings。
|
|
244
|
+
const kitPolicy = loadKitPolicy(ROOT);
|
|
245
|
+
if (!auditEnabled(kitPolicy)) {
|
|
246
|
+
warnings.push = () => warnings.length;
|
|
247
|
+
}
|
|
164
248
|
|
|
165
249
|
// ---- 已提交(HEAD)过滤:仓库模式只扫 tracked(fixture 模式恒真;ls-tree 失败退化全扫)----
|
|
166
250
|
let trackedSet = null; // null = 不过滤
|
|
@@ -179,11 +263,12 @@ const docFiles = (sub) => {
|
|
|
179
263
|
.sort();
|
|
180
264
|
};
|
|
181
265
|
|
|
182
|
-
// --- 1. intent/spec/plan 同名配对 + 状态确认 + L3 确认三件 [hard-block] ---
|
|
266
|
+
// --- 1. intent/spec/plan 同名配对 + 状态确认 + L3 确认三件 + 风险泳道一致性 [hard-block] ---
|
|
183
267
|
for (const intent of docFiles('intents')) {
|
|
184
268
|
const base = path.basename(intent);
|
|
185
269
|
const st = fmGet(intent, '状态');
|
|
186
270
|
const lvl = fmGet(intent, '级别');
|
|
271
|
+
const risk = fmGet(intent, 'risk_level'); // 风险泳道字段(fill-intent 落,=级别;2026-09-30 hybrid-governance-risk-lanes)
|
|
187
272
|
if (!stOkDoc(st)) {
|
|
188
273
|
const msg = `${base}(frontmatter 状态键当前值:『${st || '缺失'}』)`;
|
|
189
274
|
if (lvl === 'L3') blockers.push(`- [状态未确认] L3 intent 必须为 approved/done/superseded/cancelled:${msg}`);
|
|
@@ -194,9 +279,19 @@ for (const intent of docFiles('intents')) {
|
|
|
194
279
|
blockers.push(`- [配对断裂] intent 缺 spec:${base}(应在 ${WF}/specs/ 下同名)`);
|
|
195
280
|
}
|
|
196
281
|
}
|
|
197
|
-
|
|
282
|
+
// 协作道(L0)豁免 plan 配对——异步审计兜底(hybrid-governance-risk-lanes);L1 仍须 plan,L2/L3 须 spec+plan
|
|
283
|
+
if (lvl !== 'L0' && !fs.existsSync(path.join(ROOT, WF, 'plans', base))) {
|
|
198
284
|
blockers.push(`- [配对断裂] intent 缺 plan:${base}(应在 ${WF}/plans/ 下同名)`);
|
|
199
285
|
}
|
|
286
|
+
// 风险泳道一致性:L0/L1(级别或 risk_level 任一判定)勾选触达红线 = 自认触及规则/契约或数据/运行时
|
|
287
|
+
// 结构面——就高不就低,协作道不受理 → hard 判低,须升级 L2/L3 并补同名 spec
|
|
288
|
+
if (['L0', 'L1'].includes(lvl) || ['L0', 'L1'].includes(risk)) {
|
|
289
|
+
const redlineTouched = (linesOf(intent) || [])
|
|
290
|
+
.some((l) => /^- \[x\]/.test(l) && /(\$\\rightarrow\$|→)\s*级别|级别至少\s*L[23]/.test(l));
|
|
291
|
+
if (redlineTouched) {
|
|
292
|
+
blockers.push(`- [红线判低] ${base} 级别/风险泳道为 ${[lvl, risk].filter(Boolean).join('/')} 但触达红线已勾选(规则/契约或 schema/结构面)——协作道不受理 STOP 级改动:就高不就低,先把「级别/risk_level」升到 L2/L3 并补同名 spec`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
200
295
|
}
|
|
201
296
|
|
|
202
297
|
for (const spec of docFiles('specs')) {
|
|
@@ -456,6 +551,53 @@ const addedDateOf = (absDoc) => {
|
|
|
456
551
|
return iso ? iso.slice(0, 10) : '';
|
|
457
552
|
};
|
|
458
553
|
|
|
554
|
+
// 证据真相校验:检查证据字符串是否包含合法的 commit SHA,且该 commit 触及了 plan 声明的文件
|
|
555
|
+
function verifyEvidenceTruth(evidenceStr, planBase, root) {
|
|
556
|
+
const shaMatch = evidenceStr.match(/\b([a-f0-9]{7,40})\b/i);
|
|
557
|
+
// 排除纯数字串(时间戳/ID),必须是包含字母的 hex 串才疑似 SHA
|
|
558
|
+
if (!shaMatch || /^\d+$/.test(shaMatch[1])) return { ok: true, type: 'text' };
|
|
559
|
+
|
|
560
|
+
const sha = shaMatch[1];
|
|
561
|
+
const revParse = spawnSync(GIT, ['rev-parse', sha], { cwd: ROOT, encoding: 'utf8' });
|
|
562
|
+
if (revParse.status !== 0) {
|
|
563
|
+
// 区分本地伪造与外部仓库引用:
|
|
564
|
+
// 剔除 SHA 与泛指词后仍有其他内容(如项目名、路径描述)→ 视为外部仓库引用,不硬拦(本地无法核验他仓哈希);
|
|
565
|
+
// 剔除后为空(如纯「commit a1b2c3d」「提交 a1b2c3d」)→ 视为本地引用伪造,硬拦。
|
|
566
|
+
const residue = evidenceStr.replace(sha, '')
|
|
567
|
+
.replace(/证据[::]|commit|提交|[()()。—-]/gi, '')
|
|
568
|
+
.trim();
|
|
569
|
+
if (residue.length > 0) return { ok: true, type: 'external' };
|
|
570
|
+
return { ok: false, type: 'forged', msg: `提交 ${sha} 不存在` };
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// 语义核验:该 commit 必须触及 plan 中声明的任何一个文件
|
|
574
|
+
const planPath = path.join(ROOT, WF, 'plans', planBase + '.md');
|
|
575
|
+
if (!fs.existsSync(planPath)) return { ok: true, type: 'no-plan' }; // 缺 plan 走 Check 1 拦截
|
|
576
|
+
|
|
577
|
+
const planLines = linesOf(planPath) || [];
|
|
578
|
+
const declaredFiles = [];
|
|
579
|
+
// 扫描「改动面」或「任务拆解」节
|
|
580
|
+
let inDeclaredSec = false;
|
|
581
|
+
for (const line of planLines) {
|
|
582
|
+
if (/^##\s+(改动面|任务拆解)/.test(line)) { inDeclaredSec = true; continue; }
|
|
583
|
+
if (inDeclaredSec && /^##\s+/.test(line)) { inDeclaredSec = false; continue; }
|
|
584
|
+
if (inDeclaredSec) {
|
|
585
|
+
const fileMatch = line.match(/([a-zA-Z0-9._\/-]+\.[a-zA-Z0-9]+)/);
|
|
586
|
+
if (fileMatch) declaredFiles.push(fileMatch[1]);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const show = spawnSync(GIT, ['show', '--name-only', sha], { cwd: ROOT, encoding: 'utf8' });
|
|
591
|
+
if (show.status !== 0) return { ok: false, type: 'error', msg: `无法读取提交 ${sha} 的文件列表` };
|
|
592
|
+
|
|
593
|
+
const changedFiles = show.stdout.split(/\r?\n/).filter(Boolean);
|
|
594
|
+
const hasIntersection = changedFiles.some(f => declaredFiles.some(df => f.includes(df) || df.includes(f)));
|
|
595
|
+
|
|
596
|
+
if (!hasIntersection) return { ok: false, type: 'irrelevant', msg: `提交 ${sha} 未触及 plan 声明的任何文件` };
|
|
597
|
+
|
|
598
|
+
return { ok: true, type: 'sha' };
|
|
599
|
+
}
|
|
600
|
+
|
|
459
601
|
// --- 8. intent 验收标准对账(done 须逐条勾验并补证据;新建 hard,存量聚合 warning)---
|
|
460
602
|
// 生效日锚 2026-09-28 改「git 首次加入日期」(此前取文件名前 10 字符——命名规范强制的字段、
|
|
461
603
|
// 写早零成本,见 incidents/2026-09-28-check8-git-anchor)。非 git → 不可判定 → 走存量口径(不误报 hard)。
|
|
@@ -472,16 +614,40 @@ const addedDateOf = (absDoc) => {
|
|
|
472
614
|
let hs = false, uc = false, ne = false;
|
|
473
615
|
let insec = false, ex = false;
|
|
474
616
|
let pendX = false; // 上一 [x] 项尚无证据,证据可能在紧随的续行
|
|
617
|
+
let currentEvidence = ''; // 当前条目的证据内容,用于语义校验
|
|
475
618
|
const evRe = /证据[::]/;
|
|
476
|
-
const closeItem = () => {
|
|
619
|
+
const closeItem = () => {
|
|
620
|
+
if (pendX) {
|
|
621
|
+
ne = true;
|
|
622
|
+
pendX = false;
|
|
623
|
+
}
|
|
624
|
+
if (currentEvidence && !ex) {
|
|
625
|
+
const truth = verifyEvidenceTruth(currentEvidence, base.replace(/\.md$/, ''), ROOT);
|
|
626
|
+
if (!truth.ok) {
|
|
627
|
+
blockers.push(`- [证据${truth.type === 'forged' ? '伪造' : '无关'}] ${base} 验收证据校验失败: ${truth.msg}`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
currentEvidence = '';
|
|
631
|
+
};
|
|
477
632
|
for (const line of lines) {
|
|
478
633
|
if (/存量对账豁免(/.test(line)) ex = true;
|
|
479
634
|
if (/^\s*##\s+[^#]*验收标准/.test(line)) { hs = true; insec = true; closeItem(); continue; }
|
|
480
635
|
if (insec && /^\s*##\s/.test(line)) { insec = false; closeItem(); continue; }
|
|
481
636
|
if (!insec) continue;
|
|
482
637
|
if (/^\s*- \[ \]/.test(line)) { closeItem(); uc = true; continue; }
|
|
483
|
-
if (/^\s*- \[x\]/.test(line)) {
|
|
484
|
-
|
|
638
|
+
if (/^\s*- \[x\]/.test(line)) {
|
|
639
|
+
closeItem();
|
|
640
|
+
pendX = !evRe.test(line);
|
|
641
|
+
if (evRe.test(line)) currentEvidence = line.split(/证据[::]/)[1].trim();
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
if (pendX && evRe.test(line)) {
|
|
645
|
+
pendX = false;
|
|
646
|
+
currentEvidence = line.split(/证据[::]/)[1].trim();
|
|
647
|
+
} else if (currentEvidence && line.trim() && !/^\s*- /.test(line)) {
|
|
648
|
+
// 简单累加续行证据
|
|
649
|
+
currentEvidence += ' ' + line.trim();
|
|
650
|
+
}
|
|
485
651
|
}
|
|
486
652
|
closeItem(); // 节末(或全文末)仍无证据 → 计缺证据
|
|
487
653
|
// 生效日锚 = git 首次加入日期(2026-09-28 改;此前取文件名前 10 字符)。**准确收益**:关掉
|
|
@@ -496,12 +662,12 @@ const addedDateOf = (absDoc) => {
|
|
|
496
662
|
continue;
|
|
497
663
|
}
|
|
498
664
|
if (!uc && !ne) continue;
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
665
|
+
if (isNew) {
|
|
666
|
+
if (uc) blockers.push(`- [验收未对账] done intent 验收标准有未勾验项:${base}(逐条勾验并补证据:commit/用例/冒烟输出)`);
|
|
667
|
+
if (ne) blockers.push(`- [验收缺证据] ${base} 验收标准勾选项缺「证据:」标注——证据是闭环准确性的唯一机器事实`);
|
|
668
|
+
} else if (!ex) {
|
|
669
|
+
legacyUnaccounted++;
|
|
670
|
+
}
|
|
505
671
|
}
|
|
506
672
|
if (legacyUnaccounted > 0) {
|
|
507
673
|
warnings.push(`- [WARN 验收对账存量] ${legacyUnaccounted} 个存量 done intent 验收标准未对账(生效日锚之前加入仓库,豁免 hard,不回填)`);
|
|
@@ -576,8 +742,8 @@ if (gitOut(['rev-parse', '--git-dir']) !== null) {
|
|
|
576
742
|
if (!d) d = /^\d{4}-\d{2}-\d{2}$/.test(base.slice(0, 10)) ? base.slice(0, 10) : '';
|
|
577
743
|
if (mod) {
|
|
578
744
|
if (!vocab.has(mod)) warnings.push(`- [WARN 模块元数据] ${base}「模块: ${mod}」不在词表(见 .agents/workflow-modules.txt)`);
|
|
579
|
-
} else if (/^\d{4}-\d{2}-\d{2}$/.test(d) && d >=
|
|
580
|
-
warnings.push(`- [WARN 模块元数据] ${base} 缺「模块:」字段(
|
|
745
|
+
} else if (/^\d{4}-\d{2}-\d{2}$/.test(d) && d >= kitPolicy.moduleSince) {
|
|
746
|
+
warnings.push(`- [WARN 模块元数据] ${base} 缺「模块:」字段(${kitPolicy.moduleSince} 起新建文档必填)`);
|
|
581
747
|
}
|
|
582
748
|
}
|
|
583
749
|
}
|
|
@@ -605,12 +771,14 @@ if (gitOut(['rev-parse', '--git-dir']) !== null && gitOut(['rev-parse', '-q', '-
|
|
|
605
771
|
const base = path.basename(doc);
|
|
606
772
|
let d = fmGet(doc, '日期') || fmGet(doc, '发现');
|
|
607
773
|
if (!d) d = /^\d{4}-\d{2}-\d{2}$/.test(base.slice(0, 10)) ? base.slice(0, 10) : '';
|
|
608
|
-
if (!/^\d{4}-\d{2}-\d{2}$/.test(d) || d <
|
|
774
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(d) || d < kitPolicy.check14Since) continue; // policy.mjs check14Since
|
|
609
775
|
const lines = linesOf(doc) || [];
|
|
610
776
|
if (lines.some((l) => l.includes('存量确认态豁免('))) continue;
|
|
611
777
|
const rel = path.relative(ROOT, doc).split(path.sep).join('/');
|
|
612
|
-
|
|
613
|
-
|
|
778
|
+
// 命中判据单源(stage-gates.approvedTraceHit,2026-09-30 confirm-gate-approved-history):
|
|
779
|
+
// 返回 ''=从未出现 / <sha>=命中 / null=git 调用失败(不报,沿本段 fail-open 口径)
|
|
780
|
+
const hit = approvedTraceHit(ROOT, rel);
|
|
781
|
+
if (hit !== null && hit === '') {
|
|
614
782
|
warnings.push(`- [WARN 确认态缺失] ${base} 状态已 done 但 git 历史中从未出现行首「状态: approved」——确认环节未留痕(draft 直跳 done)`);
|
|
615
783
|
}
|
|
616
784
|
}
|
|
@@ -631,7 +799,7 @@ if (gitOut(['rev-parse', '--git-dir']) !== null && gitOut(['rev-parse', '-q', '-
|
|
|
631
799
|
// 独立生效日 2026-09-28 常量退役:台账起算日 = docs 生效日,两档等价)。
|
|
632
800
|
// 台账坏行容忍跳过(审计件,jsonl 追加式);frontmatter 存 16 位、台账存 64 位,按前 16 位配对。
|
|
633
801
|
{
|
|
634
|
-
const EFFECTIVE =
|
|
802
|
+
const EFFECTIVE = kitPolicy.confirmDocsEffective;
|
|
635
803
|
const ledgerPath = path.join(ROOT, '.agents', 'confirmations.jsonl');
|
|
636
804
|
const ledger = [];
|
|
637
805
|
if (fs.existsSync(ledgerPath)) {
|
|
@@ -666,7 +834,7 @@ if (gitOut(['rev-parse', '--git-dir']) !== null && gitOut(['rev-parse', '-q', '-
|
|
|
666
834
|
// 仅靠 ① 会被误判为存量豁免而静默放行)。
|
|
667
835
|
// 两条件皆不满足 = 存量豁免(生效日前既有、从未走确认门,不追溯)。
|
|
668
836
|
const docEntries = ledger.filter((e) => e && e.doc === rel && VALID_STAGES.has(e.stage));
|
|
669
|
-
const eff = sub === 'incidents' ?
|
|
837
|
+
const eff = sub === 'incidents' ? kitPolicy.confirmIncidentsEffective : EFFECTIVE;
|
|
670
838
|
let d = fmGet(doc, '日期') || fmGet(doc, '发现');
|
|
671
839
|
if (!d) d = /^\d{4}-\d{2}-\d{2}$/.test(base.slice(0, 10)) ? base.slice(0, 10) : '';
|
|
672
840
|
const dateManaged = /^\d{4}-\d{2}-\d{2}$/.test(d) && d >= eff;
|
|
@@ -690,7 +858,7 @@ if (gitOut(['rev-parse', '--git-dir']) !== null && gitOut(['rev-parse', '-q', '-
|
|
|
690
858
|
const entry = doneEntries[doneEntries.length - 1]; // append-only 台账,末次生效(重确认场景)
|
|
691
859
|
if (!entry || !entry.prev) {
|
|
692
860
|
warnings.push(`- [WARN 绑定降级] ${base} 台账 stage=${st} 行缺 prev 字段(schema 演进前行),内容绑定跳过——仅配对判定`);
|
|
693
|
-
} else if (!(typeof entry.ts === 'string' && entry.ts >=
|
|
861
|
+
} else if (!(typeof entry.ts === 'string' && entry.ts >= kitPolicy.bindingTs)) {
|
|
694
862
|
// 该 done 确认发生在生效锚前(旧关单顺序时代)——豁免内容绑定,配对判定照常
|
|
695
863
|
} else if (bindingSha256(linesOf(doc) || [], entry.prev) !== entry.fingerprint) {
|
|
696
864
|
blockers.push(`- [确认内容漂移] ${base} ${st} 后内容与确认台账不符——已关单文档不得直接改(关单编辑先于关单确认);确需修订走 superseded 或新 intent 引用`);
|
|
@@ -725,6 +893,10 @@ if (gitOut(['rev-parse', '--git-dir']) !== null && gitOut(['rev-parse', '-q', '-
|
|
|
725
893
|
// of > 1 = 一次调用落多份态。delegated 形态下这正是「并录」(入口已拒多份,此处抓历史/绕行);
|
|
726
894
|
// TTY 形态天然逐份过目(用户亲手键入),of>1 不构成违规——故只对 delegated 行报。
|
|
727
895
|
if (!rows.some((r) => r.source === 'chat-delegated')) continue;
|
|
896
|
+
// 协作道批量豁免(2026-09-30 hybrid-governance-risk-lanes):confirm-doc --batch 仅受理 L0/L1
|
|
897
|
+
// (逐份读级别,L2/L3 逐份拒绝),台账行带 brief:true——全行带标记的批次是 sanctioned 泳道行为
|
|
898
|
+
// (异步审计兜底),不出「确认并录」;缺标记(旧版脚本/手造台账)照旧告警,判据偏严不偏松。
|
|
899
|
+
if (rows.every((r) => r.brief === true)) continue;
|
|
728
900
|
const of = Math.max(...rows.map((r) => (typeof r.of === 'number' ? r.of : 0)));
|
|
729
901
|
const docs = rows.map((r) => String(r.doc).replace(/^workflow\//, '')).join('、');
|
|
730
902
|
warnings.push(`- [WARN 确认并录] 单次调用落账 ${rows.length} 份(batch ${b},of=${of}):${docs}——同一次 --delegated 调用放行多份,塌掉「逐件确认」门(build.md);口径见 workflow/papercuts.md 2026-09-28 与 incidents/2026-09-28-batch-ledger-audit.md`);
|
|
@@ -733,270 +905,290 @@ if (gitOut(['rev-parse', '--git-dir']) !== null && gitOut(['rev-parse', '-q', '-
|
|
|
733
905
|
}
|
|
734
906
|
|
|
735
907
|
// --- 16. 量化断言指标签名对账 [warning](2026-09-28 claim-exceeds-fix;登记表单源 .agents/metric-claims.txt)---
|
|
736
|
-
//
|
|
737
|
-
//
|
|
738
|
-
//
|
|
739
|
-
//
|
|
740
|
-
//
|
|
741
|
-
//
|
|
742
|
-
//
|
|
908
|
+
// 判据要点:活跃态文档(draft/approved/open)中的 `{{指标名}}`(小写点分)签名须替换为实时值,留签名=未回填=
|
|
909
|
+
// warning;只查显式签名、不全文扫数字(历史叙述假阳性恒 0);未登记签名 / 取数器缺失 → fail-loud 出账;
|
|
910
|
+
// 登记表缺失 → 静默跳过(未启用该检查的装户零噪声);装户自有指标走 .agents/metric-derivers.cjs(CJS 契约,
|
|
911
|
+
// 内置优先)。实现自 2026-09-28 check16-inline-debt 起迁出至 check-metric-claims.mjs(原内联块,判据语义/
|
|
912
|
+
// 消息文案/exit 语义零变化,迁出经全量输出 diff 基线验证)。ctx 显式携带本检查引用的外层符号
|
|
913
|
+
// (ROOT/ENUMS/docFiles/fmGet/inSet/isTracked/linesOf/readdirOrNull/warnings,以代码实际引用集为准);
|
|
914
|
+
// 调用位置须保持在检查 15 之后、输出段之前——warnings 按插入序输出,位置变化会改变输出行序。
|
|
915
|
+
runCheck16({ ROOT, ENUMS, docFiles, fmGet, inSet, isTracked, linesOf, readdirOrNull, warnings });
|
|
916
|
+
|
|
917
|
+
// --- 17. 发版提交树上仍未收口的 intent/spec/plan [hard-block] ---
|
|
918
|
+
// 最近一次 package.json version 发生变化的提交里,当时状态已是 draft 或 approved 的
|
|
919
|
+
// intent/spec/plan,被扫描的树上仍是 draft 或 approved 则阻断。
|
|
920
|
+
// 当时已是 draft:不看版本锚。当时已是 approved:只在发版版本大于
|
|
921
|
+
// policy.check17UnclosedAfter 时阻断。版本 1 没有该键,因此不启用后两格。
|
|
922
|
+
// 版本按主、次、修订三段整数比较。沿 package.json 的全部历史查找,不设次数上限。
|
|
923
|
+
// 该提交之后新建的文件不在范围内。open 的 incident 不在此列。
|
|
743
924
|
{
|
|
744
|
-
const
|
|
745
|
-
if (
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
};
|
|
759
|
-
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
'docs.count.specs': () => countDocs('specs'),
|
|
771
|
-
'docs.count.plans': () => countDocs('plans'),
|
|
772
|
-
'docs.count.incidents': () => countDocs('incidents'),
|
|
773
|
-
'docs.count.all': () => countDocs('intents') + countDocs('specs') + countDocs('plans') + countDocs('incidents'),
|
|
774
|
-
};
|
|
775
|
-
// ---- 装户侧取数器(2026-09-28 adopter-derivers;修「owned 登记表配 managed 取数器」P1)----
|
|
776
|
-
// 背景:登记表是 owned(装户自增指标),取数器却曾硬编码在本文件(managed)——装户一新增指标
|
|
777
|
-
// 就报「无取数器」,唯一出路是改本文件,而那是 managed:sync 永久报「本地已改」+ doctor WARN
|
|
778
|
-
// + 本脚本因供应链防线**跳过执行**(门禁静默停摆)。故改为分层:
|
|
779
|
-
// ① 内置优先(行为与今完全一致,装户零配置可用);② 未命中则查装户模块 `.agents/metric-derivers.cjs`
|
|
780
|
-
// **必须是 CJS(`.cjs` + module.exports)**(2026-09-28 跨版本实测修正):
|
|
781
|
-
// 首版约定 `.mjs` 并用 `createRequire` 载入,在 Node v24.12 上实测可用(**故本机没暴露问题**);
|
|
782
|
-
// 但跨版本实测(真下 18.20.5 / 20.18.0 / 22.11.0 二进制跑)**三者全部 `ERR_REQUIRE_ESM`**——
|
|
783
|
-
// `require(esm)` 只在 Node ≥20.19 / ≥22.12 默认可用,Node 18 从未支持。而本仓 engines 写
|
|
784
|
-
// `>=18.0.0`、CI 矩阵跑 18/22 → **装户取数功能在 CI 目标版本上完全不可用**(fail-loud 正常、
|
|
785
|
-
// 不崩,但功能不工作)。故改用 CJS:`createRequire` 载入 `.cjs` 在 18/20/22/24 全线可用,
|
|
786
|
-
// 且**仍是同步**(无须把本脚本改成 async 主线)。契约要求同步函数也与本检查「零网络、
|
|
787
|
-
// 本地确定性」的前提一致(异步的唯一真实收益是网络/并发 IO,此处都不允许)。
|
|
788
|
-
// **失败一律响亮、绝不静默降级**(沿 claim-exceeds-fix「假绿比红危险」与本检查自身语法错教训):
|
|
789
|
-
// 若载入失败就「只用内置指标」,装户会看到「引用了 my.metric → 未登记」,看起来像**自己忘了登记**,
|
|
790
|
-
// 而真因是模块坏了——他会去 registry 反复核对,问题永远查不到。故载入/调用失败均出明确 WARN。
|
|
791
|
-
const adopterModRel = '.agents/metric-derivers.cjs';
|
|
792
|
-
// 旧路径(首版 .mjs):若存在则**明确提示已改用 .cjs**,避免装户对着「无取数器」查不出所以然
|
|
793
|
-
const adopterLegacyRel = '.agents/metric-derivers.mjs';
|
|
794
|
-
let adopterState = null; // { ok:true, derivers } | { ok:false, errors:[...] } —— 懒载入 + 单次运行内缓存
|
|
795
|
-
const loadAdopterDerivers = () => {
|
|
796
|
-
if (adopterState) return adopterState;
|
|
797
|
-
const abs = path.join(ROOT, adopterModRel);
|
|
798
|
-
if (!fs.existsSync(abs)) {
|
|
799
|
-
// 常见误写:仍用首版的 .mjs 路径 → 给出可操作的迁移指引(而非任其对着「无取数器」猜)
|
|
800
|
-
if (fs.existsSync(path.join(ROOT, adopterLegacyRel))) {
|
|
801
|
-
adopterState = {
|
|
802
|
-
ok: false,
|
|
803
|
-
errors: [`${adopterModRel} 不存在,但发现旧版路径 ${adopterLegacyRel}——该路径已于 2026-09-28 弃用(\`require(esm)\` 在 Node 18/22 不可用,实测 ERR_REQUIRE_ESM)。请改名为 ${adopterModRel} 并把写法改为 CommonJS:\`module.exports = { derivers: { ... } }\``],
|
|
804
|
-
};
|
|
805
|
-
return adopterState;
|
|
806
|
-
}
|
|
807
|
-
adopterState = { ok: true, derivers: {} }; // 缺失=正常态,零告警
|
|
808
|
-
return adopterState;
|
|
925
|
+
const log = gitOut(['log', '--format=%H', '--', 'package.json']);
|
|
926
|
+
if (log) {
|
|
927
|
+
let release = null;
|
|
928
|
+
let releaseVer = '';
|
|
929
|
+
for (const sha of log.split('\n').map((s) => s.trim()).filter(Boolean)) {
|
|
930
|
+
const curTxt = gitOut(['show', `${sha}:package.json`]);
|
|
931
|
+
if (!curTxt) continue;
|
|
932
|
+
let cur = null;
|
|
933
|
+
try { cur = JSON.parse(curTxt).version; } catch { continue; }
|
|
934
|
+
const prevTxt = gitOut(['show', `${sha}^:package.json`]);
|
|
935
|
+
let prev = null;
|
|
936
|
+
if (prevTxt) { try { prev = JSON.parse(prevTxt).version; } catch { prev = null; } }
|
|
937
|
+
if (cur && cur !== prev) { release = sha; releaseVer = cur; break; }
|
|
938
|
+
}
|
|
939
|
+
const tree = release && gitOut(['ls-tree', '-r', '--name-only', release, '--', `${WF}/intents`, `${WF}/specs`, `${WF}/plans`]);
|
|
940
|
+
if (release && tree) {
|
|
941
|
+
for (const rel of tree.split('\n').map((s) => s.trim()).filter(Boolean)) {
|
|
942
|
+
if (!rel.endsWith('.md') || rel.endsWith('_TEMPLATE.md')) continue;
|
|
943
|
+
const thenTxt = gitOut(['show', `${release}:${rel}`]);
|
|
944
|
+
if (!thenTxt) continue;
|
|
945
|
+
const thenSt = fmStatus(thenTxt);
|
|
946
|
+
const nowSt = fmGet(path.join(ROOT, rel), '状态');
|
|
947
|
+
if ((thenSt !== 'draft' && thenSt !== 'approved') || (nowSt !== 'draft' && nowSt !== 'approved')) continue;
|
|
948
|
+
if (thenSt === 'approved' && !versionGreater(releaseVer, kitPolicy.check17UnclosedAfter)) continue;
|
|
949
|
+
const here = REV ? `提交 ${REV.slice(0, 7)}` : '工作区';
|
|
950
|
+
blockers.push(`- [发版草稿] ${rel} 在 version=${releaseVer} 的提交 ${release.slice(0, 7)} 上已是 ${thenSt},${here}仍是 ${nowSt}。发版提交树上未收口的 intent/spec/plan 须先离开 draft 与 approved。覆盖面只含该提交当时已在树上的 intent/spec/plan;其后新建的文件不在此列`);
|
|
809
951
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// --- 18. 委派台账对账 [warning] ---
|
|
957
|
+
// L2/L3 的 intent、spec、plan 状态为 done,或同级别 incident 状态为 fixed 或 closed 时,
|
|
958
|
+
// 「委派结果」表与「自做任务结果」表均没有日期不早于文档日期、且含该文件名(带/不带 .md 均匹配)的一行,则警告。
|
|
959
|
+
// 缺级别、缺日期、以及其他状态不警告。audit:false 时 warnings.push 已被换成空函数。
|
|
960
|
+
// 两表解析口径与 agg-delegations.cjs splitTables 保持一致,一边改另一边须跟。
|
|
961
|
+
{
|
|
962
|
+
const rows = delegationResultRows(path.join(ROOT, WF, 'delegations.md'));
|
|
963
|
+
for (const sub of ['intents', 'specs', 'plans', 'incidents']) {
|
|
964
|
+
for (const abs of docFiles(sub)) {
|
|
965
|
+
const lvl = fmGet(abs, '级别');
|
|
966
|
+
if (lvl !== 'L2' && lvl !== 'L3') continue;
|
|
967
|
+
const st = fmGet(abs, '状态');
|
|
968
|
+
const inScope = sub === 'incidents' ? (st === 'fixed' || st === 'closed') : st === 'done';
|
|
969
|
+
if (!inScope) continue;
|
|
970
|
+
const date = sub === 'incidents' ? (fmGet(abs, '发现') || fmGet(abs, '日期')) : fmGet(abs, '日期');
|
|
971
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) continue;
|
|
972
|
+
const base = path.basename(abs);
|
|
973
|
+
const stripped = base.replace(/\.md$/, '');
|
|
974
|
+
if (rows.some((r) => r.date >= date && (r.line.includes(base) || r.line.includes(stripped)))) continue;
|
|
975
|
+
const rel = path.relative(ROOT, abs).split(path.sep).join('/');
|
|
976
|
+
warnings.push(`- [WARN 委派台账] ${rel} 在 ${date} 及之后的委派结果/自做任务结果表中没有该文件名`);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// --- 19. 逐阶段审计 [warning](2026-09-30 stage-gate-machine;口径与 stage-gates.mjs / fill-* / confirm-doc 前置门互引) ---
|
|
982
|
+
// 判据 A(在途扫描):同名 spec/plan 为 draft 且其日期 ≥ stageGateSince 时——入口未确认(intent:状态非
|
|
983
|
+
// approved/done、或缺台账行且非存量;incident:「时间线」小节缺行首「用户确认」条目)→ warning;
|
|
984
|
+
// 入口缺合法「级别」→ warning(与起草门 fail-closed 同口径,2026-09-30 复核 P2-7);
|
|
985
|
+
// plan 另判(入口级别 L2/L3 且 spec 未确认或缺失)→ warning。
|
|
986
|
+
// 判据 B(台账顺序):同主题 {intents,specs,plans} 的 approved 行(每 doc 取最早)时间须非降序;仅对
|
|
987
|
+
// 组内最早 ts 日期 ≥ stageGateSince 的主题判定;含更早 ts 的主题整组跳过(历史豁免)。
|
|
988
|
+
// 协作道豁免(2026-09-30 hybrid-governance-risk-lanes):入口 intent 为 draft 且级别 L0/L1 → 判据 A 静默
|
|
989
|
+
// (「先动手后确认」是泳道语义;approved/done 无台账行仍照报);判据 B 对入口 L0/L1 的主题豁免顺序
|
|
990
|
+
// 倒置判定(入口文件缺失不豁免)。
|
|
991
|
+
// stageGateSince 缺键(policy v1)→ 本检查整体跳过;全部 warning、不 hard-block。
|
|
992
|
+
{
|
|
993
|
+
const since = kitPolicy.stageGateSince;
|
|
994
|
+
if (typeof since === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(since)) {
|
|
995
|
+
const ledgerRows = [];
|
|
996
|
+
{
|
|
997
|
+
const lp = path.join(ROOT, '.agents', 'confirmations.jsonl');
|
|
998
|
+
if (fs.existsSync(lp)) {
|
|
999
|
+
for (const line of linesOf(lp) || []) {
|
|
1000
|
+
if (!line.trim()) continue;
|
|
1001
|
+
try { ledgerRows.push(JSON.parse(line)); } catch { /* 坏行跳过(同检查 15 口径) */ }
|
|
819
1002
|
}
|
|
820
|
-
adopterState = { ok: true, derivers: d };
|
|
821
|
-
} catch (e) {
|
|
822
|
-
const hint = /ERR_REQUIRE_ESM/.test(String(e && e.code)) ? `(该错误表示模块被当成了 ESM——本契约要求 CommonJS:文件后缀 .cjs 且用 \`module.exports = { derivers: {...} }\`)` : '';
|
|
823
|
-
adopterState = { ok: false, errors: [`${adopterModRel} 载入失败:${String(e && e.message ? e.message : e).split('\n')[0]}${hint}——载入失败不降级(静默降级会让你误以为「忘了登记」而非「模块坏了」)`] };
|
|
824
1003
|
}
|
|
825
|
-
|
|
1004
|
+
}
|
|
1005
|
+
const hasRow = (rel) => ledgerRows.some((e) => e && e.doc === rel && (e.stage === 'approved' || e.stage === 'done'));
|
|
1006
|
+
const docConfirmed = (rel, abs) => {
|
|
1007
|
+
const st = fmGet(abs, '状态');
|
|
1008
|
+
if (st !== 'approved' && st !== 'done') return { ok: false, legacy: false, why: `状态「${st || '缺失'}」` };
|
|
1009
|
+
if (hasRow(rel)) return { ok: true, level: fmGet(abs, '级别'), legacy: false };
|
|
1010
|
+
const d = fmGet(abs, '日期') || fmGet(abs, '发现');
|
|
1011
|
+
const legacy = (linesOf(abs) || []).some((l) => /^流程: legacy/.test(l))
|
|
1012
|
+
|| (/^\d{4}-\d{2}-\d{2}$/.test(d) && d < kitPolicy.confirmDocsEffective);
|
|
1013
|
+
if (legacy) return { ok: true, level: fmGet(abs, '级别'), legacy: true };
|
|
1014
|
+
return { ok: false, legacy: false, why: '无确认台账行' };
|
|
826
1015
|
};
|
|
827
|
-
//
|
|
828
|
-
//
|
|
829
|
-
//
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
const s = String(p);
|
|
836
|
-
let re = '';
|
|
837
|
-
for (let i = 0; i < s.length; i++) {
|
|
838
|
-
const c = s[i];
|
|
839
|
-
if (c === '*' && s[i + 1] === '*') {
|
|
840
|
-
if (s[i + 2] === '/') { re += '(?:.*/)?'; i += 2; } // `**/` 含零层
|
|
841
|
-
else { re += '.*'; i += 1; } // `**` 跨段
|
|
842
|
-
} else if (c === '*') re += '[^/]*';
|
|
843
|
-
else if (c === '?') re += '[^/]';
|
|
844
|
-
else re += c.replace(/[.+^${}()|[\]\\]/, '\\$&');
|
|
1016
|
+
// incident 留痕判定:**直接复用 stage-gates.mjs 的 MARK_RE**(第七轮复核 N3:此前此处内联复制
|
|
1017
|
+
// 一份同口径字面量,两处靠注释人工同步、无测试钉住——改为单源复用,从结构上消除漂移可能)。
|
|
1018
|
+
// 判据同源:「## 时间线」小节内、行首列表条目含「用户确认」(词边界见 MARK_RE)。
|
|
1019
|
+
const incidentMarked19 = (lines) => {
|
|
1020
|
+
let inTimeline = false;
|
|
1021
|
+
for (const l of lines || []) {
|
|
1022
|
+
if (/^##\s/.test(l)) { inTimeline = /^##\s*时间线/.test(l); continue; }
|
|
1023
|
+
if (inTimeline && MARK_RE.test(l)) return true;
|
|
845
1024
|
}
|
|
846
|
-
return
|
|
1025
|
+
return false;
|
|
847
1026
|
};
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
for (const e of readdirOrNull(dir) || []) {
|
|
860
|
-
if (WALK_SKIP.has(e)) continue;
|
|
861
|
-
const abs = path.join(dir, e);
|
|
862
|
-
const r = rel ? `${rel}/${e}` : e;
|
|
863
|
-
let st = null;
|
|
864
|
-
try { st = fs.statSync(abs); } catch { continue; }
|
|
865
|
-
if (st.isDirectory()) walkFiles(abs, out, r, visited);
|
|
866
|
-
else out.push(r);
|
|
1027
|
+
const entryConfirmed19 = (base) => {
|
|
1028
|
+
const irel = `${WF}/intents/${base}.md`;
|
|
1029
|
+
const iabs = path.join(ROOT, irel);
|
|
1030
|
+
if (fs.existsSync(iabs)) {
|
|
1031
|
+
const st19 = fmGet(iabs, '状态');
|
|
1032
|
+
const lvl19 = fmGet(iabs, '级别');
|
|
1033
|
+
const r = docConfirmed(irel, iabs);
|
|
1034
|
+
const body = (linesOf(iabs) || []).join('\n');
|
|
1035
|
+
const d19 = fmGet(iabs, '日期') || fmGet(iabs, '发现');
|
|
1036
|
+
const storLegacy = /^流程: legacy/m.test(body) || (/^\d{4}-\d{2}-\d{2}$/.test(d19) && d19 < kitPolicy.confirmDocsEffective);
|
|
1037
|
+
return { ...r, level: lvl19, entryDraft: st19 === 'draft', storageLegacy: storLegacy };
|
|
867
1038
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
// resolveDeriver(name) → { fn } | { error }(**绝不抛出**:引擎不因装户代码崩溃)
|
|
877
|
-
const resolveDeriver = (name) => {
|
|
878
|
-
if (derivers[name]) return { fn: derivers[name], builtin: true };
|
|
879
|
-
const st = loadAdopterDerivers();
|
|
880
|
-
if (!st.ok) return { error: st.errors.join(';') };
|
|
881
|
-
const fn = st.derivers[name];
|
|
882
|
-
if (typeof fn !== 'function') {
|
|
883
|
-
if (fn !== undefined) return { error: `${adopterModRel} 的「${name}」不是函数(实际类型:${typeof fn})` };
|
|
884
|
-
return { error: `无对应取数器——内置 8 个指标均不含该名,${adopterModRel} 亦未定义(登记了却没实现=其实没查)` };
|
|
1039
|
+
const crel = `${WF}/incidents/${base}.md`;
|
|
1040
|
+
const cabs = path.join(ROOT, crel);
|
|
1041
|
+
if (fs.existsSync(cabs)) {
|
|
1042
|
+
const st = fmGet(cabs, '状态');
|
|
1043
|
+
if ((st === 'open' || st === 'fixed' || st === 'closed') && incidentMarked19(linesOf(cabs))) {
|
|
1044
|
+
return { ok: true, level: fmGet(cabs, '级别'), legacy: false, storageLegacy: false };
|
|
1045
|
+
}
|
|
1046
|
+
return { ok: false, legacy: false, storageLegacy: false, why: 'incident 未见过目留痕(时间线条目缺失)' };
|
|
885
1047
|
}
|
|
886
|
-
return {
|
|
1048
|
+
return { ok: false, legacy: false, storageLegacy: false, why: '无同名入口' };
|
|
887
1049
|
};
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
// 无取数器(内置无 + 装户模块无同名)或装户模块载入失败 → 明示可操作方向
|
|
915
|
-
const hint = adopterLoad.ok
|
|
916
|
-
? `若为项目自有指标,请在 ${adopterModRel} 的 derivers 中实现(契约见 .agents/metric-claims.txt 头部)`
|
|
917
|
-
: `且 ${adopterModRel} 载入失败(见上方「装户取数器载入失败」条目——**这不是「忘了登记」**)`;
|
|
918
|
-
warnings.push(`- [WARN 指标登记] 指标「${m[1]}」无对应取数器(fail-loud:登记了却没实现=其实没查)——内置 8 指标不含该名,${hint}`);
|
|
919
|
-
// **不 continue**:该指标在 registry 里确实登记过,故仍计入 declared——
|
|
920
|
-
// 否则引用它的文档会被误报为「未登记」(把「模块坏了」伪装成「忘了登记」,正是本单要防的假绿)。
|
|
921
|
-
// 取数失败在扫描阶段以「指标取数失败」单独出账,两条信息不互相掩盖。
|
|
922
|
-
} else if (r.builtin && adopterLoad.ok && typeof adopterLoad.derivers[m[1]] === 'function' && !precedenceWarned.has(m[1])) {
|
|
923
|
-
// 内置与装户同名:内置优先(保持本仓/存量行为稳定),并明示装户定义被忽略(**按指标去重**)
|
|
924
|
-
precedenceWarned.add(m[1]);
|
|
925
|
-
warnings.push(`- [WARN 指标登记] 指标「${m[1]}」由引擎内置提供,${adopterModRel} 中的同名定义**被忽略**(内置优先)——如需改用自有实现,请换一个指标名`);
|
|
1050
|
+
for (const sub of ['specs', 'plans']) {
|
|
1051
|
+
for (const abs of docFiles(sub)) {
|
|
1052
|
+
if (fmGet(abs, '状态') !== 'draft') continue;
|
|
1053
|
+
const base = path.basename(abs).replace(/\.md$/, '');
|
|
1054
|
+
// 日期:frontmatter 优先,缺则回落文件名前缀(plan 无「日期」字段;沿检查 15 口径)
|
|
1055
|
+
let d = fmGet(abs, '日期');
|
|
1056
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(d)) d = /^\d{4}-\d{2}-\d{2}$/.test(base.slice(0, 10)) ? base.slice(0, 10) : '';
|
|
1057
|
+
if (!(/^\d{4}-\d{2}-\d{2}$/.test(d) && d >= since)) continue;
|
|
1058
|
+
const rel = path.relative(ROOT, abs).split(path.sep).join('/');
|
|
1059
|
+
const ent = entryConfirmed19(base);
|
|
1060
|
+
if (!ent.ok) {
|
|
1061
|
+
// 协作道异步审计豁免(2026-09-30 hybrid-governance-risk-lanes):入口 intent 仍为 draft 且
|
|
1062
|
+
// 级别 L0/L1 → 「先动手后确认」是泳道语义,不告警;approved/done 却无台账行仍照报(防手改
|
|
1063
|
+
// 状态冒充确认,与交叉一致性口径一致);incident 入口不豁免(过目留痕本就轻量)。
|
|
1064
|
+
if (ent.entryDraft && (ent.level === 'L0' || ent.level === 'L1')) continue;
|
|
1065
|
+
warnings.push(`- [WARN 逐阶段] ${rel} 起草先于入口确认(${ent.why})`); continue;
|
|
1066
|
+
}
|
|
1067
|
+
// 库存量形态(标记 / 日期早于生效日)短路 level 校验与 spec 档(与起草门同口径,N1)
|
|
1068
|
+
const storLegacy = ent.storageLegacy === true;
|
|
1069
|
+
if (!storLegacy && !/^L[0-3]$/.test(ent.level)) { warnings.push(`- [WARN 逐阶段] ${rel} 入口缺合法「级别」字段(无法判定前置深度;与起草门 fail-closed 同口径,库存量形态除外)`); continue; }
|
|
1070
|
+
if (sub === 'plans' && !storLegacy && (ent.level === 'L2' || ent.level === 'L3')) {
|
|
1071
|
+
const srel = `${WF}/specs/${base}.md`;
|
|
1072
|
+
const sabs = path.join(ROOT, srel);
|
|
1073
|
+
const sp = fs.existsSync(sabs) ? docConfirmed(srel, sabs) : { ok: false, why: '缺同名 spec' };
|
|
1074
|
+
if (!sp.ok) warnings.push(`- [WARN 逐阶段] ${rel} 起草先于 spec 确认(${sp.why})`);
|
|
1075
|
+
}
|
|
926
1076
|
}
|
|
927
|
-
declared.set(m[1], true);
|
|
928
1077
|
}
|
|
929
|
-
//
|
|
930
|
-
const
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
scanFiles16.push({ rel: `workflow/${sub}/${f}`, abs: path.join(dir, f) });
|
|
938
|
-
}
|
|
1078
|
+
const firstApproved = new Map(); // base -> { intents?, specs?, plans? } 最早 approved ts
|
|
1079
|
+
for (const e of ledgerRows) {
|
|
1080
|
+
if (!e || e.stage !== 'approved' || typeof e.doc !== 'string') continue;
|
|
1081
|
+
const m = /^workflow\/(intents|specs|plans)\/(.+)\.md$/.exec(e.doc);
|
|
1082
|
+
if (!m) continue;
|
|
1083
|
+
const rec = firstApproved.get(m[2]) || {};
|
|
1084
|
+
if (!rec[m[1]] && typeof e.ts === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(e.ts)) rec[m[1]] = e.ts;
|
|
1085
|
+
firstApproved.set(m[2], rec);
|
|
939
1086
|
}
|
|
940
|
-
for (const
|
|
941
|
-
|
|
1087
|
+
for (const [base, rec] of firstApproved) {
|
|
1088
|
+
const seq = ['intents', 'specs', 'plans'].filter((k) => rec[k]);
|
|
1089
|
+
if (seq.length < 2) continue;
|
|
1090
|
+
// 协作道豁免(2026-09-30 hybrid-governance-risk-lanes):入口 L0/L1 的审批顺序倒置(如 plan 先于
|
|
1091
|
+
// intent approved——「动手后确认」)是异步审计泳道语义,非流程违规;入口文件缺失(纯台账历史行 /
|
|
1092
|
+
// incident 侧)不豁免,维持既有判据。
|
|
1093
|
+
const ilvlB = fmGet(path.join(ROOT, WF, 'intents', `${base}.md`), '级别');
|
|
1094
|
+
if (ilvlB === 'L0' || ilvlB === 'L1') continue;
|
|
1095
|
+
const earliest = seq.map((k) => rec[k]).sort()[0];
|
|
1096
|
+
if (!(earliest.slice(0, 10) >= since)) continue;
|
|
1097
|
+
let ordered = true;
|
|
1098
|
+
for (let i = 1; i < seq.length; i++) if (rec[seq[i]] < rec[seq[i - 1]]) ordered = false;
|
|
1099
|
+
if (!ordered) warnings.push(`- [WARN 逐阶段] 台账审批顺序倒置:${base}(${seq.map((k) => `${k}=${rec[k].slice(0, 19)}`).join(' / ')})`);
|
|
942
1100
|
}
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
// `{{Ledger.Lines}}` / `{{ ledger.lines }}`(含空格)这类**明显本意是签名**的写法
|
|
986
|
-
// 既不匹配、也不出账——静默漏过,且绕开了「未登记」的 fail-loud。此处补一道:
|
|
987
|
-
// 非转义的 `{{...}}` 中,凡**不是**严格形态、且**不是**装户模板占位符(全大写 SCREAMING_CASE)
|
|
988
|
-
// 的,一律提示形态不符(作者才能立刻发现笔误)。
|
|
989
|
-
for (const m of line.matchAll(/(?<!\\)\{\{([^{}]+)\}\}/g)) {
|
|
990
|
-
const raw = m[1];
|
|
991
|
-
if (/^[a-z][a-z0-9]*(?:\.[a-z0-9]+)+$/.test(raw)) continue; // 严格形态,上面已处理
|
|
992
|
-
if (/^[A-Z][A-Z0-9_]*$/.test(raw)) continue; // 装户模板占位符(init 渲染对象,非本检查面)
|
|
993
|
-
warnings.push(`- [WARN 指标形态] ${rel}:${i + 1} \`{{${raw}}}\` 不是合法签名形态(应为小写点分,如 {{ledger.lines}})——若本意是量化断言,请改正形态;若只是举例,请加反斜杠转义`);
|
|
994
|
-
}
|
|
995
|
-
});
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// --- 加固门(仅 --hardening 启用;2026-09-30 hybrid-governance-explore-hardening)[hard-block] ---
|
|
1105
|
+
// 探索泳道(experiment/* 分支 + intent frontmatter「阶段: exploring」标记;「状态: exploring」亦认——
|
|
1106
|
+
// 非法状态本就会在检查 5 出账)允许「先动手后确认」快速 PoC;成果转正(合入 main)前必须完成加固:
|
|
1107
|
+
// ① intent 状态离开 draft → approved/done(探索作废走显式放弃态 superseded/cancelled——留档即出列,
|
|
1108
|
+
// 不加固:放弃态无代码应随行);
|
|
1109
|
+
// ② 同名 plan 在场且状态收口(∈ doc.status.confirmed;L0 亦不豁免——本门是转正门槛,不是配对门);
|
|
1110
|
+
// ③ 泳道 L2/L3(级别优先、缺失/非法回落 risk_level,与 stage-gates 起草门同款取法)另须同名 spec
|
|
1111
|
+
// 在场且收口。
|
|
1112
|
+
// 触发面:.githooks/pre-push 对 remote=refs/heads/main 的推送以 --hardening 运行本脚本(hard 失败即阻断
|
|
1113
|
+
// push);手跑 `node check-loop.mjs --hardening` 可转正前预检。诚实边界:分支名不进 commit,扫描树无法
|
|
1114
|
+
// 机械判定「曾在 experiment/* 上」——标记靠泳道纪律写入(intent 模板 / workflow README 口径),漏标记的
|
|
1115
|
+
// 探索件不被本门覆盖;本门拦的是「已标记未收口」。
|
|
1116
|
+
if (HARDENING) {
|
|
1117
|
+
for (const intent of docFiles('intents')) {
|
|
1118
|
+
if (fmGet(intent, '阶段') !== 'exploring' && fmGet(intent, '状态') !== 'exploring') continue;
|
|
1119
|
+
const base = path.basename(intent);
|
|
1120
|
+
const st = fmGet(intent, '状态');
|
|
1121
|
+
if (st === 'superseded' || st === 'cancelled') continue; // 放弃态:探索作废留档,不加固
|
|
1122
|
+
const lvl = fmGet(intent, '级别');
|
|
1123
|
+
const lane = /^L[0-3]$/.test(lvl) ? lvl : fmGet(intent, 'risk_level');
|
|
1124
|
+
if (st !== 'approved' && st !== 'done') {
|
|
1125
|
+
blockers.push(`- [加固未过] exploring 任务入 main 须先收口:${base} intent 状态仍为『${st || '缺失'}』——先确认至 approved/done 并补齐同名 spec+plan,或显式放弃(superseded/cancelled)`);
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
const planAbs = path.join(ROOT, WF, 'plans', base);
|
|
1129
|
+
const planSt = fs.existsSync(planAbs) ? fmGet(planAbs, '状态') : '';
|
|
1130
|
+
if (!fs.existsSync(planAbs)) {
|
|
1131
|
+
blockers.push(`- [加固未过] exploring 任务入 main 缺同名 plan:${base}(应在 ${WF}/plans/ 下同名)`);
|
|
1132
|
+
} else if (!stOkDoc(planSt)) {
|
|
1133
|
+
blockers.push(`- [加固未过] exploring 任务入 main 的 plan 未收口:${base}(plan 状态『${planSt || '缺失'}』须为 approved/done)`);
|
|
1134
|
+
}
|
|
1135
|
+
if (lane === 'L2' || lane === 'L3') {
|
|
1136
|
+
const specAbs = path.join(ROOT, WF, 'specs', base);
|
|
1137
|
+
const specSt = fs.existsSync(specAbs) ? fmGet(specAbs, '状态') : '';
|
|
1138
|
+
if (!fs.existsSync(specAbs)) {
|
|
1139
|
+
blockers.push(`- [加固未过] exploring 任务入 main 缺同名 spec:${base}(级别 ${lane},应在 ${WF}/specs/ 下同名)`);
|
|
1140
|
+
} else if (!stOkDoc(specSt)) {
|
|
1141
|
+
blockers.push(`- [加固未过] exploring 任务入 main 的 spec 未收口:${base}(spec 状态『${specSt || '缺失'}』须为 approved/done)`);
|
|
1142
|
+
}
|
|
996
1143
|
}
|
|
997
1144
|
}
|
|
998
1145
|
}
|
|
999
1146
|
|
|
1147
|
+
function delegationResultRows(file) {
|
|
1148
|
+
let text = '';
|
|
1149
|
+
try { text = fs.readFileSync(file, 'utf8'); } catch { return []; }
|
|
1150
|
+
const lines = text.split(/\r?\n/);
|
|
1151
|
+
const targetSec = (line) => /^##\s+(委派结果|自做任务结果)\s*$/.test(line);
|
|
1152
|
+
let inSec = false;
|
|
1153
|
+
const rows = [];
|
|
1154
|
+
for (const line of lines) {
|
|
1155
|
+
if (/^##\s+/.test(line)) {
|
|
1156
|
+
inSec = targetSec(line);
|
|
1157
|
+
continue;
|
|
1158
|
+
}
|
|
1159
|
+
if (!inSec || !/^\s*\|/.test(line)) continue;
|
|
1160
|
+
const cells = line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim());
|
|
1161
|
+
if (!cells.length || cells.every((c) => /^[-: ]*$/.test(c))) continue;
|
|
1162
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(cells[0])) continue;
|
|
1163
|
+
rows.push({ date: cells[0], line });
|
|
1164
|
+
}
|
|
1165
|
+
return rows;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function versionGreater(a, b) {
|
|
1169
|
+
const parse = (v) => {
|
|
1170
|
+
const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(String(v ?? ''));
|
|
1171
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
1172
|
+
};
|
|
1173
|
+
const x = parse(a);
|
|
1174
|
+
const y = parse(b);
|
|
1175
|
+
if (!x || !y) return false;
|
|
1176
|
+
for (let i = 0; i < 3; i++) {
|
|
1177
|
+
if (x[i] !== y[i]) return x[i] > y[i];
|
|
1178
|
+
}
|
|
1179
|
+
return false;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function fmStatus(text) {
|
|
1183
|
+
const lines = String(text || '').split(/\r?\n/);
|
|
1184
|
+
if (!/^---\s*$/.test(lines[0] || '')) return '';
|
|
1185
|
+
for (let i = 1; i < lines.length; i++) {
|
|
1186
|
+
if (/^---\s*$/.test(lines[i])) return '';
|
|
1187
|
+
if (lines[i].startsWith('状态:')) return lines[i].slice('状态:'.length).trim();
|
|
1188
|
+
}
|
|
1189
|
+
return '';
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1000
1192
|
// --- 输出(banner 沿 sh 版字样与格式 = 稳定输出契约:banner 后空行、条目 '- ' 前缀)---
|
|
1001
1193
|
if (blockers.length) {
|
|
1002
1194
|
process.stderr.write(`闭环骨架断档(check-loop.sh)— HARD-BLOCK:\n\n${blockers.join('\n')}\n\n`);
|