#!/usr/bin/env bun import fs from "node:fs" import path from "node:path" const ROOT = path.resolve(import.meta.dir, "..") const MANIFEST_PATH = path.join(ROOT, "core", "CORE-MANIFEST.json") const CORE_ROOT = path.join(ROOT, "core") export const HARD_PATTERNS: RegExp[] = [ /\bins24\b/, /dev\.eightarms\.net/, /10\.\d+\.\d+\.\d+/, /192\.168\.\d+\.\d+/, /172\.(1[6-9]|2\d|3[01])\.\d+\.\d+/, /~\/\.config\/octopus/, ] export const SOFT_PATTERNS: RegExp[] = [ /\.octopus\//, /examples\//, /\/data\/git/, /\bins24\b/, /dev\.eightarms\.net/, /gitea_wiki__/, /gitea_issue_comment__/, /gitea_column__/, /gitea_project__/, /packages\/octopus/, /~\/\.config\/octopus/, ] export const INSTANCE_PATTERNS: RegExp[] = SOFT_PATTERNS export function stripCodeSpans(line: string): string { return line.replace(/`[^`]*`/g, "") } export function stripFencedBlocks(lines: string[]): string[] { let inFence = false return lines.filter((line) => { if (line.trimStart().startsWith("```")) { inFence = !inFence return false } return !inFence }) } function scanFile(relFile: string, patterns: RegExp[]): string[] { const file = path.join(ROOT, relFile) const lines = stripFencedBlocks(fs.readFileSync(file, "utf8").split("\n")) const violations: string[] = [] lines.forEach((line) => { const effective = stripCodeSpans(line) for (const pattern of patterns) { if (pattern.test(effective)) { violations.push(`${relFile}: ${pattern.source}`) } } }) return violations } export function findInstanceViolations(relFile: string): string[] { return scanFile(relFile, INSTANCE_PATTERNS) } export function findHardViolations(relFile: string): string[] { return scanFile(relFile, HARD_PATTERNS) } export function scanContent(relFile: string, content: string, patterns: RegExp[]): string[] { const lines = stripFencedBlocks(content.split("\n")) const violations: string[] = [] lines.forEach((line) => { const effective = stripCodeSpans(line) for (const pattern of patterns) { if (pattern.test(effective)) { violations.push(`${relFile}: ${pattern.source}`) } } }) return violations } function findPatternsFor(relFile: string): RegExp[] { if (relFile.startsWith("core/adapters/")) return HARD_PATTERNS if (relFile === "core/README.md" || relFile === "core/MIGRATION.md") return [...HARD_PATTERNS, ...SOFT_PATTERNS.filter((p) => p.source !== "examples\\/")] return [...HARD_PATTERNS, ...SOFT_PATTERNS] } interface Mapping { core: string dogfood: string | null sync?: string deferHard?: boolean } function fail(message: string): never { console.error(`check-core-cohesion: ${message}`) process.exit(1) } function listMarkdownFiles(dir: string, base = ""): string[] { const out: string[] = [] for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const rel = base ? `${base}/${entry.name}` : entry.name if (entry.isDirectory()) out.push(...listMarkdownFiles(path.join(dir, entry.name), rel)) else if (entry.name.endsWith(".md") || entry.name.endsWith(".yaml")) out.push(rel) } return out } if (import.meta.main) { const STRICT = process.argv.includes("--strict") if (!fs.existsSync(MANIFEST_PATH)) fail(`manifest not found: ${MANIFEST_PATH}`) let mappings: Mapping[] try { mappings = (JSON.parse(fs.readFileSync(MANIFEST_PATH, "utf8")) as { mappings: Mapping[] }).mappings } catch (e) { fail(`manifest is not valid JSON: ${(e as Error).message}`) } if (!Array.isArray(mappings) || mappings.length === 0) fail("manifest has no mappings") const missing: string[] = [] for (const m of mappings) { if (!fs.existsSync(path.join(ROOT, m.core))) missing.push(m.core) } if (missing.length > 0) { console.error("check-core-cohesion: manifest core path(s) missing:") for (const p of missing) console.error(` ${p}`) process.exit(1) } const violations: string[] = [] const exempt = new Set() for (const m of mappings) { if (m.sync !== "verbatim" && m.sync !== "verbatimDir") continue if (m.sync === "verbatimDir") { const dir = m.core.endsWith("/") ? m.core : `${m.core}/` for (const f of listDir(dir)) exempt.add(`${dir}${f}`) } else { exempt.add(m.core) } } const mdFiles = listMarkdownFiles(CORE_ROOT).map((f) => `core/${f}`) const scanned = mdFiles.filter((rel) => !exempt.has(rel) && !exempt.has(rel.replace(/^core\//, ""))) for (const rel of scanned) { violations.push(...scanFile(rel, findPatternsFor(rel))) } if (STRICT) { let deferred = 0 let strictScanned = 0 for (const m of mappings) { if (m.sync !== "verbatim") continue if (m.deferHard) { deferred++ continue } strictScanned++ violations.push(...scanFile(m.core, INSTANCE_PATTERNS)) } console.log(`check-core-cohesion: strict verbatim scan: ${strictScanned} scanned, ${deferred} deferred (deferHard)`) } if (violations.length > 0) { console.error("check-core-cohesion: C-1 violation(s):") for (const v of violations) console.error(` ${v}`) process.exit(1) } console.log( `check-core-cohesion: ${mappings.length} mappings present, ${scanned.length}/${mdFiles.length} core markdown files scanned, 0 violations`, ) } export function listCorpus(relDir: string, base = ""): string[] { const out: string[] = [] for (const entry of fs.readdirSync(path.join(ROOT, relDir), { withFileTypes: true })) { const rel = base ? `${base}/${entry.name}` : entry.name if (entry.isDirectory()) out.push(...listCorpus(path.join(relDir, entry.name), rel)) else if (entry.name.endsWith(".md") || entry.name.endsWith(".yaml")) out.push(rel) } return out.sort() } export function listDir(relDir: string, base = ""): string[] { const out: string[] = [] for (const entry of fs.readdirSync(path.join(ROOT, relDir), { withFileTypes: true })) { const rel = base ? `${base}/${entry.name}` : entry.name if (entry.isDirectory()) out.push(...listDir(path.join(relDir, entry.name), rel)) else if (entry.name !== ".gitkeep") out.push(rel) } return out.sort() } export function collectVerbatimDirFiles(m: Mapping): { side: string; files: string[] } | null { if (m.sync !== "verbatimDir") return null const rel = m.core.endsWith("/") ? m.core : `${m.core}/` return { side: rel, files: listDir(rel) } }