Metadata-Version: 2.4
Name: eoa-mcp
Version: 1.0.6
Summary: Encyclopedia of Abstractions — local, read-only MCP server over the published corpus snapshot
Author: Kurt Zoglmann
Project-URL: Homepage, https://abstractopedia.org/
Project-URL: MCP server, https://abstractopedia.org/mcp/
Project-URL: Downloads, https://abstractopedia.org/downloads/
Keywords: mcp,model-context-protocol,abstractions,knowledge-base,abstractopedia
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Classifier: Environment :: Console
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: mcp<2,>=1.0.0
Requires-Dist: PyYAML>=6.0
Requires-Dist: jsonschema>=4.18
Requires-Dist: numpy>=1.24
Requires-Dist: onnxruntime>=1.16
Requires-Dist: tokenizers>=0.15

# Encyclopedia of Abstractions — MCP server

> **Layout since 2026-09-20.** The server is the installable package
> `eoa_mcp/` in this directory (`pyproject.toml`, console script `eoa-mcp`).
> `server.py`, `data_loader.py`, `smoke_test*.py` here and
> `scripts/search_engine.py` are compatibility shims that alias the package,
> so `<venv>/bin/python mcp_server/server.py` registrations and
> `python scripts/search_engine.py …` build steps keep working unchanged.
> Development install: `.venv/bin/pip install -e .` from this directory.
> Distribution: `python scripts/package_mcp.py` (wheel + data bundle +
> release pointer into `dist/mcp/`), then `mcp_server/install.sh` for the
> end-user path. Data root resolution: `eoa_mcp/paths.py`. The counts and
> the tool table below are from the prototype era and are superseded by the
> generated /mcp/ site page (tool count: 48).


Phase 9 prototype of the Model Context Protocol server that exposes the
Encyclopedia of Abstractions corpus to MCP-compatible clients (Claude
Desktop, Claude Code, Cline, etc.).

This is a **prototype**, not a production server. Wave 2 of the ChatGPT
solution-archetype work may reclassify some archetypes; the slug-aliasing
layer (`aliases.yaml`) is designed to absorb those changes without API
redesign.

## What it serves

- **1,402 prime abstractions** from `prime_abstractions/v2/` (count per
  `dist/encyclopedia.manifest.json`; `corpus_stats` reports the loaded truth)
- **1,212 domain-specific abstractions** from `domain_specific_abstractions/v2/`
  (separate slug namespace; searchable via `search_domain_specific`)
- **1,134 solution archetypes** from generated and hand-curated sources
- **The canonical typed mixed DAG**: 2,614 typed nodes and 4,324 logical
  relations across prime→prime, domain-specific→prime, and
  domain-specific→domain-specific topology
- **The diagnostic applicability graph**: a separate factor graph preserving
  archetype DNF, prime/DSA trigger groundings, supporting context, gates, and
  open residuals without treating them as hierarchy relations
- **An explicit prime-only compatibility projection** for existing MCP callers
  of `get_prime_lineage` and `get_prime_siblings`
- **Components and mechanisms registries** harvested from each archetype's
  bundle YAML
- **Slug-aliasing** so legacy short slugs resolve to canonical
  v2 slugs (`pattern_design` → `pattern_in_design`, etc.)
- **The 9-step Augmented Abstract Reasoning Pipeline** as a structured
  guide
- **The ELI ladder** for 1,305 primes from `dist/learnability.jsonl`: five
  reviewed explanation rungs, everyday names, and the reviewed teaching order
- **Entity version identity** from `dist/versions.jsonl`: every entity
  response carries its version and `content_hash`
- **Substrate-independence grading** carried through the export for primes
  (complete) and domain-specific abstractions (sparse)

The server is **read-only**. There are no `update_*` or `create_*` tools.
Restart the server to pick up corpus changes.

## Tools

