Metis · Features

The Atlas of Reality and Wonder Resolution

A focused page within the Metis Features documentation. The full map and every sibling page live in the Features hub.

7sections13 minread1diagram

On this page

The Atlas is V9's unified knowledge spine — the single ConceptNode/ConceptEdge graph that makes "start from a wonder, pull the thread" possible — and wonder resolution is the function that turns a learner's free-text question ("why is the night sky dark?") into the minimal, prerequisite-ordered set of concepts a lesson should teach right now. It is the first thing that runs when anyone enters V9: the experience layer's front door (buildWonderFrontDoor), the Prometheus forge (forgeLesson stage 0), and Theia's thread continuation all stand on it. The Atlas is also the only one of V9's three net-new structures (Atlas graph, Hephaestus explorable, Lesson artifact) that is a graph — and the design doc calls it "the single largest net-new build" (graph-unifier.ts header).

What keeps it buildable rather than aspirational is the same decision that runs through all of V9: compose the real engines, reinvent nothing. The Atlas owns no retrieval algorithm and no graph-reasoning algorithm. Free-text matching is the real Sophia BM25 index (@sophia/semantic-search); prerequisite closure, topological sort, and gap analysis are the real Mnemosyne KnowledgeGraph (@mnemosyne/core). The package's own package.json declares exactly three runtime dependencies — @mnemosyne/core, @oshun/contracts, @sophia/semantic-search — and the six implementation modules under libs/v9/atlas/src/ are the connective tissue that maps three source dialects into one content-addressed store and reads the answers back out of those engines. This page is the deep companion to the section overview; the hub for the set is ../V9_features.md.

What ships, honestly#

The Atlas package is real and tested. All six implementation modules (atlas-store.ts, source-adapters.ts, graph-unifier.ts, grounded-leaves.ts, wonder-resolution.ts, grounding-validator.ts) are implemented and exercised by three spec files holding 21 test cases (atlas-graph.spec.ts — 11, wonder-resolution.spec.ts — 5, grounded-leaves.spec.ts — 5). The tests run the real substrates, not mocks: resolveWonder actually builds a Sophia BM25 index and queries the Mnemosyne knowledge graph, and grounded-leaves.spec.ts actually imports ageTodayGyr(PLANCK_2018_COSMOLOGY) from @kalika/cosmology and asserts the bound value lands in 13.6 < age < 14.0 Gyr before declaring the node grounded.

Two things are honest seams, not stubs. The dense reranker (WonderEmbedder) is the Nous model boundary: it is an optional injected interface, and when it is absent resolveWonder runs BM25-only — it does not fabricate embeddings. A wonder that matches nothing throws WonderUnresolvedError rather than inventing a concept; the front-door view-model catches that throw and reports unresolved: true so the UI asks for a rephrase instead of hallucinating.

Three claims in the originating design doc are spec-ahead-of-code, and this page corrects them rather than restating them:

  • The monolith says "Theia decides whether to answer directly, scaffold, or open a thread" (V9_features.md §1). The signal for that decision is computed and surfaced — buildWonderFrontDoor returns prerequisiteCount (described in code as "the scaffolding depth") and the full conceptSet — but there is no single shipped three-way answer | scaffold | thread decision function. The data a director would branch on exists; the explicit branch is a view-model signal, not a gate.
  • V9KernelSchema admits four kernels (nyx, kalika, nisaba, demeter), but grounded-leaves.ts ships binders for only three (kalikaKernelLeaf, nyxEphemerisLeaf, nisabaSourceLeaf), and the Aletheia kernel registry (kernel-evaluators.ts) wires no demeter: ref today. A Demeter-computed leaf is admissible by contract but unbuilt.
  • The Atlas is a runtime structure assembled from injected source graphs, not a shipped reality-corpus. The adapters, the unifier, the leaf binders, and the invariants are all real; the actual concept population is supplied by the caller (and exercised by the tests with small fixtures). There is no large persisted "map of reality" dataset checked into this package.

The concept graph: nodes, edges, and two invariants#

