feat: add domain core and verification harness

This commit is contained in:
QiuSW
2026-08-04 23:28:10 +08:00
parent a82af6939f
commit bad67fa5a8
38 changed files with 2054 additions and 43 deletions
+203
View File
@@ -0,0 +1,203 @@
import { createHash } from "node:crypto";
const trigramPatterns = Object.freeze({
QIAN: ["YANG", "YANG", "YANG"],
DUI: ["YANG", "YANG", "YIN"],
LI: ["YANG", "YIN", "YANG"],
ZHEN: ["YANG", "YIN", "YIN"],
XUN: ["YIN", "YANG", "YANG"],
KAN: ["YIN", "YANG", "YIN"],
GEN: ["YIN", "YIN", "YANG"],
KUN: ["YIN", "YIN", "YIN"],
});
const kingWenPairs = Object.freeze([
["QIAN", "QIAN"], ["KUN", "KUN"], ["KAN", "ZHEN"], ["GEN", "KAN"],
["KAN", "QIAN"], ["QIAN", "KAN"], ["KUN", "KAN"], ["KAN", "KUN"],
["XUN", "QIAN"], ["QIAN", "DUI"], ["KUN", "QIAN"], ["QIAN", "KUN"],
["QIAN", "LI"], ["LI", "QIAN"], ["KUN", "GEN"], ["ZHEN", "KUN"],
["DUI", "ZHEN"], ["GEN", "XUN"], ["KUN", "DUI"], ["XUN", "KUN"],
["LI", "ZHEN"], ["GEN", "LI"], ["GEN", "KUN"], ["KUN", "ZHEN"],
["QIAN", "ZHEN"], ["GEN", "QIAN"], ["GEN", "ZHEN"], ["DUI", "XUN"],
["KAN", "KAN"], ["LI", "LI"], ["DUI", "GEN"], ["ZHEN", "XUN"],
["QIAN", "GEN"], ["ZHEN", "QIAN"], ["LI", "KUN"], ["KUN", "LI"],
["XUN", "LI"], ["LI", "DUI"], ["KAN", "GEN"], ["ZHEN", "KAN"],
["GEN", "DUI"], ["XUN", "ZHEN"], ["DUI", "QIAN"], ["QIAN", "XUN"],
["DUI", "KUN"], ["KUN", "XUN"], ["DUI", "KAN"], ["KAN", "XUN"],
["DUI", "LI"], ["LI", "XUN"], ["ZHEN", "ZHEN"], ["GEN", "GEN"],
["XUN", "GEN"], ["ZHEN", "DUI"], ["ZHEN", "LI"], ["LI", "GEN"],
["XUN", "XUN"], ["DUI", "DUI"], ["XUN", "KAN"], ["KAN", "DUI"],
["XUN", "DUI"], ["ZHEN", "GEN"], ["KAN", "LI"], ["LI", "KAN"],
]);
const kingWenByPair = new Map(
kingWenPairs.map(([upper, lower], index) => [`${upper}/${lower}`, index + 1]),
);
const unsafeText = /(?:<\s*script\b|javascript\s*:|\bon\w+\s*=|[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f])/iu;
function isObject(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function nonBlank(value) {
return typeof value === "string" && value.trim().length > 0;
}
function validateText(value, path, errors) {
if (!nonBlank(value)) {
errors.push(`${path} must be non-blank text`);
} else if (unsafeText.test(value)) {
errors.push(`${path} contains script-like markup or an invisible control character`);
}
}
function validateSixTexts(value, path, errors) {
if (!Array.isArray(value) || value.length !== 6) {
errors.push(`${path} must contain exactly six bottom-up entries`);
return;
}
value.forEach((text, index) => validateText(text, `${path}[${index}]`, errors));
}
function canonicalize(value) {
if (Array.isArray(value)) return `[${value.map(canonicalize).join(",")}]`;
if (isObject(value)) {
return `{${Object.keys(value).sort().map((key) =>
`${JSON.stringify(key)}:${canonicalize(value[key])}`).join(",")}}`;
}
return JSON.stringify(value);
}
export function contentDigest(contentPackage) {
return createHash("sha256").update(canonicalize(contentPackage), "utf8").digest("hex");
}
export function kingWenEntries() {
return kingWenPairs.map(([upperTrigram, lowerTrigram], index) => ({
kingWenNumber: index + 1,
upperTrigram,
lowerTrigram,
patternBottomUp: [...trigramPatterns[lowerTrigram], ...trigramPatterns[upperTrigram]],
}));
}
export function validateContentPackage(contentPackage) {
const errors = [];
if (!isObject(contentPackage)) return ["content package must be an object"];
if (contentPackage.schemaVersion !== 1) errors.push("schemaVersion must be 1");
validateText(contentPackage.contentVersion, "contentVersion", errors);
const usage = contentPackage.specialUsageTexts;
if (!isObject(usage) || typeof usage.qian !== "boolean" || typeof usage.kun !== "boolean") {
errors.push("specialUsageTexts must declare boolean qian and kun flags");
}
const sourceIds = new Set();
if (!Array.isArray(contentPackage.sources) || contentPackage.sources.length === 0) {
errors.push("sources must contain at least one licensed source");
} else {
contentPackage.sources.forEach((source, index) => {
const prefix = `sources[${index}]`;
if (!isObject(source)) {
errors.push(`${prefix} must be an object`);
return;
}
for (const field of ["id", "title", "edition", "license"]) {
validateText(source[field], `${prefix}.${field}`, errors);
}
if (sourceIds.has(source.id)) errors.push(`${prefix}.id must be unique`);
if (nonBlank(source.id)) sourceIds.add(source.id);
try {
const url = new URL(source.url);
if (!["http:", "https:"].includes(url.protocol)) throw new Error("unsupported protocol");
} catch {
errors.push(`${prefix}.url must be an absolute HTTP(S) URL`);
}
});
}
if (!Array.isArray(contentPackage.hexagrams) || contentPackage.hexagrams.length !== 64) {
errors.push("hexagrams must contain exactly 64 entries");
return errors;
}
const seenIds = new Set();
const seenPatterns = new Set();
contentPackage.hexagrams.forEach((hexagram, index) => {
const prefix = `hexagrams[${index}]`;
if (!isObject(hexagram)) {
errors.push(`${prefix} must be an object`);
return;
}
if (!Number.isInteger(hexagram.kingWenNumber) || hexagram.kingWenNumber < 1 || hexagram.kingWenNumber > 64) {
errors.push(`${prefix}.kingWenNumber must be an integer from 1 through 64`);
} else if (seenIds.has(hexagram.kingWenNumber)) {
errors.push(`${prefix}.kingWenNumber must be unique`);
} else {
seenIds.add(hexagram.kingWenNumber);
}
for (const field of ["name", "symbol", "judgmentOriginal", "judgmentPlain"]) {
validateText(hexagram[field], `${prefix}.${field}`, errors);
}
validateSixTexts(hexagram.lineTextsBottomUp, `${prefix}.lineTextsBottomUp`, errors);
validateSixTexts(hexagram.linePlainBottomUp, `${prefix}.linePlainBottomUp`, errors);
const lowerPattern = trigramPatterns[hexagram.lowerTrigram];
const upperPattern = trigramPatterns[hexagram.upperTrigram];
if (!lowerPattern) errors.push(`${prefix}.lowerTrigram is unknown`);
if (!upperPattern) errors.push(`${prefix}.upperTrigram is unknown`);
if (!Array.isArray(hexagram.patternBottomUp) || hexagram.patternBottomUp.length !== 6 ||
hexagram.patternBottomUp.some((line) => line !== "YIN" && line !== "YANG")) {
errors.push(`${prefix}.patternBottomUp must contain exactly six YIN/YANG values`);
} else {
const encoded = hexagram.patternBottomUp.join("/");
if (seenPatterns.has(encoded)) errors.push(`${prefix}.patternBottomUp must be unique`);
seenPatterns.add(encoded);
if (lowerPattern && upperPattern) {
const expected = [...lowerPattern, ...upperPattern];
if (encoded !== expected.join("/")) {
errors.push(`${prefix}.patternBottomUp does not match lower/upper trigrams in bottom-up order`);
}
}
}
if (lowerPattern && upperPattern) {
const expectedId = kingWenByPair.get(`${hexagram.upperTrigram}/${hexagram.lowerTrigram}`);
if (hexagram.kingWenNumber !== expectedId) {
errors.push(`${prefix}.kingWenNumber does not match its King Wen trigram pair`);
}
}
if (!Array.isArray(hexagram.sourceRefs) || hexagram.sourceRefs.length === 0) {
errors.push(`${prefix}.sourceRefs must not be empty`);
} else {
for (const sourceRef of hexagram.sourceRefs) {
if (!sourceIds.has(sourceRef)) errors.push(`${prefix}.sourceRefs contains unknown source '${sourceRef}'`);
}
if (new Set(hexagram.sourceRefs).size !== hexagram.sourceRefs.length) {
errors.push(`${prefix}.sourceRefs must be unique`);
}
}
const hasSpecialText = nonBlank(hexagram.specialUsageText);
if (hexagram.specialUsageText !== null && !hasSpecialText) {
errors.push(`${prefix}.specialUsageText must be non-blank text or null`);
}
if (hasSpecialText) validateText(hexagram.specialUsageText, `${prefix}.specialUsageText`, errors);
if (hexagram.kingWenNumber === 1 && isObject(usage) && hasSpecialText !== usage.qian) {
errors.push(`${prefix}.specialUsageText must match specialUsageTexts.qian`);
} else if (hexagram.kingWenNumber === 2 && isObject(usage) && hasSpecialText !== usage.kun) {
errors.push(`${prefix}.specialUsageText must match specialUsageTexts.kun`);
} else if (![1, 2].includes(hexagram.kingWenNumber) && hexagram.specialUsageText !== null) {
errors.push(`${prefix}.specialUsageText is only valid for Qian or Kun`);
}
});
if (seenIds.size !== 64) errors.push("King Wen numbers 1 through 64 must each occur exactly once");
if (seenPatterns.size !== 64) errors.push("all 64 polarity patterns must each occur exactly once");
return errors;
}
+41
View File
@@ -0,0 +1,41 @@
import { readdir, readFile, stat } from "node:fs/promises";
import path from "node:path";
export const repositoryRoot = path.resolve(import.meta.dirname, "..");
const excludedDirectories = new Set([
".git",
".gradle",
".idea",
"build",
"node_modules",
]);
export async function walkFiles(directory = repositoryRoot) {
const result = [];
const entries = await readdir(directory, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && excludedDirectories.has(entry.name)) continue;
const absolutePath = path.join(directory, entry.name);
if (entry.isDirectory()) {
result.push(...(await walkFiles(absolutePath)));
} else if (entry.isFile()) {
result.push(absolutePath);
}
}
return result;
}
export async function readTextFileIfSmall(file, maximumBytes = 2_000_000) {
const metadata = await stat(file);
if (metadata.size > maximumBytes) return null;
const buffer = await readFile(file);
if (buffer.includes(0)) return null;
return buffer.toString("utf8");
}
export function relative(file) {
return path.relative(repositoryRoot, file).replaceAll(path.sep, "/");
}
+47
View File
@@ -0,0 +1,47 @@
import path from "node:path";
import {
readTextFileIfSmall,
relative,
walkFiles,
} from "./repository-files.mjs";
const patterns = [
["private key block", /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/u],
["GitHub token", /\bgh[pousr]_[A-Za-z0-9]{30,}\b/u],
["OpenAI-style key", /\bsk-[A-Za-z0-9_-]{20,}\b/u],
["Google API key", /\bAIza[0-9A-Za-z_-]{35}\b/u],
["AWS access key", /\bAKIA[0-9A-Z]{16}\b/u],
];
const excludedExtensions = new Set([
".gif",
".ico",
".jpeg",
".jpg",
".pdf",
".png",
".webp",
".zip",
]);
const findings = [];
for (const file of await walkFiles()) {
if (excludedExtensions.has(path.extname(file).toLowerCase())) continue;
const content = await readTextFileIfSmall(file);
if (content === null) continue;
for (const [label, pattern] of patterns) {
const match = pattern.exec(content);
if (!match) continue;
const line = content.slice(0, match.index).split(/\r?\n/u).length;
findings.push(`${relative(file)}:${line} (${label})`);
}
}
if (findings.length > 0) {
console.error("Potential secrets found; values are intentionally redacted:");
for (const finding of findings) console.error(`- ${finding}`);
process.exitCode = 1;
} else {
console.log("High-confidence secret scan passed.");
}
+104
View File
@@ -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)}…).`,
);
+67
View File
@@ -0,0 +1,67 @@
import { access } from "node:fs/promises";
import path from "node:path";
import {
readTextFileIfSmall,
relative,
repositoryRoot,
walkFiles,
} from "./repository-files.mjs";
const requiredDocuments = [
"AGENTS.md",
"docs/README.md",
"docs/agent-playbook.md",
"docs/implementation-plan.md",
"docs/quality-gates.md",
"docs/decisions.md",
];
const failures = [];
for (const required of requiredDocuments) {
try {
await access(path.join(repositoryRoot, required));
} catch {
failures.push(`missing required document: ${required}`);
}
}
const markdownFiles = (await walkFiles()).filter((file) => file.endsWith(".md"));
const markdownLink = /!?\[[^\]]*\]\(([^)]+)\)/g;
for (const markdownFile of markdownFiles) {
const content = await readTextFileIfSmall(markdownFile);
if (content === null) continue;
for (const match of content.matchAll(markdownLink)) {
let target = match[1].trim();
if (target.startsWith("<") && target.endsWith(">")) {
target = target.slice(1, -1);
}
target = target.split(/\s+["']/u, 1)[0];
if (
target === "" ||
target.startsWith("#") ||
/^[a-z][a-z\d+.-]*:/iu.test(target)
) {
continue;
}
const pathPart = target.split("#", 1)[0].split("?", 1)[0];
const decodedPath = decodeURIComponent(pathPart);
const resolved = path.resolve(path.dirname(markdownFile), decodedPath);
try {
await access(resolved);
} catch {
failures.push(`${relative(markdownFile)} -> ${target}`);
}
}
}
if (failures.length > 0) {
console.error("Documentation verification failed:");
for (const failure of failures) console.error(`- ${failure}`);
process.exitCode = 1;
} else {
console.log(`Documentation verification passed (${markdownFiles.length} Markdown files).`);
}
+47
View File
@@ -0,0 +1,47 @@
import path from "node:path";
import {
readTextFileIfSmall,
relative,
repositoryRoot,
walkFiles,
} from "./repository-files.mjs";
const domainRoot = path.join(repositoryRoot, "app", "src", "main", "java", "brainwave", "domain");
const forbiddenImports = [
/^android\./u,
/^androidx\./u,
/^com\.google\.dagger\./u,
/^dagger\./u,
/^okhttp3\./u,
/^retrofit2\./u,
/^io\.ktor\./u,
/^androidx\.room\./u,
];
const failures = [];
let kotlinFiles = [];
try {
kotlinFiles = (await walkFiles(domainRoot)).filter((file) => file.endsWith(".kt"));
} catch {
failures.push(`domain source directory is missing: ${relative(domainRoot)}`);
}
for (const file of kotlinFiles) {
const content = await readTextFileIfSmall(file);
if (content === null) continue;
for (const [index, line] of content.split(/\r?\n/u).entries()) {
const match = /^\s*import\s+([^\s]+)/u.exec(line);
if (!match) continue;
if (forbiddenImports.some((pattern) => pattern.test(match[1]))) {
failures.push(`${relative(file)}:${index + 1} forbidden import ${match[1]}`);
}
}
}
if (failures.length > 0) {
console.error("Domain boundary verification failed:");
for (const failure of failures) console.error(`- ${failure}`);
process.exitCode = 1;
} else {
console.log(`Domain boundary verification passed (${kotlinFiles.length} Kotlin files).`);
}
+54
View File
@@ -0,0 +1,54 @@
import path from "node:path";
import {
readTextFileIfSmall,
relative,
walkFiles,
} from "./repository-files.mjs";
const checkedExtensions = new Set([
".css",
".html",
".js",
".json",
".kt",
".kts",
".md",
".mjs",
".properties",
".toml",
".xml",
".yaml",
".yml",
]);
const failures = [];
let checked = 0;
for (const file of await walkFiles()) {
if (!checkedExtensions.has(path.extname(file).toLowerCase())) continue;
const content = await readTextFileIfSmall(file);
if (content === null) continue;
checked += 1;
if (content.length > 0 && !content.endsWith("\n")) {
failures.push(`${relative(file)} must end with a newline`);
}
content.split(/\r?\n/u).forEach((line, index) => {
if (/[ \t]+$/u.test(line)) failures.push(`${relative(file)}:${index + 1} has trailing whitespace`);
});
if (file.endsWith(".json")) {
try {
JSON.parse(content);
} catch (error) {
failures.push(`${relative(file)} is invalid JSON: ${error.message}`);
}
}
}
if (failures.length > 0) {
console.error("Formatting verification failed:");
for (const failure of failures) console.error(`- ${failure}`);
process.exitCode = 1;
} else {
console.log(`Formatting verification passed (${checked} text files).`);
}
+23
View File
@@ -0,0 +1,23 @@
import { spawnSync } from "node:child_process";
import path from "node:path";
import { repositoryRoot } from "./repository-files.mjs";
const scripts = ["prototype/app.js", "prototype/capture.mjs"];
const failures = [];
for (const script of scripts) {
const result = spawnSync(process.execPath, ["--check", path.join(repositoryRoot, script)], {
encoding: "utf8",
});
if (result.status !== 0) {
failures.push(`${script}: ${(result.stderr || result.stdout).trim()}`);
}
}
if (failures.length > 0) {
console.error("Prototype syntax verification failed:");
for (const failure of failures) console.error(`- ${failure}`);
process.exitCode = 1;
} else {
console.log(`Prototype syntax verification passed (${scripts.length} files).`);
}