refactor(cli): centralize content registry building

Extract buildRegistryFromIndex and normalizePathKey from the CLI serve
command and the browser completions module into the shared
content-registry module. This ensures both paths produce identical
pathIndex/docContent, including the spark-injection pass that was
previously only applied in the CLI.
This commit is contained in:
hyper
2026-08-07 12:46:08 +08:00
parent e8aa7165cb
commit 0a68122efd
3 changed files with 63 additions and 33 deletions
+50
View File
@@ -63,6 +63,16 @@ export const EMPTY_REGISTRY: ContentRegistry = {
docContent: {},
};
/**
* Normalize a file path into a stable registry key: forward slashes and a
* single leading `/`. Both the CLI filesystem walk and the browser folder
* scan funnel through this so identical files always share a key.
*/
export function normalizePathKey(path: string): string {
const normalized = path.split("\\").join("/");
return normalized.startsWith("/") ? normalized : `/${normalized}`;
}
// ---------------------------------------------------------------------------
// Id / hash derivation (single source of truth)
// ---------------------------------------------------------------------------
@@ -185,6 +195,46 @@ export function scanDoc(content: string, docPath: string): DocScanResult {
return { stripped: rewritten, content: contentStore };
}
/**
* Build a `ContentRegistry` from a raw `{ path: content }` index.
*
* Shared by the CLI (`buildRegistry` in `commands/serve.ts`) and the browser
* folder scan (`scanClientSide` in `components/journal/completions.ts`) so
* both modes produce identical `pathIndex`/`docContent` — including the
* `data-spark` injection pass that only the CLI used to run.
*
* Keys are normalized via `normalizePathKey`; `.md` files are run through
* `scanDoc` and the spark-injection pass, other extensions are stored raw.
*/
export function buildRegistryFromIndex(
index: Record<string, string>,
): ContentRegistry {
const registry: ContentRegistry = { pathIndex: {}, docContent: {} };
for (const [rawPath, content] of Object.entries(index)) {
const path = normalizePathKey(rawPath);
if (path.endsWith(".md")) {
const result = scanDoc(content, path);
registry.pathIndex[path] = result.stripped;
registry.docContent[path] = result.content;
} else {
registry.pathIndex[path] = content;
}
}
// Inject data-spark into real-file spark table directives (idempotent).
for (const [relPath, content] of Object.entries(registry.pathIndex)) {
if (!relPath.endsWith(".md")) continue;
registry.pathIndex[relPath] = injectSparkDirectives(
content,
relPath,
registry,
);
}
return registry;
}
// ---------------------------------------------------------------------------
// Spark table coercion
// ---------------------------------------------------------------------------