@dommaker/harness 1.8.1 → 1.9.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +1 -1
  3. package/dist/cli/commands/sync-docs/capabilities-syncer.d.ts +22 -0
  4. package/dist/cli/commands/sync-docs/capabilities-syncer.d.ts.map +1 -1
  5. package/dist/cli/commands/sync-docs/capabilities-syncer.js +113 -8
  6. package/dist/cli/commands/sync-docs/capabilities-syncer.js.map +1 -1
  7. package/dist/cli/commands/sync-docs/index.d.ts.map +1 -1
  8. package/dist/cli/commands/sync-docs/index.js +36 -4
  9. package/dist/cli/commands/sync-docs/index.js.map +1 -1
  10. package/dist/hooks/bootstrap.d.ts +9 -12
  11. package/dist/hooks/bootstrap.d.ts.map +1 -1
  12. package/dist/hooks/bootstrap.js +9 -31
  13. package/dist/hooks/bootstrap.js.map +1 -1
  14. package/dist/hooks/index.d.ts +5 -7
  15. package/dist/hooks/index.d.ts.map +1 -1
  16. package/dist/hooks/index.js +4 -10
  17. package/dist/hooks/index.js.map +1 -1
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +3 -7
  21. package/dist/index.js.map +1 -1
  22. package/package.json +1 -1
  23. package/src/CONTEXT.md +1 -1
  24. package/src/__tests__/public-exports.test.ts +30 -4
  25. package/src/__tests__/public-type-surface.test.ts +0 -8
  26. package/src/cli/commands/CONTEXT.md +3 -0
  27. package/src/cli/commands/__tests__/registry.test.ts +39 -3
  28. package/src/cli/commands/__tests__/sync-docs-table-layout.test.ts +259 -0
  29. package/src/cli/commands/sync-docs/capabilities-syncer.ts +139 -9
  30. package/src/cli/commands/sync-docs/index.ts +41 -4
  31. package/src/hooks/CONTEXT.md +12 -22
  32. package/src/hooks/__tests__/bootstrap.test.ts +21 -70
  33. package/src/hooks/bootstrap.ts +9 -48
  34. package/src/hooks/index.ts +5 -20
  35. package/src/index.ts +1 -13
  36. package/dist/hooks/config.d.ts +0 -30
  37. package/dist/hooks/config.d.ts.map +0 -1
  38. package/dist/hooks/config.js +0 -34
  39. package/dist/hooks/config.js.map +0 -1
  40. package/dist/hooks/pipeline.d.ts +0 -54
  41. package/dist/hooks/pipeline.d.ts.map +0 -1
  42. package/dist/hooks/pipeline.js +0 -173
  43. package/dist/hooks/pipeline.js.map +0 -1
  44. package/dist/hooks/registry.d.ts +0 -70
  45. package/dist/hooks/registry.d.ts.map +0 -1
  46. package/dist/hooks/registry.js +0 -141
  47. package/dist/hooks/registry.js.map +0 -1
  48. package/dist/hooks/types.d.ts +0 -113
  49. package/dist/hooks/types.d.ts.map +0 -1
  50. package/dist/hooks/types.js +0 -9
  51. package/dist/hooks/types.js.map +0 -1
  52. package/src/__tests__/hooks-pipeline.test.ts +0 -201
  53. package/src/hooks/__tests__/config.test.ts +0 -19
  54. package/src/hooks/__tests__/pipeline.test.ts +0 -195
  55. package/src/hooks/__tests__/registry.test.ts +0 -115
  56. package/src/hooks/config.ts +0 -33
  57. package/src/hooks/pipeline.ts +0 -202
  58. package/src/hooks/registry.ts +0 -166
  59. package/src/hooks/types.ts +0 -120
