diff --git a/.gitignore b/.gitignore index 8ece282..d4a9915 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ findings.jsonl # Superpowers brainstorming scratch (visual-companion mockups) .superpowers/ +.wardline \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 96659c5..6a7c2f1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -180,7 +180,7 @@ clean/allowed state. warpline facts are advisory and never gate. See the `warpline-workflow` skill for the full loop. - + ## Legis (git/CI + governance) Legis is the git/CI and governance layer of the Weft suite. Reach for it when a policy fires at the CI/git boundary and a change needs a *recordable* override or human sign-off, when you need governance attestations keyed to stable code identity (SEI), or when you need git/CI context — branches, commits, pull requests, check outcomes, and the Loomweave-bound rename feed — around the work. Enforcement is graded: agent-programmable policy cells decide whether a violation self-clears with an audit trail, is judged inline, or escalates to a human; every decision lands in an append-only, SEI-keyed audit trail that survives rename/move. diff --git a/CLAUDE.md b/CLAUDE.md index 96659c5..6a7c2f1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -180,7 +180,7 @@ clean/allowed state. warpline facts are advisory and never gate. See the `warpline-workflow` skill for the full loop. - + ## Legis (git/CI + governance) Legis is the git/CI and governance layer of the Weft suite. Reach for it when a policy fires at the CI/git boundary and a change needs a *recordable* override or human sign-off, when you need governance attestations keyed to stable code identity (SEI), or when you need git/CI context — branches, commits, pull requests, check outcomes, and the Loomweave-bound rename feed — around the work. Enforcement is graded: agent-programmable policy cells decide whether a violation self-clears with an audit trail, is judged inline, or escalates to a human; every decision lands in an append-only, SEI-keyed audit trail that survives rename/move. 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 ` - + diff --git a/src/plainweave/web/templates/_partials/edit_conflict.html b/src/plainweave/web/templates/_partials/edit_conflict.html index fcb8774..41b44b7 100644 --- a/src/plainweave/web/templates/_partials/edit_conflict.html +++ b/src/plainweave/web/templates/_partials/edit_conflict.html @@ -7,7 +7,7 @@

Your edits (not saved)

- +

Current draft

{{ current_title }}

{{ current_statement }}

Discard mine — start from current
diff --git a/src/plainweave/web/templates/_partials/link_accept_drifted_confirm.html b/src/plainweave/web/templates/_partials/link_accept_drifted_confirm.html index bec8ea0..b82d49d 100644 --- a/src/plainweave/web/templates/_partials/link_accept_drifted_confirm.html +++ b/src/plainweave/web/templates/_partials/link_accept_drifted_confirm.html @@ -5,6 +5,6 @@ {% include "_partials/csrf.html" %} - + diff --git a/src/plainweave/web/templates/_partials/link_reject_form.html b/src/plainweave/web/templates/_partials/link_reject_form.html index ed98b85..4c1274b 100644 --- a/src/plainweave/web/templates/_partials/link_reject_form.html +++ b/src/plainweave/web/templates/_partials/link_reject_form.html @@ -4,7 +4,7 @@ {% if error %}{% endif %} - - + + diff --git a/src/plainweave/web/templates/_partials/queue_action_result.html b/src/plainweave/web/templates/_partials/queue_action_result.html index e344036..b9e7c63 100644 --- a/src/plainweave/web/templates/_partials/queue_action_result.html +++ b/src/plainweave/web/templates/_partials/queue_action_result.html @@ -4,6 +4,12 @@ {% if remaining_count > 0 %}{{ remaining_count }} item{{ 's' if remaining_count != 1 }} remaining in queue. {% else %}Queue is now empty.{% endif %} +{# Visible toast mirrors the SR announcement above (M9). Host is aria-hidden; SR users hear #sr-status. #} +
+ {{ action_label }}: {{ item_desc }}. + {% if remaining_count > 0 %}{{ remaining_count }} item{{ 's' if remaining_count != 1 }} remaining in queue. + {% else %}Queue is now empty.{% endif %} +
{% if remaining_count > 0 %}{{ remaining_count }}{% endif %} {% if remaining_count == 0 %}
{% include "_partials/queue_empty.html" %}
diff --git a/src/plainweave/web/templates/_partials/queue_item_link.html b/src/plainweave/web/templates/_partials/queue_item_link.html index 9f07e49..6ff3a89 100644 --- a/src/plainweave/web/templates/_partials/queue_item_link.html +++ b/src/plainweave/web/templates/_partials/queue_item_link.html @@ -3,7 +3,7 @@ {% endif %}
LINK
-

{{ 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 %}

{% if item.drifted %} {% endif %} -
diff --git a/src/plainweave/web/templates/_partials/req_inline.html b/src/plainweave/web/templates/_partials/req_inline.html index 65c4605..646c88e 100644 --- a/src/plainweave/web/templates/_partials/req_inline.html +++ b/src/plainweave/web/templates/_partials/req_inline.html @@ -2,6 +2,6 @@

{{ statement }}

Full dossier → - +
diff --git a/src/plainweave/web/templates/base.html b/src/plainweave/web/templates/base.html index 8277c71..142b4d2 100644 --- a/src/plainweave/web/templates/base.html +++ b/src/plainweave/web/templates/base.html @@ -18,16 +18,38 @@ Intent Goals - 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. #}
{% block main %}{% endblock %}
+ + 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 %}

Corpus

+

New requirement

{% include "_partials/corpus_filter.html" %} - - - - - - {% include "_partials/corpus_rows.html" %} - -
RequirementStatusGoalCode links
+
+ + + + + + {% include "_partials/corpus_rows.html" %} + +
RequirementStatusGoalCode links
+
{% endblock %} diff --git a/src/plainweave/web/templates/error.html b/src/plainweave/web/templates/error.html new file mode 100644 index 0000000..47372ec --- /dev/null +++ b/src/plainweave/web/templates/error.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} +{% block title %}Error · Plainweave{% endblock %} +{% block main %} +
+

