import { catalogs, classes, inquiryDomains, slug } from './catalog.mjs';

/**
 * The systems layer is deliberately data-first. Everything returned by
 * buildSystems() is JSON-serializable; executable helpers are exported
 * separately for build-time validation and deterministic simulations.
 */

export const edgeTypes = [
  { id: 'same-class-as', family: 'taxonomic', directionality: 'symmetric', label: 'same class as', meaning: 'Both cards occupy the same canonical class while retaining distinct functions.' },
  { id: 'shares-inquiry-domain-with', family: 'interpretive', directionality: 'symmetric', label: 'shares an inquiry domain with', meaning: 'Both cards illuminate the same inquiry domain from different positions.' },
  { id: 'contrasts-with', family: 'dialectical', directionality: 'symmetric', label: 'contrasts with', meaning: 'The pair clarifies a consequential opposition without forcing a binary choice.' },
  { id: 'assessed-through-shared-source-lens', family: 'evidentiary', directionality: 'symmetric', label: 'assessed through a shared source lens with', meaning: 'Both cards cite the named source, allowing editors to compare how the same documented lens bounds or contextualizes each card.' },
  { id: 'inquiry-juxtaposition', family: 'exploratory', directionality: 'symmetric', label: 'may be juxtaposed in inquiry with', meaning: 'The cards share explicit catalog features that make comparison useful; no causal, historical, or oppositional relationship is asserted.' },
  { id: 'echoes', family: 'symbolic', directionality: 'symmetric', label: 'echoes', meaning: 'The pair repeats a symbolic structure across different contexts.' },
  { id: 'variant-of', family: 'taxonomic', directionality: 'directed', label: 'is a variant of', meaning: 'The source card is a recognizable variant of the target card.' },
  { id: 'subtype-of', family: 'taxonomic', directionality: 'directed', label: 'is a subtype of', meaning: 'The source card is a narrower member of the target category.' },
  { id: 'modeled-by', family: 'interpretive', directionality: 'directed', label: 'is modeled by', meaning: 'The target lens supplies one explicit model for interpreting the source.' },
  { id: 'interpreted-through', family: 'interpretive', directionality: 'directed', label: 'is interpreted through', meaning: 'The target provides an interpretive frame without claiming final explanation.' },
  { id: 'contextualizes', family: 'evidentiary', directionality: 'directed', label: 'contextualizes', meaning: 'The source supplies historical, scientific, or cultural context for the target.' },
  { id: 'investigated-by', family: 'historical', directionality: 'directed', label: 'was investigated by', meaning: 'The target program or institution investigated the source practice or claim.' },
  { id: 'later-mythologized-as', family: 'historical', directionality: 'directed', label: 'was later mythologized as', meaning: 'Documented history became source material for a later, broader mythology.' },
  { id: 'extends-lore-of', family: 'historical', directionality: 'directed', label: 'extends the lore of', meaning: 'The source narrative develops or inherits an earlier lore complex.' },
  { id: 'located-at-in-lore', family: 'spatial', directionality: 'directed', label: 'is located at in lore', meaning: 'The source program or event is narratively situated at the target place.' },
  { id: 'provides-setting-for', family: 'spatial', directionality: 'directed', label: 'provides a setting for', meaning: 'The source world or place provides the operational environment for the target.' },
  { id: 'enables', family: 'causal', directionality: 'directed', label: 'enables', meaning: 'The source increases the possibility or effectiveness of the target.' },
  { id: 'constrains', family: 'causal', directionality: 'directed', label: 'constrains', meaning: 'The source narrows the target’s available choices or outcomes.' },
  { id: 'catalyzes', family: 'causal', directionality: 'directed', label: 'catalyzes', meaning: 'The source accelerates a threshold change in the target.' },
  { id: 'precipitates', family: 'causal', directionality: 'directed', label: 'precipitates', meaning: 'The source event or condition immediately pushes the target into being.' },
  { id: 'conditions', family: 'systemic', directionality: 'directed', label: 'conditions', meaning: 'The source system shapes the range and likelihood of target states.' },
  { id: 'drives', family: 'systemic', directionality: 'directed', label: 'drives', meaning: 'The source force supplies persistent pressure within the target system.' },
  { id: 'operates', family: 'institutional', directionality: 'directed', label: 'operates', meaning: 'The source faction directs or sustains the target program.' },
  { id: 'governs', family: 'institutional', directionality: 'directed', label: 'governs', meaning: 'The source institution sets durable rules for the target.' },
  { id: 'participates-in', family: 'institutional', directionality: 'directed', label: 'participates in', meaning: 'The source actor or faction takes a consequential role in the target.' },
  { id: 'opposed-by', family: 'dialectical', directionality: 'symmetric', label: 'is opposed by', meaning: 'The pair organizes an active conflict of aims, values, or power.' },
  { id: 'politicized-through', family: 'epistemic', directionality: 'directed', label: 'is politicized through', meaning: 'The target lens turns the source into a contested political symbol.' },
  { id: 'fictionalizes', family: 'narrative', directionality: 'directed', label: 'fictionalizes', meaning: 'The source storyworld renders the target concept as narrative experience.' },
  { id: 'mythologizes', family: 'narrative', directionality: 'directed', label: 'mythologizes', meaning: 'The source reframes the target through a durable story structure.' },
  { id: 'embodied-by', family: 'symbolic', directionality: 'directed', label: 'is embodied by', meaning: 'The target gives the source concept an agent, object, or world-scale body.' },
  { id: 'reported-motif-of', family: 'experiential', directionality: 'directed', label: 'is a reported motif of', meaning: 'The source experience is recurrently reported within the target experience family.' },
  { id: 'origin-identity-with', family: 'experiential', directionality: 'directed', label: 'forms an origin identity with', meaning: 'The target supplies a named origin within the source identity framework.' },
  { id: 'develops-into', family: 'lineage', directionality: 'directed', label: 'develops into', meaning: 'The target is the next intelligible threshold in a named editorial lineage.' },
  { id: 'possible-path-to', family: 'lineage', directionality: 'directed', label: 'is a possible path to', meaning: 'The source may lead toward the target but does not make it inevitable.' },
  { id: 'service-narrative-within', family: 'narrative', directionality: 'directed', label: 'is a service narrative within', meaning: 'The source is a specific service-story nested inside the target lore complex.' },
  { id: 'extractive-model-within', family: 'systemic', directionality: 'directed', label: 'is an extractive model within', meaning: 'The source describes a particular extraction mechanism inside the target cosmology.' },
  { id: 'inspired-by', family: 'historical', directionality: 'directed', label: 'is inspired by', meaning: 'The source inherits a documented motif, problem, or structure from the target.' },
  { id: 'resembles', family: 'comparative', directionality: 'symmetric', label: 'resembles', meaning: 'The pair shares a bounded formal resemblance without asserting common origin.' },
  { id: 'contradicts', family: 'dialectical', directionality: 'symmetric', label: 'contradicts', meaning: 'Both claims cannot stand in the same form under the stated evidence boundary.' },
  { id: 'descends-from', family: 'historical', directionality: 'directed', label: 'descends from', meaning: 'The source is historically or narratively derived from the target lineage.' },
  { id: 'historically-precedes', family: 'historical', directionality: 'directed', label: 'historically precedes', meaning: 'The documented source appears earlier than the target and provides relevant context.' },
  { id: 'spiritualizes', family: 'interpretive', directionality: 'directed', label: 'spiritualizes', meaning: 'The source reframes the target as spiritual identity, practice, or cosmology.' },
  { id: 'technologizes', family: 'interpretive', directionality: 'directed', label: 'technologizes', meaning: 'The source reframes a symbolic or experiential pattern as engineered mechanism.' },
  { id: 'debunks', family: 'epistemic', directionality: 'directed', label: 'debunks', meaning: 'The source directly challenges a target claim with a specified evidentiary argument.' },
  { id: 'reframes', family: 'interpretive', directionality: 'directed', label: 'reframes', meaning: 'The source changes which question or scale makes the target intelligible.' },
  { id: 'amplifies', family: 'causal', directionality: 'directed', label: 'amplifies', meaning: 'The source increases the intensity, reach, or consequence of the target.' },
  { id: 'controls', family: 'systemic', directionality: 'directed', label: 'controls', meaning: 'The source sets or enforces the target’s available states.' },
  { id: 'liberates', family: 'systemic', directionality: 'directed', label: 'liberates', meaning: 'The source restores meaningful agency from the target constraint.' },
  { id: 'contains', family: 'structural', directionality: 'directed', label: 'contains', meaning: 'The target is a distinct nested member or process within the source.' },
  { id: 'produces', family: 'causal', directionality: 'directed', label: 'produces', meaning: 'The source reliably generates the target within the stated model.' },
  { id: 'harvests', family: 'systemic', directionality: 'directed', label: 'harvests', meaning: 'The source extracts the target as a recurring resource or value.' },
  { id: 'transforms', family: 'causal', directionality: 'directed', label: 'transforms', meaning: 'The source changes the target’s identity, structure, or capacity.' },
  { id: 'reveals', family: 'epistemic', directionality: 'directed', label: 'reveals', meaning: 'The source makes a previously hidden target legible.' },
  { id: 'conceals', family: 'epistemic', directionality: 'directed', label: 'conceals', meaning: 'The source reduces access to or legibility of the target.' },
  { id: 'predicts', family: 'epistemic', directionality: 'directed', label: 'predicts', meaning: 'The source model makes a falsifiable forecast about the target.' },
  { id: 'remembers', family: 'experiential', directionality: 'directed', label: 'remembers', meaning: 'The source carries a durable trace or account of the target.' },
  { id: 'simulates', family: 'technological', directionality: 'directed', label: 'simulates', meaning: 'The source models the target’s behavior inside a bounded representation.' },
  { id: 'inherits', family: 'historical', directionality: 'directed', label: 'inherits', meaning: 'The source receives a structure, obligation, memory, or capability from the target.' }
];

const edgeTypeById = new Map(edgeTypes.map((type) => [type.id, type]));

export const effectOperations = [
  { id: 'draw', parameters: ['count', 'from', 'to'], meaning: 'Draw one or more cards from a filtered deck.' },
  { id: 'reveal', parameters: ['target', 'count'], meaning: 'Turn hidden information face-up.' },
  { id: 'place', parameters: ['card', 'zone'], meaning: 'Place a card into a named play zone.' },
  { id: 'connect', parameters: ['from', 'to', 'edgeType'], meaning: 'Create or claim a typed connection between cards.' },
  { id: 'cite-source', parameters: ['card', 'claim'], meaning: 'Support a play with a registered source or evidence boundary.' },
  { id: 'challenge-claim', parameters: ['card', 'stake'], meaning: 'Challenge a claim while staking a named resource.' },
  { id: 'gain', parameters: ['resource', 'amount'], meaning: 'Increase a named resource track.' },
  { id: 'spend', parameters: ['resource', 'amount'], meaning: 'Pay a named resource cost.' },
  { id: 'advance-track', parameters: ['track', 'amount'], meaning: 'Advance a shared or personal progress track.' },
  { id: 'retreat-track', parameters: ['track', 'amount'], meaning: 'Move a shared or personal track backward.' },
  { id: 'tag', parameters: ['target', 'tag'], meaning: 'Apply a durable state tag.' },
  { id: 'branch', parameters: ['condition', 'then', 'otherwise'], meaning: 'Resolve one of two declarative effect lists.' },
  { id: 'vote', parameters: ['question', 'threshold'], meaning: 'Resolve a collective choice.' },
  { id: 'swap', parameters: ['first', 'second'], meaning: 'Exchange two cards, roles, or positions.' },
  { id: 'transform', parameters: ['target', 'into'], meaning: 'Move a card or role into a new declared state.' },
  { id: 'lock', parameters: ['target'], meaning: 'Make a card, route, or action unavailable.' },
  { id: 'unlock', parameters: ['target'], meaning: 'Make a card, route, or action available.' }
];

const effectOperationIds = new Set(effectOperations.map((operation) => operation.id));

export const effectOperationSchemas = {
  draw: { required: ['count', 'from', 'to'], numeric: ['count'], stateReferences: ['to'] },
  reveal: { required: ['target', 'count'], numeric: ['count'], stateReferences: ['target'] },
  place: { required: ['card', 'zone'], stateReferences: ['card', 'zone'] },
  connect: { required: ['from', 'to', 'edgeType'], stateReferences: ['from', 'to'], edgeTypeReferences: ['edgeType'] },
  'cite-source': { required: ['card', 'claim'], stateReferences: ['card'], symbolicReferences: ['claim'] },
  'challenge-claim': { required: ['card', 'stake'], stateReferences: ['card'], resourceReferences: ['stake'] },
  gain: { required: ['resource', 'amount'], numeric: ['amount'], resourceReferences: ['resource'] },
  spend: { required: ['resource', 'amount'], numeric: ['amount'], resourceReferences: ['resource'] },
  'advance-track': { required: ['track', 'amount'], numeric: ['amount'], trackReferences: ['track'] },
  'retreat-track': { required: ['track', 'amount'], numeric: ['amount'], trackReferences: ['track'] },
  tag: { required: ['target', 'tag'], stateReferences: ['target'], symbolicReferences: ['tag'] },
  branch: { required: ['condition', 'then', 'otherwise'], symbolicReferences: ['condition'], effectLists: ['then', 'otherwise'] },
  vote: { required: ['question', 'threshold'], symbolicReferences: ['question', 'threshold'] },
  swap: { required: ['first', 'second'], stateReferences: ['first', 'second'] },
  transform: { required: ['target', 'into'], stateReferences: ['target', 'into'] },
  lock: { required: ['target'], stateReferences: ['target'] },
  unlock: { required: ['target'], stateReferences: ['target'] }
};

const classDomainDefaults = {
  forces: ['reality-ontology', 'power-control', 'liberation-transcendence'],
  cosmologies: ['reality-ontology', 'origin-lineage', 'time-causality'],
  beings: ['intelligence-otherness', 'contact-communication', 'memory-identity'],
  worlds: ['civilization-ecology', 'technology-infrastructure', 'origin-lineage'],
  technologies: ['technology-infrastructure', 'body-evolution', 'mind-consciousness'],
  artifacts: ['memory-identity', 'technology-infrastructure', 'contact-communication'],
  factions: ['power-control', 'civilization-ecology', 'contact-communication'],
  programs: ['power-control', 'technology-infrastructure', 'contact-communication'],
  systems: ['power-control', 'civilization-ecology', 'technology-infrastructure'],
  events: ['time-causality', 'contact-communication', 'liberation-transcendence'],
  states: ['mind-consciousness', 'memory-identity', 'body-evolution'],
  myths: ['origin-lineage', 'memory-identity', 'liberation-transcendence'],
  hypotheses: ['reality-ontology', 'intelligence-otherness', 'contact-communication']
};

function titleOf(card) {
  return card.name ?? card.title ?? card.label ?? card.id;
}

function classIdOf(card) {
  const raw = card.classId ?? card.class?.id ?? card.class ?? card.categoryId ?? card.category;
  if (typeof raw === 'string') {
    const normalized = slug(raw);
    const direct = classes.find((entry) => entry.id === normalized);
    if (direct) return direct.id;
    const byName = classes.find((entry) => slug(entry.name) === normalized);
    if (byName) return byName.id;
  }
  return 'forces';
}

function extractDomainIds(card, classId, index = 0) {
  const raw = card.domainIds ?? card.inquiryDomains ?? card.domains ?? card.primaryDomain ?? card.primaryDomainId;
  const values = Array.isArray(raw) ? raw : raw ? [raw] : [];
  const valid = new Set(inquiryDomains.map((domain) => domain.id));
  const ids = values
    .map((value) => typeof value === 'string' ? slug(value) : value?.id)
    .filter((value) => valid.has(value));
  if (ids.length) return [...new Set(ids)];
  const defaults = classDomainDefaults[classId] ?? inquiryDomains.map((domain) => domain.id);
  return [defaults[index % defaults.length]];
}

function extractSourceIds(card) {
  const raw = [
    ...(Array.isArray(card.sourceIds) ? card.sourceIds : []),
    ...(Array.isArray(card.sources) ? card.sources : []),
    ...(Array.isArray(card.research?.sources) ? card.research.sources : []),
    ...(Array.isArray(card.provenance?.sources) ? card.provenance.sources : [])
  ];
  const ids = raw
    .map((value) => typeof value === 'string' ? value : value?.id ?? value?.sourceId)
    .filter((value) => typeof value === 'string' && value && value !== 'original-synthesis');
  // A default source is never injected here. Sourced graph edges must be
  // traceable to citations that are actually present on both endpoint cards.
  return [...new Set(ids)];
}

function extractStringIds(card, keys, excluded = []) {
  const values = keys.flatMap((key) => Array.isArray(card[key]) ? card[key] : []);
  return [...new Set(values
    .map((value) => typeof value === 'string' ? slug(value) : value?.id)
    .filter((value) => value && !excluded.includes(value)))];
}

function normalizeCards(inputCards) {
  const supplied = Array.isArray(inputCards) && inputCards.length
    ? inputCards
    : classes.flatMap((entry) => (catalogs[entry.id] ?? []).map((name) => ({ name, classId: entry.id })));
  return supplied.map((card, index) => {
    const name = titleOf(card);
    const classId = classIdOf(card);
    return {
      id: card.id ?? slug(name),
      name,
      classId,
      domainIds: extractDomainIds(card, classId, index),
      sourceIds: extractSourceIds(card),
      polarityIds: extractStringIds(card, ['polarities', 'polarityIds']),
      materialModeIds: extractStringIds(card, ['materialModes', 'materialModeIds'], ['original-synthesis']),
      source: card
    };
  }).sort((a, b) => a.id.localeCompare(b.id));
}

function sharedSourceIds(sourceCard, targetCard) {
  const targetSources = new Set(targetCard.sourceIds);
  return sourceCard.sourceIds
    .filter((sourceId) => sourceId !== 'original-synthesis' && targetSources.has(sourceId))
    .sort();
}

function intersection(first = [], second = []) {
  const right = new Set(second);
  return [...new Set(first.filter((value) => right.has(value)))].sort();
}

function sharedFeatures(sourceCard, targetCard) {
  return {
    domainIds: intersection(sourceCard.domainIds, targetCard.domainIds),
    polarityIds: intersection(sourceCard.polarityIds, targetCard.polarityIds),
    materialModeIds: intersection(sourceCard.materialModeIds, targetCard.materialModeIds),
    sourceIds: sharedSourceIds(sourceCard, targetCard),
    sameClass: sourceCard.classId === targetCard.classId
  };
}

function featureScore(features) {
  return features.domainIds.length * 20
    + features.polarityIds.length * 12
    + features.materialModeIds.length * 8
    + features.sourceIds.length * 4
    + (features.sameClass ? 2 : 0);
}

function sourcedEdgeCitationFailures(cards, edges) {
  const byId = new Map(cards.map((card) => [card.id, card]));
  const failures = [];
  for (const edge of edges) {
    if (edge.provenance?.kind !== 'sourced') continue;
    const sourceCard = byId.get(edge.source);
    const targetCard = byId.get(edge.target);
    const citations = edge.provenance?.sourceIds ?? [];
    if (!sourceCard || !targetCard) {
      failures.push(`${edge.id}: missing endpoint card`);
      continue;
    }
    if (!citations.length) {
      failures.push(`${edge.id}: sourced edge has no citations`);
      continue;
    }
    const sourceCitations = new Set(sourceCard.sourceIds);
    const targetCitations = new Set(targetCard.sourceIds);
    const invalid = citations.filter((sourceId) =>
      sourceId === 'original-synthesis' || !sourceCitations.has(sourceId) || !targetCitations.has(sourceId)
    );
    if (invalid.length) failures.push(`${edge.id}: citations not shared by both endpoints (${invalid.join(', ')})`);
  }
  return failures;
}