The vocabulary lives in libs/contracts/src/v9/concept-graph.ts and primitives.ts. A V9ConceptNode is { id, label, aka, wonderAxis, discipline, kind, groundingPins, kernelRefs, epistemicStatus, safetyPolicyRef? }, and the whole point of the type is that two structural rules are baked into V9ConceptNodeSchema via .superRefine, so a node that breaks them fails to parse:

  • Invariant 1 — grounding. A fact node must carry ≥1 V9SophiaPin (a real source id + locator + a credibility in [0,1]). No ungrounded fact exists in the graph.
  • Invariant 2 — computation. A STEM fact/principle/procedure node must carry a kernelRef instead of a stored number. The STEM set is decided by v9NodeRequiresKernel(discipline, kind), which is true exactly when the discipline is in V9_STEM_DISCIPLINES (astronomy, physics, mathematics, chemistry, earth-science) and the kind is one of the three value-bearing kinds. The node holds a callable ref like kalika:cosmology#ageTodayGyr, not the literal 13.8 Gyr, so the value is reproduced at gate time and can never silently drift.

The same two checks are also exposed as a non-throwing function, checkV9ConceptNodeInvariants(node), which returns every violation as a structured { nodeId, invariant, message }. That is the build-validator path: the parsing form fails fast at the door, the aggregating form lets the Atlas build report all violations across the whole graph at once.

Identity is content-addressed. V9ConceptIdSchema accepts either a stable namespaced slug (nyx:olbers-paradox, kalika:age-of-the-universe) or a 64-hex content hash (cn:<sha256>), enforced by the regex ^(cn:[a-f0-9]{64}|[a-z][a-z0-9-]*:[a-z0-9][a-z0-9._-]*)$. The slug form is what a single source mints; the cn: form is what the unifier mints when it collapses the same concept seen across multiple sources (below). Edges (V9ConceptEdgeSchema) carry a type from the nine-value V9ConceptEdgeTypeSchemaprerequisite, related, part_of, generalizes, specializes, enables, conflicts, complements, bridges — and a weight in [0,1]; bridges is the cross-axis thread V9 adds (cosmology ↔ deep-time), and self-loops (from === to) are rejected by the edge schema's own superRefine.

Building the Atlas: three dialects into one spine#

V9 unifies three existing graphs, each of which speaks its own dialect, so source-adapters.ts provides one pure transform per source into the shared V9ConceptNode/V9ConceptEdge vocabulary:

  • fromMetisConceptGraph (metis: namespace) maps the Metis concept-graph dataclass shape. Its topic/skill/fact/procedure/principle types already match V9 kinds, and — crucially — it grounds fact nodes from their evidence_citation_ids, turning each citation into a V9SophiaPin at a caller-supplied default credibility (0.7). This is the seed of Invariant 1.
  • fromSophiaKnowledgeGraph (sophia: namespace) maps the Sophia KG's entities (figures, texts, traditions) to V9 topics — organizational nodes, not atomic facts, so they need no grounding pin — and translates relation types (PART_OF → part_of, OPPOSITE_OF → conflicts, DERIVED_FROM → specializes, else related).
  • fromMnemosynePrereqGraph (mnemosyne: namespace) maps the real @mnemosyne/core KGNode/KGEdge types and supplies the prerequisite skeleton.

graph-unifier.ts's unifyAtlas then does the merge that is "the single largest net-new build." It groups every draft node by atlasSlug(label) (a lowercase, NFKD-normalized, hyphenated key), mints a canonical id cn:<sha256(labelKey)>, and collapses the group into one node: it takes the most specific kind (KIND_PRIORITY = principle > procedure > fact > skill > topic), unions and de-dupes the grounding pins (by pinId) and kernel refs (by ref), folds sibling labels into aka, and carries any safetyPolicyRef forward. The honesty rule is explicit in the header and verified by a test: a merged node that still violates an invariant (e.g. an ungrounded fact that no source could ground) is not coerced — it is reported in skipped with the parse error's reason and left out of the store, "a logged truncation, not a fabricated grounding." Edges are then remapped onto the canonical ids, de-duped by (from, to, type), and any edge that touches a skipped or unknown endpoint — or that became a self-loop after the merge — is counted in droppedEdgeCount. The whole operation returns an AtlasUnificationResult with the raw vs merged node counts so a build can see exactly how much collapsing happened.