| Tool | Args | Returns |
|---|---|---|
| `search_prime` | `query: str`, `limit: int = 10` | List of `{slug, name, snippet, origin_domain}` |
| `get_prime` | `slug: str`, `verbosity: 'one_liner'\|'summary'\|'full' = 'full'`, `eli_level: ''\|'eli5'\|'eli10'\|'eli15'\|'eli18'\|'specialist' = ''` | Prime record at the requested verbosity (full ≈ 21 KB avg — prefer summary for triage), plus the entity's `version` block and, on request, one rung of the ELI ladder |
| `get_primes` | `slugs: list[str]` (≤50), `verbosity = 'summary'` | Batch fetch with per-slug alias resolution; unknown slugs under `not_found` |
| `list_primes` | filters: `category`, `origin_domain`, `status`, `sf_label`, `contains`, `min_substrate_independence`; `limit`/`offset` | Metadata browse; no-arg call returns the facet census (counts per category / domain / status / sf_label / substrate-independence score) |
| `search_domain_specific` | `query: str`, `limit: int = 12`, `domain: str = ''` | Semantic search over domain-specific abstractions (shared embedding space with primes) |
| `get_domain_specific` | `slug: str`, `verbosity = 'summary'` | Domain-specific record (separate slug namespace from primes) |
| `list_domain_specific` | filters: `domain`, `subdomain`, `contains`; `limit`/`offset` | Browse domain-specific corpus; no-arg call returns counts per candidate_domain |
| `search_abstraction` | `query`, `limit`, `node_kinds`, `mode`, `domain` | Unified typed search across primes and domain-specific abstractions; returns `node_id`, `node_kind`, and route |
| `get_abstraction` | `node_id`, `verbosity = 'summary'` | Fetch either abstraction kind by typed ID or globally unique bare slug |
| `search_archetype` | `query: str`, `limit: int = 10` | List of `{slug, name, canonical_family, snippet}` |
| `get_archetype` | `slug: str` | Full archetype record (source/related primes, components, mechanisms, action logic, ...) |
| `search_archetypes_by_problem` | `query: str`, `limit: int = 12` | Uncalibrated diagnostic candidates with matched predicates and still-unretrieved conjuncts |
| `get_archetype_applicability` | `slug: str`, `verbosity: 'summary'\|'full' = 'summary'` | Diagnostic problem, DNF condition sets, trigger groundings, residuals, context, and gates |
| `find_archetypes_for_abstraction` | `node_id: str`, `role: 'trigger'\|'supporting_context'\|'any' = 'trigger'`, `limit: int = 50` | Archetypes whose exact diagnostic conditions are grounded by a typed prime/DSA, with remaining route requirements |
| `find_abstractions_for_archetype` | `slug: str` | Prime/DSA trigger groundings and separately labeled supporting-context groundings |
| `get_prime_lineage` | `slug: str`, `direction: 'up'\|'down'\|'both' = 'both'`, `edge_types`, `max_depth: int = 3`, `node_cap: int = 60` | Prime-only compatibility walk with the historical bare-slug response shape |
| `get_prime_siblings` | `slug: str`, `edge_types = 'subsumption'`, `limit_per_parent: int = 25` | Prime-only compatibility co-children grouped by parent |
| `get_abstraction_lineage` | `node_id`, `direction`, `edge_types`, `max_depth`, `node_cap` | Canonical mixed-DAG walk with typed nodes, routes, and complete schema-v2 relation proofs |
| `get_abstraction_siblings` | `node_id`, `edge_types`, `limit_per_parent` | Typed co-children across prime/domain boundaries; mutual peers remain separate |
| `find_archetypes_for_prime` | `slug: str`, `relation: 'any'\|'source'\|'related' = 'any'` | Compatibility-only discovery/catalog references; explicitly not trigger evidence |
| `find_related_primes` | `slug: str`, `depth: int = 1` | List of co-occurring primes via the catalog (depth 1 only) |
| `list_components` | `min_archetype_count: int = 3` | Components sorted by archetype recurrence |
| `find_archetypes_using_component` | `name: str` | Archetype slugs whose components include `name` |
| `list_mechanisms` | `min_archetype_count: int = 2` | Compatibility recurrence view; may omit most mechanisms—prefer `browse_mechanisms` for discovery |
| `browse_mechanisms` | classification filters, `contains`, `documented`, `group_by`, `sort`, `limit`/`offset`, `include_facets` (default off) | Complete paginated mechanism browse with bounded grouping, opt-in post-filter facet counts, and direct/inherited classification provenance. Family definitions are emitted once per response in a top-level `taxonomy` block; `facet_limit`/`group_limit`/`items_per_group` scale from `limit` unless pinned |
| `search_mechanism` | `query`, optional classification/archetype/documentation filters, `limit` | Hybrid semantic/lexical implementation search; problem language is intentionally excluded from the mechanism vector, and a missing/stale semantic index returns an actionable error rather than degraded results |
| `find_mechanisms_for_archetype` | `slug`, optional `form_family`/`documented`, `limit`/`offset`, `group_by_form`, `include_facets` (default off) | Enriched mechanisms for one archetype, grouped by concrete form |
| `find_mechanisms_for_problem` | `query`, `archetype_limit`, `mechanisms_per_archetype`, `documented` | Evidence-preserving problem → diagnostic archetype → form-diverse mechanism candidate traversal. Candidates are slim (identity, operation, own primary archetype): the enclosing path states the problem/solution families and the enclosing group states the form family — call `get_mechanism` for a candidate's full classification |
| `find_archetypes_using_mechanism` | `name: str` | Archetype slugs whose mechanisms include `name` |
| `search_plain_language` | `query`, `limit` | Everyday-English retrieval over the ELI ladder: hybrid semantic + exact-name lexical, the complement of `search_prime`'s meta-model surface. Degrades to `lexical_fallback` when the ladder index is absent |
| `get_learning_path` | `tier`, `after`, `limit` | Reviewed teaching order over primes: tier census, sequence walk, resume-after |
| `get_domain_distance` | `abstraction: str`, `domain: str`, `context_limit: int = 5` | Quantitative home→target domain distance for a prime/DSA: centroid cosine + size-normalized structural coupling, entity-weighted percentile ranks, composite band (near/moderate/far/very_far), rank among domains, nearest/farthest context. Deterministic; semantic component degrades gracefully without embeddings |
| `corpus_stats` | (none) | Counts, manifest, last-loaded timestamp, publication state, ELI-ladder coverage, grading coverage |
| `get_reasoning_pipeline_guide` | `verbosity: 'summary'\|'full' = 'full'`, `include_worked_example: bool = False` | Structured 9-step pipeline guide |
| `get_reference` | `reference: str`, `include_claims: bool = True`, `limit: int = 25` | A source's record plus **every claim in the encyclopedia resting on it** — the reverse of a bibliography. Accepts a `ref:` id, a bare hex, a retired id, a DOI (bare or doi.org URL), or a footnote key; an ambiguous footnote key returns its candidates rather than guessing |
| `get_references` | `article: str`, `limit: int = 200` | An entry's bibliography as structured records instead of markdown footnotes, with the claim and annotation per citation, and `cited_inline` separated from `bibliography_only` |
| `find_shared_sources` | `a: str`, `b: str`, `limit: int = 25` | Sources two entries have in common, rarest first, with Jaccard over their source sets |
| `find_related_by_sources` | `article: str`, `limit: int = 12` | Bibliographic coupling — entries leaning on the same literature. Cosine over IDF-weighted source vectors, so a shared work that 250 entries cite counts for far less than one only these two cite. A relatedness signal derived from evidence rather than editorial judgement, so it can disagree with the curated DAG |
| `get_claim_support` | `article: str`, `fact_anchor: str = ''`, `limit: int = 50` | The source behind one FACT anchor — the sentence, the source, and the annotation stating what that source was taken to establish (often narrower than the sentence). Empty anchor returns every anchored claim in the entry |
| `search_references` | `query`, `limit`, `type`, `base`, `year_from`, `year_to`, `has_doi`, `min_cited_by` | Lexical search over author/title/container/year with facets. Every term must match (AND), so `simon complexity` does not return everything by anyone named Simon |
| `get_canon` | `limit: int = 25`, `domain: str = ''` | Works the encyclopedia leans on hardest, ranked by **distinct citing entries** rather than total citations |
| `reference_registry_stats` | (none) | Registry size and coverage: works, citations, claim-anchored citations, link coverage, and the type/base/link census — including what is honestly `unclassified` |

