@dommaker/harness 0.11.1 → 0.12.1

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 (96) hide show
  1. package/README.md +70 -174
  2. package/dist/architecture/cross-project-checker.d.ts +4 -2
  3. package/dist/architecture/cross-project-checker.d.ts.map +1 -1
  4. package/dist/architecture/cross-project-checker.js +71 -6
  5. package/dist/architecture/cross-project-checker.js.map +1 -1
  6. package/dist/cli/commands/check.d.ts.map +1 -1
  7. package/dist/cli/commands/check.js +37 -9
  8. package/dist/cli/commands/check.js.map +1 -1
  9. package/dist/cli/commands/report.d.ts.map +1 -1
  10. package/dist/cli/commands/report.js +5 -8
  11. package/dist/cli/commands/report.js.map +1 -1
  12. package/dist/cli/commands/sync-docs.js +7 -5
  13. package/dist/cli/commands/sync-docs.js.map +1 -1
  14. package/dist/constraints/registry.d.ts +2 -1
  15. package/dist/constraints/registry.d.ts.map +1 -1
  16. package/dist/constraints/registry.js +7 -7
  17. package/dist/constraints/registry.js.map +1 -1
  18. package/dist/constraints/types.d.ts +2 -2
  19. package/dist/constraints/types.d.ts.map +1 -1
  20. package/dist/context/session-manager.d.ts +24 -7
  21. package/dist/context/session-manager.d.ts.map +1 -1
  22. package/dist/context/session-manager.js +71 -12
  23. package/dist/context/session-manager.js.map +1 -1
  24. package/dist/core/constraints/check-cache.d.ts +38 -0
  25. package/dist/core/constraints/check-cache.d.ts.map +1 -0
  26. package/dist/core/constraints/check-cache.js +75 -0
  27. package/dist/core/constraints/check-cache.js.map +1 -0
  28. package/dist/core/constraints/checker.d.ts +99 -12
  29. package/dist/core/constraints/checker.d.ts.map +1 -1
  30. package/dist/core/constraints/checker.js +432 -47
  31. package/dist/core/constraints/checker.js.map +1 -1
  32. package/dist/core/constraints/definitions.d.ts.map +1 -1
  33. package/dist/core/constraints/definitions.js +328 -34
  34. package/dist/core/constraints/definitions.js.map +1 -1
  35. package/dist/core/constraints/index.d.ts +1 -1
  36. package/dist/core/constraints/index.d.ts.map +1 -1
  37. package/dist/core/constraints/index.js +2 -1
  38. package/dist/core/constraints/index.js.map +1 -1
  39. package/dist/core/constraints/interceptor.d.ts +1 -0
  40. package/dist/core/constraints/interceptor.d.ts.map +1 -1
  41. package/dist/core/constraints/interceptor.js +38 -44
  42. package/dist/core/constraints/interceptor.js.map +1 -1
  43. package/dist/core/session/clean-state.js +1 -1
  44. package/dist/core/session/clean-state.js.map +1 -1
  45. package/dist/core/session/startup.d.ts.map +1 -1
  46. package/dist/core/session/startup.js +2 -3
  47. package/dist/core/session/startup.js.map +1 -1
  48. package/dist/core/validators/passes-gate.js +1 -1
  49. package/dist/core/validators/passes-gate.js.map +1 -1
  50. package/dist/failure/constraint-handler.d.ts +83 -0
  51. package/dist/failure/constraint-handler.d.ts.map +1 -0
  52. package/dist/failure/constraint-handler.js +140 -0
  53. package/dist/failure/constraint-handler.js.map +1 -0
  54. package/dist/failure/index.d.ts +1 -0
  55. package/dist/failure/index.d.ts.map +1 -1
  56. package/dist/failure/index.js +7 -1
  57. package/dist/failure/index.js.map +1 -1
  58. package/dist/hooks/bootstrap.d.ts +50 -0
  59. package/dist/hooks/bootstrap.d.ts.map +1 -0
  60. package/dist/hooks/bootstrap.js +137 -0
  61. package/dist/hooks/bootstrap.js.map +1 -0
  62. package/dist/hooks/index.d.ts +11 -0
  63. package/dist/hooks/index.d.ts.map +1 -0
  64. package/dist/hooks/index.js +32 -0
  65. package/dist/hooks/index.js.map +1 -0
  66. package/dist/hooks/pipeline.d.ts +36 -0
  67. package/dist/hooks/pipeline.d.ts.map +1 -0
  68. package/dist/hooks/pipeline.js +135 -0
  69. package/dist/hooks/pipeline.js.map +1 -0
  70. package/dist/hooks/registry.d.ts +50 -0
  71. package/dist/hooks/registry.d.ts.map +1 -0
  72. package/dist/hooks/registry.js +81 -0
  73. package/dist/hooks/registry.js.map +1 -0
  74. package/dist/hooks/types.d.ts +75 -0
  75. package/dist/hooks/types.d.ts.map +1 -0
  76. package/dist/hooks/types.js +9 -0
  77. package/dist/hooks/types.js.map +1 -0
  78. package/dist/index.d.ts +1 -0
  79. package/dist/index.d.ts.map +1 -1
  80. package/dist/index.js +4 -0
  81. package/dist/index.js.map +1 -1
  82. package/dist/presets/index.d.ts +1 -1
  83. package/dist/presets/index.d.ts.map +1 -1
  84. package/dist/presets/index.js +2 -1
  85. package/dist/presets/index.js.map +1 -1
  86. package/dist/presets/standard.d.ts +17 -0
  87. package/dist/presets/standard.d.ts.map +1 -1
  88. package/dist/presets/standard.js +50 -1
  89. package/dist/presets/standard.js.map +1 -1
  90. package/dist/types/constraint.d.ts +38 -1
  91. package/dist/types/constraint.d.ts.map +1 -1
  92. package/dist/types/constraint.js +29 -1
  93. package/dist/types/constraint.js.map +1 -1
  94. package/dist/types/session.d.ts +1 -0
  95. package/dist/types/session.d.ts.map +1 -1
  96. package/package.json +2 -2
