@dommaker/harness 0.17.0 → 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 +27 -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 +9 -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 +23 -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__/project-config-loader-extra.test.ts +37 -0
- 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 +9 -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 +21 -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
|
@@ -406,6 +406,96 @@ describe('sync-docs command', () => {
|
|
|
406
406
|
fs.rmSync(testDir, { recursive: true, force: true });
|
|
407
407
|
});
|
|
408
408
|
|
|
409
|
+
it('.tsx 登记行指向真实文件时不得误判为已删除(#33),--check 收敛', async () => {
|
|
410
|
+
const testDir = path.join(tempDir, 'tsx-row');
|
|
411
|
+
const srcDir = path.join(testDir, 'src', 'components');
|
|
412
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
413
|
+
// 复刻 studio 场景:同目录混合 .ts 与 .tsx
|
|
414
|
+
fs.writeFileSync(path.join(srcDir, 'statusClasses.ts'), 'export const statuses = {};');
|
|
415
|
+
fs.writeFileSync(
|
|
416
|
+
path.join(srcDir, 'DistillProposalCard.tsx'),
|
|
417
|
+
'export const DistillProposalCard = () => null;'
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
// 手工登记一行 .tsx 模块(表格格式承诺支持 ts|tsx|js|jsx)
|
|
421
|
+
fs.writeFileSync(
|
|
422
|
+
path.join(testDir, 'CAPABILITIES.md'),
|
|
423
|
+
[
|
|
424
|
+
'# CAPABILITIES.md', '',
|
|
425
|
+
'> 最后更新: 2026-01-01', '',
|
|
426
|
+
'| 模块 | 文件 | 说明 |', '|------|------|------|',
|
|
427
|
+
'| statusClasses | src/components/statusClasses.ts | 状态类 |',
|
|
428
|
+
'| DistillProposalCard | src/components/DistillProposalCard.tsx | 人审卡 |',
|
|
429
|
+
'',
|
|
430
|
+
].join('\n')
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
await syncDocs({ projectPath: testDir });
|
|
434
|
+
const content = fs.readFileSync(path.join(testDir, 'CAPABILITIES.md'), 'utf-8');
|
|
435
|
+
expect(content).toContain('DistillProposalCard.tsx');
|
|
436
|
+
|
|
437
|
+
// 修复前:--check 每跑必报「包含已删除的模块」不收敛
|
|
438
|
+
consoleSpy.mockClear();
|
|
439
|
+
const check = await syncDocs({ projectPath: testDir, check: true });
|
|
440
|
+
expect(check).toBe(true);
|
|
441
|
+
|
|
442
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('未登记的 .tsx 源文件应进入 added,写入模式生成表格行', async () => {
|
|
446
|
+
const testDir = path.join(tempDir, 'tsx-added');
|
|
447
|
+
const srcDir = path.join(testDir, 'src');
|
|
448
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
449
|
+
fs.writeFileSync(path.join(srcDir, 'util.ts'), 'export const util = 1;');
|
|
450
|
+
fs.writeFileSync(path.join(srcDir, 'Card.tsx'), 'export const Card = () => null;');
|
|
451
|
+
|
|
452
|
+
fs.writeFileSync(
|
|
453
|
+
path.join(testDir, 'CAPABILITIES.md'),
|
|
454
|
+
'# Capabilities\n\n| 模块 | 文件 | 说明 |\n|------|------|------|\n| util | src/util.ts | util |'
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
const check = await syncDocs({ projectPath: testDir, check: true });
|
|
458
|
+
expect(check).toBe(false);
|
|
459
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Card.tsx'));
|
|
460
|
+
|
|
461
|
+
await syncDocs({ projectPath: testDir });
|
|
462
|
+
const content = fs.readFileSync(path.join(testDir, 'CAPABILITIES.md'), 'utf-8');
|
|
463
|
+
expect(content).toContain('Card.tsx');
|
|
464
|
+
|
|
465
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('真实存在的非扫描扩展名登记行(.js/.jsx)也不得误删(存在性过滤兜底)', async () => {
|
|
469
|
+
const testDir = path.join(tempDir, 'js-row');
|
|
470
|
+
const srcDir = path.join(testDir, 'src');
|
|
471
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
472
|
+
fs.writeFileSync(path.join(srcDir, 'helper.js'), 'module.exports = {};');
|
|
473
|
+
fs.writeFileSync(path.join(srcDir, 'Helper.jsx'), 'export default () => null;');
|
|
474
|
+
|
|
475
|
+
fs.writeFileSync(
|
|
476
|
+
path.join(testDir, 'CAPABILITIES.md'),
|
|
477
|
+
[
|
|
478
|
+
'# CAPABILITIES.md', '',
|
|
479
|
+
'> 最后更新: 2026-01-01', '',
|
|
480
|
+
'| 模块 | 文件 | 说明 |', '|------|------|------|',
|
|
481
|
+
'| helper | src/helper.js | 手工登记 |',
|
|
482
|
+
'| Helper | src/Helper.jsx | 手工登记 |',
|
|
483
|
+
'',
|
|
484
|
+
].join('\n')
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
await syncDocs({ projectPath: testDir });
|
|
488
|
+
const content = fs.readFileSync(path.join(testDir, 'CAPABILITIES.md'), 'utf-8');
|
|
489
|
+
expect(content).toContain('src/helper.js');
|
|
490
|
+
expect(content).toContain('src/Helper.jsx');
|
|
491
|
+
|
|
492
|
+
consoleSpy.mockClear();
|
|
493
|
+
const check = await syncDocs({ projectPath: testDir, check: true });
|
|
494
|
+
expect(check).toBe(true);
|
|
495
|
+
|
|
496
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
497
|
+
});
|
|
498
|
+
|
|
409
499
|
it('basename 碰撞的幽灵条目(文件已删但同名文件存在)也应被移除', async () => {
|
|
410
500
|
const testDir = path.join(tempDir, 'ghost-basename-collision');
|
|
411
501
|
const srcA = path.join(testDir, 'src', 'a');
|
|
@@ -118,9 +118,9 @@ export function renderExportMarkdown(
|
|
|
118
118
|
export function renderInjectionDriftLines(drift: InjectionDrift): string[] {
|
|
119
119
|
const lines: string[] = [];
|
|
120
120
|
if (drift.notInjected) {
|
|
121
|
-
lines.push(' CLAUDE.md 无约束注入标记段(未注入,不算漂移);可运行 npx harness init 注入');
|
|
121
|
+
lines.push(' CLAUDE.md 无约束注入标记段(未注入,不算漂移);可运行 npx @dommaker/harness init 注入');
|
|
122
122
|
if (drift.duplicateHeading) {
|
|
123
|
-
lines.push(' 另检测到多个 "## Governance Rules" 标题(历史遗留重复章节),建议手工清理后重跑 npx harness init');
|
|
123
|
+
lines.push(' 另检测到多个 "## Governance Rules" 标题(历史遗留重复章节),建议手工清理后重跑 npx @dommaker/harness init');
|
|
124
124
|
}
|
|
125
125
|
return lines;
|
|
126
126
|
}
|
|
@@ -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);
|