/* RFC-0555: DNA-22 path validation for workpiece.read and workpiece.write commands. Checks whether a relative file path falls within the client-editable surface declared in system.md clientEditable[] (DNA-22, RFC-0025; amended by RFC-0031). Does not validate clientEditable[] entries themselves — that is client.edit.validate's job. Does not handle system.md partial field edits — future enhancement. RFC-0555: initial DNA-22 path validation for workpiece commands. */ import path from "node:path"; import { loadSystemManifest } from "@warpgogol/werkstatt-shared/content"; const CONTENT_PREFIX = "src/content/"; export interface ClientEditableChecker { isClientEditable(workpieceRoot: string, relativePath: string): Promise; } export async function isClientEditable( workpieceRoot: string, relativePath: string, ): Promise { const normalizedPath = relativePath.replace(/\\/g, "/").replace(/^\.\//, ""); const contentDir = path.join(workpieceRoot, "src", "content"); const manifest = await loadSystemManifest(contentDir); const entries = manifest.manifest.clientEditable ?? []; const stripped = stripContentPrefix(normalizedPath); if (stripped === null) return false; for (const entry of entries) { const normalizedEntry = entry.replace(/^\//, ""); if (stripped === normalizedEntry || stripped.startsWith(`${normalizedEntry}/`)) { return true; } } if (normalizedPath.endsWith(".client.ts") && normalizedPath.startsWith(CONTENT_PREFIX)) { return true; } if (normalizedPath.includes("/assets/") && normalizedPath.startsWith(CONTENT_PREFIX)) { return true; } return false; } function stripContentPrefix(p: string): string | null { if (!p.startsWith(CONTENT_PREFIX)) return null; return p.slice(CONTENT_PREFIX.length); } export function createClientEditableChecker(): ClientEditableChecker { return { isClientEditable, }; }