The merged nodes land in AtlasStore (atlas-store.ts), and the store is where the invariants become a runtime gate: addNode runs V9ConceptNodeSchema.parse(node) before inserting, so an ungrounded fact or a kernel-less STEM node is rejected at the door; addEdge refuses an edge whose endpoints are not already nodes. The store deliberately does not reimplement prerequisite reasoning — its toMnemosyneGraph(learnerMastery?) method projects every node and edge into a fresh @mnemosyne/core KnowledgeGraph (mapping prerequisite → prerequisite, part_of → part_of, everything else → related_to, and annotating each node with its mastery), and prerequisitesOf simply reads getPrerequisites back off that projection. This is the reuse seam: closure, topo-sort, and gap analysis are the Mnemosyne engine's, run over an Atlas projection.

Finally, grounded-leaves.ts is how the real domain kernels become Atlas nodes. kalikaKernelLeaf, nyxEphemerisLeaf, and nisabaSourceLeaf each build-and-validate a node whose kernelRef.ref points at an in-repo callable (kalika:cosmology#ageTodayGyr, nyx:ephemeris#sun.position) — the node holds the reference, never the value, which Aletheia recomputes at G2. The build-time enforcement is grounding-validator.ts: validateConceptNodes counts fact/STEM/grounded-fact nodes and collects violations, and assertAtlasGrounded(atlas) throws on any violation so a build pipeline blocks rather than warns. (Because the store already enforces at insert, this gate is belt-and-suspenders: a validly-built Atlas always passes it.)

Resolving a wonder#

resolveWonder(wonder, atlas, options) in wonder-resolution.ts is the function the rest of V9 calls. It runs four steps:

flowchart TD W["Wonder (free text)"] --> IDX["Build BM25 index over Atlas nodes<br/>doc text = label + aka + discipline + wonderAxis"] IDX --> BM["BM25 search<br/>(limit = max(topK·4, 16))"] BM --> HY{"WonderEmbedder<br/>injected?"} HY -->|no| CAND["Ranked candidates (BM25 score)"] HY -->|yes Nous| BLEND["Dense rerank over ALL nodes<br/>score = (1-w)·BM25 + w·cosine, w=0.5"] BLEND --> CAND CAND -->|empty| ERR["throw WonderUnresolvedError"] CAND --> TGT["target = top candidate"] TGT --> PROJ["Project Atlas → Mnemosyne KnowledgeGraph<br/>annotate learner mastery"] PROJ --> GAP["identifyKnowledgeGaps(mastery, target, masteryFloor)"] GAP --> TOPO["order gaps by topologicalSort (prereq-first)"] TOPO --> SET["conceptSet = [...orderedGaps, target]"]
  1. Lexical retrieval (Sophia BM25). A fresh createBM25LexicalIndex() is loaded with one document per Atlas node, where the indexed text is label + aka + discipline + wonderAxis. The query is the raw wonder; the index returns up to max(topK·4, 16) hits scored by the real BM25 engine (k1 = 1.2, b = 0.75, IDF with length normalization over an inverted index).
  2. Optional hybrid rerank (the Nous boundary). If an embedder is supplied, resolveWonder embeds [wonder, ...allNodeTexts] in one call and re-scores every node as (1 − denseWeight)·bm25 + denseWeight·cosine (denseWeight default 0.5). This is what lets a wonder with zero lexical overlap"the beginning of everything" — still reach the Big Bang node, which a pure BM25 path would miss; the wonder-resolution.spec.ts test proves exactly that with a toy embedder. Without an embedder, ranking is BM25 alone. (Blending a saturating BM25 score with a cosine in [0,1] is a deliberate, documented approximation, not a learned fusion.)
  3. Mastery-filtered prerequisite frontier (Mnemosyne). The target's transitive prerequisites are computed on the Mnemosyne projection, then identifyKnowledgeGaps(mastery, target, masteryFloor) drops any prerequisite the learner already holds at or above the floor ('intermediate' by default; the Mnemosyne ladder is novice → beginner → intermediate → advanced → expert → master). What remains is the real frontier — only what this learner still needs.
  4. The minimal teaching set. orderByPrerequisite runs Mnemosyne's topologicalSort (Kahn's algorithm, deterministically tie-broken) and filters it to the gap set, then appends the target: conceptSet = [...orderedGaps, target.id]. The output is prerequisite-first, target-last — cognitive-load discipline, encoded.

