@mmnto/cli 1.35.0 → 1.36.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 (55) hide show
  1. package/dist/commands/hook-run.d.ts +91 -0
  2. package/dist/commands/hook-run.d.ts.map +1 -0
  3. package/dist/commands/hook-run.js +149 -0
  4. package/dist/commands/hook-run.js.map +1 -0
  5. package/dist/commands/hook-run.test.d.ts +2 -0
  6. package/dist/commands/hook-run.test.d.ts.map +1 -0
  7. package/dist/commands/hook-run.test.js +264 -0
  8. package/dist/commands/hook-run.test.js.map +1 -0
  9. package/dist/commands/hook-test.d.ts +29 -0
  10. package/dist/commands/hook-test.d.ts.map +1 -0
  11. package/dist/commands/hook-test.js +132 -0
  12. package/dist/commands/hook-test.js.map +1 -0
  13. package/dist/hook/classification.d.ts +45 -0
  14. package/dist/hook/classification.d.ts.map +1 -0
  15. package/dist/hook/classification.js +24 -0
  16. package/dist/hook/classification.js.map +1 -0
  17. package/dist/hook/classification.test.d.ts +2 -0
  18. package/dist/hook/classification.test.d.ts.map +1 -0
  19. package/dist/hook/classification.test.js +40 -0
  20. package/dist/hook/classification.test.js.map +1 -0
  21. package/dist/hook/loader.d.ts +47 -0
  22. package/dist/hook/loader.d.ts.map +1 -0
  23. package/dist/hook/loader.js +66 -0
  24. package/dist/hook/loader.js.map +1 -0
  25. package/dist/hook/loader.test.d.ts +2 -0
  26. package/dist/hook/loader.test.d.ts.map +1 -0
  27. package/dist/hook/loader.test.js +205 -0
  28. package/dist/hook/loader.test.js.map +1 -0
  29. package/dist/hook/runtime.d.ts +47 -0
  30. package/dist/hook/runtime.d.ts.map +1 -0
  31. package/dist/hook/runtime.js +85 -0
  32. package/dist/hook/runtime.js.map +1 -0
  33. package/dist/hook/runtime.test.d.ts +2 -0
  34. package/dist/hook/runtime.test.d.ts.map +1 -0
  35. package/dist/hook/runtime.test.js +135 -0
  36. package/dist/hook/runtime.test.js.map +1 -0
  37. package/dist/hook/schema.d.ts +385 -0
  38. package/dist/hook/schema.d.ts.map +1 -0
  39. package/dist/hook/schema.js +164 -0
  40. package/dist/hook/schema.js.map +1 -0
  41. package/dist/hook/schema.test.d.ts +2 -0
  42. package/dist/hook/schema.test.d.ts.map +1 -0
  43. package/dist/hook/schema.test.js +233 -0
  44. package/dist/hook/schema.test.js.map +1 -0
  45. package/dist/hook/test-runner.d.ts +64 -0
  46. package/dist/hook/test-runner.d.ts.map +1 -0
  47. package/dist/hook/test-runner.js +57 -0
  48. package/dist/hook/test-runner.js.map +1 -0
  49. package/dist/hook/test-runner.test.d.ts +2 -0
  50. package/dist/hook/test-runner.test.d.ts.map +1 -0
  51. package/dist/hook/test-runner.test.js +237 -0
  52. package/dist/hook/test-runner.test.js.map +1 -0
  53. package/dist/index.js +57 -4
  54. package/dist/index.js.map +1 -1
  55. package/package.json +2 -2
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/hook/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;;;;;;;GAUG;AAEH,QAAA,MAAM,mBAAmB,sDAAoD,CAAC;AAE9E,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AA4EhE;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAOzB,CAAC;AAEH,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAEtD,eAAO,MAAM,yBAAyB,EAAG,CAAU,CAAC;AAEpD;;;;;GAKG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAqBxB,CAAC;AAEL,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAExD,eAAO,MAAM,6BAA6B,EAAG,CAAU,CAAC;AAExD;;;;GAIG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAEjC,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE;;;;;;;;;;GAUG;AACH,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAKtC,CAAC;AAEH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC"}
@@ -0,0 +1,164 @@
1
+ import { z } from 'zod';
2
+ import { isRegexSafe } from '@mmnto/totem';
3
+ /**
4
+ * Hook rule schemas for the bot-pack wiring engine (ADR-104).
5
+ *
6
+ * Two surfaces:
7
+ * - `HooksYamlSchema` describes a pack's `hooks.yaml` (authoring surface).
8
+ * - `CompiledHooksManifestSchema` describes `.totem/compiled-hooks.json`
9
+ * (runtime surface produced by `totem sync` from installed packs).
10
+ *
11
+ * The compiled manifest carries the staleness metadata required by
12
+ * ADR-104 § Decision 3: schemaVersion + compiledAt + sourcePackVersions.
13
+ */
14
+ const HookCheckTypeSchema = z.enum(['reject-if-match', 'reject-if-no-match']);
15
+ /**
16
+ * Generous upper bound on pack-supplied regex pattern length. Real rules in
17
+ * the existing corpus run well under 200 chars; 512 leaves headroom while
18
+ * capping the attack surface against deliberately-bloated inputs. Bounding
19
+ * length at the schema layer also makes ReDoS-style worst-case payloads
20
+ * easier to reason about (the engine never compiles 10KB patterns).
21
+ */
22
+ const MAX_PATTERN_LENGTH = 512;
23
+ /**
24
+ * Validate that a regex pattern is well-formed AND safe from catastrophic
25
+ * backtracking. Runs at parse time (Tenet 4: don't fail open silently on
26
+ * invalid input). Three layers, gated in order with `fatal: true` so an
27
+ * earlier failure short-circuits — otherwise Zod would run `isRegexSafe`
28
+ * against an unbounded-length or syntactically-invalid input:
29
+ *
30
+ * 1. Bounded length (`MAX_PATTERN_LENGTH`) — caps the attack surface against
31
+ * deliberately-bloated patterns.
32
+ * 2. Syntactically valid (compiles via `new RegExp`).
33
+ * 3. Free of nested unbounded-quantifier shapes via `safe-regex2`
34
+ * (re-exported from core as `isRegexSafe`). Without this, a pack
35
+ * publishing a pattern like `(a+)+$` would hang `evaluateHook` on
36
+ * crafted tool-call payloads. Pack-supplied patterns come from
37
+ * third-party npm, so ReDoS rejection at parse time is load-bearing
38
+ * for the engine's availability guarantee.
39
+ */
40
+ const RegexPatternSchema = z.string().superRefine((p, ctx) => {
41
+ if (p.length < 1) {
42
+ ctx.addIssue({
43
+ code: z.ZodIssueCode.custom,
44
+ message: 'pattern must not be empty',
45
+ fatal: true,
46
+ });
47
+ return z.NEVER;
48
+ }
49
+ if (p.length > MAX_PATTERN_LENGTH) {
50
+ ctx.addIssue({
51
+ code: z.ZodIssueCode.custom,
52
+ message: `pattern exceeds ${MAX_PATTERN_LENGTH}-char limit`,
53
+ fatal: true,
54
+ });
55
+ return z.NEVER;
56
+ }
57
+ try {
58
+ new RegExp(p);
59
+ // totem-context: intentional — catch below is throw-as-control-flow for regex validation
60
+ }
61
+ catch {
62
+ ctx.addIssue({
63
+ code: z.ZodIssueCode.custom,
64
+ message: 'pattern is not a valid regular expression',
65
+ fatal: true,
66
+ });
67
+ return z.NEVER;
68
+ }
69
+ if (!isRegexSafe(p)) {
70
+ ctx.addIssue({
71
+ code: z.ZodIssueCode.custom,
72
+ message: 'pattern has catastrophic-backtracking risk (ReDoS); reshape to bounded quantifiers',
73
+ fatal: true,
74
+ });
75
+ return z.NEVER;
76
+ }
77
+ });
78
+ const HookTriggerSchema = z.object({
79
+ tool: z.string().min(1),
80
+ pattern: RegexPatternSchema,
81
+ });
82
+ const HookCheckSchema = z.object({
83
+ pattern: RegexPatternSchema,
84
+ type: HookCheckTypeSchema,
85
+ });
86
+ /**
87
+ * Authoring-surface schema for a single hook rule in a pack's `hooks.yaml`.
88
+ *
89
+ * The optional `recoveryHint` field (ADR-104 § Decision 1) gives agents the
90
+ * WHAT-INSTEAD on a block — recommended but not required in V1. Adoption
91
+ * tracked toward a V2 upgrade-to-required trigger (>80% of published rules
92
+ * carry it, OR an empirically-observed retry-loop incident).
93
+ *
94
+ * The `verification_shadow` field is reserved for the Spine Rule
95
+ * classification path (ADR-104 § Convergence + Q1 binding). In V1 the engine
96
+ * MUST warn-and-ignore any verification_shadow on a hook rule (hooks are
97
+ * Interpretive Rule class — no formal verification obligation). Schema
98
+ * accepts it permissively so future Spine-Rule promotion does not require
99
+ * a schema break.
100
+ */
101
+ export const HookRuleSchema = z.object({
102
+ id: z.string().min(1),
103
+ trigger: HookTriggerSchema,
104
+ check: HookCheckSchema,
105
+ message: z.string().min(1),
106
+ recoveryHint: z.string().optional(),
107
+ verification_shadow: z.unknown().optional(),
108
+ });
109
+ export const HOOKS_YAML_SCHEMA_VERSION = 1;
110
+ /**
111
+ * Per-pack `hooks.yaml` file shape. The `version` field is the contract
112
+ * for forward-compat: when `totem sync` parses an unknown version (higher
113
+ * than the runner supports), it warns-and-skips that pack entirely
114
+ * (ADR-104 § Decision 4).
115
+ */
116
+ export const HooksYamlSchema = z
117
+ .object({
118
+ version: z.number().int().positive(),
119
+ hooks: z.array(HookRuleSchema),
120
+ })
121
+ .superRefine((data, ctx) => {
122
+ // Duplicate hook ids within a single pack would make the
123
+ // `<packId>/<ruleId>` provenance in rejection messages (ADR-104 § Decision 1)
124
+ // ambiguous. Enforce uniqueness at parse time so a misauthored pack fails
125
+ // on load rather than producing non-deterministic rejection trails.
126
+ const seen = new Set();
127
+ data.hooks.forEach((hook, i) => {
128
+ if (seen.has(hook.id)) {
129
+ ctx.addIssue({
130
+ code: z.ZodIssueCode.custom,
131
+ message: `Duplicate hook id within pack: ${hook.id}`,
132
+ path: ['hooks', i, 'id'],
133
+ });
134
+ }
135
+ seen.add(hook.id);
136
+ });
137
+ });
138
+ export const COMPILED_HOOKS_SCHEMA_VERSION = 1;
139
+ /**
140
+ * A compiled hook rule carries provenance (`packId`) so rejection messages
141
+ * can name `<packId>/<ruleId>` (ADR-104 § Decision 1) and so staleness
142
+ * checks can scope per-pack.
143
+ */
144
+ export const CompiledHookRuleSchema = HookRuleSchema.extend({
145
+ packId: z.string().min(1),
146
+ });
147
+ /**
148
+ * Runtime-surface manifest produced by `totem sync` and read on every
149
+ * `totem hook run` invocation. The metadata fields are load-bearing for
150
+ * ADR-104 § Decision 3 (staleness detection):
151
+ *
152
+ * - `schemaVersion`: bumps on breaking structural change to this manifest
153
+ * - `compiledAt`: ISO 8601 timestamp of last compile
154
+ * - `sourcePackVersions`: pack name → version at compile time; compared
155
+ * against package.json resolutions to emit `[totem:hook-stale]` warnings
156
+ * when packs have updated since last compile
157
+ */
158
+ export const CompiledHooksManifestSchema = z.object({
159
+ schemaVersion: z.literal(COMPILED_HOOKS_SCHEMA_VERSION),
160
+ compiledAt: z.string().datetime(),
161
+ sourcePackVersions: z.record(z.string(), z.string()),
162
+ hooks: z.array(CompiledHookRuleSchema),
163
+ });
164
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/hook/schema.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=schema.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.test.d.ts","sourceRoot":"","sources":["../../src/hook/schema.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,233 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { COMPILED_HOOKS_SCHEMA_VERSION, CompiledHooksManifestSchema, HookRuleSchema, HOOKS_YAML_SCHEMA_VERSION, HooksYamlSchema, } from './schema.js';
3
+ describe('hook schema', () => {
4
+ describe('HookRuleSchema', () => {
5
+ it('accepts a minimal rule with required fields only', () => {
6
+ const minimal = {
7
+ id: 'gca-tag-xor-command',
8
+ trigger: { tool: 'bash', pattern: 'gh\\s+(pr|issue)\\s+comment.*' },
9
+ check: {
10
+ pattern: '(?=.*@gemini-code-assist)(?=.*\\/gemini review)',
11
+ type: 'reject-if-match',
12
+ },
13
+ message: 'GCA tag XOR — never both.',
14
+ };
15
+ const parsed = HookRuleSchema.parse(minimal);
16
+ expect(parsed.recoveryHint).toBeUndefined();
17
+ expect(parsed.verification_shadow).toBeUndefined();
18
+ });
19
+ it('accepts a rule with the optional recoveryHint field', () => {
20
+ const withHint = {
21
+ id: 'r1',
22
+ trigger: { tool: 'bash', pattern: '.*' },
23
+ check: { pattern: 'x', type: 'reject-if-match' },
24
+ message: 'm',
25
+ recoveryHint: 'try y instead',
26
+ };
27
+ const parsed = HookRuleSchema.parse(withHint);
28
+ expect(parsed.recoveryHint).toBe('try y instead');
29
+ });
30
+ it('accepts verification_shadow permissively (V1 warn-and-ignore at runtime)', () => {
31
+ const withShadow = {
32
+ id: 'r1',
33
+ trigger: { tool: 'bash', pattern: '.*' },
34
+ check: { pattern: 'x', type: 'reject-if-match' },
35
+ message: 'm',
36
+ verification_shadow: { rego: 'package x' },
37
+ };
38
+ // V1 contract: schema accepts the block so a future Spine-Rule
39
+ // promotion doesn't break the parser. Runtime drops/warns on it.
40
+ expect(() => HookRuleSchema.parse(withShadow)).not.toThrow();
41
+ });
42
+ it('rejects rules with empty required strings', () => {
43
+ const empty = {
44
+ id: '',
45
+ trigger: { tool: 'bash', pattern: '.*' },
46
+ check: { pattern: 'x', type: 'reject-if-match' },
47
+ message: 'm',
48
+ };
49
+ expect(() => HookRuleSchema.parse(empty)).toThrow();
50
+ });
51
+ it('rejects an unknown check.type value', () => {
52
+ const badType = {
53
+ id: 'r1',
54
+ trigger: { tool: 'bash', pattern: '.*' },
55
+ check: { pattern: 'x', type: 'maybe-reject' },
56
+ message: 'm',
57
+ };
58
+ expect(() => HookRuleSchema.parse(badType)).toThrow();
59
+ });
60
+ it('rejects rules whose trigger.pattern is not a valid regex (parse-time)', () => {
61
+ // Unterminated character class — `new RegExp('[')` throws SyntaxError.
62
+ // Schema must catch this before any `evaluateHook` invocation so a
63
+ // malformed pack rule surfaces at parse time rather than at first-fire.
64
+ const badRegex = {
65
+ id: 'r1',
66
+ trigger: { tool: 'bash', pattern: '[' },
67
+ check: { pattern: 'x', type: 'reject-if-match' },
68
+ message: 'm',
69
+ };
70
+ expect(() => HookRuleSchema.parse(badRegex)).toThrow();
71
+ });
72
+ it('rejects rules whose check.pattern is not a valid regex (parse-time)', () => {
73
+ const badRegex = {
74
+ id: 'r1',
75
+ trigger: { tool: 'bash', pattern: '.*' },
76
+ check: { pattern: '*invalid', type: 'reject-if-match' },
77
+ message: 'm',
78
+ };
79
+ expect(() => HookRuleSchema.parse(badRegex)).toThrow();
80
+ });
81
+ it('rejects rules whose check.pattern has ReDoS risk (parse-time)', () => {
82
+ // `(a+)+$` is the canonical exponential-backtracking ReDoS shape — runs
83
+ // exponentially in the input length on near-matches. Pack-supplied
84
+ // patterns get a parse-time safety check via safe-regex2 to keep the
85
+ // engine's availability guarantee independent of pack quality.
86
+ const redosRule = {
87
+ id: 'r1',
88
+ trigger: { tool: 'bash', pattern: '.*' },
89
+ check: { pattern: '(a+)+$', type: 'reject-if-match' },
90
+ message: 'm',
91
+ };
92
+ const result = HookRuleSchema.safeParse(redosRule);
93
+ expect(result.success).toBe(false);
94
+ if (!result.success) {
95
+ const issue = result.error.issues.find((i) => i.message.includes('catastrophic-backtracking'));
96
+ expect(issue).toBeDefined();
97
+ }
98
+ });
99
+ it('rejects rules whose pattern exceeds the length cap', () => {
100
+ const oversized = {
101
+ id: 'r1',
102
+ trigger: { tool: 'bash', pattern: 'a'.repeat(600) },
103
+ check: { pattern: 'x', type: 'reject-if-match' },
104
+ message: 'm',
105
+ };
106
+ const result = HookRuleSchema.safeParse(oversized);
107
+ expect(result.success).toBe(false);
108
+ });
109
+ });
110
+ describe('HooksYamlSchema', () => {
111
+ it('accepts a pack-level hooks.yaml with version 1 and an empty hooks array', () => {
112
+ const empty = { version: HOOKS_YAML_SCHEMA_VERSION, hooks: [] };
113
+ expect(() => HooksYamlSchema.parse(empty)).not.toThrow();
114
+ });
115
+ it('accepts version 2 — forward-compat path (runtime warn-and-skip per Decision 4)', () => {
116
+ // Schema accepts any positive integer version. The warn-and-skip on
117
+ // unknown-version is enforced at the load layer, not the schema layer,
118
+ // so a future bot-pack publishing version 2 doesn't crash the parser.
119
+ const future = { version: 2, hooks: [] };
120
+ expect(() => HooksYamlSchema.parse(future)).not.toThrow();
121
+ });
122
+ it('rejects non-positive version values', () => {
123
+ expect(() => HooksYamlSchema.parse({ version: 0, hooks: [] })).toThrow();
124
+ expect(() => HooksYamlSchema.parse({ version: -1, hooks: [] })).toThrow();
125
+ });
126
+ it('accepts hooks with distinct ids', () => {
127
+ const distinct = {
128
+ version: HOOKS_YAML_SCHEMA_VERSION,
129
+ hooks: [
130
+ {
131
+ id: 'r1',
132
+ trigger: { tool: 'bash', pattern: '.*' },
133
+ check: { pattern: 'x', type: 'reject-if-match' },
134
+ message: 'm1',
135
+ },
136
+ {
137
+ id: 'r2',
138
+ trigger: { tool: 'bash', pattern: '.*' },
139
+ check: { pattern: 'y', type: 'reject-if-match' },
140
+ message: 'm2',
141
+ },
142
+ ],
143
+ };
144
+ expect(() => HooksYamlSchema.parse(distinct)).not.toThrow();
145
+ });
146
+ it('rejects duplicate hook ids within a pack (provenance must stay deterministic)', () => {
147
+ // Two rules with the same id within one pack would make
148
+ // `<packId>/<ruleId>` rejection trails ambiguous (ADR-104 § Decision 1).
149
+ const dup = {
150
+ version: HOOKS_YAML_SCHEMA_VERSION,
151
+ hooks: [
152
+ {
153
+ id: 'r1',
154
+ trigger: { tool: 'bash', pattern: '.*' },
155
+ check: { pattern: 'x', type: 'reject-if-match' },
156
+ message: 'first',
157
+ },
158
+ {
159
+ id: 'r1',
160
+ trigger: { tool: 'bash', pattern: '.*' },
161
+ check: { pattern: 'y', type: 'reject-if-match' },
162
+ message: 'second',
163
+ },
164
+ ],
165
+ };
166
+ const result = HooksYamlSchema.safeParse(dup);
167
+ expect(result.success).toBe(false);
168
+ if (!result.success) {
169
+ const issue = result.error.issues.find((i) => i.message === 'Duplicate hook id within pack: r1');
170
+ expect(issue).toBeDefined();
171
+ expect(issue?.path).toEqual(['hooks', 1, 'id']);
172
+ }
173
+ });
174
+ });
175
+ describe('CompiledHooksManifestSchema', () => {
176
+ it('accepts a manifest with the required staleness metadata fields', () => {
177
+ const manifest = {
178
+ schemaVersion: COMPILED_HOOKS_SCHEMA_VERSION,
179
+ compiledAt: '2026-05-11T18:43:00Z',
180
+ sourcePackVersions: {
181
+ '@mmnto/pack-bot-coderabbit': '1.0.0',
182
+ },
183
+ hooks: [],
184
+ };
185
+ expect(() => CompiledHooksManifestSchema.parse(manifest)).not.toThrow();
186
+ });
187
+ it('rejects a manifest missing sourcePackVersions (staleness check requires it)', () => {
188
+ const bad = {
189
+ schemaVersion: COMPILED_HOOKS_SCHEMA_VERSION,
190
+ compiledAt: '2026-05-11T18:43:00Z',
191
+ hooks: [],
192
+ };
193
+ expect(() => CompiledHooksManifestSchema.parse(bad)).toThrow();
194
+ });
195
+ it('rejects a non-ISO compiledAt string', () => {
196
+ const bad = {
197
+ schemaVersion: COMPILED_HOOKS_SCHEMA_VERSION,
198
+ compiledAt: 'yesterday',
199
+ sourcePackVersions: {},
200
+ hooks: [],
201
+ };
202
+ expect(() => CompiledHooksManifestSchema.parse(bad)).toThrow();
203
+ });
204
+ it('rejects a future schemaVersion (uses z.literal — caller must handle upgrades)', () => {
205
+ const future = {
206
+ schemaVersion: 2,
207
+ compiledAt: '2026-05-11T18:43:00Z',
208
+ sourcePackVersions: {},
209
+ hooks: [],
210
+ };
211
+ expect(() => CompiledHooksManifestSchema.parse(future)).toThrow();
212
+ });
213
+ it('preserves provenance via packId on each compiled rule', () => {
214
+ const manifest = {
215
+ schemaVersion: COMPILED_HOOKS_SCHEMA_VERSION,
216
+ compiledAt: '2026-05-11T18:43:00Z',
217
+ sourcePackVersions: { '@mmnto/pack-bot-coderabbit': '1.0.0' },
218
+ hooks: [
219
+ {
220
+ id: 'r1',
221
+ packId: '@mmnto/pack-bot-coderabbit',
222
+ trigger: { tool: 'bash', pattern: '.*' },
223
+ check: { pattern: 'x', type: 'reject-if-match' },
224
+ message: 'm',
225
+ },
226
+ ],
227
+ };
228
+ const parsed = CompiledHooksManifestSchema.parse(manifest);
229
+ expect(parsed.hooks[0].packId).toBe('@mmnto/pack-bot-coderabbit');
230
+ });
231
+ });
232
+ });
233
+ //# sourceMappingURL=schema.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.test.js","sourceRoot":"","sources":["../../src/hook/schema.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Hooks-surface fixture test runner (ADR-104 § Convergence).
3
+ *
4
+ * Walks fixtures with `surface: hooks` from `.totem/tests/`, looks up the
5
+ * matching compiled hook by id, and evaluates each fixture line against
6
+ * the hook. Failures are reported per-line so authors can iterate on
7
+ * specific examples.
8
+ *
9
+ * Fixture semantics for hooks (corpus-driven):
10
+ * - `corpus: fail` (or `## Should fail` block) — each line is a tool-call
11
+ * payload that MUST cause the hook to reject; lines that allow are
12
+ * `missedFails`.
13
+ * - `corpus: pass` (or `## Should pass` block) — each line is a tool-call
14
+ * payload that MUST allow; lines that reject are `falsePositives`.
15
+ *
16
+ * Both blocks may appear in a single fixture (matches the rules-surface
17
+ * dual-section pattern). The corpus frontmatter field is informational
18
+ * for hooks fixtures — the section names carry the same intent and the
19
+ * runner exercises both.
20
+ *
21
+ * The tool for each payload is sourced from the matched hook's
22
+ * `trigger.tool`. Fixtures do not encode the tool explicitly; the hook
23
+ * declares which tool it gates, so the fixture body is just the args
24
+ * payload the hook would see at runtime.
25
+ *
26
+ * The runner is deterministic Node.js — no LLM calls — mirroring the
27
+ * `totem hook run` contract. Loader warnings and structured errors flow
28
+ * through to the summary so the CLI surface can echo them to stderr.
29
+ */
30
+ export interface HookTestFailure {
31
+ line: string;
32
+ expected: 'allow' | 'reject';
33
+ actual: 'allow' | 'reject';
34
+ }
35
+ export interface HookTestResult {
36
+ hookId: string;
37
+ packId: string;
38
+ fixturePath: string;
39
+ failures: HookTestFailure[];
40
+ passed: boolean;
41
+ }
42
+ export interface HookTestSummary {
43
+ total: number;
44
+ passed: number;
45
+ failed: number;
46
+ /** Fixtures referencing a hook id not present in the loaded manifest. */
47
+ unknownHooks: {
48
+ fixturePath: string;
49
+ hookId: string;
50
+ }[];
51
+ results: HookTestResult[];
52
+ loadWarnings: string[];
53
+ loadErrors: {
54
+ code: string;
55
+ message: string;
56
+ }[];
57
+ }
58
+ export interface RunHookTestsOptions {
59
+ manifestPath: string;
60
+ testsDir: string;
61
+ installedPackVersions: Record<string, string>;
62
+ }
63
+ export declare function runHookTests(options: RunHookTestsOptions): HookTestSummary;
64
+ //# sourceMappingURL=test-runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-runner.d.ts","sourceRoot":"","sources":["../../src/hook/test-runner.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,GAAG,QAAQ,CAAC;IAC7B,MAAM,EAAE,OAAO,GAAG,QAAQ,CAAC;CAC5B;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,YAAY,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACxD,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,UAAU,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACjD;AAED,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,qBAAqB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/C;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,eAAe,CA+B1E"}
@@ -0,0 +1,57 @@
1
+ import { loadFixtures } from '@mmnto/totem';
2
+ import { loadCompiledHooks } from './loader.js';
3
+ import { evaluateHook } from './runtime.js';
4
+ export function runHookTests(options) {
5
+ const { hooks, warnings, errors } = loadCompiledHooks({
6
+ manifestPath: options.manifestPath,
7
+ installedPackVersions: options.installedPackVersions,
8
+ });
9
+ const fixtures = loadFixtures(options.testsDir).filter((f) => f.surface === 'hooks');
10
+ const hooksById = new Map(hooks.map((h) => [h.id, h]));
11
+ const results = [];
12
+ const unknownHooks = [];
13
+ for (const fixture of fixtures) {
14
+ const hook = hooksById.get(fixture.ruleHash);
15
+ if (!hook) {
16
+ unknownHooks.push({ fixturePath: fixture.fixturePath, hookId: fixture.ruleHash });
17
+ continue;
18
+ }
19
+ results.push(testHook(hook, fixture));
20
+ }
21
+ const passed = results.filter((r) => r.passed).length;
22
+ return {
23
+ total: results.length,
24
+ passed,
25
+ failed: results.length - passed,
26
+ unknownHooks,
27
+ results,
28
+ loadWarnings: warnings,
29
+ loadErrors: errors.map((e) => ({ code: e.code, message: e.message })),
30
+ };
31
+ }
32
+ function testHook(hook, fixture) {
33
+ const failures = [];
34
+ const tool = hook.trigger.tool;
35
+ for (const args of fixture.failLines) {
36
+ const payload = { tool, args };
37
+ const decision = evaluateHook(hook, payload).decision;
38
+ if (decision !== 'reject') {
39
+ failures.push({ line: args, expected: 'reject', actual: decision });
40
+ }
41
+ }
42
+ for (const args of fixture.passLines) {
43
+ const payload = { tool, args };
44
+ const decision = evaluateHook(hook, payload).decision;
45
+ if (decision !== 'allow') {
46
+ failures.push({ line: args, expected: 'allow', actual: decision });
47
+ }
48
+ }
49
+ return {
50
+ hookId: hook.id,
51
+ packId: hook.packId,
52
+ fixturePath: fixture.fixturePath,
53
+ failures,
54
+ passed: failures.length === 0,
55
+ };
56
+ }
57
+ //# sourceMappingURL=test-runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-runner.js","sourceRoot":"","sources":["../../src/hook/test-runner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=test-runner.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-runner.test.d.ts","sourceRoot":"","sources":["../../src/hook/test-runner.test.ts"],"names":[],"mappings":""}