@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.
@@ -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
- const state = JSON.parse(
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
- write(dir, '.harness/.state.json', JSON.stringify({ shownHints: ['trace_50'] }));
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 status({ anomalies: true }, io);
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 status({ anomalies: true }, io);
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 status({}, io);
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 status({}, io);
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 status({ detail: true }, io);
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 status({}, io);
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 status({}, io);
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 status({}, io);
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 status({}, io);
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 status({ detail: true }, io);
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 status({ anomalies: true }, io);
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 status({ detail: true }, io);
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('应该更新 .state.json', async () => {
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 status({}, io);
133
- expect(mockFs.writeFileSync).toHaveBeenCalled();
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 status({}, io);
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 status({ anomalies: true }, io);
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 status({ anomalies: true }, io);
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 status({}, io);
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 status({}, io);
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 status({ anomalies: true }, io);
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 status({ detail: true }, io);
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
- let state: {
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
- fs.mkdirSync(path.dirname(statePath), { recursive: true });
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 statePath = path.join(harnessDir, '.state.json');
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
- const state = {
144
- lastStatusRun: new Date().toISOString(),
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
+ }
@@ -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(管线与注册表判定只读它)