/** Cache only semantic inputs. Scope/context stay in the source-range identity; generation is scheduling. * This pure wire-format helper is shared by the runtime and presentation. */ export function cacheConfiguration( identity: string, scope: string, _generation: string, options: Record, inputs: readonly string[], ) { return JSON.stringify({ processor: identity, scope, options: Object.fromEntries([...inputs].sort().map((key) => [key, options[key]])), }); } /** Keep transaction reads bounded, without one serial RPC per historical job. * Relationship order still decides which reusable result wins. */ export async function findCachedJob( history: readonly T[], read: (reference: T) => Promise<{ key: string; status: string }>, key: string, retry: boolean, ) { let retries = 0; for (let offset = 0; offset < history.length; offset += 32) { const batch = history.slice(offset, offset + 32); const records = await Promise.all(batch.map(read)); for (let index = 0; index < batch.length; index++) { const data = records[index]; if (data.key !== key) continue; retries++; if (data.status === "queued" || (!retry && data.status === "ready")) return { reference: batch[index], retries }; } } return { reference: undefined, retries }; } /** Compare persisted semantic configurations without scheduling choices. The * original wire format retained generation; removing it preserves old results * when an author switches between preparation and intent-driven generation. */ export function sameCacheConfiguration(a: string, b: string) { if (a === b) return true; try { const semantic = (raw: string) => { const { processor, scope, options } = JSON.parse(raw); if ( typeof processor !== "string" || typeof scope !== "string" || !options || typeof options !== "object" || Array.isArray(options) ) return null; return JSON.stringify({ processor, scope, options: Object.fromEntries(Object.entries(options).sort(([a], [b]) => a.localeCompare(b))), }); }; const left = semantic(a); return left !== null && left === semantic(b); } catch { return false; } }