@dommaker/harness 1.10.0 → 1.12.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 (180) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/cli/commands/constraints-pack-proposal.d.ts +48 -0
  3. package/dist/cli/commands/constraints-pack-proposal.d.ts.map +1 -0
  4. package/dist/cli/commands/constraints-pack-proposal.js +169 -0
  5. package/dist/cli/commands/constraints-pack-proposal.js.map +1 -0
  6. package/dist/cli/commands/constraints-reactivate.d.ts +55 -0
  7. package/dist/cli/commands/constraints-reactivate.d.ts.map +1 -0
  8. package/dist/cli/commands/constraints-reactivate.js +175 -0
  9. package/dist/cli/commands/constraints-reactivate.js.map +1 -0
  10. package/dist/cli/commands/constraints-report.d.ts +12 -1
  11. package/dist/cli/commands/constraints-report.d.ts.map +1 -1
  12. package/dist/cli/commands/constraints-report.js +35 -2
  13. package/dist/cli/commands/constraints-report.js.map +1 -1
  14. package/dist/cli/commands/constraints-retire.d.ts +49 -4
  15. package/dist/cli/commands/constraints-retire.d.ts.map +1 -1
  16. package/dist/cli/commands/constraints-retire.js +116 -34
  17. package/dist/cli/commands/constraints-retire.js.map +1 -1
  18. package/dist/cli/commands/definitions.d.ts.map +1 -1
  19. package/dist/cli/commands/definitions.js +36 -11
  20. package/dist/cli/commands/definitions.js.map +1 -1
  21. package/dist/cli/commands/knowledge-view.d.ts +6 -3
  22. package/dist/cli/commands/knowledge-view.d.ts.map +1 -1
  23. package/dist/cli/commands/knowledge-view.js +8 -26
  24. package/dist/cli/commands/knowledge-view.js.map +1 -1
  25. package/dist/cli/commands/report.d.ts.map +1 -1
  26. package/dist/cli/commands/report.js +9 -5
  27. package/dist/cli/commands/report.js.map +1 -1
  28. package/dist/cli/commands/scaffold-templates.d.ts +1 -1
  29. package/dist/cli/commands/scaffold-templates.d.ts.map +1 -1
  30. package/dist/cli/commands/scaffold-templates.js +0 -16
  31. package/dist/cli/commands/scaffold-templates.js.map +1 -1
  32. package/dist/cli/state-io.d.ts +8 -1
  33. package/dist/cli/state-io.d.ts.map +1 -1
  34. package/dist/cli/state-io.js +2 -1
  35. package/dist/cli/state-io.js.map +1 -1
  36. package/dist/core/app-constraints-loader.d.ts +28 -0
  37. package/dist/core/app-constraints-loader.d.ts.map +1 -0
  38. package/dist/core/app-constraints-loader.js +187 -0
  39. package/dist/core/app-constraints-loader.js.map +1 -0
  40. package/dist/core/constraints/checker.js +1 -1
  41. package/dist/core/constraints/checker.js.map +1 -1
  42. package/dist/core/constraints/checkers/index.d.ts +20 -9
  43. package/dist/core/constraints/checkers/index.d.ts.map +1 -1
  44. package/dist/core/constraints/checkers/index.js +40 -11
  45. package/dist/core/constraints/checkers/index.js.map +1 -1
  46. package/dist/core/constraints/checkers/templated/file-exists.d.ts +16 -0
  47. package/dist/core/constraints/checkers/templated/file-exists.d.ts.map +1 -0
  48. package/dist/core/constraints/checkers/templated/file-exists.js +108 -0
  49. package/dist/core/constraints/checkers/templated/file-exists.js.map +1 -0
  50. package/dist/core/constraints/checkers/templated/glob.d.ts +10 -0
  51. package/dist/core/constraints/checkers/templated/glob.d.ts.map +1 -0
  52. package/dist/core/constraints/checkers/templated/glob.js +51 -0
  53. package/dist/core/constraints/checkers/templated/glob.js.map +1 -0
  54. package/dist/core/constraints/checkers/templated/index.d.ts +12 -0
  55. package/dist/core/constraints/checkers/templated/index.d.ts.map +1 -0
  56. package/dist/core/constraints/checkers/templated/index.js +17 -0
  57. package/dist/core/constraints/checkers/templated/index.js.map +1 -0
  58. package/dist/core/constraints/checkers/templated/regex-scan.d.ts +15 -0
  59. package/dist/core/constraints/checkers/templated/regex-scan.d.ts.map +1 -0
  60. package/dist/core/constraints/checkers/templated/regex-scan.js +129 -0
  61. package/dist/core/constraints/checkers/templated/regex-scan.js.map +1 -0
  62. package/dist/core/constraints/checkers/templates-registry.d.ts +17 -0
  63. package/dist/core/constraints/checkers/templates-registry.d.ts.map +1 -0
  64. package/dist/core/constraints/checkers/templates-registry.js +23 -0
  65. package/dist/core/constraints/checkers/templates-registry.js.map +1 -0
  66. package/dist/core/constraints/checkers/types.d.ts +14 -0
  67. package/dist/core/constraints/checkers/types.d.ts.map +1 -1
  68. package/dist/core/constraints/checkers/types.js.map +1 -1
  69. package/dist/core/constraints/definitions/guidelines.d.ts.map +1 -1
  70. package/dist/core/constraints/definitions/guidelines.js +0 -14
  71. package/dist/core/constraints/definitions/guidelines.js.map +1 -1
  72. package/dist/core/constraints/definitions/iron-laws.d.ts.map +1 -1
  73. package/dist/core/constraints/definitions/iron-laws.js +15 -0
  74. package/dist/core/constraints/definitions/iron-laws.js.map +1 -1
  75. package/dist/core/constraints/usage-report.d.ts +59 -7
  76. package/dist/core/constraints/usage-report.d.ts.map +1 -1
  77. package/dist/core/constraints/usage-report.js +62 -13
  78. package/dist/core/constraints/usage-report.js.map +1 -1
  79. package/dist/core/project-config-loader.d.ts +9 -6
  80. package/dist/core/project-config-loader.d.ts.map +1 -1
  81. package/dist/core/project-config-loader.js +18 -8
  82. package/dist/core/project-config-loader.js.map +1 -1
  83. package/dist/gates/command.d.ts.map +1 -1
  84. package/dist/gates/command.js +50 -0
  85. package/dist/gates/command.js.map +1 -1
  86. package/dist/index.d.ts +2 -0
  87. package/dist/index.d.ts.map +1 -1
  88. package/dist/index.js +14 -2
  89. package/dist/index.js.map +1 -1
  90. package/dist/knowledge/lifecycle.d.ts +14 -0
  91. package/dist/knowledge/lifecycle.d.ts.map +1 -1
  92. package/dist/knowledge/lifecycle.js +50 -0
  93. package/dist/knowledge/lifecycle.js.map +1 -1
  94. package/dist/knowledge/lint.d.ts +17 -0
  95. package/dist/knowledge/lint.d.ts.map +1 -1
  96. package/dist/knowledge/lint.js +52 -0
  97. package/dist/knowledge/lint.js.map +1 -1
  98. package/dist/knowledge/store.d.ts +7 -0
  99. package/dist/knowledge/store.d.ts.map +1 -1
  100. package/dist/knowledge/store.js +48 -13
  101. package/dist/knowledge/store.js.map +1 -1
  102. package/dist/knowledge/types.d.ts +5 -1
  103. package/dist/knowledge/types.d.ts.map +1 -1
  104. package/dist/knowledge/types.js +5 -1
  105. package/dist/knowledge/types.js.map +1 -1
  106. package/dist/presets/standard.d.ts +1 -1
  107. package/dist/presets/standard.d.ts.map +1 -1
  108. package/dist/presets/standard.js +2 -3
  109. package/dist/presets/standard.js.map +1 -1
  110. package/dist/types/constraint.d.ts +24 -0
  111. package/dist/types/constraint.d.ts.map +1 -1
  112. package/dist/types/constraint.js.map +1 -1
  113. package/package.json +1 -1
  114. package/src/__tests__/app-constraints-loader.test.ts +220 -0
  115. package/src/__tests__/iron-laws.test.ts +5 -5
  116. package/src/__tests__/public-exports.test.ts +7 -0
  117. package/src/__tests__/public-type-surface.test.ts +5 -0
  118. package/src/__tests__/usage-report-public-export.test.ts +21 -0
  119. package/src/cli/commands/CONTEXT.md +5 -4
  120. package/src/cli/commands/__tests__/check.test.ts +22 -4
  121. package/src/cli/commands/__tests__/constraints-pack-proposal.test.ts +142 -0
  122. package/src/cli/commands/__tests__/constraints-reactivate.test.ts +210 -0
  123. package/src/cli/commands/__tests__/constraints-report-json-flag.test.ts +101 -0
  124. package/src/cli/commands/__tests__/constraints-report.test.ts +175 -6
  125. package/src/cli/commands/__tests__/constraints-retire.test.ts +135 -10
  126. package/src/cli/commands/__tests__/init-ondisk.test.ts +1 -1
  127. package/src/cli/commands/__tests__/init.test.ts +1 -5
  128. package/src/cli/commands/__tests__/knowledge.test.ts +6 -16
  129. package/src/cli/commands/__tests__/numeric-flag-assembly.test.ts +1 -1
  130. package/src/cli/commands/__tests__/passes-gate-dead-options.test.ts +1 -1
  131. package/src/cli/commands/__tests__/registry.test.ts +1 -1
  132. package/src/cli/commands/__tests__/report-app-constraints.test.ts +67 -0
  133. package/src/cli/commands/constraints-pack-proposal.ts +172 -0
  134. package/src/cli/commands/constraints-reactivate.ts +197 -0
  135. package/src/cli/commands/constraints-report.ts +44 -2
  136. package/src/cli/commands/constraints-retire.ts +129 -36
  137. package/src/cli/commands/definitions.ts +36 -11
  138. package/src/cli/commands/knowledge-view.ts +9 -28
  139. package/src/cli/commands/report.ts +9 -5
  140. package/src/cli/commands/scaffold-templates.ts +0 -16
  141. package/src/cli/state-io.ts +8 -1
  142. package/src/core/CONTEXT.md +5 -4
  143. package/src/core/app-constraints-loader.ts +165 -0
  144. package/src/core/constraints/__tests__/collect-constraints.test.ts +3 -1
  145. package/src/core/constraints/__tests__/usage-report.test.ts +63 -0
  146. package/src/core/constraints/checker.ts +1 -1
  147. package/src/core/constraints/checkers/__tests__/templated-checkers.test.ts +180 -0
  148. package/src/core/constraints/checkers/__tests__/templates-registry.test.ts +81 -0
  149. package/src/core/constraints/checkers/index.ts +44 -10
  150. package/src/core/constraints/checkers/templated/file-exists.ts +75 -0
  151. package/src/core/constraints/checkers/templated/glob.ts +46 -0
  152. package/src/core/constraints/checkers/templated/index.ts +12 -0
  153. package/src/core/constraints/checkers/templated/regex-scan.ts +132 -0
  154. package/src/core/constraints/checkers/templates-registry.ts +22 -0
  155. package/src/core/constraints/checkers/types.ts +15 -0
  156. package/src/core/constraints/definitions/guidelines.ts +0 -15
  157. package/src/core/constraints/definitions/iron-laws.ts +16 -0
  158. package/src/core/constraints/usage-report.ts +129 -14
  159. package/src/core/project-config-loader.ts +19 -8
  160. package/src/gates/__tests__/command.test.ts +54 -0
  161. package/src/gates/command.ts +51 -0
  162. package/src/index.ts +20 -0
  163. package/src/knowledge/CONTEXT.md +7 -4
  164. package/src/knowledge/__tests__/lifecycle.test.ts +81 -0
  165. package/src/knowledge/__tests__/lint.test.ts +84 -0
  166. package/src/knowledge/__tests__/store.test.ts +70 -0
  167. package/src/knowledge/lifecycle.ts +49 -0
  168. package/src/knowledge/lint.ts +60 -1
  169. package/src/knowledge/store.ts +48 -11
  170. package/src/knowledge/types.ts +7 -1
  171. package/src/monitoring/CONTEXT.md +1 -1
  172. package/src/presets/__tests__/standard.test.ts +5 -4
  173. package/src/presets/standard.ts +2 -3
  174. package/src/types/constraint.ts +28 -0
  175. package/dist/cli/commands/posteval-plan.d.ts +0 -15
  176. package/dist/cli/commands/posteval-plan.d.ts.map +0 -1
  177. package/dist/cli/commands/posteval-plan.js +0 -86
  178. package/dist/cli/commands/posteval-plan.js.map +0 -1
  179. package/src/cli/commands/__tests__/posteval-plan.test.ts +0 -204
  180. package/src/cli/commands/posteval-plan.ts +0 -98
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * 项目配置加载器
3
3
  *