function hash32(value) {
  let hash = 2166136261;
  const text = String(value);
  for (let index = 0; index < text.length; index += 1) {
    hash ^= text.charCodeAt(index);
    hash = Math.imul(hash, 16777619);
  }
  return hash >>> 0;
}

export function createDeterministicRng(seed = 'oracle') {
  let state = hash32(seed) || 0x6d2b79f5;
  return () => {
    state += 0x6d2b79f5;
    let value = state;
    value = Math.imul(value ^ (value >>> 15), value | 1);
    value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
    return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
  };
}

function stablePick(values, key) {
  if (!values.length) return undefined;
  return values[hash32(key) % values.length];
}

function orderedPair(source, target, directionality) {
  if (directionality === 'symmetric' && source > target) return [target, source];
  return [source, target];
}

function edgeId(typeId, source, target) {
  const type = edgeTypeById.get(typeId);
  if (!type) throw new Error(`Unknown edge type: ${typeId}`);
  const [from, to] = orderedPair(source, target, type.directionality);
  return `edge--${typeId}--${from}--${to}`;
}

const curatedEdgeSpecs = [
  ['Simulation', 'Simulation Hypothesis', 'modeled-by', ['bostrom-simulation'], 'The hypothesis formalizes one influential philosophical argument for interpreting simulated reality.'],
  ['The Matrix', 'Simulation', 'fictionalizes', ['sf-encyclopedia'], 'The storyworld makes simulated enclosure experiential through choice, embodiment, and control.'],
  ['Prison Planet', 'Gnostic Cosmos', 'echoes', ['nag-hammadi-archons'], 'Modern prison-planet lore echoes, but should not be collapsed into, ancient Gnostic critiques of lesser cosmic rulers.'],
  ['Prison Planet', 'Reincarnation Trap', 'extractive-model-within', ['monroe-far-journeys'], 'The reincarnation-trap story supplies one proposed mechanism inside the broader enclosure cosmology.'],
  ['Loosh Farm', 'Prison Planet', 'extractive-model-within', ['monroe-far-journeys'], 'Loosh lore frames emotional extraction as one mechanism through which the enclosure is sustained.'],
  ['Loosh Farm', 'Loosh Harvest', 'enables', ['monroe-far-journeys'], 'The farm names the sustained extraction system; the harvest names its culminating transfer of affective value.'],
  ['Zero-Point Energy', 'Free Energy', 'politicized-through', ['sf-encyclopedia'], 'The physics term becomes politically charged when reframed as an allegedly suppressed abundance technology.'],
  ['Remote Viewing', 'Project Stargate', 'investigated-by', ['cia-stargate', 'cia-stargate-review'], 'The documented program evaluated claimed anomalous information acquisition under controlled protocols.'],
  ['Controlled Remote Viewing', 'Remote Viewing', 'subtype-of', ['cia-stargate'], 'Controlled Remote Viewing names a structured protocol within the wider practice family.'],
  ['MKULTRA', 'Montauk Project', 'later-mythologized-as', ['senate-mkultra', 'archives-mkultra', 'nps-camp-hero'], 'Documented behavioral-research abuses became part of the historical substrate from which later Montauk lore drew credibility.'],
  ['Montauk Project', 'Camp Hero', 'located-at-in-lore', ['nps-camp-hero'], 'Later Montauk narratives locate their underground laboratory mythology beneath the real Camp Hero site.'],
  ['Philadelphia Experiment', 'Montauk Project', 'extends-lore-of', ['nps-camp-hero'], 'Montauk lore often presents itself as a technological and personnel continuation of the Philadelphia Experiment legend.'],
  ['Area 51', 'Black Project', 'provides-setting-for', ['smithsonian-area51'], 'The real test site became the durable cultural setting for stories about highly compartmented aerospace work.'],
  ['Monolith', 'Evolution', 'catalyzes', ['clarke-2001'], 'In the 2001 lineage, the monolith marks evolutionary discontinuity without reducing growth to a simple gift.'],
  ['Spice', 'Bene Gesserit', 'enables', ['dune-publisher'], 'Spice enables the order’s expanded perception while entangling its long strategy with empire and extraction.'],
  ['Spice', 'Spacing Guild', 'enables', ['dune-publisher'], 'Guild navigation depends on a resource whose monopoly organizes interstellar power.'],
  ['United Federation of Planets', 'Starfleet', 'governs', ['startrek-prime-directive', 'startrek-post-scarcity'], 'The Federation supplies the civic framework within which Starfleet explores and acts.'],
  ['Borg', 'Borg Collective', 'embodied-by', ['startrek-borg'], 'The individual Borg body manifests the Collective’s assimilation system.'],
  ['Galactic Empire', 'Rebel Alliance', 'opposed-by', ['starwars-empire'], 'Centralized imperial power and distributed rebellion define one another through sustained conflict.'],
  ['15-Minute City', 'Panopticon', 'politicized-through', ['who-urban', 'rand-truth-decay'], 'A proximity-planning model becomes a surveillance symbol when access, data, and mobility controls are conflated.'],
  ['Panopticon', 'Digital ID', 'enables', ['rand-truth-decay'], 'Identity infrastructure can amplify legibility and self-policing when it becomes a universal access gate.'],
  ['Soft Disclosure', 'Disclosure', 'possible-path-to', ['vallée-magonia'], 'Cultural acclimatization is one proposed pathway toward open acknowledgment, not proof of an intentional program.'],
  ['Soft Disclosure', 'Predictive Programming', 'contrasts-with', ['rand-truth-decay'], 'One model emphasizes preparation through story; the other emphasizes manipulation through planted anticipation.'],
  ['Chapel Perilous', 'Rabbit Hole', 'precipitates', ['vallée-magonia'], 'An expanding inquiry can become an epistemic threshold where coincidence, insight, and paranoia are hard to distinguish.'],
  ['Life Review', 'Near-Death Experience', 'reported-motif-of', ['nih-nde'], 'Panoramic or reciprocal review is a recurrent motif in some near-death reports, not a universal feature.'],
  ['Starseed', 'Pleiadian', 'origin-identity-with', ['theosophy-akasha'], 'Pleiadian identity is one named origin pattern within the wider contemporary Starseed framework.'],
  ['Mantis', 'Insectoid', 'subtype-of', ['sf-encyclopedia'], 'Mantis imagery is a specific modern encounter form within the wider insectoid category.'],
  ['Tall Grey', 'Grey', 'variant-of', ['vallée-magonia'], 'Tall Grey is a size and role variant within contemporary Grey encounter lore.'],
  ['Secret Space Program', 'Breakaway Civilization', 'enables', ['aaro-historical'], 'The hidden program narrative supplies infrastructure for the idea of a civilization operating beyond public institutions.'],
  ['20 and Back', 'Secret Space Program', 'service-narrative-within', ['original-synthesis'], 'Twenty-and-back is a specific memory-and-service narrative nested within the broader secret-space lore complex.'],
  ['Black Goo', 'Grey Goo', 'contrasts-with', ['sf-encyclopedia'], 'Black Goo is framed as adaptive or agentic invasion; Grey Goo is uncontrolled nanotechnological replication.'],
  ['Artificial General Intelligence', 'Alignment Protocol', 'constrains', ['nist-ai-rmf', 'unesco-ai-ethics'], 'Alignment protocols seek to constrain increasingly general capability with explicit human objectives and governance.'],
  ['Superintelligence', 'Technological Singularity', 'precipitates', ['nist-ai-rmf'], 'Recursive superintelligence is one frequently proposed mechanism for an unpredictable technological threshold.'],
  ['Wormhole', 'Stargate', 'enables', ['caltech-wormholes'], 'Traversable spacetime topology supplies one scientific metaphor for addressable portal infrastructure.'],
  ['Portal', 'Stargate', 'subtype-of', ['sf-encyclopedia'], 'A Stargate is a networked and addressable instance within the broader portal category.'],
  ['Akashic Cosmos', 'Space Memory Network', 'echoes', ['theosophy-akasha', 'original-synthesis'], 'Both imagine memory as distributed through reality, while the latter is explicitly an Oracle synthesis.'],
  ['Noosphere', 'Space Memory Network', 'contextualizes', ['teilhard-noosphere', 'original-synthesis'], 'The noosphere provides an intellectual precedent for imagining cognition and memory at planetary or cosmic scale.'],
  ['Dark Forest Hypothesis', 'Dark Forest Cosmos', 'modeled-by', ['three-body-publisher'], 'The hypothesis supplies the strategic logic through which the cosmology interprets cosmic silence.'],
  ['Zoo Hypothesis', 'Zoo Cosmos', 'modeled-by', ['seti-fermi'], 'The hypothesis supplies a contact model in which observers deliberately avoid overt intervention.'],
  ['Ancient Astronaut Cosmos', 'Ancient Astronaut Hypothesis', 'modeled-by', ['sf-encyclopedia'], 'The hypothesis is the claim-level lens from which the wider cosmological story is constructed.'],
  ['Holographic Universe', 'Holographic Principle', 'modeled-by', ['nasa-black-holes'], 'The scientific principle is one source for a much broader metaphysical interpretation, which requires a clear evidence boundary.'],
  ['Mars', 'Mars Colony', 'provides-setting-for', ['nasa-astrobiology'], 'Mars provides the concrete planetary constraints that a settlement must negotiate.'],
  ['Quantum Computer', 'Artificial General Intelligence', 'enables', ['ibm-quantum', 'nist-ai-rmf'], 'Quantum computation is one speculative capability amplifier, not a necessary or sufficient cause of general intelligence.'],
  ['CRISPR', 'Gene Editing', 'subtype-of', ['nsf-crispr'], 'CRISPR is a specific family of gene-editing methods within the broader technological category.'],
  ['Brain-Computer Interface', 'Neural Lace', 'contextualizes', ['nih-bci'], 'Current BCI research grounds the more speculative neural-lace concept in real interface constraints.'],
  ['First Contact Mission', 'First Contact', 'participates-in', ['nasa-technosignatures', 'un-space-treaty'], 'The mission creates the institutional protocol through which the threshold event may be approached.'],
  ['Universal Translator', 'Communication Established', 'enables', ['startrek-prime-directive'], 'Translation makes reciprocal communication possible while leaving cultural interpretation unresolved.'],
  ['Psyop', 'Information Operation', 'subtype-of', ['nato-cognitive'], 'Psychological operations are a specific influence practice within the broader information-operations family.'],
  ['Disinformation Network', 'Misinformation Cascade', 'precipitates', ['un-disinformation', 'rand-truth-decay'], 'Coordinated falsehoods can trigger wider cascades that include uncoordinated repetition and sincere error.'],

  // Required Loosh constellation: bounded lore, cultural analogy, and an
  // explicit liberation counterpoint remain distinct relationships.
  ['Loosh', 'Energy Harvest', 'variant-of', ['sf-encyclopedia'], 'Loosh is a lore-specific form of the wider energy-harvest motif: affect is imagined as a resource that can be collected.'],
  ['Loosh', 'Prison Planet', 'extractive-model-within', ['sf-encyclopedia'], 'Within prison-planet lore, loosh supplies one proposed extraction mechanism; the relationship is a map of the lore, not a factual certification.'],
  ['Archon', 'Loosh', 'harvests', ['sf-encyclopedia'], 'Some modern Gnostic-remix narratives cast Archons as harvesters of loosh; this describes that narrative role without attributing it to all historical Gnostic traditions.'],
  ['Loosh', 'The Matrix', 'resembles', ['sf-encyclopedia'], 'Loosh extraction and the Matrix battery-farm image share a bounded motif of human experience converted into value, without implying common origin.'],
  ['Loosh', 'Attention Economy', 'resembles', ['original-synthesis'], 'The attention economy offers a grounded analogy for value extracted from attention and emotion; analogy is not evidence for literal loosh.'],
  ['Fear', 'Loosh', 'amplifies', ['sf-encyclopedia'], 'In loosh-harvest narratives, fear is portrayed as an intensifier of extractable affect; this is a reviewed lore assertion, not a psychological law.'],
  ['Loosh', 'Ascension', 'contrasts-with', ['sf-encyclopedia'], 'The loosh motif frames consciousness as captured value, while ascension frames it as developmental liberation; the contrast keeps both stories visible.'],

  // Keep the evidence-backed planning lineage separate from the control-myth
  // lineage around the 15-minute-city symbol.
  ['15-Minute City', 'Smart City', 'resembles', ['who-urban', 'c40-urban-planning'], 'Both organize services through urban proximity and infrastructure, while a smart city may add data systems not required by the 15-minute-city model.'],
  ['15-Minute City', 'Walkability', 'contains', ['who-urban', 'c40-urban-planning'], 'Walkability is a core planning affordance within the documented 15-minute-city model: daily needs should be reachable without compulsory car travel.'],
  ['15-Minute City', 'Neighborhood Access', 'contains', ['who-urban', 'c40-urban-planning'], 'Neighborhood access names the proximity-to-needs objective of the documented planning model, separate from later confinement narratives.'],
  ['15-Minute City', 'Distributed Network', 'contains', ['original-synthesis'], 'A distributed network provides a systems analogy for locating services across many neighborhood nodes rather than one distant center.'],
  ['15-Minute City', 'Geofence', 'politicized-through', ['original-synthesis'], 'Control mythology often reframes proximity planning through geofencing, although geofencing is not inherent to the documented planning model.'],
  ['15-Minute City', 'Digital ID', 'politicized-through', ['original-synthesis'], 'Control mythology links proximity planning to digital identity gates; the Oracle keeps that later claim separate from the planning evidence.'],

  // Major science-fiction lineages are represented through canonical concepts
  // rather than franchise-title nodes.
  ['Mythic Past', 'Galactic Empire', 'embodied-by', ['sf-encyclopedia'], 'The Galactic Empire embodies the mythic-past lineage through inherited conflict, imperial archetype, and moral choice.'],
  ['Mythic Past', 'Rebel Alliance', 'embodied-by', ['sf-encyclopedia'], 'The Rebel Alliance embodies the resistance branch of the mythic-past lineage.'],
  ['Mythic Past', 'Jedi Order', 'embodied-by', ['sf-encyclopedia'], 'The Jedi Order embodies the initiatory and ethical-order branch of the mythic-past lineage.'],
  ['Aspirational Future', 'Starfleet', 'embodied-by', ['sf-encyclopedia'], 'Starfleet embodies an aspirational future organized around exploration, civic service, and negotiated restraint.'],
  ['Aspirational Future', 'United Federation of Planets', 'embodied-by', ['sf-encyclopedia'], 'The Federation embodies a plural civic future whose institutions attempt cooperation beyond scarcity and conquest.'],
  ['Aspirational Future', 'Prime Directive', 'embodied-by', ['original-synthesis'], 'The Prime Directive embodies the aspirational future’s unresolved tension between exploration, responsibility, and nonintervention.'],
  ['Dune', 'Spice', 'contains', ['dune-publisher'], 'The Dune lineage contains spice as the resource joining ecology, perception, transport, monopoly, and empire.'],
  ['Dune', 'Arrakis', 'contains', ['dune-publisher'], 'The Dune lineage contains Arrakis as the ecological ground that makes its political and spiritual system possible.'],
  ['Dune', 'Bene Gesserit', 'contains', ['dune-publisher'], 'The Dune lineage contains the Bene Gesserit as a long-horizon institution of memory, embodiment, breeding, and political intervention.'],
  ['Dune', 'Spacing Guild', 'contains', ['dune-publisher'], 'The Dune lineage contains the Spacing Guild as the institution that converts spice-enabled navigation into infrastructural power.']
];

const lineageSpecs = [
  {
    id: 'contact-horizon', title: 'The Contact Horizon', domain: 'contact-communication',
    thesis: 'Contact becomes durable only when detection, interpretation, reciprocity, integration, and shared governance each receive their own threshold.',
    path: ['Signal Detected', 'First Sighting', 'Close Encounter', 'Communication Established', 'First Contact', 'Contact Integration', 'Cosmic Treaty', 'Federation Founding']
  },
  {
    id: 'disclosure-cascade', title: 'The Disclosure Cascade', domain: 'power-control',
    thesis: 'Revelation is not one moment but a contested sequence from acclimatization through institutional acknowledgment and social integration.',
    path: ['Soft Disclosure', 'Gradual Disclosure', 'Controlled Disclosure', 'Disclosure', 'Catastrophic Disclosure', 'Ontological Shock', 'Contact Integration']
  },
  {
    id: 'architecture-of-secrecy', title: 'The Architecture of Secrecy', domain: 'power-control',
    thesis: 'A hidden operation persists by dividing knowledge before it can divide accountability.',
    path: ['Classification', 'Need to Know', 'Compartmentalization', 'Special Access Program', 'Waived SAP', 'Black Project', 'Crash Retrieval Program', 'Reverse Engineering Program', 'Breakthrough']
  },
  {
    id: 'off-world-civilization', title: 'The Off-World Civilization', domain: 'civilization-ecology',
    thesis: 'A space civilization grows through a succession of increasingly independent habitats rather than one heroic leap.',
    path: ['Earth', 'Moon', 'Lunar Base', 'Mars', 'Mars Colony', 'Asteroid Belt', 'Ceres', 'Off-World Base', 'Orbital Habitat', 'Dyson Habitat']
  },
  {
    id: 'machine-threshold', title: 'The Machine Threshold', domain: 'technology-infrastructure',
    thesis: 'Capability, governance, recursive improvement, and post-biological continuity remain distinct steps in the machine-intelligence story.',
    path: ['AI Agent', 'Artificial General Intelligence', 'Alignment Protocol', 'Superintelligence', 'Technological Singularity', 'Post-Biological Intelligence', 'Post-Scarcity']
  },
  {
    id: 'memory-and-continuity', title: 'Memory and Continuity', domain: 'memory-identity',
    thesis: 'Identity changes as memory moves from inheritance to intervention, erasure, and computational continuity.',
    path: ['Memory', 'Genetic Memory', 'Ancestral Memory', 'Bloodline Memory', 'Implanted Memory', 'False Memory', 'Memory Wipe', 'Amnesia', 'Digital Continuity']
  },
  {
    id: 'gnostic-escape', title: 'The Gnostic Escape', domain: 'liberation-transcendence',
    thesis: 'The enclosure story becomes useful when it moves from cosmic explanation toward discernment, agency, and liberation.',
    path: ['Gnostic Cosmos', 'Watcher', 'Reincarnation Cycle', 'Reincarnation Trap', 'Prison Planet', 'Loosh Farm', 'Liberation', 'Return to Source']
  },
  {
    id: 'trickster-inquiry', title: 'The Trickster Inquiry', domain: 'mind-consciousness',
    thesis: 'Curiosity survives the rabbit hole when pattern recognition is paired with disconfirmation, tolerance for ambiguity, and revision.',
    path: ['Wonder', 'Synchronicity', 'Apophenia', 'Rabbit Hole', 'Chapel Perilous', 'Cognitive Dissonance', 'Discernment', 'Skeptical Suspension', 'Meaning-Making']
  },
  {
    id: 'distant-witness', title: 'The Distant Witness', domain: 'mind-consciousness',
    thesis: 'Experiential reports become inquiry when protocol, feedback, interpretation, and integration remain visible.',
    path: ['Lucid Dream', 'Out-of-Body Experience', 'Astral Projection', 'Remote Viewing', 'Controlled Remote Viewing', 'Project Stargate', 'Precognition', 'Life Review', 'Near-Death Experience']
  },
  {
    id: 'mythic-resistance', title: 'Mythic Resistance', domain: 'origin-lineage',
    thesis: 'The mythic-past story converts inheritance into agency by testing whether destiny can be chosen rather than obeyed.',
    path: ['Mythic Past', 'Bloodline', 'Hidden Heir', 'Chosen One', 'Jedi Order', 'Rebel Alliance', 'Galactic Empire', 'Resistance']
  },
  {
    id: 'aspirational-contact', title: 'Aspirational Contact', domain: 'contact-communication',
    thesis: 'An aspirational future requires civic institutions, ethical exploration, translation, treaty, and plural membership.',
    path: ['Aspirational Future', 'Starfleet', 'United Federation of Planets', 'First Contact Mission', 'First Contact', 'Universal Translator', 'Cosmic Treaty', 'Galactic Federation']
  },
  {
    id: 'scarcity-and-spice', title: 'Scarcity and Spice', domain: 'civilization-ecology',
    thesis: 'A valuable resource reorganizes mobility, institutions, inheritance, resistance, and the meaning of abundance.',
    path: ['Scarcity', 'Resource Monopoly', 'Spice', 'Spacing Guild', 'Great Houses', 'Corporate Feudalism', 'Resistance Network', 'Liberation', 'Abundance']
  },
  {
    id: 'surveillance-enclosure', title: 'The Surveillance Enclosure', domain: 'power-control',
    thesis: 'Attention becomes a prison through a chain of capture, prediction, identity, scoring, automated rule, and self-policing.',
    path: ['Attention', 'Attention Economy', 'Surveillance Capitalism', 'Panopticon', 'Digital ID', 'Social Credit', 'Algorithmic Governance', 'Smart Prison', 'Resistance']
  },
  {
    id: 'cosmic-silence', title: 'The Politics of Cosmic Silence', domain: 'intelligence-otherness',
    thesis: 'Silence supports multiple hypotheses whose consequences diverge sharply: hiding, observing, quarantining, and finally declaring.',
    path: ['Dark Forest Hypothesis', 'Dark Forest Cosmos', 'Zoo Hypothesis', 'Zoo Cosmos', 'Earth Quarantine Hypothesis', 'Quarantine Earth', 'Quarantine Protocol', 'Quarantine Declaration', 'Disclosure']
  }
];

