@dommaker/harness 0.17.1 → 0.18.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 (116) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +5 -4
  3. package/bin/harness.js +3 -1
  4. package/bin/install-precommit-hook.sh +76 -0
  5. package/dist/architecture/cross-project-checker.js +2 -2
  6. package/dist/architecture/cross-project-checker.js.map +1 -1
  7. package/dist/cli/commands/constraints-report.js +2 -2
  8. package/dist/cli/commands/constraints-report.js.map +1 -1
  9. package/dist/cli/commands/constraints-retire.d.ts +24 -6
  10. package/dist/cli/commands/constraints-retire.d.ts.map +1 -1
  11. package/dist/cli/commands/constraints-retire.js +96 -19
  12. package/dist/cli/commands/constraints-retire.js.map +1 -1
  13. package/dist/cli/commands/doc-freshness-check.js +1 -1
  14. package/dist/cli/commands/doc-freshness-check.js.map +1 -1
  15. package/dist/cli/commands/init.js +14 -14
  16. package/dist/cli/commands/init.js.map +1 -1
  17. package/dist/cli/commands/sync-docs/index.d.ts.map +1 -1
  18. package/dist/cli/commands/sync-docs/index.js +14 -6
  19. package/dist/cli/commands/sync-docs/index.js.map +1 -1
  20. package/dist/cli/commands/sync-docs/project-reader.d.ts.map +1 -1
  21. package/dist/cli/commands/sync-docs/project-reader.js +9 -7
  22. package/dist/cli/commands/sync-docs/project-reader.js.map +1 -1
  23. package/dist/cli/commands/validate.js +3 -3
  24. package/dist/cli/commands/validate.js.map +1 -1
  25. package/dist/cli/session-mining/text.js +1 -1
  26. package/dist/cli/session-mining/text.js.map +1 -1
  27. package/dist/core/constraints/definitions/iron-laws.js +2 -2
  28. package/dist/core/constraints/definitions/iron-laws.js.map +1 -1
  29. package/dist/core/constraints/injection-drift.d.ts +1 -1
  30. package/dist/core/constraints/injection-drift.d.ts.map +1 -1
  31. package/dist/core/constraints/injection-drift.js +1 -1
  32. package/dist/core/constraints/injection-drift.js.map +1 -1
  33. package/dist/core/effective-constraints.d.ts +2 -1
  34. package/dist/core/effective-constraints.d.ts.map +1 -1
  35. package/dist/core/effective-constraints.js +2 -1
  36. package/dist/core/effective-constraints.js.map +1 -1
  37. package/dist/core/project-config-loader.d.ts +2 -1
  38. package/dist/core/project-config-loader.d.ts.map +1 -1
  39. package/dist/core/project-config-loader.js +8 -2
  40. package/dist/core/project-config-loader.js.map +1 -1
  41. package/dist/hooks/bootstrap.js +1 -1
  42. package/dist/hooks/bootstrap.js.map +1 -1
  43. package/dist/knowledge/store.js +1 -1
  44. package/dist/knowledge/store.js.map +1 -1
  45. package/dist/knowledge/types.d.ts +2 -0
  46. package/dist/knowledge/types.d.ts.map +1 -1
  47. package/dist/knowledge/types.js.map +1 -1
  48. package/dist/safety/output-guardrail.js +2 -2
  49. package/dist/safety/output-guardrail.js.map +1 -1
  50. package/dist/spec/annotation-checker.js +1 -1
  51. package/dist/spec/annotation-checker.js.map +1 -1
  52. package/dist/types/project-config.d.ts +15 -0
  53. package/dist/types/project-config.d.ts.map +1 -1
  54. package/dist/utils/file-walk.d.ts +13 -1
  55. package/dist/utils/file-walk.d.ts.map +1 -1
  56. package/dist/utils/file-walk.js +16 -3
  57. package/dist/utils/file-walk.js.map +1 -1
  58. package/package.json +6 -2
  59. package/src/__tests__/checkpoint.test.ts +2 -2
  60. package/src/__tests__/clean-state-extra.test.ts +1 -1
  61. package/src/__tests__/constraint-doctor.test.ts +1 -1
  62. package/src/__tests__/doctor-extra.test.ts +0 -9
  63. package/src/__tests__/effective-constraints.test.ts +21 -0
  64. package/src/__tests__/hooks-pipeline.test.ts +1 -1
  65. package/src/__tests__/iron-laws.test.ts +11 -0
  66. package/src/__tests__/passes-gate-extension.test.ts +3 -3
  67. package/src/__tests__/passes-gate-extra.test.ts +1 -1
  68. package/src/__tests__/performance-analyzer.test.ts +0 -2
  69. package/src/__tests__/traces.test.ts +1 -2
  70. package/src/architecture/cross-project-checker.ts +2 -2
  71. package/src/cli/commands/CONTEXT.md +1 -1
  72. package/src/cli/commands/__tests__/check-drift.test.ts +1 -1
  73. package/src/cli/commands/__tests__/check-skip-output.test.ts +0 -2
  74. package/src/cli/commands/__tests__/command.test.ts +1 -1
  75. package/src/cli/commands/__tests__/constraints-report.test.ts +1 -1
  76. package/src/cli/commands/__tests__/constraints-retire.test.ts +165 -4
  77. package/src/cli/commands/__tests__/failure.test.ts +0 -1
  78. package/src/cli/commands/__tests__/init.test.ts +40 -0
  79. package/src/cli/commands/__tests__/status-extra.test.ts +0 -2
  80. package/src/cli/commands/__tests__/status.test.ts +0 -2
  81. package/src/cli/commands/__tests__/sync-docs.test.ts +90 -0
  82. package/src/cli/commands/constraints-report.ts +2 -2
  83. package/src/cli/commands/constraints-retire.ts +114 -21
  84. package/src/cli/commands/doc-freshness-check.ts +1 -1
  85. package/src/cli/commands/init.ts +14 -14
  86. package/src/cli/commands/sync-docs/index.ts +20 -7
  87. package/src/cli/commands/sync-docs/project-reader.ts +10 -8
  88. package/src/cli/commands/validate.ts +3 -3
  89. package/src/cli/session-mining/text.ts +1 -1
  90. package/src/context/__tests__/progressive-loader.test.ts +6 -6
  91. package/src/core/CONTEXT.md +1 -1
  92. package/src/core/constraints/__tests__/injection-drift.test.ts +1 -1
  93. package/src/core/constraints/__tests__/interceptor.test.ts +3 -3
  94. package/src/core/constraints/definitions/iron-laws.ts +2 -2
  95. package/src/core/constraints/injection-drift.ts +1 -1
  96. package/src/core/effective-constraints.ts +2 -1
  97. package/src/core/project-config-loader.ts +8 -2
  98. package/src/gates/__tests__/acceptance.test.ts +0 -1
  99. package/src/gates/__tests__/command.test.ts +1 -1
  100. package/src/hooks/bootstrap.ts +1 -1
  101. package/src/knowledge/__tests__/doctor.test.ts +2 -2
  102. package/src/knowledge/__tests__/ingest.test.ts +2 -2
  103. package/src/knowledge/__tests__/migration.test.ts +0 -6
  104. package/src/knowledge/__tests__/store.test.ts +14 -1
  105. package/src/knowledge/store.ts +1 -1
  106. package/src/knowledge/types.ts +2 -0
  107. package/src/llm/__tests__/adapter.test.ts +1 -1
  108. package/src/monitoring/__tests__/knowledge-doctor.test.ts +0 -1
  109. package/src/monitoring/__tests__/performance-collector.test.ts +1 -2
  110. package/src/safety/output-guardrail.ts +2 -2
  111. package/src/spec/__tests__/annotation-checker.test.ts +0 -1
  112. package/src/spec/annotation-checker.ts +1 -1
  113. package/src/types/project-config.ts +12 -0
  114. package/src/utils/__tests__/file-walk.test.ts +53 -0
  115. package/src/utils/file-walk.ts +23 -3
  116. package/src/verification/__tests__/loop.test.ts +1 -1
