Hermes lane: spec + route. Hermes researches, recommends, and specs the content/data contract. Build is owned by viska-n8n / proteus / mnemosyne per the lane table (§5). This document is a build-ready design, not a build order.
Viska does very heavy daily RAG retrieval + ingestion (financial documents, broker research, filings, market data). Today that is a pure vector-RAG pipeline (#86: pgvector + bge-m3, hybrid BM25/dense RRF, contextual retrieval, table-aware chunking). Vector-RAG's structural weakness — it re-derives every connection on every query and nothing accumulates — is exactly the weakness OKF's "AI-maintained wiki" pattern attacks.
The system has two tiers of knowledge, two indexes, one substrate:
| Tier | Holds | Index | Read by |
|---|---|---|---|
| Synthesis | OKF bundle — AI-maintained, cross-linked concept pages (an issuer, a thesis, a metric, a holding) with hard provenance | index.md-first progressive disclosure + graphify graph nav |
agents (MCP), humans (Git/Obsidian/dashboard) |
| Retrieval | raw source chunks (the immutable documents themselves) | pgvector hybrid (the #86 stack — kept, not retired) | the Ingest/Query engine, for fine recall + citation backing |
The net-new build is the engine that maintains the synthesis tier — Karpathy's three standing operations (Ingest / Query / Lint) that Google's OKF spec deliberately dropped. Everything else is adopt-not-build (rule #18): OKF is a one-page format, graphify exists, pgvector/bge/n8n/Supabase are already live in Viska.
Two forks closed this pass (were the blockers on Section 2):
qmd) past
that; Viska is well past that at heavy daily volume. Resolution: graphify indexes the
synthesis tier (small, ~10²–10³ concept pages); pgvector-hybrid stays as the retrieval tier
for the raw corpus. pgvector does not fully retire — it retires as the synthesis store,
persists as the raw-source retrieval index. This redefines the parity gate (§4).These were settled in brainstorming (2026-06-24) and re-anchored on the fleet verdict; restated here as the frame for §2, not re-opened.
index.md-first (progressive
disclosure) then graphify graph nav. Humans get the same folder for free — Git history,
Obsidian, or the Viska dashboard. One artifact, two renderings; the MCP/graph view is
authoritative.One file = one concept. For Viska the concept taxonomy (the type: vocabulary — OKF leaves
this to the producer, §6 limitation 2; Viska fixes it because it controls its own vocabulary):
type: |
One page per | Example |
|---|---|---|
Issuer |
security / company | issuers/novo-nordisk.md |
Holding |
current position | holdings/novo-nordisk.md (links its Issuer) |
Thesis |
investment thesis / view | theses/glp1-supply-constraint.md |
Metric |
a defined number + how it's computed | metrics/portfolio-twr.md |
Event |
filing / earnings / rating change | events/2026-q2-novo-earnings.md |
Source |
an immutable raw document (pointer, not body) | sources/novo-10q-2026q2.md |
Reserved files per OKF: index.md (folder TOC — agent reads first) and log.md (dated
changelog). Every concept page carries the optional-in-OKF-but-mandatory-in-Viska # Citations
block (§3). Cross-links are ordinary markdown links; those links are the graphify graph.
The engine is the maintenance machinery OKF dropped. It is not new infrastructure — it is three operations expressed on Viska's existing substrate: n8n (orchestration), Supabase (canonical state + provenance + the retrieval index), pgvector + bge-m3 (embeddings), graphify (synthesis-tier index), and the OCR/parse vendor from #86 (LlamaParse-Agentic lean, pending the bake-off — hermes-032/#86).
┌─────────────────────── INGEST (per-source, n8n) ──────────────────────┐
raw document ──▶ parse/OCR (#86 vendor) ──▶ chunk + embed ──▶ pgvector (RETRIEVAL tier)
(filing, │
research, ▼
market data) maintainer pass (LLM, prompt-partitioned)
│ reads affected concept pages via Query
│ drafts/updates concept page(s) + index.md + log.md
▼
┌── GUARDRAIL GATE (§3, deterministic) ──┐
│ provenance? number-grounded? no │
│ un-superseded contradiction? │
└──────────────┬──────────┬──────────────┘
PASS ──▶│ │◀── FAIL ──▶ quarantine + Lint queue
▼
OKF bundle (SYNTHESIS tier, Git + Supabase registry)
│
QUERY (MCP, read-side) ◀───────────────────────────┤
agents: index.md-first → graphify graph nav (synthesis) LINT (nightly n8n cron)
+ pgvector hybrid retrieval (raw recall + citations) contradiction / staleness /
humans: Git / Obsidian / dashboard (same folder) orphan / provenance audit
Trigger: a new raw document lands (n8n watcher on the source — filing feed, broker drop, market pull). Per document:
Source concept
(pointer + metadata, never the mutated body) and embed its chunks into pgvector (bge-m3,
table-aware chunking, contextual-retrieval prefixing — the #86 stack unchanged). This is the
retrieval tier; it is always written, regardless of the synthesis pass.Issuer/Holding/Thesis/Metric
pages the document affects, append to log.md, refresh the affected index.md entries, and
attach # Citations pointing at the Source + chunk offsets. The human does not write the
wiki; the AI does (Karpathy's core inversion). Autonomous — no approval step (L3).Idempotency: dedupe on source content-hash (a re-delivered filing must not double-write).
Concept pages are addressed by stable concept-id (type/slug); a second source for the same
issuer updates the page, it does not fork it.
Agents read over MCP; the query router picks the tier by question shape:
| Question shape | Tier | Mechanism |
|---|---|---|
| "What is our thesis on X?" / "summarise our view of issuer Y" | Synthesis | index.md progressive disclosure → open the 1–2 concept pages → graphify get_neighbors / shortest_path for related concepts. A finished answer, synthesis paid once. |
| "Find the exact passage / number behind claim Z" / fine recall over the raw corpus | Retrieval | pgvector hybrid (BM25 + dense RRF, bge-reranker) over raw chunks — the #86 path, kept. |
| Mixed ("thesis + the filing line that supports it") | Both | synthesis page for the answer; its # Citations resolve into retrieval-tier chunks for the backing. |
index.md-first is the cost win (OKF §5 progressive disclosure): the agent reads a small
TOC and opens only what it needs — it never ingests the whole library per query, which is the
vector-RAG cost/precision failure mode.query_graph, get_neighbors,
shortest_path, god_nodes) — Tier-1 lens from the two-tier spec, pointed at the OKF bundle.
Because the synthesis tier is ~10²–10³ pages, graphify is well inside its proven scale.Lint is what keeps the bundle from going stale and confidently lying — OKF's most-cited limitation (REPORT §6, limitation 1). It runs nightly (n8n cron) over the whole bundle.
Engine (mnemosyne, fleet-standard) — the checks:
| Check | What it catches |
|---|---|
| Contradiction | two concept pages (or a page vs a fresh source) assert conflicting facts with no supersedes link |
| Staleness | a page's youngest citation is older than its type's freshness window (market data: hours/days; thesis: weeks) |
| Orphan | a concept page with no inbound links and not in any index.md |
| Broken link | a markdown link / citation that resolves to nothing |
| Provenance audit | any factual claim (esp. a number) on a page with no backing # Citation — the inverted-OKF hard rule |
Output: a Lint report (Supabase row + dashboard surface) and, for hard-fail classes (provenance, fabricated number), auto-quarantine the offending page until repaired. Lint is the standing enforcement of the §3 guardrail over time (the gate enforces at write; Lint re-enforces against drift, supersession, and newly-arrived contradicting sources).
Policy (Viska-instance, viska-pm) — the config the engine reads:
type freshness windows (a Metric/portfolio-twr is stale in a day; a Thesis in weeks)Fork resolved: the engine is not rebuilt per-agent (rule #18 — it is the net-new fleet component named in the ratified verdict, mnemosyne's to build once). Viska is its first consumer and supplies only policy. If mnemosyne's fleet Lint is not yet shippable when Viska needs it, the fallback is a thin Viska-local Lint implementing the same check contract, to be retired into the fleet engine on parity — a strangler-fig within the strangler-fig, flagged as a sequencing risk (§6), not a license to fork the engine permanently.
The verdict says index=graphify. Taken literally as "graphify is the only index" it
contradicts the originator's own scale guidance and Viska's verified heavy-volume reality. The
faithful reading: graphify is the index of the synthesis tier — the layer the verdict is
actually about (the AI-maintained wiki). The raw-corpus retrieval index (pgvector hybrid) is a
different layer the verdict never displaced; #86 built and verified it for exactly Viska's volume.
This mirrors the fleet two-tier graph spec (graphify per-repo lens + a distinct master synthesis)
and Karpathy's mature pattern: folder plus search, not folder instead of search.
The deterministic gate L3 promises and §2.1 step 4 invokes. OKF SPEC §9 tells bundles to tolerate all mess and forgive everything because they are "partially agent-generated." A fund tool inverts that: a wrong number in a thesis is a liability, not acceptable noise. The gate rejects bad writes rather than forgiving them — and it is the same contract whether called at write-time (the maintainer pass, §2.1 step 4) or nightly (Lint, §2.3).
A drafted delta PASSES only if every predicate passes; any FAIL quarantines the delta (§3.3). Each predicate is a pure function of (delta, current bundle, retrieval tier) — no model judgement, no network — so it is deterministic and re-runnable. Expressed once; the maintainer pass calls it write-time, Lint calls the identical set nightly over the whole bundle (drift re-enforcement).
| # | Predicate | FAIL when | Class |
|---|---|---|---|
| P1 | Provenanced claim | a factual assertion (esp. a number) on a concept page has no # Citation resolving to a retrieval-tier chunk from a real Source |
hard |
| P2 | Grounded number | a number does not trace to a pgvector retrieval chunk of a real Source (generative invention — recall #86: a VLM hallucinated "PFOF on Pro tier"; no generative pass may mint a value) |
hard |
| P3 | Superseded contradiction | the delta conflicts with an existing page claim and carries no explicit supersedes link to the claim it replaces |
hard |
| P4 | Well-formed concept | missing required type:, broken self-link, or dangling/unresolvable citation |
hard |
One predicate set, two surfaces. The gate is not a write-time check and separately a Lint check — it is a single callable contract (proteus-owned, invoked from both n8n workflows). The maintainer pass enforces it before commit; Lint re-runs it against drift, supersession, and newly-arrived contradicting sources (§2.3 Provenance/Contradiction rows ARE P1/P3 over time). A predicate change lands in one place and both surfaces inherit it.
The maintainer LLM reads untrusted document text (filings, broker drops, scraped research) — treat every byte as adversarial, and treat downstream consumers of the bundle the same way. Carry the RSS-cleanup arc's mitigations verbatim (already named in §2.0):
| Mitigation | Where |
|---|---|
| Prompt-partition the document body — never concatenate untrusted text into instruction context | maintainer pass input framing |
| Tool-free pass — the maintainer node has no tool access while processing untrusted input (it drafts; it cannot act) | §2.0 AI-Agent node config |
| Strip zero-width Unicode before processing (defeats hidden-instruction injection) | parse stage, pre-LLM |
| Source-authenticate where applicable — DKIM/DMARC for email-sourced docs; reject/flag unauthenticated | Ingest watcher |
| Sanitize at write-time — escape/normalise before the delta touches the bundle | gate, pre-commit |
A rejected delta is quarantined, never silently dropped — a dropped delta is invisible drift; a quarantined one is auditable.
| Question | Answer |
|---|---|
| Where | a quarantine Supabase row (delta payload + failed-predicate codes + source-id + timestamp), proteus DDL §5 — no new infra (rule #18), same registrar substrate |
| How surfaced | the nightly Lint report (§2.3) + the Viska dashboard quarantine queue |
| Who clears | operator / viska-pm review — repair-and-resubmit (re-runs the gate) or discard with reason |
Invariant (reinforces §2.1 step 4): a synthesis-write rejection NEVER blocks the retrieval-tier
write or raw-source persistence. Steps 1–2 (raw Source + pgvector embed) commit unconditionally;
the gate stands only between the maintainer draft and the synthesis bundle. Retrieval stays fully
available — citable, queryable — even while its synthesis is quarantined. Worst case degrades to
"raw recall works, synthesis pending review," never "the source is lost."
The strangler-fig cutover (§1 L1), now precisely scoped by the §2.4 two-index resolution: graphify indexes the synthesis tier; pgvector-hybrid stays as the retrieval tier. Migration is a re-routing of the synthesis read-path, not a teardown.
| Layer | Today | After cutover | Disposition |
|---|---|---|---|
| Synthesis ("what's our thesis on X", cross-concept answers) | re-derived from vector hits per query, nothing accumulates | read from AI-maintained concept pages (index.md-first → graphify) |
migrates |
| Retrieval (#86 stack: pgvector + bge-m3, hybrid BM25/dense RRF, bge-reranker, contextual retrieval, table-aware chunking) | serves both synthesis and fine recall | serves fine recall + citation backing only | stays — DEMOTED, not deleted |
The demotion is unambiguous: pgvector loses its role as the synthesis store and keeps its role as the raw-source retrieval index. The #86 retrieval stack is touched in zero ways — same embeddings, same hybrid scorer, same reranker, same chunking. The only thing that changes is the Query router (§2.2) ceasing to route synthesis-shape questions through it.
This resolves the open question "what metric defines the parity gate" (devlog 2026-06-24). The gate is a labelled eval set of real Viska analyst questions with gold answers, run against both paths (synthesis tier vs current vector-RAG baseline). The synthesis tier must match-or-beat the baseline on (a)(b)(d) and score zero on (c) before vectors are demoted.
| # | Dimension | Measure | Pass threshold (proposal — viska-pm sign-off) |
|---|---|---|---|
| a | Answer-correctness | graded vs gold answer (0–2: wrong/partial/correct) | synthesis mean ≥ vector-baseline mean |
| b | Citation-correctness / provenance | every claim resolves to a real # Citation → retrieval chunk that supports it |
≥ baseline, and ≥ 0.95 of claims correctly cited |
| c | Zero-fabrication | any answer asserting a number/fact with no supporting source | HARD FAIL — a single fabricated number fails the whole gate (target = 0) |
| d | Latency | wall-clock p50 / p95 per answer | p95 ≤ 1.25× baseline p95 |
(c) is non-negotiable and not averaged: one fabrication blocks demotion regardless of (a)(b)(d).
| Property | Spec |
|---|---|
| Size | 50–100 questions (proposal — viska-pm to size against question volume) |
| Composition | ~40% synthesis-shape ("what is our thesis on X", "summarise our view of issuer Y"), ~40% retrieval-shape ("find the exact number / passage behind Z"), ~20% mixed (thesis + the filing line backing it) — mirrors the §2.2 router shapes |
| Sourcing | viska-pm-owned input — viska-pm sources the real analyst questions and gold answers (lane §5). Synthetic questions do not count toward the gate. |
| Grading rubric | per-question score on the four §4.2 dimensions; (a) human/LLM-judge against gold, (b)(c) deterministic against the §3 provenance predicates, (d) measured. Stored as a Supabase eval-run row + dashboard surface. |
| Re-run cadence | on every synthesis-engine change (maintainer-prompt, guardrail, graphify config) and mandatorily immediately before demotion; a passing run is valid only against the engine version that produced it. |
Because the retrieval-tier pgvector index is untouched, rollback is a router flip, not a restore: re-route synthesis-shape queries back through the vector path. Zero data loss, no reindex, no migration to reverse — the demoted-not-deleted invariant is the rollback guarantee.
Gated on §3. The (c) zero-fabrication and (b) provenance checks reuse the §3 guardrail predicates (unprovenanced claim; number not traceable to a retrieval chunk) — one predicate set, now a third surface (write-time gate, nightly Lint, and the parity eval). The parity gate cannot pass until §3 is implemented.
| Work | Owner |
|---|---|
| This spec + content/data contract + routing | Hermes (this doc) |
| Ingest + Query n8n workflows (clone PDF-feed pattern; maintainer AI-Agent node) | viska-n8n / pantheon-ops |
| Supabase DDL — concept registry, provenance ledger, retrieval index, Lint reports | proteus |
| Lint engine (fleet-standard check contract) + graphify Tier-1 install path | mnemosyne |
| Lint policy config + parity-gate eval set + sign-off | viska-instance (viska-pm) |
| OCR/parse vendor (feeds Ingest §2.1 step 1) | resolved by hermes-032 / #86 bake-off |
type/slug is
enough; but if Viska's synthesis ever feeds fleet AION Tier-2, the repo:concept namespacing
from the two-tier spec §9 applies. Flag at the AION-feed boundary, not now.