export const spreads = [
  {
    id: 'rabbit-hole', title: 'Rabbit Hole', cards: 7, purpose: 'Follow an anomaly far enough to discover structure without surrendering proportional reality or disconfirmation.',
    positions: [
      ['anomaly', 'The Anomaly', 'What observation refuses the current explanation?', ['artifacts', 'events', 'states']],
      ['first-explanation', 'The First Explanation', 'Which model becomes compelling first?', ['hypotheses', 'cosmologies', 'myths']],
      ['hidden-connection', 'The Hidden Connection', 'What meaningful connection deserves testing?', ['forces', 'systems', 'programs']],
      ['projection', 'The Projection', 'What desire, fear, or identity is being placed onto the pattern?', ['states', 'myths', 'beings']],
      ['evidence', 'The Evidence', 'What trace can be checked independently?', ['artifacts', 'technologies', 'events']],
      ['point-of-no-return', 'The Point of No Return', 'Which commitment would make revision socially or psychologically costly?', ['events', 'systems', 'factions']],
      ['discernment', 'Discernment', 'What test preserves wonder while restoring the ability to turn around?', ['forces', 'hypotheses', 'programs']]
    ]
  },
  {
    id: 'first-contact', title: 'First Contact', cards: 6, purpose: 'Prepare two unlike intelligences for contact without collapsing difference into threat or salvation.',
    positions: [
      ['other', 'The Other', 'What form of intelligence may be approaching?', ['beings', 'hypotheses']],
      ['projection', 'What You Project', 'What expectation could distort the encounter?', ['forces', 'myths', 'states']],
      ['actual-gift', 'What They Actually Bring', 'What capability, need, or difference arrives with them?', ['artifacts', 'technologies', 'forces']],
      ['boundary', 'The Boundary', 'What must remain protected, consensual, or untranslated?', ['systems', 'worlds', 'forces']],
      ['exchange', 'The Exchange', 'What can be offered without coercion or assimilation?', ['artifacts', 'technologies', 'programs']],
      ['new-world', 'The New World', 'What shared reality is created if contact becomes reciprocal?', ['worlds', 'events', 'cosmologies']]
    ]
  },
  {
    id: 'disclosure', title: 'Disclosure', cards: 6, purpose: 'Assess concealment, visibility, readiness, consequence, and the next responsible release.',
    positions: [
      ['hidden', 'What Is Hidden', 'What material, history, or relationship is not yet visible?', ['artifacts', 'programs', 'systems']],
      ['beneficiary', 'Who Benefits from Concealment', 'Who gains power, safety, time, or legitimacy while this remains hidden?', ['factions', 'beings', 'systems']],
      ['visible', 'What Is Already Visible', 'Which trace has been normalized, overlooked, or misclassified?', ['artifacts', 'events', 'states']],
      ['change', 'What Revelation Would Change', 'Which identity, institution, or system could not remain the same?', ['events', 'systems', 'states']],
      ['not-ready', 'What You Are Not Ready to Know', 'What truth requires more support before it can be integrated?', ['states', 'myths', 'forces']],
      ['reveal-now', 'What Should Be Revealed Now', 'What bounded disclosure increases agency without avoidable harm?', ['programs', 'events', 'artifacts']]
    ]
  },
  {
    id: 'system-anatomy', title: 'System Anatomy', cards: 7, purpose: 'Reveal the inputs, rules, beneficiaries, externalities, resistance, and transformation path of a system.',
    positions: [
      ['input', 'Input', 'What enters the system?', ['artifacts', 'forces', 'beings']],
      ['rule', 'Rule', 'What converts input into behavior?', ['systems', 'technologies', 'programs']],
      ['operator', 'Operator', 'Who maintains or steers the rule?', ['factions', 'beings']],
      ['beneficiary', 'Beneficiary', 'Who gains from the current output?', ['factions', 'beings', 'worlds']],
      ['cost', 'Externalized Cost', 'What is being displaced, extracted, or forgotten?', ['forces', 'states', 'worlds']],
      ['resistance', 'Resistance', 'Where is agency already appearing?', ['forces', 'factions', 'events']],
      ['redesign', 'Redesign', 'What change could alter the system without reproducing it?', ['technologies', 'programs', 'systems']]
    ]
  },
  {
    id: 'timeline', title: 'Timeline', cards: 6, purpose: 'Compare the current timeline, a branch point, two futures, their convergence, and what persists.',
    positions: [
      ['current', 'Current Timeline', 'What inherited sequence is still selecting the present?', ['myths', 'events', 'forces']],
      ['branch-point', 'Branch Point', 'Which decision or event creates divergence now?', ['events', 'programs', 'beings']],
      ['path-a', 'Path A', 'What unfolds if the dominant pattern continues?', ['systems', 'worlds', 'states']],
      ['path-b', 'Path B', 'What unfolds if the hidden option is chosen?', ['systems', 'worlds', 'states']],
      ['convergence', 'Convergence', 'Where could the two paths meet again without erasing consequence?', ['events', 'technologies', 'forces']],
      ['persists', 'What Persists', 'What value, memory, or relationship survives both timelines?', ['artifacts', 'forces', 'states']]
    ]
  },
  {
    id: 'prison-planet', title: 'Prison Planet', cards: 6, purpose: 'Diagnose an enclosure without assuming in advance whether it is physical, political, psychological, spiritual, or symbolic.',
    positions: [
      ['wall', 'The Wall', 'What boundary actually limits movement, knowledge, or choice?', ['systems', 'worlds', 'forces']],
      ['keeper', 'The Keeper', 'Who or what maintains the enclosure?', ['factions', 'beings', 'systems']],
      ['reward', 'The Reward for Staying', 'What comfort, identity, or protection makes the enclosure durable?', ['artifacts', 'states', 'forces']],
      ['fear', 'The Fear of Leaving', 'What threat—real or projected—makes exit feel impossible?', ['states', 'myths', 'events']],
      ['exit', 'The Hidden Exit', 'Where is agency already present but unrecognized?', ['technologies', 'programs', 'forces']],
      ['freedom', 'What Freedom Requires', 'What capacity and collective condition make escape more than a change of scenery?', ['forces', 'factions', 'events']]
    ]
  },
  {
    id: 'worldseed', title: 'Worldseed', cards: 8, purpose: 'Compose a world from environment, resource, intelligence, system, fracture, threshold, memory, and promise.',
    positions: [
      ['place', 'Environment', 'What physical or ontological environment holds the world?', ['worlds', 'cosmologies']],
      ['resource', 'Scarce or Sacred Resource', 'What does the world organize around?', ['artifacts', 'forces']],
      ['people', 'Inhabitants', 'Who is capable of acting here?', ['beings', 'factions']],
      ['infrastructure', 'Infrastructure', 'What makes everyday life possible?', ['technologies', 'systems']],
      ['order', 'Governing Order', 'How is coordination enforced or earned?', ['systems', 'factions']],
      ['fracture', 'Fracture', 'What contradiction can no longer be contained?', ['forces', 'states']],
      ['threshold', 'Threshold Event', 'What makes this story begin now?', ['events', 'programs']],
      ['promise', 'Unrealized Future', 'What can this world become?', ['myths', 'events', 'cosmologies']]
    ]
  },
  {
    id: 'character-continuity', title: 'Character Continuity', cards: 6, purpose: 'Build a character whose body, memory, role, secret, conflict, and transformation belong to one arc.',
    positions: [
      ['role', 'Role', 'What recurring role does the character inhabit?', ['beings']],
      ['body', 'Embodiment', 'What makes this body capable or vulnerable?', ['technologies', 'states', 'beings']],
      ['memory', 'Memory', 'What past defines identity?', ['states', 'artifacts', 'myths']],
      ['secret', 'Secret', 'What knowledge cannot yet be integrated?', ['programs', 'systems', 'artifacts']],
      ['conflict', 'Conflict', 'What force or institution opposes the character?', ['forces', 'factions', 'systems']],
      ['becoming', 'Becoming', 'What threshold changes the meaning of the self?', ['events', 'states', 'technologies']]
    ]
  },
  {
    id: 'faction-tension', title: 'Faction Tension', cards: 7, purpose: 'Create a faction from origin, promise, method, resource, internal dissent, rival, and public mask.',
    positions: [
      ['origin', 'Origin', 'What event or wound called the faction into being?', ['events', 'myths']],
      ['promise', 'Promise', 'What future does membership offer?', ['myths', 'cosmologies', 'forces']],
      ['method', 'Method', 'How does the faction act repeatedly?', ['programs', 'technologies', 'systems']],
      ['resource', 'Resource', 'What capability can it control or exchange?', ['artifacts', 'technologies', 'worlds']],
      ['dissent', 'Internal Dissent', 'What cannot be reconciled inside it?', ['forces', 'states', 'hypotheses']],
      ['rival', 'Rival', 'Who contests its legitimacy or objective?', ['factions', 'beings']],
      ['mask', 'Public Mask', 'What story does it tell about itself?', ['myths', 'systems', 'artifacts']]
    ]
  },
  {
    id: 'ascension', title: 'Ascension', cards: 6, purpose: 'Explore developmental transformation without using transcendence to bypass embodiment, accountability, or care.',
    positions: [
      ['current-density', 'Current Density', 'What level of complexity, care, and agency can the present system actually hold?', ['states', 'systems', 'cosmologies']],
      ['integrate', 'What Must Be Integrated', 'Which shadow or contradiction cannot be left behind?', ['forces', 'states', 'myths']],
      ['release', 'What Must Be Released', 'What identity, control, or scarcity pattern has completed its work?', ['forces', 'systems', 'artifacts']],
      ['threshold', 'The Threshold', 'What event or practice makes the transition irreversible?', ['events', 'programs', 'technologies']],
      ['shadow', 'Shadow of Transcendence', 'How could the promise of ascent reproduce hierarchy, denial, or capture?', ['systems', 'factions', 'states']],
      ['capacity', 'The New Capacity', 'What becomes possible through integration rather than escape?', ['technologies', 'forces', 'states']]
    ]
  },
  {
    id: 'cross-ontology-council', title: 'Cross-Ontology Council', cards: 9, purpose: 'Place competing explanations in dialogue without treating uncertainty as failure.',
    positions: [
      ['observation', 'Shared Observation', 'What must every model explain?', ['events', 'artifacts', 'states']],
      ['material', 'Material Model', 'What physical explanation is strongest?', ['technologies', 'hypotheses']],
      ['psychosocial', 'Psychosocial Model', 'What cultural or psychological dynamics matter?', ['hypotheses', 'states', 'systems']],
      ['experiential', 'Experiential Model', 'What does first-person report contribute?', ['states', 'beings']],
      ['mythic', 'Mythic Model', 'What durable story shape organizes the experience?', ['myths', 'cosmologies']],
      ['political', 'Political Model', 'Who gains authority from each explanation?', ['factions', 'systems', 'programs']],
      ['fictional', 'Fictional Rehearsal', 'Where has culture rehearsed this possibility?', ['myths', 'worlds', 'artifacts']],
      ['disconfirmation', 'Disconfirmation', 'What observation would weaken the favored model?', ['forces', 'hypotheses', 'programs']],
      ['provisional', 'Provisional Synthesis', 'What can be acted upon before ontology is settled?', ['forces', 'events', 'systems']]
    ]
  },
  {
    id: 'one-card-reality-check', title: 'One-Card Reality Check', cards: 1, purpose: 'Interrupt certainty with one card that reframes the immediate question.',
    positions: [
      ['check', 'Reality Check', 'What distinction, question, or action deserves attention before the story grows?']
    ]
  }
].map((spread) => ({
  ...spread,
  positions: spread.positions.map(([id, title, prompt, allowedClasses = []]) => ({ id, title, prompt, allowedClasses }))
}));

const standardResources = [
  { id: 'clarity', name: 'Clarity', range: [0, 12], meaning: 'Ability to separate observation, interpretation, and desire.' },
  { id: 'trust', name: 'Trust', range: [0, 12], meaning: 'Relational permission to exchange uncertainty and influence.' },
  { id: 'agency', name: 'Agency', range: [0, 12], meaning: 'Capacity to choose a meaningful next action.' },
  { id: 'stability', name: 'Stability', range: [0, 12], meaning: 'Capacity to integrate disruptive information without collapse.' },
  { id: 'secrecy', name: 'Secrecy', range: [0, 12], meaning: 'Information withheld, compartmented, or not yet safe to reveal.' },
  { id: 'evidence-debt', name: 'Evidence Debt', range: [0, 12], meaning: 'Unsupported certainty accumulated by play.' }
];

function progressTrack(id, name = id) {
  return { id, name, range: [0, 12], initial: 0, kind: 'progress' };
}

function riskTrack(id, name = id) {
  return { id, name, range: [0, 12], initial: 0, kind: 'risk' };
}

