@dommaker/harness 0.17.1 → 0.18.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 +22 -0
- package/README.md +5 -4
- package/bin/harness.js +3 -1
- package/bin/install-precommit-hook.sh +76 -0
- package/dist/architecture/cross-project-checker.js +2 -2
- package/dist/architecture/cross-project-checker.js.map +1 -1
- package/dist/cli/commands/constraints-report.js +2 -2
- package/dist/cli/commands/constraints-report.js.map +1 -1
- package/dist/cli/commands/constraints-retire.d.ts +24 -6
- package/dist/cli/commands/constraints-retire.d.ts.map +1 -1
- package/dist/cli/commands/constraints-retire.js +96 -19
- package/dist/cli/commands/constraints-retire.js.map +1 -1
- package/dist/cli/commands/doc-freshness-check.js +1 -1
- package/dist/cli/commands/doc-freshness-check.js.map +1 -1
- package/dist/cli/commands/init.js +14 -14
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/sync-docs/index.d.ts.map +1 -1
- package/dist/cli/commands/sync-docs/index.js +14 -6
- package/dist/cli/commands/sync-docs/index.js.map +1 -1
- package/dist/cli/commands/sync-docs/project-reader.d.ts.map +1 -1
- package/dist/cli/commands/sync-docs/project-reader.js +9 -7
- package/dist/cli/commands/sync-docs/project-reader.js.map +1 -1
- package/dist/cli/commands/validate.js +3 -3
- package/dist/cli/commands/validate.js.map +1 -1
- package/dist/cli/session-mining/text.js +1 -1
- package/dist/cli/session-mining/text.js.map +1 -1
- package/dist/core/constraints/definitions/iron-laws.js +2 -2
- package/dist/core/constraints/definitions/iron-laws.js.map +1 -1
- package/dist/core/constraints/injection-drift.d.ts +1 -1
- package/dist/core/constraints/injection-drift.d.ts.map +1 -1
- package/dist/core/constraints/injection-drift.js +1 -1
- package/dist/core/constraints/injection-drift.js.map +1 -1
- package/dist/core/effective-constraints.d.ts +2 -1
- package/dist/core/effective-constraints.d.ts.map +1 -1
- package/dist/core/effective-constraints.js +2 -1
- package/dist/core/effective-constraints.js.map +1 -1
- package/dist/core/project-config-loader.d.ts +2 -1
- package/dist/core/project-config-loader.d.ts.map +1 -1
- package/dist/core/project-config-loader.js +8 -2
- package/dist/core/project-config-loader.js.map +1 -1
- package/dist/hooks/bootstrap.js +1 -1
- package/dist/hooks/bootstrap.js.map +1 -1
- package/dist/knowledge/store.js +1 -1
- package/dist/knowledge/store.js.map +1 -1
- package/dist/knowledge/types.d.ts +2 -0
- package/dist/knowledge/types.d.ts.map +1 -1
- package/dist/knowledge/types.js.map +1 -1
- package/dist/safety/output-guardrail.js +2 -2
- package/dist/safety/output-guardrail.js.map +1 -1
- package/dist/spec/annotation-checker.js +1 -1
- package/dist/spec/annotation-checker.js.map +1 -1
- package/dist/types/project-config.d.ts +15 -0
- package/dist/types/project-config.d.ts.map +1 -1
- package/dist/utils/file-walk.d.ts +13 -1
- package/dist/utils/file-walk.d.ts.map +1 -1
- package/dist/utils/file-walk.js +16 -3
- package/dist/utils/file-walk.js.map +1 -1
- package/package.json +6 -2
- package/src/__tests__/checkpoint.test.ts +2 -2
- package/src/__tests__/clean-state-extra.test.ts +1 -1
- package/src/__tests__/constraint-doctor.test.ts +1 -1
- package/src/__tests__/doctor-extra.test.ts +0 -9
- package/src/__tests__/effective-constraints.test.ts +21 -0
- package/src/__tests__/hooks-pipeline.test.ts +1 -1
- package/src/__tests__/iron-laws.test.ts +11 -0
- package/src/__tests__/passes-gate-extension.test.ts +3 -3
- package/src/__tests__/passes-gate-extra.test.ts +1 -1
- package/src/__tests__/performance-analyzer.test.ts +0 -2
- package/src/__tests__/traces.test.ts +1 -2
- package/src/architecture/cross-project-checker.ts +2 -2
- package/src/cli/commands/CONTEXT.md +1 -1
- package/src/cli/commands/__tests__/check-drift.test.ts +1 -1
- package/src/cli/commands/__tests__/check-skip-output.test.ts +0 -2
- package/src/cli/commands/__tests__/command.test.ts +1 -1
- package/src/cli/commands/__tests__/constraints-report.test.ts +1 -1
- package/src/cli/commands/__tests__/constraints-retire.test.ts +165 -4
- package/src/cli/commands/__tests__/failure.test.ts +0 -1
- package/src/cli/commands/__tests__/init.test.ts +40 -0
- package/src/cli/commands/__tests__/status-extra.test.ts +0 -2
- package/src/cli/commands/__tests__/status.test.ts +0 -2
- package/src/cli/commands/__tests__/sync-docs.test.ts +90 -0
- package/src/cli/commands/constraints-report.ts +2 -2
- package/src/cli/commands/constraints-retire.ts +114 -21
- package/src/cli/commands/doc-freshness-check.ts +1 -1
- package/src/cli/commands/init.ts +14 -14
- package/src/cli/commands/sync-docs/index.ts +20 -7
- package/src/cli/commands/sync-docs/project-reader.ts +10 -8
- package/src/cli/commands/validate.ts +3 -3
- package/src/cli/session-mining/text.ts +1 -1
- package/src/context/__tests__/progressive-loader.test.ts +6 -6
- package/src/core/CONTEXT.md +1 -1
- package/src/core/constraints/__tests__/injection-drift.test.ts +1 -1
- package/src/core/constraints/__tests__/interceptor.test.ts +3 -3
- package/src/core/constraints/definitions/iron-laws.ts +2 -2
- package/src/core/constraints/injection-drift.ts +1 -1
- package/src/core/effective-constraints.ts +2 -1
- package/src/core/project-config-loader.ts +8 -2
- package/src/gates/__tests__/acceptance.test.ts +0 -1
- package/src/gates/__tests__/command.test.ts +1 -1
- package/src/hooks/bootstrap.ts +1 -1
- package/src/knowledge/__tests__/doctor.test.ts +2 -2
- package/src/knowledge/__tests__/ingest.test.ts +2 -2
- package/src/knowledge/__tests__/migration.test.ts +0 -6
- package/src/knowledge/__tests__/store.test.ts +14 -1
- package/src/knowledge/store.ts +1 -1
- package/src/knowledge/types.ts +2 -0
- package/src/llm/__tests__/adapter.test.ts +1 -1
- package/src/monitoring/__tests__/knowledge-doctor.test.ts +0 -1
- package/src/monitoring/__tests__/performance-collector.test.ts +1 -2
- package/src/safety/output-guardrail.ts +2 -2
- package/src/spec/__tests__/annotation-checker.test.ts +0 -1
- package/src/spec/annotation-checker.ts +1 -1
- package/src/types/project-config.ts +12 -0
- package/src/utils/__tests__/file-walk.test.ts +53 -0
- package/src/utils/file-walk.ts +23 -3
- package/src/verification/__tests__/loop.test.ts +1 -1
|
@@ -2,17 +2,28 @@
|
|
|
2
2
|
* harness constraints retire —— 约束退役(ADR-0001 决策 2/5)
|
|
3
3
|
*
|
|
4
4
|
* 建议层全自动(候选诊断复用 report 数据层),执行层保留一次人确认。
|
|
5
|
-
*
|
|
5
|
+
* 落盘形态(#82 D6 一处真相):
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* - 内置约束 → config.yml `enabled: false` + `retired` 元数据:
|
|
8
|
+
*
|
|
9
|
+
* constraints:
|
|
10
|
+
* <id>:
|
|
11
|
+
* enabled: false
|
|
12
|
+
* retired: { at, reason, stats: { total, fail, failRate } }
|
|
13
|
+
*
|
|
14
|
+
* - custom 约束 → custom-constraints.yml 条目内 `retired` 元数据段
|
|
15
|
+
* (不拆 config.yml 第二处):
|
|
16
|
+
*
|
|
17
|
+
* custom_constraints:
|
|
18
|
+
* <id>:
|
|
19
|
+
* rule: ...
|
|
20
|
+
* retired: { at, reason, stats: { total, fail, failRate } }
|
|
11
21
|
*
|
|
12
22
|
* 每条同时写一条 KnowledgeStore 记录(consumptionMode: 'signal'),
|
|
13
23
|
* 并同步 CLAUDE.md 注入段(存在 HARNESS_CONSTRAINTS 标记时)。
|
|
14
24
|
*
|
|
15
|
-
* retire
|
|
25
|
+
* retire 不是删除——恢复方法:内置删 config.yml 中 constraints.<id> 段;
|
|
26
|
+
* custom 删 custom-constraints.yml 中 custom_constraints.<id>.retired 段。
|
|
16
27
|
*
|
|
17
28
|
* 交互与执行分离:retireConstraint 为纯执行逻辑(同步、可测),
|
|
18
29
|
* runRetireInteractive 只做 readline 交互,IO 流可注入。
|
|
@@ -36,7 +47,6 @@ import {
|
|
|
36
47
|
readProjectTraces,
|
|
37
48
|
type RetireCandidate,
|
|
38
49
|
} from '../../core/constraints/usage-report';
|
|
39
|
-
import { getConstraintsMeta } from './constraints';
|
|
40
50
|
|
|
41
51
|
export interface RetireExecuteOptions {
|
|
42
52
|
/** 退役原因(可空) */
|
|
@@ -52,6 +62,8 @@ export interface RetireResult {
|
|
|
52
62
|
status: RetireStatus;
|
|
53
63
|
/** 是否 check 层 iron(交互模式据此追加确认) */
|
|
54
64
|
isIronLaw: boolean;
|
|
65
|
+
/** 退役落点:custom 约束落 custom-constraints.yml(#82 D6),内置落 config.yml */
|
|
66
|
+
landing: 'config.yml' | 'custom-constraints.yml';
|
|
55
67
|
stats: { total: number; fail: number; failRate: number };
|
|
56
68
|
/** CLAUDE.md 注入段是否已同步 */
|
|
57
69
|
claudeMdSynced: boolean;
|
|
@@ -62,9 +74,12 @@ export interface RetireResult {
|
|
|
62
74
|
export interface ConstraintsRetireOptions {
|
|
63
75
|
projectPath?: string;
|
|
64
76
|
reason?: string;
|
|
77
|
+
/** 直达模式显式确认(--yes):ADR-0001 决策 2 人确认闸门,无此 flag 直达拒绝执行 */
|
|
78
|
+
yes?: boolean;
|
|
65
79
|
}
|
|
66
80
|
|
|
67
81
|
interface RetireTargetInfo {
|
|
82
|
+
source: 'builtin' | 'custom';
|
|
68
83
|
isIronLaw: boolean;
|
|
69
84
|
level: Constraint['level'];
|
|
70
85
|
description?: string;
|
|
@@ -80,6 +95,7 @@ function findRetireTarget(projectRoot: string, id: string): RetireTargetInfo | u
|
|
|
80
95
|
const builtIn = getConstraint(id);
|
|
81
96
|
if (builtIn) {
|
|
82
97
|
return {
|
|
98
|
+
source: 'builtin',
|
|
83
99
|
isIronLaw: builtIn.kind === 'check' && builtIn.level === 'iron_law',
|
|
84
100
|
level: builtIn.level,
|
|
85
101
|
description: builtIn.description,
|
|
@@ -95,11 +111,13 @@ function findRetireTarget(projectRoot: string, id: string): RetireTargetInfo | u
|
|
|
95
111
|
if (custom) {
|
|
96
112
|
const level = custom.level || 'guideline';
|
|
97
113
|
return {
|
|
114
|
+
source: 'custom',
|
|
98
115
|
isIronLaw: false, // custom 约束统一 kind='prompt'(无 checker),不存在 check 层 iron
|
|
99
116
|
level,
|
|
100
117
|
description: custom.description,
|
|
101
118
|
rule: custom.rule,
|
|
102
119
|
message: custom.message,
|
|
120
|
+
promptInjection: custom.promptInjection,
|
|
103
121
|
};
|
|
104
122
|
}
|
|
105
123
|
|
|
@@ -151,6 +169,41 @@ function writeRetireConfig(
|
|
|
151
169
|
}
|
|
152
170
|
}
|
|
153
171
|
|
|
172
|
+
/**
|
|
173
|
+
* 写 custom-constraints.yml 退役段(#82 D6 统一落点)
|
|
174
|
+
*
|
|
175
|
+
* 在 custom_constraints.<id> 条目内追加 retired 元数据,保留规则原文。
|
|
176
|
+
* js-yaml 不保留注释:原文件含注释行时重写会丢失,console 说明。
|
|
177
|
+
*/
|
|
178
|
+
function writeCustomRetireYml(
|
|
179
|
+
projectRoot: string,
|
|
180
|
+
fileName: string,
|
|
181
|
+
id: string,
|
|
182
|
+
retiredMeta: { at: string; reason: string; stats: { total: number; fail: number; failRate: number } }
|
|
183
|
+
): void {
|
|
184
|
+
const filePath = path.join(projectRoot, '.harness', fileName);
|
|
185
|
+
|
|
186
|
+
let raw: Record<string, unknown> = {};
|
|
187
|
+
let hadComments = false;
|
|
188
|
+
if (fs.existsSync(filePath)) {
|
|
189
|
+
const original = fs.readFileSync(filePath, 'utf-8');
|
|
190
|
+
hadComments = original.split('\n').some(l => l.trimStart().startsWith('#'));
|
|
191
|
+
raw = (yaml.load(original) as Record<string, unknown>) ?? {};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const customs = (raw.custom_constraints ?? {}) as Record<string, Record<string, unknown>>;
|
|
195
|
+
const prev = customs[id] ?? {};
|
|
196
|
+
customs[id] = { ...prev, retired: retiredMeta };
|
|
197
|
+
raw.custom_constraints = customs;
|
|
198
|
+
|
|
199
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
200
|
+
fs.writeFileSync(filePath, yaml.dump(raw, { lineWidth: 120 }), 'utf-8');
|
|
201
|
+
|
|
202
|
+
if (hadComments) {
|
|
203
|
+
console.log(chalk.yellow(' ⚠️ custom-constraints.yml 已重写:js-yaml 不保留原文件注释'));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
154
207
|
/**
|
|
155
208
|
* 写 KnowledgeStore 退役记录(consumptionMode: 'signal')
|
|
156
209
|
*/
|
|
@@ -251,15 +304,24 @@ export function retireConstraint(
|
|
|
251
304
|
|
|
252
305
|
const target = findRetireTarget(projectRoot, id);
|
|
253
306
|
const emptyStats = { total: 0, fail: 0, failRate: 0 };
|
|
307
|
+
const landing: RetireResult['landing'] =
|
|
308
|
+
target?.source === 'custom' ? 'custom-constraints.yml' : 'config.yml';
|
|
254
309
|
if (!target) {
|
|
255
|
-
return { id, status: 'unknown_id', isIronLaw: false, stats: emptyStats, claudeMdSynced: false };
|
|
310
|
+
return { id, status: 'unknown_id', isIronLaw: false, landing, stats: emptyStats, claudeMdSynced: false };
|
|
256
311
|
}
|
|
257
312
|
|
|
258
|
-
//
|
|
313
|
+
// 已退役保护:内置看 config.yml constraints.<id>.enabled === false;
|
|
314
|
+
// custom 看 yml 条目 retired 元数据(历史落点 config.yml enabled:false 同样保护)
|
|
259
315
|
const rawConfig = loadRawProjectConfig(projectRoot);
|
|
260
316
|
const existing = (rawConfig?.constraints as Record<string, { enabled?: boolean; retired?: unknown }> | undefined)?.[id];
|
|
261
|
-
if (
|
|
262
|
-
|
|
317
|
+
if (target.source === 'custom') {
|
|
318
|
+
const loader = new ProjectConfigLoader(projectRoot);
|
|
319
|
+
loader.load();
|
|
320
|
+
if (loader.getCustomConstraints()[id]?.retired || existing?.enabled === false) {
|
|
321
|
+
return { id, status: 'already_retired', isIronLaw: target.isIronLaw, landing, stats: emptyStats, claudeMdSynced: false };
|
|
322
|
+
}
|
|
323
|
+
} else if (existing?.enabled === false) {
|
|
324
|
+
return { id, status: 'already_retired', isIronLaw: target.isIronLaw, landing, stats: emptyStats, claudeMdSynced: false };
|
|
263
325
|
}
|
|
264
326
|
|
|
265
327
|
// 历史统计(来自 traces.log)
|
|
@@ -271,8 +333,15 @@ export function retireConstraint(
|
|
|
271
333
|
failRate: evaluated > 0 ? (usage!.fail / evaluated) : 0,
|
|
272
334
|
};
|
|
273
335
|
|
|
274
|
-
// 1. config.yml
|
|
275
|
-
|
|
336
|
+
// 1. 落盘退役(custom → yml 条目 retired 段;内置 → config.yml,原文均保留)
|
|
337
|
+
if (target.source === 'custom') {
|
|
338
|
+
const loader = new ProjectConfigLoader(projectRoot);
|
|
339
|
+
loader.load();
|
|
340
|
+
const customFile = loader.getConfig().custom_constraints_file ?? 'custom-constraints.yml';
|
|
341
|
+
writeCustomRetireYml(projectRoot, customFile, id, { at: iso, reason, stats });
|
|
342
|
+
} else {
|
|
343
|
+
writeRetireConfig(projectRoot, id, { at: iso, reason, stats });
|
|
344
|
+
}
|
|
276
345
|
|
|
277
346
|
// 2. KnowledgeStore
|
|
278
347
|
const knowledgeEntryId = saveRetireKnowledge(projectRoot, id, target, reason, stats, iso);
|
|
@@ -280,7 +349,7 @@ export function retireConstraint(
|
|
|
280
349
|
// 3. CLAUDE.md 注入段同步
|
|
281
350
|
const claudeMdSynced = syncClaudeMdInjection(projectRoot);
|
|
282
351
|
|
|
283
|
-
return { id, status: 'retired', isIronLaw: target.isIronLaw, stats, claudeMdSynced, knowledgeEntryId };
|
|
352
|
+
return { id, status: 'retired', isIronLaw: target.isIronLaw, landing, stats, claudeMdSynced, knowledgeEntryId };
|
|
284
353
|
}
|
|
285
354
|
|
|
286
355
|
/**
|
|
@@ -294,14 +363,20 @@ export function printRetireResult(result: RetireResult): void {
|
|
|
294
363
|
case 'already_retired':
|
|
295
364
|
console.log(chalk.yellow(`⚠️ ${result.id}: 已处于退役状态(config.yml 中 enabled: false),跳过`));
|
|
296
365
|
return;
|
|
297
|
-
case 'retired':
|
|
366
|
+
case 'retired': {
|
|
298
367
|
console.log(chalk.green(`✅ ${result.id}: 已退役`));
|
|
299
368
|
console.log(` 历史统计: total=${result.stats.total} fail=${result.stats.fail} fail率=${Math.round(result.stats.failRate * 100)}%`);
|
|
300
369
|
console.log(` 知识沉淀: ${result.knowledgeEntryId}(.harness/knowledge)`);
|
|
301
370
|
if (result.claudeMdSynced) {
|
|
302
371
|
console.log(' 已同步 CLAUDE.md 注入段');
|
|
303
372
|
}
|
|
304
|
-
|
|
373
|
+
const recovery =
|
|
374
|
+
result.landing === 'custom-constraints.yml'
|
|
375
|
+
? `删除 custom-constraints.yml 中 custom_constraints.${result.id}.retired 段`
|
|
376
|
+
: `删除 config.yml 中 constraints.${result.id} 段`;
|
|
377
|
+
console.log(chalk.gray(` retire 不是删除——恢复方法:${recovery}`));
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
305
380
|
}
|
|
306
381
|
}
|
|
307
382
|
|
|
@@ -376,7 +451,7 @@ export async function runRetireInteractive(
|
|
|
376
451
|
}
|
|
377
452
|
|
|
378
453
|
// 逐条收集 reason + iron 二次确认
|
|
379
|
-
const plan: { id: string; reason: string }[] = [];
|
|
454
|
+
const plan: { id: string; reason: string; source: RetireTargetInfo['source'] }[] = [];
|
|
380
455
|
for (const id of selectedIds) {
|
|
381
456
|
const target = findRetireTarget(projectRoot, id);
|
|
382
457
|
if (!target) {
|
|
@@ -391,7 +466,7 @@ export async function runRetireInteractive(
|
|
|
391
466
|
}
|
|
392
467
|
}
|
|
393
468
|
const reason = await ask(`退役原因(${id},可留空): `);
|
|
394
|
-
plan.push({ id, reason });
|
|
469
|
+
plan.push({ id, reason, source: target.source });
|
|
395
470
|
}
|
|
396
471
|
|
|
397
472
|
if (plan.length === 0) {
|
|
@@ -403,7 +478,11 @@ export async function runRetireInteractive(
|
|
|
403
478
|
console.log();
|
|
404
479
|
console.log(chalk.bold('将执行以下变更:'));
|
|
405
480
|
for (const p of plan) {
|
|
406
|
-
|
|
481
|
+
if (p.source === 'custom') {
|
|
482
|
+
console.log(` - custom-constraints.yml: custom_constraints.${p.id}.retired 元数据(原因: ${p.reason || '(空)'})`);
|
|
483
|
+
} else {
|
|
484
|
+
console.log(` - config.yml: constraints.${p.id}.enabled=false + retired 元数据(原因: ${p.reason || '(空)'})`);
|
|
485
|
+
}
|
|
407
486
|
console.log(` - KnowledgeStore: 写入 constraint-retired-${p.id}`);
|
|
408
487
|
}
|
|
409
488
|
console.log(' - CLAUDE.md: 若含 HARNESS_CONSTRAINTS 标记段则同步重渲染');
|
|
@@ -425,15 +504,29 @@ export async function runRetireInteractive(
|
|
|
425
504
|
|
|
426
505
|
/**
|
|
427
506
|
* CLI handler: harness constraints retire [id]
|
|
507
|
+
*
|
|
508
|
+
* 人确认闸门(#24,ADR-0001 决策 2):带 id 的直达路径必须显式 `--yes`,
|
|
509
|
+
* 无 `--yes` 报错 + 非零退出码,提示改用 `--yes` 或交互模式;不落盘任何文件。
|
|
428
510
|
*/
|
|
429
511
|
export async function constraintsRetire(id?: string, options: ConstraintsRetireOptions = {}): Promise<void> {
|
|
430
512
|
const projectRoot = options.projectPath || process.cwd();
|
|
431
513
|
|
|
432
514
|
if (id) {
|
|
433
|
-
//
|
|
515
|
+
// 非交互直达:执行层人确认对所有入口成立(含程序化调用方)
|
|
516
|
+
if (!options.yes) {
|
|
517
|
+
console.error(
|
|
518
|
+
chalk.red(`❌ 直达退役需要显式人确认(ADR-0001 决策 2:执行层保留一次人确认),未做任何变更\n`) +
|
|
519
|
+
` 带 --yes 显式确认直达:harness constraints retire ${id} --yes` +
|
|
520
|
+
`${options.reason ? ` --reason "${options.reason}"` : ''}\n` +
|
|
521
|
+
` 或去掉 id 走交互确认:harness constraints retire`
|
|
522
|
+
);
|
|
523
|
+
process.exitCode = 1;
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
434
527
|
const result = retireConstraint(projectRoot, id, { reason: options.reason });
|
|
435
528
|
if (result.status === 'retired' && result.isIronLaw) {
|
|
436
|
-
console.log(chalk.yellow(`⚠️ ${id} 是一条 Iron Law
|
|
529
|
+
console.log(chalk.yellow(`⚠️ ${id} 是一条 Iron Law,已通过 --yes 直达退役(交互模式会要求二次确认)`));
|
|
437
530
|
}
|
|
438
531
|
printRetireResult(result);
|
|
439
532
|
return;
|
|
@@ -358,7 +358,7 @@ export async function docFreshnessCheck(
|
|
|
358
358
|
const content = fs.readFileSync(resolvedPath, 'utf-8');
|
|
359
359
|
|
|
360
360
|
// 提取声明
|
|
361
|
-
|
|
361
|
+
const claims = extractClaims(content, resolvedPath);
|
|
362
362
|
|
|
363
363
|
// 如果指定了变更文件,只验证与变更文件相关的声明
|
|
364
364
|
if (options.changedFiles) {
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -256,7 +256,7 @@ function printSnippets(): void {
|
|
|
256
256
|
console.log();
|
|
257
257
|
console.log(chalk.cyan(GITHUB_ACTIONS_SNIPPET));
|
|
258
258
|
|
|
259
|
-
console.log(chalk.blue('💡 提示: 运行 harness init 自动创建配置文件'));
|
|
259
|
+
console.log(chalk.blue('💡 提示: 运行 npx @dommaker/harness init 自动创建配置文件'));
|
|
260
260
|
}
|
|
261
261
|
|
|
262
262
|
/**
|
|
@@ -264,7 +264,7 @@ function printSnippets(): void {
|
|
|
264
264
|
*/
|
|
265
265
|
const PRE_COMMIT_SNIPPET = `
|
|
266
266
|
# Harness 约束检查
|
|
267
|
-
npx harness check --staged
|
|
267
|
+
npx @dommaker/harness check --staged
|
|
268
268
|
if [ $? -ne 0 ]; then
|
|
269
269
|
echo "❌ Iron law check failed"
|
|
270
270
|
exit 1
|
|
@@ -273,11 +273,11 @@ fi
|
|
|
273
273
|
# Plan coverage check (via PostEval)
|
|
274
274
|
STAGED=$(git diff --cached --name-only --diff-filter=ACMR 2>/dev/null || true)
|
|
275
275
|
if command -v npx > /dev/null 2>&1; then
|
|
276
|
-
PLAN_FILES=$(echo "$STAGED" | grep -E 'plans
|
|
276
|
+
PLAN_FILES=$(echo "$STAGED" | grep -E 'plans/.*\\.md$|\\.plan\\.md$' || true)
|
|
277
277
|
if [ -n "$PLAN_FILES" ]; then
|
|
278
278
|
echo "📋 Checking plan coverage..."
|
|
279
279
|
for plan in $PLAN_FILES; do
|
|
280
|
-
npx harness posteval-plan "$plan" || {
|
|
280
|
+
npx @dommaker/harness posteval-plan "$plan" || {
|
|
281
281
|
echo "🛑 Plan coverage incomplete. See above for missed items."
|
|
282
282
|
exit 1
|
|
283
283
|
}
|
|
@@ -298,7 +298,7 @@ const GITHUB_ACTIONS_SNIPPET = `
|
|
|
298
298
|
with:
|
|
299
299
|
node-version: '20'
|
|
300
300
|
- run: npm ci
|
|
301
|
-
- run: npx harness check
|
|
301
|
+
- run: npx @dommaker/harness check
|
|
302
302
|
`;
|
|
303
303
|
|
|
304
304
|
/**
|
|
@@ -313,7 +313,7 @@ async function setupGitHooks(projectPath: string): Promise<void> {
|
|
|
313
313
|
await fs.access(gitDir);
|
|
314
314
|
} catch {
|
|
315
315
|
console.log(chalk.yellow('⚠️ 未检测到 Git 仓库,跳过 Git hooks'));
|
|
316
|
-
console.log(chalk.gray('💡 初始化 Git 后可运行 harness init --print-snippets 查看配置'));
|
|
316
|
+
console.log(chalk.gray('💡 初始化 Git 后可运行 npx @dommaker/harness init --print-snippets 查看配置'));
|
|
317
317
|
return;
|
|
318
318
|
}
|
|
319
319
|
|
|
@@ -337,7 +337,7 @@ echo "🔍 Running harness checks..."
|
|
|
337
337
|
STAGED=$(git diff --cached --name-only --diff-filter=ACMR 2>/dev/null || true)
|
|
338
338
|
|
|
339
339
|
# 铁律检查
|
|
340
|
-
npx harness check --staged
|
|
340
|
+
npx @dommaker/harness check --staged
|
|
341
341
|
if [ $? -ne 0 ]; then
|
|
342
342
|
echo "❌ Iron law check failed"
|
|
343
343
|
exit 1
|
|
@@ -349,7 +349,7 @@ if command -v npx > /dev/null 2>&1; then
|
|
|
349
349
|
if [ -n "$PLAN_FILES" ]; then
|
|
350
350
|
echo "📋 Checking plan coverage..."
|
|
351
351
|
for plan in $PLAN_FILES; do
|
|
352
|
-
npx harness posteval-plan "$plan" || {
|
|
352
|
+
npx @dommaker/harness posteval-plan "$plan" || {
|
|
353
353
|
echo "🛑 Plan coverage incomplete. See above for missed items."
|
|
354
354
|
exit 1
|
|
355
355
|
}
|
|
@@ -413,13 +413,13 @@ jobs:
|
|
|
413
413
|
run: npm ci
|
|
414
414
|
|
|
415
415
|
- name: Run harness check
|
|
416
|
-
run: npx harness check
|
|
416
|
+
run: npx @dommaker/harness check
|
|
417
417
|
|
|
418
418
|
- name: Run harness validate
|
|
419
|
-
run: npx harness validate
|
|
419
|
+
run: npx @dommaker/harness validate
|
|
420
420
|
|
|
421
421
|
- name: Run harness passes-gate
|
|
422
|
-
run: npx harness passes-gate
|
|
422
|
+
run: npx @dommaker/harness passes-gate
|
|
423
423
|
`;
|
|
424
424
|
|
|
425
425
|
await fs.writeFile(workflowPath, workflowContent, 'utf-8');
|
|
@@ -820,7 +820,7 @@ async function setupGovernanceWorkflow(projectPath: string, level: string): Prom
|
|
|
820
820
|
const docsCheckStep = level !== 'minimal'
|
|
821
821
|
? `
|
|
822
822
|
- name: Check docs freshness
|
|
823
|
-
run: npx harness sync-docs --check
|
|
823
|
+
run: npx @dommaker/harness sync-docs --check
|
|
824
824
|
continue-on-error: true`
|
|
825
825
|
: '';
|
|
826
826
|
|
|
@@ -848,10 +848,10 @@ jobs:
|
|
|
848
848
|
run: npm ci
|
|
849
849
|
|
|
850
850
|
- name: Constraint check
|
|
851
|
-
run: npx harness check
|
|
851
|
+
run: npx @dommaker/harness check
|
|
852
852
|
|
|
853
853
|
- name: Quality gate
|
|
854
|
-
run: npx harness passes-gate
|
|
854
|
+
run: npx @dommaker/harness passes-gate
|
|
855
855
|
${docsCheckStep}
|
|
856
856
|
`;
|
|
857
857
|
|
|
@@ -93,7 +93,7 @@ export async function syncDocs(options: SyncDocsOptions): Promise<boolean> {
|
|
|
93
93
|
|
|
94
94
|
// 1. 扫描源码模块(从 governance config 读取目录列表,默认 src/)
|
|
95
95
|
const srcDirs = await getSourceDirs(projectPath);
|
|
96
|
-
|
|
96
|
+
const currentModules: ModuleInfo[] = [];
|
|
97
97
|
for (const srcDir of srcDirs) {
|
|
98
98
|
try {
|
|
99
99
|
const modules = await scanSourceModules(path.join(projectPath, srcDir), projectPath);
|
|
@@ -166,17 +166,30 @@ export async function syncDocs(options: SyncDocsOptions): Promise<boolean> {
|
|
|
166
166
|
)
|
|
167
167
|
.map(m => getBasename(m.file));
|
|
168
168
|
}
|
|
169
|
-
|
|
169
|
+
// removed:basename 对比 + 全路径存在性兜底(#33)。
|
|
170
|
+
// 表格格式承诺 ts|tsx|js|jsx,但扫描只覆盖 .ts/.tsx——
|
|
171
|
+
// 指向真实存在文件的登记行(.js/.jsx 等扫描盲区)不得误判为「已删除」。
|
|
172
|
+
// 兜底按 basename 豁免;路径不存在的幽灵行进不了豁免,由下方清扫按完整路径补入 removed。
|
|
173
|
+
const sourceRoots = detectSourceRoots(projectPath);
|
|
174
|
+
const entryExists = (entry: string): boolean =>
|
|
175
|
+
existsSync(path.join(projectPath, entry)) ||
|
|
176
|
+
sourceRoots.some(root => existsSync(path.join(projectPath, root, entry)));
|
|
177
|
+
|
|
178
|
+
const registeredEntries = readCapabilitiesEntries(capabilitiesPath);
|
|
179
|
+
const livePathBasenames = new Set(
|
|
180
|
+
registeredEntries
|
|
181
|
+
.filter(e => !e.endsWith('/') && e.includes('/') && entryExists(e))
|
|
182
|
+
.map(getBasename)
|
|
183
|
+
);
|
|
184
|
+
result.removed = existingFileNames.filter(
|
|
185
|
+
f => !currentBasenames.includes(f) && !livePathBasenames.has(f)
|
|
186
|
+
);
|
|
170
187
|
|
|
171
188
|
// 幽灵条目清扫(2026-08-08 studio CI 4 连红事故):上方按 basename 对比,
|
|
172
189
|
// 同名碰撞时幽灵不可见(如 agent-configs/routes.ts 已删但 agents/routes.ts 仍存在,
|
|
173
190
|
// basename routes.ts 仍在扫描结果中,永远不会被判 removed)。
|
|
174
191
|
// 这里按完整路径直接判存在性(与 docs_freshness 检查器同语义:项目根 + 源码根前缀)。
|
|
175
|
-
const
|
|
176
|
-
const entryExists = (entry: string): boolean =>
|
|
177
|
-
existsSync(path.join(projectPath, entry)) ||
|
|
178
|
-
sourceRoots.some(root => existsSync(path.join(projectPath, root, entry)));
|
|
179
|
-
for (const entry of readCapabilitiesEntries(capabilitiesPath)) {
|
|
192
|
+
for (const entry of registeredEntries) {
|
|
180
193
|
// 纯文件名条目(无路径)无法用存在性判定,交由上方 basename 对比
|
|
181
194
|
if (!entry.includes('/') || entryExists(entry)) continue;
|
|
182
195
|
const basename = entry.split('/').pop()!;
|
|
@@ -7,7 +7,7 @@ import { existsSync } from 'fs';
|
|
|
7
7
|
import * as path from 'path';
|
|
8
8
|
import { loadRawProjectConfig } from '../../../core/project-config-loader';
|
|
9
9
|
import { detectSourceRoots } from '../../../utils/detect-source-roots';
|
|
10
|
-
import { findTsSourceFiles } from '../../../utils/file-walk';
|
|
10
|
+
import { findTsSourceFiles, isTsSourceFile } from '../../../utils/file-walk';
|
|
11
11
|
|
|
12
12
|
export interface ModuleInfo {
|
|
13
13
|
name: string;
|
|
@@ -115,18 +115,20 @@ export async function scanSourceModules(srcDir: string, projectPath: string): Pr
|
|
|
115
115
|
const stat = await fs.stat(entryPath);
|
|
116
116
|
|
|
117
117
|
if (stat.isDirectory()) {
|
|
118
|
-
// 子目录:递归扫描 .ts 文件(不报告目录条目本身)
|
|
119
|
-
|
|
118
|
+
// 子目录:递归扫描 .ts/.tsx 文件(不报告目录条目本身)
|
|
119
|
+
// includeTsx 开启后,历史项目未登记过的 .tsx 会首次批量进入 added(#33 已知连带,
|
|
120
|
+
// 与 .ts 同口径属预期,一次性补登记即可)
|
|
121
|
+
const subFiles = findTsSourceFiles(entryPath, { skipIndex: true, includeTsx: true });
|
|
120
122
|
for (const f of subFiles) {
|
|
121
123
|
modules.push({
|
|
122
|
-
name: path.basename(f,
|
|
124
|
+
name: path.basename(f, path.extname(f)),
|
|
123
125
|
file: path.relative(projectPath, f),
|
|
124
126
|
description: await extractFileDescription(f),
|
|
125
127
|
});
|
|
126
128
|
}
|
|
127
|
-
} else if (entry
|
|
129
|
+
} else if (isTsSourceFile(entry, { skipIndex: true, includeTsx: true })) {
|
|
128
130
|
modules.push({
|
|
129
|
-
name: path.basename(entry,
|
|
131
|
+
name: path.basename(entry, path.extname(entry)),
|
|
130
132
|
file: path.relative(projectPath, entryPath),
|
|
131
133
|
description: await extractFileDescription(entryPath),
|
|
132
134
|
});
|
|
@@ -142,9 +144,9 @@ export async function scanSourceModules(srcDir: string, projectPath: string): Pr
|
|
|
142
144
|
async function extractFileDescription(filePath: string): Promise<string> {
|
|
143
145
|
try {
|
|
144
146
|
const content = await fs.readFile(filePath, 'utf-8');
|
|
145
|
-
return extractFirstComment(content) || path.basename(filePath,
|
|
147
|
+
return extractFirstComment(content) || path.basename(filePath, path.extname(filePath));
|
|
146
148
|
} catch {
|
|
147
|
-
return path.basename(filePath,
|
|
149
|
+
return path.basename(filePath, path.extname(filePath));
|
|
148
150
|
}
|
|
149
151
|
}
|
|
150
152
|
|
|
@@ -165,15 +165,15 @@ export async function createExampleCheckpoint(projectPath: string): Promise<void
|
|
|
165
165
|
const DEFAULT_RESOLUTIONS = {
|
|
166
166
|
no_fuzzy_completion_claim: {
|
|
167
167
|
title: 'commit message 缺少验证证据',
|
|
168
|
-
fix: '在 commit message body 中附上验证输出:\n`npx harness check --staged` | `npx harness validate` | `npm test -- --coverage`\n确认全部通过后重新 commit。',
|
|
168
|
+
fix: '在 commit message body 中附上验证输出:\n`npx @dommaker/harness check --staged` | `npx @dommaker/harness validate` | `npm test -- --coverage`\n确认全部通过后重新 commit。',
|
|
169
169
|
},
|
|
170
170
|
capability_sync: {
|
|
171
171
|
title: '缺少 CAPABILITIES.md',
|
|
172
|
-
fix: '在项目根目录创建 CAPABILITIES.md,列出所有模块能力清单。运行 `npx harness sync-docs` 可自动生成模板。',
|
|
172
|
+
fix: '在项目根目录创建 CAPABILITIES.md,列出所有模块能力清单。运行 `npx @dommaker/harness sync-docs` 可自动生成模板。',
|
|
173
173
|
},
|
|
174
174
|
context_doc_sync: {
|
|
175
175
|
title: '关键目录缺少 CONTEXT.md',
|
|
176
|
-
fix: '在 required_dirs 目录下创建 CONTEXT.md。运行 `npx harness sync-docs` 可自动生成模板。',
|
|
176
|
+
fix: '在 required_dirs 目录下创建 CONTEXT.md。运行 `npx @dommaker/harness sync-docs` 可自动生成模板。',
|
|
177
177
|
},
|
|
178
178
|
};
|
|
179
179
|
|
|
@@ -99,7 +99,7 @@ export function stripCodeBlocks(text: string): string {
|
|
|
99
99
|
|
|
100
100
|
/** 是否纯标点/空白 */
|
|
101
101
|
export function isPunctuation(str: string): boolean {
|
|
102
|
-
return /^[,。!?、;:""''
|
|
102
|
+
return /^[,。!?、;:""''()[\]【】「」『』《》〈〉\s]+$/.test(str);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
/** 是否含语义内容(汉字,或 ≥7 字符的合法英文标识词) */
|
|
@@ -18,7 +18,7 @@ describe('ProgressiveLoader', () => {
|
|
|
18
18
|
|
|
19
19
|
await loader.loadInChunks(items, {
|
|
20
20
|
chunkSize: 20,
|
|
21
|
-
onChunk: (chunk
|
|
21
|
+
onChunk: (chunk) => { chunks.push(chunk); },
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
expect(chunks.length).toBe(5);
|
|
@@ -45,7 +45,7 @@ describe('ProgressiveLoader', () => {
|
|
|
45
45
|
await loader.loadInChunks(items, {
|
|
46
46
|
chunkSize: 5,
|
|
47
47
|
parallel: true,
|
|
48
|
-
onChunk: (chunk
|
|
48
|
+
onChunk: (chunk) => { chunks.push(chunk); },
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
expect(chunks.length).toBe(4);
|
|
@@ -78,7 +78,7 @@ describe('ProgressiveLoader', () => {
|
|
|
78
78
|
|
|
79
79
|
await loader.loadInChunks(items, {
|
|
80
80
|
chunkSize: 10,
|
|
81
|
-
onChunk: (chunk
|
|
81
|
+
onChunk: (chunk) => { chunks.push(chunk); },
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
expect(chunks.length).toBe(3);
|
|
@@ -171,7 +171,7 @@ describe('ProgressiveLoader', () => {
|
|
|
171
171
|
it('should handle empty items', async () => {
|
|
172
172
|
const result = await loader.loadWithBudget([], {
|
|
173
173
|
budget: 100,
|
|
174
|
-
estimator: (
|
|
174
|
+
estimator: () => 0,
|
|
175
175
|
});
|
|
176
176
|
|
|
177
177
|
expect(result.items.length).toBe(0);
|
|
@@ -241,7 +241,7 @@ describe('ProgressiveLoader', () => {
|
|
|
241
241
|
return n * 2;
|
|
242
242
|
};
|
|
243
243
|
|
|
244
|
-
|
|
244
|
+
await loader.processBatch(items, processor, 1);
|
|
245
245
|
|
|
246
246
|
// Check that all items were processed
|
|
247
247
|
expect(processed.sort()).toEqual([1, 2, 3]);
|
|
@@ -256,7 +256,7 @@ describe('progressiveLoader (default instance)', () => {
|
|
|
256
256
|
|
|
257
257
|
await progressiveLoader.loadInChunks(items, {
|
|
258
258
|
chunkSize: 1,
|
|
259
|
-
onChunk: (chunk
|
|
259
|
+
onChunk: (chunk) => { chunks.push(chunk); return undefined; },
|
|
260
260
|
});
|
|
261
261
|
|
|
262
262
|
expect(chunks.length).toBe(3);
|
package/src/core/CONTEXT.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
## 核心导出
|
|
7
7
|
- `constraints/` — 约束定义(IRON_LAWS/GUIDELINES/PROMPTS;TIPS 已退役为空表) + 检查引擎(ConstraintChecker) + 拦截器(ConstraintInterceptor) + 缓存(CheckCache) + 注入渲染(injection-renderer)/漂移校验(injection-drift)/使用统计(usage-report)
|
|
8
|
-
- `effective-constraints.ts` — `getEffectiveConstraints(projectRoot)`:全仓唯一生效集来源(内置 → preset → config.yml
|
|
8
|
+
- `effective-constraints.ts` — `getEffectiveConstraints(projectRoot)`:全仓唯一生效集来源(内置 → preset → config.yml 禁用(内置与 custom 同效)→ custom 追加(禁用/已退役的不追加)→ scenes 过滤);`lintEffectiveConfig` 配置诊断
|
|
9
9
|
- `validators/` — checkpoints、passes-gate、CSO 验证器
|
|
10
10
|
- `session/` — 会话管理
|
|
11
11
|
- `spec/validator` — SpecValidator
|
|
@@ -45,7 +45,7 @@ describe('detectInjectionDrift', () => {
|
|
|
45
45
|
expect(drift.versionDrift).toBeUndefined();
|
|
46
46
|
expect(drift.contentDrift).toBeUndefined();
|
|
47
47
|
expect(drift.duplicateHeading).toBe(false);
|
|
48
|
-
expect(drift.fixHint).toContain('npx harness init');
|
|
48
|
+
expect(drift.fixHint).toContain('npx @dommaker/harness init');
|
|
49
49
|
expect(drift.fixHint).toBe(INJECTION_DRIFT_FIX_HINT);
|
|
50
50
|
});
|
|
51
51
|
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* ConstraintInterceptor 测试
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { ConstraintInterceptor
|
|
5
|
+
import { ConstraintInterceptor } from '../interceptor';
|
|
6
6
|
import { constraintChecker } from '../checker';
|
|
7
|
-
import type {
|
|
8
|
-
import type { EnforcementExecutor
|
|
7
|
+
import type { ConstraintContext } from '../../../types/constraint';
|
|
8
|
+
import type { EnforcementExecutor } from '../../../types/enforcement';
|
|
9
9
|
|
|
10
10
|
// Mock constraintChecker
|
|
11
11
|
jest.mock('../checker', () => ({
|
|
@@ -22,8 +22,8 @@ export const IRON_LAWS: Record<string, Constraint> = {
|
|
|
22
22
|
level: 'iron_law',
|
|
23
23
|
trigger: 'code_implementation',
|
|
24
24
|
enforcement: 'verify-completion',
|
|
25
|
-
description: '
|
|
26
|
-
promptInjection: '
|
|
25
|
+
description: '在声明任何任务完成之前,必须运行新鲜的验证命令(项目声明的测试 + type check),以新鲜输出作为完成证据。验证命令集由项目自行声明,全量验证留给 CI 流程。',
|
|
26
|
+
promptInjection: '在声明任务完成前,必须重新运行新鲜的验证命令(项目声明的测试 + type check),使用新鲜的输出作为完成证据,不得复用旧结果,不得凭记忆声称完成。',
|
|
27
27
|
},
|
|
28
28
|
|
|
29
29
|
/**
|
|
@@ -42,7 +42,7 @@ export interface InjectionDrift {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/** 修复指引(重跑 init 幂等同步注入段) */
|
|
45
|
-
export const INJECTION_DRIFT_FIX_HINT = '重跑 `npx harness init` 同步注入段(漂移仅警告,不阻断)';
|
|
45
|
+
export const INJECTION_DRIFT_FIX_HINT = '重跑 `npx @dommaker/harness init` 同步注入段(漂移仅警告,不阻断)';
|
|
46
46
|
|
|
47
47
|
const VERSION_LINE_RE = /<!-- version: ([^ ]+) -->/;
|
|
48
48
|
const ENTRY_LINE_RE = /^- \*\*.+?\*\*: .+$/;
|
|
@@ -15,7 +15,8 @@ import { ProjectConfigLoader } from './project-config-loader';
|
|
|
15
15
|
* 获取项目当前生效的约束集(check + prompt,带 kind)
|
|
16
16
|
*
|
|
17
17
|
* 内部完成完整合并:内置 → preset 裁剪 → config.yml `constraints.<id>.enabled:false`
|
|
18
|
-
*
|
|
18
|
+
* 删除(内置与 custom 同效)→ custom-constraints 追加/extend_exceptions
|
|
19
|
+
* (禁用/已退役的 custom 不追加)→ scenes 过滤
|
|
19
20
|
* (带 appliesTo 的 prompt 仅当 config.yml `scenes` 与其交集非空时保留,
|
|
20
21
|
* 缺省 scenes=[] 即场景专属 prompt 默认不进入生效集)。
|
|
21
22
|
*
|
|
@@ -156,7 +156,8 @@ export class ProjectConfigLoader {
|
|
|
156
156
|
* 合并内置约束和自定义约束(ADR-0001 生效集完整链路)
|
|
157
157
|
*
|
|
158
158
|
* 合并顺序:内置 → preset 裁剪 → config.yml `constraints.<id>.enabled:false`
|
|
159
|
-
*
|
|
159
|
+
* 删除(对内置与 custom 同效)→ custom-constraints 追加/extend_exceptions
|
|
160
|
+
* (禁用/已退役的 custom 不追加)→ scenes 过滤
|
|
160
161
|
* (带 appliesTo 的 prompt 仅当 scenes 交集非空时保留)。
|
|
161
162
|
*
|
|
162
163
|
* config.yml 中未知约束 id(如已移除约束的禁用残留)静默忽略,
|
|
@@ -201,8 +202,13 @@ export class ProjectConfigLoader {
|
|
|
201
202
|
}
|
|
202
203
|
}
|
|
203
204
|
|
|
204
|
-
// 2.
|
|
205
|
+
// 2. 添加自定义约束(config.yml 禁用的 id 不追加:step 1 已将其
|
|
206
|
+
// 收集进 disabled,custom 在 step 1 时尚未入桶,须在此兜底跳过;
|
|
207
|
+
// 条目带 retired 元数据的同样不追加——#82 D6 退役落点在条目自身)
|
|
205
208
|
for (const [id, customDef] of Object.entries(this.customConstraints)) {
|
|
209
|
+
if (result.disabled.includes(id) || customDef.retired) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
206
212
|
const extendExceptions = customDef.extend_exceptions;
|
|
207
213
|
const isExtendOnly =
|
|
208
214
|
!!extendExceptions && extendExceptions.length > 0 &&
|