{{ item.proposing_actor }}{% if item.confidence is not none %} · conf {{ item.confidence }}{% endif %}
+
{{ item.proposing_actor }}{% if item.confidence is not none %} · conf {{ item.confidence }}{% set band = 'low' if item.confidence < 0.5 else ('high' if item.confidence >= 0.8 else 'med') %} {{ band }}{% endif %}
diff --git a/src/plainweave/web/templates/base.html b/src/plainweave/web/templates/base.html
index 8277c71..0b62379 100644
--- a/src/plainweave/web/templates/base.html
+++ b/src/plainweave/web/templates/base.html
@@ -18,11 +18,13 @@
IntentGoals
- operator: {{ operator.display_name }} · {{ operator.kind }}
+ {% if operator %}operator: {{ operator.display_name }} · {{ operator.kind }}{% endif %}
{# Permanent SR status live region — NEVER replaced via outerHTML; innerHTML-OOB only. #}
+ {# Visible confirmation toast mirroring #sr-status. Decorative: SR users hear #sr-status. #}
+
{# Decorative global loader; status comes from #sr-status, so this is aria-hidden. #}
diff --git a/src/plainweave/web/templates/corpus.html b/src/plainweave/web/templates/corpus.html
index c2619bc..fbb5e94 100644
--- a/src/plainweave/web/templates/corpus.html
+++ b/src/plainweave/web/templates/corpus.html
@@ -3,13 +3,16 @@
{% block title %}Corpus · Plainweave{% endblock %}
{% block main %}
{% for g in goals %}
diff --git a/src/plainweave/web/templates/intent.html b/src/plainweave/web/templates/intent.html
index 402c0ea..a2a122c 100644
--- a/src/plainweave/web/templates/intent.html
+++ b/src/plainweave/web/templates/intent.html
@@ -9,9 +9,11 @@
Intent coverage
{% if cov.ratio is not none %}{{ "%.0f%%"|format(cov.ratio * 100) }}{% else %}—{% endif %}
{{ cov.numerator }}/{{ cov.denominator }} public surfaces answer "why does this exist?"
{% if req_id %}Edit draft{% else %}New requirement{% endif %}
{% if expected_draft_revision is not none %}{% endif %}
-
+
{% endblock %}
diff --git a/src/plainweave/web/templates/review.html b/src/plainweave/web/templates/review.html
index 8a78b0c..fbbb992 100644
--- a/src/plainweave/web/templates/review.html
+++ b/src/plainweave/web/templates/review.html
@@ -19,9 +19,20 @@
Review queue
var list = document.getElementById('queue-list');
if (!list) return;
var next = list.querySelector('.queue-action-primary');
- if (next) { next.focus(); return; }
- var empty = document.getElementById('empty-queue-heading');
- if (empty) empty.focus();
+ if (next) { next.focus(); } else {
+ var empty = document.getElementById('empty-queue-heading');
+ if (empty) empty.focus();
+ }
+ // Auto-dismiss the visible toast so the confirmation stays "brief" (M9). The
+ // #sr-status live region is untouched. Token guards against an out-of-order clear.
+ var toast = document.getElementById('toast');
+ if (toast && toast.textContent.trim()) {
+ var token = (toast.dataset.token || 0) + 1;
+ toast.dataset.token = token;
+ setTimeout(function () {
+ if (String(toast.dataset.token) === String(token)) toast.textContent = '';
+ }, 4000);
+ }
});
}());
diff --git a/src/plainweave/web/views.py b/src/plainweave/web/views.py
index 7b1b721..87f17af 100644
--- a/src/plainweave/web/views.py
+++ b/src/plainweave/web/views.py
@@ -3,7 +3,7 @@
from dataclasses import dataclass
from typing import TYPE_CHECKING
-from plainweave.intent_graph import CorpusEntry
+from plainweave.intent_graph import CorpusEntry, IntentLevel, IntentNode
from plainweave.models import RequirementRecord
if TYPE_CHECKING:
@@ -51,6 +51,52 @@ def build_corpus_rows(
return rows
+@dataclass(frozen=True)
+class OrphanItem:
+ """One orphan rendered as a human-readable, linkable row (M7)."""
+
+ label: str
+ href: str | None
+
+
+@dataclass(frozen=True)
+class OrphanSection:
+ """A non-empty group of orphans at one intent altitude."""
+
+ level: str
+ items: list[OrphanItem]
+
+
+def build_orphan_sections(
+ orphans: dict[str, list[IntentNode]],
+ req_titles: dict[str, str],
+ goal_titles: dict[str, str],
+) -> list[OrphanSection]:
+ """Resolve raw orphan nodes into titled, linked rows, dropping empty altitudes (M7).
+
+ Requirement orphans link to their detail page and show their resolved title; goal
+ orphans link to the goals page and show their title; code orphans keep their SEI
+ ``node_id`` as the label (there is no human title to resolve, and the design scopes
+ titling to requirements and goals). Zero-count altitudes are omitted entirely so the
+ page never shows an empty "Orphans — X (0)" section.
+ """
+ sections: list[OrphanSection] = []
+ for level in (IntentLevel.CODE, IntentLevel.REQUIREMENT, IntentLevel.GOAL):
+ nodes = orphans.get(level.value, [])
+ if not nodes:
+ continue
+ items: list[OrphanItem] = []
+ for node in nodes:
+ if level is IntentLevel.REQUIREMENT:
+ items.append(OrphanItem(req_titles.get(node.node_id, node.node_id), f"/req/{node.node_id}"))
+ elif level is IntentLevel.GOAL:
+ items.append(OrphanItem(goal_titles.get(node.node_id, node.node_id), "/goals"))
+ else:
+ items.append(OrphanItem(node.node_id, None))
+ sections.append(OrphanSection(level=level.value, items=items))
+ return sections
+
+
def coverage_banner(cov: object) -> str | None:
if getattr(cov, "denominator_complete", True) and not getattr(cov, "adapter_degraded", ()):
return None
diff --git a/tests/web/test_app.py b/tests/web/test_app.py
index ef6d1f4..a5b85a0 100644
--- a/tests/web/test_app.py
+++ b/tests/web/test_app.py
@@ -28,9 +28,7 @@ def test_unknown_path_404(client: TestClient) -> None:
assert client.get("/no-such-page").status_code == 404
-def test_plainweave_error_renders_error_partial(project_root: Path) -> None:
- """A route that raises PlainweaveError(NOT_FOUND) must render the error partial at 404."""
-
+def _boom_app(project_root: Path) -> TestClient:
async def boom(request: Request) -> Response:
raise PlainweaveError(
ErrorCode.NOT_FOUND,
@@ -42,13 +40,61 @@ async def boom(request: Request) -> Response:
app = create_app(actor="human:alice", root=project_root)
# Splice in a test-only route at the front of the router.
app.routes.insert(0, Route("/boom", boom))
+ return TestClient(app, raise_server_exceptions=False)
- client = TestClient(app, raise_server_exceptions=False)
- resp = client.get("/boom")
+
+def test_plainweave_error_renders_full_page_on_navigation(project_root: Path) -> None:
+ """A normal navigation that raises PlainweaveError must render a full, navigable page:
+ base chrome (nav + stylesheet + ) PLUS the error detail (M2)."""
+ resp = _boom_app(project_root).get("/boom")
assert resp.status_code == 404
+ # Error detail
assert "NOT_FOUND" in resp.text
assert "thing not found" in resp.text
assert "check the id" in resp.text
+ # Full-page chrome
+ assert "' in resp.text
+ assert 'class="topnav"' in resp.text
+ assert 'class="skip-link"' in resp.text
+ # Global pending badge mechanism reaches the error page too (M6)
+ assert 'id="review-badge"' in resp.text
+
+
+def test_plainweave_error_renders_bare_fragment_on_hx(project_root: Path) -> None:
+ """An HTMX swap that raises PlainweaveError must render a bare fragment — no base chrome (M2)."""
+ resp = _boom_app(project_root).get("/boom", headers={"HX-Request": "true"})
+ assert resp.status_code == 404
+ assert "thing not found" in resp.text
+ # No full-page chrome in the fragment
+ assert " None:
+ """If the error itself was raised while building the per-request context (e.g. a
+ launch-time POLICY_REQUIRED operator / DB-open failure), the full-page error must
+ still render with chrome — the global context processor must not raise a second
+ time and collapse the actionable page into an opaque 500."""
+
+ def explode(_request: Request) -> object:
+ raise PlainweaveError(
+ ErrorCode.POLICY_REQUIRED, "operator cannot self-register", recoverable=False, hint="register first"
+ )
+
+ # Both the route and the context processor resolve ctx via request_ctx; make it fail.
+ monkeypatch.setattr("plainweave.web.app.request_ctx", explode)
+ monkeypatch.setattr("plainweave.web.routes.requirements.request_ctx", explode, raising=False)
+
+ resp = _boom_app(project_root).get("/boom")
+ assert resp.status_code == 404
+ # Helpful detail preserved (not an opaque 500)
+ assert "thing not found" in resp.text
+ # Chrome still renders despite the degraded context...
+ assert 'class="topnav"' in resp.text
+ assert 'id="review-badge"' in resp.text
+ # ...and the operator span is gracefully omitted rather than raising UndefinedError.
+ assert "operator:" not in resp.text
def test_csrf_blocks_mutation_without_token(project_root: Path) -> None:
diff --git a/tests/web/test_intent.py b/tests/web/test_intent.py
index 0b72c6b..77c17bf 100644
--- a/tests/web/test_intent.py
+++ b/tests/web/test_intent.py
@@ -3,8 +3,10 @@
from pathlib import Path
import pytest
+from starlette.applications import Starlette
from starlette.testclient import TestClient
+from plainweave.intent_graph import IntentLevel, IntentNode
from plainweave.web import views
from plainweave.web.app import create_app
@@ -20,6 +22,56 @@ def test_intent_dashboard_renders(client: TestClient) -> None:
assert "Coverage" in resp.text
+def test_intent_orphans_render_titles_and_links(client: TestClient) -> None:
+ """M7: requirement orphans show their human title linked to /req/{id}; goal orphans
+ show their title linked to /goals; zero-count altitudes are hidden."""
+ app: Starlette = client.app # type: ignore[assignment]
+ ctx = app.state.ctx_factory()
+ req = ctx.service.create_requirement("Unladdered requirement", "body", actor="human:alice")
+ # An approved-but-unladdered requirement is also an orphan; its title resolves from
+ # the approved version record rather than a draft dossier lookup.
+ approved = ctx.service.create_requirement("Approved orphan title", "body", actor="human:alice")
+ ctx.service.approve_requirement(approved.requirement_id, actor="human:alice", expected_version=0)
+ ctx.service.create_goal("Unladdered goal", "north-star", actor="human:alice")
+ html = client.get("/intent").text
+ # Requirement orphan (draft-only): human title, linked to its detail page (not the raw node id).
+ assert "Unladdered requirement" in html
+ assert f'href="/req/{req.requirement_id}"' in html
+ # Approved orphan: version-record title, also linked.
+ assert "Approved orphan title" in html
+ assert f'href="/req/{approved.requirement_id}"' in html
+ # Goal orphan: human title, linked to the goals page.
+ assert "Unladdered goal" in html
+ assert 'href="/goals"' in html
+ # No code entities indexed → the code orphan section must be hidden, not shown as (0).
+ assert "Orphans — code" not in html
+
+
+def test_build_orphan_sections_resolves_and_drops_empty() -> None:
+ orphans = {
+ IntentLevel.CODE.value: [IntentNode(IntentLevel.CODE, "loomweave:eid:abc")],
+ IntentLevel.REQUIREMENT.value: [IntentNode(IntentLevel.REQUIREMENT, "req-1")],
+ IntentLevel.GOAL.value: [],
+ }
+ sections = views.build_orphan_sections(orphans, {"req-1": "Req Title"}, {})
+ # Empty GOAL altitude dropped; CODE + REQUIREMENT kept in altitude order.
+ assert [s.level for s in sections] == [IntentLevel.CODE.value, IntentLevel.REQUIREMENT.value]
+ code_item = sections[0].items[0]
+ assert code_item.label == "loomweave:eid:abc"
+ assert code_item.href is None
+ req_item = sections[1].items[0]
+ assert req_item.label == "Req Title"
+ assert req_item.href == "/req/req-1"
+
+
+def test_build_orphan_sections_falls_back_to_node_id() -> None:
+ """Unknown title → label falls back to the raw node id rather than crashing."""
+ orphans = {IntentLevel.GOAL.value: [IntentNode(IntentLevel.GOAL, "goal-9")]}
+ sections = views.build_orphan_sections(orphans, {}, {})
+ assert sections[0].items[0].label == "goal-9"
+ assert sections[0].items[0].href == "/goals"
+
+
def test_degraded_banner_when_denominator_incomplete() -> None:
class _Cov:
denominator_complete = False
diff --git a/tests/web/test_requirements.py b/tests/web/test_requirements.py
index 8622ca9..700c1b7 100644
--- a/tests/web/test_requirements.py
+++ b/tests/web/test_requirements.py
@@ -34,6 +34,26 @@ def test_corpus_lists_requirements(client: TestClient) -> None:
assert "Coverage is self-computable" in resp.text
+def test_corpus_has_new_requirement_link(client: TestClient) -> None:
+ """M8: the Corpus page carries a visible New-requirement primary control to /req/new."""
+ html = client.get("/").text
+ assert 'href="/req/new"' in html
+ assert "New requirement" in html
+ assert "btn--primary" in html
+
+
+def test_global_pending_badge_on_non_review_page(client: TestClient) -> None:
+ """M6: the nav "Review N" badge is populated on a non-review page (the Corpus page),
+ not only on /review — proving the global context-processor mechanism."""
+ app: Starlette = client.app # type: ignore[assignment]
+ ctx = app.state.ctx_factory()
+ ctx.service.create_requirement("Pending one", "body", actor="human:alice")
+ ctx.service.create_requirement("Pending two", "body", actor="human:alice")
+ html = client.get("/").text
+ # Two pending drafts → the badge on the Corpus page reads 2.
+ assert 'class="nav-badge">2' in html
+
+
def test_corpus_orphan_filter_no_goal(client: TestClient) -> None:
_mint(client, "Orphan req", "no goal yet")
resp = client.get("/", params={"orphan": "no-goal"})
diff --git a/tests/web/test_review.py b/tests/web/test_review.py
index 30e99f3..b09af53 100644
--- a/tests/web/test_review.py
+++ b/tests/web/test_review.py
@@ -137,7 +137,8 @@ def test_accept_link(client: TestClient) -> None:
token = client.cookies.get("pw_csrf")
resp = client.post(f"/trace/{link.id}/accept", data={"_csrf": token})
assert resp.status_code == 200
- assert 'hx-swap-oob="innerHTML:#sr-status"' in resp.text
+ assert 'hx-swap-oob="innerHTML:#sr-status"' in resp.text # SR announcement preserved
+ assert 'hx-swap-oob="innerHTML:#toast"' in resp.text # M9: visible toast mirrors it
assert not ctx.service.trace_for(state="proposed") # no longer pending
@@ -234,6 +235,56 @@ def test_accept_link_attributes_operator(client: TestClient) -> None:
)
+def _req_stub() -> object:
+ from types import SimpleNamespace
+
+ return SimpleNamespace(state=SimpleNamespace(csrf_token="test-token"))
+
+
+@pytest.mark.parametrize(
+ ("confidence", "band"),
+ [(0.3, "low"), (0.5, "med"), (0.79, "med"), (0.8, "high"), (0.95, "high")],
+)
+def test_conf_chip_band(project_root: Path, confidence: float, band: str) -> None:
+ """Fold-in: link confidence renders a .conf chip banded low/med/high alongside the raw value
+ so an operator can read calibration risk at a glance."""
+ app = create_app(actor="human:alice", root=project_root)
+ item = LinkItem(
+ kind="link",
+ link_id="LINK-1",
+ from_label="a",
+ relation="rel",
+ to_label="b",
+ proposing_actor="agent:claude",
+ confidence=confidence,
+ drifted=False,
+ )
+ rendered = app.state.templates.get_template("_partials/queue_item_link.html").render(
+ {"item": item, "request": _req_stub()}
+ )
+ assert f"conf {confidence}" in rendered # raw value preserved
+ assert f'class="conf conf--{band}"' in rendered
+ assert f">{band}" in rendered
+
+
+def test_conf_chip_absent_when_no_confidence(project_root: Path) -> None:
+ app = create_app(actor="human:alice", root=project_root)
+ item = LinkItem(
+ kind="link",
+ link_id="LINK-2",
+ from_label="a",
+ relation="rel",
+ to_label="b",
+ proposing_actor="agent:claude",
+ confidence=None,
+ drifted=False,
+ )
+ rendered = app.state.templates.get_template("_partials/queue_item_link.html").render(
+ {"item": item, "request": _req_stub()}
+ )
+ assert 'class="conf' not in rendered
+
+
def test_drift_card_branch_renders(project_root: Path) -> None:
"""Unit test: LinkItem(drifted=True) renders CODE DRIFTED + aria-describedby.
From 4c12d7f93e917e3fe275b416535314950b4cab15 Mon Sep 17 00:00:00 2001
From: John Morrissey <544926+tachyon-beep@users.noreply.github.com>
Date: Sun, 28 Jun 2026 13:43:00 +1000
Subject: [PATCH 2/4] docs: web UX design review + arch-analysis refresh +
handoffs
- docs/handoffs/2026-06-28-web-ux-design-review.md: the operator web-UI design
review (Playwright-driven, with the same-day resolution note recording the fixes).
- docs/handoffs/2026-06-28-lacuna-peer-facts-tour-demos-tasking.md: lacuna handoff.
- docs/arch-analysis-2026-06-28-0751/ replaces docs/arch-analysis-2026-06-21-1754/:
a refreshed system-archaeologist run (concurrent output, not part of the web work;
included per request to commit all changed files).
Co-Authored-By: Claude Opus 4.8 (1M context)
---
.../00-coordination.md | 65 ---
.../01-discovery-findings.md | 140 -----
.../02-subsystem-catalog.md | 256 ---------
.../03-diagrams.md | 154 ------
.../04-final-report.md | 102 ----
.../05-quality-assessment.md | 115 ----
.../06-architect-handover.md | 127 -----
.../temp/validation-catalog.md | 160 ------
.../00-coordination.md | 99 ++++
.../01-discovery-findings.md | 142 +++++
.../02-subsystem-catalog.md | 523 ++++++++++++++++++
.../03-diagrams.md | 237 ++++++++
.../04-final-report.md | 175 ++++++
.../05-quality-assessment.md | 243 ++++++++
.../06-architect-handover.md | 196 +++++++
.../temp/catalog-E1.md | 89 +++
.../temp/catalog-E2.md | 34 ++
.../temp/catalog-E3.md | 33 ++
.../temp/catalog-E4.md | 41 ++
.../temp/catalog-E5.md | 104 ++++
.../temp/dependency-reconciliation.md | 74 +++
.../temp/validation-catalog.md | 162 ++++++
.../temp/validation-synthesis.md | 137 +++++
...28-lacuna-peer-facts-tour-demos-tasking.md | 99 ++++
.../2026-06-28-web-ux-design-review.md | 209 +++++++
25 files changed, 2597 insertions(+), 1119 deletions(-)
delete mode 100644 docs/arch-analysis-2026-06-21-1754/00-coordination.md
delete mode 100644 docs/arch-analysis-2026-06-21-1754/01-discovery-findings.md
delete mode 100644 docs/arch-analysis-2026-06-21-1754/02-subsystem-catalog.md
delete mode 100644 docs/arch-analysis-2026-06-21-1754/03-diagrams.md
delete mode 100644 docs/arch-analysis-2026-06-21-1754/04-final-report.md
delete mode 100644 docs/arch-analysis-2026-06-21-1754/05-quality-assessment.md
delete mode 100644 docs/arch-analysis-2026-06-21-1754/06-architect-handover.md
delete mode 100644 docs/arch-analysis-2026-06-21-1754/temp/validation-catalog.md
create mode 100644 docs/arch-analysis-2026-06-28-0751/00-coordination.md
create mode 100644 docs/arch-analysis-2026-06-28-0751/01-discovery-findings.md
create mode 100644 docs/arch-analysis-2026-06-28-0751/02-subsystem-catalog.md
create mode 100644 docs/arch-analysis-2026-06-28-0751/03-diagrams.md
create mode 100644 docs/arch-analysis-2026-06-28-0751/04-final-report.md
create mode 100644 docs/arch-analysis-2026-06-28-0751/05-quality-assessment.md
create mode 100644 docs/arch-analysis-2026-06-28-0751/06-architect-handover.md
create mode 100644 docs/arch-analysis-2026-06-28-0751/temp/catalog-E1.md
create mode 100644 docs/arch-analysis-2026-06-28-0751/temp/catalog-E2.md
create mode 100644 docs/arch-analysis-2026-06-28-0751/temp/catalog-E3.md
create mode 100644 docs/arch-analysis-2026-06-28-0751/temp/catalog-E4.md
create mode 100644 docs/arch-analysis-2026-06-28-0751/temp/catalog-E5.md
create mode 100644 docs/arch-analysis-2026-06-28-0751/temp/dependency-reconciliation.md
create mode 100644 docs/arch-analysis-2026-06-28-0751/temp/validation-catalog.md
create mode 100644 docs/arch-analysis-2026-06-28-0751/temp/validation-synthesis.md
create mode 100644 docs/handoffs/2026-06-28-lacuna-peer-facts-tour-demos-tasking.md
create mode 100644 docs/handoffs/2026-06-28-web-ux-design-review.md
diff --git a/docs/arch-analysis-2026-06-21-1754/00-coordination.md b/docs/arch-analysis-2026-06-21-1754/00-coordination.md
deleted file mode 100644
index 97f5ec0..0000000
--- a/docs/arch-analysis-2026-06-21-1754/00-coordination.md
+++ /dev/null
@@ -1,65 +0,0 @@
-# 00 — Coordination Plan
-
-## Analysis Configuration
-
-- **Target:** Plainweave (`/home/john/plainweave`) — "permission for code to
- exist": code-up requirements traceability + intent corpus for the Weft
- federation.
-- **Scope:** `src/plainweave/` (15 modules, ~5.4K LOC). Tests (`tests/`, ~5.3K
- LOC) and docs read for evidence/context but not catalogued as subsystems.
-- **Deliverables:** **Option C — Architect-Ready.** All of:
- `01-discovery-findings`, `02-subsystem-catalog`, `03-diagrams`,
- `04-final-report`, `05-quality-assessment`, `06-architect-handover`.
-- **Complexity estimate:** Low-to-Medium. Small single-language (Python 3.12)
- codebase; tight layering; one oversized module (`service.py`, 2136 LOC).
-- **Time constraint:** None stated.
-- **Index aid:** Loomweave index is **fresh** at HEAD `72e8df2` (613 entities,
- 2161 edges). Structural facts (callers, coupling, data models, entry points)
- are sourced from Loomweave rather than re-grepped.
-
-## Orchestration Strategy
-
-**SEQUENTIAL** (analysis), with **subagent validation + quality gates** layered on.
-
-Rationale (per command criteria):
-- 6 logical subsystems but **tightly coupled** through a single `PlainweaveService`
- facade and a shared SQLite store.
-- Small codebase (~5.4K src LOC), single language → one analyst can hold the
- whole model in context; parallel fan-out would add coordination cost without
- payoff.
-- Validation gate is **mandatory** (≥3 subsystems): the subsystem catalog is
- validated by an `analysis-validator` subagent.
-- Quality assessment (Option C) is produced with `architecture-critic` and
- `debt-cataloger` subagents to get an independent, evidence-based critique
- rather than self-grading.
-
-## Execution Log
-
-- `2026-06-21 17:54` Created workspace `docs/arch-analysis-2026-06-21-1754/`.
-- `2026-06-21 17:54` Confirmed Loomweave index fresh at HEAD `72e8df2`.
-- `2026-06-21 17:55` User selected **Option C (Architect-Ready)**.
-- `2026-06-21 17:55` Holistic scan: layout, LOC distribution, entry points,
- coupling hotspots, data-model inventory, MODULE-MAP, stub state of reframe
- modules. → `01-discovery-findings.md`.
-- `2026-06-21 17:56` Strategy fixed: SEQUENTIAL analysis + subagent
- validation/quality gates.
-- `2026-06-21 17:57` Subsystem catalog written (6 subsystems) → `02`.
-- `2026-06-21 17:58` Dispatched 3 subagents in parallel: `analysis-validator`
- (catalog), `architecture-critic`, `debt-cataloger`.
-- `2026-06-21 17:59` Validation verdict **PASS-WITH-NOTES**
- (`temp/validation-catalog.md`); applied 2 corrections to the catalog
- (25 dataclasses in `models.py` not 29; fan-ins are test-inclusive) + added
- the hardcoded-relation-allow-list / no-goal-edge finding from the critic.
-- `2026-06-21 18:00` Diagrams (`03`), quality assessment (`05`, from critic +
- debt passes), final report (`04`), architect handover (`06`) written.
-- `2026-06-21 18:00` **Analysis complete.** All Option-C deliverables produced;
- validation gate satisfied.
-
-## Limitations / Confidence Notes
-
-- LLM entity summaries are **disabled** in this Loomweave index; responsibility
- descriptions derive from source reads + docstrings + the MODULE-MAP, not from
- generated summaries.
-- The reframe feature set (intent graph, SEI bindings, authoring write path) is
- **stubbed** (`NotImplementedError`); the analysis distinguishes *as-built
- precursor core* from *target interface*.
diff --git a/docs/arch-analysis-2026-06-21-1754/01-discovery-findings.md b/docs/arch-analysis-2026-06-21-1754/01-discovery-findings.md
deleted file mode 100644
index a172a23..0000000
--- a/docs/arch-analysis-2026-06-21-1754/01-discovery-findings.md
+++ /dev/null
@@ -1,140 +0,0 @@
-# 01 — Discovery Findings
-
-*Holistic assessment of Plainweave. Evidence drawn from repo layout, Loomweave
-index (HEAD `72e8df2`, fresh), `pyproject.toml`, `README.md`,
-`docs/MODULE-MAP.md`, and direct source reads.*
-
-## 1. What this project is
-
-**Plainweave** is a member of the "Weft federation" of developer tools. Its
-charter (README, `pyproject.toml`): hold the team's **code-grounded intent** as a
-traceability graph — `strategic goal ──▲── requirement ──▲── code SEI (leaf)` —
-where every code entity must "earn its existence" by laddering up to a
-requirement, and every requirement up to a goal. A node with no upward edge is a
-reviewable question ("why does this code exist?").
-
-It is deliberately a **thin, advisory** member:
-- It owns the **intent graph** + reasoning reads.
-- It **delegates** identity/rename tracking and semantic search to **Loomweave**,
- and enforcement/override/audit to **Legis** (it builds none of that itself).
-- Bindings reuse the **ADR-029 entity-association contract** (SEI-keyed, with
- `content_hash_at_attach` drift detection), not a new link store.
-
-**Critical status fact (README §Status, MODULE-MAP):** this repo is a *reframe +
-rename of a precursor* (`~/charter`). The **precursor's local
-requirements/verification core is intact and green**; the **reframed feature
-set** (intent graph, SEI bindings, authoring-time write path, Legis boundary
-cell, similarity hint) is **stubbed with backlog markers, not implemented**.
-Development Status classifier: `2 - Pre-Alpha`.
-
-## 2. Technology stack
-
-| Concern | Choice |
-| --- | --- |
-| Language | Python `>=3.12` (uses `StrEnum`, modern typing) |
-| Packaging | `hatchling`, `uv`, src-layout (`src/plainweave`) |
-| Runtime dep | **`mcp>=1.2.0`** (official Python MCP SDK) — the *only* runtime dependency |
-| Persistence | **stdlib `sqlite3`** (no ORM); schema via in-code migrations |
-| Quality tooling | `ruff` (E,F,I,UP,B,SIM), `mypy --strict`, `pytest` + `pytest-cov` (branch), coverage gate `fail_under = 90` |
-| Entry points | console scripts `plainweave` (CLI) and `plainweave-mcp` (MCP server) |
-
-Notably lean: a single third-party runtime dependency. Self-contained by design
-(enrich-only doctrine: siblings absent → degrade honestly, not crash).
-
-## 3. Entry points (Loomweave-confirmed)
-
-- `plainweave.cli.main` (`cli.py:22`) — CLI entry; console script `plainweave`.
-- `plainweave.mcp_server.main` (`mcp_server.py:127`) — MCP stdio server; console
- script `plainweave-mcp`.
-- `plainweave.__main__` — `python -m plainweave`.
-
-Two front doors (CLI, MCP) over one service core. No HTTP routes (Loomweave:
-zero `http-route` tags) — the MCP server speaks stdio via the SDK.
-
-## 4. Directory structure & size
-
-```
-src/plainweave/
- service.py 2136 ← PlainweaveService god-class (orchestration core)
- mcp_surface.py 1141 ← agentic MCP read surface (PlainweaveMcpSurface)
- cli_commands.py 1066 ← CLI command handlers
- models.py 273 ← 29 dataclasses (domain model)
- store.py 254 ← SQLite connect + migrate + event log
- mcp_server.py 132 ← MCP server wiring (tool registration)
- envelopes.py 115 ← standard JSON envelope (schema/ok/data/warnings/meta)
- intent_graph.py 113 ← REFRAME: intent-graph reads — STUB (NotImplementedError)
- bindings.py 71 ← REFRAME: ADR-029 SEI bindings — STUB (NotImplementedError)
- cli.py 35 ← CLI argparse entry
- errors.py 34 ← ErrorCode enum + PlainweaveError
- paths.py 24 ← .plainweave/ repo-local dir + db path
- __main__.py / __init__.py / _version.py (package plumbing)
- ─────
- 5408 total
-```
-
-Organization is **by layer/responsibility**, not by feature. ~80% of src LOC
-sits in three modules (`service`, `mcp_surface`, `cli_commands`).
-
-Tests: `tests/` ~5.3K LOC across `tests/contracts/` (fixture-driven contract
-tests), `tests/state/` (lifecycle/state-machine tests), and top-level
-read-surface tests, plus `tests/fixtures/contracts/` golden fixtures. Test LOC ≈
-src LOC — a strong test posture for the as-built core.
-
-## 5. Domain model (Loomweave data-model inventory: 29 dataclasses)
-
-- **Requirement identity:** `RequirementDraft` (mutable) → `RequirementVersion`
- (immutable) → `RequirementRecord`; `AcceptanceCriterion` (per ADR-002).
-- **Traceability:** `TraceRef`, `TraceLink` (ontology + authority states per
- ADR-003).
-- **Baselines:** `Baseline`, `BaselineMember`, `BaselineDiff(+Item)`.
-- **Verification:** `VerificationMethod`, `VerificationEvidence`,
- `RequirementVerificationStatus`, `VerificationReason`.
-- **Dossiers:** `RequirementDossier` + ~8 `Dossier*` section dataclasses (the
- agent-facing aggregate read).
-- **Actor:** `Actor` (attribution / actor registry).
-- **Reframe (stubbed):** `IntentNode`, `Trace`, `CorpusEntry`, `IntentLevel`
- (`intent_graph.py`); `SeiBinding` (`bindings.py`).
-
-## 6. Candidate subsystems (6)
-
-Identified by responsibility cohesion + dependency direction:
-
-1. **Domain Model & Errors** — `models.py`, `errors.py` (+ `paths.py`).
-2. **Persistence / Store** — `store.py` (SQLite, migrations, event log).
-3. **Service Core (PlainweaveService)** — `service.py`. The orchestration
- god-class all front doors call.
-4. **MCP Read Surface** — `mcp_surface.py`, `mcp_server.py`, `envelopes.py`.
-5. **CLI** — `cli.py`, `cli_commands.py`, `__main__.py`.
-6. **Intent Graph & Bindings (Reframe target)** — `intent_graph.py`,
- `bindings.py`. *Interface-only stubs.*
-
-## 7. Architectural shape (first read)
-
-- Classic **layered / transaction-script** architecture: CLI + MCP (presentation)
- → `PlainweaveService` (application/business) → `store.connect`/`migrate`
- (persistence) → SQLite. `models` is the shared data layer; `envelopes`/`errors`
- are cross-cutting output/contract concerns.
-- `PlainweaveService` is a **facade + god-object**: highest-coupled methods in
- the repo are all `PlainweaveService.*` (`create_requirement` fan-in 31,
- `record_verification_evidence` 24, `approve_requirement` 21…); `store.connect`
- has fan-in 48 (every write path opens a connection).
-- **Append-only event log** (`store.migrate`, `service._record_event`) +
- **idempotency** machinery (`_store_idempotency`, `_idempotent_*`) indicate a
- deliberate auditability/replay-safety posture in the core.
-- **Contract discipline:** standard JSON envelope (`envelopes.py`), explicit
- `ErrorCode` vocab including peer `PEER_ABSENT`/`PEER_STALE` (enrich-only honest
- degradation), 6 ADRs, and golden contract fixtures under `tests/fixtures`.
-
-## 8. Key risks surfaced for downstream docs
-
-- **`service.py` god-class** (2136 LOC, ~29 public + ~40 private methods) — the
- dominant maintainability risk and refactor target.
-- **`mcp_surface.py` / `cli_commands.py`** are both 1K+ LOC presentation modules
- that likely duplicate shaping logic over the same service calls.
-- **As-built vs. target gap:** the headline feature (the code-up intent graph) is
- unimplemented; the repo today is the precursor requirements core under a new
- name. Any architecture verdict must hold these two layers apart.
-
-**Confidence:** High for the as-built core (direct source + Loomweave + tests +
-MODULE-MAP corroborate). High for the stub status of the reframe (explicit
-`NotImplementedError` + docstrings).
diff --git a/docs/arch-analysis-2026-06-21-1754/02-subsystem-catalog.md b/docs/arch-analysis-2026-06-21-1754/02-subsystem-catalog.md
deleted file mode 100644
index 2476785..0000000
--- a/docs/arch-analysis-2026-06-21-1754/02-subsystem-catalog.md
+++ /dev/null
@@ -1,256 +0,0 @@
-# 02 — Subsystem Catalog
-
-*Six subsystems, identified by responsibility cohesion and dependency direction.
-Dependency edges are import-confirmed; coupling/fan-in figures from the Loomweave
-index (HEAD `72e8df2`). "Inbound/Outbound" list **intra-package** subsystems
-only.*
-
-> **Validation note (see `temp/validation-catalog.md`):** fan-in figures below
-> are **test-inclusive** (they count test callers). App-only *production* fan-in
-> is lower — e.g. `create_requirement` 6 (not 31), `approve_requirement` 6,
-> `store.connect` 32 (not 48). `store.connect` is still the repo's
-> highest-coupled entity. The god-object verdict rests on LOC + method count,
-> which are independent of caller counts.
-
-Dependency direction (leaf → root):
-
-```
-CLI ─┐ Intent Graph & Bindings
- ├─► Service Core ─► Store ─► (SQLite) (reframe stubs: standalone,
-MCP ─┘ │ not yet wired to anything)
- │ │ └─► Domain Model & Errors ◄── (used by every layer)
- └──┴─► Envelopes (cross-cutting output contract)
-```
-
----
-
-## 1. Domain Model & Errors
-
-**Location:** `src/plainweave/models.py`, `src/plainweave/errors.py`,
-`src/plainweave/paths.py`
-
-**Responsibility:** Define the immutable domain vocabulary (dataclasses), the
-error taxonomy, and repo-local path resolution shared by every other layer.
-
-**Key Components:**
-- `models.py` — **25** frozen dataclasses (the project-wide data-model count of
- 29 = 25 here + 3 in `intent_graph` + 1 in `bindings`): requirement identity
- (`RequirementDraft`/`RequirementVersion`/`RequirementRecord`),
- `AcceptanceCriterion`, traceability (`TraceRef`/`TraceLink`), baselines
- (`Baseline`/`BaselineMember`/`BaselineDiff`), verification
- (`VerificationMethod`/`VerificationEvidence`/`RequirementVerificationStatus`),
- dossier sections (`RequirementDossier` + ~8 `Dossier*`), `Actor`.
-- `errors.py` — `ErrorCode` enum (incl. `PEER_ABSENT`/`PEER_STALE` for
- enrich-only degradation) + `PlainweaveError`.
-- `paths.py` — `.plainweave/` dir, `plainweave_db_path`, `project_root`,
- `default_project_key`.
-
-**Dependencies:**
-- Inbound: Service Core, MCP Read Surface, CLI, Envelopes.
-- Outbound: none (pure leaf; no intra-package imports).
-
-**Patterns Observed:** Immutable value objects (frozen dataclasses); explicit
-closed error vocab switched on by `code` not message; identity-vs-version split
-(ADR-002).
-
-**Concerns:** `models.py` mixes core domain types with read-model/DTO types
-(the `Dossier*` aggregate sections are presentation-shaped). Minor — but the
-boundary between "domain" and "read model" is not physically separated.
-
-**Confidence:** High — full data-model inventory from Loomweave + source.
-
----
-
-## 2. Persistence / Store
-
-**Location:** `src/plainweave/store.py`
-
-**Responsibility:** Own the SQLite connection, schema creation/migration, schema
-metadata, and the append-only event log substrate.
-
-**Key Components:**
-- `connect(db_path)` — connection factory. **Fan-in 48** — the single
- highest-coupled entity in the repo; every write path opens through it.
-- `migrate(...)` — 227-line in-code schema migration (fan-in 19).
-- `read_schema_meta(...)` — schema-version metadata read.
-
-**Dependencies:**
-- Inbound: Service Core, CLI (`cli_commands` calls `connect`/`migrate` directly).
-- Outbound: none (stdlib `sqlite3` only).
-
-**Patterns Observed:** No ORM — raw `sqlite3` with `Row` factory. In-code
-migration ladder. Append-only event stream + idempotency tables (consumed by the
-Service Core) give the core a replay-safe, auditable spine.
-
-**Concerns:** (1) `cli_commands` calls `store.connect`/`migrate` directly rather
-than only through the Service Core — persistence access is not fully funneled
-through the service boundary. (2) Migrations are a single growing in-code
-function rather than versioned migration files (acceptable at this size; watch
-as schema grows for the reframe).
-
-**Confidence:** High.
-
----
-
-## 3. Service Core — `PlainweaveService`
-
-**Location:** `src/plainweave/service.py` (2136 LOC)
-
-**Responsibility:** The application/business layer. One class orchestrating the
-entire as-built domain: requirement lifecycle, acceptance criteria, trace
-link propose/accept/reject/stale/orphan, baselines + diff, verification methods +
-evidence + status computation, requirement dossiers, idempotency, and the event
-log.
-
-**Key Components (all `PlainweaveService` methods):**
-- Requirement lifecycle: `create_requirement` (fan-in 31), `update_draft`,
- `approve_requirement` (21), `supersede_requirement`, `reject_requirement`,
- `deprecate_requirement`.
-- Trace: `propose_trace_link`, `create_trace_link`, `accept/reject/mark_stale/
- mark_orphaned`, `trace_for`.
-- Verification: `add_verification_method`, `record_verification_evidence`
- (fan-in 24), `verification_status`, `_compute_verification_status`.
-- Baselines: `create_baseline`, `diff_baseline`, `show/list_baseline`.
-- Aggregates: `requirement_dossier`, `requirement_preflight_profile`.
-- 64 private helpers: `_record_event`, `_store_idempotency`/`_idempotent_*`,
- `_dossier_*`, `_validate_*`, row↔dataclass mappers, ID/sequence allocators.
-
-**Dependencies:**
-- Inbound: CLI, MCP Read Surface.
-- Outbound: Store, Domain Model & Errors.
-
-**Patterns Observed:** Facade over the store; transaction-script per operation;
-state-machine validation for trace links and requirement status; explicit
-idempotency keys; event sourcing of mutations.
-
-**Concerns:** **God-object.** 2136 LOC / one class / ~29 public + ~40 private
-methods (29 public + 64 private) spanning six distinct responsibility clusters
-(requirements, criteria, trace, verification, baselines, dossiers). Its trace
-ontology is a **hardcoded allow-list** of `(from_kind, relation, to_kind)`
-triples in `_validate_trace_relation` (`service.py:1877`) — which already
-contains `loomweave_entity satisfies requirement_version` but **no `goal` node
-kind and no `requirement → goal` edge**; the reframe's central edge type does not
-exist here yet. This is the repo's dominant
-maintainability and testability risk and the clearest refactor target (extract
-per-aggregate services/repositories). Detailed in `05-quality-assessment.md`.
-
-**Confidence:** High — full method inventory + coupling data.
-
----
-
-## 4. MCP Read Surface
-
-**Location:** `src/plainweave/mcp_surface.py` (1141), `mcp_server.py` (132),
-`envelopes.py` (115)
-
-**Responsibility:** Expose the read-only, agent-facing MCP tool surface
-(`plainweave_*` tools: project context, requirement search/get/dossier, trace
-listing, baselines, verification status, preflight facts, entity-intent context)
-as standard JSON envelopes; register them on an MCP stdio server.
-
-**Key Components:**
-- `mcp_surface.PlainweaveMcpSurface` — the tool methods (e.g.
- `plainweave_preflight_facts_get` fan-out 11, plus requirement/dossier/baseline/
- verification reads). `MCP_RESOURCE_URIS`.
-- `mcp_server.create_mcp_server` (fan-out 14) / `main` — SDK wiring + entry point.
-- `envelopes.py` — `success_envelope`/`error_envelope`/`list_envelope`
- (`schema`/`ok`/`data`/`warnings`/`meta.producer`); cross-cutting output contract
- (ADR-004).
-
-**Dependencies:**
-- Inbound: none (it is a front door); `mcp_server` ← console script.
-- Outbound: Service Core, Domain Model & Errors, Envelopes, `paths`,
- **and `cli_commands` (CLI subsystem)** — see Concerns.
-
-**Patterns Observed:** Read-only surface (tests assert tools do not mutate
-state); envelope-wrapped, schema-tagged responses; honest peer-absence labelling.
-
-**Concerns:** **Cross-presentation coupling.** `mcp_surface` imports *private*
-serializers from `cli_commands` (`_baseline_dict`, `_baseline_diff_dict`, `_dossier_dict`,
-`_record_dict`, `_trace_dict`, `_requirement_verification_status_dict`,
-`_current_project_key`, `inspect_project`). The DTO→dict shaping layer lives in
-the CLI module
-and is shared via underscore-private imports — a misplaced-shared-layer smell.
-Either presentation surface breaks if the other's privates move.
-
-**Confidence:** High.
-
----
-
-## 5. CLI
-
-**Location:** `src/plainweave/cli.py` (35), `cli_commands.py` (1066),
-`__main__.py`
-
-**Responsibility:** The full command-line interface over the as-built service:
-argparse command registration, argument handling, service invocation, and
-result→JSON/text rendering. Also hosts the shared DTO→dict serialization helpers.
-
-**Key Components:**
-- `cli.main` (fan-in 21) — argparse entry; console script `plainweave`.
-- `cli_commands.register_commands` — command table.
-- `cli_commands._handle_service_result` (fan-in 18) — uniform result/error
- rendering through the envelope contract.
-- `cli_commands.inspect_project` + `_*_dict` serializers — also imported by the
- MCP surface (the de-facto shared serialization layer).
-
-**Dependencies:**
-- Inbound: MCP Read Surface (imports its serializers), `cli` ← console script.
-- Outbound: Service Core, Store, Domain Model & Errors, Envelopes, `paths`.
-
-**Patterns Observed:** Thin entry (`cli.py`) + fat command module; uniform
-envelope-based result handling shared with MCP.
-
-**Concerns:** (1) Hosts serialization helpers consumed by another subsystem
-(MCP) — these belong in a neutral `serializers`/`views` module, not in `cli_commands`.
-(2) 1066 LOC — large; command handling + serialization + project inspection are
-co-mingled. (3) Calls `store.connect`/`migrate` directly.
-
-**Confidence:** High.
-
----
-
-## 6. Intent Graph & Bindings — *Reframe target (stubbed)*
-
-**Location:** `src/plainweave/intent_graph.py` (113), `bindings.py` (71)
-
-**Responsibility (target):** The headline reframe capability — model intent as a
-directed graph (goal → requirement → code SEI) and expose the three composable
-read primitives `orphans(level)` / `trace(node)` / `corpus()`; bind code leaves
-to requirements via the **ADR-029 entity-association contract**, SEI-keyed
-(`loomweave:eid:...`) with `content_hash_at_attach` drift detection.
-
-**Key Components:**
-- `intent_graph.py` — `IntentLevel` (StrEnum CODE/REQUIREMENT/GOAL), `IntentNode`,
- `Trace`, `CorpusEntry` dataclasses, and `IntentGraphReads.{orphans,trace,corpus}`.
-- `bindings.py` — `SeiBinding` dataclass, `bind_sei_to_requirement`, `is_drifted`.
-
-**Dependencies:**
-- Inbound: none yet. Outbound: none (standalone modules; not wired to Service
- Core or Store).
-
-**Patterns Observed:** **Interface-first stubs** — every behavioural method
-`raise NotImplementedError(_PENDING)` with docstrings pointing at the design doc
-and `.filigree` backlog. Data shapes are defined; behaviour is not.
-
-**Concerns:** This is the *raison d'être* of the reframe and it is **not
-implemented**. The dataclasses define the target contract but nothing ties them
-to the existing `trace_links`/requirements store, to Loomweave SEI resolution, or
-to a write path. Tracked: `plainweave-c2d58800a0` (epic) and siblings. Not a
-defect — a deliberate, documented standup boundary — but the architecture's
-center of gravity is still aspirational.
-
-**Confidence:** High — explicit `NotImplementedError` + docstrings + MODULE-MAP.
-
----
-
-## Cross-cutting observations
-
-- **Output contract (`envelopes.py`) + error vocab (`errors.py`)** are
- consistent cross-cutting concerns used by both front doors — good contract
- discipline (ADR-004).
-- **Serialization layer is homeless:** the `_*_dict` DTO mappers live in
- `cli_commands` but serve both front doors. This is the single clearest
- structural correction (extract a `views`/`serializers` subsystem).
-- **Persistence boundary is leaky:** both CLI and (transitively) the service
- open the store; CLI bypasses the service for `connect`/`migrate`.
diff --git a/docs/arch-analysis-2026-06-21-1754/03-diagrams.md b/docs/arch-analysis-2026-06-21-1754/03-diagrams.md
deleted file mode 100644
index 5b78525..0000000
--- a/docs/arch-analysis-2026-06-21-1754/03-diagrams.md
+++ /dev/null
@@ -1,154 +0,0 @@
-# 03 — Architecture Diagrams (C4)
-
-*Mermaid C4-style diagrams. Solid arrows = direct import/call dependency
-(import-confirmed). Dashed = planned/unbuilt (reframe). Diagrams reflect the
-**as-built** structure at HEAD `72e8df2`; reframe targets are marked.*
-
-## C1 — System Context
-
-Plainweave in the Weft federation. Plainweave is advisory/enrich-only; siblings
-are optional (absent → honest degradation).
-
-```mermaid
-graph TB
- agent["AI Agent / Developer"]
- subgraph weft["Weft federation"]
- pw["Plainweave intent graph + reasoning reads (permission for code to exist)"]
- loom["Loomweave entity catalog, SEI identity, rename feed, semantic search"]
- legis["Legis git/CI boundary, graded enforcement, audit trail"]
- end
- db[("SQLite .plainweave/ (repo-local)")]
-
- agent -->|"CLI / MCP stdio"| pw
- pw --> db
- pw -.->|"consumes SEIs, rename feed, semantic hint (PLANNED)"| loom
- pw -.->|"surfaces coverage facts at git/CI boundary (PLANNED)"| legis
-
- classDef planned stroke-dasharray:5 5,fill:#f5f5f5;
- class loom,legis planned;
-```
-
-> Today Plainweave is self-contained (one runtime dep: the MCP SDK; local
-> SQLite). The Loomweave/Legis seams are *additive, hub-blessed, prove-the-need*
-> and not yet wired.
-
-## C2 — Container / Module view
-
-The six subsystems and their import-confirmed dependencies.
-
-```mermaid
-graph TD
- cli["CLI cli.py, cli_commands.py, __main__.py + homeless _*_dict serializers"]
- mcp["MCP Read Surface mcp_surface.py, mcp_server.py"]
- env["Envelopes envelopes.py (output contract)"]
- svc["Service Core service.py — PlainweaveService (2136 LOC god-object)"]
- store["Persistence / Store store.py (SQLite, migrate, event log)"]
- model["Domain Model & Errors models.py, errors.py, paths.py"]
- db[("SQLite")]
-
- subgraph reframe["Reframe target — STUBBED (NotImplementedError)"]
- ig["Intent Graph intent_graph.py orphans/trace/corpus"]
- bind["Bindings bindings.py ADR-029 SEI binding"]
- end
-
- cli --> svc
- cli --> store
- cli --> env
- cli --> model
- mcp --> svc
- mcp --> env
- mcp --> model
- mcp -->|"imports PRIVATE _*_dict (smell)"| cli
- svc --> store
- svc --> model
- env --> model
- store --> db
-
- ig -.->|"PLANNED: walk over trace_links + goal nodes"| svc
- bind -.->|"PLANNED: ADR-029 assoc + Loomweave SEI"| svc
-
- classDef smell stroke:#c00,stroke-width:2px;
- classDef god stroke:#e69500,stroke-width:3px;
- classDef planned stroke-dasharray:5 5,fill:#f5f5f5;
- class svc god;
- class ig,bind planned;
-```
-
-**Read this diagram for two things:**
-1. **`mcp ──► cli`** (red): the MCP surface depends on *private* serializers in
- the CLI module — a layering inversion. Both front doors should depend on a
- neutral `serializers`/`views` module instead.
-2. **`svc` (god-object, orange):** every front door funnels through one
- 2136-LOC class, and both reframe stubs point back at it — the intent graph is
- on a trajectory to be absorbed into the god-object unless it is decomposed
- first.
-
-## C3 — Component view: the as-built request path
-
-How a single operation flows (e.g. `create_requirement` / a requirement read).
-
-```mermaid
-graph LR
- subgraph front["Front doors"]
- a1["plainweave CLI cli.main → register_commands"]
- a2["plainweave-mcp mcp_server.main → create_mcp_server"]
- end
- h["cli_commands.handle_* / PlainweaveMcpSurface.plainweave_*"]
- ser["_*_dict serializers (in cli_commands)"]
- s["PlainweaveService."]
- ev["_record_event + _idempotent_* (event log)"]
- c["store.connect / migrate"]
- db[("SQLite tables: requirements, versions, trace_links, baselines, verification_*, events")]
- out["JSON envelope schema/ok/data/warnings/meta"]
-
- a1 --> h
- a2 --> h
- h --> s
- s --> ev
- s --> c
- c --> db
- s --> ser
- ser --> out
- h --> out
-```
-
-## C4 — Domain model (intent ladder: built vs. target)
-
-```mermaid
-graph BT
- code["code SEI (leaf) loomweave:eid:..."]
- reqv["RequirementVersion / Record (+ Draft, AcceptanceCriterion)"]
- goal["Strategic Goal node (IntentLevel.GOAL)"]
-
- code -->|"satisfies (BUILT: trace_links loomweave_entity→requirement_version)"| reqv
- reqv -.->|"justified by (TARGET: no goal kind / no req→goal triple yet)"| goal
-
- subgraph built["Built today — requirements/trace/verification core"]
- reqv
- v["Verification: Method, Evidence, Status"]
- b["Baseline (+Member, +Diff)"]
- d["RequirementDossier (+ Dossier* sections)"]
- reqv --- v
- reqv --- b
- reqv --- d
- end
-
- classDef target stroke-dasharray:5 5,fill:#f5f5f5;
- class goal target;
-```
-
-**Key:** the *lower* half of the intent ladder (`code → requirement`) is modeled
-today in the generic `trace_links` edge table. The *upper* half
-(`requirement → goal`) — the reframe's defining edge — has **no node kind and no
-relation triple** in the as-built validation set (`_validate_trace_relation`,
-`service.py:1877`). The storage substrate is reusable; the graph behavior is
-net-new.
-
-## Legend
-
-| Marker | Meaning |
-| --- | --- |
-| solid arrow | import/call dependency, confirmed in source |
-| dashed arrow / grey box | planned (reframe), not implemented |
-| red edge | architectural smell (layering inversion) |
-| orange node | god-object / dominant risk |
diff --git a/docs/arch-analysis-2026-06-21-1754/04-final-report.md b/docs/arch-analysis-2026-06-21-1754/04-final-report.md
deleted file mode 100644
index 9683474..0000000
--- a/docs/arch-analysis-2026-06-21-1754/04-final-report.md
+++ /dev/null
@@ -1,102 +0,0 @@
-# 04 — Final Report
-
-**Project:** Plainweave · **Commit:** `72e8df2` · **Date:** 2026-06-21 ·
-**Deliverable:** Architect-Ready (Option C) · **Strategy:** sequential analysis +
-subagent validation/quality gates
-
-## Executive summary
-
-Plainweave is a small (~5.4K src LOC), single-language (Python 3.12) tool that is
-a **reframe + rename of a precursor (`~/charter`)**. It aspires to be the Weft
-federation's holder of *code-grounded intent*: a traceability graph where every
-code entity (a Loomweave SEI) must ladder up to a requirement and every
-requirement up to a goal, surfacing a readable, queryable **intent corpus**.
-
-The most important finding of this analysis is a **two-layer reality**:
-
-1. **What is built** — a competent, well-tested requirements/verification engine
- carried forward from the precursor: requirement lifecycle, acceptance
- criteria, trace links, baselines, verification, dossiers, all over an
- append-only event-logged SQLite store, exposed through a CLI and a read-only
- MCP surface with disciplined JSON envelopes and a closed error vocabulary.
- **This core is green and dependable (quality 3/5).**
-
-2. **What is named but not built** — the headline reframe (the intent graph's
- `orphans`/`trace`/`corpus` primitives, the goal altitude, ADR-029 SEI
- bindings, the authoring-time write path). These are **honest
- `NotImplementedError` stubs**; the project has, in effect, *built a solid
- requirements engine and a correct edge-store, then named itself after a
- graph-traversal feature it has not started.*
-
-The structure carries the reframe's **data** well (the generic `trace_links`
-table is the right substrate) but its **behavior** poorly: the reframe will, on
-the current trajectory, be absorbed into a 2136-LOC `PlainweaveService`
-god-object that is already the dominant maintainability risk — and the reframe's
-defining edge (`requirement → goal`) does not yet exist even as a relation triple.
-
-## Architecture at a glance
-
-- **Style:** layered / transaction-script. CLI + MCP (presentation) →
- `PlainweaveService` (application) → `store` (persistence) → SQLite. `models`
- is the shared data layer; `envelopes` + `errors` are cross-cutting contracts.
-- **Six subsystems:** Domain Model & Errors · Persistence/Store · Service Core ·
- MCP Read Surface · CLI · Intent Graph & Bindings (reframe, stubbed). See
- `02-subsystem-catalog.md`; diagrams in `03-diagrams.md`.
-- **Two entry points:** `plainweave` (CLI) and `plainweave-mcp` (MCP stdio).
-- **One runtime dependency** (the MCP SDK); raw `sqlite3`; enrich-only/advisory
- doctrine (siblings absent → honest degradation).
-
-## Top findings (ranked)
-
-| # | Finding | Severity | Where |
-| --- | --- | --- | --- |
-| 1 | `PlainweaveService` god-object: 2136 LOC, 1 class, 6 responsibility clusters | **High** | `service.py` |
-| 2 | Reframe's `requirement → goal` edge + graph-walk behavior do not exist; trace ontology is a hardcoded allow-list; `trace_for` is single-hop SQL | **High** (reframe-blocking) | `service.py:1877`, `:977` |
-| 3 | Homeless serialization layer: `mcp_surface` imports private `_*_dict` from `cli_commands` (layering inversion) | **High** | `mcp_surface.py:9–17` |
-| 4 | Leaky persistence boundary: CLI calls `store.connect/migrate` directly | High | `cli_commands.py:39` |
-| 5 | Oversized presentation modules + partial read-shaping duplication | Medium | `mcp_surface.py`, `cli_commands.py` |
-| 6 | Domain model mixes value objects with `Dossier*` read-models | Medium | `models.py:186–272` |
-| 7 | Monolithic in-code migration; no version-upgrade ladder | Medium (time-bomb) | `store.py:22–251` |
-
-Full register + evidence: `05-quality-assessment.md`.
-
-## Strengths worth protecting
-
-The generic typed-edge store, the code→requirement edge already modeled, the
-append-only event log + idempotency spine, the closed error/envelope contracts
-(ADR-004), 6 ADRs, and a test suite roughly equal in size to the source with
-golden contract fixtures and a 90% branch-coverage gate. These are real and
-should be preserved through any refactor.
-
-## The one decision that matters
-
-**Decompose `PlainweaveService` first; build the intent graph second.** Doing it
-in the other order produces a ~2500-LOC god-object that is materially harder to
-break up, and bolts the reframe's defining feature onto the project's worst
-structural defect. The decomposition is behavior-preserving and backstopped by
-the existing test suite. Sequenced remediation is in `06-architect-handover.md`.
-
-## Method & confidence
-
-Sequential analysis by one analyst over a Loomweave-indexed tree (fresh at
-`72e8df2`), with the subsystem catalog independently **validated**
-(`temp/validation-catalog.md`, verdict PASS-WITH-NOTES — two corrections applied:
-`models.py` holds 25 dataclasses not 29; coupling fan-ins are test-inclusive) and
-the quality assessment produced by independent `architecture-critic` and
-`debt-cataloger` passes. Confidence **High** for the as-built findings (all
-line-cited and corroborated). Known gaps: coverage not re-executed; the
-`.filigree` backlog + implementation plan not read (the decompose-vs-build
-sequencing may already be planned there — verify).
-
-## Deliverables index
-
-| Doc | Contents |
-| --- | --- |
-| `00-coordination.md` | configuration, strategy, execution log |
-| `01-discovery-findings.md` | holistic scan, stack, subsystem identification |
-| `02-subsystem-catalog.md` | six subsystem entries (validated) |
-| `03-diagrams.md` | C4 context / module / component / domain diagrams |
-| `04-final-report.md` | this synthesis |
-| `05-quality-assessment.md` | strengths, debt register, sequencing |
-| `06-architect-handover.md` | prioritized improvement plan |
-| `temp/validation-catalog.md` | catalog validation report |
diff --git a/docs/arch-analysis-2026-06-21-1754/05-quality-assessment.md b/docs/arch-analysis-2026-06-21-1754/05-quality-assessment.md
deleted file mode 100644
index 73fef0a..0000000
--- a/docs/arch-analysis-2026-06-21-1754/05-quality-assessment.md
+++ /dev/null
@@ -1,115 +0,0 @@
-# 05 — Code Quality & Architecture Assessment
-
-*Synthesized from an independent `architecture-critic` pass and a `debt-cataloger`
-pass, both evidence-backed at HEAD `72e8df2` and cross-checked against the
-catalog. Severity reflects as-built maintainability + reframe-blocking impact,
-not runtime/operational risk (Pre-Alpha, local SQLite, stdio MCP, no HTTP
-surface, single runtime dep → low operational blast radius).*
-
-## Verdict
-
-The **as-built core is a competently engineered, well-tested layered application
-with one serious structural defect (the `PlainweaveService` god-object) and two
-real but contained coupling smells.** Core quality: **3/5** — acceptable, no
-critical or security issues, dragged down by the god-object and a homeless
-serialization layer.
-
-The structure is a **sound data foundation but a poor behavioral foundation for
-the reframe.** The generic `trace_links` edge table is genuinely reusable for the
-intent graph. The *service layer* is not: on the current trajectory the reframe
-gets absorbed into the same god-object that is already the dominant risk, and the
-reframe's defining edge (`requirement → goal`) does not yet exist even in embryo.
-**The highest-leverage decision in the project — decompose the service before
-building the graph, or staple the graph onto the god-object — is currently
-undecided and undocumented in code.**
-
-## Strengths (specific, evidence-backed)
-
-1. **The edge store is the right substrate for the graph.** `trace_links`
- (`service.py:926`) stores `from_kind/from_id → relation → to_kind/to_id` with
- `state`/`authority`/`freshness`/`confidence` — structurally a typed directed
- edge table, exactly what `goal → requirement → code` needs. The reframe does
- not need a new link store (MODULE-MAP's "storage carries forward" is correct).
-2. **The code→requirement edge already exists.** `_validate_trace_relation`
- (`service.py:1877`) canonicalizes `(loomweave_entity, satisfies,
- requirement_version)`. The lower half of the intent ladder is modeled today.
-3. **Disciplined contract surface.** Standard JSON envelope (`envelopes.py`),
- closed `ErrorCode` vocab switched on `code` not message,
- `PEER_ABSENT`/`PEER_STALE` modeling honest enrich-only degradation (ADR-004).
- Doctrine-to-code fidelity, not aspiration.
-4. **Auditable spine.** Append-only event log (`_record_event`,
- `service.py:1901`) + idempotency machinery + explicit state-machine validation
- for trace transitions (`_validate_trace_transition`, `service.py:1891`) — a
- deliberate replay-safe posture, rare at this size. **Preserve it through any
- refactor.**
-5. **Strong test posture for the core.** Test LOC ≈ src LOC, golden contract
- fixtures (`tests/fixtures/contracts/`), branch coverage gate `fail_under=90`.
- The carried-forward core is genuinely green, and the suite is the safety net
- that makes the recommended decomposition low-risk.
-6. **Honest stubs.** `intent_graph.py` / `bindings.py` define data shapes and
- `raise NotImplementedError(_PENDING)` with docstrings pointing at the design +
- backlog; `service.py` imports neither. The unbuilt is unambiguously unbuilt —
- the correct way to stand up a reframe.
-
-## Technical Debt Register (as-built)
-
-> Reframe stubs are **planned scope, excluded** from debt. `src/` carries **zero
-> TODO/FIXME/HACK markers** (grep + Loomweave `entity_todo_list` both empty) and
-> **no confirmed dead code** in as-built code (the 143 Loomweave dead-candidates
-> are low-confidence false positives — argparse-registered `handle_*`/`_register_*`
-> thunks + the reframe stubs).
-
-| ID | Title | Location | Category | Sev | Effort |
-| --- | --- | --- | --- | --- | --- |
-| **DEBT-01** | `PlainweaveService` god-object (2136 LOC, 1 class, 29 pub + 64 priv, 6 clusters) | `service.py` | god-object | **High** | L |
-| **DEBT-02** | Homeless serialization layer shared via private cross-module imports | `cli_commands.py:717–1066` → imported by `mcp_surface.py:9–17` | misplaced-layer / coupling | **High** | M |
-| **DEBT-03** | Leaky persistence boundary: CLI bypasses service to hit the store | `cli_commands.py:39`, used `:627–629,647–648` | coupling / misplaced-layer | **High** | S |
-| **DEBT-04** | Oversized presentation modules, co-mingled concerns | `mcp_surface.py` (1141), `cli_commands.py` (1066) | cohesion | Med | M |
-| **DEBT-05** | Partial duplication of read-shaping (shared mappers + inline MCP literals) | `mcp_surface.py` (many `:345…:1126`) vs `cli_commands` mappers | duplication | Med | M |
-| **DEBT-06** | Domain model mixes value objects with presentation read-models (10 of 25 are `Dossier*`) | `models.py:186–272` | misplaced-layer | Med | M |
-| **DEBT-07** | In-code monolithic migration; flat `SCHEMA_VERSION = 1`, no upgrade ladder | `store.py:22–251` | migration | Med | M now / L later |
-| **DEBT-08** | Weak subsystem modularity (Loomweave clustering signal) — *symptom of 01/02/03* | whole import graph | architecture | Low | — |
-| **DEBT-09** | `.env` flagged with high-entropy secret — **not committed** (gitignored), hygiene only | `.env:1` | hygiene | Low | S |
-
-### The reframe-readiness finding (cross-cuts DEBT-01)
-
-`_validate_trace_relation` (`service.py:1877–1889`) is a hardcoded `set` of
-`(from_kind, relation, to_kind)` triples with **no `goal` kind and no
-`requirement → goal` triple**, and `trace_for` (`service.py:977`) is a
-**single-hop SQL `WHERE` filter, not a graph walk**. So the reframe's two named
-primitives are net-new behavior: `trace(node)`/`orphans(level)` need recursive
-graph traversal that has no precursor, and the "altitudes are just node types,
-not fixed levels" design promise (`intent_graph.py:36`) directly contradicts a
-hardcoded triple allow-list. *"The data model carries forward" is true for the
-table and misleading for the behavior.*
-
-## Risk assessment & sequencing
-
-- **DEBT-01 is highest-leverage and most expensive to defer:** every reframe
- reshape (binding contract, goal altitude, orphan computation) lands on this
- class; deferring the split compounds against planned scope, producing a
- ~2500-LOC god-object that is materially harder to break up.
-- **DEBT-02 / DEBT-03 are low-risk, high-value mechanical extractions** — safe to
- do first; they de-risk DEBT-04 / DEBT-05 and remove the only layering inversion.
-- **DEBT-07 is a time-bomb:** cheap now (single version), expensive once the
- reframe adds goal-node / binding-cache tables and existing DBs need in-place
- upgrade. Address before the reframe touches the schema.
-- Decomposition is **behavior-preserving and test-backed** — the existing suite
- backstops it. Risk of acting: Low. Risk of not acting: rises monotonically.
-
-## Confidence, gaps, caveats
-
-- **Confidence: High** for DEBT-01…07 and all strengths — every claim is a
- line-cited source read cross-checked against Loomweave and the independent
- catalog. **Medium** on DEBT-05 *severity* (partial duplication, drift-risk not
- gross copy-paste).
-- **Gaps:** (1) coverage not re-executed — the 90% gate + green status are taken
- from config + prior analysis, not re-run; no per-cluster `service.py` unit
- tests were observed (plausible but unverified test gap). (2) The `.filigree`
- backlog epics (`plainweave-c2d58800a0` + siblings) and the referenced
- implementation plan were **not** read — the decompose-vs-build *sequencing* may
- already be planned there; if so, the "unmanaged gap" framing softens to "verify
- the plan decomposes first."
-- **Caveats:** no security/perf assessment warranted by current scope; if a
- future seam adds a network surface (e.g. a Legis HTTP boundary), trust-boundary
- review (wardline) becomes load-bearing and is out of scope here.
diff --git a/docs/arch-analysis-2026-06-21-1754/06-architect-handover.md b/docs/arch-analysis-2026-06-21-1754/06-architect-handover.md
deleted file mode 100644
index 33581aa..0000000
--- a/docs/arch-analysis-2026-06-21-1754/06-architect-handover.md
+++ /dev/null
@@ -1,127 +0,0 @@
-# 06 — Architect Handover
-
-*Transition document from archaeology → improvement planning. Turns the findings
-in `05-quality-assessment.md` into a sequenced, behavior-preserving plan. The
-governing principle: **decompose the service before building the reframe**, and
-do the cheap mechanical extractions first to de-risk the larger ones.*
-
-## Guardrails for all work below
-
-- **Behavior-preserving.** The existing test suite (≈ src LOC, golden contract
- fixtures, 90% branch gate) is the safety net. Run `make ci` after each step;
- no green, no merge.
-- **Preserve the spine.** The append-only event log (`_record_event`),
- idempotency machinery (`_idempotent_*`), and trace state-machine validation are
- load-bearing correctness assets — carry them into the new structure intact, do
- not rewrite them.
-- **SEI scheme is frozen.** `loomweave:eid:...` is consumed, never minted or
- reinterpreted (`bindings.py` docstring). Reframe work depends on Loomweave for
- identity/rename and on Legis for any teeth/audit — build neither here.
-- **Verify the backlog first.** Read `plainweave-c2d58800a0` (epic) + siblings
- and the referenced implementation plan before acting — the sequencing below may
- already be partly planned; reconcile rather than duplicate.
-
-## Recommended sequence
-
-### Phase 0 — Cheap, high-value extractions (de-risk everything else)
-
-These are mechanical, low-risk, and remove the layering inversions that make the
-big refactor cleaner.
-
-1. **Extract `serializers.py` / `views.py` (DEBT-02).** Move the `_*_dict`
- mappers + `inspect_project` + `_current_project_key` out of `cli_commands.py`
- into a neutral module as public API. Repoint both `cli_commands` and
- `mcp_surface` at it. Removes the `mcp → cli` private-import inversion.
- *Effort: M. Risk: Low.*
-2. **Close the persistence boundary (DEBT-03).** Add `PlainweaveService.initialize()`
- / `inspect()` (or a small `bootstrap` seam) owning `migrate`/`connect`/
- `read_schema_meta`; remove the `store` import from `cli_commands`. Service
- becomes the sole funnel into persistence. *Effort: S. Risk: Low.*
-3. **Split `models.py` (DEBT-06).** Move the 10 `Dossier*` read-model dataclasses
- to `read_models.py` (or co-locate with the dossier assembler from Phase 1).
- *Effort: M. Risk: Low.*
-
-### Phase 1 — Decompose `PlainweaveService` (DEBT-01) — *do before the reframe*
-
-Split the god-object along its six existing responsibility clusters, behind a
-thin facade so callers (CLI, MCP) need not change all at once:
-
-- `RequirementService` (lifecycle: create/update/approve/supersede/reject/deprecate
- + acceptance criteria)
-- `TraceService` (propose/create/accept/reject/stale/orphan + `trace_for`)
-- `VerificationService` (methods, evidence, status computation)
-- `BaselineService` (create/diff/list/show)
-- `DossierAssembler` (the `_dossier_*` read aggregation)
-- A shared **persistence-repository / unit-of-work** layer holding `connect`,
- `_record_event`, `_idempotent_*`, the row↔dataclass mappers, and ID allocators.
-
-Keep `PlainweaveService` as a façade delegating to these, then migrate callers
-incrementally. *Effort: L. Risk: Low (test-backed). This is the highest-leverage
-item; deferring it compounds against the reframe.*
-
-### Phase 2 — Schema upgrade ladder (DEBT-07) — *before the reframe touches the DB*
-
-Replace the flat `SCHEMA_VERSION = 1` + monolithic `migrate()` with a versioned
-step structure (ordered `(from_version, fn)` ladder or per-version SQL keyed off
-the stored `schema_version`). Extend `tests/test_store_migrations.py` to assert
-*upgrade* steps, not just first-creation. Needed because the reframe adds
-goal-node and (possibly) binding-cache tables to existing databases.
-*Effort: M now / L if deferred.*
-
-### Phase 3 — Build the reframe on the clean base
-
-Only after Phases 0–2. The reframe-readiness finding (`05`, `service.py:1877`,
-`:977`) dictates the shape:
-
-1. **Data-drive the trace ontology.** Move the hardcoded `(from_kind, relation,
- to_kind)` allow-list out of the (now-decomposed) service into a registry/table
- so new altitudes don't require hand-editing a `set`. Add the `goal` node kind
- and the `requirement → goal` ("justified by") triple — the reframe's defining
- edge.
-2. **Implement the graph walk.** Build `trace(node)` / `orphans(level)` as
- recursive-CTE graph queries over `trace_links` inside a dedicated
- **`IntentGraphService`** (wiring up `intent_graph.IntentGraphReads`), *not* as
- more methods on the façade. `trace_for`'s single-hop filter is the precursor to
- replace, not extend.
-3. **Wire ADR-029 bindings (`bindings.py`).** Implement `bind_sei_to_requirement`
- / `is_drifted` against the entity-association contract, keyed by SEI with
- `content_hash_at_attach` drift detection. Consume Loomweave for SEI
- resolution; do not build identity locally.
-4. **Authoring-time write path + Legis boundary cell** per the design — advisory
- by default; surface coverage facts at the git/CI boundary through Legis.
-
-## Cross-pack recommendations
-
-- **Security / threat modeling:** *Not warranted now* — local SQLite, stdio MCP,
- no network surface, one runtime dep. **Re-evaluate** (e.g. `ordis-security-architect`,
- `wardline` trust-boundary review) if/when a network seam is added (Legis HTTP
- boundary). The repo already wires `wardline` as its trust-boundary gate
- (CLAUDE.md) — run `wardline scan . --fail-on ERROR` on any boundary-touching
- reframe code.
-- **Test gaps:** run a coverage pass (`ordis-quality-engineering:analyze-test-gaps`)
- to confirm per-cluster `service.py` coverage before decomposition, so the safety
- net is verified, not assumed.
-- **Decomposition execution:** the `axiom-python-engineering:refactoring-architect`
- agent is the right tool to sequence the Phase-1 extraction as behavior-preserving
- moves.
-
-## Open questions for the owner / next architect
-
-1. Does the `.filigree` epic + implementation plan already sequence
- *decompose-before-build*? If not, this handover argues it should.
-2. Is the goal altitude meant to live in `trace_links` (generic edge reuse) or a
- dedicated table? The analysis recommends edge reuse (the substrate already
- supports it), with the ontology data-driven.
-3. What is the intended migration story for *existing* `.plainweave` databases
- once goal nodes ship — confirm Phase 2 lands first.
-
-## Definition of done for the improvement effort
-
-- `PlainweaveService` is a thin façade; no single core class exceeds ~400 LOC.
-- No presentation module imports another presentation module's privates.
-- `store` is imported only by the persistence/service layer.
-- The trace ontology is data-driven and includes the `goal` kind + `requirement →
- goal` edge.
-- `orphans`/`trace`/`corpus` are implemented and tested over the graph; the
- `NotImplementedError` stubs are gone.
-- `make ci` green throughout; coverage gate held at ≥90%.
diff --git a/docs/arch-analysis-2026-06-21-1754/temp/validation-catalog.md b/docs/arch-analysis-2026-06-21-1754/temp/validation-catalog.md
deleted file mode 100644
index 4d203b4..0000000
--- a/docs/arch-analysis-2026-06-21-1754/temp/validation-catalog.md
+++ /dev/null
@@ -1,160 +0,0 @@
-# Validation Report — 02-subsystem-catalog.md
-
-**Validator:** analysis-validator (independent, fresh-eyes verification)
-**Document:** `docs/arch-analysis-2026-06-21-1754/02-subsystem-catalog.md`
-**Context:** `docs/arch-analysis-2026-06-21-1754/01-discovery-findings.md`
-**Codebase HEAD:** `72e8df2`
-**Date:** 2026-06-21
-
----
-
-## Overall Verdict: **PASS-WITH-NOTES**
-
-The catalog is structurally sound and substantively accurate on every load-bearing
-architectural claim the coordinator asked to verify: the cross-presentation coupling
-(`mcp_surface` → `cli_commands` privates), the leaky persistence boundary
-(`cli_commands` calling `store.connect`/`migrate` directly), the `service.py` god-class,
-and the two unimplemented reframe stubs are all **confirmed against source**. Subsystem
-membership and LOC figures are correct.
-
-Two classes of defect keep this from a clean PASS, neither blocking:
-
-1. **One factual error** — entry 1 attributes "29 frozen dataclasses" to `models.py`;
- `models.py` actually contains **25**. The "29" is the project-wide dataclass
- inventory (25 in models + 3 in `intent_graph` + 1 in `bindings`).
-2. **Overstated/inconsistently-sourced coupling figures** — the fan-in numbers for the
- service methods (`create_requirement` 31, `record_verification_evidence` 24,
- `approve_requirement` 21) are **test-inclusive caller counts**, not production
- coupling. App-only production fan-in for `create_requirement` is **6**. The numbers
- trace to a real Loomweave measure but the framing ("highest-coupled methods in the
- repo") overstates production centrality.
-
-No critical (blocking) issues. Corrections below should be applied before the catalog is
-treated as canonical, but downstream phases may proceed.
-
----
-
-## Contract compliance
-
-All 6 entries carry every required field: **Location, Responsibility, Key Components,
-Dependencies (Inbound/Outbound), Patterns Observed, Concerns, Confidence (with
-reasoning).** Format is consistent across entries. Catalog claims 6 subsystems and
-delivers 6. **PASS** on contract structure.
-
----
-
-## Per-claim findings table
-
-| # | Claim | Verified? | Evidence |
-|---|-------|-----------|----------|
-| 1 | 6 subsystems, each with full contract fields | ✅ Yes | All 6 entries present with all 7 required fields |
-| 2 | Every cited file exists and is in its assigned subsystem | ✅ Yes | All 15 `src/plainweave/*.py` files exist; assignments match module responsibility |
-| 3 | `mcp_surface.py` imports private `_*_dict` helpers + `inspect_project` from `cli_commands` | ✅ Yes | `mcp_surface.py:9-18` imports `_baseline_dict, _baseline_diff_dict, _current_project_key, _dossier_dict, _record_dict, _requirement_verification_status_dict, _trace_dict, inspect_project` |
-| 3a | Catalog's enumerated import list is complete | ⚠️ Minor | Catalog Concerns lists 5 `_*_dict` names + `_current_project_key` + `inspect_project`; **omits `_baseline_diff_dict`** (actual = 7 underscore names). Understates, does not misstate |
-| 4 | `cli_commands.py` calls `store.connect`/`migrate` directly | ✅ Yes | `cli_commands.py:39` imports `connect, migrate, read_schema_meta`; calls at `:627` (`migrate`), `:628`, `:647` (`connect`) |
-| 5 | `service.py` is ~2136 LOC single class | ✅ Yes | `wc -l` = 2136; exactly one `class PlainweaveService` (line 43); 29 public + 64 private methods |
-| 5a | "~29 public + ~40 private methods" | ⚠️ Minor | Public = 29 ✅. Private = **64**, not ~40. Understates private method count substantially (god-object claim is if anything strengthened) |
-| 6 | `intent_graph.py` + `bindings.py` are NotImplementedError stubs | ✅ Yes | `intent_graph.py` raises at `:97,105,113`; `bindings.py` raises at `:62,71` |
-| 7 | Stubs have no intra-package imports (standalone) | ✅ Yes | Both import only stdlib (`__future__`, `dataclasses`, `enum`). Zero `from plainweave`/`from .` imports |
-| 8 | LOC figures for all modules | ✅ Yes | All match exactly: service 2136, mcp_surface 1141, cli_commands 1066, models 273, store 254, mcp_server 132, envelopes 115, intent_graph 113, bindings 71, cli 35, errors 34, paths 24 |
-| 9 | `store.migrate` is "227-line" migration | ✅ Yes | `migrate` spans lines 22–249 (≈227 lines incl. signature). Accurate |
-| 10 | `store.py` outbound deps = stdlib `sqlite3` only | ✅ Yes | Imports only `sqlite3`, `collections.abc`, `contextlib`, `pathlib`. No intra-package imports |
-| 11 | `models.py`/`errors.py`/`paths.py` are pure leaf (no intra-package imports) | ✅ Yes | `models.py` has zero `from plainweave` imports; errors/paths likewise |
-| 12 | Service Core outbound = Store + Domain Model & Errors | ⚠️ Minor | `service.py:12` imports errors, `:13` models, `:40` `store.connect, read_schema_meta`. Correct, but service imports **only `connect`/`read_schema_meta`, not `migrate`** — direction is right, granularity unstated (not an error) |
-| 13 | `models.py` — "29 frozen dataclasses" | ❌ **No** | `models.py` has **25** `@dataclass` (all 25 `frozen=True`). The "29" is project-wide (25 + 3 stub + 1 binding). **Factual error in entry 1** |
-| 14 | `store.connect` fan-in 48, highest-coupled in repo | ⚠️ Partial | "Highest-coupled" ✅ (tops coupling hotspot list). "48": resolved callers incl. tests ≈ 50; **app-only production coupling = 32**. Figure is test-inclusive; approximately right but mis-framed |
-| 15 | `create_requirement` fan-in 31 | ⚠️ Overstated | Resolved callers ≈ 33 but **all are test functions**; **app-only production fan_in = 6**. "31" is test-inflated |
-| 16 | `approve_requirement` fan-in 21 | ⚠️ Overstated | App-only fan_in = **6** (coupling 18 = 6 in + 12 out). "21" is test-inclusive |
-| 17 | `record_verification_evidence` fan-in 24 | ⚠️ Overstated | Not in top-15 production hotspots; caller list dominated by tests. App-only fan-in far below 24 |
-| 18 | `_handle_service_result` fan-in 18 | ✅ Yes | Loomweave: fan_in 18 exactly |
-| 19 | `mcp_server.create_mcp_server` fan-out 14 | ✅ Yes | Loomweave: fan_out 14 exactly |
-| 20 | `plainweave_preflight_facts_get` fan-out 11 | ✅ Yes | Loomweave: fan_out 11 exactly |
-| 21 | `cli.main` fan-in 21 | ⚠️ Likely test-inclusive | Consistent with the other CLI/service figures being test-inclusive; not independently re-counted here |
-| 22 | Dependency direction (leaf→root) diagram | ✅ Yes | Import graph confirms: models/errors/paths are leaves; store depends on nothing intra-package; service → store+models+errors; CLI+MCP → service; MCP → cli_commands |
-| 23 | Reframe stubs "not wired to anything" | ✅ Yes | Zero intra-package imports in/out of `intent_graph.py`/`bindings.py`; no other module imports them |
-
----
-
-## Corrections needed (apply before treating catalog as canonical)
-
-1. **Entry 1, Key Components (BLOCKING for accuracy, not for progression):**
- Change "`models.py` — 29 frozen dataclasses" → "`models.py` — **25** frozen
- dataclasses". If the intent was the project-wide count, state it as "25 in
- `models.py`; 29 domain dataclasses project-wide including the 4 reframe-stub shapes."
- (The discovery doc §5 already handles this correctly — the catalog regressed it.)
-
-2. **Entries 2 & 3, coupling figures (framing fix):** Qualify the fan-in numbers.
- Either (a) report the **app-only/production** figures (`store.connect` 32,
- `create_requirement` 6, `approve_requirement` 6) when arguing architectural
- centrality, or (b) explicitly label the current numbers as "total callers incl.
- tests." As written, "highest-coupled methods in the repo" reads as production
- coupling but is dominated by test callers. The god-object verdict does **not** depend
- on these numbers (LOC + method count carry it), so this is presentation hygiene, not a
- load-bearing correction.
-
-3. **Entry 3, Concerns (completeness):** The imported-privates list omits
- `_baseline_diff_dict`. Add it for an accurate enumeration (7 underscore-private names,
- not 6).
-
-4. **Entry 3, Service Core:** "~40 private methods" → **64** private methods (the
- god-object concern is strengthened, not weakened, by the correction).
-
----
-
-## Cross-document consistency
-
-- Discovery (01) and catalog (02) agree on subsystem set, module assignment, and LOC.
-- **Divergence:** Discovery §5 correctly separates models' 25 dataclasses from the 4
- reframe-stub shapes; catalog entry 1 collapses them into "29 in `models.py`". Catalog
- should inherit discovery's more careful framing.
-- Both docs carry the same test-inclusive fan-in figures; the inconsistency is
- systemic to the analysis, not a catalog-only regression.
-
----
-
-## Scope boundary note
-
-This validation covers **structural** correctness: contract compliance, file/module
-existence and assignment, dependency-edge reality, LOC accuracy, and figure
-verification against source + Loomweave. It does **not** adjudicate technical-accuracy
-judgments (e.g., whether "extract per-aggregate services" is the right refactor, or
-whether the idempotency/event-log design is sound) — those require a Python/architecture
-SME (`axiom-python-engineering:python-code-reviewer` /
-`axiom-system-architect:architecture-critic`).
-
----
-
-## Confidence Assessment
-
-**High.** Every claim in the findings table was checked against primary source
-(`grep`/`wc`/`Read`) and/or the Loomweave index, not against the document's assertions.
-The two defects (the 25-vs-29 error and the test-inclusive fan-in framing) were
-independently reproduced from `models.py` source and Loomweave `entity_callers_list` /
-`entity_coupling_hotspot_list` respectively.
-
-## Risk Assessment
-
-**Low.** No blocking errors. The factual error (29 dataclasses) is contained to one
-sentence and self-correcting against the discovery doc. The coupling-figure framing
-could mislead a downstream quality/architecture pass into overstating production
-coupling of service methods, but the structural verdicts (god-class, leaky persistence,
-homeless serializers, unimplemented reframe) stand on independent, verified evidence.
-
-## Information Gaps
-
-- `cli.main` fan-in 21 was not independently re-counted; flagged as "likely
- test-inclusive" by consistency with the other figures, not verified.
-- I did not enumerate the full ~29 public method inventory of `PlainweaveService`
- against the catalog's bulleted list one-by-one; public count (29) matches and the named
- methods all exist, but exhaustive name-by-name parity was not performed.
-- Loomweave's "resolved callers" excludes attribute-receiver dynamic call sites
- (e.g. `service.create_requirement`), which it lists as unresolved; the true production
- fan-in of service methods is therefore *higher than the app-only resolved count but
- still test-dominated* — the qualitative finding (test-inflated) holds regardless.
-
-## Caveats
-
-- Coupling figures depend on Loomweave index freshness (HEAD `72e8df2`,
- reported fresh); if re-indexed, re-verify.
-- "Approximately correct" LOC tolerance was treated generously; all figures in fact
- matched **exactly**, so no tolerance was needed.
diff --git a/docs/arch-analysis-2026-06-28-0751/00-coordination.md b/docs/arch-analysis-2026-06-28-0751/00-coordination.md
new file mode 100644
index 0000000..851243b
--- /dev/null
+++ b/docs/arch-analysis-2026-06-28-0751/00-coordination.md
@@ -0,0 +1,99 @@
+# 00 — Coordination Plan
+
+## Analysis Configuration
+
+- **Subject:** Plainweave — "permission for code to exist." Code-grounded
+ requirements-traceability / intent-corpus member of the Weft federation.
+- **Scope:** `src/plainweave/` (production source, ~9.6K LOC across 27 `.py`
+ files) + supporting `tests/` (~10.1K LOC) for verification evidence. Docs
+ read for product framing, not analyzed as code.
+- **Deliverables:** **Option C — Architect-Ready** (full analysis + code-quality
+ assessment + architect handover):
+ - `01-discovery-findings.md` (holistic assessment)
+ - `02-subsystem-catalog.md` (per-subsystem evidence-based entries)
+ - `03-diagrams.md` (C4 context / container / component)
+ - `04-final-report.md` (synthesis)
+ - `05-quality-assessment.md` (tech debt, hotspots)
+ - `06-architect-handover.md` (improvement backlog → feeds axiom-system-architect)
+- **Complexity estimate:** Medium. ~9.6K LOC, Python 3.12, single deployable
+ with three delivery surfaces (CLI / MCP / web) over one domain service +
+ SQLite + two sibling-tool adapters. Loomweave index is **fresh**
+ (1256 entities, 4282 edges, 46 fine-grained clusters), so structural facts
+ are queryable rather than grep-derived.
+
+## Orchestration Strategy — PARALLEL (with validation gates)
+
+**Decision: parallel subsystem exploration.** Rationale:
+
+- 7 cohesive subsystems identified (below), loosely coupled: the three
+ surfaces depend on the service but not on each other; adapters and store are
+ isolated; cross-cutting (envelopes/errors/paths) is leaf-level.
+- Although LOC (~9.6K) is under the 20K "large" threshold, the heaviest unit
+ (`service.py`, 3027 LOC) plus two ~1.6K-LOC surface modules make
+ single-pass sequential analysis lossy. Parallel explorers each own a bounded
+ region and return schema-conforming catalog entries with file/line evidence.
+- Multi-subsystem (≥3) ⇒ **validation subagent is MANDATORY** after the
+ catalog and after the final report (per the analyze-codebase contract).
+
+**Explorer fan-out (5 agents, balanced by LOC):**
+
+| Agent | Subsystem(s) | Modules | ~LOC |
+|-------|--------------|---------|------|
+| E1 | Domain Service Core | `service.py`, `models.py`, `intent_graph.py`, `bindings.py` | 3619 |
+| E2 | MCP Surface | `mcp_server.py`, `mcp_surface.py` | 1837 |
+| E3 | CLI Surface | `cli.py`, `cli_commands.py` | 1669 |
+| E4 | Web UI | `web/` (app, server, context, views, errors, routes/*) | ~900 |
+| E5 | Persistence + Sibling Adapters + Response Contract | `store.py`, `loomweave_adapter.py`, `wardline_adapter.py`, `envelopes.py`, `errors.py`, `paths.py` | ~1500 |
+
+Then: validation gate (analysis-validator) → diagrams → final report →
+validation gate → quality assessment → architect handover.
+
+## Execution Log
+
+- 2026-06-28 07:51 — Created workspace `docs/arch-analysis-2026-06-28-0751/`.
+- 2026-06-28 07:52 — User selected **Option C (Architect-Ready)**.
+- 2026-06-28 07:53 — Holistic scan complete (filesystem + Loomweave index):
+ tree mapped, LOC distribution measured, entry points + coupling hotspots
+ pulled, README/product framing read. Subsystems identified.
+- 2026-06-28 07:54 — Strategy set to PARALLEL (5 explorers). Wrote coordination
+ plan + discovery findings. Advisor sanity-checked the decomposition.
+- 2026-06-28 07:55 — Dispatched 5 `codebase-explorer` agents (E1–E5). In
+ parallel, orchestrator built the authoritative dependency map from the
+ Loomweave global graph (no cycles; N+1 confirmed at source; CLI→store leak;
+ warpline = producer seam) and gathered the test/coverage taxonomy.
+- 2026-06-28 08:01–08:07 — Explorers returned (8 catalog entries). **Fidelity
+ correction:** live HEAD is `8258f76`, 6 commits / +80 LOC past the indexed
+ `e95b6ad` (confined to cli_commands.py + mcp_surface.py — peer-facts CLI/MCP
+ parity); basis label corrected in `01`.
+- 2026-06-28 08:08 — Merged `02-subsystem-catalog.md` (8 entries + cross-cutting
+ themes), reconciling one-sided explorer edge claims against the global graph.
+- 2026-06-28 08:09 — **Validation gate:** dispatched `analysis-validator`.
+ Verdict **PASS-WITH-FIXES** — 8/8 entries contract-complete, all 8 high-stakes
+ claims VERIFIED against live source, no over-claim. 3 Low citation fixes
+ applied (web route locus, local-import line nums, `_register_*` count).
+- 2026-06-28 08:10 — Wrote `03-diagrams.md` (6 C4-style + sequence views) from
+ the verified edge map.
+- 2026-06-28 08:12 — Wrote `04-final-report.md`, `05-quality-assessment.md`
+ (22-item severity-rated register), `06-architect-handover.md` (7 sequenced
+ initiatives → axiom-system-architect).
+- 2026-06-28 08:13 — Advisor review caught (a) a dropped second validation gate
+ and (b) an over-claim: Initiative A "removes the `ResourceWarning`" — but
+ production connections already close deterministically; verified the warning is
+ a **test-fixture** leak. Corrected → new finding **Q23**; fixed `05`/`06`.
+- 2026-06-28 08:14 — **Validation gate 2:** dispatched `analysis-validator` over
+ the synthesis layer (`03`–`06`). Verdict **PASS-WITH-FIXES** — all 23 Q-items
+ trace to source/catalog, severity 4/8/11 consistent, diagrams faithful,
+ remediations sound. 3 fixes applied: stale `ResourceWarning` clause in `04`
+ (the required one); tracker `3edcd19943`→Q5 remapped to preflight Q3/Q4; SQLite
+ mapping precision (UNIQUE/PK→CONFLICT; `BEGIN IMMEDIATE` prevents the race).
+- _status_ — **COMPLETE & DOUBLE-VALIDATED.** All Option-C deliverables produced;
+ both mandatory validation gates passed with fixes applied.
+
+## Outcome
+
+Plainweave is a clean, conventional, well-tested layered service (3 surfaces → 1
+domain service → SQLite + adapters + contract). No module cycles; ≥90% branch
+gate; golden-vector seam tests. Risk concentrates in (1) a 3027-LOC god object
+and (2) a pervasive connect-per-call / N+1 / no-WAL persistence pattern; one real
+correctness gap (DB exceptions escape the `ErrorCode` contract). All bounded by
+the single-operator local-first scope and addressable via the handover backlog.
diff --git a/docs/arch-analysis-2026-06-28-0751/01-discovery-findings.md b/docs/arch-analysis-2026-06-28-0751/01-discovery-findings.md
new file mode 100644
index 0000000..fbef090
--- /dev/null
+++ b/docs/arch-analysis-2026-06-28-0751/01-discovery-findings.md
@@ -0,0 +1,142 @@
+# 01 — Discovery Findings (Holistic Assessment)
+
+**Subject:** Plainweave · **Live tree:** HEAD `8258f76` · **Date:** 2026-06-28
+**Method:** filesystem scan + Loomweave index + product docs. Evidence is cited
+as `file:line` (live tree) or Loomweave fan-in/out (indexed commit).
+
+> **Basis fidelity (read this).** The catalog and concerns reflect the **live
+> working tree at HEAD `8258f76`** (what the explorers read). Loomweave's
+> structural metrics (coupling, fan-in/out, caller lists, `cycles:[]`) reflect
+> the **indexed commit `e95b6ad`**, which is **6 commits / +80 net src LOC
+> behind** — and that delta is confined to **two files**
+> (`cli_commands.py` +77, `mcp_surface.py` +9/−3): the `wardline-peer-facts`
+> and `requirements-enrichment` CLI/MCP parity subcommands plus a peer-facts
+> fix. **All other modules are byte-identical**, so the god-object / N+1 /
+> layering findings are unaffected. Two index-vs-live discrepancies to carry
+> forward: (a) the live CLI exposes 16 commands / 38 handlers vs. the indexed
+> set; (b) the new `handle_wardline_peer_facts` adds a **function-local (lazy)
+> import** of `PlainweaveMcpSurface` — a CLI↔MCP coupling absent from the
+> e95b6ad graph (hence the index's `cycles:[]`), confirmed by explorers reading
+> live. Refresh with `loomweave analyze` before treating the edge metrics as
+> current.
+
+---
+
+## 1. What Plainweave is
+
+A **code-grounded requirements-traceability and intent-corpus** service — the
+"permission for code to exist" member of the Weft federation. It maintains a
+traceability graph:
+
+```
+strategic goal ──▲── requirement ──▲── code SEI (leaf)
+ (root intent) (obligation) (the thing that exists)
+```
+
+Edges mean *"justified by / satisfies."* Leaves are Loomweave SEIs (stable
+code identities surviving rename/move); interior nodes are typed intent nodes.
+The headline metric is **`intent_coverage`** — the fraction of public code
+surfaces that ladder up to a requirement. Doctrine: **advisory / enrich-only**
+(Plainweave never gates; it delegates teeth + audit to Legis and identity +
+semantics to Loomweave). README:50–115.
+
+**Maturity:** README declares 1.0 / "Production/Stable" (pyproject classifier
+`Development Status :: 5`); green gate = ruff + mypy `strict` + pytest at
+≥90% coverage. Tests (~10.1K LOC) slightly exceed source (~9.6K LOC).
+
+## 2. Technology stack
+
+- **Language:** Python ≥3.12 (`.python-version`, `requires-python`).
+- **Runtime deps:** `mcp>=1.2.0` only (the official MCP SDK). Deliberately thin.
+- **Optional extra `[web]`:** `starlette`, `uvicorn`, `jinja2` — the operator UI.
+- **Persistence:** SQLite via stdlib `sqlite3` (no ORM); `store.py` owns
+ connect + schema migration.
+- **Build/tooling:** `hatchling`, `uv`, `ruff` (E,F,I,UP,B,SIM; line 120),
+ `mypy --strict`, `pytest` + `coverage` (gated). `Makefile`/`make ci`.
+- **Entry points (`pyproject [project.scripts]`):**
+ - `plainweave = plainweave.cli:main` — CLI
+ - `plainweave-mcp = plainweave.mcp_server:main` — read-only MCP server
+ - (web is a CLI subcommand `plainweave web`, gated behind the extra)
+- Loomweave confirms exactly **two `entry-point`-tagged functions**:
+ `plainweave.cli.main`, `plainweave.mcp_server.main`.
+
+## 3. Codebase shape (measured)
+
+Source is a **mostly-flat module package** under `src/plainweave/` (17 direct
+modules, 8653 LOC) plus a `web/` subpackage (~900 LOC). LOC by module:
+
+| Module | LOC | Role (first read) |
+|--------|-----|-------------------|
+| `service.py` | **3027** | `PlainweaveService` — the domain god-object; all use-cases |
+| `mcp_surface.py` | 1653 | MCP tool implementations (read + preflight + peer facts) |
+| `cli_commands.py` | 1631 | CLI subcommand handlers |
+| `loomweave_adapter.py` | 657 | Loomweave catalog/SEI/semantic enrichment seam |
+| `wardline_adapter.py` | 373 | Wardline peer-facts seam |
+| `store.py` | 311 | SQLite connect + schema migration |
+| `models.py` | 310 | domain dataclasses / typed records |
+| `web/routes/review.py` | 258 | ratification queue routes |
+| `web/routes/requirements.py` | 215 | requirement authoring routes |
+| `mcp_server.py` | 184 | MCP server wiring (`create_mcp_server`) |
+| `intent_graph.py` | 184 | coverage / orphans / trace graph computation |
+| `web/views.py` | 130 | view-model assembly |
+| `envelopes.py` | 115 | versioned success/error JSON envelopes |
+| `bindings.py` | 98 | ADR-029 SEI entity-association bindings |
+| `web/app.py` | 80 | Starlette app factory |
+| `web/context.py` | 61 | request/service context |
+| `web/server.py` | 59 | uvicorn launcher |
+| smaller: `web/routes/{goals,intent}.py`, `cli.py`, `errors.py`, `paths.py`, `web/errors.py`, `__main__.py` | <50 each | wiring / leaf contracts |
+
+`src/plainweave/experimental/` exists but holds **no live `.py`** (only stale
+`__pycache__` for a `plan_check` module) — a dead/abandoned package to flag.
+
+## 4. Subsystems identified (7)
+
+1. **Domain Service Core** — `service.py`, `models.py`, `intent_graph.py`,
+ `bindings.py`. The `PlainweaveService` orchestrates every use-case (create/
+ approve requirement, acceptance criteria, verification evidence, trace links,
+ dossier, baseline, events). `intent_graph` computes coverage/orphans/trace.
+2. **MCP Surface** — `mcp_server.py` (`create_mcp_server`, fan-out 21),
+ `mcp_surface.py`. Read-only tools (`mutates:false`, `local_only:true`)
+ mirroring intent reads + `preflight_facts` + Wardline peer facts.
+3. **CLI Surface** — `cli.py` (`main`, entry-point), `cli_commands.py`.
+ Argparse dispatch over `init/intent/req/goal/bind/catalog/criterion/verify/
+ status/dossier/baseline/actor/doctor/web`.
+4. **Web UI** — `web/` Starlette operator console (browse corpus, author
+ requirements, ratify drafts/trace links). Local-first, single-operator,
+ advisory.
+5. **Persistence** — `store.py`. SQLite `connect` (fan-in **44** — the single
+ most-coupled entity in the codebase) + `migrate` (schema, fan-in 14).
+6. **Sibling-Tool Adapters** — `loomweave_adapter.py` (catalog/SEI/semantic),
+ `wardline_adapter.py` (peer facts). Enrich-only seams; siblings absent ⇒
+ explicit `unavailable`, never an implied clean state.
+7. **Response Contract / Cross-cutting** — `envelopes.py` (versioned envelopes),
+ `errors.py` (closed error-code vocab, fan-in 18), `paths.py` (store-path
+ resolution), `_version.py`.
+
+## 5. Coupling & risk signals (from the index, pre-catalog)
+
+- **`store.connect` fan-in 44** — every persistence path opens its own
+ connection (per-call connect pattern). Two open P3 tracker tasks confirm the
+ smell: *"N+1 SQLite connections per scoped requirement"* and *"`project`
+ scope fans out over all requirements with no cap / facts pagination."*
+- **`service.py` at 3027 LOC** is a god-object: `_error`, `_now`,
+ `_require_actor`, `_record_event`, `_requirement_row` are high-fan-in private
+ helpers shared across dozens of use-cases. Prime refactor target for the
+ quality pass.
+- Envelope/error helpers (`_error` fan-in 36, `envelopes.success_envelope`
+ fan-in 11, `errors` module fan-in 18) show a **consistent response contract**
+ applied uniformly — a strength.
+- Surfaces are thin over the service: `_handle_service_result` (CLI, fan-in 24)
+ and `_result` (MCP, fan-in 16) are the uniform service→surface adapters.
+
+## 6. Open questions for the catalog/quality pass
+
+- Exact dependency direction between `service` ↔ adapters (does the service call
+ adapters, or do surfaces compose them?) — confirm inbound/outbound per entry.
+- Web UI mutation surface vs. the "MCP is read-only" claim — where do writes
+ land and how is the single-operator actor enforced?
+- SQLite migration/versioning discipline in `store.migrate` (idempotency,
+ forward-only?).
+- The dead `experimental/` package — confirm it is truly unreferenced.
+- Test-to-subsystem mapping for the quality pass (conformance/contracts dirs
+ suggest golden-vector seams).
diff --git a/docs/arch-analysis-2026-06-28-0751/02-subsystem-catalog.md b/docs/arch-analysis-2026-06-28-0751/02-subsystem-catalog.md
new file mode 100644
index 0000000..dea829a
--- /dev/null
+++ b/docs/arch-analysis-2026-06-28-0751/02-subsystem-catalog.md
@@ -0,0 +1,523 @@
+# 02 — Subsystem Catalog
+
+**Subject:** Plainweave · **Live tree:** HEAD `8258f76` · **Date:** 2026-06-28
+**Method:** 5 parallel `codebase-explorer` agents read 100% of their assigned
+source; the orchestrator reconciled all inter-subsystem edges against the
+Loomweave global graph (`02`/`temp/dependency-reconciliation.md`). Citations are
+`file:line` against the live tree; coupling/fan-in figures are Loomweave at
+`e95b6ad` (see basis-fidelity note in `01-discovery-findings.md`).
+
+8 subsystems are cataloged. The defining structural fact is that **all three
+delivery surfaces (CLI, MCP, Web) are thin adapters over one
+`PlainweaveService`**, which is simultaneously the use-case tier, the
+data-access tier, and the intent-graph engine.
+
+## Cross-cutting themes (verified by ≥2 explorers)
+
+- **Connect-per-call / N+1 is pervasive and is the dominant scaling+concurrency
+ risk.** `store.connect` opens a fresh SQLite connection per operation (no
+ pool), in `DELETE` journal mode (no WAL → writers exclusive-lock readers).
+ It surfaces as: a connection per catalog entity in coverage
+ (`service.py:1467→1479→1529-1550`); two connections per Loomweave
+ `list_catalog`; 3 service calls × entire-corpus in MCP preflight
+ (`mcp_surface.py:990,1084-1086`, no pagination); and per-draft dossier fetches
+ per web render (`views.py:82-100`). Two open P3 tracker tasks already name it.
+- **DB exceptions escape the `ErrorCode` contract.** Only domain failures route
+ through `_error`→`PlainweaveError`; raw `connection.execute` is unguarded, so
+ `sqlite3.IntegrityError`/`OperationalError` propagate past callers that switch
+ on `ErrorCode`. Compounded by `count(*)+1` id generation (race) and no
+ `busy_timeout`/WAL/`BEGIN IMMEDIATE` — fails *safe* (UNIQUE/PK) but a
+ concurrent loser surfaces raw, not as a clean `CONFLICT`.
+- **`cli_commands.py` is a de-facto shared services/DTO layer.** The MCP surface
+ imports its private serializers + `inspect_project`; the new CLI
+ `wardline-peer-facts` handler lazily imports `PlainweaveMcpSurface`. **No
+ module-load cycle exists** (Loomweave `cycles:[]`; the function-local import
+ deliberately dodges one) — but it is a real bidirectional surface↔surface
+ coupling that inverts the intended layering.
+- **Honest enrich-only degradation everywhere** (a strength): typed
+ `unavailable`/closed degrade vocab, never an implied-clean; `live_peer_calls`
+ is hard-`False`; drift is flagged `stale`, never silently dropped.
+- **Non-deterministic `generated_at`** (`datetime.now(UTC)` default) defeats
+ byte-stable golden comparison unless callers inject the timestamp.
+
+---
+
+## Domain Service Core
+
+**Location:** `src/plainweave/service.py`, `src/plainweave/models.py`, `src/plainweave/bindings.py`
+
+**Responsibility:** The domain orchestrator — a single `PlainweaveService`
+(`service.py:64`) owning every traceability use case (requirement lifecycle,
+acceptance criteria, trace links, goals/intent edges, code-entity & SEI
+bindings, baselines, verification, dossiers, and the intent-graph reads) over a
+directly-accessed SQLite store, emitting an append-only event log; advisory and
+enrich-only.
+
+**Key Components:**
+- `service.py` (3027 LOC, one class, ~40 public use-case methods + ~70 private
+ helpers): requirement lifecycle (`create_requirement:454`, `update_draft:512`,
+ `approve_requirement:565`, `supersede:626`, `reject:720`, `deprecate:758`);
+ acceptance criteria (`810`/`871`); trace links funnel through
+ `_transition_trace:2386` with relation/transition whitelists (`2767`/`2782`);
+ goals & intent edges (`create_goal:1020`, `link_goal_to_requirement:1060`);
+ ADR-029 SEI bindings (`record_code_entity:1137`, `bind_sei_to_requirement:1198`
+ → `entity_associations`); baselines (`create_baseline:304`, `diff_baseline:372`,
+ immutability by DB triggers `store.py:178`); verification
+ (`add_verification_method:148`, `record_verification_evidence:197`,
+ `_compute_verification_status:2307`, `_evidence_authority:2994`); dossier
+ (`requirement_dossier:260` + `_dossier_*` assemblers); actor registry with
+ genesis-attester bootstrap (`register_actor:78`, `108-124`).
+- **High-fan-in private cluster** (the monolith's cohesion): `_error:3020` (sole
+ `PlainweaveError` factory, fan-in 36), `_now:3026`, `_require_actor:2968`,
+ `_record_event:2792` (append-only `events` writer), `_requirement_row:2086`
+ (multi-key resolver).
+- `models.py` (310 LOC) — ~30 frozen domain dataclasses; pure leaf (stdlib only).
+- `bindings.py` (98 LOC) — ADR-029 `SeiBinding` value object (`:29`), drift helper
+ `is_drifted:91`; declares the `loomweave:eid:` scheme FROZEN (consumed, never
+ minted, `:16`). Pure leaf.
+
+**Dependencies:**
+- Inbound: CLI (`cli_commands.py:1226`), MCP (`mcp_surface.py:743`), Web
+ (`web/context.py:28`) each construct `PlainweaveService`; tests.
+- Outbound: Persistence (`from plainweave.store import connect`, `service.py:60`;
+ calls `connect()` in every method — it *is* the data-access tier, no repo);
+ Sibling-Tool Adapters (built per-call, `_loomweave_adapter:2595`,
+ `_wardline_adapter:2598`); Intent Graph (imports its types, `service.py:15`);
+ Response Contract (`from plainweave.errors import ErrorCode, PlainweaveError`).
+
+**Patterns Observed:**
+- Canonical use-case template: `_require_actor` → validate → `_now()` →
+ `with connect()` → `_requirement_row()` → SQL mutate → `_record_event()` →
+ `commit()` → return a frozen `*_from_row` dataclass.
+- Event sourcing: every mutation appends `EVT-{uuid4}` to the trigger-protected
+ append-only `events` table (`_record_event:2792`, `store.py:269`).
+- Optimistic concurrency + idempotency: writes gate on
+ `expected_version`/`expected_draft_revision`; approve/supersede/deprecate cache
+ & replay responses in `idempotency_keys` (`574-582`, `_idempotency_payload:2872`).
+- Authority from the registry, not the actor string: `_evidence_authority:2994`
+ derives authority from `actors.kind`; a free-form `--actor` defaults to
+ least-privileged `agent_reported`. `_require_actor` is only a non-empty check —
+ normal-write attribution is honour-system by design.
+- Enrich-only trace hydration with explicit drift (`_trace_from_row:2449` →
+ `freshness=stale` + `content_hash_drift` on mismatch).
+
+**Concerns:**
+- **God object / no layering** — 3027-LOC class spanning ~13 aggregates is both
+ use-case and data-access tier (raw inline SQL). The dominant maintainability
+ risk (`service.py:64-3027`).
+- **DB exceptions escape `ErrorCode`** (`connection.execute` unwrapped) — see
+ cross-cutting themes. Fully verifiable in `service.py` + `store.py`.
+- **`count(*)+1` id generation is not concurrency-safe** (`2110`,`2137`,`2149`);
+ fails safe via UNIQUE/PK but yields a raw exception under contention.
+- **N+1 connections in coverage** — `intent_coverage:1415` →
+ `_goal_nodes_for_surface:1529` opens a fresh `connect()` per surface inside the
+ per-entity loop.
+
+**Confidence:** High — 100% of `service.py`/`models.py`/`bindings.py`/`store.py`
+read; inbound edges cross-checked via Loomweave callers (dynamic-construction
+candidates corroborated by import grep).
+
+---
+
+## Intent Graph
+
+**Location:** `src/plainweave/intent_graph.py`
+
+**Responsibility:** Defines the intent-graph vocabulary and read contract — the
+`goal ▲ requirement ▲ code-SEI` node/altitude types and the
+coverage/orphans/trace/corpus result records (including the honest north-star
+`IntentCoverage`) — over which `PlainweaveService` computes the actual reads.
+
+**Key Components:**
+- `intent_graph.py` (184 LOC, pure type/contract, no DB): `IntentLevel:28`
+ (CODE/REQUIREMENT/GOAL); `IntentNode:44`, `Trace:55`, `CorpusEntry:67`;
+ `IntentCoverage:100` + `IntentCoverageSurface:84` with honesty fields
+ `denominator_complete:117`, `surfaces_truncated:124`, `excluded_*:121-122`,
+ `adapter_status/adapter_degraded:125-126` (docstring: ADVISORY, never
+ pass/fail, `:108`); `DEFAULT_INTENT_COVERAGE_EXCLUDED_NAMESPACES=("scripts.",
+ "tests.")` (`:80`); injectable `IntentGraphReads:141` facade.
+- **Computation lives in the service, not here:** `intent_orphans:1311`,
+ `intent_trace:1346`, `intent_corpus:1388`, `intent_coverage:1415` are all in
+ `service.py`. This file is the typed boundary they return across.
+
+**Dependencies:**
+- Inbound: Domain Service Core (imports types, `service.py:15`); CLI/MCP/Web
+ serialize the result types; `IntentGraphReads` is constructed **only by tests**.
+- Outbound: none — stdlib `dataclasses`/`enum`/`typing` only. Dependency-free leaf.
+
+**Patterns Observed:**
+- Honesty qualifiers surfaced, not computed away: counts are always full while
+ evidence lists are bounded by `max_surfaces` with `surfaces_truncated`
+ flagging drops; `denominator_complete` from `coverage["complete"]`
+ (`service.py:1497`); `present_plugins` carried verbatim from
+ `loomweave_adapter.py:203`.
+- Coverage counts LIVE justification only (`_live_requirement_ids_for_entity`,
+ `status in ('draft','approved')`), deliberately diverging from `intent_trace`
+ which keeps surfacing deprecated reqs — "trace *explains*, coverage *counts*"
+ (`service.py:1537-1539`).
+- Prescribe-nothing: three composable primitives (orphans/trace/corpus), not
+ canned reports. Frozen-dataclass value objects throughout.
+
+**Concerns:**
+- **`IntentGraphReads` facade is dead in production** — advertised as injectable
+ for adapters but only tests construct it; contract and real reads can drift
+ independently. Either wire it in or mark it test scaffolding.
+- **Contract/implementation split is non-obvious** — a reader expecting the
+ coverage logic here finds only types; the algorithms are 1100+ lines away in
+ `service.py` (docstring points there, mitigating not removing).
+- `IntentCoverage` is a wide 15-field record for downstream serializers to track.
+
+**Confidence:** High — 100% of `intent_graph.py` + the four computing methods
+read; dead-facade verified via Loomweave callers (test-only).
+
+---
+
+## CLI Surface
+
+**Location:** `src/plainweave/cli.py`, `src/plainweave/cli_commands.py`
+
+**Responsibility:** Exposes local-core operations as the `plainweave` console
+command — an argparse subcommand tree whose handlers call the service and render
+its envelopes as stdout (JSON/text) + exit codes.
+
+**Key Components:**
+- `cli.py` (38 LOC) — `build_parser:14-22` (late-imports `add_web_subcommand` to
+ keep web optional), `main:25-38` (entry point `pyproject.toml:39`); dispatch is
+ a table over argparse `set_defaults(handler=)`, no if/elif ladder.
+- `cli_commands.py` (1631 LOC) — `register_commands:56-135` + eleven `_register_*`
+ helpers define **16 top-level commands / 38 leaf handlers**: `init`, `doctor`,
+ `req` (8), `criterion`, `trace`, `catalog`, `goal`, `bind`, `intent`,
+ `baseline`, `actor`, `verify`, `status`, `dossier`, `wardline-peer-facts`,
+ `web`.
+- Adapter pair: `_handle_service_result:1138` (fan-in 24) / `_handle_service_list`
+ → `_handle_output:1157` (instantiates service, catches `PlainweaveError`,
+ prints envelope or data). Exit mapping `_emit_error:1169` (2; 4 on INTERNAL).
+- ~21 `_*_dict` shapers (`1231-1631`); `_render_dossier:1634` is the one true
+ text renderer. Doctor checks store (auto-`--fix`), Loomweave, Wardline, MCP
+ import (`442-663`).
+
+**Dependencies:**
+- Inbound: `plainweave` entry point; tests (14 modules). **MCP Surface reaches
+ back in**: `inspect_project` imported by `mcp_surface.py:395-413,825-829` — this
+ module is not a pure CLI layer.
+- Outbound: Domain Service Core (`_service():1208`); Response Contract; Persistence
+ (`store`/`paths` directly — `init`/`doctor` bypass the service, `cli_commands.py:50,52`);
+ Sibling-Tool Adapters (doctor probes); MCP Surface (function-local imports
+ `:1103`,`:1114`); Web UI
+ (`add_web_subcommand`); Intent Graph + models (DTOs).
+
+**Patterns Observed:**
+- Dispatch table via `set_defaults(handler=)`; thin lambda-thunk handlers pass
+ `lambda service: …` to the central adapter; web kept optional via lazy import;
+ envelope-everywhere with exit codes from `PlainweaveError.code`.
+
+**Concerns:**
+- **Bidirectional CLI↔MCP coupling** (shared services/DTO layer) — see
+ cross-cutting themes; the function-local `PlainweaveMcpSurface` imports at
+ `cli_commands.py:1103,1114` carry a comment naming the cycle they dodge.
+- **Exit-code divergence** — `init`→0, `doctor`→0/1, service handlers→0/2/4,
+ argparse→2; no uniform interpretation for a CI wrapper.
+- "Human-readable" output is `json.dumps(data)` for ~34/38 commands; only
+ `dossier`/`doctor` produce genuine text.
+- **Dead/duplicate route** — `status requirement` (`1051-1052`) merely delegates
+ to `verify status`; same command exposed twice.
+
+**Confidence:** High — both files read in full; entry point + outbound deps +
+the MCP-coupling confirmed via Loomweave callers (`_handle_service_result`
+fan-in 24, `traversal_complete:true`). Gap: `web/server.py` internals scoped to
+the Web UI slice.
+
+---
+
+## MCP Surface
+
+**Location:** `src/plainweave/mcp_server.py`, `src/plainweave/mcp_surface.py`
+
+**Responsibility:** Exposes Plainweave's read-only advisory state to agents as a
+FastMCP server — **19 `plainweave_*` tools + 15 versioned contract resources** —
+translating service results into the `weft.plainweave.*` envelope contract
+without mutating state or making live peer calls.
+
+**Key Components:**
+- `mcp_server.py` (184) — `create_mcp_server:11-176` builds `FastMCP("plainweave",
+ json_response=True)`, registers 19 thin `@mcp.tool()` forwarders + 15
+ `@mcp.resource()` readers; `main:179` is the `plainweave-mcp` entry point.
+- `mcp_surface.py` (1653) — `PlainweaveMcpSurface:391` holds all tool impls
+ (project/context, requirements, traces, intent graph incl. `_coverage:536`
+ north-star, baselines, verification, and the three sibling surfaces
+ `entity_intent_context_get:577`, `preflight_facts_get:598`,
+ `wardline_peer_facts_list:751`, `requirements_enrichment_get:759`).
+- `_result:724-731` (fan-in 16) is the service→envelope choke point (lazy
+ `_service`, runs `action(service)`, wraps in `success_envelope`, maps
+ `PlainweaveError`→`error_envelope`); 16/19 tools route through it.
+- `MCP_TOOL_METADATA:43-194` — every entry asserts `"mutates":False,
+ "local_only":True, "peer_side_effects":[]` (the doctrine field names live HERE,
+ not in the adapters).
+
+**Dependencies:**
+- Inbound: `mcp_server.main`; `cli_commands.py` (function-local instantiation for
+ parity); tests.
+- Outbound: Domain Service Core (sole data authority); **CLI Surface**
+ (`cli_commands.py:9` — imports its private serializers + `inspect_project`);
+ Response Contract; Sibling-Tool Adapters; Intent Graph; Persistence/paths;
+ models; FastMCP.
+
+**Patterns Observed:**
+- Thin-wrapper delegation; closure-over-service adapter; lazy per-call
+ service/adapter construction; defensive degrade-not-fail (`NOT_FOUND` soft-
+ degraded to warnings); self-describing contract via `MCP_TOOL_METADATA` +
+ `CONTRACT_RESOURCES`; boundary input validation (pagination/choice/entity-ref
+ caps).
+
+**Concerns:**
+- **Preflight project-scope fan-out (the surface's #1 scaling risk).** A bare
+ `preflight_facts_get()` (default `pending_diff`, no ids) can't resolve the diff
+ locally → falls back to the *entire* corpus via `search_requirements()` (`:990`),
+ then **3 service calls per requirement** (`:1084-1086`, dossier composite),
+ O(corpus), **no `limit`/`offset` exposed**. `scope_kind="project"` same path.
+- **Bidirectional CLI↔MCP coupling** (no module-load cycle; see themes).
+- **Vestigial no-op `list_result` param** in `_result` (both branches identical).
+- **Post-materialization pagination** in `_list:841` (slices after building the
+ full list — bounds payload, not work).
+- **Unguarded `project_context_get:395`** — the one of three non-`_result` tools
+ without `try/except`; a `PlainweaveError` would escape the envelope contract.
+- Non-deterministic `generated_at` in preflight (`:658`).
+
+**Confidence:** High — both files read in full; tool count, `_result` fan-in,
+entry point, and absence of a module cycle cross-verified against Loomweave.
+Gap: service/adapter internals out of slice (claims rest on call-shape).
+
+---
+
+## Web UI
+
+**Location:** `src/plainweave/web/`
+
+**Responsibility:** Optional local-first single-operator Starlette + HTMX
+server-rendered console to browse the corpus and **author/ratify** requirements,
+drafts, goals, and agent-proposed trace links — the federation's **sole write
+surface** over `PlainweaveService` (MCP is read-only).
+
+**Key Components:**
+- `app.py` (80) — `create_app(actor, root)` factory: `/healthz`, `/static`,
+ double-submit-cookie CSRF middleware (`:43-62`), `PlainweaveError` handler,
+ lazy `routes.register_all`.
+- `server.py` (59) — CLI `web` subcommand + uvicorn launcher; `--host`
+ (default `127.0.0.1`)/`--port`(8765)/`--actor`/`--no-open`; lazy uvicorn import
+ (`WEB_EXTRA_HINT` if extra absent).
+- `context.py` (61) — `RequestContext.from_root` builds per-call service +
+ resolves `OperatorIdentity`; `_ensure_operator:34-51` self-registers a `human`
+ actor (genesis-allowed; `POLICY_REQUIRED` once an attester exists); CSRF
+ helpers (constant-time). Default operator `human:operator`.
+- `views.py` (130) — pure view-model layer (`build_corpus_rows`, `filter_rows`,
+ `pending_items`, `coverage_banner`).
+- `errors.py` (20) — `error_to_status` (`ErrorCode`→HTTP; PEER_*→502/503).
+- `routes/` — **21 routes (14 GET + 7 POST)**; +`/healthz` in `app.py` = **22
+ app-wide**: requirements.py (215, corpus +
+ CRUD, optimistic `expected_draft_revision`, CONFLICT→200 partial), review.py
+ (258, `/review` queue + approve/accept/reject), goals.py (42), intent.py (35,
+ coverage dashboard). 7 writes: create_requirement, update_draft,
+ approve_requirement, accept/reject_trace_link, create_goal,
+ link_goal_to_requirement.
+- `templates/` + `static/` — Jinja2 (base + 6 pages + 13 partials) + `app.css` /
+ `htmx.min.js`; a11y structural contracts in `base.html` (skip-link, SR live
+ region, `aria-current`), locked by `tests/web/test_a11y_contracts.py`.
+
+**Dependencies:**
+- Inbound: **CLI Surface only** (`plainweave web`, `cli.py:19-21`).
+- Outbound: Domain Service Core (every read/write via `ctx.service`); Intent
+ Graph; models; Response Contract; Persistence path resolution; Starlette/
+ Jinja2/uvicorn (`[web]` extra).
+
+**Patterns Observed:**
+- App-factory + lazy route registration (package imports without the extra);
+ HTMX partial-vs-full on `HX-Request`; pure unit-testable view-model layer;
+ optimistic-concurrency UX (CONFLICT→200 partial preserving operator text);
+ double-submit-cookie CSRF on all unsafe methods; centralized error→template
+ mapping; **process-singleton operator** bound once at `create_app`; a11y
+ structural contracts test-locked.
+
+**Concerns:**
+- **No authN/authZ + a settable `--host`.** Identity is a launch-time singleton;
+ CSRF is the only request-level control. `--host 0.0.0.0` exposes all 7 write
+ endpoints to the network with zero auth (by-design local-first, but no
+ compensating gate on the flag) (`server.py:15`).
+- **Core review-queue a11y behaviour unverified in CI** — only structural
+ contracts are automated; focus-move + live-region announcement need a manual
+ NVDA/VoiceOver pass (README:188-192).
+- **CSRF middleware re-parses body as urlencoded** (`parse_qsl`, `app.py:49-50`)
+ — a multipart form would 403; implicit undocumented coupling.
+- **O(requirements) round-trips per render** — `pending_items` does
+ search + dossier-per-draft (`views.py:82-100`); `_pending_count` recomputes the
+ queue after every mutation. N+1 at single-operator scale.
+- Minor: non-`PlainweaveError` exceptions hit Starlette's default 500, not the
+ themed partial.
+
+**Confidence:** High — all 11 `.py` + `base.html` + a11y test + README AT-gate
+read; sole inbound launcher confirmed by grep; all 7 writes traced to concrete
+service calls.
+
+---
+
+## Persistence
+
+**Location:** `src/plainweave/store.py` (+ shared `paths.py`)
+
+**Responsibility:** Owns the single SQLite database — connection lifecycle,
+forward schema migration, and store-path resolution — for the whole domain.
+
+**Key Components:**
+- `connect:11-19` — `@contextmanager`, fresh `sqlite3.connect`, `row_factory=Row`,
+ `pragma foreign_keys=on`, deterministic close. **Fan-in 44 (most-coupled
+ entity).**
+- `migrate:22-306` — idempotent: `mkdir` then one `executescript` of 17
+ `create table if not exists` + immutability/append-only triggers + a guarded
+ `ALTER … add column request_hash` (`:296-297`); stamps `schema_meta`.
+ `SCHEMA_VERSION=2` (`:8`).
+- `read_schema_meta:309` — the SCHEMA_MISMATCH detection input.
+- `paths.py:9-24` — `plainweave_db_path` (`.plainweave/plainweave.db`),
+ `default_project_key`.
+- SQL-level invariants: immutable approved text (`:76-84`), locked-baseline +
+ member immutability (`:178-217`), append-only `verification_evidence`/`events`
+ (`:244-281`); ADR-029 `entity_associations.content_hash_at_attach` declared
+ here (`:160`), drift *comparison* in the service layer.
+
+**Dependencies:**
+- Inbound: Domain Service Core (~38 methods), CLI (`initialize_project`,
+ `inspect_project` — layering exception), tests.
+- Outbound: stdlib `sqlite3`/`pathlib`/`contextlib` only. No ORM/driver/pool.
+
+**Patterns Observed:**
+- Connect-per-operation (confirmed via the 44-edge caller set); idempotent but
+ **non-version-aware** migration (stamps `SCHEMA_VERSION`, never branches; no
+ upgrade steps, no down-migrations); per-connection `foreign_keys=on`;
+ invariants in SQL triggers.
+
+**Concerns:**
+- **Connect-per-call + no WAL (`DELETE` mode)** → a writer's exclusive lock
+ blocks all readers; with 3 concurrent surfaces this is the contention ceiling.
+- **Confirmed N+1** at `service.py:1467→1479→1529-1550` (one connection per
+ catalog entity) — the open tracker's N+1 / unbounded project-scope item.
+- **Undocumented reliance on the stdlib `busy_timeout` default** —
+ `test_store_connections_configure_busy_timeout` passes only because
+ `sqlite3.connect`'s default `timeout=5.0` yields `busy_timeout=5000`; `connect`
+ sets no pragma and exposes no timeout param. A latent implicit contract.
+
+**Confidence:** High — `store.py`/`paths.py` 100% read; connect-per-call checked
+against the full 44-edge caller set; busy_timeout test run empirically.
+
+---
+
+## Sibling-Tool Adapters
+
+**Location:** `src/plainweave/loomweave_adapter.py` (657), `src/plainweave/wardline_adapter.py` (373)
+
+**Responsibility:** Read-only seams that pull *enrichment* facts from sibling
+tools' local artifacts (Loomweave catalog/SEI; Wardline findings) and translate
+presence/absence/staleness into an honest closed degrade vocabulary — never an
+implied-clean state.
+
+**Key Components:**
+- `loomweave_adapter.py`: `LoomweaveAdapter.list_catalog:101-194` (paginates
+ public-surface + module entities; `public_surface_coverage` over closed tag set
+ `:18,196-204`; `public_surface_tags_incomplete` degrade); identity resolution
+ `resolve_identity:232-415` (HTTP when endpoint configured else SQLite;
+ `resolve_identity_local:237` never calls a peer); `_probe_sei_capability:256`
+ (degrades `unsupported` orthogonally to "remote down"); `_entity_from_mapping:456`
+ (read-time freshness: `content_hash` vs SEI `body_hash` → `stale` +
+ `content_hash_drift`); `_connect:596-605` opens Loomweave's DB **read-only**
+ (`?mode=ro`).
+- `wardline_adapter.py`: `WardlineAdapter.list_peer_facts:242-326` (loads latest
+ `.wardline/*-findings.jsonl`, splits metrics from findings, computes
+ `resolved_or_unseen` by snapshot diff); hard-coded `authority_boundary`
+ (`local_only:True, live_peer_calls:False, governance_verdicts:False,
+ trust_policy_owner:"wardline"`, `:244-249`); closed kind vocab (`:17,354-373`).
+
+**Dependencies:**
+- Inbound: Domain Service Core (`_loomweave_adapter`/`_wardline_adapter:2595-2599`),
+ MCP Surface (`:746-749`), CLI doctor (`:535,592`). All construct fresh,
+ root-bound, stateless.
+- Outbound: Loomweave `.weft/loomweave/loomweave.db` (ro SQLite) + optional
+ Loomweave HTTP identity API (`urllib`, 1.5s); Wardline `.jsonl` files. **No
+ shell-out, no MCP client.**
+
+**Patterns Observed:**
+- Enrich-only via explicit `unavailable`/degrade, never silence ("result is
+ unavailable, not clean", wardline_adapter.py:250-266; `loomweave_db_missing`
+ degrade, loomweave_adapter.py:517-524).
+- **Two local-only postures:** Wardline is *structurally* local (files only, zero
+ network code); Loomweave is local SQLite by default but carries a **live HTTP
+ identity path gated on `WEFT_LOOMWEAVE_URL`** — local-only holds only if the
+ caller picks `resolve_identity_local`.
+- Closed degrade-code vocabularies both sides; defensive parsing throughout.
+
+**Concerns:**
+- **Multi-connection per read** — `list_catalog` opens one connection in
+ `_schema_state()` then a second for the page query (same in
+ `_resolve_identity_sqlite`); compounds the persistence connect-per-call pattern.
+- **Doctrine field names absent here** — `meta.local_only`/`peer_side_effects:[]`
+ do not appear in the adapters (they live in the MCP metadata); the realized
+ adapter contract is the `authority_boundary` dict + `status:"unavailable"` +
+ closed degrade vocab. Cite the actual fields.
+- **No warpline adapter, by design** — Plainweave is the *producer* of
+ `plainweave_requirements_enrichment_get` ("for Warpline's reserved enrichment
+ slot", mcp_surface.py:189,759), not a consumer; the seam lives in the MCP/CLI
+ surface, so `tests/test_warpline_requirements_enrichment.py` has no `src`
+ adapter counterpart.
+
+**Confidence:** High — both adapter files 100% read; instantiation sites + absence
+of shell-out/MCP-client confirmed by grep; producer-direction confirmed at
+`mcp_surface.py:189,759`.
+
+---
+
+## Response Contract / Cross-cutting
+
+**Location:** `src/plainweave/envelopes.py` (115), `errors.py` (34), `paths.py` (24), `_version.py`
+
+**Responsibility:** Defines the versioned JSON envelope shapes and the closed
+error-code vocabulary every CLI/MCP/service response is wrapped in, so all
+surfaces emit one uniform machine-switchable contract.
+
+**Key Components:**
+- `envelopes.py`: `success_envelope:37-51` (`{schema, ok:True, data, warnings,
+ meta}`; `meta.producer={tool:"plainweave", version:__version__}`,
+ `generated_at`, `project`; fan-in 11); `error_envelope:54-78` (hard-codes
+ `schema:"weft.plainweave.error.v1"`, `ok:False`; `_error_code:28-34` raises on
+ unknown code — fail-closed); `list_envelope`/`batch_envelope:81-115` (uniform
+ pagination/batch).
+- `errors.py`: `ErrorCode(StrEnum):6-16` — closed **10-value** vocab `VALIDATION,
+ NOT_FOUND, CONFLICT, POLICY_REQUIRED, PEER_ABSENT, PEER_STALE, PEER_CONTRACT,
+ LOCKED, UNSUPPORTED, INTERNAL` (three `PEER_*` carry sibling-degradation into
+ the error contract); `PlainweaveError:19-34` bridges raised → `error_envelope`.
+- `_version.py`: `__version__ = "1.1.0"` (stamped into every envelope).
+
+**Dependencies:**
+- Inbound: MCP (`_result:724` + others), CLI (`_handle_service_result:1148`,
+ doctor/dossier/init), Domain Service Core (raises `PlainweaveError`;
+ `_loomweave_error:2601` maps adapter reasons → `ErrorCode`).
+- Outbound: `_version`, `errors`, stdlib `datetime`/`collections.abc`.
+
+**Patterns Observed:**
+- **Uniform envelope via central choke points** (`_result` / `_handle_service_result`
+ + `list_/batch_` helpers) rather than scattered dict-building (Loomweave
+ `traversal_complete`).
+- **Versioning split by direction:** success/list/batch take a caller-supplied
+ per-payload `schema`; errors use one hard-coded `error.v1`; producer
+ identity+version in `meta`.
+- **Closed, fail-closed error vocab** (unknown codes raise); adapter degrade
+ reasons translated into the closed `ErrorCode` set at the service boundary
+ (`_loomweave_error:2601-2604`), so sibling failures surface as
+ `NOT_FOUND`/`CONFLICT`/`PEER_*`, not leaked reason strings.
+
+**Concerns:**
+- **`meta.generated_at` non-deterministic by default** (`datetime.now(UTC)`,
+ `:12-13`) — goldens must inject it.
+- **Error-schema version hard-coded in one place** (`:67`) while success schemas
+ are caller-supplied — no symmetric constant/registry tying the two, so
+ error/success version drift is possible.
+- `paths.py`/`_version.py` — no structural concern.
+
+**Confidence:** High — all four files 100% read; cross-surface uniformity
+confirmed by reading the full `success_envelope` caller set (both choke points
+present, `traversal_complete`).
diff --git a/docs/arch-analysis-2026-06-28-0751/03-diagrams.md b/docs/arch-analysis-2026-06-28-0751/03-diagrams.md
new file mode 100644
index 0000000..3b288df
--- /dev/null
+++ b/docs/arch-analysis-2026-06-28-0751/03-diagrams.md
@@ -0,0 +1,237 @@
+# 03 — Architecture Diagrams (C4-style)
+
+**Subject:** Plainweave · **Live tree:** HEAD `8258f76` · **Date:** 2026-06-28
+Diagrams encode the Loomweave-verified dependency map
+(`temp/dependency-reconciliation.md`). Mermaid `flowchart`/`sequenceDiagram`
+syntax (portable rendering); the C4 level is noted per diagram.
+
+---
+
+## 1. System Context (C4 L1) — who uses Plainweave and what it touches
+
+```mermaid
+flowchart TB
+ agent["AI coding agent (authoring / reasoning)"]
+ operator["Human operator (single, local)"]
+
+ subgraph PW["Plainweave — code-grounded intent corpus"]
+ direction TB
+ core["Intent graph + reads (advisory, enrich-only)"]
+ end
+
+ db[("SQLite store .plainweave/plainweave.db")]
+ lw["Loomweave (catalog / SEI / rename / semantic)"]
+ wl["Wardline (trust-boundary findings)"]
+ legis["Legis (git/CI boundary + teeth + audit)"]
+ warp["Warpline (temporal change-impact)"]
+
+ agent -->|"read-only MCP tools"| PW
+ operator -->|"CLI + web console (writes)"| PW
+ PW -->|"owns / persists"| db
+ PW -->|"reads catalog (ro SQLite) + opt. HTTP identity"| lw
+ PW -->|"reads .wardline/*-findings.jsonl"| wl
+ PW -. "coverage facts ride out at git/CI (advisory; teeth dialed via Legis cells)" .-> legis
+ PW -. "PRODUCES requirements_enrichment.v1 for Warpline's reserved slot" .-> warp
+
+ classDef ext fill:#eef,stroke:#88a;
+ class lw,wl,legis,warp ext;
+```
+
+**Reading:** Plainweave is a *thin* member. It owns the intent graph + reads and
+its own SQLite store; it **reads** Loomweave and Wardline local artifacts
+(enrich-only), and **produces** facts that Legis (at the git/CI boundary) and
+Warpline (its enrichment slot) consume. Dotted edges are enrich/produce seams
+that never gate.
+
+---
+
+## 2. Container (C4 L2) — the deployable pieces
+
+```mermaid
+flowchart TB
+ agent["AI agent"]
+ operator["Human operator"]
+
+ subgraph deploy["plainweave package (one wheel)"]
+ direction TB
+ cli["CLI plainweave (console script) cli.py + cli_commands.py"]
+ mcp["MCP server (READ-ONLY) plainweave-mcp mcp_server.py + mcp_surface.py"]
+ web["Web console (WRITE surface, [web] extra) Starlette + HTMX web/"]
+ svc["PlainweaveService (domain + data-access + intent engine) service.py 3027 LOC"]
+ ig["Intent Graph types intent_graph.py"]
+ store["Persistence store.py (connect-per-call)"]
+ adapters["Sibling adapters loomweave_adapter / wardline_adapter"]
+ contract["Response contract envelopes.py / errors.py"]
+ end
+
+ db[("SQLite .plainweave/")]
+ lwdb[("Loomweave DB .weft/loomweave/ (ro)")]
+ wljson[/".wardline/*-findings.jsonl"/]
+
+ agent --> mcp
+ operator --> cli
+ operator --> web
+ cli --> svc
+ mcp --> svc
+ web --> svc
+ cli -. "init/inspect only (layering exception)" .-> store
+ mcp -. "serializers + inspect_project (surface↔surface)" .-> cli
+
+ svc --> store
+ svc --> ig
+ svc --> adapters
+ svc --> contract
+ cli --> contract
+ mcp --> contract
+ web --> contract
+ store --> db
+ adapters --> lwdb
+ adapters --> wljson
+
+ classDef write fill:#fee,stroke:#c66;
+ classDef read fill:#efe,stroke:#6a6;
+ class web write;
+ class mcp read;
+```
+
+**Reading:** Three surfaces, one service, one store. The MCP server is read-only;
+the **web console is the only write surface**. Two dotted edges are the
+architectural exceptions: the CLI hits the store directly for `init`/`inspect`,
+and the MCP surface reaches back into `cli_commands` for serializers +
+`inspect_project` (a function-local coupling — **no module-load cycle**).
+
+---
+
+## 3. Component (C4 L3) — the 8 subsystems and their edges
+
+```mermaid
+flowchart LR
+ subgraph surfaces["Delivery surfaces"]
+ CLI["CLI Surface 16 cmds / 38 handlers"]
+ MCP["MCP Surface 19 tools / 15 resources"]
+ WEB["Web UI 22 routes (15 GET / 7 POST)"]
+ end
+
+ subgraph coredom["Core domain"]
+ SVC["Domain Service Core PlainweaveService (god object)"]
+ IG["Intent Graph (types/contract; logic in service)"]
+ end
+
+ subgraph infra["Infrastructure / cross-cutting"]
+ PERS["Persistence store.connect (fan-in 44)"]
+ ADP["Sibling-Tool Adapters Loomweave + Wardline"]
+ RC["Response Contract envelopes + ErrorCode(10)"]
+ end
+
+ CLI --> SVC
+ MCP --> SVC
+ WEB --> SVC
+ MCP -. "DTO/serializers + inspect_project" .-> CLI
+ CLI -. "init/inspect" .-> PERS
+
+ SVC --> PERS
+ SVC --> IG
+ SVC --> ADP
+ SVC --> RC
+ CLI --> RC
+ MCP --> RC
+ WEB --> RC
+ SVC -. "produces requirements_enrichment.v1" .-> MCP
+
+ classDef god fill:#fdd,stroke:#c44,stroke-width:2px;
+ class SVC god;
+```
+
+**Reading:** Every surface depends on the Domain Service Core and the Response
+Contract; the core fans out to Persistence, Intent Graph, and the Adapters. The
+red node is the 3027-LOC god object that is simultaneously use-case tier,
+data-access tier, and intent-graph engine — the dominant refactor target.
+
+---
+
+## 4. The intent graph data model (the product's reason to exist)
+
+```mermaid
+flowchart BT
+ code["Code SEI (leaf) loomweave:eid:… (rename-stable)"]
+ req["Requirement (obligation) draft → approved → superseded/deprecated"]
+ goal["Strategic Goal (root intent)"]
+
+ code -->|"bind_sei_to_requirement (ADR-029, content_hash_at_attach)"| req
+ req -->|"link_goal_to_requirement"| goal
+
+ note1["orphans(level): nodes with NO upward edge at any altitude"]
+ note2["coverage(): fraction of public surfaces with a LIVE upward edge (north-star; advisory, honestly qualified)"]
+ note3["trace(node): up to goals, down to code"]
+```
+
+**Reading:** Edges mean *"justified by / satisfies."* A node with no upward edge
+is a reviewable question. `coverage()` counts **live** justification only
+(excludes deprecated); `trace()` still *explains* deprecated links — "trace
+explains, coverage counts" (`service.py:1537-1539`).
+
+---
+
+## 5. Sequence — MCP `intent_coverage` read (illustrates connect-per-call / N+1)
+
+```mermaid
+sequenceDiagram
+ actor A as AI agent
+ participant M as MCP Surface (_result)
+ participant S as PlainweaveService.intent_coverage
+ participant LA as LoomweaveAdapter
+ participant DB as SQLite (per-call connect)
+
+ A->>M: plainweave_intent_coverage(...)
+ M->>S: action(service)
+ S->>LA: list_catalog() (opens 2 ro connections to Loomweave DB)
+ LA-->>S: public surfaces + coverage block (present_plugins verbatim)
+ loop per catalog surface (N+1)
+ S->>DB: with connect(): _goal_nodes_for_surface(sei)
+ DB-->>S: live requirement ids
+ end
+ S-->>M: IntentCoverage (full counts, evidence bounded by max_surfaces)
+ M->>M: success_envelope(schema, data) [generated_at = now(UTC)]
+ M-->>A: weft.plainweave.intent_coverage.v1 envelope
+```
+
+**Reading:** Each catalog surface opens its **own** SQLite connection inside the
+loop (`service.py:1529-1550`) — the confirmed N+1 / connect-per-call pattern.
+Combined with no WAL (`DELETE` journal mode), concurrent surfaces serialize on
+the writer lock. Correct and honest at single-operator scale; the scaling risk
+the two open P3 tracker tasks name.
+
+---
+
+## 6. Sequence — Web write (ratify a draft) showing the sole mutation path
+
+```mermaid
+sequenceDiagram
+ actor O as Human operator
+ participant W as Web /review (POST approve)
+ participant CSRF as CSRF middleware
+ participant CTX as RequestContext (process-singleton operator)
+ participant S as PlainweaveService.approve_requirement
+ participant DB as SQLite
+
+ O->>W: POST /req/{id}/approve (_csrf, expected_version)
+ W->>CSRF: double-submit-cookie check (constant-time)
+ CSRF-->>W: ok (else 403)
+ W->>CTX: ctx.operator.actor_id (bound at create_app, human:operator)
+ W->>S: approve_requirement(id, actor, expected_version)
+ S->>DB: with connect(): optimistic check + immutable version row + event
+ alt version conflict
+ DB-->>S: stale expected_version
+ S-->>W: PlainweaveError(CONFLICT)
+ W-->>O: 200 + conflict partial (preserves operator text)
+ else ok
+ DB-->>S: committed (+ EVT-uuid event)
+ S-->>W: RequirementRecord
+ W-->>O: updated card + SR live-region announcement
+ end
+```
+
+**Reading:** Writes flow only through the web console → service. Identity is a
+launch-time process singleton (no per-request auth); CSRF is the sole
+request-level control. Optimistic concurrency surfaces `CONFLICT` as a 200 HTMX
+partial that preserves the operator's text — a deliberate UX choice.
diff --git a/docs/arch-analysis-2026-06-28-0751/04-final-report.md b/docs/arch-analysis-2026-06-28-0751/04-final-report.md
new file mode 100644
index 0000000..c573607
--- /dev/null
+++ b/docs/arch-analysis-2026-06-28-0751/04-final-report.md
@@ -0,0 +1,175 @@
+# 04 — Final Report
+
+**Subject:** Plainweave · **Live tree:** HEAD `8258f76` · **Date:** 2026-06-28
+**Analysis:** Architect-Ready track. 5 parallel subsystem explorers (100% source
+read per slice) + orchestrator edge-reconciliation against the Loomweave graph +
+an independent validation gate (verdict **PASS-WITH-FIXES**, all 8 high-stakes
+claims VERIFIED against live source, fixes applied). Companion docs:
+`01-discovery-findings`, `02-subsystem-catalog`, `03-diagrams`,
+`05-quality-assessment`, `06-architect-handover`.
+
+---
+
+## Executive summary
+
+Plainweave is a **code-grounded requirements-traceability / intent-corpus**
+service — the "permission for code to exist" member of the Weft federation. It
+maintains a graph (`strategic goal ▲ requirement ▲ code-SEI`) where every public
+code surface should ladder up to a requirement and every requirement up to a
+goal; the north-star read is **`intent_coverage`** (the fraction of public
+surfaces that are justified). Its doctrine is **advisory / enrich-only**: it
+never gates, delegating teeth + audit to Legis and identity + semantics to
+Loomweave.
+
+**The architecture is clean, conventional, and well-tested — a textbook layered
+service whose discipline is real, not aspirational.** Three thin delivery
+surfaces (a `plainweave` CLI, a read-only `plainweave-mcp` server, an optional
+single-operator web console) sit over one `PlainweaveService`, an SQLite store,
+two read-only sibling adapters, and a uniform versioned-envelope response
+contract. There are **no circular module imports**, a **closed 10-value error
+vocabulary**, **event-sourcing with DB-trigger immutability**, **golden-vector
+contract tests for every cross-member seam**, and an enforced **≥90% branch-
+coverage gate**. For a 1.1.0 product the contract discipline is genuinely strong.
+
+**The risk is concentrated in two places, both well-understood and bounded by
+the product's single-operator local-first model:**
+1. **One 3027-LOC god object** (`PlainweaveService`) that is simultaneously the
+ use-case tier, the data-access tier (raw inline SQL, no repository), *and* the
+ intent-graph engine. It is the dominant maintainability liability.
+2. **A pervasive connect-per-call / N+1 SQLite pattern with no WAL**, which makes
+ reads O(corpus) on the hot paths (MCP preflight, coverage, the web review
+ queue) and serializes concurrent surfaces on the writer lock. The team already
+ tracks this (two open P3 tasks).
+
+Neither is a correctness defect at the intended scale; both are scaling/
+maintainability ceilings. One genuine correctness gap exists — **DB exceptions
+escape the documented `ErrorCode` contract** — that is cheap to close.
+
+**Verdict: architecturally sound and production-credible for its stated scope
+(single-operator, local-first, advisory). The refactor levers are a service
+decomposition and a persistence-layer fix; both are well-isolated.**
+
+---
+
+## Architecture at a glance
+
+| Dimension | Finding |
+|-----------|---------|
+| Shape | Layered service: 3 surfaces → 1 domain service → store + adapters + contract |
+| Source size | ~9.6K LOC src / ~10.1K LOC tests (361 test functions) |
+| Subsystems | 8 (Domain Service Core, Intent Graph, CLI, MCP, Web UI, Persistence, Sibling Adapters, Response Contract) |
+| Entry points | `plainweave` (CLI), `plainweave-mcp` (read-only MCP); web is a CLI subcommand behind the `[web]` extra |
+| Persistence | SQLite (stdlib `sqlite3`, no ORM), schema v2, connect-per-call, `DELETE` journal mode |
+| Write surface | Web console **only** (MCP is read-only; CLI mutates via the service) |
+| External seams | reads Loomweave (catalog/SEI) + Wardline (findings); produces enrichment for Warpline; coverage facts ride to Legis at git/CI |
+| Module cycles | none (`cycles:[]`) |
+| Quality gates | ruff + `mypy --strict` + pytest `fail_under=90` (branch) — enforced via `make ci` |
+
+The dependency story (verified against the Loomweave graph): **all three surfaces
+depend on the service and the response contract; the service fans out to
+persistence, the intent-graph types, and the adapters.** Two edges break the
+clean layering — see "Structural exceptions" below.
+
+---
+
+## What is done well (evidence-anchored strengths)
+
+- **Uniform, machine-switchable response contract.** Every CLI/MCP/service
+ response is wrapped through central choke points (`_handle_service_result`
+ fan-in 24, `_result` fan-in 16) into a versioned `weft.plainweave.*` envelope
+ with a closed `ErrorCode` StrEnum (fail-closed: unknown codes raise). Three
+ `PEER_*` codes carry sibling-degradation *into* the error contract — a
+ thoughtful federation-aware design.
+- **Honest enrich-only degradation, never an implied-clean.** Sibling absence is
+ a typed `unavailable` with a closed degrade vocabulary ("result is unavailable,
+ not clean"); `live_peer_calls` is hard-`False`; content-hash drift is flagged
+ `stale`, never silently dropped. This is the doctrine actually implemented, not
+ just documented.
+- **Integrity pushed into the database.** Append-only `events`, immutable
+ approved requirement text, locked-baseline immutability — all enforced by SQL
+ triggers, not just Python. Writes use optimistic concurrency (`expected_version`)
+ + replayable idempotency keys.
+- **Identity that survives refactors.** Code leaves are keyed by Loomweave SEI
+ (ADR-029 entity-associations with `content_hash_at_attach` drift detection),
+ so bindings outlive rename/move.
+- **Test discipline is real.** 361 tests; a genuine ≥90% branch-coverage gate;
+ golden-vector wire-contract tests for *every* seam (preflight, wardline/warpline
+ peer facts, envelopes); a vendored SEI-conformance oracle with a drift gate.
+- **Clean module graph.** No import cycles; pure-leaf models/types; lazy imports
+ keep the web framework optional.
+
+## Where the risk concentrates (ranked)
+
+1. **God object — `PlainweaveService` (3027 LOC).** One class, ~13 aggregates,
+ use-case + data-access + intent-engine in one file. Cohesion is held together
+ by a high-fan-in private helper cluster (`_error`, `_now`, `_require_actor`,
+ `_record_event`, `_requirement_row`). Dominant maintainability risk; the
+ product's *defining capability* (coverage/orphans/trace) is buried at
+ `service.py:1311-1507` rather than isolated in the `intent_graph` module that
+ names it.
+2. **Connect-per-call / N+1 + no WAL.** `store.connect` (fan-in 44) opens a fresh
+ connection per op with no pool, in `DELETE` journal mode. Hot paths are
+ O(corpus): MCP `preflight_facts_get` defaults to scanning the entire corpus
+ with 3 service calls per requirement and *no pagination*; `intent_coverage`
+ opens a connection per catalog surface; the web review queue re-fetches a
+ dossier per draft per render. Concurrent surfaces serialize on the writer lock.
+3. **DB exceptions escape the `ErrorCode` contract (correctness).** Only domain
+ failures route through `_error`→`PlainweaveError`; raw `connection.execute` is
+ unguarded, so `sqlite3.IntegrityError`/`OperationalError` propagate past
+ callers that switch on `ErrorCode`. Validated end-to-end (both surface result
+ paths catch `except PlainweaveError` only).
+4. **Surface↔surface coupling.** `cli_commands.py` is a de-facto shared
+ services/DTO layer: the MCP surface imports its private serializers +
+ `inspect_project`; the new CLI handler lazily imports `PlainweaveMcpSurface`.
+ No module-load cycle (the function-local import dodges it), but a real
+ architectural inversion.
+5. **Web exposure with no authN/authZ.** The sole write surface authenticates a
+ process-singleton operator with CSRF as the only request-level control; a
+ settable `--host 0.0.0.0` exposes all 7 write endpoints with no compensating
+ gate. By design for local-first, but the flag has no guard.
+
+Full severity-rated catalogue with remediations in `05-quality-assessment.md`;
+sequenced backlog in `06-architect-handover.md`.
+
+## Structural exceptions to the clean layering
+
+- **CLI → Persistence (direct).** `init`/`inspect` call `store.connect()`
+ directly, bypassing the service — plausibly justified (`init` migrates before a
+ service exists) but a documented hole in "surfaces only talk to the service."
+- **MCP → CLI (serializers).** The DTO layer lives in `cli_commands.py`, not a
+ neutral contract module, so the two agent surfaces are mutually dependent.
+
+## Maturity & fitness for purpose
+
+The README claims 1.0/"Production-Stable"; the code is at **1.1.0**. The green
+gate (lint + strict types + 90% branch coverage) is enforced and the contract
+discipline is real, so "stable behaviour and contracts" is a *fair* claim. Two
+honesty caveats worth carrying: (a) the web review-queue **accessibility
+behaviour** (focus management + live-region announcements) the README emphasizes
+is **manual-AT-only and ungated in CI** — only structural contracts are tested;
+(b) the suppressed `ResourceWarning: unclosed database` in `pyproject.toml` is
+blamed on "store-layer connections," but production connections close
+deterministically in `finally` — the warning is actually a **test-fixture** leak
+(see Q23 in `05`), and the suppression comment is misleading, not evidence of a
+production defect.
+
+Against its doctrine — **coordinate-not-gate, enrich-only, speak-SEI-at-entry,
+don't-duplicate, prescribe-nothing** — the implementation is faithful: it is
+genuinely thin (one runtime dep, `mcp`), genuinely advisory (no enforcement
+path), genuinely enrich-only (typed degradation), and genuinely composable
+(three primitives, not canned reports). The architecture serves the product
+thesis well.
+
+## Limitations of this analysis
+
+- **Static analysis only.** No runtime profiling; performance claims rest on
+ call-shape (e.g. "O(corpus)"), not measured latency. The N+1 is confirmed in
+ *code structure*, not benchmarked.
+- **Index/live split.** Loomweave structural integers (fan-in/out, coupling)
+ derive from commit `e95b6ad`; the live tree is `8258f76` (+80 LOC in 2 files —
+ the peer-facts CLI/MCP parity subcommands). All catalog claims were read from
+ live source; the integers were additionally grep-confirmed by the validator
+ (the index DB was mid-rebuild during validation). **Re-run `loomweave analyze`
+ before treating coupling integers as current-HEAD.**
+- **Tests read for evidence, not audited.** Coverage *gate* confirmed from config;
+ the suite was not independently re-run in this pass.
diff --git a/docs/arch-analysis-2026-06-28-0751/05-quality-assessment.md b/docs/arch-analysis-2026-06-28-0751/05-quality-assessment.md
new file mode 100644
index 0000000..01baa5c
--- /dev/null
+++ b/docs/arch-analysis-2026-06-28-0751/05-quality-assessment.md
@@ -0,0 +1,243 @@
+# 05 — Code Quality Assessment
+
+**Subject:** Plainweave · **Live tree:** HEAD `8258f76` · **Date:** 2026-06-28
+A severity-rated technical-debt register with `file:line` evidence and a
+remediation per item. Findings are reconciled across all 5 explorers + the
+validation gate (all structural claims VERIFIED against live source).
+
+**Severity is rated against the product's stated scope** — single-operator,
+local-first, advisory/enrich-only. An item marked Medium that says "fine at
+single-operator scale" would be High for a multi-tenant server; the rating
+reflects *Plainweave's* intended deployment, and each item names the scenario
+that escalates it.
+
+| Severity | Count | Meaning |
+|----------|-------|---------|
+| High | 4 | Should be addressed before scale or before relying on a stated contract |
+| Medium | 8 | Real debt; bounded today, escalates under concurrency/growth |
+| Low | 11 | Hygiene / clarity / dead code / test discipline |
+
+---
+
+## Hotspot map (files by risk concentration)
+
+| File | LOC | Risk | Why |
+|------|-----|------|-----|
+| `service.py` | 3027 | **High** | God object: use-case + data-access + intent engine; N+1; unguarded SQL |
+| `mcp_surface.py` | 1653 | **High** | Preflight O(corpus) fan-out, no pagination; CLI coupling; unguarded tool |
+| `store.py` | 311 | **Medium** | Connect-per-call, no WAL, no explicit busy_timeout, non-version-aware migrate |
+| `cli_commands.py` | 1631 | **Medium** | De-facto shared DTO layer (surface↔surface coupling); exit-code divergence |
+| `web/` | ~900 | **Medium** | No authN/authZ + `--host` exposure; a11y ungated in CI; per-render N+1 |
+| `loomweave_adapter.py` | 657 | **Low** | Multi-connection per read; conditional local-only |
+| `envelopes.py` | 115 | **Low** | Non-deterministic `generated_at`; hard-coded error schema version |
+
+---
+
+## Correctness
+
+**Q1 · DB exceptions escape the `ErrorCode` contract · High**
+Only domain failures route through `_error`→`PlainweaveError` (`service.py:3020`).
+Raw `connection.execute(...)` is never wrapped, so `sqlite3.IntegrityError`/
+`OperationalError` propagate past callers that switch on `ErrorCode` — both
+surface result paths (`_result`, `_handle_output`) catch `except PlainweaveError`
+only (validated end-to-end). A caller relying on the documented closed vocab
+cannot catch a DB failure.
+→ **Fix:** wrap store operations in a boundary that maps `sqlite3.Error`
+subclasses to `PlainweaveError` — UNIQUE/PK constraint violations → `CONFLICT`,
+other `IntegrityError` (FK/NOT NULL/CHECK) → `VALIDATION`, else `INTERNAL` — or
+add a generic-exception arm to the two result adapters. Low effort, closes a
+stated contract.
+
+**Q2 · `count(*) + 1` id generation is racy · Medium**
+`_next_requirement_number:2110`, `_next_link_number:2137`, `_next_evidence_number:2149`
+derive ids from a live `count(*)`. Fails *safe* (every id feeds a UNIQUE/PK
+column, `store.py:40-41`), so a concurrent second writer collides on a constraint
+rather than duplicating — but it surfaces as a raw `IntegrityError` (see Q1), not
+a clean `CONFLICT`. Escalates the moment two agents/processes write concurrently.
+→ **Fix:** `BEGIN IMMEDIATE` serializes writers so the second transaction sees
+the committed count and *prevents* the race outright (an alternative to mapping
+the collision to `CONFLICT` per Q1, not a pairing), or use a monotonic sequence
+table; pairs with Q3.
+
+## Performance / scalability
+
+**Q3 · Connect-per-call + no WAL = contention ceiling · High**
+`store.connect:11-19` opens a fresh connection per op (fan-in 44, no pool) and
+never sets `journal_mode`, so the DB runs in default `DELETE` mode where a writer
+takes an exclusive lock blocking all readers. With three concurrent surfaces
+(MCP/web/CLI) this is the hard concurrency ceiling. It also relies on the
+*implicit* stdlib `busy_timeout` default (5000ms from `sqlite3.connect(timeout=5.0)`)
+— `connect` sets no pragma and exposes no timeout param.
+→ **Fix:** enable WAL (`pragma journal_mode=WAL`) + set `busy_timeout` explicitly
+in `connect`; introduce a request-scoped/unit-of-work connection so a logical
+operation reuses one connection. (Tracker P3 `3edcd19943`.)
+
+**Q4 · MCP preflight project-scope fan-out, no pagination · High**
+A bare `plainweave_preflight_facts_get()` (default `scope_kind="pending_diff"`,
+no ids) can't resolve the diff locally → falls back to the *entire* corpus via
+`search_requirements()` (`mcp_surface.py:990`), then runs **3 service calls per
+requirement** (`requirement_preflight_profile`/`verification_status`/
+`requirement_dossier`, `:1084-1086`, the dossier itself composite). O(corpus),
+**no `limit`/`offset` exposed** on the tool. `scope_kind="project"` same path.
+→ **Fix:** expose `limit`/`offset`; batch the per-requirement queries into
+set-based reads; cap/paginate the default scope. (Tracker P3 `706d80dc8e`.)
+
+**Q5 · N+1 connections in `intent_coverage` · Medium**
+`service.py:1467` loops catalog entities → `:1479 _goal_nodes_for_surface` →
+`:1529-1550` opens its own `with connect(...)` **per entity**. (Sibling helpers
+correctly take an existing connection — this one regressed.)
+→ **Fix:** thread the open connection into `_goal_nodes_for_surface`. Small,
+local, high payoff. (No dedicated tracker task — a distinct N+1 site from the
+preflight one in `3edcd19943`; closed by Initiative A.)
+
+**Q6 · Web review queue O(requirements) per render · Medium**
+`pending_items` does `search_requirements()` then a `requirement_dossier()` per
+draft (`views.py:82-100`); `_pending_count` recomputes the whole queue after
+every mutation (`review.py:41-42,116,198,215`); `_resolve_titles` fetches a
+dossier per draft-only requirement.
+→ **Fix:** batch the per-draft fetches; cache/derive the pending count.
+
+**Q7 · Adapter multi-connection per read · Low**
+`LoomweaveAdapter.list_catalog` opens one connection in `_schema_state()` then a
+second for the page query (same in `_resolve_identity_sqlite`).
+→ **Fix:** reuse one connection per adapter call.
+
+**Q8 · Post-materialization pagination in MCP `_list` · Low**
+`_list:841` builds the full result then slices `items[offset:offset+limit]` —
+bounds the payload, not the underlying work.
+→ **Fix:** push limit/offset into the service query.
+
+## Maintainability / structure
+
+**Q9 · God object: `PlainweaveService` (3027 LOC) · High**
+One class spans ~13 aggregates and is use-case + data-access (raw inline SQL, no
+repository) + intent-graph engine. The product's defining capability lives at
+`service.py:1311-1507`, not in the `intent_graph` module that names it.
+→ **Fix (staged, behaviour-preserving):** (1) extract a thin repository/data-
+access layer (centralizes `connect` — also the seam for Q1/Q3); (2) move
+coverage/orphans/trace computation into `intent_graph`; (3) split aggregates
+(requirements, traces, baselines, verification) into per-aggregate service
+modules sharing the helper cluster. Sequenced in `06-architect-handover.md`.
+
+**Q10 · Surface↔surface coupling (`cli_commands` as shared DTO layer) · Medium**
+The MCP surface imports CLI-owned private serializers (`mcp_surface.py:9`) +
+`inspect_project` (`:395-413,825-829`); the CLI handler lazily imports
+`PlainweaveMcpSurface` (`cli_commands.py:1103,1114`). No module-load cycle (the
+function-local import dodges it), but the DTO layer is misplaced.
+→ **Fix:** extract the shared `_*_dict` serializers + `inspect_project`/
+`_current_project_key` into a neutral `serialization.py`/read-helpers module both
+surfaces depend on; removes the function-local-import workaround.
+
+**Q11 · Migration is not version-aware · Medium**
+`migrate:22-306` stamps `SCHEMA_VERSION=2` but never branches on it — it re-runs
+`create … if not exists` + one guarded `ALTER`; no per-version upgrade steps, no
+down-migrations.
+→ **Fix:** introduce a migration ladder keyed on `schema_meta.schema_version`
+before the next breaking schema change (the `read_schema_meta`/SCHEMA_MISMATCH
+plumbing already exists to detect drift).
+
+**Q12 · Intent-graph contract/impl split · Low**
+A reader expecting coverage logic in `intent_graph.py` finds only types; the
+algorithms are 1100+ lines away in `service.py`. (Resolved by Q9 step 2.)
+
+## Reliability / security
+
+**Q13 · Web: no authN/authZ + settable `--host` · Medium**
+Identity is a launch-time process singleton (`context.py:26-51`); CSRF is the
+only request-level control. `--host 0.0.0.0` (`server.py:15`) exposes all 7
+write endpoints with zero auth and no compensating gate. By design for
+local-first, but the flag is unguarded.
+→ **Fix:** refuse a non-loopback bind unless an explicit `--insecure`/token is
+supplied; or add a shared-secret on unsafe methods. At minimum, document the
+exposure on the flag's help text.
+
+**Q14 · Review-queue accessibility ungated in CI · Medium**
+Only *structural* a11y contracts are automated (`tests/web/test_a11y_contracts.py`);
+the focus-move + live-region announcement behaviour the README emphasizes
+(README:188-192) is manual NVDA/VoiceOver only.
+→ **Fix:** add a headless driver assertion (the repo already has a web Playwright
+harness) for focus target + `#sr-status` text after approve/accept/reject — turns
+a documented manual gate into a CI gate.
+
+**Q15 · CSRF middleware assumes urlencoded bodies · Low**
+`app.py:49-50` re-parses the raw body with `parse_qsl`; a multipart form yields
+no `_csrf` field → 403. Works because all forms are urlencoded; implicit coupling.
+→ **Fix:** branch on `Content-Type`, or assert the constraint in a test + comment.
+
+**Q16 · Unguarded `project_context_get` MCP tool · Low**
+Two of the three non-`_result` tools wrap work in `try/except PlainweaveError`;
+`project_context_get:395-413` does not, so an error from `inspect_project`/adapter
+capability escapes the envelope contract.
+→ **Fix:** wrap it like its siblings (relates to Q1).
+
+## Determinism / contract hygiene
+
+**Q17 · Non-deterministic `meta.generated_at` · Medium**
+`envelopes.py:12-13` defaults `generated_at` to `datetime.now(UTC)`; preflight
+stamps it too (`mcp_surface.py:658`). Defeats byte-stable golden comparison /
+response caching unless the caller injects a value.
+→ **Fix:** thread an injectable clock/`generated_at` through the envelope
+boundary (goldens already inject; make it a first-class parameter).
+
+**Q18 · Error-schema version hard-coded; no registry · Low**
+Success schemas are caller-supplied per-payload; the error schema is hard-coded
+`error.v1` in one place (`envelopes.py:67`) with no constant/registry tying error
+and success versions together.
+→ **Fix:** a `SCHEMA_VERSIONS` constant/registry.
+
+## Dead / vestigial code
+
+**Q19 · `IntentGraphReads` facade dead in production · Low** — advertised as
+injectable for adapters but only tests construct it (`intent_graph.py:141`).
+→ **Fix:** wire into the read path or mark test-scaffolding.
+
+**Q20 · `experimental/` package is empty · Low** — only stale `__pycache__` for a
+`plan_check` module; `grep -rn experimental src tests` is empty (confirmed).
+→ **Fix:** delete the directory.
+
+**Q21 · Duplicate command `status requirement` == `verify status` · Low**
+(`cli_commands.py:1051-1052` delegates). Two names, one behaviour.
+→ **Fix:** alias explicitly or remove one.
+
+**Q22 · Vestigial no-op `list_result` param in `_result` · Low** — both branches
+return the identical envelope (`mcp_surface.py:724-731`); threaded through ~7 call
+sites.
+→ **Fix:** remove the parameter.
+
+## Test & comment hygiene
+
+**Q23 · Misleading `ResourceWarning` suppression comment + test fixtures that
+leak connections · Low**
+`pyproject.toml:89-93` suppresses `ResourceWarning: unclosed database`,
+attributing it to "pre-existing **store-layer** connections… track the
+underlying leak separately." That attribution is inaccurate: **both production
+connection sites close deterministically in `finally`** — `store.connect`
+(`store.py:11-19`) and `LoomweaveAdapter._connect` (`loomweave_adapter.py:596-605`,
+whose own comment says "closed deterministically rather than left to GC"). The
+warning under `--cov` actually originates from **test fixtures** using
+`with sqlite3.connect(...) as conn` (e.g. `tests/loomweave_test_utils.py:10,90`),
+which commits the transaction but does **not** close the connection (a stdlib
+`sqlite3` gotcha), leaving it to GC. So this is test hygiene + a misleading
+comment, **not** a production leak — and it is **not** fixed by Initiative A.
+→ **Fix:** close test connections explicitly (`contextlib.closing` or an explicit
+`.close()`), then narrow/remove the blanket suppression; correct the comment's
+"store-layer" attribution. *(Note: this corrects a corroboration earlier drafts
+attached to Q3 — the connect-per-call concern stands on connection **count** and
+journal mode, not on this warning.)*
+
+---
+
+## Metrics snapshot
+
+- **Tests:** 361 functions; `tests/{state(7), contracts(8), conformance(1),
+ web(12)}` + 22 top-level; 62 fixtures. Branch coverage gated at
+ `fail_under = 90` (`pyproject.toml`).
+- **Contract tests:** golden-vector wire tests for every seam (envelopes,
+ preflight, wardline/warpline peer facts, CLI contract outputs) + an SEI
+ conformance drift oracle (`-m sei_drift`). Strong.
+- **Suppressed `ResourceWarning: unclosed database`** (`pyproject.toml:89-93`) —
+ a test-fixture leak with a misleading "store-layer" comment, **not** a
+ production leak (production closes deterministically). See **Q23**; do not read
+ it as evidence of a `store.py` defect.
+- **Static gates:** ruff (`E,F,I,UP,B,SIM`, line 120) + `mypy --strict` over
+ `src` + `tests`. Clean module graph (no import cycles).
diff --git a/docs/arch-analysis-2026-06-28-0751/06-architect-handover.md b/docs/arch-analysis-2026-06-28-0751/06-architect-handover.md
new file mode 100644
index 0000000..7a64b6c
--- /dev/null
+++ b/docs/arch-analysis-2026-06-28-0751/06-architect-handover.md
@@ -0,0 +1,196 @@
+# 06 — Architect Handover
+
+**Subject:** Plainweave · **Live tree:** HEAD `8258f76` · **Date:** 2026-06-28
+**Purpose:** Convert the analysis into a sequenced, behaviour-preserving
+improvement backlog an architect can drive. Each initiative names the
+quality-register items it closes (`Q#` → `05-quality-assessment.md`), its
+dependencies, effort, risk, and acceptance criteria. Feeds
+`/axiom-system-architect` (assess → prioritize → catalog-debt).
+
+## Guiding constraints (read before sequencing)
+
+1. **Preserve the doctrine.** Advisory / enrich-only / thin / prescribe-nothing
+ is the product thesis and is faithfully implemented. No initiative here adds
+ enforcement, a heavier dependency, or a sibling-machinery rebuild.
+2. **Behaviour-preserving first.** The contract discipline (versioned envelopes,
+ closed `ErrorCode`, golden-vector seam tests, ≥90% branch gate) is an asset —
+ it makes refactors *safe*. Every initiative must keep `make ci` green and the
+ golden vectors byte-stable (Q17 is a prerequisite for that on timestamped
+ payloads).
+3. **Right-size for the target.** Plainweave is single-operator/local-first. Do
+ **not** gold-plate it into a multi-tenant server (no auth framework, no
+ connection-pool library, no async rewrite). Fixes should remove ceilings and
+ close stated contracts, not chase scale the product does not target.
+
+---
+
+## Initiative A — Persistence hardening *(do first; unblocks B & D)*
+
+**Closes:** Q1, Q2, Q3, Q5, Q7 · **Effort:** S–M · **Risk:** Low · **Blast radius:** `store.py` + the data-access call sites
+
+The single highest-leverage move: make the persistence seam correct and
+contention-survivable before anything else touches it.
+
+- Set `pragma journal_mode=WAL` and an explicit `busy_timeout` in
+ `store.connect` (Q3) — removes the implicit stdlib-default dependency and the
+ writer-blocks-readers ceiling.
+- Add a `sqlite3.Error` → `PlainweaveError` mapping at the store boundary
+ (UNIQUE/PK violation→`CONFLICT`; other `IntegrityError`→`VALIDATION`; else
+ `INTERNAL`) so DB failures honour the documented `ErrorCode` contract (Q1).
+ Prevent the `count(*)+1` race (Q2) by serializing writers with `BEGIN
+ IMMEDIATE` (or a monotonic sequence), not by retry alone.
+- Reuse one connection per logical operation; thread the open connection into
+ `_goal_nodes_for_surface` (Q5) and the Loomweave adapter reads (Q7).
+
+**Acceptance:** a concurrent-writer test produces `CONFLICT` (not raw
+`IntegrityError`); WAL confirmed via `pragma journal_mode`; `busy_timeout` set
+explicitly (no reliance on the stdlib default); `make ci` green.
+*(Note: this does NOT remove the suppressed `ResourceWarning: unclosed database`
+— production connections already close deterministically; that warning is a
+test-fixture issue, see Q23 / Initiative G.)*
+
+## Initiative B — Service decomposition *(depends on A)*
+
+**Closes:** Q9, Q12 · **Effort:** L · **Risk:** Medium (mitigated by the test suite) · **Blast radius:** `service.py`
+
+Break the 3027-LOC god object in behaviour-preserving stages, each landing
+independently with green CI:
+
+1. **Extract a repository / data-access layer** (enabled by A's connection
+ seam) — moves the raw SQL out of the use-case methods. This is the structural
+ keystone.
+2. **Relocate the intent-graph computation** (`service.py:1311-1507`) into
+ `intent_graph.py` so the product's defining capability lives in the module
+ that names it (Q12); wire the dead `IntentGraphReads` facade or retire it (Q19).
+3. **Split aggregates** (requirements / traces / baselines / verification) into
+ per-aggregate service modules sharing the `_error`/`_now`/`_require_actor`/
+ `_record_event`/`_requirement_row` helper cluster.
+
+**Acceptance:** no single module > ~1000 LOC; coverage/orphans/trace logic
+importable from `intent_graph`; every stage keeps the golden vectors and ≥90%
+gate; public method signatures unchanged (surfaces untouched).
+
+## Initiative C — Surface-contract cleanup
+
+**Closes:** Q10, Q16, Q17, Q18, Q22 · **Effort:** M · **Risk:** Low · **Blast radius:** `cli_commands.py`, `mcp_surface.py`, `envelopes.py`
+
+- Extract the shared `_*_dict` serializers + `inspect_project`/
+ `_current_project_key` into a neutral `serialization.py` both surfaces import,
+ dissolving the surface↔surface coupling and the function-local-import
+ workaround (Q10).
+- Make `generated_at` an injectable clock parameter at the envelope boundary
+ (Q17) — also the prerequisite for byte-stable goldens under Initiative B.
+- Guard `project_context_get` like its sibling tools (Q16); add a schema-version
+ registry (Q18); drop the no-op `list_result` param (Q22).
+
+**Acceptance:** `mcp_surface` no longer imports from `cli_commands`; a golden
+vector is byte-identical across two runs without test-side timestamp injection.
+
+## Initiative D — Read-path scaling *(depends on A)*
+
+**Closes:** Q4, Q6, Q8 · **Effort:** M · **Risk:** Low · **Blast radius:** `mcp_surface.py`, `web/views.py`, `service.py`
+
+- Add `limit`/`offset` to `plainweave_preflight_facts_get`; batch the 3-calls-
+ per-requirement into set-based reads; cap/paginate the default project scope
+ (Q4 — tracker P3 `706d80dc8e`).
+- Batch the web review-queue per-draft dossier fetches; derive/cache the pending
+ count (Q6). Push pagination into the service query for MCP `_list` (Q8).
+
+**Acceptance:** a bare `preflight_facts_get` on a large corpus issues O(1)
+set-queries, not O(corpus); the two open P3 tracker tasks close.
+
+## Initiative E — Web hardening
+
+**Closes:** Q13, Q14, Q15 · **Effort:** S–M · **Risk:** Low · **Blast radius:** `web/`
+
+- Refuse a non-loopback `--host` bind unless an explicit `--insecure` / token is
+ given; at minimum document the exposure on the flag (Q13).
+- Promote the manual review-queue a11y behaviour (focus move + `#sr-status`
+ announcement) to a CI gate using the existing web Playwright harness (Q14).
+- Branch CSRF body-parsing on `Content-Type` or test+document the urlencoded
+ constraint (Q15).
+
+**Acceptance:** starting `--host 0.0.0.0` without the ack fails closed; a headless
+test asserts focus + live-region text after approve/accept/reject.
+
+## Initiative F — Migration framework *(before the next breaking schema change)*
+
+**Closes:** Q11 · **Effort:** M · **Risk:** Medium · **Blast radius:** `store.py`
+
+Introduce a version-aware migration ladder keyed on
+`schema_meta.schema_version` (the `read_schema_meta`/SCHEMA_MISMATCH plumbing
+already detects drift). Not urgent while the schema is stable; **required before
+any change that needs a data transform** (the current `create-if-not-exists`
+approach cannot evolve existing rows).
+
+**Acceptance:** a v2→v3 migration with a data transform runs forward
+idempotently and is covered by `test_store_migrations`.
+
+## Initiative G — Hygiene sweep *(any time; trivial)*
+
+**Closes:** Q19, Q20, Q21, Q23 · **Effort:** S · **Risk:** None
+
+Delete the empty `experimental/` package (Q20, confirmed unreferenced); resolve
+the `status requirement` / `verify status` duplicate (Q21); decide
+`IntentGraphReads` (Q19, folds into B); close test-fixture connections explicitly
+and correct/narrow the misleading `ResourceWarning` suppression (Q23).
+
+---
+
+## Sequencing & dependency graph
+
+```
+A (persistence) ──► B (service decomposition)
+ │ │
+ └──────► D (read scaling)
+C (surface contract) ──► (enables byte-stable goldens for B)
+E (web) F (migration) G (hygiene) ── independent, schedule freely
+```
+
+**Recommended order:** A → C → B → D, with E/F/G interleaved opportunistically.
+A is first because it is the shared seam every other data-touching change passes
+through; C is cheap and removes the goldens' non-determinism that B needs.
+
+## Effort × impact
+
+| Initiative | Effort | Impact | When |
+|------------|--------|--------|------|
+| A Persistence hardening | S–M | **High** | Now |
+| C Surface contract | M | Medium | Now (cheap, unblocks B goldens) |
+| B Service decomposition | L | **High** | After A/C |
+| D Read scaling | M | Medium-High | After A (closes 2 P3s) |
+| E Web hardening | S–M | Medium | Scheduleable |
+| F Migration framework | M | Medium | Before next schema break |
+| G Hygiene | S | Low | Any time |
+
+## What to leave alone (anti-gold-plating)
+
+- **The enrich-only / advisory posture** — do not add enforcement.
+- **The single-runtime-dependency footprint** — no ORM, no pool library, no
+ async rewrite; the connection-reuse fix in A is sufficient.
+- **The honest-degradation contract** (typed `unavailable`, `PEER_*` codes,
+ drift flagging) — a strength; extend the pattern, don't replace it.
+- **Web as a single-operator console** — do not build a multi-user auth system;
+ the bind-guard in E is the proportionate control.
+
+## Cross-pack recommendations
+
+- **`/axiom-system-architect`** — drive this backlog: `assess-architecture` for
+ an independent critique, then `prioritize-improvements` / `catalog-debt`.
+- **`/axiom-embedded-database` (`audit-sqlite-discipline`)** — Initiatives A & F
+ are squarely SQLite-discipline work (WAL, busy_timeout, isolation, migration
+ ladder); this pack's reviewer covers exactly those sheets.
+- **`/ordis-security-architect` (`threat-model`)** — scope the web write-surface
+ exposure (Q13) for the `--host` threat model before promoting any non-loopback
+ deployment.
+- **`/axiom-python-engineering` (`refactoring-architect`)** — the staged,
+ behaviour-preserving god-object split in Initiative B.
+
+## Open tracker linkage (already filed)
+
+- P3 `706d80dc8e` — preflight `project` scope fan-out, no cap/pagination → **D/Q4**
+- P3 `3edcd19943` — preflight N+1 connections per scoped requirement → **A/Q3 + D/Q4**
+ (Q5 — the *intent_coverage* N+1 — is a separate site with no dedicated task,
+ also closed by A)
+- P3 `02376962ab` — optional Loomweave-semantic similarity hint (a feature, not
+ debt; out of scope for this remediation backlog)
diff --git a/docs/arch-analysis-2026-06-28-0751/temp/catalog-E1.md b/docs/arch-analysis-2026-06-28-0751/temp/catalog-E1.md
new file mode 100644
index 0000000..ff28e59
--- /dev/null
+++ b/docs/arch-analysis-2026-06-28-0751/temp/catalog-E1.md
@@ -0,0 +1,89 @@
+## Domain Service Core
+
+**Location:** `src/plainweave/service.py`, `src/plainweave/models.py`, `src/plainweave/bindings.py`
+
+**Responsibility:** The domain orchestrator — a single `PlainweaveService` class that owns every requirements-traceability use case (requirement lifecycle, acceptance criteria, trace links, goals/intent edges, code-entity & SEI bindings, baselines, verification methods/evidence, dossiers, and the intent-graph reads) over a directly-accessed SQLite store, emitting an append-only event log and advisory enrich-only reads.
+
+**Key Components:**
+- `service.py` (3027 LOC, one class `PlainweaveService` at `service.py:64`) — ~40 public use-case methods plus ~70 private helpers. Notable surfaces:
+ - Requirement lifecycle: `create_requirement` (`service.py:454`), `update_draft` (`512`), `approve_requirement` (`565`), `supersede_requirement` (`626`), `reject_requirement` (`720`), `deprecate_requirement` (`758`).
+ - Acceptance criteria: `add_acceptance_criterion` (`810`), `list_acceptance_criteria` (`871`).
+ - Trace links: `propose_trace_link`/`create_trace_link` (`905`/`923`), `accept`/`reject`/`mark_stale`/`mark_orphaned` (`994`–`1004`) all funnel through `_transition_trace` (`2386`); canonical relation/transition whitelists at `_validate_trace_relation` (`2767`) and `_validate_trace_transition` (`2782`).
+ - Goals & intent edges: `create_goal` (`1020`), `link_goal_to_requirement` (`1060`), `goals_for_requirement` (`1112`).
+ - Code entities & SEI bindings (ADR-029): `record_code_entity` (`1137`), `bind_sei_to_requirement` (`1198`, persists into `entity_associations`), `list_sei_bindings` (`1286`).
+ - Baselines: `create_baseline` (`304`), `diff_baseline` (`372`); locked-baseline immutability enforced by DB triggers (`store.py:178`).
+ - Verification: `add_verification_method` (`148`), `record_verification_evidence` (`197`), status engine `_compute_verification_status` (`2307`), authority resolver `_evidence_authority` (`2994`).
+ - Dossier: `requirement_dossier` (`260`) assembles authority summary, traces, verification, baseline exposure, `_dossier_computed_gaps` (`1802`), `_dossier_next_actions` (`1876`), `_dossier_peer_facts` (`2565`).
+ - Actor registry / trust boundary: `register_actor` (`78`) with genesis-attester bootstrap (`108`–`124`).
+ - High-fan-in private cluster: `_error` (`3020`, the only `PlainweaveError` factory), `_now` (`3026`), `_require_actor` (`2968`), `_record_event` (`2792`, the append-only `events` writer), `_requirement_row` (`2086`, multi-key resolver matching `display_id`/`requirement_id`/`stable_id`). These five are called by nearly every method and are what keep the monolith cohesive.
+- `models.py` (310 LOC) — ~30 frozen domain dataclasses (typed records): `Actor`, `RequirementDraft`/`Version`/`Record`, `AcceptanceCriterion`, `TraceRef`/`TraceLink`, `IntentGoal`/`IntentEdge`, `CodeEntity`, `Baseline`/`Member`/`Diff`, verification records, and the full `RequirementDossier` section tree (`models.py:222`–`311`). Pure leaf — imports only `dataclasses`/`typing` (`models.py:1`–`4`).
+- `bindings.py` (98 LOC) — ADR-029 SEI value object `SeiBinding` (`bindings.py:29`) with a `sei` back-compat alias (`51`), storage-free constructor `bind_sei_to_requirement` (`56`), and drift helper `is_drifted` (`91`). Pure leaf — only `dataclasses`/`datetime`/`typing`. Comment declares the `loomweave:eid:` scheme FROZEN (consumed, never minted, `bindings.py:16`).
+
+**Dependencies:**
+- Inbound:
+ - CLI Surface — `cli_commands.py` instantiates `PlainweaveService` (`cli_commands.py:1226`, Loomweave unresolved-candidate) and calls its methods (e.g. `service.intent_coverage`, `service.bind_sei_to_requirement`).
+ - MCP Surface — `mcp_surface.py` instantiates it (`mcp_surface.py:743`) and calls e.g. `service.intent_coverage` (`mcp_surface.py:546`).
+ - Web UI — `web/context.py:28` constructs it; `web/routes/intent.py:15` calls `ctx.service.intent_coverage`; `web/routes/{goals,requirements,review}.py` and `web/views.py` import the domain models (grep over `from plainweave.models import`).
+ - Tests — `tests/state/*` and `tests/test_intent_*` exercise the service directly (the dominant resolved caller set).
+- Outbound:
+ - Persistence — `from plainweave.store import connect, read_schema_meta` (`service.py:60`); the service calls `connect(self.db_path)` directly in every method and is itself the data-access tier (no repository layer).
+ - Sibling-Tool Adapters — `loomweave_adapter` (`PUBLIC_SURFACE_TAGS`, `LoomweaveAdapter`, `LoomweaveCatalogEntity`, `LoomweaveIdentityError`, `service.py:24`) and `wardline_adapter` (`WardlineAdapter`, `service.py:61`); built per-call via `_loomweave_adapter` (`2595`) / `_wardline_adapter` (`2598`).
+ - Intent Graph — imports its types (`CorpusEntry`, `IntentCoverage`, `IntentCoverageSurface`, `IntentLevel`, `IntentNode`, `Trace`, `DEFAULT_INTENT_COVERAGE_EXCLUDED_NAMESPACES`, `service.py:15`).
+ - Response Contract / Cross-cutting — `from plainweave.errors import ErrorCode, PlainweaveError` (`service.py:14`).
+
+**Patterns Observed:**
+- Canonical use-case template: `_require_actor` → validate → `_now()` → `with connect() as connection:` → `_requirement_row()` resolve → SQL mutate → `_record_event()` → `connection.commit()` → return a `*_from_row` dataclass (e.g. `create_requirement` `service.py:454`–`510`, `approve_requirement` `565`–`624`).
+- Event sourcing: every mutation appends to the append-only `events` table via `_record_event` (`service.py:2792`); table is update/delete-blocked by DB triggers (`store.py:269`). Event ids are `EVT-{uuid4}` (`service.py:2812`).
+- Optimistic concurrency + idempotency: writes gate on `expected_version`/`expected_draft_revision` (`_require_current_version` `2101`, draft check `529`); `approve`/`supersede`/`deprecate` cache responses in `idempotency_keys` keyed by a request hash and replay them (`574`–`582`, `_store_idempotency` `2823`, `_idempotency_payload` `2872`).
+- Authority from the registry, not the actor string: `_evidence_authority` (`service.py:2994`) and `register_actor` (`78`) derive attestation authority from a registered `actors.kind`; a free-form `--actor human:fake` defaults to least-privileged `agent_reported`. `_require_actor` (`2968`) is only a non-empty check — normal-write attribution is honour-system by design.
+- Multi-key entity resolution: `_requirement_row` (`2086`) and `_goal_row` (`2616`) resolve by `display_id` OR canonical id OR `stable_id`, so any identifier form works at the boundary.
+- Sequential `count(*) + 1` id generation: `_next_requirement_number` (`2110`), `_next_link_number` (`2137`), `_next_evidence_number` (`2149`), etc.
+- Enrich-only Loomweave trace hydration with drift: `_trace_from_row` (`2449`) re-resolves `loomweave_entity` refs via `_enrich_loomweave_trace` (`2483`), flagging `freshness=stale` + a `content_hash_drift` degraded note on hash mismatch (`2496`–`2505`); orphaned/unreachable handled by `_trace_with_degraded_snapshot` (`2521`). Degraded states are always explicit, never an implied-clean.
+- Peer-fact honesty: `_dossier_peer_facts` (`service.py:2565`) holds `live_peer_calls=False` always and records a configured HTTP endpoint as a *capability*, not as evidence of a call (`2585`–`2588`).
+
+**Concerns:**
+- God object / missing layering: a single 3027-LOC class spans ~13 aggregates and is simultaneously the use-case layer AND the data-access layer (raw SQL inline, no repository). High-fan-in helpers keep it cohesive, but every aggregate's logic, SQL, and serialization live in one file (`service.py:64`–`3027`) — the dominant maintainability risk.
+- DB exceptions escape the `ErrorCode` contract: only domain failures route through `_error`→`PlainweaveError` (`service.py:3020`); `connection.execute(...)` is never wrapped, so `sqlite3.IntegrityError`/`OperationalError` propagate raw. Callers that switch on `ErrorCode` (the documented contract) cannot catch them. Fully verifiable from `service.py` + `store.py`.
+- `count(*) + 1` id generation is not concurrency-safe by construction (`service.py:2110`, `2137`, `2149`, …). It fails *safe* — every counter-derived id feeds a UNIQUE/PK column (`requirements.display_id`/`stable_id` NOT NULL UNIQUE `store.py:40`–`41`; `intent_goals` `118`–`119`; all `*_id` PKs), so a concurrent second writer collides on a constraint rather than silently duplicating. But `store.connect()` (`store.py:12`–`19`) sets only `pragma foreign_keys=on` — no `busy_timeout`, WAL, or `BEGIN IMMEDIATE` — so under contention the loser surfaces as a raw `IntegrityError`/`OperationalError` (per the contract gap above) instead of a clean `CONFLICT`. Severity is low for the documented single-writer local-first model; it would bite any multi-process/agent concurrent-write scenario.
+- Per-surface connection fan-out in coverage: `intent_coverage` (`service.py:1415`) calls `_goal_nodes_for_surface` (`1529`) per catalog surface, and that helper opens a fresh `connect()` each call (`1542`) inside the per-entity loop — an N+1-connection pattern that could be slow on a large catalog. Adapters are likewise rebuilt per call (`2595`–`2599`).
+
+**Confidence:** High — read 100% of `service.py` (3027 LOC), `models.py`, `bindings.py`, and `store.py`; cross-checked inbound edges against Loomweave `entity_callers_list`/unresolved-candidate data for `PlainweaveService`, `bind_sei_to_requirement`, and `intent_coverage`, and outbound imports against the source. Inbound mapping leans on Loomweave's dynamic-instantiation candidates (callers unresolved because the class is constructed dynamically) corroborated by the import grep, so subsystem names are well-evidenced but exact call counts are not exhaustive. Gap: I did not read `loomweave_adapter.py`/`wardline_adapter.py` internals (only their consumed surface) or the CLI/MCP/Web serializers in full — left to E-peers owning those subsystems.
+
+---
+
+## Intent Graph
+
+**Location:** `src/plainweave/intent_graph.py`
+
+**Responsibility:** Defines the intent-graph vocabulary and read contract — the node/altitude types (`goal ▲ requirement ▲ code-SEI`), the coverage/orphans/trace/corpus result records (including the honest north-star `IntentCoverage`), and a small injectable `IntentGraphReads` facade — over which `PlainweaveService` computes the actual code-up traceability reads.
+
+**Key Components:**
+- `intent_graph.py` (184 LOC) — pure type + contract module, no DB access:
+ - `IntentLevel(StrEnum)` (`intent_graph.py:28`) — the three altitudes `CODE`/`REQUIREMENT`/`GOAL`; doc notes the graph does not fix the level count.
+ - `IntentNode` (`44`), `Trace` (`55`, up/down justification neighbourhood), `CorpusEntry` (`67`, requirement + goal/code links).
+ - `IntentCoverage` (`100`) and `IntentCoverageSurface` (`84`) — the north-star reading; honesty fields `denominator_complete` (`117`), `coverage` verbatim block (`118`), `surfaces_truncated` (`124`), `excluded_namespaces`/`excluded_count` (`121`–`122`), `adapter_status`/`adapter_degraded` (`125`–`126`). Docstring states the reading is ADVISORY, never a pass/fail (`108`).
+ - `DEFAULT_INTENT_COVERAGE_EXCLUDED_NAMESPACES = ("scripts.", "tests.")` (`80`) — the default denominator scoping.
+ - `IntentGraphReads` (`141`) — injectable facade with three composable reader Protocols (`orphans`/`trace`/`corpus`, `129`–`138`); each method no-ops to an empty result when its reader is unset (`164`, `173`, `181`).
+- Note on where computation actually lives: the graph queries are implemented on `PlainweaveService`, NOT in this module — `intent_orphans` (`service.py:1311`), `intent_trace` (`1346`), `intent_corpus` (`1388`), `intent_coverage` (`1415`). This file is the typed boundary they return across.
+
+**Dependencies:**
+- Inbound:
+ - Domain Service Core — `service.py:15` imports the types; the service's four `intent_*` methods construct and return them.
+ - CLI / MCP / Web Surfaces — consume the result types via the service: `intent_coverage` is called by `cli_commands.py`, `mcp_surface.py:546`, and `web/routes/intent.py:15` (Loomweave callee candidates), which serialize `IntentCoverage`.
+ - Tests — `IntentGraphReads` (the facade class itself) is constructed ONLY by tests (`tests/test_target_interfaces.py:32`, `tests/test_target_interface_stubs.py`); no production caller wires it (Loomweave `entity_callers_list` on `IntentGraphReads` returns only test candidates).
+- Outbound:
+ - None within Plainweave — imports only stdlib `dataclasses`, `enum.StrEnum`, `typing.Protocol` (`intent_graph.py:21`–`25`). A dependency-free leaf module.
+
+**Patterns Observed:**
+- Honesty qualifiers surfaced, not computed-away: `IntentCoverage.numerator`/`denominator`/`ratio` are always the full counts while the evidence lists are bounded by `max_surfaces`, with `surfaces_truncated` flagging dropped rows (`service.py:1483`–`1492`). `denominator_complete` is derived from `coverage["complete"]` (`service.py:1497`), and the whole `coverage` block (including `present_plugins`) is carried verbatim from the Loomweave adapter (`coverage = dict(page.coverage)` `service.py:1456`; `present_plugins` originates in `loomweave_adapter.py:203`, not in this subsystem). A degraded denominator is therefore never presented as a complete-surface reading.
+- Coverage counts LIVE justification only: `_goal_nodes_for_surface` (`service.py:1529`) uses `_live_requirement_ids_for_entity` (`service.py:2730`, `status in ('draft','approved')`), deliberately diverging from `intent_trace`, which keeps surfacing deprecated requirements — documented in-code as "trace *explains*, coverage *counts*" (`service.py:1537`–`1539`). A binding whose only requirement is deprecated is correctly not counted.
+- Namespace scoping + surface-class filtering: default `scripts.`/`tests.` exclusion (`intent_graph.py:80`) applied in `intent_coverage` (`service.py:1476`), with caller override and validated `surface_classes` (`_validated_surface_classes` `service.py:1509`).
+- Prescribe-nothing read surface: `IntentGraphReads` offers three composable graph primitives rather than canned reports (docstring `intent_graph.py:142`–`147`); orphans = "nodes with no upward edge" at any altitude.
+- Frozen-dataclass value objects throughout — every result type is `@dataclass(frozen=True)`.
+
+**Concerns:**
+- `IntentGraphReads` facade is dead in the production path: its docstring advertises "a small injectable facade for tests and adapters" (`intent_graph.py:18`, `142`), but no production adapter constructs it — every shipping read goes directly through `PlainweaveService.intent_*`. The contract and the real reads can drift independently because nothing but tests binds them together (Loomweave callers: tests only). Either wire it into the read path or mark it test-scaffolding.
+- Contract/implementation split is non-obvious: a reader expecting the coverage/orphan/trace logic in `intent_graph.py` finds only types; the algorithms are 1100+ lines away in `service.py`. The module docstring points at `plainweave.service` (`intent_graph.py:15`–`18`), which mitigates but does not remove the indirection.
+- `IntentCoverage` is a wide record (15 fields, `intent_graph.py:114`–`126`) mixing headline counts, evidence lists, scoping echoes, and adapter-health blocks; correct for an honest reading but a lot of surface for downstream serializers to keep in sync.
+
+**Confidence:** High — read 100% of `intent_graph.py` (184 LOC) and the four computing methods plus their private helpers in `service.py`; verified the dead-facade claim via Loomweave `entity_callers_list` on `IntentGraphReads` (test-only candidates) and the `present_plugins` provenance via grep into `loomweave_adapter.py:203`. Gap: I did not read `loomweave_adapter.py`'s `_public_surface_coverage` body in full (only confirmed where `present_plugins`/`coverage.complete` originate), so the exact contents of the verbatim `coverage` block are owned by the Sibling-Tool Adapters slice.
diff --git a/docs/arch-analysis-2026-06-28-0751/temp/catalog-E2.md b/docs/arch-analysis-2026-06-28-0751/temp/catalog-E2.md
new file mode 100644
index 0000000..49dce27
--- /dev/null
+++ b/docs/arch-analysis-2026-06-28-0751/temp/catalog-E2.md
@@ -0,0 +1,34 @@
+## MCP Surface
+
+**Location:** `src/plainweave/mcp_server.py`, `src/plainweave/mcp_surface.py`
+
+**Responsibility:** Exposes Plainweave's read-only, advisory intent-traceability state to agents as a FastMCP server — 19 `plainweave_*` tools plus 15 versioned contract resources — translating `PlainweaveService` results into the `weft.plainweave.*` success/error envelope contract without mutating state or making live peer calls.
+
+**Key Components:**
+- `mcp_server.py` - `create_mcp_server` (`mcp_server.py:11-176`) builds a `FastMCP("plainweave", json_response=True)` and registers 19 thin `@mcp.tool()` wrappers, each delegating to a `PlainweaveMcpSurface` method; `register_resource` (`:167-171`) loops `MCP_RESOURCE_URIS` to register 15 `@mcp.resource()` readers. `main` (`:179-180`, tagged `entry-point`) is the `plainweave-mcp` console script (`pyproject.toml:40` → `plainweave.mcp_server:main`) and runs the stdio server.
+- `mcp_surface.py` - `PlainweaveMcpSurface` (`:391-1653`, 1653 LOC) holds every tool implementation. The 19 tools: project/context (`plainweave_project_context_get` `:395`, `plainweave_loomweave_catalog_list` `:415`); requirements (`plainweave_requirement_search` `:433`, `_get` `:459`, `_dossier_get` `:465`); traces (`plainweave_trace_link_list` `:471`); intent graph (`plainweave_intent_orphans` `:502`, `_trace` `:519`, `_corpus` `:525`, `_coverage` `:536` — the north-star metric); baselines (`plainweave_baseline_list` `:554`, `_get` `:565`, `_diff` `:571`); verification (`plainweave_verification_status_get` `:677`, `_list` `:683`); and the three sibling-tool surfaces (`plainweave_entity_intent_context_get` `:577`, `plainweave_preflight_facts_get` `:598`, `plainweave_wardline_peer_facts_list` `:751`, `plainweave_requirements_enrichment_get` `:759`).
+- `mcp_surface.py` - `_result` adapter (`:724-731`, fan-in 16) is the service→envelope choke point: it lazily builds `PlainweaveService` via `_service` (`:733-743`), runs the tool's `action(service)` closure, wraps the dict in `success_envelope(schema, data, project=…)`, and converts any raised `PlainweaveError` to `error_envelope` via `_error` (`:831-839`). 16 of 19 tools route through it.
+- `mcp_surface.py` - `MCP_TOOL_METADATA` (`:43-194`, 19 entries) and `CONTRACT_RESOURCES`/`MCP_RESOURCE_URIS` (`:196-388`) are the static capability + contract catalog surfaced by `plainweave_project_context_get` (`:395-413`) and `read_resource` (`:705-722`). Every metadata entry asserts `"mutates": False, "local_only": True, "peer_side_effects": []`.
+- `mcp_surface.py` - `plainweave_preflight_facts_get` + helpers (`:598-1312`) compose the Legis-facing preflight fact bundle from local requirement/verification/dossier/baseline/trace state; `plainweave_requirements_enrichment_get` (`:759-823`) and `_entity_intent_context_item` (`:1314-1355`) compose the Warpline/Loomweave-facing peer-fact views over the local trace graph and Loomweave identity catalog.
+
+**Dependencies:**
+- Inbound: `mcp_server.main` (the `plainweave-mcp` entry point) wires `create_mcp_server`; `cli_commands.py` instantiates `PlainweaveMcpSurface` in a function-local scope (CLI↔MCP parity — e.g. the `wardline-peer-facts` subcommand and the preflight/peer-facts golden contract tests reuse the surface directly); test suites (`tests/test_mcp_server.py`, `test_mcp_read_surface.py`, `test_mcp_intent_coverage.py`, `test_warpline_requirements_enrichment.py`) drive it.
+- Outbound: **Domain Service Core** (`PlainweaveService` `service.py`, the sole data authority — all reads go through it); **CLI Surface** (`cli_commands.py:9` — imports the private serializers `_record_dict`, `_dossier_dict`, `_trace_dict`, `_baseline_dict`, `_intent_*_dict`, `_corpus_entry_dict`, `_requirement_verification_status_dict`, `inspect_project`, `_current_project_key`; the DTO layer is owned by CLI, not a neutral contract module); **Response Contract / Cross-cutting** (`envelopes.success_envelope`/`error_envelope`, `errors.PlainweaveError`/`ErrorCode`); **Sibling-Tool Adapters** (`loomweave_adapter.LoomweaveAdapter`/`LoomweaveIdentityError`/`PUBLIC_SURFACE_TAGS`, `wardline_adapter.WardlineAdapter`); **Intent Graph** (`intent_graph.IntentLevel`/`IntentNode`); **Persistence/paths** (`paths.plainweave_db_path`/`project_root`); domain models (`models.TraceLink`/`TraceRef`); FastMCP (`mcp.server.fastmcp`).
+
+**Patterns Observed:**
+- Thin-wrapper / delegation: every `@mcp.tool()` in `mcp_server.py` is a pure forwarder to a same-named `PlainweaveMcpSurface` method (`mcp_server.py:15-165`), keeping transport registration separate from logic.
+- Closure-over-service adapter: tools pass an `action(service)` lambda/inner-function to `_result` (`:724-731`); `_result` owns service construction, envelope wrapping, and `PlainweaveError`→`error_envelope` mapping uniformly (`:730-731`).
+- Lazy, per-call service/adapter construction: `_service` (`:733`), `_loomweave_adapter` (`:745`), `_wardline_adapter` (`:748`) are rebuilt every call — no caching, no shared connection.
+- Defensive degrade-not-fail: scoped-subject failures soft-degrade to warnings instead of aborting reports (`NOT_FOUND` caught at `:633-640`, `:644-652`, `:1188-1193`, `:1445-1455`); peer/identity gaps emit explicit `unavailable` states, never an implied clean/`absent` result (`:808-823`, `:1519-1524`).
+- Self-describing contract: `MCP_TOOL_METADATA` + `CONTRACT_RESOURCES` carry per-tool `authority_boundary` strings and per-contract required sections/enums, advertised via `project_context_get` and resource reads (`:395-413`, `:705-722`).
+- Input validation at the boundary: pagination (`_validate_pagination` `:847`), choice/filter (`_validate_choice` `:865`), entity-ref batch caps (`_validate_entity_refs` `:875`, max 100), and preflight inputs (`:909`) reject before touching the service.
+
+**Concerns:**
+- **Preflight project-scope fan-out (performance hotspot, no pagination).** A bare `plainweave_preflight_facts_get()` uses the default `scope_kind="pending_diff"` with no `requirement_ids`; the live diff is never resolved locally, so it falls back to the *entire* requirement corpus via `service.search_requirements()` (`:990`). It then runs **3 service calls per requirement** — `requirement_preflight_profile`, `verification_status`, and `requirement_dossier` (`:1084-1086`), the last itself a composite query — making the default call O(corpus) with no `limit`/`offset` parameter exposed on the tool at all. `scope_kind="project"` takes the same all-requirements path. This is the surface's single most significant scaling risk.
+- **Bidirectional CLI↔MCP coupling.** `mcp_surface.py:9` imports CLI-owned private serializers from `cli_commands`, while `cli_commands` instantiates `PlainweaveMcpSurface` from inside a function body. `module_circular_import_list` reports no module-level cycle — the function-local reference deliberately dodges one — but the DTO/serialization layer lives in the CLI module rather than a neutral contract module, so the two agent surfaces are mutually dependent and a serialization change ripples across both.
+- **Vestigial `list_result` parameter.** In `_result` (`:724-731`) both branches of `if list_result:` return the identical `success_envelope(...)` (`:727-729`); the parameter is threaded through ~7 call sites but is a no-op, inviting a reader to assume list-specific handling that does not exist.
+- **Post-materialization pagination.** `_list` (`:841-845`) builds the full result list in memory and then slices `items[offset:offset+limit]`; `limit`/`offset` bound the response payload but not the underlying service work, so large corpora are fully materialized per list call.
+- **Inconsistent error-guarding across the 3 non-`_result` tools.** `loomweave_catalog_list` (`:415`) and `wardline_peer_facts_list` (`:751`) wrap their work in `try/except PlainweaveError`, but `plainweave_project_context_get` (`:395-413`) is **unguarded** — a `PlainweaveError` from `inspect_project`/adapter capability would escape the envelope contract instead of becoming an `error_envelope` like every other tool.
+- **Read-only contract holds; one non-determinism caveat.** No method mutates state, opens write paths, or makes live peer calls — adapters are local-snapshot readers and `_service` builds read paths only, so the `mutates:false`/`local_only:true`/`peer_side_effects:[]` metadata is faithful. Caveat: `plainweave_preflight_facts_get` stamps `generated_at` with `datetime.now(UTC)` (`:658`), so that one field is non-deterministic across otherwise-identical calls (not a contract violation, but defeats byte-stable response caching/golden comparison of the full envelope).
+
+**Confidence:** High — Read 100% of both source files (`mcp_server.py` 184 LOC, `mcp_surface.py` 1653 LOC) and cross-verified every structural claim against Loomweave: tool count (19 `@mcp.tool()` decorations vs 19 `MCP_TOOL_METADATA` entries), `_result` fan-in (16 resolved + 16 name-match callers = the same `self._result` sites), inbound wiring (`create_mcp_server` ← `main`/test; `PlainweaveMcpSurface` ← `mcp_server.py:12` + `cli_commands.py` function-local), entry point (`pyproject.toml:40` `plainweave-mcp = plainweave.mcp_server:main`), envelope signatures (`envelopes.py:37/54`), and absence of a module-level import cycle (`module_circular_import_list` → `cycles: []`). The preflight hotspot was traced line-by-line through the corpus-fallback path. Information gaps: did not read `service.py`/adapter internals (out of slice — performance claims rest on the *call shape* at the surface, not service-side query cost), and did not execute the server, so contract-faithfulness is asserted from static reads, not runtime traces.
diff --git a/docs/arch-analysis-2026-06-28-0751/temp/catalog-E3.md b/docs/arch-analysis-2026-06-28-0751/temp/catalog-E3.md
new file mode 100644
index 0000000..a4f2692
--- /dev/null
+++ b/docs/arch-analysis-2026-06-28-0751/temp/catalog-E3.md
@@ -0,0 +1,33 @@
+## CLI Surface
+
+**Location:** `src/plainweave/cli.py`, `src/plainweave/cli_commands.py`
+
+**Responsibility:** Exposes Plainweave's local-core domain operations as the `plainweave` console command — an argparse subcommand tree whose handlers call the Domain Service Core and render its envelopes as stdout (JSON or text) plus process exit codes.
+
+**Key Components:**
+- `cli.py` (38 LOC) — `build_parser` (`cli.py:14-22`) constructs the argparse root, calls `register_commands`, then *late-imports* `add_web_subcommand` from `plainweave.web.server` to attach `web` (`cli.py:19-21`, comment "local import keeps web optional"). `main` (`cli.py:25-38`) is the entry point declared at `pyproject.toml:39` (`plainweave = "plainweave.cli:main"`); it parses argv, handles `--version`, and dispatches via `args.handler` set by `set_defaults` — a dispatch-table-over-argparse pattern, no if/elif ladder.
+- `cli_commands.py` (1631 LOC) — `register_commands` (`56-135`) plus nine `_register_*` helpers (`138-428`) define **16 top-level commands / 38 leaf handlers** through nested subparsers: `init`, `doctor`, `req` (add/edit/show/search/approve/supersede/deprecate/reject), `criterion` (add/list), `trace` (propose/accept/reject/list), `catalog` (record), `goal` (add/link), `bind` (sei), `intent` (orphans/trace/corpus/coverage), `baseline` (create/show/list/diff), `actor` (register), `verify` (method add / evidence record / status), `status` (requirement/unverified/stale), `dossier`, `wardline-peer-facts`, `web`.
+- Service→CLI adapter pair: `_handle_service_result` (`1138-1146`, single object, **fan-in 24**) and `_handle_service_list` (`1149-1154`, list) both build an envelope and funnel to `_handle_output` (`1157-1166`), which instantiates the service (`_service()`, `1208-1219`), catches `PlainweaveError`, and prints `envelope` (`--json`) or `envelope["data"]` (human).
+- Error/exit mapping: `_emit_error` (`1169-1182`) → exit 2 (4 on `INTERNAL`); `_emit_surface_result` (`1185-1205`) mirrors it for the MCP-surface-backed `wardline-peer-facts`.
+- ~21 `_*_dict` shapers (`1231-1631`) convert domain dataclasses (models/intent_graph/bindings) into JSON-serialisable envelope payloads; `_render_dossier` (`1634-1678`) is the one true human-text renderer.
+- Doctor: `_doctor_store_check` / `_doctor_catalog_check` / `_doctor_wardline_check` / `_doctor_mcp_check` (`442-639`) aggregated by `run_doctor` (`642-663`); checks the store schema (auto-`--fix` init/migrate), the Loomweave catalog binding, the Wardline findings snapshot, and that `plainweave.mcp_server` imports — store is fixable, sibling/MCP checks are report-only at the consumer boundary.
+
+**Dependencies:**
+- Inbound: the `plainweave` console-script entry point (`pyproject.toml:39`); the test suite (14 test modules call `cli.main`). **MCP Surface reaches back into this module**: `PlainweaveMcpSurface._project_key` and `.plainweave_project_context_get` import `cli_commands.inspect_project` (`mcp_surface.py:395-413, 825-829`) — `cli_commands.py` is not a pure CLI layer.
+- Outbound: Domain Service Core (`PlainweaveService` via `_service()`, `cli_commands.py:51,1208`); Response Contract / Cross-cutting (`envelopes` success/list/error, `errors` `ErrorCode`/`PlainweaveError`, `cli_commands.py:9-10`); Persistence (`store` `SCHEMA_VERSION`/`connect`/`migrate`/`read_schema_meta`, `paths`, `cli_commands.py:50,52` — `init`/`doctor` touch the store directly, bypassing the service); Sibling-Tool Adapters (`LoomweaveAdapter`, `WardlineAdapter`, `cli_commands.py:19,53`, probed by doctor); MCP Surface (`PlainweaveMcpSurface`, local import `cli_commands.py:1095`, backs `wardline-peer-facts`); Web UI (`add_web_subcommand`, `cli.py:19`, `cli web` shells in); Intent Graph + models (`cli_commands.py:8,11-49`, DTOs for dict shaping).
+
+**Patterns Observed:**
+- Dispatch table via argparse `set_defaults(handler=...)` + `getattr(args, "handler", None)` (`cli.py:34-37`); no central command switch.
+- Thin-handler / lambda-thunk: each handler passes a `lambda service: ...` to the adapter (`cli_commands.py:685-1070`); error handling and output are centralised, not per-command.
+- Per-subcommand `--json` and `--actor` (default `""`) flags rather than global options (`register_commands` throughout).
+- Envelope-everywhere contract: every result is a `weft.plainweave.*.vN` envelope; exit codes derive from `PlainweaveError.code` (`_emit_error`, `1169-1182`).
+- Web kept optional via lazy import so the core CLI loads without the web extra (`cli.py:19`).
+
+**Concerns:**
+- **Module-level cycle between CLI Surface and MCP Surface.** `handle_wardline_peer_facts` must function-locally import `PlainweaveMcpSurface` (`cli_commands.py:1095`, comment "local import: cli_commands<->mcp_surface cycle") while `mcp_surface.py` imports `inspect_project` from `cli_commands` (`mcp_surface.py:395-413,825-829`). Shared domain helpers (`inspect_project`, `initialize_project`, `run_doctor`, the `_*_dict` shapers) live in the CLI module and are consumed by a sibling subsystem — the layering says "CLI" but the module is a de-facto shared services/DTO layer.
+- **Exit-code divergence across handlers.** `init` always returns 0 (`handle_init:439`); `doctor` returns 0/1 (`handle_doctor:682`); service-backed handlers return 0/2/4 (`_emit_error`); argparse contributes its own 2 on parse errors. A wrapper/CI script cannot interpret `plainweave` exit codes uniformly across commands.
+- "Human-readable" output is largely `json.dumps(envelope["data"])` for ~34/38 commands (`_handle_output:1165`); only `dossier` (`_render_dossier`) and `doctor` produce genuine text. The non-`--json` mode is mostly undifferentiated JSON.
+- Dead/duplicate route: `handle_status_requirement` (`1051-1052`) merely delegates to `handle_verify_status`; `status requirement` and `verify status` are the same command exposed twice.
+- Scope/reconciliation note: `wardline-peer-facts` (`124-135`) postdates the analysis basis commit `e95b6ad` (added in branch HEAD `a6044a1`); the Loomweave index reflects current source, so it is documented here — flag for explorers working from `e95b6ad`.
+
+**Confidence:** High — `cli.py` (38 LOC) and `cli_commands.py` (1631 LOC) both read in full; entry point cross-verified against `pyproject.toml:39`; outbound deps cross-validated against import statements; inbound edges and the MCP-Surface cycle confirmed via Loomweave `entity_callers_list` on `main`, `_handle_service_result` (fan-in 24, `traversal_complete:true`), `inspect_project`, `initialize_project`, and `run_doctor`. Information gap / caveat: `web`-handler internals were scoped OUT to the Web UI slice — the `cli.py:19-21` registration edge is confirmed but `web/server.py` was not read, so the single `web` leaf count is registration-confirmed, not source-verified.
diff --git a/docs/arch-analysis-2026-06-28-0751/temp/catalog-E4.md b/docs/arch-analysis-2026-06-28-0751/temp/catalog-E4.md
new file mode 100644
index 0000000..9e71876
--- /dev/null
+++ b/docs/arch-analysis-2026-06-28-0751/temp/catalog-E4.md
@@ -0,0 +1,41 @@
+## Web UI
+
+**Location:** `src/plainweave/web/`
+
+**Responsibility:** An optional, local-first, single-operator Starlette + HTMX server-rendered console that lets one human browse the intent corpus and **author/ratify** requirements, drafts, goals, and agent-proposed trace links — the federation's sole write surface over `PlainweaveService`, contrasted with the read-only MCP server (README.md:143, README.md:173-174).
+
+**Key Components:**
+- `web/app.py` (80) — `create_app(actor, root)` Starlette factory: builds `/healthz` + `/static` mount, installs the CSRF middleware, registers the `PlainweaveError` exception handler, binds `app.state.ctx_factory` (closes over the launch-time `actor`/`root`), then lazily imports `routes.register_all` to append all route modules (app.py:23-80). Also defines the double-submit-cookie CSRF middleware (app.py:43-62).
+- `web/server.py` (59) — CLI `web` subcommand wiring + uvicorn launcher. `add_web_subcommand` adds `--host`(default `127.0.0.1`)/`--port`(8765)/`--actor`/`--no-open` flags (server.py:13-24); `_serve` lazy-imports uvicorn + `create_app` so the framework is only touched when the `[web]` extra is installed, otherwise prints `WEB_EXTRA_HINT` (server.py:31-51).
+- `web/context.py` (61) — `RequestContext.from_root` constructs the per-call `PlainweaveService` + resolves the `OperatorIdentity` (context.py:21-32). `_ensure_operator` self-registers the actor as a `human` actor; allowed at genesis but raises `POLICY_REQUIRED` with a CLI hint once an attester exists (context.py:34-51). Houses CSRF helpers `new_csrf_token`/`csrf_ok` (constant-time compare, context.py:54-61). Default operator `human:operator` (context.py:11,29).
+- `web/views.py` (130) — pure, unit-testable view-model layer: `CorpusRow`/`DraftItem`/`LinkItem` dataclasses, `build_corpus_rows`, `filter_rows`, and `pending_items` (unifies pending drafts + proposed trace links for the review queue), plus `coverage_banner` for the incomplete-denominator warning (views.py:13-130).
+- `web/errors.py` (20) — `error_to_status` maps `ErrorCode` → HTTP status (VALIDATION→400, NOT_FOUND→404, CONFLICT/POLICY_REQUIRED/LOCKED→409, PEER_*→502/503, default→500) (errors.py:5-20).
+- `web/routes/__init__.py` (13) — `register_all` imports the four route modules and calls each `.register(app)` (routes/__init__.py:6-13).
+- `web/routes/requirements.py` (215) — corpus list + requirement CRUD: `GET /` (corpus, HTMX-aware partial vs full, requirements.py:65-85), `GET/POST /req/new` (create_requirement, :140-146), `GET /req/{id}`, `/inline`, `/inline/collapsed`, `GET/POST /req/{id}/edit` (update_draft with optimistic `expected_draft_revision`, CONFLICT→200 conflict partial, :168-202). Form coercion helpers raise VALIDATION (requirements.py:16-42).
+- `web/routes/review.py` (258) — the `/review` ratification queue (review.py:58-71) and its action endpoints: `POST /req/{id}/approve` (approve_requirement, optimistic `expected_version`, :92-125), `POST /trace/{id}/accept` (accept_trace_link, :210-225), `POST /trace/{id}/reject` (reject_trace_link, reason-required, :179-207), plus GET confirm/card partials (approve-confirm, draft-card, reject-form, card, accept-drifted-confirm).
+- `web/routes/goals.py` (42) — `GET /goals` (goals_page + orphan highlighting, goals.py:12-21), `POST /goals/new` (create_goal, :24-28), `POST /req/{id}/ladder` (link_goal_to_requirement, :31-36).
+- `web/routes/intent.py` (35) — `GET /intent` coverage dashboard: `intent_coverage()` + per-level orphans + `coverage_banner` (intent.py:13-31).
+- `web/templates/` + `web/static/` — server-rendered Jinja2 (base + 6 pages + 13 `_partials/`) and static assets (`app.css`, `htmx.min.js`); force-included in the wheel. `base.html` carries the skip-link, the permanent SR live region (`role="status" aria-live="polite"`, innerHTML-OOB only), `aria-current` nav, and the review badge (base.html:11,13-27).
+
+**Dependencies:**
+- Inbound: **CLI Surface** only — `plainweave web` (cli.py:19-21 lazy-imports `add_web_subcommand`; `_handle`→`run_web`→`_serve`→`create_app`, server.py:27-51). No other subsystem launches it.
+- Outbound: **Domain Service Core** (`PlainweaveService` — every read and write goes through `ctx.service`; context.py:9,28); **Intent Graph** (`intent_graph.CorpusEntry`/`IntentLevel`, views.py:6, goals.py:9, intent.py:9); domain `models` (`RequirementRecord`/`RequirementDraft`, requirements.py:11, review.py:13); **Response Contract / Cross-cutting** (`errors.PlainweaveError`/`ErrorCode`, app.py:15, errors.py:3, context.py:7); **Persistence** path resolution (`paths.plainweave_db_path`, context.py:8); and external Starlette/Jinja2/uvicorn (the optional `[web]` extra).
+
+**Patterns Observed:**
+- App-factory + lazy route registration; web framework imports deferred so the package imports cleanly without the `[web]` extra (app.py:77-79, server.py:42-45, cli.py:19, routes/__init__.py:6-13).
+- HTMX partial-vs-full rendering: handlers branch on the `HX-Request` header to return a `_partials/*` fragment vs a full page (requirements.py:74); server-rendered Jinja2 throughout, `htmx.min.js` the only client JS (base.html:8).
+- Pure view-model layer separated from request handling for unit-testability (views.py dataclasses + `build_corpus_rows`/`filter_rows`/`pending_items`).
+- Optimistic-concurrency UX: edit/approve thread an expected-revision field and locally catch `ErrorCode.CONFLICT`, returning **200** with a conflict partial (HTMX only swaps 2xx) that preserves the operator's submitted text (requirements.py:184-202, review.py:101-115).
+- Double-submit-cookie CSRF on all unsafe methods: `pw_csrf` httponly/samesite=strict cookie compared constant-time to the `_csrf` form field, 403 on mismatch, token minted per render into `request.state.csrf_token` (app.py:43-62, context.py:58-61).
+- Centralized error→template mapping: `PlainweaveError` caught by the app-level exception handler → `_partials/error.html` with `ErrorCode`→HTTP status (app.py:32-41, errors.py:5-20).
+- Process-singleton operator identity: the actor is bound once at `create_app` (not per-request/session), defaulted to `human:operator`, self-registered as a `human` actor with a genesis/attester guard (app.py:23-27, context.py:11,26-51); every write passes `actor=ctx.operator.actor_id`.
+- Accessibility structural contracts baked into templates (skip-link, permanent SR live region, per-item `aria-label` on Approve buttons) and locked by `tests/web/test_a11y_contracts.py` (base.html:11,24-25; README.md:188-190).
+
+**Concerns:**
+- **No authentication/authorization + a settable `--host` escape hatch.** Identity is a launch-time process singleton with no per-request verification; CSRF is the only request-level control. Default bind is loopback (server.py:15), but `--host 0.0.0.0` exposes all 7 mutation endpoints (create/edit/approve/accept/reject/create-goal/ladder) to the network with zero auth. The README frames the tool as local-first single-operator (README.md:174), so this is by design, but the `--host` flag offers no compensating control when used.
+- **Core review-queue accessibility behaviour is unverified in CI.** Only *structural* contracts (live-region presence, skip-link, labelled search, per-item aria-labels) are automated; the actual focus-move + live-region announcement on approve/accept/reject require a manual NVDA/VoiceOver pass and "cannot be automated in the current test harness" (README.md:188-192). Documented, but the AX UX the README emphasizes is not gated.
+- **CSRF middleware re-parses the raw body as `application/x-www-form-urlencoded`** via `parse_qsl` (app.py:49-50); a multipart form would yield no `_csrf` field and a 403. All current forms are urlencoded so it works, but it's an implicit, undocumented coupling between the middleware and form encoding.
+- **O(requirements) DB round-trips per render.** `pending_items` calls `search_requirements()` then a `requirement_dossier()` per draft (views.py:82-100), `_pending_count` recomputes the entire queue after every mutation (review.py:41-42,116,198,215), and `_resolve_titles` issues a dossier fetch per draft-only requirement (requirements.py:54-62). Fine at single-operator scale; an N+1 pattern nonetheless.
+- Minor: non-`PlainweaveError` exceptions are re-raised to Starlette's default 500 with no templated page (app.py:32-41) — acceptable, but inconsistent with the themed error partial.
+
+**Confidence:** High - Read all 11 `.py` files in full (app/server/context/views/errors + 4 route modules + 2 `__init__`), `base.html`, `tests/web/test_a11y_contracts.py`, and the README AT-gate section; confirmed the CLI is the sole inbound launcher via grep (cli.py:19-21). Route inventory enumerated directly from each `register()` body; all 7 writes traced to concrete `PlainweaveService` calls; operator-actor enforcement traced through the `ctx_factory` binding. Outbound subsystem mapping rests on imports + call sites (not a manifest), the one inference-based element.
diff --git a/docs/arch-analysis-2026-06-28-0751/temp/catalog-E5.md b/docs/arch-analysis-2026-06-28-0751/temp/catalog-E5.md
new file mode 100644
index 0000000..583f12b
--- /dev/null
+++ b/docs/arch-analysis-2026-06-28-0751/temp/catalog-E5.md
@@ -0,0 +1,104 @@
+# Explorer E5 — Catalog Entries
+
+Basis commit `e95b6ad`. All claims cite `file:line` against the working tree as read; Loomweave index fresh.
+
+---
+
+## Persistence
+
+**Location:** `src/plainweave/store.py`, `src/plainweave/paths.py`
+
+**Responsibility:** Owns the single SQLite database — connection lifecycle, the forward-only schema migration, and store-path resolution — for the whole intent-corpus / traceability domain.
+
+**Key Components:**
+- `store.py:11-19` `connect(db_path)` — a `@contextmanager` that opens a fresh `sqlite3.connect(db_path)`, sets `row_factory = sqlite3.Row` and `pragma foreign_keys = on`, yields, and closes deterministically in `finally`. Loomweave fan-in **44** (single most-coupled entity) — confirmed below.
+- `store.py:22-306` `migrate(db_path, *, project_key)` — idempotent schema setup: `mkdir(parents=True, exist_ok=True)` then one `executescript` of 17 `create table if not exists` + immutability/append-only triggers, a guarded ad-hoc `alter table idempotency_keys add column request_hash` (store.py:296-297), and stamps `schema_meta` with `project_key` (insert-or-ignore) and `schema_version` (upsert). `SCHEMA_VERSION = 2` (store.py:8).
+- `store.py:309-311` `read_schema_meta(connection)` — read side of `schema_meta`; the SCHEMA_MISMATCH detection input.
+- `paths.py:9-24` — `project_root` / `plainweave_dir` / `plainweave_db_path` (`.plainweave/plainweave.db`) / `default_project_key` (sanitised dir name, fallback `"LOCAL"`).
+- Schema enforces domain invariants in SQL: immutable approved requirement text (`requirement_versions_text_immutable` trigger, store.py:76-84), locked-baseline + baseline-member immutability (store.py:178-217), append-only `verification_evidence` (store.py:244-256) and `events` (store.py:269-281). ADR-029 drift column `entity_associations.content_hash_at_attach` is declared here (store.py:160); the drift *comparison* lives in the service layer, not here.
+
+**Dependencies:**
+- Inbound: Domain Service Core (`PlainweaveService.*` — ~38 methods each open `with connect(...)`), CLI Surface (`cli_commands.initialize_project`, `inspect_project`), test suites.
+- Outbound: stdlib `sqlite3`, `pathlib`, `contextlib` only. No ORM, no driver, no pool.
+
+**Patterns Observed:**
+- **Connect-per-operation (confirmed).** `entity_callers_list` on `store.connect` returns one edge per service method (`create_requirement`, `approve_requirement`, `intent_trace`, `get_requirement`, …) plus the two CLI handlers and `migrate` — every domain operation opens and closes its own connection; there is no shared/long-lived connection and no pool (store.py:11-19; verified against Loomweave caller set).
+- Idempotent, **non-version-aware** migration: `migrate` *stamps* `SCHEMA_VERSION` but never branches on it — it re-runs the full `create … if not exists` script every call and applies one guarded `ALTER`; no per-version upgrade steps and no down-migrations (store.py:293-305).
+- Per-connection `pragma foreign_keys = on` (store.py:15) — correct, since FK enforcement is per-connection in SQLite.
+- Invariants pushed into SQL triggers rather than enforced only in Python (store.py:76-281).
+
+**Concerns:**
+- **Connect-per-call + no WAL = cross-surface write/read serialization.** `connect` never sets `journal_mode`, so the DB runs in default `DELETE` (rollback-journal) mode where a writer takes an exclusive lock that blocks all readers. With three concurrent surfaces (MCP, web, CLI) all opening their own connections, this is the contention ceiling (store.py:11-19; no `journal_mode`/`wal` anywhere in `src/plainweave/`).
+- **Confirmed N+1 connections.** In the catalog-with-goals path, `for entity in items:` (service.py:1467) calls `self._goal_nodes_for_surface(entity.sei)` (service.py:1479), and `_goal_nodes_for_surface` (service.py:1529-1550) opens its own `with connect(...)` — i.e. one fresh SQLite connection **per catalog entity**. (Distinct from the in-loop helpers `_goal_ids_for_requirement`/`_entity_ids_for_requirement`, which correctly take an existing `connection` and do *not* re-open.) This is the open tracker's "N+1 SQLite connections per scoped requirement / unbounded project-scope fan-out."
+- **Undocumented dependence on the stdlib busy_timeout default.** `test_store_connections_configure_busy_timeout` (tests/test_store_migrations.py:60-65) asserts `pragma busy_timeout >= 5000` and **passes** — but `connect` sets no `busy_timeout` pragma. The 5000 ms comes solely from `sqlite3.connect`'s default `timeout=5.0`. `connect` exposes no timeout parameter, so this is a latent, undocumented reliance on a stdlib default (the lock-wait window that makes connect-per-call survivable), not an explicit contract.
+
+**Confidence:** High — read 100% of `store.py` (311 LOC) and `paths.py` (24 LOC); cross-checked the connect-per-call claim against the full Loomweave `entity_callers_list` (44 edges); empirically ran the busy_timeout test (passes) to resolve the source-vs-test discrepancy; traced the N+1 loop to `_goal_nodes_for_surface` in source.
+
+---
+
+## Sibling-Tool Adapters
+
+**Location:** `src/plainweave/loomweave_adapter.py` (657 LOC), `src/plainweave/wardline_adapter.py` (373 LOC)
+
+**Responsibility:** Read-only seams that pull *enrichment* facts from sibling Weft tools' local artifacts (Loomweave catalog/SEI; Wardline findings) and translate sibling presence/absence/staleness into an honest, closed degrade vocabulary — never an implied clean state.
+
+**Key Components:**
+- `loomweave_adapter.py:101-194` `LoomweaveAdapter.list_catalog` — paginates public-surface + module entities from the Loomweave SQLite catalog; computes `public_surface_coverage` (closed tag set `exported-api|entry-point|http-route|cli-command`, loomweave_adapter.py:18, 196-204) and emits a `public_surface_tags_incomplete` degrade when tag classes are absent (loomweave_adapter.py:182-184, 215-223).
+- `loomweave_adapter.py:232-415` identity resolution: `resolve_identity` routes to **HTTP** (`_resolve_identity_http`) when an endpoint is configured, else **SQLite** (`_resolve_identity_sqlite`); `resolve_identity_local` (loomweave_adapter.py:237-244) is the explicit local-only entry that never calls a peer.
+- `loomweave_adapter.py:256-285` `_probe_sei_capability` — probes `GET /api/v1/_capabilities` and degrades `unsupported` when SEI isn't advertised, keeping "no SEI capability" orthogonal to "remote is down."
+- `loomweave_adapter.py:456-500` `_entity_from_mapping` — read-time freshness: compares catalog `content_hash` vs the SEI binding `body_hash`; on mismatch sets `freshness="stale"` + `content_hash_drift` degrade (loomweave_adapter.py:472-477). `visibility_unknown` is a permanent *signal*, deliberately kept out of `degraded` (loomweave_adapter.py:462).
+- `loomweave_adapter.py:596-605` `_connect` — opens Loomweave's DB **read-only** via `f"{db_path.as_uri()}?mode=ro"`, closed in `finally` ("Plainweave never mutates the Loomweave catalog").
+- `wardline_adapter.py:242-326` `WardlineAdapter.list_peer_facts` — loads the latest `.wardline/*-findings.jsonl` snapshot (wardline_adapter.py:57-60), splits engine-metric records from entity findings, and computes `resolved_or_unseen` by diffing latest vs prior snapshot with scope/ruleset guards (wardline_adapter.py:136-181, 210-240). Hard-codes an `authority_boundary` block: `local_only: True`, `live_peer_calls: False`, `governance_verdicts: False`, `trust_policy_owner: "wardline"` (wardline_adapter.py:244-249).
+- `wardline_adapter.py:17, 354-373` `_finding_from_record` — closed Wardline kind vocab; `non_defect = kind in {metric,fact,classification,suggestion}`.
+
+**Dependencies:**
+- Inbound: Domain Service Core (`service.py:2595-2599` `_loomweave_adapter`/`_wardline_adapter`), MCP Surface (`mcp_surface.py:746-749`), CLI Surface (`cli_commands.py:535,592` doctor health). All construct adapters fresh and root-bound (stateless).
+- Outbound: Loomweave's `.weft/loomweave/loomweave.db` (read-only SQLite) and optional Loomweave HTTP identity API (`urllib`, 1.5 s timeout, loomweave_adapter.py:551-581); Wardline's `.wardline/*-findings.jsonl` files. stdlib only — **no shell-out to sibling CLIs, no MCP client**.
+
+**Patterns Observed:**
+- **Enrich-only enforced via explicit `unavailable`/degrade, never silence.** Wardline with no snapshot returns `freshness:"unavailable"`, empty facts, a `wardline_findings_absent` degrade, and the note *"result is unavailable, not clean"* (wardline_adapter.py:250-266). Loomweave with a missing DB returns adapter `status:"unavailable"` + `loomweave_db_missing` degrade (loomweave_adapter.py:517-524). Sibling absence is always typed, never an implied pass.
+- **Two different local-only postures.** Wardline is *structurally* local (files only, zero network code). Loomweave is local SQLite (`mode=ro`) by default but carries a **live HTTP identity path** gated on `WEFT_LOOMWEAVE_URL` / `.weft/loomweave/ephemeral.port` (loomweave_adapter.py:583-594); the local-only guarantee therefore depends on the caller choosing `resolve_identity_local` over `resolve_identity`.
+- Closed degrade-code vocabularies on both sides (loomweave: `loomweave_db_missing|loomweave_schema_missing|sei_support_missing|content_hash_drift|public_surface_tags_incomplete|…`; wardline: the five `WARDLINE_DEGRADE_*` constants, wardline_adapter.py:10-14).
+- Defensive parsing throughout — malformed JSONL lines skipped (wardline_adapter.py:98-106), non-object HTTP bodies → `identity_contract` degrade (loomweave_adapter.py:575-580).
+
+**Concerns:**
+- **Multi-connection per read inside the Loomweave adapter.** `list_catalog` calls `_schema_state()` (opens one read-only connection, loomweave_adapter.py:119/526) and then opens a *second* `with self._connect()` for the page query (loomweave_adapter.py:135) — two connections per call; same shape in `_resolve_identity_sqlite` (schema probe + query). Compounds the persistence connect-per-call pattern on the read path.
+- **Doctrine field-names from the brief are not all present in code.** `meta.local_only` and `peer_side_effects: []` do **not** appear in these adapters; the realized contract is Wardline's `authority_boundary` dict + `status:"unavailable"` + the closed degrade vocab. Downstream cross-references should cite the actual fields, not the doctrine labels.
+- **No warpline adapter exists, by design** — and this is the correct cross-reference, not a gap: `grep -rn warpline src/plainweave/` is empty. Plainweave does not *consume* warpline; it *produces* `plainweave_requirements_enrichment_get`, described as "local Plainweave requirement facts for **Warpline's reserved enrichment slot**" (mcp_surface.py:189, 759). That producer seam lives in the MCP Surface (`mcp_surface.py`/`mcp_server.py`/`cli_commands.py`), so `tests/test_warpline_requirements_enrichment.py` has no `src` adapter counterpart by intent.
+- ADR-029 association-level drift (`content_hash_at_attach`) is **not** handled here — the adapter only compares its own catalog hash vs SEI `body_hash`. Association drift detection is a Domain Service Core / Intent Graph concern (evidenced by `tests/state/test_trace_links.py` read-time-drift tests).
+
+**Confidence:** High — read 100% of both adapter files; verified instantiation sites and the absence of CLI shell-out / MCP-client code by grep; confirmed via `mcp_surface.py:189,759` that warpline is the consumer (Plainweave the producer), which fully explains the missing adapter.
+
+---
+
+## Response Contract / Cross-cutting
+
+**Location:** `src/plainweave/envelopes.py` (115 LOC), `src/plainweave/errors.py` (34 LOC), `src/plainweave/paths.py` (24 LOC), `src/plainweave/_version.py`
+
+**Responsibility:** Defines the versioned JSON envelope shapes and the closed error-code vocabulary that every CLI / MCP / service response is wrapped in, so all surfaces emit one uniform, machine-switchable contract.
+
+**Key Components:**
+- `envelopes.py:37-51` `success_envelope(schema, data, …)` — `{schema, ok:True, data, warnings, meta}`; `meta` carries `producer={tool:"plainweave", version:__version__}`, `generated_at` (ISO, defaults to `now(UTC)`), and `project` (envelopes.py:16-21). Loomweave fan-in 11.
+- `envelopes.py:54-78` `error_envelope(code, message, *, recoverable, hint, details, …)` — hard-codes `schema:"weft.plainweave.error.v1"` and `ok:False`; `_error_code` (envelopes.py:28-34) coerces a str into `ErrorCode`, raising `ValueError` on an unknown code (fail-closed contract).
+- `envelopes.py:81-115` `list_envelope` (`items`/`has_more`/`next_offset`) and `batch_envelope` (`succeeded`/`failed`) — thin wrappers over `success_envelope`, so pagination/batch shapes are uniform.
+- `errors.py:6-16` `ErrorCode(StrEnum)` — closed 10-value vocab: `VALIDATION, NOT_FOUND, CONFLICT, POLICY_REQUIRED, PEER_ABSENT, PEER_STALE, PEER_CONTRACT, LOCKED, UNSUPPORTED, INTERNAL` (three `PEER_*` codes carry sibling-degradation into the error contract).
+- `errors.py:19-34` `PlainweaveError` — exception carrying `code`/`message`/`recoverable`/`hint`/`details`, the bridge from raised errors to `error_envelope`.
+- `paths.py` (shared with Persistence) — store-path/project-key resolution.
+- `_version.py:1` `__version__ = "1.1.0"` — stamped into every envelope's `meta.producer.version`.
+
+**Dependencies:**
+- Inbound: MCP Surface (`mcp_surface.py:724` `_result`, plus `read_resource`, `plainweave_loomweave_catalog_list`, `plainweave_project_context_get`, `plainweave_wardline_peer_facts_list`), CLI Surface (`cli_commands.py:1148` `_handle_service_result`, `handle_doctor`, `handle_dossier`, `handle_init`), Domain Service Core (raises `PlainweaveError`; `service.py:2601` `_loomweave_error` maps adapter reasons → `ErrorCode`).
+- Outbound: `_version` (`__version__`), `errors` (`ErrorCode`), stdlib `datetime`/`collections.abc`.
+
+**Patterns Observed:**
+- **Uniform envelope via central choke points.** `entity_callers_list` on `success_envelope` shows it is reached through one wrapper per surface — `mcp_surface._result` and `cli_commands._handle_service_result` — plus the `list_/batch_` helpers, rather than scattered ad-hoc dict-building, giving consistent cross-surface shape (verified, traversal_complete).
+- **Versioning split by direction.** Success/list/batch take a *caller-supplied* `schema` string (per-payload contract id, e.g. `weft.plainweave.requirements_enrichment.v1`); errors use a *single hard-coded* `weft.plainweave.error.v1`. Producer identity + version live in `meta`, decoupled from the payload schema.
+- **Closed, fail-closed error vocab.** Unknown error codes raise rather than pass through (envelopes.py:33-34); switch-on-`code` is guaranteed by `StrEnum` (errors.py).
+- Adapter degrade reasons are translated into the closed `ErrorCode` set at the service boundary (`service.py:2601-2604` `_loomweave_error`), so sibling failures surface as `NOT_FOUND`/`CONFLICT`/`PEER_*` rather than leaking adapter-internal reason strings.
+
+**Concerns:**
+- **`meta.generated_at` is non-deterministic by default** — `_generated_at` falls back to `datetime.now(UTC)` (envelopes.py:12-13) unless the caller threads a value through; envelope snapshots/goldens must inject `generated_at` to be reproducible.
+- **Error-schema version is hard-coded in one place** (`envelopes.py:67`) while success schemas are caller-supplied — a future `error.v2` is a single-point edit, but there is no symmetric constant/registry tying error and success schema versions together, so drift between them is possible.
+- No structural concern in `paths.py`/`_version.py` (verified: error handling N/A for pure path/string resolution; both are trivially correct).
+
+**Confidence:** High — read 100% of `envelopes.py`, `errors.py`, `paths.py`, `_version.py`; confirmed cross-surface uniformity by reading the full `success_envelope` caller set (traversal_complete, both CLI and MCP choke points present) and the `_loomweave_error` reason→ErrorCode mapping in source.
diff --git a/docs/arch-analysis-2026-06-28-0751/temp/dependency-reconciliation.md b/docs/arch-analysis-2026-06-28-0751/temp/dependency-reconciliation.md
new file mode 100644
index 0000000..e745d37
--- /dev/null
+++ b/docs/arch-analysis-2026-06-28-0751/temp/dependency-reconciliation.md
@@ -0,0 +1,74 @@
+# Dependency Reconciliation (orchestrator-owned, from Loomweave global graph)
+
+Region-isolated explorers report one-sided edges. This is the authoritative
+inter-subsystem map, derived from the global index (commit `e95b6ad`), to
+override conflicting explorer claims at merge time.
+
+## Hard facts (Loomweave-verified)
+
+- **No circular imports** anywhere (`module_circular_import_list` → `cycles: []`).
+- **Surfaces are thin over one service.** All three import `PlainweaveService`
+ and dispatch through it:
+ - CLI: `cli_commands.py:51 from plainweave.service import PlainweaveService`;
+ handlers are `lambda service: service.(...)` routed via
+ `_handle_service_result` (fan-in 24).
+ - MCP: `mcp_surface.py:30` same import; tools are `def action(service): ...`
+ closures routed via `_result` (fan-in 16).
+ - Web: `web.context`, `web.views`, `web.routes.requirements`,
+ `web.routes.review` all import `plainweave.service` (Loomweave `imports_in`).
+- **Service → outbound:** `bindings`, `errors`, `intent_graph`,
+ `loomweave_adapter`, `wardline_adapter` (references_out / imports_out).
+ So the **service composes the sibling adapters**, not the surfaces.
+- **Persistence is reached per-use-case, not pooled.** `store.connect`
+ (contextmanager, fresh `sqlite3.connect` each call; sets `foreign_keys=on`,
+ `row_factory=Row`; store.py:11–19) has fan-in 44. Callers = ~35 distinct
+ `PlainweaveService.*` methods, each opening its own connection
+ (`create_requirement`, `approve_requirement`, `intent_corpus`,
+ `intent_orphans`, `intent_trace`, `requirement_dossier`, `search_requirements`,
+ `requirement_preflight_profile`, …). **This is the N+1 / per-call-connect
+ pattern at the source.**
+
+## Layering exceptions / leaks to flag
+
+- **CLI bypasses the service to hit the store directly** in
+ `cli_commands.initialize_project` (cli_commands.py:1115) and
+ `cli_commands.inspect_project` (cli_commands.py:1129) — both call
+ `store.connect()` directly. Plausibly justified (init runs `migrate` before a
+ service exists; inspect/doctor does raw introspection) but it IS a hole in the
+ "surfaces only talk to the service" rule. Note in catalog + quality.
+- **`loomweave_adapter.py:600` opens its own `sqlite3.connect`** — reading
+ *Loomweave's* catalog DB (a different database), not the plainweave store.
+ Expected for a read adapter, but means two distinct SQLite touch-points.
+
+## Canonical dependency direction (for diagrams)
+
+```
+CLI ─┐
+MCP ─┼─▶ PlainweaveService (Domain Service Core)
+Web ─┘ │
+ ├─▶ Intent Graph (types/contract; logic lives IN service.py:1311-1507)
+ ├─▶ Sibling-Tool Adapters ─▶ Loomweave DB (ro SQLite, +cond. HTTP identity)
+ │ └▶ Wardline (.wardline/*-findings.jsonl, files only)
+ ├─▶ Persistence (store.connect, per-call, no pool/WAL) ─▶ SQLite (.plainweave/, schema v2)
+ └─▶ Response Contract (envelopes/errors) ◀─ also used by all surfaces
+CLI ┄┄(init/inspect only)┄┄▶ Persistence [layering exception]
+MCP ◀┄┄(serializers + inspect_project)┄┄ CLI [surface↔surface coupling]
+MCP/CLI ──produce──▶ requirements_enrichment.v1 ▶ (Warpline's reserved slot; Plainweave is PRODUCER, no warpline adapter)
+```
+
+**Corrections from explorer reads (override my pre-catalog assumptions):**
+- **Warpline is consumed-by, not depended-on.** No warpline adapter in `src/`
+ (grep empty). Plainweave *produces* `plainweave_requirements_enrichment_get`
+ ("for Warpline's reserved enrichment slot", mcp_surface.py:189,759).
+- **Two honesty contracts at two layers** (both real): the MCP tool metadata
+ uses `mutates/local_only/peer_side_effects:[]` (mcp_surface.py:43-194); the
+ Wardline adapter uses an `authority_boundary{local_only, live_peer_calls:False,
+ …}` dict (wardline_adapter.py:244-249). The doctrine field names live in the
+ MCP layer, not the adapter layer.
+- **Loomweave local-only is conditional:** read-only SQLite by default, but a
+ live HTTP identity path is gated on `WEFT_LOOMWEAVE_URL`; local-only holds
+ only if the caller picks `resolve_identity_local` (loomweave_adapter.py:237).
+
+Schema: `SCHEMA_VERSION = 2` (store.py:8); intent tables added in v2 over the
+v1 precursor without rewriting precursor rows (test:
+`test_schema_v2_adds_intent_tables_without_rewriting_precursor_rows`).
diff --git a/docs/arch-analysis-2026-06-28-0751/temp/validation-catalog.md b/docs/arch-analysis-2026-06-28-0751/temp/validation-catalog.md
new file mode 100644
index 0000000..92e758b
--- /dev/null
+++ b/docs/arch-analysis-2026-06-28-0751/temp/validation-catalog.md
@@ -0,0 +1,162 @@
+# Validation Report — 02-subsystem-catalog.md
+
+**Validator:** analysis-validator (independent gate) · **Date:** 2026-06-28
+**Artifact under test:** `docs/arch-analysis-2026-06-28-0751/02-subsystem-catalog.md`
+**Live tree validated against:** HEAD `8258f76` (confirmed via `git rev-parse`)
+**Basis docs read:** `01-discovery-findings.md`, `temp/dependency-reconciliation.md`,
+`temp/catalog-E{1..5}.md`
+
+---
+
+## (a) VERDICT: **PASS-WITH-FIXES**
+
+The catalog is contract-conformant (8/8 entries complete) and substantively
+accurate. **Every one of the 8 high-stakes claims I was asked to spot-verify is
+VERIFIED against live source** (line-precise in most cases). No claim was
+refuted as architecturally wrong; no over-claim found. The fixes below are
+**citation/count corrections** — none change a single architectural conclusion,
+none block the downstream quality pass.
+
+> **Environmental caveat (material to scope):** Loomweave has **no index** for
+> this project right now (`.weft/loomweave/loomweave.db` is absent — every
+> `mcp__loomweave__*` tool returns "NO INDEX"). I therefore could **not**
+> re-verify the Loomweave-derived integer metrics (`store.connect` fan-in **44**,
+> `_error` fan-in **36**, `_handle_service_result` fan-in **24**, `_result`
+> fan-in **16**, `success_envelope` fan-in **11**, `cycles:[]`). I verified the
+> *qualitative facts those integers stand for* directly from source and at
+> runtime instead (see §c). The fan-in integers themselves are **UNCONFIRMED**
+> (not refuted) — the catalog's own basis-fidelity note honestly discloses the
+> index↔live delta, which mitigates.
+
+---
+
+## (b) Contract-conformance table (8 entries × required fields)
+
+Required per entry: Location · Responsibility · Key Components · Dependencies
+(Inbound + Outbound) · Patterns Observed · Concerns · Confidence (w/ reasoning).
+
+| # | Entry | Loc | Resp | KeyComp | Dep-In | Dep-Out | Patterns | Concerns | Conf+reason | Verdict |
+|---|-------|-----|------|---------|--------|---------|----------|----------|-------------|---------|
+| 1 | Domain Service Core | ✅ | ✅¹ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | PASS |
+| 2 | Intent Graph | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | PASS |
+| 3 | CLI Surface | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | PASS |
+| 4 | MCP Surface | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | PASS |
+| 5 | Web UI | ✅ | ✅¹ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | PASS |
+| 6 | Persistence | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | PASS |
+| 7 | Sibling-Tool Adapters | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | PASS |
+| 8 | Response Contract / X-cut | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | PASS |
+
+¹ Single sentence per the contract, but very long/semicolon-dense (Domain
+Service Core, Web UI). Conformant; readability-only.
+² Intent Graph Outbound = "none — stdlib only." Explicitly stated, not empty.
+
+All 8 Confidence fields carry evidence-based reasoning (files read in full,
+cross-checks named). **Contract conformance: PASS for all 8.**
+
+---
+
+## (c) Spot-verified claims (the 8 high-stakes checks)
+
+| # | Claim | Verdict | Evidence (live tree `8258f76`) |
+|---|-------|---------|--------------------------------|
+| 1 | **N+1 connections** `service.py:1467→1479→1529-1550` opens own `connect()` per surface | **VERIFIED** | `:1467` `for entity in items:`; `:1479` `goals = self._goal_nodes_for_surface(entity.sei)`; `:1529` def; **`:1542` `with connect(self.db_path) as connection:`** inside, one fresh connection per catalog entity. Exactly as described. |
+| 2 | **`store.connect` connect-per-call, no pool, no WAL, only `foreign_keys=on`** (store.py:11-19) | **VERIFIED** | `store.py:11-19` verbatim: fresh `sqlite3.connect`, `row_factory=Row`, `execute("pragma foreign_keys = on")`, close in `finally`. **No `journal_mode`/`wal`/`busy_timeout` anywhere in `src/plainweave/`.** Fan-in 44 itself UNCONFIRMED (no Loomweave index) — connect-per-call pattern confirmed from the code shape. |
+| 3 | **DB exceptions escape `ErrorCode`** — `connection.execute` unguarded; only `_error`→`PlainweaveError` wrapped | **VERIFIED (end-to-end)** | Service side: `_error:3020` is the sole `PlainweaveError` factory; **86 `connection.execute(...)` calls; exactly 3 `except` total — `LoomweaveIdentityError`×2 (`:2477,:2489`) + `ValueError`×1 (`:2959`). None guard a DB exec.** Surface side (closes the "escapes past callers" half): **both adapters catch `except PlainweaveError` ONLY** — `_result` (`mcp_surface.py:730`), `_handle_output` (`cli_commands.py:1179`); no generic `except Exception` on the result path (the two `except Exception` in cli_commands `:536,:593` are scoped to *doctor sibling probes*, not the service path). So a raw `sqlite3.IntegrityError`/`OperationalError` is **not** normalized to INTERNAL — it propagates past the `ErrorCode` contract. Concern is accurate, not over-claimed. |
+| 4 | **MCP preflight fan-out** — bare call → whole corpus via `search_requirements` (`:990`) → 3 calls/req (`:1084-1086`), no limit/offset | **VERIFIED** | `_preflight_requirement_ids:983`: when `requirement_ids is None` → `return [record.id for record in service.search_requirements()]` (**`:990`**, whole corpus, no pagination). `:1084-1086` = `requirement_preflight_profile` + `verification_status` + `requirement_dossier` (**3 service calls/req**). Line-exact. |
+| 5 | **No module-load cycle; function-local import dodges it** (correction of E3's "module-level cycle") | **VERIFIED — correction is RIGHT** | `mcp_surface.py:9` imports from `cli_commands` at **module level**; `cli_commands.py` imports `PlainweaveMcpSurface` **only function-locally** (`:1103`, `:1114`, comment `# local import: cli_commands<->mcp_surface cycle`). `grep` confirms **NO module-level `mcp_surface` import in `cli_commands`**; runtime `import plainweave.cli_commands; import plainweave.mcp_surface` succeeds with **no ImportError**. Genuinely no import cycle. (Couldn't run `module_circular_import_list` — index absent — but source+runtime are dispositive.) ⚠ **Catalog cites the import at `cli_commands.py:1095`; actual sites are `:1103` and `:1114` (two sites).** |
+| 6 | **Warpline = producer, not consumer** — no warpline in src; `requirements_enrichment_get` at mcp_surface.py:189,759 | **VERIFIED** | `grep -rn warpline src/plainweave/` → **empty**. `mcp_surface.py:189` = `authority_boundary` string "...for Warpline's reserved enrichment slot..."; `plainweave_requirements_enrichment_get` def at **`:761`** (catalog cites 759 — section header; def is 761, ±2). No warpline adapter exists. |
+| 7 | **Version `1.1.0`** (catalog) vs README's 1.0 | **VERIFIED** | `_version.py` → `__version__ = "1.1.0"`. Code says 1.1.0. (README/classifier "1.0 / Development Status 5" is a separate doc-vs-code drift already noted in `01`.) |
+| 8a | **19 MCP tools** | **VERIFIED** | `mcp_server.py`: **19** `@mcp.tool()`; `mcp_surface.py`: **19** `MCP_TOOL_METADATA` entries (counted by `"mutates"`). Match. |
+| 8b | **15 contract resources** | **VERIFIED (count) / phrasing loose** | `len(MCP_RESOURCE_URIS) == 15` (registered via loop `mcp_server.py:173-174`). Count right. Catalog/E2 phrasing "registers 15 `@mcp.resource()` readers" is loose — there is **1** `@mcp.resource()` decorator applied in a loop, not 15 literal decorators. Cosmetic. |
+| 8c | **16 commands / 38 CLI handlers** | **VERIFIED (38 direct) / 16 by enumeration** | `set_defaults(handler=` → **38** leaf handlers (exact). 16 top-level commands matches the entry's own enumerated list (init/doctor/req/criterion/trace/catalog/goal/bind/intent/baseline/actor/verify/status/dossier/wardline-peer-facts/web). |
+| 8d | **22 web routes (15 GET + 7 POST)** | **VERIFIED (app-wide) — attribution nit only** | The numbers are **correct app-wide**: `web/routes/` registers **21** (requirements 8, review 9, goals 3, intent 1 = **14 GET + 7 POST**); **`app.py:85` `/healthz`** adds the 15th GET → **15 GET + 7 POST = 22**. The only defect is the catalog attributing all 22 to "`routes/`" when one GET lives in `app.py`. **POST=7 is exact** and matches the 7 named write endpoints. (Downgraded from an initial "REFUTED" — the count is right; only the locus label is imprecise.) |
+
+---
+
+## (d) Inconsistencies / over-claims (with severity)
+
+**No CRITICAL or HIGH findings.** No architectural claim is wrong; no concern is
+stated as fact beyond what source supports. Findings are citation/count drift:
+
+| ID | Severity | Finding | Evidence | Fix |
+|----|----------|---------|----------|-----|
+| F1 | **Low** | Web route tally **mis-attributed** (not miscounted). Catalog: "routes/ — 22 routes (15 GET + 7 POST)". App-wide count is correct (22/15/7); but `routes/` alone = **21 (14 GET + 7 POST)** — the 15th GET is `app.py:85` `/healthz`. | route greps; `app.py:85` | Re-attribute: "21 routes in `routes/` (14 GET + 7 POST) + `/healthz` & `/static` mount in `app.py`" (or label the 22 as app-wide). Keep "7 writes" — exact. |
+| F2 | **Low** | Local-import citation drift. Catalog (×2: cross-cutting + CLI concern) cites the `PlainweaveMcpSurface` function-local import at **`cli_commands.py:1095`**; actual = **`:1103` and `:1114`** (two sites). | grep | Change `:1095` → `:1103,:1114`. |
+| F3 | **Low** | Count error: "register_commands + **nine** `_register_*` helpers". Actual = **11** (`_register_{requirement,criterion,trace,catalog,goal,bind,intent,baseline,actor,verify,status}_commands`). | 11 `def _register_*` | "nine" → "eleven". |
+| F4 | **Low/Info** | Loomweave integer metrics not re-derivable via MCP (index absent): fan-in 44/36/24/16/11. **Grep call-site bounds are all consistent** (not contradicted): `self._result(`=**16** (exact), `_handle_service_result(`=25 raw vs 24, `self._error(`=48 raw calls ≥ 36 entity-fan-in, `connect(`=36 in service.py alone ≤ 44 codebase-wide, `success_envelope(` external callers ≈10 ≈ 11. | NO-INDEX state + grep | Leave figures (consistent); carry the basis-fidelity caveat wherever reused; `loomweave analyze` before treating integers as current-HEAD. |
+| F5 | **Low/Info** | Coverage: `__init__.py` and `__main__.py` (`python -m plainweave`→`cli.main`) are not explicitly placed in any of the 8 entries. Trivial wiring, zero architectural weight; `experimental/` correctly excluded as dead. | `find` + `cat __main__.py` | Optional one-line note that trivial package wiring (`__init__`/`__main__`) is intentionally not cataloged. |
+| F6 | **Info** | Minor ±1–9 line drift on a few non-headline citations (`requirements_enrichment_get` def 761 vs cited 759; `inspect_project` use 396/828 vs cited 395-413/825-829). Consistent with the disclosed +77 LOC `cli_commands.py` delta. Reconciliation-map temp doc cites `initialize_project`/`inspect_project` at 1115/1129 vs actual **1124/1138** (temp doc, not the artifact). | reads | No action required for the catalog; substantively correct. |
+
+**Internal-consistency cross-checks that PASS:**
+- Bidirectional edges reconcile: Service Inbound{CLI,MCP,Web} ↔ each surface's
+ Outbound{Service}; MCP Outbound{CLI} ↔ CLI Inbound{MCP reaches back};
+ Adapters Inbound{Service,MCP,CLI-doctor} ↔ Service/MCP Outbound{Adapters};
+ Persistence Inbound{Service,CLI} ↔ Service/CLI Outbound{Persistence}.
+- "No module-load cycle" is stated consistently across cross-cutting themes +
+ CLI + MCP entries (E3's contradicting "module-level cycle" was correctly
+ overridden at merge — and the override is factually right, §c #5).
+- The catalog matches the reconciliation map on direction (surfaces→service;
+ service composes adapters; CLI init/inspect bypasses service to store).
+- Minor asymmetry (Info): Web Outbound lists "Persistence path resolution"
+ (`paths.py`) but the Persistence entry's Inbound omits Web. Not an error —
+ `paths.py` is explicitly double-homed (shared by Persistence + Response
+ Contract); Web touches `paths`, not `store.connect`.
+
+---
+
+## (e) Punch-list (to reach a clean PASS — all non-blocking)
+
+1. **F1** — Correct the web-route count: `routes/` = 21 (14 GET + 7 POST); state
+ `/healthz` + `/static` live in `app.py`. (Keep "7 write endpoints".)
+2. **F2** — Fix the local-import citation `:1095` → `:1103,:1114` (two sites).
+3. **F3** — "nine `_register_*` helpers" → "eleven".
+4. **F4** — Tag the Loomweave fan-in integers as index-basis (`e95b6ad`) and
+ carry the basis-fidelity caveat wherever they're reused; re-run
+ `loomweave analyze` before downstream phases treat them as current.
+5. **F5** (optional) — One line acknowledging `__init__.py`/`__main__.py` as
+ intentionally-uncataloged trivial wiring.
+
+---
+
+## SME Agent Protocol sections
+
+**Confidence Assessment — High (structural) / Medium (a few integer metrics).**
+- High on all 8 contract-conformance verdicts and on 7 of 8 spot-checks (read
+ the relevant source spans directly; line-precise).
+- High on the route-count refutation and the no-module-cycle correction (source
+ + runtime import test are dispositive).
+- Medium-only where I depended on the catalog's Loomweave integers, which I
+ could not independently re-derive (index absent).
+
+**Risk Assessment.**
+- *Low residual risk to downstream phases.* The catalog's architectural
+ conclusions (god-object/no-layering, connect-per-call + no WAL, DB-exceptions
+ escape `ErrorCode`, preflight O(corpus) fan-out, surface↔surface coupling,
+ enrich-only honesty, Warpline-as-producer) are all source-verified and safe to
+ build the quality/architect passes on.
+- *Risk if F1–F3 propagate verbatim:* a downstream consumer quoting "22 routes",
+ "nine helpers", or ":1095" inherits a small inaccuracy. Cosmetic, not
+ decision-altering. Fix before publication.
+- *Risk from F4:* if anyone treats the fan-in integers as live HEAD truth they
+ may be a few commits stale (confined to `cli_commands.py`/`mcp_surface.py`).
+
+**Information Gaps.**
+- Loomweave index absent → exact fan-in 44/36/24/16/11 not re-derivable via
+ MCP; **bounded consistent** by grep call-site counts (one exact: `_result`=16)
+ and `cycles:[]` confirmed by source + runtime import test. No contradiction
+ found; precise integers remain index-basis (`e95b6ad`).
+- I did not re-execute the `busy_timeout` test (E5 reports it passes); I
+ confirmed only the precondition — `connect` sets no `busy_timeout` pragma.
+- Technical-accuracy judgements (is connect-per-call the *right* design, is the
+ god-object the *top* refactor target) are out of a structural validator's
+ remit — left to the quality/architecture pass.
+
+**Caveats.**
+- This is a **structural** validation: contract conformance, cross-document
+ consistency, evidence presence, and source-fidelity of the high-stakes claims.
+ It is **not** a code-quality or architectural-soundness judgement.
+- "PASS-WITH-FIXES" means the artifact is fit to proceed; the punch-list is
+ polish, and zero items block the next phase.
+- Per the adversarial mandate: the findings above are deliberately exhaustive
+ down to Info severity — a zero-finding pass would have been an audit defect,
+ not a clean bill of health.
diff --git a/docs/arch-analysis-2026-06-28-0751/temp/validation-synthesis.md b/docs/arch-analysis-2026-06-28-0751/temp/validation-synthesis.md
new file mode 100644
index 0000000..7fabc78
--- /dev/null
+++ b/docs/arch-analysis-2026-06-28-0751/temp/validation-synthesis.md
@@ -0,0 +1,137 @@
+# Validation Report — Synthesis Layer (03 / 04 / 05 / 06)
+
+**Validator:** analysis-validator (second independent gate — synthesis layer) · **Date:** 2026-06-28
+**Artifacts under test:** `03-diagrams.md`, `04-final-report.md`, `05-quality-assessment.md`, `06-architect-handover.md`
+**Ground truth checked against:** `02-subsystem-catalog.md` (V1-validated, fixes applied), `temp/dependency-reconciliation.md`, `temp/validation-catalog.md` (V1 report), and **live source at HEAD `8258f76`**.
+**Mandate:** adversarial — catch synthesis-time judgment claims that drift from evidence. V1 never saw 03–06 (they post-date that gate).
+
+---
+
+## (a) VERDICT: **PASS-WITH-FIXES**
+
+The synthesis layer is contract-conformant, internally near-consistent, and substantively well-supported. Every high-stakes claim I was asked to re-derive against source is **VERIFIED** — including the just-added Q23 `ResourceWarning` correction, whose three sub-claims hold end-to-end. The four initiatives' remediations are technically sound for the stated scope.
+
+**One required fix (Medium) and a short Low/Info punch-list.** The required fix is a genuine over-claim: the Q23 correction was propagated to docs **05** and **06** but **not** to doc **04**, which still carries the retracted pre-Q23 framing — a direct 04↔05 contradiction in the most-read document. It does **not** block the analysis from proceeding to `/axiom-system-architect`; it must be corrected before publication.
+
+---
+
+## (b) Per-document findings
+
+### Doc 04 — Final Report
+
+**S1 · ResourceWarning over-claim contradicts the corrected evidence (Q23) · MEDIUM · required fix · HEADLINE**
+`04:150-152` (Maturity & fitness, caveat *(b)*) states:
+> "the **persistence layer** carries a documented, suppressed `ResourceWarning: unclosed database` ('track the underlying leak separately') that **corroborates the connect-per-call concern** in the team's own words."
+
+This is the **pre-Q23 framing that Q23 explicitly retracts.** I verified Q23 against live source (see §c) and it is correct, so 04's clause is the wrong one:
+- **"persistence layer carries"** → false attribution. Production connection sites close deterministically in `finally` — `store.connect` (`store.py:16-19`) and `LoomweaveAdapter._connect` (`loomweave_adapter.py:602-605`, comment: "closed deterministically rather than left to GC"). The warning originates in **test fixtures** (`tests/loomweave_test_utils.py:10,90`, `with sqlite3.connect(...) as conn` — commits but does not close). It is **not** a production/store-layer leak.
+- **"corroborates the connect-per-call concern"** → directly contradicted by Q23's own correction note: *"the connect-per-call concern stands on connection **count** and journal mode, **not** on this warning."*
+
+Doc 05's metrics snapshot (`05:234-237`) and Q23 (`05:205-222`), and doc 06's Initiative A note (`06:47-49`) + Initiative G (`06:130-135`), are all already Q23-consistent. **Only 04 is stale.**
+→ **Fix:** delete caveat *(b)* or rewrite Q23-consistently, e.g.: *"(b) a suppressed `ResourceWarning: unclosed database` traces to **test fixtures** (`with sqlite3.connect()` that never close), not a production leak — production closes deterministically; the suppression comment's 'store-layer' attribution is itself inaccurate (see Q23). It is **not** evidence for the connect-per-call concern, which stands on connection count + journal mode."* Keep caveat *(a)* (a11y) — it is accurate.
+
+**S4 · Ranked-risk list mixes severity tiers without saying so · INFO (transparency, not an error)**
+`04:103-129` ranks 5 risks: #1 god object (Q9-High), #2 connect-per-call/N+1+no-WAL (Q3-High, **with Q4-High folded into its prose**), #3 DB exceptions escape (Q1-High), #4 surface↔surface coupling (Q10-**Medium**), #5 web exposure (Q13-**Medium**). All four register-High items (Q1/Q3/Q4/Q9) **are** represented (Q4 inside #2), so this is *not* a miscount — but a reader mapping "top-5 ranked" onto "4 High" sees Q4 missing as a line item and two Mediums promoted. The exec summary's "concentrated in two places + one correctness gap" framing is fully consistent. No fix required; optionally add one clause noting the ranking reflects *risk concentration*, not severity order.
+
+**S7 · "clean / textbook layered service" headline — qualification check · INFO**
+`04:25-26` ("textbook layered service whose discipline is real, not aspirational") sits next to a 3027-LOC god object rated risk #1. It is **defensible** if "clean" is read as *module-graph topology* — `cycles:[]`, which I independently reconfirmed (§c). The exec summary immediately names the god object as concentrated risk, so it is adequately qualified. Flagging only so a downstream architect does not read "sound/clean" as "no structural problem." Soundness adjudication is `architecture-critic`'s remit, not mine — no change recommended.
+
+### Doc 05 — Quality Assessment
+
+**Clean on the synthesis-integrity axis.** Severity tally **4 High / 8 Medium / 11 Low = 23** matches the 23 Q-items exactly (High: Q1,Q3,Q4,Q9; Medium: Q2,Q5,Q6,Q10,Q11,Q13,Q14,Q17; Low: the remaining 11). No item's severity contradicts its own impact description (§d). Q23 is correctly framed and self-aware. Every Q traces to a `02` concern or to source (§e).
+
+**S2 · Tracker linkage `3edcd19943` → Q5 is imprecise · LOW**
+`05:87` (and `06:191`) cite tracker `plainweave-3edcd19943` against **Q5 (N+1 in `intent_coverage`)**. The task's actual title/body is *"Preflight: N+1 SQLite connections per scoped requirement"* (`mcp_surface.py:698-700`) — a **different code site** from Q5's `intent_coverage` loop (`service.py:1467→1479→1529-1550`). Same per-call-connect *pattern*, but closing `3edcd19943` would not by itself fix Q5's site. Its `→ Q3` linkage is sound (the task self-describes as "the repo-wide per-call connect pattern"); its natural Q-home is **Q3/Q4** (preflight).
+→ **Fix:** remap `3edcd19943 → Q3/Q4`; note that **Q5 (intent_coverage N+1) has no dedicated tracker task** (file one, or fold explicitly under Initiative A). Low — both are remediated together by A's unit-of-work, so planning impact is nil.
+
+**S5 · "361 test functions" is approximate · INFO**
+`04:59`, `05:228` cite 361. A raw `def test_` sweep of `tests/` yields ≈369. Within counting-method noise (parametrize expansion, helper defs, collected-vs-defined); not decision-altering. Optionally label as "~360".
+
+### Doc 06 — Architect Handover
+
+**Clean on traceability and sequencing.** All 23 Q-items map to exactly one initiative (A:Q1,2,3,5,7 · B:Q9,12[,19] · C:Q10,16,17,18,22 · D:Q4,6,8 · E:Q13,14,15 · F:Q11 · G:Q19,20,21,23) — no orphan Q, no phantom Q. Q19's double-listing (B step 2 + G) is explicitly reconciled ("folds into B"). Dependency graph (A⇒B, A⇒D, C⇒B-goldens) is coherent. The "what to leave alone" anti-gold-plating section correctly preserves the doctrine.
+
+**S3 · Initiative A remediation menu — minor technical caveats · LOW/INFO (route to embedded-database)**
+The remediation set (WAL + explicit `busy_timeout` + `sqlite3.Error→PlainweaveError` + `BEGIN IMMEDIATE`/sequence) is **technically sound and will work** for single-operator/local-first. Two precision caveats for the implementer:
+- `IntegrityError → CONFLICT` (`06:38-40`, `05:46-48`) is slightly **too broad**: NOT-NULL/CHECK violations are also `IntegrityError` but are not "conflicts." Map only UNIQUE/PK collisions to `CONFLICT`; route other `IntegrityError` to `INTERNAL`/`VALIDATION`.
+- `BEGIN IMMEDIATE` **serializes** the `count(*)+1` read-then-insert, which *prevents* the id collision rather than needing "retry-to-`CONFLICT`" (`05:56-57`). The two listed fixes are alternatives, not a sequence; the "retry-to-CONFLICT" phrasing conflates them. Mechanisms are all valid.
+These are implementation-detail refinements, not contract breakers. Doc 06 already routes Initiatives A & F to `/axiom-embedded-database (audit-sqlite-discipline)` — the authoritative SQLite-discipline pass. No verdict impact.
+
+### Doc 03 — Diagrams
+
+**Faithful to the reconciled edge map and the V1-corrected catalog.** Spot-checks all PASS:
+- 3 surfaces → 1 service (D2/D3 CLI/MCP/WEB→SVC); MCP read-only + Web sole-write (D2 `classDef web write` / `mcp read`, reading text). ✅ Matches `02`.
+- Two layering exceptions present as dotted edges: CLI⤏store (init/inspect), MCP⤏CLI (serializers + inspect_project). ✅ Matches reconciliation map; D2 reading correctly notes "no module-load cycle."
+- Warpline = **producer**, dotted, off the MCP/CLI surface (not an adapter): D1 `PW -. PRODUCES requirements_enrichment.v1 .-> warp`; D3 `SVC -. produces requirements_enrichment.v1 .-> MCP`. ✅ Matches the reconciliation correction.
+- Web route counts: D3 labels Web "22 routes (15 GET / 7 POST)" — the **app-wide** figure, consistent with V1's F1 fix (21 in `routes/` [14 GET+7 POST] + `/healthz` in `app.py` = 22 app-wide). ✅ Numbers right.
+- N+1 sequence (D5) matches `service.py:1467` (`for entity in items`) → `:1479` (`_goal_nodes_for_surface`) → `:1542` (`with connect(...)`) inside `:1529-1550`. ✅ Verified line-exact (§c). D5's "list_catalog opens 2 ro connections" matches Q7.
+- Web-write sequence (D6) matches the catalog's optimistic-concurrency + CSRF double-submit + process-singleton-operator + CONFLICT→200-partial patterns. ✅
+
+**S6 · D3 web label drops the routes/-vs-app.py locus · INFO**
+D3's "22 routes" is the correct app-wide count but does not carry the "21 in `routes/` + `/healthz` in `app.py`" attribution V1's F1 added to the catalog. Numbers are right; only the locus nuance is absent. Optional annotation; no fix required.
+
+---
+
+## (c) Source re-derivations (the high-stakes checks)
+
+| # | Claim | Verdict | Evidence (live `8258f76`) |
+|---|-------|---------|---------------------------|
+| 1 | **Q23a** — `store.connect` closes in `finally` | **VERIFIED** | `store.py:16-19`: `try: yield connection / finally: connection.close()`. Production closes deterministically. |
+| 2 | **Q23b** — `LoomweaveAdapter._connect` closes in `finally` | **VERIFIED** | `loomweave_adapter.py:602-605`: `try: yield / finally: connection.close()`; comment `:598-599` "closed deterministically rather than left to GC". |
+| 3 | **Q23c** — test fixtures `with sqlite3.connect()` do NOT close | **VERIFIED** | `tests/loomweave_test_utils.py:10` and `:90`: `with sqlite3.connect(db_path) as connection:` — stdlib `sqlite3` `__exit__` commits the txn but does **not** close; connection left to GC. This is the true origin of the suppressed warning. **Q23 reasoning is SOUND.** |
+| 4 | **Q23 quote** — pyproject suppression attributes to "store-layer" | **VERIFIED** | `pyproject.toml:89-93`: comment "pre-existing **store-layer** connections surfaced only under --cov… Track the underlying leak separately." Q23's quote and its "inaccurate attribution" verdict both hold. |
+| 5 | **N+1** `1467→1479→1529-1550` per-entity connect | **VERIFIED** | `service.py:1467` `for entity in items:`; `:1479` `goals = self._goal_nodes_for_surface(entity.sei)`; `:1542` `with connect(self.db_path) as connection:` inside def `:1529`. One fresh connection per catalog surface. |
+| 6 | **`count(*)+1`** racy id sites (Q2) | **VERIFIED** | `service.py:2110` `_next_requirement_number`, `:2137` `_next_link_number`, `:2149` `_next_evidence_number` (plus 7 sibling `_next_*` at 2114-2147) all `select count(*) … + 1`. |
+| 7 | **`fail_under = 90`** branch gate | **VERIFIED** | `pyproject.toml:101` `branch = true`; `:106` `fail_under = 90`. The repeated "≥90% branch gate" claim holds. |
+| 8 | **One runtime dependency** ("thin / one runtime dep, mcp") | **VERIFIED** | `pyproject.toml:24-26` `dependencies = ["mcp>=1.2.0"]`. starlette/uvicorn/jinja2 are `[web]` extras (`:48-53`); coverage/mypy/pytest/ruff are dev-group. Exactly one runtime dep. |
+| 9 | **No import cycles** (`cycles:[]`) | **VERIFIED (now index-confirmed)** | Loomweave index — **absent during V1, now rebuilt** — `module_circular_import_list` → `cycles:[]`, confidence `resolved`. Independently reconfirms what V1 could only show via source+runtime. |
+| 10 | **Tracker tasks exist, open P3, subjects match** | **VERIFIED (w/ S2 nuance)** | `plainweave-706d80dc8e` "Preflight: project scope fans out … no cap or facts pagination" (open, P3) = Q4 exactly. `plainweave-3edcd19943` "Preflight: N+1 SQLite connections per scoped requirement" (open, P3) = Q3 pattern (not Q5's site — see S2). `plainweave-02376962ab` = semantic-similarity feature, correctly tagged out-of-scope in `06:192-193`. Note: docs cite bare hashes; full IDs carry the `plainweave-` prefix. |
+
+---
+
+## (d) Severity-consistency audit (check 4)
+
+Counts internally consistent (4/8/11 = 23 = Q1–Q23). No item's severity contradicts its own impact:
+- **Q1 High** — contract breach is concurrency-independent (any `OperationalError`: disk-full/locked/corrupt escapes the closed vocab), so High "before relying on a stated contract" holds, not merely a scaling artefact.
+- **Q3 High** — single-operator still runs MCP-agent reads concurrent with web-operator writes → the writer-lock ceiling is reachable in-scope. Defensible.
+- **Q4 High vs Q6 Medium** — both O(corpus) read amplification, but Q4 is **agent-facing, unpaginated, 3×-amplified, defaults to whole corpus**; Q6 is **operator-facing** (one human, naturally bounded). The High/Medium split tracks the threat surface, not a contradiction.
+- **Q9 High** — 3027 LOC / ~13 aggregates; dominant maintainability liability. Defensible.
+- **Q13 Medium** (web no-authN + `--host`) — the item most likely to be contested upward by a security review, but against the stated local-first/default-loopback scope and the "unguarded flag, latent on misconfig" framing, Medium is internally consistent. (`06` already routes the `--host` threat model to `/ordis-security-architect`.)
+
+## (e) Traceability sweep (checks 1 & 6) — no unbacked synthesis claims
+
+All 23 Q-items trace to a `02` concern or to live source; all map to exactly one initiative; no phantom/orphan items. The **only** synthesis-time claim found that the corrected evidence refutes is **S1** (doc 04's ResourceWarning clause) — the very class of error the mandate primed for ("like the ResourceWarning one already caught"). Q23 itself is a synthesis-time *new* item but is source-backed and corrective, not an over-claim. Report "Limitations" (`04:161-173`) is honestly hedged (static-only, index/live split, tests-read-not-rerun) — no hedge needs firming.
+
+---
+
+## (f) Punch-list (required + optional)
+
+1. **S1 (required, Medium)** — Rewrite or delete doc `04:150-152` caveat *(b)* so it is Q23-consistent (test-fixture origin, not a production/persistence leak; **not** corroboration of the connect-per-call concern). Eliminates the 04↔05 contradiction.
+2. **S2 (Low)** — Remap tracker `3edcd19943` from Q5 → **Q3/Q4** (it is the *preflight* N+1); note Q5 (intent_coverage N+1) has no dedicated tracker task. Update `05:87` and `06:191`.
+3. **S3 (Low/Info)** — Tighten Initiative A's mapping note: UNIQUE/PK→`CONFLICT` only (not all `IntegrityError`); present `BEGIN IMMEDIATE` and "retry-to-CONFLICT" as alternatives. (Defer to `/axiom-embedded-database`.)
+4. **S4 / S6 / S5 / S7 (Info, optional)** — one-clause note that 04's risk ranking is concentration-not-severity-order; annotate D3's web-route locus; soften "361" to "~360"; ensure "clean" reads as module-graph topology.
+
+None of items 2–4 block progression. Item 1 blocks **publication** of 04, not the downstream architect pass.
+
+---
+
+## SME Agent Protocol
+
+**Confidence Assessment — High (structural + the re-derived source claims) / Medium (deep remediation soundness).**
+- High on all four documents' contract conformance, cross-document consistency, traceability completeness, and the 10 source re-derivations in §c (read the spans directly, line-precise; `cycles:[]` now index-confirmed).
+- High on S1: it is a direct, verbatim contradiction between `04:150-152` and the Q23 correction I independently verified.
+- Medium on remediation **technical** soundness (S3): I reasoned about WAL/busy_timeout/BEGIN IMMEDIATE/error-mapping from first principles and found them sound, but a definitive SQLite-discipline adjudication is `/axiom-embedded-database`'s remit (already routed in 06).
+
+**Risk Assessment.**
+- *Low residual risk to the downstream `/axiom-system-architect` pass.* The synthesis conclusions (god object #1, persistence/read-path #2, DB-contract gap, surface coupling, web exposure) are all source-grounded and safe to build on.
+- *Publication risk if S1 ships:* doc 04 would assert, in its most-read section, a production "leak corroborates connect-per-call" claim that the same analysis (05/06) retracts — an internal contradiction a careful reader will catch and an over-claim the evidence refutes. Fix before publishing 04.
+- *Minor planning risk from S2:* a reader could think closing `3edcd19943` resolves the intent_coverage N+1 (it does not); harmless because Initiative A remediates both.
+
+**Information Gaps.**
+- I did **not** re-execute the test suite or benchmark the N+1 — consistent with the report's stated static-only limitation; performance claims rest on call-shape, which I confirmed structurally.
+- Loomweave fan-in integers (44/36/24/16/11) were not re-derived to exact values; the index is now rebuilt and queryable (cycles reconfirmed), so 04's "re-run `loomweave analyze` before treating integers as current-HEAD" caveat could now be **discharged** by a quick re-pull — out of synthesis-layer scope, noted for the orchestrator.
+- README "1.0/Production-Stable" vs code 1.1.0 / classifier "Development Status :: 5" is a pre-existing doc-vs-code drift owned by `01`, not re-adjudicated here.
+
+**Caveats.**
+- This is a **structural + evidence-fidelity** gate over the synthesis layer: contract conformance, cross-document consistency, traceability, and source-truth of the high-stakes and newly-introduced claims. It is **not** an architectural-soundness judgement — whether the god object is *the* right #1 target, or connect-per-call *the* right design, is `architecture-critic`'s call.
+- Per the adversarial mandate, findings are deliberately exhaustive to Info severity; a zero-finding pass over a freshly-synthesised layer would be an audit defect, not a clean bill of health. The verdict is **PASS-WITH-FIXES** — one required fix (S1), the remainder polish.
diff --git a/docs/handoffs/2026-06-28-lacuna-peer-facts-tour-demos-tasking.md b/docs/handoffs/2026-06-28-lacuna-peer-facts-tour-demos-tasking.md
new file mode 100644
index 0000000..10e9537
--- /dev/null
+++ b/docs/handoffs/2026-06-28-lacuna-peer-facts-tour-demos-tasking.md
@@ -0,0 +1,99 @@
+# Tasking prompt — Draw out Plainweave 1.1 peer-facts in the Lacuna tour (+ CLI parity)
+
+> Self-contained brief for an implementing agent. Plan it, then execute it. You have no
+> prior conversation context — everything you need is below or at the cited paths.
+
+## Mission
+
+Extend the Lacuna cross-member tour so it **demonstrates and regression-protects** the two
+peer-facts producers Plainweave shipped in 1.1 — which the current tour does NOT exercise.
+This adds two new capability cells to Lacuna's matrix: **`plainweave+wardline`** and
+**`plainweave+warpline`**. It also closes a real CLI/MCP parity gap in Plainweave (the
+producers are MCP-only today).
+
+This is a **harness-completeness / regression bet, not a correctness fix** (the producers
+are already contract- and unit-tested in the Plainweave repo) and **not a release blocker**.
+
+## Verified current state (2026-06-28)
+
+**Plainweave** (`/home/john/plainweave`) — 1.1 shipped two local-first, advisory producers
+with frozen `.v1` contracts (design spec:
+`docs/superpowers/specs/2026-06-27-peer-facts-wardline-warpline-design.md`; PDR-014):
+- `weft.plainweave.wardline_peer_facts.v1` — surfaces `.wardline/*-findings.jsonl` findings
+ (active/waived/baselined/judged, defect/non-defect) + resolved-or-unseen (scan-identity
+ manifest PRIMARY, path-set heuristic FALLBACK). MCP tool
+ `plainweave_wardline_peer_facts_list` (`src/plainweave/mcp_server.py:156`); a *summary*
+ also rides `plainweave dossier`/`doctor`, but the full envelope is MCP-only.
+- `weft.plainweave.requirements_enrichment.v1` — Plainweave's producer for Warpline's
+ reserved `enrichment.requirements` slot: per entity → `present|absent|unavailable` (+ item
+ array). MCP tool `plainweave_requirements_enrichment_get` (`mcp_server.py:162`).
+ **No CLI surface at all.**
+
+**Lacuna** (`/home/john/lacuna`) — the cross-member tour harness:
+- The Plainweave leg is `plainweave_intent()` (`tour/steps.py:665`), seeded by
+ `tour/plainweave_seed.py` (a deterministic **covered + uncovered** intent mix over the
+ specimen). It drives `plainweave --json` over the **CLI** and asserts four `pw-*`
+ capability facts over frozen anchors.
+- `docs/matrix.md` (auto-generated by `make tour` — never hand-edit) has `plainweave` and
+ `plainweave+loomweave`, but **no `plainweave+wardline` / `plainweave+warpline`**.
+- Data already present: a wardline-shaped `findings.jsonl` and a `.wardline/` with several
+ snapshots — but they are byte-identical and carry no scan-identity manifest, so a diff
+ resolves nothing.
+
+## Scope
+
+### Part A — Plainweave CLI parity (do FIRST; repo `/home/john/plainweave`)
+Add CLI subcommands that emit the producers' full `.v1` envelopes via `--json`, **reusing
+the existing `PlainweaveMcpSurface` / service logic** (do not duplicate it):
+- `plainweave wardline-peer-facts [--json] [--limit N --offset N]` → `weft.plainweave.wardline_peer_facts.v1`
+- `plainweave requirements-enrichment ... [--json]` → `weft.plainweave.requirements_enrichment.v1`
+
+Run the existing no-verdict structural validator over the CLI output too. (Alternative
+considered and rejected for uniformity: drive the tour via Lacuna's MCP-attachment path
+`tour/mcp_attachment.py` with no Plainweave change — note it in the design, prefer CLI
+parity since it closes a genuine product gap and keeps the tour leg uniform.)
+
+### Part B — Lacuna tour demos (repo `/home/john/lacuna`)
+Add capability demos that exercise the two new cells, following the `plainweave_intent()`
+pattern (frozen anchors, deterministic, self-seeding, advisory):
+- **`pw-requirements-enrichment`** (`plainweave+warpline`): over the existing covered+uncovered
+ seed, assert covered surface → `present` (non-empty items), uncovered → `absent`, and an
+ unresolvable/identity-gap ref → `unavailable` (**NOT `absent`** — the load-bearing
+ no-silent-clean invariant).
+- **`pw-wardline-peer-facts`** (`plainweave+wardline`): over Lacuna's `.wardline/` snapshots,
+ assert active + non-defect findings surface as advisory context; assert that an absent
+ `.wardline/` yields `unavailable` (**never clean**).
+- **resolved/unseen + scope honesty**: plant a deterministic scenario — a finding present in
+ an earlier snapshot, gone from a later one, with scan-identity manifests on both — so
+ `resolved_or_unseen` is exercised and a scope mismatch is honestly flagged. If out of scope
+ for a first cut, surface only active/suppressed/non-defect and **say so explicitly — do not
+ fake resolution**.
+- Update `docs/tour.md` narrative; regenerate `docs/matrix.md` via `make tour`.
+
+## Discipline (required)
+
+- **Plan before code**: brainstorming → writing-plans → TDD (the superpowers flow). Get owner
+ approval on the design; write the failing capability assertion first, then make it pass.
+- **Preserve the invariants end to end**: advisory only, **zero verdict vocabulary**,
+ local-first (no live peer calls), and **no-silent-clean** — `unresolved`/dead-binding →
+ `unavailable` never `absent`; absent `.wardline/` → `unavailable` never clean. The demos
+ must assert these, not just happy paths.
+- **Deterministic + frozen anchors** — no hardcoded hex SEIs; resolve by stable locator like
+ `plainweave_seed.py`. Demos repeat identically across `make verify` / `make tour`.
+- **Gates**: Plainweave — `make ci` green (mypy --strict, ruff, coverage ≥90%) and
+ `wardline scan . --fail-on ERROR` clean. Lacuna — its own `make verify` / `make tour` green.
+
+## Authority / process
+
+- Lacuna is a **sibling repo** — record the work Lacuna-side as a new PDR (the PDR-010
+ pattern) and note it in Plainweave's product workspace. Branch + commit per each repo's
+ convention. **Do not push public remotes without owner sign-off.**
+
+## Acceptance criteria
+
+1. `plainweave wardline-peer-facts --json` and `plainweave requirements-enrichment --json`
+ emit the correct frozen `.v1` envelopes (no-verdict-validated), reusing the MCP-surface logic.
+2. Lacuna `make tour` shows new `plainweave+wardline` and `plainweave+warpline` cells in
+ `docs/matrix.md`, driven by the two new demos with **substantive no-silent-clean assertions**
+ (covered→present, uncovered→absent, unresolved→unavailable; absent-findings→unavailable).
+3. Both repos green; the demos are deterministic across repeated runs.
diff --git a/docs/handoffs/2026-06-28-web-ux-design-review.md b/docs/handoffs/2026-06-28-web-ux-design-review.md
new file mode 100644
index 0000000..5bb6efa
--- /dev/null
+++ b/docs/handoffs/2026-06-28-web-ux-design-review.md
@@ -0,0 +1,209 @@
+# Design Review: Plainweave Operator Web UI
+
+> **Resolution (2026-06-28, same day):** all 9 Major findings + the folded Minors were
+> implemented in the working tree (26 web files + new `error.html`) and verified — `make ci`
+> green (ruff/mypy/pytest, 91.14% coverage) and `wardline` clean. Adopted site-kit's linen/ink/
+> brass tokens; drift badge now 5.11:1; all action buttons 44px; 320px reflow eliminated; global
+> pending badge; linked orphan titles; New-requirement button; visible auto-dismissing toast;
+> confidence chips. An adversarial review caught a regression (the global context processor could
+> double-fault and naked-500 the error page on a launch-time ctx failure) — fixed and covered by a
+> regression test. Before/after screenshots in the session scratchpad `shots/` (`desk_*` vs `after_*`).
+
+**Reviewer:** lyra-ux-designer / design-review · **Date:** 2026-06-28
+**Method:** Live Playwright drive (Chromium 1226) against a freshly-seeded instance
+(`src/plainweave/web`, Starlette + HTMX + Jinja2), plus static read of all
+templates/CSS/routes and a computed-style + contrast + target-size + reflow audit.
+Seed: 5 requirements (approved / draft / orphan mix), 2 goals (1 orphan), 2 proposed
+trace links (1 clean conf 0.82, 1 **drifted** conf 0.55), coverage 1/2 = 50%.
+Screenshots + raw audit JSON: session scratchpad `shots/`.
+
+## Summary
+
+**Overall Assessment:** Needs Work (visual layer), Strong (markup/interaction/a11y semantics)
+
+**Critical Issues:** 0 · **Major Issues:** 9 · **Minor Issues:** 6
+
+The single defining finding: **the markup is accessibility-literate and the interaction
+design is genuinely good, but `app.css` (19 lines) styles only ~20 classes while the
+templates reference ~25 more.** The result is a UI that is ~60% unstyled — and among the
+queue items the one component that *is* fully styled is the *drift warning*, producing an
+inverted hierarchy where the alarming state looks more finished than the normal one. None
+of this trips a WCAG **A** gate, there is no data-loss or security hole (CSRF is enforced),
+keyboard works, and focus is visible — so the severity is concentrated in **AA** +
+visual-hierarchy, not showstoppers.
+
+**Important context for the fix:** the operator UI hand-rolls its own minimal `app.css` and
+class vocabulary (`.banner--warn`, `.type-badge`, `.big-number`, its own amber `#c47b1a`) and
+references **nothing** from the Weft design system. Yet `site/vendor/site-kit` already ships a
+**framework-free** token + component CSS layer — `tokens/*.css` (`--brass-*` amber family,
+`--ink-*` text scale, `--linen-*` surfaces) and `components/components.css` (`.wf-banner--warn`,
+`.wf-badge--warn`, `.wf-fresh`, `.wf-enr`, plain CSS, no React) — whose vocabulary maps almost
+one-to-one onto the operator UI's unstyled classes. The operator tool's hand-picked palette
+(`#c47b1a` amber, pure white/black) also **drifts from the suite's linen theme** ("natural dyes
+on unbleached cloth"; `--brass-500 #AC8222`, `--linen-100` page). So the right remedy is largely
+"adopt the existing tokens," not "invent new CSS" — see Recommendation #1.
+
+---
+
+## Visual Design
+
+### Strengths
+- Body copy is `#1c1c1c` on white = **17:1** contrast — excellent legibility, 16px base.
+- The **drift state is exemplary**: amber left-border, warn-background card, "CODE DRIFTED"
+ badge, an explicit note, and the action morphs from "Accept" to "Accept…". It is the one
+ place where visual treatment, copy, and affordance all align.
+- Restraint is appropriate for an operator tool — no decorative noise.
+
+### Issues
+| Issue | Severity | Evidence | Recommendation |
+|-------|----------|----------|----------------|
+| ~25 referenced classes have **no CSS rule** — most UI renders unstyled | Major | Computed: `.banner--warn` color `rgb(28,28,28)` / bg transparent / border none / padding 0; `.queue-item` border 0 / padding 0; `.type-badge` no bg; `.queue-action-primary` = native UA button; `.muted` opacity 1; `.big-number` 16px/400 | Write component CSS for banners, cards, badges, primary buttons, muted/warn text |
+| `role="alert"` / `role="status"` banners have **zero visual emphasis** | Major | `.banner--warn` renders as plain black text on Intent page, error banners, edit-conflict | Give alerts a colored bg + left border + padding (site-kit `.wf-banner--warn`). NB: not a 1.4.1 fail — the meaning is in the text; the gap is visual *emphasis/hierarchy* |
+| **Inverted hierarchy on the KPI** — the Intent "50% 1/2" north-star number renders at body size while "Orphans" section `
`s dominate | Major | `.big-number` computed 16px/weight 400; see `desk_intent.png` | Make `.big-number` large/bold (e.g. 2.5rem 700); demote orphan-section headers |
+| **Review queue is a wall of text** — clean DRAFT/LINK cards have no separation; only the drifted card is bordered | Major | `desk_review.png`: `.queue-item` border/padding/margin all 0 | Card-style every queue item (border, padding, gap); reserve amber strictly for drift |
+| **Primary action has no visual primacy** — Approve/Accept look identical to Reject/Cancel | Major | `.queue-action-primary` bg `rgb(239,239,239)`, native `2px outset` border — same as every other button | Style primary as filled/high-contrast; secondary as outline/ghost |
+| **"CODE DRIFTED" badge fails contrast** | Major | White on amber `#c47b1a` = **3.39:1** for 11.2px bold (needs 4.5, SC 1.4.3) | Darken amber to ≥ `#a8650f` *or* use dark text on amber |
+| Secondary text not de-emphasised — IDs/version labels (`.muted`) render at full black | Minor→Major | `.muted` color `rgb(28,28,28)`, opacity 1; titles and their IDs are indistinguishable (`Audit log is append-only REQ-SEEDROOT-0001`) | Define `.muted { color:#6b6b6b }` |
+| Orphan/gap markers (`.warn` "none") not colored — the gap signal is invisible | Minor→Major | Corpus "none" cells + Goals "— no requirements ladder here" render as plain text | Color `.warn` (e.g. amber/red ink) — text alone is the only current cue |
+| Control boundaries faint | Minor | Toggle/table borders `#d9d9d9` = **1.41:1** (needs 3.0, SC 1.4.11) | Darken to ≥ `#767676` |
+
+---
+
+## Information Architecture
+
+### Strengths
+- Flat, legible top nav (Corpus / Review / Intent / Goals) with `aria-current="page"`.
+- Corpus filtering is well-modelled: search + Status + Orphans facets as `fieldset`/`legend`
+ groups; orphan facet (No goal / No code / Both) maps directly to the product's gap concept.
+
+### Issues
+| Issue | Severity | Evidence | Recommendation |
+|-------|----------|----------|----------------|
+| **"New requirement" is undiscoverable** — no affordance anywhere; reachable only by typing `/req/new` | Major | Corpus page has no create button; Goals page *does* have an inline create form (inconsistent) | Add a "New requirement" button to Corpus |
+| **Intent orphans are raw node IDs, not titles, and not links** | Major | `desk_intent.png`: lists `req-3 req-4 req-5`, `goal-2` — an operator can't tell which requirement, nor click to fix | Render titles + link each orphan to its detail page |
+| **Nav "pending review" badge is blank on every page except `/review`** | Major | Code-confirmed: `corpus`, `intent_dashboard`, `goals_page` omit `pending_count` from context; `base.html` `{% if pending_count %}` → falsy elsewhere | Compute `pending_count` in a shared context layer so the badge is global |
+| Intent shows empty section headers (`Orphans — code (0)`) | Minor | `desk_intent.png` | Hide zero-count sections |
+
+---
+
+## Interaction Design
+
+### Strengths
+- **Review-queue flow is genuinely good:** optimistic card removal, the pending-count badge
+ updates out-of-band, the SR live region announces e.g. *"Approved: Exports respect per-field
+ redaction rules. 3 items remaining in queue."*, and focus advances to the next primary action
+ (or the "All caught up" heading). Verified live.
+- **Reversibility is handled with care:** approve confirm ("*This cannot be undone — there is
+ no un-approve*"); drifted-link accept is a deliberate two-step ("*ratifies the link in its
+ current drifted state*"); reject **requires a typed reason** (inline error if blank).
+- **Edit-conflict recovery is excellent:** optimistic-concurrency clash shows your unsaved text
+ beside the current draft — no silent data loss.
+- CSRF tokens on every mutating form; HTMX bound to real `