@dommaker/harness 0.17.0 → 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 (117) hide show
  1. package/CHANGELOG.md +27 -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 +9 -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 +23 -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__/project-config-loader-extra.test.ts +37 -0
  70. package/src/__tests__/traces.test.ts +1 -2
  71. package/src/architecture/cross-project-checker.ts +2 -2
  72. package/src/cli/commands/CONTEXT.md +1 -1
  73. package/src/cli/commands/__tests__/check-drift.test.ts +1 -1
  74. package/src/cli/commands/__tests__/check-skip-output.test.ts +0 -2
  75. package/src/cli/commands/__tests__/command.test.ts +1 -1
  76. package/src/cli/commands/__tests__/constraints-report.test.ts +1 -1
  77. package/src/cli/commands/__tests__/constraints-retire.test.ts +165 -4
  78. package/src/cli/commands/__tests__/failure.test.ts +0 -1
  79. package/src/cli/commands/__tests__/init.test.ts +40 -0
  80. package/src/cli/commands/__tests__/status-extra.test.ts +0 -2
  81. package/src/cli/commands/__tests__/status.test.ts +0 -2
  82. package/src/cli/commands/__tests__/sync-docs.test.ts +90 -0
  83. package/src/cli/commands/constraints-report.ts +2 -2
  84. package/src/cli/commands/constraints-retire.ts +114 -21
  85. package/src/cli/commands/doc-freshness-check.ts +1 -1
  86. package/src/cli/commands/init.ts +14 -14
  87. package/src/cli/commands/sync-docs/index.ts +20 -7
  88. package/src/cli/commands/sync-docs/project-reader.ts +10 -8
  89. package/src/cli/commands/validate.ts +3 -3
  90. package/src/cli/session-mining/text.ts +1 -1
  91. package/src/context/__tests__/progressive-loader.test.ts +6 -6
  92. package/src/core/CONTEXT.md +1 -1
  93. package/src/core/constraints/__tests__/injection-drift.test.ts +1 -1
  94. package/src/core/constraints/__tests__/interceptor.test.ts +3 -3
  95. package/src/core/constraints/definitions/iron-laws.ts +2 -2
  96. package/src/core/constraints/injection-drift.ts +1 -1
  97. package/src/core/effective-constraints.ts +2 -1
  98. package/src/core/project-config-loader.ts +9 -2
  99. package/src/gates/__tests__/acceptance.test.ts +0 -1
  100. package/src/gates/__tests__/command.test.ts +1 -1
  101. package/src/hooks/bootstrap.ts +1 -1
  102. package/src/knowledge/__tests__/doctor.test.ts +2 -2
  103. package/src/knowledge/__tests__/ingest.test.ts +2 -2
  104. package/src/knowledge/__tests__/migration.test.ts +0 -6
  105. package/src/knowledge/__tests__/store.test.ts +14 -1
  106. package/src/knowledge/store.ts +1 -1
  107. package/src/knowledge/types.ts +2 -0
  108. package/src/llm/__tests__/adapter.test.ts +1 -1
  109. package/src/monitoring/__tests__/knowledge-doctor.test.ts +0 -1
  110. package/src/monitoring/__tests__/performance-collector.test.ts +1 -2
  111. package/src/safety/output-guardrail.ts +2 -2
  112. package/src/spec/__tests__/annotation-checker.test.ts +0 -1
  113. package/src/spec/annotation-checker.ts +1 -1
  114. package/src/types/project-config.ts +21 -0
  115. package/src/utils/__tests__/file-walk.test.ts +53 -0
  116. package/src/utils/file-walk.ts +23 -3
  117. package/src/verification/__tests__/loop.test.ts +1 -1
@@ -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 &&
@@ -300,6 +306,7 @@ export class ProjectConfigLoader {
300
306
  trigger: (def.trigger || 'manual') as ConstraintTrigger | ConstraintTrigger[],
301
307
  exceptions: def.exceptions,
302
308
  description: def.description,
309
+ promptInjection: def.promptInjection,
303
310
  enabled: def.enabled !== false,
304
311
  enforcement: 'custom',
305
312
  };
@@ -4,7 +4,6 @@
4
4
 
5
5
  import { SpecAcceptanceGate, createSpecAcceptanceGate } from '../acceptance';
6
6
  import * as fs from 'fs/promises';
