@mmnto/totem 1.44.0 → 1.46.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.
@@ -2,7 +2,7 @@ import { Lang, parse } from '@ast-grep/napi';
2
2
  import { extensionToLanguage } from './ast-classifier.js';
3
3
  import { rethrowAsParseError } from './errors.js';
4
4
  // ─── Constants ──────────────────────────────────────
5
- const AST_GREP_HINT = 'Check the rule pattern syntax. If valid, the source file may contain syntax that crashes the parser.';
5
+ const AST_GREP_HINT = "Check the rule pattern syntax. If valid, the source file may contain syntax that crashes the parser, OR the language isn't supported on this platform (e.g. Rust on Windows). Operator escape: run `totem lint --ast-parse-mode lenient` (or set TOTEM_LINT_AST_PARSE_MODE=lenient) to skip AST rules. Durable per-file degrade tracked at mmnto-ai/totem#1786.";
6
6
  // ─── Language mapping ───────────────────────────────
7
7
  /**
8
8
  * Map our `SupportedLanguage` string (e.g., `'typescript'`) to the
@@ -1 +1 @@
1
- {"version":3,"file":"ast-grep-query.js","sourceRoot":"","sources":["../src/ast-grep-query.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ast-grep-query.js","sourceRoot":"","sources":["../src/ast-grep-query.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Per-lesson compile cache (Proposal 281 — Per-Lesson Hash Stability).
3
+ *
4
+ * Short-circuits `totem lesson compile` for lessons whose source content is
5
+ * unchanged across compile runs. Eliminates the per-lesson hash rotation on
6
+ * unrelated lessons when a single lesson is added or modified.
7
+ *
8
+ * Cache key is layered (not composite):
9
+ * 1. `stableId` (when present — reserved for P280; P281 never writes it)
10
+ * 2. `sourceHash` — sha256 of normalized lesson source content
11
+ * 3. `fingerprint` — compile_worker_fingerprint from Proposal 278; mismatch
12
+ * invalidates the entry (e.g., model bump rotates every entry once)
13
+ *
14
+ * `cli_version` is intentionally NOT part of the cache key — including it
15
+ * would invalidate every cohort bump, defeating the purpose. Per the impl
16
+ * contract § Cache key composition.
17
+ *
18
+ * Storage: `.totem/cache/compile-lesson/<sourceHash-first-16-chars>.json`,
19
+ * one entry per file, flat directory for v1 (see follow-on for fan-out
20
+ * cutover at ~1000 lessons).
21
+ *
22
+ * Emergency escape: set `TOTEM_DISABLE_COMPILE_CACHE=1` to bypass the
23
+ * cache entirely (lookup always returns null; write becomes a no-op).
24
+ * Emergency only — not a long-term flag.
25
+ */
26
+ import { z } from 'zod';
27
+ import type { CompileLessonResult } from './compile-lesson.js';
28
+ /**
29
+ * Cache entry for one lesson's compile output.
30
+ *
31
+ * `stableId` is reserved for P280 (Wind-Tunnel Decoupling). P281 does not
32
+ * populate or query it. Reserving the slot from day one avoids a full cache
33
+ * re-key event when P280 lands; with the reservation, P280's integration is
34
+ * an additive lookup extension, not a refactor. Per
35
+ * `mmnto-ai/totem-strategy#387` § Dependencies (load-bearing).
36
+ */
37
+ export declare const CacheEntrySchema: z.ZodObject<{
38
+ sourceHash: z.ZodString;
39
+ stableId: z.ZodOptional<z.ZodString>;
40
+ fingerprint: z.ZodString;
41
+ output: z.ZodDiscriminatedUnion<"status", [z.ZodObject<{
42
+ status: z.ZodLiteral<"compiled">;
43
+ rule: z.ZodRecord<z.ZodString, z.ZodUnknown>;
44
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
45
+ status: z.ZodLiteral<"compiled">;
46
+ rule: z.ZodRecord<z.ZodString, z.ZodUnknown>;
47
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
48
+ status: z.ZodLiteral<"compiled">;
49
+ rule: z.ZodRecord<z.ZodString, z.ZodUnknown>;
50
+ }, z.ZodTypeAny, "passthrough">>, z.ZodObject<{
51
+ status: z.ZodLiteral<"skipped">;
52
+ hash: z.ZodString;
53
+ reasonCode: z.ZodString;
54
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
55
+ status: z.ZodLiteral<"skipped">;
56
+ hash: z.ZodString;
57
+ reasonCode: z.ZodString;
58
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
59
+ status: z.ZodLiteral<"skipped">;
60
+ hash: z.ZodString;
61
+ reasonCode: z.ZodString;
62
+ }, z.ZodTypeAny, "passthrough">>, z.ZodObject<{
63
+ status: z.ZodLiteral<"failed">;
64
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
65
+ status: z.ZodLiteral<"failed">;
66
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
67
+ status: z.ZodLiteral<"failed">;
68
+ }, z.ZodTypeAny, "passthrough">>, z.ZodObject<{
69
+ status: z.ZodLiteral<"noop">;
70
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
71
+ status: z.ZodLiteral<"noop">;
72
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
73
+ status: z.ZodLiteral<"noop">;
74
+ }, z.ZodTypeAny, "passthrough">>]>;
75
+ compiledAt: z.ZodString;
76
+ }, "strip", z.ZodTypeAny, {
77
+ compiledAt: string;
78
+ sourceHash: string;
79
+ fingerprint: string;
80
+ output: z.objectOutputType<{
81
+ status: z.ZodLiteral<"compiled">;
82
+ rule: z.ZodRecord<z.ZodString, z.ZodUnknown>;
83
+ }, z.ZodTypeAny, "passthrough"> | z.objectOutputType<{
84
+ status: z.ZodLiteral<"skipped">;
85
+ hash: z.ZodString;
86
+ reasonCode: z.ZodString;
87
+ }, z.ZodTypeAny, "passthrough"> | z.objectOutputType<{
88
+ status: z.ZodLiteral<"failed">;
89
+ }, z.ZodTypeAny, "passthrough"> | z.objectOutputType<{
90
+ status: z.ZodLiteral<"noop">;
91
+ }, z.ZodTypeAny, "passthrough">;
92
+ stableId?: string | undefined;
93
+ }, {
94
+ compiledAt: string;
95
+ sourceHash: string;
96
+ fingerprint: string;
97
+ output: z.objectInputType<{
98
+ status: z.ZodLiteral<"compiled">;
99
+ rule: z.ZodRecord<z.ZodString, z.ZodUnknown>;
100
+ }, z.ZodTypeAny, "passthrough"> | z.objectInputType<{
101
+ status: z.ZodLiteral<"skipped">;
102
+ hash: z.ZodString;
103
+ reasonCode: z.ZodString;
104
+ }, z.ZodTypeAny, "passthrough"> | z.objectInputType<{
105
+ status: z.ZodLiteral<"failed">;
106
+ }, z.ZodTypeAny, "passthrough"> | z.objectInputType<{
107
+ status: z.ZodLiteral<"noop">;
108
+ }, z.ZodTypeAny, "passthrough">;
109
+ stableId?: string | undefined;
110
+ }>;
111
+ export type CacheEntry = Omit<z.infer<typeof CacheEntrySchema>, 'output'> & {
112
+ output: CompileLessonResult;
113
+ };
114
+ /**
115
+ * Discrete cache decisions, emitted per lesson per compile run as
116
+ * telemetry. Maps to the `compile_cache_decision` ledger event's
117
+ * `activity_name` field.
118
+ */
119
+ export type CacheDecision = 'cache_hit' | 'cache_miss_source_changed' | 'cache_miss_fingerprint_changed' | 'cache_miss_force' | 'cache_miss_no_prior_record';
120
+ /**
121
+ * Compute the cache key for a lesson source. SHA-256 of the content with
122
+ * line endings normalized to `\n` — same normalization as
123
+ * `generateInputHash` in compile-manifest.ts so both surfaces produce
124
+ * identical hashes for identical inputs.
125
+ */
126
+ export declare function computeLessonSourceHash(lessonSource: string): string;
127
+ /**
128
+ * Compose the hashable lesson source from a parsed lesson's heading and body.
129
+ * Use this from any call site that has a parsed `LessonInput`-shaped object to
130
+ * ensure runtime and migration paths produce identical hashes for the same
131
+ * lesson. Without a shared composition helper, the runtime hash (computed from
132
+ * `lesson.heading` + `lesson.body`) and a migration hash (computed from raw
133
+ * file content with `## Lesson — ` framing) would diverge — exactly the bug
134
+ * GCA R2 surfaced on `#1983`.
135
+ */
136
+ export declare function composeLessonSourceForHash(heading: string, body: string): string;
137
+ /**
138
+ * Resolve the on-disk cache file path for a given source hash. Pure;
139
+ * does not check existence. Callers handle missing files.
140
+ */
141
+ export declare function cacheEntryPath(totemDir: string, sourceHash: string): string;
142
+ interface LookupResult {
143
+ entry: CacheEntry | null;
144
+ decision: CacheDecision;
145
+ }
146
+ /**
147
+ * Look up a cache entry by `(sourceHash, fingerprint)`. Returns the entry
148
+ * with `decision: 'cache_hit'` on a clean hit, or `null` with a
149
+ * decision discriminating the miss reason.
150
+ *
151
+ * `stableId` parameter is reserved for P280 wiring — when present, the
152
+ * lookup tries the stable-id-indexed entry first, falling back to
153
+ * `sourceHash` on miss. v1 (this PR) never receives a non-undefined
154
+ * value here.
155
+ */
156
+ export declare function lookupCacheEntry(totemDir: string, sourceHash: string, fingerprint: string, options?: {
157
+ force?: boolean;
158
+ stableId?: string;
159
+ }): LookupResult;
160
+ /**
161
+ * Persist a cache entry after a successful compile. Idempotent — writing
162
+ * the same `(sourceHash, fingerprint, output)` twice is a no-op-shaped
163
+ * overwrite. Fire-and-forget: I/O failures are surfaced via `onWarn` and
164
+ * never propagate (a failed cache write should not crash a compile).
165
+ *
166
+ * Returns `true` when the entry was successfully persisted, `false` on any
167
+ * I/O failure or when the cache is disabled via env var. Callers that need
168
+ * to track migration outcomes (e.g., `migrateFromCompiledRules`) inspect
169
+ * the return; callers that don't care can ignore it.
170
+ */
171
+ export declare function writeCacheEntry(totemDir: string, entry: CacheEntry, onWarn?: (msg: string) => void): boolean;
172
+ /**
173
+ * Construct a `CacheEntry` from the inputs of a fresh compile. Pure;
174
+ * callers persist via `writeCacheEntry`.
175
+ */
176
+ export declare function buildCacheEntry(sourceHash: string, fingerprint: string, output: CompileLessonResult): CacheEntry;
177
+ interface MigrationSeedInput {
178
+ /** Canonical lesson hash (rotation-prone). Carried for diagnostics only. */
179
+ lessonHash: string;
180
+ /**
181
+ * Parsed lesson heading — the `## Lesson — …` heading-text minus the
182
+ * leading `##` markdown. MUST match what `readAllLessons` (or equivalent
183
+ * parser) provides at runtime to the compile path. Composed with `body`
184
+ * via `composeLessonSourceForHash` to produce the same sourceHash the
185
+ * runtime cache lookup will compute.
186
+ */
187
+ heading: string;
188
+ /**
189
+ * Parsed lesson body — the content between the heading and the next
190
+ * lesson heading. MUST match what `readAllLessons` provides at runtime.
191
+ */
192
+ body: string;
193
+ /** The compile output to seed into the cache. */
194
+ output: CompileLessonResult;
195
+ }
196
+ interface MigrationResult {
197
+ seeded: number;
198
+ skipped: number;
199
+ }
200
+ /**
201
+ * One-shot seed migration: walk an existing compiled-rules.json + lesson
202
+ * sources, materialize the cache entries that would have been produced
203
+ * if the cache had existed from day one. After this step, the first
204
+ * post-migration compile run produces 100% cache hits and `compiled-
205
+ * rules.json` byte-for-byte matches its prior state.
206
+ *
207
+ * Idempotent — running twice with the same inputs writes the same entries
208
+ * a second time (overwrite). Per the impl contract § Migration sequence.
209
+ *
210
+ * Heading + body are taken separately (rather than a single raw-source
211
+ * string) to enforce the canonical `composeLessonSourceForHash` call shape.
212
+ * If migration accepted a free-form `lessonSource` string, the migration
213
+ * and runtime hash paths could diverge (per GCA R2 critical on `#1983`).
214
+ */
215
+ export declare function migrateFromCompiledRules(totemDir: string, fingerprint: string, inputs: MigrationSeedInput[], onWarn?: (msg: string) => void): MigrationResult;
216
+ /**
217
+ * List all cache-entry filenames currently on disk. Returns the file basenames
218
+ * (not full paths). Useful for `totem cache --prune-orphans` (out of scope for
219
+ * v1) and for test isolation cleanup.
220
+ */
221
+ export declare function listCacheEntries(totemDir: string): string[];
222
+ export {};
223
+ //# sourceMappingURL=compile-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compile-cache.d.ts","sourceRoot":"","sources":["../src/compile-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAMH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAsF/D;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAM3B,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,EAAE,QAAQ,CAAC,GAAG;IAC1E,MAAM,EAAE,mBAAmB,CAAC;CAC7B,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,aAAa,GACrB,WAAW,GACX,2BAA2B,GAC3B,gCAAgC,GAChC,kBAAkB,GAClB,4BAA4B,CAAC;AAIjC;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAWpE;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhF;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAc3E;AAED,UAAU,YAAY;IACpB,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,aAAa,CAAC;CACzB;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GACnD,YAAY,CA4Cd;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,UAAU,EACjB,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,GAC7B,OAAO,CAiBT;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,mBAAmB,GAC1B,UAAU,CAOZ;AAED,UAAU,kBAAkB;IAC1B,4EAA4E;IAC5E,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;OAMG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,MAAM,EAAE,mBAAmB,CAAC;CAC7B;AAED,UAAU,eAAe;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,kBAAkB,EAAE,EAC5B,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,GAC7B,eAAe,CA8BjB;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAM3D"}
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Per-lesson compile cache (Proposal 281 — Per-Lesson Hash Stability).
3
+ *
4
+ * Short-circuits `totem lesson compile` for lessons whose source content is
5
+ * unchanged across compile runs. Eliminates the per-lesson hash rotation on
6
+ * unrelated lessons when a single lesson is added or modified.
7
+ *
8
+ * Cache key is layered (not composite):
9
+ * 1. `stableId` (when present — reserved for P280; P281 never writes it)
10
+ * 2. `sourceHash` — sha256 of normalized lesson source content
11
+ * 3. `fingerprint` — compile_worker_fingerprint from Proposal 278; mismatch
12
+ * invalidates the entry (e.g., model bump rotates every entry once)
13
+ *
14
+ * `cli_version` is intentionally NOT part of the cache key — including it
15
+ * would invalidate every cohort bump, defeating the purpose. Per the impl
16
+ * contract § Cache key composition.
17
+ *
18
+ * Storage: `.totem/cache/compile-lesson/<sourceHash-first-16-chars>.json`,
19
+ * one entry per file, flat directory for v1 (see follow-on for fan-out
20
+ * cutover at ~1000 lessons).
21
+ *
22
+ * Emergency escape: set `TOTEM_DISABLE_COMPILE_CACHE=1` to bypass the
23
+ * cache entirely (lookup always returns null; write becomes a no-op).
24
+ * Emergency only — not a long-term flag.
25
+ */
26
+ import * as crypto from 'node:crypto';
27
+ import * as fs from 'node:fs';
28
+ import * as path from 'node:path';
29
+ import { z } from 'zod';
30
+ // ─── Constants ──────────────────────────────────────
31
+ // CACHE_DIR is resolved relative to `totemDir` (the already-configured
32
+ // `.totem` location). Including `.totem` in this constant duplicated the
33
+ // prefix and landed cache files at `<totemDir>/.totem/cache/...` instead of
34
+ // `<totemDir>/cache/...`. Per CR R3 Major on `mmnto-ai/totem#1983`.
35
+ const CACHE_DIR = path.join('cache', 'compile-lesson');
36
+ const HASH_FILENAME_PREFIX_LENGTH = 16;
37
+ const DISABLE_ENV_VAR = 'TOTEM_DISABLE_COMPILE_CACHE';
38
+ /**
39
+ * Strict sha256-hex shape for `sourceHash`. Used to gate `cacheEntryPath`
40
+ * against path-traversal inputs (`/`, `..`, etc.) per CR R4 Major on
41
+ * `mmnto-ai/totem#1983`. Matches the digest computeLessonSourceHash emits.
42
+ */
43
+ const SOURCE_HASH_RE = /^[a-f0-9]{64}$/;
44
+ /**
45
+ * Best-effort wrapper around an optional warning callback. The cache module's
46
+ * non-throwing contract requires that a caller-supplied `onWarn` cannot
47
+ * propagate exceptions out of cache write/seed paths — even an `onWarn` that
48
+ * throws must not abort compile. Per CR R4 Major on `mmnto-ai/totem#1983`.
49
+ */
50
+ function safeOnWarn(onWarn, message) {
51
+ if (onWarn === undefined)
52
+ return;
53
+ try {
54
+ onWarn(message);
55
+ // totem-context: intentional cleanup — onWarn must never propagate exceptions per the cache's non-throwing contract; swallow any caller-thrown errors silently.
56
+ }
57
+ catch (err) {
58
+ // totem-context: intentional cleanup — onWarn must never propagate exceptions per the cache's non-throwing contract; swallow any caller-thrown errors silently.
59
+ void err;
60
+ }
61
+ }
62
+ // ─── Schema ─────────────────────────────────────────
63
+ /**
64
+ * Validator for the `output` field. CompileLessonResult is a discriminated
65
+ * union (status: 'compiled' | 'skipped' | 'failed' | 'noop'); each variant
66
+ * carries required fields that downstream consumers dereference unconditionally
67
+ * (e.g., the cache-hit path reads `result.rule` for compiled outputs and
68
+ * `result.hash` / `result.reasonCode` for skipped outputs). A minimal
69
+ * status-only schema would accept truncated cache files like
70
+ * `{ output: { status: 'compiled' } }` and break the file's own "bad cache
71
+ * entry → miss, not crash" contract on the next lookup.
72
+ *
73
+ * Per CR R3 Major on `mmnto-ai/totem#1983`: validate the required-per-variant
74
+ * shape so structurally incomplete payloads are rejected upfront. `rule` is
75
+ * `z.unknown()` to avoid coupling this module to the full `CompiledRule`
76
+ * shape (changes to that schema must not invalidate every cohort's cache);
77
+ * variant-required fields like `hash` and `reasonCode` are typed concretely
78
+ * since they're the dereferences that would crash.
79
+ */
80
+ const CompileLessonResultMinimalSchema = z.discriminatedUnion('status', [
81
+ z
82
+ .object({
83
+ status: z.literal('compiled'),
84
+ // `rule` MUST be a non-null object (the CompiledRule payload). z.unknown()
85
+ // would accept missing fields too, which defeats the contract — a payload
86
+ // like `{ status: 'compiled' }` would survive validation and crash the
87
+ // cache-hit path on `result.rule` dereference. `z.record(z.unknown())`
88
+ // requires an object without coupling to CompiledRule's exact shape.
89
+ rule: z.record(z.unknown()),
90
+ })
91
+ .passthrough(),
92
+ z
93
+ .object({
94
+ status: z.literal('skipped'),
95
+ hash: z.string(),
96
+ reasonCode: z.string(),
97
+ })
98
+ .passthrough(),
99
+ z
100
+ .object({
101
+ status: z.literal('failed'),
102
+ })
103
+ .passthrough(),
104
+ z
105
+ .object({
106
+ status: z.literal('noop'),
107
+ })
108
+ .passthrough(),
109
+ ]);
110
+ /**
111
+ * Cache entry for one lesson's compile output.
112
+ *
113
+ * `stableId` is reserved for P280 (Wind-Tunnel Decoupling). P281 does not
114
+ * populate or query it. Reserving the slot from day one avoids a full cache
115
+ * re-key event when P280 lands; with the reservation, P280's integration is
116
+ * an additive lookup extension, not a refactor. Per
117
+ * `mmnto-ai/totem-strategy#387` § Dependencies (load-bearing).
118
+ */
119
+ export const CacheEntrySchema = z.object({
120
+ sourceHash: z.string(),
121
+ stableId: z.string().optional(),
122
+ fingerprint: z.string(),
123
+ output: CompileLessonResultMinimalSchema,
124
+ compiledAt: z.string(),
125
+ });
126
+ // ─── Public API ─────────────────────────────────────
127
+ /**
128
+ * Compute the cache key for a lesson source. SHA-256 of the content with
129
+ * line endings normalized to `\n` — same normalization as
130
+ * `generateInputHash` in compile-manifest.ts so both surfaces produce
131
+ * identical hashes for identical inputs.
132
+ */
133
+ export function computeLessonSourceHash(lessonSource) {
134
+ // Normalization: collapse CRLF → LF ONLY. No trailing-whitespace trim — the
135
+ // canonical `generateInputHash` (`compile-manifest.ts`) and `lessonHash` (per
136
+ // `.gemini/styleguide.md` line 142) are both trailing-whitespace-sensitive.
137
+ // Trimming here would desynchronize the cache key from the canonical hashes:
138
+ // a whitespace-only edit would hit the cache (return stale lessonHash output)
139
+ // while the manifest pipeline would compute a fresh lessonHash for the same
140
+ // edit, breaking the deterministic link between lessons and rules.
141
+ // Reverses an over-correction from R1; per GCA R2 critical on `#1983`.
142
+ const normalized = lessonSource.replace(/\r\n/g, '\n');
143
+ return crypto.createHash('sha256').update(normalized).digest('hex');
144
+ }
145
+ /**
146
+ * Compose the hashable lesson source from a parsed lesson's heading and body.
147
+ * Use this from any call site that has a parsed `LessonInput`-shaped object to
148
+ * ensure runtime and migration paths produce identical hashes for the same
149
+ * lesson. Without a shared composition helper, the runtime hash (computed from
150
+ * `lesson.heading` + `lesson.body`) and a migration hash (computed from raw
151
+ * file content with `## Lesson — ` framing) would diverge — exactly the bug
152
+ * GCA R2 surfaced on `#1983`.
153
+ */
154
+ export function composeLessonSourceForHash(heading, body) {
155
+ return `${heading}\n${body}`;
156
+ }
157
+ /**
158
+ * Resolve the on-disk cache file path for a given source hash. Pure;
159
+ * does not check existence. Callers handle missing files.
160
+ */
161
+ export function cacheEntryPath(totemDir, sourceHash) {
162
+ // Reject non-SHA inputs at the boundary (CR R4 Major on `mmnto-ai/totem#1983`).
163
+ // Without this, a sourceHash carrying `/` or `..` would flow through
164
+ // path.join() and escape `<totemDir>/cache/compile-lesson`, letting lookup or
165
+ // write touch arbitrary files. computeLessonSourceHash always emits the
166
+ // matching 64-char hex shape; this catches misuse where a caller hands us
167
+ // unvalidated input.
168
+ if (!SOURCE_HASH_RE.test(sourceHash)) {
169
+ throw new Error(`[Totem Error] Invalid sourceHash: expected 64-char sha256 hex digest, got ${JSON.stringify(sourceHash.slice(0, 80))}`);
170
+ }
171
+ const filename = `${sourceHash.slice(0, HASH_FILENAME_PREFIX_LENGTH)}.json`;
172
+ return path.join(totemDir, CACHE_DIR, filename);
173
+ }
174
+ /**
175
+ * Look up a cache entry by `(sourceHash, fingerprint)`. Returns the entry
176
+ * with `decision: 'cache_hit'` on a clean hit, or `null` with a
177
+ * decision discriminating the miss reason.
178
+ *
179
+ * `stableId` parameter is reserved for P280 wiring — when present, the
180
+ * lookup tries the stable-id-indexed entry first, falling back to
181
+ * `sourceHash` on miss. v1 (this PR) never receives a non-undefined
182
+ * value here.
183
+ */
184
+ export function lookupCacheEntry(totemDir, sourceHash, fingerprint, options = {}) {
185
+ if (options.force === true) {
186
+ return { entry: null, decision: 'cache_miss_force' };
187
+ }
188
+ if (process.env[DISABLE_ENV_VAR] === '1') {
189
+ // totem-context: intentional cleanup — emergency escape hatch reverts
190
+ // cache to no-op without throwing.
191
+ return { entry: null, decision: 'cache_miss_no_prior_record' };
192
+ }
193
+ const filePath = cacheEntryPath(totemDir, sourceHash);
194
+ if (!fs.existsSync(filePath)) {
195
+ return { entry: null, decision: 'cache_miss_no_prior_record' };
196
+ }
197
+ let raw;
198
+ try {
199
+ const content = fs.readFileSync(filePath, 'utf-8');
200
+ raw = JSON.parse(content);
201
+ // totem-context: intentional cleanup — a bad cache entry must downgrade to a miss, never propagate; the cache is a side-channel optimization, not a load-bearing data path.
202
+ }
203
+ catch (err) {
204
+ // totem-context: intentional cleanup — a bad cache entry must downgrade to a miss, never propagate; the cache is a side-channel optimization, not a load-bearing data path.
205
+ void err;
206
+ return { entry: null, decision: 'cache_miss_no_prior_record' };
207
+ }
208
+ const parsed = CacheEntrySchema.safeParse(raw);
209
+ if (!parsed.success) {
210
+ return { entry: null, decision: 'cache_miss_no_prior_record' };
211
+ }
212
+ // CacheEntrySchema validates the discriminator only; downstream casts back
213
+ // to the rich CompileLessonResult union type. See COMPILE_LESSON_STATUS.
214
+ const entry = parsed.data;
215
+ if (entry.sourceHash !== sourceHash) {
216
+ // File was named by hash but its content disagrees — corrupted state,
217
+ // treat as miss. Defensive against manual cache edits.
218
+ return { entry: null, decision: 'cache_miss_source_changed' };
219
+ }
220
+ if (entry.fingerprint !== fingerprint) {
221
+ return { entry: null, decision: 'cache_miss_fingerprint_changed' };
222
+ }
223
+ return { entry, decision: 'cache_hit' };
224
+ }
225
+ /**
226
+ * Persist a cache entry after a successful compile. Idempotent — writing
227
+ * the same `(sourceHash, fingerprint, output)` twice is a no-op-shaped
228
+ * overwrite. Fire-and-forget: I/O failures are surfaced via `onWarn` and
229
+ * never propagate (a failed cache write should not crash a compile).
230
+ *
231
+ * Returns `true` when the entry was successfully persisted, `false` on any
232
+ * I/O failure or when the cache is disabled via env var. Callers that need
233
+ * to track migration outcomes (e.g., `migrateFromCompiledRules`) inspect
234
+ * the return; callers that don't care can ignore it.
235
+ */
236
+ export function writeCacheEntry(totemDir, entry, onWarn) {
237
+ if (process.env[DISABLE_ENV_VAR] === '1') {
238
+ return false;
239
+ }
240
+ try {
241
+ const filePath = cacheEntryPath(totemDir, entry.sourceHash);
242
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
243
+ fs.writeFileSync(filePath, JSON.stringify(entry, null, 2) + '\n', 'utf-8');
244
+ return true;
245
+ // totem-context: intentional cleanup — cache writes are fire-and-forget; a failed write degrades to "no cache entry exists" on next lookup, never blocks compile.
246
+ }
247
+ catch (err) {
248
+ // totem-context: intentional cleanup — cache writes are fire-and-forget; a failed write degrades to "no cache entry exists" on next lookup, never blocks compile.
249
+ const msg = err instanceof Error ? err.message : String(err);
250
+ safeOnWarn(onWarn, `Compile cache write failed: ${msg}`);
251
+ return false;
252
+ }
253
+ }
254
+ /**
255
+ * Construct a `CacheEntry` from the inputs of a fresh compile. Pure;
256
+ * callers persist via `writeCacheEntry`.
257
+ */
258
+ export function buildCacheEntry(sourceHash, fingerprint, output) {
259
+ return {
260
+ sourceHash,
261
+ fingerprint,
262
+ output,
263
+ compiledAt: new Date().toISOString(),
264
+ };
265
+ }
266
+ /**
267
+ * One-shot seed migration: walk an existing compiled-rules.json + lesson
268
+ * sources, materialize the cache entries that would have been produced
269
+ * if the cache had existed from day one. After this step, the first
270
+ * post-migration compile run produces 100% cache hits and `compiled-
271
+ * rules.json` byte-for-byte matches its prior state.
272
+ *
273
+ * Idempotent — running twice with the same inputs writes the same entries
274
+ * a second time (overwrite). Per the impl contract § Migration sequence.
275
+ *
276
+ * Heading + body are taken separately (rather than a single raw-source
277
+ * string) to enforce the canonical `composeLessonSourceForHash` call shape.
278
+ * If migration accepted a free-form `lessonSource` string, the migration
279
+ * and runtime hash paths could diverge (per GCA R2 critical on `#1983`).
280
+ */
281
+ export function migrateFromCompiledRules(totemDir, fingerprint, inputs, onWarn) {
282
+ if (process.env[DISABLE_ENV_VAR] === '1') {
283
+ return { seeded: 0, skipped: inputs.length };
284
+ }
285
+ let seeded = 0;
286
+ let skipped = 0;
287
+ for (const input of inputs) {
288
+ try {
289
+ const sourceForHash = composeLessonSourceForHash(input.heading, input.body);
290
+ const sourceHash = computeLessonSourceHash(sourceForHash);
291
+ const entry = buildCacheEntry(sourceHash, fingerprint, input.output);
292
+ // writeCacheEntry returns false on I/O failure or when the cache is
293
+ // disabled. Both count as "skipped" — seeded reflects entries actually
294
+ // persisted to disk, matching the operator's mental model of the
295
+ // post-migration cache hit rate.
296
+ if (writeCacheEntry(totemDir, entry, onWarn)) {
297
+ seeded += 1;
298
+ }
299
+ else {
300
+ skipped += 1;
301
+ }
302
+ // totem-context: intentional cleanup — migration is best-effort per-input; one bad lesson source must not abort seeding the rest. Failed inputs accumulate in `skipped` count and surface via safeOnWarn for operator visibility.
303
+ }
304
+ catch (err) {
305
+ // totem-context: intentional cleanup — migration is best-effort per-input; one bad lesson source must not abort seeding the rest. Failed inputs accumulate in `skipped` count and surface via safeOnWarn for operator visibility.
306
+ const msg = err instanceof Error ? err.message : String(err);
307
+ safeOnWarn(onWarn, `Compile cache seed skipped for ${input.lessonHash}: ${msg}`);
308
+ skipped += 1;
309
+ }
310
+ }
311
+ return { seeded, skipped };
312
+ }
313
+ /**
314
+ * List all cache-entry filenames currently on disk. Returns the file basenames
315
+ * (not full paths). Useful for `totem cache --prune-orphans` (out of scope for
316
+ * v1) and for test isolation cleanup.
317
+ */
318
+ export function listCacheEntries(totemDir) {
319
+ const dir = path.join(totemDir, CACHE_DIR);
320
+ if (!fs.existsSync(dir)) {
321
+ return [];
322
+ }
323
+ return fs.readdirSync(dir).filter((name) => name.endsWith('.json'));
324
+ }
325
+ //# sourceMappingURL=compile-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compile-cache.js","sourceRoot":"","sources":["../src/compile-cache.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Tests for the per-lesson compile cache (Proposal 281).
3
+ *
4
+ * Falsifying-metric test is `partial_mutation_invariant` — exercises the
5
+ * load-bearing claim that a +1-lesson PR produces a 1-row compiled-rules
6
+ * delta, not a 130-row rotation.
7
+ */
8
+ export {};
9
+ //# sourceMappingURL=compile-cache.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compile-cache.test.d.ts","sourceRoot":"","sources":["../src/compile-cache.test.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}