@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
@@ -12,11 +12,21 @@
12
12
  * retired: { at, reason, stats: { total, fail, failRate } }
13
13
  *
14
14
  * 每条同时写一条 KnowledgeStore 记录(consumptionMode: 'signal')。
15
+ baseDir 不硬编码 projectRoot 拼接,走 openKnowledgeStore 同一解析点(harness#177):
16
+ 缺省与 `harness knowledge` 读口同根,KNOWLEDGE_BASE_DIR 覆盖对写口同步生效。
15
17
  *
16
- * ADR-0029:custom 纯文本约束与治理注入段同步已随文本注入层关停一并退役,
17
- * retire 只处理内置 check 约束。
18
+ * ADR-0029:custom 纯文本约束与治理注入段同步已随文本注入层关停一并退役。
19
+ * ADR-0032 观察名单(块 3 子项 4):零拦截命中先进观察名单(`.harness/.state.json`
20
+ * `constraintWatchlist` 段,StateIO 读-改-写),挂一个季度且样本足才转退役候选——
21
+ * 交互模式的候选清单因此不含观察期内的零拦截约束。
22
+ * ADR-0033:retire 处理内置 + 应用层(`.harness/constraints.yml`)check 约束;
23
+ * 应用层退休墓碑同样写 config.yml,constraints.yml 条文保留不删(退休=停用不是删除),
24
+ * 沉淀条目 tags 加 `source:app`。
18
25
  *
19
- * retire 不是删除——恢复方法:删 config.yml constraints.<id> 段。
26
+ * retire 不是删除——恢复走 `harness constraints reactivate <id>`(ADR-0032 决策 6.5:
27
+ * 复活不改历史,写 constraint-reactivated-<id> 新条目;手动删段无沉淀,不提倡)。
28
+ * already_retired 幂等只认 retired 墓碑(ADR-0032 决策 6.6,票 02 断点 6):
29
+ * 裸 enabled:false 是"禁用"不是"退休",会被退休流程接管补上墓碑与沉淀。
20
30
  *
21
31
  * 交互与执行分离:retireConstraint 为纯执行逻辑(同步、可测),
22
32
  * runRetireInteractive 只做 readline 交互,IO 流可注入。
@@ -30,15 +40,19 @@ import * as yaml from 'js-yaml';
30
40
  import chalk from 'chalk';
31
41
  import { getConstraint } from '../../core/constraints/definitions';
32
42
  import { ProjectConfigLoader } from '../../core/project-config-loader';
33
- import { FileKnowledgeStore } from '../../knowledge/store';
43
+ import { loadAppConstraints } from '../../core/app-constraints-loader';
44
+ import type { RunTarget } from '../../core/constraints/run-env';
34
45
  import type { KnowledgeEntry } from '../../knowledge/types';
35
46
  import type { Constraint } from '../../types/constraint';
47
+ import { openKnowledgeStore } from './knowledge-view';
48
+ import { fileStateIO } from '../state-io';
36
49
  import {
37
50
  buildConstraintsUsageReport,
38
51
  CANDIDATE_KIND_LABEL,
39
52
  collectUsageByConstraint,
40
53
  readProjectTraces,
41
54
  readProjectTracesReport,
55
+ WATCHLIST_PERIOD_DAYS,
42
56
  } from '../../core/constraints/usage-report';
43
57
 
44
58
  export interface RetireExecuteOptions {
@@ -46,6 +60,8 @@ export interface RetireExecuteOptions {
46
60
  reason?: string;
47
61
  /** 注入当前时间(测试用) */
48
62
  now?: Date;
63
+ /** 知识库路径解析的 io(legacy 兜底告警走 stderr 需要;缺省 processIO) */
64
+ io?: CommandIO;
49
65
  }
50
66
 
51
67
  export type RetireStatus = 'retired' | 'already_retired' | 'unknown_id';
@@ -58,6 +74,8 @@ export interface RetireResult {
58
74
  stats: { total: number; fail: number; failRate: number };
59
75
  /** KnowledgeStore 条目 id(status='retired' 时存在) */
60
76
  knowledgeEntryId?: string;
77
+ /** 退役记录实际落盘的知识库根(status='retired' 时存在,harness#177) */
78
+ knowledgeBaseDir?: string;
61
79
  }
62
80
 
63
81
  export interface ConstraintsRetireOptions {
@@ -67,28 +85,48 @@ export interface ConstraintsRetireOptions {
67
85
  yes?: boolean;
68
86
  }
69
87
 
70
- interface RetireTargetInfo {
88
+ export interface RetireTargetInfo {
71
89
  severity: Constraint['severity'];
72
90
  description?: string;
73
91
  rule?: string;
74
92
  message?: string;
93
+ /** 约束来源(ADR-0033):app 层退休沉淀条目 tags 加 source:app */
94
+ source?: Constraint['source'];
75
95
  }
76
96
 
77
97
  /**
78
- * 查找约束定义(内置 definitions)
98
+ * 查找约束定义(内置 definitions + 应用层 constraints.yml,ADR-0033
79
99
  *
80
100
  * loader 由调用方给(一次退役一份观察面,见 retireConstraint):本函数只读它的装载结果,
81
- * 不再自造 ProjectConfigLoader
101
+ * 不再自造 ProjectConfigLoader。应用层查找经 target 参数(项目根路径或观察面),
102
+ * 不传 = 只查内置(历史行为)。
103
+ *
104
+ * 导出给 constraints-reactivate 复用(复活同样认内置 + 应用层 check 约束)。
82
105
  */
83
- function findRetireTarget(id: string): RetireTargetInfo | undefined {
106
+ export function findRetireTarget(id: string, target?: RunTarget): RetireTargetInfo | undefined {
84
107
  const builtIn = getConstraint(id);
85
- if (!builtIn) return undefined;
86
- return {
87
- severity: builtIn.severity,
88
- description: builtIn.description,
89
- rule: builtIn.rule,
90
- message: builtIn.message,
91
- };
108
+ if (builtIn) {
109
+ return {
110
+ severity: builtIn.severity,
111
+ description: builtIn.description,
112
+ rule: builtIn.rule,
113
+ message: builtIn.message,
114
+ source: 'builtin',
115
+ };
116
+ }
117
+ if (target !== undefined) {
118
+ const app = loadAppConstraints(target).find(c => c.id === id);
119
+ if (app) {
120
+ return {
121
+ severity: app.severity,
122
+ description: app.description,
123
+ rule: app.rule,
124
+ message: app.message,
125
+ source: 'app',
126
+ };
127
+ }
128
+ }
129
+ return undefined;
92
130
  }
93
131
 
94
132
  /**
@@ -126,17 +164,45 @@ function setYamlEntry(
126
164
  }
127
165
  }
128
166
 
167
+ /**
168
+ * YAML 条目的删除单点(constraints reactivate 用,与 setYamlEntry 同一读-改-写口径)
169
+ *
170
+ * 删除 config.yml `section.id` 整个 key;段/条目不存在时零写盘。返回是否实际删除。
171
+ */
172
+ export function removeYamlEntry(filePath: string, section: string, id: string): boolean {
173
+ if (!fs.existsSync(filePath)) return false;
174
+ const raw = (yaml.load(fs.readFileSync(filePath, 'utf-8')) as Record<string, unknown>) ?? {};
175
+ const entries = (raw[section] ?? {}) as Record<string, unknown>;
176
+ if (!(id in entries)) return false;
177
+ delete entries[id];
178
+ raw[section] = entries;
179
+ fs.writeFileSync(filePath, yaml.dump(raw, { lineWidth: 120 }), 'utf-8');
180
+ return true;
181
+ }
182
+
183
+ /**
184
+ * 落盘后 commit 提示(票 02 断点 4):仅 git 仓内提示,不替用户动 git。
185
+ * studio 审卡通道由 applier 自动 commit,本提示面向 CLI 直达/裸项目场景。
186
+ */
187
+ export function logCommitHint(projectRoot: string, io: CommandIO): void {
188
+ if (!fs.existsSync(path.join(projectRoot, '.git'))) return;
189
+ log(io, chalk.gray(' 提示:config.yml 已改未提交——git add .harness/config.yml && git commit(studio 审卡通道会自动 commit)'));
190
+ }
191
+
129
192
  /**
130
193
  * 写 KnowledgeStore 退役记录(consumptionMode: 'signal')
194
+ *
195
+ * baseDir 走 openKnowledgeStore 同一解析点(harness#177):缺省与 knowledge 读口同根,
196
+ * 不再硬编码 projectRoot 拼接(那会写进没有任何默认读口的 <repo>/.harness/knowledge)。
131
197
  */
132
198
  function saveRetireKnowledge(
133
- projectRoot: string,
134
199
  id: string,
135
200
  target: RetireTargetInfo,
136
201
  reason: string,
137
202
  stats: { total: number; fail: number; failRate: number },
138
- iso: string
139
- ): string {
203
+ iso: string,
204
+ io: CommandIO
205
+ ): { entryId: string; baseDir: string } {
140
206
  const entryId = `constraint-retired-${id}`;
141
207
  const contentLines = [
142
208
  `# 约束退役:${id}`,
@@ -171,7 +237,13 @@ function saveRetireKnowledge(
171
237
  lastReferenced: iso,
172
238
  contributors: [],
173
239
  projects: [],
174
- tags: ['constraint-retired', `constraint:${id}`, `severity:${target.severity}`],
240
+ // ADR-0033:应用层退休沉淀带 source:app 标签(内置条目不加,历史形状不动)
241
+ tags: [
242
+ 'constraint-retired',
243
+ `constraint:${id}`,
244
+ `severity:${target.severity}`,
245
+ ...(target.source === 'app' ? ['source:app'] : []),
246
+ ],
175
247
  applicablePhases: [],
176
248
  sourceReferences: [{ timestamp: iso }],
177
249
  referencedBy: [],
@@ -180,9 +252,9 @@ function saveRetireKnowledge(
180
252
  origin: 'human',
181
253
  };
182
254
 
183
- const store = new FileKnowledgeStore({ baseDir: path.join(projectRoot, '.harness', 'knowledge') });
255
+ const store = openKnowledgeStore({}, io);
184
256
  store.save(entry);
185
- return entryId;
257
+ return { entryId, baseDir: store.getBaseDir() };
186
258
  }
187
259
 
188
260
  /**
@@ -203,15 +275,18 @@ export function retireConstraint(
203
275
  const loader = new ProjectConfigLoader(projectRoot);
204
276
  loader.load();
205
277
 
206
- const target = findRetireTarget(id);
278
+ const target = findRetireTarget(id, projectRoot);
207
279
  const emptyStats = { total: 0, fail: 0, failRate: 0 };
208
280
  if (!target) {
209
281
  return { id, status: 'unknown_id', isError: false, stats: emptyStats };
210
282
  }
211
283
  const isError = target.severity === 'error';
212
284
 
213
- // 已退役保护:退役落点只有 config.yml constraints.<id>.enabled:false 一处
214
- if (loader.getConfig().constraints?.[id]?.enabled === false) {
285
+ // 已退役保护:只认 retired 墓碑(ADR-0032 决策 6.6,票 02 断点 6)——
286
+ // enabled:false 是"禁用"不是"退休",落到下面正常退休流程:覆写墓碑 + 补写沉淀,
287
+ // 不让一次裸 disable 吞掉 retire 的知识沉淀。
288
+ const existing = loader.getConfig().constraints?.[id] as { enabled?: boolean; retired?: unknown } | undefined;
289
+ if (existing?.enabled === false && existing.retired) {
215
290
  return { id, status: 'already_retired', isError, stats: emptyStats };
216
291
  }
217
292
 
@@ -238,7 +313,14 @@ export function retireConstraint(
238
313
  );
239
314
 
240
315
  // 2. KnowledgeStore
241
- const knowledgeEntryId = saveRetireKnowledge(projectRoot, id, target, reason, stats, iso);
316
+ const { entryId: knowledgeEntryId, baseDir: knowledgeBaseDir } = saveRetireKnowledge(
317
+ id,
318
+ target,
319
+ reason,
320
+ stats,
321
+ iso,
322
+ options.io ?? processIO
323
+ );
242
324
 
243
325
  return {
244
326
  id,
@@ -246,25 +328,27 @@ export function retireConstraint(
246
328
  isError,
247
329
  stats,
248
330
  knowledgeEntryId,
331
+ knowledgeBaseDir,
249
332
  };
250
333
  }
251
334
 
252
335
  /**
253
336
  * 打印单条退役结果(含回滚语义提示)
254
337
  */
255
- export function printRetireResult(result: RetireResult, io: CommandIO = processIO): CommandResult {
338
+ export function printRetireResult(result: RetireResult, io: CommandIO = processIO, projectRoot?: string): CommandResult {
256
339
  switch (result.status) {
257
340
  case 'unknown_id':
258
- log(io, chalk.red(`❌ ${result.id}: 约束不存在(非内置约束),未做任何变更`));
341
+ log(io, chalk.red(`❌ ${result.id}: 约束不存在(内置与应用层约束中都未找到),未做任何变更`));
259
342
  return { kind: 'skip', reason: `${result.id}: 约束不存在,未做任何变更` };
260
343
  case 'already_retired':
261
- log(io, chalk.yellow(`⚠️ ${result.id}: 已处于退役状态(config.yml enabled: false),跳过`));
344
+ log(io, chalk.yellow(`⚠️ ${result.id}: 已处于退役状态(config.yml retired 墓碑),跳过`));
262
345
  return { kind: 'skip', reason: `${result.id}: 已处于退役状态,跳过` };
263
346
  case 'retired': {
264
347
  log(io, chalk.green(`✅ ${result.id}: 已退役`));
265
348
  log(io, ` 历史统计: total=${result.stats.total} fail=${result.stats.fail} fail率=${Math.round(result.stats.failRate * 100)}%`);
266
- log(io, ` 知识沉淀: ${result.knowledgeEntryId}(.harness/knowledge)`);
267
- log(io, chalk.gray(` retire 不是删除——恢复方法:删除 config.yml constraints.${result.id} 段`));
349
+ log(io, ` 知识沉淀: ${result.knowledgeEntryId}(${result.knowledgeBaseDir})`);
350
+ log(io, chalk.gray(` retire 不是删除——恢复方法:harness constraints reactivate ${result.id}(会写复活沉淀;手动删 config.yml 段则无沉淀)`));
351
+ if (projectRoot) logCommitHint(projectRoot, io);
268
352
  break;
269
353
  }
270
354
  }
@@ -297,7 +381,13 @@ export async function runRetireInteractive(
297
381
  projectRoot: string,
298
382
  io: RetireIO = { input: process.stdin, output: process.stdout }
299
383
  ): Promise<CommandResult> {
300
- const report = buildConstraintsUsageReport(projectRoot);
384
+ // 观察名单中间态(ADR-0032,块 3 子项 4):名单内约束不进候选;新列入经 StateIO 写回
385
+ const stateIO = fileStateIO(projectRoot);
386
+ const state = stateIO.read();
387
+ const report = buildConstraintsUsageReport(projectRoot, {}, { watchlist: state.constraintWatchlist });
388
+ if (Object.keys(report.nextWatchlist).length > Object.keys(state.constraintWatchlist ?? {}).length) {
389
+ stateIO.write({ ...state, constraintWatchlist: report.nextWatchlist });
390
+ }
301
391
  // 候选诊断是退役决策的依据:数据不完整必须先说(harness#100,与 report 同一降级维度);
302
392
  // 告知行走 stderr,与 status / failure list / 直达分支同一去向(交互正文仍走 console.log)
303
393
  if (report.skippedLines > 0) {
@@ -323,6 +413,9 @@ export async function runRetireInteractive(
323
413
  report.candidates.forEach((c, i) => {
324
414
  console.log(` ${i + 1}. [${CANDIDATE_KIND_LABEL[c.kind]}] ${c.id} — ${c.reason}`);
325
415
  });
416
+ if (report.watchlist.length > 0) {
417
+ console.log(chalk.gray(` (另有 ${report.watchlist.length} 条零拦截约束在观察名单中,满 ${WATCHLIST_PERIOD_DAYS} 天且样本足才转候选——见 harness constraints report)`));
418
+ }
326
419
  console.log();
327
420
  const answer = await ask('输入编号(逗号分隔多选)或约束 id,留空取消: ');
328
421
  if (!answer) {
@@ -344,7 +437,7 @@ export async function runRetireInteractive(
344
437
  // 逐条收集 reason + error 级二次确认
345
438
  const plan: { id: string; reason: string }[] = [];
346
439
  for (const id of selectedIds) {
347
- const target = findRetireTarget(id);
440
+ const target = findRetireTarget(id, projectRoot);
348
441
  if (!target) {
349
442
  console.log(chalk.red(`❌ ${id}: 约束不存在,跳过`));
350
443
  continue;
@@ -380,8 +473,8 @@ export async function runRetireInteractive(
380
473
 
381
474
  console.log();
382
475
  for (const p of plan) {
383
- const result = retireConstraint(projectRoot, p.id, { reason: p.reason });
384
- printRetireResult(result, out);
476
+ const result = retireConstraint(projectRoot, p.id, { reason: p.reason, io: out });
477
+ printRetireResult(result, out, projectRoot);
385
478
  }
386
479
  } finally {
387
480
  close();
@@ -421,11 +514,11 @@ export async function constraintsRetire(
421
514
  logError(io, chalk.yellow(`⚠️ trace 文件有 ${skippedLines} 行损坏已跳过,落盘的退役统计只基于其余合法记录`));
422
515
  }
423
516
 
424
- const result = retireConstraint(projectRoot, id, { reason: options.reason });
517
+ const result = retireConstraint(projectRoot, id, { reason: options.reason, io });
425
518
  if (result.status === 'retired' && result.isError) {
426
519
  log(io, chalk.yellow(`⚠️ ${id} 是一条 error 级约束,已通过 --yes 直达退役(交互模式会要求二次确认)`));
427
520
  }
428
- return printRetireResult(result, io);
521
+ return printRetireResult(result, io, projectRoot);
429
522
  }
430
523
 
431
524
  return runRetireInteractive(projectRoot);
@@ -295,14 +295,6 @@ export const COMMAND_DEFINITIONS: CommandDefinition[] = [
295
295
  clear: { impl: { module: 'failure', export: 'failureClear' } },
296
296
  },
297
297
  },
298
- {
299
- command: 'posteval-plan',
300
- argument: '<planPath>',
301
- description: '验证 plan 文件的 checklist items 是否都有对应的 staged diff',
302
- options: [],
303
- action: { module: 'posteval-plan', export: 'postevalPlan' },
304
- mapActionArgs: (positionals) => [{ planPath: positionals[0] }],
305
- },
306
298
  {
307
299
  command: 'release',
308
300
  description: 'npm 发布流水线:tsc → dist 验证 → npm version → git push → npm publish → gh release。不依赖 Studio API。',
@@ -327,7 +319,10 @@ export const COMMAND_DEFINITIONS: CommandDefinition[] = [
327
319
  options: [
328
320
  { flags: '-p, --project-path <path>', description: '项目路径' },
329
321
  { flags: '--export [file]', description: '导出脱敏 markdown 摘要(缺省 .harness/reports/constraints-<YYYYMMDD>.md)' },
330
- { flags: '--json', description: '输出 JSON 格式', defaultValue: false },
322
+ // E1 复盘修正 M3.1:原名 --json 与父命令 constraints 同名相撞,
323
+ // commander 把 flag 消费在父命令上导致 JSON 分支不可达;子命令换名
324
+ // --json-output(父 --json 有活跃消费者,子 --json 零消费者,换名零破坏)
325
+ { flags: '--json-output', description: '输出 JSON 格式', defaultValue: false },
331
326
  { flags: '--zero-intercept-min <n>', description: '零拦截候选的最小评估样本数', defaultValue: '50' },
332
327
  { flags: '--noise-fail-rate <rate>', description: '高噪候选 fail 率阈值', defaultValue: '0.8' },
333
328
  { flags: '--noise-min-total <n>', description: '高噪候选最小评估样本数', defaultValue: '20' },
@@ -336,7 +331,7 @@ export const COMMAND_DEFINITIONS: CommandDefinition[] = [
336
331
  mapActionArgs: (_pos, options) => [{
337
332
  projectPath: options.projectPath,
338
333
  export: options.export,
339
- json: options.json,
334
+ json: options.jsonOutput,
340
335
  zeroInterceptMin: requireNumericFlag('--zero-intercept-min', options.zeroInterceptMin as string | undefined, 'int'),
341
336
  noiseFailRate: requireNumericFlag('--noise-fail-rate', options.noiseFailRate as string | undefined, 'float'),
342
337
  noiseMinTotal: requireNumericFlag('--noise-min-total', options.noiseMinTotal as string | undefined, 'int'),
@@ -345,7 +340,7 @@ export const COMMAND_DEFINITIONS: CommandDefinition[] = [
345
340
  {
346
341
  command: 'retire',
347
342
  argument: '[id]',
348
- description: '退役约束:无 id 进入交互式候选选择(人确认);带 id 直达退役需显式 --yes(写 config.yml + KnowledgeStore + 同步 CLAUDE.md 注入段)',
343
+ description: '退役约束:无 id 进入交互式候选选择(人确认);带 id 直达退役需显式 --yes(写 config.yml retired 墓碑 + KnowledgeStore 沉淀)',
349
344
  options: [
350
345
  { flags: '-p, --project-path <path>', description: '项目路径' },
351
346
  { flags: '--reason <text>', description: '退役原因' },
@@ -358,6 +353,36 @@ export const COMMAND_DEFINITIONS: CommandDefinition[] = [
358
353
  yes: options.yes,
359
354
  }],
360
355
  },
356
+ {
357
+ command: 'reactivate',
358
+ argument: '<id>',
359
+ description: '复活已退役约束:删 config.yml retired 墓碑段 + 写 constraint-reactivated-<id> 沉淀(不改历史);直达需显式 --yes',
360
+ options: [
361
+ { flags: '-p, --project-path <path>', description: '项目路径' },
362
+ { flags: '--reason <text>', description: '复活原因' },
363
+ { flags: '-y, --yes', description: '显式确认直达复活(与 retire 同一道人确认闸门)', defaultValue: false },
364
+ ],
365
+ action: { module: 'constraints-reactivate', export: 'constraintsReactivate' },
366
+ mapActionArgs: (positionals, options) => [positionals[0], {
367
+ projectPath: options.projectPath,
368
+ reason: options.reason,
369
+ yes: options.yes,
370
+ }],
371
+ },
372
+ {
373
+ command: 'pack-proposal',
374
+ argument: '<id>',
375
+ description: '打包升级提案材料(ADR-0033 决策 4):条文 + checker 模板与参数 + traces 使用统计 + 升级理由留白,脱敏 markdown 落 .harness/reports/proposal-<id>-<YYYYMMDD>.md(--stdout 打印不落盘)',
376
+ options: [
377
+ { flags: '-p, --project-path <path>', description: '项目路径' },
378
+ { flags: '--stdout', description: '打印到 stdout 而不落盘', defaultValue: false },
379
+ ],
380
+ action: { module: 'constraints-pack-proposal', export: 'constraintsPackProposal' },
381
+ mapActionArgs: (positionals, options) => [positionals[0], {
382
+ projectPath: options.projectPath,
383
+ stdout: options.stdout,
384
+ }],
385
+ },
361
386
  ],
362
387
  },
363
388
  {
@@ -9,18 +9,18 @@
9
9
  * 要么声明是人读面独有的派生量(derived,须在测试的豁免表逐条登记理由)。
10
10
  * - **角色→样式单表 + 键→中文 label 映射**:维度 label、成熟度颜色、规则 label 的唯一权威。
11
11
  * - **json / 人读的唯一分派与退出码**:`--json` 打 `data` 的序列化,否则渲染 display model。
12
- * - **路径解析与 store 构造**:`-p` / `KNOWLEDGE_BASE_DIR` / 旧目录兜底只在这一处。
12
+ * - **路径解析与 store 构造**:`-p` / `KNOWLEDGE_BASE_DIR` / 缺省目录解析只在这一处
13
+ * (ADR-0034:旧 ~/.studio/knowledge 兼容沿用已退役,studio 正本由编排侧显式注入)。
13
14
  */
14
15
 
15
16
  import chalk from 'chalk';
16
- import * as fs from 'fs';
17
17
  import * as os from 'os';
18
18
  import * as path from 'path';
19
19
  import { FileKnowledgeStore } from '../../knowledge/store';
20
20
  import { AUDIT_RULE_LABELS } from '../../knowledge/audit-scoring';
21
21
  import type { AuditIssue, AuditReport, AuditRuleName } from '../../knowledge/audit-scoring';
22
22
  import type { MaturityLevel } from '../../knowledge/types';
23
- import { log, logError, type CommandIO, type CommandResult } from '../command-contract';
23
+ import { log, type CommandIO, type CommandResult } from '../command-contract';
24
24
 
25
25
  // ── display model ─────────────────────────────────────────
26
26
 
@@ -176,20 +176,8 @@ export function emitKnowledgeView<T>(io: CommandIO, options: { json?: boolean },
176
176
 
177
177
  // ── 路径解析与 store 构造 ─────────────────────────────────
178
178
 
179
- /** 缺省知识库数据根(相对用户 home) */
179
+ /** 缺省知识库数据根(相对用户 home)。裸 CLI 项目缺省目录;studio 场景唯一正本 ~/.studio/knowledge 由编排侧经 KNOWLEDGE_BASE_DIR 显式注入(ADR-0034) */
180
180
  const KNOWLEDGE_DATA_DIR = path.join('.harness', 'knowledge');
181
- /** 旧缺省数据根(曾寄居 studio home),仍有数据时兼容沿用,免迁移 */
182
- const LEGACY_KNOWLEDGE_DATA_DIR = path.join('.studio', 'knowledge');
183
-
184
- let legacyKnowledgeDirNotified = false;
185
-
186
- function hasKnowledgeData(dir: string): boolean {
187
- try {
188
- return fs.existsSync(dir) && fs.readdirSync(dir).length > 0;
189
- } catch {
190
- return false;
191
- }
192
- }
193
181
 
194
182
  export interface KnowledgePathOptions {
195
183
  /** 覆盖知识库目录(audit/snapshot/migrate/index/health 收) */
@@ -200,22 +188,15 @@ export interface KnowledgePathOptions {
200
188
 
201
189
  /**
202
190
  * 知识库数据根的唯一解析点。`-p` 优先,其次 KNOWLEDGE_BASE_DIR,
203
- * 最后落到用户 home(旧目录仍有数据时沿用并只提示一次,提示走 stderr 不污染 --json)。
191
+ * 最后落到用户 home 缺省目录(ADR-0034 目录收编:旧 ~/.studio/knowledge
192
+ * 兼容沿用逻辑已退役——双目录分叉是历史包袱,studio 正本由编排侧显式注入;
193
+ * io 参数随兼容提示一并失去用途,签名保留免动全部调用方)。
204
194
  */
205
- export function resolveKnowledgeBaseDir(options: KnowledgePathOptions, io: CommandIO): string {
195
+ export function resolveKnowledgeBaseDir(options: KnowledgePathOptions, _io: CommandIO): string {
206
196
  if (options.dir) return options.dir;
207
197
  if (options.projectPath) return `${options.projectPath}/.harness/knowledge`;
208
198
  if (process.env.KNOWLEDGE_BASE_DIR) return process.env.KNOWLEDGE_BASE_DIR;
209
- const defaultDir = path.join(os.homedir(), KNOWLEDGE_DATA_DIR);
210
- const legacyDir = path.join(os.homedir(), LEGACY_KNOWLEDGE_DATA_DIR);
211
- if (hasKnowledgeData(legacyDir)) {
212
- if (!legacyKnowledgeDirNotified) {
213
- legacyKnowledgeDirNotified = true;
214
- logError(io, toneText(`⚠ 缺省知识库根已改为 ${defaultDir};旧目录 ${legacyDir} 仍有数据,本次沿用(免迁移,可用 KNOWLEDGE_BASE_DIR 覆盖)`, 'warn'));
215
- }
216
- return legacyDir;
217
- }
218
- return defaultDir;
199
+ return path.join(os.homedir(), KNOWLEDGE_DATA_DIR);
219
200
  }
220
201
 
221
202
  /** 知识库句柄的唯一构造点(路径解析全权交给 resolveKnowledgeBaseDir) */
@@ -7,7 +7,7 @@
7
7
  import chalk from 'chalk';
8
8
  import * as fs from 'fs/promises';
9
9
  import { ConstraintChecker } from '../../core/constraints/checker';
10
- import { CONSTRAINTS } from '../../core/constraints/definitions';
10
+ import { getMergedConstraintsConfig } from '../../core/effective-constraints';
11
11
  import { TraceCollector } from '../../monitoring/traces';
12
12
  import type { ConstraintContext } from '../../types/constraint';
13
13
  import { log, processIO, type CommandIO, type CommandResult } from '../command-contract';
@@ -50,7 +50,11 @@ export async function report(options: ReportOptions, io: CommandIO = processIO):
50
50
  // #139:收集器锚根构造——不传 projectPath 时 trace 会落进调用方 cwd,B 侧读不到
51
51
  const checker = new ConstraintChecker(new TraceCollector({ projectPath }));
52
52
 
53
- const totalConstraints = Object.keys(CONSTRAINTS).length;
53
+ // 生效集口径与 check.ts 同一入口(ADR-0033:应用层约束进 report 面的前提)——
54
+ // 此前跑的是内置全集(CONSTRAINTS),config.yml 禁用与应用层条目都不进报告
55
+ const merged = getMergedConstraintsConfig(projectPath);
56
+ const effectiveConstraints = Object.values(merged.constraints);
57
+ const totalConstraints = effectiveConstraints.length;
54
58
 
55
59
  const context: ConstraintContext = {
56
60
  operation: 'file_modification',
@@ -58,7 +62,7 @@ export async function report(options: ReportOptions, io: CommandIO = processIO):
58
62
  };
59
63
 
60
64
  // 收集模式(架构评审候选1):不抛、全量收集,error 级违规如实进报告
61
- const result = await checker.collectConstraints(context);
65
+ const result = await checker.collectConstraints(context, merged);
62
66
 
63
67
  const failedErrors = result.errors.filter(r => !r.satisfied);
64
68
  const failedWarnings = result.warnings.filter(r => !r.satisfied);
@@ -74,8 +78,8 @@ export async function report(options: ReportOptions, io: CommandIO = processIO):
74
78
  projectPath,
75
79
  constraints: {
76
80
  total: totalConstraints,
77
- errors: Object.values(CONSTRAINTS).filter(c => c.severity === 'error').length,
78
- warnings: Object.values(CONSTRAINTS).filter(c => c.severity === 'warning').length,
81
+ errors: effectiveConstraints.filter(c => c.severity === 'error').length,
82
+ warnings: effectiveConstraints.filter(c => c.severity === 'warning').length,
79
83
  passed: result.passed ? totalConstraints : totalConstraints - violations.length,
80
84
  failed: failedErrors.length,
81
85
  warningViolations: failedWarnings.length,
@@ -17,8 +17,6 @@ import type { CiPlatform } from '../../types/project-config';
17
17
  export const PRE_COMMIT_SNIPPET = `
18
18
  echo "🔍 Running harness checks..."
19
19
 
20
- STAGED=$(git diff --cached --name-only --diff-filter=ACMR 2>/dev/null || true)
21
-
22
20
  # Harness 约束检查
23
21
  npx @dommaker/harness check --staged
24
22
  if [ $? -ne 0 ]; then
@@ -26,20 +24,6 @@ if [ $? -ne 0 ]; then
26
24
  exit 1
27
25
  fi
28
26
 
29
- # Plan coverage check (via PostEval)
30
- if command -v npx > /dev/null 2>&1; then
31
- PLAN_FILES=$(echo "$STAGED" | grep -E 'plans/.*\\.md$|\\.plan\\.md$' || true)
32
- if [ -n "$PLAN_FILES" ]; then
33
- echo "📋 Checking plan coverage..."
34
- for plan in $PLAN_FILES; do
35
- npx @dommaker/harness posteval-plan "$plan" || {
36
- echo "🛑 Plan coverage incomplete. See above for missed items."
37
- exit 1
38
- }
39
- done
40
- fi
41
- fi
42
-
43
27
  echo "✅ All checks passed"
44
28
  `;
45
29
 
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * `.harness/.state.json` 是 harness **自身**的运行期状态(与 RunEnv 观察的项目上行
5
5
  * 数据不是一类东西,RunEnv 保持 ADR-0023 的只读契约)。本模块是它读-改-写的唯一
6
- * 入口:`check` 的智能提示与 `status` 两个写者都经本接缝,命令函数以可选参数注入,
6
+ * 入口:`check` 的智能提示、`status` `constraints report`/`retire` 的观察名单
7
+ * (块 3 子项 4)都经本接缝,命令函数以可选参数注入,
7
8
  * 缺省 = 真实 fs 实现(照 `CommandIO` 的注入模式,但不扩 `CommandIO`——输出面与
8
9
  * 状态面是两个概念)。
9
10
  *
@@ -12,6 +13,7 @@
12
13
 
13
14
  import * as fs from 'fs';
14
15
  import * as path from 'path';
16
+ import type { ConstraintWatchlistState } from '../core/constraints/usage-report';
15
17
 
16
18
  /**
17
19
  * `.harness/.state.json` 的具名类型(从 check.ts 内联类型提出,ADR-0026 决策 1)。
@@ -23,6 +25,11 @@ export interface HarnessState {
23
25
  shownHints?: string[];
24
26
  /** 最近一次 `harness status` 运行时刻(ISO 串) */
25
27
  lastStatusRun?: string;
28
+ /**
29
+ * 零拦截观察名单(ADR-0032,块 3 子项 4):constraintId → 列入时刻。
30
+ * `constraints report` / `constraints retire` 经本接缝读-改-写(只增不删)。
31
+ */
32
+ constraintWatchlist?: ConstraintWatchlistState;
26
33
  }
27
34
 
28
35
  /** harness 自身运行期状态文件的可注入接缝(读-改-写的唯一入口) */