/** * Per-lesson compile cache (Proposal 281 — Per-Lesson Hash Stability). * * Short-circuits `totem lesson compile` for lessons whose source content is * unchanged across compile runs. Eliminates the per-lesson hash rotation on * unrelated lessons when a single lesson is added or modified. * * Cache key is layered (not composite): * 1. `stableId` (when present — reserved for P280; P281 never writes it) * 2. `sourceHash` — sha256 of normalized lesson source content * 3. `fingerprint` — compile_worker_fingerprint from Proposal 278; mismatch * invalidates the entry (e.g., model bump rotates every entry once) * * `cli_version` is intentionally NOT part of the cache key — including it * would invalidate every cohort bump, defeating the purpose. Per the impl * contract § Cache key composition. * * Storage: `.totem/cache/compile-lesson/.json`, * one entry per file, flat directory for v1 (see follow-on for fan-out * cutover at ~1000 lessons). * * Emergency escape: set `TOTEM_DISABLE_COMPILE_CACHE=1` to bypass the * cache entirely (lookup always returns null; write becomes a no-op). * Emergency only — not a long-term flag. */ import { z } from 'zod'; import type { CompileLessonResult } from './compile-lesson.js'; /** * Cache entry for one lesson's compile output. * * `stableId` is reserved for P280 (Wind-Tunnel Decoupling). P281 does not * populate or query it. Reserving the slot from day one avoids a full cache * re-key event when P280 lands; with the reservation, P280's integration is * an additive lookup extension, not a refactor. Per * `mmnto-ai/totem-strategy#387` § Dependencies (load-bearing). */ export declare const CacheEntrySchema: z.ZodObject<{ sourceHash: z.ZodString; stableId: z.ZodOptional; fingerprint: z.ZodString; output: z.ZodDiscriminatedUnion<"status", [z.ZodObject<{ status: z.ZodLiteral<"compiled">; rule: z.ZodRecord; }, "passthrough", z.ZodTypeAny, z.objectOutputType<{ status: z.ZodLiteral<"compiled">; rule: z.ZodRecord; }, z.ZodTypeAny, "passthrough">, z.objectInputType<{ status: z.ZodLiteral<"compiled">; rule: z.ZodRecord; }, z.ZodTypeAny, "passthrough">>, z.ZodObject<{ status: z.ZodLiteral<"skipped">; hash: z.ZodString; reasonCode: z.ZodString; }, "passthrough", z.ZodTypeAny, z.objectOutputType<{ status: z.ZodLiteral<"skipped">; hash: z.ZodString; reasonCode: z.ZodString; }, z.ZodTypeAny, "passthrough">, z.objectInputType<{ status: z.ZodLiteral<"skipped">; hash: z.ZodString; reasonCode: z.ZodString; }, z.ZodTypeAny, "passthrough">>, z.ZodObject<{ status: z.ZodLiteral<"failed">; }, "passthrough", z.ZodTypeAny, z.objectOutputType<{ status: z.ZodLiteral<"failed">; }, z.ZodTypeAny, "passthrough">, z.objectInputType<{ status: z.ZodLiteral<"failed">; }, z.ZodTypeAny, "passthrough">>, z.ZodObject<{ status: z.ZodLiteral<"noop">; }, "passthrough", z.ZodTypeAny, z.objectOutputType<{ status: z.ZodLiteral<"noop">; }, z.ZodTypeAny, "passthrough">, z.objectInputType<{ status: z.ZodLiteral<"noop">; }, z.ZodTypeAny, "passthrough">>]>; compiledAt: z.ZodString; }, "strip", z.ZodTypeAny, { compiledAt: string; output: z.objectOutputType<{ status: z.ZodLiteral<"compiled">; rule: z.ZodRecord; }, z.ZodTypeAny, "passthrough"> | z.objectOutputType<{ status: z.ZodLiteral<"skipped">; hash: z.ZodString; reasonCode: z.ZodString; }, z.ZodTypeAny, "passthrough"> | z.objectOutputType<{ status: z.ZodLiteral<"failed">; }, z.ZodTypeAny, "passthrough"> | z.objectOutputType<{ status: z.ZodLiteral<"noop">; }, z.ZodTypeAny, "passthrough">; sourceHash: string; fingerprint: string; stableId?: string | undefined; }, { compiledAt: string; output: z.objectInputType<{ status: z.ZodLiteral<"compiled">; rule: z.ZodRecord; }, z.ZodTypeAny, "passthrough"> | z.objectInputType<{ status: z.ZodLiteral<"skipped">; hash: z.ZodString; reasonCode: z.ZodString; }, z.ZodTypeAny, "passthrough"> | z.objectInputType<{ status: z.ZodLiteral<"failed">; }, z.ZodTypeAny, "passthrough"> | z.objectInputType<{ status: z.ZodLiteral<"noop">; }, z.ZodTypeAny, "passthrough">; sourceHash: string; fingerprint: string; stableId?: string | undefined; }>; export type CacheEntry = Omit, 'output'> & { output: CompileLessonResult; }; /** * Discrete cache decisions, emitted per lesson per compile run as * telemetry. Maps to the `compile_cache_decision` ledger event's * `activity_name` field. */ export type CacheDecision = 'cache_hit' | 'cache_miss_source_changed' | 'cache_miss_fingerprint_changed' | 'cache_miss_force' | 'cache_miss_no_prior_record'; /** * Compute the cache key for a lesson source. SHA-256 of the content with * line endings normalized to `\n` — same normalization as * `generateInputHash` in compile-manifest.ts so both surfaces produce * identical hashes for identical inputs. */ export declare function computeLessonSourceHash(lessonSource: string): string; /** * Compose the hashable lesson source from a parsed lesson's heading and body. * Use this from any call site that has a parsed `LessonInput`-shaped object to * ensure runtime and migration paths produce identical hashes for the same * lesson. Without a shared composition helper, the runtime hash (computed from * `lesson.heading` + `lesson.body`) and a migration hash (computed from raw * file content with `## Lesson — ` framing) would diverge — exactly the bug * GCA R2 surfaced on `#1983`. */ export declare function composeLessonSourceForHash(heading: string, body: string): string; /** * Resolve the on-disk cache file path for a given source hash. Pure; * does not check existence. Callers handle missing files. */ export declare function cacheEntryPath(totemDir: string, sourceHash: string): string; interface LookupResult { entry: CacheEntry | null; decision: CacheDecision; } /** * Look up a cache entry by `(sourceHash, fingerprint)`. Returns the entry * with `decision: 'cache_hit'` on a clean hit, or `null` with a * decision discriminating the miss reason. * * `stableId` parameter is reserved for P280 wiring — when present, the * lookup tries the stable-id-indexed entry first, falling back to * `sourceHash` on miss. v1 (this PR) never receives a non-undefined * value here. */ export declare function lookupCacheEntry(totemDir: string, sourceHash: string, fingerprint: string, options?: { force?: boolean; stableId?: string; }): LookupResult; /** * Persist a cache entry after a successful compile. Idempotent — writing * the same `(sourceHash, fingerprint, output)` twice is a no-op-shaped * overwrite. Fire-and-forget: I/O failures are surfaced via `onWarn` and * never propagate (a failed cache write should not crash a compile). * * Returns `true` when the entry was successfully persisted, `false` on any * I/O failure or when the cache is disabled via env var. Callers that need * to track migration outcomes (e.g., `migrateFromCompiledRules`) inspect * the return; callers that don't care can ignore it. */ export declare function writeCacheEntry(totemDir: string, entry: CacheEntry, onWarn?: (msg: string) => void): boolean; /** * Construct a `CacheEntry` from the inputs of a fresh compile. Pure; * callers persist via `writeCacheEntry`. */ export declare function buildCacheEntry(sourceHash: string, fingerprint: string, output: CompileLessonResult): CacheEntry; interface MigrationSeedInput { /** Canonical lesson hash (rotation-prone). Carried for diagnostics only. */ lessonHash: string; /** * Parsed lesson heading — the `## Lesson — …` heading-text minus the * leading `##` markdown. MUST match what `readAllLessons` (or equivalent * parser) provides at runtime to the compile path. Composed with `body` * via `composeLessonSourceForHash` to produce the same sourceHash the * runtime cache lookup will compute. */ heading: string; /** * Parsed lesson body — the content between the heading and the next * lesson heading. MUST match what `readAllLessons` provides at runtime. */ body: string; /** The compile output to seed into the cache. */ output: CompileLessonResult; } interface MigrationResult { seeded: number; skipped: number; } /** * One-shot seed migration: walk an existing compiled-rules.json + lesson * sources, materialize the cache entries that would have been produced * if the cache had existed from day one. After this step, the first * post-migration compile run produces 100% cache hits and `compiled- * rules.json` byte-for-byte matches its prior state. * * Idempotent — running twice with the same inputs writes the same entries * a second time (overwrite). Per the impl contract § Migration sequence. * * Heading + body are taken separately (rather than a single raw-source * string) to enforce the canonical `composeLessonSourceForHash` call shape. * If migration accepted a free-form `lessonSource` string, the migration * and runtime hash paths could diverge (per GCA R2 critical on `#1983`). */ export declare function migrateFromCompiledRules(totemDir: string, fingerprint: string, inputs: MigrationSeedInput[], onWarn?: (msg: string) => void): MigrationResult; /** * List all cache-entry filenames currently on disk. Returns the file basenames * (not full paths). Useful for `totem cache --prune-orphans` (out of scope for * v1) and for test isolation cleanup. */ export declare function listCacheEntries(totemDir: string): string[]; export {}; //# sourceMappingURL=compile-cache.d.ts.map