feat: add domain core and verification harness
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import {
|
||||
contentDigest,
|
||||
kingWenEntries,
|
||||
validateContentPackage,
|
||||
} from "./content-contract.mjs";
|
||||
import { repositoryRoot } from "./repository-files.mjs";
|
||||
|
||||
const schemaPath = path.join(repositoryRoot, "content", "schema", "hexagram-content.schema.json");
|
||||
const schema = JSON.parse(await readFile(schemaPath, "utf8"));
|
||||
if (schema.$schema !== "https://json-schema.org/draft/2020-12/schema") {
|
||||
throw new Error("Content JSON Schema must use draft 2020-12");
|
||||
}
|
||||
|
||||
const kotlinCatalogPath = path.join(
|
||||
repositoryRoot,
|
||||
"app",
|
||||
"src",
|
||||
"main",
|
||||
"java",
|
||||
"brainwave",
|
||||
"domain",
|
||||
"casting",
|
||||
"HexagramCatalog.kt",
|
||||
);
|
||||
const kotlinCatalog = await readFile(kotlinCatalogPath, "utf8");
|
||||
const kotlinPairPattern = /TrigramPair\(Trigram\.(\w+), Trigram\.(\w+)\) to (\d+)/gu;
|
||||
const kotlinPairs = new Map(
|
||||
[...kotlinCatalog.matchAll(kotlinPairPattern)].map((match) => [
|
||||
`${match[1]}/${match[2]}`,
|
||||
Number(match[3]),
|
||||
]),
|
||||
);
|
||||
const contractEntries = kingWenEntries();
|
||||
if (kotlinPairs.size !== 64 || contractEntries.some((entry) =>
|
||||
kotlinPairs.get(`${entry.upperTrigram}/${entry.lowerTrigram}`) !== entry.kingWenNumber)) {
|
||||
throw new Error("Kotlin King Wen catalog and content-contract lookup must match exactly");
|
||||
}
|
||||
|
||||
function validFixture() {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
contentVersion: "fixture-only-v1",
|
||||
specialUsageTexts: { qian: false, kun: false },
|
||||
sources: [{
|
||||
id: "fixture-source",
|
||||
title: "Automated test fixture",
|
||||
edition: "not publishable",
|
||||
license: "test data only",
|
||||
url: "https://example.invalid/fixture",
|
||||
}],
|
||||
hexagrams: contractEntries.map((entry) => ({
|
||||
...entry,
|
||||
name: `fixture-${entry.kingWenNumber}`,
|
||||
symbol: `fixture-symbol-${entry.kingWenNumber}`,
|
||||
judgmentOriginal: "fixture original text",
|
||||
judgmentPlain: "fixture plain text",
|
||||
lineTextsBottomUp: Array.from({ length: 6 }, (_, index) => `fixture original line ${index + 1}`),
|
||||
linePlainBottomUp: Array.from({ length: 6 }, (_, index) => `fixture plain line ${index + 1}`),
|
||||
specialUsageText: null,
|
||||
sourceRefs: ["fixture-source"],
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function clone(value) {
|
||||
return structuredClone(value);
|
||||
}
|
||||
|
||||
const fixture = validFixture();
|
||||
const validErrors = validateContentPackage(fixture);
|
||||
if (validErrors.length > 0) {
|
||||
throw new Error(`Valid content fixture was rejected:\n${validErrors.join("\n")}`);
|
||||
}
|
||||
|
||||
const digest = contentDigest(fixture);
|
||||
if (!/^[a-f0-9]{64}$/u.test(digest) || digest !== contentDigest(clone(fixture))) {
|
||||
throw new Error("Content digest must be a stable SHA-256 value");
|
||||
}
|
||||
|
||||
const negativeCases = [
|
||||
["unsupported schema", (value) => { value.schemaVersion = 2; }, "schemaVersion"],
|
||||
["duplicate id", (value) => { value.hexagrams[1].kingWenNumber = 1; }, "unique"],
|
||||
["wrong bottom-up pattern", (value) => { value.hexagrams[0].patternBottomUp[0] = "YIN"; }, "bottom-up"],
|
||||
["five line texts", (value) => { value.hexagrams[0].lineTextsBottomUp.pop(); }, "exactly six"],
|
||||
["unknown source", (value) => { value.hexagrams[0].sourceRefs = ["missing"]; }, "unknown source"],
|
||||
["blank license", (value) => { value.sources[0].license = " "; }, "non-blank"],
|
||||
["script content", (value) => { value.hexagrams[0].judgmentPlain = "<script>alert(1)</script>"; }, "script-like"],
|
||||
["special declaration mismatch", (value) => { value.specialUsageTexts.qian = true; }, "specialUsageTexts.qian"],
|
||||
];
|
||||
|
||||
for (const [name, mutate, expected] of negativeCases) {
|
||||
const candidate = clone(fixture);
|
||||
mutate(candidate);
|
||||
const errors = validateContentPackage(candidate);
|
||||
if (!errors.some((error) => error.includes(expected))) {
|
||||
throw new Error(`${name} fixture did not fail with '${expected}':\n${errors.join("\n")}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Content contract verification passed (64 entries cross-checked with Kotlin, ${negativeCases.length} negative fixtures, digest ${digest.slice(0, 12)}…).`,
|
||||
);
|
||||
Reference in New Issue
Block a user