const gameContracts = {
  'rabbit-hole': {
    tracks: [progressTrack('model-integrity', 'Model Integrity')],
    actionTrackDeltas: { source: { 'model-integrity': 2 }, disconfirm: { 'model-integrity': 1 } },
    resolutionPredicates: {
      win: { all: [{ track: 'model-integrity', operator: '>=', value: 6 }, { track: 'clarity', operator: '>=', value: 8 }] },
      loss: { any: [{ track: 'evidence-debt', operator: '>=', value: 12 }, { track: 'trust', operator: '<=', value: 0 }] }
    },
    fixtures: {
      win: ['connect', 'source', 'connect', 'source', 'connect', 'source'],
      loss: Array(12).fill('connect')
    }
  },
  'ontology-war': {
    tracks: [progressTrack('model-quality', 'Model Quality')],
    actionTrackDeltas: { revise: { 'model-quality': 1 }, falsify: { 'model-quality': 2 } },
    resolutionPredicates: {
      win: { all: [{ track: 'model-quality', operator: '>=', value: 6 }] },
      loss: { any: [{ track: 'trust', operator: '<=', value: 0 }] }
    },
    fixtures: { win: Array(6).fill('revise'), loss: Array(6).fill('cross-examine') }
  },
  'first-contact': {
    tracks: [progressTrack('communication', 'Reciprocal Communication'), riskTrack('projection', 'Projection')],
    actionTrackDeltas: { exchange: { communication: 2 }, signal: { projection: 2 } },
    resolutionPredicates: {
      win: { all: [{ track: 'communication', operator: '>=', value: 10 }, { track: 'trust', operator: '>', value: 0 }, { track: 'stability', operator: '>', value: 0 }] },
      loss: { any: [{ track: 'projection', operator: '>=', value: 6 }, { track: 'trust', operator: '<=', value: 0 }] }
    },
    fixtures: { win: ['exchange', 'exchange', 'exchange'], loss: ['signal', 'signal', 'signal'] }
  },
  disclosure: {
    tracks: [progressTrack('disclosure', 'Verified Disclosure'), riskTrack('rupture', 'Institutional Rupture')],
    actionTrackDeltas: { acknowledge: { disclosure: 1 }, release: { rupture: 2 } },
    resolutionPredicates: {
      win: { all: [{ track: 'disclosure', operator: '>=', value: 8 }] },
      loss: { any: [{ track: 'rupture', operator: '>=', value: 6 }, { track: 'stability', operator: '<=', value: 0 }] }
    },
    fixtures: { win: ['acknowledge', 'acknowledge'], loss: ['release', 'release', 'release'] }
  },
  'soft-disclosure': {
    tracks: [progressTrack('readiness', 'Audience Readiness'), riskTrack('overload', 'Audience Overload')],
    actionTrackDeltas: { 'test-audience': { readiness: 1 }, encode: { overload: 2 } },
    resolutionPredicates: {
      win: { all: [{ track: 'readiness', operator: '>=', value: 8 }] },
      loss: { any: [{ track: 'overload', operator: '>=', value: 6 }, { track: 'stability', operator: '<=', value: 0 }] }
    },
    fixtures: {
      win: Array(4).fill(null).map(() => ({ actionId: 'test-audience', branchChoice: 'otherwise' })),
      loss: ['encode', 'encode', 'encode']
    }
  },
  'evidence-assembly': {
    tracks: [progressTrack('audit-completeness', 'Audit Completeness')],
    actionTrackDeltas: { support: { 'audit-completeness': 2 }, classify: { 'audit-completeness': 1 }, challenge: { 'evidence-debt': 2 } },
    resolutionPredicates: {
      win: { all: [{ track: 'audit-completeness', operator: '>=', value: 6 }] },
      loss: { any: [{ track: 'evidence-debt', operator: '>=', value: 6 }] }
    },
    fixtures: { win: ['support', 'support', 'support'], loss: ['challenge', 'challenge', 'challenge'] }
  },
  'timeline-repair': {
    tracks: [progressTrack('continuity', 'Continuity'), riskTrack('paradox', 'Unresolved Paradox')],
    actionTrackDeltas: { integrate: { continuity: 1 }, branch: { paradox: 2 } },
    resolutionPredicates: {
      win: { all: [{ track: 'continuity', operator: '>=', value: 8 }] },
      loss: { any: [{ track: 'paradox', operator: '>=', value: 6 }, { track: 'stability', operator: '<=', value: 0 }] }
    },
    fixtures: {
      win: ['integrate', 'integrate', 'integrate'],
      loss: Array(3).fill(null).map(() => ({ actionId: 'branch', branchChoice: 'then' }))
    }
  },
  'threshold-governance': {
    tracks: [progressTrack('legitimacy', 'Charter Legitimacy'), riskTrack('capture', 'Institutional Capture')],
    actionTrackDeltas: { 'propose-rule': { legitimacy: 2, capture: 1 }, audit: { legitimacy: 1, capture: -1 } },
    resolutionPredicates: {
      win: { all: [{ track: 'legitimacy', operator: '>=', value: 6 }, { track: 'capture', operator: '<', value: 6 }] },
      loss: { any: [{ track: 'capture', operator: '>=', value: 6 }, { track: 'trust', operator: '<=', value: 0 }] }
    },
    fixtures: { win: ['propose-rule', 'audit', 'propose-rule', 'audit'], loss: Array(6).fill('minority-protection') }
  },
  'worldseed-engine': {
    tracks: [progressTrack('coherence', 'World Coherence'), riskTrack('entropy', 'World Entropy')],
    actionTrackDeltas: { install: { coherence: 2 }, future: { coherence: 1 }, complicate: { entropy: 2 } },
    resolutionPredicates: {
      win: { all: [{ track: 'coherence', operator: '>=', value: 6 }] },
      loss: { any: [{ track: 'entropy', operator: '>=', value: 6 }, { track: 'stability', operator: '<=', value: 0 }] }
    },
    fixtures: { win: ['install', 'install', 'install'], loss: ['complicate', 'complicate', 'complicate'] }
  },
  'secret-space-program': {
    tracks: [progressTrack('readiness', 'Operational Readiness'), riskTrack('exposure', 'Unsupported Exposure')],
    initialOverrides: { secrecy: 0 },
    actionTrackDeltas: { commission: { readiness: 1 }, 'establish-base': { readiness: 1 }, compartment: { exposure: 2 } },
    resolutionPredicates: {
      win: { all: [{ track: 'readiness', operator: '>=', value: 6 }, { track: 'evidence-debt', operator: '<', value: 6 }] },
      loss: { any: [{ track: 'exposure', operator: '>=', value: 6 }, { track: 'evidence-debt', operator: '>=', value: 12 }] }
    },
    fixtures: { win: ['commission', 'commission', 'commission'], loss: ['compartment', 'compartment', 'compartment'] }
  },
  ascension: {
    tracks: [progressTrack('development', 'Integrated Development'), riskTrack('collapse', 'Transition Collapse')],
    actionTrackDeltas: { develop: { development: 1 }, 'cross-threshold': { collapse: 2 } },
    resolutionPredicates: {
      win: { all: [{ track: 'development', operator: '>=', value: 6 }, { track: 'stability', operator: '>=', value: 4 }] },
      loss: { any: [{ track: 'collapse', operator: '>=', value: 6 }, { track: 'stability', operator: '<=', value: 0 }] }
    },
    fixtures: { win: ['develop', 'develop', 'develop'], loss: ['cross-threshold', 'cross-threshold', 'cross-threshold'] }
  },
  'prison-planet': {
    tracks: [progressTrack('liberation', 'Collective Liberation')],
    actionTrackDeltas: { 'collective-escape': { liberation: 1 }, 'trace-benefit': { 'evidence-debt': 1 } },
    resolutionPredicates: {
      win: { all: [{ track: 'liberation', operator: '>=', value: 10 }, { track: 'evidence-debt', operator: '<', value: 5 }] },
      loss: { any: [{ track: 'evidence-debt', operator: '>=', value: 6 }, { track: 'stability', operator: '<=', value: 0 }] }
    },
    fixtures: { win: ['collective-escape', 'collective-escape', 'collective-escape'], loss: ['trace-benefit', 'trace-benefit', 'trace-benefit'] }
  }
};

function collectEffectStateReferences(effects, references) {
  for (const effect of effects ?? []) {
    const schema = effectOperationSchemas[effect.op];
    for (const key of schema?.stateReferences ?? []) if (typeof effect[key] === 'string') references.add(effect[key]);
    if (effect.op === 'draw' && typeof effect.from?.exclude === 'string') references.add(effect.from.exclude);
    if (effect.op === 'branch') {
      collectEffectStateReferences(effect.then, references);
      collectEffectStateReferences(effect.otherwise, references);
    }
  }
}

function finalizeGame(game) {
  const contract = gameContracts[game.id];
  if (!contract) throw new Error(`Missing executable game contract for ${game.id}.`);
  const resourceDefinitions = standardResources
    .filter((resource) => game.resources.includes(resource.id))
    .map((resource) => ({
      ...resource,
      kind: 'resource',
      initial: contract.initialOverrides?.[resource.id] ?? (resource.id === 'evidence-debt' ? 0 : 6)
    }));
  const trackDefinitions = [...resourceDefinitions, ...contract.tracks]
    .filter((track, index, values) => values.findIndex((candidate) => candidate.id === track.id) === index);
  const references = new Set();
  collectEffectStateReferences(game.setup, references);
  for (const action of game.turn) collectEffectStateReferences(action.effects, references);
  const trackIds = new Set(trackDefinitions.map((track) => track.id));
  return {
    ...game,
    resourceDefinitions,
    trackDefinitions,
    zoneDefinitions: [...references]
      .filter((id) => !trackIds.has(id))
      .sort()
      .map((id) => ({ id, kind: 'state-reference', initialCount: 0 })),
    edgeTypeDefinitions: [...new Set(['declared', ...game.turn.flatMap((action) => action.effects
      .filter((effect) => effect.op === 'connect')
      .map((effect) => effect.edgeType))])].sort(),
    actionTrackDeltas: contract.actionTrackDeltas,
    resolutionPredicates: contract.resolutionPredicates,
    fixtures: contract.fixtures
  };
}

export const games = [
  {
    id: 'rabbit-hole', title: 'Rabbit Hole', players: [1, 6], minutes: [35, 90], mode: 'cooperative inquiry',
    objective: 'Reach a provisional model with four sourced connections before Evidence Debt reaches twelve.',
    resources: ['clarity', 'trust', 'evidence-debt'],
    setup: [
      { op: 'draw', count: 1, from: { classes: ['events', 'artifacts', 'states'] }, to: 'anomaly' },
      { op: 'draw', count: 5, from: { exclude: 'anomaly' }, to: 'inquiry-hand' },
      { op: 'advance-track', track: 'clarity', amount: 3 }
    ],
    turn: [
      { id: 'observe', label: 'Observe', cost: [], effects: [{ op: 'draw', count: 1, from: { classes: ['artifacts', 'events'] }, to: 'inquiry-hand' }] },
      { id: 'connect', label: 'Connect', cost: [], effects: [{ op: 'connect', from: 'hand-card', to: 'inquiry-map', edgeType: 'declared' }, { op: 'advance-track', track: 'evidence-debt', amount: 1 }] },
      { id: 'source', label: 'Source', cost: [], effects: [{ op: 'cite-source', card: 'connected-card', claim: 'declared-claim' }, { op: 'retreat-track', track: 'evidence-debt', amount: 1 }, { op: 'gain', resource: 'clarity', amount: 1 }] },
      { id: 'disconfirm', label: 'Disconfirm', cost: [{ resource: 'trust', amount: 1 }], effects: [{ op: 'challenge-claim', card: 'connected-card', stake: 'trust' }, { op: 'gain', resource: 'clarity', amount: 2 }] }
    ],
    resolution: { win: 'Four distinct edge families are sourced and Clarity is at least eight.', loss: 'Evidence Debt reaches twelve or Trust reaches zero.' }
  },
  {
    id: 'ontology-war', title: 'Ontology War', players: [2, 5], minutes: [45, 100], mode: 'competitive model-building',
    objective: 'Build the most explanatory, revisable model of a shared anomaly without exhausting collective Trust.',
    resources: ['clarity', 'trust', 'evidence-debt'],
    setup: [
      { op: 'draw', count: 1, from: { classes: ['events', 'states'] }, to: 'shared-anomaly' },
      { op: 'draw', count: 1, from: { classes: ['hypotheses'] }, to: 'each-player-lens' },
      { op: 'draw', count: 4, from: {}, to: 'each-player-hand' }
    ],
    turn: [
      { id: 'explain', label: 'Explain', cost: [], effects: [{ op: 'place', card: 'hand-card', zone: 'model' }, { op: 'connect', from: 'hand-card', to: 'shared-anomaly', edgeType: 'interpreted-through' }] },
      { id: 'cross-examine', label: 'Cross-examine', cost: [{ resource: 'trust', amount: 1 }], effects: [{ op: 'challenge-claim', card: 'opponent-model', stake: 'clarity' }] },
      { id: 'revise', label: 'Revise', cost: [], effects: [{ op: 'swap', first: 'model-card', second: 'hand-card' }, { op: 'gain', resource: 'trust', amount: 1 }] },
      { id: 'falsify', label: 'Name Disconfirmation', cost: [], effects: [{ op: 'cite-source', card: 'lens', claim: 'disconfirmation-condition' }, { op: 'retreat-track', track: 'evidence-debt', amount: 2 }] }
    ],
    resolution: { win: 'After six rounds, score sourced edges + declared disconfirmations + revisions; ties favor higher Trust.', loss: 'All players lose if collective Trust reaches zero.' }
  },
  {
    id: 'first-contact', title: 'First Contact', players: [2, 8], minutes: [40, 90], mode: 'cooperative translation',
    objective: 'Establish reciprocal communication before Fear or Projection overwhelms the encounter.',
    resources: ['trust', 'clarity', 'stability'],
    setup: [
      { op: 'draw', count: 2, from: { classes: ['beings'] }, to: 'delegations' },
      { op: 'draw', count: 1, from: { classes: ['worlds'] }, to: 'contact-site' },
      { op: 'advance-track', track: 'trust', amount: 3 },
      { op: 'advance-track', track: 'stability', amount: 4 }
    ],
    turn: [
      { id: 'signal', label: 'Send Signal', cost: [], effects: [{ op: 'draw', count: 1, from: { classes: ['artifacts', 'technologies'] }, to: 'medium' }] },
      { id: 'translate', label: 'Translate', cost: [{ resource: 'clarity', amount: 1 }], effects: [{ op: 'connect', from: 'medium', to: 'other-delegation', edgeType: 'enables' }, { op: 'gain', resource: 'trust', amount: 1 }] },
      { id: 'set-boundary', label: 'Set Boundary', cost: [], effects: [{ op: 'lock', target: 'one-contact-route' }, { op: 'gain', resource: 'stability', amount: 1 }] },
      { id: 'exchange', label: 'Exchange', cost: [{ resource: 'trust', amount: 2 }], effects: [{ op: 'reveal', target: 'delegation-secret', count: 1 }, { op: 'advance-track', track: 'communication', amount: 2 }] }
    ],
    resolution: { win: 'Communication reaches ten with Trust and Stability above zero.', loss: 'Projection reaches ten or either delegation loses all Trust.' }
  },
  {
    id: 'disclosure', title: 'Disclosure', players: [3, 9], minutes: [50, 110], mode: 'asymmetric negotiation',
    objective: 'Reconcile public truth, witness safety, institutional continuity, and accountability.',
    resources: ['trust', 'stability', 'secrecy', 'agency'],
    setup: [
      { op: 'draw', count: 1, from: { classes: ['programs'] }, to: 'hidden-program' },
      { op: 'draw', count: 3, from: { classes: ['artifacts'] }, to: 'evidence-vault' },
      { op: 'tag', target: 'players', tag: 'role:witness|keeper|public|mediator' }
    ],
    turn: [
      { id: 'release', label: 'Release Evidence', cost: [{ resource: 'secrecy', amount: 2 }], effects: [{ op: 'reveal', target: 'evidence-vault', count: 1 }, { op: 'gain', resource: 'agency', amount: 1 }, { op: 'retreat-track', track: 'stability', amount: 1 }] },
      { id: 'verify', label: 'Verify', cost: [{ resource: 'agency', amount: 1 }], effects: [{ op: 'cite-source', card: 'revealed-evidence', claim: 'public-claim' }, { op: 'gain', resource: 'trust', amount: 2 }] },
      { id: 'protect', label: 'Protect a Witness', cost: [{ resource: 'secrecy', amount: 1 }], effects: [{ op: 'lock', target: 'witness-identity' }, { op: 'gain', resource: 'stability', amount: 1 }] },
      { id: 'acknowledge', label: 'Official Acknowledgment', cost: [{ resource: 'trust', amount: 3 }], effects: [{ op: 'advance-track', track: 'disclosure', amount: 3 }, { op: 'retreat-track', track: 'secrecy', amount: 2 }] }
    ],
    resolution: { win: 'Disclosure reaches ten, at least two evidence cards are sourced, and no role loses all Agency.', loss: 'Stability reaches zero or the evidence vault is exhausted without verification.' }
  },
  {
    id: 'soft-disclosure', title: 'Soft Disclosure', players: [1, 6], minutes: [35, 80], mode: 'collaborative narrative design',
    objective: 'Create a fictional rehearsal that expands readiness without disguising speculation as fact.',
    resources: ['clarity', 'trust', 'stability'],
    setup: [
      { op: 'draw', count: 1, from: { classes: ['events'] }, to: 'hidden-truth' },
      { op: 'draw', count: 1, from: { classes: ['worlds'] }, to: 'storyworld' },
      { op: 'draw', count: 3, from: { classes: ['myths', 'beings', 'artifacts'] }, to: 'motif-hand' }
    ],
    turn: [
      { id: 'encode', label: 'Encode', cost: [], effects: [{ op: 'place', card: 'motif', zone: 'story' }, { op: 'connect', from: 'motif', to: 'hidden-truth', edgeType: 'fictionalizes' }] },
      { id: 'label', label: 'Label the Boundary', cost: [], effects: [{ op: 'tag', target: 'story-claim', tag: 'fiction|speculation|documented' }, { op: 'gain', resource: 'clarity', amount: 1 }] },
      { id: 'test-audience', label: 'Test Audience', cost: [{ resource: 'trust', amount: 1 }], effects: [{ op: 'reveal', target: 'audience-response', count: 1 }, { op: 'branch', condition: 'response=overload', then: [{ op: 'gain', resource: 'stability', amount: 1 }], otherwise: [{ op: 'advance-track', track: 'readiness', amount: 1 }] }] },
      { id: 'revise', label: 'Revise the Story', cost: [], effects: [{ op: 'swap', first: 'story-motif', second: 'motif-hand' }, { op: 'gain', resource: 'trust', amount: 1 }] }
    ],
    resolution: { win: 'Readiness reaches eight with at least three explicit evidence-state labels.', loss: 'Audience Stability reaches zero or all claims remain unlabeled.' }
  },
  {
    id: 'evidence-assembly', title: 'Evidence Assembly', players: [1, 5], minutes: [25, 70], mode: 'cooperative source reasoning',
    objective: 'Build an auditable claim graph that distinguishes document, inference, testimony, fiction, and synthesis.',
    resources: ['clarity', 'evidence-debt'],
    setup: [
      { op: 'draw', count: 1, from: {}, to: 'claim' },
      { op: 'draw', count: 6, from: {}, to: 'evidence-hand' }
    ],
    turn: [
      { id: 'classify', label: 'Classify Evidence', cost: [], effects: [{ op: 'tag', target: 'evidence-card', tag: 'documented|active-research|unverified|fictional|symbolic|synthesis' }] },
      { id: 'support', label: 'Support', cost: [], effects: [{ op: 'connect', from: 'evidence-card', to: 'claim', edgeType: 'contextualizes' }, { op: 'cite-source', card: 'evidence-card', claim: 'claim' }] },
      { id: 'challenge', label: 'Challenge', cost: [], effects: [{ op: 'challenge-claim', card: 'claim', stake: 'clarity' }, { op: 'gain', resource: 'clarity', amount: 1 }] },
      { id: 'narrow', label: 'Narrow the Claim', cost: [], effects: [{ op: 'transform', target: 'claim', into: 'bounded-claim' }, { op: 'retreat-track', track: 'evidence-debt', amount: 2 }] }
    ],
    resolution: { win: 'The claim has three evidence states, two source tiers, and an explicit disconfirmation condition.', loss: 'Evidence Debt reaches ten.' }
  },
  {
    id: 'timeline-repair', title: 'Timeline Repair', players: [1, 6], minutes: [40, 90], mode: 'cooperative causality puzzle',
    objective: 'Restore a coherent sequence without erasing paradox, loss, or consequence.',
    resources: ['stability', 'agency', 'clarity'],
    setup: [
      { op: 'draw', count: 5, from: { classes: ['events'] }, to: 'timeline' },
      { op: 'swap', first: 'timeline[random]', second: 'timeline[random]' },
      { op: 'draw', count: 2, from: { classes: ['states', 'artifacts'] }, to: 'anomalies' }
    ],
    turn: [
      { id: 'trace', label: 'Trace Cause', cost: [], effects: [{ op: 'connect', from: 'event-a', to: 'event-b', edgeType: 'precipitates' }, { op: 'gain', resource: 'clarity', amount: 1 }] },
      { id: 'branch', label: 'Declare Branch', cost: [{ resource: 'stability', amount: 1 }], effects: [{ op: 'branch', condition: 'paradox', then: [{ op: 'tag', target: 'timeline', tag: 'branch' }], otherwise: [{ op: 'advance-track', track: 'continuity', amount: 1 }] }] },
      { id: 'anchor', label: 'Place Anchor', cost: [{ resource: 'agency', amount: 1 }], effects: [{ op: 'lock', target: 'event-position' }, { op: 'gain', resource: 'stability', amount: 1 }] },
      { id: 'integrate', label: 'Integrate Anomaly', cost: [{ resource: 'clarity', amount: 2 }], effects: [{ op: 'place', card: 'anomaly', zone: 'timeline' }, { op: 'advance-track', track: 'continuity', amount: 2 }] }
    ],
    resolution: { win: 'Continuity reaches ten and every anomaly has a typed edge.', loss: 'Stability reaches zero or three unresolved paradox tags accumulate.' }
  },
  {
    id: 'threshold-governance', title: 'Governance at the Threshold', players: [3, 8], minutes: [55, 120], mode: 'negotiated institution design',
    objective: 'Create legitimate rules for a transformative capability before one faction captures it.',
    resources: ['trust', 'agency', 'stability', 'secrecy'],
    setup: [
      { op: 'draw', count: 1, from: { classes: ['technologies', 'artifacts'] }, to: 'breakthrough' },
      { op: 'draw', count: 4, from: { classes: ['factions'] }, to: 'stakeholders' },
      { op: 'draw', count: 2, from: { classes: ['systems', 'forces'] }, to: 'constraints' }
    ],
    turn: [
      { id: 'propose-rule', label: 'Propose Rule', cost: [{ resource: 'agency', amount: 1 }], effects: [{ op: 'place', card: 'rule', zone: 'charter' }, { op: 'vote', question: 'adopt-rule', threshold: 'majority' }] },
      { id: 'minority-protection', label: 'Add Minority Protection', cost: [{ resource: 'trust', amount: 1 }], effects: [{ op: 'tag', target: 'charter-rule', tag: 'minority-protection' }, { op: 'gain', resource: 'stability', amount: 1 }] },
      { id: 'audit', label: 'Audit Power', cost: [], effects: [{ op: 'reveal', target: 'stakeholder-interest', count: 1 }, { op: 'cite-source', card: 'constraint', claim: 'risk' }] },
      { id: 'sunset', label: 'Add Sunset Clause', cost: [], effects: [{ op: 'tag', target: 'charter-rule', tag: 'sunset' }, { op: 'gain', resource: 'trust', amount: 1 }] }
    ],
    resolution: { win: 'A five-rule charter passes with audit, minority protection, review, and sunset provisions.', loss: 'One stakeholder controls three rules or collective Trust reaches zero.' }
  },
  {
    id: 'worldseed-engine', title: 'Worldseed Engine', players: [1, 7], minutes: [30, 85], mode: 'collaborative world generation',
    objective: 'Create a coherent world whose environment, infrastructure, institutions, conflict, and future are connected.',
    resources: ['clarity', 'agency', 'stability'],
    setup: [
      { op: 'draw', count: 1, from: { classes: ['worlds'] }, to: 'world' },
      { op: 'draw', count: 7, from: {}, to: 'world-hand' }
    ],
    turn: [
      { id: 'install', label: 'Install Component', cost: [], effects: [{ op: 'place', card: 'hand-card', zone: 'world' }, { op: 'connect', from: 'hand-card', to: 'world', edgeType: 'declared' }] },
      { id: 'complicate', label: 'Reveal Consequence', cost: [], effects: [{ op: 'draw', count: 1, from: { classes: ['forces', 'events', 'states'] }, to: 'consequence' }, { op: 'retreat-track', track: 'stability', amount: 1 }] },
      { id: 'institutionalize', label: 'Institutionalize', cost: [{ resource: 'agency', amount: 1 }], effects: [{ op: 'connect', from: 'faction', to: 'program', edgeType: 'operates' }, { op: 'gain', resource: 'clarity', amount: 1 }] },
      { id: 'future', label: 'Declare Unrealized Future', cost: [{ resource: 'clarity', amount: 2 }], effects: [{ op: 'draw', count: 1, from: { classes: ['myths', 'cosmologies', 'events'] }, to: 'future' }, { op: 'advance-track', track: 'coherence', amount: 2 }] }
    ],
    resolution: { win: 'The world reaches ten Coherence with at least five edge families and one unresolved future question.', loss: 'Stability reaches zero or one card carries more than half the connections.' }
  },
  {
    id: 'secret-space-program', title: 'Secret Space Program', players: [2, 7], minutes: [55, 130], mode: 'network construction and audit',
    objective: 'Build an internally coherent off-world program while auditors test whether its infrastructure, institutions, evidence, and secrecy can coexist.',
    resources: ['secrecy', 'stability', 'agency', 'evidence-debt'],
    setup: [
      { op: 'draw', count: 2, from: { classes: ['worlds'] }, to: 'sites' },
      { op: 'draw', count: 2, from: { classes: ['factions'] }, to: 'sponsors' },
      { op: 'draw', count: 4, from: { classes: ['technologies', 'artifacts'] }, to: 'capabilities' },
      { op: 'tag', target: 'one-player', tag: 'role:auditor' },
      { op: 'advance-track', track: 'secrecy', amount: 6 }
    ],
    turn: [
      { id: 'commission', label: 'Commission Capability', cost: [{ resource: 'secrecy', amount: 1 }], effects: [{ op: 'place', card: 'capability', zone: 'program-network' }, { op: 'connect', from: 'sponsor', to: 'capability', edgeType: 'operates' }, { op: 'advance-track', track: 'readiness', amount: 1 }] },
      { id: 'establish-base', label: 'Establish Base', cost: [{ resource: 'agency', amount: 1 }], effects: [{ op: 'place', card: 'site', zone: 'program-network' }, { op: 'connect', from: 'site', to: 'capability', edgeType: 'provides-setting-for' }, { op: 'gain', resource: 'stability', amount: 1 }] },
      { id: 'compartment', label: 'Compartment', cost: [], effects: [{ op: 'lock', target: 'one-network-edge' }, { op: 'gain', resource: 'secrecy', amount: 2 }, { op: 'advance-track', track: 'evidence-debt', amount: 1 }] },
      { id: 'audit-trace', label: 'Audit Trace', cost: [], effects: [{ op: 'reveal', target: 'locked-network-edge', count: 1 }, { op: 'challenge-claim', card: 'revealed-component', stake: 'evidence-debt' }] },
      { id: 'document', label: 'Document Infrastructure', cost: [{ resource: 'secrecy', amount: 1 }], effects: [{ op: 'cite-source', card: 'network-component', claim: 'operational-necessity' }, { op: 'retreat-track', track: 'evidence-debt', amount: 2 }] }
    ],
    resolution: { win: 'Builders win with two sites, two sponsors, four capabilities, and Evidence Debt below six; the auditor wins by exposing three unsupported dependencies.', loss: 'All sides lose if the network reaches maximum Evidence Debt without an auditable causal chain.' }
  },
  {
    id: 'ascension', title: 'Ascension', players: [2, 8], minutes: [50, 120], mode: 'cooperative civilizational transition',
    objective: 'Move a civilization toward greater agency and interconnection without triggering collapse, authoritarian capture, ecological failure, runaway AI, or war.',
    resources: ['stability', 'agency', 'trust', 'clarity'],
    setup: [
      { op: 'draw', count: 1, from: { classes: ['worlds'] }, to: 'civilization' },
      { op: 'draw', count: 3, from: { classes: ['systems', 'factions'] }, to: 'institutions' },
      { op: 'draw', count: 3, from: { classes: ['forces', 'events'] }, to: 'crisis-row' },
      { op: 'advance-track', track: 'stability', amount: 6 },
      { op: 'advance-track', track: 'agency', amount: 4 },
      { op: 'advance-track', track: 'trust', amount: 4 }
    ],
    turn: [
      { id: 'develop', label: 'Develop Capacity', cost: [{ resource: 'stability', amount: 1 }], effects: [{ op: 'draw', count: 1, from: { classes: ['technologies', 'programs'] }, to: 'capacity' }, { op: 'advance-track', track: 'development', amount: 1 }] },
      { id: 'distribute', label: 'Distribute Agency', cost: [{ resource: 'trust', amount: 1 }], effects: [{ op: 'connect', from: 'institution', to: 'capacity', edgeType: 'governs' }, { op: 'gain', resource: 'agency', amount: 2 }] },
      { id: 'integrate-shadow', label: 'Integrate Shadow', cost: [{ resource: 'clarity', amount: 1 }], effects: [{ op: 'reveal', target: 'crisis-row', count: 1 }, { op: 'transform', target: 'crisis', into: 'integration-task' }, { op: 'gain', resource: 'stability', amount: 1 }] },
      { id: 'set-limits', label: 'Set Limits', cost: [{ resource: 'agency', amount: 1 }], effects: [{ op: 'lock', target: 'unsafe-capability-route' }, { op: 'gain', resource: 'trust', amount: 1 }] },
      { id: 'cross-threshold', label: 'Cross Threshold', cost: [{ resource: 'stability', amount: 2 }, { resource: 'trust', amount: 2 }], effects: [{ op: 'advance-track', track: 'development', amount: 2 }, { op: 'draw', count: 1, from: { classes: ['states', 'cosmologies'] }, to: 'new-condition' }] }
    ],
    resolution: { win: 'Development reaches twelve while Stability, Agency, and Trust each remain at four or more.', loss: 'Any collapse track reaches eight or any shared resource reaches zero.' }
  },
  {
    id: 'prison-planet', title: 'Prison Planet', players: [1, 7], minutes: [45, 105], mode: 'cooperative diagnosis and liberation',
    objective: 'Determine what kind of enclosure exists, who benefits, how it reproduces itself, and which escape preserves reality contact and collective agency.',
    resources: ['clarity', 'agency', 'trust', 'stability', 'evidence-debt'],
    setup: [
      { op: 'draw', count: 1, from: { classes: ['cosmologies', 'worlds'] }, to: 'enclosure' },
      { op: 'draw', count: 2, from: { classes: ['systems'] }, to: 'walls' },
      { op: 'draw', count: 1, from: { classes: ['factions', 'beings'] }, to: 'beneficiary' },
      { op: 'draw', count: 3, from: { classes: ['forces', 'states', 'technologies'] }, to: 'mechanisms' }
    ],
    turn: [
      { id: 'name-wall', label: 'Name the Wall', cost: [], effects: [{ op: 'tag', target: 'wall', tag: 'physical|political|psychological|spiritual|symbolic' }, { op: 'gain', resource: 'clarity', amount: 1 }] },
      { id: 'trace-benefit', label: 'Trace the Benefit', cost: [{ resource: 'trust', amount: 1 }], effects: [{ op: 'connect', from: 'beneficiary', to: 'mechanism', edgeType: 'harvests' }, { op: 'advance-track', track: 'evidence-debt', amount: 1 }] },
      { id: 'ground-claim', label: 'Ground the Claim', cost: [], effects: [{ op: 'cite-source', card: 'mechanism', claim: 'enclosure-claim' }, { op: 'retreat-track', track: 'evidence-debt', amount: 2 }, { op: 'gain', resource: 'stability', amount: 1 }] },
      { id: 'test-exit', label: 'Test an Exit', cost: [{ resource: 'agency', amount: 1 }], effects: [{ op: 'unlock', target: 'exit-route' }, { op: 'branch', condition: 'exit-increases-agency', then: [{ op: 'advance-track', track: 'liberation', amount: 2 }], otherwise: [{ op: 'advance-track', track: 'evidence-debt', amount: 2 }] }] },
      { id: 'collective-escape', label: 'Collective Escape', cost: [{ resource: 'trust', amount: 2 }, { resource: 'clarity', amount: 2 }], effects: [{ op: 'transform', target: 'wall', into: 'boundary-with-appeal' }, { op: 'advance-track', track: 'liberation', amount: 3 }] }
    ],
    resolution: { win: 'Liberation reaches ten, at least three wall types have been tested, and Evidence Debt is below five.', loss: 'Evidence Debt reaches twelve, Stability reaches zero, or escape becomes available only to one player.' }
  }
].map(finalizeGame);

