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