@dommaker/harness 1.8.0 → 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 (79) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/README.md +1 -1
  3. package/dist/cli/commands/check.d.ts +8 -0
  4. package/dist/cli/commands/check.d.ts.map +1 -1
  5. package/dist/cli/commands/check.js +10 -11
  6. package/dist/cli/commands/check.js.map +1 -1
  7. package/dist/cli/commands/status.d.ts +8 -0
  8. package/dist/cli/commands/status.d.ts.map +1 -1
  9. package/dist/cli/commands/status.js +5 -7
  10. package/dist/cli/commands/status.js.map +1 -1
  11. package/dist/cli/commands/sync-docs/capabilities-syncer.d.ts +22 -0
  12. package/dist/cli/commands/sync-docs/capabilities-syncer.d.ts.map +1 -1
  13. package/dist/cli/commands/sync-docs/capabilities-syncer.js +113 -8
  14. package/dist/cli/commands/sync-docs/capabilities-syncer.js.map +1 -1
  15. package/dist/cli/commands/sync-docs/index.d.ts.map +1 -1
  16. package/dist/cli/commands/sync-docs/index.js +36 -4
  17. package/dist/cli/commands/sync-docs/index.js.map +1 -1
  18. package/dist/cli/state-io.d.ts +34 -0
  19. package/dist/cli/state-io.d.ts.map +1 -0
  20. package/dist/cli/state-io.js +71 -0
  21. package/dist/cli/state-io.js.map +1 -0
  22. package/dist/hooks/bootstrap.d.ts +9 -12
  23. package/dist/hooks/bootstrap.d.ts.map +1 -1
  24. package/dist/hooks/bootstrap.js +9 -31
  25. package/dist/hooks/bootstrap.js.map +1 -1
  26. package/dist/hooks/index.d.ts +5 -7
  27. package/dist/hooks/index.d.ts.map +1 -1
  28. package/dist/hooks/index.js +4 -10
  29. package/dist/hooks/index.js.map +1 -1
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +3 -7
  33. package/dist/index.js.map +1 -1
  34. package/package.json +1 -1
  35. package/src/CONTEXT.md +2 -2
  36. package/src/__tests__/public-exports.test.ts +30 -4
  37. package/src/__tests__/public-type-surface.test.ts +0 -8
  38. package/src/cli/__tests__/state-io.test.ts +50 -0
  39. package/src/cli/commands/CONTEXT.md +4 -0
  40. package/src/cli/commands/__tests__/check-read-count.test.ts +3 -0
  41. package/src/cli/commands/__tests__/check.test.ts +20 -8
  42. package/src/cli/commands/__tests__/registry.test.ts +39 -3
  43. package/src/cli/commands/__tests__/status-extra.test.ts +25 -8
  44. package/src/cli/commands/__tests__/status.test.ts +46 -18
  45. package/src/cli/commands/__tests__/sync-docs-table-layout.test.ts +259 -0
  46. package/src/cli/commands/check.ts +20 -18
  47. package/src/cli/commands/status.ts +12 -7
  48. package/src/cli/commands/sync-docs/capabilities-syncer.ts +139 -9
  49. package/src/cli/commands/sync-docs/index.ts +41 -4
  50. package/src/cli/state-io.ts +53 -0
  51. package/src/hooks/CONTEXT.md +12 -22
  52. package/src/hooks/__tests__/bootstrap.test.ts +21 -70
  53. package/src/hooks/bootstrap.ts +9 -48
  54. package/src/hooks/index.ts +5 -20
  55. package/src/index.ts +1 -13
  56. package/dist/hooks/config.d.ts +0 -30
  57. package/dist/hooks/config.d.ts.map +0 -1
  58. package/dist/hooks/config.js +0 -34
  59. package/dist/hooks/config.js.map +0 -1
  60. package/dist/hooks/pipeline.d.ts +0 -36
  61. package/dist/hooks/pipeline.d.ts.map +0 -1
  62. package/dist/hooks/pipeline.js +0 -133
  63. package/dist/hooks/pipeline.js.map +0 -1
  64. package/dist/hooks/registry.d.ts +0 -70
  65. package/dist/hooks/registry.d.ts.map +0 -1
  66. package/dist/hooks/registry.js +0 -141
  67. package/dist/hooks/registry.js.map +0 -1
  68. package/dist/hooks/types.d.ts +0 -111
  69. package/dist/hooks/types.d.ts.map +0 -1
  70. package/dist/hooks/types.js +0 -9
  71. package/dist/hooks/types.js.map +0 -1
  72. package/src/__tests__/hooks-pipeline.test.ts +0 -201
  73. package/src/hooks/__tests__/config.test.ts +0 -19
  74. package/src/hooks/__tests__/pipeline.test.ts +0 -90
  75. package/src/hooks/__tests__/registry.test.ts +0 -115
  76. package/src/hooks/config.ts +0 -33
  77. package/src/hooks/pipeline.ts +0 -155
  78. package/src/hooks/registry.ts +0 -166
  79. package/src/hooks/types.ts +0 -118
