/* Collision guards for the Uni UI Ontology cosmic catalogs (DNA-19, DNA-23): RFC-1300 PlanetCatalog extension invariants plus RFC-1308 declared-exception enforcement — every exact or normalized collision, cross-catalog or same-catalog, must be recorded in KNOWN_*_CATALOG_COLLISIONS, and stale declarations whose collision was resolved fail closed. Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only. RFC-1308: consume KNOWN_*_CATALOG_COLLISIONS instead of a private whitelist; add the undeclared same-catalog normalized-collision check and declared-exception liveness check; header scaffolded (file previously carried no Compass contract). */ import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, join } from "node:path"; import { test, expect, describe } from "vitest"; import YAML from "yaml"; import { PlanetCatalog } from "./planet-catalog.ts"; import { MoonCatalog } from "./moon-catalog.ts"; import { StarCatalog } from "./star-catalog.ts"; import { KNOWN_CROSS_CATALOG_COLLISIONS, KNOWN_SAME_CATALOG_COLLISIONS, } from "./known-collisions.ts"; // RFC-1300 rule 3, ported verbatim: lowercase; k→c, j→i, ae→e, ph→f, y→i; // doubled letters collapsed. const normalize = (s: string): string => s .toLowerCase() .replace(/k/g, "c") .replace(/j/g, "i") .replace(/ae/g, "e") .replace(/ph/g, "f") .replace(/y/g, "i") .replace(/(.)\1+/g, "$1"); const ORIGINAL_COUNT = 39; const here = dirname(fileURLToPath(import.meta.url)); const sources = YAML.parse(readFileSync(join(here, "planet-catalog.sources.yaml"), "utf8")) as { names: Record; }; const CATALOGS = { StarCatalog, PlanetCatalog, MoonCatalog, } as const; describe("RFC-1300 PlanetCatalog extension", () => { test("AC-1: catalog holds at least 239 names", () => { expect(PlanetCatalog.length).toBeGreaterThanOrEqual(239); }); test("AC-2: no duplicate names within PlanetCatalog", () => { expect(new Set(PlanetCatalog).size).toBe(PlanetCatalog.length); }); test("AC-3: no exact or normalized collision with Star/Moon catalogs", () => { // RFC-1308: declared cross-catalog exceptions live in // KNOWN_CROSS_CATALOG_COLLISIONS — the single reviewable surface; // the list ships empty (Atlas resolved via the StarCatalog rename). const declared = new Map( KNOWN_CROSS_CATALOG_COLLISIONS.flatMap((c) => c.names.map((n) => [n, c] as const)), ); const others = new Map(); for (const n of [...StarCatalog, ...MoonCatalog]) { others.set(normalize(n), n); } for (const name of PlanetCatalog) { const hit = others.get(normalize(name)); const exception = declared.get(name); if (exception) { expect( exception.names, `declared "${name}" no longer collides — remove the stale entry`, ).toContain(hit); continue; } expect(hit, `"${name}" collides with "${hit}"`).toBeUndefined(); } // Rule 3 also forbids collisions inside PlanetCatalog: added names vs the // original entries (same-catalog duplicates are covered by AC-2). const originalNorm = new Map(); for (const n of PlanetCatalog.slice(0, ORIGINAL_COUNT)) { originalNorm.set(normalize(n), n); } for (const name of PlanetCatalog.slice(ORIGINAL_COUNT)) { const hit = originalNorm.get(normalize(name)); expect(hit, `added "${name}" collides with original "${hit}"`).toBeUndefined(); } }); test("RFC-1308: no undeclared same-catalog normalized collision", () => { // Declared exceptions: KNOWN_SAME_CATALOG_COLLISIONS (e.g. the // pre-existing Alfecca/Alphecca pair in StarCatalog, recorded not // renamed). Any NEW same-catalog normalized dup fails here. const declared = new Map( KNOWN_SAME_CATALOG_COLLISIONS.map((c) => [`${c.catalog}::${c.normalized}`, c.names]), ); for (const [catalogName, entries] of Object.entries(CATALOGS)) { const seen = new Map(); for (const name of entries) { const key = normalize(name); const prior = seen.get(key); if (prior === undefined) { seen.set(key, name); continue; } const exception = declared.get(`${catalogName}::${key}`); if (exception) { expect( exception, `declared ${catalogName} exception "${key}" lists different names`, ).toEqual(expect.arrayContaining([prior, name])); continue; } expect( prior, `undeclared normalized collision in ${catalogName}: "${prior}" and "${name}" both normalize to "${key}" — record it in KNOWN_SAME_CATALOG_COLLISIONS with an RFC`, ).toBeUndefined(); } } }); test("RFC-1308: declared collisions stay live — resolved entries must be removed", () => { // A declaration whose collision no longer exists is dead weight: the // module's non-goals forbid recording resolved collisions, so each entry // must still describe a real collision in the current catalogs. for (const c of KNOWN_CROSS_CATALOG_COLLISIONS) { const holders = c.catalogs.filter((cat) => c.names.some((n) => (CATALOGS[cat] as readonly string[]).includes(n)), ); expect( holders.length, `declared cross-catalog collision "${c.names.join("/")}" no longer spans ${c.catalogs.join(" and ")} — remove the resolved entry`, ).toBeGreaterThanOrEqual(2); expect( new Set(c.names.map(normalize)).size, `declared cross-catalog entry "${c.names.join("/")}" lists spellings that do not normalize together`, ).toBe(1); } for (const c of KNOWN_SAME_CATALOG_COLLISIONS) { const catalog = CATALOGS[c.catalog] as readonly string[]; const present = c.names.filter((n) => catalog.includes(n)); expect( present.length, `declared ${c.catalog} collision "${c.names.join("/")}" no longer holds — remove the resolved entry`, ).toBeGreaterThanOrEqual(2); expect( present.every((n) => normalize(n) === c.normalized), `declared ${c.catalog} entry expects normalized "${c.normalized}" — got ${present.map(normalize).join(", ")}`, ).toBe(true); } }); test("AC-4: every added name has a sources entry; every sources entry is in the catalog", () => { const catalog = new Set(PlanetCatalog); const added = PlanetCatalog.slice(ORIGINAL_COUNT); for (const name of added) { expect(sources.names[name], `missing sources entry for ${name}`).toBeDefined(); } for (const name of Object.keys(sources.names)) { expect(catalog.has(name), `sources entry ${name} not in catalog`).toBe(true); } }); });