4
- * 解读 `.harness/config.yml` 与自定义约束文件、合并内置与项目自定义约束。
5
- * 本模块不碰文件系统:两份配置的读取口径 = 运行级观察面(`constraints/run-env.ts`,
6
- * ADR-0023 决策 2),一次运行内各至多读一次。
4
+ * 解读 `.harness/config.yml`、合并内置与应用层(`.harness/constraints.yml`,ADR-0033)约束。
5
+ * 本模块不碰文件系统:config.yml 的读取口径 = 运行级观察面(`constraints/run-env.ts`,
6
+ * ADR-0023 决策 2),一次运行内至多读一次;constraints.yml 的读取收在
7
+ * `app-constraints-loader.ts`(memo 口径与 rawConfig 同形)。
7
8
  */
8
9
 
9
10
  import type { Constraint } from '../types/constraint';
@@ -16,6 +17,7 @@ import type {
16
17
  import { CONSTRAINTS } from './constraints/definitions';
17
18
  import { PRESETS_BY_NAME, STANDARD_PRESET } from '../presets';
18
19
  import { filterEnabledEntries } from './effective-set';
20
+ import { loadAppConstraints } from './app-constraints-loader';
19
21
  import { resolveRunEnv, type RunEnv, type RunTarget } from './constraints/run-env';
20
22
 
21
23
  /**
@@ -131,12 +133,14 @@ export class ProjectConfigLoader {
131
133
  }
132
134
 
133
135
  /**
134
- * 合并内置约束(生效集完整链路,ADR-0029)
136
+ * 合并内置约束(生效集完整链路,ADR-0029 + ADR-0033
135
137
  *
136
- * 合并顺序:内置 → preset 裁剪 → config.yml `constraints.<id>.enabled:false` 删除。
138
+ * 合并顺序:内置 → preset 裁剪 → 并入应用层(`.harness/constraints.yml`)→
139
+ * config.yml `constraints.<id>.enabled:false` 删除(对内置与应用层同口径生效,
140
+ * retired 墓碑随 enabled:false 天然生效)。
137
141
  *
138
142
  * config.yml 中未知约束 id(如已移除约束的禁用残留)静默忽略,
139
- * 记录在结果 unknownIds 中供诊断。
143
+ * 记录在结果 unknownIds 中供诊断;应用层 id 是已知 id,不进 unknownIds。
140
144
  *
141
145
  * @param options.preset 覆盖 config.yml 的 preset(CLI --preset 用);
142
146
  * 未知预设名回落 standard + stderr 警告
@@ -184,9 +188,16 @@ export class ProjectConfigLoader {
184
188
  unknownIds,
185
189
  };
186
190
 
187
- // 1. 处理启用/禁用配置(未知 id 静默忽略,记录供诊断)
191
+ // 1. 并入应用层约束(.harness/constraints.yml,ADR-0033):先进桶再走 config.yml
192
+ // 筛选,enabled:false/retired 墓碑对应用层 id 才能同口径生效
193
+ const appConstraints = loadAppConstraints(this.env);
194
+ for (const c of appConstraints) {
195
+ result.constraints[c.id] = c;
196
+ }
197
+
198
+ // 2. 处理启用/禁用配置(未知 id 静默忽略,记录供诊断;应用层 id 属已知 id)
188
199
  if (this.config.constraints) {
189
- const knownIds = new Set(Object.keys(CONSTRAINTS));
200
+ const knownIds = new Set([...Object.keys(CONSTRAINTS), ...appConstraints.map(c => c.id)]);
190
201
  const filtered = filterEnabledEntries(knownIds, this.config.constraints);
191
202
  unknownIds.push(...filtered.unknownIds);
192
203
  for (const constraintId of filtered.disabledIds) {
@@ -196,4 +196,58 @@ describe('CommandGate', () => {
196
196
  expect(r.passed).toBe(true);
197
197
  });
198
198
  });
199
+
200
+ describe('git destructive rules(ADR-0031,票08 P1-6)', () => {
201
+ let gate: CommandGate;
202
+ beforeEach(() => { gate = new CommandGate(); });
203
+
204
+ it('should block git push --force', async () => {
205
+ const r = await gate.check('git push --force origin master');
206
+ expect(r.passed).toBe(false);
207
+ expect(r.details?.blocked.some((b: any) => b.id === 'git-push-force')).toBe(true);
208
+ });
209
+
210
+ it('should block git push -f and --force-with-lease', async () => {
211
+ expect((await gate.check('git push -f origin master')).passed).toBe(false);
212
+ expect((await gate.check('git push --force-with-lease')).passed).toBe(false);
213
+ });
214
+
215
+ it('should block git clean -f / -fd / -fdx', async () => {
216
+ for (const cmd of ['git clean -f', 'git clean -fd', 'git clean -fdx']) {
217
+ const r = await gate.check(cmd);
218
+ expect(r.passed).toBe(false);
219
+ expect(r.details?.blocked.some((b: any) => b.id === 'git-clean-force')).toBe(true);
220
+ }
221
+ });
222
+
223
+ it('should warn on git reset --hard / checkout -- / branch -D / filter-branch / reflog expire', async () => {
224
+ const cases: Array<[string, string]> = [
225
+ ['git reset --hard HEAD~3', 'git-reset-hard'],
226
+ ['git checkout -- src/foo.ts', 'git-checkout-discard'],
227
+ ['git branch -D feature-x', 'git-branch-force-delete'],
228
+ ['git filter-branch --tree-filter x', 'git-history-rewrite'],
229
+ ['git reflog expire --expire=now --all', 'git-reflog-expire'],
230
+ ];
231
+ for (const [cmd, id] of cases) {
232
+ const r = await gate.check(cmd);
233
+ expect(r.details?.warnings.some((w: any) => w.id === id)).toBe(true);
234
+ }
235
+ });
236
+
237
+ it('should allow safe git commands', async () => {
238
+ for (const cmd of [
239
+ 'git status',
240
+ 'git push origin master',
241
+ 'git reset --soft HEAD~1',
242
+ 'git clean -n',
243
+ 'git checkout -b feature',
244
+ 'git branch -d feature-x',
245
+ 'git commit -m "msg"',
246
+ ]) {
247
+ const r = await gate.check(cmd);
248
+ expect(r.passed).toBe(true);
249
+ expect(r.details?.warnings ?? []).toEqual([]);
250
+ }
251
+ });
252
+ });
199
253
  });
@@ -192,6 +192,57 @@ export const DEFAULT_COMMAND_BLACKLIST: CommandBlacklistRule[] = [
192
192
  message: '卸载 Python 包需要确认',
193
193
  category: 'package',
194
194
  },
195
+
196
+ // ========== git 破坏类(ADR-0031,wayfinder 票08 P1-6:只补不可逆损失面) ==========
197
+ {
198
+ id: 'git-push-force',
199
+ pattern: /\bgit\s+push\b[^|;&]*(--force\b|-f\b)/i,
200
+ level: 'block',
201
+ message: '禁止强推远程(覆盖共享历史不可逆)',
202
+ category: 'git',
203
+ },
204
+ {
205
+ id: 'git-clean-force',
206
+ pattern: /\bgit\s+clean\b[^|;&]*-[a-z]*f/i,
207
+ level: 'block',
208
+ message: '禁止 git clean -f(未跟踪文件删除不可恢复)',
209
+ category: 'git',
210
+ },
211
+ {
212
+ id: 'git-reset-hard',
213
+ pattern: /\bgit\s+reset\s+--hard\b/i,
214
+ level: 'warn',
215
+ message: 'git reset --hard 丢弃未提交改动,需要确认',
216
+ category: 'git',
217
+ },
218
+ {
219
+ id: 'git-checkout-discard',
220
+ pattern: /\bgit\s+checkout\s+--\s/i,
221
+ level: 'warn',
222
+ message: 'git checkout -- 丢弃工作区改动,需要确认',
223
+ category: 'git',
224
+ },
225
+ {
226
+ id: 'git-branch-force-delete',
227
+ pattern: /\bgit\s+branch\s+-D\b/,
228
+ level: 'warn',
229
+ message: 'git branch -D 强制删分支(未合并提交可能丢失),需要确认',
230
+ category: 'git',
231
+ },
232
+ {
233
+ id: 'git-history-rewrite',
234
+ pattern: /\bgit\s+(filter-branch|filter-repo)\b/i,
235
+ level: 'warn',
236
+ message: '历史重写操作不可逆,需要确认',
237
+ category: 'git',
238
+ },
239
+ {
240
+ id: 'git-reflog-expire',
241
+ pattern: /\bgit\s+reflog\s+expire\b/i,
242
+ level: 'warn',
243
+ message: 'reflog expire 拆除恢复网,需要确认',
244
+ category: 'git',
245
+ },
195
246
  ];
196
247
 
197
248
  /**
package/src/index.ts CHANGED
@@ -54,6 +54,26 @@ export {
54
54
  } from './core/effective-constraints';
55
55
  export type { EffectiveConfigLint } from './core/effective-constraints';
56
56
 
57
+ // ========================================
58
+ // 约束使用报告(退役候选诊断数据层,studio#602 E1 (a) 链路消费)
59
+ // ========================================
60
+ export {
61
+ buildConstraintsUsageReport,
62
+ diagnoseRetireCandidates,
63
+ collectUsageByConstraint,
64
+ readProjectTraces,
65
+ readProjectTracesReport,
66
+ CANDIDATE_KIND_LABEL,
67
+ DEFAULT_DIAGNOSE_THRESHOLDS,
68
+ } from './core/constraints/usage-report';
69
+ export type {
70
+ ConstraintUsageStats,
71
+ RetireCandidate,
72
+ RetireCandidateKind,
73
+ DiagnoseThresholds,
74
+ ConstraintsUsageReport,
75
+ } from './core/constraints/usage-report';
76
+
57
77
  // ========================================
58
78
  // 约束检查引擎(便捷 API;options 统一签名,ADR-0003)
59
79
  // ========================================
@@ -17,9 +17,9 @@
17
17
  - 所有模式支持 `decayAt` 硬过期
18
18
 
19
19
  ## 核心导出
20
- - `KnowledgeStore`(类型面)/ `FileKnowledgeStore`(`store.ts`,barrel 与包根交出的**值**符号——store 的文件系统实现,构造收 `{ baseDir }`;仓内构造点两处:`cli/commands/knowledge-view.ts` 的 `openKnowledgeStore()`(knowledge 各子命令的取数口)与 `cli/commands/constraints-retire.ts` 的退役沉淀写入)— 知识条目 CRUD + 结构化存储;`saveAll()` 全量批量写入(循环内只更新内存索引、结束一次 writeIndex,harness#107)、`applyAll()` 按 id 部分更新的批量出口(`update(id, partial)` 的批量形,语义含未知 id 逐条跳过与空批零读写,harness#134)、`getConsumptionStats()` 读 `.consumption-stats.json` 的当日消费计数(打分核心唯一的消费统计取数口,harness#134
20
+ - `KnowledgeStore`(类型面)/ `FileKnowledgeStore`(`store.ts`,barrel 与包根交出的**值**符号——store 的文件系统实现,构造收 `{ baseDir }`;仓内构造点两处:`cli/commands/knowledge-view.ts` 的 `openKnowledgeStore()`(knowledge 各子命令的取数口)与 `cli/commands/constraints-retire.ts` 的退役沉淀写入)— 知识条目 CRUD + 结构化存储;`saveAll()` 全量批量写入(循环内只更新内存索引、结束一次 writeIndex,harness#107)、`applyAll()` 按 id 部分更新的批量出口(`update(id, partial)` 的批量形,语义含未知 id 逐条跳过与空批零读写,harness#134)、`getConsumptionStats()` 读 `.consumption-stats.json` 的当日消费计数(打分核心唯一的消费统计取数口,harness#134)。**写入闸(E1 复盘修正 M1)**:save/saveAll/applyAll 落盘前校验 maturity/layer 枚举(update 经 save 继承),未声明值拒写抛错不静默降级;值域正本 = `types.ts` 的 `MATURITY_LEVELS`/`STORAGE_LAYERS` 常量(不进 barrel)。批量路径先整批预校验再落盘(任一脏值全批拒写);applyAll 批内同 id 多条按序累积合并语义不变
21
21
  - `KnowledgeQuery` — 语义搜索 + 类型/标签过滤 + `queryByMode()` + `consume(taskContext)`;`query()` 是 budget 截断管线(仅用于 prompt 注入),`search()` 是全语料文本搜索(先匹配→排序→limit,交互搜索专用,勿用 query() 代替)
22
- - `KnowledgeLifecycle` — per-mode 生命周期管理;缺省阈值常量 `DEFAULT_DECAY_CONFIG`(`provenDecayMonths: 12` / `verifiedDecayMonths: 6` / `draftDecayMonths: 3` / `autoPromoteSources: []`,正本 `types.ts`)经构造器 `{...DEFAULT_DECAY_CONFIG, ...config}` 合并,barrel 与包根一并交出
22
+ - `KnowledgeLifecycle` — per-mode 生命周期管理;缺省阈值常量 `DEFAULT_DECAY_CONFIG`(`provenDecayMonths: 12` / `verifiedDecayMonths: 6` / `draftDecayMonths: 3` / `autoPromoteSources: []`,正本 `types.ts`)经构造器 `{...DEFAULT_DECAY_CONFIG, ...config}` 合并,barrel 与包根一并交出;候选判定两族同位(tryPromote 升 proven 后自动触发):`checkSkillCandidate`(AC-8a)与 `checkConstraintCandidate`(块 3 子项 6,ADR-0033——口径 = proven + 未打标 + referencedBy≥5 + contributors≥3 个不同来源会话,打 `constraintCandidate` 标 + `constraintCandidate:marked` 事件,与 skillCandidate 互不干扰可同挂)
23
23
  - `extractCodeStructure(dir)`(`primitives/code-structure.ts`,配 `CodeStructure`/`DeclarationInfo`/`ImportInfo` 三类型对外)— 目录代码结构提取;仓内零调用方,是留给下游的公开面
24
24
  - `KnowledgeIngest` — 知识摄取引擎(含 ingest gate 质量门)+ `ingestExternal()` 外部内容摄入
25
25
  - `sanitizeExternalContent` — 外部内容安全清洗(过滤注入模式 + 长度限制)
@@ -29,7 +29,7 @@
29
29
  - `tree-walker`(包内,不进导出面)— 知识树排除口径单点(harness#134):`isEntryFile()` / `isInfraDir()` / `INDEX_MD_FILE` / `SNAPSHOTS_DIR`,由 store 顶层扫描、migration 顶层扫描、index-generator 递归扫描三处共同消费
30
30
  - `flywheel-metrics`(包内,不进导出面)— 知识飞轮指标唯一实现:`evaluateFlywheel(env)` 出 canonical 比例、`genuineRefs()` 出 synthetic 过滤口径,audit D6 / `knowledge stats` / `knowledge health` 三处共消费(ADR-0013)
31
31
  - `ReferenceTracker` — 知识引用关系图谱
32
- - `KnowledgeLinter` — 知识质量检查(完整性/一致性/时效性)
32
+ - `KnowledgeLinter` — 知识质量检查(完整性/一致性/时效性);maturity/layer 枚举校验两个入口共用判定正本 `invalidEnumIssues`:`run()` 经 `checkInvalidEnums()` 扫存量(缺字段即脏、issue 带 entryId,`invalid_enum` 无 autoFix 分支——映射需人工裁定),`validateEntry()` 管摄入前(字段缺省跳过以兼容只传四字段的 pre-ingest 调用方)
33
33
  - `ColdStartImporter` — 冷启动知识导入
34
34
  - `KnowledgeHealthScorer` — 知识健康评分(doctor.ts)
35
35
 
@@ -43,6 +43,7 @@
43
43
  - 知识条目文件的 frontmatter 语法只由 `src/utils/frontmatter` 定义(harness#89):缺头/空 meta = `absent`(合法输入,按非条目静默处理、不上报——migration 对 `absent` 不计 `errors`、不迁移,只计 `skipped` 保证 `total` 计数闭环,harness#161 对齐 #89 裁决 2);未闭合/YAML 非法 = `malformed`(必须显式上报后按消费方语义恢复——migration 落 `errors`、store 与 index-generator 打一行 stderr 后跳过或走 best-effort),禁止静默丢条目;canonical 字段序是 `store.toFrontmatter` 的私有策略,`join` 只管包裹格式
44
44
  - **知识树的排除口径只有一个正本 `tree-walker`**(harness#134):`_index.md`(索引生成物)、`.snapshots`、`.archive` / `archived`、`resolutions` 是树基建、不是条目人口。store 与 migration 的顶层扫描、index-generator 的递归扫描三处都走它;新增 walker 禁止另立排除清单。统一的是**排除口径**不是遍历深度(store/migration 顶层、index-generator 递归)。例外须原地记名理由:`import.ts` 的 docs 扫描吃的是**项目文档树**,本口径在它那里没有对应物
45
45
  - **循环内禁止逐条 `store.update()`**(harness#134):`update()` 的形状是 get→save、`save()` 每次全量重写 index.json,N 条修复 = N 次全量重写。批量形是 `applyAll(id → partial)`;一次 `audit --fix`、一轮 `runDecayCycle()`、一次 `updateReferencedBy()` 各只重写一次索引,计数闸见 `audit-write-count.test.ts` 与 `lifecycle.test.ts` / `reference-tracker.test.ts` 的 stringify 计数项
46
+ - **写入闸不管读**(E1 复盘修正 M1):枚举校验只在 save/saveAll/applyAll/update 落盘前执行,读取(parseFile)不校验——盘上脏条目可读可列,但任一写入路径触及即抛错(含「只改其他字段」的部分更新),先修脏值再写;绕过本 store 直写文件的外部写入方不受闸约束
46
47
  - **审计判定脱离文件系统可测**(harness#134):规则表与 D1–D7 打分住 `audit-scoring.ts`,零 fs(源形状闸钉在 `audit-scoring.test.ts`);引擎 `audit.ts` 只做 store 装配,环境数据经 `store.getConsumptionStats()` / `getSurvivalRate()` 取好喂入
47
48
  - 知识条目有明确的生命周期状态(按 consumptionMode 分化)
48
49
  - 约束退役(`harness constraints retire`,人确认)时写入 KnowledgeStore:规则原文 + 退役原因 + 历史统计
@@ -56,7 +57,9 @@
56
57
  ## 注意事项
57
58
  - Phase 1+4 实现的知识引擎核心
58
59
  - 约束"退役不删除"——retire 落盘 config.yml `enabled: false` + retired 元数据,保留规则原文 + 退役原因 + 历史统计(可回滚)
59
- - `MaturityLevel` 包含 6 个值: draft/verified/proven/archived/active/deprecated
60
+ - `MaturityLevel` / `StorageLayer` 的值域不抄清单——运行时正本是 `types.ts` 的 `MATURITY_LEVELS` / `STORAGE_LAYERS` 常量(写入闸与 lint 枚举校验共用)
60
61
  - `excludeArchived` 同时排除 archived 和 deprecated
62
+ - `list()` 的过滤层是 `index.json`(tags/types 匹配都打在 IndexEntry 上),返回体才回读 .md 全文——手工清洗/直改 .md 数据区时须同步 index(`rebuildIndex()` 或同改 index.json),否则过滤口径与文件内容漂移(#614 存量 tags 清洗同步了两边)
63
+ - tag 写入纪律(#614 审计):凡 append tag 处先判存在或走 Set 去重——`audit.ts`/`ingest.ts` 的 low_quality 有 includes 闸、ingest 合并走 `new Set`、`lifecycle.checkSkillCandidate`/`checkConstraintCandidate` 由前置谓词挡住重复;studio 侧 rule-scanner/pattern-miner 曾因缺闸造成实盘 tags 千次重复腐蚀
61
64
  - 仍有两处按**原始** `referencedBy.length` 判定,不属飞轮指标、ADR-0013 明确列为范围外:`lifecycle.ts` 的 signal 饱和 / reference 激活(退役阈值调整另票)、`knowledge health` 的 D1「verified 零引用」线索提示(逐条 issue 线索,非聚合分子)
62
65
  - #134 收口后仍在的逐条写点(同型问题,本票裁决点名的循环之外,需要时另票):`lint.ts` 的三处 autoFix `store.update()`、`lifecycle.recordReference()`(逐事件调用,返回更新后条目并触发回调,批量化会改它的契约)、`ingest.mergeEntries()`;`store.list()` 每条一次 `findFile()` 线性扫索引的 O(N²) 按 #134 裁决**未动**,待把知识树实际规模重新量一次再判是否单开票
@@ -956,4 +956,85 @@ describe('KnowledgeLifecycle', () => {
956
956
  expect(lifecycle.checkEntryDecay('SIG-001')).toBeUndefined();
957
957
  });
958
958
  });
959
+
960
+ describe('checkConstraintCandidate(块 3 子项 6,ADR-0033)', () => {
961
+ /** 达标基线:proven + 5 条引用 + 3 个不同来源会话 */
962
+ const eligible = (overrides?: Partial<KnowledgeEntry>): KnowledgeEntry => makeEntry({
963
+ maturity: 'proven',
964
+ referencedBy: ['a:2026-05-01', 'b:2026-05-02', 'c:2026-05-03', 'a:2026-05-04', 'b:2026-05-05'],
965
+ contributors: ['a', 'b', 'c'],
966
+ ...overrides,
967
+ });
968
+
969
+ it('达标:打 constraintCandidate 标 + 触发 constraintCandidate:marked 事件,返回 true', () => {
970
+ const events: Array<{ entryId: string; context?: string }> = [];
971
+ lifecycle.onReference(e => events.push({ entryId: e.entryId, context: e.context }));
972
+ store.save(eligible());
973
+
974
+ expect(lifecycle.checkConstraintCandidate('DEC-001')).toBe(true);
975
+ expect(store.get('DEC-001')!.tags).toContain('constraintCandidate');
976
+ expect(events).toEqual([{ entryId: 'DEC-001', context: 'constraintCandidate:marked' }]);
977
+ });
978
+
979
+ it('不达标不打标:非 proven / 引用不足 5 / 来源会话不足 3', () => {
980
+ store.save(eligible({ id: 'E-1', maturity: 'verified' }));
981
+ store.save(eligible({ id: 'E-2', referencedBy: ['a:2026-05-01', 'b:2026-05-02', 'c:2026-05-03', 'a:2026-05-04'] }));
982
+ store.save(eligible({ id: 'E-3', contributors: ['a', 'b'] }));
983
+
984
+ for (const id of ['E-1', 'E-2', 'E-3']) {
985
+ expect(lifecycle.checkConstraintCandidate(id)).toBe(false);
986
+ expect(store.get(id)!.tags).not.toContain('constraintCandidate');
987
+ }
988
+ });
989
+
990
+ it('幂等:已打标不重复打、不重复触发事件', () => {
991
+ const events: string[] = [];
992
+ lifecycle.onReference(e => { if (e.context) events.push(e.context); });
993
+ store.save(eligible({ tags: ['constraintCandidate'] }));
994
+
995
+ expect(lifecycle.checkConstraintCandidate('DEC-001')).toBe(false);
996
+ expect(store.get('DEC-001')!.tags.filter(t => t === 'constraintCandidate')).toHaveLength(1);
997
+ expect(events).toEqual([]);
998
+ });
999
+
1000
+ it('不存在的条目返回 false', () => {
1001
+ expect(lifecycle.checkConstraintCandidate('NON-EXISTENT')).toBe(false);
1002
+ });
1003
+
1004
+ it('与 skillCandidate 互不干扰:达标条目可同时挂两标', () => {
1005
+ store.save(eligible({
1006
+ content: 'a'.repeat(200),
1007
+ executionResults: [
1008
+ { contributor: 'a', success: true, timestamp: '2026-05-01' },
1009
+ { contributor: 'b', success: true, timestamp: '2026-05-02' },
1010
+ { contributor: 'c', success: true, timestamp: '2026-05-03' },
1011
+ { contributor: 'a', success: true, timestamp: '2026-05-04' },
1012
+ { contributor: 'b', success: true, timestamp: '2026-05-05' },
1013
+ ],
1014
+ }));
1015
+
1016
+ expect(lifecycle.checkSkillCandidate('DEC-001')).toBe(true);
1017
+ expect(lifecycle.checkConstraintCandidate('DEC-001')).toBe(true);
1018
+ const tags = store.get('DEC-001')!.tags;
1019
+ expect(tags).toContain('skillCandidate');
1020
+ expect(tags).toContain('constraintCandidate');
1021
+ });
1022
+
1023
+ it('tryPromote 升到 proven 后自动触发判定(与 checkSkillCandidate 同位)', () => {
1024
+ const events: string[] = [];
1025
+ lifecycle.onReference(e => { if (e.context) events.push(e.context); });
1026
+ store.save(makeEntry({
1027
+ maturity: 'verified',
1028
+ content: 'a'.repeat(100),
1029
+ contributors: ['a', 'b', 'c'],
1030
+ projects: ['p1', 'p2'],
1031
+ referencedBy: ['a:2026-05-01', 'b:2026-05-02', 'c:2026-05-03', 'a:2026-05-04', 'b:2026-05-05'],
1032
+ }));
1033
+
1034
+ const change = lifecycle.tryPromote('DEC-001');
1035
+ expect(change?.to).toBe('proven');
1036
+ expect(store.get('DEC-001')!.tags).toContain('constraintCandidate');
1037
+ expect(events).toContain('constraintCandidate:marked');
1038
+ });
1039
+ });
959
1040
  });
