agentic-flow-kit 0.6.0 → 0.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/modules/hosts/opencode/commands/{sync-hosts.md → wf-sync-hosts.md} +1 -1
- package/modules/hosts/trae/commands/wf-sync-hosts.md +1 -1
- package/package.json +1 -1
- package/src/doctor.mjs +9 -4
- package/src/profiles.mjs +19 -2
- package/src/sync-hosts.mjs +12 -6
- package/src/sync-hosts.test.mjs +14 -11
- package/templates/AGENTS.md +1 -1
- package/templates/_agents/commands/sync-hosts.md +1 -1
- package/templates/_agents/metric-claims.txt +81 -0
- package/templates/_agents/scripts/check-ledger.mjs +65 -0
- package/templates/_agents/scripts/check-ledger.test.mjs +127 -0
- package/templates/_agents/scripts/check-loop.mjs +277 -1
- package/templates/_agents/scripts/check-loop.test.mjs +496 -1
- package/templates/_agents/scripts/doctor.test.mjs +10 -5
- package/templates/_agents/scripts/gate-checklist.mjs +1 -0
- package/templates/_agents/scripts/gen-workflow-metrics.mjs +21 -2
- package/templates/_agents/scripts/source-sync-check.mjs +13 -2
- package/templates/_agents/scripts/source-sync-check.test.mjs +37 -0
- package/templates/_agents/scripts/workflows-check.test.mjs +14 -3
- package/templates/_githooks/pre-commit +17 -1
- /package/modules/hosts/opencode/commands/{build.md → wf-build.md} +0 -0
- /package/modules/hosts/opencode/commands/{deploy.md → wf-deploy.md} +0 -0
- /package/modules/hosts/opencode/commands/{design.md → wf-design.md} +0 -0
- /package/modules/hosts/opencode/commands/{gate-checklist.md → wf-gate-checklist.md} +0 -0
- /package/modules/hosts/opencode/commands/{maintain.md → wf-maintain.md} +0 -0
- /package/modules/hosts/opencode/commands/{new-task.md → wf-new-task.md} +0 -0
- /package/modules/hosts/opencode/commands/{plan.md → wf-plan.md} +0 -0
- /package/modules/hosts/opencode/commands/{review.md → wf-review.md} +0 -0
- /package/modules/hosts/opencode/commands/{source-sync-check.md → wf-source-sync-check.md} +0 -0
- /package/modules/hosts/opencode/commands/{test.md → wf-test.md} +0 -0
|
@@ -38,7 +38,9 @@
|
|
|
38
38
|
// **受管准入 = 两条件取或(2026-09-28 改锚;此前仅「自报日期 ≥ 生效日」单条件——
|
|
39
39
|
// 新档只要把日期写早即整段跳过判定,incident 2026-09-28-confirm-gate-effective-date-anchor 实证)**:
|
|
40
40
|
// ① 该 doc 在台账中有合法跳转行(stage∈approved/done/fixed/closed/superseded/cancelled,append-only
|
|
41
|
-
// 取末次)
|
|
41
|
+
// 取末次)——「确认确实发生过」的机器事实,作者**不可顺手填**(经 confirm-doc 走确认门才产生);
|
|
42
|
+
// **诚实边界**:台账是本地可写文件、本检查不验签,刻意手改台账仍可伪造——与「伪造 git 时间戳」
|
|
43
|
+
// 同属本地信任边界内不可机器防,事后对质靠 git 历史与台账(2026-09-28-claim-exceeds-fix 口径统一);
|
|
42
44
|
// ② 自报日期(日期/发现/文件名兜底)≥ 生效日——覆盖「新档完全没跑 confirm-doc」的漏网面
|
|
43
45
|
// (此类文档无台账行,仅靠 ① 会误判为存量豁免)。
|
|
44
46
|
// 两条件皆不满足 = 存量豁免(生效日前既有、从未走过确认门,不追溯)。
|
|
@@ -63,6 +65,14 @@
|
|
|
63
65
|
// 等于引导伪装)/噪声不可消退(不按日期门豁免,退役前本仓 22 条 advisory 中 14 条为该告警)/
|
|
64
66
|
// quote 字段职责冲突(对质凭据 vs 并录指纹,现归还单一职责)。无 batch 的历史行静默跳过
|
|
65
67
|
// (无判定依据的行不产出不可消除噪声——沿 audit-gate-hardening P3 教训)
|
|
68
|
+
// 16. 量化断言指标签名对账 [warning](2026-09-28 起;登记表单源 .agents/metric-claims.txt):
|
|
69
|
+
// 判据 = 活跃态文档(draft/approved/open)中的 `{{指标名}}` 签名须替换为实时值,留签名=未回填=warning。
|
|
70
|
+
// **只查显式签名、不全文扫数字**(据实说明):本仓活跃文档「N 行/N 条/N 份」类表述数十处,绝大多数是
|
|
71
|
+
// 历史叙述(描述某次提交当时的规模,天然不随当前事实变化)——全文扫描对这些正确表述产生大量假阳性,
|
|
72
|
+
// 而本脚本已立红线「无判定依据的行不产出不可消除噪声」(P3:噪声淹没真漏点)。故本检查**假阳性恒 0**,
|
|
73
|
+
// 代价是覆盖面依赖作者登记。签名收窄为小写点分形态——装户模板占位符走全大写 SCREAMING_CASE
|
|
74
|
+
// (由 init 渲染替换,如构建/端口/项目名三类,不属本检查面)。未登记签名 / 取数器缺失 → fail-loud 出账
|
|
75
|
+
// (静默会让「登记了却没查」不可见);登记表不存在 → 静默跳过(未启用该检查的装户不应被噪声打扰)。
|
|
66
76
|
//
|
|
67
77
|
// 注:清单条目 5(状态字段+L3 复核)与 1(配对)在同一遍 intents/specs/plans 循环里实现(沿 sh 版代码结构);
|
|
68
78
|
// 条目 6 的旧委派残留/钉死模型子项在「角色契约与 Adapter」代码段实现。
|
|
@@ -80,6 +90,7 @@ import fs from 'node:fs';
|
|
|
80
90
|
import path from 'node:path';
|
|
81
91
|
import { spawnSync } from 'node:child_process';
|
|
82
92
|
import { createHash } from 'node:crypto';
|
|
93
|
+
import { createRequire } from 'node:module';
|
|
83
94
|
import { fileURLToPath } from 'node:url';
|
|
84
95
|
import { loadEnums } from './workflow-enums.mjs';
|
|
85
96
|
|
|
@@ -721,6 +732,271 @@ if (gitOut(['rev-parse', '--git-dir']) !== null && gitOut(['rev-parse', '-q', '-
|
|
|
721
732
|
}
|
|
722
733
|
}
|
|
723
734
|
|
|
735
|
+
// --- 16. 量化断言指标签名对账 [warning](2026-09-28 claim-exceeds-fix;登记表单源 .agents/metric-claims.txt)---
|
|
736
|
+
// 判据:活跃态文档(draft/approved/open)中出现的 `{{指标名}}` 签名,实时取数比对。
|
|
737
|
+
// **为什么只查显式签名、不全文扫数字**(据实说明):本仓活跃文档「N 行/N 条/N 份」类表述数十处,
|
|
738
|
+
// 绝大多数是**历史叙述**(描述某次提交当时的规模,天然不随当前事实变化)——全文扫描会对这些
|
|
739
|
+
// 正确表述产生大量假阳性,而本脚本已把「无判定依据的行不产出不可消除噪声」立为红线
|
|
740
|
+
// (沿 audit-gate-hardening P3:噪声淹没真漏点)。故本检查假阳性恒为 0,代价是覆盖面靠登记。
|
|
741
|
+
// 未登记签名 / 取数器缺失 → fail-loud(不静默跳过:静默会让「登记了但其实没查」不可见)。
|
|
742
|
+
// 登记表缺失 → 静默跳过(未启用该检查的装户不应被噪声打扰)。
|
|
743
|
+
{
|
|
744
|
+
const mcFile = path.join(ROOT, '.agents', 'metric-claims.txt');
|
|
745
|
+
if (fs.existsSync(mcFile)) {
|
|
746
|
+
// 取数器闭集:全部本地确定性、零网络。新增指标须同时改本表与 metric-claims.txt(缺一 fail-loud)
|
|
747
|
+
// **取数须与扫描面同口径(复核 P2 更正)**:文档扫描走 `docFiles()`(仓库模式只取 HEAD **tracked**
|
|
748
|
+
// 内容,见 isTracked),首版 `countDocs` 却直接 readdir 数盘面——实测本仓 `docs.count.plans` 会
|
|
749
|
+
// 把并行会话的**未跟踪**新档算进去(盘面 60 / HEAD 59),于是「签名填对了」却是别的读者复现不出的
|
|
750
|
+
// 数。故此处同样以 isTracked 过滤,保证「扫描看得到的文档集 == 取数统计的文档集」。
|
|
751
|
+
const countDocs = (sub) => {
|
|
752
|
+
const dir = path.join(ROOT, 'workflow', sub);
|
|
753
|
+
return (readdirOrNull(dir) || []).filter((f) => {
|
|
754
|
+
// 与 docFiles() 同口径:.md + 以数字开头(排除 _TEMPLATE.md 等非实例件)+ tracked
|
|
755
|
+
if (!f.endsWith('.md') || !/^\d/.test(f)) return false;
|
|
756
|
+
return isTracked(path.relative(ROOT, path.join(dir, f)).split(path.sep).join('/'));
|
|
757
|
+
}).length;
|
|
758
|
+
};
|
|
759
|
+
const ledgerLines = () => {
|
|
760
|
+
const p = path.join(ROOT, '.agents', 'confirmations.jsonl');
|
|
761
|
+
const ls = (linesOf(p) || []).filter((l) => l.trim());
|
|
762
|
+
return { total: ls.length, withBatch: ls.filter((l) => /"batch"/.test(l)).length };
|
|
763
|
+
};
|
|
764
|
+
// 内置取数器:全部本地确定性、零网络。**装户零配置即用**(不依赖任何装户文件)。
|
|
765
|
+
const derivers = {
|
|
766
|
+
'ledger.lines': () => ledgerLines().total,
|
|
767
|
+
'ledger.linesWithBatch': () => ledgerLines().withBatch,
|
|
768
|
+
'ledger.linesWithoutBatch': () => ledgerLines().total - ledgerLines().withBatch,
|
|
769
|
+
'docs.count.intents': () => countDocs('intents'),
|
|
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;
|
|
809
|
+
}
|
|
810
|
+
try {
|
|
811
|
+
const req = createRequire(import.meta.url);
|
|
812
|
+
const mod = req(abs);
|
|
813
|
+
// CJS / ESM 双兼容取导出:CJS 走 module.exports.derivers;若装户误用了 ESM 语法而环境恰好
|
|
814
|
+
// 支持(新 Node 的 require(esm)),default 里也可能挂 derivers——两处都认,避免假阴性。
|
|
815
|
+
const d = (mod && mod.derivers) || (mod && mod.default && mod.default.derivers);
|
|
816
|
+
if (!d || typeof d !== 'object' || Array.isArray(d)) {
|
|
817
|
+
adopterState = { ok: false, errors: [`${adopterModRel} 未导出 \`derivers\` 对象(实际导出形态:${d === undefined ? '无 derivers 键' : Array.isArray(d) ? 'array' : typeof d})——载入失败不降级,请用 \`module.exports = { derivers: { ... } }\``] };
|
|
818
|
+
return adopterState;
|
|
819
|
+
}
|
|
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
|
+
}
|
|
825
|
+
return adopterState;
|
|
826
|
+
};
|
|
827
|
+
// ctx:注入给装户取数器的同步辅助(免其重复造轮子)
|
|
828
|
+
// **glob 逐字符转义**(自查修正):首版用「先整体转义特殊字符、再 replace 星号」的写法,
|
|
829
|
+
// 实测 `src/**/*.ts` 匹配不到 `src/a/b/c.ts`、「**/*.md」匹配不到 `a/b/c.md`——因为 `**/`
|
|
830
|
+
// 在被替换前已被字符类转义破坏。改为逐字符状态机,语义明确:
|
|
831
|
+
// `**/` → 任意层级(含零层);`**` → 任意(可跨 /);`*` → 段内任意(不跨 /);`?` → 段内单字符
|
|
832
|
+
// 其余一律字面转义(故 `a.b.sql` 的点是字面点、`[`/`{` 也按字面处理——本 helper 不承诺
|
|
833
|
+
// 字符类 / 花括号展开,避免装户误以为支持完整 glob 语法;不支持即按字面匹配,行为可预测)
|
|
834
|
+
const globToRegExp = (p) => {
|
|
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(/[.+^${}()|[\]\\]/, '\\$&');
|
|
845
|
+
}
|
|
846
|
+
return new RegExp('^' + re + '$');
|
|
847
|
+
};
|
|
848
|
+
// 遍历时**跳过重目录 + 防环**(自查 + 复核 P2 修正):
|
|
849
|
+
// · 剪枝 node_modules / .git / cache——无剪枝时 `ctx.glob('**/*')` 会走进依赖树,既慢又会把
|
|
850
|
+
// 依赖文件算进取数(对「我项目有多少个 X」是错的数)。
|
|
851
|
+
// · **visited 集合防环**:复核实测软链接/junction 成环时递归到 depth 128、同一文件重复 64 份
|
|
852
|
+
// 才被 statSync 的 ELOOP 拦下;POSIX 无 MAX_PATH 时该递归无界。故按**真实路径**去重。
|
|
853
|
+
const WALK_SKIP = new Set(['node_modules', '.git', 'cache']);
|
|
854
|
+
const walkFiles = (dir, out = [], rel = '', visited = new Set()) => {
|
|
855
|
+
let real = dir;
|
|
856
|
+
try { real = fs.realpathSync(dir); } catch { /* 不可解析则按原路径处理 */ }
|
|
857
|
+
if (visited.has(real)) return out; // 环 → 剪枝
|
|
858
|
+
visited.add(real);
|
|
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);
|
|
867
|
+
}
|
|
868
|
+
return out;
|
|
869
|
+
};
|
|
870
|
+
const ctx = {
|
|
871
|
+
root: ROOT,
|
|
872
|
+
read: (rel) => { try { return fs.readFileSync(path.join(ROOT, rel), 'utf8').replace(/\r\n/g, '\n'); } catch { return ''; } },
|
|
873
|
+
glob: (pattern) => { const re = globToRegExp(pattern); return walkFiles(ROOT).filter((r) => re.test(r)); },
|
|
874
|
+
countFiles: (dir, pred) => walkFiles(path.join(ROOT, dir)).filter((r) => (typeof pred === 'function' ? pred(r) : true)).length,
|
|
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} 亦未定义(登记了却没实现=其实没查)` };
|
|
885
|
+
}
|
|
886
|
+
return { fn, builtin: false };
|
|
887
|
+
};
|
|
888
|
+
// 装户模块载入失败:**独立成条**(2026-09-28 adopter-derivers)。
|
|
889
|
+
// 关键设计(真绿 vs 假绿的分界):载入失败若只表现为「该指标无取数器」,装户会看到
|
|
890
|
+
// 「引用了 my.metric → 未登记」,看起来像**自己忘了登记**,而真因是模块坏了——他会去 registry
|
|
891
|
+
// 反复核对,永远查不到。故此处单独出一条,明示文件与错误,与「真的没登记」可区分。
|
|
892
|
+
//
|
|
893
|
+
// **仅在真的需要装户指标时才报**(复核 P2 更正):首版在解析登记表前无条件出账——若登记表只有
|
|
894
|
+
// 内置指标、而装户留了个半成品模块(或模块坏掉但没人用),会平白多一条不可消除的 advisory。
|
|
895
|
+
// 沿本仓红线「无判定依据的行不产出噪声」:先看登记表是否真的有**非内置**指标,再决定报不报。
|
|
896
|
+
const mcLines = (linesOf(mcFile) || []).map((l) => l.trim()).filter((l) => l && !l.startsWith('#'));
|
|
897
|
+
const hasAdopterMetric = mcLines.some((l) => {
|
|
898
|
+
const mm = /^([A-Za-z][\w.]*)\s*=\s*([A-Za-z][\w.]*)$/.exec(l);
|
|
899
|
+
return mm && !derivers[mm[1]];
|
|
900
|
+
});
|
|
901
|
+
const adopterLoad = loadAdopterDerivers();
|
|
902
|
+
if (!adopterLoad.ok && hasAdopterMetric) {
|
|
903
|
+
for (const e of adopterLoad.errors) warnings.push(`- [WARN 装户取数器载入失败] ${e}`);
|
|
904
|
+
}
|
|
905
|
+
// 解析登记表:`<指标名> = <取数表达式>`(表达式须与指标名同形,二者不一致即登记笔误)
|
|
906
|
+
const declared = new Map();
|
|
907
|
+
const precedenceWarned = new Set(); // 同名 advisory 按指标去重(复核 P2 更正:首版按行重复出账)
|
|
908
|
+
for (const line of mcLines) {
|
|
909
|
+
const m = /^([A-Za-z][\w.]*)\s*=\s*([A-Za-z][\w.]*)$/.exec(line);
|
|
910
|
+
if (!m) { warnings.push(`- [WARN 指标登记] .agents/metric-claims.txt 行格式非法(应为 \`指标名 = 取数表达式\`):${line}`); continue; }
|
|
911
|
+
if (m[1] !== m[2]) { warnings.push(`- [WARN 指标登记] 登记名与取数表达式不一致:${m[1]} ≠ ${m[2]}`); continue; }
|
|
912
|
+
const r = resolveDeriver(m[1]);
|
|
913
|
+
if (r.error) {
|
|
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} 中的同名定义**被忽略**(内置优先)——如需改用自有实现,请换一个指标名`);
|
|
926
|
+
}
|
|
927
|
+
declared.set(m[1], true);
|
|
928
|
+
}
|
|
929
|
+
// 扫活跃态文档(沿检查 4 现有尺度:终态件的历史数字是历史叙述,不扫)
|
|
930
|
+
const activeSet16 = (sub) => (sub === 'incidents' ? ENUMS['incident.status.active'] : ENUMS['doc.status.active']);
|
|
931
|
+
const scanFiles16 = [];
|
|
932
|
+
for (const sub of ['intents', 'specs', 'plans', 'incidents']) {
|
|
933
|
+
const dir = path.join(ROOT, 'workflow', sub);
|
|
934
|
+
for (const f of readdirOrNull(dir) || []) {
|
|
935
|
+
if (!f.endsWith('.md') || f === '_TEMPLATE.md') continue;
|
|
936
|
+
if (!inSet(fmGet(path.join(dir, f), '状态'), activeSet16(sub))) continue;
|
|
937
|
+
scanFiles16.push({ rel: `workflow/${sub}/${f}`, abs: path.join(dir, f) });
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
for (const f of readdirOrNull(path.join(ROOT, 'workflow')) || []) {
|
|
941
|
+
if (f.endsWith('.md')) scanFiles16.push({ rel: `workflow/${f}`, abs: path.join(ROOT, 'workflow', f) });
|
|
942
|
+
}
|
|
943
|
+
for (const { rel, abs } of scanFiles16) {
|
|
944
|
+
const ls = linesOf(abs) || [];
|
|
945
|
+
ls.forEach((line, i) => {
|
|
946
|
+
// 签名形态收窄为**小写点分**(如 `{{ledger.lines}}`):装户模板占位符是全大写 SCREAMING_CASE
|
|
947
|
+
// (构建命令 / 看板端口 / 项目名三类,由 init 渲染替换,非本检查对象)
|
|
948
|
+
// ——不收窄会对这些合法的模板占位符产生大量假阳性(实测:本仓 plans 内 11 处)。
|
|
949
|
+
// **转义(2026-09-28,本检查在自己的文档上抓到该需求)**:讲语法 / 举例 / 引用告警原文的
|
|
950
|
+
// 文档在签名前加反斜杠(`\{{ledger.lines}}`)表示「此处是示意、非断言」——否则本检查
|
|
951
|
+
// 会把「文档在教怎么用签名」误判为「作者忘了回填」。选显式转义而非「同句含『如/例』等标记词
|
|
952
|
+
// 即豁免」的启发式:后者会按措辞松紧漂移,且作者能无意中触发豁免而漏过真断言。
|
|
953
|
+
// **转义按「逐个出现」生效、不按整行**(复核自查 P1 更正):首版实现是「行内出现任一 `\{{`
|
|
954
|
+
// 即整行跳过」,实测可被这样藏住真断言——`… \{{a.b}} 示意… {{ledger.lines}} 未回填` 整行静默
|
|
955
|
+
// 放过。故改为用带后顾的否匹配排除转义位,同行的真签名照常出账。
|
|
956
|
+
for (const m of line.matchAll(/(?<!\\)\{\{([a-z][a-z0-9]*(?:\.[a-z0-9]+)+)\}\}/g)) {
|
|
957
|
+
const name = m[1];
|
|
958
|
+
if (!declared.has(name)) {
|
|
959
|
+
warnings.push(`- [WARN 指标未登记] ${rel}:${i + 1} 引用 {{${name}}} 但 .agents/metric-claims.txt 未登记该指标(未登记=无从对账)`);
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
const r = resolveDeriver(name);
|
|
963
|
+
if (r.error) {
|
|
964
|
+
// 取数器不可用(含装户模块载入失败 / 无该名 / 定义非法)——**响亮出账,绝不静默跳过**
|
|
965
|
+
warnings.push(`- [WARN 指标取数失败] ${rel}:${i + 1} {{${name}}} 无法取数:${r.error}`);
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
let real;
|
|
969
|
+
try {
|
|
970
|
+
real = r.fn(ctx);
|
|
971
|
+
} catch (e) {
|
|
972
|
+
warnings.push(`- [WARN 指标取数失败] ${rel}:${i + 1} {{${name}}} 取数器抛异常:${String(e && e.message ? e.message : e).split('\n')[0]}`);
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
// 返回值必须是有限数字:Promise(误写 async)/ NaN / 字符串 / undefined 一律拦下并明示类型
|
|
976
|
+
if (typeof real !== 'number' || !Number.isFinite(real)) {
|
|
977
|
+
const kind = real && typeof real.then === 'function' ? 'Promise(本检查要求**同步**函数,请去掉 async/await)' : `${typeof real}${typeof real === 'number' ? `(${real})` : ''}`;
|
|
978
|
+
warnings.push(`- [WARN 指标取数失败] ${rel}:${i + 1} {{${name}}} 取数器返回值不是有限数字:${kind}`);
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
// 判据:签名**必须**被替换为实时值——留有 `{{...}}` 即视为未回填(写作期占位,关单前须落实)
|
|
982
|
+
warnings.push(`- [WARN 指标待回填] ${rel}:${i + 1} {{${name}}} 实时值 = ${real}——请把签名替换为该数字(留签名=未回填)`);
|
|
983
|
+
}
|
|
984
|
+
// **形态不符的签名也不静默放过(复核 P2 更正)**:首版只认严格小写点分形态,于是
|
|
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
|
+
});
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
724
1000
|
// --- 输出(banner 沿 sh 版字样与格式 = 稳定输出契约:banner 后空行、条目 '- ' 前缀)---
|
|
725
1001
|
if (blockers.length) {
|
|
726
1002
|
process.stderr.write(`闭环骨架断档(check-loop.sh)— HARD-BLOCK:\n\n${blockers.join('\n')}\n\n`);
|