/*
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",
},
],
};
}