@@ -54,12 +54,22 @@ export async function updateCapabilitiesFile(
54
54
 
55
55
  // 如果有表格行,更新表格
56
56
  if (existingFiles.length > 0) {
57
- // 移除已删除文件的行
58
- for (const removed of result.removed) {
59
- const escapedFile = removed.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
60
- // 第二列存完整路径,basename 只在末尾出现,用 [^|]* 匹配路径前缀
61
- const rowRegex = new RegExp(`^\\|[^|]*\\|[^|]*\\b${escapedFile}\\s*\\|.*$`, 'gm');
62
- content = content.replace(rowRegex, '');
57
+ // 移除已删除文件的行(整行连行尾一起删——只清行内容会留一个空行,
58
+ // CommonMark 据此把一张表切成若干小表,harness#171)。
59
+ // 这里**不**豁免围栏代码块,与下方排版收拢刻意不同口径:登记条目由
60
+ // capabilities-parser 全文扫描得出(ADR-0009),围栏内的行同样算登记项;
61
+ // 只让删除认围栏而条目不认,块内示例行会一直被解析成幽灵条目,`--check`
62
+ // 从此每轮都报同一个已删文件且永远修不掉。豁免要生效得连登记面一起改。
63
+ if (result.removed.length > 0) {
64
+ const deadRowRegexes = result.removed.map((removed) => {
65
+ const escapedFile = removed.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
66
+ // 第二列存完整路径,basename 只在末尾出现,用 [^|]* 匹配路径前缀
67
+ return new RegExp(`^\\|[^|]*\\|[^|]*\\b${escapedFile}\\s*\\|.*\\r?$`);
68
+ });
69
+ content = content
70
+ .split('\n')
71
+ .filter((line) => !deadRowRegexes.some((rowRegex) => rowRegex.test(line)))
72
+ .join('\n');
63
73
  }
64
74
 
65
75
  // 添加新文件的行(在最后一个表格行之后);module 模式跳过
@@ -80,14 +90,17 @@ export async function updateCapabilitiesFile(
80
90
  content = content.replace(lastTableRow, lastTableRow + '\n' + newRows);
81
91
  }
82
92
  }
83
-
84
- // 清理多余空行
85
- content = content.replace(/\n{3,}/g, '\n\n');
86
93
  } else {
87
94
  // 没有表格,追加模块表格(module 模式按目录聚合)
88
95
  content += '\n\n' + (mode === 'module' ? generateDirTable(currentModules) : generateModuleTable(currentModules));
89
96
  }
90
97
 
98
+ // 表格排版收拢放在增删之后:有新行的表不会被误判为空表(#171)
99
+ content = normalizeCapabilitiesTableLayout(content).content;
100
+ // 多余空行的清理收在收拢之后一处:收拢本身会新产出连续空行
101
+ // (收掉一张夹在两段散文之间的空表就留下 `\n\n\n`,#171)
102
+ content = content.replace(/\n{3,}/g, '\n\n');
103
+
91
104
  // 更新最后更新时间
92
105
  const now = new Date().toISOString().split('T')[0];
93
106
  content = content.replace(
@@ -98,6 +111,123 @@ export async function updateCapabilitiesFile(
98
111
  await fs.writeFile(capabilitiesPath, content, 'utf-8');
99
112
  }
100
113
 
114
+ /** 表格行:以 `|` 起始(表头、分隔行、数据行都算) */
115
+ const TABLE_ROW_REGEX = /^\s*\|/;
116
+
117
+ /** 表格分隔行(|------|------|) */
118
+ const TABLE_SEPARATOR_REGEX = /^\s*\|[\s:|-]+\|\s*$/;
119
+
120
+ /** 围栏代码块的起止行(``` / ~~~,允许缩进与信息串) */
121
+ const FENCE_LINE_REGEX = /^\s*(?:```|~~~)/;
122
+
123
+ export interface TableLayoutNormalization {
124
+ /** 收拢后的内容 */
125
+ content: string;
126
+ /** 被删掉的「表格内空行」行数 */
127
+ blankLines: number;
128
+ /** 被收掉的「空表」(表头+分隔行且无数据行)张数 */
129
+ emptyTables: number;
130
+ }
131
+
132
+ /** 参与排版判定的行:正文 + 是否落在围栏代码块内 */
133
+ interface LayoutLine {
134
+ text: string;
135
+ fenced: boolean;
136
+ }
137
+
138
+ /**
139
+ * 按行切开并标出围栏代码块(``` / ~~~)内的行
140
+ *
141
+ * 只服务排版收拢:CAPABILITIES.md 是手写文档,块内长得像表格的示例行不是排版脏行,
142
+ * 收它就是删用户正文(harness#171)。登记条目面不在此列——见 `updateCapabilitiesFile`
143
+ * 里「幽灵行删除不豁免围栏」的取舍注释。
144
+ */
145
+ function parseLayoutLines(content: string): LayoutLine[] {
146
+ const parsed: LayoutLine[] = [];
147
+ let inFence = false;
148
+ for (const text of content.split('\n')) {
149
+ if (FENCE_LINE_REGEX.test(text)) {
150
+ parsed.push({ text, fenced: true });
151
+ inFence = !inFence;
152
+ continue;
153
+ }
154
+ parsed.push({ text, fenced: inFence });
155
+ }
156
+ return parsed;
157
+ }
158
+
159
+ const isTableRow = (line: LayoutLine | undefined): line is LayoutLine =>
160
+ line !== undefined && !line.fenced && TABLE_ROW_REGEX.test(line.text);
161
+
162
+ const isTableSeparator = (line: LayoutLine | undefined): boolean =>
163
+ line !== undefined && !line.fenced && TABLE_SEPARATOR_REGEX.test(line.text);
164
+
165
+ /** 走一遍两条收拢规则(不迭代) */
166
+ function collapseTableLayoutOnce(content: string): TableLayoutNormalization {
167
+ const parsed = parseLayoutLines(content);
168
+
169
+ // ① 收拢表格内空行:一段连续空行,两侧最近非空行都是表格行 → 整段丢弃
170
+ const kept: LayoutLine[] = [];
171
+ let blankLines = 0;
172
+ for (let i = 0; i < parsed.length; ) {
173
+ if (parsed[i].text.trim() !== '') {
174
+ kept.push(parsed[i]);
175
+ i++;
176
+ continue;
177
+ }
178
+ let end = i;
179
+ while (end < parsed.length && parsed[end].text.trim() === '') end++;
180
+ if (isTableRow(kept[kept.length - 1]) && isTableRow(parsed[end])) {
181
+ blankLines += end - i;
182
+ } else {
183
+ kept.push(...parsed.slice(i, end));
184
+ }
185
+ i = end;
186
+ }
187
+
188
+ // ② 收掉空表:表头 + 分隔行后面没有数据行
189
+ const out: string[] = [];
190
+ let emptyTables = 0;
191
+ for (let i = 0; i < kept.length; i++) {
192
+ if (isTableRow(kept[i]) && isTableSeparator(kept[i + 1]) && !isTableRow(kept[i + 2])) {
193
+ emptyTables++;
194
+ i++;
195
+ continue;
196
+ }
197
+ out.push(kept[i].text);
198
+ }
199
+
200
+ return { content: out.join('\n'), blankLines, emptyTables };
201
+ }
202
+
203
+ /**
204
+ * 收拢 CAPABILITIES.md 的表格排版(harness#171)
205
+ *
206
+ * 两条规则:① 删掉夹在两个表格行之间的空行(CommonMark 会在此切断表格);
207
+ * ② 收掉没有数据行的表头+分隔行。表格外的空行(段落分隔)不动;围栏代码块内的行整体豁免。
208
+ *
209
+ * 规则互相制造对方的触发点(①把两张空表之间的那个空行吃掉后,②一轮只收得掉后一张),
210
+ * 所以跑到不动点:**一次 `sync-docs` 必须把 `--check` 报出来的东西全清掉**,
211
+ * 否则下游 CI 修完还是红的。计数 = 各轮合计(每轮只删当轮存在的行,不会重计)。
212
+ *
213
+ * 幂等,且 `--check` 与写模式共用此正本——判定面就是「返回内容与入参是否不同」,
214
+ * 因此不存在「check 报了 fix 修不掉」的不收敛(ADR-0009 口径)。
215
+ */
216
+ export function normalizeCapabilitiesTableLayout(content: string): TableLayoutNormalization {
217
+ let current = content;
218
+ let blankLines = 0;
219
+ let emptyTables = 0;
220
+ for (;;) {
221
+ const pass = collapseTableLayoutOnce(current);
222
+ if (pass.blankLines === 0 && pass.emptyTables === 0) {
223
+ return { content: current, blankLines, emptyTables };
224
+ }
225
+ blankLines += pass.blankLines;
226
+ emptyTables += pass.emptyTables;
227
+ current = pass.content;
228
+ }
229
+ }
230
+
101
231
  /**
102
232
  * 生成 CAPABILITIES.md 内容
103
233
  */
@@ -34,6 +34,7 @@ import {
34
34
  parseCapabilitiesFiles,
35
35
  updateCapabilitiesFile,
36
36
  compactCapabilitiesContent,
37
+ normalizeCapabilitiesTableLayout,
37
38
  } from './capabilities-syncer';
38
39
  import {
39
40
  createContextMd,
@@ -264,7 +265,15 @@ export async function syncDocs(
264
265
  }
265
266
  const hasAgentsIssues = options.agents === true && agentsMdStale;
266
267
 
267
- const hasTableIssues = result.added.length > 0 || result.removed.length > 0;
268
+ // 表格排版脏行(#171):撤登记时残留的空行会把 CAPABILITIES.md 的表格切断。
269
+ // 判定与修复共用 normalizeCapabilitiesTableLayout 正本——同一份规则,check 报的 fix 必清得掉。
270
+ const tableLayout = capsIsCapabilityListing
271
+ ? { blankLines: 0, emptyTables: 0 }
272
+ : normalizeCapabilitiesTableLayout(capsContent);
273
+ const hasTableLayoutIssues = tableLayout.blankLines > 0 || tableLayout.emptyTables > 0;
274
+
275
+ const hasTableEntryIssues = result.added.length > 0 || result.removed.length > 0;
276
+ const hasTableIssues = hasTableEntryIssues || hasTableLayoutIssues;
268
277
  const hasCapIssues = capCountMismatches.length > 0;
269
278
  // mtime 只作提示,不参与判定(harness#142):判定面是内容漂移与缺失
270
279
  const hasContextIssues = result.contextMissing.length > 0 || result.contextContentDrift.length > 0;
@@ -287,6 +296,7 @@ export async function syncDocs(
287
296
  contextMissing: result.contextMissing.length,
288
297
  contextStale: result.contextStale.length,
289
298
  contextContentDrift: result.contextContentDrift.length,
299
+ tableLayoutDirty: hasTableLayoutIssues,
290
300
  },
291
301
  contextMissing: result.contextMissing.map(d => ({
292
302
  dir: d,
@@ -314,7 +324,7 @@ export async function syncDocs(
314
324
  });
315
325
  }
316
326
 
317
- if (!capsIsCapabilityListing && hasTableIssues) {
327
+ if (!capsIsCapabilityListing && hasTableEntryIssues) {
318
328
  if (capsMode === 'module') {
319
329
  // module 模式:added 为聚合后的未覆盖目录,需人工登记目录条目
320
330
  jsonOutput.added = result.added.map(d => ({ dir: d }));
@@ -341,6 +351,19 @@ export async function syncDocs(
341
351
  }
342
352
  }
343
353
 
354
+ if (hasTableLayoutIssues) {
355
+ jsonOutput.tableLayout = {
356
+ blankLines: tableLayout.blankLines,
357
+ emptyTables: tableLayout.emptyTables,
358
+ };
359
+ (jsonOutput.resolution as Array<Record<string, unknown>>).push({
360
+ action: 'sync-capabilities-table-layout',
361
+ command: 'harness sync-docs',
362
+ details:
363
+ 'CAPABILITIES.md 的表格被空行切断、或残留无数据行的空表头,运行 harness sync-docs 收拢',
364
+ });
365
+ }
366
+
344
367
  if (hasContextIssues) {
345
368
  (jsonOutput.resolution as Array<Record<string, unknown>>).push(
346
369
  ...(result.contextMissing.length > 0
@@ -400,6 +423,16 @@ export async function syncDocs(
400
423
  result.removed.forEach(f => log(io, chalk.gray(` - ${f}`)));
401
424
  }
402
425
 
426
+ if (hasTableLayoutIssues) {
427
+ log(io, chalk.yellow(`\n🧹 CAPABILITIES.md 表格排版待收拢:`));
428
+ if (tableLayout.blankLines > 0) {
429
+ log(io, chalk.gray(` - 表格内空行 ${tableLayout.blankLines} 处(CommonMark 会在此把表格切断)`));
430
+ }
431
+ if (tableLayout.emptyTables > 0) {
432
+ log(io, chalk.gray(` - 无数据行的空表 ${tableLayout.emptyTables} 张(连表头/分隔行一起收掉)`));
433
+ }
434
+ }
435
+
403
436
  if (result.contextMissing.length > 0) {
404
437
  log(io, chalk.yellow(`\n📋 缺少 CONTEXT.md:`));
405
438
  result.contextMissing.forEach(d => log(io, chalk.gray(` - ${d}/CONTEXT.md`)));
@@ -450,10 +483,14 @@ export async function syncDocs(
450
483
  if (isCheck) {
451
484
  log(io, chalk.red('\n❌ 文档不是最新的,请运行 harness sync-docs 更新'));
452
485
  // reason 必须可定位(harness#142):CI 判红时要直接拿到文件与符号,不靠翻 stdout
486
+ const tableLayoutReason = hasTableLayoutIssues
487
+ ? `;CAPABILITIES.md 表格排版待收拢(表格内空行 ${tableLayout.blankLines} 处、`
488
+ + `空表 ${tableLayout.emptyTables} 张)`
489
+ : '';
453
490
  return drift(
454
491
  contextDriftReason
455
- ? `文档不是最新的(CONTEXT.md 与实现漂移:${contextDriftReason})`
456
- : '文档不是最新的,请运行 harness sync-docs 更新'
492
+ ? `文档不是最新的(CONTEXT.md 与实现漂移:${contextDriftReason})${tableLayoutReason}`
493
+ : `文档不是最新的,请运行 harness sync-docs 更新${tableLayoutReason}`
457
494
  );
458
495
  }
459
496
 
@@ -1,34 +1,24 @@
1
1
  # hooks/
2
2
 
3
3
  ## 职责
4
- 通用 Hook 管线:注册 → 排序 → 错误隔离 → 采样执行。无业务逻辑,consumer 自行定义 hook 名称和语义。
4
+ Harness 运行环境的组合根:一次调用装配起约束检查器、会话管理器与 trace 记录器,并加载项目配置。
5
5
 
6
- H5(#44)起增加两个机制(G2/G7):
7
- - **注册表闭环**:`assertHookRegistryClosed(configs, hooks)` 声明(HookConfig)↔ 实现(HookDefinition)双向校验——引用未注册/注册无定义/重复均抛错,复制 checker 闭环模式;断言限构建/测试期,不进运行时热路径
8
- - **配置归一**:`HookConfig { name, enabled, errorStrategy }` 为 per-hook 配置唯一真相,且自 #159 起是 `enabled` / `errorStrategy` 的**唯一声明点**——`HookDefinition` 不再携带这两个字段,注册环节以配置表填充有效值(`EffectiveHook`),声明一处、读取一处,两侧矛盾在构造上不可能出现;`toErrorStrategy(blocking)` 承载 studio `blocking` → errorStrategy 的无损映射
6
+ ADR-0027(#170)起本层只剩 bootstrap 一个面。原先的通用 hook 管线(`registry` / `pipeline` / `config` / `types` 四文件:注册 → 排序 → 错误隔离 → 采样执行)双仓零生产消费者,整体删除——裁决记录 `docs/adr/0027-hooks-pipeline-surface-trim.md`,事实前提 studio#562(studio 侧 hooks 层删除)。目录名沿用历史,不再表示「提供 hook 能力」。
9
7
 
10
8
  ## 核心导出
11
- - `HookRegistry` — Hook 注册表(register/registerAll/unregister/get/getEnabled/listNames/listAll/setEnabled/clear;register HookConfig 填充有效值)
12
- - `HookPipeline` — Hook 执行管线(注册/排序/错误隔离/采样;errorStrategy block/warn;`run(phase)` 时机粒度、`runFull` 全套、`runOne(name)` 按名执行单个 hook——#167 新增:enabled:false 返回 skipped:true 记录且实现体零调用,block 失败抛错 / warn 失败返回 passed:false 记录,未知名抛错(口径同注册表闭环),采样与错误隔离复用 executeOne 单路径)
13
- - `assertHookRegistryClosed` 注册表闭环双向校验(构建/测试期)
14
- - `HookConfig`(type)— per-hook 配置声明(enabled / errorStrategy 唯一声明点)
15
- - `EffectiveHook`(type)— HookDefinition + 配置填充的有效 enabled / errorStrategy(管线与注册表判定只读它)
16
- - `toErrorStrategy` — blocking → errorStrategy 无损映射(G7)
17
- - `bootstrapHarness` / `bootstrapHarnessSync` — Harness 启动引导;也是 trace 记录器的组合根:`new ConstraintChecker(new TraceCollector({ projectPath }))`(harness#88 接线,core 不上行依赖 monitoring,故由本层接线;#139 收根:两个入口本就收 projectPath,落点随之锚定,不再取 cwd 锚定的 `getTraceCollector()` 单例)
9
+ - `bootstrapHarness` — 异步组合根(S9:配置异步加载,不阻塞事件循环);也是 trace 记录器的接线点:`new ConstraintChecker(new TraceCollector({ projectPath }))`(harness#88:core 不上行依赖 monitoring,故由本层接线;#139 收根:两个入口本就收 projectPath,落点随之锚定,不再取 cwd 锚定的 `getTraceCollector()` 单例)
10
+ - `bootstrapHarnessSync` — 同形状的同步版,配置走 `readFileSync`,供不支持 top-level await 的环境与 `bootstrapHarness` 失败时的回落路径
11
+ - `HarnessBootstrap`(type)— 返回值形状 `{ checker, sessions, projectPath, mergedConstraints }`
18
12
 
19
13
  ## 依赖关系
20
- - 类型正本在**本模块** `src/hooks/types.ts`(`HookPhase`/`HookErrorStrategy`/`HookConfig`/`HookDefinition`/`EffectiveHook`/`HookResult`/`PipelineResult`);`src/types/` 里没有 hook 类型,本层只从 `src/types/project-config` 取 `MergedConstraintsConfig` 类型
21
- - 向下依赖(仅 `bootstrap.ts` 组合根):`core/constraints/checker`、`core/project-config-loader`、`context/session-manager`、`monitoring/traces`——`registry/pipeline/config/types` 四文件零上层依赖,是纯管线
22
- - 消费方:包根 `src/index.ts` 的 `./hooks` 出口与下游项目(studio 的 hook 装配)。**harness 内部无生产消费方**——core/cli 侧的调用边已随零消费者清账删除(harness#141 同判据,ADR-0022),「被 core 核心引擎/CLI 初始化流程消费」的旧说法已失效(harness#142 核对)
14
+ - 向下依赖:`core/constraints/checker`、`core/project-config-loader`、`context/session-manager`、`monitoring/traces`;类型面只从 `src/types/project-config` 取 `MergedConstraintsConfig`
15
+ - 消费方:包根 `src/index.ts` 的 bootstrap 三符号出口,与下游项目的运行环境初始化。生产唯一调用方是 studio(经 studio-shared `runtime/bootstrap.ts`,**无参调用**、只以类型持有 `HarnessBootstrap`)。**harness 内部无生产消费方**——core/cli 侧的调用边已随零消费者清账删除(harness#141 同判据,ADR-0022)
23
16
 
24
17
  ## 约定
25
- - 无业务逻辑,只提供管线能力
26
- - Consumer 自行定义 hook 名称和触发时机
27
- - **闭环**:声明配置与注册实现必须一一对应,缺一抛错(消灭「hook 定义不注册 = 死代码」类人记规矩)
28
- - **映射语义**:blocking=true → 'block'(失败阻断管线,停止后续 hook、passed=false);blocking=false → 'warn'(记录警告继续);有效策略集合仅 'block' | 'warn'(#159 起原 'ignore' 与「未声明 strategy」形态退出有效面——策略由 HookConfig 必填声明)
29
- - 错误隔离:单个 hook 失败不影响其他 hook
18
+ - 参数面只有 `projectPath`(缺省 `process.cwd()`):不给 hook 定义、不给配置表,`hookDefinitions` / `hookConfigs` 与注册闭环语义随管线面一同退场
19
+ - 组合根只做装配不做判定,`mergedConstraints` 原样透出,用不用由调用方决定
20
+ - 本层无内置 hook 定义,也不再提供 hook 管线能力;provider 侧的 PreToolUse 执法入口是 `src/pretool-use-hook.ts`(门禁 CLI 的另一条路),与本目录无关
30
21
 
31
22
  ## 注意事项
32
- - 通用管线设计,不绑定特定生命周期
33
- - 采样执行用于高频 hook(减少性能影响)
34
- - 闭环断言是纯函数,由 consumer 在其注册点/测试中调用;harness 无内置 hook 定义,不自动断言
23
+ - 无 unload/dispose:`SessionManager` 与 `TraceCollector` 随进程生命周期,本层不提供逆操作(口径同 `src/CONTEXT.md` 术语「文件驱动 CLI」)
24
+ - 删除属公共面 breaking:包根不再可达的符号有 4 个值符号(注册表、管线、闭环断言、blocking 映射)与 8 个类型,迁移路径 = 删引用;可达性负钉在 `src/__tests__/public-exports.test.ts`,两道全量清单闸(`public-exports` / `public-type-surface`)的条目已同步收缩
@@ -5,17 +5,17 @@
5
5
  * bootstrapHarnessSync uses synchronous fs reads and is more straightforward.
6
6
  *
7
7
  * Uses temp directories to provide harness config.
8
+ *
9
+ * ADR-0027(#170):hooks 管线面删除后本层只剩组合根职责——加载项目配置、
10
+ * 装配 checker / SessionManager / TraceCollector。`hookDefinitions` / `hookConfigs`
11
+ * 两参数与 `hooks` / `pipeline` 两字段随之消失,形状由编译期 + 运行期双钉冻结
12
+ * (手法照 ADR-0022 对 AC-007 的改写)。
8
13
  */
9
14
 
10
15
  import * as fs from 'fs';
11
16
  import * as path from 'path';
12
17
  import * as yaml from 'js-yaml';
13
18
  import { bootstrapHarnessSync, bootstrapHarness } from '../bootstrap';
14
- import type { HookConfig, HookDefinition } from '../types';
15
-
16
- function makeHookConfig(name: string, overrides: Partial<HookConfig> = {}): HookConfig {
17
- return { name, enabled: true, errorStrategy: 'warn', ...overrides };
18
- }
19
19
 
20
20
  function setupTempDir(dir: string): string {
21
21
  const harnessDir = path.join(dir, '.harness');
@@ -28,6 +28,16 @@ function setupTempDir(dir: string): string {
28
28
  return harnessDir;
29
29
  }
30
30
 
31
+ /** 已删字段的编译期钉:回灌(重新声明该字段)即 TS2578 红。纯读,不产生任何落盘副作用。 */
32
+ function assertPipelineSurfaceUnreachable(result: ReturnType<typeof bootstrapHarnessSync>): void {
33
+ // @ts-expect-error hooks 字段随 ADR-0027 管线面删除
34
+ const gone = result.hooks;
35
+ expect(gone).toBeUndefined();
36
+ // @ts-expect-error pipeline 字段随 ADR-0027 管线面删除
37
+ const gone2 = result.pipeline;
38
+ expect(gone2).toBeUndefined();
39
+ }
40
+
31
41
  describe('bootstrapHarness', () => {
32
42
  let tempDir: string;
33
43
 
@@ -44,10 +54,11 @@ describe('bootstrapHarness', () => {
44
54
  const result = await bootstrapHarness(tempDir);
45
55
  expect(result).toHaveProperty('checker');
46
56
  expect(result).toHaveProperty('sessions');
47
- expect(result).toHaveProperty('hooks');
48
- expect(result).toHaveProperty('pipeline');
49
57
  expect(result).toHaveProperty('projectPath', tempDir);
50
58
  expect(result).toHaveProperty('mergedConstraints');
59
+ expect(Object.keys(result).sort()).toEqual(
60
+ ['checker', 'mergedConstraints', 'projectPath', 'sessions'],
61
+ );
51
62
  });
52
63
 
53
64
  it('initializes with the provided project path', async () => {
@@ -55,33 +66,8 @@ describe('bootstrapHarness', () => {
55
66
  expect(result.projectPath).toBe(tempDir);
56
67
  });
57
68
 
58
- it('registers hook definitions when provided (with configs)', async () => {
59
- const hookDef: HookDefinition = {
60
- name: 'async-hook',
61
- phase: 'before',
62
- execute: async () => ({ passed: true }),
63
- };
64
- const result = await bootstrapHarness(tempDir, [hookDef], [makeHookConfig('async-hook')]);
65
- expect(result.hooks.listNames()).toContain('async-hook');
66
- expect(result.hooks.get('async-hook')?.errorStrategy).toBe('warn');
67
- });
68
-
69
- it('throws when definitions are provided without configs (#159)', async () => {
70
- const hookDef: HookDefinition = {
71
- name: 'async-hook',
72
- phase: 'before',
73
- execute: async () => ({ passed: true }),
74
- };
75
- await expect(bootstrapHarness(tempDir, [hookDef])).rejects.toThrow(/HookConfig/);
76
- });
77
-
78
- it('does not register hooks when no definitions provided', async () => {
79
- const result = await bootstrapHarness(tempDir, []);
80
- expect(result.hooks.listNames()).toEqual([]);
81
- });
82
-
83
69
  it('uses process.cwd() when no project path is given', async () => {
84
- const result = await bootstrapHarness(undefined, []);
70
+ const result = await bootstrapHarness();
85
71
  expect(result).toHaveProperty('projectPath');
86
72
  expect(result.projectPath).toBeDefined();
87
73
  });
@@ -122,10 +108,9 @@ describe('bootstrapHarnessSync', () => {
122
108
  const result = bootstrapHarnessSync(tempDir);
123
109
  expect(result).toHaveProperty('checker');
124
110
  expect(result).toHaveProperty('sessions');
125
- expect(result).toHaveProperty('hooks');
126
- expect(result).toHaveProperty('pipeline');
127
111
  expect(result).toHaveProperty('projectPath', tempDir);
128
112
  expect(result).toHaveProperty('mergedConstraints');
113
+ assertPipelineSurfaceUnreachable(result);
129
114
  });
130
115
 
131
116
  it('initializes with the provided project path', () => {
@@ -140,34 +125,10 @@ describe('bootstrapHarnessSync', () => {
140
125
  expect(result.mergedConstraints).toHaveProperty('guidelines');
141
126
  });
142
127
 
143
- it('registers hook definitions when provided (with configs)', () => {
144
- const hookDef: HookDefinition = {
145
- name: 'test-hook',
146
- phase: 'before',
147
- execute: async () => ({ passed: true }),
148
- };
149
- const result = bootstrapHarnessSync(tempDir, [hookDef], [makeHookConfig('test-hook')]);
150
- expect(result.hooks.listNames()).toContain('test-hook');
151
- });
152
-
153
- it('throws when definitions are provided without configs (#159)', () => {
154
- const hookDef: HookDefinition = {
155
- name: 'test-hook',
156
- phase: 'before',
157
- execute: async () => ({ passed: true }),
158
- };
159
- expect(() => bootstrapHarnessSync(tempDir, [hookDef])).toThrow(/HookConfig/);
160
- });
161
-
162
- it('does not register hooks when no definitions provided', () => {
163
- const result = bootstrapHarnessSync(tempDir, []);
164
- expect(result.hooks.listNames()).toEqual([]);
165
- });
166
-
167
128
  it('uses process.cwd() when no project path is given', () => {
168
129
  // We cannot easily test process.cwd() fallback without mocking cwd,
169
130
  // but we can verify the function accepts undefined
170
- const result = bootstrapHarnessSync(undefined, []);
131
+ const result = bootstrapHarnessSync(undefined);
171
132
  // Should not throw with process.cwd() — harness directory may not exist
172
133
  expect(result).toHaveProperty('projectPath');
173
134
  expect(result.projectPath).toBeDefined();
@@ -182,16 +143,6 @@ describe('bootstrapHarnessSync', () => {
182
143
  expect(fs.existsSync(sessionDir)).toBe(true);
183
144
  });
184
145
 
185
- it('creates a working HookPipeline', () => {
186
- const hookDef: HookDefinition = {
187
- name: 'pipeline-hook',
188
- phase: 'after',
189
- execute: async () => ({ passed: true, data: 'ok' }),
190
- };
191
- const result = bootstrapHarnessSync(tempDir, [hookDef], [makeHookConfig('pipeline-hook')]);
192
- expect(result.pipeline).toBeDefined();
193
- });
194
-
195
146
  it('loads from .harness/config.yml when present', () => {
196
147
  // Write a custom config with specific settings
197
148
  const harnessDir = path.join(tempDir, '.harness');
@@ -8,21 +8,22 @@
8
8
  * Consumer 用法:
9
9
  * ```typescript
10
10
  * const harness = await bootstrapHarness('/path/to/project');
11
- * // harness.checker, harness.config, harness.hooks, harness.sessions
11
+ * // harness.checker, harness.sessions, harness.mergedConstraints
12
12
  * ```
13
13
  *
14
14
  * checker 的 trace 记录器在此接线(harness#88:core 不上行依赖 monitoring),
15
15
  * 且锚在本函数收到的 projectPath 上(#139:落点跟根走,不落调用方 cwd)。
16
+ *
17
+ * ADR-0027(#170):hooks 管线面(registry/pipeline/config/types)双仓零生产消费者,
18
+ * 整体删除;本层的 `hookDefinitions` / `hookConfigs` 两参数与 `hooks` / `pipeline`
19
+ * 两字段随之消失——生产唯一调用方本就无参调用、不访问被删字段。
16
20
  */
17
21
 
18
22
  import { ConstraintChecker } from '../core/constraints/checker';
19
23
  import { SessionManager } from '../context/session-manager';
20
24
  import { ProjectConfigLoader } from '../core/project-config-loader';
21
25
  import { TraceCollector } from '../monitoring/traces';
22
- import { HookRegistry } from './registry';
23
- import { HookPipeline } from './pipeline';
24
26
  import type { MergedConstraintsConfig } from '../types/project-config';
25
- import type { HookConfig, HookDefinition } from './types';
26
27
 
27
28
  /**
28
29
  * 异步加载项目配置(S9:异步 I/O)
@@ -50,46 +51,19 @@ export interface HarnessBootstrap {
50
51
  checker: ConstraintChecker;
51
52
  /** 会话管理器 */
52
53
  sessions: SessionManager;
53
- /** Hook 注册表 */
54
- hooks: HookRegistry;
55
- /** Hook 管线 */
56
- pipeline: HookPipeline;
57
54
  /** 项目路径 */
58
55
  projectPath: string;
59
56
  /** 合并后的约束配置 */
60
57
  mergedConstraints: MergedConstraintsConfig;
61
58
  }
62
59
 
63
- /**
64
- * 注册初始 hook(定义必须配对配置声明表——#159 起 HookConfig 是
65
- * enabled / errorStrategy 的唯一声明点,缺表即抛错)
66
- */
67
- function registerInitialHooks(
68
- hooks: HookRegistry,
69
- hookDefinitions: HookDefinition[] | undefined,
70
- hookConfigs: HookConfig[] | undefined
71
- ): void {
72
- if (!hookDefinitions || hookDefinitions.length === 0) return;
73
- if (!hookConfigs) {
74
- throw new Error(
75
- '[harness] bootstrap 失败:注册 hook 定义必须同时提供 HookConfig 声明表' +
76
- '(enabled / errorStrategy 的唯一声明点)。'
77
- );
78
- }
79
- hooks.registerAll(hookDefinitions, hookConfigs);
80
- }
81
-
82
60
  /**
83
61
  * 初始化 harness 运行环境(异步,不阻塞事件循环)
84
62
  *
85
63
  * @param projectPath 项目根路径
86
- * @param hookDefinitions 可选,初始化时注册的 hook
87
- * @param hookConfigs 可选,与 hookDefinitions 配对的配置声明表(注册定义时必填)
88
64
  */
89
65
  export async function bootstrapHarness(
90
- projectPath?: string,
91
- hookDefinitions?: HookDefinition[],
92
- hookConfigs?: HookConfig[]
66
+ projectPath?: string
93
67
  ): Promise<HarnessBootstrap> {
94
68
  const resolvedPath = projectPath || process.cwd();
95
69
 
@@ -101,17 +75,10 @@ export async function bootstrapHarness(
101
75
  const checker = new ConstraintChecker(new TraceCollector({ projectPath: resolvedPath }));
102
76
 
103
77
  const sessions = new SessionManager(resolvedPath);
104
- const hooks = new HookRegistry();
105
- const pipeline = new HookPipeline(hooks);
106
-
107
- // 3. 注册初始 hook
108
- registerInitialHooks(hooks, hookDefinitions, hookConfigs);
109
78
 
110
79
  return {
111
80
  checker,
112
81
  sessions,
113
- hooks,
114
- pipeline,
115
82
  projectPath: resolvedPath,
116
83
  mergedConstraints,
117
84
  };
@@ -121,11 +88,11 @@ export async function bootstrapHarness(
121
88
  * 同步 bootstrap(兼容不支持 top-level await 的环境)
122
89
  *
123
90
  * 配置加载仍为同步(readFileSync),其他组件初始化同上。
91
+ *
92
+ * @param projectPath 项目根路径
124
93
  */
125
94
  export function bootstrapHarnessSync(
126
- projectPath?: string,
127
- hookDefinitions?: HookDefinition[],
128
- hookConfigs?: HookConfig[]
95
+ projectPath?: string
129
96
  ): HarnessBootstrap {
130
97
  const resolvedPath = projectPath || process.cwd();
131
98
 
@@ -136,16 +103,10 @@ export function bootstrapHarnessSync(
136
103
  const checker = new ConstraintChecker(new TraceCollector({ projectPath: resolvedPath }));
137
104
 
138
105
  const sessions = new SessionManager(resolvedPath);
139
- const hooks = new HookRegistry();
140
- const pipeline = new HookPipeline(hooks);
141
-
142
- registerInitialHooks(hooks, hookDefinitions, hookConfigs);
143
106
 
144
107
  return {
145
108
  checker,
146
109
  sessions,
147
- hooks,
148
- pipeline,
149
110
  projectPath: resolvedPath,
150
111
  mergedConstraints,
151
112
  };
@@ -1,25 +1,10 @@
1
1
  /**
2
2
  * Hooks 模块
3
3
  *
4
- * 通用 hook 管线:注册 排序 → 错误隔离 → 采样执行。
5
- * 无业务逻辑,consumer 自行定义 hook 名称和语义。
4
+ * ADR-0027(#170)起本层只剩 `bootstrap.ts` 一个面:通用 hook 管线
5
+ * (registry / pipeline / config / types 四文件)双仓零生产消费者,整体删除。
6
+ * 目录名沿用历史,职责是 harness 运行环境的组合根。
6
7
  */
7
8
 
8
- export type {
9
- HookPhase,
10
- HookErrorStrategy,
11
- HookConfig,
12
- HookDefinition,
13
- EffectiveHook,
14
- HookResult,
15
- HookExecutionRecord,
16
- PipelineResult,
17
- } from './types';
18
- export { HookRegistry, assertHookRegistryClosed } from './registry';
19
- export { HookPipeline } from './pipeline';
20
- export { toErrorStrategy } from './config';
21
- export {
22
- bootstrapHarness,
23
- bootstrapHarnessSync,
24
- type HarnessBootstrap,
25
- } from './bootstrap';
9
+ export { bootstrapHarness, bootstrapHarnessSync } from './bootstrap';
10
+ export type { HarnessBootstrap } from './bootstrap';
package/src/index.ts CHANGED
@@ -342,25 +342,13 @@ export { getCriticalArtifacts, verifyReleaseArtifacts } from './release';
342
342
  export type { ArtifactIntegrityResult } from './release';
343
343
 
344
344
  // ========================================
345
- // Hooks 管线
345
+ // Harness 运行环境引导(bootstrap 单面;hooks 管线面已随 ADR-0027 删除)
346
346
  // ========================================
347
347
  export {
348
- HookRegistry,
349
- assertHookRegistryClosed,
350
- HookPipeline,
351
- toErrorStrategy,
352
348
  bootstrapHarness,
353
349
  bootstrapHarnessSync,
354
350
  } from './hooks';
355
351
  export type {
356
- HookDefinition,
357
- HookConfig,
358
- EffectiveHook,
359
- HookErrorStrategy,
360
- HookExecutionRecord,
361
- HookPhase,
362
- HookResult,
363
- PipelineResult,
364
352
  HarnessBootstrap,
365
353
  } from './hooks';
366
354