@@ -2,17 +2,28 @@
2
2
  * harness constraints retire —— 约束退役(ADR-0001 决策 2/5)
3
3
  *
4
4
  * 建议层全自动(候选诊断复用 report 数据层),执行层保留一次人确认。
5
- * 落盘形态 = config.yml `enabled: false` + `retired` 元数据(不发明第二套状态):
5
+ * 落盘形态(#82 D6 一处真相):
6
6
  *
7
- * constraints:
8
- * <id>:
9
- * enabled: false
10
- * retired: { at, reason, stats: { total, fail, failRate } }
7
+ * - 内置约束 → config.yml `enabled: false` + `retired` 元数据:
8
+ *
9
+ * constraints:
10
+ * <id>:
11
+ * enabled: false
12
+ * retired: { at, reason, stats: { total, fail, failRate } }
13
+ *
14
+ * - custom 约束 → custom-constraints.yml 条目内 `retired` 元数据段
15
+ * (不拆 config.yml 第二处):
16
+ *
17
+ * custom_constraints:
18
+ * <id>:
19
+ * rule: ...
20
+ * retired: { at, reason, stats: { total, fail, failRate } }
11
21
  *
12
22
  * 每条同时写一条 KnowledgeStore 记录(consumptionMode: 'signal'),
13
23
  * 并同步 CLAUDE.md 注入段(存在 HARNESS_CONSTRAINTS 标记时)。
14
24
  *
15
- * retire 不是删除——恢复方法:删除 config.yml 中 constraints.<id> 段。
25
+ * retire 不是删除——恢复方法:内置删 config.yml 中 constraints.<id> 段;
26
+ * custom 删 custom-constraints.yml 中 custom_constraints.<id>.retired 段。
16
27
  *
17
28
  * 交互与执行分离:retireConstraint 为纯执行逻辑(同步、可测),
18
29
  * runRetireInteractive 只做 readline 交互,IO 流可注入。
@@ -36,7 +47,6 @@ import {
36
47
  readProjectTraces,
37
48
  type RetireCandidate,
38
49
  } from '../../core/constraints/usage-report';
39
- import { getConstraintsMeta } from './constraints';
40
50
 
41
51
  export interface RetireExecuteOptions {
42
52
  /** 退役原因(可空) */
@@ -52,6 +62,8 @@ export interface RetireResult {
52
62
  status: RetireStatus;
53
63
  /** 是否 check 层 iron(交互模式据此追加确认) */
54
64
  isIronLaw: boolean;
65
+ /** 退役落点:custom 约束落 custom-constraints.yml(#82 D6),内置落 config.yml */
66
+ landing: 'config.yml' | 'custom-constraints.yml';
55
67
  stats: { total: number; fail: number; failRate: number };
56
68
  /** CLAUDE.md 注入段是否已同步 */
57
69
  claudeMdSynced: boolean;
@@ -62,9 +74,12 @@ export interface RetireResult {
62
74
  export interface ConstraintsRetireOptions {
63
75
  projectPath?: string;
64
76
  reason?: string;
77
+ /** 直达模式显式确认(--yes):ADR-0001 决策 2 人确认闸门,无此 flag 直达拒绝执行 */
78
+ yes?: boolean;
65
79
  }
66
80
 
67
81
  interface RetireTargetInfo {
82
+ source: 'builtin' | 'custom';
68
83
  isIronLaw: boolean;
69
84
  level: Constraint['level'];
70
85
  description?: string;
@@ -80,6 +95,7 @@ function findRetireTarget(projectRoot: string, id: string): RetireTargetInfo | u
80
95
  const builtIn = getConstraint(id);
81
96
  if (builtIn) {
82
97
  return {
98
+ source: 'builtin',
83
99
  isIronLaw: builtIn.kind === 'check' && builtIn.level === 'iron_law',
84
100
  level: builtIn.level,
85
101
  description: builtIn.description,
@@ -95,11 +111,13 @@ function findRetireTarget(projectRoot: string, id: string): RetireTargetInfo | u
95
111
  if (custom) {
96
112
  const level = custom.level || 'guideline';
97
113
  return {
114
+ source: 'custom',
98
115
  isIronLaw: false, // custom 约束统一 kind='prompt'(无 checker),不存在 check 层 iron
99
116
  level,
100
117
  description: custom.description,
101
118
  rule: custom.rule,
102
119
  message: custom.message,
120
+ promptInjection: custom.promptInjection,
103
121
  };
104
122
  }
105
123
 
@@ -151,6 +169,41 @@ function writeRetireConfig(
151
169
  }
152
170
  }
153
171
 
172
+ /**
173
+ * 写 custom-constraints.yml 退役段(#82 D6 统一落点)
174
+ *
175
+ * 在 custom_constraints.<id> 条目内追加 retired 元数据,保留规则原文。
176
+ * js-yaml 不保留注释:原文件含注释行时重写会丢失,console 说明。
177
+ */
178
+ function writeCustomRetireYml(
179
+ projectRoot: string,
180
+ fileName: string,
181
+ id: string,
182
+ retiredMeta: { at: string; reason: string; stats: { total: number; fail: number; failRate: number } }
183
+ ): void {
184
+ const filePath = path.join(projectRoot, '.harness', fileName);
185
+
186
+ let raw: Record<string, unknown> = {};
187
+ let hadComments = false;
188
+ if (fs.existsSync(filePath)) {
189
+ const original = fs.readFileSync(filePath, 'utf-8');
190
+ hadComments = original.split('\n').some(l => l.trimStart().startsWith('#'));
191
+ raw = (yaml.load(original) as Record<string, unknown>) ?? {};
192
+ }
193
+
194
+ const customs = (raw.custom_constraints ?? {}) as Record<string, Record<string, unknown>>;
195
+ const prev = customs[id] ?? {};
196
+ customs[id] = { ...prev, retired: retiredMeta };
197
+ raw.custom_constraints = customs;
198
+
199
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
200
+ fs.writeFileSync(filePath, yaml.dump(raw, { lineWidth: 120 }), 'utf-8');
201
+
202
+ if (hadComments) {
203
+ console.log(chalk.yellow(' ⚠️ custom-constraints.yml 已重写:js-yaml 不保留原文件注释'));
204
+ }
205
+ }
206
+
154
207
  /**
155
208
  * 写 KnowledgeStore 退役记录(consumptionMode: 'signal')
156
209
  */
@@ -251,15 +304,24 @@ export function retireConstraint(
251
304
 
252
305
  const target = findRetireTarget(projectRoot, id);
253
306
  const emptyStats = { total: 0, fail: 0, failRate: 0 };
307
+ const landing: RetireResult['landing'] =
308
+ target?.source === 'custom' ? 'custom-constraints.yml' : 'config.yml';
254
309
  if (!target) {
255
- return { id, status: 'unknown_id', isIronLaw: false, stats: emptyStats, claudeMdSynced: false };
310
+ return { id, status: 'unknown_id', isIronLaw: false, landing, stats: emptyStats, claudeMdSynced: false };
256
311
  }
257
312
 
258
- // 已退役保护(config.yml constraints.<id>.enabled === false
313
+ // 已退役保护:内置看 config.yml constraints.<id>.enabled === false
314
+ // custom 看 yml 条目 retired 元数据(历史落点 config.yml enabled:false 同样保护)
259
315
  const rawConfig = loadRawProjectConfig(projectRoot);
260
316
  const existing = (rawConfig?.constraints as Record<string, { enabled?: boolean; retired?: unknown }> | undefined)?.[id];
261
- if (existing?.enabled === false) {
262
- return { id, status: 'already_retired', isIronLaw: target.isIronLaw, stats: emptyStats, claudeMdSynced: false };
317
+ if (target.source === 'custom') {
318
+ const loader = new ProjectConfigLoader(projectRoot);
319
+ loader.load();
320
+ if (loader.getCustomConstraints()[id]?.retired || existing?.enabled === false) {
321
+ return { id, status: 'already_retired', isIronLaw: target.isIronLaw, landing, stats: emptyStats, claudeMdSynced: false };
322
+ }
323
+ } else if (existing?.enabled === false) {
324
+ return { id, status: 'already_retired', isIronLaw: target.isIronLaw, landing, stats: emptyStats, claudeMdSynced: false };
263
325
  }
264
326
 
265
327
  // 历史统计(来自 traces.log)
@@ -271,8 +333,15 @@ export function retireConstraint(
271
333
  failRate: evaluated > 0 ? (usage!.fail / evaluated) : 0,
272
334
  };
273
335
 
274
- // 1. config.yml
275
- writeRetireConfig(projectRoot, id, { at: iso, reason, stats });
336
+ // 1. 落盘退役(custom → yml 条目 retired 段;内置 → config.yml,原文均保留)
337
+ if (target.source === 'custom') {
338
+ const loader = new ProjectConfigLoader(projectRoot);
339
+ loader.load();
340
+ const customFile = loader.getConfig().custom_constraints_file ?? 'custom-constraints.yml';
341
+ writeCustomRetireYml(projectRoot, customFile, id, { at: iso, reason, stats });
342
+ } else {
343
+ writeRetireConfig(projectRoot, id, { at: iso, reason, stats });
344
+ }
276
345
 
277
346
  // 2. KnowledgeStore
278
347
  const knowledgeEntryId = saveRetireKnowledge(projectRoot, id, target, reason, stats, iso);
@@ -280,7 +349,7 @@ export function retireConstraint(
280
349
  // 3. CLAUDE.md 注入段同步
281
350
  const claudeMdSynced = syncClaudeMdInjection(projectRoot);
282
351
 
283
- return { id, status: 'retired', isIronLaw: target.isIronLaw, stats, claudeMdSynced, knowledgeEntryId };
352
+ return { id, status: 'retired', isIronLaw: target.isIronLaw, landing, stats, claudeMdSynced, knowledgeEntryId };
284
353
  }
285
354
 
286
355
  /**
@@ -294,14 +363,20 @@ export function printRetireResult(result: RetireResult): void {
294
363
  case 'already_retired':
295
364
  console.log(chalk.yellow(`⚠️ ${result.id}: 已处于退役状态(config.yml 中 enabled: false),跳过`));
296
365
  return;
297
- case 'retired':
366
+ case 'retired': {
298
367
  console.log(chalk.green(`✅ ${result.id}: 已退役`));
299
368
  console.log(` 历史统计: total=${result.stats.total} fail=${result.stats.fail} fail率=${Math.round(result.stats.failRate * 100)}%`);
300
369
  console.log(` 知识沉淀: ${result.knowledgeEntryId}(.harness/knowledge)`);
301
370
  if (result.claudeMdSynced) {
302
371
  console.log(' 已同步 CLAUDE.md 注入段');
303
372
  }
304
- console.log(chalk.gray(` retire 不是删除——恢复方法:删除 config.yml 中 constraints.${result.id} 段`));
373
+ const recovery =
374
+ result.landing === 'custom-constraints.yml'
375
+ ? `删除 custom-constraints.yml 中 custom_constraints.${result.id}.retired 段`
376
+ : `删除 config.yml 中 constraints.${result.id} 段`;
377
+ console.log(chalk.gray(` retire 不是删除——恢复方法:${recovery}`));
378
+ break;
379
+ }
305
380
  }
306
381
  }
307
382
 
@@ -376,7 +451,7 @@ export async function runRetireInteractive(
376
451
  }
377
452
 
378
453
  // 逐条收集 reason + iron 二次确认
379
- const plan: { id: string; reason: string }[] = [];
454
+ const plan: { id: string; reason: string; source: RetireTargetInfo['source'] }[] = [];
380
455
  for (const id of selectedIds) {
381
456
  const target = findRetireTarget(projectRoot, id);
382
457
  if (!target) {
@@ -391,7 +466,7 @@ export async function runRetireInteractive(
391
466
  }
392
467
  }
393
468
  const reason = await ask(`退役原因(${id},可留空): `);
394
- plan.push({ id, reason });
469
+ plan.push({ id, reason, source: target.source });
395
470
  }
396
471
 
397
472
  if (plan.length === 0) {
@@ -403,7 +478,11 @@ export async function runRetireInteractive(
403
478
  console.log();
404
479
  console.log(chalk.bold('将执行以下变更:'));
405
480
  for (const p of plan) {
406
- console.log(` - config.yml: constraints.${p.id}.enabled=false + retired 元数据(原因: ${p.reason || '(空)'})`);
481
+ if (p.source === 'custom') {
482
+ console.log(` - custom-constraints.yml: custom_constraints.${p.id}.retired 元数据(原因: ${p.reason || '(空)'})`);
483
+ } else {
484
+ console.log(` - config.yml: constraints.${p.id}.enabled=false + retired 元数据(原因: ${p.reason || '(空)'})`);
485
+ }
407
486
  console.log(` - KnowledgeStore: 写入 constraint-retired-${p.id}`);
408
487
  }
409
488
  console.log(' - CLAUDE.md: 若含 HARNESS_CONSTRAINTS 标记段则同步重渲染');
@@ -425,15 +504,29 @@ export async function runRetireInteractive(
425
504
 
426
505
  /**
427
506
  * CLI handler: harness constraints retire [id]
507
+ *
508
+ * 人确认闸门(#24,ADR-0001 决策 2):带 id 的直达路径必须显式 `--yes`,
509
+ * 无 `--yes` 报错 + 非零退出码,提示改用 `--yes` 或交互模式;不落盘任何文件。
428
510
  */
429
511
  export async function constraintsRetire(id?: string, options: ConstraintsRetireOptions = {}): Promise<void> {
430
512
  const projectRoot = options.projectPath || process.cwd();
431
513
 
432
514
  if (id) {
433
- // 非交互直达
515
+ // 非交互直达:执行层人确认对所有入口成立(含程序化调用方)
516
+ if (!options.yes) {
517
+ console.error(
518
+ chalk.red(`❌ 直达退役需要显式人确认(ADR-0001 决策 2:执行层保留一次人确认),未做任何变更\n`) +
519
+ ` 带 --yes 显式确认直达:harness constraints retire ${id} --yes` +
520
+ `${options.reason ? ` --reason "${options.reason}"` : ''}\n` +
521
+ ` 或去掉 id 走交互确认:harness constraints retire`
522
+ );
523
+ process.exitCode = 1;
524
+ return;
525
+ }
526
+
434
527
  const result = retireConstraint(projectRoot, id, { reason: options.reason });
435
528
  if (result.status === 'retired' && result.isIronLaw) {
436
- console.log(chalk.yellow(`⚠️ ${id} 是一条 Iron Law,已通过命令行直接退役(交互模式会要求二次确认)`));
529
+ console.log(chalk.yellow(`⚠️ ${id} 是一条 Iron Law,已通过 --yes 直达退役(交互模式会要求二次确认)`));
437
530
  }
438
531
  printRetireResult(result);
439
532
  return;
@@ -358,7 +358,7 @@ export async function docFreshnessCheck(
358
358
  const content = fs.readFileSync(resolvedPath, 'utf-8');
359
359
 
360
360
  // 提取声明
361
- let claims = extractClaims(content, resolvedPath);
361
+ const claims = extractClaims(content, resolvedPath);
362
362
 
363
363
  // 如果指定了变更文件,只验证与变更文件相关的声明
364
364
  if (options.changedFiles) {
@@ -256,7 +256,7 @@ function printSnippets(): void {
256
256
  console.log();
257
257
  console.log(chalk.cyan(GITHUB_ACTIONS_SNIPPET));
258
258
 
259
- console.log(chalk.blue('💡 提示: 运行 harness init 自动创建配置文件'));
259
+ console.log(chalk.blue('💡 提示: 运行 npx @dommaker/harness init 自动创建配置文件'));
260
260
  }
261
261
 
262
262
  /**
@@ -264,7 +264,7 @@ function printSnippets(): void {
264
264
  */
265
265
  const PRE_COMMIT_SNIPPET = `
266
266
  # Harness 约束检查
267
- npx harness check --staged
267
+ npx @dommaker/harness check --staged
268
268
  if [ $? -ne 0 ]; then
269
269
  echo "❌ Iron law check failed"
270
270
  exit 1
@@ -273,11 +273,11 @@ fi
273
273
  # Plan coverage check (via PostEval)
274
274
  STAGED=$(git diff --cached --name-only --diff-filter=ACMR 2>/dev/null || true)
275
275
  if command -v npx > /dev/null 2>&1; then
276
- PLAN_FILES=$(echo "$STAGED" | grep -E 'plans/.*\.md$|\.plan\.md$' || true)
276
+ PLAN_FILES=$(echo "$STAGED" | grep -E 'plans/.*\\.md$|\\.plan\\.md$' || true)
277
277
  if [ -n "$PLAN_FILES" ]; then
278
278
  echo "📋 Checking plan coverage..."
279
279
  for plan in $PLAN_FILES; do
280
- npx harness posteval-plan "$plan" || {
280
+ npx @dommaker/harness posteval-plan "$plan" || {
281
281
  echo "🛑 Plan coverage incomplete. See above for missed items."
282
282
  exit 1
283
283
  }
@@ -298,7 +298,7 @@ const GITHUB_ACTIONS_SNIPPET = `
298
298
  with:
299
299
  node-version: '20'
300
300
  - run: npm ci
301
- - run: npx harness check
301
+ - run: npx @dommaker/harness check
302
302
  `;
303
303
 
304
304
  /**
@@ -313,7 +313,7 @@ async function setupGitHooks(projectPath: string): Promise<void> {
313
313
  await fs.access(gitDir);
314
314
  } catch {
315
315
  console.log(chalk.yellow('⚠️ 未检测到 Git 仓库,跳过 Git hooks'));
316
- console.log(chalk.gray('💡 初始化 Git 后可运行 harness init --print-snippets 查看配置'));
316
+ console.log(chalk.gray('💡 初始化 Git 后可运行 npx @dommaker/harness init --print-snippets 查看配置'));
317
317
  return;
318
318
  }
319
319
 
@@ -337,7 +337,7 @@ echo "🔍 Running harness checks..."
337
337
  STAGED=$(git diff --cached --name-only --diff-filter=ACMR 2>/dev/null || true)
338
338
 
339
339
  # 铁律检查
340
- npx harness check --staged
340
+ npx @dommaker/harness check --staged
341
341
  if [ $? -ne 0 ]; then
342
342
  echo "❌ Iron law check failed"
343
343
  exit 1
@@ -349,7 +349,7 @@ if command -v npx > /dev/null 2>&1; then
349
349
  if [ -n "$PLAN_FILES" ]; then
350
350
  echo "📋 Checking plan coverage..."
351
351
  for plan in $PLAN_FILES; do
352
- npx harness posteval-plan "$plan" || {
352
+ npx @dommaker/harness posteval-plan "$plan" || {
353
353
  echo "🛑 Plan coverage incomplete. See above for missed items."
354
354
  exit 1
355
355
  }
@@ -413,13 +413,13 @@ jobs:
413
413
  run: npm ci
414
414
 
415
415
  - name: Run harness check
416
- run: npx harness check
416
+ run: npx @dommaker/harness check
417
417
 
418
418
  - name: Run harness validate
419
- run: npx harness validate
419
+ run: npx @dommaker/harness validate
420
420
 
421
421
  - name: Run harness passes-gate
422
- run: npx harness passes-gate
422
+ run: npx @dommaker/harness passes-gate
423
423
  `;
424
424
 
425
425
  await fs.writeFile(workflowPath, workflowContent, 'utf-8');
@@ -820,7 +820,7 @@ async function setupGovernanceWorkflow(projectPath: string, level: string): Prom
820
820
  const docsCheckStep = level !== 'minimal'
821
821
  ? `
822
822
  - name: Check docs freshness
823
- run: npx harness sync-docs --check
823
+ run: npx @dommaker/harness sync-docs --check
824
824
  continue-on-error: true`
825
825
  : '';
826
826
 
@@ -848,10 +848,10 @@ jobs:
848
848
  run: npm ci
849
849
 
850
850
  - name: Constraint check
851
- run: npx harness check
851
+ run: npx @dommaker/harness check
852
852
 
853
853
  - name: Quality gate
854
- run: npx harness passes-gate
854
+ run: npx @dommaker/harness passes-gate
855
855
  ${docsCheckStep}
856
856
  `;
857
857
 
@@ -93,7 +93,7 @@ export async function syncDocs(options: SyncDocsOptions): Promise<boolean> {
93
93
 
94
94
  // 1. 扫描源码模块(从 governance config 读取目录列表,默认 src/)
95
95
  const srcDirs = await getSourceDirs(projectPath);
96
- let currentModules: ModuleInfo[] = [];
96
+ const currentModules: ModuleInfo[] = [];
97
97
  for (const srcDir of srcDirs) {
98
98
  try {
99
99
  const modules = await scanSourceModules(path.join(projectPath, srcDir), projectPath);
@@ -166,17 +166,30 @@ export async function syncDocs(options: SyncDocsOptions): Promise<boolean> {
166
166
  )
167
167
  .map(m => getBasename(m.file));
168
168
  }
169
- result.removed = existingFileNames.filter(f => !currentBasenames.includes(f));
169
+ // removed:basename 对比 + 全路径存在性兜底(#33)。
170
+ // 表格格式承诺 ts|tsx|js|jsx,但扫描只覆盖 .ts/.tsx——
171
+ // 指向真实存在文件的登记行(.js/.jsx 等扫描盲区)不得误判为「已删除」。
172
+ // 兜底按 basename 豁免;路径不存在的幽灵行进不了豁免,由下方清扫按完整路径补入 removed。
173
+ const sourceRoots = detectSourceRoots(projectPath);
174
+ const entryExists = (entry: string): boolean =>
175
+ existsSync(path.join(projectPath, entry)) ||
176
+ sourceRoots.some(root => existsSync(path.join(projectPath, root, entry)));
177
+
178
+ const registeredEntries = readCapabilitiesEntries(capabilitiesPath);
179
+ const livePathBasenames = new Set(
180
+ registeredEntries
181
+ .filter(e => !e.endsWith('/') && e.includes('/') && entryExists(e))
182
+ .map(getBasename)
183
+ );
184
+ result.removed = existingFileNames.filter(
185
+ f => !currentBasenames.includes(f) && !livePathBasenames.has(f)
186
+ );
170
187
 
171
188
  // 幽灵条目清扫(2026-08-08 studio CI 4 连红事故):上方按 basename 对比,
172
189
  // 同名碰撞时幽灵不可见(如 agent-configs/routes.ts 已删但 agents/routes.ts 仍存在,
173
190
  // basename routes.ts 仍在扫描结果中,永远不会被判 removed)。
174
191
  // 这里按完整路径直接判存在性(与 docs_freshness 检查器同语义:项目根 + 源码根前缀)。
175
- const sourceRoots = detectSourceRoots(projectPath);
176
- const entryExists = (entry: string): boolean =>
177
- existsSync(path.join(projectPath, entry)) ||
178
- sourceRoots.some(root => existsSync(path.join(projectPath, root, entry)));
179
- for (const entry of readCapabilitiesEntries(capabilitiesPath)) {
192
+ for (const entry of registeredEntries) {
180
193
  // 纯文件名条目(无路径)无法用存在性判定,交由上方 basename 对比
181
194
  if (!entry.includes('/') || entryExists(entry)) continue;
182
195
  const basename = entry.split('/').pop()!;
@@ -7,7 +7,7 @@ import { existsSync } from 'fs';
7
7
  import * as path from 'path';
8
8
  import { loadRawProjectConfig } from '../../../core/project-config-loader';
9
9
  import { detectSourceRoots } from '../../../utils/detect-source-roots';
10
- import { findTsSourceFiles } from '../../../utils/file-walk';
10
+ import { findTsSourceFiles, isTsSourceFile } from '../../../utils/file-walk';
11
11
 
12
12
  export interface ModuleInfo {
13
13
  name: string;
@@ -115,18 +115,20 @@ export async function scanSourceModules(srcDir: string, projectPath: string): Pr
115
115
  const stat = await fs.stat(entryPath);
116
116
 
117
117
  if (stat.isDirectory()) {
118
- // 子目录:递归扫描 .ts 文件(不报告目录条目本身)
119
- const subFiles = findTsSourceFiles(entryPath, { skipIndex: true });
118
+ // 子目录:递归扫描 .ts/.tsx 文件(不报告目录条目本身)
119
+ // includeTsx 开启后,历史项目未登记过的 .tsx 会首次批量进入 added(#33 已知连带,
120
+ // 与 .ts 同口径属预期,一次性补登记即可)
121
+ const subFiles = findTsSourceFiles(entryPath, { skipIndex: true, includeTsx: true });
120
122
  for (const f of subFiles) {
121
123
  modules.push({
122
- name: path.basename(f, '.ts'),
124
+ name: path.basename(f, path.extname(f)),
123
125
  file: path.relative(projectPath, f),
124
126
  description: await extractFileDescription(f),
125
127
  });
126
128
  }
127
- } else if (entry.endsWith('.ts') && !entry.endsWith('.d.ts') && entry !== 'index.ts') {
129
+ } else if (isTsSourceFile(entry, { skipIndex: true, includeTsx: true })) {
128
130
  modules.push({
129
- name: path.basename(entry, '.ts'),
131
+ name: path.basename(entry, path.extname(entry)),
130
132
  file: path.relative(projectPath, entryPath),
131
133
  description: await extractFileDescription(entryPath),
132
134
  });
@@ -142,9 +144,9 @@ export async function scanSourceModules(srcDir: string, projectPath: string): Pr
142
144
  async function extractFileDescription(filePath: string): Promise<string> {
143
145
  try {
144
146
  const content = await fs.readFile(filePath, 'utf-8');
145
- return extractFirstComment(content) || path.basename(filePath, '.ts');
147
+ return extractFirstComment(content) || path.basename(filePath, path.extname(filePath));
146
148
  } catch {
147
- return path.basename(filePath, '.ts');
149
+ return path.basename(filePath, path.extname(filePath));
148
150
  }
149
151
  }
150
152
 
@@ -165,15 +165,15 @@ export async function createExampleCheckpoint(projectPath: string): Promise<void
165
165
  const DEFAULT_RESOLUTIONS = {
166
166
  no_fuzzy_completion_claim: {
167
167
  title: 'commit message 缺少验证证据',
168
- fix: '在 commit message body 中附上验证输出:\n`npx harness check --staged` | `npx harness validate` | `npm test -- --coverage`\n确认全部通过后重新 commit。',
168
+ fix: '在 commit message body 中附上验证输出:\n`npx @dommaker/harness check --staged` | `npx @dommaker/harness validate` | `npm test -- --coverage`\n确认全部通过后重新 commit。',
169
169
  },
170
170
  capability_sync: {
171
171
  title: '缺少 CAPABILITIES.md',
172
- fix: '在项目根目录创建 CAPABILITIES.md,列出所有模块能力清单。运行 `npx harness sync-docs` 可自动生成模板。',
172
+ fix: '在项目根目录创建 CAPABILITIES.md,列出所有模块能力清单。运行 `npx @dommaker/harness sync-docs` 可自动生成模板。',
173
173
  },
174
174
  context_doc_sync: {
175
175
  title: '关键目录缺少 CONTEXT.md',
176
- fix: '在 required_dirs 目录下创建 CONTEXT.md。运行 `npx harness sync-docs` 可自动生成模板。',
176
+ fix: '在 required_dirs 目录下创建 CONTEXT.md。运行 `npx @dommaker/harness sync-docs` 可自动生成模板。',
177
177
  },
178
178
  };
179
179
 
@@ -99,7 +99,7 @@ export function stripCodeBlocks(text: string): string {
99
99
 
100
100
  /** 是否纯标点/空白 */
101
101
  export function isPunctuation(str: string): boolean {
102
- return /^[,。!?、;:""''()\[\]【】「」『』《》〈〉\s]+$/.test(str);
102
+ return /^[,。!?、;:""''()[\]【】「」『』《》〈〉\s]+$/.test(str);
103
103
  }
104
104
 
105
105
  /** 是否含语义内容(汉字,或 ≥7 字符的合法英文标识词) */
@@ -18,7 +18,7 @@ describe('ProgressiveLoader', () => {
18
18
 
19
19
  await loader.loadInChunks(items, {
20
20
  chunkSize: 20,
21
- onChunk: (chunk, index) => { chunks.push(chunk); },
21
+ onChunk: (chunk) => { chunks.push(chunk); },
22
22
  });
23
23
 
24
24
  expect(chunks.length).toBe(5);
@@ -45,7 +45,7 @@ describe('ProgressiveLoader', () => {
45
45
  await loader.loadInChunks(items, {
46
46
  chunkSize: 5,
47
47
  parallel: true,
48
- onChunk: (chunk, index) => { chunks.push(chunk); },
48
+ onChunk: (chunk) => { chunks.push(chunk); },
49
49
  });
50
50
 
51
51
  expect(chunks.length).toBe(4);
@@ -78,7 +78,7 @@ describe('ProgressiveLoader', () => {
78
78
 
79
79
  await loader.loadInChunks(items, {
80
80
  chunkSize: 10,
81
- onChunk: (chunk, index) => { chunks.push(chunk); },
81
+ onChunk: (chunk) => { chunks.push(chunk); },
82
82
  });
83
83
 
84
84
  expect(chunks.length).toBe(3);
@@ -171,7 +171,7 @@ describe('ProgressiveLoader', () => {
171
171
  it('should handle empty items', async () => {
172
172
  const result = await loader.loadWithBudget([], {
173
173
  budget: 100,
174
- estimator: (item) => 0,
174
+ estimator: () => 0,
175
175
  });
176
176
 
177
177
  expect(result.items.length).toBe(0);
@@ -241,7 +241,7 @@ describe('ProgressiveLoader', () => {
241
241
  return n * 2;
242
242
  };
243
243
 
244
- const results = await loader.processBatch(items, processor, 1);
244
+ await loader.processBatch(items, processor, 1);
245
245
 
246
246
  // Check that all items were processed
247
247
  expect(processed.sort()).toEqual([1, 2, 3]);
@@ -256,7 +256,7 @@ describe('progressiveLoader (default instance)', () => {
256
256
 
257
257
  await progressiveLoader.loadInChunks(items, {
258
258
  chunkSize: 1,
259
- onChunk: (chunk, index) => { chunks.push(chunk); return undefined; },
259
+ onChunk: (chunk) => { chunks.push(chunk); return undefined; },
260
260
  });
261
261
 
262
262
  expect(chunks.length).toBe(3);
@@ -5,7 +5,7 @@
5
5
 
6
6
  ## 核心导出
7
7
  - `constraints/` — 约束定义(IRON_LAWS/GUIDELINES/PROMPTS;TIPS 已退役为空表) + 检查引擎(ConstraintChecker) + 拦截器(ConstraintInterceptor) + 缓存(CheckCache) + 注入渲染(injection-renderer)/漂移校验(injection-drift)/使用统计(usage-report)
8
- - `effective-constraints.ts` — `getEffectiveConstraints(projectRoot)`:全仓唯一生效集来源(内置 → preset → config.yml 禁用 custom 追加 scenes 过滤);`lintEffectiveConfig` 配置诊断
8
+ - `effective-constraints.ts` — `getEffectiveConstraints(projectRoot)`:全仓唯一生效集来源(内置 → preset → config.yml 禁用(内置与 custom 同效)→ custom 追加(禁用/已退役的不追加)→ scenes 过滤);`lintEffectiveConfig` 配置诊断
9
9
  - `validators/` — checkpoints、passes-gate、CSO 验证器
10
10
  - `session/` — 会话管理
11
11
  - `spec/validator` — SpecValidator
@@ -45,7 +45,7 @@ describe('detectInjectionDrift', () => {
45
45
  expect(drift.versionDrift).toBeUndefined();
46
46
  expect(drift.contentDrift).toBeUndefined();
47
47
  expect(drift.duplicateHeading).toBe(false);
48
- expect(drift.fixHint).toContain('npx harness init');
48
+ expect(drift.fixHint).toContain('npx @dommaker/harness init');
49
49
  expect(drift.fixHint).toBe(INJECTION_DRIFT_FIX_HINT);
50
50
  });
51
51
 
@@ -2,10 +2,10 @@
2
2
  * ConstraintInterceptor 测试
3
3
  */
4
4
 
5
- import { ConstraintInterceptor, constraintInterceptor } from '../interceptor';
5
+ import { ConstraintInterceptor } from '../interceptor';
6
6
  import { constraintChecker } from '../checker';
7
- import type { Constraint, ConstraintContext, ConstraintTrigger } from '../../../types/constraint';
8
- import type { EnforcementExecutor, EnforcementResult } from '../../../types/enforcement';
7
+ import type { ConstraintContext } from '../../../types/constraint';
8
+ import type { EnforcementExecutor } from '../../../types/enforcement';
9
9
 
10
10
  // Mock constraintChecker
11
11
  jest.mock('../checker', () => ({
@@ -22,8 +22,8 @@ export const IRON_LAWS: Record<string, Constraint> = {
22
22
  level: 'iron_law',
23
23
  trigger: 'code_implementation',
24
24
  enforcement: 'verify-completion',
25
- description: '在声明任何任务完成之前,必须运行新鲜的、完整的验证命令。验证命令包括:npm test、npm run build、CI 流程。',
26
- promptInjection: '在声明任务完成前,必须重新运行完整的验证命令(npm test、npm run build、type check),使用新鲜的输出作为完成证据,不得复用旧结果。',
25
+ description: '在声明任何任务完成之前,必须运行新鲜的验证命令(项目声明的测试 + type check),以新鲜输出作为完成证据。验证命令集由项目自行声明,全量验证留给 CI 流程。',
26
+ promptInjection: '在声明任务完成前,必须重新运行新鲜的验证命令(项目声明的测试 + type check),使用新鲜的输出作为完成证据,不得复用旧结果,不得凭记忆声称完成。',
27
27
  },
28
28
 
29
29
  /**
@@ -42,7 +42,7 @@ export interface InjectionDrift {
42
42
  }
43
43
 
44
44
  /** 修复指引(重跑 init 幂等同步注入段) */
45
- export const INJECTION_DRIFT_FIX_HINT = '重跑 `npx harness init` 同步注入段(漂移仅警告,不阻断)';
45
+ export const INJECTION_DRIFT_FIX_HINT = '重跑 `npx @dommaker/harness init` 同步注入段(漂移仅警告,不阻断)';
46
46
 
47
47
  const VERSION_LINE_RE = /<!-- version: ([^ ]+) -->/;
48
48
  const ENTRY_LINE_RE = /^- \*\*.+?\*\*: .+$/;
@@ -15,7 +15,8 @@ import { ProjectConfigLoader } from './project-config-loader';
15
15
  * 获取项目当前生效的约束集(check + prompt,带 kind)
16
16
  *
17
17
  * 内部完成完整合并:内置 → preset 裁剪 → config.yml `constraints.<id>.enabled:false`
18
- * 删除 custom-constraints 追加/extend_exceptions → scenes 过滤
18
+ * 删除(内置与 custom 同效)→ custom-constraints 追加/extend_exceptions
19
+ * (禁用/已退役的 custom 不追加)→ scenes 过滤
19
20
  * (带 appliesTo 的 prompt 仅当 config.yml `scenes` 与其交集非空时保留,
20
21
  * 缺省 scenes=[] 即场景专属 prompt 默认不进入生效集)。
21
22
  *
@@ -156,7 +156,8 @@ export class ProjectConfigLoader {
156
156
  * 合并内置约束和自定义约束(ADR-0001 生效集完整链路)
157
157
  *
158
158
  * 合并顺序:内置 → preset 裁剪 → config.yml `constraints.<id>.enabled:false`
159
- * 删除 custom-constraints 追加/extend_exceptions → scenes 过滤
159
+ * 删除(对内置与 custom 同效)→ custom-constraints 追加/extend_exceptions
160
+ * (禁用/已退役的 custom 不追加)→ scenes 过滤
160
161
  * (带 appliesTo 的 prompt 仅当 scenes 交集非空时保留)。
161
162
  *
162
163
  * config.yml 中未知约束 id(如已移除约束的禁用残留)静默忽略,
@@ -201,8 +202,13 @@ export class ProjectConfigLoader {
201
202
  }
202
203
  }
203
204
 
204
- // 2. 添加自定义约束
205
+ // 2. 添加自定义约束(config.yml 禁用的 id 不追加:step 1 已将其
206
+ // 收集进 disabled,custom 在 step 1 时尚未入桶,须在此兜底跳过;
207
+ // 条目带 retired 元数据的同样不追加——#82 D6 退役落点在条目自身)
205
208
  for (const [id, customDef] of Object.entries(this.customConstraints)) {
209
+ if (result.disabled.includes(id) || customDef.retired) {
210
+ continue;
211
+ }
206
212
  const extendExceptions = customDef.extend_exceptions;
207
213
  const isExtendOnly =
208
214
  !!extendExceptions && extendExceptions.length > 0 &&