Something went wrong

+

{{ code }}

+

{{ message }}

+ {% if hint %}

{{ hint }}

{% endif %} +

Back to corpus

+
+{% endblock %} diff --git a/src/plainweave/web/templates/goals.html b/src/plainweave/web/templates/goals.html index 2fcb2f3..b970124 100644 --- a/src/plainweave/web/templates/goals.html +++ b/src/plainweave/web/templates/goals.html @@ -6,8 +6,8 @@

Goals

{% include "_partials/csrf.html" %} - - + +
    {% 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?"

    -{% for level, nodes in orphans.items() %} -

    Orphans — {{ level }} ({{ nodes|length }})

    -
      {% for n in nodes %}
    • {{ n.node_id }}
    • {% endfor %}
    +{% for section in orphan_sections %} +

    Orphans — {{ section.level }} ({{ section.items|length }})

    +
      {% for it in section.items %} +
    • {% if it.href %}{{ it.label }}{% else %}{{ it.label }}{% endif %}
    • + {% endfor %}
    {% endfor %} {% endblock %} diff --git a/src/plainweave/web/templates/requirement_detail.html b/src/plainweave/web/templates/requirement_detail.html index 746ff54..0429ba4 100644 --- a/src/plainweave/web/templates/requirement_detail.html +++ b/src/plainweave/web/templates/requirement_detail.html @@ -14,9 +14,9 @@

    {% if section.current_version %}{{ section.current_version.title }}{% elif s {% if section.active_draft %}

    Draft{% if section.current_version %} (proposed changes){% else %} (new — no approved version yet){% endif %}

    {{ section.active_draft.statement }}

    -

    Edit draft

    +

    Edit draft

    - +
    {% endif %} @@ -32,7 +32,7 @@

    Ladder to a goal

    {% endfor %} - +
    {% endif %} diff --git a/src/plainweave/web/templates/requirement_form.html b/src/plainweave/web/templates/requirement_form.html index 63eb23e..19b92da 100644 --- a/src/plainweave/web/templates/requirement_form.html +++ b/src/plainweave/web/templates/requirement_form.html @@ -7,6 +7,6 @@

    {% 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..57cdc5b 100644 --- a/src/plainweave/web/templates/review.html +++ b/src/plainweave/web/templates/review.html @@ -13,15 +13,20 @@

    Review queue

    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.