Initial curated itworx-knowledge-hub source from 3b1751d0d9dee551aa6994eee0093704dc334817

This commit is contained in:
Public Source Publisher committed 2026-10-01 01:37:37 +02:00
commit ad8ad252a9
825 files changed
+124216

No files matched your search

+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env node
// Contract-coverage gate (docs/adr/ADR-029-v2-r5-final-hardening.md, brief Phase 1):
// fails when a route actually registered by an ASP.NET Minimal API endpoint file under
// src/backend/ItWorx.KnowledgeHub.Api/Endpoints does not appear in
// docs/api/openapi.yaml. This is what makes "the API must be reliably usable by a
// future separate MCP server" (CLAUDE.md) an enforced property, not an aspiration —
// a route can no longer silently ship undocumented.
//
// Deliberately a source-scan, not a live-server introspection: it runs with no
// database, no built application and no network, so it is cheap enough to run on every
// `node scripts/verify.mjs` (fast and full), not only the release gate.
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import yaml from "js-yaml";
const endpointsDir = "src/backend/ItWorx.KnowledgeHub.Api/Endpoints";
const openApiPath = "docs/api/openapi.yaml";
// Routes intentionally excluded from OpenAPI coverage: not `/api/v1/*` REST endpoints
// at all (health probes, the raw attachment byte stream already documented once under
// its real path, etc.) — see docs/09-api-contract.md "Base path: /api/v1".
const EXCLUDED_ROUTE_PREFIXES = ["/health"];
function listEndpointFiles(dir) {
return fs
.readdirSync(dir)
.filter((file) => file.endsWith(".cs"))
.map((file) => path.join(dir, file));
}
function extractRoutesFromFile(filePath) {
const source = fs.readFileSync(filePath, "utf8");
const routes = [];
// MapGroup("/api/v1/xyz") establishes a prefix for every Map* call chained off the
// returned variable within the same file — every endpoint file in this codebase uses
// exactly this one-group-per-file pattern (verified by inspection), so a single
// "most recent MapGroup literal in this file" tracking variable is sufficient and
// avoids needing a real C# parser.
const groupMatch = source.match(/MapGroup\(\s*"([^"]+)"\s*\)/);
const groupPrefix = groupMatch ? groupMatch[1] : "";
const mapCallPattern = /\.Map(Get|Post|Put|Delete|Patch)\(\s*"([^"]*)"/g;
let match;
while ((match = mapCallPattern.exec(source)) !== null) {
const method = match[1].toUpperCase();
const routeLiteral = match[2];
const fullPath = groupPrefix + routeLiteral;
routes.push({ method, path: normalizeRoute(fullPath), file: filePath });
}
return routes;
}
// Normalizes an ASP.NET route template's typed route constraints
// (`{attachmentId:guid}`) to OpenAPI's untyped `{attachmentId}` style, and strips the
// leading `/api/v1` prefix to match how docs/api/openapi.yaml keys its paths (relative
// to the documented `servers[0].url`).
function normalizeRoute(route) {
let normalized = route.replace(/\{([a-zA-Z0-9_]+)(:[a-zA-Z]+)?\}/g, "{$1}");
if (normalized.startsWith("/api/v1")) {
normalized = normalized.slice("/api/v1".length);
}
if (normalized === "") {
normalized = "/";
}
return normalized;
}
function isExcluded(routePath) {
return EXCLUDED_ROUTE_PREFIXES.some((prefix) => routePath.startsWith(prefix));
}
const registeredRoutes = listEndpointFiles(endpointsDir).flatMap(extractRoutesFromFile).filter((route) => !isExcluded(route.path));
const document = yaml.load(fs.readFileSync(openApiPath, "utf8"));
const documentedPaths = document?.paths ?? {};
const missing = [];
for (const route of registeredRoutes) {
const pathEntry = documentedPaths[route.path];
const documentedMethods = pathEntry ? Object.keys(pathEntry).map((key) => key.toUpperCase()) : [];
if (!documentedMethods.includes(route.method)) {
missing.push(route);
}
}
if (missing.length > 0) {
console.error(`OpenAPI coverage gate failed: ${missing.length} registered route(s) not documented in ${openApiPath}:`);
for (const route of missing.sort((a, b) => a.path.localeCompare(b.path))) {
console.error(`- ${route.method} /api/v1${route.path === "/" ? "" : route.path} (${route.file})`);
}
process.exit(1);
}
console.log(`OpenAPI coverage gate passed: all ${registeredRoutes.length} registered /api/v1 routes are documented.`);
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import { spawnSync } from "node:child_process";
import { capture, commandExists, run } from "./lib/exec.mjs";
const root = process.cwd();
const requiredRootFiles = [
"CLAUDE.md",
"product/milestones.json",
"ItWorx.KnowledgeHub.slnx",
"src/frontend/package.json"
];
const missingRoot = requiredRootFiles.filter((entry) => !fs.existsSync(path.join(root, entry)));
if (missingRoot.length > 0) {
console.error("Run this command from the repository root. Missing:", missingRoot.join(", "));
process.exit(1);
}
const checks = [];
async function check(name, command, args, predicate, required = true) {
if (!commandExists(command)) {
checks.push({ name, value: "missing", ok: false, required });
return;
}
try {
const value = await capture(command, args);
checks.push({ name, value: value.split(/\r?\n/)[0], ok: predicate(value), required });
} catch (error) {
checks.push({ name, value: `error: ${error.message}`, ok: false, required });
}
}
await check("Git", "git", ["--version"], (value) => /git version/i.test(value));
await check("Node.js", "node", ["--version"], (value) => {
const major = Number(value.match(/v?(\d+)/)?.[1] ?? 0);
return major >= 24;
});
await check("Corepack", "corepack", ["--version"], (value) => value.length > 0);
await check("pnpm", "corepack", ["pnpm", "--version"], (value) => {
const major = Number(value.match(/(\d+)/)?.[1] ?? 0);
return major >= 11;
});
await check(".NET SDK", "dotnet", ["--version"], (value) => value.trim().startsWith("10."));
function findWorkingPython() {
for (const candidate of ["python3", "python"]) {
if (!commandExists(candidate)) {
continue;
}
const probe = spawnSync(candidate, ["--version"], { stdio: "ignore", shell: false });
if (probe.status === 0) {
return candidate;
}
}
return null;
}
const python = findWorkingPython();
if (python) {
await check("Python", python, ["--version"], (value) => {
const match = value.match(/Python\s+(\d+)\.(\d+)/i);
return Boolean(match && Number(match[1]) >= 3 && Number(match[2]) >= 11);
});
} else {
checks.push({ name: "Python", value: "missing or non-working", ok: false, required: true });
}
await check("Docker", "docker", ["--version"], (value) => /Docker version/i.test(value));
if (commandExists("docker")) {
try {
const compose = await capture("docker", ["compose", "version"]);
checks.push({ name: "Docker Compose", value: compose, ok: /Compose version\s+v?\d+\./i.test(compose), required: true });
} catch (error) {
checks.push({ name: "Docker Compose", value: `error: ${error.message}`, ok: false, required: true });
}
}
const width = Math.max(...checks.map((entry) => entry.name.length), 10);
for (const entry of checks) {
const marker = entry.ok ? "OK " : entry.required ? "ERR" : "WARN";
console.log(`${marker} ${entry.name.padEnd(width)} ${entry.value}`);
}
const failures = checks.filter((entry) => entry.required && !entry.ok);
if (failures.length > 0) {
console.error("\nRequired toolchain checks failed.");
process.exit(1);
}
if (!fs.existsSync(path.join(root, "pnpm-lock.yaml"))) {
console.warn("\nWARN pnpm-lock.yaml is not present yet. M0 must generate and commit it.");
}
console.log("\nToolchain baseline is available.");
+131
View File
@@ -0,0 +1,131 @@
#!/usr/bin/env node
// Canonical, machine-readable release identity (docs/adr/ADR-029-v2-r5-final-hardening.md,
// brief Phase 9). Computes everything genuinely knowable at *build* time — source
// commit, OpenAPI contract hash, frontend build hash, semantic version, build
// timestamp — and writes release-manifest.json, which the Dockerfile copies into the
// image and the running application reads at startup to enrich GET /api/v1/system/info.
//
// Deliberately leaves deployment-time-only fields (image tag, image ID, immutable
// digest, deployment timestamp, database migration range as actually applied,
// inventory snapshot ID, backup ID) as null here — those are not knowable until the
// image has actually been built and deployed (the registry/daemon assigns a digest
// only after push; a migration range depends on which database the running instance
// is actually talking to). Per the brief: "Tag de nieuwe release pas nadat de
// daadwerkelijke productie-image ... is gebouwd en succesvol gedeployed" — this script
// never invents those values, and a real deployment record (docs/deployments/*.md)
// is the source of truth for them, cross-referenced from here only after the fact.
import { createHash } from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import { spawnSync } from "node:child_process";
const repoRoot = process.cwd();
const outputPath = process.argv[2] ?? path.join(repoRoot, "release-manifest.json");
function sha256File(relativePath) {
const fullPath = path.join(repoRoot, relativePath);
if (!fs.existsSync(fullPath)) {
return null;
}
const hash = createHash("sha256");
hash.update(fs.readFileSync(fullPath));
return `sha256:${hash.digest("hex")}`;
}
// The built frontend bundle's content-hashed filenames already make every asset's own
// hash visible in its filename (Vite's default), but there is no single "the bundle"
// file — hash the whole dist/ directory's file list + each file's own bytes instead,
// so any output change (including a filename/hash change) changes this value too.
function sha256Directory(relativeDir) {
const fullDir = path.join(repoRoot, relativeDir);
if (!fs.existsSync(fullDir)) {
return null;
}
const files = [];
const walk = (dir) => {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const entryPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
walk(entryPath);
} else {
files.push(entryPath);
}
}
};
walk(fullDir);
files.sort();
const hash = createHash("sha256");
for (const file of files) {
hash.update(path.relative(fullDir, file).replace(/\\/g, "/"));
hash.update(fs.readFileSync(file));
}
return `sha256:${hash.digest("hex")}`;
}
function gitCommit() {
if (process.env.BUILD_COMMIT && process.env.BUILD_COMMIT !== "development") {
return process.env.BUILD_COMMIT;
}
const result = spawnSync("git", ["rev-parse", "--short=12", "HEAD"], {
cwd: repoRoot,
encoding: "utf8"
});
return result.status === 0 ? result.stdout.trim() : "unknown";
}
function readOpenApiVersion() {
try {
const contents = fs.readFileSync(path.join(repoRoot, "docs/api/openapi.yaml"), "utf8");
const match = contents.match(/^\s*version:\s*(.+)$/m);
return match ? match[1].trim() : null;
} catch {
return null;
}
}
function readSemanticVersion() {
const versionFile = path.join(repoRoot, "VERSION");
if (fs.existsSync(versionFile)) {
return fs.readFileSync(versionFile, "utf8").trim();
}
// Falls back to the most recent annotated/lightweight tag reachable from HEAD, the
// same source `git describe` uses — never invents a version number.
const described = spawnSync("git", ["describe", "--tags", "--always"], {
cwd: repoRoot,
encoding: "utf8"
});
return described.status === 0 ? described.stdout.trim() : null;
}
// An empty string (as opposed to an unset variable — Docker build args with no
// override are still passed as "") means "not actually known" just as much as
// unset does; never record it as a real, empty-but-present value.
const nonEmpty = (value) => (value && value.trim().length > 0 ? value.trim() : null);
const manifest = {
schemaVersion: 1,
semanticVersion: readSemanticVersion(),
releaseTag: nonEmpty(process.env.RELEASE_TAG),
sourceCommit: gitCommit(),
imageRepository: process.env.IMAGE_REPOSITORY ?? "itworx/knowledge-hub",
imageTag: nonEmpty(process.env.IMAGE_TAG),
imageId: null,
imageDigest: null,
buildTimestamp: new Date().toISOString(),
deploymentTimestamp: null,
environment: process.env.BUILD_ENVIRONMENT ?? "production",
openApiVersion: readOpenApiVersion(),
openApiHash: sha256File("docs/api/openapi.yaml"),
frontendBuildHash: sha256Directory("src/frontend/dist"),
databaseMigrationRange: null,
inventorySnapshotId: null,
backupId: null
};
fs.writeFileSync(outputPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
console.log(`Wrote release manifest to ${outputPath}:`);
console.log(JSON.stringify(manifest, null, 2));
+106
View File
@@ -0,0 +1,106 @@
import { spawn, spawnSync } from "node:child_process";
import process from "node:process";
export function commandExists(command) {
const locator = process.platform === "win32" ? "where" : "which";
const result = spawnSync(locator, [command], {
stdio: "ignore",
shell: false
});
return result.status === 0;
}
// On Windows, some tools (corepack, pnpm, npm, ...) are installed as `.cmd`/`.bat`
// shims. Those are not real PE executables, so libuv/CreateProcess cannot launch
// them directly: `child_process.spawn` fails with ENOENT even though the same
// command works in an interactive shell. Real executables (dotnet.exe, docker.exe,
// node.exe, python.exe) do not have this problem and must NOT be routed through a
// shell, because shell-quoting a path that contains spaces (e.g. "C:\Program
// Files\...") breaks it. Detect which case applies per command instead of
// guessing globally.
function requiresWindowsShell(command) {
if (process.platform !== "win32") {
return false;
}
if (/[\\/]/.test(command)) {
// Already a path; trust its extension.
return /\.(cmd|bat)$/i.test(command);
}
const located = spawnSync("where", [command], {
encoding: "utf8",
shell: false
});
if (located.status !== 0) {
return false;
}
// `where` can list several matches on Windows (e.g. an extension-less shebang
// launcher script alongside a `.cmd` shim for the same tool). Node can only
// ever run the `.cmd`/`.bat` variant through a shell, so treat the command as
// shell-requiring if *any* match needs one, not just the first line.
const matches = located.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
return matches.some((match) => /\.(cmd|bat)$/i.test(match));
}
function quoteForWindowsShell(value) {
if (value === "") {
return '""';
}
if (!/[\s"^&|<>()%]/.test(value)) {
return value;
}
return `"${value.replace(/"/g, '\\"')}"`;
}
export async function run(command, args = [], options = {}) {
const printable = [command, ...args].join(" ");
if (!options.quiet) {
process.stdout.write(`\n> ${printable}\n`);
}
const useShell = requiresWindowsShell(command);
const spawnCommand = useShell
? [command, ...args].map(quoteForWindowsShell).join(" ")
: command;
const spawnArgs = useShell ? [] : args;
return await new Promise((resolve, reject) => {
const child = spawn(spawnCommand, spawnArgs, {
cwd: options.cwd ?? process.cwd(),
env: { ...process.env, ...options.env },
stdio: options.capture ? ["ignore", "pipe", "pipe"] : "inherit",
shell: useShell
});
let stdout = "";
let stderr = "";
if (options.capture) {
child.stdout?.on("data", (chunk) => {
stdout += chunk.toString();
});
child.stderr?.on("data", (chunk) => {
stderr += chunk.toString();
});
}
child.on("error", reject);
child.on("close", (code, signal) => {
if (code === 0) {
resolve({ code, stdout, stderr });
return;
}
const suffix = signal ? ` (signal ${signal})` : "";
const error = new Error(`Command failed (${code}${suffix}): ${printable}`);
error.code = code;
error.stdout = stdout;
error.stderr = stderr;
reject(error);
});
});
}
export async function capture(command, args = [], options = {}) {
const result = await run(command, args, { ...options, capture: true, quiet: true });
return result.stdout.trim();
}
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env node
// Lightweight, dependency-free OpenAPI consistency check: valid YAML, every internal
// $ref resolves, and every path/schema referenced by a $ref actually exists. This is
// not a full OpenAPI 3.1 schema validator, but it catches the class of error that
// actually breaks generated clients and docs sites: dangling references and typos.
import fs from "node:fs";
import process from "node:process";
import yaml from "js-yaml";
const path = "docs/api/openapi.yaml";
const document = yaml.load(fs.readFileSync(path, "utf8"));
const errors = [];
function resolveRef(ref) {
if (!ref.startsWith("#/")) {
errors.push(`External or malformed $ref not supported: ${ref}`);
return;
}
const segments = ref
.slice(2)
.split("/")
.map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
let cursor = document;
for (const segment of segments) {
if (cursor === undefined || cursor === null || !(segment in cursor)) {
errors.push(`Dangling $ref: ${ref}`);
return;
}
cursor = cursor[segment];
}
}
function walk(node) {
if (Array.isArray(node)) {
for (const item of node) {
walk(item);
}
return;
}
if (node && typeof node === "object") {
for (const [key, value] of Object.entries(node)) {
if (key === "$ref" && typeof value === "string") {
resolveRef(value);
} else {
walk(value);
}
}
}
}
if (!document || typeof document !== "object") {
errors.push("Document did not parse to an object.");
} else {
if (document.openapi === undefined) {
errors.push("Missing top-level 'openapi' version field.");
}
if (!document.paths || Object.keys(document.paths).length === 0) {
errors.push("No paths defined.");
}
walk(document);
}
if (errors.length > 0) {
console.error("OpenAPI validation failed:");
for (const error of errors) {
console.error(`- ${error}`);
}
process.exit(1);
}
const pathCount = Object.keys(document.paths ?? {}).length;
const schemaCount = Object.keys(document.components?.schemas ?? {}).length;
console.log(`OpenAPI document is well-formed: ${pathCount} paths, ${schemaCount} schemas, all $ref pointers resolve.`);
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
const required = [
"README.md", "SECURITY.md", "CONTRIBUTING.md", "LICENSE", "PUBLIC_SOURCE_EXPORT.md",
"PUBLIC_SOURCE_MANIFEST.json", "MANIFEST.json", "ItWorx.KnowledgeHub.slnx", "docker-compose.yml",
"deploy/docker/Dockerfile", "docs/api/openapi.yaml", "src/frontend/package.json"
];
const missing = required.filter((entry) => !fs.existsSync(path.join(process.cwd(), entry)));
if (missing.length > 0) {
console.error(`Missing public source files: ${missing.join(", ")}`);
process.exit(1);
}
for (const privatePath of [".claude", ".agents", ".codex", "artifacts", "docs/deployments", "docs/v2", "n8n"]) {
if (fs.existsSync(path.join(process.cwd(), privatePath))) {
console.error(`Private-only path present in public export: ${privatePath}`);
process.exit(1);
}
}
const manifest = JSON.parse(fs.readFileSync("PUBLIC_SOURCE_MANIFEST.json", "utf8"));
if (!Array.isArray(manifest.files) || manifest.files.length === 0) {
console.error("PUBLIC_SOURCE_MANIFEST.json contains no files.");
process.exit(1);
}
console.log(`Public source structure passed (${manifest.files.length} allowlisted files).`);
+238
View File
@@ -0,0 +1,238 @@
#!/usr/bin/env node
import fs from "node:fs";
import process from "node:process";
import { spawnSync } from "node:child_process";
import { commandExists, run } from "./lib/exec.mjs";
const flags = new Set(process.argv.slice(2));
const structureOnly = flags.has("--structure-only");
const fast = flags.has("--fast");
const ci = flags.has("--ci");
const release = flags.has("--release");
const publicSourceExport = fs.existsSync("PUBLIC_SOURCE_EXPORT.md");
function findWorkingPython() {
// On some Windows setups `python3` resolves via PATH to the Microsoft Store
// app-execution-alias stub (present even when no Store install exists),
// which `where`/commandExists reports as found but which fails at runtime.
// Probe candidates by actually invoking them rather than trusting PATH
// resolution alone.
const candidates = ["python3", "python"];
for (const candidate of candidates) {
if (!commandExists(candidate)) {
continue;
}
const probe = spawnSync(candidate, ["--version"], { stdio: "ignore", shell: false });
if (probe.status === 0) {
return candidate;
}
}
throw new Error("No working Python 3 interpreter found (tried python3, python).");
}
const python = findWorkingPython();
async function section(name, action) {
console.log(`\n=== ${name} ===`);
await action();
}
try {
await section("Bundle structure", async () => {
if (publicSourceExport) {
await run(process.execPath, ["scripts/validate-public-source.mjs"]);
} else {
await run(process.execPath, ["scripts/validate-bundle.mjs"]);
await run(process.execPath, ["scripts/test-safety-hook.mjs"]);
}
});
await section("LeafWiki inspector tests", async () => {
await run(python, [
"-m",
"unittest",
"discover",
"-s",
"tools/leafwiki-inspector/tests",
"-p",
"test_*.py"
]);
});
await section("Knowledge-ingestion policy tests", async () => {
if (!publicSourceExport) {
await run(process.execPath, ["--test", "tools/knowledge-ingestion/tests/*.test.mjs"]);
}
});
if (structureOnly) {
console.log("\nStructure-only verification passed.");
process.exit(0);
}
await section(".NET restore, format, build and tests", async () => {
await run("dotnet", ["restore", "ItWorx.KnowledgeHub.slnx"]);
await run("dotnet", [
"format",
"ItWorx.KnowledgeHub.slnx",
"--verify-no-changes",
"--no-restore"
]);
await run("dotnet", [
"build",
"ItWorx.KnowledgeHub.slnx",
"--configuration",
"Release",
"--no-restore"
]);
await run("dotnet", [
"test",
"ItWorx.KnowledgeHub.slnx",
"--configuration",
"Release",
"--no-build",
// Excludes the M8 full-scale search/tree suite (100,000 pages / 1,000,000
// revisions, docs/10-search-and-indexing.md "Scale verification") — a
// deliberate, occasional release-gate exercise (run explicitly in the
// "release" section below), not a per-commit check; it alone would add well
// over a minute to every routine verification run.
"--filter",
"Category!=ReleaseScale",
// No explicit LogFileName: `dotnet test` runs every project in the solution
// through the same invocation, so a fixed name would have each project's
// results overwrite the previous one. Omitting it lets each project write its
// own default `<assembly>_<timestamp>.trx` file instead.
"--logger",
"trx",
"--results-directory",
"artifacts/test-results"
]);
});
await section("Frontend install and verification", async () => {
const installArgs = fs.existsSync("pnpm-lock.yaml")
? ["pnpm", "install", "--frozen-lockfile"]
: ["pnpm", "install", "--no-frozen-lockfile"];
if (!fs.existsSync("pnpm-lock.yaml")) {
console.warn("Pre-M0: generating pnpm-lock.yaml. Commit it after reviewing dependency versions.");
}
await run("corepack", installArgs);
await run("corepack", ["pnpm", "--filter", "@itworx/knowledge-hub-web", "lint"]);
await run("corepack", ["pnpm", "--filter", "@itworx/knowledge-hub-web", "typecheck"]);
await run("corepack", ["pnpm", "--filter", "@itworx/knowledge-hub-web", "test"]);
await run("corepack", ["pnpm", "--filter", "@itworx/knowledge-hub-web", "build"]);
// Depends on the root workspace's node_modules (js-yaml), so it must run after
// `pnpm install` above, not in the earlier structure-only "Bundle structure"
// section that runs before any dependency install exists on a clean checkout.
await run(process.execPath, ["scripts/validate-openapi.mjs"]);
// Contract-coverage gate (docs/adr/ADR-029-v2-r5-final-hardening.md): fails when a
// registered /api/v1 endpoint is not documented in docs/api/openapi.yaml. Same
// js-yaml dependency, same placement.
await run(process.execPath, ["scripts/check-openapi-coverage.mjs"]);
});
if (!fast) {
await section("Docker configuration", async () => {
await run("docker", ["compose", "config"], {
env: {
POSTGRES_PASSWORD: "verification-only-not-for-production"
}
});
});
}
if (ci || release) {
await section("Production image build", async () => {
await run("docker", ["compose", "build", "app"], {
env: {
POSTGRES_PASSWORD: "verification-only-not-for-production",
BUILD_COMMIT: process.env.GITHUB_SHA ?? process.env.CI_COMMIT_SHA ?? "verification"
}
});
});
}
if (release) {
await section("Full-scale search/tree suite (release-gate only)", async () => {
await run("dotnet", [
"test",
"tests/backend/ItWorx.KnowledgeHub.IntegrationTests/ItWorx.KnowledgeHub.IntegrationTests.csproj",
"--configuration",
"Release",
"--no-build",
"--filter",
"Category=ReleaseScale"
]);
});
await section(".NET dependency vulnerability scan", async () => {
const result = await run(
"dotnet",
["list", "ItWorx.KnowledgeHub.slnx", "package", "--vulnerable", "--include-transitive"],
{ capture: true }
);
// `dotnet list package --vulnerable` always exits 0 and just prints findings —
// the gate itself must fail the release on any reported advisory (M8-A02: "No
// known Critical or High security finding remains unresolved" — since the
// command does not separate by severity in its text output, any finding at all
// blocks release pending explicit triage, matching the release gate's
// zero-tolerance-until-reviewed posture).
if (/has the following vulnerable packages/i.test(result.stdout)) {
throw new Error(
"dotnet reported vulnerable NuGet packages — review the output above before releasing.\n" + result.stdout
);
}
});
await section("Frontend dependency vulnerability scan", async () => {
await run("corepack", ["pnpm", "audit", "--prod"]);
});
await section("Secret scan", async () => {
if (!commandExists("gitleaks")) {
throw new Error("gitleaks is required for the release secret scan but was not found on PATH.");
}
await run("gitleaks", ["detect", "--source", ".", "--redact"]);
});
if (commandExists("trivy")) {
await section("Container vulnerability scan", async () => {
await run("trivy", [
"image",
"--severity",
"CRITICAL,HIGH",
"--exit-code",
"1",
"--scanners",
"vuln",
"itworx/knowledge-hub:local"
]);
});
} else {
console.warn(
"\ntrivy not found on PATH — container vulnerability scan skipped. Install " +
"trivy before a real release; this is not a substitute for the scan."
);
}
console.warn(
"\nRelease verification still requires the milestone-defined E2E (desktop + " +
"mobile, axe), backup/restore rehearsal and runtime-inspection gates to be run " +
"manually against the built image, per the documented procedure in EVIDENCE.md " +
"— these involve bringing up docker compose and are recorded there rather than " +
"orchestrated here."
);
}
console.log("\nVerification completed successfully for the selected baseline gates.");
} catch (error) {
console.error(`\nVerification failed: ${error.message}`);
process.exit(typeof error.code === "number" ? error.code : 1);
}