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.
Files changed (31) hide show
  1. package/modules/hosts/opencode/commands/{sync-hosts.md → wf-sync-hosts.md} +1 -1
  2. package/modules/hosts/trae/commands/wf-sync-hosts.md +1 -1
  3. package/package.json +1 -1
  4. package/src/doctor.mjs +9 -4
  5. package/src/profiles.mjs +19 -2
  6. package/src/sync-hosts.mjs +12 -6
  7. package/src/sync-hosts.test.mjs +14 -11
  8. package/templates/AGENTS.md +1 -1
  9. package/templates/_agents/commands/sync-hosts.md +1 -1
  10. package/templates/_agents/metric-claims.txt +81 -0
  11. package/templates/_agents/scripts/check-ledger.mjs +65 -0
  12. package/templates/_agents/scripts/check-ledger.test.mjs +127 -0
  13. package/templates/_agents/scripts/check-loop.mjs +277 -1
  14. package/templates/_agents/scripts/check-loop.test.mjs +496 -1
  15. package/templates/_agents/scripts/doctor.test.mjs +10 -5
  16. package/templates/_agents/scripts/gate-checklist.mjs +1 -0
  17. package/templates/_agents/scripts/gen-workflow-metrics.mjs +21 -2
  18. package/templates/_agents/scripts/source-sync-check.mjs +13 -2
  19. package/templates/_agents/scripts/source-sync-check.test.mjs +37 -0
  20. package/templates/_agents/scripts/workflows-check.test.mjs +14 -3
  21. package/templates/_githooks/pre-commit +17 -1
  22. /package/modules/hosts/opencode/commands/{build.md → wf-build.md} +0 -0
  23. /package/modules/hosts/opencode/commands/{deploy.md → wf-deploy.md} +0 -0
  24. /package/modules/hosts/opencode/commands/{design.md → wf-design.md} +0 -0
  25. /package/modules/hosts/opencode/commands/{gate-checklist.md → wf-gate-checklist.md} +0 -0
  26. /package/modules/hosts/opencode/commands/{maintain.md → wf-maintain.md} +0 -0
  27. /package/modules/hosts/opencode/commands/{new-task.md → wf-new-task.md} +0 -0
  28. /package/modules/hosts/opencode/commands/{plan.md → wf-plan.md} +0 -0
  29. /package/modules/hosts/opencode/commands/{review.md → wf-review.md} +0 -0
  30. /package/modules/hosts/opencode/commands/{source-sync-check.md → wf-source-sync-check.md} +0 -0
  31. /package/modules/hosts/opencode/commands/{test.md → wf-test.md} +0 -0
@@ -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
- console.error(`doctor.test.mjs:未找到 ${SRC}——仅在包源仓库(dogfooding)跑 npm test 时调用`);
23
- process.exit(1);
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
 
@@ -209,7 +214,7 @@ const CMD_BODY = '# Build\n\n正文 build v1\n';
209
214
  'roles/implementer.md': `---\ndesc: r\n---\n\n${ROLE_BODY}`,
210
215
  },
211
216
  adapters: {
212
- '.opencode/commands/build.md': `---\ndesc: oc\n---\n\n${CMD_BODY}`,
217
+ '.opencode/commands/wf-build.md': `---\ndesc: oc\n---\n\n${CMD_BODY}`,
213
218
  '.trae/commands/wf-build.md': `---\nname: wf-build\ndesc: trae\n---\n\n${CMD_BODY}`,
214
219
  '.zcode/agents/implementer.md': `---\nname: impl\ndesc: zcode\n---\n\n${ROLE_BODY}`,
215
220
  '.opencode/agents/implementer.md': `---\nname: impl\ndesc: oc\n---\n\n${ROLE_BODY}`,
@@ -244,7 +249,7 @@ const CMD_BODY = '# Build\n\n正文 build v1\n';
244
249
  const root = mkfixAdapter({
245
250
  pkgMarker: true,
246
251
  authority: { 'commands/build.md': `---\ndesc: x\n---\n\n${CMD_BODY}` },
247
- adapters: { '.opencode/commands/build.md': `---\ndesc: oc\n---\n\n漂移` },
252
+ adapters: { '.opencode/commands/wf-build.md': `---\ndesc: oc\n---\n\n漂移` },
248
253
  });
249
254
  const r = checkAdapterDrift(root);
250
255
  check('场景 11:包源环境(templates/ + modules/ 同时存在)→ skipped=true',
@@ -286,7 +291,7 @@ const CMD_BODY = '# Build\n\n正文 build v1\n';
286
291
  'roles/implementer.md': `---\ndesc: r\n---\n\n${ROLE_BODY}`,
287
292
  },
288
293
  adapters: {
289
- '.opencode/commands/build.md': `---\ndesc: oc\n---\n\n${CMD_BODY}`,
294
+ '.opencode/commands/wf-build.md': `---\ndesc: oc\n---\n\n${CMD_BODY}`,
290
295
  },
291
296
  });
292
297
  const r = checkAdapterDrift(root);
@@ -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
- const ENUMS = loadEnums(path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '.agents', 'workflow-enums.txt'));
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
- const TARGET_EXCLUDE = RENDER_OUTPUT_FILES; // 向后兼容别名
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
- const pkgFiles = walkMd(authorityRoot);
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);