7
- import * as path from 'path';
8
7
  import { exec } from 'child_process';
9
8
 
10
9
  // Mock dependencies
@@ -2,7 +2,7 @@
2
2
  * CommandGate 测试
3
3
  */
4
4
 
5
- import { CommandGate, createCommandGate, isCommandAllowed, getCommandRiskLevel, DEFAULT_COMMAND_BLACKLIST } from '../command';
5
+ import { CommandGate, isCommandAllowed, getCommandRiskLevel, DEFAULT_COMMAND_BLACKLIST } from '../command';
6
6
 
7
7
  describe('CommandGate', () => {
8
8
  describe('check', () => {
@@ -70,7 +70,7 @@ export async function bootstrapHarness(
70
70
  const resolvedPath = projectPath || process.cwd();
71
71
 
72
72
  // 1. 异步加载配置(S9 fix)
73
- const { config: _config, mergedConstraints } = await loadConfigAsync(resolvedPath);
73
+ const { mergedConstraints } = await loadConfigAsync(resolvedPath);
74
74
 
75
75
  // 2. 初始化核心组件
76
76
  const checker = ConstraintChecker.getInstance();
@@ -5,10 +5,10 @@
5
5
  import * as fs from 'fs';
6
6
  import * as path from 'path';
7
7
  import { FileKnowledgeStore as KnowledgeStore } from '../store';
8
- import { KnowledgeLinter, type LintReport } from '../lint';
8
+ import { KnowledgeLinter } from '../lint';
9
9
  import { ReferenceTracker } from '../reference-tracker';
10
10
  import { KnowledgeHealthScorer } from '../doctor';
11
- import type { KnowledgeEntry, IndexEntry } from '../types';
11
+ import type { KnowledgeEntry } from '../types';
12
12
 
13
13
  describe('KnowledgeHealthScorer', () => {
14
14
  const tempDir = path.join(process.cwd(), 'temp-test-knowledge-doctor');
@@ -245,7 +245,7 @@ describe('KnowledgeIngest', () => {
245
245
  });
246
246
 
247
247
  // After stripping [prefix], normalizing: "api超时导致请求失败" contains "api超时导致"
248
- const merged = ingest.ingestEntry(
248
+ ingest.ingestEntry(
249
249
  { title: '[Triage Fix] API 超时导致', content: 'different content but same issue', type: 'pitfall' },
250
250
  { source: 'test', layer: 'system' },
251
251
  );
@@ -287,7 +287,7 @@ describe('KnowledgeIngest', () => {
287
287
 
288
288
  // Keywords: prisma, 连接, 超时, 导致, api, 请求, 失败
289
289
  // Incoming: prisma, 超时, 请求, 失败 → 4/7 overlap
290
- const merged = ingest.ingestEntry(
290
+ ingest.ingestEntry(
291
291
  { title: 'Prisma 超时导致请求失败', content: 'totally different content', type: 'pitfall' },
292
292
  { source: 'test', layer: 'system' },
293
293
  );
@@ -37,12 +37,6 @@ describe('migrateKnowledgeEntries', () => {
37
37
  fs.writeFileSync(path.join(tempDir, filename), raw, 'utf-8');
38
38
  }
39
39
 
40
- function readMeta(filename: string): Record<string, unknown> {
41
- const raw = fs.readFileSync(path.join(tempDir, filename), 'utf-8');
42
- const match = raw.match(/^---\n([\s\S]*?)\n---/)!;
43
- return JSON.parse(JSON.stringify(eval(`(${match[1].replace(/(\w+):/g, '"$1":').replace(/'/g, '"')})`)));
44
- }
45
-
46
40
  it('should add consumptionMode and origin to entries missing them', () => {
47
41
  writeEntry('DEC-001.md', {
48
42
  id: 'DEC-001', type: 'decision', title: 'Test', maturity: 'draft',
@@ -184,6 +184,19 @@ describe('KnowledgeStore', () => {
184
184
  });
185
185
 
186
186
  describe('edge cases', () => {
187
+ it('should round-trip sourceReferences with entryId through save/load', () => {
188
+ const sourceRefs = [
189
+ { workflow: 'test-flow', step: 'distill', commit: 'abc1234', timestamp: '2026-05-01', entryId: 'SRC-001' },
190
+ { workflow: 'test-flow', timestamp: '2026-05-02' },
191
+ ];
192
+ store.save(makeEntry({ id: 'DEC-001', sourceReferences: sourceRefs }));
193
+ const loaded = store.get('DEC-001');
194
+ expect(loaded?.sourceReferences).toStrictEqual(sourceRefs);
195
+ // 缺省 entryId 的 ref 不落 undefined 键:原始 frontmatter 中 entryId 只出现一次
196
+ const raw = fs.readFileSync(path.join(tempDir, 'decision-DEC-001.md'), 'utf-8');
197
+ expect(raw.match(/entryId/g)).toHaveLength(1);
198
+ });
199
+
187
200
  it('should round-trip consumptionMode and origin through save/load', () => {
188
201
  store.save(makeEntry({
189
202
  id: 'DEC-001',
@@ -212,7 +225,7 @@ describe('KnowledgeStore', () => {
212
225
 
213
226
  it('should create directory if it does not exist', () => {
214
227
  const newDir = path.join(process.cwd(), 'temp-test-knowledge-nested', 'sub', 'deep');
215
- const s = new KnowledgeStore({ baseDir: newDir });
228
+ new KnowledgeStore({ baseDir: newDir });
216
229
  expect(fs.existsSync(newDir)).toBe(true);
217
230
  fs.rmSync(path.join(process.cwd(), 'temp-test-knowledge-nested'), { recursive: true, force: true });
218
231
  });
@@ -68,7 +68,7 @@ export class FileKnowledgeStore implements KnowledgeStore {
68
68
  list(filter?: QueryFilter): KnowledgeEntry[] {
69
69
  const index = this.readIndex();
70
70
  const effectiveFilter: QueryFilter = { excludeArchived: true, ...filter };
71
- let entries = index.filter(e => this.matchesFilter(e, effectiveFilter));
71
+ const entries = index.filter(e => this.matchesFilter(e, effectiveFilter));
72
72
 
73
73
  return entries.map(idx => {
74
74
  const full = this.get(idx.id);
@@ -58,6 +58,8 @@ export interface SourceRef {
58
58
  step?: string;
59
59
  commit?: string;
60
60
  timestamp: string;
61
+ /** ID of the source material entry this knowledge was distilled/evolved from (entry-level back-reference) */
62
+ entryId?: string;
61
63
  }
62
64
 
63
65
  export interface ExecutionResult {
@@ -53,7 +53,7 @@ describe('LLM Adapter', () => {
53
53
  it('应该抛出未实现错误', async () => {
54
54
  const gen = adapter.streamChat([{ role: 'user', content: 'test' }]);
55
55
  await expect(async () => {
56
- for await (const _ of gen) { /* noop */ }
56
+ for await (const _ of gen) { void _; }
57
57
  }).rejects.toThrow('LLM streamChat 未实现');
58
58
  });
59
59
  });
@@ -226,7 +226,6 @@ describe('KnowledgeDoctor', () => {
226
226
 
227
227
  it('critical 严重度扣 10 分', () => {
228
228
  const doctor = new KnowledgeDoctor();
229
- const issue: LintIssue = { type: 'contradiction', severity: 'high', entryId: 'e1', description: 'critical issue', suggestion: 'fix' };
230
229
  // 直接构造 critical 诊断来测试 calculateHealthScore 的 critical 分支
231
230
  // LintIssue severity 只有 low/medium/high,所以通过 checkDecay 的 high 来测试
232
231
  const entry = makeEntry({ maturity: 'proven', lastReferenced: '2020-01-01T00:00:00Z' });
@@ -2,10 +2,9 @@
2
2
  * PerformanceCollector 测试
3
3
  */
4
4
 
5
- import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
5
+ import { describe, it, expect, beforeEach } from '@jest/globals';
6
6
  import { PerformanceCollector } from '../performance-collector';
7
7
  import * as fs from 'fs';
8
- import * as path from 'path';
9
8
 
10
9
  // Mock fs
11
10
  jest.mock('fs', () => ({
@@ -11,7 +11,7 @@ import type { OutputSafetyCheckResult, OutputViolation, OutputGuardrailConfig }
11
11
 
12
12
  const DEFAULT_SENSITIVE_PATTERNS: RegExp[] = [
13
13
  // API keys
14
- /(?:api[_-]?key|apikey)\s*[:=]\s*['"]?([a-zA-Z0-9_\-]{20,})['"]?/gi,
14
+ /(?:api[_-]?key|apikey)\s*[:=]\s*['"]?([a-zA-Z0-9_-]{20,})['"]?/gi,
15
15
  // AWS
16
16
  /(?:AKIA|ASIA)[A-Z0-9]{16}/g,
17
17
  // Generic secrets
@@ -21,7 +21,7 @@ const DEFAULT_SENSITIVE_PATTERNS: RegExp[] = [
21
21
  // GitHub tokens
22
22
  /gh[ps]_[A-Za-z0-9_]{36,}/g,
23
23
  // Bearer tokens
24
- /Bearer\s+[A-Za-z0-9_\-\.]{20,}/g,
24
+ /Bearer\s+[A-Za-z0-9_\-.]{20,}/g,
25
25
  // Connection strings
26
26
  /(?:mongodb|postgres|mysql|redis):\/\/[^\s'"]+/gi,
27
27
  ];
@@ -4,7 +4,6 @@
4
4
 
5
5
  import { checkFile, checkDirectory, generateReport } from '../annotation-checker';
6
6
  import * as fs from 'fs';
7
- import * as path from 'path';
8
7
 
9
8
  // Mock fs
10
9
  jest.mock('fs', () => ({
@@ -152,7 +152,7 @@ function parseJSDoc(lines: string[], startLine: number): SpecAnnotation | null {
152
152
  }
153
153
 
154
154
  // @dependencies
155
- const depsMatch = content.match(/@dependencies\s+([\w\-_,\s\/]+)/);
155
+ const depsMatch = content.match(/@dependencies\s+([\w\-_,\s/]+)/);
156
156
  if (depsMatch) {
157
157
  annotation.dependencies = depsMatch[1]
158
158
  .split(/[,\s]+/)
@@ -34,8 +34,29 @@ export interface CustomConstraintDefinition {
34
34
  /** 描述(可选) */
35
35
  description?: string;
36
36
 
37
+ /**
38
+ * Prompt 注入文本(可选)
39
+ *
40
+ * 自定义约束统一 kind='prompt'(无 checker),该字段是进入 CLAUDE.md
41
+ * 注入段(renderConstraintsSection)的唯一通道——缺省不出现在注入段,
42
+ * 与内置无 promptInjection 的 check 条目行为一致。
43
+ */
44
+ promptInjection?: string;
45
+
37
46
  /** 是否启用(可选,默认 true) */
38
47
  enabled?: boolean;
48
+
49
+ /**
50
+ * 退役元数据(#82 D6 统一落点)
51
+ *
52
+ * 存在即视为已退役:不进生效集、不注入 CLAUDE.md。恢复 = 删除该段。
53
+ * 落点在 custom-constraints.yml 条目内(一处真相,不拆 config.yml 第二处)。
54
+ */
55
+ retired?: {
56
+ at?: string;
57
+ reason?: string;
58
+ stats?: { total: number; fail: number; failRate: number };
59
+ };
39
60
  }
40
61
 
41
62
  /**
@@ -0,0 +1,53 @@
1
+ /**
2
+ * file-walk 工具测试
3
+ */
4
+
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import { findTsSourceFiles } from '../file-walk';
8
+
9
+ describe('findTsSourceFiles', () => {
10
+ const tempDir = path.join(process.cwd(), 'temp-test-file-walk');
11
+
12
+ beforeAll(() => {
13
+ fs.mkdirSync(tempDir, { recursive: true });
14
+ });
15
+
16
+ afterAll(() => {
17
+ try {
18
+ fs.rmSync(tempDir, { recursive: true, force: true });
19
+ } catch {
20
+ // ignore
21
+ }
22
+ });
23
+
24
+ it('默认只收集 .ts(排除 .d.ts),.tsx 不进入结果', () => {
25
+ const dir = path.join(tempDir, 'default-ts-only');
26
+ fs.mkdirSync(dir, { recursive: true });
27
+ fs.writeFileSync(path.join(dir, 'a.ts'), '');
28
+ fs.writeFileSync(path.join(dir, 'b.d.ts'), '');
29
+ fs.writeFileSync(path.join(dir, 'c.tsx'), '');
30
+
31
+ const names = findTsSourceFiles(dir).map(f => path.basename(f)).sort();
32
+ expect(names).toEqual(['a.ts']);
33
+
34
+ fs.rmSync(dir, { recursive: true, force: true });
35
+ });
36
+
37
+ it('includeTsx: true 时同时收集 .tsx,skipIndex 排除 index.ts/index.tsx', () => {
38
+ const dir = path.join(tempDir, 'include-tsx');
39
+ fs.mkdirSync(dir, { recursive: true });
40
+ fs.writeFileSync(path.join(dir, 'a.ts'), '');
41
+ fs.writeFileSync(path.join(dir, 'b.d.ts'), '');
42
+ fs.writeFileSync(path.join(dir, 'c.tsx'), '');
43
+ fs.writeFileSync(path.join(dir, 'index.ts'), '');
44
+ fs.writeFileSync(path.join(dir, 'index.tsx'), '');
45
+
46
+ const names = findTsSourceFiles(dir, { includeTsx: true, skipIndex: true })
47
+ .map(f => path.basename(f))
48
+ .sort();
49
+ expect(names).toEqual(['a.ts', 'c.tsx']);
50
+
51
+ fs.rmSync(dir, { recursive: true, force: true });
52
+ });
53
+ });
@@ -53,6 +53,8 @@ export function walkFiles(root: string, options: FileWalkOptions = {}): string[]
53
53
  export interface TsSourceWalkOptions {
54
54
  /** 是否跳过 barrel 文件 index.ts,默认 false */
55
55
  skipIndex?: boolean;
56
+ /** 是否同时收集 .tsx 文件,默认 false(只收集 .ts) */
57
+ includeTsx?: boolean;
56
58
  /** 跳过的目录名,默认 node_modules/__tests__/dist */
57
59
  skipDirs?: string[];
58
60
  /** 是否跳过 `.` 开头的条目,默认 false */
@@ -60,11 +62,30 @@ export interface TsSourceWalkOptions {
60
62
  }
61
63
 
62
64
  /**
63
- * 递归收集 TypeScript 源文件(.ts,排除 .d.ts)
65
+ * 文件名是否为 TypeScript 源文件(includeTsx 时含 .tsx,排除 .d.ts 与可选 barrel index
66
+ *
67
+ * findTsSourceFiles 的过滤谓词;需要同口径判断的调用方(如 sync-docs 顶层文件分支)
68
+ * 复用此函数,避免扩展名规则在多处漂移。
69
+ */
70
+ export function isTsSourceFile(
71
+ name: string,
72
+ options: { skipIndex?: boolean; includeTsx?: boolean } = {}
73
+ ): boolean {
74
+ const { skipIndex = false, includeTsx = false } = options;
75
+ return (
76
+ (name.endsWith('.ts') || (includeTsx && name.endsWith('.tsx'))) &&
77
+ !name.endsWith('.d.ts') &&
78
+ (!skipIndex || (name !== 'index.ts' && name !== 'index.tsx'))
79
+ );
80
+ }
81
+
82
+ /**
83
+ * 递归收集 TypeScript 源文件(.ts,includeTsx 时含 .tsx,排除 .d.ts)
64
84
  */
65
85
  export function findTsSourceFiles(root: string, options: TsSourceWalkOptions = {}): string[] {
66
86
  const {
67
87
  skipIndex = false,
88
+ includeTsx = false,
68
89
  skipDirs = ['node_modules', '__tests__', 'dist'],
69
90
  skipHidden = false,
70
91
  } = options;
@@ -72,7 +93,6 @@ export function findTsSourceFiles(root: string, options: TsSourceWalkOptions = {
72
93
  return walkFiles(root, {
73
94
  skipDirs,
74
95
  skipHidden,
75
- filter: (name) =>
76
- name.endsWith('.ts') && !name.endsWith('.d.ts') && (!skipIndex || name !== 'index.ts'),
96
+ filter: (name) => isTsSourceFile(name, { skipIndex, includeTsx }),
77
97
  });
78
98
  }
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { VerificationLoop } from '../loop';
6
- import type { VerificationLoopConfig, GatherState, VerificationResult } from '../types';
6
+ import type { VerificationLoopConfig, GatherState } from '../types';
7
7
 
8
8
  describe('VerificationLoop', () => {
9
9
  const baseConfig: VerificationLoopConfig = {