/* RFC-0707: nachweis.manifest.generate command handler — generates public/nachweise/manifest.json from published records. Does not publish records — that is nachweis.publish. Does not validate gate conditions — that is nachweis.validate. RFC-0886: include display and websiteUrl fields in manifest entries. RFC-0888: append sichtpass Bordbuch entry after manifest file is written (unless --skip-bordbuch is set). 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-0871, RFC-0872 */ import path from "node:path"; import { createHash } from "node:crypto"; import type { KernelCommandInput, KernelCommandResult, KernelRuntimeContext, } from "@warpgogol/werkstatt-engine/kernel"; import { writeFileIfChanged } from "@warpgogol/werkstatt-engine/kernel"; import { parseMarkdownFrontmatter } from "@warpgogol/werkstatt-shared/content"; import { readBordbuch } from "../bordbuch/bordbuch-io.ts"; import { appendAndCommitBordbuch } from "../bordbuch/bordbuch-commit-helper.ts"; import { acquireLock, releaseLock, generateOperationId } from "../werkstatt/index.ts"; import { isNachweisEntitled, makeSkipResult, resolveNachweisCachePath, resolvePbpEntityDir, resolveDefaultLang, type NachweisManifest, type NachweisManifestEntry, } from "./nachweis-io.ts"; const NACHWEIS_EVIDENCE_KINDS = new Set([ "client-statement", "project-confirmation", "certificate", "operational-evidence", // RFC-0872: technical assessment evidence type "technical-assessment", ]); const MANIFEST_SCHEMA_VERSION = "1.0.0"; const MANIFEST_OUTPUT_DIR = path.join("public", "nachweise"); const MANIFEST_OUTPUT_FILE = "manifest.json"; 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 runNachweisManifestGenerate( input: KernelCommandInput, context: KernelRuntimeContext, ): Promise> { const { workspaceRoot, logger } = context; const systemId = flagString(input, "system") ?? context.site?.name; if (!systemId) throw new Error("[nachweis.manifest.generate] --system is required"); const entitled = await isNachweisEntitled(workspaceRoot, systemId); if (!entitled) { return makeSkipResult( "nachweis.manifest.generate", systemId, ) as unknown as KernelCommandResult; } const cachePath = await resolveNachweisCachePath(workspaceRoot, systemId); const lang = await resolveDefaultLang(cachePath); const evidenceDir = resolvePbpEntityDir(cachePath, lang, "evidence-source"); const records: NachweisManifestEntry[] = []; // RFC-0871: read Bordbuch to resolve timestampAssurance per slug const bordbuchEntries = await readBordbuch(workspaceRoot, systemId); const timestampedEntries = new Map(); for (const e of bordbuchEntries) { if (e.kind === "nachweis-timestamped" && e.metadata?.slug) { if (!timestampedEntries.has(e.metadata.slug as string)) { timestampedEntries.set(e.metadata.slug as string, { timestampAssurance: e.metadata?.timestampAssurance as string | undefined, }); } } } if (await context.io.exists(evidenceDir)) { const entries = await context.io.readdir(evidenceDir); for (const entry of entries) { if (!entry.isFile) continue; if (!entry.name.endsWith(".md") && !entry.name.endsWith(".yaml")) continue; const filePath = path.join(evidenceDir, entry.name); const raw = await context.io.readFile(filePath); let data: Record; if (entry.name.endsWith(".md")) { const parsed = parseMarkdownFrontmatter(raw); data = parsed.data; } else { try { data = JSON.parse(raw) as Record; } catch { continue; } } const kind = data.kind as string | undefined; if (!kind || !NACHWEIS_EVIDENCE_KINDS.has(kind)) continue; const publication = data.publication as Record | undefined; if (publication?.visibility !== "public") continue; const slug = (data.slug as string | undefined) ?? entry.name.replace(/\.(md|yaml)$/, ""); const items = data.items as Record | undefined; const firstSha = items ? Object.values(items)[0]?.sha256 : undefined; records.push({ recordId: (data.recordId as string | undefined) ?? `nr_${slug}`, slug, recordType: kind, titleDe: (data.titleDe as string | undefined) ?? "", titleUk: (data.titleUk as string | undefined) ?? "", ...(data.titleEn ? { titleEn: data.titleEn as string } : {}), qualityStatus: (data.qualityStatus as string | undefined) ?? "unverified", sourceSha256: firstSha ?? "", publishedAt: (publication.publishedAt as string | null) ?? null, timestampAssurance: (timestampedEntries.get(slug)?.timestampAssurance as "rfc3161" | "eidas-qualified" | undefined) ?? "rfc3161", // RFC-0872: observation identity fields for technical assessments ...(kind === "technical-assessment" ? { kind, seriesId: (data.assessment as Record)?.seriesId as string | undefined, observationId: (data.assessment as Record)?.observationId as string | undefined, observedAt: (data.assessment as Record)?.observedAt as string | undefined, assessmentProviderId: ( (data.assessment as Record)?.provider as Record )?.id as string | undefined, } : {}), // RFC-0886: display and websiteUrl fields ...(data.display ? { display: data.display as { document: string; screenshot: string; websiteLink: string; }, } : {}), ...(data.websiteUrl ? { websiteUrl: data.websiteUrl as string } : {}), }); } } records.sort((a, b) => a.slug.localeCompare(b.slug)); const manifest: NachweisManifest = { schemaVersion: MANIFEST_SCHEMA_VERSION, generatedAt: null, expiresAt: null, records, }; const outputDir = path.join(cachePath, MANIFEST_OUTPUT_DIR); if (!(await context.io.exists(outputDir))) { await context.io.mkdir(outputDir); } const outputPath = path.join(outputDir, MANIFEST_OUTPUT_FILE); const manifestJson = JSON.stringify(manifest, null, 2) + "\n"; await writeFileIfChanged(outputPath, manifestJson); logger.info( `[nachweis.manifest.generate] wrote ${records.length} record(s) to ${MANIFEST_OUTPUT_DIR}/${MANIFEST_OUTPUT_FILE}`, ); // RFC-0888/RFC-0947: Append sichtpass Bordbuch entry unless --skip-bordbuch is set. // RFC-0947: Deduplicate — skip append when manifest hash is unchanged from last __manifest__ entry. const skipBordbuch = flagBool(input, "skip-bordbuch"); if (!skipBordbuch) { const manifestHash = createHash("sha256").update(manifestJson).digest("hex"); // RFC-0947: Deduplication — check last sichtpass __manifest__ entry let deduplicated = false; try { const entries = await readBordbuch(workspaceRoot, systemId); for (let i = entries.length - 1; i >= 0; i--) { const entry = entries[i]; if (entry.kind !== "sichtpass") continue; const meta = entry.metadata as Record | undefined; if (meta?.slug !== "__manifest__") continue; const lastHash = typeof meta.recordHash === "string" ? meta.recordHash : null; if (lastHash === manifestHash) { deduplicated = true; logger.info( `[nachweis.manifest.generate] manifest hash unchanged (${manifestHash.slice(0, 16)}...) — skipping sichtpass Bordbuch append`, ); } break; } } catch { // Bordbuch read failure is non-fatal — proceed with append } if (!deduplicated) { const sichtpassOperationId = generateOperationId(); await acquireLock( workspaceRoot, `system:${systemId}`, sichtpassOperationId, "nachweis.manifest.generate", "agent", ); await acquireLock( workspaceRoot, `bordbuch:${systemId}`, sichtpassOperationId, "nachweis.manifest.generate", "agent", ); try { await appendAndCommitBordbuch( workspaceRoot, systemId, "sichtpass", `Sichtpass manifest regenerated for '${systemId}'`, "agent", { writerRole: "nachweis", metadata: { slug: "__manifest__", manifestVersion: MANIFEST_SCHEMA_VERSION, recordHash: manifestHash, signaturePresent: false, timestampPresent: false, verificationLevel: "N0", }, }, `Bordbuch: sichtpass ${systemId} manifest-regenerated`, ); } finally { await releaseLock(workspaceRoot, `bordbuch:${systemId}`); await releaseLock(workspaceRoot, `system:${systemId}`); } } } else { logger.info( "[nachweis.manifest.generate] --skip-bordbuch set, skipping sichtpass Bordbuch entry", ); } return { data: manifest, exitCode: 0, summary: `[nachweis.manifest.generate] ${systemId}: ${records.length} public record(s)`, }; }