@dommaker/harness 1.8.0 → 1.8.1
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 +8 -1
- package/dist/cli/commands/check.d.ts +8 -0
- package/dist/cli/commands/check.d.ts.map +1 -1
- package/dist/cli/commands/check.js +10 -11
- package/dist/cli/commands/check.js.map +1 -1
- package/dist/cli/commands/status.d.ts +8 -0
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +5 -7
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/state-io.d.ts +34 -0
- package/dist/cli/state-io.d.ts.map +1 -0
- package/dist/cli/state-io.js +71 -0
- package/dist/cli/state-io.js.map +1 -0
- package/dist/hooks/pipeline.d.ts +19 -1
- package/dist/hooks/pipeline.d.ts.map +1 -1
- package/dist/hooks/pipeline.js +40 -0
- package/dist/hooks/pipeline.js.map +1 -1
- package/dist/hooks/types.d.ts +2 -0
- package/dist/hooks/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/CONTEXT.md +1 -1
- package/src/cli/__tests__/state-io.test.ts +50 -0
- package/src/cli/commands/CONTEXT.md +1 -0
- package/src/cli/commands/__tests__/check-read-count.test.ts +3 -0
- package/src/cli/commands/__tests__/check.test.ts +20 -8
- package/src/cli/commands/__tests__/status-extra.test.ts +25 -8
- package/src/cli/commands/__tests__/status.test.ts +46 -18
- package/src/cli/commands/check.ts +20 -18
- package/src/cli/commands/status.ts +12 -7
- package/src/cli/state-io.ts +53 -0
- package/src/hooks/CONTEXT.md +1 -1
- package/src/hooks/__tests__/pipeline.test.ts +105 -0
- package/src/hooks/pipeline.ts +47 -0
- package/src/hooks/types.ts +2 -0
|
@@ -10,6 +10,7 @@ H5(#44)起:
|
|
|
10
10
|
## 核心导出
|
|
11
11
|
- `COMMAND_DEFINITIONS`(definitions.ts)— 全部非门禁命令定义(纯数据模块、零闭包,禁止 import 命令实现;ADR-0010)
|
|
12
12
|
- 命令契约(`src/cli/command-contract.ts`,上层目录)— `CommandResult` / `CommandKind` / `CommandIO` / `processIO` / `captureIO` / `lastJsonOutput` / `log` / `logError`
|
|
13
|
+
- 状态文件接缝(`src/cli/state-io.ts`,上层目录)— `.harness/.state.json` 读-改-写的唯一入口 `StateIO`(`read()`/`write()`)+ 状态类型 `HarnessState` + 缺省真实 fs 实现 `fileStateIO(projectPath)`;`check` / `status` 经可选 `stateIO` 参数注入(ADR-0026,harness#148)
|
|
13
14
|
- 门禁命令共享面(`src/cli/gate-command.ts`,上层目录)— `GateDecision → CommandResult` 的唯一映射 `gateCommandResult`,加 ✓/✗ 输出骨架 `reportGateDecision` 与出错横幅 `reportGateError`。同一句失败措辞此前抄在 6 个 handler 里(「`<id>` gate denied」×6 / 「`<id>` gate error」×4),门禁特有的指标行经 `onPass`/`onFail` 闭包传入(架构评审候选1)
|
|
14
15
|
- 脚手架落盘面(`src/cli/commands/scaffold.ts`)— 受管文件(managed file)三态判定 `writeManagedFile` / 一组落盘 `runPlan` + 9 个站点工厂(含对外文案);模板正文住 `scaffold-templates.ts`,落盘注入面 `ScaffoldFileSystem` 可换内存替身(harness#132);CI 站点工厂 `harnessCheckCiFile` 带平台维度(github / gitlab,harness#143);两道 Git hook 工厂 `preCommitHookFile` / `prePushHookFile`(后者 harness#144,带可执行位)
|
|
15
16
|
- knowledge 投影面(`src/cli/commands/knowledge-view.ts`)— 11 个 knowledge 子操作的**display model** 渲染与出口:`emitKnowledgeView`(json/人读唯一分派 + 退出码)、`announce`(取数期进度行,`--json` 下静默)、`TONE_STYLES` 角色→样式单表、维度/规则 label 与 `toneForMaturity`/`toneForScore`/`toneForSeverity` 三张映射、`resolveKnowledgeBaseDir` + `openKnowledgeStore`(路径兜底与 store 构造单点)(harness#133)
|
|
@@ -90,6 +90,8 @@ function fixtureRepo(): string {
|
|
|
90
90
|
write(dir, 'src/existing.ts', 'export const a = 1;\n');
|
|
91
91
|
write(dir, 'src/nested/deep.ts', 'export const d = 1;\n');
|
|
92
92
|
write(dir, '.harness/logs/traces.log', '{"constraintId":"fixture","result":"pass"}\n');
|
|
93
|
+
// ADR-0026:状态文件进夹具,智能提示的状态读取随之进计数表(一次运行至多读一次)
|
|
94
|
+
write(dir, '.harness/.state.json', '{}');
|
|
93
95
|
git(dir, 'add', '.');
|
|
94
96
|
git(dir, 'commit', '-q', '-m', 'baseline');
|
|
95
97
|
// 改一个已登记的源文件并 stage → 触发条件 module_modification
|
|
@@ -139,6 +141,7 @@ describe('一次 check 的文件读取计数闸(ADR-0023 决策 5 ①)', ()
|
|
|
139
141
|
['.harness/custom-constraints.yml', 1],
|
|
140
142
|
['CAPABILITIES.md', 1],
|
|
141
143
|
['.harness/logs/traces.log', 2],
|
|
144
|
+
['.harness/.state.json', 1], // ADR-0026:智能提示经 StateIO 读状态,恰一次
|
|
142
145
|
// —— 已知例外(改动需明写理由)——
|
|
143
146
|
// traces.log = 2:head 50(智能提示的阈值判定,决策 3)与 tail 20(有无失败证据,
|
|
144
147
|
// 决策 4)是两个不同窗口的**有界**读;并成一个窗口就等于回到整读。
|
|
@@ -18,6 +18,7 @@ import { execFileSync } from 'child_process';
|
|
|
18
18
|
|
|
19
19
|
import { check, listLaws } from '../check';
|
|
20
20
|
import { captureIO, type CapturingIO } from '../../command-contract';
|
|
21
|
+
import type { HarnessState, StateIO } from '../../state-io';
|
|
21
22
|
import { DEFAULT_TRACE_FILE } from '../../../types/trace';
|
|
22
23
|
import {
|
|
23
24
|
createGitEvidence,
|
|
@@ -92,6 +93,19 @@ function projectTraces(
|
|
|
92
93
|
.map(line => JSON.parse(line));
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
/**
|
|
97
|
+
* 内存 StateIO 假件(ADR-0026):状态相关测试经注入面驱动,不碰真文件系统。
|
|
98
|
+
* `snapshot()` 给断言用——读的是假件当前持有的状态,不是磁盘。
|
|
99
|
+
*/
|
|
100
|
+
function memoryStateIO(initial: HarnessState = {}): StateIO & { snapshot(): HarnessState } {
|
|
101
|
+
let state = initial;
|
|
102
|
+
return {
|
|
103
|
+
read: () => state,
|
|
104
|
+
write: (next: HarnessState) => { state = next; },
|
|
105
|
+
snapshot: () => state,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
95
109
|
/** 计数 git 证据:既记录证据方法请求,也记录实际 spawn 的 git 命令(执行走真 adapter) */
|
|
96
110
|
function recordingEvidence(projectPath: string): {
|
|
97
111
|
evidence: GitEvidence;
|
|
@@ -482,27 +496,25 @@ describe('check command(真 git fixture)', () => {
|
|
|
482
496
|
});
|
|
483
497
|
});
|
|
484
498
|
|
|
485
|
-
describe('智能提示(真 traces.log
|
|
499
|
+
describe('智能提示(真 traces.log + 注入 StateIO 假件)', () => {
|
|
486
500
|
it('记录数首次达到 50 时提示,并落盘已提示状态', async () => {
|
|
487
501
|
const dir = gitRepo();
|
|
488
502
|
passTraces(dir, 50);
|
|
503
|
+
const stateIO = memoryStateIO();
|
|
489
504
|
|
|
490
|
-
await check({ preset: 'standard', staged: true, projectPath: dir, trigger: 'manual' }, io);
|
|
505
|
+
await check({ preset: 'standard', staged: true, projectPath: dir, trigger: 'manual', stateIO }, io);
|
|
491
506
|
|
|
492
507
|
expect(io.outText()).toContain('记录已足够,运行 harness status 查看统计');
|
|
493
508
|
expect(io.outText()).toContain('────────────────');
|
|
494
|
-
|
|
495
|
-
fs.readFileSync(path.join(dir, '.harness', '.state.json'), 'utf-8')
|
|
496
|
-
);
|
|
497
|
-
expect(state.shownHints).toContain('trace_50');
|
|
509
|
+
expect(stateIO.snapshot().shownHints).toContain('trace_50');
|
|
498
510
|
});
|
|
499
511
|
|
|
500
512
|
it('已提示过的不再重复提示', async () => {
|
|
501
513
|
const dir = gitRepo();
|
|
502
514
|
passTraces(dir, 50);
|
|
503
|
-
|
|
515
|
+
const stateIO = memoryStateIO({ shownHints: ['trace_50'] });
|
|
504
516
|
|
|
505
|
-
await check({ preset: 'standard', staged: true, projectPath: dir, trigger: 'manual' }, io);
|
|
517
|
+
await check({ preset: 'standard', staged: true, projectPath: dir, trigger: 'manual', stateIO }, io);
|
|
506
518
|
|
|
507
519
|
expect(io.outText()).not.toContain('记录已足够');
|
|
508
520
|
expect(io.outText()).not.toContain('────────────────');
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* ADR-0020 起不再 mock TraceAnalyzer/TraceCollector:异常判定走真实纯函数,
|
|
5
5
|
* 用例喂能真的判出异常的 trace(passRate < 0.3 → low_pass_rate)。
|
|
6
|
+
* ADR-0026 起状态文件读写经注入的 StateIO 假件,不碰(被 mock 的)fs。
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
|
-
import { status } from '../status';
|
|
9
|
-
import { captureIO, type CapturingIO } from '../../command-contract';
|
|
9
|
+
import { status, type StatusOptions } from '../status';
|
|
10
|
+
import { captureIO, type CapturingIO, type CommandResult } from '../../command-contract';
|
|
11
|
+
import type { HarnessState, StateIO } from '../../state-io';
|
|
10
12
|
import * as fs from 'fs';
|
|
11
13
|
import type { ExecutionTrace } from '../../../types/trace';
|
|
12
14
|
|
|
@@ -46,6 +48,21 @@ beforeEach(() => {
|
|
|
46
48
|
io = captureIO();
|
|
47
49
|
});
|
|
48
50
|
|
|
51
|
+
/** 内存 StateIO 假件(ADR-0026):缺省 fileStateIO 在全 mock 的 fs 下会吃到 trace 假数据 */
|
|
52
|
+
function memoryStateIO(initial: HarnessState = {}): StateIO & { snapshot(): HarnessState } {
|
|
53
|
+
let state = initial;
|
|
54
|
+
return {
|
|
55
|
+
read: () => state,
|
|
56
|
+
write: (next: HarnessState) => { state = next; },
|
|
57
|
+
snapshot: () => state,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 全部用例经注入假件调用 status */
|
|
62
|
+
function runStatus(options: StatusOptions = {}): Promise<CommandResult> {
|
|
63
|
+
return status({ stateIO: memoryStateIO(), ...options }, io);
|
|
64
|
+
}
|
|
65
|
+
|
|
49
66
|
describe('status command - 补充覆盖', () => {
|
|
50
67
|
|
|
51
68
|
beforeEach(() => {
|
|
@@ -64,7 +81,7 @@ describe('status command - 补充覆盖', () => {
|
|
|
64
81
|
...repeats('no_completion_without_verification', 2, 'fail'),
|
|
65
82
|
]));
|
|
66
83
|
|
|
67
|
-
await
|
|
84
|
+
await runStatus({ anomalies: true });
|
|
68
85
|
|
|
69
86
|
const output = io.outText();
|
|
70
87
|
expect(output).toContain('发现 2 个异常');
|
|
@@ -78,7 +95,7 @@ describe('status command - 补充覆盖', () => {
|
|
|
78
95
|
{ constraintId: 'test', result: 'pass' },
|
|
79
96
|
]));
|
|
80
97
|
|
|
81
|
-
await
|
|
98
|
+
await runStatus({ anomalies: true });
|
|
82
99
|
|
|
83
100
|
expect(io.outText()).toContain('✅ 未发现异常');
|
|
84
101
|
});
|
|
@@ -91,7 +108,7 @@ describe('status command - 补充覆盖', () => {
|
|
|
91
108
|
{ constraintId: 'no_completion_without_verification' },
|
|
92
109
|
]));
|
|
93
110
|
|
|
94
|
-
await
|
|
111
|
+
await runStatus();
|
|
95
112
|
|
|
96
113
|
const output = io.outText();
|
|
97
114
|
expect(output).toContain('📈 约束统计:');
|
|
@@ -104,7 +121,7 @@ describe('status command - 补充覆盖', () => {
|
|
|
104
121
|
{ constraintId: 'capability_sync', level: 'guideline', result: 'pass' },
|
|
105
122
|
]));
|
|
106
123
|
|
|
107
|
-
await
|
|
124
|
+
await runStatus();
|
|
108
125
|
|
|
109
126
|
expect(io.outText()).toContain('🟡 Guidelines:');
|
|
110
127
|
});
|
|
@@ -118,7 +135,7 @@ describe('status command - 补充覆盖', () => {
|
|
|
118
135
|
{ constraintId: 'capability_sync', level: 'guideline' },
|
|
119
136
|
]));
|
|
120
137
|
|
|
121
|
-
await
|
|
138
|
+
await runStatus({ detail: true });
|
|
122
139
|
|
|
123
140
|
const output = io.outText();
|
|
124
141
|
expect(output).toContain('📈 约束统计:');
|
|
@@ -134,7 +151,7 @@ describe('status command - 补充覆盖', () => {
|
|
|
134
151
|
['invalid json', JSON.stringify({ constraintId: 'valid', level: 'iron_law', timestamp: 1, result: 'pass' }), 'also invalid'].join('\n')
|
|
135
152
|
);
|
|
136
153
|
|
|
137
|
-
await
|
|
154
|
+
await runStatus();
|
|
138
155
|
|
|
139
156
|
// 应该成功处理,不会抛出异常:坏行进 stderr 告知,合法行进统计
|
|
140
157
|
const output = io.outText();
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* ADR-0020 起不再 mock TraceAnalyzer/TraceCollector:统计与异常由 trace-analyzer
|
|
5
5
|
* 的模块级纯函数真算,用例喂合法 trace 行、断言真实输出。
|
|
6
|
+
* ADR-0026 起状态文件读写经注入的 StateIO 假件,不碰(被 mock 的)fs。
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
|
-
import { status } from '../status';
|
|
9
|
-
import { captureIO, type CapturingIO } from '../../command-contract';
|
|
9
|
+
import { status, type StatusOptions } from '../status';
|
|
10
|
+
import { captureIO, type CapturingIO, type CommandResult } from '../../command-contract';
|
|
11
|
+
import type { HarnessState, StateIO } from '../../state-io';
|
|
10
12
|
import * as fs from 'fs';
|
|
11
13
|
import type { ExecutionTrace } from '../../../types/trace';
|
|
12
14
|
|
|
@@ -42,10 +44,27 @@ function repeats(constraintId: string, n: number, result: ExecutionTrace['result
|
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
let io: CapturingIO;
|
|
47
|
+
let stateIO: ReturnType<typeof memoryStateIO>;
|
|
45
48
|
beforeEach(() => {
|
|
46
49
|
io = captureIO();
|
|
50
|
+
stateIO = memoryStateIO();
|
|
47
51
|
});
|
|
48
52
|
|
|
53
|
+
/** 内存 StateIO 假件(ADR-0026):状态读写走注入面,缺省 fileStateIO 在全 mock 的 fs 下会吃到 trace 假数据 */
|
|
54
|
+
function memoryStateIO(initial: HarnessState = {}): StateIO & { snapshot(): HarnessState } {
|
|
55
|
+
let state = initial;
|
|
56
|
+
return {
|
|
57
|
+
read: () => state,
|
|
58
|
+
write: (next: HarnessState) => { state = next; },
|
|
59
|
+
snapshot: () => state,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 全部用例经注入假件调用 status(stateIO 可被单例覆盖以预置状态) */
|
|
64
|
+
function runStatus(options: StatusOptions = {}): Promise<CommandResult> {
|
|
65
|
+
return status({ stateIO, ...options }, io);
|
|
66
|
+
}
|
|
67
|
+
|
|
49
68
|
describe('status command', () => {
|
|
50
69
|
|
|
51
70
|
beforeEach(() => {
|
|
@@ -59,7 +78,7 @@ describe('status command', () => {
|
|
|
59
78
|
describe('未初始化情况', () => {
|
|
60
79
|
it('应该显示未初始化提示', async () => {
|
|
61
80
|
mockFs.existsSync.mockReturnValue(false);
|
|
62
|
-
await
|
|
81
|
+
await runStatus();
|
|
63
82
|
expect(io.outText()).toContain('未初始化');
|
|
64
83
|
});
|
|
65
84
|
});
|
|
@@ -71,7 +90,7 @@ describe('status command', () => {
|
|
|
71
90
|
.mockReturnValueOnce(true) // .harness dir
|
|
72
91
|
.mockReturnValueOnce(false); // traces file
|
|
73
92
|
|
|
74
|
-
await
|
|
93
|
+
await runStatus();
|
|
75
94
|
expect(io.outText()).toContain('暂无 Trace');
|
|
76
95
|
});
|
|
77
96
|
});
|
|
@@ -83,7 +102,7 @@ describe('status command', () => {
|
|
|
83
102
|
{ constraintId: 'test2', level: 'guideline' },
|
|
84
103
|
]));
|
|
85
104
|
|
|
86
|
-
await
|
|
105
|
+
await runStatus();
|
|
87
106
|
expect(io.outText()).toContain('记录数: 2 条');
|
|
88
107
|
expect(io.outText()).toContain('📈 约束统计:');
|
|
89
108
|
});
|
|
@@ -93,7 +112,7 @@ describe('status command', () => {
|
|
|
93
112
|
{ constraintId: 'no_bypass_checkpoint' },
|
|
94
113
|
]));
|
|
95
114
|
|
|
96
|
-
await
|
|
115
|
+
await runStatus({ detail: true });
|
|
97
116
|
expect(io.outText()).toContain('🔴 Iron Laws:');
|
|
98
117
|
expect(io.outText()).toContain('✅ no_bypass_checkpoint');
|
|
99
118
|
expect(io.outText()).toContain('检查: 1 | 通过: 100% | 失败: 0%');
|
|
@@ -106,7 +125,7 @@ describe('status command', () => {
|
|
|
106
125
|
{ constraintId: 'test1', result: 'fail' },
|
|
107
126
|
]));
|
|
108
127
|
|
|
109
|
-
await
|
|
128
|
+
await runStatus({ anomalies: true });
|
|
110
129
|
expect(io.outText()).toContain('发现 1 个异常');
|
|
111
130
|
expect(io.outText()).toContain(' test1');
|
|
112
131
|
expect(io.outText()).toContain('类型: low_pass_rate');
|
|
@@ -120,17 +139,26 @@ describe('status command', () => {
|
|
|
120
139
|
{ constraintId: 'test1', result: 'fail' },
|
|
121
140
|
]));
|
|
122
141
|
|
|
123
|
-
await
|
|
142
|
+
await runStatus({ detail: true });
|
|
124
143
|
expect(io.outText()).toContain('检查: 5 | 通过: 80% | 失败: 20%');
|
|
125
144
|
});
|
|
126
145
|
});
|
|
127
146
|
|
|
128
147
|
describe('状态文件更新', () => {
|
|
129
|
-
it('
|
|
148
|
+
it('应该经 StateIO 写入 lastStatusRun', async () => {
|
|
149
|
+
mockFs.readFileSync.mockReturnValue(traceFile([{ constraintId: 'test' }]));
|
|
150
|
+
|
|
151
|
+
await runStatus();
|
|
152
|
+
expect(stateIO.snapshot().lastStatusRun).toBeTruthy();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('读-改-写:已有的 shownHints 不被 status 抹掉(ADR-0026 决策 2 的行为修复)', async () => {
|
|
130
156
|
mockFs.readFileSync.mockReturnValue(traceFile([{ constraintId: 'test' }]));
|
|
157
|
+
stateIO = memoryStateIO({ shownHints: ['trace_50'] });
|
|
131
158
|
|
|
132
|
-
await
|
|
133
|
-
expect(
|
|
159
|
+
await runStatus();
|
|
160
|
+
expect(stateIO.snapshot().shownHints).toEqual(['trace_50']);
|
|
161
|
+
expect(stateIO.snapshot().lastStatusRun).toBeTruthy();
|
|
134
162
|
});
|
|
135
163
|
});
|
|
136
164
|
|
|
@@ -140,7 +168,7 @@ describe('status command', () => {
|
|
|
140
168
|
{ constraintId: 'test_guide', level: 'guideline' },
|
|
141
169
|
]));
|
|
142
170
|
|
|
143
|
-
await
|
|
171
|
+
await runStatus();
|
|
144
172
|
expect(io.outText()).toContain('🟡 Guidelines:');
|
|
145
173
|
expect(io.outText()).toContain('✅ test_guide');
|
|
146
174
|
});
|
|
@@ -150,7 +178,7 @@ describe('status command', () => {
|
|
|
150
178
|
it('应该显示未发现异常当无异常', async () => {
|
|
151
179
|
mockFs.readFileSync.mockReturnValue(traceFile([{ constraintId: 'test' }]));
|
|
152
180
|
|
|
153
|
-
await
|
|
181
|
+
await runStatus({ anomalies: true });
|
|
154
182
|
expect(io.outText()).toContain('✅ 未发现异常');
|
|
155
183
|
});
|
|
156
184
|
|
|
@@ -159,7 +187,7 @@ describe('status command', () => {
|
|
|
159
187
|
{ constraintId: 'test', result: 'fail' },
|
|
160
188
|
]));
|
|
161
189
|
|
|
162
|
-
await
|
|
190
|
+
await runStatus({ anomalies: true });
|
|
163
191
|
expect(io.outText()).toContain('当前值: 0');
|
|
164
192
|
expect(io.outText()).toContain('阈值: 0.3');
|
|
165
193
|
expect(io.outText()).toContain('下一步建议');
|
|
@@ -170,7 +198,7 @@ describe('status command', () => {
|
|
|
170
198
|
it('应该显示良好建议当 trace >= 100', async () => {
|
|
171
199
|
mockFs.readFileSync.mockReturnValue(traceFile(repeats('test', 100, 'pass')));
|
|
172
200
|
|
|
173
|
-
await
|
|
201
|
+
await runStatus();
|
|
174
202
|
expect(io.outText()).toContain('记录数: 100 条');
|
|
175
203
|
expect(io.outText()).toContain('状态良好');
|
|
176
204
|
});
|
|
@@ -178,7 +206,7 @@ describe('status command', () => {
|
|
|
178
206
|
it('应该显示积累数据建议当 trace < 100', async () => {
|
|
179
207
|
mockFs.readFileSync.mockReturnValue(traceFile([{ constraintId: 'test' }]));
|
|
180
208
|
|
|
181
|
-
await
|
|
209
|
+
await runStatus();
|
|
182
210
|
expect(io.outText()).toContain('继续积累数据');
|
|
183
211
|
});
|
|
184
212
|
|
|
@@ -187,7 +215,7 @@ describe('status command', () => {
|
|
|
187
215
|
{ constraintId: 'test', result: 'fail' },
|
|
188
216
|
]));
|
|
189
217
|
|
|
190
|
-
await
|
|
218
|
+
await runStatus({ anomalies: true });
|
|
191
219
|
expect(io.outText()).toContain('harness status --detail');
|
|
192
220
|
});
|
|
193
221
|
});
|
|
@@ -199,7 +227,7 @@ describe('status command', () => {
|
|
|
199
227
|
...repeats('test_guide', 2, 'fail'),
|
|
200
228
|
].map(t => ({ ...t, level: 'guideline' as const }))));
|
|
201
229
|
|
|
202
|
-
await
|
|
230
|
+
await runStatus({ detail: true });
|
|
203
231
|
expect(io.outText()).toContain('⚠️ test_guide');
|
|
204
232
|
expect(io.outText()).toContain('检查: 5 | 通过: 60% | 失败: 40%');
|
|
205
233
|
});
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import chalk from 'chalk';
|
|
12
|
-
import * as fs from 'fs';
|
|
13
12
|
import * as path from 'path';
|
|
14
13
|
import { ConstraintChecker } from '../../core/constraints/checker';
|
|
15
14
|
import { IRON_LAWS, GUIDELINES, PROMPTS } from '../../core/constraints/definitions';
|
|
@@ -24,6 +23,7 @@ import { readJsonl } from '../../utils/jsonl';
|
|
|
24
23
|
import { DEFAULT_TRACE_FILE, type ExecutionTrace } from '../../types/trace';
|
|
25
24
|
import type { ConstraintResult, ConstraintTrigger } from '../../types/constraint';
|
|
26
25
|
import { log, processIO, type CommandIO, type CommandResult } from '../command-contract';
|
|
26
|
+
import { fileStateIO, type StateIO } from '../state-io';
|
|
27
27
|
|
|
28
28
|
/** 证据行着色(与调用处所属结论块一致) */
|
|
29
29
|
const EVIDENCE_PAINT = {
|
|
@@ -74,6 +74,13 @@ export interface CheckOptions {
|
|
|
74
74
|
* 测试据此断言「同一次运行内同一项目文件至多读一次」。
|
|
75
75
|
*/
|
|
76
76
|
runEnv?: RunEnv;
|
|
77
|
+
/**
|
|
78
|
+
* 状态文件接缝(非 CLI flag;ADR-0026)
|
|
79
|
+
*
|
|
80
|
+
* 缺省 = 真实 fs 实现(fileStateIO)。注入则 `.harness/.state.json` 的读写
|
|
81
|
+
* 走替身,测试据此不碰真文件系统。
|
|
82
|
+
*/
|
|
83
|
+
stateIO?: StateIO;
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
/**
|
|
@@ -91,6 +98,7 @@ export async function check(
|
|
|
91
98
|
// 一次 run 一份证据与观察面(#87 / ADR-0023):入口构造,沿生效集、context、checker 向下传
|
|
92
99
|
const evidence = options.evidence ?? createGitEvidence(projectPath);
|
|
93
100
|
const runEnv = options.runEnv ?? createRunEnv(projectPath);
|
|
101
|
+
const stateIO = options.stateIO ?? fileStateIO(projectPath);
|
|
94
102
|
|
|
95
103
|
// 生效约束集(ADR-0001):内置 → preset → config.yml 禁用 → custom 追加 → scenes 过滤。
|
|
96
104
|
// --preset 仅在没有项目自定义配置时覆盖 config.yml 的 preset(工单 23 语义:
|
|
@@ -221,7 +229,7 @@ export async function check(
|
|
|
221
229
|
log(io, chalk.green('✅ 约束检查通过'));
|
|
222
230
|
|
|
223
231
|
// 智能提示
|
|
224
|
-
const hint = await getSmartHint(projectPath);
|
|
232
|
+
const hint = await getSmartHint(projectPath, stateIO);
|
|
225
233
|
if (hint) {
|
|
226
234
|
log(io);
|
|
227
235
|
log(io, chalk.gray('────────────────────────────────────'));
|
|
@@ -241,10 +249,12 @@ const TRACE_HINT_THRESHOLD = 50;
|
|
|
241
249
|
|
|
242
250
|
/**
|
|
243
251
|
* 智能提示:检查是否需要提示用户下一步操作
|
|
252
|
+
*
|
|
253
|
+
* 状态读写经 StateIO(ADR-0026):读-改-写,`status` 不再把 `shownHints` 抹掉,
|
|
254
|
+
* 「首次达到阈值」的去重自此真生效。
|
|
244
255
|
*/
|
|
245
|
-
async function getSmartHint(projectPath: string): Promise<string | null> {
|
|
256
|
+
async function getSmartHint(projectPath: string, stateIO: StateIO): Promise<string | null> {
|
|
246
257
|
const tracesPath = path.join(projectPath, DEFAULT_TRACE_FILE);
|
|
247
|
-
const statePath = path.join(projectPath, '.harness', '.state.json');
|
|
248
258
|
|
|
249
259
|
// 只读够 TRACE_HINT_THRESHOLD 行即停(坏行照旧占位,条数口径与改前的纯计数逐字一致)——
|
|
250
260
|
// traces.log 是 append-only 无上限文件,为一个比较符整读不成立
|
|
@@ -256,19 +266,12 @@ async function getSmartHint(projectPath: string): Promise<string | null> {
|
|
|
256
266
|
if (traceCount === 0) {
|
|
257
267
|
return null;
|
|
258
268
|
}
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
shownHints?: string[];
|
|
262
|
-
lastStatusRun?: string;
|
|
263
|
-
lastDiagnoseRun?: string;
|
|
264
|
-
} = {};
|
|
265
|
-
if (fs.existsSync(statePath)) {
|
|
266
|
-
state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
267
|
-
}
|
|
269
|
+
// 读取状态(经接缝;文件缺失 → {})
|
|
270
|
+
const state = stateIO.read();
|
|
268
271
|
state.shownHints = state.shownHints || [];
|
|
269
|
-
|
|
272
|
+
|
|
270
273
|
const hints: string[] = [];
|
|
271
|
-
|
|
274
|
+
|
|
272
275
|
// 条件 1: 记录数首次达到阈值
|
|
273
276
|
if (traceCount >= TRACE_HINT_THRESHOLD && !state.shownHints.includes('trace_50')) {
|
|
274
277
|
hints.push('📊 记录已足够,运行 harness status 查看统计');
|
|
@@ -277,11 +280,10 @@ async function getSmartHint(projectPath: string): Promise<string | null> {
|
|
|
277
280
|
|
|
278
281
|
// 保存状态
|
|
279
282
|
if (hints.length > 0) {
|
|
280
|
-
|
|
281
|
-
fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
283
|
+
stateIO.write(state);
|
|
282
284
|
return hints.join('\n');
|
|
283
285
|
}
|
|
284
|
-
|
|
286
|
+
|
|
285
287
|
return null;
|
|
286
288
|
}
|
|
287
289
|
|
|
@@ -13,6 +13,7 @@ import { DEFAULT_TRACE_FILE } from '../../types/trace';
|
|
|
13
13
|
import type { ExecutionTrace } from '../../types/trace';
|
|
14
14
|
import type { TraceSummary, TraceAnomaly } from '../../types/trace';
|
|
15
15
|
import { log, logError, processIO, type CommandIO, type CommandResult } from '../command-contract';
|
|
16
|
+
import { fileStateIO, type StateIO } from '../state-io';
|
|
16
17
|
|
|
17
18
|
export interface StatusOptions {
|
|
18
19
|
/** 项目路径 */
|
|
@@ -23,6 +24,13 @@ export interface StatusOptions {
|
|
|
23
24
|
anomalies?: boolean;
|
|
24
25
|
/** 时间范围(小时) */
|
|
25
26
|
hours?: number;
|
|
27
|
+
/**
|
|
28
|
+
* 状态文件接缝(非 CLI flag;ADR-0026)
|
|
29
|
+
*
|
|
30
|
+
* 缺省 = 真实 fs 实现(fileStateIO)。注入则 `.harness/.state.json` 的读写
|
|
31
|
+
* 走替身,测试据此不碰真文件系统。
|
|
32
|
+
*/
|
|
33
|
+
stateIO?: StateIO;
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
/**
|
|
@@ -32,7 +40,7 @@ export async function status(options: StatusOptions, io: CommandIO = processIO):
|
|
|
32
40
|
const projectPath = options.projectPath || process.cwd();
|
|
33
41
|
const harnessDir = path.join(projectPath, '.harness');
|
|
34
42
|
const tracesPath = path.join(projectPath, DEFAULT_TRACE_FILE);
|
|
35
|
-
const
|
|
43
|
+
const stateIO = options.stateIO ?? fileStateIO(projectPath);
|
|
36
44
|
|
|
37
45
|
log(io, chalk.blue('📊 Harness 状态'));
|
|
38
46
|
log(io);
|
|
@@ -139,12 +147,9 @@ export async function status(options: StatusOptions, io: CommandIO = processIO):
|
|
|
139
147
|
log(io);
|
|
140
148
|
}
|
|
141
149
|
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
};
|
|
146
|
-
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
147
|
-
fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
150
|
+
// 更新状态文件:读-改-写(ADR-0026 决策 2)——不再整文件重写,
|
|
151
|
+
// 否则 check 写入的 shownHints 会被抹掉、智能提示去重失效
|
|
152
|
+
stateIO.write({ ...stateIO.read(), lastStatusRun: new Date().toISOString() });
|
|
148
153
|
|
|
149
154
|
// 下一步建议
|
|
150
155
|
log(io, chalk.blue('💡 下一步建议:'));
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 状态文件 IO 接缝(StateIO,ADR-0026 / harness#148)
|
|
3
|
+
*
|
|
4
|
+
* `.harness/.state.json` 是 harness **自身**的运行期状态(与 RunEnv 观察的项目上行
|
|
5
|
+
* 数据不是一类东西,RunEnv 保持 ADR-0023 的只读契约)。本模块是它读-改-写的唯一
|
|
6
|
+
* 入口:`check` 的智能提示与 `status` 两个写者都经本接缝,命令函数以可选参数注入,
|
|
7
|
+
* 缺省 = 真实 fs 实现(照 `CommandIO` 的注入模式,但不扩 `CommandIO`——输出面与
|
|
8
|
+
* 状态面是两个概念)。
|
|
9
|
+
*
|
|
10
|
+
* 落点 cli 层:两个消费者都在 cli,不进 core、不进包根导出面(零公共面变化)。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as fs from 'fs';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* `.harness/.state.json` 的具名类型(从 check.ts 内联类型提出,ADR-0026 决策 1)。
|
|
18
|
+
* `lastDiagnoseRun` 字段已删除——全仓零生产者零消费者(ADR-0022 口径);
|
|
19
|
+
* 已落盘旧文件里残留该键无害,不做迁移。
|
|
20
|
+
*/
|
|
21
|
+
export interface HarnessState {
|
|
22
|
+
/** 已展示过的智能提示 id(去重依据) */
|
|
23
|
+
shownHints?: string[];
|
|
24
|
+
/** 最近一次 `harness status` 运行时刻(ISO 串) */
|
|
25
|
+
lastStatusRun?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** harness 自身运行期状态文件的可注入接缝(读-改-写的唯一入口) */
|
|
29
|
+
export interface StateIO {
|
|
30
|
+
/** 文件缺失/空 → `{}`;损坏 → 现状语义(JSON.parse 抛即抛,不兜底,ADR-0026 决策 4) */
|
|
31
|
+
read(): HarnessState;
|
|
32
|
+
write(state: HarnessState): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 缺省真实 fs 实现:锚 projectPath(与 trace 落点同锚,#139),
|
|
37
|
+
* 状态文件路径知识只在本模块一份。
|
|
38
|
+
*/
|
|
39
|
+
export function fileStateIO(projectPath: string): StateIO {
|
|
40
|
+
const statePath = path.join(projectPath, '.harness', '.state.json');
|
|
41
|
+
return {
|
|
42
|
+
read(): HarnessState {
|
|
43
|
+
if (!fs.existsSync(statePath)) return {};
|
|
44
|
+
const content = fs.readFileSync(statePath, 'utf-8');
|
|
45
|
+
if (content.trim() === '') return {};
|
|
46
|
+
return JSON.parse(content);
|
|
47
|
+
},
|
|
48
|
+
write(state: HarnessState): void {
|
|
49
|
+
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
50
|
+
fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
package/src/hooks/CONTEXT.md
CHANGED
|
@@ -9,7 +9,7 @@ H5(#44)起增加两个机制(G2/G7):
|
|
|
9
9
|
|
|
10
10
|
## 核心导出
|
|
11
11
|
- `HookRegistry` — Hook 注册表(register/registerAll/unregister/get/getEnabled/listNames/listAll/setEnabled/clear;register 以 HookConfig 填充有效值)
|
|
12
|
-
- `HookPipeline` — Hook 执行管线(注册/排序/错误隔离/采样;errorStrategy block/warn
|
|
12
|
+
- `HookPipeline` — Hook 执行管线(注册/排序/错误隔离/采样;errorStrategy block/warn;`run(phase)` 时机粒度、`runFull` 全套、`runOne(name)` 按名执行单个 hook——#167 新增:enabled:false 返回 skipped:true 记录且实现体零调用,block 失败抛错 / warn 失败返回 passed:false 记录,未知名抛错(口径同注册表闭环),采样与错误隔离复用 executeOne 单路径)
|
|
13
13
|
- `assertHookRegistryClosed` — 注册表闭环双向校验(构建/测试期)
|
|
14
14
|
- `HookConfig`(type)— per-hook 配置声明(enabled / errorStrategy 唯一声明点)
|
|
15
15
|
- `EffectiveHook`(type)— HookDefinition + 配置填充的有效 enabled / errorStrategy(管线与注册表判定只读它)
|