From 3a705cb5013b5b7b95180c7eef7b2806cef543da Mon Sep 17 00:00:00 2001 From: Lex Christopherson Date: Sat, 14 Mar 2026 13:34:40 -0600 Subject: [PATCH] perf: add content-hash-keyed parse cache for file parsing parseRoadmap, parsePlan, parseSummary, and parseContinue are pure functions that get called repeatedly with the same content during deriveState dispatch cycles. A module-scoped Map keyed by a fast composite key (length + first 100 chars + last 100 chars) avoids redundant parsing. Cache caps at 50 entries and clears when full. Exports clearParseCache() for explicit invalidation. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/resources/extensions/gsd/files.ts | 46 ++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/src/resources/extensions/gsd/files.ts b/src/resources/extensions/gsd/files.ts index 1e88b4161..7e4c135e1 100644 --- a/src/resources/extensions/gsd/files.ts +++ b/src/resources/extensions/gsd/files.ts @@ -22,6 +22,34 @@ import { checkExistingEnvKeys } from '../get-secrets-from-user.js'; import { parseRoadmapSlices } from './roadmap-slices.js'; import { nativeParseRoadmap, nativeExtractSection, NATIVE_UNAVAILABLE } from './native-parser-bridge.js'; +// ─── Parse Cache ────────────────────────────────────────────────────────── + +const CACHE_MAX = 50; + +/** Fast composite key: length + first/last 100 chars. Unique enough for distinct markdown files. */ +function cacheKey(content: string): string { + const len = content.length; + const head = content.slice(0, 100); + const tail = len > 100 ? content.slice(-100) : ''; + return `${len}:${head}:${tail}`; +} + +const _parseCache = new Map(); + +function cachedParse(content: string, tag: string, parseFn: (c: string) => T): T { + const key = tag + '|' + cacheKey(content); + if (_parseCache.has(key)) return _parseCache.get(key) as T; + if (_parseCache.size >= CACHE_MAX) _parseCache.clear(); + const result = parseFn(content); + _parseCache.set(key, result); + return result; +} + +/** Clear the module-scoped parse cache. Call when files change on disk. */ +export function clearParseCache(): void { + _parseCache.clear(); +} + // ─── Helpers ─────────────────────────────────────────────────────────────── /** @@ -188,6 +216,10 @@ export function extractBoldField(text: string, key: string): string | null { // ─── Roadmap Parser ──────────────────────────────────────────────────────── export function parseRoadmap(content: string): Roadmap { + return cachedParse(content, 'roadmap', _parseRoadmapImpl); +} + +function _parseRoadmapImpl(content: string): Roadmap { // Try native parser first for better performance const nativeResult = nativeParseRoadmap(content); if (nativeResult) return nativeResult; @@ -210,7 +242,7 @@ export function parseRoadmap(content: string): Roadmap { })(); const successCriteria = scSection ? parseBullets(scSection) : []; - // Slices + // Slices const slices = parseRoadmapSlices(content); // Boundary map @@ -318,6 +350,10 @@ export function formatSecretsManifest(manifest: SecretsManifest): string { // ─── Slice Plan Parser ───────────────────────────────────────────────────── export function parsePlan(content: string): SlicePlan { + return cachedParse(content, 'plan', _parsePlanImpl); +} + +function _parsePlanImpl(content: string): SlicePlan { const lines = content.split('\n'); const h1 = lines.find(l => l.startsWith('# ')); @@ -396,6 +432,10 @@ export function parsePlan(content: string): SlicePlan { // ─── Summary Parser ──────────────────────────────────────────────────────── export function parseSummary(content: string): Summary { + return cachedParse(content, 'summary', _parseSummaryImpl); +} + +function _parseSummaryImpl(content: string): Summary { const [fmLines, body] = splitFrontmatter(content); const fm = fmLines ? parseFrontmatterMap(fmLines) : {}; @@ -460,6 +500,10 @@ export function parseSummary(content: string): Summary { // ─── Continue Parser ─────────────────────────────────────────────────────── export function parseContinue(content: string): Continue { + return cachedParse(content, 'continue', _parseContinueImpl); +} + +function _parseContinueImpl(content: string): Continue { const [fmLines, body] = splitFrontmatter(content); const fm = fmLines ? parseFrontmatterMap(fmLines) : {};