export const generatorSpecs = {
  story: {
    id: 'story-engine', title: 'Story Engine',
    slots: [
      ['world', 'The World', ['worlds', 'cosmologies'], 'The environment whose current order can no longer hold.'],
      ['protagonist', 'The Protagonist', ['beings'], 'The role forced to act before understanding the whole.'],
      ['inheritance', 'The Inheritance', ['artifacts', 'myths', 'states'], 'The memory, object, or story already shaping the choice.'],
      ['institution', 'The Institution', ['factions', 'systems'], 'The collective actor that stabilizes or exploits the current order.'],
      ['capability', 'The Capability', ['technologies'], 'The repeatable power that makes the threshold possible.'],
      ['operation', 'The Operation', ['programs'], 'The deliberate project moving beneath the visible conflict.'],
      ['rupture', 'The Rupture', ['events'], 'The before-and-after moment that makes neutrality impossible.'],
      ['pressure', 'The Pressure', ['forces'], 'The recurring dynamic that turns complication into moral choice.'],
      ['lens', 'The Competing Explanation', ['hypotheses'], 'The model that may explain the rupture—or conceal a more useful question.'],
      ['becoming', 'The Becoming', ['states'], 'The transformation that costs the protagonist an old identity.']
    ]
  },
  character: {
    id: 'character-engine', title: 'Character Engine',
    slots: [
      ['origin', 'Origin', ['worlds', 'myths'], 'The place or founding story from which the character departs.'],
      ['being', 'Being / Role', ['beings'], 'The form of agency others recognize.'],
      ['lineage', 'Lineage', ['myths', 'beings', 'states'], 'The inherited memory, ancestry, or identity pattern.'],
      ['faction', 'Faction', ['factions'], 'The collective that provides belonging and obligation.'],
      ['technology', 'Technology', ['technologies'], 'The capacity that changes what the character can perceive or do.'],
      ['secret', 'Secret', ['programs', 'systems', 'artifacts'], 'The knowledge that cannot yet be integrated safely.'],
      ['mission', 'Mission', ['programs', 'events'], 'The explicit objective that turns identity into action.'],
      ['shadow', 'Shadow', ['forces', 'states'], 'The recurring distortion that could capture the mission.']
    ]
  },
  world: {
    id: 'world-engine', title: 'World Engine',
    slots: [
      ['environment', 'Environment', ['worlds', 'cosmologies'], 'The physical and ontological ground.'],
      ['civilization-level', 'Civilization Level', ['technologies', 'systems'], 'The scale of energy, coordination, and infrastructure available.'],
      ['scarcity', 'Scarcity', ['forces', 'artifacts'], 'The resource or capacity around which the world must organize.'],
      ['political-structure', 'Political Structure', ['factions', 'systems'], 'How authority, membership, appeal, and coercion are arranged.'],
      ['dominant-technology', 'Dominant Technology', ['technologies'], 'The repeatable capability that shapes everyday possibility.'],
      ['consciousness-model', 'Dominant Consciousness Model', ['states', 'cosmologies', 'hypotheses'], 'What the world believes a mind or self to be.'],
      ['contact-status', 'Contact Status', ['events', 'programs', 'beings'], 'How the world currently relates to other intelligences.'],
      ['hidden-history', 'Hidden History', ['myths', 'artifacts', 'programs'], 'The buried past whose return would alter legitimacy.'],
      ['existential-threat', 'Existential Threat', ['events', 'forces', 'systems'], 'The pressure capable of ending or transforming the world.']
    ]
  },
  faction: {
    id: 'faction-engine', title: 'Faction Engine',
    slots: [
      ['identity', 'Identity', ['factions'], 'The collective form recognized by members and rivals.'],
      ['goal', 'Goal', ['events', 'myths', 'forces'], 'The future condition the faction is organized to produce.'],
      ['governance', 'Governance', ['systems'], 'How authority, membership, dissent, and appeal operate internally.'],
      ['technology', 'Technology', ['technologies'], 'The capability on which its strategy depends.'],
      ['secret', 'Secret', ['programs', 'artifacts'], 'The hidden treaty, history, dependency, or compromise.'],
      ['shadow', 'Shadow', ['forces', 'states'], 'The unresolved distortion that could turn the goal against itself.']
    ]
  },
  quest: {
    id: 'quest-engine', title: 'Quest Engine',
    slots: [
      ['calling', 'Calling', ['events', 'myths', 'forces'], 'The disruption or promise that makes remaining unchanged impossible.'],
      ['seeker', 'Seeker', ['beings'], 'The agent who can accept, refuse, or redefine the calling.'],
      ['world', 'Quest World', ['worlds', 'cosmologies'], 'The environment whose rules shape every available path.'],
      ['threshold', 'Threshold', ['artifacts', 'programs', 'events'], 'The gate, test, or commitment that turns intention into a journey.'],
      ['ally', 'Ally or Guide', ['beings', 'factions'], 'The other intelligence whose help brings an obligation or changed perspective.'],
      ['ordeal', 'Ordeal', ['forces', 'systems', 'states'], 'The recurring pressure that cannot be solved by the seeker’s existing method.'],
      ['gift', 'Gift', ['artifacts', 'technologies', 'states'], 'The capability, memory, or transformation carried out of the ordeal.'],
      ['return', 'Return', ['events', 'myths', 'programs'], 'The act that makes the quest consequential for a wider community.']
    ]
  },
  cosmology: {
    id: 'cosmology-engine', title: 'Cosmology Engine',
    slots: [
      ['origin', 'Origin Condition', ['cosmologies', 'myths', 'hypotheses'], 'The account of why anything exists and what remains unresolved in that account.'],
      ['substrate', 'Substrate of Reality', ['forces', 'states', 'technologies'], 'What reality is made from or through at its most consequential scale.'],
      ['inhabitants', 'Inhabitants', ['beings', 'factions'], 'The forms of intelligence able to perceive or change this cosmos.'],
      ['world-order', 'World Order', ['worlds', 'systems'], 'The durable arrangement that turns metaphysics into lived conditions.'],
      ['law', 'Cosmic Law', ['systems', 'forces', 'hypotheses'], 'The regularity, constraint, or interpretation that organizes possibility.'],
      ['memory', 'Cosmic Memory', ['artifacts', 'states', 'myths'], 'How the cosmos retains identity, history, debt, or recurrence.'],
      ['rupture', 'Cosmic Rupture', ['events', 'programs'], 'The intervention that exposes limits in the reigning order.'],
      ['horizon', 'Ultimate Horizon', ['cosmologies', 'events', 'states'], 'The future, return, liberation, or ending toward which the cosmos tends.']
    ]
  }
};

function selectFeatureGroundedCandidate(cards, source, predicate = () => true, excluded = new Set()) {
  return cards
    .filter((candidate) => candidate.id !== source.id && !excluded.has(candidate.id) && predicate(candidate))
    .map((candidate) => ({ candidate, score: featureScore(sharedFeatures(source, candidate)) }))
    .sort((left, right) => right.score - left.score || left.candidate.id.localeCompare(right.candidate.id))[0]?.candidate;
}

function buildLineages(addEdge, bySlug, missing) {
  const results = [];
  for (const spec of lineageSpecs) {
    const resolved = spec.path.map((name) => bySlug.get(slug(name))).filter(Boolean);
    const missingNames = spec.path.filter((name) => !bySlug.has(slug(name)));
    if (missingNames.length) missing.push(...missingNames.map((name) => `${spec.id}:${name}`));
    const lineageEdges = [];
    for (let index = 0; index < resolved.length - 1; index += 1) {
      const from = resolved[index];
      const to = resolved[index + 1];
      const edge = addEdge({
        typeId: 'develops-into', source: from.id, target: to.id,
        rationale: `Within “${spec.title},” ${from.name} marks the threshold immediately before ${to.name}; this is an editorial pathway, not a claim of inevitable causation.`,
        provenanceKind: 'original-synthesis', sourceIds: ['original-synthesis'], confidence: 'editorial',
        derivation: { kind: 'lineage', lineageId: spec.id, step: index },
        reviewStatus: 'reviewed'
      });
      if (edge) lineageEdges.push(edge.id);
    }
    results.push({
      id: spec.id,
      title: spec.title,
      thesis: spec.thesis,
      inquiryDomainId: spec.domain,
      cardIds: resolved.map((card) => card.id),
      edgeIds: lineageEdges,
      missingCardNames: missingNames,
      reading: {
        forward: 'Read as an unfolding sequence in which each threshold creates the problem addressed by the next.',
        reverse: 'Read as an integration path that traces a present condition back toward its enabling assumptions.',
        diagnostic: 'Stop where the sequence first feels implausible; that seam names the contested transition.'
      }
    });
  }
  return results;
}

