/* RFC-0707: nachweis.publish command handler — enforces publication gate and transitions record to published. Does not create EvidenceSource entities — that is done during ingest/content authoring. RFC-0872: replace legacy boolean gate with policy-driven V2 gate. RFC-0888: pass --skip-bordbuch to manifest.generate and append sichtpass Bordbuch entry after manifest regeneration. 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, RFC-0715 */ import path from "node:path"; import type { KernelCommandInput, KernelCommandResult, KernelRuntimeContext, } from "@warpgogol/werkstatt-engine/kernel"; import { executeKernelCommand } 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, evaluateGateV2, type NachweisPublishResult, } from "./nachweis-io.ts"; function flagString(input: KernelCommandInput, key: string): string | undefined { const v = input.flags[key]; return typeof v === "string" ? v : undefined; } function _flagBool(input: KernelCommandInput, key: string): boolean { const v = input.flags[key]; return v === true || v === "true"; } export async function runNachweisPublish( input: KernelCommandInput, context: KernelRuntimeContext, ): Promise> { const { workspaceRoot, logger } = context; const systemId = flagString(input, "system") ?? context.site?.name; const slug = flagString(input, "slug"); if (!systemId) throw new Error("[nachweis.publish] --system is required"); if (!slug) throw new Error("[nachweis.publish] --slug is required"); const entitled = await isNachweisEntitled(workspaceRoot, systemId); if (!entitled) { return makeSkipResult( "nachweis.publish", systemId, ) as unknown as KernelCommandResult; } const cachePath = await resolveNachweisCachePath(workspaceRoot, systemId); const lang = await resolveDefaultLang(cachePath); // Read EvidenceSource entity const evidenceDir = resolvePbpEntityDir(cachePath, lang, "evidence-source"); const evidenceFile = path.join(evidenceDir, `${slug}.md`); if (!(await context.io.exists(evidenceFile))) { throw new Error( `[nachweis.publish] NOT_FOUND: evidence-source '${slug}' not found at ${evidenceFile}`, ); } const rawEvidence = await context.io.readFile(evidenceFile); const { data: evidenceData, content: evidenceContent } = parseMarkdownFrontmatter(rawEvidence); // Read Consent entity const consentDir = resolvePbpEntityDir(cachePath, lang, "consent"); const consentFile = path.join(consentDir, `${slug}.md`); let consentData: Record | undefined; if (await context.io.exists(consentFile)) { const rawConsent = await context.io.readFile(consentFile); consentData = parseMarkdownFrontmatter(rawConsent).data; } // Read bordbuch entries for gate evaluation const { readBordbuch } = await import("../bordbuch/bordbuch-io.ts"); const bordbuchEntries = await readBordbuch(workspaceRoot, systemId); const nachweisEntries = bordbuchEntries.filter( (e) => e.kind === "nachweis-record" || e.kind === "nachweis-consent", ); // Evaluate gate V2 (RFC-0872: policy-driven, shared with nachweis.validate) const kind = evidenceData.kind as string | undefined; if (!kind) { throw new Error(`[nachweis.publish] evidence-source '${slug}' has no kind field`); } const gate = evaluateGateV2(slug, kind, { evidenceData: evidenceData as Record, consentData: consentData as Record | undefined, bordbuchEntries: nachweisEntries, }); if (!gate.allPassed) { const failedConditions = gate.conditions .filter((c) => c.required && c.status !== "pass") .map((c) => c.id); return { data: { recordId: (evidenceData.recordId as string | undefined) ?? `nr_${slug}`, systemId, published: false, gateResult: gate, bordbuchEventId: null, }, exitCode: 1, summary: `[nachweis.publish] ${systemId}: gate failed for '${slug}' — policy: ${gate.policyId}, failed: ${failedConditions.join(", ")}`, nextSteps: [ { action: `Fix the failed gate conditions (${failedConditions.join(", ")}), then re-run: pnpm exec werkstatt run nachweis.publish --site ${systemId} --slug ${slug}`, kind: "required", }, ], }; } // Gate passed — update EvidenceSource publication const publication = (evidenceData.publication as Record | undefined) ?? {}; publication.visibility = "public"; publication.publishedAt = new Date().toISOString(); evidenceData.publication = publication; const updatedContent = stringifyMarkdownFrontmatter(evidenceContent, evidenceData); await context.io.writeFile(evidenceFile, updatedContent); // Append Bordbuch entry const operationId = generateOperationId(); await acquireLock(workspaceRoot, `system:${systemId}`, operationId, "nachweis.publish", "agent"); await acquireLock( workspaceRoot, `bordbuch:${systemId}`, operationId, "nachweis.publish", "agent", ); let bordbuchEventId: string | null = null; try { const { entry } = await appendAndCommitBordbuch( workspaceRoot, systemId, "nachweis-record", `Published '${slug}'`, "agent", { writerRole: "nachweis", metadata: { slug, verificationLevel: "N3", publishedAt: publication.publishedAt, }, }, `Bordbuch: nachweis-record ${systemId} ${slug}`, ); bordbuchEventId = entry.id; } finally { await releaseLock(workspaceRoot, `bordbuch:${systemId}`); await releaseLock(workspaceRoot, `system:${systemId}`); } // Regenerate manifest — pass --skip-bordbuch to prevent duplicate sichtpass entry (RFC-0888) await executeKernelCommand({ workspaceRoot, commandName: "nachweis.manifest.generate", siteName: systemId, argv: [`--system=${systemId}`, "--skip-bordbuch"], }); logger.info(`[nachweis.publish] published '${slug}' — regenerating manifest`); // RFC-0888: Append sichtpass Bordbuch entry for the published slug const sichtpassOperationId = generateOperationId(); await acquireLock( workspaceRoot, `system:${systemId}`, sichtpassOperationId, "nachweis.publish", "agent", ); await acquireLock( workspaceRoot, `bordbuch:${systemId}`, sichtpassOperationId, "nachweis.publish", "agent", ); try { await appendAndCommitBordbuch( workspaceRoot, systemId, "sichtpass", `Sichtpass manifest entry generated for '${slug}'`, "agent", { writerRole: "nachweis", metadata: { slug, manifestVersion: "1.0.0", recordHash: (evidenceData.items as Record | undefined) ? (Object.values(evidenceData.items as Record)[0] ?.sha256 ?? "") : "", signaturePresent: bordbuchEntries.some( (e) => e.kind === "nachweis-signed" && e.metadata?.slug === slug, ), timestampPresent: bordbuchEntries.some( (e) => e.kind === "nachweis-timestamped" && e.metadata?.slug === slug, ), verificationLevel: "N3", }, }, `Bordbuch: sichtpass ${systemId} ${slug} published`, ); } finally { await releaseLock(workspaceRoot, `bordbuch:${systemId}`); await releaseLock(workspaceRoot, `system:${systemId}`); } return { data: { recordId: (evidenceData.recordId as string | undefined) ?? `nr_${slug}`, systemId, published: true, gateResult: gate, bordbuchEventId, }, exitCode: 0, summary: `[nachweis.publish] ${systemId}: published '${slug}' (bordbuch: ${bordbuchEventId})`, nextSteps: [ { action: `Verify the published nachweis: pnpm exec werkstatt run nachweis.verify-signature --site ${systemId} --slug ${slug}`, kind: "optional", }, ], }; }