chore: initialize POM 2 repo (docs guards green; release gates recorded NOT RUN pending install)
This commit is contained in:
409
scripts/check-docs.mjs
Normal file
409
scripts/check-docs.mjs
Normal file
@@ -0,0 +1,409 @@
|
||||
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
||||
import { join, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
|
||||
|
||||
// `/pom resume [state-path]` is documented in the README command map and in the POM.yml
|
||||
// `commands:` block, but it is deliberately absent from the HELP template literal in
|
||||
// src/command.ts. HELP is canonical for what the built-in help advertises, so the verb is
|
||||
// neither removed from the docs nor added to HELP. It is recorded here instead, so the
|
||||
// divergence stays visible and the parity check still passes deterministically.
|
||||
// Any command divergence that is not listed here is a failure.
|
||||
const KNOWN_COMMAND_DIVERGENCES = new Set(["resume"]);
|
||||
|
||||
// Roots that src/*.ts loads through `new URL("../<root>", import.meta.url)`. Check 1 derives
|
||||
// this set from source instead of hardcoding it, so a new bundled-resource root cannot be
|
||||
// added in code and silently omitted from the published package.
|
||||
const RESOURCE_SOURCES = ["src/prompts.ts", "src/themes.ts", "src/events.ts"];
|
||||
const BUNDLED_ROOT = /new URL\(\s*["'`]\.\.\/([A-Za-z0-9._-]+)/g;
|
||||
|
||||
// Generated-layout names that drifted in earlier revisions and must never come back.
|
||||
const FORBIDDEN_LAYOUT_NAMES = ["06_exports", "06_logs"];
|
||||
const REQUIRED_LAYOUT_NAMES = ["06_ledgers", "07_exports"];
|
||||
const LAYOUT_DOCS = ["docs/INVENTORY.md", "skills/pom/references/artifacts.md"];
|
||||
|
||||
const VERIFICATION_STATUSES = ["PASS", "FAIL", "NOT RUN", "BLOCKED"];
|
||||
|
||||
const MARKDOWN_LINK = /\[[^\]]*\]\(\s*([^)\s]+?)(?:\s+"[^"]*")?\s*\)/g;
|
||||
const NAVIGATION_PAGES = ["index.md", "navigation.md"];
|
||||
|
||||
// Canonical project coordinates. These live on infrastructure the maintainer owns
|
||||
// (loca.zone). There is no public forge mirror, so a plausible-looking third-party URL
|
||||
// must never be substituted, and the earlier `UNRESOLVED:` placeholders must never return.
|
||||
const METADATA_FIELDS = ["repository", "homepage", "bugs"];
|
||||
const CANONICAL_HOST = "loca.zone";
|
||||
const PLACEHOLDER_MARKER = "UNRESOLVED";
|
||||
|
||||
const TOTAL_CHECKS = 7;
|
||||
|
||||
const checks = [];
|
||||
const failed = [];
|
||||
|
||||
function text(rel) {
|
||||
return readFileSync(join(root, rel), "utf8");
|
||||
}
|
||||
|
||||
function lineNumbers(body, pattern) {
|
||||
return body
|
||||
.split("\n")
|
||||
.map((line, index) => (pattern.test(line) ? index + 1 : 0))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
/** Blanks fenced blocks and inline code spans so illustrative links are not link-checked. */
|
||||
function withoutCode(body) {
|
||||
const blank = (matched) => matched.replace(/[^\n]/g, " ");
|
||||
return body.replace(/^```[\s\S]*?^```/gm, blank).replace(/`[^`\n]*`/g, blank);
|
||||
}
|
||||
|
||||
/** Reads the indented body of a top-level or nested block key, e.g. `commands:`. */
|
||||
function blockLines(rel, key) {
|
||||
const lines = text(rel).split("\n");
|
||||
const start = lines.findIndex((line) => new RegExp(`^\\s*${key}:\\s*$`).test(line));
|
||||
if (start < 0) return null;
|
||||
const indent = lines[start].length - lines[start].trimStart().length;
|
||||
const body = [];
|
||||
for (const line of lines.slice(start + 1)) {
|
||||
if (line.trim() === "") continue;
|
||||
if (line.length - line.trimStart().length <= indent) break;
|
||||
body.push(line.trim());
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
/** `/pom` -> `dashboard`; `/pom theme install` -> `theme`. */
|
||||
function verbOf(command) {
|
||||
const match = /^\/pom(?:\s+([a-z][a-z0-9-]*))?\b/.exec(command);
|
||||
if (!match) return null;
|
||||
return match[1] ?? "dashboard";
|
||||
}
|
||||
|
||||
function check(index, name, run) {
|
||||
const problems = [];
|
||||
const notes = [];
|
||||
try {
|
||||
run((message) => problems.push(message), (message) => notes.push(message));
|
||||
} catch (error) {
|
||||
problems.push(`unexpected error: ${error.message}`);
|
||||
}
|
||||
console.log(`${problems.length === 0 ? "PASS" : "FAIL"} check ${index}/${TOTAL_CHECKS} ${name}`);
|
||||
for (const problem of problems) console.log(` ${problem}`);
|
||||
for (const note of notes) console.log(` note: ${note}`);
|
||||
if (problems.length > 0) failed.push(name);
|
||||
}
|
||||
|
||||
// 1. Resource/package parity: every bundled-resource root loaded by source must ship.
|
||||
checks.push((index) =>
|
||||
check(index, "resource-package-parity", (fail) => {
|
||||
const roots = new Map();
|
||||
for (const rel of RESOURCE_SOURCES) {
|
||||
if (!existsSync(join(root, rel))) {
|
||||
fail(`${rel}: resource-loading module is missing; check 1 cannot derive bundled roots from source`);
|
||||
continue;
|
||||
}
|
||||
const body = text(rel);
|
||||
for (const match of body.matchAll(BUNDLED_ROOT)) {
|
||||
if (!roots.has(match[1])) roots.set(match[1], new Set());
|
||||
roots.get(match[1]).add(rel);
|
||||
}
|
||||
}
|
||||
if (roots.size === 0) {
|
||||
fail(
|
||||
`no bundled-resource roots discovered in ${RESOURCE_SOURCES.join(", ")}; the ` +
|
||||
`\`new URL("../<root>", import.meta.url)\` pattern in scripts/check-docs.mjs no longer ` +
|
||||
"matches source and must be repaired (this is a broken check, not an empty result)",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const files = JSON.parse(text("package.json")).files;
|
||||
if (!Array.isArray(files)) {
|
||||
fail("package.json: `files` must be an array listing every published path");
|
||||
return;
|
||||
}
|
||||
for (const [name, sources] of [...roots].sort(([left], [right]) => left.localeCompare(right))) {
|
||||
if (!files.includes(name))
|
||||
fail(`package.json: \`files\` must contain "${name}" because ${[...sources].join(" and ")} load it at runtime`);
|
||||
if (!existsSync(join(root, name)))
|
||||
fail(`${name}: directory is loaded by ${[...sources].join(" and ")} but does not exist in the repository`);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// 2. Command parity: HELP, the README command map, and POM.yml `commands:` must agree.
|
||||
checks.push((index) =>
|
||||
check(index, "command-parity", (fail, note) => {
|
||||
const command = text("src/command.ts");
|
||||
const help = /const HELP\s*=\s*`([\s\S]*?)`\s*;/.exec(command);
|
||||
if (!help) {
|
||||
fail("src/command.ts: could not locate the `const HELP = `...`;` template literal that defines the command surface");
|
||||
return;
|
||||
}
|
||||
const helpVerbs = new Set();
|
||||
for (const line of help[1].split("\n")) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed.startsWith("/pom")) continue;
|
||||
const verb = verbOf(trimmed);
|
||||
if (verb) helpVerbs.add(verb);
|
||||
}
|
||||
|
||||
const readmeLines = text("README.md").split("\n");
|
||||
const heading = readmeLines.findIndex((line) => /^##\s+Command map\s*$/.test(line));
|
||||
if (heading < 0) {
|
||||
fail("README.md: missing the `## Command map` heading that check 2 reads the command table from");
|
||||
return;
|
||||
}
|
||||
const after = readmeLines.slice(heading + 1);
|
||||
const end = after.findIndex((line) => /^##\s/.test(line));
|
||||
const readmeVerbs = new Set();
|
||||
for (const line of end < 0 ? after : after.slice(0, end)) {
|
||||
if (!line.trimStart().startsWith("|")) continue;
|
||||
for (const span of line.match(/`[^`]+`/g) ?? []) {
|
||||
const verb = verbOf(span.slice(1, -1).trim());
|
||||
if (verb) readmeVerbs.add(verb);
|
||||
}
|
||||
}
|
||||
|
||||
const commandBlock = blockLines("POM.yml", "commands");
|
||||
if (!commandBlock) {
|
||||
fail("POM.yml: missing the top-level `commands:` block that check 2 reads the command map from");
|
||||
return;
|
||||
}
|
||||
const manifestVerbs = new Set();
|
||||
for (const entry of commandBlock) {
|
||||
const value = /:\s*(.+)$/.exec(entry);
|
||||
if (!value) continue;
|
||||
const verb = verbOf(value[1].trim().replace(/^["']|["']$/g, ""));
|
||||
if (verb) manifestVerbs.add(verb);
|
||||
}
|
||||
|
||||
if (helpVerbs.size === 0) fail("src/command.ts: HELP advertises no `/pom` verbs");
|
||||
if (readmeVerbs.size === 0) fail("README.md: the `## Command map` table lists no `/pom` commands");
|
||||
if (manifestVerbs.size === 0) fail("POM.yml: the `commands:` block lists no `/pom` commands");
|
||||
|
||||
const sources = [
|
||||
["src/command.ts HELP", helpVerbs],
|
||||
["README.md `## Command map`", readmeVerbs],
|
||||
["POM.yml `commands:`", manifestVerbs],
|
||||
];
|
||||
const union = [...new Set(sources.flatMap(([, verbs]) => [...verbs]))].sort();
|
||||
for (const verb of union) {
|
||||
const missing = sources.filter(([, verbs]) => !verbs.has(verb)).map(([label]) => label);
|
||||
if (missing.length === 0) continue;
|
||||
if (KNOWN_COMMAND_DIVERGENCES.has(verb)) {
|
||||
note(`/pom ${verb} is absent from ${missing.join(", ")}; allowlisted in KNOWN_COMMAND_DIVERGENCES`);
|
||||
continue;
|
||||
}
|
||||
const present = sources.filter(([, verbs]) => verbs.has(verb)).map(([label]) => label);
|
||||
fail(
|
||||
`/pom ${verb} is documented in ${present.join(", ")} but absent from ${missing.join(", ")}; ` +
|
||||
"add it there or record it in KNOWN_COMMAND_DIVERGENCES in scripts/check-docs.mjs",
|
||||
);
|
||||
}
|
||||
for (const verb of KNOWN_COMMAND_DIVERGENCES) {
|
||||
if (!union.includes(verb))
|
||||
fail(
|
||||
`KNOWN_COMMAND_DIVERGENCES lists "${verb}" but no source documents /pom ${verb}; ` +
|
||||
"remove the stale allowlist entry from scripts/check-docs.mjs",
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// 3. Layout contract: the generated project tree names in docs must match src/persistence.ts.
|
||||
checks.push((index) =>
|
||||
check(index, "layout-contract", (fail) => {
|
||||
for (const rel of LAYOUT_DOCS) {
|
||||
if (!existsSync(join(root, rel))) {
|
||||
fail(`${rel}: layout documentation is missing`);
|
||||
continue;
|
||||
}
|
||||
const body = text(rel);
|
||||
for (const name of FORBIDDEN_LAYOUT_NAMES) {
|
||||
const lines = lineNumbers(body, new RegExp(`\\b${name}\\b`));
|
||||
if (lines.length > 0)
|
||||
fail(
|
||||
`${rel}:${lines.join(",")} names "${name}", which src/persistence.ts bootstrapProject never ` +
|
||||
`creates; the canonical names are ${REQUIRED_LAYOUT_NAMES.join(" and ")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const inventory = text("docs/INVENTORY.md");
|
||||
for (const name of REQUIRED_LAYOUT_NAMES) {
|
||||
if (!new RegExp(`\\b${name}\\b`).test(inventory))
|
||||
fail(`docs/INVENTORY.md: must document the canonical generated directory "${name}"`);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// 4. Prompt-order contract: POM.yml must mirror the composition order in src/prompts.ts.
|
||||
checks.push((index) =>
|
||||
check(index, "prompt-order-contract", (fail) => {
|
||||
const items = blockLines("POM.yml", "prompt_precedence");
|
||||
if (!items) {
|
||||
fail("POM.yml: missing the `prompt_precedence:` list under `knowledge:`");
|
||||
return;
|
||||
}
|
||||
const entries = items.filter((item) => item.startsWith("- ")).map((item) => item.slice(2).trim());
|
||||
if (entries.length < 3) {
|
||||
fail(`POM.yml: \`prompt_precedence\` must list the full composition order, found ${entries.length} entries`);
|
||||
return;
|
||||
}
|
||||
const first = entries[0];
|
||||
const last = entries[entries.length - 1];
|
||||
if (!/immutable/i.test(first) || !/law/i.test(first))
|
||||
fail(
|
||||
`POM.yml: \`prompt_precedence\` entry 1 must name the immutable POM production law that ` +
|
||||
`composePromptStack prepends, found "${first}"`,
|
||||
);
|
||||
if (!/executable stage contract/i.test(last))
|
||||
fail(
|
||||
`POM.yml: \`prompt_precedence\` must end with the executable stage contract appended by ` +
|
||||
`renderStagePrompt, found "${last}" as the final entry`,
|
||||
);
|
||||
const misplaced = entries
|
||||
.slice(0, -1)
|
||||
.map((entry, position) => (/executable stage contract/i.test(entry) ? position + 1 : 0))
|
||||
.filter(Boolean);
|
||||
if (misplaced.length > 0)
|
||||
fail(
|
||||
`POM.yml: \`prompt_precedence\` mentions the executable stage contract at entry ` +
|
||||
`${misplaced.join(",")}; renderStagePrompt appends it last, so it must be the final entry only`,
|
||||
);
|
||||
if (!entries.some((entry) => /replac/i.test(entry) && /\bID\b/i.test(entry)))
|
||||
fail(
|
||||
"POM.yml: `prompt_precedence` must state that project-local fragments replace bundled fragments " +
|
||||
"by matching basename ID, as src/prompts.ts composePromptStack does",
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
// 5. Link/orphan check: relative doc links must resolve, and every page must be reachable.
|
||||
checks.push((index) =>
|
||||
check(index, "link-and-orphan", (fail) => {
|
||||
const pages = readdirSync(join(root, "docs"))
|
||||
.filter((name) => name.endsWith(".md"))
|
||||
.sort();
|
||||
if (pages.length === 0) {
|
||||
fail("docs: contains no Markdown pages");
|
||||
return;
|
||||
}
|
||||
const reachable = new Set();
|
||||
for (const page of pages) {
|
||||
const rel = `docs/${page}`;
|
||||
const body = withoutCode(text(rel));
|
||||
for (const match of body.matchAll(MARKDOWN_LINK)) {
|
||||
const target = match[1];
|
||||
if (/^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith("#") || target.startsWith("/")) continue;
|
||||
const path = target.split("#")[0].split("?")[0];
|
||||
if (path === "") continue;
|
||||
const resolved = resolve(join(root, "docs"), path);
|
||||
if (NAVIGATION_PAGES.includes(page)) reachable.add(resolved);
|
||||
if (!existsSync(resolved))
|
||||
fail(
|
||||
`${rel}: broken relative link \`${target}\` resolves to ${relative(root, resolved)}, ` +
|
||||
"which does not exist on disk",
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const page of pages) {
|
||||
if (page === "index.md") continue;
|
||||
if (reachable.has(resolve(join(root, "docs", page)))) continue;
|
||||
fail(
|
||||
`docs/${page}: unreachable page, no link from ${NAVIGATION_PAGES.map((name) => `docs/${name}`).join(" or ")}; ` +
|
||||
"add it to the navigation tables so the wiki has no orphans",
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// 6. Release-evidence freshness: verification gates must be evidence objects, never literals.
|
||||
checks.push((index) =>
|
||||
check(index, "release-evidence", (fail) => {
|
||||
const release = JSON.parse(text("RELEASE.json"));
|
||||
const verification = release.verification;
|
||||
if (verification === undefined || verification === null || typeof verification !== "object" || Array.isArray(verification)) {
|
||||
fail("RELEASE.json: `verification` must be an object mapping each gate to an evidence object");
|
||||
return;
|
||||
}
|
||||
const gates = Object.entries(verification);
|
||||
if (gates.length === 0) {
|
||||
fail("RELEASE.json: `verification` lists no gates");
|
||||
return;
|
||||
}
|
||||
for (const [gate, value] of gates) {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
||||
fail(
|
||||
`RELEASE.json: verification.${gate} is the bare ${typeof value} ${JSON.stringify(value)}; every gate must ` +
|
||||
`be an evidence object carrying a \`status\` of ${VERIFICATION_STATUSES.join(" | ")}, never a hard-coded literal`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (typeof value.status !== "string") {
|
||||
fail(`RELEASE.json: verification.${gate} is missing a string \`status\` field`);
|
||||
continue;
|
||||
}
|
||||
if (!VERIFICATION_STATUSES.includes(value.status))
|
||||
fail(
|
||||
`RELEASE.json: verification.${gate}.status is "${value.status}"; allowed values are ` +
|
||||
`${VERIFICATION_STATUSES.join(" | ")}`,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// 7. Package metadata: canonical coordinates must stay resolved and stay on owned infrastructure.
|
||||
checks.push((index) =>
|
||||
check(index, "package-metadata", (fail, note) => {
|
||||
const pkg = JSON.parse(text("package.json"));
|
||||
for (const field of METADATA_FIELDS) {
|
||||
const value = pkg[field];
|
||||
if (value === undefined) {
|
||||
fail(`package.json: \`${field}\` is missing; canonical project coordinates are required`);
|
||||
continue;
|
||||
}
|
||||
const urls = [];
|
||||
if (typeof value === "string") urls.push(value);
|
||||
else if (typeof value === "object" && value !== null) {
|
||||
if (typeof value.url === "string") urls.push(value.url);
|
||||
else fail(`package.json: \`${field}\` is an object without a string \`url\``);
|
||||
} else {
|
||||
fail(`package.json: \`${field}\` must be a string or an object with a \`url\``);
|
||||
continue;
|
||||
}
|
||||
for (const url of urls) {
|
||||
if (url.includes(PLACEHOLDER_MARKER)) {
|
||||
fail(
|
||||
`package.json: \`${field}\` still carries the ${PLACEHOLDER_MARKER} placeholder "${url}"; ` +
|
||||
"set a real canonical URL before publishing",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let host;
|
||||
try {
|
||||
host = new URL(url.replace(/^git\+/, "")).host;
|
||||
} catch {
|
||||
fail(`package.json: \`${field}\` value "${url}" is not a parseable absolute URL`);
|
||||
continue;
|
||||
}
|
||||
if (host !== CANONICAL_HOST && !host.endsWith(`.${CANONICAL_HOST}`))
|
||||
fail(
|
||||
`package.json: \`${field}\` points at "${host}", which is outside the canonical ${CANONICAL_HOST} ` +
|
||||
"infrastructure; no public forge mirror has been confirmed for this package",
|
||||
);
|
||||
}
|
||||
}
|
||||
const repository = typeof pkg.repository?.url === "string" ? pkg.repository.url : "";
|
||||
if (repository.includes(CANONICAL_HOST))
|
||||
note(`${repository} is reserved on owned infrastructure and is not yet serving Git`);
|
||||
}),
|
||||
);
|
||||
|
||||
checks.forEach((run, position) => run(position + 1));
|
||||
|
||||
if (failed.length === 0) {
|
||||
console.log("POM docs verification PASS");
|
||||
} else {
|
||||
console.log(`POM docs verification FAIL (${failed.length}/${checks.length}: ${failed.join(", ")})`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
5
scripts/install.ps1
Normal file
5
scripts/install.ps1
Normal file
@@ -0,0 +1,5 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
npm install
|
||||
npm run verify
|
||||
omp plugin link .
|
||||
Write-Host "POM linked. Restart OMP or run /reload-plugins, then run /pom theme install."
|
||||
6
scripts/install.sh
Executable file
6
scripts/install.sh
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
npm install
|
||||
npm run verify
|
||||
omp plugin link .
|
||||
printf '%s\n' 'POM linked. Restart OMP or run /reload-plugins, then run /pom theme install.'
|
||||
117
scripts/release-manifest.mjs
Normal file
117
scripts/release-manifest.mjs
Normal file
@@ -0,0 +1,117 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
||||
import { join, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
|
||||
const packageJson = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
||||
|
||||
function walk(path) {
|
||||
if (!existsSync(path)) throw new Error(`Missing packaged path: ${relative(root, path)}`);
|
||||
if (statSync(path).isFile()) return [path];
|
||||
return readdirSync(path, { withFileTypes: true }).flatMap((entry) => walk(join(path, entry.name)));
|
||||
}
|
||||
|
||||
const candidates = packageJson.files
|
||||
.filter((item) => item !== "RELEASE.json")
|
||||
.flatMap((item) => walk(join(root, item)));
|
||||
candidates.push(join(root, "package.json"));
|
||||
|
||||
const sourceInventory = [...new Set(candidates)]
|
||||
.sort()
|
||||
.map((path) => {
|
||||
const data = readFileSync(path);
|
||||
return {
|
||||
path: relative(root, path).replaceAll("\\", "/"),
|
||||
bytes: data.length,
|
||||
sha256: createHash("sha256").update(data).digest("hex"),
|
||||
};
|
||||
});
|
||||
|
||||
function readGate(file, gate) {
|
||||
const path = join(root, file);
|
||||
if (!existsSync(path)) return { status: "NOT RUN", reason: `Missing evidence file: ${file}` };
|
||||
try {
|
||||
const data = JSON.parse(readFileSync(path, "utf8"));
|
||||
const passed = data.passed === true || (data.summary && data.summary.passed === true);
|
||||
return {
|
||||
status: passed ? "PASS" : "FAIL",
|
||||
timestamp: data.timestamp || null,
|
||||
scope: data.scope || gate,
|
||||
command: data.command || null,
|
||||
details: data,
|
||||
};
|
||||
} catch {
|
||||
return { status: "NOT RUN", reason: `Unparsable evidence: ${file}` };
|
||||
}
|
||||
}
|
||||
|
||||
const gates = {
|
||||
typescript: readGate("00_admin/validation/typecheck.json", "tsc --noEmit"),
|
||||
behavioralTests: readGate("00_admin/validation/tests.json", "npm test"),
|
||||
runtimeSmoke: readGate("00_admin/validation/smoke.json", "npm run smoke"),
|
||||
structuralVerification: readGate("00_admin/validation/structural.json", "node scripts/verify.mjs"),
|
||||
themeSchemaValidation: readGate("00_admin/validation/theme.json", "theme schema validation"),
|
||||
npmPackDryRun: readGate("00_admin/validation/pack.json", "npm pack --dry-run"),
|
||||
};
|
||||
|
||||
const releaseDate = new Date().toISOString().split("T")[0];
|
||||
|
||||
const release = {
|
||||
name: "POM — Produce Magnum Opus",
|
||||
package: packageJson.name,
|
||||
version: packageJson.version,
|
||||
schemaVersion: "2.0.0",
|
||||
releaseDate,
|
||||
status: "release-candidate",
|
||||
summary: "OMP-native production kernel with evidence-gated stages, native task/IRC delegation, optional Swarm DAGs, Obsidian/Quartz knowledge projection, cinematic TUI hooks, and verified archive delivery.",
|
||||
compatibility: {
|
||||
bun: packageJson.engines.bun,
|
||||
ompPackages: ">=16.4.6 <17",
|
||||
module: "ESM",
|
||||
},
|
||||
inventory: {
|
||||
stages: 9,
|
||||
customTools: 7,
|
||||
agents: 14,
|
||||
prompts: 13,
|
||||
rules: 3,
|
||||
themes: 2,
|
||||
behavioralTests: 9,
|
||||
sourceFilesHashed: sourceInventory.length,
|
||||
sourceBytesHashed: sourceInventory.reduce((sum, item) => sum + item.bytes, 0),
|
||||
selfHashPolicy: "RELEASE.json is excluded from its own source inventory; final package and archive hashes are recorded externally.",
|
||||
},
|
||||
verification: {
|
||||
typescript: gates.typescript,
|
||||
behavioralTests: gates.behavioralTests,
|
||||
runtimeSmoke: gates.runtimeSmoke,
|
||||
structuralVerification: gates.structuralVerification,
|
||||
themeSchemaValidation: gates.themeSchemaValidation,
|
||||
npmPackDryRun: gates.npmPackDryRun,
|
||||
productionDependencyAudit: { status: "NOT RUN", reason: "Requires network access to npm registry" },
|
||||
fullDevelopmentTreeAudit: { status: "NOT RUN", reason: "Requires network access to npm registry" },
|
||||
ompPluginDoctor: { status: "NOT RUN", reason: "Requires Bun + OMP installation" },
|
||||
interactiveTuiSmoke: { status: "NOT RUN", reason: "Requires a live OMP terminal host" },
|
||||
},
|
||||
releaseGates: [
|
||||
"Run npm run verify",
|
||||
"Run npm run doctor in a Bun + OMP installation",
|
||||
"Open /pom and verify the overlay, HUD, status, cards, and /pom theme install",
|
||||
"Create a smoke project, validate stage 0, and verify a checkpoint archive",
|
||||
],
|
||||
sourceInventory,
|
||||
};
|
||||
|
||||
const rendered = `${JSON.stringify(release, null, 2)}\n`;
|
||||
const output = join(root, "RELEASE.json");
|
||||
if (process.argv.includes("--check")) {
|
||||
if (!existsSync(output) || readFileSync(output, "utf8") !== rendered) {
|
||||
console.error("RELEASE.json is stale; run npm run release:manifest");
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("POM release manifest PASS");
|
||||
} else {
|
||||
writeFileSync(output, rendered);
|
||||
console.log(`Wrote RELEASE.json · ${sourceInventory.length} files · ${release.inventory.sourceBytesHashed} bytes`);
|
||||
}
|
||||
26
scripts/smoke.ts
Normal file
26
scripts/smoke.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { DEFAULT_CONFIG } from "../src/domain";
|
||||
import { bootstrapProject } from "../src/persistence";
|
||||
import { createState } from "../src/state";
|
||||
import { validatePom } from "../src/validators";
|
||||
|
||||
const sandbox = await mkdtemp(join(tmpdir(), "pom-smoke-"));
|
||||
try {
|
||||
const config = { ...DEFAULT_CONFIG, projectRootMode: "cwd" as const };
|
||||
const state = createState("POM Smoke", sandbox, config, true);
|
||||
await bootstrapProject(state);
|
||||
const ompConfig = await readFile(join(sandbox, ".omp", "config.yml"), "utf8");
|
||||
assert.match(ompConfig, /dark: pom-nocturne/);
|
||||
assert.match(ompConfig, /task:\n\s+eager: preferred/);
|
||||
assert.match(ompConfig, /compaction:\n\s+strategy: snapcompact/);
|
||||
const board = await readFile(join(sandbox, "content", "production-board.md"), "utf8");
|
||||
assert.match(board, /# Production Board/);
|
||||
const report = await validatePom(state, "quick", { writeReport: false });
|
||||
assert.equal(report.passed, true);
|
||||
console.log(`POM runtime smoke PASS · ${report.checks.length} checks`);
|
||||
} finally {
|
||||
await rm(sandbox, { recursive: true, force: true });
|
||||
}
|
||||
77
scripts/verify.mjs
Normal file
77
scripts/verify.mjs
Normal file
@@ -0,0 +1,77 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
|
||||
const required = [
|
||||
"POM.yml",
|
||||
"README.md",
|
||||
"RELEASE.json",
|
||||
"LICENSE",
|
||||
"package.json",
|
||||
"src/index.ts",
|
||||
"src/events.ts",
|
||||
"src/tools.ts",
|
||||
"src/visual.ts",
|
||||
"skills/pom/SKILL.md",
|
||||
"config/APPEND_SYSTEM.md",
|
||||
"prompts/stage-run.md",
|
||||
"themes/pom-nocturne.json",
|
||||
"themes/pom-parchment.json",
|
||||
"scripts/smoke.ts",
|
||||
];
|
||||
for (const rel of required) assert(existsSync(join(root, rel)), `missing ${rel}`);
|
||||
|
||||
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
||||
const release = JSON.parse(readFileSync(join(root, "RELEASE.json"), "utf8"));
|
||||
assert.equal(release.version, pkg.version, "release manifest version mismatch");
|
||||
assert.equal(release.inventory.stages, 9, "release stage inventory mismatch");
|
||||
assert.equal(release.inventory.customTools, 7, "release tool inventory mismatch");
|
||||
assert.equal(release.inventory.behavioralTests, 9, "release test inventory mismatch");
|
||||
assert.deepEqual(pkg.omp.extensions, ["./src/index.ts"], "exactly one OMP entry required");
|
||||
assert.equal(pkg.version, "2.0.0", "release version must match POM 2");
|
||||
for (const rel of pkg.files) assert(existsSync(join(root, rel)), `package files entry does not exist: ${rel}`);
|
||||
|
||||
const command = readFileSync(join(root, "src/command.ts"), "utf8");
|
||||
assert.equal((command.match(/registerCommand\("pom"/g) ?? []).length, 1, "exactly one /pom registration required");
|
||||
assert(command.includes('registerShortcut("alt+p"'), "Alt+P command-center shortcut required");
|
||||
|
||||
const domain = readFileSync(join(root, "src/domain.ts"), "utf8");
|
||||
assert.equal((domain.match(/\bid:\s*[0-8],/g) ?? []).length, 9, "nine stages required");
|
||||
|
||||
const tools = readFileSync(join(root, "src/tools.ts"), "utf8");
|
||||
assert.equal((tools.match(/name:\s*"pom_[a-z]+"/g) ?? []).length, 7, "seven POM tools required");
|
||||
for (const forbidden of ["allowArgs", "formatApprovalDetails", 'concurrency: "exclusive"', "ExtensionAPI = any"]) {
|
||||
assert(!tools.includes(forbidden), `unsupported or unsafe registration field present: ${forbidden}`);
|
||||
}
|
||||
|
||||
assert(!existsSync(join(root, "src/omp-shim.d.ts")), "OMP declaration shim must not ship");
|
||||
assert(!existsSync(join(root, "src/node-shim.d.ts")), "Node declaration shim must not ship");
|
||||
|
||||
const markdownCount = (dir) => readdirSync(join(root, dir)).filter((name) => name.endsWith(".md")).length;
|
||||
assert.equal(markdownCount("agents"), 14, "fourteen specialist agents required");
|
||||
assert.equal(markdownCount("prompts"), 13, "thirteen prompt fragments required");
|
||||
assert.equal(markdownCount("rules"), 3, "three rules required");
|
||||
assert.equal(readdirSync(join(root, "themes")).filter((name) => name.endsWith(".json")).length, 2, "two POM themes required");
|
||||
|
||||
for (const file of readdirSync(join(root, "agents")).filter((name) => name.endsWith(".md"))) {
|
||||
const text = readFileSync(join(root, "agents", file), "utf8");
|
||||
assert(text.startsWith("---\n"), `${file} missing frontmatter`);
|
||||
assert(/^name:\s*pom-[a-z0-9-]+$/m.test(text), `${file} missing valid agent name`);
|
||||
assert(/^description:\s*\S.+$/m.test(text), `${file} missing description`);
|
||||
assert(/^tools:\s*.*\byield\b.*$/m.test(text), `${file} must expose yield`);
|
||||
}
|
||||
|
||||
const themeSchema = JSON.parse(readFileSync(join(root, "node_modules/@oh-my-pi/pi-coding-agent/src/modes/theme/theme-schema.json"), "utf8"));
|
||||
for (const file of readdirSync(join(root, "themes")).filter((name) => name.endsWith(".json"))) {
|
||||
const theme = JSON.parse(readFileSync(join(root, "themes", file), "utf8"));
|
||||
assert.equal(theme.name, file.slice(0, -5), `${file} name mismatch`);
|
||||
for (const key of themeSchema.properties.colors.required) assert(key in theme.colors, `${file} missing color ${key}`);
|
||||
}
|
||||
|
||||
for (const rel of ["src", "skills", "agents", "rules", "prompts", "themes", "config", "docs", "scripts"]) {
|
||||
assert(statSync(join(root, rel)).isDirectory(), `${rel} must be a directory`);
|
||||
}
|
||||
|
||||
console.log("POM structural verification PASS");
|
||||
Reference in New Issue
Block a user