function graphDiagnostics(cards, edges) {
  const nodeState = Object.fromEntries(cards.map((card) => [card.id, {
    neighbors: new Set(), families: new Set(), provenanceKinds: new Set(), hasCrossDomain: false, edgeIds: [],
    substantiveNeighbors: new Set(), substantiveEdgeIds: [], reviewedSubstantiveEdgeIds: []
  }]));
  const byId = new Map(cards.map((card) => [card.id, card]));
  for (const edge of edges) {
    const type = edgeTypeById.get(edge.type);
    const source = byId.get(edge.source);
    const target = byId.get(edge.target);
    if (!source || !target || !type) continue;
    for (const [self, other] of [[source, target], [target, source]]) {
      const state = nodeState[self.id];
      state.neighbors.add(other.id);
      state.families.add(type.family);
      state.provenanceKinds.add(edge.provenance.kind);
      state.edgeIds.push(edge.id);
      if (self.domainIds[0] !== other.domainIds[0]) state.hasCrossDomain = true;
      if (edge.substantive) {
        state.substantiveNeighbors.add(other.id);
        state.substantiveEdgeIds.push(edge.id);
        if (edge.reviewStatus === 'reviewed') state.reviewedSubstantiveEdgeIds.push(edge.id);
      }
    }
  }
  const nodes = Object.fromEntries(Object.entries(nodeState).map(([id, state]) => [id, {
    degree: state.neighbors.size,
    neighborIds: [...state.neighbors].sort(),
    edgeFamilies: [...state.families].sort(),
    provenanceKinds: [...state.provenanceKinds].sort(),
    hasCrossDomain: state.hasCrossDomain,
    edgeIds: [...new Set(state.edgeIds)].sort(),
    substantiveDegree: state.substantiveNeighbors.size,
    substantiveEdgeIds: [...new Set(state.substantiveEdgeIds)].sort(),
    reviewedSubstantiveEdgeIds: [...new Set(state.reviewedSubstantiveEdgeIds)].sort()
  }]));
  const values = Object.values(nodes);
  const substantiveEdges = edges.filter((edge) => edge.substantive);
  const reviewedSubstantiveEdges = substantiveEdges.filter((edge) => edge.reviewStatus === 'reviewed');
  return {
    nodeCount: cards.length,
    edgeCount: edges.length,
    substantiveEdgeCount: substantiveEdges.length,
    reviewedSubstantiveEdgeCount: reviewedSubstantiveEdges.length,
    edgesByDerivation: Object.fromEntries([...new Set(edges.map((edge) => edge.derivation?.kind ?? 'missing'))]
      .sort()
      .map((kind) => [kind, edges.filter((edge) => (edge.derivation?.kind ?? 'missing') === kind).length])),
    minimumDegree: values.length ? Math.min(...values.map((state) => state.degree)) : 0,
    maximumDegree: values.length ? Math.max(...values.map((state) => state.degree)) : 0,
    minimumEdgeFamilies: values.length ? Math.min(...values.map((state) => state.edgeFamilies.length)) : 0,
    orphanIds: Object.entries(nodes).filter(([, state]) => state.degree === 0).map(([id]) => id),
    belowFourNeighborIds: Object.entries(nodes).filter(([, state]) => state.degree < 4).map(([id]) => id),
    belowThreeFamilyIds: Object.entries(nodes).filter(([, state]) => state.edgeFamilies.length < 3).map(([id]) => id),
    missingSourcedIds: Object.entries(nodes).filter(([, state]) => !state.provenanceKinds.includes('sourced')).map(([id]) => id),
    missingOriginalSynthesisIds: Object.entries(nodes).filter(([, state]) => !state.provenanceKinds.includes('original-synthesis')).map(([id]) => id),
    missingCrossDomainIds: Object.entries(nodes).filter(([, state]) => !state.hasCrossDomain).map(([id]) => id),
    missingReviewedSubstantiveIds: Object.entries(nodes).filter(([, state]) => !state.reviewedSubstantiveEdgeIds.length).map(([id]) => id),
    reviewedSubstantiveCoverage: values.length
      ? values.filter((state) => state.reviewedSubstantiveEdgeIds.length).length / values.length
      : 0,
    nodes
  };
}

const literalRuleTypes = new Set([
  'same-class-as',
  'shares-inquiry-domain-with',
  'assessed-through-shared-source-lens'
]);

const forbiddenAssertivePairs = [
  ['Akashic Records', 'Operation Highjump'],
  ['Antikythera Mechanism', 'Waived SAP'],
  ['Crop Circle', 'Philadelphia Experiment'],
  ['Pyramid', 'MKULTRA'],
  ['Life Review Mirror', 'False Disclosure']
];

const requiredAnchorGroups = [
  { anchor: 'Loosh', targets: [['Energy Harvest'], ['Prison Planet'], ['Archon'], ['The Matrix'], ['Attention Economy'], ['Fear'], ['Ascension']] },
  {
    anchor: '15-Minute City',
    targets: [
      { alternatives: ['Walkability'], typeIds: ['contains'], interpretation: 'documented-planning' },
      { alternatives: ['Neighborhood Access', 'Localism'], typeIds: ['contains'], interpretation: 'documented-planning' },
      { alternatives: ['Local Services', 'Distributed Services', 'Distributed Network'], typeIds: ['contains'], interpretation: 'documented-planning' },
      { alternatives: ['Smart City'], typeIds: ['resembles'], interpretation: 'bounded-comparison' },
      { alternatives: ['Geofence'], typeIds: ['politicized-through'], interpretation: 'control-myth' },
      { alternatives: ['Digital ID'], typeIds: ['politicized-through'], interpretation: 'control-myth' },
      { alternatives: ['Panopticon'], typeIds: ['politicized-through'], interpretation: 'control-myth' }
    ]
  },
  { anchor: 'Mythic Past', targets: [['Galactic Empire'], ['Rebel Alliance'], ['Jedi Order']] },
  { anchor: 'Aspirational Future', targets: [['Starfleet'], ['United Federation of Planets'], ['Prime Directive']] },
  { anchor: 'Dune', targets: [['Spice'], ['Arrakis'], ['Bene Gesserit'], ['Spacing Guild']] }
];

function edgeAssertiveness(typeId) {
  if (literalRuleTypes.has(typeId)) return 'literal-classification';
  if (typeId === 'inquiry-juxtaposition') return 'nonassertive';
  return 'assertive';
}

function semanticGraphFailures(cards, edges) {
  const byId = new Map(cards.map((card) => [card.id, card]));
  const byName = new Map(cards.map((card) => [card.name, card]));
  const failures = [];
  const pairKey = (first, second) => [first, second].sort().join('::');
  const forbiddenKeys = new Set(forbiddenAssertivePairs.map(([first, second]) => pairKey(first, second)));

  for (const edge of edges) {
    const source = byId.get(edge.source);
    const target = byId.get(edge.target);
    const features = source && target ? sharedFeatures(source, target) : null;
    if (!['sourced', 'original-synthesis'].includes(edge.provenance?.kind) || !edge.provenance?.sourceIds?.length) {
      failures.push(`${edge.id}: missing typed provenance`);
    }
    if (edge.provenance?.kind === 'original-synthesis'
      && (edge.provenance.sourceIds.length !== 1 || edge.provenance.sourceIds[0] !== 'original-synthesis')) {
      failures.push(`${edge.id}: original-synthesis provenance must cite only original-synthesis`);
    }
    if (!edge.derivation?.kind || !edge.reviewStatus || !edge.assertiveness) failures.push(`${edge.id}: missing derivation, reviewStatus, or assertiveness`);
    if (edge.assertiveness === 'assertive'
      && (!['curated', 'lineage'].includes(edge.derivation?.kind) || edge.reviewStatus !== 'reviewed')) {
      failures.push(`${edge.id}: assertive edge is not reviewed curated/lineage work`);
    }
    if (edge.derivation?.kind === 'rule' && !literalRuleTypes.has(edge.type) && edge.type !== 'inquiry-juxtaposition') {
      failures.push(`${edge.id}: rule-derived edge uses nonliteral type ${edge.type}`);
    }
    if (edge.type === 'same-class-as' && source?.classId !== target?.classId) failures.push(`${edge.id}: same-class rule lacks a shared class`);
    if (edge.type === 'shares-inquiry-domain-with' && !features?.domainIds.length) failures.push(`${edge.id}: shared-domain rule lacks an explicit shared domain`);
    if (edge.type === 'assessed-through-shared-source-lens' && !features?.sourceIds.length) failures.push(`${edge.id}: shared-source rule lacks an explicit shared citation`);
    if (edge.type === 'inquiry-juxtaposition') {
      const featureFamilies = [features?.domainIds.length, features?.polarityIds.length, features?.materialModeIds.length].filter(Boolean).length;
      if (featureFamilies < 2) failures.push(`${edge.id}: inquiry juxtaposition has fewer than two explicit shared feature families`);
      if (edge.confidence > 0.45) failures.push(`${edge.id}: inquiry juxtaposition confidence exceeds 0.45`);
      if (edge.substantive) failures.push(`${edge.id}: inquiry juxtaposition cannot be substantive`);
    }
    if (/cross-class systems model|productive cross-domain contrast|share a recurring symbolic structure/i.test(edge.rationale ?? '')) {
      failures.push(`${edge.id}: prohibited boilerplate rationale`);
    }
    if (source && target && forbiddenKeys.has(pairKey(source.name, target.name)) && edge.assertiveness === 'assertive') {
      failures.push(`${edge.id}: forbidden unreviewed semantic pair ${source.name} / ${target.name}`);
    }
  }

  for (const group of requiredAnchorGroups) {
    const anchor = byName.get(group.anchor);
    if (!anchor) {
      failures.push(`anchor card missing: ${group.anchor}`);
      continue;
    }
    for (const targetDeclaration of group.targets) {
      const alternatives = Array.isArray(targetDeclaration) ? targetDeclaration : targetDeclaration.alternatives;
      const target = alternatives.map((name) => byName.get(name)).find(Boolean);
      if (!target) {
        failures.push(`${group.anchor}: required anchor target missing (${alternatives.join(' or ')})`);
        continue;
      }
      const match = edges.find((edge) =>
        ((edge.source === anchor.id && edge.target === target.id) || (edge.source === target.id && edge.target === anchor.id))
        && edge.substantive
        && edge.reviewStatus === 'reviewed'
        && (Array.isArray(targetDeclaration) || !targetDeclaration.typeIds?.length || targetDeclaration.typeIds.includes(edge.type))
      );
      if (!match) {
        const typeNote = Array.isArray(targetDeclaration) ? '' : ` via ${targetDeclaration.typeIds.join(' or ')}`;
        failures.push(`${group.anchor}: missing reviewed substantive anchor to ${target.name}${typeNote}`);
      }
    }
  }
  return failures;
}

export function buildRelationships(inputCards) {
  const cards = normalizeCards(inputCards);
  const byId = new Map(cards.map((card) => [card.id, card]));
  const bySlug = new Map(cards.map((card) => [slug(card.name), card]));
  const edges = [];
  const edgeMap = new Map();
  const missingCuratedCards = [];

  const addEdge = ({
    typeId, source, target, rationale,
    provenanceKind = 'original-synthesis', sourceIds = [], confidence = 'editorial',
    derivation = { kind: 'unknown' }, reviewStatus = 'unreviewed', assertiveness = edgeAssertiveness(typeId)
  }) => {
    if (!source || !target || source === target || !byId.has(source) || !byId.has(target)) return undefined;
    const type = edgeTypeById.get(typeId);
    if (!type) throw new Error(`Unknown edge type “${typeId}”.`);
    const [from, to] = orderedPair(source, target, type.directionality);
    const id = edgeId(typeId, from, to);
    const sharedCitations = sharedSourceIds(byId.get(from), byId.get(to));
    const requestedCitations = [...new Set(sourceIds.filter((sourceId) => sourceId && sourceId !== 'original-synthesis'))];
    const cleanedSources = provenanceKind === 'sourced'
      ? sharedCitations.filter((sourceId) => requestedCitations.includes(sourceId))
      : ['original-synthesis'];
    if (provenanceKind === 'sourced' && !cleanedSources.length) {
      throw new Error(`${id} cannot be sourced: its provenance citations are not cited by both endpoint cards.`);
    }
    const existing = edgeMap.get(id);
    if (existing) {
      if (provenanceKind === 'sourced') {
        existing.provenance.sourceIds = existing.provenance.kind === 'sourced'
          ? [...new Set([...existing.provenance.sourceIds, ...cleanedSources])].sort()
          : cleanedSources;
        existing.provenance.kind = 'sourced';
        existing.provenance.basis = 'sourced';
        existing.provenance.note = 'Every cited source is registered on both endpoint cards; it bounds or contextualizes the relationship without automatically proving either card’s claims.';
        existing.basis = 'sourced';
      }
      return existing;
    }
    const edge = {
      id,
      type: typeId,
      relationship: typeId,
      family: type.family,
      directionality: type.directionality,
      source: from,
      target: to,
      label: type.label,
      rationale,
      basis: provenanceKind,
      confidence: typeof confidence === 'number' ? confidence : confidence === 'source-guided' ? 0.84 : 0.72,
      confidenceLabel: typeof confidence === 'string' ? confidence : confidence >= 0.85 ? 'high' : confidence >= 0.7 ? 'moderate' : 'low',
      derivation,
      reviewStatus,
      assertiveness,
      substantive: (derivation.kind === 'curated' || derivation.kind === 'lineage')
        && reviewStatus === 'reviewed'
        && !literalRuleTypes.has(typeId)
        && typeId !== 'inquiry-juxtaposition',
      provenance: {
        kind: provenanceKind,
        basis: provenanceKind,
        sourceIds: cleanedSources.sort(),
        note: provenanceKind === 'sourced'
          ? 'Every cited source is registered on both endpoint cards; it bounds or contextualizes the relationship without automatically proving either card’s claims.'
          : 'This relationship is an explicit Oracle editorial synthesis and is not attributed to an external authority.'
      }
    };
    edges.push(edge);
    edgeMap.set(id, edge);
    return edge;
  };

  for (const [sourceName, targetName, typeId, sourceIds, rationale] of curatedEdgeSpecs) {
    const source = bySlug.get(slug(sourceName));
    const target = bySlug.get(slug(targetName));
    if (!source || !target) {
      missingCuratedCards.push(...[!source ? sourceName : null, !target ? targetName : null].filter(Boolean));
      continue;
    }
    const sharedCitations = sharedSourceIds(source, target);
    const externalSources = sourceIds.filter((id) => id !== 'original-synthesis' && sharedCitations.includes(id));
    addEdge({
      typeId, source: source.id, target: target.id, rationale,
      provenanceKind: externalSources.length ? 'sourced' : 'original-synthesis',
      sourceIds: externalSources.length ? externalSources : ['original-synthesis'],
      confidence: externalSources.length ? 'source-guided' : 'editorial',
      derivation: { kind: 'curated', specificationId: `curated--${slug(sourceName)}--${typeId}--${slug(targetName)}` },
      reviewStatus: 'reviewed'
    });
  }

  const missingLineageCards = [];
  const lineages = buildLineages(addEdge, bySlug, missingLineageCards);

  const byClass = new Map();
  const byDomain = new Map();
  for (const card of cards) {
    if (!byClass.has(card.classId)) byClass.set(card.classId, []);
    byClass.get(card.classId).push(card);
    for (const domainId of card.domainIds) {
      if (!byDomain.has(domainId)) byDomain.set(domainId, []);
      byDomain.get(domainId).push(card);
    }
  }
  for (const values of byClass.values()) values.sort((a, b) => a.id.localeCompare(b.id));
  for (const values of byDomain.values()) values.sort((a, b) => a.id.localeCompare(b.id));

  // A same-class ring gives every card two stable taxonomic neighbors.
  for (const [classId, group] of byClass) {
    if (group.length < 2) continue;
    group.forEach((card, index) => {
      const target = group[(index + 1) % group.length];
      addEdge({
        typeId: 'same-class-as', source: card.id, target: target.id,
        rationale: `${card.name} and ${target.name} are distinct members of the ${classes.find((entry) => entry.id === classId)?.name ?? classId} class; compare their functions rather than merging their meanings.`,
        provenanceKind: 'original-synthesis', sourceIds: ['original-synthesis'], confidence: 0.99,
        derivation: { kind: 'rule', ruleId: 'same-class-navigation', features: { classId } },
        reviewStatus: 'rule-validated', assertiveness: 'literal-classification'
      });
    });
  }

  // Each declared domain receives a literal cross-primary-domain navigation
  // edge. Candidate ranking uses only explicit catalog features, never index
  // position or a generic semantic verb.
  for (const card of cards) {
    const usedTargets = new Set();
    for (const domainId of card.domainIds) {
      const domainCards = byDomain.get(domainId) ?? [];
      const target = selectFeatureGroundedCandidate(
        domainCards,
        card,
        (candidate) => candidate.classId !== card.classId && candidate.domainIds[0] !== card.domainIds[0],
        usedTargets
      );
      if (!target) continue;
      usedTargets.add(target.id);
      const domainName = inquiryDomains.find((domain) => domain.id === domainId)?.name ?? domainId;
      addEdge({
        typeId: 'shares-inquiry-domain-with', source: card.id, target: target.id,
        rationale: `${card.name} and ${target.name} are both explicitly classified under ${domainName}; this edge records that shared catalog fact and makes no causal or oppositional claim.`,
        provenanceKind: 'original-synthesis', sourceIds: ['original-synthesis'], confidence: 0.98,
        derivation: { kind: 'rule', ruleId: 'shared-explicit-domain', features: { domainIds: [domainId] } },
        reviewStatus: 'rule-validated', assertiveness: 'literal-classification'
      });
    }
  }

  // Shared-source navigation is equally literal and is never counted as a
  // substantive assertion. Every citation is verified on both endpoints.
  for (const card of cards) {
    const target = selectFeatureGroundedCandidate(cards, card, (candidate) => sharedSourceIds(card, candidate).length > 0);
    if (!target) continue;
    const sourceLens = sharedSourceIds(card, target)[0];
    addEdge({
      typeId: 'assessed-through-shared-source-lens', source: card.id, target: target.id,
      rationale: `${card.name} and ${target.name} both cite “${sourceLens}”; this edge records shared bibliography only and makes no claim that the source equates the cards.`,
      provenanceKind: 'sourced', sourceIds: [sourceLens], confidence: 0.99,
      derivation: { kind: 'rule', ruleId: 'shared-explicit-source', features: { sourceIds: [sourceLens] } },
      reviewStatus: 'rule-validated', assertiveness: 'literal-classification'
    });
  }

  // Repair only with literal feature rules. If an invariant cannot be met
  // honestly, diagnostics fail the build rather than inventing an edge.
  for (let pass = 0; pass < 6; pass += 1) {
    const diagnostics = graphDiagnostics(cards, edges);
    let changed = false;
    for (const card of cards) {
      const state = diagnostics.nodes[card.id];
      const neighbors = new Set(state.neighborIds);
      if (state.degree >= 4 && state.edgeFamilies.length >= 3 && state.hasCrossDomain) continue;
      const target = selectFeatureGroundedCandidate(
        cards,
        card,
        (candidate) => intersection(card.domainIds, candidate.domainIds).length > 0
          && candidate.domainIds[0] !== card.domainIds[0],
        neighbors
      );
      if (!target) continue;
      const domainId = intersection(card.domainIds, target.domainIds)[0];
      const domainName = inquiryDomains.find((domain) => domain.id === domainId)?.name ?? domainId;
      addEdge({
        typeId: 'shares-inquiry-domain-with', source: card.id, target: target.id,
        rationale: `${card.name} and ${target.name} are both explicitly classified under ${domainName}; this repair edge records only that catalog fact.`,
        provenanceKind: 'original-synthesis', sourceIds: ['original-synthesis'], confidence: 0.98,
        derivation: { kind: 'rule', ruleId: 'shared-explicit-domain-repair', features: { domainIds: [domainId] } },
        reviewStatus: 'rule-validated', assertiveness: 'literal-classification'
      });
      changed = true;
    }
    if (!changed) break;
  }

  edges.sort((a, b) => a.id.localeCompare(b.id));
  const diagnostics = graphDiagnostics(cards, edges);
  const citationFailures = sourcedEdgeCitationFailures(cards, edges);
  const semanticFailures = semanticGraphFailures(cards, edges);
  const adjacency = Object.fromEntries(cards.map((card) => [card.id, diagnostics.nodes[card.id]?.edgeIds ?? []]));

  return {
    edgeTypes,
    edges,
    adjacency,
    lineages,
    diagnostics: {
      ...diagnostics,
      invalidSourcedEdgeCitations: citationFailures,
      semanticFailures,
      missingCuratedCardNames: [...new Set(missingCuratedCards)].sort(),
      missingLineageCardNames: [...new Set(missingLineageCards)].sort()
    }
  };
}

