fix(library-decisions): traceSchema accepts the committed trace shape
The strict schema rejected 38 of 39 approved traces in docs/library-decisions: lastRevalidated was required (most traces omit it), and the compliance frontmatter fields the _template.md documents (is-sub-processor, processes-pii, plus the sub-processor block: data-sent, region, dpa-signed, sccs-required, contact) were unrecognized keys. Make lastRevalidated optional, add the compliance fields (booleanish coercion for YAML string scalars), and require data-sent when is-sub-processor is true. New loop test asserts every committed trace parses so the schema can never drift away from the repo's own corpus again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,12 @@ import fs from "node:fs";
|
|||||||
|
|
||||||
// ---- Zod schema ----
|
// ---- Zod schema ----
|
||||||
|
|
||||||
|
/** YAML scalars arrive as strings — coerce "true"/"false" (or booleans). */
|
||||||
|
const booleanish = z.preprocess(
|
||||||
|
(v) => (v === "true" ? true : v === "false" ? false : v),
|
||||||
|
z.boolean(),
|
||||||
|
);
|
||||||
|
|
||||||
const filterResultsSchema = z
|
const filterResultsSchema = z
|
||||||
.object({
|
.object({
|
||||||
license: z.string().min(1),
|
license: z.string().min(1),
|
||||||
@@ -30,12 +36,34 @@ export const traceSchema = z
|
|||||||
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "date must be YYYY-MM-DD"),
|
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "date must be YYYY-MM-DD"),
|
||||||
deciders: z.array(z.string()),
|
deciders: z.array(z.string()),
|
||||||
adr: z.string().nullable(),
|
adr: z.string().nullable(),
|
||||||
lastRevalidated: z.string().nullable(),
|
// Optional: most committed traces have never been revalidated and omit
|
||||||
|
// the key entirely (requiring it rejected 38 of 39 approved traces).
|
||||||
|
lastRevalidated: z.string().nullable().optional(),
|
||||||
"filter-results": filterResultsSchema,
|
"filter-results": filterResultsSchema,
|
||||||
"verification-commands": z.array(z.string()),
|
"verification-commands": z.array(z.string()),
|
||||||
"accepted-cves": z.array(z.string()).optional(),
|
"accepted-cves": z.array(z.string()).optional(),
|
||||||
|
// ADR-022 amendment (compliance frontmatter, see _template.md): every
|
||||||
|
// newer trace declares whether the dependency ships data to a third
|
||||||
|
// party; sub-processor traces carry the extra contact/DPA fields.
|
||||||
|
"is-sub-processor": booleanish.optional(),
|
||||||
|
"processes-pii": booleanish.optional(),
|
||||||
|
"data-sent": z.string().nullable().optional(),
|
||||||
|
region: z.string().nullable().optional(),
|
||||||
|
"dpa-signed": z.union([booleanish, z.string()]).nullable().optional(),
|
||||||
|
"sccs-required": z.union([booleanish, z.string()]).nullable().optional(),
|
||||||
|
contact: z.string().nullable().optional(),
|
||||||
})
|
})
|
||||||
.strict();
|
.strict()
|
||||||
|
.superRefine((trace, ctx) => {
|
||||||
|
if (trace["is-sub-processor"] === true && trace["data-sent"] == null) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message:
|
||||||
|
"sub-processor traces must declare data-sent (what leaves the machine)",
|
||||||
|
path: ["data-sent"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ---- Helpers ----
|
// ---- Helpers ----
|
||||||
|
|
||||||
|
|||||||
@@ -218,3 +218,65 @@ describe("parseTrace", () => {
|
|||||||
assert.throws(() => parseTrace(file), /invalid_type|Required/i);
|
assert.throws(() => parseTrace(file), /invalid_type|Required/i);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("validateTrace > compliance frontmatter (ADR-022 amendment)", () => {
|
||||||
|
test("accepts is-sub-processor / processes-pii as YAML string booleans", () => {
|
||||||
|
const parsed = validateTrace(
|
||||||
|
validRaw({ "is-sub-processor": "false", "processes-pii": "true" }),
|
||||||
|
);
|
||||||
|
assert.equal(parsed["is-sub-processor"], false);
|
||||||
|
assert.equal(parsed["processes-pii"], true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("accepts a trace that omits lastRevalidated entirely", () => {
|
||||||
|
const raw = validRaw();
|
||||||
|
delete raw.lastRevalidated;
|
||||||
|
assert.equal(validateTrace(raw).lastRevalidated, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("accepts a full sub-processor block", () => {
|
||||||
|
const parsed = validateTrace(
|
||||||
|
validRaw({
|
||||||
|
"is-sub-processor": "true",
|
||||||
|
"processes-pii": "true",
|
||||||
|
"data-sent": "error events, stack traces",
|
||||||
|
region: "eu",
|
||||||
|
"dpa-signed": "true",
|
||||||
|
"sccs-required": "false",
|
||||||
|
contact: "https://example.com/dpa",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
assert.equal(parsed["is-sub-processor"], true);
|
||||||
|
assert.equal(parsed["data-sent"], "error events, stack traces");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects is-sub-processor: true without data-sent", () => {
|
||||||
|
assert.throws(
|
||||||
|
() => validateTrace(validRaw({ "is-sub-processor": "true" })),
|
||||||
|
/data-sent/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("parseTrace > every committed trace parses", () => {
|
||||||
|
// The schema exists to validate the traces actually in the repo — a schema
|
||||||
|
// that rejects committed, approved traces is broken (it silently rejected
|
||||||
|
// 38 of 39 before lastRevalidated became optional and the compliance
|
||||||
|
// fields were added).
|
||||||
|
test("all docs/library-decisions traces validate", () => {
|
||||||
|
const docsDir = path.join(
|
||||||
|
path.dirname(new URL(import.meta.url).pathname),
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
"docs",
|
||||||
|
"library-decisions",
|
||||||
|
);
|
||||||
|
const files = fs
|
||||||
|
.globSync(path.join(docsDir, "**", "*.md"))
|
||||||
|
.filter((f) => !f.endsWith("_template.md"));
|
||||||
|
assert.ok(files.length > 0, "expected committed traces to exist");
|
||||||
|
for (const f of files) {
|
||||||
|
assert.doesNotThrow(() => parseTrace(f), `trace failed to parse: ${f}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user