@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.
- package/CHANGELOG.md +10 -0
- package/README.md +1 -1
- package/dist/cli/commands/sync-docs/capabilities-syncer.d.ts +22 -0
- package/dist/cli/commands/sync-docs/capabilities-syncer.d.ts.map +1 -1
- package/dist/cli/commands/sync-docs/capabilities-syncer.js +113 -8
- package/dist/cli/commands/sync-docs/capabilities-syncer.js.map +1 -1
- package/dist/cli/commands/sync-docs/index.d.ts.map +1 -1
- package/dist/cli/commands/sync-docs/index.js +36 -4
- package/dist/cli/commands/sync-docs/index.js.map +1 -1
- package/dist/hooks/bootstrap.d.ts +9 -12
- package/dist/hooks/bootstrap.d.ts.map +1 -1
- package/dist/hooks/bootstrap.js +9 -31
- package/dist/hooks/bootstrap.js.map +1 -1
- package/dist/hooks/index.d.ts +5 -7
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +4 -10
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/CONTEXT.md +1 -1
- package/src/__tests__/public-exports.test.ts +30 -4
- package/src/__tests__/public-type-surface.test.ts +0 -8
- package/src/cli/commands/CONTEXT.md +3 -0
- package/src/cli/commands/__tests__/registry.test.ts +39 -3
- package/src/cli/commands/__tests__/sync-docs-table-layout.test.ts +259 -0
- package/src/cli/commands/sync-docs/capabilities-syncer.ts +139 -9
- package/src/cli/commands/sync-docs/index.ts +41 -4
- package/src/hooks/CONTEXT.md +12 -22
- package/src/hooks/__tests__/bootstrap.test.ts +21 -70
- package/src/hooks/bootstrap.ts +9 -48
- package/src/hooks/index.ts +5 -20
- package/src/index.ts +1 -13
- package/dist/hooks/config.d.ts +0 -30
- package/dist/hooks/config.d.ts.map +0 -1
- package/dist/hooks/config.js +0 -34
- package/dist/hooks/config.js.map +0 -1
- package/dist/hooks/pipeline.d.ts +0 -54
- package/dist/hooks/pipeline.d.ts.map +0 -1
- package/dist/hooks/pipeline.js +0 -173
- package/dist/hooks/pipeline.js.map +0 -1
- package/dist/hooks/registry.d.ts +0 -70
- package/dist/hooks/registry.d.ts.map +0 -1
- package/dist/hooks/registry.js +0 -141
- package/dist/hooks/registry.js.map +0 -1
- package/dist/hooks/types.d.ts +0 -113
- package/dist/hooks/types.d.ts.map +0 -1
- package/dist/hooks/types.js +0 -9
- package/dist/hooks/types.js.map +0 -1
- package/src/__tests__/hooks-pipeline.test.ts +0 -201
- package/src/hooks/__tests__/config.test.ts +0 -19
- package/src/hooks/__tests__/pipeline.test.ts +0 -195
- package/src/hooks/__tests__/registry.test.ts +0 -115
- package/src/hooks/config.ts +0 -33
- package/src/hooks/pipeline.ts +0 -202
- package/src/hooks/registry.ts +0 -166
- 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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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 &&
|
|
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
|
-
:
|
|
492
|
+
? `文档不是最新的(CONTEXT.md 与实现漂移:${contextDriftReason})${tableLayoutReason}`
|
|
493
|
+
: `文档不是最新的,请运行 harness sync-docs 更新${tableLayoutReason}`
|
|
457
494
|
);
|
|
458
495
|
}
|
|
459
496
|
|
package/src/hooks/CONTEXT.md
CHANGED
|
@@ -1,34 +1,24 @@
|
|
|
1
1
|
# hooks/
|
|
2
2
|
|
|
3
3
|
## 职责
|
|
4
|
-
|
|
4
|
+
Harness 运行环境的组合根:一次调用装配起约束检查器、会话管理器与 trace 记录器,并加载项目配置。
|
|
5
5
|
|
|
6
|
-
|
|
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
|
-
- `
|
|
12
|
-
- `
|
|
13
|
-
- `
|
|
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
|
-
-
|
|
21
|
-
-
|
|
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
|
-
-
|
|
27
|
-
-
|
|
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
|
-
-
|
|
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(
|
|
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');
|
package/src/hooks/bootstrap.ts
CHANGED
|
@@ -8,21 +8,22 @@
|
|
|
8
8
|
* Consumer 用法:
|
|
9
9
|
* ```typescript
|
|
10
10
|
* const harness = await bootstrapHarness('/path/to/project');
|
|
11
|
-
* // harness.checker, harness.
|
|
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
|
};
|
package/src/hooks/index.ts
CHANGED
|
@@ -1,25 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Hooks 模块
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* ADR-0027(#170)起本层只剩 `bootstrap.ts` 一个面:通用 hook 管线
|
|
5
|
+
* (registry / pipeline / config / types 四文件)双仓零生产消费者,整体删除。
|
|
6
|
+
* 目录名沿用历史,职责是 harness 运行环境的组合根。
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
|
-
export
|
|
9
|
-
|
|
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
|
-
//
|
|
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
|
|