const orientations = [
  { id: 'illuminated', weight: 0.50, instruction: 'Read the card through its gift, capacity, and available agency.' },
  { id: 'shadowed', weight: 0.35, instruction: 'Read the card through its distortion, cost, or captured form.' },
  { id: 'threshold', weight: 0.15, instruction: 'Read the card as an unresolved transition that can still move in more than one direction.' }
];

function weightedOrientation(rng) {
  const value = rng();
  let cursor = 0;
  for (const orientation of orientations) {
    cursor += orientation.weight;
    if (value <= cursor) return orientation;
  }
  return orientations[orientations.length - 1];
}

function publicCard(card) {
  return { id: card.id, name: card.name, classId: card.classId, domainIds: card.domainIds };
}

function drawSpreadFromNormalized(cards, spread, seed, options = {}) {
  const rng = createDeterministicRng(`${seed}:${spread.id}`);
  const used = new Set();
  const positions = spread.positions.map((position, index) => {
    let eligible = cards.filter((card) => !used.has(card.id) && (!position.allowedClasses.length || position.allowedClasses.includes(card.classId)));
    if (!eligible.length && options.allowRepeats) eligible = cards.filter((card) => !position.allowedClasses.length || position.allowedClasses.includes(card.classId));
    if (!eligible.length) eligible = cards.filter((card) => !used.has(card.id));
    if (!eligible.length) eligible = cards;
    const card = eligible[Math.floor(rng() * eligible.length)];
    if (!card) throw new Error('Cannot draw from an empty Oracle.');
    used.add(card.id);
    const orientation = weightedOrientation(rng);
    return {
      positionId: position.id,
      positionTitle: position.title,
      prompt: position.prompt,
      index,
      card: publicCard(card),
      orientation: orientation.id,
      orientationInstruction: orientation.instruction
    };
  });
  return {
    spreadId: spread.id,
    title: spread.title,
    purpose: spread.purpose,
    seed: String(seed),
    positions,
    integrationQuestion: 'What action remains responsible across more than one plausible interpretation of this reading?'
  };
}

export function drawSpread(inputCards, spreadId = 'rabbit-hole', seed = 'oracle-spread', options = {}) {
  const cards = normalizeCards(inputCards);
  const spread = spreads.find((entry) => entry.id === spreadId);
  if (!spread) throw new Error(`Unknown spread “${spreadId}”.`);
  return drawSpreadFromNormalized(cards, spread, seed, options);
}

function prepareGenerationContext(cards, graph) {
  const byClass = new Map();
  for (const card of cards) {
    if (!byClass.has(card.classId)) byClass.set(card.classId, []);
    byClass.get(card.classId).push(card);
  }
  return {
    cards,
    graph,
    edgeById: new Map(graph.edges.map((edge) => [edge.id, edge])),
    // Generative traversal may only claim graph guidance when the bridge is a
    // reviewed substantive assertion. Taxonomy, bibliography, and navigation
    // edges remain useful for browsing but do not create story causality.
    adjacency: Object.fromEntries(cards.map((card) => [card.id, graph.edges
      .filter((edge) => edge.substantive && edge.reviewStatus === 'reviewed'
        && (edge.source === card.id || edge.target === card.id))
      .map((edge) => edge.id)
      .sort()])),
    byId: new Map(cards.map((card) => [card.id, card])),
    byClass
  };
}

function buildConnectedGeneration(inputCards, spec, seed, options = {}) {
  const cards = options.generationContext?.cards ?? normalizeCards(inputCards);
  const rng = createDeterministicRng(`${seed}:${spec.id}`);
  const graph = options.relationships ?? options.generationContext?.graph ?? buildRelationships(inputCards);
  const context = options.generationContext ?? prepareGenerationContext(cards, graph);
  const { edgeById, adjacency, byId, byClass } = context;
  const used = new Set();
  const selections = [];
  const bridgeEdgeIds = [];
  const bridgeEdges = [];
  let previous;

  for (const [slotId, title, allowedClasses, prompt] of spec.slots) {
    const eligible = allowedClasses.flatMap((classId) => byClass.get(classId) ?? []).filter((card) => !used.has(card.id));
    if (!eligible.length) throw new Error(`Generator slot “${slotId}” has no eligible cards.`);
    let connected = [];
    if (previous) {
      connected = (adjacency[previous.id] ?? [])
        .map((id) => edgeById.get(id))
        .filter(Boolean)
        .map((edge) => edge.source === previous.id ? edge.target : edge.source)
        .map((id) => byId.get(id))
        .filter((card) => card && !used.has(card.id) && allowedClasses.includes(card.classId));
    }
    const pool = connected.length ? connected.sort((a, b) => a.id.localeCompare(b.id)) : eligible;
    const selected = pool[Math.floor(rng() * pool.length)];
    used.add(selected.id);
    if (previous) {
      const bridge = (adjacency[previous.id] ?? [])
        .map((id) => edgeById.get(id))
        .find((edge) => edge
          && edge.substantive
          && edge.reviewStatus === 'reviewed'
          && ((edge.source === previous.id && edge.target === selected.id)
            || (edge.target === previous.id && edge.source === selected.id)));
      if (bridge) {
        bridgeEdgeIds.push(bridge.id);
        bridgeEdges.push({
          edgeId: bridge.id,
          fromSlotId: selections[selections.length - 1].slotId,
          toSlotId: slotId,
          sourceCardId: previous.id,
          targetCardId: selected.id
        });
      }
    }
    selections.push({ slotId, title, prompt, card: publicCard(selected), graphGuided: connected.some((card) => card.id === selected.id) });
    previous = selected;
  }

  const names = Object.fromEntries(selections.map((selection) => [selection.slotId, selection.card.name]));
  return {
    generatorId: spec.id,
    title: spec.title,
    seed: String(seed),
    selections,
    bridgeEdgeIds,
    bridgeEdges,
    graphGuidedSelectionCount: selections.filter((selection) => selection.graphGuided).length,
    prompts: {
      premise: `${names.protagonist ?? names.being ?? names.identity ?? names.inhabitants ?? 'An intelligence'} encounters ${names.rupture ?? names.crisis ?? names.origin ?? names.threshold ?? names['existential-threat'] ?? 'a threshold'} within ${names.world ?? names.environment ?? 'an unstable world'}.`,
      pressure: `${names.pressure ?? names.contradiction ?? names.desire ?? 'A recurring force'} makes the visible solution insufficient.`,
      institutionalQuestion: `How does ${names.institution ?? names.faction ?? names.allegiance ?? names.regime ?? names.governance ?? names.identity ?? 'the governing collective'} change when its method can no longer contain the consequences?`,
      evidenceQuestion: 'Which details belong to established knowledge, fiction, experiential report, lore, or original synthesis?',
      transformationQuestion: `What must be relinquished for ${names.becoming ?? names.future ?? names.desiredFuture ?? 'a new continuity'} to become possible?`
    }
  };
}

export function generateStory(inputCards, seed = 'oracle-story', options = {}) {
  return buildConnectedGeneration(inputCards, generatorSpecs.story, seed, options);
}

export function generateCharacter(inputCards, seed = 'oracle-character', options = {}) {
  return buildConnectedGeneration(inputCards, generatorSpecs.character, seed, options);
}

export function generateWorld(inputCards, seed = 'oracle-world', options = {}) {
  return buildConnectedGeneration(inputCards, generatorSpecs.world, seed, options);
}

export function generateFaction(inputCards, seed = 'oracle-faction', options = {}) {
  return buildConnectedGeneration(inputCards, generatorSpecs.faction, seed, options);
}

export function generateQuest(inputCards, seed = 'oracle-quest', options = {}) {
  return buildConnectedGeneration(inputCards, generatorSpecs.quest, seed, options);
}

export function generateCosmology(inputCards, seed = 'oracle-cosmology', options = {}) {
  return buildConnectedGeneration(inputCards, generatorSpecs.cosmology, seed, options);
}

function countInto(record, key, amount = 1) {
  record[key] = (record[key] ?? 0) + amount;
}

function checksum(value) {
  return hash32(JSON.stringify(value)).toString(16).padStart(8, '0');
}

function validateSpreadReading(reading, spread) {
  const failures = [];
  if (reading.positions.length !== spread.cards || spread.positions.length !== spread.cards) {
    failures.push(`${spread.id}: expected ${spread.cards} positions, received ${reading.positions.length}`);
  }
  const ids = reading.positions.map((position) => position.card.id);
  if (new Set(ids).size !== ids.length) failures.push(`${spread.id}: repeated card in one reading`);
  reading.positions.forEach((position, index) => {
    const declaration = spread.positions[index];
    if (!declaration || position.positionId !== declaration.id) {
      failures.push(`${spread.id}: position ${index} does not match its declaration`);
      return;
    }
    if (declaration.allowedClasses.length && !declaration.allowedClasses.includes(position.card.classId)) {
      failures.push(`${spread.id}.${declaration.id}: ${position.card.id} is outside allowedClasses`);
    }
  });
  return failures;
}

export function validateSpreadSpecs(inputCards) {
  const cards = normalizeCards(inputCards);
  const failures = [];
  for (const spread of spreads) {
    const seed = `spread-contract:${spread.id}`;
    const first = drawSpreadFromNormalized(cards, spread, seed);
    const second = drawSpreadFromNormalized(cards, spread, seed);
    failures.push(...validateSpreadReading(first, spread));
    if (JSON.stringify(first) !== JSON.stringify(second)) failures.push(`${spread.id}: same seed is not deterministic`);
  }
  return failures;
}

function validateGeneratorOutput(output, spec, graph) {
  const failures = [];
  if (output.selections.length !== spec.slots.length) failures.push(`${spec.id}: incorrect slot count`);
  const selectedIds = output.selections.map((selection) => selection.card.id);
  if (new Set(selectedIds).size !== selectedIds.length) failures.push(`${spec.id}: repeated card in one generation`);
  output.selections.forEach((selection, index) => {
    const slot = spec.slots[index];
    if (!slot || selection.slotId !== slot[0]) {
      failures.push(`${spec.id}: slot ${index} does not match its declaration`);
      return;
    }
    if (!slot[2].includes(selection.card.classId)) failures.push(`${spec.id}.${slot[0]}: ${selection.card.id} is outside allowedClasses`);
  });
  const edgeById = new Map(graph.edges.map((edge) => [edge.id, edge]));
  for (const bridge of output.bridgeEdges ?? []) {
    const edge = edgeById.get(bridge.edgeId);
    if (!edge) {
      failures.push(`${spec.id}: missing bridge ${bridge.edgeId}`);
      continue;
    }
    const endpoints = new Set([edge.source, edge.target]);
    if (!endpoints.has(bridge.sourceCardId) || !endpoints.has(bridge.targetCardId)) {
      failures.push(`${spec.id}: bridge ${bridge.edgeId} does not connect its selected cards`);
    }
    if (!edge.substantive || edge.reviewStatus !== 'reviewed') {
      failures.push(`${spec.id}: bridge ${bridge.edgeId} is not reviewed substantive`);
    }
  }
  if (output.bridgeEdgeIds.length !== (output.bridgeEdges ?? []).length) failures.push(`${spec.id}: bridge registry mismatch`);
  return failures;
}

export function validateGeneratorSpecs(inputCards, relationships = buildRelationships(inputCards)) {
  const cards = normalizeCards(inputCards);
  const generationContext = prepareGenerationContext(cards, relationships);
  const failures = [];
  for (const spec of Object.values(generatorSpecs)) {
    const seed = `generator-contract:${spec.id}`;
    const options = { relationships, generationContext };
    const first = buildConnectedGeneration(inputCards, spec, seed, options);
    const second = buildConnectedGeneration(inputCards, spec, seed, options);
    failures.push(...validateGeneratorOutput(first, spec, relationships));
    if (JSON.stringify(first) !== JSON.stringify(second)) failures.push(`${spec.id}: same seed is not deterministic`);
  }
  return failures;
}

export function simulateDivination(inputCards, options = {}) {
  const iterations = Number.isInteger(options.iterations) ? options.iterations : 10000;
  const seed = options.seed ?? 'oracle-divination-simulation';
  const spreadIds = options.spreadIds?.length ? options.spreadIds : spreads.map((spread) => spread.id);
  const normalized = normalizeCards(inputCards);
  const spreadById = new Map(spreads.map((spread) => [spread.id, spread]));
  const cardHits = {};
  const orientationHits = {};
  const spreadHits = {};
  let totalDraws = 0;
  const specificationFailures = validateSpreadSpecs(inputCards);
  for (let iteration = 0; iteration < iterations; iteration += 1) {
    const spreadId = spreadIds[iteration % spreadIds.length];
    const spread = spreadById.get(spreadId);
    if (!spread) throw new Error(`Unknown spread “${spreadId}”.`);
    const reading = drawSpreadFromNormalized(normalized, spread, `${seed}:${iteration}`);
    specificationFailures.push(...validateSpreadReading(reading, spread));
    countInto(spreadHits, spreadId);
    for (const position of reading.positions) {
      countInto(cardHits, position.card.id);
      countInto(orientationHits, position.orientation);
      totalDraws += 1;
    }
  }
  const unselectedCardIds = normalized.map((card) => card.id).filter((id) => !cardHits[id]);
  const result = {
    seed: String(seed), iterations, totalDraws,
    uniqueCardsDrawn: Object.keys(cardHits).length,
    coverage: normalized.length ? Object.keys(cardHits).length / normalized.length : 0,
    unselectedCardIds,
    spreadHits,
    orientationHits,
    leastSelected: Object.entries(cardHits).sort((a, b) => a[1] - b[1] || a[0].localeCompare(b[0])).slice(0, 12),
    mostSelected: Object.entries(cardHits).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 12),
    specificationFailures: [...new Set(specificationFailures)]
  };
  return { ...result, checksum: checksum(result) };
}

export function simulateGenerators(inputCards, options = {}) {
  const iterations = Number.isInteger(options.iterations) ? options.iterations : 10000;
  const seed = options.seed ?? 'oracle-generator-simulation';
  const relationships = options.relationships ?? buildRelationships(inputCards);
  const normalized = normalizeCards(inputCards);
  const generationContext = prepareGenerationContext(normalized, relationships);
  const generators = [
    ['story', generatorSpecs.story], ['character', generatorSpecs.character], ['world', generatorSpecs.world],
    ['faction', generatorSpecs.faction], ['quest', generatorSpecs.quest], ['cosmology', generatorSpecs.cosmology]
  ];
  const cardHits = {};
  const generatorHits = {};
  let graphGuidedSelections = 0;
  let totalSelections = 0;
  const specificationFailures = validateGeneratorSpecs(inputCards, relationships);
  for (let iteration = 0; iteration < iterations; iteration += 1) {
    const [id, spec] = generators[iteration % generators.length];
    const output = buildConnectedGeneration(inputCards, spec, `${seed}:${iteration}`, { relationships, generationContext });
    specificationFailures.push(...validateGeneratorOutput(output, spec, relationships));
    countInto(generatorHits, id);
    graphGuidedSelections += output.graphGuidedSelectionCount;
    totalSelections += output.selections.length;
    for (const selection of output.selections) countInto(cardHits, selection.card.id);
  }
  const result = {
    seed: String(seed), iterations, generatorHits, totalSelections, graphGuidedSelections,
    graphGuidedRate: totalSelections ? graphGuidedSelections / totalSelections : 0,
    uniqueCardsSelected: Object.keys(cardHits).length,
    coverage: normalized.length ? Object.keys(cardHits).length / normalized.length : 0,
    unselectedCardIds: normalized.map((card) => card.id).filter((id) => !cardHits[id]),
    leastSelected: Object.entries(cardHits).sort((a, b) => a[1] - b[1] || a[0].localeCompare(b[0])).slice(0, 12),
    mostSelected: Object.entries(cardHits).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 12),
    specificationFailures: [...new Set(specificationFailures)]
  };
  return { ...result, checksum: checksum(result) };
}

function validateEffects(effects, location, game, failures) {
  const trackIds = new Set(game.trackDefinitions.map((track) => track.id));
  const resourceIds = new Set(game.resourceDefinitions.map((resource) => resource.id));
  const stateReferenceIds = new Set(game.zoneDefinitions.map((zone) => zone.id));
  const edgeTypeIds = new Set(game.edgeTypeDefinitions);
  for (const [index, effect] of (effects ?? []).entries()) {
    const here = `${location}[${index}]`;
    const schema = effectOperationSchemas[effect.op];
    if (!effectOperationIds.has(effect.op) || !schema) {
      failures.push(`${here}: unknown operation “${effect.op}”`);
      continue;
    }
    for (const key of schema.required) if (effect[key] === undefined) failures.push(`${here}: missing required ${key}`);
    for (const key of schema.numeric ?? []) {
      if (!Number.isFinite(effect[key]) || effect[key] <= 0) failures.push(`${here}.${key}: expected a positive number`);
    }
    for (const key of schema.trackReferences ?? []) if (!trackIds.has(effect[key])) failures.push(`${here}.${key}: undeclared track “${effect[key]}”`);
    for (const key of schema.resourceReferences ?? []) if (!resourceIds.has(effect[key])) failures.push(`${here}.${key}: undeclared resource “${effect[key]}”`);
    for (const key of schema.stateReferences ?? []) if (!stateReferenceIds.has(effect[key])) failures.push(`${here}.${key}: undeclared state reference “${effect[key]}”`);
    for (const key of schema.edgeTypeReferences ?? []) if (!edgeTypeIds.has(effect[key])) failures.push(`${here}.${key}: undeclared edge type “${effect[key]}”`);
    if (effect.op === 'draw') {
      if (!effect.from || typeof effect.from !== 'object' || Array.isArray(effect.from)) failures.push(`${here}.from: expected a filter object`);
      for (const classId of effect.from?.classes ?? []) if (!classes.some((entry) => entry.id === classId)) failures.push(`${here}.from.classes: unknown class “${classId}”`);
      if (effect.from?.exclude && !stateReferenceIds.has(effect.from.exclude)) failures.push(`${here}.from.exclude: undeclared state reference “${effect.from.exclude}”`);
    }
    if (effect.op === 'branch') {
      if (!Array.isArray(effect.then) || !Array.isArray(effect.otherwise)) failures.push(`${here}: branch paths must be effect arrays`);
      validateEffects(effect.then, `${here}.then`, game, failures);
      validateEffects(effect.otherwise, `${here}.otherwise`, game, failures);
    }
  }
}

