/* RFC-0707/RFC-0886: nachweis.consent.update command handler — updates PBP Consent entity's consentScope[scope] and appends Bordbuch entry. Does not validate consent text or legal sufficiency — that is a human review step. RFC-0885: update consentScope instead of consentStatus (document aspect only). RFC-0886: add --scope flag for granular per-aspect consent (document|screenshot|websiteLink). RFC-1097: step 6 — compass.migrate codemod run Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order. RFC-1097: sweep — werkstatt-engine clean Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics. RFC-1254: drop unused io-migration leftovers RFC-0707 */ import path from "node:path"; import type { KernelCommandInput, KernelCommandResult, KernelRuntimeContext, } from "@warpgogol/werkstatt-engine/kernel"; import { parseMarkdownFrontmatter, stringifyMarkdownFrontmatter, } from "@warpgogol/werkstatt-shared/content"; import { appendAndCommitBordbuch } from "../bordbuch/bordbuch-commit-helper.ts"; import { acquireLock, releaseLock, generateOperationId } from "../werkstatt/index.ts"; import { isNachweisEntitled, makeSkipResult, resolveNachweisCachePath, resolvePbpEntityDir, resolveDefaultLang, type NachweisConsentUpdateResult, } from "./nachweis-io.ts"; function flagString(input: KernelCommandInput, key: string): string | undefined { const v = input.flags[key]; return typeof v === "string" ? v : undefined; } export async function runNachweisConsentUpdate( input: KernelCommandInput, context: KernelRuntimeContext, ): Promise> { const { workspaceRoot, logger } = context; const systemId = flagString(input, "system") ?? context.site?.name; const consentId = flagString(input, "consent-id"); const scope = flagString(input, "scope"); const newStatus = flagString(input, "status"); const method = flagString(input, "method") ?? "none"; if (!systemId) throw new Error("[nachweis.consent.update] --system is required"); if (!consentId) throw new Error("[nachweis.consent.update] --consent-id is required"); if (!scope) throw new Error( "[nachweis.consent.update] --scope is required (document|screenshot|websiteLink)", ); const VALID_SCOPES = new Set(["document", "screenshot", "websiteLink"]); if (!VALID_SCOPES.has(scope)) { throw new Error( `[nachweis.consent.update] invalid --scope '${scope}'. Must be one of: document, screenshot, websiteLink`, ); } if (!newStatus) throw new Error("[nachweis.consent.update] --status is required"); const entitled = await isNachweisEntitled(workspaceRoot, systemId); if (!entitled) { return makeSkipResult( "nachweis.consent.update", systemId, ) as unknown as KernelCommandResult; } const cachePath = await resolveNachweisCachePath(workspaceRoot, systemId); const lang = await resolveDefaultLang(cachePath); const consentDir = resolvePbpEntityDir(cachePath, lang, "consent"); const consentFile = path.join(consentDir, `${consentId}.md`); if (!(await context.io.exists(consentFile))) { throw new Error( `[nachweis.consent.update] NOT_FOUND: consent '${consentId}' not found at ${consentFile}`, ); } const raw = await context.io.readFile(consentFile); const { data, content } = parseMarkdownFrontmatter(raw); // RFC-0886: Read consentScope for the requested scope aspect const existingScope = (data.consentScope as | { document?: { status?: string; grantedAt?: string | null; method?: string }; screenshot?: { status?: string; grantedAt?: string | null; method?: string }; websiteLink?: { status?: string; grantedAt?: string | null; method?: string }; } | undefined) ?? {}; const previousStatus = existingScope[scope as "document" | "screenshot" | "websiteLink"]?.status ?? "not_requested"; const now = new Date().toISOString(); const defaultEntry = { status: "not_requested", grantedAt: null, method: "none" }; const docEntry = existingScope.document ?? defaultEntry; const screenshotEntry = existingScope.screenshot ?? defaultEntry; const websiteLinkEntry = existingScope.websiteLink ?? defaultEntry; // RFC-0886: Update only the requested scope aspect, preserve others const newScopeEntry = { status: newStatus, grantedAt: newStatus === "granted" ? now : (existingScope[scope as "document" | "screenshot" | "websiteLink"]?.grantedAt ?? null), method, }; data.consentScope = { document: scope === "document" ? newScopeEntry : docEntry, screenshot: scope === "screenshot" ? newScopeEntry : screenshotEntry, websiteLink: scope === "websiteLink" ? newScopeEntry : websiteLinkEntry, }; delete data.consentStatus; delete data.grantedAt; delete data.method; const updatedContent = stringifyMarkdownFrontmatter(content, data); await context.io.writeFile(consentFile, updatedContent); logger.info( `[nachweis.consent.update] updated consent '${consentId}' ${scope}: ${previousStatus} → ${newStatus}`, ); // Append Bordbuch entry const operationId = generateOperationId(); await acquireLock( workspaceRoot, `system:${systemId}`, operationId, "nachweis.consent.update", "agent", ); await acquireLock( workspaceRoot, `bordbuch:${systemId}`, operationId, "nachweis.consent.update", "agent", ); let bordbuchEventId: string; try { const { entry } = await appendAndCommitBordbuch( workspaceRoot, systemId, "nachweis-consent", `Consent '${consentId}' ${scope}: ${previousStatus} → ${newStatus}`, "agent", { writerRole: "nachweis", metadata: { consentId, scope, previousStatus, newStatus, method, }, }, `Bordbuch: nachweis-consent ${systemId} ${consentId}`, ); bordbuchEventId = entry.id; } finally { await releaseLock(workspaceRoot, `bordbuch:${systemId}`); await releaseLock(workspaceRoot, `system:${systemId}`); } return { data: { consentId, systemId, scope, previousStatus, newStatus, bordbuchEventId, }, exitCode: 0, summary: `[nachweis.consent.update] ${systemId}: consent '${consentId}' ${scope}: ${previousStatus} → ${newStatus} (bordbuch: ${bordbuchEventId})`, nextSteps: [ { action: `Validate the nachweis: pnpm exec werkstatt run nachweis.validate --site ${systemId}`, kind: "optional", }, ], }; }