@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.
- package/CHANGELOG.md +31 -0
- package/dist/cli/commands/constraints-pack-proposal.d.ts +48 -0
- package/dist/cli/commands/constraints-pack-proposal.d.ts.map +1 -0
- package/dist/cli/commands/constraints-pack-proposal.js +169 -0
- package/dist/cli/commands/constraints-pack-proposal.js.map +1 -0
- package/dist/cli/commands/constraints-reactivate.d.ts +55 -0
- package/dist/cli/commands/constraints-reactivate.d.ts.map +1 -0
- package/dist/cli/commands/constraints-reactivate.js +175 -0
- package/dist/cli/commands/constraints-reactivate.js.map +1 -0
- package/dist/cli/commands/constraints-report.d.ts +12 -1
- package/dist/cli/commands/constraints-report.d.ts.map +1 -1
- package/dist/cli/commands/constraints-report.js +35 -2
- package/dist/cli/commands/constraints-report.js.map +1 -1
- package/dist/cli/commands/constraints-retire.d.ts +49 -4
- package/dist/cli/commands/constraints-retire.d.ts.map +1 -1
- package/dist/cli/commands/constraints-retire.js +116 -34
- package/dist/cli/commands/constraints-retire.js.map +1 -1
- package/dist/cli/commands/definitions.d.ts.map +1 -1
- package/dist/cli/commands/definitions.js +36 -11
- package/dist/cli/commands/definitions.js.map +1 -1
- package/dist/cli/commands/knowledge-view.d.ts +6 -3
- package/dist/cli/commands/knowledge-view.d.ts.map +1 -1
- package/dist/cli/commands/knowledge-view.js +8 -26
- package/dist/cli/commands/knowledge-view.js.map +1 -1
- package/dist/cli/commands/report.d.ts.map +1 -1
- package/dist/cli/commands/report.js +9 -5
- package/dist/cli/commands/report.js.map +1 -1
- package/dist/cli/commands/scaffold-templates.d.ts +1 -1
- package/dist/cli/commands/scaffold-templates.d.ts.map +1 -1
- package/dist/cli/commands/scaffold-templates.js +0 -16
- package/dist/cli/commands/scaffold-templates.js.map +1 -1
- package/dist/cli/state-io.d.ts +8 -1
- package/dist/cli/state-io.d.ts.map +1 -1
- package/dist/cli/state-io.js +2 -1
- package/dist/cli/state-io.js.map +1 -1
- package/dist/core/app-constraints-loader.d.ts +28 -0
- package/dist/core/app-constraints-loader.d.ts.map +1 -0
- package/dist/core/app-constraints-loader.js +187 -0
- package/dist/core/app-constraints-loader.js.map +1 -0
- package/dist/core/constraints/checker.js +1 -1
- package/dist/core/constraints/checker.js.map +1 -1
- package/dist/core/constraints/checkers/index.d.ts +20 -9
- package/dist/core/constraints/checkers/index.d.ts.map +1 -1
- package/dist/core/constraints/checkers/index.js +40 -11
- package/dist/core/constraints/checkers/index.js.map +1 -1
- package/dist/core/constraints/checkers/templated/file-exists.d.ts +16 -0
- package/dist/core/constraints/checkers/templated/file-exists.d.ts.map +1 -0
- package/dist/core/constraints/checkers/templated/file-exists.js +108 -0
- package/dist/core/constraints/checkers/templated/file-exists.js.map +1 -0
- package/dist/core/constraints/checkers/templated/glob.d.ts +10 -0
- package/dist/core/constraints/checkers/templated/glob.d.ts.map +1 -0
- package/dist/core/constraints/checkers/templated/glob.js +51 -0
- package/dist/core/constraints/checkers/templated/glob.js.map +1 -0
- package/dist/core/constraints/checkers/templated/index.d.ts +12 -0
- package/dist/core/constraints/checkers/templated/index.d.ts.map +1 -0
- package/dist/core/constraints/checkers/templated/index.js +17 -0
- package/dist/core/constraints/checkers/templated/index.js.map +1 -0
- package/dist/core/constraints/checkers/templated/regex-scan.d.ts +15 -0
- package/dist/core/constraints/checkers/templated/regex-scan.d.ts.map +1 -0
- package/dist/core/constraints/checkers/templated/regex-scan.js +129 -0
- package/dist/core/constraints/checkers/templated/regex-scan.js.map +1 -0
- package/dist/core/constraints/checkers/templates-registry.d.ts +17 -0
- package/dist/core/constraints/checkers/templates-registry.d.ts.map +1 -0
- package/dist/core/constraints/checkers/templates-registry.js +23 -0
- package/dist/core/constraints/checkers/templates-registry.js.map +1 -0
- package/dist/core/constraints/checkers/types.d.ts +14 -0
- package/dist/core/constraints/checkers/types.d.ts.map +1 -1
- package/dist/core/constraints/checkers/types.js.map +1 -1
- package/dist/core/constraints/definitions/guidelines.d.ts.map +1 -1
- package/dist/core/constraints/definitions/guidelines.js +0 -14
- package/dist/core/constraints/definitions/guidelines.js.map +1 -1
- package/dist/core/constraints/definitions/iron-laws.d.ts.map +1 -1
- package/dist/core/constraints/definitions/iron-laws.js +15 -0
- package/dist/core/constraints/definitions/iron-laws.js.map +1 -1
- package/dist/core/constraints/usage-report.d.ts +59 -7
- package/dist/core/constraints/usage-report.d.ts.map +1 -1
- package/dist/core/constraints/usage-report.js +62 -13
- package/dist/core/constraints/usage-report.js.map +1 -1
- package/dist/core/project-config-loader.d.ts +9 -6
- package/dist/core/project-config-loader.d.ts.map +1 -1
- package/dist/core/project-config-loader.js +18 -8
- package/dist/core/project-config-loader.js.map +1 -1
- package/dist/gates/command.d.ts.map +1 -1
- package/dist/gates/command.js +50 -0
- package/dist/gates/command.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -2
- package/dist/index.js.map +1 -1
- package/dist/knowledge/lifecycle.d.ts +14 -0
- package/dist/knowledge/lifecycle.d.ts.map +1 -1
- package/dist/knowledge/lifecycle.js +50 -0
- package/dist/knowledge/lifecycle.js.map +1 -1
- package/dist/knowledge/lint.d.ts +17 -0
- package/dist/knowledge/lint.d.ts.map +1 -1
- package/dist/knowledge/lint.js +52 -0
- package/dist/knowledge/lint.js.map +1 -1
- package/dist/knowledge/store.d.ts +7 -0
- package/dist/knowledge/store.d.ts.map +1 -1
- package/dist/knowledge/store.js +48 -13
- package/dist/knowledge/store.js.map +1 -1
- package/dist/knowledge/types.d.ts +5 -1
- package/dist/knowledge/types.d.ts.map +1 -1
- package/dist/knowledge/types.js +5 -1
- package/dist/knowledge/types.js.map +1 -1
- package/dist/presets/standard.d.ts +1 -1
- package/dist/presets/standard.d.ts.map +1 -1
- package/dist/presets/standard.js +2 -3
- package/dist/presets/standard.js.map +1 -1
- package/dist/types/constraint.d.ts +24 -0
- package/dist/types/constraint.d.ts.map +1 -1
- package/dist/types/constraint.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/app-constraints-loader.test.ts +220 -0
- package/src/__tests__/iron-laws.test.ts +5 -5
- package/src/__tests__/public-exports.test.ts +7 -0
- package/src/__tests__/public-type-surface.test.ts +5 -0
- package/src/__tests__/usage-report-public-export.test.ts +21 -0
- package/src/cli/commands/CONTEXT.md +5 -4
- package/src/cli/commands/__tests__/check.test.ts +22 -4
- package/src/cli/commands/__tests__/constraints-pack-proposal.test.ts +142 -0
- package/src/cli/commands/__tests__/constraints-reactivate.test.ts +210 -0
- package/src/cli/commands/__tests__/constraints-report-json-flag.test.ts +101 -0
- package/src/cli/commands/__tests__/constraints-report.test.ts +175 -6
- package/src/cli/commands/__tests__/constraints-retire.test.ts +135 -10
- package/src/cli/commands/__tests__/init-ondisk.test.ts +1 -1
- package/src/cli/commands/__tests__/init.test.ts +1 -5
- package/src/cli/commands/__tests__/knowledge.test.ts +6 -16
- package/src/cli/commands/__tests__/numeric-flag-assembly.test.ts +1 -1
- package/src/cli/commands/__tests__/passes-gate-dead-options.test.ts +1 -1
- package/src/cli/commands/__tests__/registry.test.ts +1 -1
- package/src/cli/commands/__tests__/report-app-constraints.test.ts +67 -0
- package/src/cli/commands/constraints-pack-proposal.ts +172 -0
- package/src/cli/commands/constraints-reactivate.ts +197 -0
- package/src/cli/commands/constraints-report.ts +44 -2
- package/src/cli/commands/constraints-retire.ts +129 -36
- package/src/cli/commands/definitions.ts +36 -11
- package/src/cli/commands/knowledge-view.ts +9 -28
- package/src/cli/commands/report.ts +9 -5
- package/src/cli/commands/scaffold-templates.ts +0 -16
- package/src/cli/state-io.ts +8 -1
- package/src/core/CONTEXT.md +5 -4
- package/src/core/app-constraints-loader.ts +165 -0
- package/src/core/constraints/__tests__/collect-constraints.test.ts +3 -1
- package/src/core/constraints/__tests__/usage-report.test.ts +63 -0
- package/src/core/constraints/checker.ts +1 -1
- package/src/core/constraints/checkers/__tests__/templated-checkers.test.ts +180 -0
- package/src/core/constraints/checkers/__tests__/templates-registry.test.ts +81 -0
- package/src/core/constraints/checkers/index.ts +44 -10
- package/src/core/constraints/checkers/templated/file-exists.ts +75 -0
- package/src/core/constraints/checkers/templated/glob.ts +46 -0
- package/src/core/constraints/checkers/templated/index.ts +12 -0
- package/src/core/constraints/checkers/templated/regex-scan.ts +132 -0
- package/src/core/constraints/checkers/templates-registry.ts +22 -0
- package/src/core/constraints/checkers/types.ts +15 -0
- package/src/core/constraints/definitions/guidelines.ts +0 -15
- package/src/core/constraints/definitions/iron-laws.ts +16 -0
- package/src/core/constraints/usage-report.ts +129 -14
- package/src/core/project-config-loader.ts +19 -8
- package/src/gates/__tests__/command.test.ts +54 -0
- package/src/gates/command.ts +51 -0
- package/src/index.ts +20 -0
- package/src/knowledge/CONTEXT.md +7 -4
- package/src/knowledge/__tests__/lifecycle.test.ts +81 -0
- package/src/knowledge/__tests__/lint.test.ts +84 -0
- package/src/knowledge/__tests__/store.test.ts +70 -0
- package/src/knowledge/lifecycle.ts +49 -0
- package/src/knowledge/lint.ts +60 -1
- package/src/knowledge/store.ts +48 -11
- package/src/knowledge/types.ts +7 -1
- package/src/monitoring/CONTEXT.md +1 -1
- package/src/presets/__tests__/standard.test.ts +5 -4
- package/src/presets/standard.ts +2 -3
- package/src/types/constraint.ts +28 -0
- package/dist/cli/commands/posteval-plan.d.ts +0 -15
- package/dist/cli/commands/posteval-plan.d.ts.map +0 -1
- package/dist/cli/commands/posteval-plan.js +0 -86
- package/dist/cli/commands/posteval-plan.js.map +0 -1
- package/src/cli/commands/__tests__/posteval-plan.test.ts +0 -204
- package/src/cli/commands/posteval-plan.ts +0 -98
package/src/knowledge/lint.ts
CHANGED
|
@@ -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({
|
package/src/knowledge/store.ts
CHANGED
|
@@ -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
|
-
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
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
|
|
package/src/knowledge/types.ts
CHANGED
|
@@ -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(
|
|
36
|
-
expect(RELAXED_PRESET.warnings).toHaveLength(
|
|
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.
|
|
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
|
|
package/src/presets/standard.ts
CHANGED
|
@@ -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
|
/**
|
package/src/types/constraint.ts
CHANGED
|
@@ -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":";AAAA;;;;;;;;GAQG;;;;;AAoCH,oCAqDC;AAvFD,kDAA0B;AAC1B,0DAAmG;AAMnG,MAAM,WAAW,GAAG,CAAC,CAAC;AACtB,MAAM,cAAc,GAAG,IAAI,CAAC;AAE5B,KAAK,UAAU,cAAc,CAAC,GAAW,EAAE,IAAY,EAAE,OAAe;IACtE,IAAI,SAAc,CAAC;IACnB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,EAAE,CAAC,EAAE,EAAE,CAAC;QACjC,IAAI,CAAC;YACH,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;YACzC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,CAAC;YAC3D,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;gBAC3B,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;gBAC/C,IAAI;gBACJ,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAC;YACH,YAAY,CAAC,OAAO,CAAC,CAAC;YACtB,OAAO,GAAG,CAAC;QACb,CAAC;QAAC,OAAO,KAAU,EAAE,CAAC;YACpB,SAAS,GAAG,KAAK,CAAC;YAClB,IAAI,CAAC,GAAG,OAAO,GAAG,CAAC,EAAE,CAAC;gBACpB,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,cAAc,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,sBAAsB;YACrG,CAAC;QACH,CAAC;IACH,CAAC;IACD,MAAM,SAAS,CAAC;AAClB,CAAC;AAEM,KAAK,UAAU,YAAY,CAChC,OAA4B,EAC5B,KAAgB,4BAAS;IAEzB,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,MAAM,CAAC;IAC/C,MAAM,GAAG,GAAG,oBAAoB,OAAO,wCAAwC,CAAC;IAEhF,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,cAAc,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,EAAE,WAAW,CAAC,CAAC;QAEnG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG,EAAE,CAAC;gBACtB,mFAAmF;gBACnF,IAAA,2BAAQ,EAAC,EAAE,EAAE,eAAK,CAAC,MAAM,CAAC,iCAAiC,GAAG,CAAC,MAAM,iCAAiC,CAAC,CAAC,CAAC;gBACzG,IAAA,2BAAQ,EAAC,EAAE,EAAE,eAAK,CAAC,MAAM,CAAC,YAAY,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;gBAC3D,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,sBAAsB,GAAG,CAAC,MAAM,MAAM,EAAE,CAAC;YAC1E,CAAC;YACD,IAAA,2BAAQ,EAAC,EAAE,EAAE,eAAK,CAAC,GAAG,CAAC,yBAAyB,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;YACjF,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,uBAAuB,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,UAAU,EAAE,EAAE,CAAC;QACzF,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,IAAI,EAI5B,CAAC;QAEF,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,YAAY,GAAG,GAAG,CAAC,CAAC;QAElD,IAAI,MAAM,CAAC,YAAY,GAAG,CAAC,EAAE,CAAC;YAC5B,IAAA,2BAAQ,EAAC,EAAE,EAAE,eAAK,CAAC,GAAG,CAAC,oBAAoB,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,MAAM,IAAI,MAAM,CAAC,UAAU,CAAC,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YACxI,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAChC,IAAA,2BAAQ,EAAC,EAAE,EAAE,eAAK,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC;gBACzC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,IAAA,2BAAQ,EAAC,EAAE,EAAE,eAAK,CAAC,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;YACrF,CAAC;YACD,OAAO;gBACL,IAAI,EAAE,MAAM;gBACZ,MAAM,EAAE,iBAAiB,GAAG,cAAc,MAAM,CAAC,SAAS,CAAC,MAAM,OAAO,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;aACtG,CAAC;QACJ,CAAC;QAED,IAAA,sBAAG,EAAC,EAAE,EAAE,eAAK,CAAC,KAAK,CAAC,oBAAoB,GAAG,GAAG,CAAC,CAAC,CAAC;QACjD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IACxB,CAAC;IAAC,OAAO,KAAU,EAAE,CAAC;QACpB,wFAAwF;QACxF,IAAA,2BAAQ,EAAC,EAAE,EAAE,eAAK,CAAC,MAAM,CAAC,oCAAoC,WAAW,wCAAwC,CAAC,CAAC,CAAC;QACpH,IAAA,2BAAQ,EAAC,EAAE,EAAE,eAAK,CAAC,MAAM,CAAC,YAAY,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;QAC3D,IAAA,2BAAQ,EAAC,EAAE,EAAE,eAAK,CAAC,MAAM,CAAC,aAAa,KAAK,EAAE,IAAI,IAAI,KAAK,EAAE,OAAO,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;QAC1F,OAAO;YACL,IAAI,EAAE,MAAM;YACZ,MAAM,EAAE,kBAAkB,KAAK,EAAE,IAAI,IAAI,KAAK,EAAE,OAAO,IAAI,MAAM,CAAC,KAAK,CAAC,OAAO;SAChF,CAAC;IACJ,CAAC;AACH,CAAC"}
|
|
@@ -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
|
-
});
|