function validateResolutionPredicates(game, failures) {
  const trackById = new Map(game.trackDefinitions.map((track) => [track.id, track]));
  for (const outcome of ['win', 'loss']) {
    const declaration = game.resolutionPredicates?.[outcome];
    const groups = declaration?.all ?? declaration?.any;
    if (!Array.isArray(groups) || !groups.length) {
      failures.push(`${game.id}.resolutionPredicates.${outcome}: missing declarative predicates`);
      continue;
    }
    for (const [index, condition] of groups.entries()) {
      const track = trackById.get(condition.track);
      if (!track) failures.push(`${game.id}.${outcome}[${index}]: undeclared track “${condition.track}”`);
      if (!['>', '>=', '<', '<=', '==='].includes(condition.operator)) failures.push(`${game.id}.${outcome}[${index}]: invalid operator`);
      if (!Number.isFinite(condition.value)) failures.push(`${game.id}.${outcome}[${index}]: invalid comparison value`);
      if (track && (condition.value < track.range[0] || condition.value > track.range[1])) failures.push(`${game.id}.${outcome}[${index}]: comparison falls outside track range`);
    }
  }
}

function cloneGameState(state) {
  return JSON.parse(JSON.stringify(state));
}

function gameById(gameOrId) {
  if (typeof gameOrId !== 'string') return gameOrId;
  const game = games.find((entry) => entry.id === gameOrId);
  if (!game) throw new Error(`Unknown game “${gameOrId}”.`);
  return game;
}

function applyTrackDelta(state, game, trackId, amount, location) {
  const definition = game.trackDefinitions.find((track) => track.id === trackId);
  if (!definition) throw new Error(`${location}: undeclared track “${trackId}”`);
  const next = state.tracks[trackId] + amount;
  if (!Number.isFinite(next) || next < definition.range[0] || next > definition.range[1]) {
    throw new Error(`${location}: ${trackId} would leave its declared range ${definition.range.join('..')}`);
  }
  state.tracks[trackId] = next;
}

function applyEffects(state, game, effects, options = {}, location = 'effects') {
  for (const [index, effect] of (effects ?? []).entries()) {
    const here = `${location}[${index}]`;
    switch (effect.op) {
      case 'draw':
        state.zones[effect.to] = (state.zones[effect.to] ?? 0) + effect.count;
        break;
      case 'reveal':
        state.reveals[effect.target] = (state.reveals[effect.target] ?? 0) + effect.count;
        break;
      case 'place':
        state.zones[effect.zone] = (state.zones[effect.zone] ?? 0) + 1;
        break;
      case 'connect':
        state.connections.push({ from: effect.from, to: effect.to, edgeType: effect.edgeType });
        break;
      case 'cite-source':
        state.citations.push({ card: effect.card, claim: effect.claim });
        break;
      case 'challenge-claim':
        state.challenges.push({ card: effect.card, stake: effect.stake });
        break;
      case 'gain':
      case 'advance-track':
        applyTrackDelta(state, game, effect.resource ?? effect.track, effect.amount, here);
        break;
      case 'spend':
      case 'retreat-track':
        applyTrackDelta(state, game, effect.resource ?? effect.track, -effect.amount, here);
        break;
      case 'tag':
        state.tags.push({ target: effect.target, tag: effect.tag });
        break;
      case 'branch': {
        const branchChoice = options.branchChoice === 'then' ? 'then' : 'otherwise';
        state.branches.push({ condition: effect.condition, choice: branchChoice });
        applyEffects(state, game, effect[branchChoice], options, `${here}.${branchChoice}`);
        break;
      }
      case 'vote':
        state.votes.push({ question: effect.question, threshold: effect.threshold });
        break;
      case 'swap':
        state.swaps.push({ first: effect.first, second: effect.second });
        break;
      case 'transform':
        state.transformations.push({ target: effect.target, into: effect.into });
        break;
      case 'lock':
        state.locks[effect.target] = true;
        break;
      case 'unlock':
        state.locks[effect.target] = false;
        break;
      default:
        throw new Error(`${here}: unsupported operation “${effect.op}”`);
    }
  }
}

function predicatePasses(value, operator, expected) {
  if (operator === '>') return value > expected;
  if (operator === '>=') return value >= expected;
  if (operator === '<') return value < expected;
  if (operator === '<=') return value <= expected;
  return value === expected;
}

function predicateGroupPasses(state, declaration) {
  const conditions = declaration.all ?? declaration.any ?? [];
  const values = conditions.map((condition) => predicatePasses(state.tracks[condition.track], condition.operator, condition.value));
  return declaration.all ? values.every(Boolean) : values.some(Boolean);
}

export function evaluateGameOutcome(gameOrId, state) {
  const game = gameById(gameOrId);
  // Loss wins ties: a progress threshold never excuses a simultaneously
  // exhausted resource or triggered failure condition.
  if (predicateGroupPasses(state, game.resolutionPredicates.loss)) return 'loss';
  if (predicateGroupPasses(state, game.resolutionPredicates.win)) return 'win';
  return 'ongoing';
}

export function createGameState(gameOrId) {
  const game = gameById(gameOrId);
  const state = {
    gameId: game.id,
    tracks: Object.fromEntries(game.trackDefinitions.map((track) => [track.id, track.initial])),
    zones: Object.fromEntries(game.zoneDefinitions.map((zone) => [zone.id, zone.initialCount ?? 0])),
    reveals: {}, tags: [], locks: {}, connections: [], citations: [], challenges: [], branches: [], votes: [], swaps: [], transformations: [],
    turns: 0, actionLog: [], outcome: 'ongoing'
  };
  applyEffects(state, game, game.setup, {}, `${game.id}.setup`);
  state.outcome = evaluateGameOutcome(game, state);
  return state;
}

export function reduceGameAction(gameOrId, inputState, actionId, options = {}) {
  const game = gameById(gameOrId);
  if (inputState.gameId !== game.id) throw new Error(`State belongs to ${inputState.gameId}, not ${game.id}.`);
  if (inputState.outcome !== 'ongoing') throw new Error(`${game.id}: cannot act after ${inputState.outcome}.`);
  const action = game.turn.find((entry) => entry.id === actionId);
  if (!action) throw new Error(`${game.id}: unknown action “${actionId}”.`);
  const state = cloneGameState(inputState);
  for (const cost of action.cost ?? []) applyTrackDelta(state, game, cost.resource, -cost.amount, `${game.id}.${action.id}.cost`);
  applyEffects(state, game, action.effects, options, `${game.id}.${action.id}.effects`);
  for (const [trackId, amount] of Object.entries(game.actionTrackDeltas[action.id] ?? {})) {
    applyTrackDelta(state, game, trackId, amount, `${game.id}.${action.id}.actionTrackDeltas`);
  }
  state.turns += 1;
  state.actionLog.push({ actionId, branchChoice: options.branchChoice ?? null });
  state.outcome = evaluateGameOutcome(game, state);
  return state;
}

export function runGameFixture(gameOrId, fixtureName) {
  const game = gameById(gameOrId);
  const fixture = game.fixtures?.[fixtureName];
  if (!Array.isArray(fixture)) throw new Error(`${game.id}: missing ${fixtureName} fixture.`);
  let state = createGameState(game);
  for (const step of fixture) {
    if (state.outcome !== 'ongoing') break;
    const declaration = typeof step === 'string' ? { actionId: step } : step;
    state = reduceGameAction(game, state, declaration.actionId, { branchChoice: declaration.branchChoice });
  }
  return state;
}

export function validateGameSpecs() {
  const failures = [];
  for (const game of games) {
    const trackIds = new Set(game.trackDefinitions.map((track) => track.id));
    const resourceIds = new Set(game.resourceDefinitions.map((resource) => resource.id));
    if (trackIds.size !== game.trackDefinitions.length) failures.push(`${game.id}: duplicate track declaration`);
    if (new Set(game.zoneDefinitions.map((zone) => zone.id)).size !== game.zoneDefinitions.length) failures.push(`${game.id}: duplicate state-reference declaration`);
    for (const resourceId of game.resources) if (!resourceIds.has(resourceId)) failures.push(`${game.id}: undeclared resource “${resourceId}”`);
    for (const definition of game.trackDefinitions) {
      if (!Array.isArray(definition.range) || definition.range.length !== 2 || definition.initial < definition.range[0] || definition.initial > definition.range[1]) {
        failures.push(`${game.id}.${definition.id}: invalid range or initial value`);
      }
    }
    validateEffects(game.setup, `${game.id}.setup`, game, failures);
    const actionIds = new Set();
    for (const action of game.turn) {
      if (actionIds.has(action.id)) failures.push(`${game.id}: duplicate action ${action.id}`);
      actionIds.add(action.id);
      if (!action.effects?.length) failures.push(`${game.id}.${action.id}: action has no effects`);
      for (const cost of action.cost ?? []) {
        if (!resourceIds.has(cost.resource)) failures.push(`${game.id}.${action.id}: undeclared cost resource “${cost.resource}”`);
        if (!Number.isFinite(cost.amount) || cost.amount <= 0) failures.push(`${game.id}.${action.id}: invalid cost amount`);
      }
      validateEffects(action.effects, `${game.id}.${action.id}.effects`, game, failures);
      for (const [trackId, amount] of Object.entries(game.actionTrackDeltas[action.id] ?? {})) {
        if (!trackIds.has(trackId)) failures.push(`${game.id}.${action.id}: undeclared action-delta track “${trackId}”`);
        if (!Number.isFinite(amount) || amount === 0) failures.push(`${game.id}.${action.id}: invalid action-delta amount`);
      }
    }
    if (!game.resolution?.win || !game.resolution?.loss) failures.push(`${game.id}: incomplete human-readable resolution`);
    validateResolutionPredicates(game, failures);
    for (const outcome of ['win', 'loss']) {
      for (const step of game.fixtures?.[outcome] ?? []) {
        const actionId = typeof step === 'string' ? step : step.actionId;
        if (!actionIds.has(actionId)) failures.push(`${game.id}.${outcome}: fixture references unknown action “${actionId}”`);
      }
      try {
        const first = runGameFixture(game, outcome);
        const second = runGameFixture(game, outcome);
        if (first.outcome !== outcome) failures.push(`${game.id}.${outcome}: fixture ended ${first.outcome}`);
        if (JSON.stringify(first) !== JSON.stringify(second)) failures.push(`${game.id}.${outcome}: fixture is not deterministic`);
      } catch (error) {
        failures.push(`${game.id}.${outcome}: ${error.message}`);
      }
    }
  }
  return failures;
}

function countEffectOperations(effects, operationHits, branchChoice = 'otherwise') {
  for (const effect of effects ?? []) {
    countInto(operationHits, effect.op);
    if (effect.op === 'branch') {
      countEffectOperations(effect[branchChoice === 'then' ? 'then' : 'otherwise'], operationHits, branchChoice);
    }
  }
}

export function simulateGames(options = {}) {
  const iterations = Number.isInteger(options.iterations) ? options.iterations : 10000;
  const seed = options.seed ?? 'oracle-game-simulation';
  const rng = createDeterministicRng(seed);
  const gameHits = {};
  const actionHits = {};
  const operationHits = {};
  const outcomes = { win: 0, loss: 0, ongoing: 0 };
  const specificationFailures = validateGameSpecs();
  for (let iteration = 0; iteration < iterations; iteration += 1) {
    const game = games[iteration % games.length];
    const intendedOutcome = rng() < 0.5 ? 'win' : 'loss';
    countInto(gameHits, game.id);
    let state = createGameState(game);
    for (const step of game.fixtures[intendedOutcome]) {
      if (state.outcome !== 'ongoing') break;
      const declaration = typeof step === 'string' ? { actionId: step } : step;
      const action = game.turn.find((entry) => entry.id === declaration.actionId);
      countInto(actionHits, `${game.id}:${declaration.actionId}`);
      countEffectOperations(action.effects, operationHits, declaration.branchChoice);
      state = reduceGameAction(game, state, declaration.actionId, { branchChoice: declaration.branchChoice });
    }
    countInto(outcomes, state.outcome);
    if (state.outcome !== intendedOutcome) specificationFailures.push(`${game.id}: ${intendedOutcome} simulation ended ${state.outcome}`);
  }
  const result = {
    seed: String(seed), iterations, gameHits, actionHits, operationHits, outcomes,
    reducer: 'declarative-game-state-v1',
    specificationFailures: [...new Set(specificationFailures)]
  };
  return { ...result, checksum: checksum(result) };
}

export function buildSystems(inputCards) {
  const relationships = buildRelationships(inputCards);
  return {
    id: 'ultimate-science-fiction-oracle-systems',
    version: '1.0.0',
    principles: [
      'Typed edges carry meaning; no generic related-to edge is permitted.',
      'Every relationship records provenance; sourced claims cite both endpoints, while explicit Oracle synthesis remains visibly authored.',
      'Only reviewed curated or lineage edges make substantive semantic assertions; taxonomy, bibliography, and navigation never masquerade as causality.',
      'Cross-domain edges keep local clusters open to alternative questions.',
      'Games spend clarity, trust, agency, stability, secrecy, and evidence debt rather than treating certainty as a score.',
      'Generation follows reviewed substantive graph edges where possible and records when it falls back to class fit.'
    ],
    edgeTypes,
    relationships: relationships.edges,
    adjacency: relationships.adjacency,
    graphDiagnostics: relationships.diagnostics,
    lineages: relationships.lineages,
    spreads,
    games,
    effectOperations,
    effectOperationSchemas,
    resources: standardResources,
    generators: generatorSpecs,
    orientations
  };
}

/**
 * Attach graph results to card records after buildSystems(). Keeping this
 * transformation explicit avoids mutating the authored pre-graph cards.
 */
export function completeCardRelationships(inputCards, systems) {
  const edgeIdsByCard = new Map((inputCards ?? []).map((card) => [card.id, []]));
  for (const edge of systems?.relationships ?? []) {
    edgeIdsByCard.get(edge.source)?.push(edge.id);
    edgeIdsByCard.get(edge.target)?.push(edge.id);
  }
  return (inputCards ?? []).map((card) => {
    const node = systems?.graphDiagnostics?.nodes?.[card.id];
    if (!node) throw new Error(`Cannot complete relationship status for ${card.id}: graph diagnostics are missing.`);
    const requirements = card.relationshipRequirements ?? {};
    const hasSourcedEdge = node.provenanceKinds.includes('sourced');
    const hasOriginalSynthesisEdge = node.provenanceKinds.includes('original-synthesis');
    const isComplete = node.degree >= (requirements.minimumDegree ?? 4)
      && node.edgeFamilies.length >= (requirements.minimumEdgeFamilies ?? 3)
      && (!requirements.requireCrossDomain || node.hasCrossDomain)
      && (!(requirements.requireOriginalSynthesisEdge ?? requirements.requireSymbolicEdge) || hasOriginalSynthesisEdge);
    return {
      ...card,
      relationships: [...new Set(edgeIdsByCard.get(card.id) ?? [])].sort(),
      relationshipRequirements: {
        ...requirements,
        status: isComplete ? 'complete' : 'incomplete',
        actualDegree: node.degree,
        actualEdgeFamilies: node.edgeFamilies.length,
        edgeFamilies: node.edgeFamilies,
        hasCrossDomain: node.hasCrossDomain,
        hasSourcedEdge,
        hasOriginalSynthesisEdge
      }
    };
  });
}

export function assertCompletedCardRelationships(cards, systems) {
  const failures = [];
  const relationshipIds = new Set((systems?.relationships ?? []).map((edge) => edge.id));
  for (const card of cards ?? []) {
    const status = card.relationshipRequirements?.status;
    if (status === 'awaiting-typed-graph-build') failures.push(`${card.id}: awaiting-typed-graph-build`);
    else if (status !== 'complete') failures.push(`${card.id}: relationship status is ${status ?? 'missing'}`);
    const node = systems?.graphDiagnostics?.nodes?.[card.id];
    if (!node) failures.push(`${card.id}: graph diagnostics missing`);
    const ids = [...new Set(card.relationships ?? [])];
    if (ids.some((id) => !relationshipIds.has(id))) failures.push(`${card.id}: unknown relationship ID`);
    if (node && ids.length !== node.edgeIds.length) failures.push(`${card.id}: attached relationship count does not match graph diagnostics`);
  }
  if (failures.length) throw new Error(`Generated card relationship completion failed:\n${failures.join('\n')}`);
  return true;
}

export function assertSystems(inputCards) {
  const systems = buildSystems(inputCards);
  const failures = [];
  if (systems.edgeTypes.some((type) => type.id === 'related-to')) failures.push('generic related-to edge is forbidden');
  if (systems.graphDiagnostics.orphanIds.length) failures.push(`${systems.graphDiagnostics.orphanIds.length} orphan cards`);
  if (systems.graphDiagnostics.belowFourNeighborIds.length) failures.push(`${systems.graphDiagnostics.belowFourNeighborIds.length} cards have fewer than four neighbors`);
  if (systems.graphDiagnostics.belowThreeFamilyIds.length) failures.push(`${systems.graphDiagnostics.belowThreeFamilyIds.length} cards have fewer than three edge families`);
  if (systems.graphDiagnostics.missingOriginalSynthesisIds.length) failures.push(`${systems.graphDiagnostics.missingOriginalSynthesisIds.length} cards lack original-synthesis edges`);
  if (systems.graphDiagnostics.missingCrossDomainIds.length) failures.push(`${systems.graphDiagnostics.missingCrossDomainIds.length} cards lack cross-domain edges`);
  if (systems.graphDiagnostics.invalidSourcedEdgeCitations.length) failures.push(...systems.graphDiagnostics.invalidSourcedEdgeCitations);
  if (systems.graphDiagnostics.semanticFailures.length) failures.push(...systems.graphDiagnostics.semanticFailures);
  if (systems.lineages.length !== 14) failures.push(`expected 14 lineages, found ${systems.lineages.length}`);
  for (const lineage of systems.lineages) {
    if (lineage.missingCardNames.length) failures.push(`${lineage.id} is missing ${lineage.missingCardNames.join(', ')}`);
    if (lineage.edgeIds.length !== Math.max(0, lineage.cardIds.length - 1)) failures.push(`${lineage.id} has an incomplete edge path`);
    for (const id of lineage.edgeIds) if (!systems.relationships.some((edge) => edge.id === id)) failures.push(`${lineage.id} references missing edge ${id}`);
  }
  if (systems.spreads.length !== 12) failures.push(`expected 12 spreads, found ${systems.spreads.length}`);
  if (systems.games.length < 8) failures.push(`expected at least 8 games, found ${systems.games.length}`);
  if (!generatorSpecs.quest || !generatorSpecs.cosmology) failures.push('quest and cosmology generators are required');
  failures.push(...validateSpreadSpecs(inputCards));
  failures.push(...validateGeneratorSpecs(inputCards, {
    edges: systems.relationships,
    adjacency: systems.adjacency,
    diagnostics: systems.graphDiagnostics
  }));
  failures.push(...validateGameSpecs());
  if (failures.length) throw new Error(failures.join('\n'));
  return systems;
}