@@ -57,6 +57,7 @@ describe('KnowledgeLinter', () => {
57
57
  expect(report.fixed).toBe(0);
58
58
  });
59
59
 
60
+
60
61
  it('应该检测所有问题类型', () => {
61
62
  // 孤儿条目
62
63
  saveEntry({ id: 'orphan-1', title: 'Orphan', maturity: 'draft', contributors: [], projects: [] });
@@ -79,6 +80,39 @@ describe('KnowledgeLinter', () => {
79
80
  });
80
81
  });
81
82
 
83
+ // M1 收口:run() 存量扫描必须报出 maturity/layer 脏值(spec:lint 能报出存量脏条目)
84
+ describe('run 存量枚举校验', () => {
85
+ /** 绕过写入闸直写脏条目(模拟实盘直写路径) */
86
+ function writeDirtyFile(name: string, fm: string) {
87
+ const dir = path.join(tmpDir, 'knowledge');
88
+ fs.writeFileSync(path.join(dir, name), `---\n${fm}\n---\n\nbody content long enough.\n`, 'utf-8');
89
+ store.rebuildIndex();
90
+ }
91
+
92
+ it('盘上脏 maturity / 脏 layer 的存量条目 → run() 报 invalid_enum', () => {
93
+ writeDirtyFile('resolution-res_a.md',
94
+ 'id: res_a\ntype: resolution\ntitle: Dirty M\nmaturity: pending\nlayer: project\ncreated: 2026-09-01T00:00:00.000Z');
95
+ writeDirtyFile('resolution-res_b.md',
96
+ 'id: res_b\ntype: resolution\ntitle: Dirty L\nmaturity: draft\nlayer: L3_tool_behavior\ncreated: 2026-09-01T00:00:00.000Z');
97
+ writeDirtyFile('resolution-res_c.md',
98
+ 'id: res_c\ntype: resolution\ntitle: Missing Layer\nmaturity: draft\ncreated: 2026-09-01T00:00:00.000Z');
99
+
100
+ const report = linter.run();
101
+ const enumIssues = report.issues.filter(i => i.type === 'invalid_enum');
102
+ expect(enumIssues).toHaveLength(3);
103
+ expect(enumIssues.map(i => i.entryId).sort()).toEqual(['res_a', 'res_b', 'res_c']);
104
+ expect(report.summary.invalid_enum).toBe(3);
105
+ });
106
+
107
+ it('全合法条目库 → 零 invalid_enum(存量合法条目无新噪音)', () => {
108
+ saveEntry({ id: 'clean-1', maturity: 'verified', layer: 'project' });
109
+ saveEntry({ id: 'clean-2', maturity: 'active', layer: 'tech', title: 'Another' });
110
+
111
+ const report = linter.run();
112
+ expect(report.summary.invalid_enum).toBe(0);
113
+ });
114
+ });
115
+
82
116
  describe('checkIndexConsistency', () => {
83
117
  it('应该检测索引中有但文件不存在的条目', () => {
84
118
  saveEntry({ id: 'entry-1' });
@@ -370,6 +404,56 @@ describe('KnowledgeLinter', () => {
370
404
  });
371
405
  expect(issues.some(i => i.type === 'duplicate')).toBe(true);
372
406
  });
407
+
408
+ // E1 复盘修正 M1:maturity/layer 枚举值域校验(实盘脏值:pending/canonical/L3_tool_behavior)
409
+ it('flags undeclared maturity value', () => {
410
+ const issues = linter.validateEntry({
411
+ title: 'Specific Entry With Valid Title',
412
+ content: 'This is a sufficiently long content for the enum check test case.',
413
+ tags: [],
414
+ type: 'decision',
415
+ maturity: 'pending',
416
+ });
417
+ const issue = issues.find(i => i.type === 'invalid_enum');
418
+ expect(issue).toBeDefined();
419
+ expect(issue!.severity).toBe('high');
420
+ expect(issue!.description).toContain('pending');
421
+ });
422
+
423
+ it('flags undeclared layer value', () => {
424
+ const issues = linter.validateEntry({
425
+ title: 'Specific Entry With Valid Title',
426
+ content: 'This is a sufficiently long content for the enum check test case.',
427
+ tags: [],
428
+ type: 'decision',
429
+ layer: 'L3_tool_behavior',
430
+ });
431
+ const issue = issues.find(i => i.type === 'invalid_enum');
432
+ expect(issue).toBeDefined();
433
+ expect(issue!.description).toContain('L3_tool_behavior');
434
+ });
435
+
436
+ it('accepts declared maturity/layer values', () => {
437
+ const issues = linter.validateEntry({
438
+ title: 'Specific Entry With Valid Title',
439
+ content: 'This is a sufficiently long content for the enum check test case.',
440
+ tags: [],
441
+ type: 'decision',
442
+ maturity: 'verified',
443
+ layer: 'project',
444
+ });
445
+ expect(issues.filter(i => i.type === 'invalid_enum')).toHaveLength(0);
446
+ });
447
+
448
+ it('skips enum check when maturity/layer are not provided (pre-ingest caller compat)', () => {
449
+ const issues = linter.validateEntry({
450
+ title: 'Specific Entry With Valid Title',
451
+ content: 'This is a sufficiently long content for the enum check test case.',
452
+ tags: [],
453
+ type: 'decision',
454
+ });
455
+ expect(issues.filter(i => i.type === 'invalid_enum')).toHaveLength(0);
456
+ });
373
457
  });
