@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
@@ -13,6 +13,7 @@
13
13
  import type { KnowledgeStore } from './store';
14
14
  import { ReferenceTracker } from './reference-tracker';
15
15
  import type { KnowledgeEntry, LintIssue, LintIssueType, MaturityLevel } from './types';
16
+ import { MATURITY_LEVELS, STORAGE_LAYERS } from './types';
16
17
 
17
18
  const MAX_SOURCE_REFS = 20;
18
19
 
@@ -46,6 +47,7 @@ export class KnowledgeLinter {
46
47
  issues.push(...this.checkOutdated(entries));
47
48
  issues.push(...this.checkDuplicates(entries));
48
49
  issues.push(...this.checkContradictions(entries));
50
+ issues.push(...this.checkInvalidEnums(entries));
49
51
 
50
52
  // 自动修复
51
53
  let fixed = 0;
@@ -60,6 +62,7 @@ export class KnowledgeLinter {
60
62
  outdated: 0,
61
63
  duplicate: 0,
62
64
  index_inconsistent: 0,
65
+ invalid_enum: 0,
63
66
  };
64
67
  for (const issue of issues) {
65
68
  summary[issue.type]++;
@@ -204,6 +207,55 @@ export class KnowledgeLinter {
204
207
  return issues;
205
208
  }
206
209
 
210
+ /**
211
+ * 检查存量条目的 maturity/layer 枚举值域(E1 复盘修正 M1 收口:
212
+ * 写入闸只挡新写,存量脏值由本检查进 lint 报告;缺字段在存量侧算脏值)。
213
+ * 与 validateEntry 共用同一判定正本(invalidEnumIssues),autoFix 不动此类
214
+ * issue——枚举映射需人工裁定,不自动改。
215
+ */
216
+ checkInvalidEnums(entries: KnowledgeEntry[]): LintIssue[] {
217
+ const issues: LintIssue[] = [];
218
+ for (const entry of entries) {
219
+ issues.push(...this.invalidEnumIssues(entry, true, entry.id));
220
+ }
221
+ return issues;
222
+ }
223
+
224
+ /**
225
+ * maturity/layer 枚举判定正本。missingIsDirty=false 时字段缺省跳过
226
+ * (pre-ingest 调用方只传四字段的兼容面);存量扫描传 true(缺字段即脏)。
227
+ */
228
+ private invalidEnumIssues(
229
+ fields: { maturity?: string; layer?: string },
230
+ missingIsDirty: boolean,
231
+ entryId?: string
232
+ ): LintIssue[] {
233
+ const issues: LintIssue[] = [];
234
+ if ((missingIsDirty || fields.maturity !== undefined) && !(MATURITY_LEVELS as readonly string[]).includes(fields.maturity as string)) {
235
+ issues.push({
236
+ type: 'invalid_enum',
237
+ entryId,
238
+ severity: 'high',
239
+ description: fields.maturity === undefined
240
+ ? `maturity is missing (declared values: ${MATURITY_LEVELS.join(' | ')}).`
241
+ : `maturity "${fields.maturity}" is not a declared value (${MATURITY_LEVELS.join(' | ')}).`,
242
+ suggestion: 'Map to a declared maturity (e.g. pending → draft) before writing; the store write gate rejects undeclared values.',
243
+ });
244
+ }
245
+ if ((missingIsDirty || fields.layer !== undefined) && !(STORAGE_LAYERS as readonly string[]).includes(fields.layer as string)) {
246
+ issues.push({
247
+ type: 'invalid_enum',
248
+ entryId,
249
+ severity: 'high',
250
+ description: fields.layer === undefined
251
+ ? `layer is missing (declared values: ${STORAGE_LAYERS.join(' | ')}).`
252
+ : `layer "${fields.layer}" is not a declared value (${STORAGE_LAYERS.join(' | ')}).`,
253
+ suggestion: 'Map to a declared layer or drop the field; the store write gate rejects undeclared values.',
254
+ });
255
+ }
256
+ return issues;
257
+ }
258
+
207
259
  /**
208
260
  * 检查矛盾(同主题相反结论)
209
261
  *
@@ -260,10 +312,17 @@ export class KnowledgeLinter {
260
312
  * - Title too vague (common stop words only) → flag
261
313
  * - Contradicts a proven entry with same tags → reject
262
314
  * - Near-duplicate of existing entry (title similarity > 80%) → flag merge
315
+ * - maturity/layer 未声明枚举值 → reject(E1 复盘修正 M1;字段缺省则跳过,
316
+ * 兼容只传 title/content/tags/type 的 pre-ingest 调用方;存量报告由调用方
317
+ * 把条目字段显式传入触发)
263
318
  */
264
- validateEntry(entry: { title: string; content: string; tags: string[]; type: string }): LintIssue[] {
319
+ validateEntry(entry: { title: string; content: string; tags: string[]; type: string; maturity?: string; layer?: string }): LintIssue[] {
265
320
  const issues: LintIssue[] = [];
266
321
 
322
+ // Undeclared maturity/layer enum values(判定正本 = invalidEnumIssues;字段缺省跳过,
323
+ // 兼容只传四字段的 pre-ingest 调用方;存量扫描走 run() → checkInvalidEnums)
324
+ issues.push(...this.invalidEnumIssues(entry, false));
325
+
267
326
  // Content too short
268
327
  if ((entry.content || '').length < 20) {
269
328
  issues.push({
@@ -18,6 +18,7 @@ import type {
18
18
  SnapshotSurvival,
19
19
  StoreUpdate,
20
20
  } from './types';
21
+ import { MATURITY_LEVELS, STORAGE_LAYERS } from './types';
21
22
 
22
23
  const DEFAULT_DIR = '.harness/knowledge';
23
24
  const INDEX_FILE = 'index.json';
@@ -109,12 +110,36 @@ export class FileKnowledgeStore implements KnowledgeStore {
109
110
  }
110
111
 
111
112
  save(entry: KnowledgeEntry): void {
113
+ this.assertEnumFields(entry);
112
114
  this.writeEntryFile(entry);
113
115
  this.updateIndexEntry(entry);
114
116
  }
115
117
 
118
+ /**
119
+ * 写入闸(E1 复盘修正 M1):maturity/layer 是声明枚举(值域正本在 types.ts),
120
+ * 未声明值拒写报错,不静默降级。save/saveAll/applyAll 三条落盘路径共用;
121
+ * update 经 save 继承同一闸。存量脏条目(绕过本 store 直写文件的)在被
122
+ * 任一写入路径触及时抛错——先修脏值再写。
123
+ */
124
+ private assertEnumFields(entry: KnowledgeEntry): void {
125
+ if (!MATURITY_LEVELS.includes(entry.maturity)) {
126
+ throw new Error(
127
+ `[harness] 知识条目 ${entry.id} 的 maturity 值 "${entry.maturity}" 未声明` +
128
+ `(允许: ${MATURITY_LEVELS.join(' | ')}),拒写`
129
+ );
130
+ }
131
+ if (!STORAGE_LAYERS.includes(entry.layer)) {
132
+ throw new Error(
133
+ `[harness] 知识条目 ${entry.id} 的 layer 值 "${entry.layer}" 未声明` +
134
+ `(允许: ${STORAGE_LAYERS.join(' | ')}),拒写`
135
+ );
136
+ }
137
+ }
138
+
116
139
  saveAll(entries: KnowledgeEntry[]): void {
117
140
  if (entries.length === 0) return;
141
+ // 整批预校验(写入闸):任一脏值全批拒写,不落半个批次
142
+ for (const entry of entries) this.assertEnumFields(entry);
118
143
  const index = this.readIndex();
119
144
  const position = new Map(index.map((e, i) => [e.id, i]));
120
145
  for (const entry of entries) {
@@ -133,25 +158,37 @@ export class FileKnowledgeStore implements KnowledgeStore {
133
158
 
134
159
  applyAll(updates: StoreUpdate[]): void {
135
160
  if (updates.length === 0) return;
161
+ // 先合并再整批预校验(写入闸):任一合并结果脏值全批拒写,不落半个批次。
162
+ // 批内同 id 多条按序累积合并(后者基于前者的合并结果,与逐条 update 同语义)。
163
+ const merged: KnowledgeEntry[] = [];
164
+ const pending = new Map<string, number>(); // id → merged 下标
165
+ for (const { id, partial } of updates) {
166
+ const idx = pending.get(id);
167
+ const base = idx !== undefined ? merged[idx] : this.get(id);
168
+ if (!base) continue;
169
+ const next: KnowledgeEntry = { ...base, ...partial, id };
170
+ if (idx !== undefined) {
171
+ merged[idx] = next;
172
+ } else {
173
+ pending.set(id, merged.length);
174
+ merged.push(next);
175
+ }
176
+ }
177
+ if (merged.length === 0) return;
178
+ for (const entry of merged) this.assertEnumFields(entry);
136
179
  const index = this.readIndex();
137
180
  const position = new Map(index.map((e, i) => [e.id, i]));
138
- let applied = 0;
139
- for (const { id, partial } of updates) {
140
- const existing = this.get(id);
141
- if (!existing) continue;
142
- const updated: KnowledgeEntry = { ...existing, ...partial, id };
143
- this.writeEntryFile(updated);
144
- const indexEntry = this.toIndexEntry(updated);
145
- const idx = position.get(id);
181
+ for (const entry of merged) {
182
+ this.writeEntryFile(entry);
183
+ const indexEntry = this.toIndexEntry(entry);
184
+ const idx = position.get(entry.id);
146
185
  if (idx !== undefined) {
147
186
  index[idx] = indexEntry;
148
187
  } else {
149
- position.set(id, index.length);
188
+ position.set(entry.id, index.length);
150
189
  index.push(indexEntry);
151
190
  }
152
- applied++;
153
191
  }
154
- if (applied === 0) return;
155
192
  this.writeIndex(index);
156
193
  }
157
194
 
@@ -15,6 +15,12 @@ export type MaturityLevel = 'draft' | 'verified' | 'proven' | 'archived' | 'acti
15
15
 
16
16
  export type StorageLayer = 'personal' | 'team' | 'tech' | 'domain' | 'project' | 'system';
17
17
 
18
+ /** MaturityLevel 的运行时值域正本(store 写入闸与 lint 枚举校验共用) */
19
+ export const MATURITY_LEVELS: readonly MaturityLevel[] = ['draft', 'verified', 'proven', 'archived', 'active', 'deprecated'];
20
+
21
+ /** StorageLayer 的运行时值域正本(store 写入闸与 lint 枚举校验共用) */
22
+ export const STORAGE_LAYERS: readonly StorageLayer[] = ['personal', 'team', 'tech', 'domain', 'project', 'system'];
23
+
18
24
  /** How agent consumes this knowledge — drives injection strategy and lifecycle */
19
25
  export type ConsumptionMode = 'rule' | 'reference' | 'context' | 'signal';
20
26
 
@@ -101,7 +107,7 @@ export interface QueryFilter {
101
107
 
102
108
  // ── Lint ────────────────────────────────────────────────────
103
109
 
104
- export type LintIssueType = 'orphan' | 'contradiction' | 'outdated' | 'duplicate' | 'index_inconsistent';
110
+ export type LintIssueType = 'orphan' | 'contradiction' | 'outdated' | 'duplicate' | 'index_inconsistent' | 'invalid_enum';
105
111
 
106
112
  export interface LintIssue {
107
113
  type: LintIssueType;
@@ -22,7 +22,7 @@
22
22
  - 零 Token 成本:不调用 LLM
23
23
  - **凡以 `skip` 策略读 JSONL,坏行计数必须到达该消费面的用户可见输出,或在该调用点显式记名豁免并写明理由(harness#100)**——静默吞掉「数据不全」不是一种可选项。承载方式纯增量:新报告入口带计数,旧入口退化为丢计数的薄包装(#82 裁决 4 的兼容约束继续成立),不另立第二份计数概念、不把 `JsonlReadResult` 提升进包根(ADR-0003)
24
24
  - 机器可检:`src/utils/__tests__/jsonl-skip-disposition.test.ts` 三道闸——每个 `readJsonl` 调用点必须判得出策略(判不出即形状逃逸,失败)、skip 读点集合冻结(新增未声明即失败,站点消失却不删条目也失败)、每个读点上方 6 行内有 `计数去向:` 声明且邻居声明不得顶替
25
- - 三类去向(逐条对得上代码):**报告体内部的降级位**随报告体走 stdout——`constraints report` 的坏行提示紧挨既有的 `traceFileExists` 说明;**不挤动既有输出的诊断告知**走 stderr——`status`(`logError`,stdout 的 `记录数` 口径与字节不变)、`failure list`(#96 定稿的 `console.error`)、`constraints retire`(交互与 `--yes` 直达两条路径各自告知);**结构化面用字段**——`constraints report --json` 的 `skippedLines`、`--export` markdown 的警示行、studio 端点响应体(配套票 studio#451)
25
+ - 三类去向(逐条对得上代码):**报告体内部的降级位**随报告体走 stdout——`constraints report` 的坏行提示紧挨既有的 `traceFileExists` 说明;**不挤动既有输出的诊断告知**走 stderr——`status`(`logError`,stdout 的 `记录数` 口径与字节不变)、`failure list`(#96 定稿的 `console.error`)、`constraints retire`(交互与 `--yes` 直达两条路径各自告知);**结构化面用字段**——`constraints report --json-output` 的 `skippedLines`、`--export` markdown 的警示行、studio 端点响应体(配套票 studio#451)
26
26
 
27
27
  ## 注意事项
28
28
  - 追踪数据供 `harness constraints report` 统计与退役候选诊断消费(观测用途,不做自动降级)
@@ -32,16 +32,17 @@ describe('预设数据', () => {
32
32
  describe('RELAXED_PRESET', () => {
33
33
  it('应该只启用核心约束', () => {
34
34
  expect(RELAXED_PRESET.name).toBe('relaxed');
35
- expect(RELAXED_PRESET.errors).toHaveLength(1);
36
- expect(RELAXED_PRESET.warnings).toHaveLength(1);
35
+ expect(RELAXED_PRESET.errors).toHaveLength(2);
36
+ expect(RELAXED_PRESET.warnings).toHaveLength(0);
37
37
  });
38
38
 
39
39
  it('应该包含 no_completion_without_verification', () => {
40
40
  expect(RELAXED_PRESET.errors).toContain('no_completion_without_verification');
41
41
  });
42
42
 
43
- it('应该包含 no_hardcoded_credentials', () => {
44
- expect(RELAXED_PRESET.warnings).toContain('no_hardcoded_credentials');
43
+ it('应该包含 no_hardcoded_credentials(ADR-0032 起为 error 级,同桶)', () => {
44
+ expect(RELAXED_PRESET.errors).toContain('no_hardcoded_credentials');
45
+ expect(RELAXED_PRESET.warnings).toEqual([]);
45
46
  });
46
47
  });
47
48
 
@@ -47,16 +47,15 @@ export const STANDARD_PRESET: PresetConfig = {
47
47
  /**
48
48
  * 宽松预设
49
49
  *
50
- * 仅启用核心 error 级与凭证扫描
50
+ * 仅启用核心 error 级与凭证扫描(ADR-0032 起凭证扫描为 error 级,同桶)
51
51
  */
52
52
  export const RELAXED_PRESET: PresetConfig = {
53
53
  name: 'relaxed',
54
54
  errors: [
55
55
  'no_completion_without_verification',
56
- ],
57
- warnings: [
58
56
  'no_hardcoded_credentials',
59
57
  ],
58
+ warnings: [],
60
59
  };
61
60
 
62
61
  /**
@@ -32,6 +32,15 @@ export type ConstraintSeverity = 'error' | 'warning' | 'info';
32
32
  */
33
33
  export type ConstraintTrigger = string;
34
34
 
35
+ /**
36
+ * 约束来源(ADR-0033 两层约束模型)
37
+ *
38
+ * - builtin:内置约束(definitions/ 字面量定义,checker 按约束 id 注册查找)
39
+ * - app:应用层约束(应用仓 `.harness/constraints.yml` 定义,checker 按模板 id +
40
+ * params 实例化);缺省 builtin
41
+ */
42
+ export type ConstraintSource = 'builtin' | 'app';
43
+
35
44
  /**
36
45
  * 约束定义
37
46
  */
@@ -65,6 +74,25 @@ export interface Constraint {
65
74
 
66
75
  /** 是否启用 */
67
76
  enabled?: boolean;
77
+
78
+ /**
79
+ * 约束来源(ADR-0033):缺省 builtin
80
+ *
81
+ * app = 应用层约束(应用仓 `.harness/constraints.yml` 定义);
82
+ * report/退休/提案据此区分来源
83
+ */
84
+ source?: ConstraintSource;
85
+
86
+ /**
87
+ * 填空式 checker 模板 id(仅 source='app' 使用)
88
+ *
89
+ * 内置约束省略 = checker 按约束 id 在内置注册表查找;应用层约束按模板 id
90
+ * 在模板注册表(checkers/index.ts TEMPLATES)查找并带 params 实例化
91
+ */
92
+ checker?: string;
93
+
94
+ /** 模板参数(仅 source='app' 使用,形状由各模板的 validateParams 校验) */
95
+ params?: Record<string, unknown>;
68
96
  }
69
97
 
70
98
  /**
@@ -1,15 +0,0 @@
1
- /**
2
- * harness posteval-plan 命令
3
- *
4
- * 调用 Studio PostEval API,验证 plan 文件的 checklist items 是否都有对应的 staged diff。
5
- * pre-commit hook 使用此命令防止"假装完成"。
6
- *
7
- * 判定经返回值外溢(架构评审候选7):API 不可用/5xx 属"未判定"→ skip(退出码面 0 不变);
8
- * 覆盖率不足与 4xx → fail,reason 指明是哪一项否决。
9
- */
10
- import { type CommandIO, type CommandResult } from '../command-contract';
11
- export interface PostEvalPlanOptions {
12
- planPath: string;
13
- }
14
- export declare function postevalPlan(options: PostEvalPlanOptions, io?: CommandIO): Promise<CommandResult>;
15
- //# sourceMappingURL=posteval-plan.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"posteval-plan.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/posteval-plan.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAA4B,KAAK,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEnG,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;CAClB;AA6BD,wBAAsB,YAAY,CAChC,OAAO,EAAE,mBAAmB,EAC5B,EAAE,GAAE,SAAqB,GACxB,OAAO,CAAC,aAAa,CAAC,CAkDxB"}
@@ -1,86 +0,0 @@
1
- "use strict";
2
- /**
3
- * harness posteval-plan 命令
4
- *
5
- * 调用 Studio PostEval API,验证 plan 文件的 checklist items 是否都有对应的 staged diff。
6
- * pre-commit hook 使用此命令防止"假装完成"。
7
- *
8
- * 判定经返回值外溢(架构评审候选7):API 不可用/5xx 属"未判定"→ skip(退出码面 0 不变);
9
- * 覆盖率不足与 4xx → fail,reason 指明是哪一项否决。
10
- */
11
- var __importDefault = (this && this.__importDefault) || function (mod) {
12
- return (mod && mod.__esModule) ? mod : { "default": mod };
13
- };
14
- Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.postevalPlan = postevalPlan;
16
- const chalk_1 = __importDefault(require("chalk"));
17
- const command_contract_1 = require("../command-contract");
18
- const MAX_RETRIES = 3;
19
- const RETRY_DELAY_MS = 2000;
20
- async function fetchWithRetry(url, body, retries) {
21
- let lastError;
22
- for (let i = 0; i < retries; i++) {
23
- try {
24
- const controller = new AbortController();
25
- const timeout = setTimeout(() => controller.abort(), 5000);
26
- const res = await fetch(url, {
27
- method: 'POST',
28
- headers: { 'Content-Type': 'application/json' },
29
- body,
30
- signal: controller.signal,
31
- });
32
- clearTimeout(timeout);
33
- return res;
34
- }
35
- catch (error) {
36
- lastError = error;
37
- if (i < retries - 1) {
38
- await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS * (i + 1))); // exponential backoff
39
- }
40
- }
41
- }
42
- throw lastError;
43
- }
44
- async function postevalPlan(options, io = command_contract_1.processIO) {
45
- const apiPort = process.env.API_PORT || '3001';
46
- const url = `http://localhost:${apiPort}/api/v1/agents/post-eval/plan-coverage`;
47
- try {
48
- const res = await fetchWithRetry(url, JSON.stringify({ planPath: options.planPath }), MAX_RETRIES);
49
- if (!res.ok) {
50
- if (res.status >= 500) {
51
- // Server error — allow commit with warning (don't block on infrastructure failure)
52
- (0, command_contract_1.logError)(io, chalk_1.default.yellow(`⚠️ PostEval API unavailable (${res.status}), allowing commit with warning`));
53
- (0, command_contract_1.logError)(io, chalk_1.default.yellow(` Plan: ${options.planPath}`));
54
- return { kind: 'skip', reason: `PostEval API 服务端错误 ${res.status},未判定` };
55
- }
56
- (0, command_contract_1.logError)(io, chalk_1.default.red(`❌ PostEval API error: ${res.status} ${res.statusText}`));
57
- return { kind: 'fail', reason: `PostEval API error: ${res.status} ${res.statusText}` };
58
- }
59
- const report = await res.json();
60
- const pct = Math.round(report.completeness * 100);
61
- if (report.completeness < 1) {
62
- (0, command_contract_1.logError)(io, chalk_1.default.red(`❌ Plan coverage: ${pct}% (${report.matchedAcs.length}/${report.matchedAcs.length + report.missedAcs.length})`));
63
- if (report.missedAcs.length > 0) {
64
- (0, command_contract_1.logError)(io, chalk_1.default.red('Missed items:'));
65
- report.missedAcs.forEach((item) => (0, command_contract_1.logError)(io, chalk_1.default.red(` - ${item}`)));
66
- }
67
- return {
68
- kind: 'fail',
69
- reason: `plan coverage ${pct}% < 100%,缺 ${report.missedAcs.length} 项: ${report.missedAcs.join('; ')}`,
70
- };
71
- }
72
- (0, command_contract_1.log)(io, chalk_1.default.green(`✅ Plan coverage: ${pct}%`));
73
- return { kind: 'ok' };
74
- }
75
- catch (error) {
76
- // API unreachable after all retries — allow with warning (don't block commits on infra)
77
- (0, command_contract_1.logError)(io, chalk_1.default.yellow(`⚠️ Studio API unreachable after ${MAX_RETRIES} retries, allowing commit with warning`));
78
- (0, command_contract_1.logError)(io, chalk_1.default.yellow(` Plan: ${options.planPath}`));
79
- (0, command_contract_1.logError)(io, chalk_1.default.yellow(` Error: ${error?.code || error?.message || String(error)}`));
80
- return {
81
- kind: 'skip',
82
- reason: `Studio API 不可达(${error?.code || error?.message || String(error)}),未判定`,
83
- };
84
- }
85
- }
86
- //# sourceMappingURL=posteval-plan.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"posteval-plan.js","sourceRoot":"","sources":["../../../src/cli/commands/posteval-plan.ts"],"names":[],"mappings":"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"}
@@ -1,204 +0,0 @@
1
- /**
2
- * posteval-plan 命令测试
3
- */
4
-
5
- import * as fs from 'fs';
6
- import { captureIO, type CapturingIO } from '../../command-contract';
7
- import * as path from 'path';
8
- import * as os from 'os';
9
-
10
- let io: CapturingIO;
11
- beforeEach(() => {
12
- io = captureIO();
13
- });
14
-
15
- describe('postevalPlan', () => {
16
- jest.setTimeout(30000); // Retry-based tests with exponential backoff need longer timeout
17
- let tempDir: string;
18
- let planPath: string;
19
- let originalFetch: typeof fetch;
20
- let originalEnv: NodeJS.ProcessEnv;
21
-
22
- beforeAll(() => {
23
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'temp-test-posteval-'));
24
- planPath = path.join(tempDir, 'plan.md');
25
- fs.writeFileSync(planPath, '# Test plan\n- [AC-001] Task 1\n- [AC-002] Task 2');
26
- });
27
-
28
- afterAll(() => {
29
- try {
30
- fs.rmSync(tempDir, { recursive: true, force: true });
31
- } catch {
32
- // ignore
33
- }
34
- });
35
-
36
- beforeEach(() => {
37
- jest.clearAllMocks();
38
- originalFetch = global.fetch;
39
- originalEnv = { ...process.env };
40
- delete process.env.API_PORT;
41
- delete process.env.POSTEVAL_GRACE_MS;
42
- });
43
-
44
- afterEach(() => {
45
- global.fetch = originalFetch;
46
- process.env = originalEnv;
47
- jest.useRealTimers();
48
- });
49
-
50
- it('应该成功验证 plan 覆盖率并打印绿色信息', async () => {
51
- global.fetch = jest.fn().mockResolvedValue({
52
- ok: true,
53
- json: () => Promise.resolve({
54
- completeness: 1,
55
- matchedAcs: ['AC-001', 'AC-002'],
56
- missedAcs: [],
57
- }),
58
- }) as any;
59
-
60
- const { postevalPlan } = await import('../posteval-plan');
61
- await postevalPlan({ planPath }, io);
62
-
63
- expect(io.outText()).toContain('100%');
64
- });
65
-
66
- it('completeness < 1:fail(覆盖率不足,含缺失项)', async () => {
67
- global.fetch = jest.fn().mockResolvedValue({
68
- ok: true,
69
- json: () => Promise.resolve({
70
- completeness: 0.5,
71
- matchedAcs: ['AC-001'],
72
- missedAcs: ['AC-002'],
73
- }),
74
- }) as any;
75
-
76
- const { postevalPlan } = await import('../posteval-plan');
77
- const result = await postevalPlan({ planPath }, io);
78
-
79
- expect(result).toEqual({ kind: 'fail', reason: expect.stringContaining('plan coverage 50% < 100%') });
80
- expect(io.errText()).toContain('50%');
81
- });
82
-
83
- it('completeness < 1 时应该列出缺失项', async () => {
84
- global.fetch = jest.fn().mockResolvedValue({
85
- ok: true,
86
- json: () => Promise.resolve({
87
- completeness: 0.33,
88
- matchedAcs: ['AC-001'],
89
- missedAcs: ['AC-002', 'AC-003'],
90
- }),
91
- }) as any;
92
-
93
- const { postevalPlan } = await import('../posteval-plan');
94
- const result = await postevalPlan({ planPath }, io);
95
-
96
- expect(result).toEqual({
97
- kind: 'fail',
98
- reason: 'plan coverage 33% < 100%,缺 2 项: AC-002; AC-003',
99
- });
100
- expect(io.errText()).toContain('AC-002');
101
- });
102
-
103
- it('4xx 错误应该 exit(1)', async () => {
104
- global.fetch = jest.fn().mockResolvedValue({
105
- ok: false,
106
- status: 400,
107
- statusText: 'Bad Request',
108
- }) as any;
109
-
110
- const { postevalPlan } = await import('../posteval-plan');
111
- const result = await postevalPlan({ planPath }, io);
112
-
113
- expect(result).toEqual({ kind: 'fail', reason: 'PostEval API error: 400 Bad Request' });
114
- expect(io.errText()).toContain('400');
115
- });
116
-
117
- it('5xx 错误:skip(未判定,允许提交;退出码面 0 不变)', async () => {
118
- global.fetch = jest.fn().mockResolvedValue({
119
- ok: false,
120
- status: 503,
121
- statusText: 'Service Unavailable',
122
- }) as any;
123
-
124
- const { postevalPlan } = await import('../posteval-plan');
125
- const result = await postevalPlan({ planPath }, io);
126
-
127
- expect(result).toEqual({ kind: 'skip', reason: expect.stringContaining('服务端错误 503') });
128
- expect(io.errText()).toContain('unavailable');
129
- });
130
-
131
- it('网络错误重试耗尽:skip(未判定,允许提交)', async () => {
132
- global.fetch = jest.fn().mockRejectedValue(new Error('ECONNREFUSED'));
133
-
134
- const { postevalPlan } = await import('../posteval-plan');
135
- const result = await postevalPlan({ planPath }, io);
136
-
137
- expect(result).toEqual({ kind: 'skip', reason: expect.stringContaining('Studio API 不可达') });
138
- expect(io.errText()).toContain('unreachable');
139
- });
140
-
141
- it('应该支持自定义 API_PORT', async () => {
142
- process.env.API_PORT = '3999';
143
- global.fetch = jest.fn().mockResolvedValue({
144
- ok: true,
145
- json: () => Promise.resolve({
146
- completeness: 1,
147
- matchedAcs: ['AC-001'],
148
- missedAcs: [],
149
- }),
150
- }) as any;
151
-
152
- const { postevalPlan } = await import('../posteval-plan');
153
- await postevalPlan({ planPath }, io);
154
-
155
- // Verify the fetch was called with port 3999
156
- const fetchCall = (global.fetch as jest.Mock).mock.calls[0][0];
157
- expect(fetchCall).toContain(':3999');
158
- });
159
-
160
- it('网络错误重试后成功应该不退出', async () => {
161
- // First 2 calls fail, 3rd succeeds
162
- const mockFetch = jest.fn()
163
- .mockRejectedValueOnce(new Error('ECONNREFUSED'))
164
- .mockRejectedValueOnce(new Error('ECONNREFUSED'))
165
- .mockResolvedValueOnce({
166
- ok: true,
167
- json: () => Promise.resolve({
168
- completeness: 1,
169
- matchedAcs: ['AC-001'],
170
- missedAcs: [],
171
- }),
172
- });
173
-
174
- global.fetch = mockFetch as any;
175
-
176
- const { postevalPlan } = await import('../posteval-plan');
177
- const result = await postevalPlan({ planPath }, io);
178
-
179
- // Should have been called 3 times
180
- expect(mockFetch).toHaveBeenCalledTimes(3);
181
- expect(io.outText()).toContain('100%');
182
- expect(result.kind).toBe('ok');
183
- });
184
-
185
- it('completeness 为 0 时应该正确显示 0%', async () => {
186
- global.fetch = jest.fn().mockResolvedValue({
187
- ok: true,
188
- json: () => Promise.resolve({
189
- completeness: 0,
190
- matchedAcs: [],
191
- missedAcs: ['AC-001'],
192
- }),
193
- }) as any;
194
-
195
- const { postevalPlan } = await import('../posteval-plan');
196
- const result = await postevalPlan({ planPath }, io);
197
-
198
- expect(result).toEqual({
199
- kind: 'fail',
200
- reason: 'plan coverage 0% < 100%,缺 1 项: AC-001',
201
- });
202
- expect(io.errText()).toContain('0%');
203
- });
204
- });