## Explanation register, version identity, grading

Three corpus layers that the website already consumed and the MCP could not
see. All three degrade honestly: a missing artifact is reported with the
command that regenerates it, never as an empty result.

**The ELI ladder** (`dist/learnability.jsonl`). Every covered prime carries
five reviewed explanation rungs — `eli5`, `eli10`, `eli15`, `eli18`,
`specialist` — plus everyday names for the lower three, a tier (1-5) and a
place in a global teaching order. This is canonical prime content, not a
side-artifact: `scripts/versioning.py` hashes `eli_ladder` as a part of a
prime's `content_hash`, so editing the ladder changes what the prime *is*.

`eli_level` on `get_prime` is a different axis from `verbosity`: verbosity
chooses how much of the record you get, the ladder chooses who it is written
for. It is off by default because the ladder runs ~1,300 words per prime.
Eighteen primes have no `eli5` — the corpus declines to claim a five-year-old
explanation of entanglement rather than inventing a misleading one — and those
return `available: false` with the lowest valid rung named. Do not simplify a
higher rung in its place.

Coverage is 1,305 of 1,402 primes; the export skips retired curriculum rows.

`search_plain_language` is the retrieval half. `search_prime` embeds
domain-stripped structural signatures and tells callers to strip domain
vocabulary first, which makes it strong on abstract structure and blind to
ordinary phrasing. The ladder holds exactly the missing register.