@@ -44,7 +44,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
44
44
  exports.ironLawChecker = exports.IronLawChecker = exports.constraintChecker = exports.ConstraintChecker = void 0;
45
45
  exports.checkConstraint = checkConstraint;
46
46
  exports.checkConstraints = checkConstraints;
47
+ exports.checkConstraintsSafe = checkConstraintsSafe;
47
48
  exports.checkBeforeExecution = checkBeforeExecution;
49
+ exports.buildConstraintPrompt = buildConstraintPrompt;
48
50
  exports.checkIronLaw = checkIronLaw;
49
51
  exports.checkAllIronLaws = checkAllIronLaws;
50
52
  const constraint_1 = require("../../types/constraint");
@@ -55,6 +57,7 @@ const exec_2 = require("../../utils/exec");
55
57
  const fs_1 = require("fs");
56
58
  const path_1 = require("path");
57
59
  const yaml = __importStar(require("js-yaml"));
60
+ const check_cache_1 = require("./check-cache");
58
61
  /**
59
62
  * 例外名称 → ConstraintContext 中布尔字段的映射
60
63
  */
@@ -91,6 +94,8 @@ class ConstraintChecker {
91
94
  static instance;
92
95
  /** 自定义约束配置(项目级) */
93
96
  customConfig = null;
97
+ /** 检查结果缓存(S7:减少重复 git diff / src scan I/O,TTL=1s 防跨测试污染) */
98
+ cache = new check_cache_1.CheckCache({ ttlMs: 1000 });
94
99
  constructor() { }
95
100
  /**
96
101
  * 获取单例实例
@@ -103,26 +108,31 @@ class ConstraintChecker {
103
108
  }
104
109
  /**
105
110
  * 设置项目级约束配置
111
+ *
112
+ * @deprecated 修改单例状态,多项目并发时存在污染风险。
113
+ * 请改用 checkConstraints() / beforeExecution() 的 customConfig 参数实现 per-request 隔离。
106
114
  */
107
115
  setCustomConfig(config) {
108
116
  this.customConfig = config;
109
117
  }
110
118
  /**
111
119
  * 获取当前的约束集合(内置 + 自定义)
120
+ *
121
+ * @param customConfig 可选,per-request 自定义配置(优先级高于 setCustomConfig 的单例状态)
112
122
  */
113
- getConstraints() {
114
- if (this.customConfig) {
115
- return {
116
- ironLaws: this.customConfig.ironLaws,
117
- guidelines: this.customConfig.guidelines,
118
- tips: this.customConfig.tips,
119
- };
120
- }
121
- return {
122
- ironLaws: definitions_1.IRON_LAWS,
123
- guidelines: definitions_1.GUIDELINES,
124
- tips: definitions_1.TIPS,
123
+ getConstraints(customConfig) {
124
+ const config = customConfig ?? this.customConfig;
125
+ const source = config
126
+ ? { ironLaws: config.ironLaws, guidelines: config.guidelines, tips: config.tips }
127
+ : { ironLaws: definitions_1.IRON_LAWS, guidelines: definitions_1.GUIDELINES, tips: definitions_1.TIPS };
128
+ // Wire unified check() method on every constraint
129
+ const wire = (constraints) => {
130
+ for (const c of Object.values(constraints)) {
131
+ c.check = (ctx) => this.check(c, ctx);
132
+ }
133
+ return constraints;
125
134
  };
135
+ return { ironLaws: wire(source.ironLaws), guidelines: wire(source.guidelines), tips: wire(source.tips) };
126
136
  }
127
137
  /**
128
138
  * 检查单个约束
@@ -234,6 +244,28 @@ class ConstraintChecker {
234
244
  case 'no_implementation_without_requirement':
235
245
  // 检查是否有需求文档
236
246
  return context.hasRequirement === true;
247
+ case 'must_use_worktree':
248
+ // 检查是否在 worktree 中执行
249
+ return context.hasWorktree === true;
250
+ case 'no_fuzzy_completion_claim':
251
+ // 检查完成声明是否包含模糊词(含"我记得""之前说""大部分"等无验证模式)
252
+ return this.checkNoFuzzyWords(context.completionClaimText || '');
253
+ case 'no_claim_without_evidence':
254
+ // 检查是否提供了验证证据(test output / file check / spec AC matrix)
255
+ return context.hasVerificationEvidence === true ||
256
+ (context.taskDescription || '').includes('test') ||
257
+ context.hasTest === true;
258
+ case 'no_delete_without_context':
259
+ // 检查删除前是否审查了设计文档
260
+ return context.hasRequirementReview === true ||
261
+ context.hasRequirement === true ||
262
+ context.isExistingDesign === true;
263
+ case 'no_performative_agreement':
264
+ // 检查是否有表演性同意(主要由 promptInjection 驱动,这里做辅助检查)
265
+ return this.checkNoPerformativePatterns(context.taskDescription || '');
266
+ case 'two_stage_review_required':
267
+ // 检查是否完成两阶段审查
268
+ return context.hasTwoStageReview === true;
237
269
  // Guidelines
238
270
  case 'no_fix_without_root_cause':
239
271
  // 检查是否有根本原因调查
@@ -266,6 +298,21 @@ class ConstraintChecker {
266
298
  return await this.checkContextDocSync(projectPath);
267
299
  case 'docs_freshness':
268
300
  return await this.checkDocsFreshness(projectPath);
301
+ case 'no_excuse_patterns':
302
+ // 检查是否使用了借口模式
303
+ return this.checkNoExcusePatterns(context.completionClaimText || context.taskDescription || '');
304
+ case 'yagni_check':
305
+ // 检查是否有过度设计(主要由 promptInjection 驱动)
306
+ return this.checkYagni(projectPath, context.changedFiles);
307
+ // Mnilax guidelines + first-principles (injectPrompt=true, behavior-level, always pass — post-checked)
308
+ case 'surgical_changes_only':
309
+ case 'no_model_for_deterministic':
310
+ case 'no_conflict_blending':
311
+ case 'read_before_write':
312
+ case 'follow_conventions':
313
+ case 'first_principles_first':
314
+ // Guideline: behavior constraints, enforced via promptInjection + post-check
315
+ return true;
269
316
  // Tips - 总是返回 true(仅提示)
270
317
  case 'readme_required':
271
318
  return true;
@@ -276,6 +323,118 @@ class ConstraintChecker {
276
323
  return true;
277
324
  }
278
325
  }
326
+ /**
327
+ * 检查文本中是否包含模糊词
328
+ */
329
+ checkNoFuzzyWords(text) {
330
+ if (!text)
331
+ return true; // 没有文本时默认通过(由其他检查覆盖)
332
+ const fuzzyPatterns = [
333
+ /应该没问题/,
334
+ /应该可以/,
335
+ /大概/,
336
+ /可能/,
337
+ /好像/,
338
+ /似乎/,
339
+ /差不多/,
340
+ /基本完成/,
341
+ /大部分/,
342
+ // audit-learned (2026-05-18): unverified completion claims
343
+ /我记得删[过掉]了/,
344
+ /之前说删了/,
345
+ /已删除.*但/,
346
+ /大部分实现/,
347
+ /大部分完成/,
348
+ /已修复.*但/,
349
+ ];
350
+ return !fuzzyPatterns.some(p => p.test(text));
351
+ }
352
+ /**
353
+ * 检查文本中是否包含表演性同意模式
354
+ */
355
+ checkNoPerformativePatterns(text) {
356
+ if (!text)
357
+ return true;
358
+ // 检查是否只有"好的/明白了"等同意词,没有后续分析
359
+ const performativeStart = /^(好的[,,]*\s*我[来去]做|明白了[,,]*|没问题[,,]*|ok[,,]*\s*i.?ll)/i;
360
+ // 如果以表演性模式开头,且总长度很短(< 50 字符),视为表演性同意
361
+ if (performativeStart.test(text) && text.length < 100) {
362
+ return false;
363
+ }
364
+ return true;
365
+ }
366
+ /**
367
+ * 检查文本中是否包含借口模式
368
+ */
369
+ checkNoExcusePatterns(text) {
370
+ if (!text)
371
+ return true;
372
+ const excusePatterns = [
373
+ /稍后修复/,
374
+ /小问题/,
375
+ /不影响功能/,
376
+ /以后再说/,
377
+ /先这样/,
378
+ /临时方案/,
379
+ /暂时这样/,
380
+ ];
381
+ return !excusePatterns.some(p => p.test(text));
382
+ }
383
+ /**
384
+ * 检查 YAGNI:过度设计信号
385
+ *
386
+ * 检查是否存在只有一个实现的 interface/abstract class。
387
+ * 主要由 promptInjection 驱动,这里是辅助检查。
388
+ */
389
+ async checkYagni(projectPath, changedFiles) {
390
+ // 无上下文时默认通过(主要由 promptInjection 驱动)
391
+ if (!projectPath || !changedFiles || changedFiles.length === 0)
392
+ return true;
393
+ try {
394
+ const path = await Promise.resolve().then(() => __importStar(require('path')));
395
+ const fs = await Promise.resolve().then(() => __importStar(require('fs')));
396
+ for (const file of changedFiles) {
397
+ if (!file.endsWith('.ts') && !file.endsWith('.tsx'))
398
+ continue;
399
+ const filePath = path.join(projectPath, file);
400
+ if (!fs.existsSync(filePath))
401
+ continue;
402
+ const content = fs.readFileSync(filePath, 'utf-8');
403
+ // 检查是否有 export interface/abstract class
404
+ const hasInterface = /export\s+(?:default\s+)?interface\s+(\w+)/g;
405
+ const hasAbstract = /export\s+(?:default\s+)?abstract\s+class\s+(\w+)/g;
406
+ const interfaces = [...content.matchAll(hasInterface)].map(m => m[1]);
407
+ const abstracts = [...content.matchAll(hasAbstract)].map(m => m[1]);
408
+ const exportedTypes = [...interfaces, ...abstracts];
409
+ for (const typeName of exportedTypes) {
410
+ // 检查同一个 typeName 的 implements/extends 出现次数
411
+ const implPattern = new RegExp(`(?:implements|extends)\\s+${typeName}\\b`, 'g');
412
+ // 在整个项目中搜索(简化版:只在 changedFiles 中搜索)
413
+ let implCount = 0;
414
+ for (const f of changedFiles) {
415
+ if (!f.endsWith('.ts') && !f.endsWith('.tsx'))
416
+ continue;
417
+ const fp = path.join(projectPath, f);
418
+ if (!fs.existsSync(fp))
419
+ continue;
420
+ const fc = fs.readFileSync(fp, 'utf-8');
421
+ const matches = fc.match(implPattern);
422
+ if (matches)
423
+ implCount += matches.length;
424
+ }
425
+ // 只有一个实现者 → YAGNI 违规
426
+ if (implCount <= 1) {
427
+ return false;
428
+ }
429
+ }
430
+ }
431
+ }
432
+ catch {
433
+ // 文件读取失败,默认通过
434
+ return true;
435
+ }
436
+ return true;
437
+ }
279
438
  /**
280
439
  * 检查 no_bypass_checkpoint:检查代码中是否有 skip/bypass 关键词
281
440
  */
@@ -372,35 +531,48 @@ class ConstraintChecker {
372
531
  }
373
532
  }
374
533
  /**
375
- * 检查 capability_sync:检查 CAPABILITIES.md 是否更新
534
+ * 检查 capability_sync:检查 CAPABILITIES.md 是否与代码同步
535
+ *
536
+ * 两步验证:
537
+ * 1. Git diff 检查 — 当前变更的非测试文件是否在文档中有记录(增量检查)
538
+ * 2. 全量扫描 — src/ 下的所有源文件是否都在 CAPABILITIES.md 中(完整性检查)
376
539
  */
377
540
  async checkCapabilitySync(projectPath) {
378
541
  try {
379
- // 检查是否有代码变更
380
- const diff = await (0, exec_2.runCommand)('git diff --cached --name-only', projectPath);
381
- const changedCodeFiles = diff.split('\n').filter((f) => f.endsWith('.ts') || f.endsWith('.tsx') || f.endsWith('.js'));
382
- if (changedCodeFiles.length === 0) {
383
- return true; // 无代码变更
384
- }
385
- // 检查 CAPABILITIES.md 是否存在
386
542
  const capabilitiesPath = (0, path_1.join)(projectPath, 'CAPABILITIES.md');
387
543
  if (!(0, fs_1.existsSync)(capabilitiesPath)) {
388
- return false; // 有代码变更但没有 CAPABILITIES.md
544
+ // 检查是否有代码变更,无变更则跳过
545
+ const diff = await (0, exec_2.runCommand)('git diff --cached --name-only', projectPath);
546
+ const changedCodeFiles = diff.split('\n').filter((f) => f.endsWith('.ts') || f.endsWith('.tsx') || f.endsWith('.js'));
547
+ return changedCodeFiles.length === 0;
389
548
  }
390
- // 解析 CAPABILITIES.md 中的文件列表
391
549
  const listedFiles = this.parseCapabilitiesFiles(capabilitiesPath);
392
- // 无表格内容 → 向后兼容,存在即通过
393
- if (listedFiles.length === 0) {
394
- return true;
550
+ // ── Step 1: Git diff 增量检查 ──
551
+ const diff = await (0, exec_2.runCommand)('git diff --cached --name-only', projectPath);
552
+ const changedCodeFiles = diff.split('\n').filter((f) => f.endsWith('.ts') || f.endsWith('.tsx') || f.endsWith('.js'));
553
+ if (changedCodeFiles.length > 0 && listedFiles.length > 0) {
554
+ const significantChanges = changedCodeFiles.filter((f) => !f.includes('__tests__') && !f.includes('.test.') && !f.includes('.spec.'));
555
+ if (significantChanges.length > 0) {
556
+ const covered = significantChanges.some((changed) => listedFiles.some((listed) => changed.endsWith(listed) || changed.includes(listed)));
557
+ if (!covered)
558
+ return false;
559
+ }
395
560
  }
396
- // 检查变更的非测试代码文件是否在 CAPABILITIES.md 中有记录
397
- const significantChanges = changedCodeFiles.filter((f) => !f.includes('__tests__') && !f.includes('.test.') && !f.includes('.spec.'));
398
- if (significantChanges.length === 0) {
399
- return true; // 只有测试文件变更
561
+ // ── Step 2: 全量 src/ 扫描(T-058 修复核心)──
562
+ // 检查是否有 src/ 下的源文件未在 CAPABILITIES.md 中记录
563
+ if (listedFiles.length > 0) {
564
+ const srcDir = (0, path_1.join)(projectPath, 'src');
565
+ if ((0, fs_1.existsSync)(srcDir)) {
566
+ // S7: 缓存 src/ 递归扫描结果
567
+ const actualFiles = this.cache.getSync('src_scan', projectPath, () => this.findSourceFiles(srcDir, projectPath));
568
+ for (const file of actualFiles) {
569
+ if (!listedFiles.includes(file)) {
570
+ return false; // 有新增文件未在 CAPABILITIES.md 中列出
571
+ }
572
+ }
573
+ }
400
574
  }
401
- // 至少有一个变更文件被 CAPABILITIES.md 覆盖
402
- const covered = significantChanges.some((changed) => listedFiles.some((listed) => changed.endsWith(listed) || changed.includes(listed)));
403
- return covered;
575
+ return true;
404
576
  }
405
577
  catch {
406
578
  return true; // 检查失败,默认通过
@@ -501,9 +673,20 @@ class ConstraintChecker {
501
673
  }
502
674
  }
503
675
  /**
504
- * 检查 docs_freshness:CAPABILITIES.md 是否与源码同步
676
+ * 检查 docs_freshness:CAPABILITIES.md + CLAUDE.md 是否与源码同步
505
677
  */
506
678
  async checkDocsFreshness(projectPath) {
679
+ // 检查 CAPABILITIES.md
680
+ const capResult = await this.checkCapabilitiesFreshness(projectPath);
681
+ if (!capResult)
682
+ return false;
683
+ // CONTEXT.md 已删除。harness 的目录描述集中在 CLAUDE.md 的 Key Subsystems 表中。不再检查。
684
+ return true;
685
+ }
686
+ /**
687
+ * 检查 CAPABILITIES.md 是否与源码同步
688
+ */
689
+ async checkCapabilitiesFreshness(projectPath) {
507
690
  try {
508
691
  const capabilitiesPath = (0, path_1.join)(projectPath, 'CAPABILITIES.md');
509
692
  const content = (0, fs_1.readFileSync)(capabilitiesPath, 'utf-8');
@@ -518,12 +701,16 @@ class ConstraintChecker {
518
701
  if (listedFiles.length === 0) {
519
702
  return true;
520
703
  }
521
- // 扫描 src/ 目录中的实际文件
704
+ // 扫描 src/ 目录中的实际文件(S7: 缓存)
522
705
  const srcDir = (0, path_1.join)(projectPath, 'src');
523
- const actualFiles = this.findSourceFiles(srcDir, projectPath);
706
+ const actualFiles = this.cache.getSync('src_scan', projectPath, () => this.findSourceFiles(srcDir, projectPath));
707
+ // 标准化路径: 去掉 src/ 前缀(sync-docs 和 scanner 可能用不同格式)
708
+ const normalize = (f) => f.replace(/^src\//, '');
709
+ const normalizedListed = listedFiles.map(normalize);
710
+ const normalizedActual = actualFiles.map(normalize);
524
711
  // 检查是否有新增文件未列出
525
- for (const file of actualFiles) {
526
- if (!listedFiles.includes(file)) {
712
+ for (const file of normalizedActual) {
713
+ if (!normalizedListed.includes(file)) {
527
714
  return false; // 有新增文件未列出
528
715
  }
529
716
  }
@@ -533,6 +720,103 @@ class ConstraintChecker {
533
720
  return true; // CAPABILITIES.md 不存在或检查失败,跳过
534
721
  }
535
722
  }
723
+ /**
724
+ * 检查 CLAUDE.md 的 Domain Packages 和 Key Architecture Paths 段落是否与实际代码结构同步
725
+ */
726
+ async checkClaudeMdFreshness(projectPath) {
727
+ try {
728
+ const claudePath = (0, path_1.join)(projectPath, 'CLAUDE.md');
729
+ if (!(0, fs_1.existsSync)(claudePath))
730
+ return true; // 无 CLAUDE.md,跳过
731
+ const content = (0, fs_1.readFileSync)(claudePath, 'utf-8');
732
+ // 1. 检查 Domain Packages 段落:packages/studio-* 目录是否存在
733
+ const domainPkgResult = this.checkDomainPackages(projectPath, content);
734
+ if (!domainPkgResult)
735
+ return false;
736
+ // 2. 检查 Key Architecture Paths 段落:引用的文件/目录是否存在
737
+ const archPathsResult = this.checkArchitecturePaths(projectPath, content);
738
+ if (!archPathsResult)
739
+ return false;
740
+ return true;
741
+ }
742
+ catch {
743
+ return true; // 检查失败,跳过
744
+ }
745
+ }
746
+ /**
747
+ * 检查 CLAUDE.md Domain Packages 段落中列出的 packages/studio-* 目录是否都存在
748
+ */
749
+ checkDomainPackages(projectPath, claudeContent) {
750
+ try {
751
+ // 提取 Domain Packages 段落内容(到下一个 ## 或 --- 为止)
752
+ const sectionMatch = claudeContent.match(/## Domain Packages\s*\n([\s\S]*?)(?=\n## |\n---|\n$)/);
753
+ if (!sectionMatch)
754
+ return true; // 段落不存在,跳过
755
+ const section = sectionMatch[1];
756
+ // 提取 CLAUDE.md 中 packages/* 的包名
757
+ const pkgRegex = /packages\/([\w-]+)/g;
758
+ let match;
759
+ while ((match = pkgRegex.exec(section)) !== null) {
760
+ const pkgDir = (0, path_1.join)(projectPath, 'packages', match[1]);
761
+ if (!(0, fs_1.existsSync)(pkgDir)) {
762
+ return false; // CLAUDE.md 引用的包目录不存在
763
+ }
764
+ }
765
+ // 反向检查:实际存在的包目录是否都在 CLAUDE.md 中被提及
766
+ const packagesDir = (0, path_1.join)(projectPath, 'packages');
767
+ if ((0, fs_1.existsSync)(packagesDir)) {
768
+ const entries = (0, fs_1.readdirSync)(packagesDir);
769
+ for (const entry of entries) {
770
+ if (entry.startsWith('.') || entry.includes('node_modules'))
771
+ continue;
772
+ const entryStat = (0, fs_1.statSync)((0, path_1.join)(packagesDir, entry));
773
+ if (!entryStat.isDirectory())
774
+ continue;
775
+ if (!section.includes(entry)) {
776
+ return false; // 实际包未在 CLAUDE.md 中记录
777
+ }
778
+ }
779
+ }
780
+ return true;
781
+ }
782
+ catch {
783
+ return true;
784
+ }
785
+ }
786
+ /**
787
+ * 检查 CLAUDE.md Key Architecture Paths 段落中引用的路径是否存在
788
+ */
789
+ checkArchitecturePaths(projectPath, claudeContent) {
790
+ try {
791
+ // 提取 Key Architecture Paths 段落
792
+ const sectionMatch = claudeContent.match(/## Key Architecture Paths\s*\n([\s\S]*?)(?=\n## |\n---|\n$)/);
793
+ if (!sectionMatch)
794
+ return true; // 段落不存在,跳过
795
+ const section = sectionMatch[1];
796
+ // 从表格中提取路径(第二列,格式 `path/to/file.ts` 或 `path/to/dir/`)
797
+ const pathRegex = /\|\s*`([^`]+)`\s*\|/g;
798
+ let match;
799
+ while ((match = pathRegex.exec(section)) !== null) {
800
+ const refPath = match[1].trim();
801
+ // 跳过非路径内容(如命令、URL)
802
+ if (refPath.startsWith('http') || refPath.startsWith('搜'))
803
+ continue;
804
+ const fullPath = (0, path_1.join)(projectPath, refPath);
805
+ // 检查文件或目录是否存在(也尝试去掉尾部 /)
806
+ if (!(0, fs_1.existsSync)(fullPath) && !(0, fs_1.existsSync)(fullPath.replace(/\/$/, ''))) {
807
+ // 路径可能指向模块内部(如 `src/modules/example/`),尝试去掉通配符
808
+ const basePath = refPath.replace(/\/?\*$/, '');
809
+ if (!(0, fs_1.existsSync)((0, path_1.join)(projectPath, basePath))) {
810
+ return false; // 引用的路径不存在
811
+ }
812
+ }
813
+ }
814
+ return true;
815
+ }
816
+ catch {
817
+ return true;
818
+ }
819
+ }
536
820
  /**
537
821
  * 查找 src/ 目录中的源文件
538
822
  */
@@ -566,10 +850,13 @@ class ConstraintChecker {
566
850
  }
567
851
  /**
568
852
  * 查找适用于当前操作的约束
853
+ *
854
+ * @param context 约束上下文
855
+ * @param customConfig 可选,per-request 自定义配置(避免多请求间的单例状态污染)
569
856
  */
570
- findApplicableConstraints(context) {
857
+ findApplicableConstraints(context, customConfig) {
571
858
  const trigger = context.operation;
572
- const constraints = this.getConstraints();
859
+ const constraints = this.getConstraints(customConfig);
573
860
  const filterByTrigger = (constraintSet) => {
574
861
  return Object.values(constraintSet).filter(constraint => {
575
862
  const triggers = Array.isArray(constraint.trigger)
@@ -590,8 +877,11 @@ class ConstraintChecker {
590
877
  * - Iron Laws:检查失败立即抛出异常
591
878
  * - Guidelines:检查失败记录警告
592
879
  * - Tips:检查失败记录提示
880
+ *
881
+ * @param context 约束上下文
882
+ * @param customConfig 可选,per-request 自定义配置(避免多请求间的单例状态污染)
593
883
  */
594
- async checkConstraints(context) {
884
+ async checkConstraints(context, customConfig) {
595
885
  const result = {
596
886
  ironLaws: [],
597
887
  guidelines: [],
@@ -601,7 +891,7 @@ class ConstraintChecker {
601
891
  tipCount: 0,
602
892
  };
603
893
  const traceCollector = (0, traces_1.getTraceCollector)();
604
- const constraints = this.getConstraints();
894
+ const constraints = this.getConstraints(customConfig);
605
895
  // 1. Iron Laws: 必须全部通过
606
896
  for (const constraint of Object.values(constraints.ironLaws)) {
607
897
  if (!this.matchesTrigger(constraint, context.operation))
@@ -638,13 +928,61 @@ class ConstraintChecker {
638
928
  }
639
929
  return result;
640
930
  }
931
+ /**
932
+ * S11: 安全全量检查 — 全部三层约束检查,不抛异常
933
+ *
934
+ * 区别 checkConstraints(): Iron Law 违规收集在结果中而不是抛异常。
935
+ * 调用方通过 result.passed + result.warningCount 判断状态。
936
+ */
937
+ async checkConstraintsSafe(context, customConfig) {
938
+ const result = {
939
+ ironLaws: [],
940
+ guidelines: [],
941
+ tips: [],
942
+ passed: true,
943
+ warningCount: 0,
944
+ tipCount: 0,
945
+ };
946
+ const traceCollector = (0, traces_1.getTraceCollector)();
947
+ const constraints = this.getConstraints(customConfig);
948
+ for (const constraint of Object.values(constraints.ironLaws)) {
949
+ if (!this.matchesTrigger(constraint, context.operation))
950
+ continue;
951
+ const checkResult = await this.check(constraint, context);
952
+ result.ironLaws.push(checkResult);
953
+ this.recordTrace(traceCollector, constraint, checkResult, context);
954
+ if (!checkResult.satisfied)
955
+ result.passed = false;
956
+ }
957
+ for (const constraint of Object.values(constraints.guidelines)) {
958
+ if (!this.matchesTrigger(constraint, context.operation))
959
+ continue;
960
+ const checkResult = await this.check(constraint, context);
961
+ result.guidelines.push(checkResult);
962
+ this.recordTrace(traceCollector, constraint, checkResult, context);
963
+ if (!checkResult.satisfied)
964
+ result.warningCount++;
965
+ }
966
+ for (const constraint of Object.values(constraints.tips)) {
967
+ if (!this.matchesTrigger(constraint, context.operation))
968
+ continue;
969
+ const checkResult = await this.check(constraint, context);
970
+ result.tips.push(checkResult);
971
+ this.recordTrace(traceCollector, constraint, checkResult, context);
972
+ if (!checkResult.satisfied)
973
+ result.tipCount++;
974
+ }
975
+ return result;
976
+ }
641
977
  /**
642
978
  * 执行前检查(仅检查 Iron Laws)
643
979
  *
980
+ * @param context 约束上下文
981
+ * @param customConfig 可选,per-request 自定义配置(避免多请求间的单例状态污染)
644
982
  * @throws ConstraintViolationError 如果有铁律违规
645
983
  */
646
- async beforeExecution(context) {
647
- const constraints = this.getConstraints();
984
+ async beforeExecution(context, customConfig) {
985
+ const constraints = this.getConstraints(customConfig);
648
986
  const operations = (0, exec_1.normalizeTriggers)(context.operation);
649
987
  for (const constraint of Object.values(constraints.ironLaws)) {
650
988
  if (!(0, exec_1.normalizeTriggers)(constraint.trigger).some((t) => operations.includes(t)))
@@ -682,15 +1020,62 @@ async function checkConstraint(constraintId, context) {
682
1020
  }
683
1021
  /**
684
1022
  * 快捷函数:执行三层检查
1023
+ *
1024
+ * @param context 约束上下文
1025
+ * @param customConfig 可选,per-request 自定义配置
1026
+ */
1027
+ async function checkConstraints(context, customConfig) {
1028
+ return ConstraintChecker.getInstance().checkConstraints(context, customConfig);
1029
+ }
1030
+ /**
1031
+ * S11: 快捷函数 — 安全全量检查(不抛异常)
685
1032
  */
686
- async function checkConstraints(context) {
687
- return ConstraintChecker.getInstance().checkConstraints(context);
1033
+ async function checkConstraintsSafe(context, customConfig) {
1034
+ return ConstraintChecker.getInstance().checkConstraintsSafe(context, customConfig);
688
1035
  }
689
1036
  /**
690
1037
  * 快捷函数:执行前检查
1038
+ *
1039
+ * @param context 约束上下文
1040
+ * @param customConfig 可选,per-request 自定义配置
691
1041
  */
692
- async function checkBeforeExecution(context) {
693
- return ConstraintChecker.getInstance().beforeExecution(context);
1042
+ async function checkBeforeExecution(context, customConfig) {
1043
+ return ConstraintChecker.getInstance().beforeExecution(context, customConfig);
1044
+ }
1045
+ /**
1046
+ * 构建约束注入 prompt
1047
+ *
1048
+ * 收集所有适用约束的 promptInjection 字段,格式化为 Agent system prompt 片段。
1049
+ * 纯计算函数,无副作用,不调用 LLM。
1050
+ *
1051
+ * @param context 约束上下文(至少需要 operation 字段)
1052
+ * @returns 格式化的约束提示文本,可直接注入 Agent system prompt;无匹配约束时返回空字符串
1053
+ */
1054
+ function buildConstraintPrompt(context) {
1055
+ const constraints = (0, definitions_1.findConstraintsByTrigger)(context.operation);
1056
+ // Iron laws: always injected (blocking — Agent must know)
1057
+ // Guidelines: only injected if explicitly marked injectPrompt: true
1058
+ const injected = constraints
1059
+ .filter(c => c.promptInjection && (c.level === 'iron_law' || c.injectPrompt === true))
1060
+ .map(c => ({ level: c.level, text: c.promptInjection }));
1061
+ if (injected.length === 0)
1062
+ return '';
1063
+ const ironLaws = injected.filter(i => i.level === 'iron_law');
1064
+ const guidelines = injected.filter(i => i.level === 'guideline');
1065
+ const lines = [];
1066
+ // Iron laws section
1067
+ if (ironLaws.length > 0) {
1068
+ lines.push('## 铁律(违反将阻断执行)', '');
1069
+ ironLaws.forEach(i => lines.push(`- ${i.text.slice(0, 80)}${i.text.length > 80 ? '...' : ''}`));
1070
+ lines.push('');
1071
+ }
1072
+ // Behavior guidelines section
1073
+ if (guidelines.length > 0) {
1074
+ lines.push('## 行为准则', '');
1075
+ guidelines.forEach(i => lines.push(`- ${i.text.slice(0, 80)}${i.text.length > 80 ? '...' : ''}`));
1076
+ lines.push('');
1077
+ }
1078
+ return lines.join('\n');
694
1079
  }
695
1080
  // 导出单例
696
1081
  exports.constraintChecker = ConstraintChecker.getInstance();