@@ -1,201 +0,0 @@
1
- /**
2
- * HookRegistry + HookPipeline 测试(Phase 1)
3
- *
4
- * #159 起 `enabled` / `errorStrategy` 由 HookConfig 唯一声明,
5
- * 注册时以配置填充有效值;HookDefinition 不再携带这两个字段。
6
- */
7
- import { describe, it, expect, beforeEach } from '@jest/globals';
8
- import { HookRegistry } from '../hooks/registry';
9
- import { HookPipeline } from '../hooks/pipeline';
10
- import type { HookConfig, HookDefinition } from '../hooks/types';
11
-
12
- interface TestContext {
13
- value: string;
14
- calls: string[];
15
- }
16
-
17
- function makeHook(
18
- name: string,
19
- opts: Partial<HookDefinition<TestContext, unknown>> = {}
20
- ): HookDefinition<TestContext, unknown> {
21
- return {
22
- name,
23
- phase: 'before',
24
- priority: 100,
25
- execute: async (ctx) => {
26
- ctx.calls.push(name);
27
- return { passed: true };
28
- },
29
- ...opts,
30
- };
31
- }
32
-
33
- function makeConfig(name: string, overrides: Partial<HookConfig> = {}): HookConfig {
34
- return { name, enabled: true, errorStrategy: 'block', ...overrides };
35
- }
36
-
37
- describe('HookRegistry', () => {
38
- let registry: HookRegistry<TestContext, unknown>;
39
-
40
- beforeEach(() => {
41
- registry = new HookRegistry();
42
- });
43
-
44
- it('应注册 hook', () => {
45
- registry.register(makeHook('test'), makeConfig('test'));
46
- expect(registry.get('test')).toBeDefined();
47
- });
48
-
49
- it('同名注册应覆盖', () => {
50
- registry.register(makeHook('test', { priority: 50 }), makeConfig('test'));
51
- registry.register(makeHook('test', { priority: 90 }), makeConfig('test'));
52
- expect(registry.get('test')?.priority).toBe(90);
53
- });
54
-
55
- it('应获取已启用的 hook 按优先级排序', () => {
56
- registry.register(makeHook('low', { priority: 200 }), makeConfig('low'));
57
- registry.register(makeHook('high', { priority: 10 }), makeConfig('high'));
58
- registry.register(makeHook('mid', { priority: 100 }), makeConfig('mid'));
59
- registry.register(makeHook('disabled'), makeConfig('disabled', { enabled: false }));
60
-
61
- const enabled = registry.getEnabled('before');
62
- expect(enabled.map(h => h.name)).toEqual(['high', 'mid', 'low']);
63
- });
64
-
65
- it('应注销 hook', () => {
66
- registry.register(makeHook('test'), makeConfig('test'));
67
- expect(registry.unregister('test')).toBe(true);
68
- expect(registry.get('test')).toBeUndefined();
69
- });
70
-
71
- it('应设置启用/禁用', () => {
72
- registry.register(makeHook('test'), makeConfig('test'));
73
- expect(registry.setEnabled('test', false)).toBe(true);
74
- expect(registry.getEnabled('before').length).toBe(0);
75
- expect(registry.setEnabled('test', true)).toBe(true);
76
- expect(registry.getEnabled('before').length).toBe(1);
77
- });
78
-
79
- it('应正确统计数量', () => {
80
- expect(registry.size).toBe(0);
81
- registry.register(makeHook('a'), makeConfig('a'));
82
- registry.register(makeHook('b'), makeConfig('b'));
83
- expect(registry.size).toBe(2);
84
- });
85
- });
86
-
87
- describe('HookPipeline', () => {
88
- let registry: HookRegistry<TestContext, unknown>;
89
- let pipeline: HookPipeline<TestContext>;
90
-
91
- beforeEach(() => {
92
- registry = new HookRegistry();
93
- pipeline = new HookPipeline(registry);
94
- });
95
-
96
- it('应执行所有 before hook', async () => {
97
- const ctx: TestContext = { value: '', calls: [] };
98
- registry.register(makeHook('h1'), makeConfig('h1'));
99
- registry.register(makeHook('h2'), makeConfig('h2'));
100
-
101
- const result = await pipeline.run('before', ctx);
102
-
103
- expect(result.passed).toBe(true);
104
- expect(ctx.calls).toEqual(['h1', 'h2']);
105
- expect(result.records.length).toBe(2);
106
- });
107
-
108
- it('blocking hook 失败应停止后续', async () => {
109
- const ctx: TestContext = { value: '', calls: [] };
110
- registry.register(makeHook('pass1'), makeConfig('pass1'));
111
- registry.register(makeHook('block', {
112
- execute: async () => ({ passed: false, error: 'blocked' }),
113
- }), makeConfig('block', { errorStrategy: 'block' }));
114
- registry.register(makeHook('never_runs'), makeConfig('never_runs'));
115
-
116
- const result = await pipeline.run('before', ctx);
117
-
118
- expect(result.passed).toBe(false);
119
- expect(result.blockedBy).toEqual(['block']);
120
- // block hook 的 execute 被覆盖,不 push calls
121
- expect(ctx.calls).toEqual(['pass1']);
122
- expect(ctx.calls).not.toContain('never_runs');
123
- });
124
-
125
- it('warn hook 失败应继续但记录警告', async () => {
126
- const ctx: TestContext = { value: '', calls: [] };
127
- registry.register(makeHook('warn_hook', {
128
- execute: async () => ({ passed: false, error: 'warning' }),
129
- }), makeConfig('warn_hook', { errorStrategy: 'warn' }));
130
- registry.register(makeHook('should_still_run'), makeConfig('should_still_run'));
131
-
132
- const result = await pipeline.run('before', ctx);
133
-
134
- expect(result.passed).toBe(true);
135
- expect(result.warnings).toEqual(['warn_hook']);
136
- expect(ctx.calls).toContain('should_still_run');
137
- });
138
-
139
- it('hook 抛异常应被捕获', async () => {
140
- const ctx: TestContext = { value: '', calls: [] };
141
- registry.register(makeHook('throws', {
142
- execute: async () => { throw new Error('crash'); },
143
- }), makeConfig('throws', { errorStrategy: 'warn' }));
144
- registry.register(makeHook('after_crash', { priority: 200 }), makeConfig('after_crash'));
145
-
146
- const result = await pipeline.run('before', ctx);
147
-
148
- expect(result.records[0].error).toContain('crash');
149
- expect(ctx.calls).toContain('after_crash');
150
- });
151
-
152
- it('runFull 应在 before/after 之间执行操作', async () => {
153
- const ctx: TestContext = { value: '', calls: [] };
154
- registry.register(makeHook('before_hook'), makeConfig('before_hook'));
155
-
156
- const afterHook = makeHook('after_hook', { phase: 'after' });
157
- registry.register(afterHook, makeConfig('after_hook'));
158
-
159
- const { pipelineResult, operationResult } = await pipeline.runFull(ctx, async () => {
160
- ctx.calls.push('operation');
161
- return 'done';
162
- });
163
-
164
- expect(pipelineResult.passed).toBe(true);
165
- expect(operationResult).toBe('done');
166
- expect(ctx.calls).toEqual(['before_hook', 'operation', 'after_hook']);
167
- });
168
-
169
- it('before hook 失败不应执行操作', async () => {
170
- const ctx: TestContext = { value: '', calls: [] };
171
- registry.register(makeHook('block', {
172
- execute: async () => ({ passed: false }),
173
- }), makeConfig('block', { errorStrategy: 'block' }));
174
-
175
- const { pipelineResult, operationResult } = await pipeline.runFull(ctx, async () => {
176
- ctx.calls.push('should_not_run');
177
- return 'nope';
178
- });
179
-
180
- expect(pipelineResult.passed).toBe(false);
181
- expect(operationResult).toBeUndefined();
182
- expect(ctx.calls).not.toContain('should_not_run');
183
- });
184
-
185
- it('空注册表应返回通过', async () => {
186
- const result = await pipeline.run('before', { value: '', calls: [] });
187
- expect(result.passed).toBe(true);
188
- expect(result.records).toEqual([]);
189
- });
190
-
191
- it('应忽略配置禁用的 hook', async () => {
192
- const ctx: TestContext = { value: '', calls: [] };
193
- registry.register(makeHook('enabled'), makeConfig('enabled'));
194
- registry.register(makeHook('disabled'), makeConfig('disabled', { enabled: false }));
195
-
196
- await pipeline.run('before', ctx);
197
-
198
- expect(ctx.calls).toEqual(['enabled']);
199
- expect(ctx.calls).not.toContain('disabled');
200
- });
201
- });
@@ -1,19 +0,0 @@
1
- /**
2
- * Hook 配置归一映射语义测试(G7)
3
- *
4
- * blocking → errorStrategy 无损映射:
5
- * - blocking=true → 'block'(失败阻断管线)
6
- * - blocking=false → 'warn'(记录警告继续)
7
- */
8
-
9
- import { toErrorStrategy } from '../config';
10
-
11
- describe('toErrorStrategy(blocking → errorStrategy 无损映射,G7)', () => {
12
- it('blocking=true → block(失败阻断管线)', () => {
13
- expect(toErrorStrategy(true)).toBe('block');
14
- });
15
-
16
- it('blocking=false → warn(记录警告继续)', () => {
17
- expect(toErrorStrategy(false)).toBe('warn');
18
- });
19
- });
@@ -1,90 +0,0 @@
1
- /**
2
- * HookPipeline errorStrategy 语义测试(G7 映射语义确认)
3
- *
4
- * block/warn 行为是 studio safeCallHook 语义在管线侧的无损承载面:
5
- * - 'block':hook 失败 → passed=false、blockedBy 记名、停止执行后续 hook
6
- * - 'warn':hook 失败 → 记录警告、继续执行后续 hook、passed 不受影响
7
- *
8
- * #159 起 `enabled` / `errorStrategy` 的唯一声明点是 HookConfig:
9
- * HookDefinition 不再携带这两个字段,有效值在注册环节由配置表填充。
10
- */
11
-
12
- import { HookPipeline } from '../pipeline';
13
- import { HookRegistry } from '../registry';
14
- import type { HookConfig, HookDefinition } from '../types';
15
-
16
- function makeConfig(name: string, overrides: Partial<HookConfig> = {}): HookConfig {
17
- return { name, enabled: true, errorStrategy: 'warn', ...overrides };
18
- }
19
-
20
- function makeRegistry(
21
- entries: Array<{ hook: HookDefinition; config?: Partial<HookConfig> }>
22
- ): { registry: HookRegistry; pipeline: HookPipeline } {
23
- const registry = new HookRegistry();
24
- for (const { hook, config } of entries) {
25
- registry.register(hook, makeConfig(hook.name, config));
26
- }
27
- return { registry, pipeline: new HookPipeline(registry) };
28
- }
29
-
30
- describe('HookPipeline errorStrategy 语义', () => {
31
- it('block:失败阻断管线(passed=false、blockedBy 记名、停止后续 hook)', async () => {
32
- const second = jest.fn().mockResolvedValue({ passed: true });
33
- const { pipeline } = makeRegistry([
34
- { hook: { name: 'first', phase: 'before', execute: async () => ({ passed: false, error: 'boom' }) }, config: { errorStrategy: 'block' } },
35
- { hook: { name: 'second', phase: 'before', execute: second } },
36
- ]);
37
-
38
- const result = await pipeline.run('before', {});
39
-
40
- expect(result.passed).toBe(false);
41
- expect(result.blockedBy).toEqual(['first']);
42
- expect(second).not.toHaveBeenCalled();
43
- });
44
-
45
- it('warn:失败记录警告并继续(passed 不受影响、后续 hook 照常执行)', async () => {
46
- const second = jest.fn().mockResolvedValue({ passed: true });
47
- const { pipeline } = makeRegistry([
48
- { hook: { name: 'first', phase: 'before', execute: async () => ({ passed: false, error: 'soft' }) }, config: { errorStrategy: 'warn' } },
49
- { hook: { name: 'second', phase: 'before', execute: second } },
50
- ]);
51
-
52
- const result = await pipeline.run('before', {});
53
-
54
- expect(result.passed).toBe(true);
55
- expect(result.warnings).toEqual(['first']);
56
- expect(result.blockedBy).toEqual([]);
57
- expect(second).toHaveBeenCalled();
58
- });
59
-
60
- it('block 失败停在当前 phase,不改变其它 phase 的 hook', async () => {
61
- const afterHook = jest.fn().mockResolvedValue({ passed: true });
62
- const { pipeline } = makeRegistry([
63
- { hook: { name: 'blocker', phase: 'before', execute: async () => ({ passed: false }) }, config: { errorStrategy: 'block' } },
64
- { hook: { name: 'afterHook', phase: 'after', execute: afterHook } },
65
- ]);
66
-
67
- const before = await pipeline.run('before', {});
68
- expect(before.passed).toBe(false);
69
-
70
- const after = await pipeline.run('after', {});
71
- expect(after.passed).toBe(true);
72
- expect(afterHook).toHaveBeenCalled();
73
- });
74
-
75
- it('配置 enabled:false 的 hook 不进入管线执行(配置表是唯一声明点)', async () => {
76
- const disabledHook = jest.fn().mockResolvedValue({ passed: true });
77
- const enabledHook = jest.fn().mockResolvedValue({ passed: true });
78
- const { pipeline } = makeRegistry([
79
- { hook: { name: 'disabled', phase: 'before', execute: disabledHook }, config: { enabled: false } },
80
- { hook: { name: 'enabled', phase: 'before', execute: enabledHook } },
81
- ]);
82
-
83
- const result = await pipeline.run('before', {});
84
-
85
- expect(result.passed).toBe(true);
86
- expect(disabledHook).not.toHaveBeenCalled();
87
- expect(enabledHook).toHaveBeenCalled();
88
- expect(result.records.map(r => r.hookName)).toEqual(['enabled']);
89
- });
90
- });
@@ -1,115 +0,0 @@
1
- /**
2
- * Hook 注册表闭环双向校验测试(H5,复制 checker 闭环模式)
3
- *
4
- * 两个失败方向 + 重复声明/注册都必须抛错;闭环通过时不抛错。
5
- *
6
- * #159 起注册即配对:`register(hook, config)` 以 HookConfig 填充有效
7
- * `enabled` / `errorStrategy`——配置表是这两个语义的唯一声明点,
8
- * HookDefinition 类型上不再存在同名字段(矛盾在构造上不可能出现)。
9
- */
10
-
11
- import { assertHookRegistryClosed, HookRegistry } from '../registry';
12
- import type { HookConfig, HookDefinition } from '../types';
13
-
14
- function makeHook(name: string): HookDefinition {
15
- return {
16
- name,
17
- phase: 'before',
18
- execute: async () => ({ passed: true }),
19
- };
20
- }
21
-
22
- function makeConfig(name: string, overrides: Partial<HookConfig> = {}): HookConfig {
23
- return { name, enabled: true, errorStrategy: 'block', ...overrides };
24
- }
25
-
26
- describe('assertHookRegistryClosed', () => {
27
- it('声明与注册一一对应时通过', () => {
28
- const configs = [makeConfig('beforeAgentExecute'), makeConfig('afterReview')];
29
- const hooks = [makeHook('beforeAgentExecute'), makeHook('afterReview')];
30
- expect(() => assertHookRegistryClosed(configs, hooks)).not.toThrow();
31
- });
32
-
33
- it('空声明 + 空注册通过(无 hook 的 consumer 合法)', () => {
34
- expect(() => assertHookRegistryClosed([], [])).not.toThrow();
35
- });
36
-
37
- it('引用未注册 → 抛错:声明了配置但没有注册实现', () => {
38
- const configs = [makeConfig('beforeAgentExecute'), makeConfig('ghostHook')];
39
- const hooks = [makeHook('beforeAgentExecute')];
40
- expect(() => assertHookRegistryClosed(configs, hooks)).toThrow(
41
- /hook 配置 "ghostHook" 引用的实现未注册/
42
- );
43
- });
44
-
45
- it('注册无定义 → 抛错:注册了实现但没有配置声明', () => {
46
- const configs = [makeConfig('beforeAgentExecute')];
47
- const hooks = [makeHook('beforeAgentExecute'), makeHook('orphanHook')];
48
- expect(() => assertHookRegistryClosed(configs, hooks)).toThrow(
49
- /hook "orphanHook" 注册了实现但没有对应配置声明/
50
- );
51
- });
52
-
53
- it('重复声明 → 抛错', () => {
54
- const configs = [makeConfig('dup'), makeConfig('dup')];
55
- const hooks = [makeHook('dup')];
56
- expect(() => assertHookRegistryClosed(configs, hooks)).toThrow(/重复声明/);
57
- });
58
-
59
- it('重复注册 → 抛错', () => {
60
- const configs = [makeConfig('dup')];
61
- const hooks = [makeHook('dup'), makeHook('dup')];
62
- expect(() => assertHookRegistryClosed(configs, hooks)).toThrow(/重复注册/);
63
- });
64
-
65
- it('与 HookRegistry.listAll 组合可用(consumer 注册点用法)', () => {
66
- const registry = new HookRegistry();
67
- registry.registerAll([makeHook('beforeAgentExecute')], [makeConfig('beforeAgentExecute')]);
68
- expect(() => assertHookRegistryClosed([makeConfig('beforeAgentExecute')], registry.listAll()))
69
- .not.toThrow();
70
- });
71
- });
72
-
73
- describe('HookRegistry 注册即配对(#159 配置表唯一化)', () => {
74
- it('register 以 HookConfig 填充有效 enabled / errorStrategy', () => {
75
- const registry = new HookRegistry();
76
- registry.register(makeHook('a'), makeConfig('a', { enabled: false, errorStrategy: 'block' }));
77
-
78
- const effective = registry.get('a');
79
- expect(effective?.enabled).toBe(false);
80
- expect(effective?.errorStrategy).toBe('block');
81
- // 配置 enabled:false → 不进 enabled 列表
82
- expect(registry.getEnabled('before')).toEqual([]);
83
- });
84
-
85
- it('register 定义与配置名称不一致 → 抛错', () => {
86
- const registry = new HookRegistry();
87
- expect(() => registry.register(makeHook('a'), makeConfig('b'))).toThrow(/名称不一致/);
88
- });
89
-
90
- it('registerAll 有实现缺配置声明 → 抛错', () => {
91
- const registry = new HookRegistry();
92
- expect(() =>
93
- registry.registerAll([makeHook('a'), makeHook('orphan')], [makeConfig('a')])
94
- ).toThrow(/hook "orphan" 没有对应的 HookConfig 声明/);
95
- });
96
-
97
- it('HookDefinition 类型上不再存在 enabled / errorStrategy(编译期钉)', () => {
98
- const withStrategy: HookDefinition = {
99
- name: 'x',
100
- phase: 'before',
101
- // @ts-expect-error HookDefinition 不再存在 errorStrategy 字段(#159)
102
- errorStrategy: 'block',
103
- execute: async () => ({ passed: true }),
104
- };
105
- const withEnabled: HookDefinition = {
106
- name: 'y',
107
- phase: 'before',
108
- // @ts-expect-error HookDefinition 不再存在 enabled 字段(#159)
109
- enabled: false,
110
- execute: async () => ({ passed: true }),
111
- };
112
- expect(withStrategy.name).toBe('x');
113
- expect(withEnabled.name).toBe('y');
114
- });
115
- });
@@ -1,33 +0,0 @@
1
- /**
2
- * Hook 配置归一(G7)
3
- *
4
- * studio 侧 per-hook 运行时配置(`blocking: boolean`)与 harness 侧
5
- * `HookConfig.errorStrategy` 双轨归一:以 errorStrategy 为唯一真相,
6
- * `blocking` → errorStrategy 为无损映射。
7
- *
8
- * 映射语义(确认于 H5):
9
- * - blocking=true → 'block':hook 失败阻断管线(停止执行后续 hook,
10
- * 管线结果 passed=false、blockedBy 记名)——对应 studio safeCallHook 抛错
11
- * - blocking=false → 'warn':hook 失败记录警告并继续执行后续 hook
12
- * (管线结果 warnings 记名,passed 不受影响)——对应 studio safeCallHook
13
- * console.warn 继续
14
- *
15
- * #159 起 `HookConfig` 是 `enabled` / `errorStrategy` 的唯一声明点:
16
- * HookDefinition 不再携带这两个字段,注册环节以配置表填充有效值,
17
- * 有效策略集合收敛为 'block' | 'warn'(原 'ignore' 退出有效面)。
18
- *
19
- * studio 侧随动(studio #150 A4):hooks/config.ts 删 DEFAULTS/safeCallHook,
20
- * blocking→errorStrategy 无损映射进 toHookDef;HARNESS_HOOK_DISABLE 保留 studio。
21
- */
22
-
23
- import type { HookErrorStrategy } from './types';
24
-
25
- /**
26
- * blocking 布尔 → errorStrategy 无损映射(block/warn)
27
- *
28
- * @param blocking studio 侧「失败是否阻断」语义
29
- * @returns 'block'(阻断)| 'warn'(警告继续)
30
- */
31
- export function toErrorStrategy(blocking: boolean): HookErrorStrategy {
32
- return blocking ? 'block' : 'warn';
33
- }
@@ -1,155 +0,0 @@
1
- /**
2
- * HookPipeline — hook 有序执行、错误隔离、采样
3
- *
4
- * 执行顺序:before hooks → consumer operation → after hooks
5
- * 每个 hook 独立 try/catch,单个 hook 失败不影���其他 hook。
6
- */
7
-
8
- import type {
9
- EffectiveHook,
10
- HookPhase,
11
- HookExecutionRecord,
12
- PipelineResult,
13
- } from './types';
14
- import type { HookRegistry } from './registry';
15
-
16
- export class HookPipeline<C = unknown> {
17
- private registry: HookRegistry<C, unknown>;
18
-
19
- constructor(registry: HookRegistry<C, unknown>) {
20
- this.registry = registry;
21
- }
22
-
23
- /**
24
- * 执行指定 phase 的所有 enabled hook
25
- *
26
- * @param phase hook 时机
27
- * @param context 传递给每个 hook 的上下文
28
- * @returns 管线执行结果
29
- */
30
- async run(
31
- phase: HookPhase,
32
- context: C
33
- ): Promise<PipelineResult> {
34
- const hooks = this.registry.getEnabled(phase);
35
- const records: HookExecutionRecord[] = [];
36
- const blockedBy: string[] = [];
37
- const warnings: string[] = [];
38
- let passed = true;
39
-
40
- for (const hook of hooks) {
41
- const record = await this.executeOne(hook, context);
42
- records.push(record);
43
-
44
- if (!record.passed) {
45
- if (hook.errorStrategy === 'block') {
46
- passed = false;
47
- blockedBy.push(hook.name);
48
- break; // blocking hook 失败,停止执行后续 hook
49
- }
50
- // 'warn':记录警告继续(有效集合仅 block/warn,策略由 HookConfig 必填声明)
51
- warnings.push(hook.name);
52
- }
53
- }
54
-
55
- return { passed, records, blockedBy, warnings };
56
- }
57
-
58
- /**
59
- * 执行 before + after 全套管线
60
- *
61
- * @param context 上下文
62
- * @param operation 业务操作(在 before 和 after 之间执行)
63
- * @returns 管线结果 + 操作结果
64
- */
65
- async runFull<R>(
66
- context: C,
67
- operation: () => Promise<R>
68
- ): Promise<{ pipelineResult: PipelineResult; operationResult?: R }> {
69
- // 1. Before hooks
70
- const beforeResult = await this.run('before', context);
71
- if (!beforeResult.passed) {
72
- return { pipelineResult: beforeResult };
73
- }
74
-
75
- // 2. Execute operation
76
- let operationResult: R;
77
- try {
78
- operationResult = await operation();
79
- } catch (err) {
80
- // 操作失败:仍执行 after hooks,但传递错误信息
81
- const afterResult = await this.run('after', {
82
- ...context,
83
- _operationError: (err as Error).message,
84
- } as unknown as C);
85
- return {
86
- pipelineResult: {
87
- passed: false,
88
- records: [...beforeResult.records, ...afterResult.records],
89
- blockedBy: [...beforeResult.blockedBy, '_operation'],
90
- warnings: [...beforeResult.warnings, ...afterResult.warnings],
91
- },
92
- };
93
- }
94
-
95
- // 3. After hooks
96
- const afterResult = await this.run('after', context);
97
- return {
98
- pipelineResult: {
99
- passed: beforeResult.passed && afterResult.passed,
100
- records: [...beforeResult.records, ...afterResult.records],
101
- blockedBy: [...beforeResult.blockedBy, ...afterResult.blockedBy],
102
- warnings: [...beforeResult.warnings, ...afterResult.warnings],
103
- },
104
- operationResult,
105
- };
106
- }
107
-
108
- /**
109
- * 执行单个 hook(带采样和错误隔离)
110
- */
111
- private async executeOne(
112
- hook: EffectiveHook<C, unknown>,
113
- context: C
114
- ): Promise<HookExecutionRecord> {
115
- const startedAt = Date.now();
116
-
117
- // 采样检查
118
- if (hook.sampleRate !== undefined && hook.sampleRate < 1) {
119
- if (Math.random() > hook.sampleRate) {
120
- return {
121
- hookName: hook.name,
122
- phase: hook.phase,
123
- startedAt,
124
- completedAt: Date.now(),
125
- durationMs: 0,
126
- passed: true,
127
- sampled: true,
128
- };
129
- }
130
- }
131
-
132
- try {
133
- const result = await hook.execute(context);
134
- return {
135
- hookName: hook.name,
136
- phase: hook.phase,
137
- startedAt,
138
- completedAt: Date.now(),
139
- durationMs: Date.now() - startedAt,
140
- passed: result.passed !== false,
141
- error: result.error,
142
- };
143
- } catch (err) {
144
- return {
145
- hookName: hook.name,
146
- phase: hook.phase,
147
- startedAt,
148
- completedAt: Date.now(),
149
- durationMs: Date.now() - startedAt,
150
- passed: false,
151
- error: (err as Error).message,
152
- };
153
- }
154
- }
155
- }