@mmnto/cli 1.35.0 → 1.37.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 (63) hide show
  1. package/dist/commands/config-drift.test.js +40 -24
  2. package/dist/commands/config-drift.test.js.map +1 -1
  3. package/dist/commands/doctor.d.ts +14 -0
  4. package/dist/commands/doctor.d.ts.map +1 -1
  5. package/dist/commands/doctor.js +83 -1
  6. package/dist/commands/doctor.js.map +1 -1
  7. package/dist/commands/doctor.test.js +111 -1
  8. package/dist/commands/doctor.test.js.map +1 -1
  9. package/dist/commands/hook-run.d.ts +91 -0
  10. package/dist/commands/hook-run.d.ts.map +1 -0
  11. package/dist/commands/hook-run.js +149 -0
  12. package/dist/commands/hook-run.js.map +1 -0
  13. package/dist/commands/hook-run.test.d.ts +2 -0
  14. package/dist/commands/hook-run.test.d.ts.map +1 -0
  15. package/dist/commands/hook-run.test.js +264 -0
  16. package/dist/commands/hook-run.test.js.map +1 -0
  17. package/dist/commands/hook-test.d.ts +29 -0
  18. package/dist/commands/hook-test.d.ts.map +1 -0
  19. package/dist/commands/hook-test.js +132 -0
  20. package/dist/commands/hook-test.js.map +1 -0
  21. package/dist/hook/classification.d.ts +45 -0
  22. package/dist/hook/classification.d.ts.map +1 -0
  23. package/dist/hook/classification.js +24 -0
  24. package/dist/hook/classification.js.map +1 -0
  25. package/dist/hook/classification.test.d.ts +2 -0
  26. package/dist/hook/classification.test.d.ts.map +1 -0
  27. package/dist/hook/classification.test.js +40 -0
  28. package/dist/hook/classification.test.js.map +1 -0
  29. package/dist/hook/loader.d.ts +47 -0
  30. package/dist/hook/loader.d.ts.map +1 -0
  31. package/dist/hook/loader.js +66 -0
  32. package/dist/hook/loader.js.map +1 -0
  33. package/dist/hook/loader.test.d.ts +2 -0
  34. package/dist/hook/loader.test.d.ts.map +1 -0
  35. package/dist/hook/loader.test.js +205 -0
  36. package/dist/hook/loader.test.js.map +1 -0
  37. package/dist/hook/runtime.d.ts +47 -0
  38. package/dist/hook/runtime.d.ts.map +1 -0
  39. package/dist/hook/runtime.js +85 -0
  40. package/dist/hook/runtime.js.map +1 -0
  41. package/dist/hook/runtime.test.d.ts +2 -0
  42. package/dist/hook/runtime.test.d.ts.map +1 -0
  43. package/dist/hook/runtime.test.js +135 -0
  44. package/dist/hook/runtime.test.js.map +1 -0
  45. package/dist/hook/schema.d.ts +385 -0
  46. package/dist/hook/schema.d.ts.map +1 -0
  47. package/dist/hook/schema.js +164 -0
  48. package/dist/hook/schema.js.map +1 -0
  49. package/dist/hook/schema.test.d.ts +2 -0
  50. package/dist/hook/schema.test.d.ts.map +1 -0
  51. package/dist/hook/schema.test.js +233 -0
  52. package/dist/hook/schema.test.js.map +1 -0
  53. package/dist/hook/test-runner.d.ts +64 -0
  54. package/dist/hook/test-runner.d.ts.map +1 -0
  55. package/dist/hook/test-runner.js +57 -0
  56. package/dist/hook/test-runner.js.map +1 -0
  57. package/dist/hook/test-runner.test.d.ts +2 -0
  58. package/dist/hook/test-runner.test.d.ts.map +1 -0
  59. package/dist/hook/test-runner.test.js +237 -0
  60. package/dist/hook/test-runner.test.js.map +1 -0
  61. package/dist/index.js +57 -4
  62. package/dist/index.js.map +1 -1
  63. package/package.json +2 -2