374
458
 
375
459
  describe('autoFix', () => {
@@ -174,6 +174,76 @@ describe('KnowledgeStore', () => {
174
174
  });
175
175
  });
176
176
 
177
+ // 写入闸(E1 复盘修正 M1):maturity/layer 是声明枚举,未声明值拒写报错,不静默降级
178
+ describe('schema gate: maturity/layer 枚举校验', () => {
179
+ it('save 拒绝未声明的 maturity 值,不落盘', () => {
180
+ expect(() => store.save(makeEntry({ maturity: 'pending' as KnowledgeEntry['maturity'] })))
181
+ .toThrow(/maturity.*pending/);
182
+ expect(store.get('DEC-001')).toBeUndefined();
183
+ expect(store.readIndex()).toHaveLength(0);
184
+ });
185
+
186
+ it('save 拒绝未声明的 layer 值,不落盘', () => {
187
+ expect(() => store.save(makeEntry({ layer: 'L3_tool_behavior' as KnowledgeEntry['layer'] })))
188
+ .toThrow(/layer.*L3_tool_behavior/);
189
+ expect(store.get('DEC-001')).toBeUndefined();
190
+ });
191
+
192
+ it('save 拒绝缺失 layer 的条目', () => {
193
+ const entry = makeEntry();
194
+ delete (entry as Partial<KnowledgeEntry>).layer;
195
+ expect(() => store.save(entry)).toThrow(/layer/);
196
+ expect(store.get('DEC-001')).toBeUndefined();
197
+ });
198
+
199
+ it('update 传入脏 maturity 值 → 拒写,既有条目不变', () => {
200
+ store.save(makeEntry());
201
+ expect(() => store.update('DEC-001', { maturity: 'canonical' as KnowledgeEntry['maturity'] }))
202
+ .toThrow(/maturity.*canonical/);
203
+ expect(store.get('DEC-001')!.maturity).toBe('draft');
204
+ });
205
+
206
+ it('存量脏条目(绕过闸直写文件)经 update 修补其他字段 → 拒写报错', () => {
207
+ // 模拟实盘脏条目:resolution.service 直写文件的形状(maturity/layer 均未声明)
208
+ fs.writeFileSync(
209
+ path.join(tempDir, 'resolution-res_1.md'),
210
+ '---\nid: res_1\ntype: resolution\ntitle: Dirty\nmaturity: pending\nlayer: L3_tool_behavior\ncreated: 2026-09-01T00:00:00.000Z\n---\n\nbody\n',
211
+ 'utf-8'
212
+ );
213
+ store.rebuildIndex();
214
+ expect(() => store.update('res_1', { tags: ['touched'] })).toThrow(/maturity.*pending/);
215
+ });
216
+
217
+ it('存量脏条目经 update 把脏字段修成合法值 → 放行(清洗路径)', () => {
218
+ fs.writeFileSync(
219
+ path.join(tempDir, 'resolution-res_2.md'),
220
+ '---\nid: res_2\ntype: resolution\ntitle: Dirty\nmaturity: pending\nlayer: L3_tool_behavior\ncreated: 2026-09-01T00:00:00.000Z\n---\n\nbody\n',
221
+ 'utf-8'
222
+ );
223
+ store.rebuildIndex();
224
+ const updated = store.update('res_2', { maturity: 'draft', layer: 'project' });
225
+ expect(updated!.maturity).toBe('draft');
226
+ expect(store.get('res_2')!.layer).toBe('project');
227
+ });
228
+
229
+ it('saveAll 含一条脏值 → 整批拒写(合法条目也不落盘)', () => {
230
+ expect(() => store.saveAll([
231
+ makeEntry({ id: 'DEC-001' }),
232
+ makeEntry({ id: 'DEC-002', layer: 'process' as KnowledgeEntry['layer'] }),
233
+ ])).toThrow(/layer.*process/);
234
+ expect(store.get('DEC-001')).toBeUndefined();
235
+ expect(store.readIndex()).toHaveLength(0);
236
+ });
237
+
238
+ it('applyAll 部分更新把 maturity 改成脏值 → 拒写', () => {
239
+ store.save(makeEntry());
240
+ expect(() => store.applyAll([
241
+ { id: 'DEC-001', partial: { maturity: 'pending' as KnowledgeEntry['maturity'] } },
242
+ ])).toThrow(/maturity.*pending/);
243
+ expect(store.get('DEC-001')!.maturity).toBe('draft');
244
+ });
245
+ });
246
+
177
247
  describe('rebuildIndex', () => {
178
248
  it('should rebuild index from files', () => {
179
249
  store.save(makeEntry({ id: 'DEC-001' }));
@@ -199,8 +199,10 @@ export class KnowledgeLifecycle {
199
199
  this.store.update(entryId, { maturity: target });
200
200
 
201
201
  // AC-8a: After promotion, check skillCandidate eligibility
202
+ // 块 3 子项 6(ADR-0033):同位触发 constraintCandidate 判定——两标独立,可同挂
202
203
  if (target === 'proven') {
203
204
  this.checkSkillCandidate(entryId);
205
+ this.checkConstraintCandidate(entryId);
204
206
  }
205
207
 
206
208
  return change;
@@ -253,6 +255,53 @@ export class KnowledgeLifecycle {
253
255
  return true;
254
256
  }
255
257
 
258
+ /**
259
+ * 块 3 子项 6(ADR-0033):Check if a proven entry qualifies as a constraintCandidate.
260
+ * Marks with 'constraintCandidate' tag and emits event if eligible.
261
+ * Returns true if newly marked.
262
+ *
263
+ * 判定口径(人类已批准,设计稿 Q4):proven + 未打过标 + references≥5
264
+ * (referencedBy 条数,与 recordReference 的 refKey 口径一致)+ ≥3 个不同来源会话
265
+ * (contributors,与 skill 候选同字段)。与 skillCandidate 互不干扰,可同挂两标。
266
+ */
267
+ checkConstraintCandidate(entryId: string): boolean {
268
+ const entry = this.store.get(entryId);
269
+ if (!entry) return false;
270
+ if (!this.isConstraintCandidate(entry)) return false;
271
+
272
+ // Mark as constraintCandidate
273
+ this.store.update(entryId, {
274
+ tags: [...entry.tags, 'constraintCandidate'],
275
+ });
276
+
277
+ // Emit event for downstream consumers
278
+ const now = new Date().toISOString();
279
+ for (const cb of this.onReferenceCallbacks) {
280
+ try {
281
+ cb({
282
+ entryId,
283
+ contributor: 'lifecycle',
284
+ timestamp: now,
285
+ context: 'constraintCandidate:marked',
286
+ });
287
+ } catch { /* non-blocking */ }
288
+ }
289
+
290
+ return true;
291
+ }
292
+
293
+ /**
294
+ * 块 3 子项 6:Check if an entry meets constraintCandidate criteria.
295
+ */
296
+ private isConstraintCandidate(entry: KnowledgeEntry): boolean {
297
+ if (entry.maturity !== 'proven') return false;
298
+ if (entry.tags.includes('constraintCandidate')) return false;
299
+ if (entry.referencedBy.length < 5) return false;
300
+ if (entry.contributors.length < 3) return false;
301
+
302
+ return true;
303
+ }
304
+
256
305
  /**
257
306
  * AC-8c: Remove skillCandidate tag if success rate drops below 50%.
258
307
  * Called after execution writeback.