agentic-flow-kit 0.6.0 → 0.7.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/package.json +1 -1
- package/src/profiles.mjs +19 -2
- package/templates/_agents/metric-claims.txt +81 -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 +7 -2
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-flow-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "AI-Native 闭环工作流 + wiki 知识层脚手架——flow-kit init 一条命令为新项目装好 .agents 工作流引擎(阶段命令/角色契约/门禁钩子/kb 检索/看板/规则面预算)+ workflow 文档闭环 + wiki 知识库,装完自包含、零运行时依赖",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
package/src/profiles.mjs
CHANGED
|
@@ -6,10 +6,14 @@
|
|
|
6
6
|
// 配置的项目自动跳过,不 fail-closed 误拦。none = 不配置(项目日后自填)。
|
|
7
7
|
export const HOSTS = {
|
|
8
8
|
zcode: { dir: '.zcode', localOnly: true },
|
|
9
|
-
opencode: { dir: '.opencode', localOnly: false },
|
|
10
|
-
trae: { dir: '.trae', localOnly: false },
|
|
9
|
+
opencode: { dir: '.opencode', localOnly: false, commandPrefix: 'wf-' },
|
|
10
|
+
trae: { dir: '.trae', localOnly: false, commandPrefix: 'wf-' },
|
|
11
11
|
omp: { dir: '.omp', localOnly: true },
|
|
12
12
|
};
|
|
13
|
+
// commandPrefix = 该宿主的命令层薄适配文件名(= 宿主命令名)统一前缀,2026-09-28 opencode-cmd-wf-prefix
|
|
14
|
+
// 与 trae 对齐后单源:命令名不再裸占宿主顶层命名空间(撞宿主内置/第三方命令无从规避);缺该字段的宿主
|
|
15
|
+
// 表示只有角色层适配、不参与 commands 权威源映射(zcode/omp)。sync-hosts(包源侧)与 doctor §7.x
|
|
16
|
+
// (装户侧)两份实现均从本表派生,新增命令层宿主只改这一处。
|
|
13
17
|
|
|
14
18
|
const ESLINT_CONFIGS = [
|
|
15
19
|
'eslint.config.js', 'eslint.config.mjs', 'eslint.config.cjs',
|
|
@@ -78,10 +82,23 @@ export function pickStackVars(stackKey) {
|
|
|
78
82
|
|
|
79
83
|
// 两态文件模型的归属判定:managed(随包升级)之外皆 owned(项目内容,sync 永不覆盖)
|
|
80
84
|
// local-pre-commit 是项目门禁挂载点——一旦接线即含项目内容,按 owned 起步(旧台账里若在 managed,改动后 sync 也会保守跳过)
|
|
85
|
+
// metric-claims.txt 是**项目自持的指标登记表**(2026-09-28 metric-claim-gate 复核 P2 更正):装户按
|
|
86
|
+
// 设计要在其中新增自有指标,若按 managed 记账则 sync 每次都报「本地已改、升级时需合并」,且将来包侧
|
|
87
|
+
// 新增内置指标**永远下不来**(台账停在包侧基线)。故归 owned——随包附带一份起步内容,此后归项目。
|
|
88
|
+
// metric-derivers.cjs 是**项目自持的取数器模块**(2026-09-28 adopter-derivers):它正是上面那份登记表的
|
|
89
|
+
// 扩展点——装户在此实现自有指标的取数逻辑。**必须与 metric-claims.txt 同归 owned**:若落 managed,
|
|
90
|
+
// 装户一用该功能就会让 sync 永久报「本地已改」、doctor WARN,并因供应链防线使 check-loop 跳过执行
|
|
91
|
+
// (门禁静默停摆)——那正是本单要修的缺陷本身(同名 incident 有实测链路)。
|
|
92
|
+
// **后缀为 .cjs 而非 .mjs**(2026-09-28 跨版本实测修正):首版用 .mjs + require(esm),实测在
|
|
93
|
+
// Node 18.20 / 20.18 / 22.11 全部 ERR_REQUIRE_ESM(只有 ≥20.19/≥22.12 可用),而本仓 engines
|
|
94
|
+
// 写 >=18.0.0、CI 跑 18/22 → 装户在 CI 目标版本上根本用不了。改 .cjs 后 createRequire 同步载入
|
|
95
|
+
// 全线可用。旧 .mjs 路径同样登记为 owned(免装户残留旧文件时落 managed 重演缺陷)。
|
|
81
96
|
export function isOwned(rel) {
|
|
82
97
|
return rel === 'AGENTS.md' || rel === '.gitattributes' || rel.startsWith('workflow/') || rel.startsWith('wiki/')
|
|
83
98
|
|| rel.startsWith('.agents/notes/')
|
|
84
99
|
|| rel === '.agents/workflow-modules.txt' || rel === '.agents/rule-budgets.txt'
|
|
100
|
+
|| rel === '.agents/metric-claims.txt' || rel === '.agents/metric-derivers.cjs'
|
|
101
|
+
|| rel === '.agents/metric-derivers.mjs'
|
|
85
102
|
|| rel === '.agents/hooks/local-pre-commit';
|
|
86
103
|
}
|
|
87
104
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# 量化断言「指标签名」单源登记表(check-loop 检查 16 共读)
|
|
2
|
+
#
|
|
3
|
+
# 背景(2026-09-28 claim-exceeds-fix):结论文档里跨文档引用的量化值若各自手写,
|
|
4
|
+
# 极易在事实变更后不同步——本仓已实证:同一指标在一处改了、另一处没改,
|
|
5
|
+
# 而「五处文件同步」这句话本身无人核对(该单真正被门禁抓住的方式,是事后人工复核)。
|
|
6
|
+
# 本表把「哪个指标、当前取值多少」收敛为单源,供 check-loop 检查 16 对账。
|
|
7
|
+
#
|
|
8
|
+
# 用法:活跃态文档(draft/approved/open)引用某个已登记指标时,按**签名形态**书写,
|
|
9
|
+
# 检查 16 会实时取数比对。签名形态:\{\{指标名\}\}(双花括号包裹指标名,小写点分)。
|
|
10
|
+
# 例(下面是「讲语法」,故加反斜杠转义;**写真断言时不加反斜杠**):
|
|
11
|
+
# 本仓台账共 \{\{ledger.lines\}\} 行,其中无 batch 字段 \{\{ledger.linesWithoutBatch\}\} 行。
|
|
12
|
+
# 写作时**不要手写数字**——写完跑 check-loop,工具会把实时值填进提示里供你替换。
|
|
13
|
+
# 转义按「逐个出现」生效:同一行里 \{\{x\}\} 是示意、\{\{y\}\} 是真断言时,只豁免前者。
|
|
14
|
+
# (本例的指标名曾写成 ledger.total / ledger.noBatch 两个**并不存在**的名字——本表不被
|
|
15
|
+
# 检查 16 扫描(它是 .txt 非 .md),故这类笔误不会被自动抓到,只能靠复核。已据实更正。)
|
|
16
|
+
#
|
|
17
|
+
# 为什么用显式签名而非全文数字扫描(据实说明,非偷懒):
|
|
18
|
+
# 本仓活跃文档中「N 行 / N 条 / N 份」类表述有 49 处(实测:活跃态 21 份文档逐行计),绝大多数是**历史叙述**
|
|
19
|
+
# (描述某次提交当时的规模,天然不会随当前事实变化)。全文扫描会对这些正确的
|
|
20
|
+
# 历史叙述产生大量假阳性——而 check-loop.mjs 已把「无判定依据的行不产出不可
|
|
21
|
+
# 消除噪声」立为红线(沿 2026-09-27 audit-gate-hardening P3 教训:噪声淹没真漏点)。
|
|
22
|
+
# 故本检查**只校验显式声明的签名**:假阳性恒为 0,代价是覆盖面依赖作者登记。
|
|
23
|
+
#
|
|
24
|
+
# 格式:每行 `<指标名> = <取数表达式>`;`#` 开头为注释;空行忽略。
|
|
25
|
+
# 取数表达式(闭集,全部本地确定性、零网络):
|
|
26
|
+
# ledger.lines 台账 .agents/confirmations.jsonl 的非空行数
|
|
27
|
+
# ledger.linesWithoutBatch 台账中不含 "batch" 字段的行数(schema 演进前历史行)
|
|
28
|
+
# ledger.linesWithBatch 台账中含 "batch" 字段的行数
|
|
29
|
+
# docs.count.<sub> workflow/<sub> 下文档份数——**与文档扫描同口径**:以数字开头的
|
|
30
|
+
# .md、排除 _TEMPLATE.md、且仓库模式下只计 HEAD 已跟踪件
|
|
31
|
+
# (故并行会话的未跟踪新档不计入);sub ∈ intents/specs/plans/incidents
|
|
32
|
+
# docs.count.all 上述四类合计
|
|
33
|
+
# ── 新增指标(2026-09-28 adopter-derivers 起)────────────────────────────────
|
|
34
|
+
# 分两层取数,**内置优先**:
|
|
35
|
+
# ① 引擎内置 8 个指标(下表)——装户零配置即用,不需要做任何事。
|
|
36
|
+
# ② 项目自有指标——写 `.agents/metric-derivers.cjs`(**owned**,sync 永不覆盖),
|
|
37
|
+
# 用 CommonJS 导出 `derivers` 对象(指标名 → 取数函数),并在此表加同名一行。
|
|
38
|
+
# **后缀必须是 `.cjs`、写法必须是 CommonJS**(`module.exports`)。
|
|
39
|
+
# 为什么不用 `.mjs`:引擎用 `require()` 同步载入,而 `require(esm)` 仅在
|
|
40
|
+
# Node ≥20.19 / ≥22.12 可用——**实测 Node 18.20 / 20.18 / 22.11 全部
|
|
41
|
+
# ERR_REQUIRE_ESM**,而本仓 engines 声明 `>=18.0.0`。故 `.cjs` 是唯一
|
|
42
|
+
# 全版本可用的形态(同步、零版本依赖)。
|
|
43
|
+
# 旧的 `.mjs` 路径已弃用:若残留在盘上,检查 16 会**明确提示迁移**。
|
|
44
|
+
#
|
|
45
|
+
# 示例 `.agents/metric-derivers.cjs`:
|
|
46
|
+
# module.exports = {
|
|
47
|
+
# derivers: {
|
|
48
|
+
# 'my.migrations': (ctx) => ctx.glob('backend/migrations/*.sql').length,
|
|
49
|
+
# 'my.apiCount': (ctx) => ctx.countFiles('src/api', (f) => f.endsWith('.controller.ts')),
|
|
50
|
+
# },
|
|
51
|
+
# };
|
|
52
|
+
# ctx 提供(同步,免你重复造轮子;其余请自行 `require('node:fs')`):
|
|
53
|
+
# ctx.root 仓库根绝对路径
|
|
54
|
+
# ctx.read(rel) 读文本文件(LF 归一),不存在返回 ''
|
|
55
|
+
# ctx.glob(pattern) 极简 glob(* / **),返回相对路径数组
|
|
56
|
+
# ctx.countFiles(dir, pred) 递归计数(可传谓词)
|
|
57
|
+
#
|
|
58
|
+
# 三条硬约定(违反即 fail-loud):
|
|
59
|
+
# 1. **必须是同步函数**(返回 number,不得 async/Promise)——本检查零网络、要求本地确定性
|
|
60
|
+
# 2. 必须**确定性**:不得依赖网络、不得依赖当前时间
|
|
61
|
+
# 3. 返回值须为**有限数字**(Number.isFinite)
|
|
62
|
+
#
|
|
63
|
+
# 失败语义(**绝不静默降级**):模块语法错 / 抛异常 / 未导出 derivers / 返回值非法
|
|
64
|
+
# → 一律出 `[WARN 装户取数器载入失败]` 或 `[WARN 指标取数失败]`,**不会**退化成
|
|
65
|
+
# 「只用内置指标」——否则你会误以为「忘了登记」而查不到真因。
|
|
66
|
+
# 指标名与内置同名时:**内置优先**,你的定义被忽略(会出 advisory 明示)。
|
|
67
|
+
#
|
|
68
|
+
# 注:本检查**不设取数超时**——请让取数器保持轻量(秒级)。死循环会拖慢门禁。
|
|
69
|
+
# 另:装户模块的「取数器」在引擎侧(check-loop.mjs,managed);本表与 derivers.cjs 同归 owned。
|
|
70
|
+
#
|
|
71
|
+
# 注意:不登记 advisory 总数类指标——它由 check-loop 自身产出(检查 16 是其一部分),
|
|
72
|
+
# 自引用会让「报告本身改变被报告的数」,产生不可稳定对账的循环。
|
|
73
|
+
|
|
74
|
+
ledger.lines = ledger.lines
|
|
75
|
+
ledger.linesWithoutBatch = ledger.linesWithoutBatch
|
|
76
|
+
ledger.linesWithBatch = ledger.linesWithBatch
|
|
77
|
+
docs.count.intents = docs.count.intents
|
|
78
|
+
docs.count.specs = docs.count.specs
|
|
79
|
+
docs.count.plans = docs.count.plans
|
|
80
|
+
docs.count.incidents = docs.count.incidents
|
|
81
|
+
docs.count.all = docs.count.all
|
|
@@ -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`);
|
|
@@ -1061,7 +1061,9 @@ const mkConfirmedDoc = (root, rel, fmBody) => {
|
|
|
1061
1061
|
// ---- 场景 72-74:检查 8 生效日锚改 git 首次加入日期(2026-09-28 check8-git-anchor)----
|
|
1062
1062
|
// 换锚前锚取**文件名前 10 字符**——项目命名规范本身即 YYYY-MM-DD-<主题>,作者日常手写该前缀,
|
|
1063
1063
|
// 「写早」无需任何额外动作、顺手即发生 → 一条 hard 门可被平凡绕过(有未勾验项却 exit 0)。
|
|
1064
|
-
// 换锚后锚取 git
|
|
1064
|
+
// 换锚后锚取 git 首次加入日期(**日常不可顺手改动**的事实)→ 关掉「改文件名」零成本通道;
|
|
1065
|
+
// **诚实边界(复核 P1 更正)**:该锚取 `%aI` = author date(作者自报时间戳),`git commit --date=<过去>`
|
|
1066
|
+
// 仍可伪造 → **不宣称「通道关闭」**,仅把伪造成本从「改文件名」抬到「主动伪造时间戳」。
|
|
1065
1067
|
{
|
|
1066
1068
|
{
|
|
1067
1069
|
// 【核心回归】文件名日期写早(2026-09-01 < accCutoff 2026-09-12),但**实际是新建档**
|
|
@@ -1098,5 +1100,498 @@ const mkConfirmedDoc = (root, rel, fmBody) => {
|
|
|
1098
1100
|
}
|
|
1099
1101
|
}
|
|
1100
1102
|
|
|
1103
|
+
// ---- 场景 75-79:检查 16 量化断言指标签名对账(2026-09-28 claim-exceeds-fix)----
|
|
1104
|
+
// 设计要点:本检查**只查显式签名**(`{{小写.点分}}`),不全文扫数字——理由是本仓「N 行/N 条/N 份」
|
|
1105
|
+
// 类表述多为历史叙述(描述某次提交当时的规模),全文扫描会产生不可消退的假阳性(P3 红线)。
|
|
1106
|
+
// 故本组用例的核心断言是**假阳性恒 0**:无签名时零告警;装户模板占位符(全大写)不误报。
|
|
1107
|
+
{
|
|
1108
|
+
const MC = 'ledger.lines = ledger.lines\ndocs.count.plans = docs.count.plans\n';
|
|
1109
|
+
const seedLedger = (T, lines) => w(T, '.agents/confirmations.jsonl', lines.join('\n') + '\n');
|
|
1110
|
+
{
|
|
1111
|
+
// 【核心回归】活跃文档留签名未回填 → warning 且提示实时值
|
|
1112
|
+
const T = mkfix();
|
|
1113
|
+
w(T, '.agents/metric-claims.txt', MC);
|
|
1114
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}', '{"doc":"b","stage":"done"}', '{"doc":"c","stage":"done"}']);
|
|
1115
|
+
w(T, 'workflow/plans/p1.md', PLAN('p1', '状态: draft\n级别: L1', '\n台账共 {{ledger.lines}} 行\n'));
|
|
1116
|
+
const r = run(T);
|
|
1117
|
+
const o = outOf(r);
|
|
1118
|
+
check('检查16:活跃文档留签名未回填 → WARN 且给出实时值 3(advisory 不阻断)',
|
|
1119
|
+
r.status === 0 && o.includes('指标待回填') && o.includes('实时值 = 3'), `exit=${r.status}\n${o}`);
|
|
1120
|
+
rmfix(T);
|
|
1121
|
+
}
|
|
1122
|
+
{
|
|
1123
|
+
// 【假阳性恒 0 · 核心】无签名 → 零告警(历史叙述里的裸数字不得被扫)
|
|
1124
|
+
const T = mkfix();
|
|
1125
|
+
w(T, '.agents/metric-claims.txt', MC);
|
|
1126
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1127
|
+
w(T, 'workflow/plans/p2.md', PLAN('p2', '状态: draft\n级别: L1', '\n台账共 47 行,其中 13 条并录(历史叙述,非签名)\n'));
|
|
1128
|
+
const r = run(T);
|
|
1129
|
+
check('检查16:无签名 → 零告警(裸数字是历史叙述,不得产生假阳性)',
|
|
1130
|
+
r.status === 0 && !outOf(r).includes('指标'), `exit=${r.status}\n${outOf(r)}`);
|
|
1131
|
+
rmfix(T);
|
|
1132
|
+
}
|
|
1133
|
+
{
|
|
1134
|
+
// 【假阳性恒 0 · 关键边界】装户模板占位符(全大写 SCREAMING_CASE)不误报
|
|
1135
|
+
// ——不收窄形态会对本仓 plans 内 11 处合法模板占位符全部误报(实测量级)。
|
|
1136
|
+
// 注:占位符**运行时拼装**而非字面写入——本仓的占位符残留门禁(doctor §5)会把字面
|
|
1137
|
+
// 全大写 `{{X}}` 判为残留并改写包源副本,导致本用例在包源侧退化为测「<填写> 不误报」
|
|
1138
|
+
// (仍绿但不再证明 SCREAMING_CASE 被排除)。拼装后两侧语义一致、断言强度不退。
|
|
1139
|
+
const SCREAMING = (n) => '{{' + n + '}}';
|
|
1140
|
+
const T = mkfix();
|
|
1141
|
+
w(T, '.agents/metric-claims.txt', MC);
|
|
1142
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1143
|
+
w(T, 'workflow/plans/p3.md', PLAN('p3', '状态: draft\n级别: L1',
|
|
1144
|
+
'\n构建 = ' + SCREAMING('BUILD_CMD') + ';端口 ' + SCREAMING('BOARD_PORT') + ';项目 ' + SCREAMING('PROJECT_NAME') + '\n'));
|
|
1145
|
+
const r = run(T);
|
|
1146
|
+
check('检查16:装户模板占位符(全大写 SCREAMING_CASE)→ 不误报(形态收窄)',
|
|
1147
|
+
r.status === 0 && !outOf(r).includes('指标'), `exit=${r.status}\n${outOf(r)}`);
|
|
1148
|
+
rmfix(T);
|
|
1149
|
+
}
|
|
1150
|
+
{
|
|
1151
|
+
// 【fail-loud】引用未登记指标 → 明示(未登记=无从对账,不静默放过)
|
|
1152
|
+
const T = mkfix();
|
|
1153
|
+
w(T, '.agents/metric-claims.txt', MC);
|
|
1154
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1155
|
+
w(T, 'workflow/plans/p4.md', PLAN('p4', '状态: draft\n级别: L1', '\n引用 {{nope.metric}}\n'));
|
|
1156
|
+
const r = run(T);
|
|
1157
|
+
check('检查16:引用未登记指标 → fail-loud 出账(不静默跳过)',
|
|
1158
|
+
outOf(r).includes('指标未登记'), `exit=${r.status}\n${outOf(r)}`);
|
|
1159
|
+
rmfix(T);
|
|
1160
|
+
}
|
|
1161
|
+
{
|
|
1162
|
+
// 【转义】讲语法 / 举例的行加反斜杠 → 不报(否则「文档教怎么用签名」被误判为「忘了回填」)
|
|
1163
|
+
// 该需求由本检查在自己的 spec 上抓到(实现后立刻 5 处误报)。
|
|
1164
|
+
const T = mkfix();
|
|
1165
|
+
w(T, '.agents/metric-claims.txt', MC);
|
|
1166
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1167
|
+
w(T, 'workflow/plans/p8.md', PLAN('p8', '状态: draft\n级别: L1',
|
|
1168
|
+
'\n写法示例如 `\\{{ledger.lines}}`(本行是讲语法,非断言)\n'));
|
|
1169
|
+
const r = run(T);
|
|
1170
|
+
check('检查16:转义 \\{{...}} 的行为示意 → 不报(区分「讲语法」与「真断言」)',
|
|
1171
|
+
r.status === 0 && !outOf(r).includes('指标'), `exit=${r.status}\n${outOf(r)}`);
|
|
1172
|
+
rmfix(T);
|
|
1173
|
+
}
|
|
1174
|
+
{
|
|
1175
|
+
// 【守卫】check-loop 自身可加载(防语法错致整条门禁静默失效)
|
|
1176
|
+
// 实证教训:本次实现期把 `continue` 误用在 forEach 回调内 → 模块 SyntaxError →
|
|
1177
|
+
// check-loop 崩溃退出,而「指标告警 0 条」看起来像通过。**假绿比红更危险**,故钉死。
|
|
1178
|
+
const r = run(mkfix());
|
|
1179
|
+
check('检查16:check-loop 可正常加载运行(防语法错致门禁静默失效——假绿比红危险)',
|
|
1180
|
+
r.status === 0 && !/SyntaxError|Illegal continue/.test(outOf(r)), `exit=${r.status}\n${outOf(r)}`);
|
|
1181
|
+
}
|
|
1182
|
+
{
|
|
1183
|
+
// 【fail-loud】登记了指标但无取数器 → 明示「登记了却没查」(防假阴性:登记表写了却不实现)
|
|
1184
|
+
const T = mkfix();
|
|
1185
|
+
w(T, '.agents/metric-claims.txt', MC + 'ghost.metric = ghost.metric\n');
|
|
1186
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1187
|
+
w(T, 'workflow/plans/p5.md', PLAN('p5', '状态: draft\n级别: L1'));
|
|
1188
|
+
const r = run(T);
|
|
1189
|
+
check('检查16:登记无取数器 → fail-loud(登记了却没实现=其实没查)',
|
|
1190
|
+
outOf(r).includes('无对应取数器'), `exit=${r.status}\n${outOf(r)}`);
|
|
1191
|
+
rmfix(T);
|
|
1192
|
+
}
|
|
1193
|
+
{
|
|
1194
|
+
// 【退化语义】登记表不存在 → 静默跳过(未启用该检查的装户不应被噪声打扰)
|
|
1195
|
+
const T = mkfix();
|
|
1196
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1197
|
+
w(T, 'workflow/plans/p6.md', PLAN('p6', '状态: draft\n级别: L1', '\n台账共 {{ledger.lines}} 行\n'));
|
|
1198
|
+
const r = run(T);
|
|
1199
|
+
check('检查16:登记表缺失 → 静默跳过(不误报,未启用装户零噪声)',
|
|
1200
|
+
r.status === 0 && !outOf(r).includes('指标'), `exit=${r.status}\n${outOf(r)}`);
|
|
1201
|
+
rmfix(T);
|
|
1202
|
+
}
|
|
1203
|
+
{
|
|
1204
|
+
// 【活跃态尺度】终态文档不扫(沿检查 4 现有尺度:历史数字是历史叙述)
|
|
1205
|
+
const T = mkfix();
|
|
1206
|
+
w(T, '.agents/metric-claims.txt', MC);
|
|
1207
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1208
|
+
w(T, 'workflow/plans/p7.md', PLAN('p7', '状态: done\n级别: L1', '\n台账共 {{ledger.lines}} 行\n'));
|
|
1209
|
+
const r = run(T);
|
|
1210
|
+
check('检查16:终态文档不扫(历史叙述豁免,沿检查 4 尺度)',
|
|
1211
|
+
r.status === 0 && !outOf(r).includes('指标待回填'), `exit=${r.status}\n${outOf(r)}`);
|
|
1212
|
+
rmfix(T);
|
|
1213
|
+
}
|
|
1214
|
+
{
|
|
1215
|
+
// 【取数正确性】docs.count.plans 应计活跃+终态全部 plans(排除 _TEMPLATE)
|
|
1216
|
+
// 注:取数器与 docFiles() 同口径——须**以数字开头**(命名规范强制 `YYYY-MM-DD-` 前缀),
|
|
1217
|
+
// 故 fixture 文件名必须合规(首版用 a.md/b.md 使取数为 0,暴露的是用例不合规而非判据错)。
|
|
1218
|
+
const T = mkfix();
|
|
1219
|
+
w(T, '.agents/metric-claims.txt', 'docs.count.plans = docs.count.plans\n');
|
|
1220
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1221
|
+
w(T, 'workflow/plans/2026-09-01-a.md', PLAN('a', '状态: draft\n级别: L1'));
|
|
1222
|
+
w(T, 'workflow/plans/2026-09-02-b.md', PLAN('b', '状态: done\n级别: L1'));
|
|
1223
|
+
w(T, 'workflow/plans/_TEMPLATE.md', PLAN('t', '状态: draft\n级别: L1'));
|
|
1224
|
+
w(T, 'workflow/plans/2026-09-03-c.md', PLAN('c', '状态: draft\n级别: L1', '\n共 {{docs.count.plans}} 份\n'));
|
|
1225
|
+
const r = run(T);
|
|
1226
|
+
check('检查16:取数正确——docs.count.plans 计 3 份(排除 _TEMPLATE,含终态)',
|
|
1227
|
+
outOf(r).includes('实时值 = 3'), `exit=${r.status}\n${outOf(r)}`);
|
|
1228
|
+
rmfix(T);
|
|
1229
|
+
}
|
|
1230
|
+
{
|
|
1231
|
+
// 【复核 P1 回归】转义按「逐个出现」而非整行——同一行的真签名不得被转义位藏住
|
|
1232
|
+
// 复核实证:首版「行内任一 `\{{` 即整行跳过」可被这样绕过:
|
|
1233
|
+
// `… \{{a.b}} 示意… {{ledger.lines}} 未回填` → 整行静默放过(0 告警)。
|
|
1234
|
+
const T = mkfix();
|
|
1235
|
+
w(T, '.agents/metric-claims.txt', MC);
|
|
1236
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1237
|
+
w(T, 'workflow/plans/p9.md', PLAN('p9', '状态: draft\n级别: L1',
|
|
1238
|
+
'\n讲语法 \\{{a.b}} 示意,但本行也有真断言 {{ledger.lines}} 未回填\n'));
|
|
1239
|
+
const r = run(T);
|
|
1240
|
+
check('检查16:同行「转义示意 + 真未回填」→ 真签名仍报(转义按逐个出现,不按整行)',
|
|
1241
|
+
outOf(r).includes('指标待回填'), `exit=${r.status}\n${outOf(r)}`);
|
|
1242
|
+
rmfix(T);
|
|
1243
|
+
}
|
|
1244
|
+
{
|
|
1245
|
+
// 【复核 P2 回归】形态不符的签名不得静默漏过(首版只认严格小写点分 → 笔误零告警)
|
|
1246
|
+
const T = mkfix();
|
|
1247
|
+
w(T, '.agents/metric-claims.txt', MC);
|
|
1248
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1249
|
+
w(T, 'workflow/plans/p10.md', PLAN('p10', '状态: draft\n级别: L1', '\n引用 {{Ledger.Lines}} 笔误\n'));
|
|
1250
|
+
const r = run(T);
|
|
1251
|
+
check('检查16:形态不符(大写点分笔误)→ 出账「指标形态」(不静默漏过)',
|
|
1252
|
+
outOf(r).includes('指标形态'), `exit=${r.status}\n${outOf(r)}`);
|
|
1253
|
+
rmfix(T);
|
|
1254
|
+
}
|
|
1255
|
+
{
|
|
1256
|
+
// 【复核 P2 回归】形态检查不得误伤装户模板占位符(全大写 SCREAMING_CASE 仍豁免)
|
|
1257
|
+
const T = mkfix();
|
|
1258
|
+
w(T, '.agents/metric-claims.txt', MC);
|
|
1259
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1260
|
+
const S = (n) => '{{' + n + '}}';
|
|
1261
|
+
w(T, 'workflow/plans/p11.md', PLAN('p11', '状态: draft\n级别: L1',
|
|
1262
|
+
'\n模板 ' + S('BUILD_CMD') + ' 与 ' + S('BOARD_PORT') + '\n'));
|
|
1263
|
+
const r = run(T);
|
|
1264
|
+
check('检查16:形态检查不误伤装户模板占位符(全大写仍豁免)',
|
|
1265
|
+
r.status === 0 && !outOf(r).includes('指标'), `exit=${r.status}\n${outOf(r)}`);
|
|
1266
|
+
rmfix(T);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// ---- 场景 80-90:检查 16 装户侧 derivers 动态载入(2026-09-28 adopter-derivers)----
|
|
1271
|
+
// 本组核心是**三条假绿防线**:装户模块坏掉时必须响亮出账,绝不静默降级成「只用内置指标」。
|
|
1272
|
+
// 对照:模块**缺失**是正常态(零告警)——两组行为必须可区分,否则装户无法判断自己错在哪。
|
|
1273
|
+
{
|
|
1274
|
+
const MC2 = 'ledger.lines = ledger.lines\nmy.custom = my.custom\n';
|
|
1275
|
+
const MCB = 'ledger.lines = ledger.lines\ndocs.count.plans = docs.count.plans\n'; // 仅内置指标
|
|
1276
|
+
const seedLedger = (T, lines) => w(T, '.agents/confirmations.jsonl', lines.join('\n') + '\n');
|
|
1277
|
+
const activePlan = (T, name, body) => {
|
|
1278
|
+
// 同时写同名 intent:否则「配对断裂」hard-block 会干扰本组断言(本组只测取数器分层)
|
|
1279
|
+
w(T, `workflow/intents/2026-09-01-${name}.md`, INTENT(name, '状态: draft\n级别: L1'));
|
|
1280
|
+
w(T, `workflow/plans/2026-09-01-${name}.md`, PLAN(name, '状态: draft\n级别: L1', body));
|
|
1281
|
+
};
|
|
1282
|
+
{
|
|
1283
|
+
// 【正向】装户模块存在 + 自定义指标 → 取数正确
|
|
1284
|
+
const T = mkfix();
|
|
1285
|
+
w(T, '.agents/metric-claims.txt', MC2);
|
|
1286
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1287
|
+
w(T, '.agents/metric-derivers.cjs', "module.exports = { derivers: { 'my.custom': () => 7 } };\n");
|
|
1288
|
+
activePlan(T, 'ok', '\n值 {{my.custom}}\n');
|
|
1289
|
+
const r = run(T);
|
|
1290
|
+
check('检查16装户:模块存在 + 自定义指标 → 实时值 = 7(取数正确)',
|
|
1291
|
+
outOf(r).includes('my.custom') && outOf(r).includes('实时值 = 7'), `exit=${r.status}\n${outOf(r)}`);
|
|
1292
|
+
rmfix(T);
|
|
1293
|
+
}
|
|
1294
|
+
{
|
|
1295
|
+
// 【假绿防线 ①】模块语法错 → 响亮出账 + check-loop 仍正常退出(不崩)
|
|
1296
|
+
const T = mkfix();
|
|
1297
|
+
w(T, '.agents/metric-claims.txt', MC2);
|
|
1298
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1299
|
+
w(T, '.agents/metric-derivers.cjs', "module.exports = { derivers: { 'my.custom': () => { \n"); // 故意不闭合
|
|
1300
|
+
activePlan(T, 'syn', '\n值 {{my.custom}}\n');
|
|
1301
|
+
const r = run(T);
|
|
1302
|
+
const o = outOf(r);
|
|
1303
|
+
// 断言必须用**唯一的 warning 前缀**匹配——「装户取数器载入失败」这串字也出现在
|
|
1304
|
+
// 「无对应取数器」那条的提示语里(「见上方「…载入失败」条目」),只用它匹配会**在真实条目
|
|
1305
|
+
// 消失时仍然通过**(变异自验实测:静默吞掉 emit 行后该断言不翻红)。故用 `- [WARN 装户取数器载入失败]`。
|
|
1306
|
+
check('检查16装户:模块语法错 → 响亮出账「装户取数器载入失败」(独立成条,明示文件)',
|
|
1307
|
+
o.includes('- [WARN 装户取数器载入失败]') && o.includes('metric-derivers.cjs') && !o.includes('未登记该指标'),
|
|
1308
|
+
`exit=${r.status}\n${o}`);
|
|
1309
|
+
check('检查16装户:模块语法错 → 引擎自身不因装户代码崩溃(无裸 SyntaxError 崩栈)',
|
|
1310
|
+
r.status === 0 && !/Illegal|at ModuleLoader|at compileSourceTextModule/.test(o),
|
|
1311
|
+
`exit=${r.status}\n${o}`);
|
|
1312
|
+
rmfix(T);
|
|
1313
|
+
}
|
|
1314
|
+
{
|
|
1315
|
+
// 【假绿防线 ②】取数器抛异常 → 响亮出账(明示错误)
|
|
1316
|
+
const T = mkfix();
|
|
1317
|
+
w(T, '.agents/metric-claims.txt', MC2);
|
|
1318
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1319
|
+
w(T, '.agents/metric-derivers.cjs', "module.exports = { derivers: { 'my.custom': () => { throw new Error('boom-xyz'); } } };\n");
|
|
1320
|
+
activePlan(T, 'thr', '\n值 {{my.custom}}\n');
|
|
1321
|
+
const r = run(T);
|
|
1322
|
+
check('检查16装户:取数器抛异常 → 响亮出账且明示错误',
|
|
1323
|
+
outOf(r).includes('指标取数失败') && outOf(r).includes('boom-xyz'), `exit=${r.status}\n${outOf(r)}`);
|
|
1324
|
+
rmfix(T);
|
|
1325
|
+
}
|
|
1326
|
+
{
|
|
1327
|
+
// 【假绿防线 ③】返回非有限数字(字符串 / NaN / Promise)→ 拦下并明示类型
|
|
1328
|
+
const T = mkfix();
|
|
1329
|
+
w(T, '.agents/metric-claims.txt', 'my.str = my.str\nmy.nan = my.nan\nmy.prom = my.prom\n');
|
|
1330
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1331
|
+
w(T, '.agents/metric-derivers.cjs', [
|
|
1332
|
+
"module.exports = { derivers: {",
|
|
1333
|
+
" 'my.str': () => 'not-a-number',",
|
|
1334
|
+
" 'my.nan': () => NaN,",
|
|
1335
|
+
" 'my.prom': () => Promise.resolve(1),",
|
|
1336
|
+
"} };",
|
|
1337
|
+
'',
|
|
1338
|
+
].join('\n'));
|
|
1339
|
+
activePlan(T, 'types', '\n{{my.str}} {{my.nan}} {{my.prom}}\n');
|
|
1340
|
+
const r = run(T);
|
|
1341
|
+
const o = outOf(r);
|
|
1342
|
+
check('检查16装户:返回字符串 → 取数失败(明示类型 string)',
|
|
1343
|
+
o.includes('不是有限数字') && o.includes('string'), `exit=${r.status}\n${o}`);
|
|
1344
|
+
check('检查16装户:返回 NaN → 取数失败', o.includes('number(NaN)'), `exit=${r.status}\n${o}`);
|
|
1345
|
+
check('检查16装户:返回 Promise(误写 async)→ 取数失败并提示须同步',
|
|
1346
|
+
o.includes('Promise') && o.includes('同步'), `exit=${r.status}\n${o}`);
|
|
1347
|
+
rmfix(T);
|
|
1348
|
+
}
|
|
1349
|
+
{
|
|
1350
|
+
// 【区分性】模块缺失 → 零告警(与「模块坏」必须行为可区分)
|
|
1351
|
+
const T = mkfix();
|
|
1352
|
+
w(T, '.agents/metric-claims.txt', MCB);
|
|
1353
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1354
|
+
activePlan(T, 'none', '');
|
|
1355
|
+
const r = run(T);
|
|
1356
|
+
check('检查16装户:模块缺失 → 零告警(正常态,与「模块坏」可区分)',
|
|
1357
|
+
r.status === 0 && !outOf(r).includes('指标'), `exit=${r.status}\n${outOf(r)}`);
|
|
1358
|
+
rmfix(T);
|
|
1359
|
+
}
|
|
1360
|
+
{
|
|
1361
|
+
// 【优先级】装户定义内置同名 → 内置生效 + advisory 明示被忽略
|
|
1362
|
+
const T = mkfix();
|
|
1363
|
+
w(T, '.agents/metric-claims.txt', 'ledger.lines = ledger.lines\n');
|
|
1364
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}', '{"doc":"b","stage":"done"}']);
|
|
1365
|
+
w(T, '.agents/metric-derivers.cjs', "module.exports = { derivers: { 'ledger.lines': () => 999 } };\n");
|
|
1366
|
+
activePlan(T, 'prio', '\n台账 {{ledger.lines}}\n');
|
|
1367
|
+
const r = run(T);
|
|
1368
|
+
const o = outOf(r);
|
|
1369
|
+
check('检查16装户:内置同名 → 内置值生效(2,非装户的 999)',
|
|
1370
|
+
o.includes('实时值 = 2'), `exit=${r.status}\n${o}`);
|
|
1371
|
+
check('检查16装户:内置同名 → advisory 明示装户定义被忽略',
|
|
1372
|
+
o.includes('被忽略'), `exit=${r.status}\n${o}`);
|
|
1373
|
+
rmfix(T);
|
|
1374
|
+
}
|
|
1375
|
+
{
|
|
1376
|
+
// 【未导出】模块存在但无 derivers 导出 → fail-loud
|
|
1377
|
+
const T = mkfix();
|
|
1378
|
+
w(T, '.agents/metric-claims.txt', MC2);
|
|
1379
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1380
|
+
w(T, '.agents/metric-derivers.cjs', "module.exports = {};\n");
|
|
1381
|
+
activePlan(T, 'noexp', '\n{{my.custom}}\n');
|
|
1382
|
+
const r = run(T);
|
|
1383
|
+
check('检查16装户:模块无 derivers 导出 → 响亮出账「载入失败」(独立成条,不静默)',
|
|
1384
|
+
outOf(r).includes('- [WARN 装户取数器载入失败]') && outOf(r).includes('未导出'), `exit=${r.status}\n${outOf(r)}`);
|
|
1385
|
+
rmfix(T);
|
|
1386
|
+
}
|
|
1387
|
+
{
|
|
1388
|
+
// 【旧路径迁移提示】残留首版 .mjs 文件时,须明示「已改用 .cjs」——
|
|
1389
|
+
// 否则装户对着「无取数器」查不出所以然(跨版本实测:require(esm) 在 Node 18/22 不可用,
|
|
1390
|
+
// 故首版 .mjs 契约在 CI 目标版本上根本不工作,必须给出可操作的迁移指引)
|
|
1391
|
+
const T = mkfix();
|
|
1392
|
+
w(T, '.agents/metric-claims.txt', MC2);
|
|
1393
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1394
|
+
w(T, '.agents/metric-derivers.mjs', "export const derivers = { 'my.custom': () => 7 };\n"); // 旧路径
|
|
1395
|
+
activePlan(T, 'legacy', '\n{{my.custom}}\n');
|
|
1396
|
+
const r = run(T);
|
|
1397
|
+
const o = outOf(r);
|
|
1398
|
+
check('检查16装户:残留旧 .mjs → 明示「已改用 .cjs」并给出 CJS 写法',
|
|
1399
|
+
o.includes('装户取数器载入失败') && o.includes('metric-derivers.mjs') && o.includes('.cjs') && o.includes('module.exports'),
|
|
1400
|
+
`exit=${r.status}\n${o}`);
|
|
1401
|
+
rmfix(T);
|
|
1402
|
+
}
|
|
1403
|
+
{
|
|
1404
|
+
// 【未登记指标仍走原路】装户定义了函数但没在 registry 登记 → 报「未登记」
|
|
1405
|
+
const T = mkfix();
|
|
1406
|
+
w(T, '.agents/metric-claims.txt', 'ledger.lines = ledger.lines\n');
|
|
1407
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1408
|
+
w(T, '.agents/metric-derivers.cjs', "module.exports = { derivers: { 'my.custom': () => 7 } };\n");
|
|
1409
|
+
activePlan(T, 'unreg', '\n{{my.custom}}\n');
|
|
1410
|
+
const r = run(T);
|
|
1411
|
+
check('检查16装户:函数有但 registry 未登记 → 报「未登记」(两处都要)',
|
|
1412
|
+
outOf(r).includes('指标未登记'), `exit=${r.status}\n${outOf(r)}`);
|
|
1413
|
+
rmfix(T);
|
|
1414
|
+
}
|
|
1415
|
+
{
|
|
1416
|
+
// 【ctx 能力】glob / read 取数正确
|
|
1417
|
+
const T = mkfix();
|
|
1418
|
+
w(T, '.agents/metric-claims.txt', 'my.sql = my.sql\nmy.read = my.read\n');
|
|
1419
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1420
|
+
fs.mkdirSync(path.join(T, 'backend', 'migrations'), { recursive: true }); // w() 不建父目录
|
|
1421
|
+
w(T, 'backend/migrations/001.sql', 'x');
|
|
1422
|
+
w(T, 'backend/migrations/002.sql', 'x');
|
|
1423
|
+
w(T, 'backend/other.txt', 'x');
|
|
1424
|
+
w(T, '.agents/metric-derivers.cjs', [
|
|
1425
|
+
"module.exports = { derivers: {",
|
|
1426
|
+
" 'my.sql': (ctx) => ctx.glob('backend/migrations/*.sql').length,",
|
|
1427
|
+
" 'my.read': (ctx) => ctx.read('backend/migrations/001.sql').length,",
|
|
1428
|
+
"} };",
|
|
1429
|
+
'',
|
|
1430
|
+
].join('\n'));
|
|
1431
|
+
activePlan(T, 'ctx', '\n{{my.sql}} {{my.read}}\n');
|
|
1432
|
+
const r = run(T);
|
|
1433
|
+
const o = outOf(r);
|
|
1434
|
+
check('检查16装户:ctx.glob 计数正确(2 个 sql)', o.includes('my.sql') && o.includes('实时值 = 2'), `exit=${r.status}\n${o}`);
|
|
1435
|
+
check('检查16装户:ctx.read 读取正确(内容 1 字符)', o.includes('my.read') && o.includes('实时值 = 1'), `exit=${r.status}\n${o}`);
|
|
1436
|
+
rmfix(T);
|
|
1437
|
+
}
|
|
1438
|
+
{
|
|
1439
|
+
// 【存量零差异】无装户模块时,内置指标行为与改前一致
|
|
1440
|
+
const T = mkfix();
|
|
1441
|
+
w(T, '.agents/metric-claims.txt', MCB);
|
|
1442
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}', '{"doc":"b","stage":"done"}']);
|
|
1443
|
+
activePlan(T, 'base', '\n台账 {{ledger.lines}};plans {{docs.count.plans}}\n');
|
|
1444
|
+
const r = run(T);
|
|
1445
|
+
check('检查16装户:无模块时内置指标照常取数(零差异)',
|
|
1446
|
+
outOf(r).includes('实时值 = 2') && outOf(r).includes('实时值 = 1'), `exit=${r.status}\n${outOf(r)}`);
|
|
1447
|
+
rmfix(T);
|
|
1448
|
+
}
|
|
1449
|
+
{
|
|
1450
|
+
// 【ctx.glob 语义】自查修正回归:`**/` 须含零层、可跨段;`*` 不跨 `/`
|
|
1451
|
+
// (首版实现用「先转义后 replace 星号」,实测 `src/**/*.ts` 匹配不到 `src/a/b/c.ts`)
|
|
1452
|
+
const T = mkfix();
|
|
1453
|
+
w(T, '.agents/metric-claims.txt', 'g.deep = g.deep\ng.flat = g.flat\ng.skip = g.skip\n');
|
|
1454
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1455
|
+
fs.mkdirSync(path.join(T, 'src', 'a', 'b'), { recursive: true });
|
|
1456
|
+
fs.mkdirSync(path.join(T, 'src', 'nested'), { recursive: true });
|
|
1457
|
+
w(T, 'src/a/b/c.ts', 'x');
|
|
1458
|
+
w(T, 'src/top.ts', 'x');
|
|
1459
|
+
w(T, 'src/nested/deep.ts', 'x');
|
|
1460
|
+
fs.mkdirSync(path.join(T, 'node_modules', 'pkg'), { recursive: true });
|
|
1461
|
+
w(T, 'node_modules/pkg/dep.ts', 'x');
|
|
1462
|
+
w(T, '.agents/metric-derivers.cjs', [
|
|
1463
|
+
"module.exports = { derivers: {",
|
|
1464
|
+
" 'g.deep': (ctx) => ctx.glob('src/**/*.ts').length,",
|
|
1465
|
+
" 'g.flat': (ctx) => ctx.glob('src/*.ts').length,",
|
|
1466
|
+
" 'g.skip': (ctx) => ctx.glob('**/*.ts').length,",
|
|
1467
|
+
"} };",
|
|
1468
|
+
'',
|
|
1469
|
+
].join('\n'));
|
|
1470
|
+
activePlan(T, 'glob', '\n{{g.deep}} {{g.flat}} {{g.skip}}\n');
|
|
1471
|
+
const r = run(T);
|
|
1472
|
+
const o = outOf(r);
|
|
1473
|
+
check('检查16装户:ctx.glob 的 `**/` 含零层且跨段(src/**/*.ts 计 3)',
|
|
1474
|
+
o.includes('g.deep') && o.includes('实时值 = 3'), `exit=${r.status}\n${o}`);
|
|
1475
|
+
check('检查16装户:ctx.glob 的 `*` 不跨 /(src/*.ts 计 1)',
|
|
1476
|
+
o.includes('g.flat') && o.includes('实时值 = 1'), `exit=${r.status}\n${o}`);
|
|
1477
|
+
check('检查16装户:ctx.glob 剪枝 node_modules(**/*.ts 计 3,非 4)',
|
|
1478
|
+
o.includes('g.skip') && o.includes('实时值 = 3'), `exit=${r.status}\n${o}`);
|
|
1479
|
+
rmfix(T);
|
|
1480
|
+
}
|
|
1481
|
+
{
|
|
1482
|
+
// 【ctx.glob 字面转义】点号须为字面(首版漏转义会误匹配)
|
|
1483
|
+
const T = mkfix();
|
|
1484
|
+
w(T, '.agents/metric-claims.txt', 'g.lit = g.lit\n');
|
|
1485
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1486
|
+
w(T, 'a.b.sql', 'x');
|
|
1487
|
+
w(T, 'axb.sql', 'x');
|
|
1488
|
+
w(T, '.agents/metric-derivers.cjs',
|
|
1489
|
+
"module.exports = { derivers: { 'g.lit': (ctx) => ctx.glob('a.b.sql').length } };\n");
|
|
1490
|
+
activePlan(T, 'lit', '\n{{g.lit}}\n');
|
|
1491
|
+
const r = run(T);
|
|
1492
|
+
check('检查16装户:ctx.glob 点号按字面(a.b.sql 计 1,不含 axb.sql)',
|
|
1493
|
+
outOf(r).includes('实时值 = 1'), `exit=${r.status}\n${outOf(r)}`);
|
|
1494
|
+
rmfix(T);
|
|
1495
|
+
}
|
|
1496
|
+
{
|
|
1497
|
+
// 【复核 P2 回归】模块坏但**没有任何装户指标**时,不得平白出账
|
|
1498
|
+
// (首版无条件报「载入失败」——半成品模块会给清白的装户增加不可消除的噪声)
|
|
1499
|
+
const T = mkfix();
|
|
1500
|
+
w(T, '.agents/metric-claims.txt', MCB); // 只有内置指标
|
|
1501
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1502
|
+
w(T, '.agents/metric-derivers.cjs', "module.exports = { derivers: { 'x.y': () => { \n"); // 坏模块
|
|
1503
|
+
activePlan(T, 'noise', '');
|
|
1504
|
+
const r = run(T);
|
|
1505
|
+
check('检查16装户:坏模块 + 无装户指标 → 不出账(无判定依据不产噪声)',
|
|
1506
|
+
r.status === 0 && !outOf(r).includes('装户取数器载入失败'), `exit=${r.status}\n${outOf(r)}`);
|
|
1507
|
+
rmfix(T);
|
|
1508
|
+
}
|
|
1509
|
+
{
|
|
1510
|
+
// 【复核 P2 回归】内置同名 advisory 按**指标**去重(首版按 registry 行重复出账)
|
|
1511
|
+
const T = mkfix();
|
|
1512
|
+
w(T, '.agents/metric-claims.txt', 'ledger.lines = ledger.lines\nledger.lines = ledger.lines\nledger.lines = ledger.lines\n');
|
|
1513
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1514
|
+
w(T, '.agents/metric-derivers.cjs', "module.exports = { derivers: { 'ledger.lines': () => 999 } };\n");
|
|
1515
|
+
activePlan(T, 'dedup', '\n台账 {{ledger.lines}}\n');
|
|
1516
|
+
const r = run(T);
|
|
1517
|
+
const n = (outOf(r).match(/被忽略/g) || []).length;
|
|
1518
|
+
check('检查16装户:内置同名 advisory 按指标去重(3 行登记 → 恰 1 条)', n === 1, `实际 ${n} 条\n${outOf(r)}`);
|
|
1519
|
+
rmfix(T);
|
|
1520
|
+
}
|
|
1521
|
+
{
|
|
1522
|
+
// 【复核 P2 回归】软链接成环 → 不得无界递归(复核实测首版 depth 128、同文件重复 64 份)
|
|
1523
|
+
const T = mkfix();
|
|
1524
|
+
w(T, '.agents/metric-claims.txt', 'g.loop = g.loop\n');
|
|
1525
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1526
|
+
fs.mkdirSync(path.join(T, 'loopdir', 'inner'), { recursive: true });
|
|
1527
|
+
w(T, 'loopdir/inner/f.txt', 'x');
|
|
1528
|
+
// 用 **dir 型 symlink**(POSIX 语义的目录软链接),而非 junction(Windows 特有、不具代表性)。
|
|
1529
|
+
// 用户 2026-09-28 追问「这里用到 sh 了吗」促使追验:防环走 fs.realpathSync(Node 原生跨平台),
|
|
1530
|
+
// **与 sh 无关**——故本断言在两种链接语义下都应成立。
|
|
1531
|
+
let linked = false;
|
|
1532
|
+
try {
|
|
1533
|
+
fs.symlinkSync(path.join(T, 'loopdir'), path.join(T, 'loopdir', 'inner', 'back'), 'dir');
|
|
1534
|
+
linked = true;
|
|
1535
|
+
} catch { /* 无建链接权限(未开开发者模式)→ 跳过该断言 */ }
|
|
1536
|
+
w(T, '.agents/metric-derivers.cjs',
|
|
1537
|
+
"module.exports = { derivers: { 'g.loop': (ctx) => ctx.glob('loopdir/**/*.txt').length } };\n");
|
|
1538
|
+
activePlan(T, 'loop', '\n{{g.loop}}\n');
|
|
1539
|
+
const r = run(T);
|
|
1540
|
+
const o16 = outOf(r);
|
|
1541
|
+
check(linked
|
|
1542
|
+
// 防环生效 ⇒ 计数为 1(只有 inner/f.txt);无防环会沿环重复到 64
|
|
1543
|
+
? '检查16装户:dir 型 symlink 成环(POSIX 语义)→ 防环生效,计数恰 1'
|
|
1544
|
+
: '检查16装户:软链接成环用例跳过(本机无建链接权限)',
|
|
1545
|
+
linked ? o16.includes('实时值 = 1') : true, `exit=${r.status}\n${o16}`);
|
|
1546
|
+
rmfix(T);
|
|
1547
|
+
}
|
|
1548
|
+
{
|
|
1549
|
+
// 【归属契约】装户取数模块必须归 owned——否则落回 managed,重演本单要修的缺陷本身
|
|
1550
|
+
// (sync 永久报「本地已改」+ doctor WARN + check-loop 因供应链防线静默停摆)
|
|
1551
|
+
//
|
|
1552
|
+
// **本用例不得依赖包源 `src/`**(复核 P1 更正):首版写 `await import('../../../src/profiles.mjs')`,
|
|
1553
|
+
// 该路径在**本仓**解析到 `<repo>/src/`(存在),但在**装户安装**里 `.agents/scripts/` 只往上三层
|
|
1554
|
+
// 即 `<install>/../src/`——不存在 → `ERR_MODULE_NOT_FOUND` → **整个套件崩掉**。而本测试文件是
|
|
1555
|
+
// managed/shipped,故每个装户拿到的测试套件都是坏的;`npm test` 又只跑 `templates/` 副本
|
|
1556
|
+
// (见 src/run-tests.mjs),所以 CI 看不见。修法:优先动态 import,失败则**降级为读源码断言**
|
|
1557
|
+
// (isOwned 的判定是纯字面量列表,读源即可判,且包源/装副本都在同一路径下)。
|
|
1558
|
+
let isOwned = null;
|
|
1559
|
+
try {
|
|
1560
|
+
({ isOwned } = await import('../../../src/profiles.mjs'));
|
|
1561
|
+
} catch { /* 装户环境无包源 src/ → 走下面的降级断言 */ }
|
|
1562
|
+
if (typeof isOwned === 'function') {
|
|
1563
|
+
check('检查16装户:isOwned(.agents/metric-derivers.cjs) 为真(与登记表同归 owned)',
|
|
1564
|
+
isOwned('.agents/metric-derivers.cjs') === true);
|
|
1565
|
+
check('检查16装户:isOwned(.agents/metric-claims.txt) 仍为真(未被本单破坏)',
|
|
1566
|
+
isOwned('.agents/metric-claims.txt') === true);
|
|
1567
|
+
check('检查16装户:check-loop.mjs 仍归 managed(引擎不被误划入 owned)',
|
|
1568
|
+
isOwned('.agents/scripts/check-loop.mjs') === false);
|
|
1569
|
+
} else {
|
|
1570
|
+
// **装户环境降级断言**(复核 P1 更正):装户没有包源 `src/profiles.mjs`,故改验**可观测事实**——
|
|
1571
|
+
// 若 `metric-derivers.cjs` 已存在于盘上,它必须**不**记在 kit.json 的 managed 侧(在 managed 即为错,
|
|
1572
|
+
// 那正是本单要修的缺陷形态)。未安装该模块时跳过(无判定依据的行不产出噪声)。
|
|
1573
|
+
// 路径:本文件在 `<root>/.agents/scripts/`,故 kit.json 在 `../kit.json`(= `<root>/.agents/kit.json`)。
|
|
1574
|
+
let verdict = null;
|
|
1575
|
+
let detail = '';
|
|
1576
|
+
try {
|
|
1577
|
+
const kit = JSON.parse(fs.readFileSync(new URL('../kit.json', import.meta.url), 'utf8'));
|
|
1578
|
+
const rel = '.agents/metric-derivers.cjs';
|
|
1579
|
+
const inManaged = (kit.managed || []).some((f) => f.rel === rel);
|
|
1580
|
+
const inOwned = (kit.owned || []).some((f) => f.rel === rel);
|
|
1581
|
+
verdict = !inManaged;
|
|
1582
|
+
detail = `managed=${inManaged} owned=${inOwned}`;
|
|
1583
|
+
} catch (e) {
|
|
1584
|
+
verdict = null; detail = String(e.message).slice(0, 60);
|
|
1585
|
+
}
|
|
1586
|
+
if (verdict === null) {
|
|
1587
|
+
check('检查16装户:装户环境无法判定归属(无 kit.json)→ 跳过(不产噪声)', true);
|
|
1588
|
+
} else {
|
|
1589
|
+
check('检查16装户:装户环境——metric-derivers.cjs 不在 managed 侧(不在即正确)',
|
|
1590
|
+
verdict === true, detail);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1101
1596
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
1102
1597
|
process.exit(fail ? 1 : 0);
|
|
@@ -19,8 +19,13 @@ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
|
19
19
|
const ROOT = path.resolve(SCRIPT_DIR, '..', '..', '..');
|
|
20
20
|
const SRC = path.join(ROOT, 'src', 'doctor.mjs');
|
|
21
21
|
if (!fs.existsSync(SRC)) {
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
// **环境不适用 ≠ 失败**(2026-09-28 修正):本套件测的是包源 `src/doctor.mjs` 的内部函数,
|
|
23
|
+
// 装户环境没有 `src/`(那是包源)——故此处应 **SKIP 并以 0 退出**,与 check-loop.test.mjs 的
|
|
24
|
+
// 「无 sh 环境打印 SKIP 不算失败」同一口径(既有先例)。
|
|
25
|
+
// 首版 exit(1) 的代价是实测出来的:CI 新增「shipped 套件」步骤后,装户径下逐套件批量跑会把
|
|
26
|
+
// 这条 SKIP 当成真失败(doctor.test 单独 exit 1),使批量结果不可用。
|
|
27
|
+
console.error(`doctor.test.mjs:SKIP——未找到 ${SRC}(装户环境无包源 src/;本套件仅在包源仓库/dogfooding 跑 npm test 时有意义)`);
|
|
28
|
+
process.exit(0);
|
|
24
29
|
}
|
|
25
30
|
const { checkOwnedDrift, checkAdapterDrift, checkLedgerCoverage } = await import(pathToFileURL(SRC).href);
|
|
26
31
|
|
|
@@ -80,6 +80,7 @@ const PAIRS = [
|
|
|
80
80
|
{ doctor: '7', cl: '13', note: '经 §7 整体运行覆盖' },
|
|
81
81
|
{ doctor: '7', cl: '14', note: '经 §7 整体运行覆盖' },
|
|
82
82
|
{ doctor: '7', cl: '15', note: '经 §7 整体运行覆盖' },
|
|
83
|
+
{ doctor: '7', cl: '16', note: '经 §7 整体运行覆盖(量化断言指标签名对账,2026-09-28 claim-exceeds-fix)' },
|
|
83
84
|
];
|
|
84
85
|
|
|
85
86
|
// gateChecklist:按登记表对照两侧检查项(pure function;pairs 可注入供测试)
|
|
@@ -44,8 +44,27 @@ const METRICS_P = path.join(WORKFLOW, 'metrics.md');
|
|
|
44
44
|
const BUDGETS = path.join('.agents', 'rule-budgets.txt');
|
|
45
45
|
const DOC_TYPES = ['intents', 'specs', 'plans', 'incidents'];
|
|
46
46
|
// 活跃口径单源(board-kb-p1:枚举字面量硬编码退役——workflow-enums.txt 是唯一源);
|
|
47
|
-
// 枚举路径锚定脚本位置(fixture 测试 cwd 在临时目录,按 cwd 解析会 ENOENT
|
|
48
|
-
|
|
47
|
+
// 枚举路径锚定脚本位置(fixture 测试 cwd 在临时目录,按 cwd 解析会 ENOENT)。
|
|
48
|
+
// **不能用固定上溯级数**(2026-09-28 修正):本脚本在两处运行、深度不同——
|
|
49
|
+
// 包源 `templates/_agents/scripts/` → 上溯 3 级到仓库根
|
|
50
|
+
// 装户 `.agents/scripts/` → 上溯 2 级到安装根
|
|
51
|
+
// 首版硬编码 3 级,故**在每个装户安装里都解析不到枚举文件**(报错路径 `<install>/../.agents/…`)。
|
|
52
|
+
// 实测暴露路径:CI 新增「shipped 套件」步骤后,`.agents/scripts/gen-workflow-metrics.test.mjs`
|
|
53
|
+
// 场景 1 即 exit 1(workflow-enums 单源文件不可读),而包源侧同字节跑出全绿。
|
|
54
|
+
// 改为**逐级上溯探测**:找到含 `.agents/workflow-enums.txt` 的那一级即用,找不到则回退标准位置让
|
|
55
|
+
// loadEnums fail-loud(保持「缺失即响亮」的既有纪律,不静默兜底)。
|
|
56
|
+
const resolveEnumsPath = () => {
|
|
57
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
58
|
+
for (let i = 0; i < 6; i++) {
|
|
59
|
+
const cand = path.join(dir, '.agents', 'workflow-enums.txt');
|
|
60
|
+
if (fs.existsSync(cand)) return cand;
|
|
61
|
+
const parent = path.dirname(dir);
|
|
62
|
+
if (parent === dir) break; // 到根
|
|
63
|
+
dir = parent;
|
|
64
|
+
}
|
|
65
|
+
return path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '.agents', 'workflow-enums.txt');
|
|
66
|
+
};
|
|
67
|
+
const ENUMS = loadEnums(resolveEnumsPath());
|
|
49
68
|
const ACTIVE_STATUS = [...ENUMS['doc.status.active'], ...ENUMS['incident.status.active']];
|
|
50
69
|
const BEGIN = '<!-- GENERATED:BEGIN — gen-workflow-metrics.mjs 整段重写,手工说明写在本行之前 -->';
|
|
51
70
|
const END = '<!-- GENERATED:END -->';
|
|
@@ -18,7 +18,15 @@ function fail(msg) { console.error('❌ ' + msg); process.exit(1); }
|
|
|
18
18
|
const VALID_EXTS = new Set(['.md', '.mjs', '.json', '.txt']);
|
|
19
19
|
// 装副本独有文件(init 渲染产物,不属双源结构)
|
|
20
20
|
const RENDER_OUTPUT_FILES = new Set(['kit.json', 'settings.json', 'hooks/commit-check.config.json']);
|
|
21
|
-
|
|
21
|
+
// **项目自持、按设计就该与包源骨架不同**的架子文件(2026-09-28 追加):
|
|
22
|
+
// `notes/runtime-env.md` 自述「记录本机/本项目的运行时差异……**本文件是骨架:项目按实际填写,
|
|
23
|
+
// 不随 flow-kit 升级覆盖**」——即包源只发骨架、装户填实况,**两处本就应当不同**。
|
|
24
|
+
// 但本工具此前按 `.md` 全量比对,导致装户一旦按文件自述填写,`--gate` 即 exit 1(CI 红):
|
|
25
|
+
// 实测(本仓自装)填写后 --gate 报 `漂移 1: notes/runtime-env.md`,回滚即恢复 0。
|
|
26
|
+
// 这属「门禁范围与文件自述冲突」,非真漂移——故按渲染产物同类处理(排除)。
|
|
27
|
+
// 判据:该文件是**项目内容**而非**引擎件**;引擎件(.mjs 脚本等)不在排除内,仍受双源门禁约束。
|
|
28
|
+
const PROJECT_OWNED_FILES = new Set(['notes/runtime-env.md']);
|
|
29
|
+
const TARGET_EXCLUDE = new Set([...RENDER_OUTPUT_FILES, ...PROJECT_OWNED_FILES]); // 向后兼容别名(原为 RENDER_OUTPUT_FILES)
|
|
22
30
|
|
|
23
31
|
function sha256(p) {
|
|
24
32
|
try {
|
|
@@ -50,7 +58,10 @@ function walkMd(root) {
|
|
|
50
58
|
export function sourceSyncCheck({ pkgRoot, target }) {
|
|
51
59
|
const authorityRoot = path.join(pkgRoot, 'templates', '_agents');
|
|
52
60
|
const adapterRoot = path.join(target, '.agents');
|
|
53
|
-
|
|
61
|
+
// **两侧都要按 TARGET_EXCLUDE 过滤**(2026-09-28 修正):只过滤装副本一侧会把排除件
|
|
62
|
+
// 变成「包源有 / 装副本无」的**缺失**(实测:加排除后 --gate 从「漂移 1」变成「缺失 1」,
|
|
63
|
+
// 仍 exit 1)。排除的语义是「不参与双源比对」,故包源侧同样滤掉。
|
|
64
|
+
const pkgFiles = walkMd(authorityRoot).filter((f) => !TARGET_EXCLUDE.has(f));
|
|
54
65
|
const tgtFiles = walkMd(adapterRoot).filter((f) => !TARGET_EXCLUDE.has(f));
|
|
55
66
|
const pkgSet = new Set(pkgFiles);
|
|
56
67
|
const tgtSet = new Set(tgtFiles);
|
|
@@ -240,5 +240,42 @@ if (!SRC_ROOT) {
|
|
|
240
240
|
check('S14 内容差异(甲 vs 乙)→ 报漂移(归一只认 CRLF 不吞内容)', r.drift.length === 1 && r.drift[0].rel === 'commands/a.md', JSON.stringify(r.drift));
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
// ---- 场景 15-16:项目自持架子文件排除(2026-09-28 runtime-env 双源冲突)----
|
|
244
|
+
{
|
|
245
|
+
// S15 notes/runtime-env.md **两侧都被排除**——包源只发骨架、装户填实况,两处本就该不同。
|
|
246
|
+
// 回归:首版只过滤装副本一侧 → 排除件变成「包源有/装副本无」的**缺失**,--gate 仍 exit 1
|
|
247
|
+
// (实测:漂移 1 → 缺失 1)。故断言 missing/drift 皆为空。
|
|
248
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-ssc-owned-'));
|
|
249
|
+
const pkgRoot = path.join(root, 'pkg');
|
|
250
|
+
const target = path.join(root, 'target');
|
|
251
|
+
const pkgBase = path.join(pkgRoot, 'templates', '_agents', 'notes');
|
|
252
|
+
const tgtBase = path.join(target, '.agents', 'notes');
|
|
253
|
+
fs.mkdirSync(pkgBase, { recursive: true });
|
|
254
|
+
fs.mkdirSync(tgtBase, { recursive: true });
|
|
255
|
+
fs.writeFileSync(path.join(pkgBase, 'runtime-env.md'), '# 骨架\n> 项目按实际填写\n');
|
|
256
|
+
fs.writeFileSync(path.join(tgtBase, 'runtime-env.md'), '# 实况\n> 本机 sh 路径已接\n');
|
|
257
|
+
const r = sourceSyncCheck({ pkgRoot, target });
|
|
258
|
+
check('S15 项目自持 runtime-env.md 两侧内容不同 → 既不报缺失也不报漂移(双侧排除)',
|
|
259
|
+
r.missing.length === 0 && r.drift.length === 0,
|
|
260
|
+
JSON.stringify({ missing: r.missing, drift: r.drift }));
|
|
261
|
+
}
|
|
262
|
+
{
|
|
263
|
+
// S16 排除**只针对该文件**——同目录其他 .md 仍受双源门禁约束(防排除范围被误放大)
|
|
264
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-ssc-owned2-'));
|
|
265
|
+
const pkgRoot = path.join(root, 'pkg');
|
|
266
|
+
const target = path.join(root, 'target');
|
|
267
|
+
const pkgBase = path.join(pkgRoot, 'templates', '_agents', 'notes');
|
|
268
|
+
const tgtBase = path.join(target, '.agents', 'notes');
|
|
269
|
+
fs.mkdirSync(pkgBase, { recursive: true });
|
|
270
|
+
fs.mkdirSync(tgtBase, { recursive: true });
|
|
271
|
+
fs.writeFileSync(path.join(pkgBase, 'runtime-env.md'), '# 骨架\n');
|
|
272
|
+
fs.writeFileSync(path.join(tgtBase, 'runtime-env.md'), '# 实况\n');
|
|
273
|
+
fs.writeFileSync(path.join(pkgBase, 'other.md'), '# v1\n');
|
|
274
|
+
fs.writeFileSync(path.join(tgtBase, 'other.md'), '# v2\n');
|
|
275
|
+
const r = sourceSyncCheck({ pkgRoot, target });
|
|
276
|
+
check('S16 排除不放大——同目录 other.md 内容不同仍报漂移',
|
|
277
|
+
r.drift.length === 1 && r.drift[0].rel === 'notes/other.md', JSON.stringify(r.drift));
|
|
278
|
+
}
|
|
279
|
+
|
|
243
280
|
console.log('\n合计: PASS ' + pass + ' / FAIL ' + failCount);
|
|
244
281
|
process.exit(failCount ? 1 : 0);
|
|
@@ -226,12 +226,23 @@ const fm = (extra = '') => `---\nname: t\ndescription: t\n${extra}---\n\n# w\n\n
|
|
|
226
226
|
|
|
227
227
|
// ---- S14 真实仓库自扫 baseline:0 error(_TEMPLATE 排除后三份可执行件全过)----
|
|
228
228
|
{
|
|
229
|
+
// **本场景是包源仓库的 baseline 断言,装户环境应 SKIP**(2026-09-28 修正):
|
|
230
|
+
// 它上溯三级定位「真实仓库」,从 `templates/_agents/scripts/` 出发正确落在包源根;
|
|
231
|
+
// 但从装户的 `.agents/scripts/` 出发会落到 `<install>/..`,扫不到可执行编排件 →
|
|
232
|
+
// `files.length >= 3` 必假。首版没区分这两种上下文,导致**装户径下该套件恒红**——
|
|
233
|
+
// 实测:CI 新增「shipped 套件」步骤后,`.agents/scripts/workflows-check.test.mjs`
|
|
234
|
+
// 报 `FAIL S14(扫描面:)`(空扫描面),而包源侧同字节跑出 15/0。
|
|
235
|
+
// 判据:扫不到任何可执行件 = 本环境不适用 → SKIP(沿 doctor.test.mjs 同口径)。
|
|
229
236
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
|
|
230
237
|
const r = lintWorkflows({ root: ROOT });
|
|
231
238
|
const names = r.files.map((f) => f.file).join('、');
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
239
|
+
if (r.files.length === 0) {
|
|
240
|
+
check('S14 真实仓库自扫 SKIP(本环境无 .agents/workflows/ 可执行件——装户径,沿 doctor.test 同口径)', true);
|
|
241
|
+
} else {
|
|
242
|
+
check('S14 真实仓库 .agents/workflows/ 自扫 0 error(扫描面:' + names + ')',
|
|
243
|
+
r.errorCount === 0 && r.files.length >= 3,
|
|
244
|
+
JSON.stringify(r.files.filter((f) => f.errors.length).map((f) => ({ f: f.file, e: f.errors }))));
|
|
245
|
+
}
|
|
235
246
|
}
|
|
236
247
|
|
|
237
248
|
// ---- S14 human 确认门(第四形态):单形态全绿 / 双填 E9 / ×retries W4 / 缺列兼容 ----
|