@@ -0,0 +1,45 @@
1
+ import type { HookRule } from './schema.js';
2
+ /**
3
+ * Rule classification per ADR-104 § Convergence + § Target-aware dispatch
4
+ * (strategy-Gemini Q1 binding synthesis, T1930Z 2026-05-11).
5
+ *
6
+ * Two classes:
7
+ * - `spine`: has Rego-shadow representation; formally verified by SMT;
8
+ * eligible for the top ~30 invariant set. Ships in ADR-103's lint-rule
9
+ * pipeline. Not produced by this V1 hook engine.
10
+ * - `interpretive`: ast-grep / regex only; no formal verification obligation;
11
+ * ships outside the core invariant set. All bot-pack hooks in V1 fall here.
12
+ *
13
+ * The hook runtime treats every rule as `interpretive`. The class is named
14
+ * explicitly so the loader's warn-and-ignore signal on a future Spine-Rule
15
+ * promotion attempt (`verification_shadow:` on a hook rule) carries the
16
+ * dispatch contract in its error message rather than relying on prose docs.
17
+ *
18
+ * V2 may promote bot-pack hooks to `spine` via a follow-on ADR; this seam
19
+ * is where that promotion lands.
20
+ */
21
+ export type RuleClassification = 'spine' | 'interpretive';
22
+ export interface ClassificationResult {
23
+ classification: RuleClassification;
24
+ /**
25
+ * When set, the loader SHOULD emit this string to stderr but continue
26
+ * loading the rule. Empty when no warn-and-ignore signal applies.
27
+ */
28
+ warning?: string;
29
+ }
30
+ /**
31
+ * Classify a hook rule for V1 dispatch.
32
+ *
33
+ * Always returns `interpretive` per ADR-104 § Target-aware dispatch (hooks
34
+ * are Interpretive Rule class — no formal-verification obligation; PreToolUse
35
+ * payloads are not source code, so the Rego/OPA value proposition is weaker
36
+ * than for lint rules).
37
+ *
38
+ * If the rule carries a `verification_shadow:` block (forward-compat schema
39
+ * permits this for future Spine promotion), the returned result includes a
40
+ * structured warning. Per ADR-104 § Convergence: "the engine MUST
41
+ * warn-and-ignore the block (the rule itself still executes as
42
+ * Interpretive)."
43
+ */
44
+ export declare function classifyHookRule(rule: HookRule): ClassificationResult;
45
+ //# sourceMappingURL=classification.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classification.d.ts","sourceRoot":"","sources":["../../src/hook/classification.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,cAAc,CAAC;AAE1D,MAAM,WAAW,oBAAoB;IACnC,cAAc,EAAE,kBAAkB,CAAC;IACnC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,QAAQ,GAAG,oBAAoB,CAQrE"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Classify a hook rule for V1 dispatch.
3
+ *
4
+ * Always returns `interpretive` per ADR-104 § Target-aware dispatch (hooks
5
+ * are Interpretive Rule class — no formal-verification obligation; PreToolUse
6
+ * payloads are not source code, so the Rego/OPA value proposition is weaker
7
+ * than for lint rules).
8
+ *
9
+ * If the rule carries a `verification_shadow:` block (forward-compat schema
10
+ * permits this for future Spine promotion), the returned result includes a
11
+ * structured warning. Per ADR-104 § Convergence: "the engine MUST
12
+ * warn-and-ignore the block (the rule itself still executes as
13
+ * Interpretive)."
14
+ */
15
+ export function classifyHookRule(rule) {
16
+ if (rule.verification_shadow !== undefined) {
17
+ return {
18
+ classification: 'interpretive',
19
+ warning: `[totem:hook-shadow-ignored] ${rule.id}: verification_shadow block ignored in V1 (hooks are Interpretive Rule class); rule still executes`,
20
+ };
21
+ }
22
+ return { classification: 'interpretive' };
23
+ }
24
+ //# sourceMappingURL=classification.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classification.js","sourceRoot":"","sources":["../../src/hook/classification.ts"],"names":[],"mappings":"AAgCA;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAc;IAC7C,IAAI,IAAI,CAAC,mBAAmB,KAAK,SAAS,EAAE,CAAC;QAC3C,OAAO;YACL,cAAc,EAAE,cAAc;YAC9B,OAAO,EAAE,+BAA+B,IAAI,CAAC,EAAE,oGAAoG;SACpJ,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,CAAC;AAC5C,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=classification.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classification.test.d.ts","sourceRoot":"","sources":["../../src/hook/classification.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { classifyHookRule } from './classification.js';
3
+ const baseRule = {
4
+ id: 'r1',
5
+ trigger: { tool: 'bash', pattern: '.*' },
6
+ check: { pattern: 'x', type: 'reject-if-match' },
7
+ message: 'm',
8
+ };
9
+ describe('classifyHookRule', () => {
10
+ it('returns interpretive with no warning for a plain hook rule', () => {
11
+ const result = classifyHookRule(baseRule);
12
+ expect(result.classification).toBe('interpretive');
13
+ expect(result.warning).toBeUndefined();
14
+ });
15
+ it('returns interpretive but emits a warn-and-ignore signal when verification_shadow is present', () => {
16
+ const withShadow = {
17
+ ...baseRule,
18
+ verification_shadow: { rego: 'package x' },
19
+ };
20
+ const result = classifyHookRule(withShadow);
21
+ expect(result.classification).toBe('interpretive');
22
+ expect(result.warning).toBeDefined();
23
+ expect(result.warning).toContain('[totem:hook-shadow-ignored]');
24
+ expect(result.warning).toContain('r1');
25
+ });
26
+ it('treats verification_shadow: null as present (warns and continues)', () => {
27
+ // null is a JS-truthy distinct from undefined; if the schema admits it the
28
+ // classification helper should still emit the warn-and-ignore signal so
29
+ // the dispatch contract holds for any non-undefined value.
30
+ const withNullShadow = {
31
+ ...baseRule,
32
+ id: 'r-null',
33
+ verification_shadow: null,
34
+ };
35
+ const result = classifyHookRule(withNullShadow);
36
+ expect(result.classification).toBe('interpretive');
37
+ expect(result.warning).toBeDefined();
38
+ });
39
+ });
40
+ //# sourceMappingURL=classification.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classification.test.js","sourceRoot":"","sources":["../../src/hook/classification.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAGvD,MAAM,QAAQ,GAAa;IACzB,EAAE,EAAE,IAAI;IACR,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE;IACxC,KAAK,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,iBAAiB,EAAE;IAChD,OAAO,EAAE,GAAG;CACb,CAAC;AAEF,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,4DAA4D,EAAE,GAAG,EAAE;QACpE,MAAM,MAAM,GAAG,gBAAgB,CAAC,QAAQ,CAAC,CAAC;QAC1C,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QACnD,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,aAAa,EAAE,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6FAA6F,EAAE,GAAG,EAAE;QACrG,MAAM,UAAU,GAAa;YAC3B,GAAG,QAAQ;YACX,mBAAmB,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE;SAC3C,CAAC;QACF,MAAM,MAAM,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAAC;QAC5C,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QACnD,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC;QACrC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,6BAA6B,CAAC,CAAC;QAChE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mEAAmE,EAAE,GAAG,EAAE;QAC3E,2EAA2E;QAC3E,wEAAwE;QACxE,2DAA2D;QAC3D,MAAM,cAAc,GAAa;YAC/B,GAAG,QAAQ;YACX,EAAE,EAAE,QAAQ;YACZ,mBAAmB,EAAE,IAAI;SAC1B,CAAC;QACF,MAAM,MAAM,GAAG,gBAAgB,CAAC,cAAc,CAAC,CAAC;QAChD,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QACnD,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC;IACvC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,47 @@
1
+ import { TotemError } from '@mmnto/totem';
2
+ import { type CompiledHookRule } from './schema.js';
3
+ /**
4
+ * Compiled-hooks manifest loader (ADR-104 § Decision 3 staleness check + a
5
+ * forward-compat warn-and-skip path for an evolving manifest schema).
6
+ *
7
+ * The loader is a pure function over `(manifestPath, installedPackVersions)`.
8
+ * It does NOT read `package.json` itself — callers (typically a
9
+ * bootstrap helper) resolve installed pack versions once and pass them in.
10
+ * Keeps the loader testable in isolation; mirrors the substrate-wiring
11
+ * pattern from `bootstrapEngine` (1.25.0 wiring lesson).
12
+ *
13
+ * Three failure modes are surfaced via the `warnings` array, not by
14
+ * throwing:
15
+ *
16
+ * 1. Manifest file missing — empty result, no warnings (a fresh repo without
17
+ * installed pack hooks is a valid state).
18
+ * 2. Manifest schemaVersion is not the runner's expected version — warn and
19
+ * skip the entire manifest (no hooks loaded). Composes with ADR-104
20
+ * § Decision 4's forward-compat ethos.
21
+ * 3. Pack version drift — for each pack whose installed version differs from
22
+ * the compiled-against version, emit a `[totem:hook-stale]` warning per
23
+ * the format in ADR-104 § Decision 3. Hooks still load (Tenet 4 carve-out:
24
+ * hooks are best-effort; staleness is signal, not a fail-closed condition).
25
+ *
26
+ * Structural errors (corrupt JSON, schema-validation failure on a manifest
27
+ * claiming the supported schemaVersion) populate `errors` and yield an empty
28
+ * hooks array — distinct from a missing manifest.
29
+ */
30
+ export interface LoadCompiledHooksOptions {
31
+ manifestPath: string;
32
+ installedPackVersions: Record<string, string>;
33
+ }
34
+ export interface LoadCompiledHooksResult {
35
+ hooks: CompiledHookRule[];
36
+ warnings: string[];
37
+ /**
38
+ * Errors carry the original cause via `Error.cause` so debug consumers
39
+ * can traverse the chain (per the codebase styleguide rule against
40
+ * concatenating `err.message` into new strings — destroys the stack).
41
+ * Callers that just need to log can use `err.message`; debug tooling
42
+ * walks `err.cause` recursively.
43
+ */
44
+ errors: TotemError[];
45
+ }
46
+ export declare function loadCompiledHooks(options: LoadCompiledHooksOptions): LoadCompiledHooksResult;
47
+ //# sourceMappingURL=loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/hook/loader.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,OAAO,EAEL,KAAK,gBAAgB,EAEtB,MAAM,aAAa,CAAC;AAErB;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,MAAM,WAAW,wBAAwB;IACvC,YAAY,EAAE,MAAM,CAAC;IACrB,qBAAqB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/C;AAED,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAC1B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB;;;;;;OAMG;IACH,MAAM,EAAE,UAAU,EAAE,CAAC;CACtB;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,uBAAuB,CA+F5F"}
@@ -0,0 +1,66 @@
1
+ import fs from 'node:fs';
2
+ import { TotemError } from '@mmnto/totem';
3
+ import { COMPILED_HOOKS_SCHEMA_VERSION, CompiledHooksManifestSchema, } from './schema.js';
4
+ export function loadCompiledHooks(options) {
5
+ const warnings = [];
6
+ const errors = [];
7
+ // No `fs.existsSync` pre-check: that returns false for any filesystem
8
+ // error (permission denied, symlink loops, EBUSY, etc.), not just ENOENT.
9
+ // Treating those as "missing manifest" would silently swallow real
10
+ // diagnostics. Catch ENOENT explicitly inside the read; everything else
11
+ // surfaces as a HOOKS_LOAD_FAILED entry.
12
+ let raw;
13
+ try {
14
+ raw = fs.readFileSync(options.manifestPath, 'utf8');
15
+ // totem-context: intentional — error captured into diagnostics array (the loader's contract is diagnostics-not-throws per Tenet 4 carve-out for hooks being best-effort)
16
+ }
17
+ catch (err) {
18
+ if (err.code === 'ENOENT') {
19
+ // Fresh repo without installed pack hooks is a valid state, not a fault.
20
+ return { hooks: [], warnings, errors };
21
+ }
22
+ errors.push(new TotemError('HOOKS_LOAD_FAILED', `failed to read compiled-hooks manifest at ${options.manifestPath}`, 'verify the file is readable and re-run `totem sync` to regenerate', err));
23
+ return { hooks: [], warnings, errors };
24
+ }
25
+ let parsed;
26
+ try {
27
+ parsed = JSON.parse(raw);
28
+ // totem-context: intentional — error captured into diagnostics array (the loader's contract is diagnostics-not-throws per Tenet 4 carve-out for hooks being best-effort)
29
+ }
30
+ catch (err) {
31
+ errors.push(new TotemError('HOOKS_LOAD_FAILED', `compiled-hooks manifest at ${options.manifestPath} is not valid JSON`, 're-run `totem sync` to regenerate the manifest', err));
32
+ return { hooks: [], warnings, errors };
33
+ }
34
+ // Forward-compat: peek at schemaVersion BEFORE invoking the strict z.literal
35
+ // schema validator, so an unknown version surfaces as a warn-and-skip
36
+ // rather than a thrown ZodError. Mirrors the per-pack warn-and-skip pattern
37
+ // for `hooks.yaml :: version` from ADR-104 § Decision 4.
38
+ const peekedVersion = typeof parsed === 'object' && parsed !== null
39
+ ? parsed.schemaVersion
40
+ : undefined;
41
+ if (peekedVersion !== COMPILED_HOOKS_SCHEMA_VERSION) {
42
+ warnings.push(`[totem:hook-schema] compiled-hooks manifest schemaVersion ${JSON.stringify(peekedVersion)} unsupported by this runner (expected ${COMPILED_HOOKS_SCHEMA_VERSION})\n → upgrade totem CLI or re-run \`totem sync\` to regenerate`);
43
+ return { hooks: [], warnings, errors };
44
+ }
45
+ const validation = CompiledHooksManifestSchema.safeParse(parsed);
46
+ if (!validation.success) {
47
+ const summary = validation.error.issues
48
+ .map((i) => `${i.path.join('.')}: ${i.message}`)
49
+ .join('; ');
50
+ errors.push(new TotemError('HOOKS_LOAD_FAILED', `compiled-hooks manifest at ${options.manifestPath} failed schema validation: ${summary}`, 're-run `totem sync` to regenerate the manifest, or upgrade totem CLI if the manifest was authored by a newer version', validation.error));
51
+ return { hooks: [], warnings, errors };
52
+ }
53
+ const manifest = validation.data;
54
+ for (const [packId, compiledVersion] of Object.entries(manifest.sourcePackVersions)) {
55
+ const installedVersion = options.installedPackVersions[packId];
56
+ if (installedVersion === undefined) {
57
+ warnings.push(`[totem:hook-stale] ${packId}: compiled against ${compiledVersion}, not currently installed\n → run \`totem sync\` to refresh .totem/compiled-hooks.json`);
58
+ continue;
59
+ }
60
+ if (installedVersion !== compiledVersion) {
61
+ warnings.push(`[totem:hook-stale] ${packId}: compiled against ${compiledVersion}, installed ${installedVersion}\n → run \`totem sync\` to refresh .totem/compiled-hooks.json`);
62
+ }
63
+ }
64
+ return { hooks: manifest.hooks, warnings, errors };
65
+ }
66
+ //# sourceMappingURL=loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.js","sourceRoot":"","sources":["../../src/hook/loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAEzB,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,OAAO,EACL,6BAA6B,EAE7B,2BAA2B,GAC5B,MAAM,aAAa,CAAC;AAgDrB,MAAM,UAAU,iBAAiB,CAAC,OAAiC;IACjE,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,MAAM,GAAiB,EAAE,CAAC;IAEhC,sEAAsE;IACtE,0EAA0E;IAC1E,mEAAmE;IACnE,wEAAwE;IACxE,yCAAyC;IACzC,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,OAAO,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;QACpD,yKAAyK;IAC3K,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACrD,yEAAyE;YACzE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;QACzC,CAAC;QACD,MAAM,CAAC,IAAI,CACT,IAAI,UAAU,CACZ,mBAAmB,EACnB,6CAA6C,OAAO,CAAC,YAAY,EAAE,EACnE,mEAAmE,EACnE,GAAG,CACJ,CACF,CAAC;QACF,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IACzC,CAAC;IAED,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACzB,yKAAyK;IAC3K,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,IAAI,CACT,IAAI,UAAU,CACZ,mBAAmB,EACnB,8BAA8B,OAAO,CAAC,YAAY,oBAAoB,EACtE,gDAAgD,EAChD,GAAG,CACJ,CACF,CAAC;QACF,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IACzC,CAAC;IAED,6EAA6E;IAC7E,sEAAsE;IACtE,4EAA4E;IAC5E,yDAAyD;IACzD,MAAM,aAAa,GACjB,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;QAC3C,CAAC,CAAE,MAAsC,CAAC,aAAa;QACvD,CAAC,CAAC,SAAS,CAAC;IAEhB,IAAI,aAAa,KAAK,6BAA6B,EAAE,CAAC;QACpD,QAAQ,CAAC,IAAI,CACX,6DAA6D,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,yCAAyC,6BAA6B,iEAAiE,CAClO,CAAC;QACF,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IACzC,CAAC;IAED,MAAM,UAAU,GAAG,2BAA2B,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACjE,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC;QACxB,MAAM,OAAO,GAAG,UAAU,CAAC,KAAK,CAAC,MAAM;aACpC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC;aAC/C,IAAI,CAAC,IAAI,CAAC,CAAC;QACd,MAAM,CAAC,IAAI,CACT,IAAI,UAAU,CACZ,mBAAmB,EACnB,8BAA8B,OAAO,CAAC,YAAY,8BAA8B,OAAO,EAAE,EACzF,sHAAsH,EACtH,UAAU,CAAC,KAAK,CACjB,CACF,CAAC;QACF,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IACzC,CAAC;IAED,MAAM,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC;IAEjC,KAAK,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,CAAC;QACpF,MAAM,gBAAgB,GAAG,OAAO,CAAC,qBAAqB,CAAC,MAAM,CAAC,CAAC;QAC/D,IAAI,gBAAgB,KAAK,SAAS,EAAE,CAAC;YACnC,QAAQ,CAAC,IAAI,CACX,sBAAsB,MAAM,sBAAsB,eAAe,yFAAyF,CAC3J,CAAC;YACF,SAAS;QACX,CAAC;QACD,IAAI,gBAAgB,KAAK,eAAe,EAAE,CAAC;YACzC,QAAQ,CAAC,IAAI,CACX,sBAAsB,MAAM,sBAAsB,eAAe,eAAe,gBAAgB,gEAAgE,CACjK,CAAC;QACJ,CAAC;IACH,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;AACrD,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=loader.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.test.d.ts","sourceRoot":"","sources":["../../src/hook/loader.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,205 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import { loadCompiledHooks } from './loader.js';
6
+ let workDir;
7
+ beforeEach(() => {
8
+ workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'totem-hook-loader-'));
9
+ });
10
+ afterEach(() => {
11
+ fs.rmSync(workDir, { recursive: true, force: true });
12
+ });
13
+ function writeManifest(content) {
14
+ const manifestPath = path.join(workDir, 'compiled-hooks.json');
15
+ fs.writeFileSync(manifestPath, JSON.stringify(content), 'utf8');
16
+ return manifestPath;
17
+ }
18
+ const validRule = {
19
+ id: 'r1',
20
+ packId: '@mmnto/pack-bot-coderabbit',
21
+ trigger: { tool: 'bash', pattern: '.*' },
22
+ check: { pattern: 'x', type: 'reject-if-match' },
23
+ message: 'm',
24
+ };
25
+ describe('loadCompiledHooks', () => {
26
+ it('returns an empty result when the manifest file does not exist (fresh repo, ENOENT)', () => {
27
+ const result = loadCompiledHooks({
28
+ manifestPath: path.join(workDir, 'missing.json'),
29
+ installedPackVersions: {},
30
+ });
31
+ expect(result.hooks).toEqual([]);
32
+ expect(result.warnings).toEqual([]);
33
+ expect(result.errors).toEqual([]);
34
+ });
35
+ it('surfaces non-ENOENT read errors as HOOKS_LOAD_FAILED (does not pretend the file is missing)', () => {
36
+ // Reading a directory as a file produces EISDIR on POSIX / EBADF or
37
+ // ENOTSUP-flavoured failures on Windows. Whatever the platform-specific
38
+ // errno, it is NOT ENOENT — so the loader must surface it, not silently
39
+ // return the "manifest absent" result that the prior existsSync pre-check
40
+ // would have masked.
41
+ const dirAsManifest = path.join(workDir, 'a-directory');
42
+ fs.mkdirSync(dirAsManifest);
43
+ const result = loadCompiledHooks({
44
+ manifestPath: dirAsManifest,
45
+ installedPackVersions: {},
46
+ });
47
+ expect(result.hooks).toEqual([]);
48
+ expect(result.errors.length).toBe(1);
49
+ const err = result.errors[0];
50
+ expect(err.code).toBe('HOOKS_LOAD_FAILED');
51
+ expect(err.message).toContain('failed to read compiled-hooks manifest');
52
+ expect(err.cause).toBeDefined();
53
+ });
54
+ it('records a structural error on invalid JSON and preserves the original SyntaxError via cause', () => {
55
+ const manifestPath = path.join(workDir, 'compiled-hooks.json');
56
+ fs.writeFileSync(manifestPath, '{ not valid json', 'utf8');
57
+ const result = loadCompiledHooks({
58
+ manifestPath,
59
+ installedPackVersions: {},
60
+ });
61
+ expect(result.hooks).toEqual([]);
62
+ expect(result.errors.length).toBe(1);
63
+ const err = result.errors[0];
64
+ expect(err.message).toContain('not valid JSON');
65
+ expect(err.code).toBe('HOOKS_LOAD_FAILED');
66
+ // Original parse error preserved on `.cause` so debug consumers can
67
+ // walk the chain without the stack being collapsed into a string.
68
+ expect(err.cause).toBeInstanceOf(SyntaxError);
69
+ });
70
+ it('warns and skips when schemaVersion is higher than the runner supports', () => {
71
+ const manifestPath = writeManifest({
72
+ schemaVersion: 2,
73
+ compiledAt: '2026-05-11T18:43:00Z',
74
+ sourcePackVersions: {},
75
+ hooks: [],
76
+ });
77
+ const result = loadCompiledHooks({
78
+ manifestPath,
79
+ installedPackVersions: {},
80
+ });
81
+ expect(result.hooks).toEqual([]);
82
+ expect(result.errors).toEqual([]);
83
+ expect(result.warnings.length).toBe(1);
84
+ expect(result.warnings[0]).toContain('[totem:hook-schema]');
85
+ expect(result.warnings[0]).toContain('schemaVersion 2');
86
+ });
87
+ it('warns and skips when schemaVersion is missing', () => {
88
+ const manifestPath = writeManifest({
89
+ compiledAt: '2026-05-11T18:43:00Z',
90
+ sourcePackVersions: {},
91
+ hooks: [],
92
+ });
93
+ const result = loadCompiledHooks({
94
+ manifestPath,
95
+ installedPackVersions: {},
96
+ });
97
+ expect(result.hooks).toEqual([]);
98
+ expect(result.warnings.length).toBe(1);
99
+ expect(result.warnings[0]).toContain('[totem:hook-schema]');
100
+ });
101
+ it('records a structural error when the supported-version manifest fails schema validation', () => {
102
+ const manifestPath = writeManifest({
103
+ schemaVersion: 1,
104
+ compiledAt: 'not-a-real-date',
105
+ sourcePackVersions: {},
106
+ hooks: [],
107
+ });
108
+ const result = loadCompiledHooks({
109
+ manifestPath,
110
+ installedPackVersions: {},
111
+ });
112
+ expect(result.hooks).toEqual([]);
113
+ expect(result.errors.length).toBe(1);
114
+ const err = result.errors[0];
115
+ expect(err.message).toContain('schema validation');
116
+ expect(err.code).toBe('HOOKS_LOAD_FAILED');
117
+ // Zod's ZodError preserved as the cause for debug-mode chain traversal.
118
+ expect(err.cause).toBeDefined();
119
+ });
120
+ it('returns hooks with no warnings when installed pack versions match compiled versions', () => {
121
+ const manifestPath = writeManifest({
122
+ schemaVersion: 1,
123
+ compiledAt: '2026-05-11T18:43:00Z',
124
+ sourcePackVersions: { '@mmnto/pack-bot-coderabbit': '1.0.0' },
125
+ hooks: [validRule],
126
+ });
127
+ const result = loadCompiledHooks({
128
+ manifestPath,
129
+ installedPackVersions: { '@mmnto/pack-bot-coderabbit': '1.0.0' },
130
+ });
131
+ expect(result.hooks).toHaveLength(1);
132
+ expect(result.warnings).toEqual([]);
133
+ expect(result.errors).toEqual([]);
134
+ });
135
+ it('emits a staleness warning when the installed pack version differs from compiled', () => {
136
+ const manifestPath = writeManifest({
137
+ schemaVersion: 1,
138
+ compiledAt: '2026-05-11T18:43:00Z',
139
+ sourcePackVersions: { '@mmnto/pack-bot-coderabbit': '1.0.0' },
140
+ hooks: [validRule],
141
+ });
142
+ const result = loadCompiledHooks({
143
+ manifestPath,
144
+ installedPackVersions: { '@mmnto/pack-bot-coderabbit': '1.1.0' },
145
+ });
146
+ expect(result.hooks).toHaveLength(1);
147
+ expect(result.warnings.length).toBe(1);
148
+ expect(result.warnings[0]).toContain('[totem:hook-stale]');
149
+ expect(result.warnings[0]).toContain('@mmnto/pack-bot-coderabbit');
150
+ expect(result.warnings[0]).toContain('compiled against 1.0.0, installed 1.1.0');
151
+ });
152
+ it('emits a staleness warning when a compiled-against pack is not installed at all', () => {
153
+ const manifestPath = writeManifest({
154
+ schemaVersion: 1,
155
+ compiledAt: '2026-05-11T18:43:00Z',
156
+ sourcePackVersions: { '@mmnto/pack-bot-coderabbit': '1.0.0' },
157
+ hooks: [validRule],
158
+ });
159
+ const result = loadCompiledHooks({
160
+ manifestPath,
161
+ installedPackVersions: {},
162
+ });
163
+ expect(result.hooks).toHaveLength(1);
164
+ expect(result.warnings.length).toBe(1);
165
+ expect(result.warnings[0]).toContain('not currently installed');
166
+ });
167
+ it('ignores extra installed packs not in sourcePackVersions (no warning)', () => {
168
+ // A pack installed after the last `totem sync` is benign — its hooks
169
+ // are not yet active, but that is not staleness in the compiled set.
170
+ const manifestPath = writeManifest({
171
+ schemaVersion: 1,
172
+ compiledAt: '2026-05-11T18:43:00Z',
173
+ sourcePackVersions: { '@mmnto/pack-bot-coderabbit': '1.0.0' },
174
+ hooks: [validRule],
175
+ });
176
+ const result = loadCompiledHooks({
177
+ manifestPath,
178
+ installedPackVersions: {
179
+ '@mmnto/pack-bot-coderabbit': '1.0.0',
180
+ '@mmnto/pack-bot-gemini-code-assist': '1.0.0',
181
+ },
182
+ });
183
+ expect(result.warnings).toEqual([]);
184
+ });
185
+ it('emits one staleness warning per drifting pack', () => {
186
+ const manifestPath = writeManifest({
187
+ schemaVersion: 1,
188
+ compiledAt: '2026-05-11T18:43:00Z',
189
+ sourcePackVersions: {
190
+ '@mmnto/pack-bot-coderabbit': '1.0.0',
191
+ '@mmnto/pack-bot-gemini-code-assist': '2.0.0',
192
+ },
193
+ hooks: [validRule],
194
+ });
195
+ const result = loadCompiledHooks({
196
+ manifestPath,
197
+ installedPackVersions: {
198
+ '@mmnto/pack-bot-coderabbit': '1.1.0',
199
+ '@mmnto/pack-bot-gemini-code-assist': '2.0.1',
200
+ },
201
+ });
202
+ expect(result.warnings.length).toBe(2);
203
+ });
204
+ });
205
+ //# sourceMappingURL=loader.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.test.js","sourceRoot":"","sources":["../../src/hook/loader.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ import type { CompiledHookRule } from './schema.js';
2
+ /**
3
+ * Hook runtime evaluator (ADR-104 § Decisions 1, 2 + § Convergence).
4
+ *
5
+ * Takes a single hook rule plus a tool-call payload (tool name + tool args
6
+ * as a single string) and returns a structured allow/reject decision. The
7
+ * runtime is deterministic Node.js — no LLM calls in this path (Tenet 15
8
+ * corollary, ADR-103 § 8).
9
+ *
10
+ * V1 matcher class is regex-only per execution plan § 4. ast-grep and
11
+ * other matcher classes are deferred to V2; the schema permits future
12
+ * `verification_shadow` blocks but the V1 runtime ignores them.
13
+ */
14
+ export interface ToolCallPayload {
15
+ /** The tool the agent is attempting to invoke (e.g. "bash"). */
16
+ tool: string;
17
+ /** Serialized tool arguments. For bash this is the command string;
18
+ * for structured tools, callers serialize to a stable string form. */
19
+ args: string;
20
+ }
21
+ export type AllowDecision = {
22
+ decision: 'allow';
23
+ };
24
+ export type RejectDecision = {
25
+ decision: 'reject';
26
+ message: string;
27
+ packId: string;
28
+ ruleId: string;
29
+ recoveryHint?: string;
30
+ };
31
+ export type HookDecision = AllowDecision | RejectDecision;
32
+ /**
33
+ * Build the structured rejection message per ADR-104 § Decision 1:
34
+ *
35
+ * [totem:hook-block] <packId>/<ruleId>: <message>
36
+ * → <recoveryHint>
37
+ *
38
+ * The `→ <recoveryHint>` line is omitted when no recoveryHint is provided.
39
+ * Agents and operators grep for the `[totem:hook-block]` prefix; the
40
+ * `<packId>/<ruleId>` carries provenance.
41
+ *
42
+ * Parameter is narrowed to `RejectDecision` so the type system prevents
43
+ * passing an allow decision — no runtime guard needed.
44
+ */
45
+ export declare function formatRejection(decision: RejectDecision): string;
46
+ export declare function evaluateHook(rule: CompiledHookRule, payload: ToolCallPayload): HookDecision;
47
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/hook/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,eAAe;IAC9B,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb;2EACuE;IACvE,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,aAAa,GAAG;IAAE,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC;AAElD,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,aAAa,GAAG,cAAc,CAAC;AAE1D;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,CAMhE;AA4CD,wBAAgB,YAAY,CAAC,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,eAAe,GAAG,YAAY,CA4B3F"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Build the structured rejection message per ADR-104 § Decision 1:
3
+ *
4
+ * [totem:hook-block] <packId>/<ruleId>: <message>
5
+ * → <recoveryHint>
6
+ *
7
+ * The `→ <recoveryHint>` line is omitted when no recoveryHint is provided.
8
+ * Agents and operators grep for the `[totem:hook-block]` prefix; the
9
+ * `<packId>/<ruleId>` carries provenance.
10
+ *
11
+ * Parameter is narrowed to `RejectDecision` so the type system prevents
12
+ * passing an allow decision — no runtime guard needed.
13
+ */
14
+ export function formatRejection(decision) {
15
+ const header = `[totem:hook-block] ${decision.packId}/${decision.ruleId}: ${decision.message}`;
16
+ if (decision.recoveryHint) {
17
+ return `${header}\n → ${decision.recoveryHint}`;
18
+ }
19
+ return header;
20
+ }
21
+ /**
22
+ * Evaluate a single compiled hook rule against a tool-call payload.
23
+ *
24
+ * Two-stage gate:
25
+ * 1. Trigger gate: does this rule apply to this tool + args?
26
+ * Rule applies when `rule.trigger.tool` equals `payload.tool` AND
27
+ * `rule.trigger.pattern` matches `payload.args`.
28
+ * 2. Check gate: when the trigger matches, apply `rule.check.pattern` to
29
+ * args. `reject-if-match` rejects on match; `reject-if-no-match`
30
+ * rejects on non-match.
31
+ *
32
+ * Returns `{ decision: 'allow' }` when either gate passes the call through.
33
+ *
34
+ * V1 invariant: regex matching only. Future matcher classes (ast-grep,
35
+ * Rego-shadow) ship in V2 follow-on ADRs.
36
+ *
37
+ * Per ADR-104 § Convergence, any `verification_shadow` block on the rule
38
+ * is silently ignored at the runtime layer (V1 hooks are Interpretive
39
+ * Rule class — no formal-verification obligation). Warn-and-ignore of
40
+ * verification_shadow happens at the load layer when compiling pack
41
+ * hooks.yaml, not on every hook-run invocation.
42
+ */
43
+ /**
44
+ * Memoizes compiled RegExp instances keyed by pattern string. Pack rule
45
+ * patterns are stable for the lifetime of a CLI process (the manifest is
46
+ * loaded once at startup), so a string-keyed cache is sufficient — there
47
+ * is no unbounded-growth concern in the single-shot CLI use case.
48
+ *
49
+ * Safe to memoize because the patterns are compiled with no flags, so the
50
+ * returned RegExp has no stateful `lastIndex` carry-over between `.test()`
51
+ * calls.
52
+ */
53
+ const regexCache = new Map();
54
+ function getCompiledRegex(pattern) {
55
+ const cached = regexCache.get(pattern);
56
+ if (cached)
57
+ return cached;
58
+ const compiled = new RegExp(pattern);
59
+ regexCache.set(pattern, compiled);
60
+ return compiled;
61
+ }
62
+ export function evaluateHook(rule, payload) {
63
+ if (rule.trigger.tool !== payload.tool) {
64
+ return { decision: 'allow' };
65
+ }
66
+ const triggerRegex = getCompiledRegex(rule.trigger.pattern);
67
+ if (!triggerRegex.test(payload.args)) {
68
+ return { decision: 'allow' };
69
+ }
70
+ const checkRegex = getCompiledRegex(rule.check.pattern);
71
+ const matched = checkRegex.test(payload.args);
72
+ const shouldReject = (rule.check.type === 'reject-if-match' && matched) ||
73
+ (rule.check.type === 'reject-if-no-match' && !matched);
74
+ if (!shouldReject) {
75
+ return { decision: 'allow' };
76
+ }
77
+ return {
78
+ decision: 'reject',
79
+ message: rule.message,
80
+ packId: rule.packId,
81
+ ruleId: rule.id,
82
+ recoveryHint: rule.recoveryHint,
83
+ };
84
+ }
85
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../src/hook/runtime.ts"],"names":[],"mappings":"AAmCA;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,eAAe,CAAC,QAAwB;IACtD,MAAM,MAAM,GAAG,sBAAsB,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,MAAM,KAAK,QAAQ,CAAC,OAAO,EAAE,CAAC;IAC/F,IAAI,QAAQ,CAAC,YAAY,EAAE,CAAC;QAC1B,OAAO,GAAG,MAAM,SAAS,QAAQ,CAAC,YAAY,EAAE,CAAC;IACnD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH;;;;;;;;;GASG;AACH,MAAM,UAAU,GAAG,IAAI,GAAG,EAAkB,CAAC;AAE7C,SAAS,gBAAgB,CAAC,OAAe;IACvC,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACvC,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAC1B,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC;IACrC,UAAU,CAAC,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAClC,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,IAAsB,EAAE,OAAwB;IAC3E,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,OAAO,CAAC,IAAI,EAAE,CAAC;QACvC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;IAC/B,CAAC;IAED,MAAM,YAAY,GAAG,gBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5D,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACrC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;IAC/B,CAAC;IAED,MAAM,UAAU,GAAG,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACxD,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAE9C,MAAM,YAAY,GAChB,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,iBAAiB,IAAI,OAAO,CAAC;QAClD,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,oBAAoB,IAAI,CAAC,OAAO,CAAC,CAAC;IAEzD,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;IAC/B,CAAC;IAED,OAAO;QACL,QAAQ,EAAE,QAAQ;QAClB,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,MAAM,EAAE,IAAI,CAAC,EAAE;QACf,YAAY,EAAE,IAAI,CAAC,YAAY;KAChC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=runtime.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.test.d.ts","sourceRoot":"","sources":["../../src/hook/runtime.test.ts"],"names":[],"mappings":""}