feat: add domain core and verification harness
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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, "/");
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
@@ -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)}…).`,
|
||||
);
|
||||
@@ -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).`);
|
||||
}
|
||||
@@ -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).`);
|
||||
}
|
||||
@@ -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).`);
|
||||
}
|
||||
@@ -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).`);
|
||||
}
|
||||
Reference in New Issue
Block a user