Worked example — "why is the night sky dark?" (Olbers' paradox). With an Atlas of three nodes (How light travels, The universe has a finite age, Olbers' paradox) and two prerequisite edges into Olbers, BM25 picks a:olbers as the target. A learner who knows nothing gets the full frontier [a:finite-age, a:light] and a conceptSet of length 3 ending in a:olbers. A learner already advanced on a:light gets the frontier trimmed to [a:finite-age] and a two-element teaching set [a:finite-age, a:olbers]. These are the literal assertions in wonder-resolution.spec.ts.

The front door, and the threads it opens#

buildWonderFrontDoor (libs/v9/experience/src/wonder-front-door.ts) is the tested view-model the "ask a wonder" screen binds to. It wraps resolveWonder in a try/catch: on success it returns the disambiguated resolved candidate, the ranked candidates, the prerequisiteCount ("the scaffolding depth"), the conceptSet, and a few suggestedWonders; on a thrown WonderUnresolvedError it returns a fully-formed unresolved: true state with empty fields, so the UI degrades to "try rephrasing" rather than crashing or faking a concept.

The two "surprise me" mechanisms are worth distinguishing, because they are different code with different intent. The front door's suggestFrontier is a cheap adjacency walk: a concept that some edge leads to from inside the teaching set, but that is not itself in the set — the immediate next thing. Theia owns the richer one: surpriseMe (in libs/v9/theia/src/theia.ts) ranks unexplored concepts by readiness — the fraction of a concept's prerequisites the learner has already mastered — surfacing "the frontier of what they almost know." Theia also owns thread continuation (nextWonders walks the Atlas edges in the priority order enables → bridges → specializes → related) and the emotional arc; those are documented in threads-and-mastery-loop.md.

The conceptSet does not stop at the front door. It is the seam into the lesson forge: V9LessonArtifact (libs/contracts/src/v9/lesson.ts) carries conceptSet: z.array(V9ConceptIdSchema).min(1).max(64) as part of its cache-key digest, and the Prometheus forge's stage 0 is resolveWonder. So the same resolution that decides what to show on the front door also seeds what gets forged, gated, and cached — see prometheus-lesson-forge.md.

Edge cases, failure modes, and configuration#

The Atlas's correctness story is mostly about what it refuses to do:

  • Empty Atlas / no match ⇒ fail loud. resolveWonder throws WonderUnresolvedError if the store has no nodes or nothing scores above zero. It never returns a "best guess" concept.
  • No lexical overlap ⇒ needs the Nous seam. A wonder phrased entirely outside a concept's vocabulary will not be found by BM25 alone; bridging it is the injected embedder's job, and the system is honest that the seam is required rather than approximating semantics with a stand-in.
  • Cyclic prerequisite graph ⇒ graceful fallback. topologicalSort throws on a cycle (the Mnemosyne engine refuses to invent an order); orderByPrerequisite catches that and falls back to the given order rather than crashing the resolution.
  • Ungrounded merges ⇒ skipped, not coerced. As above, a merged node that cannot satisfy an invariant is dropped into skipped; the unifier never back-fills a fake pin or kernel to force it through.
  • Dangling / self-loop edges ⇒ dropped, counted. Both the adapters and the unifier drop edges with a missing endpoint or from === to, and the unifier surfaces the count in droppedEdgeCount so truncation is visible.
  • The demeter gap is admitted, not hidden. The contract permits a Demeter kernel, but no binder or evaluator exists yet — a STEM leaf needing Demeter would have to wait on that wiring, which the registry will reject loudly (an unknown kernel ref is a G2 accuracy-gate failure) rather than silently passing.

Configuration is the WonderResolutionOptions bag: learnerMastery (the ReadonlyMap<V9ConceptId, MasteryLevel> that drives gap filtering), topK (default 5), masteryFloor (default 'intermediate'), embedder (the Nous boundary), and denseWeight (default 0.5, clamped to [0,1]). One implementation detail worth knowing: because V9ConceptId and Mnemosyne's KGNodeId are both opaque string ids, the store performs a structural cast between them when it projects into the engine. That cast is sound here precisely because the Atlas's content-addressed ids are stable and unique across sources — the same property the unifier exists to guarantee.

How it connects#

The Atlas is the substrate every other V9 subsystem reads from. Aletheia recomputes the values its kernelRefs point at (G1/G2/G6); Hephaestus and Prometheus consume the conceptSet; Theia walks its edges for threads and "surprise me"; the mastery loop feeds learner MasteryLevels back in to re-scope the next wonder. For the surrounding mechanics: