feat(scripts): add library-decisions trace schema + template

Creates the shared schema module for library evaluation traces
(ADR-022 §4): Zod-validated frontmatter with all 8 filter fields and
enum constraints, plus parseTrace/validateTrace exports and a custom
YAML frontmatter parser for the nested trace format.

Also adds docs/library-decisions/_template.md with all 11 required
headings (8 Filter + 3 Prompt) in machine-checkable ADR-022 order.

Adds zod as a root devDependency so the script is runnable directly
from the workspace root without a package context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-14 05:10:06 +00:00
parent 16a03e026b
commit f8908e5e4b
5 changed files with 421 additions and 0 deletions

View File

@@ -0,0 +1,143 @@
// scripts/library-decisions/schema.mjs
// Zod-validated schema for library decision trace files.
// Shared by: evaluate-library skill, pre-commit check, sandcastle reviewer.
import { z } from "zod";
import fs from "node:fs";
// ---- Zod schema ----
const filterResultsSchema = z
.object({
license: z.string().min(1),
types: z.string().min(1),
maintenance: z.enum(["active", "dormant", "abandoned"]),
"boundary-fit": z.enum(["pass", "fail"]),
"shadow-check": z.string().min(1),
"eu-residency": z.enum(["ok", "n/a", "self-hostable", "fail"]),
"cve-scan": z.string().min(1),
"named-consumer": z.enum(["pass", "fail"]),
})
.strict();
export const traceSchema = z
.object({
package: z.string().min(1),
version: z.string().min(1),
tier: z.enum(["app", "feature", "core"]),
decision: z.enum(["approved", "rejected"]),
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "date must be YYYY-MM-DD"),
deciders: z.array(z.string()),
adr: z.string().nullable(),
"filter-results": filterResultsSchema,
"verification-commands": z.array(z.string()),
"accepted-cves": z.array(z.string()).optional(),
})
.strict();
// ---- Helpers ----
/** Strip surrounding single- or double-quotes from a YAML scalar. */
function unquote(s) {
if (
(s.startsWith('"') && s.endsWith('"')) ||
(s.startsWith("'") && s.endsWith("'"))
) {
return s.slice(1, -1);
}
return s;
}
/** Parse a YAML inline flow array: `[a, "b", c]` → `["a", "b", "c"]`. */
function parseInlineArray(rawValue) {
const inner = rawValue.slice(1, -1).trim();
return inner === "" ? [] : inner.split(",").map((s) => unquote(s.trim()));
}
/**
* Consume indented children (nested object or block-sequence array) starting
* at `startIdx` in `lines`. Returns the parsed value and the index of the
* first non-indented line (i.e. where the parent should resume scanning).
*/
function parseNestedBlock(lines, startIdx) {
const nested = {};
const arr = [];
let i = startIdx;
while (i < lines.length && /^ {2}/.test(lines[i])) {
const child = lines[i];
const itemMatch = child.match(/^ {2}- (.+)$/);
if (itemMatch) {
arr.push(unquote(itemMatch[1].trim()));
} else {
const nestedMatch = child.match(/^ {2}([\w-]+):\s*(.*)$/);
if (nestedMatch) {
nested[nestedMatch[1]] = unquote(nestedMatch[2].trim());
}
}
i++;
}
return { value: arr.length > 0 ? arr : nested, nextIdx: i };
}
/**
* Parse the YAML frontmatter of a markdown file into a plain JS object.
* Handles the trace format: scalar values, inline flow arrays, one-level
* nested objects, and block-sequence arrays.
*/
export function parseFrontmatter(text) {
const match = text.match(/^---\n([\s\S]+?)\n---/);
if (!match) throw new Error("No YAML frontmatter found");
const lines = match[1].split("\n");
const result = {};
let i = 0;
while (i < lines.length) {
const line = lines[i];
const topMatch = line.match(/^([\w-]+):\s*(.*)$/);
if (!topMatch) {
i++;
continue;
}
const key = topMatch[1];
const rawValue = topMatch[2].trim();
if (rawValue === "") {
const { value, nextIdx } = parseNestedBlock(lines, i + 1);
result[key] = value;
i = nextIdx;
} else if (rawValue.startsWith("[") && rawValue.endsWith("]")) {
result[key] = parseInlineArray(rawValue);
i++;
} else {
const v = unquote(rawValue);
result[key] = v === "null" ? null : v;
i++;
}
}
return result;
}
// ---- Public API ----
/**
* Validate an already-parsed frontmatter object against the trace schema.
* Throws ZodError on failure; returns the parsed (typed) value on success.
*/
export function validateTrace(raw) {
return traceSchema.parse(raw);
}
/**
* Read a trace `.md` file, parse its frontmatter, and validate it.
* Throws on missing frontmatter or schema violations.
*/
export function parseTrace(filePath) {
const text = fs.readFileSync(filePath, "utf8");
const raw = parseFrontmatter(text);
return validateTrace(raw);
}