It is **hybrid**, on the same pattern as `search_mechanism`:

- **A semantic index over the ladder text** — `ladder_emb.latest.npy`, built
  by `search_engine.py ensure-ladder` — does the paraphrase. This is the
  primary lane and the reason the tool exists: only a vector can connect "a
  small change tips the whole system" to a prime whose everyday name is
  "Sudden flip", because the two share no word. The projection embeds the
  everyday names (repeated, so four high-signal words are not drowned by two
  paragraphs), the prime name, and `eli5` + `eli10`. `eli15` and above are
  technical registers that restate what the structural index already covers;
  including them would pull this projection back toward the one it exists to
  complement, and editing them costs no rebuild.
- **An exact lexical lane** keeps verbatim everyday names deterministically on
  top — "magic twin coins" *is* `entanglement`'s everyday name and should be
  rank 1 by identity, not by cosine luck. It stems ("copies" / "Copying the
  Crowd"), drops corpus stopwords (`the` is in 1,300 of the 1,305 ladders)
  above a 0.5 document ratio — a threshold sitting in a real gap between
  concept words (`chang` 0.28) and noise (`you` 0.87) — and weights by field,
  taking the best rather than the sum so a 200-word `eli10` cannot win on
  surface area.

Measured live against the built index, the semantic lane does the work the
lexical one could not:

| query | lexical only | hybrid |
|---|---|---|
| a small change tips the whole system | `broken_windows_theory` | `leverage_points` ("Tiny push, big change") @ 0.71 |
| when everyone copies everyone else | `information_cascade` at rank 9 | `herding_behavior` ("Following the Crowd") at rank 3 |
| magic twin coins | `entanglement` | `entanglement`, 3x the runner-up's fused score |

Its residual limit is **negation**, and it is not a tuning problem.
`linear_independence`'s everyday name is "No Copies Allowed", and it comes
back first for "when everyone copies everyone else" in *both* lanes — the
embedding scored it 0.7239 against 0.7137 for `herding_behavior`. Sentence
embeddings represent negation poorly; `test_learnability.py` asserts this so
it stays visible rather than being rediscovered.

Coverage is the other boundary: 97 primes have no ladder row and are
unreachable here, which the `coverage` block reports on every response.

Ranks fuse by RRF; scores do not, because the two lanes' numbers are not
commensurable. If the ladder index is missing or stale the tool degrades to
`mode: "lexical_fallback"` with a `degraded` block naming the rebuild command,
rather than failing closed — the lexical lane is a real retrieval method and
the caller is usually a person groping for a word, not a pipeline making a
claim. In that state paraphrase is off, and the block says so, so a weak
result is not read as "the corpus has nothing".

`get_learning_path` walks the tier census and the global `order` sequence —
the only ordering in the corpus that reflects a teaching judgement rather than
a graph or a score.

**Version identity** (`dist/versions.jsonl`). Each entity response now carries
a `version` block: the monotonic version, the `content_hash`, and the
extractor version. Provenance travels with the entity rather than living in a
separate lookup, and the hash exposed is the *meaning* hash — the one that is
stable across a reformat that changes no claim — not the build-side
`source_revision_sha256`. `corpus_stats.versioning` reports corpus-level
state; at v0 it says plainly that nothing has been published, so a caller
knows any citation is against a moving baseline. The 26 MB file is loaded as a
projection: part hashes and source components answer build-time questions and
stay out of memory.

**Grading** (`substrate_independence`, `structural_framed`). Carried through
`scripts/mcp_jsonl_export.py` as exactly the two frontmatter blocks
`versioning.py` hashes, so the flat catalog and the version identity describe
the same thing. Ungraded entries export `null`: no grade is not a low grade,
and `min_substrate_independence` on `list_primes` excludes them rather than
scoring them zero.

One caveat worth knowing: `sf_label` — used by the `list_primes` filter and by
search annotations — comes from `dist/derived/structural_framed.latest.jsonl`,
whose own rows claim `grader: "frontmatter (curated)"`. It should therefore
agree with the canonical label by construction, and for some primes it does
not. `corpus_stats.grading.structural_framed_label` counts the gap instead of
quietly preferring one source; prefer the entity's own
`grading.structural_framed.label` when they differ.


## Response economy

The consumer of these tools is a language model spending context on every
byte, so the mechanism-discovery responses are budgeted deliberately.

**Definitions are vocabulary, not record content.** Each mechanism carries
four family classifications, and each family has a prose definition. Inlining
those definitions per record meant one browse response held 263 description
strings for 116 distinct definitions, one of them repeated 34 times. Every
multi-record response now emits a top-level `taxonomy` block — keyed by field,
then slug, giving a display name for every classification slug and a
definition for the four controlled family fields — and the records carry the
slug plus its provenance. Single-entity lookups (`get_mechanism`) keep their
names and definitions inline: there is no block to dereference, and the point
of that call is a self-contained record.

Two things deliberately do *not* enter the taxonomy block. Archetype
*essences* are entity content rather than vocabulary — 67 of them cost more
than every family definition combined — so archetypes contribute only their
name; ask `get_archetype` for the essence. `documented` renders a boolean, not
a term, so it has no slug worth dereferencing.

**The size of the answer tracks the size of the request.** `limit` used to
bound only the `results` array: a caller asking for three mechanisms still got
100 facet values per field across eight fields and 100 group boxes on top —
119k characters for three records. Facets describe the whole post-filter match
set rather than the page, so they cost the same at `limit=3` as at
`limit=100`; they are now opt-in via `include_facets`. `facet_limit`,
`group_limit` and `items_per_group` scale from `limit` when left unset, and an
explicit value still wins.

**Nested results do not restate their container.** In
`find_mechanisms_for_problem`, every mechanism under one archetype inherits
that archetype's problem and solution families, and every mechanism in one
form group shares the group's form family. Candidates there are slim records —
identity, one-line operation, own primary archetype, origin — and
`get_mechanism` supplies the rest for a candidate worth pursuing.

Measured against the same four calls before and after:

| Call | Before | After |
|---|---|---|
| `browse_mechanisms(limit=3, items_per_group=2)` | 119,264 | 22,621 |
| `browse_mechanisms()` defaults | 96,917 | 37,578 |
| `find_mechanisms_for_archetype(limit=25)` | 60,204 | 32,125 |
| `find_mechanisms_for_problem()` defaults | 190,049 | 82,513 |


## Data flow

```text
prime_abstractions/v2/*.md ──────────────┐
domain_specific_abstractions/v2/*.md ────┼─> build_dag.py
                                         │     ├─> dist/mixed_dag.* (canonical)
                                         │     └─> dist/prime_dag.* (compatibility)
solution_archetypes/* ───────────────────┼─> mcp_jsonl_export.py
                                         └─> dist/encyclopedia.*.jsonl
                                                       │
                                                       └─> mcp_server/

problem/solution/form family artifacts ────────────────┘
mechanism search projection ─> ensure-mechanisms ─> local mechanisms_emb.latest.*

validated trigger presentation ─> build_applicability_graph.py
                                 ├─> diagnostic applicability factor graph
                                 ├─> diagnostic search documents + embeddings
                                 └─> MCP applicability tools
```

**After ANY corpus change, rebuild every derived index in one shot and
restart the server:**

```bash
# From the repo root:
./scripts/rebuild_mcp_indexes.sh
```

This chains: JSONL exports → prime signature embeddings + distinctiveness +
k-means families → archetype, mechanism, and domain-specific embeddings →
typed graph → a coverage report
that hard-fails if any mechanical index doesn't cover the full corpus.
(S/F grades are LLM-produced and not rebuilt; ungraded primes surface as
`sf_label: "unlabeled"`.) The server also self-reports: every semantic
search response carries an `index_coverage` warning whenever the index
covers less than the loaded corpus, and `get_prime_neighborhood` flags
primes missing from the graph (`in_graph: false`) so empty neighborhoods
can't masquerade as "no neighbors". `corpus_stats.index_coverage` has the
full picture.

The ordinary site build conditionally maintains the mechanism semantic index
after regenerating the catalog export. Its projection hash covers only
search-relevant mechanism fields, controlled-family definitions, and primary
archetype essence, so citations and unrelated body edits do not trigger vector
generation. To run the same check directly:

```bash
mcp_server/.venv/bin/python scripts/search_engine.py ensure-mechanisms
```

If the live artifacts are current, this exits after the cheap validation. If
they are missing, stale, incomplete, or corrupt, it rebuilds and validates only
the mechanism index. The live mechanism vector and metadata files are ignored
local build products rather than committed release inputs. Restart the MCP
server after regeneration.

This guard exists because the corpus once doubled (654 → 1,325 primes)
while the embeddings stayed frozen — half the corpus was silently invisible
to semantic search for weeks.

Only needed when NEW archetype bundle batches are added
(`--preprocess` flag, or manually):

```bash
python3 scripts/mcp_preprocess.py --all-batches
python3 scripts/mcp_jsonl_export.py
```

## Install (Claude Desktop)

1. Install Python 3.11+ if you don't have it.
2. Create a virtual environment and install dependencies:
   ```bash
   cd /path/to/Encyclopedia\ Work/mcp_server
   python3 -m venv .venv
   source .venv/bin/activate
   pip install -r requirements.txt
   ```
3. Build the JSONL exports (one-time, repeat after corpus changes):
   ```bash
   cd /path/to/Encyclopedia\ Work
   python3 scripts/mcp_preprocess.py --all-batches
   python3 scripts/mcp_jsonl_export.py
   ```
4. Add the server to `~/Library/Application Support/Claude/claude_desktop_config.json`:
   ```json
   {
     "mcpServers": {
       "encyclopedia-of-abstractions": {
         "command": "/path/to/Encyclopedia Work/mcp_server/.venv/bin/python",
         "args": [
           "/path/to/Encyclopedia Work/mcp_server/server.py"
         ],
         "env": {
           "ENCYCLOPEDIA_REPO_ROOT": "/path/to/Encyclopedia Work"
         }
       }
     }
   }
   ```
5. Restart Claude Desktop. The 35 tools should appear in the tool picker.

## Install (Claude Code)

The Claude Code CLI passes the command and args as positional arguments after
a `--` separator. Env vars use `-e KEY=VALUE`. Quote paths that contain
spaces:

```bash
claude mcp add encyclopedia-of-abstractions \
    -e ENCYCLOPEDIA_REPO_ROOT="/path/to/Encyclopedia Work" \
    -- "/path/to/Encyclopedia Work/mcp_server/.venv/bin/python" \
       "/path/to/Encyclopedia Work/mcp_server/server.py"
```

Add `-s user` if you want the server available across all projects rather
than only the current project (`local` is the default scope).

If the CLI fights the spaces in your path, you can edit
`~/.claude.json` directly using the same JSON shape shown in the Claude
Desktop section above (under `"mcpServers"`).

## Tests

```bash
cd mcp_server
python3 -m unittest test_reference_registry test_typed_graph \
                   test_applicability_graph test_domain_distance
```

`test_typed_graph` and `test_applicability_graph` each have two tests that need
the embedder (`sentence_transformers`); without it they fall back to lexical
search and those two fail. That is an environment gap, not a regression — the
rest of both modules passes.

## Smoke-test (no MCP client required)

```bash
cd mcp_server
python3 -m smoke_test
```

The ELI-ladder semantic index is built like the mechanism one and is included
in `build_eoa_site.sh`; to build it on its own:

```bash
mcp_server/.venv/bin/python scripts/search_engine.py ensure-ladder
```

This loads the JSONL via `data_loader.py`, runs the tool entry points
against a few fixture queries, and prints pass/fail. See `smoke_test.py`
and `smoke_test_search.py` for the scenarios.

## Production release gate

Run the complete build-and-release gate from the repository root:

```bash
./scripts/release_acceptance_gate.sh
```

It runs the compatibility and repository suites, performs a full production
site build with Pagefind, validates the fresh typed graph, exercises the MCP
runtime, verifies deployed routes and public-download hashes, and writes
`dist/release_acceptance.json`. There is deliberately no fast or skip-build
release mode. See
`conceptual/build-and-release-acceptance-gates.md` for the failure policy and
seal contract.

## Slug aliasing

Slug arguments to all `*_prime` and `*_archetype` tools are alias-resolved.
The map is built at startup from every prime's and archetype's `aliases:`
frontmatter (2,200+ entries, slugified — `bifurcation` →
`tipping_points_or_phase_transitions`, 'Circular Causality' works too),
then overlaid with the manual `aliases.yaml` entries (manual wins).
A key that is itself a canonical slug is never treated as an alias, and an
alias claimed by 2+ records is NOT silently resolved — lookups return the
candidate list (`ambiguous_alias_of`) instead of guessing.

Edit `aliases.yaml` to pin or add entries; both sources load at server
startup.

The typed abstraction tools use a stricter graph-identity rule. Prefer
`prime:<slug>` or `domain_specific:<slug>`. A bare canonical slug is accepted
only when one typed node owns it; an ambiguous future homonym returns both
candidates and requires a typed ID. These tools do not silently apply prime
precedence or cross-namespace aliases.

## Hierarchy compatibility boundary

- `get_prime_lineage` and `get_prime_siblings` read `dist/prime_dag.*` and
  retain their existing inputs, bare-slug node records, and relation view.
- `get_abstraction_lineage` and `get_abstraction_siblings` read the canonical
  `dist/mixed_dag.*` artifacts and expose typed identity plus complete relation
  explanations.
- The mixed graph loads lazily. If it is absent or malformed, typed tools
  return `typed_mixed_dag_unavailable`; prime-only callers remain operational.
- `corpus_stats` reports both contracts separately under `curated_dag`
  (prime-only projection) and `typed_mixed_dag` (canonical).

## Out of scope

- **Auth, rate limiting, multi-user** — single-user local server.
- **Editing/writing tools** — read-only.
- **Real-time corpus refresh** — derived indexes are refreshed by builds, not
  while the read-only server is running; restart the server afterward.

Now IN scope (were prototype limitations): semantic search over primes /
archetypes / domain-specific / mechanisms (bge-small); controlled mechanism
browse and problem→archetype→mechanism traversal; the canonical typed mixed
hierarchy and prime-only compatibility projection; frontmatter-alias
resolution; metadata browse via `list_primes` / `list_domain_specific`.

## Layout

```
mcp_server/
  server.py                # FastMCP entry point + tool definitions
  data_loader.py           # JSONL loader, search, slug resolution
  learnability.py          # ELI ladder, plain-language retrieval, teaching order
  mechanism_discovery.py   # mechanism filters, facets, grouping, and hybrid fusion
  typed_graph.py           # schema-v2 mixed-DAG loader + typed resolver
  reference_registry.py    # reference registry loader, resolver, coupling, search
  pipeline_guide.py        # Structured Augmented Abstract Reasoning guide
  aliases.yaml             # Slug-aliasing map
  smoke_test.py            # Smoke-test runner (no MCP client required)
  test_learnability.py     # ELI ladder, version identity, grading contract tests
  test_typed_graph.py       # typed graph + compatibility contract tests
  test_reference_registry.py # registry index + reference tool contract tests
  requirements.txt
  README.md
```