View File

@@ -0,0 +1,163 @@
import { test, describe } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { validateTrace, parseTrace, parseFrontmatter } from "./schema.mjs";
function validRaw(overrides = {}) {
return {
package: "example-lib",
version: "^1.0.0",
tier: "feature",
decision: "approved",
date: "2026-05-14",
deciders: ["alice"],
adr: null,
"filter-results": {
license: "MIT",
types: "native",
maintenance: "active",
"boundary-fit": "pass",
"shadow-check": "pass",
"eu-residency": "ok",
"cve-scan": "clean",
"named-consumer": "pass",
},
"verification-commands": ["pnpm audit --audit-level=moderate"],
...overrides,
};
}
function writeTempTrace(frontmatter, body = "") {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "trace-"));
const file = path.join(dir, "trace.md");
fs.writeFileSync(file, `---\n${frontmatter}\n---\n\n${body}`);
return file;
}
const VALID_FM = `package: example-lib
version: "^1.0.0"
tier: feature
decision: approved
date: 2026-05-14
deciders: [alice, bob]
adr: null
filter-results:
license: MIT
types: native
maintenance: active
boundary-fit: pass
shadow-check: pass
eu-residency: ok
cve-scan: clean
named-consumer: pass
verification-commands:
- pnpm audit --audit-level=moderate`;
describe("validateTrace > valid cases", () => {
test("valid trace round-trips without error", () => {
const result = validateTrace(validRaw());
assert.equal(result.package, "example-lib");
assert.equal(result["filter-results"].license, "MIT");
assert.deepEqual(result["verification-commands"], [
"pnpm audit --audit-level=moderate",
]);
});
test("accepted-cves absent is valid", () => {
const result = validateTrace(validRaw());
assert.equal(result["accepted-cves"], undefined);
});
test("accepted-cves present is valid", () => {
const result = validateTrace(
validRaw({ "accepted-cves": ["CVE-2024-0001"] }),
);
assert.deepEqual(result["accepted-cves"], ["CVE-2024-0001"]);
});
test("null adr is valid", () => {
assert.equal(validateTrace(validRaw({ adr: null })).adr, null);
});
test("string adr is valid", () => {
assert.equal(validateTrace(validRaw({ adr: "adr-022" })).adr, "adr-022");
});
});
describe("validateTrace > rejection cases", () => {
test("missing required field throws", () => {
const raw = validRaw();
delete raw.package;
assert.throws(() => validateTrace(raw), /invalid_type|Required/i);
});
test("invalid tier enum throws", () => {
assert.throws(
() => validateTrace(validRaw({ tier: "invalid" })),
/invalid_enum_value|Invalid enum value/i,
);
});
test("invalid maintenance enum throws", () => {
const raw = validRaw({
"filter-results": {
...validRaw()["filter-results"],
maintenance: "stale",
},
});
assert.throws(
() => validateTrace(raw),
/invalid_enum_value|Invalid enum value/i,
);
});
test("unknown key in filter-results rejected by strict schema", () => {
const raw = validRaw({
"filter-results": {
...validRaw()["filter-results"],
"unknown-filter": "x",
},
});
assert.throws(
() => validateTrace(raw),
/unrecognized_keys|Unrecognized key/i,
);
});
});
describe("parseFrontmatter", () => {
test("parses scalar, inline array, nested object, block array", () => {
const text = `---\n${VALID_FM}\n---\n\n## Filter: license`;
const raw = parseFrontmatter(text);
assert.equal(raw.package, "example-lib");
assert.equal(raw.version, "^1.0.0");
assert.deepEqual(raw.deciders, ["alice", "bob"]);
assert.equal(raw.adr, null);
assert.equal(raw["filter-results"].license, "MIT");
assert.deepEqual(raw["verification-commands"], [
"pnpm audit --audit-level=moderate",
]);
});
test("throws when no frontmatter delimiters found", () => {
assert.throws(
() => parseFrontmatter("# No frontmatter"),
/No YAML frontmatter/,
);
});
});
describe("parseTrace", () => {
test("reads and validates a valid trace file", () => {
const file = writeTempTrace(VALID_FM, "## Filter: license\n\nok");
assert.equal(parseTrace(file).package, "example-lib");
});
test("throws on missing required field in file", () => {
const fm = VALID_FM.replace(/^package: example-lib\n/m, "");
const file = writeTempTrace(fm, "");
assert.throws(() => parseTrace(file), /invalid_type|Required/i);
});
});