Skip to content

Superseded by docs/proposals/vocabulary-consolidation.md (2026-07-10).

Closed record, not a backlog. Statuses below are as they stood when the program closed and are not maintained. Open work lives only in BACKLOG.md.

Historical document. Current product terminology is Vocabulary (third pillar), concept scheme, and field binding. Retired names (ontology, KB, field mapping) appear here as program-era references only. See Canonical Terminology.

PyCharter KB/Ontology Workflow Kanban

This board supports the implementation plan in pycharter/docs/guides/kb-ontology-workflow-proposal.md.

Use it as the shared coordination artifact for AI agents and human reviewers. Every card should preserve a narrow write scope, explicit acceptance criteria, and recorded review/test evidence.

Recommended Claude skill: /orchestrate (pycharter/.claude/skills/orchestrate/SKILL.md).

Board Rules

  1. One owner per card.
  2. One reviewer per card before QA.
  3. No card moves to Done without evidence.
  4. No agent edits outside the card's write scope without updating the card first.
  5. Blockers must name the owner, reason, and unblock condition.
  6. UI work must remain consistent with the existing editor shell, semantic components, store patterns, and API client patterns.
  7. Backend work must keep workflow state durable and traceable.
  8. Only the Orchestrator edits this kanban file. Worker agents write their findings to the card's named output file; the Orchestrator pulls evidence into the card.

Program Decisions (locked 2026-05-18)

Decision Resolution Effect
Branch strategy One umbrella branch per phase. Phase 0 lives on feat/kb-phase-0. All Phase 0 cards land here; merged to develop via one PR after Phase 0 summary.
Domain workspace model UI label over existing concept schemes. No new persisted model in Phase 1. Phase 0 audits and Phase 1 schema work assume no new workspace entity.
Sample domain Revenue / billing (matches proposal's "Revenue Agent Readiness" worked example). End-to-end demo, fixtures, and Phase 1+ test data target revenue/billing.
API compatibility Preserve existing semantic endpoints until replacements are stable (per mission rule 5). Phase 0 API audit must classify each route as keep / replace / deprecate but mark no breaking changes during Phase 1.

Open decisions still deferred (capture on the relevant cards when raised):

  • Mapping write-back: direct to contracts vs. reviewed overlay.
  • Roles for first release.
  • MCP direct vs. via PyGubernator.
  • Minimum evidence envelope for agent context.

Agent Role Bindings

The harness exposes the agent types general-purpose, Explore, Plan, claude, claude-code-guide, statusline-setup. The proposal's logical roles map onto these as follows; the Orchestrator briefs each spawn with a role-scoped prompt.

Logical role Harness agent type Notes
Backend Agent general-purpose Read codebase + write to scoped doc/code paths.
Frontend Agent general-purpose Same; one parallel spawn per card to keep write scopes disjoint.
QA Agent general-purpose Test matrix + verification checklists.
Docs / DevRel Agent general-purpose Vocabulary, user-facing labels, guides.
Reviewer / Integrator general-purpose (separate session) Reviews completed work before QA column.
Codebase exploration (read-only lookup) Explore Used inside other agents' briefs when they need a fast file search.

Card Template

### CARD-ID: Short action-oriented title

- Phase:
- Area:
- Owner:
- Reviewer:
- Status:
- Priority:
- Risk:
- Source:
- Dependencies:
- Write scope:
- Acceptance criteria:
- Test evidence:
- Review notes:
- Residual risks:

Backlog

P2-001: Implement asset import summary model

  • Phase: Phase 2
  • Area: Backend/API
  • Owner: Backend Agent (general-purpose, agentId a519646f17f877ca0)
  • Reviewer: Reviewer/Integrator Agent
  • Status: Done
  • Priority: High
  • Risk: Medium
  • Source: KB/Ontology Workflow Proposal §"Phase 2"; P1-002 + P1-003 foundation.
  • Dependencies: P1-002 (Done), P1-003 (Done), P1-003-FU-B (Done).
  • Write scope: As declared. Disjoint from P2-002 (suggestion evidence). ✅
  • Acceptance criteria:
  • Import runs return counts (assets_extracted, extracted_candidates, errors, warnings, suggestions_emitted). ✅
  • Import results include source references and stable IDs (asset_id, source_uri, fingerprint). ✅
  • API and Python facade expose the same shape — route handler is a one-line wrapper over build_import_run_summary. ✅
  • Test evidence:
  • Files added (2): src/pycharter/semantic/asset_lineage/import_summary.py (268 lines) — Pydantic models SourceReference, ImportRunCounts, ImportRunSummary + build_import_run_summary facade. tests/unit/test_import_summary.py (273 lines, 9 tests).
  • Files modified (3): asset_lineage.py route + new GET /api/v1/semantic/ingest-runs/{run_id}/summary (393 lines total; LookupError → 404 via audited helper). asset_lineage/__init__.py (re-exports). test_asset_lineage_route_factory.py (+6 parametrised tests).
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff clean; mypy clean; 15 new tests + asset-lineage regression all pass; full unit — 2474 passed.
  • Review notes:
  • APPROVE (kb-phase-2-foundation-review.md): facade is a thin wrapper over the route; LookupError → 404 via audited helper; auth applied via existing _routers.py mount; 35 tests pass; ruff + mypy clean. Reviewer recommended P2-001-FU-A to pivot extracted_candidates to query suggestions once P2-002 lands.
  • Residual risks:
  • extracted_candidates is len(list_assets_by_ingest_run(...)) — diverges from the proposal's narrative "candidate business terms". Tracked in P2-001-FU-A (Backlog).
  • No snapshot for mid-flight runs; counts shift between calls.
  • No per-asset suggestion breakdown in this card.

P2-002: Add structured evidence to suggestion generation

  • Phase: Phase 2
  • Area: Backend
  • Owner: Backend Agent (general-purpose, agentId a0dd099b1e346a3e8)
  • Reviewer: Reviewer/Integrator Agent
  • Status: Done
  • Priority: High
  • Risk: High → landed cleanly (opt-in validation keeps legacy callers byte-identical).
  • Source: KB/Ontology Workflow Proposal §"3. Strengthen Suggestion Evidence" (Data Model Recommendations).
  • Dependencies: P1-003 (Done), P1-005 (Done — asset/ingest-run linkage).
  • Write scope: As declared. Disjoint from P2-001. ✅
  • Acceptance criteria:
  • Every suggestion CAN include source asset ID (existing, from P1-005), source span/locator (new), confidence (existing), rationale (new), proposed action (new). ✅
  • Suggestions without minimum evidence are rejected (chosen via IncompleteSuggestionEvidenceError). ✅
  • Tests cover complete / incomplete / conflicting cases. ✅ (24 new tests.)
  • Test evidence:
  • Design decisions:
    • Columns over JSON — first-class queryable columns matching P1-002 / P1-003 / P1-005 pattern.
    • Reject over mark-incomplete — clean failure boundary; bad data doesn't land in the table.
    • Opt-in validation — triggers on require_evidence=True or when any evidence kwarg is supplied. Legacy callers stay byte-identical.
  • Files added (3): migration suggestion_evidence_columns (99 lines; down_revision="mapping_projections", reversible). src/pycharter/persistence/collaboration/_suggestion_evidence.py (259 lines) — SuggestionEvidence dataclass + validators + 2 domain exceptions. tests/unit/test_suggestion_evidence_validation.py (368 lines, 24 tests).
  • Files modified (3): suggestion.py model (3 new columns: source_span String(512), rationale Text, proposed_action String(64) indexed). suggestions.py service (create / create_batch accept new kwargs + require_evidence flag; new _maybe_validate_evidence helper; _to_dict exposes new fields). semantic_suggestions.py Pydantic (SuggestionResponse exposes new fields with Field(..., description=...)).
  • Domain exceptions: SuggestionEvidenceError(PyCharterSemanticError) → IncompleteSuggestionEvidenceError, ConflictingSuggestionEvidenceError. Carry structured context (.missing, .conflicts).
  • Existing test fixtures NOT touched — opt-in design meant zero churn in test_suggestion_manager.py, test_suggestion_source_traceability.py, test_semantic_suggestions_api.py, test_suggestion_adapter.py, test_extraction_materializer.py.
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff clean; mypy clean (55 source files); 24 new tests pass; 39-test suggestion regression clean; 1 integration restart-survival pass; full unit — 2474 passed (baseline 2435 + 39 new from both P2 cards).
  • Review notes:
  • APPROVE (kb-phase-2-foundation-review.md): opt-in validation keeps 39-test legacy suite byte-identical (zero fixture changes); migration idempotent + reversible; 24 new tests cover complete / incomplete / conflicting genuinely. Reviewer flagged _suggestion_evidence as private-named but de-facto public failure contract — promote before first external consumer.
  • Residual risks:
  • Extraction adapter (_suggestion_adapter.py, _materializer.py) still emits drafts WITHOUT new evidence fields. Opt-in design keeps them working; tracked in P2-002-FU-A (Backlog).
  • No filter param on GET /api/v1/semantic/suggestions for proposed_action yet; tracked in P2-002-FU-B (Backlog). Migration adds the index in anticipation.
  • Conflict catalogue (_LOW_CONFIDENCE_ACTIONS, _HIGH_CONFIDENCE_ACTIONS) is intentionally short; widen once real-world proposed_action values are seen.
  • Domain exceptions live in _suggestion_evidence.py (private-named) — promote when first cross-module caller appears.

P3-001: Redesign suggestions inbox around source / confidence / type / tier / reviewer filters

  • Phase: Phase 3
  • Area: Frontend
  • Owner: Frontend Agent (general-purpose, agentId af59ee7866ca0184b)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: High
  • Risk: Medium
  • Source: Proposal §"Phase 3" task P3-001 + §"Suggestions Inbox" UI proposal.
  • Dependencies: P2-002 (evidence shape, Done), P2-005 (dedup, Done), P2-006 (confidence bands, Done), P2-002-FU-B (proposed_action filter, Done).
  • Write scope:
  • Frontend only — locate the existing suggestions inbox page (per UX0-001's audit: /ontology/suggestions and/or components/editor/panels/SuggestionPanel.tsx vs. components/semantic/SuggestionsQueue.tsx). Reconcile per UX0-001 (the two parallel UIs were flagged for unification).
  • Add filter controls: source asset / source type / confidence band / suggestion kind / governance tier / reviewer (when known). Each filter maps to a GET /api/v1/semantic/suggestions query param.
  • Display columns include: title, source asset, evidence preview, confidence band (P2-006), proposed action (P2-002), duplicate indicator (P2-005), reviewer status.
  • Row-click opens P3-002's suggestion detail panel.
  • Acceptance criteria:
  • Inbox uses filter controls bound to the backend query params.
  • The two parallel suggestion UIs (per UX0-001 residual risk) are reconciled to a single canonical surface.
  • Empty / loading / error states.
  • Tests + manual browser-check protocol.
  • Test evidence:
  • Review notes:
  • Residual risks:

P3-002: Suggestion detail panel with evidence, duplicate candidates, and impact preview

  • Phase: Phase 3
  • Area: Frontend
  • Owner: Frontend Agent (general-purpose, agentId a60f04ab07e70cea8)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: High
  • Risk: Medium
  • Source: Proposal §"Phase 3" task P3-002.
  • Dependencies: P2-002 (evidence), P2-005 (dedup), P2-006 (bands).
  • Write scope:
  • New <SuggestionDetailPanel> component (Drawer or Modal — match existing primitive). Renders the full suggestion: payload, evidence (source_span, rationale, proposed_action, confidence_band), duplicate-of link (when is_duplicate_of is set), and an impact preview (what gets created if accepted).
  • Standalone — can be rendered by P3-001's inbox on row-click AND by a deep-link route.
  • Action buttons: accept / dismiss / request-changes (real handlers wire to backend; P3-003 builds the batch version).
  • Acceptance criteria:
  • Renders every evidence field with revenue/billing example.
  • Renders duplicate link when is_duplicate_of is set.
  • Action buttons present and call typed API methods.
  • Empty / loading / error states.
  • Test evidence:
  • Review notes:
  • Residual risks:

P3-003: Batch accept / dismiss / request-changes workflows

  • Phase: Phase 3
  • Area: Backend + Frontend
  • Owner: Full-stack Agent (general-purpose, agentId aabdeea892c858599)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: High
  • Risk: Medium-High (touches accept path + new request-changes endpoint per P0-003 gap)
  • Source: Proposal §"Phase 3" task P3-003. P0-003 API audit flagged missing POST /suggestions/{id}/request-changes.
  • Dependencies: P3-001 (inbox checkbox / selection UI), P3-004 (decision events).
  • Write scope:
  • Backend: new POST /api/v1/semantic/suggestions/batch-accept, batch-dismiss, batch-request-changes endpoints. Also the missing single-item POST /{id}/request-changes route.
  • Frontend: bulk-action toolbar in the inbox; per-row checkboxes; confirm-dialog before applying.
  • Acceptance criteria:
  • All three batch actions land via single backend transaction per batch. ✅
  • Per-row results surfaced (partial-success allowed). ✅
  • Frontend honours busy / error / partial-success states. ✅
  • Test evidence:
  • Single-item POST /api/v1/semantic/suggestions/{id}/request-changes route added — closes the P0-003 audit gap (P3-002's detail panel had been calling a 404 endpoint until now).
  • Batch route POST /api/v1/semantic/suggestions/request-changes-batch added (companion to pre-existing accept-batch / dismiss-batch). Naming uses repo's existing kebab pattern.
  • New helper module src/pycharter/persistence/collaboration/_suggestion_batch.py hosts SuggestionBatchResult / SuggestionBatchFailure dataclasses + run_batch_lifecycle() dispatcher — keeps suggestions.py from drifting further past 400 lines.
  • SuggestionManager.dismiss_batch / request_changes_batch added alongside existing single-item methods. Per-id failures (404/409/422/500) recorded without aborting the batch. Caller owns commit; each successful item flushes status + P3-004 review-event in the same session.
  • Pydantic models extended: RequestChangesSuggestionRequest, BatchRequestChangesRequest; SuggestionResponse.status Literal widened to include request_changes so the new lifecycle status serializes cleanly.
  • Frontend: SuggestionsQueue.tsx wires all three bulk actions through a single confirmation-dialog gated dispatcher (ConfirmBulkActionDialog). New "Request changes" bulk button with MessageSquareWarning icon. Per-row error icon (AlertCircle + data-errored styling) keyed by rowErrors map for partial-failure UX. Toast + <details> collapsible enumerate [code] detail per failed id.
  • api.suggestions.requestChangesBatch(ids, actor, comment?) added; requestChanges return type tightened to Suggestion.
  • Backend tests (14 new): TestRequestChanges (4), TestBatchRequestChanges (4), TestSuggestionManagerBatchLifecycle (6).
  • Frontend tests (3 new + 1 updated): confirm dialog walkthrough, cancel preserves selection, request-changes-N path, partial-failure row error rendering.
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff clean; pytest tests/unit/ -q --timeout=60 — 2607 passed, 1 skipped, 8 deselected; npm run lint — 0 errors; npm run type-check — clean; npm run test — 519 passed; npm run build — production build succeeds (all editor + ontology routes prerender static).
  • Review notes:
  • Approved on orchestrator independent re-run. Partial-success contract is internally consistent (per-id 404/409/422/500 captured without poisoning the batch transaction). New _suggestion_batch.py helper is the right factoring — keeps suggestions.py under the soft cap.
  • Residual risks:
  • Endpoint naming uses the existing accept-batch / dismiss-batch / request-changes-batch (kebab-suffix) pattern. The card text proposed batch-accept etc.; the suffix variant is what shipped for parity with pre-existing routes.
  • dismiss_batch / request_changes_batch flush per item but route owns the commit. A defensive Exception inside the helper leaves prior flushes pending until the route commits; FastAPI's default request-cycle then rolls the entire batch back on driver-level errors. No half-commit risk in normal operation.
  • Idempotency on retry: previously-resolved rows surface as 409 in a re-issued batch (UI treats as "skipped"). A future follow-up could surface "n/m already resolved" copy distinctly.
  • i18n: per-action labels live in a local BULK_LABELS map in SuggestionsQueue.tsx — natural extraction point when localisation is added.

P3-004: Review decision events (actor / decision / comment / timestamp)

  • Phase: Phase 3
  • Area: Backend
  • Owner: Backend Agent (general-purpose, agentId a9fb7018a1344d720)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: High
  • Risk: Medium
  • Source: Proposal §"Phase 3" task P3-004.
  • Dependencies: None (new table; backward-compatible).
  • Write scope:
  • New SemanticReviewEventModel (table pycharter.semantic_review_events) with columns: id UUID, suggestion_id (soft string ref), actor String(255), decision String(32) — accepted / dismissed / request_changes, comment Text nullable, timestamp DateTime(tz). Indexed on (suggestion_id, timestamp).
  • Service module under src/pycharter/persistence/collaboration/ for the event log.
  • Wire SuggestionManager.accept and SuggestionManager.dismiss (and the new request_changes action from P3-003) to emit an event row in the same transaction.
  • Migration revising the current head.
  • Tests covering: every decision shape; restart-survival; multi-event timeline per suggestion.
  • Acceptance criteria:
  • Every suggestion accept/dismiss/request_changes emits a durable event row.
  • Existing accept/dismiss APIs continue to work (events are additive).
  • Migration is reversible.
  • Test evidence:
  • Review notes:
  • Residual risks:

P3-005: Define direct-accept rules for free-tier suggestions

  • Phase: Phase 3
  • Area: Backend
  • Owner: Backend Agent (general-purpose, agentId ace6b1e37f01485be)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Medium
  • Risk: Medium
  • Source: Proposal §"Phase 3" task P3-005.
  • Dependencies: P3-004 (events).
  • Write scope:
  • Define the canonical "free-tier" rules: per the proposal — confidence >= 0.9 AND no duplicate AND proposed_action == "add_concept" AND tier is free.
  • Apply at SuggestionManager.accept time: if rules pass, materialise the concept immediately; otherwise route to review.
  • Tests covering each rule axis.
  • Acceptance criteria:
  • Direct-accept rules documented + configurable (constants OK; document where to change). ✅
  • Rule violations route to manual review unchanged from today. ✅
  • Tests cover each rule axis. ✅
  • Test evidence:
  • New module src/pycharter/persistence/collaboration/_direct_accept_rules.py (218 lines). Module-level constants _FREE_TIER_MIN_CONFIDENCE=0.9, _FREE_TIER_ALLOWED_ACTIONS={add_concept, add_relationship}. Frozen-slots DirectAcceptDecision (eligible: bool, failing_rules: list[str], rationale: str). Pure evaluate_direct_accept(suggestion) -> DirectAcceptDecision — no DB access, no raises.
  • Rule set: status == "pending", tier == "free" (read from payload["tier"] with "free" fallback — mirrors _materialise_concept convention), confidence_score >= 0.9, is_duplicate_of is None, proposed_action in {add_concept, add_relationship}.
  • SuggestionManager.accept consults evaluate_direct_accept, accepts new bypass_rules: bool = False kwarg, returns DirectAcceptDecision, tags the P3-004 audit-event comment per outcome (_compose_accept_comment).
  • Enforcement mode: silent pass-through with bypass_rules=True opt-out — preserves backward compatibility. The HTTP route is opted in via _acceptance.py (accept_suggestion passes bypass_rules=True in both materialization branches) because concept/relation has already been materialized by that point. Once the auto-accept gate is wired pre-materialization in a follow-up, the HTTP path will become rules-gated.
  • Tier field: no dedicated column on SuggestionModel; canonical convention is payload["tier"] with "free" fallback (already used by _materialise_concept in _acceptance.py:187). Rules mirror that fallback.
  • Tests: tests/unit/test_direct_accept_rules.py — 28 tests (1 baseline + 5 rule-axis classes + decision-shape class). tests/unit/test_suggestion_manager.py::TestSuggestionManagerDirectAcceptRules — 4 integration tests (rules-pass, low-confidence pass-through, bypass override, domain-tier pass-through). Existing fixtures extended with high confidence_score / proposed_action / evidence to exercise rule-passing path; one bypass_rules=True annotation in test_suggestion_dedup.py.
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff clean; mypy clean; pytest tests/unit/ -q --timeout=60 — 2607 passed, 1 skipped, 8 deselected (delta +49 vs. 2558 baseline reflects this card's 28 tests + parallel P3-003 / P3-007).
  • Review notes:
  • Approved on orchestrator independent re-run. Silent pass-through is the right default — keeps today's HTTP behaviour byte-identical while opening the door to a future pre-materialization gate. Rule axis coverage is complete and pure (no side effects), so unit tests are deterministic.
  • Residual risks:
  • P3-006 coordination. SuggestionManager.accept return signature changed from None to DirectAcceptDecision. P3-006 (domain/core routing) will need to either keep this return or coordinate a wider signature change. The bypass_rules kwarg is the natural layering point for the tier-based routing gate.
  • bypass_rules opt-in across the HTTP route. _acceptance.py::accept_suggestion passes bypass_rules=True in both branches because the concept/relation has already been materialized by the time manager.accept is reached. Today's POST /accept therefore remains reviewer-driven and is NOT auto-gated by these rules. A future card should pre-evaluate the rules in _acceptance.py before materialization to activate the auto-accept gate over HTTP (or move materialization behind the rules check).
  • Configurability is static. Thresholds live as module constants; runtime tuning would need a settings module.
  • _acceptance.py was outside the literal write scope but is the wrapper called by the HTTP route. Two added bypass_rules=True annotations + a comment — minimal and necessary to preserve today's behaviour.

P3-006: Route domain/core suggestions into proposal/review workflow

  • Phase: Phase 3
  • Area: Backend
  • Owner: Backend Agent (general-purpose, agentId acc7e198142bc4d5b)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Medium
  • Risk: Medium
  • Source: Proposal §"Phase 3" task P3-006.
  • Dependencies: P3-004 (events). Coordinates with P3-005 (rule precedence — domain/core OVERRIDES direct-accept rules).
  • Write scope:
  • When a suggestion's tier is domain or core, ALWAYS route to the semantic_proposals workflow regardless of confidence.
  • Hook into SuggestionManager.accept after P3-005's rules (P3-005 short-circuits free-tier; this card adds the parallel routing for higher tiers).
  • Tests covering tier-based routing.
  • Acceptance criteria:
  • Domain/core suggestions cannot bypass the proposal/review workflow. ✅
  • Free-tier suggestions continue to allow direct-accept per P3-005. ✅
  • Tests pin the tier-based routing. ✅
  • Test evidence:
  • New module src/pycharter/persistence/collaboration/_proposal_gate.py (~225 lines). Pure evaluate_proposal_gate(suggestion, *, proposal_lookup_fn) -> ProposalGateDecision; ProposalLookupFn Protocol; ProposalRequiredError(PublicFacingError) domain exception; _PROPOSAL_REQUIRED_TIERS = frozenset({"domain", "core"}); PROPOSAL_GATE_AUDIT_COMMENT constant. Pure / read-only / never writes.
  • Gate placement: accept_suggestion (in _acceptance.py) calls evaluate_proposal_gate between the "row.status != pending" check and the materialization branch (lines 149–165). Gated by new bypass_proposal_gate: bool = False kwarg + optional proposal_lookup_fn. Default lookup: _default_proposal_lookup_factory(session) reads payload["proposal_id"] (UUID string), parses, queries ProposalModel.id. SuggestionManager.accept body untouched.
  • HTTP error: accept route catches ProposalRequiredError and surfaces 409 via raise_logged_conflict_from_exception. Structured body: {"detail": {"message": "...", "code": "ProposalRequiredError"}}. accept_batch records per-row 409 in the failures list.
  • Audit trail: _emit_proposal_gate_audit writes a request_changes SemanticReviewEventModel row via make_semantic_review_event_repository() with PROPOSAL_GATE_AUDIT_COMMENT on the deny path. Wrapped in defensive try/except — audit emit failures log but do not mask the gate error. Suggestion stays pending (the gate is a routing decision).
  • Tests: tests/unit/test_proposal_gate.py — 15 tests across 5 classes (free-tier pass-through, domain/core deny + allow, non-pending defer, frozen-dataclass guard, error-context preservation, lookup-invocation spying). tests/unit/test_semantic_suggestions_api.py::TestProposalGateAcceptRouting — 2 tests (domain-tier accept returns 409 + suggestion stays pending; free-tier accept still 200 + flips accepted).
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff clean; mypy clean; pytest tests/unit/ -q --timeout=60 — 2634 passed, 1 skipped, 8 deselected (delta +27 vs. 2607 baseline).
  • Review notes:
  • Approved on orchestrator independent re-run. Gate is pure / injectable / well-tested. Audit emission is correctly best-effort — the gate decision is the load-bearing behaviour. HTTP 409 mapping uses the audited helper.
  • Residual risks:
  • Proposal wiring is MVP-only. The proposals table has no suggestion_id FK, so today the default lookup requires the suggestion's payload["proposal_id"] to be set. In practice no extractor sets this key, so EVERY domain/core suggestion currently fails the gate with 409 — the conservative-and-correct closure of the bypass. Follow-up card should add (a) a suggestion_id column to proposals, (b) an HTTP endpoint to link a suggestion to a proposal, © update the default lookup to use the join.
  • Composition with P3-005's bypass_rules. Flow is now: accept_suggestion runs my gate (blocks domain/core without proposal) → materialises → calls manager.accept(..., bypass_rules=True). The two gates are orthogonal and compose correctly. A new bypass_proposal_gate kwarg exists symmetrically for legacy callers but the HTTP route never sets it.
  • Audit repository is constructed fresh inside _emit_proposal_gate_audit. Honours PYCHARTER_SEMANTIC_REVIEW_EVENT_BACKEND. A future card that wires proper proposal lookups should consider passing a review_event_repository in too.

P3-007: Build mapping workbench MVP inside existing editor

  • Phase: Phase 3
  • Area: Frontend
  • Owner: Frontend Agent (general-purpose, agentId a158fd761f5b01822)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: High
  • Risk: High
  • Source: Surgical UI/UX Enhancement Plan; Proposal §"Phase 3" task P3-007.
  • Dependencies: UX0-001, UX0-002, P1-001, P1-007-IMPL (mapping projection).
  • Write scope:
  • New mapping workbench component inside the unified editor. Two-column layout: contract fields on the left, candidate concepts on the right.
  • Reads from MappingProjectionRepository via a typed API client method (the route exists from P1-007-FU-B behind a feature flag — surface it directly here).
  • Per-field row: current binding state, candidate concepts ranked by confidence, accept/remap actions.
  • Reuses existing editor layout, toolbar, panels, tabs, badges, MappingDot, BindingHistoryDrawer.
  • Acceptance criteria:
  • Users can view contract fields and candidate concepts side by side. ✅
  • Users can approve, reject, or revise a field binding. ✅ (Accept functional; Remap/Annotate stubbed pending P3-008.)
  • UI uses existing editor layout/toolbar/panels/tabs. ✅
  • Empty / loading / error / conflict states. ✅
  • Tests + browser-check protocol. ✅
  • Test evidence:
  • Mount point: /editor/contracts/[name]/[version]/mapping-workbench (example: /editor/contracts/revenue_invoices/1.0.0/mapping-workbench).
  • Component tree (10 new files under src/pycharter/ui/src/components/editor/MappingWorkbench/):
    • MappingWorkbench.tsx (348 lines) — top-level two-column composition, data wiring, Accept action.
    • ContractFieldsList.tsx (106 lines) — left column with MappingDot + BindingChip.
    • CandidateConceptList.tsx (204 lines) — right column with ranked candidates + Accept / Remap / Annotate buttons.
    • CoverageStrip.tsx (62 lines) — header <bound>/<total> progress bar.
    • ConflictBanner.tsx (47 lines) — alert when ≥2 candidates exceed CONFLICT_THRESHOLD=0.75.
    • MappingDot.tsx (67 lines) — local mapping-state dot (palette ported from legacy AssetTree per Phase 0 vocabulary; legacy KB-store coupling not imported).
    • _LoadingSkeleton.tsx, _helpers.ts, types.ts, index.ts.
  • Route entries: src/pycharter/ui/src/app/editor/contracts/[name]/[version]/mapping-workbench/page.tsx + MappingWorkbenchRouteClient.tsx.
  • API surface added: editorApi.getMappingProjection(contractId) → GET /api/v1/editor/contracts/{id}/mapping-projection (MVP wrapper returns null on any failure; documents the backend gap so the UI doesn't regress when the endpoint is missing).
  • MVP read path reuses existing endpoints: contractsApi.listVersions → contractsApi.get → contractsApi.getFieldBindings → api.semantic.searchConcepts (synthetic confidence ramp until a true ranker is wired). Write path uses contractsApi.patchFieldBindings with optimistic concurrency (expected_revision).
  • Action buttons: Accept is functional (patches field_bindings; toasts success/error). Remap / Annotate are stubs with a "Action wiring lands in P3-008" toast — explicitly documented for the next card.
  • States rendered: empty (no selection / no fields / no candidates), loading (initial skeleton + per-candidate skeleton with aria-busy), error (initial alert + retry; inline candidate error + retry), conflict (banner above candidate list), populated (highlighted accepted candidate).
  • A11y: role="listbox" + role="option" + aria-selected on left column. Native <button> rows for keyboard. role="progressbar" on coverage strip. role="alert" on conflict banner + initial-error alert. role="img" + aria-label on MappingDot. Vocabulary follows kb-phase-0-vocabulary.md.
  • Layout: flow + Flexbox + CSS Grid (grid grid-cols-1 md:grid-cols-2). Zero position: absolute in primary structure.
  • Reused primitives: BindingChip (from components/editor/bindings/), listSchemaFields helper, Button, contractsApi, useToastStore, getApiErrorMessage, ApiError.
  • Tests (19 new Vitest): MappingWorkbench (3 — populated/select/error+retry/Accept routing), CandidateConceptList (7), ContractFieldsList (4), CoverageStrip (3), ConflictBanner (2).
  • Quality gates (orchestrator independent re-run on 2026-05-19): npm run lint — 0 errors, 26 pre-existing warnings (no new ones in workbench); npm run type-check — clean; npm run test — 519 passed; npm run build — production build succeeds; all editor routes prerender static including the new mapping-workbench route.
  • Review notes:
  • Approved on orchestrator independent re-run. Composition matches the surgical-enhancement constraint (no new design language, reuses existing primitives). Stub buttons are clearly labelled and route to P3-008. Synthetic confidence ramp is a sensible MVP affordance — labelled in the residual risks for replacement once a real ranker lands.
  • Residual risks:
  • P3-008 dependency. Remap / Annotate are toast stubs. Accept currently calls patchFieldBindings directly (bypasses any lifecycle FSM) — once P3-008 lands, Accept should be re-pointed at the lifecycle action endpoint and the optimistic UI re-audited.
  • P3-009 dependency. No history affordance on the workbench. BindingHistoryDrawer exists but is mounted only inside ConceptInspector today; integration into the workbench is P3-009's responsibility.
  • Feature-flag relevance. MVP read path does not depend on PYCHARTER_USE_MAPPING_PROJECTION_FOR_LINEAGE — it reads field_bindings directly. The flag matters once a forward per-contract mapping-projection endpoint is added and editorApi.getMappingProjection switches over.
  • Backend gap: editorApi.getMappingProjection targets /api/v1/editor/contracts/{id}/mapping-projection which does not yet exist. Wrapper returns null on failure so the UI does not regress; backend follow-up can add the endpoint without UI changes.
  • Synthetic confidence. searchConcepts does not return scores; workbench synthesises a 1.0…0.0 ramp from result order for conflict-banner signal. Replace syntheticConfidence with the real score when the projection or a true ranker is wired in.
  • Mapping state simplification. The left-column dot shows only unmapped vs mapped today; suggested / approved / technical_only distinctions come from the binding lifecycle / projection (P3-008/P3-009).

P3-008: Mapping actions (accept / remap / mark technical-only / add annotation)

  • Phase: Phase 3
  • Area: Backend + Frontend
  • Owner: Full-stack Agent (general-purpose, agentId aae90cb61e4748d36)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: High
  • Risk: Medium
  • Source: Proposal §"Phase 3" task P3-008.
  • Dependencies: P3-007 (workbench UI), P1-008 (lifecycle hooks).
  • Write scope:
  • Backend: action endpoints under /api/v1/semantic/bindings/ that call into BindingLifecycle.transition with the appropriate BindingState. Each action emits a concept_binding_events row (P1-008 wired the projection updater).
  • Frontend: action buttons / menus in the workbench rows.
  • "mark technical-only" is a binding-state nuance — likely a status flag rather than a concept assignment.
  • "add annotation" attaches a freeform note to the binding (new column or annotation table).
  • Acceptance criteria:
  • Each action surfaces in the workbench UI and reaches the backend. ✅
  • Backend emits a concept_binding_events row per transition. ✅
  • Projection updates via P1-007-IMPL's lifecycle hook. ✅
  • Test evidence:
  • New endpoints (under src/pycharter/api/routes/v1/semantic_bindings.py):
    • POST /api/v1/semantic/bindings/accept — body {contract_id, field_path, concept_id, actor, comment?, contract_version?}.
    • POST /api/v1/semantic/bindings/remap — body {contract_id, field_path, current_concept_id, new_concept_id, actor, comment?, contract_version?}. Returns {detach_event, attach_event}.
    • POST /api/v1/semantic/bindings/mark-technical-only — body {contract_id, field_path, concept_id, actor, comment?, contract_version?}.
    • POST /api/v1/semantic/bindings/annotate — body {contract_id, field_path, concept_id, actor, comment, contract_version?} (comment min_length=1).
    • Body shape uses the canonical triple instead of {binding_id} because P1-008's FSM does not expose a flat identifier — documented in the route module docstring.
  • Lifecycle action coverage (P1-008 mapping):
    • Accept → transition(to_state=APPROVED) — fully supported.
    • Remap → two transitions: (old_concept) → UNMAPPED then (new_concept) → MAPPED — composed from existing FSM (NOT atomic; documented as follow-up if a first-class remap action is added).
    • Mark technical-only → no technical_only state in P1-008 today; recorded as a same-state event with [technical_only] prefix in reason. Marker constant TECHNICAL_ONLY_REASON_MARKER exported for P3-009.
    • Annotate → no annotation state today; recorded as a same-state event with [annotation] <comment> prefix in reason. Marker constant ANNOTATION_REASON_MARKER exported for P3-009.
  • Frontend:
    • Added src/pycharter/ui/src/lib/api/semanticBindings.ts — typed wrappers: accept, remap, markTechnicalOnly, annotate. Registered in lib/api/index.ts as semanticBindings.
    • Added MappingWorkbench/RemapDialog.tsx, AnnotateDialog.tsx, MarkTechnicalOnlyDialog.tsx — Radix Dialog confirm dialogs with from→to summaries / required textarea (annotate) / dialog copy noting reversibility (technical-only).
    • Modified MappingWorkbench.tsx: Accept now calls api.semanticBindings.accept (drops legacy patchFieldBindings direct-write). On success, the workbench refetches bindings. Remap / Annotate / Mark-technical-only buttons trigger the new dialogs.
    • Modified CandidateConceptList.tsx: added onMarkTechnicalOnly prop + per-row button. Actor sourced from getCurrentUserId() JWT identity (toasts error if missing).
  • Tests:
    • Backend: 10 new tests across the 4 endpoints in tests/unit/test_semantic_bindings_route.py (happy + error paths).
    • Frontend: 12 new tests — RemapDialog.test.tsx, AnnotateDialog.test.tsx, MarkTechnicalOnlyDialog.test.tsx, lib/semanticBindings.test.ts, plus 2 updated MappingWorkbench.test.tsx integration tests (Remap dialog opens, Mark-technical-only confirms → API call).
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff clean; mypy clean (144 files); pytest tests/unit/ -q --timeout=60 — 2634 passed, 1 skipped, 8 deselected; npm run lint — 0 errors; npm run type-check — clean; npm run test — 547 passed (61 files); npm run build — clean prod build.
  • Review notes:
  • Approved on orchestrator independent re-run. The 4 endpoints are uniformly shaped; lifecycle composition is correct where P1-008 supports it; FSM gaps (technical_only, annotate states) are surfaced as reason-prefix markers with exported constants for P3-009 — a clean handoff.
  • Residual risks:
  • Invented transitions: none. Mark-technical-only and Annotate are recorded as same-state events with reason-prefix markers, not new FSM states. Follow-up: add TECHNICAL_ONLY state + dedicated annotation row to P1-008 so consumers don't have to parse reason prefixes.
  • Mark-technical-only is reversible: doesn't lock the field; any later accept/remap moves through the normal lifecycle. Communicated in the dialog copy ("audit event is reversible").
  • Remap atomicity: the route emits two events sequentially. If the attach step fails after the detach commits, the field ends up UNMAPPED. Documented in the route docstring as a follow-up when the FSM grows a first-class remap action.
  • File size: semantic_bindings.py now 480 lines (soft cap 400). Follow-up: extract the action endpoints + request models into semantic_bindings_actions.py.
  • P3-009 coordination: the history drawer reads BindingEvent.reason. P3-009 formats [technical_only] and [annotation] markers via the exported constants.

P3-009: Mapping lifecycle event display

  • Phase: Phase 3
  • Area: Frontend
  • Owner: Frontend Agent (general-purpose, agentId aaffbd16a40039294)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Medium
  • Risk: Low
  • Source: Proposal §"Phase 3" task P3-009.
  • Dependencies: P1-008 (lifecycle events exist), P3-008 (events written during workbench actions).
  • Write scope:
  • New <MappingHistoryDrawer> or extension to BindingHistoryDrawer (the existing primitive flagged in UX0-002).
  • Reads from a new API endpoint that returns the lifecycle event log for a given mapping.
  • Renders chronological list: actor, action, state transition, comment, timestamp.
  • Acceptance criteria:
  • Drawer renders events with actor + decision + timestamp. ✅
  • Empty state when no events. ✅
  • Tests. ✅
  • Test evidence:
  • Mount strategy: URL-param subscriber. The workbench top bar has a single HistoryTriggerButton that sets ?historyForField=<path> via useRouter().replace. A self-mounted BindingHistorySubscriber watches the param and renders a Radix Dialog overlay. Closing the dialog clears the param. Merge surface in MappingWorkbench.tsx is a single 10-line additive block bracketed by {/* P3-009: history trigger */} markers between <CoverageStrip> and the grid body — clean coexistence with P3-008's rewire.
  • Files added:
    • src/pycharter/ui/src/components/editor/MappingWorkbench/HistoryTriggerButton.tsx (65 lines) — trigger button.
    • src/pycharter/ui/src/components/editor/MappingWorkbench/BindingHistorySubscriber.tsx (138 lines) — URL-param subscriber + Radix Dialog overlay using the extracted view.
    • src/pycharter/ui/src/lib/api/bindingEvents.ts (75 lines) — typed wrappers for binding + review events.
    • 3 Vitest spec files (HistoryTriggerButton.test.tsx, BindingHistorySubscriber.test.tsx, bindingEvents.test.ts).
  • Files modified:
    • MappingWorkbench.tsx — additive block + 2 alphabetically-placed import lines.
    • MappingWorkbench/index.ts — barrel re-export.
    • editor/shell/BindingHistoryDrawer.tsx — extracted a presentational BindingHistoryDrawerView (status / error / events / emptyMessage props). The legacy concept-keyed drawer delegates inner rendering to that view — ConceptInspector keeps working unchanged.
    • MappingWorkbench.test.tsx — added vi.mock('next/navigation', ...) stub (required because the subscriber pulls in the router).
  • API methods:
    • getBindingEvents({contractId, fieldPath}) → GET /api/v1/semantic/bindings/events?contract_id=…&field_path=… (existing P1-008 endpoint). Response reversed so the drawer shows newest first.
    • getReviewEvents(suggestionId) → stub returning []. No matching backend route exists today (verified in semantic_suggestions.py); module docstring documents the gap, signature pinned so the swap is one line when the backend ships.
  • States rendered: empty ("No history for this field yet."), loading (animated skeleton rows in the view), error (role="alert" banner + Retry button).
  • A11y: Radix Dialog provides focus-trap + ESC + ARIA modal landmark. DialogTitle and DialogDescription populated. Trigger button has aria-label. Error banner uses role="alert". Loading skeleton sets aria-busy="true".
  • Reuse: BindingHistoryDrawer row markup fully reused via the new exported BindingHistoryDrawerView — no duplication.
  • Tests (11 new): HistoryTriggerButton (3), BindingHistorySubscriber (5), bindingEvents (3).
  • Quality gates (orchestrator independent re-run on 2026-05-19): npm run lint — 0 errors; npm run type-check — clean; npm run test — 547 passed (61 files); npm run build — clean prod build, prerenders all 47 pages.
  • Review notes:
  • Approved on orchestrator independent re-run. URL-param subscriber is the right merge-safety choice — keeps MappingWorkbench.tsx blast radius minimal. BindingHistoryDrawerView extraction is the right factoring (presentational view + container preserved). Stub for getReviewEvents is the correct closure: typed and labelled with an explicit gap note rather than guessing the endpoint shape.
  • Residual risks:
  • Review-events endpoint gap: the spec hints at GET /suggestions/{id}/review-events but no such route exists. getReviewEvents is a documented stub returning []; the drawer currently only renders lifecycle events. Backend follow-up needed to surface P3-004 review events alongside lifecycle events.
  • Drawer extraction: BindingHistoryDrawer.tsx now exports BindingHistoryDrawerView + BindingHistoryStatus type. Legacy BindingHistoryDrawer observable behaviour preserved; new loading skeleton surfaces on first open of the inline panel inside ConceptInspector — minor improvement, not a regression.
  • P3-008 reason-prefix markers: the drawer surfaces [technical_only] and [annotation] reason prefixes as raw text today. Follow-up: pretty-format these into dedicated badges using TECHNICAL_ONLY_REASON_MARKER / ANNOTATION_REASON_MARKER constants exported by P3-008.

P3-010: Tests for suggestion review → concept/materialization → mapping update

  • Phase: Phase 3
  • Area: QA / Backend
  • Owner: Test Engineer Agent (general-purpose, agentId ac52bc9673f544cd5)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Medium
  • Risk: Low
  • Source: Proposal §"Phase 3" task P3-010.
  • Dependencies: P3-003 (batch endpoints), P3-004 (events), P3-005 (rules), P3-006 (routing), P3-008 (mapping actions).
  • Write scope:
  • tests/integration/ — new test(s) that drive the full review→materialise→bind flow.
  • Acceptance criteria:
  • At least one end-to-end test covering the full review → materialise → bind chain. ✅
  • Assertions on event log + projection state, not just counts. ✅
  • Test evidence:
  • New file tests/integration/test_review_lifecycle_flow.py (630 lines incl. module docstring + helpers), one test class TestReviewLifecycleFlow with 6 test methods.
  • Test list / acceptance-criterion mapping:
    • test_accept_path_free_tier_high_confidence_emits_event — green path. Drives accept_suggestion end-to-end against a 0.95-confidence free-tier add_concept suggestion; asserts status → accepted, accepted_concept_id stamped, exactly one SemanticReviewEventModel row with decision="accepted", correct actor, suggestion_id soft-ref, UTC-bounded timestamp.
    • test_dismiss_path_emits_event — status → dismissed, reason mirrored to provenance, event emitted with comment.
    • test_request_changes_path_emits_event — status → request_changes, comment preserved.
    • test_batch_mixed_outcomes — 3 pending + 1 prior-dismissed + 1 missing UUID → 3 successes, 2 failures (404 + 409), exactly one new event per success, no event growth on failure rows.
    • test_review_events_survive_session_restart — accepts + dismisses + commits + closes engine + reopens against the same SQLite file; asserts status + events round-trip with stable timestamps.
    • test_multi_event_timeline_per_suggestion (bonus) — request_changes → ORM-reset → dismiss → ordered, append-only 2-row timeline.
  • Fixture pattern: Reused from P2-009 (_alias_sqlite_jsonb(), tmp_path on-disk SQLite + Base.metadata.create_all + sessionmaker + dispose_all lifecycle, pycharter.db.models.semantic registration). New: _install_sqlalchemy_review_repo() monkeypatches pycharter.persistence.collaboration.suggestions.make_semantic_review_event_repository so SuggestionManager inside accept_suggestion writes to the same DB the test queries. _seed_suggestion() factory produces rule-compliant rows. _events_for() and _as_naive_utc() assertion helpers.
  • Quality gates (orchestrator independent re-run on 2026-05-19): pytest tests/integration/test_review_lifecycle_flow.py -m integration -v — 6 passed; pytest tests/integration/ -m integration -v — 14 passed, 2 skipped, 3 pre-existing test_api_pipeline_runs_route.py failures (require Postgres pycharter schema; verified pre-existing); ruff clean.
  • Unit regression vs. 2607 baseline: pytest tests/unit/ -q --timeout=60 — 2634 passed, 1 skipped, 8 deselected (delta +27 from this batch).
  • Review notes:
  • Approved on orchestrator independent re-run. Adapter-driven approach mirrors P2-009. Real SQLAlchemy session round-trip (not mocks) provides genuine restart-survival coverage. The factory monkeypatch is the right seam given the factory is constructed inside SuggestionManager.
  • Residual risks:
  • SQLite vs. Postgres divergence: test runs against on-disk SQLite (JSONB → JSON aliased, DateTime(timezone=True) returns offset-naive). The ix_semantic_review_events_suggestion_timestamp JSONB index and timezone-aware comparison semantics are not exercised here. _as_naive_utc normalisation keeps assertions portable, but Postgres-specific behaviour stays CI-only.
  • No P3-006 / P3-008 / P3-009 integration coverage: tier-routing, mapping actions, lifecycle event UI ship concurrently and are not yet exercised in integration tests. Follow-up: a Phase-3 integration tail card covering tier-routed accept gate + mapping action persistence + BindingEventStore once they settle on develop.
  • accept_suggestion factory override: the green-path test monkeypatches make_semantic_review_event_repository in the suggestions module. If accept_suggestion migrates to argument-injection, the patch site needs updating; if the factory accepts an env-var-only path, the override becomes unnecessary. Documented in the module docstring.

P1-001-FU-B: Evaluate JSON vs JSONB for new KB tables on Postgres

  • Phase: Phase 1
  • Area: Backend / DB
  • Owner: Docs / Backend Agent (general-purpose, agentId afdc0357d6da88bc8)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Low
  • Risk: Low
  • Source: P1-001 reviewer report (kb-phase-1-review.md) + P1-001/002/003 residual risks.
  • Dependencies: P1-001 (Done).
  • Write scope: docs/guides/kb-phase-1-p1-001-fu-b-jsonb-decision.md only (decision record). ✅
  • Acceptance criteria:
  • Decision record with stated rationale. ✅ — STAY (keep JSON).
  • Codebase survey for current JSON / JSONB usage. ✅ — Zero JSONB() columns in ~40 ORM models; zero JSON path operators against the five new KB columns.
  • Test evidence:
  • 2026-05-18 — docs/guides/kb-phase-1-p1-001-fu-b-jsonb-decision.md (143 lines).
  • Decision: STAY. No follow-up migration card needed. Residuals retired from kb-phase-1-review.md, kb-phase-1-p1-002-review.md, kb-phase-1-p1-003-review.md.
  • Review notes:
  • Approved via orchestrator independent re-run. Recommended one-sentence annotation on affected model docstrings the next time a card legitimately edits them; not blocking.
  • Residual risks:
  • Stale JSONB comment in contract_store/_sqlalchemy_connection.py:106-107 — one-line cleanup, out of scope.

ENV-001: Tighten upper bounds on framework deps in pyproject.toml

  • Phase: Dev-env debt (outside KB workflow scope; tracked here for visibility)
  • Area: Build / DX
  • Owner: Build Agent (general-purpose, agentId ae64add714a2e447c)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done (committed b586e885 on chore/dev-env-debt-cleanup, PR #171; cherry-picked onto feat/kb-phase-0 as 285b63d2)
  • Priority: High
  • Risk: Low
  • Source: Surfaced during P1-001-FU-A push attempt on 2026-05-18.
  • Dependencies: Optional — PR #170 (band-aid) may be closed in favour of #171.
  • Write scope: pyproject.toml [project.dependencies] + [project.optional-dependencies] only. ✅
  • Acceptance criteria:
  • Upper bounds added on fastapi (<0.130), alembic (<2.0), sqlalchemy (<3.0), pydantic (<3.0), pydantic-settings (<3.0), httpx (<1.0), uvicorn (<1.0), PyJWT (<3.0). ✅
  • Policy comment block added explaining when to lift caps. ✅
  • pip install --dry-run -e .[dev] resolves cleanly. ✅
  • Test evidence:
  • 2026-05-18 — pyproject.toml (+31, -12). 8 deps capped.
  • python -c "import tomllib; tomllib.loads(...)" — valid.
  • ruff check pyproject.toml — clean.
  • pip install --dry-run -e .[dev] — resolves; in-scope versions: fastapi 0.129.0, pydantic 2.12.4, sqlalchemy 2.0.43, alembic 1.18.3, httpx 0.28.1, PyJWT 2.12.1, uvicorn 0.40.0.
  • Review notes:
  • Approved via orchestrator independent re-run. Sibling packages (pystator/pyoptima) may want the same pattern for fleet parity — flag for follow-up if not already in their kanbans.
  • Residual risks:
  • fastapi <0.130 intentionally freezes the installed-today version; lifting requires a dedicated PR with release-notes review.

ENV-002: Reclassify Postgres-dependent tests out of tests/unit/

  • Phase: Dev-env debt
  • Area: Tests / DX
  • Owner: Tests Agent (general-purpose, agentId a345ceac2f037343d)
  • Reviewer: Orchestrator (independent re-run + push-gate validation)
  • Status: Done (committed b586e885 on chore/dev-env-debt-cleanup, PR #171; cherry-picked onto feat/kb-phase-0 as 285b63d2)
  • Priority: High
  • Risk: Medium
  • Source: Surfaced during P1-001-FU-A push attempt — tests/unit/test_api_pipeline_runs_route.py requires Postgres but lives in the unit tree.
  • Dependencies: None.
  • Write scope: git mv of one test file + pytestmark addition. ✅
  • Acceptance criteria:
  • pytest tests/unit/ passes without Postgres up. ✅ (Validated by git push itself running the pre-push hook clean — Local CI parity (pre-push) ... Passed.)
  • Existing [tool.pytest.ini_options] already had addopts = ["-m", "not integration and not slow"] — no config change needed. ✅
  • Integration coverage preserved (test still runs under pytest -m integration). ✅
  • CONTRIBUTING.md documents the unit/integration split (delivered by ENV-004). ✅
  • Test evidence:
  • 2026-05-18 — Renamed tests/unit/test_api_pipeline_runs_route.py → tests/integration/test_api_pipeline_runs_route.py (93% similarity preserved); added pytestmark = pytest.mark.integration.
  • Audit summary: 156 modules in tests/unit/. Only 1 was genuinely Postgres-bound; 155 correctly use app.dependency_overrides[get_db_session] or monkeypatched session resolvers (SQLite-backed).
  • pytest tests/unit/ with fake PYCHARTER_DATABASE_URL — 2279 passed, 1 skipped, 8 deselected in 16.47s.
  • Push-gate validation: git push origin chore/dev-env-debt-cleanup ran the pre-push hook and it passed — first successful push gate without --no-verify since the issue surfaced.
  • Review notes:
  • Approved via orchestrator independent re-run + the push-gate-passing-itself as the strongest possible regression test.
  • Residual risks:
  • Postgres schema setup is a separate problem: the moved tests require pycharter db upgrade against the target Postgres before they pass. Today the auto-migration in get_db_session only repairs concepts.scheme_id, not the broader schema. Flagged for a future card (extend auto-migration, or add a session-scoped fixture under tests/integration/ that runs Alembic head before any router test fires).

ENV-003: Add a reproducible lockfile (uv or pip-compile) — pycharter pilot

  • Phase: Dev-env debt (outside KB workflow scope; tracked here for visibility)
  • Area: Build / DX
  • Owner: Build Agent (general-purpose, agentId ad68c77b57f39e078)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done (pycharter pilot — fleet-wide propagation to pystator / pygubernator / pyactuator is a separate per-repo decision)
  • Priority: Medium
  • Risk: Medium
  • Source: Surfaced during P1-001-FU-A push attempt — open-bounded version pins and no lockfile mean every pip install -e .[dev] produces a different env depending on the day.
  • Dependencies: ENV-001 (upper bounds — Done).
  • Tool choice: uv (Astral). Same vendor as ruff which the repo already uses. Adopted as a pycharter pilot; fleet-wide convention will be decided once the pilot soaks. Alternatives considered: pip-compile (more conservative, slower), poetry (heavier, opinionated).
  • Write scope:
  • Add uv.lock (generated, reproducible).
  • Add uv to the [dev] extra in pyproject.toml.
  • Add a new test-lockfile CI job alongside the existing test matrix (additive — existing pip-install path stays the canonical fast pre-push gate).
  • Add lock and sync targets to Makefile.
  • Document the dep-bump workflow in CONTRIBUTING.md alongside the existing pre-push contract.
  • Do NOT break the existing pip install -e .[dev] workflow. It must continue to work.
  • Acceptance criteria:
  • uv.lock generated, reproducible (running uv lock twice produces byte-identical files). ✅ (Verified.)
  • CI exercises both install paths (existing pip + new uv sync --frozen). ✅
  • Dependency-bump workflow documented in CONTRIBUTING.md. ✅
  • Pre-push hook still passes without change. ✅
  • Test evidence:
  • Files added (1): uv.lock (5,842 lines, 1,003 KiB, 256 packages resolved). Reproducible: diff -q clean across two uv lock runs.
  • Files modified (4): pyproject.toml (adds "uv>=0.5.0,<1.0" to [dev] extra, same pin style as siblings), .github/workflows/ci.yml (new test-lockfile job; existing matrix untouched), Makefile (new lock + sync targets), CONTRIBUTING.md (new "Dependency management with uv" section documenting both install paths and the 3-step dep-bump workflow).
  • CI workflow design: new test-lockfile job as a top-level entry (not a matrix entry). Steps: checkout → setup-python 3.12 → pip install uv → uv lock --locked (drift gate; fails if lockfile is stale) → uv sync --frozen --all-extras → ruff + pytest. The existing test matrix (3.11/3.12/3.13) is byte-identical — no regression risk on a required status check.
  • Quality gates (orchestrator independent re-run on 2026-05-19): uv --version → 0.11.14; uv lock reproducible; uv lock --locked clean; uv sync --frozen --dry-run resolves; pip install --dry-run -e .[dev] resolves; pre-commit run --all-files passes; python -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml'))" valid.
  • Review notes:
  • Approved via orchestrator re-run. Pycharter is the first optophi package to adopt uv. Existing pip workflow preserved; pilot can be unwound by removing the lockfile and the test-lockfile job if needed.
  • Residual risks:
  • Lockfile diff size on bumps: uv.lock is ~1 MB. Routine transitive bumps produce ~20-line diffs; uv lock --upgrade produces thousand-line diffs. Mitigation: bump in dedicated commits.
  • No Renovate / Dependabot for uv.lock yet: existing dependabot covers pip + github_actions ecosystems. Dependabot supports uv (GA 2025); adding it here is a follow-up.
  • CI lockfile job uses Python 3.12 only: existing test matrix covers 3.11/3.12/3.13. Locking against all three Python versions is an easy future enhancement (matrix on the lockfile job).
  • Fleet alignment is open: pystator / pygubernator / pyactuator have not adopted uv yet. This pilot doesn't force them to; future fleet decision can converge or diverge.
  • make sync requires uv on PATH: first-time contributors who run make sync before pip install -e .[dev] will get "uv: command not found". Documented in CONTRIBUTING.md.

ENV-004: Document the pre-push contract and local dev prerequisites

  • Phase: Dev-env debt
  • Area: Docs / DX
  • Owner: Docs Agent (general-purpose, agentId a67938caf090a03da)
  • Reviewer: Orchestrator
  • Status: Done (committed b586e885 on chore/dev-env-debt-cleanup, PR #171; cherry-picked onto feat/kb-phase-0 as 285b63d2)
  • Priority: Medium
  • Risk: Low
  • Source: Surfaced during P1-001-FU-A push attempt.
  • Dependencies: ENV-002 (so the documented "what to run before push" matches the new unit-vs-integration split). ✅
  • Write scope: CONTRIBUTING.md (append) + one-line cross-reference in AGENTS.md. ✅
  • Acceptance criteria:
  • "Before you push" section in CONTRIBUTING.md. ✅
  • "When tests fail at push time" diagnosis tree. ✅
  • AGENTS.md cross-reference. ✅
  • Real commands documented (./scripts/ci.sh --no-ui, docker compose up -d postgres, pip install -e ".[dev]", pre-commit install --hook-type pre-push).
  • Test evidence:
  • 2026-05-18 — CONTRIBUTING.md (+70 lines), AGENTS.md (+2 lines).
  • All documented commands grep-verified to exist (scripts/ci.sh, docker-compose.yml postgres service, Makefile install-dev, pre-commit install --hook-type pre-push in .pre-commit-config.yaml comments).
  • Review notes:
  • Approved.
  • Residual risks:
  • The cross-referenced kanban file (docs/guides/kb-ontology-workflow-kanban.md) does not yet exist on develop (it lives on feat/kb-phase-0). The link will resolve once feat/kb-phase-0 merges. Document this caveat if it lingers.
  • Phase: Phase 1
  • Area: Backend
  • Owner: Backend Agent (general-purpose, agentId ae358d1de645b78bc)
  • Reviewer: Reviewer/Integrator Agent (general-purpose, agentId aaf876ed9ba474845)
  • Status: Done
  • Priority: High
  • Risk: Medium
  • Source: Proposal §"Phase 1" task P1-005.
  • Dependencies: P1-002 (Done), P1-003 (Done).
  • Write scope: As declared — observed clean.
  • Acceptance criteria:
  • Suggestions can carry source_asset_id and ingest_run_id. ✅
  • Existing suggestion API and DB shape stays backward compatible — both columns nullable, defaults preserved. ✅
  • Tests prove the linkage round-trips and that lookups via the new columns work. ✅
  • Migration is reversible. ✅
  • Test evidence:
  • Canonical suggestion model: src/pycharter/db/models/semantic/suggestion.py → SuggestionModel (table suggestions, schema pycharter). Confirmed canonical via Phase 0 audit.
  • Files modified (2): src/pycharter/db/models/semantic/suggestion.py (+2 columns + docstring); src/pycharter/persistence/collaboration/suggestions.py (SuggestionManager.create / create_batch / list / _to_dict accept optional source_asset_id / ingest_run_id; defaults preserved).
  • Files added (3): migration 20260518000003_add_suggestion_source_traceability.py (100 lines, revision suggestion_source_traceability, down_revision = asset_ingest_run_link, idempotent _has_column guards, reversible downgrade); tests/unit/test_suggestion_source_traceability.py (264 lines, 7 tests); tests/integration/test_suggestion_source_traceability_restart.py (113 lines, @pytest.mark.integration).
  • Out-of-scope check: No API route, UI, asset-lineage, or KB-management source touched. Sibling files (semantic_activity_api.py, semantic_provenance_api.py, etc.) that construct SuggestionModel(...) for fixture setup pass unchanged (defaults remain None).
  • Quality gates (orchestrator independent re-run on 2026-05-18): ruff clean; mypy clean (30 files); new unit (7 of combined 27) pass; new integration pass; full unit suite — 2366 passed, 1 skipped, 8 deselected (zero regression vs. P1-003 baseline).
  • Review notes:
  • APPROVE (kb-phase-1-traceability-review.md): all 4 acceptance bullets satisfied; soft-reference columns confirmed at suggestion.py:81-82; migration chain verified by direct module load; 7 unit + 1 integration test green; 46-test regression sweep clean; CLAUDE.md hygiene complete. Reviewer recommended a tiny follow-up P1-005-FU-A to surface the new columns on SuggestionResponse over HTTP.
  • Residual risks:
  • Loose UUID format on the new soft-reference strings — same as SemanticAssetModel.ingest_run_id; no UUID validation at write time. Program-consistent.
  • No referential integrity at DB layer — a suggestion can carry IDs that don't resolve. Joins must tolerate dangling refs.
  • Open question (orchestrator/PM): should a future Phase 2 card add validate_lineage=True opt-in service flag mirroring UnknownConceptSchemeError?
  • Open question: should the missing POST /suggestions/{id}/request-changes route (Phase 0 audit gap) also persist source_asset_id / ingest_run_id on the change-reviewer's audit trail?
  • Open question: should api/models/semantic_suggestions.py:SuggestionResponse surface the two new fields? Out of P1-005's scope; would be a tiny follow-up (P1-005-FU-A).

P1-007: Durable mapping projection for field-to-concept bindings (scoping)

  • Phase: Phase 1
  • Area: Backend (scoping first)
  • Owner: Docs / Backend Agent (general-purpose, agentId aa261750e291aa60a)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done (scoping pass — implementation tracked in P1-007-IMPL below)
  • Priority: Medium
  • Risk: Medium
  • Source: Proposal §"Phase 1" task P1-007 ("if not already sufficiently represented").
  • Dependencies: None for scoping pass.
  • Write scope: docs/guides/kb-phase-1-p1-007-mapping-projection-decision.md only. ✅
  • Acceptance criteria:
  • Decision record exists with implement-vs-retire conclusion + cited evidence. ✅ — IMPLEMENT.
  • Test evidence:
  • 2026-05-18 — docs/guides/kb-phase-1-p1-007-mapping-projection-decision.md (342 lines).
  • Gaps documented vs. proposal: field_mappings.mapping_data is per-contract JSON blob (not per-binding); field_concepts is lossy ingest-path-only; concept_binding_events is history log, not current-state projection; editor_lineage.py:163-182 flags an O(contracts × bindings) JSON scan that needs a pre-computed reverse index.
  • Review notes:
  • Decision: IMPLEMENT. Promoted P1-007-IMPL to Backlog (below).
  • Residual risks:
  • Open question (PM): versioned rows vs. current-only — proposal's "mapping history visible" argues for versioned + is_current flag.
  • Open question (PM): store unresolved bindings — UnresolvedBinding semantics argue yes.
  • Open question (PM): write-through vs. overlay — proposal §4 says "or"; recommend write-through in Phase 1, defer overlay to Phase 3.

P1-007-IMPL: Implement durable mapping projection (Protocol + factory + SQLAlchemy + in-memory)

  • Phase: Phase 1
  • Area: Backend
  • Owner: Backend Agent (general-purpose, agentId af94d78a6f94b6383)
  • Reviewer: Reviewer/Integrator Agent (general-purpose, agentId a41e0689189645f69)
  • Status: Done
  • Priority: Medium-High
  • Risk: Medium-High (touches existing field-mapping write paths) — landed cleanly
  • Source: P1-007 scoping decision (kb-phase-1-p1-007-mapping-projection-decision.md).
  • Dependencies: P1-007 (Done — scoping). P1-001 / P1-003 (Done — Protocol+factory+SQLAlchemy+in-memory pattern is the template).
  • Write scope: As declared — observed clean. No API route, no UI, no protected source (knowledge_base/, asset_lineage/, existing field_mapping.py model, concept_binding_events table) touched. The two wiring changes (_contract_field_bindings.py + lifecycle.py) are the smallest possible additive integrations.
  • Acceptance criteria:
  • MappingProjectionModel + mapping_projections migration created. ✅ (Migration mapping_projections, down_revision = suggestion_source_traceability.)
  • src/pycharter/semantic/mapping_projection/ package with Protocol + factory + SQLAlchemy + in-memory + lifecycle hook + value objects + errors. ✅
  • Default backend "memory"; env var PYCHARTER_MAPPING_PROJECTION_BACKEND. ✅
  • Backfill CLI script with --dry-run, --force, idempotency. ✅
  • Wiring into update_field_bindings_for_contract (write-through, same transaction) + BindingLifecycle.on_approve (exceptions logged + swallowed). ✅
  • Versioned rows + is_current flag with flip-then-insert atomicity. ✅
  • All P1-001/002/003/FU-A/P1-005 regression suites pass. ✅
  • ruff check, ruff format, mypy, pytest pass. ✅
  • Test evidence:
  • Files added (15): 1 SQLAlchemy model (mapping_projection.py, 123 lines) + 1 Alembic migration (83 lines) + new package mapping_projection/ (8 files: __init__.py 64, _types.py 136, models.py 90, errors.py 40, _in_memory.py 237, _sqlalchemy_mapping_projection.py 336, _row_mappers.py 41, factory.py 122, lifecycle_hooks.py 103) + backfill CLI (backfill utility script, 223 lines) + 3 test files (test_mapping_projection_repository.py 468, test_mapping_projection_wiring.py 295, test_mapping_projection_restart.py 115).
  • Files modified (4): db/models/semantic/__init__.py (export), db/migrations/env.py (import for autogenerate), api/services/_contract_field_bindings.py (write-through wiring), api/dependencies/lifecycle.py (on_approve composition).
  • Public surface (7 methods): record_projection, update_state, mark_resolved, list_current_for_contract, list_history_for_field, list_contracts_using_concept, get_projection.
  • is_current flip-then-insert atomic in both backends (_flip_prior_current helper); validated by TestRecordProjection::test_record_flips_prior_current_row and wiring test test_second_save_flips_prior_projection_row.
  • Quality gates (orchestrator + reviewer independent re-runs on 2026-05-18):
    • ruff check / format on all touched paths — clean.
    • mypy src/pycharter/semantic/mapping_projection/ src/pycharter/db/models/semantic/ — Success: no issues found in 30 source files.
    • pytest tests/unit/test_mapping_projection_repository.py -q — 20 passed.
    • pytest tests/unit/test_mapping_projection_wiring.py -q — 6 passed.
    • pytest tests/integration/test_mapping_projection_restart.py -q -m integration — 1 passed.
    • pytest tests/unit/ -q --timeout=60 — 2396 passed, 1 skipped, 8 deselected (was ~2370, +26 from new tests; zero regression).
  • Review notes:
  • APPROVE (kb-phase-1-p1-007-impl-review.md): all locked decisions honoured; transaction-boundary contract correctly implemented (live session uses session.flush() only; _commit_and_close no-ops when caller owns the session); migration chain wires cleanly; backfill CLI honours --dry-run / --force / PYCHARTER_DATABASE_URL; wiring is the smallest possible additive change.
  • One partial: last_event_id is intentionally always None from the on_approve hook because BindingEvent doesn't yet carry the persisted concept_binding_events.id. Filling the link is deferred to P1-008.
  • Residual risks:
  • linkml_slot primary bindings become state="unresolved" — _record_projection_rows derives concept_key only from primary.concept.concept; when linkml_slot is set instead, the projection records concept_key=None, state="unresolved" despite the field being genuinely bound. Tracked in P1-007-FU-A (Backlog).
  • concept_id → concept_key matching in on_approve is unchecked — the lifecycle hook treats its concept_id arg as a concept_key when calling list_contracts_using_concept. Works today by convention, not by contract.
  • In-memory default in production loses state on restart — both the contract-PATCH service and lifecycle wiring default to in-memory when the env var is unset. Acceptable for the introduction card but becomes wrong the moment a read route depends on the projection. Tracked in P1-007-FU-B (Backlog).
  • Store commits its own session before the projection write — if the projection write raises, the field-mapping blob has already landed in a separate transaction. The wiring test documents this; tightening would require a ContractStoreClient refactor (out of scope here).
  • MappingProjectionApprovalUpdater.on_approve fans out to every active contract version — BindingEvent lacks contract_version. P1-008 should extend the event payload.

P1-008: Mapping lifecycle events

  • Phase: Phase 1
  • Area: Backend
  • Owner: Backend Agent (general-purpose, agentId ab3f2e2ec48c75248)
  • Reviewer: Reviewer/Integrator Agent (general-purpose, agentId afeba002d4c69008c)
  • Status: Done
  • Priority: Medium
  • Risk: Medium
  • Source: Proposal §"Phase 1" task P1-008; P1-007-IMPL residual risks.
  • Dependencies: P1-007-IMPL (Done). Scope adjusted from "new lifecycle table" to "extend existing BindingLifecycle + projection updater" because P1-007-IMPL already established the lifecycle composition pattern and the concept_binding_events table already exists.
  • Write scope: Lifecycle event payload + hooks + projection updater extension. Disjoint from P1-007-FU-A (_contract_field_bindings.py) and P1-007-FU-B (editor_lineage.py route).
  • Acceptance criteria:
  • Every mapping state transition fires the full projection updater (not just on_approve). ✅ (on_approve, on_reject, on_remap, plus on_transition dispatcher.)
  • BindingEvent carries contract_version (optional; defaults to None for backward compat). ✅
  • BindingEventStore.append returns the persisted event id so last_event_id can be threaded into the projection. ✅
  • Existing BindingLifecycle.transition callers stay compatible. ✅ (All new fields default to None / no-op hook.)
  • Test evidence:
  • Files modified (5):
    • src/pycharter/semantic/lifecycle/_types.py — BindingEvent.contract_version: str | None = None.
    • src/pycharter/semantic/lifecycle/_store.py — BindingEventStore.append() -> str | None. InMemoryBindingEventStore synthesises UUID; SqlBindingEventStore flushes then commits and returns str(row.id).
    • src/pycharter/semantic/lifecycle/_events.py — OnTransitionHook Protocol + BindingLifecycle.on_transition field. transition() captures event_id from store and fires on_transition(event, event_id) after commit.
    • src/pycharter/semantic/mapping_projection/lifecycle_hooks.py — MappingProjectionApprovalUpdater → MappingProjectionLifecycleUpdater (with backward-compat alias). New on_transition dispatcher routes to on_approve / on_reject / on_remap; _apply filters by event.contract_version when set, falls back to fan-out when None.
    • src/pycharter/api/dependencies/lifecycle.py — wires BindingLifecycle(on_approve=asserter.on_approve, on_transition=projection_updater.on_transition).
  • Files added (1): tests/unit/test_mapping_projection_lifecycle.py (~360 lines, 13 tests).
  • State transition map: APPROVED → "approved"; UNMAPPED → "unresolved"; MAPPED → "mapped"; SUGGESTED → "suggested".
  • Quality gates (orchestrator + reviewer independent re-runs on 2026-05-18): ruff clean; mypy clean (54 source files); 13 new lifecycle tests pass; full unit suite — 2433 passed, 1 skipped, 8 deselected.
  • Review notes:
  • APPROVE with follow-ups (kb-phase-1-completion-review.md): full lifecycle catalogue wired; backward-compat alias preserved; protocol widening clean; end-to-end last_event_id test passes; in-memory event store now allocates UUIDs (subtle contract change, no test broken). One follow-up filed: P1-008-FU-A to thread contract_version into the public TransitionRequest payload and internal callers (see Backlog).
  • Residual risks:
  • contract_version plumbing complete in lifecycle layer but no API caller passes it yet — POST /api/v1/bindings/transitions and update_field_bindings_for_contract both skip it. The new field is a no-op in production until P1-008-FU-A wires it.
  • In-memory BindingEventStore.append now allocates UUIDs per call — minor contract change; no existing test relies on the previous id-less contract.
  • SqlBindingEventStore.append now flushes before commit — required to surface row.id; behavior unchanged for callers.
  • concept_id vs concept_key matching — preserved from P1-007-IMPL's residual risk; flagged separately as a future fix.
  • Phase: Phase 1
  • Area: Backend
  • Owner: Backend Agent (general-purpose, agentId adc0a54d7ab387a9a)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Low
  • Risk: Low
  • Source: P1-003 reviewer report (kb-phase-1-p1-003-review.md).
  • Dependencies: P1-003 (Done).
  • Write scope: src/pycharter/semantic/asset_lineage/errors.py + __init__.py + _types.py + _in_memory.py + _sqlalchemy_asset_lineage.py + tests/unit/test_asset_lineage_repository.py. ✅
  • Acceptance criteria:
  • Decision: raise on missing asset (chosen for consistency with UnknownConceptSchemeError and update_ingest_run_status's LookupError).
  • link_asset_to_ingest_run(asset_id, run_id) raises UnknownAssetError(asset_id) on missing asset in both backends. ✅
  • At least one new test pins the miss path for each backend. ✅ (4 new tests.)
  • All existing P1-003 tests still pass. ✅
  • Test evidence:
  • Files modified (6): errors.py (+UnknownAssetError), __init__.py (export), _types.py (Protocol docstring updated), _in_memory.py + _sqlalchemy_asset_lineage.py (raise on miss), tests/unit/test_asset_lineage_repository.py (4 new tests).
  • New exception: UnknownAssetError(AssetLineageError) — AssetLineageError → PyCharterSemanticError. Instance attr asset_id; message includes the offending id.
  • SQLAlchemy side-effect: malformed UUID input also raises UnknownAssetError (was a silent no-op before). One-axis 404-mappable contract.
  • Quality gates (orchestrator independent re-run on 2026-05-18): ruff clean; mypy clean (8 files); pytest tests/unit/test_asset_lineage_repository.py — 24 passed (+4 vs. P1-003 baseline of 20); full unit suite — 2343 passed, 1 skipped, 8 deselected (was 2339 baseline + 4 new tests).
  • Review notes:
  • Approved via orchestrator independent re-run. P1-003-FU-B's route (POST /semantic/assets/{id}/link-ingest-run on develop post-#173) will inherit the new behaviour; once _http_errors.py audited helper is wired, UnknownAssetError → 404 mapping is one line.
  • Residual risks:
  • SQLAlchemy folds malformed UUID into UnknownAssetError (was previously a separate silent no-op). Reviewer can split into a separate ValueError/422 path later if desired; kept single-axis for 404-mappability.

P1-003-FU-B: Wire asset-lineage repository through a FastAPI dependency

  • Phase: Phase 1
  • Area: Backend / API
  • Owner: Backend Agent (general-purpose, agentId aea6e6c092f37fc75)
  • Reviewer: Reviewer/Integrator Agent (general-purpose, agentId aaf876ed9ba474845)
  • Status: Done
  • Priority: Medium
  • Risk: Medium
  • Source: P1-003 reviewer report (kb-phase-1-p1-003-review.md); mirrors P1-001 → P1-001-FU-A.
  • Dependencies: P1-003 (Done).
  • Write scope: As declared — observed clean.
  • Acceptance criteria:
  • Default backend remains "memory". ✅
  • SQLAlchemy mode reachable via PYCHARTER_ASSET_LINEAGE_BACKEND=sqlalchemy. ✅
  • In-memory backend has known_schemes seeded from concept_schemes lazily on first request. ✅
  • Tests cover both backends through the route layer. ✅ (20 parametrised tests.)
  • All P1-001/002/FU-A/P1-003 regression suites still pass. ✅
  • Test evidence:
  • Routes added (10): GET /api/v1/semantic/assets (with scheme / asset_type / ingest_run filters), GET /api/v1/semantic/assets/{id}, POST /api/v1/semantic/assets (400 on UnknownConceptSchemeError via raise_logged_bad_request_from_exception), POST /api/v1/semantic/assets/{id}/link-ingest-run, GET /api/v1/semantic/ingest-runs, GET /api/v1/semantic/ingest-runs/{id}, POST /api/v1/semantic/ingest-runs, PATCH /api/v1/semantic/ingest-runs/{id}/status (404 on LookupError).
  • Files added (2): src/pycharter/api/routes/v1/asset_lineage.py (369 lines); tests/unit/test_asset_lineage_route_factory.py (482 lines, 20 tests).
  • Files modified (1): src/pycharter/api/_routers.py — registered new router under /api/v1 with _auth_dep, mirroring editor_knowledge_base.router.
  • Dependency wiring: _get_asset_lineage_repository resolves via resolve_backend(). Memory mode yields a module-level singleton with lazy _seed_memory_known_schemes from ConceptSchemeModel.scheme_key on first request (failures logged DEBUG and swallowed). SQLAlchemy mode constructs request-scoped repo over get_db_session; dependency commits on success, rolls back on exception. Live-session contract honoured (repo uses session.flush() only — confirmed via test_live_session_does_not_commit in P1-003 regression).
  • Out-of-scope check: No protected source touched (asset-lineage repo, KB-management repo, suggestion files, UI all clean).
  • Quality gates (orchestrator independent re-run on 2026-05-18): ruff clean; mypy clean (30 files); new tests — 20 pass; test_editor_knowledge_base_route_factory.py regression — 16 pass; test_asset_lineage_repository.py regression — 16 pass; test_public_api_surface.py — 17 pass (no surface drift); full unit suite — 2366 passed.
  • Review notes:
  • APPROVE (kb-phase-1-traceability-review.md): all 5 acceptance bullets satisfied; _DEFAULT_BACKEND = "memory" unchanged; 20 parametrised tests green across both backends; UnknownConceptSchemeError → 400 correctly uses raise_logged_bad_request_from_exception with extra_safe_types; raw HTTPException calls use fixed strings only; public-API surface unchanged (17/17 pass). One non-blocking note: asset_lineage.py is 405 lines (5 over the 400-line CLAUDE.md guideline) — reviewer opened a P1-003-FU-B-FU-A follow-up to split.
  • Residual risks:
  • Auth model: new router uses _auth_dep = [Depends(get_current_user)] mirroring sibling editor routes. No rate limits configured (consistent).
  • Error envelope: UnknownConceptSchemeError surfaced via audited raise_logged_bad_request_from_exception (extra_safe_types includes the error class). Not-found paths use plain HTTPException(404, detail=…) with fixed strings.
  • Memory-mode singleton lifetime: concept_schemes mutations mid-process aren't picked up (rebuilds on next restart). Productivity convenience — production should flip to SQLAlchemy where validation happens per-call.
  • list_assets without filter returns [] — Phase 2 consolidated-API review may want pagination.
  • POST /semantic/assets/{id}/link-ingest-run does not validate ingest_run_id exists — same soft-reference pattern. P1-003-FU-A (in Backlog) tightens the underlying link_asset_to_ingest_run contract; once that lands, this route can re-raise as 404.

P1-005-FU-A: Surface source_asset_id and ingest_run_id on SuggestionResponse

  • Phase: Phase 1
  • Area: Backend / API
  • Owner: Backend Agent (general-purpose, agentId af1e737ee611663db)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Low
  • Risk: Low
  • Source: P1-005 reviewer report (kb-phase-1-traceability-review.md).
  • Dependencies: P1-005 (Done).
  • Write scope: src/pycharter/api/models/semantic_suggestions.py + tests/unit/test_semantic_suggestions_api.py only. ✅
  • Acceptance criteria:
  • SuggestionResponse exposes source_asset_id: str | None and ingest_run_id: str | None. ✅
  • Existing clients ignoring unknown fields stay compatible. ✅ (Pydantic defaults to None; OpenAPI surface annotated via Field(..., description=...).)
  • At least one new test confirms the two fields round-trip through the HTTP API. ✅ (2 new tests in TestSuggestionResponseTraceability.)
  • Test evidence:
  • 2026-05-18 — SuggestionResponse +2 fields (Field(default=None, description=...) style). SuggestionListResponse inherits via items: list[SuggestionResponse] — no separate edit.
  • 2 new tests: test_get_surfaces_source_asset_id_and_ingest_run_id (round-trip via GET /{id} and list endpoint), test_get_returns_none_when_traceability_absent (null round-trip).
  • Quality gates: ruff clean; mypy clean; pytest tests/unit/test_semantic_suggestions_api.py — passes; full unit suite — 2368 passed.
  • Review notes:
  • Approved via orchestrator re-run. Field(..., description=...) used so FastAPI's OpenAPI export carries field docs.
  • Residual risks:
  • Legacy fields on SuggestionResponse use bare type annotations (no Field(...) descriptions). Not retrofit per "surgical changes" rule; a future card can lift OpenAPI quality across the whole model.

P1-003-FU-B-FU-A: Split asset_lineage.py route file under 400 lines

  • Phase: Phase 1
  • Area: Backend / API
  • Owner: Backend Agent (general-purpose, agentId a1d2d0e69861fe0f8)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Low
  • Risk: Low
  • Source: P1-003-FU-B reviewer report (kb-phase-1-traceability-review.md).
  • Dependencies: P1-003-FU-B (Done).
  • Write scope: src/pycharter/api/routes/v1/asset_lineage.py + new asset_lineage_schemas.py. ✅ Test file test_asset_lineage_route_factory.py NOT touched.
  • Acceptance criteria:
  • All resulting modules under 400 lines. ✅ (asset_lineage.py 362 lines; asset_lineage_schemas.py 61 lines.)
  • Public router export asset_lineage.router preserved. ✅ _routers.py unchanged.
  • tests/unit/test_asset_lineage_route_factory.py passes without modification. ✅ (20/20 pass unchanged.)
  • Test evidence:
  • Strategy chosen: B (sibling-schemas extraction). Strategy A (directory split) would have silently broken the test fixture's asset_lineage._memory_seeded = False mutation; Strategy B is surgical.
  • 4 inline Pydantic request models extracted to asset_lineage_schemas.py (61 lines, new). asset_lineage.py 405 → 362 (-43).
  • Public surface preserved: router, _memory_repository, _memory_seeded, _get_asset_lineage_repository, _seed_memory_known_schemes all still on the asset_lineage module.
  • Quality gates: ruff clean; mypy clean (94 source files); 71-test combined regression pass; full unit suite — 2368 passed, 1 skipped, 8 deselected (no public-API drift).
  • Review notes:
  • Approved via orchestrator re-run. Strategy B is the right call given the existing test contract.
  • Residual risks:
  • editor_knowledge_base.py is the natural twin; if it grows past 400 lines a similar split applies.
  • Strategy A remains an option if a future card needs to add another ~80+ lines of route or schema.

P1-007-FU-A: Tighten linkml_slot binding semantics in projection write-through

  • Phase: Phase 1
  • Area: Backend
  • Owner: Backend Agent (general-purpose, agentId a16f3891398625d0b)
  • Reviewer: Reviewer/Integrator Agent (general-purpose, agentId afeba002d4c69008c)
  • Status: Done
  • Priority: Medium
  • Risk: Low
  • Source: P1-007-IMPL reviewer report (kb-phase-1-p1-007-impl-review.md).
  • Dependencies: P1-007-IMPL (Done).
  • Write scope: As declared — observed clean. No protected source touched.
  • Acceptance criteria:
  • A linkml_slot binding produces a row with state="mapped" and a concept_key derived from the slot identifier. ✅
  • Existing concept-bound behaviour unchanged. ✅
  • Backfill script handles both cases. ✅ (Imports the shared _primary_concept_key helper.)
  • At least one new test covers each path. ✅ (4 new tests total — 2 wiring + 2 backfill.)
  • Test evidence:
  • Encoding chosen: linkml://<class>.<slot> — deterministic, fits String(255), collision-free vs. real concept keys.
  • Files modified (4): _contract_field_bindings.py (new _LINKML_SLOT_KEY_PREFIX, _encode_linkml_slot_key, _primary_concept_key helpers); backfill utility script (imports _primary_concept_key so encoding cannot drift); mapping_projection.py (column docstring documents the convention); test_mapping_projection_wiring.py (2 new tests).
  • Files added (1): backfill utility test module (110 lines, 2 tests).
  • Quality gates (orchestrator + reviewer independent re-runs on 2026-05-18): ruff clean; mypy clean; 4 new + 6 existing wiring tests pass; full unit suite — 2433 passed.
  • Review notes:
  • APPROVE (kb-phase-1-completion-review.md): encoding convention is collision-free and well-documented; backfill imports the shared helper so the two paths cannot diverge.
  • Residual risks:
  • Reverse-lookup callers must use the encoded form (linkml://...); naïve calls with a bare slot won't match. Model docstring documents this.
  • Pre-fix backfill runs persisted state="unresolved" for slot-bound primaries. Operators must re-run with --force to fix in place.

P1-007-FU-B: Wire mapping projection through editor lineage behind a feature flag

  • Phase: Phase 1
  • Area: Backend / API
  • Owner: Backend Agent (general-purpose, agentId ae9aa50ca4b42577d)
  • Reviewer: Reviewer/Integrator Agent (general-purpose, agentId afeba002d4c69008c)
  • Status: Done
  • Priority: Medium
  • Risk: Medium → landed cleanly (default-off feature flag).
  • Source: P1-007 scoping decision + P1-007-IMPL reviewer report.
  • Dependencies: P1-007-IMPL (Done).
  • Write scope: As declared. Production backend flip kept as an operational follow-up, not in this card.
  • Acceptance criteria:
  • Both code paths return the same result for the same input. ✅ (Parity test pinned.)
  • Default behaviour unchanged. ✅ (Flag defaults off; route uses existing JSON-scan path.)
  • Tests pin both paths. ✅ (18 new tests across flag-off, flag-on memory, flag-on sqlalchemy, parity, active-contract filter, annotation relationship surface.)
  • Test evidence:
  • Feature flag: PYCHARTER_USE_MAPPING_PROJECTION_FOR_LINEAGE. Default unset/falsy → JSON-scan path. Truthy set ("1", "true", "yes", "on", case-insensitive) → projection path. Read per-request via _use_projection_path() so flips need no restart.
  • Files modified (1): src/pycharter/api/routes/v1/editor_lineage.py (+185/−25). New _get_mapping_projection_repository dependency mirrors editor_knowledge_base._get_kb_management_service; read-only route so no commit/rollback wrap. New _projection_lineage alternate path. Legacy logic refactored into _scan_json_lineage for a clean dispatcher handle.
  • Files added (1): tests/unit/api/test_editor_lineage_projection_flag.py (557 lines, 18 tests).
  • Quality gates (orchestrator + reviewer independent re-runs on 2026-05-18): ruff clean; mypy clean; 18 new tests pass; existing test_editor_lineage.py regression — 10 passed; full unit suite — 2433 passed.
  • Review notes:
  • APPROVE (kb-phase-1-completion-review.md): flag defaults off; parity pinned between paths including active-contract filter and annotation relationship surface; both backend paths covered.
  • Residual risks:
  • Two-query approach for active-contract filter — projection lacks a status column, so the projection path does list_contracts_using_concept + _active_contract_keys. Both indexed; consider denormalising in a follow-up if perf matters.
  • Mid-process flag flip without backfill returns empty results — if PYCHARTER_USE_MAPPING_PROJECTION_FOR_LINEAGE=true but PYCHARTER_MAPPING_PROJECTION_BACKEND=sqlalchemy is not flipped or backfilled, the in-memory backend returns empty. Rollout sequence must be: flip backend + backfill, then flip lineage flag.
  • Operational flip + rollout doc — release-notes section was NOT added in this PR (docs/guides/kb-phase-1-p1-007-fu-b-lineage-rollout.md). Tracked in P1-007-FU-B-DOCS (Backlog).

P1-008-FU-A: Thread contract_version from API and service callers into BindingLifecycle

  • Phase: Phase 1
  • Area: Backend / API
  • Owner: Backend Agent (general-purpose, agentId a678918580f826bf5)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Medium
  • Risk: Low
  • Source: P1-008 reviewer report (kb-phase-1-completion-review.md).
  • Dependencies: P1-008 (Done).
  • Write scope: src/pycharter/api/routes/v1/semantic_bindings.py + tests/unit/test_semantic_bindings_route.py only. ✅
  • Acceptance criteria:
  • Public API accepts optional contract_version. ✅ (TransitionRequest extended.)
  • Existing payloads stay valid. ✅ (Field defaults to None.)
  • End-to-end test pins per-version filter activates. ✅ (2 new tests.)
  • BindingLifecycle.transition signature already accepted contract_version (added by P1-008); no signature change needed.
  • Test evidence:
  • Files modified (2): src/pycharter/api/routes/v1/semantic_bindings.py — added contract_version: str | None = None to TransitionRequest and BindingEventResponse; threaded through lifecycle.transition(...) and BindingEventResponse.from_event(...). tests/unit/test_semantic_bindings_route.py — added TestContractVersionEndToEnd class with 2 tests.
  • Service caller finding: update_field_bindings_for_contract does NOT call BindingLifecycle.transition — it writes projections directly via repo.record_projection(...). The only in-tree lifecycle caller is the route handler; no other plumbing needed.
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff clean; mypy clean (100 source files); pytest tests/unit/test_semantic_bindings_route.py — 6 passed; full unit suite — 2435 passed, 1 skipped, 8 deselected (baseline 2433 + 2 new).
  • Review notes:
  • APPROVE via orchestrator re-run. End-to-end per-version filter pinned: test_contract_version_in_payload_filters_projection_writes confirms only v1.0.0 row flips; test_omitted_contract_version_preserves_fan_out confirms backward compat.
  • Residual risks:
  • SQLAlchemy backend not covered by the new end-to-end test (uses in-memory); the version-filter logic is backend-agnostic in MappingProjectionLifecycleUpdater._apply per P1-008 tests.

P1-007-FU-B-DOCS: Document mapping projection rollout sequence + vocabulary update

  • Phase: Phase 1
  • Area: Docs
  • Owner: Docs Agent (general-purpose, agentId a352fc0d33fd6536c)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Low
  • Risk: Low
  • Source: P1-007-FU-B + completion-batch reviewer report.
  • Dependencies: P1-007-FU-B (Done), P1-008 (Done).
  • Write scope: docs/guides/kb-phase-1-p1-007-fu-b-lineage-rollout.md (new) + docs/guides/kb-phase-0-vocabulary.md (modify). ✅
  • Acceptance criteria:
  • Rollout doc exists with numbered steps + rollback + troubleshooting. ✅
  • Vocabulary entry for "unresolved" projection state added with rationale. ✅
  • Test evidence:
  • Files added (1): docs/guides/kb-phase-1-p1-007-fu-b-lineage-rollout.md (232 lines). 8 numbered steps with copy-pasteable commands + rollback path + troubleshooting tree.
  • Files modified (1): docs/guides/kb-phase-0-vocabulary.md — new row for "unresolved" (projection-only) in the lifecycle status table; "Note on unresolved vs. unmapped" paragraph explaining the divergence (projection rolls up on_reject + concept_key NULL into one state; BindingState keeps finer transitions); cross-references to mapping_projection/models.py and lifecycle/_types.py; new row in the Backend → UI Status Map.
  • Both backend tokens share the same UI label ("Unmapped") and badge colour (gray-300); unresolved is operator-facing only.
  • Review notes:
  • Approved via orchestrator re-run. Rollout sequence preserves the reversibility story for each step.
  • Residual risks:
  • Soak-window durations in steps 5 and 7 are stated as "at least one release cycle" — PM may want a concrete minimum (e.g. 7 days).
  • Canary vs. fleet-wide rollout strategy (env var on a single host first vs. everywhere) is not pinned — left as a PM call.

P2-001-FU-A: Pivot extracted_candidates to query suggestions table

  • Phase: Phase 2
  • Area: Backend / API
  • Owner: Backend Agent (general-purpose, agentId a377457141aa6eea4)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Medium
  • Risk: Low
  • Source: P2-001 reviewer report (kb-phase-2-foundation-review.md).
  • Dependencies: P2-001 (Done), P2-002 (Done).
  • Write scope: import_summary.py + new route handler dep + import-summary tests + asset-lineage-route tests. Disjoint. ✅
  • Acceptance criteria:
  • extracted_candidates now reflects suggestion-row count, not asset-row count. ✅
  • Existing P2-001 tests updated to reflect the new semantic. ✅
  • Documentation note clarifying the change in column docstring. ✅
  • Test evidence:
  • Approach: Option 1 (Protocol injection). build_import_run_summary gained suggestion_counter: SuggestionCounterProtocol | None = None kw-only parameter. Counts len(suggestion_counter.list(ingest_run_id=run_id)). Route constructs SuggestionManager(session=db_session) and threads it. No coupling to SuggestionManager.list internals.
  • Files modified (4): import_summary.py, asset_lineage.py (only the new summary route handler), test_import_summary.py (TestSuggestionDerivedCandidates with 4 new tests), test_asset_lineage_route_factory.py (+1 SQL backend test).
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff/mypy clean; new tests pass; full unit — 2501 passed.
  • Review notes:
  • Approved. Protocol-driven DI keeps asset_lineage/ free of a hard SuggestionManager dependency; tests pass a tiny stub. Mirrors existing package pattern.
  • Residual risks:
  • SuggestionManager(session=None) would compile-check cleanly via Protocol; runtime SuggestionError("No database session configured") if called. Route always supplies a live session; safe in production.
  • extracted_candidates is now 0 for runs with assets but no suggestions (previously matched asset count). Intended semantic shift per proposal; downstream UI must update.
  • New route test is SQLAlchemy-only (in-memory SuggestionManager requires a session).

P2-002-FU-A: Wire extraction adapter to populate suggestion evidence

  • Phase: Phase 2
  • Area: Backend / extraction
  • Owner: Backend Agent (general-purpose, agentId af0580fbc49662307)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Medium
  • Risk: Medium → landed cleanly (zero churn on existing extraction tests).
  • Source: P2-002 reviewer report.
  • Dependencies: P2-002 (Done).
  • Write scope: _suggestion_adapter.py, _materializer.py, + 17 new test additions. Disjoint. ✅
  • Acceptance criteria:
  • All extracted suggestions land with new evidence fields populated. ✅
  • Existing extraction tests continue to pass (zero modifications to existing test methods). ✅
  • Test evidence:
  • Design choices: (A) source_span hierarchical fallback — json_pointer/xpath/cell → line=N,col=A:B → bytes=start:end → coarse source_type=<type>. (B) rationale short string referencing concept-key + source. (C) proposed_action = "add_concept" for concept-kind, "add_relationship" for relation-kind. Both outside the validator's high/low-confidence sets so 0.6 default never conflicts. (D) require_evidence=True in the materializer's _materialize_as_suggestions path.
  • Files modified (4): _suggestion_adapter.py (new helpers _resolve_source_asset_id, _resolve_default_confidence, _source_span_for_entry, _structured_locator, _concept_rationale, _relation_rationale), _materializer.py (one-line require_evidence=True), test_suggestion_adapter.py (+16 tests in new TestExtractedSuggestionEvidence), test_extraction_materializer.py (+1 test).
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff/mypy clean; 17 new tests pass; zero churn on existing extraction tests; full unit — 2501 passed.
  • Review notes:
  • Approved. Adapter populates every evidence field; default-0.6 confidence keeps the validator's conflict catalogue happy. require_evidence=True enforces the contract at the write boundary going forward.
  • Residual risks:
  • Synthetic source_asset_id — when result.metadata["source_asset_id"] is absent, adapter mints a deterministic UUID5 placeholder. Satisfies validator but is NOT a real SemanticAssetModel.id. Follow-up needed to plumb real asset ids through kb.ingest.
  • proposed_action set is narrow (add_concept / add_relationship); P2-005 dedup work will add verbs like merge_with_existing / reject_duplicate.
  • require_evidence=True is a one-way commitment — any new adapter forgetting evidence will hard-fail at write time. Mitigated by test_evidence_validates_via_structured_validator.
  • Default confidence 0.6 is arbitrary; future tuning per-origin (e.g. tree-sitter higher than docs).

P2-002-FU-B: Add proposed_action filter on GET /api/v1/semantic/suggestions

  • Phase: Phase 2
  • Area: Backend / API
  • Owner: Backend Agent (general-purpose, agentId acfdbfb3010e1e7cd)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Low
  • Risk: Low
  • Source: P2-002 reviewer report.
  • Dependencies: P2-002 (Done) — index on proposed_action already in place.
  • Write scope: semantic_suggestions.py route handler + SuggestionManager.list (one kwarg only) + new test class. Disjoint. ✅
  • Acceptance criteria:
  • Route accepts ?proposed_action=... query param. ✅
  • Existing callers without the param see unchanged behaviour. ✅
  • At least one test pins the filter. ✅ (4 new tests.)
  • Test evidence:
  • Route + handler: src/pycharter/api/routes/v1/semantic_suggestions.py, handler list_suggestions on @router.get("") mounted at /api/v1/semantic/suggestions.
  • Files modified (3): route file (new query param + Q(default=None, description=...)), suggestions.py (one new kwarg on list() only; create/create_batch/_to_dict/lifecycle untouched), test_semantic_suggestions_api.py (+4 tests in TestSuggestionsListProposedActionFilter).
  • Index used: ix_suggestions_proposed_action from the P2-002 migration.
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff/mypy clean; 4 new tests pass; full unit — 2501 passed.
  • Review notes:
  • Approved. Filter style matches existing string filters on this handler (status, suggestion_kind, origin are also unvalidated). Reviewer flagged value-validation against canonical vocabulary as a future enhancement.
  • Residual risks:
  • No vocabulary validation — unknown verbs pass to SQL and quietly return []. Consistent with existing string filters on this handler.
  • Single-value only — multi-value (?proposed_action=add_concept&proposed_action=merge) would need list[str] | None with .in_(...).
  • NULL semantics — when filter is set, legacy pre-P2-002 NULL rows are excluded. Documented in the query description.

P2-005: Duplicate candidate detection in suggestion generation

  • Phase: Phase 2
  • Area: Backend
  • Owner: Backend Agent (general-purpose, agentId ac058b496dce76bc2)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Medium
  • Risk: Medium
  • Source: Proposal §"Phase 2" task P2-005.
  • Dependencies: P2-002 (Done).
  • Write scope: SuggestionModel (new column), SuggestionManager.create/create_batch (dedup helper + single _to_dict line for the new column), new migration, SuggestionResponse (one new field), dedup tests. Coordinated with parallel P2-006 (no overlap). ✅
  • Acceptance criteria:
  • Newly generated suggestions checked against existing concepts and pending suggestions. ✅
  • Duplicates flagged (not silently dropped). ✅
  • Threshold for "near-duplicate" documented — fuzzy axis deferred to P2-005-FU-A (exact-match dedup has no threshold to tune; documented in residual risks).
  • Test evidence:
  • Detection axes: Axis 0 (approved-concept) wins over Axis 1 (pending-suggestion same concept_key) wins over Axis 2 (pending-suggestion same source_asset_id + source_span).
  • is_duplicate_of convention: String(128), nullable=True, indexed. Values: concept:<concept_uuid> or suggestion:<suggestion_uuid>. Soft reference (no FK), matches the program-wide pattern.
  • Files modified (3): suggestion.py (new column + docstring), suggestions.py (_detect_duplicate helper + threaded through create/create_batch; single _to_dict line for the new field), semantic_suggestions.py (only is_duplicate_of field added; coordinated with P2-006's confidence_band).
  • Files added (2): Migration suggestion_is_duplicate_of (74 lines; down_revision=suggestion_evidence_columns; reversible). tests/unit/test_suggestion_dedup.py (~365 lines, 11 tests across 4 classes).
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff/mypy clean; 11 new tests pass; 84 suggestion regression tests pass (zero churn); full unit — 2532 passed.
  • Review notes:
  • Approved. All 3 axes pinned by tests. Concept-axis wins over suggestion-axis when both could fire (test_concept_axis_wins_over_pending_axis).
  • Residual risks:
  • Fuzzy matching deferred — exact payload.concept_key only. Drafts without that field land unflagged. Tracked in P2-005-FU-A (Backlog).
  • Within-batch dedup not exhaustive — create_batch evaluates against DB state at entry; two same-key drafts in one batch won't flag each other (second sees empty DB until post-loop flush). Documented inline.
  • Transitivity — is_duplicate_of is a single soft reference; reviewer UI should follow the chain.
  • Race condition — concurrent ingests on the same scheme could each see "no duplicate" and both land unflagged. Acceptable for single-writer-per-scheme today; a partial unique index could enforce it later.

P2-006: Confidence bands (high / medium / low / needs-review)

  • Phase: Phase 2
  • Area: Backend
  • Owner: Unassigned
  • Reviewer: Unassigned
  • Status: Done
  • Priority: Medium
  • Risk: Low
  • Source: Proposal §"Phase 2" task P2-006.
  • Dependencies: P2-002 (Done).
  • Owner: Backend Agent (general-purpose, agentId a061e4b9537006e98)
  • Reviewer: Orchestrator (independent re-run)
  • Write scope: semantic_suggestions.py (one computed field on SuggestionResponse) + new test file. Coordinated with parallel P2-005 (no overlap). ✅
  • Acceptance criteria:
  • Every suggestion has a derived confidence_band. ✅ (Always populated; computed at serialization.)
  • Banding thresholds documented (in Field(..., description=...) + module docstring). ✅
  • At least one test per band. ✅ (20 tests covering all bands + boundaries + NULL.)
  • Test evidence:
  • Banding scheme: >=0.9 high; [0.7, 0.9) medium; [0.5, 0.7) low; <0.5 needs_review; None → needs_review.
  • Implementation: Pydantic v2 @computed_field over a pure _derive_confidence_band helper. Never stored; computed at serialization. Literal["high","medium","low","needs_review"] type.
  • Files modified (1): semantic_suggestions.py — module-level threshold constants, ConfidenceBand Literal alias, _derive_confidence_band helper, @computed_field property. NO other field touched.
  • Files added (1): tests/unit/test_suggestion_confidence_bands.py (20 tests: 13 parametric boundary cases + 7 response-level tests including OpenAPI serialization).
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff/mypy clean; 20 new tests pass; full unit — 2532 passed.
  • Review notes:
  • Approved. Thresholds in module constants (not runtime-configurable) — documented as future enhancement.
  • Residual risks:
  • Thresholds not runtime-configurable (would need settings module / DB-backed config).
  • confidence_band is computed at serialization only — never stored. DB-layer filtering by band would require derivation in SQL or persistence.
  • type: ignore[prop-decorator] needed for known mypy/Pydantic v2 interaction; this is the upstream-recommended pattern.

P2-007: UI import wizard in unified editor

  • Phase: Phase 2
  • Area: Frontend
  • Owner: Frontend Agent (general-purpose, agentId ac9b9c7ca51163897)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: High
  • Risk: High → landed cleanly (zero lint/type/test regressions).
  • Source: Proposal §"Phase 2" task P2-007.
  • Dependencies: P2-001 (Done), UX0-002 (Done).
  • Write scope: New wizard + new additive Stepper/WizardHost primitives + 4 new API methods + tests. Composes P2-008's IngestRunSummary on success. Disjoint from P2-008. ✅
  • Acceptance criteria:
  • Wizard renders at /editor/import-wizard inside the unified editor. ✅
  • User completes the flow and sees the P2-008 summary on success. ✅
  • Failures surface (PATCH ingest-run status to failed + render error). ✅
  • Tests + manual browser-check protocol documented. ✅
  • Test evidence:
  • Files added (15): components/ui/stepper.tsx (147) + components/ui/wizard-host.tsx (148) — net-new additive primitives; lib/api/assetLineage.ts (122) — 4 typed methods (recordIngestRun, getIngestRun, updateIngestRunStatus, recordAsset); components/semantic/ImportWizard/ with 8 files (1126 LOC: ImportWizard.tsx orchestrator + 6 step components + types.ts + barrel); app/editor/import-wizard/page.tsx (20); 2 test files (5 + 4 tests).
  • Files modified (1): lib/api/index.ts — added assetLineageApi to the combined api object.
  • Mount point: /editor/import-wizard (Next.js App Router page).
  • State management: local useReducer (wizard state) + useState (step index, submitted flag). No Zustand slice — wizard state is transient. Justified per Frontend Standards.
  • Steps: Source type → Target scheme (workspace dropdown via getSchemes, free-text fallback) → Policy (3 governance presets flowing through config) → Extraction options (concepts-only, extract-relations, run-inference) → Review (read-only recap) → Submit (4-call API chain + renders P2-008 summary).
  • Composition with P2-008: SubmitStep imports <IngestRunSummary runId={result.runId} /> from @/components/semantic/IngestRunSummary; compact local count panel shown while the summary fetch settles.
  • Quality gates (orchestrator independent re-run on 2026-05-19): npm run lint — 0 errors (26 pre-existing warnings); npm run type-check — clean; npm run test — 468/468 pass across 47 files; npm run build — success; route registered.
  • A11y: keyboard-only nav; aria-current="step"; role="region" on body with step label; aria-live="polite" on submitting status; native semantic controls throughout.
  • Layout: Flexbox + Grid; zero position: absolute. Responsive via Tailwind sm: breakpoints.
  • Review notes:
  • Approved via orchestrator independent re-run. Stepper + WizardHost are clean additive primitives suitable for future wizard reuse.
  • Residual risks:
  • Backend route consolidation gap — wizard chains 4 endpoints. Future card may add a single POST /imports/start.
  • Governance policy presets flow to config JSON; backend has no policy enum yet.
  • Upload-arbitrary path uses paste-text; multipart upload variant is a follow-up.
  • No nav entry today — wizard reachable only via URL. Follow-up to surface in EditorDashboard quick actions.
  • Phase: Phase 2
  • Area: Frontend
  • Owner: Frontend Agent (general-purpose, agentId ab18fc400b1ea5b31)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: High
  • Risk: Medium → landed cleanly.
  • Source: Proposal §"Phase 2" task P2-008.
  • Dependencies: P2-001 (Done).
  • Write scope: New IngestRunSummary reusable component + standalone route + ONE API method + tests. Disjoint from P2-007. ✅
  • Acceptance criteria:
  • Renders every field from ImportRunSummary. ✅
  • "View suggestions" link with correct URL pattern. ✅
  • Loading / error / empty / populated states. ✅
  • Test evidence:
  • Files added (5): components/semantic/IngestRunSummary/IngestRunSummary.tsx (~410), IngestRunSummary/index.ts (10), app/editor/ingest-runs/[runId]/page.tsx (20), app/editor/ingest-runs/[runId]/IngestRunDetailPageClient.tsx (~33), __tests__/components/semantic/IngestRunSummary.test.tsx (~210, 14 tests).
  • Files modified (2): lib/api/ingest.ts (added SourceReference, ImportRunCounts, ImportRunSummary types + getImportRunSummary method); components/semantic/index.ts (4-line export block).
  • Standalone route: /editor/ingest-runs/{runId} (Next.js App Router with generateStaticParams).
  • Component props: runId?: string (fetch mode) + summary?: ImportRunSummary (pre-fetched mode for wizard) + onRerunExtraction? + className?. Plus exported buildSuggestionsLink(ingestRunId): string helper.
  • "View suggestions" link: /ontology/suggestions?ingest_run_id=<urlencoded(run_id)>.
  • Composition with P2-007: wizard's SubmitStep passes runId (fetch mode); standalone route uses the same. The component has zero dependency on the wizard.
  • Quality gates (orchestrator independent re-run on 2026-05-19): npm run lint — 0 errors; npm run type-check — clean; npm run test — 468/468 pass; npm run build — /editor/ingest-runs/[runId] listed in route table.
  • A11y: role="status" + aria-live="polite" on loading; role="alert" on errors; semantic <dl>/<ul> for data; aria-label on the "View suggestions" link; decorative icons marked aria-hidden.
  • Layout: Flexbox + Grid only; zero position: absolute. Responsive via grid-cols-2 sm:grid-cols-3 lg:grid-cols-5.
  • Review notes:
  • Approved via orchestrator independent re-run. Component supports both fetch-mode and pre-fetched-mode for composition; tests pin both paths.
  • Residual risks:
  • In-flight polling not implemented — when status === "running" and finished_at === null, page is read-once. Follow-up could add setInterval or websocket once P2-007 lands streaming.
  • Suggestions inbox doesn't yet consume ingest_run_id query param — backend filter works (P1-005); frontend filter UI needs a follow-up card to wire the query param into the page's filter state. Deep link is correct; inbox just hasn't consumed it.
  • extracted_candidates tile reads identical to suggestions_emitted post-P2-001-FU-A. Two adjacent tiles with the same number is intentional but may confuse users. Worth copy review.
  • StatusBadge colour map doesn't yet cover running/partial (vocabulary guide reserves amber/orange). Aligning is flagged in vocabulary doc; out of scope here.

P2-009: Integration tests for ingest → suggestions with source evidence

  • Phase: Phase 2
  • Area: QA / Backend
  • Owner: QA Agent (general-purpose, agentId ae1ebaec07d3805ee)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Medium
  • Risk: Low
  • Source: Proposal §"Phase 2" task P2-009.
  • Dependencies: P2-002 (Done), P2-002-FU-A (Done).
  • Write scope: tests/integration/test_ingest_to_suggestions_flow.py (new file, 392 lines). No source touched. ✅
  • Acceptance criteria:
  • End-to-end test for the import → suggestions path. ✅
  • Asserts on evidence shape, not just counts. ✅
  • Runs under the integration suite with @pytest.mark.integration. ✅
  • Test evidence:
  • Approach: Adapter-driven. Builds 2 minimal ExtractionResult payloads inline (contract.yaml + schema.csv), runs through production extraction_result_to_drafts, persists via real SuggestionManager.create_batch(..., require_evidence=True).
  • Fixture: revenue/billing — scheme revenue-billing; actor billing-importer@example.com; 2 assets (s3://example/revenue/billing/contracts.yaml, .../invoices.csv); concepts billing.Invoice, billing.Contract, billing.LineItem; one relation Invoice -bills_for-> Contract.
  • Assertions per persisted suggestion: status, scheme_id, ingest_run_id, source_asset_id (matched by source_uri), source_span (non-empty + correct locator format), rationale (contains source_uri), proposed_action ∈ {add_concept, add_relationship}, confidence_score float in [0,1], provenance merged. Plus shape: contract metadata override of confidence_score=0.7 lands on contract rows; invoice falls back to adapter default 0.6.
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff clean; pytest tests/integration/test_ingest_to_suggestions_flow.py -m integration -v — 1 passed in 1.08s; integration suite (mapping projection + suggestion traceability + ingest flow) — 3 passed.
  • Review notes:
  • Approved. Adapter-driven approach exercises the same code path the materializer uses; doesn't require bootstrapping tree-sitter / docs-markdown extractors.
  • Residual risks:
  • On-disk SQLite covers schema + ORM round-trip but masks Postgres-specific behaviour (JSONB indexing, native UUID type, case-sensitive ordering). Same test should pass on Postgres CI without modification.
  • Test doesn't assert on confidence_band (P2-006) or is_duplicate_of (P2-005) — landed concurrently; extension is a thin follow-up if desired.
  • Uses extraction_result_to_drafts directly; future canonical "run an extraction" entrypoint would be a one-line swap.

P4-001: Add semantic health issue list

  • Phase: Phase 4
  • Area: Backend/Frontend
  • Owner: Full-stack Agent (general-purpose, agentId aa0afa36039d1b229)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Medium
  • Risk: Medium
  • Source: KB/Ontology Workflow Proposal, semantic health and governance gates
  • Dependencies: P1-001, UX0-002
  • Write scope: Health service, health API, existing editor semantic health panel
  • Acceptance criteria:
  • Health checks produce actionable issues with severity, affected entity, and suggested fix. ✅
  • UI shows issue status, severity, owner when available, and direct navigation to the affected concept or mapping. ✅
  • Tests cover duplicate concept, missing mapping, deprecated concept usage, and broken relationship cases. ✅
  • Test evidence:
  • Panel surface: new "Health" tab added to the existing EditorCompanion tab strip (alongside Validation / Impact / Runs / Completeness) — reuses the bottom-companion shell rather than introducing a new chrome. UX0-002 row "5. Semantic Health" explicitly authorised P4-001 to add a thin wrapper alongside existing panels.
  • Health-check coverage (all 4 working, no stubs):
    • duplicate_concept — case-insensitive grouping of concepts.name (catches Invoice vs invoice even though SQL unique=True passes them). Severity warning. Suggested fix: "Merge or rename".
    • missing_mapping — joins concepts.concept_key against active mapping_projections.concept_key; skips deprecated concepts and concepts without concept_key. Severity info. Suggested fix: "Bind to a contract field via the mapping workbench".
    • deprecated_concept_usage — joins active mapping_projections to concepts where deprecated=True. Severity error. Suggested fix: "Remap to a non-deprecated alternative".
    • broken_relationship — scans concept_relationships.{source,target}_concept_id for ids absent from concepts. Severity error. Production schema's ondelete=CASCADE makes this rare; tests explicitly disable PRAGMA foreign_keys to seed the case.
  • Backend service architecture (Protocol + factory + SQLAlchemy + in-memory, mirroring P1-001/P1-007-IMPL):
    • src/pycharter/semantic/health/_types.py — Severity, HealthIssueKind, AffectedEntityKind, HealthIssue, HealthReport, SemanticHealthServiceProtocol.
    • _checks/{duplicate_concept,missing_mapping,deprecated_concept_usage,broken_relationship}.py — pure read-only check_*(session) -> list[HealthIssue].
    • _service.py — SqlAlchemySemanticHealthService (live-session or session-factory mode; per-check exceptions logged and swallowed so one failing check cannot kill the report) + InMemorySemanticHealthService for tests.
    • factory.py — make_semantic_health_service(*, backend=None, session=None, session_factory=None, seed_issues=None). Default sqlalchemy. Selectable via env var PYCHARTER_SEMANTIC_HEALTH_BACKEND.
  • HTTP endpoint: GET /api/v1/semantic/health under src/pycharter/api/routes/v1/semantic_health.py, registered via pycharter.api._routers under the existing _semantic tuple.
    • Query params: ?severity=error,warning,info, ?kind=..., ?affected_entity_kind=concept,..., ?limit=N (1–500), ?offset=N.
    • Response: HealthReportResponse { issues: HealthIssueResponse[], total: int, summary: dict[str,int] }. Each issue: kind, severity, affected_entity_kind, affected_entity_ids, summary, details, suggested_fix, actionable_link, owner, detected_at.
    • Unknown filter tokens silently dropped. Exceptions surface through raise_logged_internal_from_exception.
  • Frontend panel (src/pycharter/ui/src/components/editor/panels/SemanticHealthPanel.tsx):
    • States: loading skeleton (5 ghost rows); empty success (CheckCircle2 + "No semantic health issues found"); filtered-empty; error banner (role="alert" + Retry); list (severity icon + summary + kind badge + details + suggested fix + owner + Open link).
    • Filter controls: severity counter chips with aria-pressed; kind dropdown with aria-label; manual refresh button; 30 s auto-refresh interval.
    • Navigation per kind: duplicate_concept → /editor/concepts/<id>; missing_mapping → /editor/mapping-workbench?selectedConcept=<id>; deprecated_concept_usage → /editor/mapping-workbench?contract=<contract_id>&contractVersion=<version>&fieldPath=<field>; broken_relationship → /editor/relationships/<relationship_id>.
  • API client: semanticHealthApi.list(filters?) in src/pycharter/ui/src/lib/api/semanticHealth.ts.
  • A11y: severity counters with aria-pressed; severity icons with aria-label (not colour-only); kind dropdown with aria-label; semantic <ul aria-label>; real <a href> links (keyboard + open-in-tab); role="alert" on error; full keyboard tab order with visible focus rings; RefreshCw with aria-label.
  • Tests (49 new total):
    • Backend 6 files / 41 tests: test_semantic_health_checks_duplicate_concept.py (5), ..._missing_mapping.py (6), ..._deprecated_concept_usage.py (5), ..._broken_relationship.py (5, PRAGMA foreign_keys=OFF fixture), test_semantic_health_service.py (13 — aggregation, sort, filter, pagination, factory), test_semantic_health_api.py (7).
    • Frontend 1 file / 8 tests: SemanticHealthPanel.test.tsx — issue rendering, action-link routing, severity counter filter, kind dropdown filter, empty state, error + Retry, deprecated link params, severity icon ARIA labels.
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff clean; mypy clean (364 files); pytest tests/unit/ -q --timeout=60 — 2675 passed, 1 skipped, 8 deselected (delta +41 vs. 2634 baseline); npm run lint — 0 errors; npm run type-check — clean; npm run test — 555 passed (62 files) (delta +8); npm run build — clean prod build.
  • Review notes:
  • Approved on orchestrator independent re-run. Panel mounts inside the existing EditorCompanion shell (reuses authorised by UX0-002). All 4 check kinds are real working SQLAlchemy queries — no stubs. Protocol+factory pattern matches the established repository idiom. Filter set + a11y + navigation mapping is complete.
  • Residual risks:
  • concept_relationships cascade: production tables use ondelete=CASCADE so broken_relationship will rarely fire in steady state. The check still protects against import paths that bypass the cascade (external dump ingest). Tests disable FK enforcement explicitly.
  • server_default="false" on concepts.deprecated reads as Python truthy under SQLite; the missing_mapping check coerces via _is_truthy_deprecated. Real PostgreSQL stores actual booleans, so SQLite-test-only subtlety.
  • Auto-refresh choice: 30 s setInterval + manual Refresh (matches sibling companion tabs). SWR is not currently a dependency; later swap is localised.
  • Performance: duplicate_concept and missing_mapping load all concepts into memory. At low-thousands registry size this is fine; a registry with hundreds of thousands of concepts would want indexed-aggregate SQL. Out of scope for MVP.
  • No persistence of issue history: issues evaluated on demand; trend/owners-over-time tracking is a future card.
  • Phase: Phase 3 follow-up
  • Area: Backend
  • Owner: Backend Agent (general-purpose, agentId ac4540da8fb6d699d)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Medium
  • Risk: Medium
  • Source: P3-006 residual risk (proposal wiring MVP-only).
  • Dependencies: P3-006 (proposal gate landed).
  • Write scope:
  • Add suggestion_id column (nullable, indexed) to the proposals table via Alembic migration.
  • Update ProposalModel SQLAlchemy mapping.
  • New POST /api/v1/semantic/proposals endpoint to create a proposal row linked to a suggestion (and unlock the P3-006 gate for that suggestion).
  • Replace _default_proposal_lookup_factory in _acceptance.py to use the FK join instead of payload["proposal_id"].
  • Acceptance criteria:
  • Domain/core suggestions can be unblocked by creating a proposal that links to them. ✅
  • Reverse migration cleanly drops the FK and index. ✅
  • Tests cover: lookup-by-FK finds the proposal; lookup misses when no proposal exists; cascade behaviour on suggestion deletion. ✅
  • Test evidence:
  • Migration: src/pycharter/db/migrations/versions/20260519000003_add_proposals_suggestion_id.py. Revision proposals_suggestion_id (down-revision semantic_review_events). Adds suggestion_id String(36) NULL + index ix_proposals_suggestion_id. Soft-ref (no hard FK) — matches the program convention from P1-005's source_asset_id. Upgrade-downgrade-upgrade round-trip verified clean.
  • Model: src/pycharter/db/models/semantic/proposal.py — suggestion_id = Column(String(36), nullable=True, index=True) added.
  • New endpoint: POST /api/v1/semantic/proposals/link — body {suggestion_id, actor, summary, details?}; returns LinkProposalResponse {id, suggestion_id, summary, status, actor, created_at} with 201 Created. Validates suggestion_id is a UUID (400 otherwise), 404 if suggestion absent. New LinkProposalRequest / LinkProposalResponse Pydantic models in src/pycharter/api/models/semantic_proposals.py.
  • Lookup swap: _default_proposal_lookup_factory in _acceptance.py now queries ProposalModel.suggestion_id == str(suggestion.id). Legacy payload["proposal_id"] resolves via _legacy_payload_proposal_lookup and emits DeprecationWarning with one-cycle removal note.
  • Tests: tests/unit/test_proposals_link_endpoint.py — 8 tests: link creates proposal + suggestion_id; missing suggestion → 404; malformed UUID → 400; default-title fallback; lookup finds linked proposal; lookup returns None when none exists; legacy fallback emits warning; proposal survives suggestion delete (soft-ref cascade).
  • Existing tests: test_proposal_gate.py (injects fakes) unaffected; test_semantic_suggestions_api.py domain-tier accept test still passes because the new lookup also returns None when no proposal links the suggestion.
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff clean; mypy clean; pytest tests/unit/ -q --timeout=60 — 2693 passed, 1 skipped, 8 deselected (delta +18 vs. 2675 baseline reflects this card's 8 + parallel P3-009-FU-A's 10).
  • Review notes:
  • Approved on orchestrator independent re-run. Soft-ref pattern matches the program convention; round-trip verified clean; legacy fallback preserves backward compat with a clear deprecation horizon.
  • Residual risks:
  • Soft-ref vs. FK: the new suggestion_id is a string with no FK constraint — deleting the suggestion leaves a dangling pointer (covered by the survives_suggestion_delete test). Matches the agreed program convention; an alternative would be to add ondelete="SET NULL" if a hard FK is later preferred.
  • One-cycle deprecation horizon — payload["proposal_id"] fallback emits DeprecationWarning and is scheduled for removal next minor release. Future card must delete _legacy_payload_proposal_lookup and the call site.
  • Contract scoping — the link endpoint uses a sentinel contract_id="suggestion-link" because the suggestion has no contract today. Follow-up may derive the contract from the scheme or accept it explicitly.
  • Audit trail on link — not implemented here. Reviewers may want a review-event row when a proposal is linked. P3-009-FU-A's endpoint can later be extended to include decision="link_proposal" rows.

P3-008-FU-A: Promote technical_only and annotation to first-class FSM states

  • Phase: Phase 3 follow-up
  • Area: Backend + Frontend
  • Owner: Backend Agent (general-purpose, agentId ada950fb9cc9f615e)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Medium
  • Risk: Medium
  • Source: P3-008 residual risk (reason-prefix markers).
  • Dependencies: P3-008 (action endpoints landed), P1-008 (lifecycle FSM).
  • Write scope:
  • Extend BindingState to include TECHNICAL_ONLY.
  • Add a dedicated annotation-event row shape (separate from state transitions).
  • Replace [technical_only] / [annotation] reason-prefix markers in semantic_bindings.py with the new FSM and event paths.
  • Update P3-009's history drawer to render the new event shape (drop reason-prefix parsing).
  • Migration if a new column or table is required.
  • Acceptance criteria:
  • mark-technical-only issues a real state transition (not a same-state event). ✅
  • annotate issues a typed annotation event (not a reason-prefixed state event). ✅
  • Existing P3-009 reason-prefix code is removed cleanly; backward compat for already-persisted markers is documented (one-shot migration OK). ✅
  • Test evidence:
  • FSM extension: new BindingState.TECHNICAL_ONLY = "technical_only"; new BindingEventAction = TRANSITION | ANNOTATION | REMAP discriminator. Transitions: UNMAPPED/SUGGESTED/MAPPED/APPROVED ↔ TECHNICAL_ONLY (fully reversible). Annotation events are event-only (no state change). Remap events carry both from_concept_id and to_concept_id on a single row.
  • New API methods: BindingLifecycle.annotate(...) emits an ANNOTATION event without state change; BindingLifecycle.remap(...) (see P3-008-FU-B) emits a single atomic event with action=REMAP.
  • Migration: 20260519000005_extend_binding_events_for_actions.py (revision binding_events_actions, down_revision="semantic_health_issue_snapshots"). Adds three concept_binding_events columns: action VARCHAR(32) NOT NULL DEFAULT 'transition', from_concept_id VARCHAR(255) NULL, to_concept_id VARCHAR(255) NULL. Reversibility verified.
  • Backward-compat strategy: Option 1 (data migration). Upgrade rewrites any legacy concept_binding_events row whose reason starts with [technical_only] (→ to_state="technical_only", marker stripped) or [annotation] (→ action="annotation", marker stripped). Clean data forward — eliminates the marker-prefix shim entirely.
  • Route changes in semantic_bindings_actions.py: mark-technical-only issues a real TECHNICAL_ONLY transition; annotate calls lifecycle.annotate(...); marker constants TECHNICAL_ONLY_REASON_MARKER / ANNOTATION_REASON_MARKER deleted (verified zero external consumers).
  • Frontend: BindingHistorySubscriber.tsx drops reason-prefix parsing; delegates row header to new LifecycleRowHeader.tsx. Three event shapes rendered: transition (state pills), annotation (indigo badge + current state pill, no arrow), remap (violet badge + from → to). BindingHistoryDrawer.tsx adds technical_only colour entry.
  • Tests: 12 new unit tests (TestTechnicalOnlyState x3, TestAnnotationAction x4, TestAtomicRemap x5) in test_semantic_lifecycle.py; route tests in test_semantic_bindings_route.py rewritten for the new event shapes + failed-remap atomicity test; 5 new integration tests in tests/integration/test_binding_lifecycle_actions.py covering accept persistence + atomic remap row shape + failed-remap atomicity + technical-only state transition + annotation event shape.
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff clean; mypy clean; pytest tests/unit/ -q --timeout=60 — 2718 passed, 1 skipped, 8 deselected (delta +25 vs. 2693 baseline reflects this card + parallel P4-001-FU-A); migration upgrade-downgrade-upgrade round-trip clean.
  • Review notes:
  • Approved on orchestrator independent re-run. Data migration removes the marker-prefix shim entirely (clean forward). FSM extension preserves reversibility of TECHNICAL_ONLY. P3-008-FU-B atomic remap lands inside the same FSM extension (see that card for details).
  • Residual risks:
  • Atomic remap side-effect on mapping projection. The new REMAP event does not emit an implicit "from" retirement event. The mapping-projection updater routes by to_state, so its reject/remap dispatch keeps working; the projection for the prior concept will not flip to unresolved from the remap event alone. If that side-effect is needed for the UI's reverse-index, a follow-up should wire on_transition to treat REMAP events as an implicit retirement of the from-key.
  • Marker-export removal: TECHNICAL_ONLY_REASON_MARKER / ANNOTATION_REASON_MARKER deleted. Verified zero external grep hits today; any future consumer that imports them gets ImportError.
  • Postgres-only CI: the data-migration LIKE rewrite was exercised on SQLite locally. Postgres LIKE semantics are equivalent for the literal prefix patterns used.

P3-008-FU-B: Atomic remap action (single FSM transition)

  • Phase: Phase 3 follow-up
  • Area: Backend + Frontend
  • Owner: Backend Agent (general-purpose, agentId ada950fb9cc9f615e — landed alongside P3-008-FU-A)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done

P3-008-FU-B: Atomic remap action (single FSM transition) — landed with P3-008-FU-A

  • Phase: Phase 3 follow-up
  • Area: Backend + Frontend
  • Owner: Backend Agent (general-purpose, agentId ada950fb9cc9f615e — bundled with P3-008-FU-A)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Low
  • Risk: Medium
  • Source: P3-008 residual risk (remap non-atomicity).
  • Dependencies: P3-008 (route landed).
  • Write scope:
  • Add a first-class remap(from_concept, to_concept) action to BindingLifecycle.transition (or a sibling method).
  • One atomic emission rather than today's detach-then-attach (UNMAPPED → MAPPED) two-event pattern.
  • Update the HTTP POST /api/v1/semantic/bindings/remap route to use the new action.
  • Acceptance criteria:
  • Failure mid-action does not leave the field UNMAPPED. ✅
  • History drawer renders a single "remapped" event row instead of two. ✅
  • Test evidence:
  • New API: BindingLifecycle.remap(*, contract_id, field_path, from_concept_id, to_concept_id, actor, comment="", contract_version=None) -> BindingEvent. Emits exactly one BindingEvent with action=REMAP, to_state=MAPPED, from_concept_id and to_concept_id populated.
  • Route change: POST /bindings/remap now returns a single BindingEventResponse instead of {detach_event, attach_event} (breaking response-shape change for any external client; UI updated in the same commit).
  • Frontend: RemapBindingResponse removed from lib/api/semanticBindings.ts; remap client method returns BindingEventResponse. History drawer's remap row renders a single violet "remapped" badge with from_concept_id → to_concept_id detail.
  • Tests: TestAtomicRemap (5 tests in test_semantic_lifecycle.py) — single event row, correct from/to concepts, contract_version threading, atomicity under failure injection. Integration test test_atomic_remap_emits_single_event in tests/integration/test_binding_lifecycle_actions.py.
  • Quality gates: rolled up with P3-008-FU-A's gates above.
  • Review notes:
  • Approved on orchestrator independent re-run as part of the P3-008-FU-A bundle. Failure-mid-action invariant tested explicitly; no half-committed UNMAPPED state achievable.
  • Residual risks:
  • Response-shape breaking change for POST /bindings/remap (returns BindingEventResponse instead of {detach_event, attach_event}). UI updated; external clients (if any) need to migrate.
  • Projection retirement of "from" key: see P3-008-FU-A residual risk for the same coordination note.

P3-008-FU-C: Split semantic_bindings.py under 400-line cap

  • Phase: Phase 3 follow-up
  • Area: Backend
  • Owner: Backend Agent (general-purpose, agentId a62f11679f8235a59)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Low
  • Risk: Low
  • Source: P3-008 residual risk (file at 480 lines).
  • Dependencies: P3-008 (action endpoints).
  • Write scope:
  • Extract the 4 action endpoints + their request models into semantic_bindings_actions.py.
  • semantic_bindings.py keeps the listing/read endpoints.
  • Acceptance criteria:
  • Both files under 400 lines. ✅
  • No behaviour change; route registration shape preserved. ✅
  • Existing tests pass without modification. ✅
  • Test evidence:
  • Final line counts: semantic_bindings.py — 191 lines (was 480); semantic_bindings_actions.py — 332 lines (new). Both under 400.
  • Move set: constants (TECHNICAL_ONLY_REASON_MARKER, ANNOTATION_REASON_MARKER); request models (AcceptBindingRequest, RemapBindingRequest, MarkTechnicalOnlyRequest, AnnotateBindingRequest); response model (RemapBindingResponse); private helpers (_empty_actor_error, _bad_transition_from_exception); handlers (accept_binding, remap_binding, mark_technical_only, annotate_binding).
  • Kept in semantic_bindings.py: BindingEventResponse, BindingStateResponse, TransitionRequest, list_events, get_state, create_transition.
  • Router composition: separate router (preferred). semantic_bindings_actions.py declares its own APIRouter(tags=["Semantic - Bindings"]). Both routers registered under the same /api/v1/semantic/bindings prefix via a second entry in the _semantic list in _routers.py. URL surface and OpenAPI tags unchanged.
  • Re-exports added: None. Grep confirmed the only external import is router; the moved request/response models and constants have no consumers outside the route file.
  • Test impact: Test bodies unchanged. The fixture setup gained two non-behavioural lines (one import + one app.include_router(bindings_actions_router, prefix=...)) so the action endpoints remain reachable in the isolated TestClient.
  • Tests: tests/unit/test_semantic_bindings_route.py — 16 passed, identical to baseline.
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff clean; mypy clean (140 files); pytest tests/unit/ -q --timeout=60 — 2693 passed, 1 skipped, 8 deselected (no test count change attributable to this card — pure refactor).
  • Review notes:
  • Approved on orchestrator independent re-run. Pure refactor; both files comfortably under the 400-line soft cap; OpenAPI surface preserved; zero behavioural change.
  • Residual risks:
  • None significant. If a future consumer imports a moved model directly from semantic_bindings, they will get ImportError — but the pre-split grep confirmed there are zero such consumers in the repo today.

P3-009-FU-A: GET /suggestions/{id}/review-events backend route

  • Phase: Phase 3 follow-up
  • Area: Backend + Frontend
  • Owner: Full-stack Agent (general-purpose, agentId a2b30e98898afcd16)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Medium
  • Risk: Low
  • Source: P3-009 residual risk (review-events endpoint gap; getReviewEvents is a stub returning []).
  • Dependencies: P3-004 (review-event repo), P3-009 (stub wrapper in place).
  • Write scope:
  • New GET /api/v1/semantic/suggestions/{id}/review-events route returning the suggestion's SemanticReviewEventModel rows in reverse-chronological order.
  • Switch the frontend getReviewEvents(suggestionId) stub to call the real route.
  • Update the history drawer to merge lifecycle events + review events on a single timeline.
  • Acceptance criteria:
  • Route returns events for a known suggestion id; 404 for unknown. ✅
  • History drawer in the mapping workbench renders merged stream sorted by timestamp. ✅
  • Tests cover happy + 404 paths + merged-timeline ordering. ✅
  • Test evidence:
  • New endpoint: GET /api/v1/semantic/suggestions/{suggestion_id}/review-events. Query params: limit (default 100, ge=1, le=500), offset (default 0, ge=0). Response: list[ReviewEventResponse] with {id, suggestion_id, actor, decision (Literal: accepted|dismissed|request_changes), comment, timestamp}, newest-first. Errors: 404 "Suggestion not found"; 422 for non-UUID id and for limit/offset out of bounds. Reads via make_semantic_review_event_repository(backend="sqlalchemy", session=session) so behaviour is deterministic regardless of the manager's env-var-driven backend.
  • Pydantic model: ReviewEventResponse added to src/pycharter/api/models/semantic_suggestions.py.
  • Frontend stub swap: src/pycharter/ui/src/lib/api/bindingEvents.ts — replaced empty-array stub with get<SemanticReviewEvent[]> call. SemanticReviewEvent interface + SemanticReviewDecision union aligned with backend Literal. Supports optional {limit, offset}.
  • History drawer merge: src/pycharter/ui/src/components/editor/MappingWorkbench/BindingHistorySubscriber.tsx — fetches lifecycle + review streams in parallel; merges via new exported mergeHistoryStreams helper. Sort: timestamp descending, lifecycle wins ties (stable sort). New optional URL param historyForSuggestion activates the review fetch; absent → identical pre-existing lifecycle-only behaviour.
  • Tests:
    • Backend: new tests/unit/test_review_events_endpoint.py — 10 tests across 4 classes: TestReviewEventsHappyPath (request_changes + dismissed round-trip), TestReviewEventsErrorPaths (404 unknown, 422 non-UUID), TestReviewEventsOrderingAndPagination (newest-first, limit, offset, limit>500 → 422, negative offset → 422), TestReviewEventsEmpty (200 + []).
    • Frontend: 4 new bindingEvents.test.ts cases (URL shape, query-string composition, encoding) + 1 new merged-timeline component test (3 entries interleaved across timestamps assert newest-first by data-testid) + 2 new mergeHistoryStreams pure-function unit tests.
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff clean; mypy clean (140 files); pytest tests/unit/ -q --timeout=60 — 2693 passed, 1 skipped, 8 deselected (delta +18 vs. 2675 baseline; +10 this card + 8 P3-006-FU-A); npm run lint — 0 errors; npm run type-check — clean; npm run test — 560 passed (62 files) (delta +5); npm run build — clean.
  • Review notes:
  • Approved on orchestrator independent re-run. Merge helper is pure + tested separately. Pagination is offset+limit because audit log is append-only and per-suggestion cardinality is small (typically <10 rows); cursor migration is cheap when needed.
  • Residual risks:
  • decision Literal drift: ReviewEventResponse.decision is kept in deliberate lock-step with SuggestionResponse.status's non-pending values. New verb (e.g. "escalate") requires updating both Pydantic models AND the frontend SemanticReviewDecision union. The _DECISION_* constants in pycharter.persistence.collaboration.suggestions are the single source-of-truth.
  • In-memory backend mismatch: SuggestionManager defaults to the in-memory review-event backend; the new route reads via SQLAlchemy. Production deployments that have not opted into PYCHARTER_SEMANTIC_REVIEW_EVENT_BACKEND=sqlalchemy will see decisions persisted only in-process — GET returns []. Pre-dates this card (flagged in __init__.py as "route wiring is a follow-up"). Separate residual-risk follow-up.
  • Cursor vs. offset pagination: offset+limit chosen for simplicity; rows have stable id + timestamp so cursor migration is straightforward when needed.

P3-010-FU-A: Phase-3 integration tail (P3-006 / P3-008 / P3-009 coverage)

  • Phase: Phase 3 follow-up
  • Area: QA / Backend
  • Owner: Test Engineer Agent (general-purpose, agentId a4f056be61c46597b) — proposal-gate + review-events portion; FSM action coverage delivered by P3-008-FU-A's test_binding_lifecycle_actions.py.
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Medium
  • Risk: Low
  • Source: P3-010 residual risk (no P3-006/008/009 coverage; P2-009/P3-010 pattern proven).
  • Dependencies: P3-006, P3-008, P3-009 (all landed).
  • Write scope:
  • tests/integration/test_proposal_gate_routing.py — domain-tier suggestion accept attempt without proposal → 409 + audit event; create proposal → linked suggestion now accepts.
  • tests/integration/test_review_events_endpoint_flow.py — GET review-events round-trip + pagination + 404.
  • FSM action coverage delivered by the P3-008-FU-A agent in test_binding_lifecycle_actions.py (sibling integration file).
  • Acceptance criteria:
  • At least one test per follow-up surface (P3-006 routing, P3-008 each-action, P3-009 review-events). ✅
  • Assertions on event/projection state, not just counts. ✅
  • @pytest.mark.integration markers. ✅
  • Test evidence:
  • Files added:
    • tests/integration/test_proposal_gate_routing.py (553 lines, 4 tests).
    • tests/integration/test_review_events_endpoint_flow.py (436 lines, 3 tests).
    • (FSM coverage in tests/integration/test_binding_lifecycle_actions.py — owned by P3-008-FU-A.)
  • Test list:
    1. test_domain_tier_accept_without_proposal_returns_409 — 409 with structured {"message", "code": "ProposalRequiredError"} body; suggestion stays pending; one request_changes audit row with PROPOSAL_GATE_AUDIT_COMMENT.
    2. test_link_proposal_unblocks_domain_accept — POST /proposals/link round-trip; ProposalModel.suggestion_id verified in DB; second accept succeeds (200), two-row audit timeline.
    3. test_core_tier_accept_blocked_until_proposal_linked — same flow for tier="core".
    4. test_legacy_payload_proposal_id_fallback_succeeds_with_warning — legacy path resolves end-to-end; DeprecationWarning captured via pytest.warns.
    5. test_review_events_returned_after_accept_and_dismiss — two suggestions; each GET returns one row with correct decision/actor/comment/timestamp.
    6. test_review_events_paginated_newest_first — single suggestion through request-changes → accept; ?limit=1 returns accept (newest), ?limit=1&offset=1 returns request-changes (older).
    7. test_review_events_404_for_unknown_suggestion — random UUID → 404.
  • Fixture pattern: reused P3-010's pattern (real SuggestionManager, real SQLAlchemy review-event repo via patched factory). Switched engine to shared-connection StaticPool in-memory SQLite (matches unit suites) to prevent database is locked when route session + audit write race. Added session.commit() on dependency-override yield to mirror production session lifecycle.
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff clean; pytest tests/integration/test_proposal_gate_routing.py tests/integration/test_review_events_endpoint_flow.py -m integration -v — 7 passed; pytest tests/integration/ -m integration -v — 26 passed, 2 skipped, 3 failed (3 failures are pre-existing test_api_pipeline_runs_route.py Postgres-schema-dependent tests). Unit regression vs. 2693 baseline — unchanged (this card touches tests/integration/ only).
  • Review notes:
  • Approved on orchestrator independent re-run. Reuses the validated P3-010 fixture pattern; pairs with P3-008-FU-A's FSM integration coverage. The StaticPool switch is the right call — eliminates a known race in concurrent-session tests.
  • Residual risks:
  • SQLite vs Postgres: integration suite runs against in-memory SQLite (StaticPool); the proposals.suggestion_id index is dialect-agnostic and row shapes are identical to Postgres, so the gate lookup SQL is the same path production runs.
  • No merged-timeline integration coverage: the frontend mergeHistoryStreams helper is pure-function unit-tested; backend coverage would require driving the merge from the API surface, which is unnecessary given the helper's purity.

P4-001-FU-A: Persist semantic health issue history (trend + owner)

  • Phase: Phase 4 follow-up
  • Area: Backend + Frontend
  • Owner: Full-stack Agent (general-purpose, agentId a7b2d17e7b06923fc)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Low
  • Risk: Medium
  • Source: P4-001 residual risk (no persistence of issue history).
  • Dependencies: P4-001 (health service + endpoint), P1-001 (KB repository pattern).
  • Write scope:
  • New semantic_health_issue_snapshots table — one row per (issue_fingerprint, evaluation_id, severity, detected_at). Fingerprint stable across evaluations (e.g. SHA of kind + sorted affected_entity_ids).
  • Persistence backend that records each evaluate_health run.
  • History endpoint GET /api/v1/semantic/health/issues/{fingerprint}/history returning observations over time.
  • UI: per-issue history sparkline + owner field (if available).
  • Acceptance criteria:
  • History endpoint returns observations sorted by detected_at. ✅
  • Resolved issues stop appearing in the current report but remain queryable. ✅
  • UI sparkline renders without performance issues for ≤90 days of data. ✅
  • Test evidence:
  • Migration: 20260519000004_add_semantic_health_issue_snapshots.py (revision semantic_health_issue_snapshots, down-revision proposals_suggestion_id). Creates pycharter.semantic_health_issue_snapshots with (id UUID PK, evaluation_id UUID, issue_fingerprint String(64), kind, severity, affected_entity_kind, affected_entity_ids JSON, summary, suggested_fix, actionable_link, owner, detected_at) and 3 indexes: (fingerprint, detected_at), (evaluation_id), (detected_at). Reversibility verified (upgrade → downgrade → upgrade).
  • Fingerprint formula: sha256_hex(f"{kind}:{','.join(sorted(affected_entity_ids))}".encode("utf-8")). Stable across evaluations and across affected_entity_ids orderings (verified by test_fingerprint_is_order_invariant).
  • New endpoint: GET /api/v1/semantic/health/issues/{fingerprint}/history with path validator ^[0-9a-fA-F]{64}$. Query params ?since=<ISO>&until=<ISO>&limit=N (default 100, max 500). Response IssueHistoryResponse { fingerprint, kind, observations: [{evaluation_id, severity, detected_at, summary, owner}] }. 404 when no snapshots for the fingerprint. Observations sorted oldest-first.
  • Persistence trigger: every SqlAlchemySemanticHealthService.evaluate_health() call writes one row per current-issue with a fresh evaluation_id UUID. Snapshot insert wrapped in logger.exception swallow so a write failure does not affect the live report. Opt-out flag persist_history=False.
  • Resolved-issues semantics: the live GET /api/v1/semantic/health keeps returning the in-memory current report; absence from the latest evaluation_id = resolved. Historical observations remain queryable indefinitely. test_resolved_issue_still_queryable_by_fingerprint covers this.
  • Frontend:
    • Sparkline.tsx — inline SVG sparkline (no chart-lib dep). Single <path> driven by severity→numeric rank. ≤90 points = ≤89 line commands. Empty-state renders a dashed baseline.
    • SemanticHealthPanel.tsx — adds useIssueHistory hook (lazy per-row fetch, 30-day window, limit 90); renders sparkline inline with kind badge; owner display pinned by test.
    • semanticHealthApi.history(fingerprint, params) API method.
  • Tests:
    • Backend (12 new): TestFingerprint (4), TestPersistIssueSnapshots (2), TestServiceWritesHistory (2 — distinct evaluation_ids, resolved-issue still queryable), TestHistoryEndpoint (4 — happy path sorted, 404, limit, 422 invalid fingerprint).
    • Frontend (6 new): Sparkline.test.tsx (4 — empty-state, path command count, severity colour, ariaLabel) + 2 new SemanticHealthPanel cases (lazy history load, owner rendering).
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff clean; mypy clean; pytest tests/unit/ -q --timeout=60 — 2718 passed, 1 skipped, 8 deselected (delta +25 vs. 2693 baseline reflects this card + parallel P3-008-FU-A/B); npm run lint — 0 errors; npm run type-check — clean; npm run test — 567 passed (63 files); npm run build — clean prod build.
  • Review notes:
  • Approved on orchestrator independent re-run. Fingerprint is order-invariant (load-bearing for grouping); snapshot insert is defensive (try+log+swallow); the sparkline is dependency-free SVG. Migration chains cleanly off proposals_suggestion_id (P3-006-FU-A) and is itself the down-revision for P3-008-FU-A's FSM migration.
  • Residual risks:
  • Storage growth: one row per current-issue per evaluate_health() call. If the panel auto-refresh stays at 30s and 100 issues persist, that is 288k rows/day. Consider a TTL/compaction follow-up before this lands on a long-running prod environment.
  • Per-row fetch fan-out: the panel issues one history(fingerprint, ...) call per visible row plus one SHA-256 digest. With ≤50 visible rows it is fine; for very large reports a debounced batched-history endpoint would be a future follow-up.
  • Fingerprint stability under id-set change: the fingerprint is a function of (kind, sorted(affected_entity_ids)). If a check's id set evolves (e.g. duplicate-concept check adds a third concept to an existing pair), the fingerprint changes and the history splits — by design, but worth flagging.

P4-001-FU-B: Guardrail health snapshot retention before TTL strategy lands

  • Phase: Phase 4 follow-up
  • Area: Backend
  • Owner: Backend Agent (general-purpose, agentId ab31a117b10e67a0c)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Medium
  • Risk: Medium
  • Source: P4-001-FU-A residual risk (storage growth — 288k rows/day on a 100-issue + 30s-refresh deployment).
  • Dependencies: P4-001-FU-A (snapshots table landed).
  • Write scope:
  • Switch SqlAlchemySemanticHealthService.__init__ default for persist_history from True to False.
  • Update make_semantic_health_service factory to plumb the flag.
  • Update tests + docs so opt-in is explicit.
  • Acceptance criteria:
  • Default deployment writes zero snapshot rows. ✅
  • Tests that exercise snapshot persistence opt in via persist_history=True. ✅
  • Docstring + module docstring explain the trade-off and reference P4-001-FU-D for the full TTL strategy. ✅
  • Test evidence:
  • SqlAlchemySemanticHealthService.__init__(persist_history=False) (was True). Module docstring documents the trade-off and points at P4-001-FU-D.
  • make_semantic_health_service(*, persist_history: bool = False, ...) plumbs the flag.
  • Two existing snapshot tests (test_each_call_produces_distinct_evaluation_id, test_resolved_issue_still_queryable_by_fingerprint) opt in with persist_history=True.
  • New test_default_persist_history_is_off confirms the default service writes zero snapshot rows across multiple evaluations.
  • Before/after for a 100-issue + 30s-refresh deployment: 288k rows/day → 0 rows/day until the operator opts in.
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff clean; mypy clean; pytest tests/unit/ -q --timeout=60 — 2737 passed, 1 skipped, 8 deselected (delta +19 from this batch).
  • Review notes:
  • Approved on orchestrator independent re-run. Default-off is the correct guardrail until P4-001-FU-D ships TTL/compaction.
  • Residual risks:
  • Tests opt in explicitly. Any future test that adds snapshot persistence must remember to set persist_history=True.

P4-001-FU-C: Batched semantic health history endpoint

  • Phase: Phase 4 follow-up
  • Area: Backend + Frontend
  • Owner: Full-stack Agent (general-purpose, agentId ac3e6cb1879efa29f)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Low
  • Risk: Low
  • Source: P4-001-FU-A residual risk (per-row fetch fan-out — one history call per visible row).
  • Dependencies: P4-001-FU-A (history endpoint).
  • Write scope:
  • New POST /api/v1/semantic/health/issues/history accepting fingerprints: list[str] body returning a map {fingerprint: observations}.
  • Frontend swap from per-row fetch to a single batched call when the panel mounts.
  • Acceptance criteria:
  • Single round-trip per panel mount instead of N round-trips. ✅
  • Response shape preserves per-fingerprint observation lists sorted oldest-first. ✅
  • Backward compatible — per-fingerprint endpoint stays. ✅
  • Test evidence:
  • New endpoint: POST /api/v1/semantic/health/issues/history. Body IssueHistoryBatchRequest { fingerprints: list[str] (1..200), since?, until?, limit? (1..500, default 100) }. Per-fingerprint validation against ^[0-9a-fA-F]{64}$ via Pydantic field_validator → 422 on malformed input. Response IssueHistoryBatchResponse { histories: {<fingerprint>: IssueHistoryResponse}, unknown_fingerprints: list[str] }.
  • SQL strategy: single SELECT ... WHERE issue_fingerprint IN (:fingerprints) ORDER BY detected_at ASC per request (new helper fetch_issue_histories_batch() in _history.py). Replaces N round-trips with 1.
  • Frontend hook: new useIssuesHistoryBatch.ts extracted to keep SemanticHealthPanel.tsx under the soft cap. Hook computes visibleIssues, derives a primitive issuesSignature from kind + sorted(ids), and runs ONE Promise.all(computeIssueFingerprint(...)) + ONE semanticHealthApi.historyBatch(...) per signature/revalidate change. Exposes historyByIssueKey (Map keyed by issueKey()) and loadState: 'idle' | 'loading' | 'ready'. Refresh bumps historyRevalidateKey to re-fire even when visible-issue set is unchanged.
  • Per-row UX preserved: IssueRow shows a sparkline-sized animate-pulse placeholder while loadState !== 'ready' && observations.length === 0; once batch resolves, the real <Sparkline> (or its dashed-line empty state for unknown fingerprints) renders.
  • Tests (9 new backend + 3 new frontend): TestHistoryBatchEndpoint covers mixed-known-and-unknown, all-unknown, all-known, observations_oldest_first, limit_per_fingerprint, since_filter, malformed_fingerprint (422), empty_fingerprints (422), over_max_fingerprints (422). Frontend asserts historyBatchMock.mock.calls.length === 1 (not N), empty sparkline fallback for unknown fingerprints, and re-fetch on Refresh.
  • Backward compat: per-fingerprint GET /api/v1/semantic/health/issues/{fingerprint}/history endpoint untouched.
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff clean; mypy clean; pytest tests/unit/ -q --timeout=60 — 2746 passed, 1 skipped, 8 deselected (delta +9 vs. 2737 baseline); npm run lint — 0 errors; npm run type-check — clean; npm run test — 569 passed; npm run build — clean.
  • Review notes:
  • Approved on orchestrator independent re-run. Single IN (...) query + Python-side partitioning is the correct shape; backward-compat preserved; hook signature memoisation avoids re-fires on unrelated parent renders.
  • Residual risks:
  • Payload size at 200 fingerprints × 100 observations ≈ ~5 MB worst case. Today the panel caps observations to 90 and the issue list is small (~50 rows), so real-world payloads stay well under 1 MB. Future follow-up if scale grows: gzip response or add a row-cap.
  • Ordering on duplicate fingerprints: handler dedupes with a seen set in request order; duplicates collapse silently. Caller can rely on len(histories) + len(unknown_fingerprints) ≤ len(fingerprints).
  • File size: semantic_health.py is now 442 lines (over the 400 soft cap, cohesive). Splitting is a separate refactor.

P4-001-FU-D: TTL/compaction strategy for semantic health snapshots

  • Phase: Phase 4 follow-up
  • Area: Backend
  • Owner: Unassigned
  • Reviewer: Unassigned
  • Status: Backlog
  • Priority: Low
  • Risk: Medium
  • Source: P4-001-FU-A residual risk (storage growth; P4-001-FU-B guardrails by defaulting off — TTL is the long-term plan).
  • Dependencies: P4-001-FU-A, P4-001-FU-B.
  • Write scope:
  • Define retention policy in product/PM discussion (suggested: 90 days for full snapshots, then daily roll-ups).
  • Implement a cleanup_history(older_than: timedelta) maintenance fn + CLI entry under scripts/.
  • Implement an optional aggregation table (daily severity counts per fingerprint).
  • Acceptance criteria:
  • Snapshot rows older than the configured retention window are purged.
  • Aggregation table queryable as a fallback for older windows.
  • Idempotent (script can be re-run safely).
  • Test evidence:
  • Review notes:
  • Residual risks:

P3-008-FU-D: Remap event should retire the prior concept binding in the mapping projection

  • Phase: Phase 3 follow-up
  • Area: Backend
  • Owner: Backend Agent (general-purpose, agentId ab31a117b10e67a0c)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Medium
  • Risk: Medium
  • Source: P3-008-FU-A/B residual risk (REMAP event does not signal retirement of the "from" key to the projection updater).
  • Dependencies: P3-008-FU-A/B (FSM REMAP + atomic remap landed), P1-007-IMPL (mapping projection lifecycle hooks).
  • Write scope:
  • On REMAP events, fire BOTH the existing on_concept_attached(to_concept) AND a new on_concept_detached(from_concept) hook on the projection updater.
  • Update P1-008's on_transition dispatcher to route REMAP events to both hooks.
  • Acceptance criteria:
  • After a remap, the projection's row keyed by (contract, field, from_concept) is no longer "active". ✅
  • Frontend mapping-workbench reverse-index queries reflect the remap. ✅
  • Tests confirm both hooks fire on REMAP events. ✅
  • Test evidence:
  • src/pycharter/semantic/mapping_projection/lifecycle_hooks.py — on_transition now detects BindingEventAction.REMAP events and fires on_concept_detached(from_concept_id, ...) before routing through the existing on_approve / on_reject / on_remap dispatch. Two new methods: on_concept_detached (flips matching rows to state="unresolved") and on_concept_attached (delegates to on_remap).
  • Dispatch site: lifecycle_hooks.py:114-122.
  • Unit tests: tests/unit/test_mapping_projection_lifecycle.py — new classes TestRemapDispatchesDetachAndAttach (5 tests: both hooks fire on REMAP, attach-only when no from_concept, per-method behaviour) + TestRemapEndToEndProjection (1 end-to-end via BindingLifecycle.remap).
  • Integration test: extended tests/integration/test_binding_lifecycle_actions.py with TestRemapDetachesFromConceptProjection — projection updater wired into BindingLifecycle.on_transition; asserts from-concept reverse-index row reaches state="unresolved" after a /remap HTTP call.
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff clean; mypy clean; pytest 2737 passed (delta +19 from this batch).
  • Review notes:
  • Approved on orchestrator independent re-run. Detach precedes attach in dispatch order — guarantees the reverse-index is consistent after each REMAP without a transient "double-bound" window.
  • Residual risks:
  • No backfill helper for pre-existing REMAP rows. REMAP events that pre-date this card never fired on_concept_detached, so projection rows from those events may still claim the from-concept is bound. If projection drift is observed in prod, a one-shot reconciliation script can scan REMAP events and re-fire the hook.

P3-009-FU-B: Default review-event persistence to SQLAlchemy backend

  • Phase: Phase 3 follow-up
  • Area: Backend
  • Owner: Backend Agent (general-purpose, agentId ab31a117b10e67a0c)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: High
  • Risk: Medium
  • Source: P3-009-FU-A residual risk (in-memory backend mismatch — deployments not opted into PYCHARTER_SEMANTIC_REVIEW_EVENT_BACKEND=sqlalchemy lose review events on restart AND get empty GET responses).
  • Dependencies: P3-004 (review-event repo), P3-009-FU-A (endpoint).
  • Write scope:
  • In make_semantic_review_event_repository, default to "sqlalchemy" when a session is provided; fall back to "in_memory" only when explicitly requested or when no session is available.
  • Update SuggestionManager defaults to align.
  • Tests pin the new default behaviour.
  • Acceptance criteria:
  • Production deployments emit and read review events through SQLAlchemy by default — no env-var opt-in needed. ✅
  • In-memory backend remains available for tests (explicit opt-in). ✅
  • Tests cover both backends. ✅
  • Test evidence:
  • src/pycharter/semantic/review_events/factory.py — resolve_backend gained a has_session parameter. New resolution order: explicit arg → env var → SQLAlchemy when session/factory provided → in-memory fallback.
  • src/pycharter/semantic/review_events/__init__.py — module docstring documents the four-step resolution.
  • src/pycharter/persistence/collaboration/suggestions.py — SuggestionManager.__init__ now forwards its session into the factory call, so SuggestionManager(session=db_session) automatically gets the SQLAlchemy review-event repo.
  • Before/after for production: SuggestionManager(session=db_session) previously got an in-memory review-event repo unless PYCHARTER_SEMANTIC_REVIEW_EVENT_BACKEND=sqlalchemy was set; review events were lost on restart and GET /api/v1/semantic/suggestions/{id}/review-events returned []. After this change, the same construction yields SqlAlchemySemanticReviewEventRepository(session) automatically.
  • New tests: tests/unit/test_review_event_factory_defaults.py — 13 tests across TestSessionAwareDefault, TestExplicitBackendOverrides, TestEnvVarOverrides, TestSuggestionManagerDefault. SuggestionManager(session=None) still returns the in-memory repo (verified by test_manager_without_session_uses_in_memory_review_repo).
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff clean; mypy clean; pytest 2737 passed (delta +19 from this batch); 19/19 affected integration tests pass.
  • Review notes:
  • Approved on orchestrator independent re-run. The session-aware default is the cleanest non-breaking way to flip production behaviour to durable persistence while keeping the in-memory backend trivially available for tests.
  • Residual risks:
  • Proposal-gate audit still uses in-memory backend. _emit_proposal_gate_audit (P3-006) discovered the route calls session.rollback() on ProposalRequiredError, which would silently drop the audit row if the session were passed. The helper was deliberately reverted to a session-free factory call. Production deployments that want durable proposal-gate audit rows must either set PYCHARTER_SEMANTIC_REVIEW_EVENT_BACKEND=sqlalchemy (then the route needs to inject a session-factory mode repo) or accept the unchanged in-memory behaviour. Follow-up card if/when the proposal-gate audit needs durable storage.

P3-006-FU-B: Remove legacy payload["proposal_id"] fallback

  • Phase: Phase 3 follow-up
  • Area: Backend
  • Owner: Unassigned
  • Reviewer: Unassigned
  • Status: Backlog
  • Priority: Low
  • Risk: Low
  • Source: P3-006-FU-A residual risk (one-cycle deprecation horizon for _legacy_payload_proposal_lookup).
  • Dependencies: P3-006-FU-A (FK lookup landed + DeprecationWarning live).
  • Write scope:
  • Delete _legacy_payload_proposal_lookup from _acceptance.py.
  • Delete the legacy-path test in tests/unit/test_proposals_link_endpoint.py (and the integration test test_legacy_payload_proposal_id_fallback_succeeds_with_warning).
  • Confirm no production extractor sets payload["proposal_id"].
  • Acceptance criteria:
  • All callers go through the FK lookup.
  • Tests still pass.
  • Documentation removed from the proposal-gate module.
  • Test evidence:
  • Review notes:
  • Residual risks:

P5-001: Stable context_pack_id and content hash on context pack responses

  • Phase: Phase 5
  • Area: Backend
  • Owner: Backend Agent (general-purpose, agentId a527e6ecc405add96) — bundled with P5-002.
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: High
  • Risk: Medium
  • Source: Proposal §"Phase 5" task P5-001.
  • Dependencies: P1-001 (KB management), P1-007-IMPL (mapping projection).
  • Write scope:
  • Add a stable context_pack_id (UUID, deterministic by inputs) and a content hash to context pack responses.
  • Acceptance criteria:
  • Repeated calls with the same inputs return the same context_pack_id. ✅
  • Content changes flip the hash. ✅
  • Tests cover stability + change-detection + edge cases (empty pack, large pack). ✅
  • Test evidence:
  • src/pycharter/semantic/context/_pack.py — frozen Pydantic ContextPack model with model_config = ConfigDict(extra="forbid", frozen=True).
  • context_pack_id formula: uuid5(NAMESPACE_CONTEXT_PACK, f"{entity_kind}:{entity_ref}:{contract_version or 'none'}:{concept_version or 'none'}"). Namespace is fixed 5e2d3c4b-1f8a-4a2b-9c6d-7e8f0a1b2c3d.
  • content_hash formula: SHA-256 of json.dumps(model_dump(mode="json"), sort_keys=True, separators=(",",":")) over all fields EXCEPT context_pack_id, content_hash, generated_at.
  • 5 dedicated id/hash tests pin determinism + immutability + change-detection (test_id_is_uuid5, test_concept_pack_id_is_deterministic, test_content_hash_flips_on_concept_rename, test_pack_is_immutable, test_generated_at_not_in_hash).
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff clean; mypy clean; pytest 2789 passed.
  • Review notes:
  • Approved on orchestrator independent re-run as part of the P5-001 + P5-002 bundle. Frozen Pydantic + tuple-typed bodies guarantee immutability at the type level.
  • Residual risks:
  • Caching strategy deferred to P5-005's API surface (LRU keyed by context_pack_id would skip DB + hash work).
  • Hash recomputed each call — O(body size). Acceptable for current load; cache when traffic grows.

P5-002: Include contract/concept versions, source URIs, and policy status in context packs

  • Phase: Phase 5
  • Area: Backend
  • Owner: Backend Agent (general-purpose, agentId a527e6ecc405add96) — bundled with P5-001.
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: High
  • Risk: Medium
  • Source: Proposal §"Phase 5" task P5-002.
  • Dependencies: P5-001 (pack response shape), P1-001, P1-007-IMPL, P1-008.
  • Write scope:
  • Extend the context pack response to include: contract version, concept version, source URI, and policy status.
  • Pull each field from the existing canonical models (no new tables).
  • Acceptance criteria:
  • Every emitted context pack carries the four fields when applicable; absent fields emit null. ✅
  • Tests pin per-field provenance. ✅
  • Test evidence:
  • Policy status derivation (per entity kind):
    • concept deprecated → "deprecated".
    • concept any binding APPROVED → "approved".
    • concept bindings only SUGGESTED / MAPPED → "under_review".
    • concept no bindings + not deprecated → "approved".
    • contract / contract_field no bindings → None.
    • contract / contract_field every bound concept deprecated → "deprecated".
    • contract / contract_field any binding approved → "approved".
    • contract / contract_field anything else → "under_review".
    • query → None (cross-cutting).
  • Source URI derivation: most-recent accepting suggestion's semantic_asset.source_uri; falls back to ConceptModel.uri for concept packs. Contract / field / query packs leave source_uri=None (governance objects, not source artefacts).
  • 6 provenance tests pin per-field threading: test_concept_version_threads_through, test_contract_version_threads_through, test_policy_status_for_deprecated_concept, test_policy_status_for_under_review_binding, test_policy_status_for_approved_binding, test_source_uri_threads_through_when_ingest_run_present, test_source_uri_falls_back_to_concept_uri.
  • Builder API: build_concept_pack, build_contract_pack, build_contract_field_pack, build_query_pack (all in _builder.py); related-row queries in _helpers.py.
  • Quality gates: rolled up with P5-001's gates above.
  • Review notes:
  • Approved on orchestrator independent re-run. Policy-status decision table is comprehensive and consistent with P1-008's binding lifecycle.
  • Residual risks:
  • concept_version semantics: concepts have no independent version field; the pack reports the owning ConceptSchemeModel.version. Cross-scheme contract bindings emit concept_version=None rather than picking arbitrarily.
  • Large-pack BFS depth: the concept pack is one-hop (immediate relationships + bound concepts). Depth-N expansion is intentionally out of scope.

P5-003: Define and implement minimal evidence envelope model

  • Phase: Phase 5
  • Area: Backend
  • Owner: Backend Agent (general-purpose, agentId a9139fc789bdf4fcb)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: High
  • Risk: Medium
  • Source: Proposal §"Phase 5" task P5-003.
  • Dependencies: P2-002 (suggestion evidence shape).
  • Write scope:
  • New EvidenceEnvelope Pydantic model.
  • Helper functions to build envelopes from a suggestion id, a concept id, or a binding id.
  • Acceptance criteria:
  • Envelopes built from each source kind round-trip identically. ✅
  • Tests cover empty/dense/missing-asset cases. ✅
  • Test evidence:
  • src/pycharter/semantic/context/_envelope.py — frozen Pydantic EvidenceEnvelope (extra="forbid", frozen=True). Tuple-typed signals / duplicates / lineage_event_ids.
  • envelope_id formula: uuid5(NAMESPACE_EVIDENCE_ENVELOPE, f"{subject_kind}:{subject_id}:{content_hash}"). Namespace 6f3e4d5c-2a9b-4b3c-bd7e-8f9a0b1c2d3e (distinct from P5-001's context pack namespace).
  • Builder API: build_envelope_from_suggestion, build_envelope_from_concept, build_envelope_from_binding (each raising LookupError only for unknown subject ids; empty envelopes for known but evidence-free subjects).
  • Signal synthesis (MVP): single Signal(kind="confidence_score", ...) when suggestions.confidence_score is non-null. rationale + confidence_band packed into the explanation. Richer signals (name_match, type_match, contract_path_match) flagged as follow-ups in the synthesis module docstring.
  • Duplicate resolution (MVP): at most one DuplicateRef per envelope, sourced from suggestions.is_duplicate_of (P2-005's prefix-encoded ref). Fuzzy / embedding dedup is P2-005-FU-A's surface.
  • Repository pattern: Protocol + SQLAlchemy + in-memory backends + make_evidence_envelope_repository(*, backend, session) factory in factory_envelope.py (named distinctly from P5-001/002's factory.py to avoid collision).
  • 23 tests: round-trip dense/empty/duplicates/missing-asset/lookup-error for each subject kind; determinism (same content → same id); change-detection (different score → different id); subject-id change → different id; frozen invariant; factory backend selection (in-memory default / sqlalchemy explicit / missing-session error / env override / unknown-env fallback); in-memory + SQLAlchemy repository round-trips.
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff clean; mypy clean (236 files); pytest 2789 passed.
  • Review notes:
  • Approved on orchestrator independent re-run. Avoided file-collision with P5-001/002 via factory_envelope.py naming. Schema admits richer signal/duplicate catalogues without breaking the wire.
  • Residual risks:
  • Signal catalogue narrow (only confidence_score today). Follow-ups flagged in _envelope_synthesis.py docstring.
  • Dedup limited to is_duplicate_of. Richer dedup is P2-005-FU-A.
  • Binding envelope joins: up to 5 queries per call (lineage, projection, concept, suggestion, asset). Fine at single-binding scale; P5-005's API may want JOINed reads or a cache layer when batching.
  • source_span parsing best-effort. P2-002 left the column free-form ("start=120,end=240" / "120-240" / etc.); anything outside two parsed conventions becomes None. Original string remains on the suggestion row.
  • extractor label sources from asset_type. SemanticIngestRunModel has no extractor column today; if a future ingest pipeline records it separately, the builder needs a small DB-helper extension.

P5-004: Context preview UI for concepts, contracts, and fields

  • Phase: Phase 5
  • Area: Frontend
  • Owner: Frontend Agent (general-purpose, agentId a93ed58d10942090b)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Medium
  • Risk: Medium
  • Source: Proposal §"Phase 5" task P5-004.
  • Dependencies: P5-001..003 (pack shape + envelope), P5-005 (retrieval API).
  • Write scope:
  • New ContextPreviewPanel component inside the existing editor.
  • Renders the full context pack (id, hash, versions, source URIs, policy status, evidence envelope).
  • "Copy as JSON" / "Open lineage" actions.
  • Acceptance criteria:
  • UI honours empty / loading / error states. ✅
  • Reuses existing primitives (Badge, BindingChip, MappingDot). ✅ (Badge + MappingDot directly; BindingChip rejected because its FieldBinding shape diverges from the pack's BindingRef — inline span equivalent used instead so the visual language stays consistent without coercing the wire shape.)
  • Tests cover all states + the two modes. ✅
  • Test evidence:
  • Mount point: "Context" tab in EditorCompanion (alongside Validation / Impact / Runs / Completeness / Health) — mirrors P4-001's mounting pattern.
  • Components added (split across three files to honour 400-line cap; functionally one component):
    • ContextPreviewPanel.tsx (272 lines) — host + data wiring + state machine.
    • ContextPreviewPanel.toolbar.tsx (230 lines) — mode segmented control + Copy/Lineage/Refresh actions.
    • ContextPreviewPanel.parts.tsx (389 lines) — populated pack body (header strip, sections, footer).
  • Auto-detection: contract-field mode wins when both selectedContractId + selectedFieldPath are set; concept mode when selectedConceptId is set; otherwise empty. Selection changes auto-reset the mode. User can force a mode via role="radiogroup" segmented control.
  • Toolbar actions:
    • Copy as JSON → navigator.clipboard.writeText(JSON.stringify(pack, null, 2)).
    • Open lineage → concept mode: /editor/concepts/<id>; contract-field mode: /editor/mapping-workbench?contract=<id>&historyForField=<path>. Uses injected onNavigate host hook with window.location.assign fallback.
    • Refresh → refires active fetch with spinner.
  • States rendered: idle/no-selection (NoSelectionEmpty with mode-aware copy), loading (PanelSkeleton ghost rows), 404 (NoContextEmpty via ApiError.status === 404), error (role="alert" banner + Retry), populated (PackBody).
  • API client in src/pycharter/ui/src/lib/api/semanticContext.ts: getConcept, getContract, getContractField, query. URL builders exported for testing. getContractField splits dotted paths + percent-encodes each segment (backend joins / → .). Auth headers not sent (backend defaults to viewer; real auth wiring is P6-001 follow-up — single insertion point helpers.authHeaders()).
  • A11y: section aria-label="Context preview" outer landmark; role="radiogroup" + aria-checked on mode buttons; aria-label on all icon-only buttons (Refresh, Copy, Open lineage, id-chip copy buttons); role="alert" on error; role="status" on 404 empty; per-section aria-label="<name> (<count>)"; MappingDot keeps its role="img" + state aria-label; focus rings via focus-visible:ring-2.
  • Layout: Flexbox + scrollable sections (max-h-44 / max-h-32 + overflow-y-auto). Zero position: absolute in any new code (verified via grep on the three new files).
  • Reused primitives: Badge, MappingDot. BindingChip rejected — BindingRef shape (state + concept_key) diverges from BindingChip's FieldBinding shape (primary.linkml_slot + primary.concept); inline span styled equivalently keeps wire shape clean.
  • Tests (22 new): 12 API client (URL builders + per-method invocation + limit omission); 10 panel (empty / loading / populated all sections / 404 / error + Retry / auto-detect contract-field / mode switch fires correct API / Copy-as-JSON writes pack to clipboard / concept Open-lineage URL / contract-field Open-lineage URL / Refresh refetches).
  • Quality gates (orchestrator independent re-run on 2026-05-19): npm run lint — 0 errors (26 pre-existing warnings unchanged); npm run type-check — clean; npm run test — 591 passed (65 files) (delta +22 vs. 569 baseline); npm run build — clean.
  • Review notes:
  • Approved on orchestrator independent re-run. Component split across three companion files is the right move under the 400-line budget. BindingChip-rejection rationale is sound — wire shape divergence trumps cosmetic reuse. Two-mode auto-detection matches the editor's selection model cleanly.
  • Residual risks:
  • Evidence envelope retrieval not wired. Panel shows evidence_envelope_id as a truncated copy-able chip + "retrieval not yet wired" hint. Single insertion point (EvidenceRefSection) for when the follow-up endpoint lands.
  • Auth headers placeholder. X-Role / X-Workspace not sent; backend defaults to viewer. Real auth swaps helpers.authHeaders() only — no client changes.
  • Large-pack rendering. Each section has max-h-* cap + overflow-y-auto but no virtualization. For packs with hundreds of concepts/bindings, DOM size could grow; GlossaryView's TanStack Virtual is the precedent if this becomes a problem.
  • Concept lineage URL is provisional. /editor/concepts/<id> matches the backend's actionable_link convention used by SemanticHealthPanel, but no concept-detail route exists yet; 404 falls back to editor default route.

Phase 5 Summary

Phase 5 (Governed Context and Agent Readiness) complete. 6 active cards Done (P5-001..006); 2 deferred per program decision (P5-007 MCP server, P5-008 PyGubernator integration tests). Three waves shipped across PRs #194 (foundation), #195 (API + policy filter), #196 (preview UI).

Definition of done met: - Agents and APIs can request governed semantic context ✅ - Every context response includes enough evidence for audit ✅ - Context can be traced back to approved concepts, mappings, and sources ✅

P5-005: API for retrieving context by concept, field, contract, or query

  • Phase: Phase 5
  • Area: Backend
  • Owner: Backend Agent (general-purpose, agentId a87670fc00d9cb402) — bundled with P5-006.
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: High
  • Risk: Medium
  • Source: Proposal §"Phase 5" task P5-005.
  • Dependencies: P5-001 (pack id/hash), P5-002 (fields), P5-003 (envelope).
  • Write scope:
  • New GET /api/v1/context/concepts/{id}.
  • New GET /api/v1/context/contracts/{id}/fields/{field_path:path}.
  • New GET /api/v1/context/contracts/{id}.
  • New GET /api/v1/context/query?q=...&limit=N.
  • Acceptance criteria:
  • All four routes return the canonical context pack shape from P5-001/002/003. ✅
  • 404 / 422 / 400 behaviour audited via _http_errors.py. ✅
  • Tests cover happy + 404 + 422 per route. ✅
  • Test evidence:
  • All four routes registered under existing /api/v1/semantic/context prefix (no _routers.py change needed; semantic_context already wired).
  • Response model: ContextPackResponse is a thin re-export of the domain ContextPack — wire shape matches domain verbatim. src/pycharter/api/models/semantic_context.py exists as the rename point if divergence is ever needed.
  • Errors flow through audited helpers: raise_logged_not_found, raise_logged_unprocessable, raise_logged_internal_from_exception.
  • Query route: substring ilike only (no relevance scoring); returns a single-element list to keep wire contract stable for future multi-strategy ranking; limit bounded [1, 100].
  • Field-path slash: field_path:path allows / in path; segments collapsed to . before lookup.
  • 12 route tests in tests/unit/test_semantic_context_routes.py (concept/contract/field/query happy + 404 + 422 + slash-path + limit).
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff clean; mypy clean (379 files); pytest 2821 passed.
  • Review notes:
  • Approved on orchestrator independent re-run. Audited HTTP helpers correctly applied; field-path slash handling matches FastAPI conventions.
  • Residual risks:
  • Query route ranker quality: substring-only; future card adds proper relevance scoring (BM25 or vector). Wire returns a list to admit multi-result ranking later.
  • Field-path normalisation: / segments collapsed to .; a contract that legitimately uses / in a field name would collide. Not observed in existing seed data.

P5-006: Policy-aware context filtering (hierarchical roles: viewer < reviewer < admin < superuser)

  • Phase: Phase 5
  • Area: Backend
  • Owner: Backend Agent (general-purpose, agentId a87670fc00d9cb402) — bundled with P5-005.
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Medium
  • Risk: Medium
  • Source: Proposal §"Phase 5" task P5-006. Program decision (2026-05-19): hierarchical role model.
  • Dependencies: P5-005 (retrieval surface to filter), policy fields on concepts/contracts (P1-008's lifecycle).
  • Write scope:
  • Filter context responses by caller role.
  • RequesterContext (role, workspace_id) FastAPI dependency.
  • Document the role mapping in docs/reference/.
  • Acceptance criteria:
  • Role-by-role filtering pinned in tests for each retrieval route from P5-005. ✅
  • Workspace scoping enforced for non-superuser roles. ✅
  • Defense-in-depth: filter at the DB query layer AND at the response serialization layer. ✅
  • Test evidence:
  • RequesterContext in src/pycharter/api/dependencies/requester_context.py — Role StrEnum (viewer / reviewer / admin / superuser); frozen-slots dataclass. get_requester_context reads X-Role and X-Workspace headers; unknown role → viewer; superuser ignores workspace. Header-based auth is a documented placeholder for P6-001.
  • Role → visible-states mapping:
    • viewer → {"approved"} binding states; concept deprecated invisible; workspace-scoped.
    • reviewer → {"approved", "mapped", "suggested"}; concept deprecated invisible; workspace-scoped.
    • admin → all states; concept deprecated visible; workspace-scoped.
    • superuser → all states; concept deprecated visible; cross-workspace.
  • Workspace = concept scheme. ConceptModel.scheme_id and ConceptRelationshipModel.scheme_id filtered directly. MappingProjectionModel has no native workspace column; binding queries JOIN against ConceptModel on concept_key. Bindings with null concept_key invisible to non-superuser. Documented in docs/reference/semantic-context-roles.md.
  • Defense in depth — two layers:
    1. DB query layer (_policy.py + loaders): WHERE clauses on MappingProjectionModel.state, ConceptModel.scheme_id, ConceptRelationshipModel.scheme_id.
    2. Response layer (_filter_pack in the route module): re-applies the binding-state allowlist and concept-deprecated drop on the already-built pack body. Envelope (id/hash) preserved; only concepts and bindings collections pruned.
  • _policy.py (new) — pure filter primitives (binding_state_allowlist, filter_concepts_for_role, apply_concept_scope, apply_relationship_scope, apply_binding_state_filter, apply_binding_workspace_scope). Split out so _helpers.py stays under 400 lines.
  • Builders gained optional requester_context kwarg (default None — backward compat preserved for tests).
  • Tests (20): test_semantic_context_policy_filtering.py — viewer/reviewer/admin/superuser × each route, workspace isolation across schemes, defense-in-depth direct unit tests on _filter_pack.
  • Quality gates: rolled up with P5-005's gates above.
  • Review notes:
  • Approved on orchestrator independent re-run. Two-layer filter design is correct defense-in-depth: query-layer optimises performance; response-layer catches leaks. RequesterContext is a clean swap point for P6-001's real auth.
  • Residual risks:
  • Placeholder auth source. X-Role / X-Workspace headers trusted verbatim. Swap point is get_requester_context body only; the RequesterContext shape and all downstream code remain unchanged when P6-001 lands.
  • Workspace == concept scheme. No native workspace_id column on MappingProjectionModel; binding scoping via JOIN on concept_key. Bindings with null concept_key invisible to non-superuser.
  • Builder requester_context=None default. Preserved for backward compat with existing tests. New callers should explicitly pass a context; the response-layer filter is the safety net.

P5-007: MCP server / tool surface for agent consumers (DEFERRED)

  • Phase: Phase 5
  • Area: Backend / Integrations
  • Owner: Unassigned
  • Reviewer: Unassigned
  • Status: Deferred (program decision 2026-05-19 — explicitly out of scope for the current batch)
  • Priority: Medium
  • Risk: Medium
  • Source: Proposal §"Phase 5" task P5-007.
  • Dependencies: P5-001..006 (context surface).
  • Write scope:
  • Expose governed context via Model Context Protocol. Minimum tool surface: fetch_concept_pack, list_concepts, query_context.
  • Acceptance criteria:
  • MCP server boots, advertises tools, returns governed packs.
  • Tools enforce policy filtering from P5-006.
  • Test evidence:
  • Review notes:
  • Residual risks: Deferred — not in the active dispatch batch. Revisit once Phase 5 base surface ships.

P5-008: PyGubernator integration tests (DEFERRED)

  • Phase: Phase 5
  • Area: Integrations / QA
  • Owner: Unassigned
  • Reviewer: Unassigned
  • Status: Deferred (program decision 2026-05-19 — explicitly out of scope for the current batch)
  • Priority: Medium
  • Risk: Medium
  • Source: Proposal §"Phase 5" task P5-008.
  • Dependencies: P5-001..006 (context surface), pygubernator package availability.
  • Write scope:
  • Drive the PyGubernator agent loop against the pycharter context API in integration tests.
  • Assert the agent can fetch, cite, and act on governed context.
  • Acceptance criteria:
  • End-to-end test passes from agent prompt → context fetch → response with citations.
  • Test evidence:
  • Review notes:
  • Residual risks: Deferred — revisit when MCP + agent runtime story is settled.

P6-001: Role / permission checks across import, review, mapping, governance

  • Phase: Phase 6
  • Area: Backend
  • Owner: Backend Agent (general-purpose, agentId a5475ff893cf2fad2)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: High
  • Risk: High
  • Source: Proposal §"Phase 6" task P6-001. Program decision (2026-05-19): hierarchical role model — viewer < reviewer < admin < superuser; non-superuser workspace-scoped (matches P5-006).
  • Dependencies: P5-006 (RequesterContext shape + Role enum); existing auth subsystem (PyJWT-based, was already present in src/pycharter/api/dependencies/auth.py).
  • Write scope:
  • Replace P5-006's header-based placeholder (X-Role / X-Workspace) with the real auth dependency.
  • Wire role checks across the import / review / mapping / governance route surfaces.
  • Document the role-to-action matrix in docs/reference/.
  • Acceptance criteria:
  • Real auth resolves RequesterContext from JWT (with feature-flagged header fallback for dev/Playwright). ✅
  • Each protected route enforces the appropriate minimum role. ✅
  • Unit tests pin behaviour per role × per route. ✅
  • Migration path from P5-006's header-placeholder is documented. ✅
  • Test evidence:
  • Auth subsystem inventory: existed (src/pycharter/api/dependencies/auth.py) with create_access_token, verify_jwt, get_current_user, require_admin + login/logout/me routes. Extended rather than rebuilt.
  • RequesterContext swap: JWT-first resolution — Authorization: Bearer <token> → decode → use role + workspace_id claims. Header fallback (X-Role / X-Workspace) feature-flagged via PYCHARTER_REQUESTER_CONTEXT_FROM_HEADERS (default on so dev/Playwright/pytest fixtures keep working; set to false in production). Anonymous viewer as final fallback. Legacy Admin / User JWT role labels map to admin / viewer.
  • require_role(min_role) factory in src/pycharter/api/dependencies/role_check.py — resolves RequesterContext, compares ROLE_ORDER[requester.role] vs ROLE_ORDER[min_role], raises 403 via audited raise_logged_forbidden (log_event="role_check_denied") on insufficient role. Returns the context so workspace scoping can chain.
  • Role-to-action matrix (full table in docs/reference/role-permissions-matrix.md):
    • reviewer: suggestion accept/dismiss/request-changes (+ batches), binding transitions/accept/remap/mark-technical-only/annotate, ingest import / import-all / ingest-text / ingest-upload.
    • admin: /proposals/link.
    • viewer: all GETs (response-layer policy filters from P5-006 unchanged).
  • Routes gated: semantic_suggestions.py, semantic_bindings.py, semantic_bindings_actions.py, semantic_proposals.py, semantic_ingestion.py, semantic_kb_ingest.py. JWT workspace_id claim threaded through create_access_token. Legacy test fixtures patched to send X-Role: admin via the fallback so they keep passing during the migration window.
  • Migration plan documented: 4-step horizon (Day-0 headers → emit new JWT claims → flip flag to false → retire fallback code). Client migration header-by-header table. docs/reference/semantic-context-roles.md now points at the new matrix.
  • Tests added (51): test_auth_jwt.py (5), test_role_check_dependency.py (27 — hierarchy, JWT path, header fallback, feature flag off, legacy role mapping, JWT overrides headers, garbled token falls through), test_routes_role_enforcement.py (19 — every protected route × viewer-denied / reviewer-or-admin-passes).
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff clean; mypy clean; pytest tests/unit/ -q --timeout=60 — 2872 passed, 1 skipped, 8 deselected (delta +51 vs. 2821 baseline).
  • Review notes:
  • Approved on orchestrator independent re-run. Reuses the existing PyJWT subsystem; feature-flagged header fallback is the right migration tool. The 4-step migration horizon plan in role-permissions-matrix.md is detailed enough for operators.
  • Residual risks:
  • Env-var user store (PYCHARTER_AUTH_USERS JSON). Migration to a real users table is a follow-up.
  • Header fallback removal horizon. Default-on today; flipping PYCHARTER_REQUESTER_CONTEXT_FROM_HEADERS=false will break unmigrated clients.
  • First-superuser bootstrap. Operators seed PYCHARTER_AUTH_USERS with {"role":"superuser"}. Proper bootstrap CLI deferred.
  • Password hashing. Existing auth module uses hmac.compare_digest on plaintext from env var (not real hashing). Out of scope for P6-001; flag for user-management follow-up.
  • No iss/aud/jti JWT claims. Acceptable for in-process tokens; revisit if external IdP federation arrives.

P6-002: Pagination + filtering for high-volume routes

  • Phase: Phase 6
  • Area: Backend
  • Owner: Backend Agent (general-purpose, agentId a469e11d87c976afa)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: High
  • Risk: Medium
  • Source: Proposal §"Phase 6" task P6-002.
  • Dependencies: None (additive on existing routes).
  • Write scope:
  • Audit + gap-close pagination across list routes.
  • Convention reference doc.
  • Acceptance criteria:
  • Every high-volume list route honours limit + offset. ✅
  • Responses carry total when cheap, next_offset when applicable. ✅
  • Tests cover pagination + 422 edge cases. ✅
  • Test evidence:
  • Inventory (existing/added):
    • Already paginated (kept as-is): /suggestions, /suggestions/{id}/review-events, /health, /health/issues/{fp}/history, /context/query.
    • Added limit/offset: GET /semantic/bindings/events, GET /semantic/assets, GET /semantic/ingest-runs, GET /semantic/proposals/.
  • PaginatedResponse[T] in src/pycharter/api/models/pagination.py: items / total / next_offset / limit / offset. from_slice() constructor derives next_offset from total (when known) else from len(items) == limit. Constants DEFAULT_LIMIT=50, MAX_LIMIT=500. Used for NEW routes; legacy routes keep bare list[T] shape (backward compat).
  • paginate_in_memory() helper for route-level slicing when the repo doesn't yet honour SQL-level limit/offset (used on assets/ingest-runs; future card pushes into the protocol).
  • Filter convention documented in docs/reference/pagination-and-filtering.md: param names match model field names; multi-value comma-separated (?status=accepted,dismissed); unknown tokens silently dropped; booleans use literal true/false; AND across facets, OR within multi-value. Canonical helper _normalise_csv in semantic_health.py. P5-006 context filters explicitly called out as the exception.
  • Tests: tests/unit/test_pagination_convention.py — 32 tests (TestPaginateInMemory 4 + TestPaginatedResponseFromSlice 5 + 16 parameterized 422 edge cases + 7 happy-path route slicing tests).
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff clean; mypy clean (147 files); uv run --frozen pytest tests/unit/ -q --timeout=60 — 2935 passed, 1 skipped, 8 deselected (delta +63 from this batch; +32 P6-002 + +14 P6-003 + +17 P6-004).
  • Review notes:
  • Approved on orchestrator independent re-run. Convention doc anchors future routes. Legacy bare-list compatibility preserved at the call sites that need it.
  • Residual risks:
  • total expense: Bindings events + ingest-runs intentionally omit total from the bare-list shape. PaginatedResponse[T] envelope ready for new routes.
  • Route-level slicing on assets/ingest-runs: post-repo slicing today; push into the AssetLineageRepositoryProtocol in a future card once row counts exceed ~10k per scheme.
  • Cursor pagination deferred: new rows arriving mid-page can shift offsets. Document as a P7 follow-up if workloads expose drift.

P6-003: Background job support for large imports

  • Phase: Phase 6
  • Area: Backend + Frontend
  • Owner: Full-stack Agent (general-purpose, agentId acc3775069dc56038)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Medium
  • Risk: Medium
  • Source: Proposal §"Phase 6" task P6-003. Program decision (2026-05-19): use FastAPI BackgroundTasks.
  • Dependencies: P2-001 (ingest model), P2-007 (import wizard).
  • Write scope:
  • Threshold-gated sync/async dispatch on ingest endpoints.
  • Polling endpoint for run status + progress.
  • Frontend wizard polling mode.
  • Acceptance criteria:
  • Small imports stay synchronous. ✅
  • Large imports return 202 + polling URL. ✅
  • Polling endpoint surfaces progress + final result. ✅
  • Frontend wizard switches to polling mode for large imports. ✅
  • Tests cover sync + background paths. ✅
  • Test evidence:
  • Thresholds in src/pycharter/api/routes/v1/_ingest_background.py: IMPORT_SYNC_THRESHOLD_BYTES = 1_048_576 (1 MiB), IMPORT_SYNC_THRESHOLD_ROWS = 1_000. Async iff size > bytes-threshold OR estimated rows > row-threshold.
  • Async response shape: 202 Accepted with IngestAcceptedResponse {"ingest_run_id": "<uuid>", "status": "pending", "poll_url": "/api/v1/semantic/ingest-runs/<uuid>/status"}.
  • Status lifecycle: pending → processing → succeeded | failed. New nullable progress JSON column on semantic_ingest_runs (migration 20260519000006_add_ingest_run_progress.py). Progress payload: { rows_processed, rows_total, message, result_summary, error_summary }.
  • Background worker in _ingest_background.py opens its own session via get_session_factory(); marks processing → succeeded | failed; stamps progress.result_summary (mirrors IngestReportResponse) on success and error_summary on failure.
  • Polling endpoint: GET /api/v1/semantic/ingest-runs/{run_id}/status → IngestRunStatusResponse { ingest_run_id, status, progress, started_at, finished_at, error_summary, result_summary }. 404 on unknown / malformed UUID.
  • Idempotency: if client supplies ingest_run_id and a non-terminal run with that id exists, server returns that run's poll URL without scheduling a second pipeline. TestIdempotency.test_repeat_post_returns_same_run_id_while_active pins.
  • Frontend wizard (ImportWizard/SubmitStep.tsx): switches to PollingView on 202; polls every 2 s via useEffect + setTimeout; renders determinate progress bar (when rows_total known) or indeterminate bar; role="progressbar" + ARIA attrs; "Stop monitoring" button cancels the loop and surfaces a "still running server-side" message. On terminal status, finalises the wizard's run row and shows result summary.
  • API client (lib/api/ingest.ts): new ingestTextSubmit, ingestUploadSubmit (handle 202), pollIngestRunStatus. Legacy ingestText / ingestUpload kept for IngestGraphModal.
  • Tests: 14 new backend (TestThresholdHelpers 5 + TestSynchronousPath 2 + TestBackgroundPath 6 + TestIdempotency 1); 1 new frontend test (switches to polling mode on 202 and finalises on terminal status (P6-003)).
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff clean; mypy clean; uv run --frozen pytest tests/unit/ -q --timeout=60 — 2935 passed; npm run lint 0 errors; npm run type-check clean; npm run test 592 passed (65 files); npm run build clean.
  • Review notes:
  • Approved on orchestrator independent re-run. Threshold + idempotency design is sound; in-process BackgroundTasks is appropriate for current scale. Wizard polling UX preserves usability with cancellation + result-summary handoff.
  • Residual risks:
  • Restart durability: BackgroundTasks is in-process; an in-flight run is lost on app restart and its row stays in processing indefinitely. Follow-up: migrate to ARQ/Celery + a janitor that flips orphaned processing rows to failed on startup.
  • Threshold tuning: 1 MiB / 1,000 rows starting point; tune via the constants when operator telemetry lands.
  • Polling chattiness: 2 s interval. Consider exponential backoff or SSE/WebSocket once volume warrants.
  • Idempotency cache backed by the durable run table — no in-memory cache, no eviction needed beyond row lifecycle.
  • File size: semantic_kb_ingest.py at 472 lines (just over 400). Could split sync helpers if it grows.
  • Two run rows per wizard async import: wizard creates one via recordIngestRun, backend creates another via create_pending_run. Both terminate with consistent counts. Future cleanup plumbs the wizard's id through.

P6-004: Audit export for governance decisions and mappings

  • Phase: Phase 6
  • Area: Backend
  • Owner: Backend Agent (general-purpose, agentId a4f9ce58e642954ae)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Medium
  • Risk: Medium
  • Source: Proposal §"Phase 6" task P6-004.
  • Dependencies: P3-004 (review-event repo), P3-009-FU-A (review-events endpoint), P1-008 (binding events), P4-001 (health snapshots).
  • Write scope:
  • New GET /api/v1/audit/export JSONL streaming endpoint.
  • Acceptance criteria:
  • Export covers all governance event types. ✅
  • JSONL stream is parseable line-by-line. ✅
  • Auth: admin or superuser only. ✅
  • Tests cover happy + auth-denial + date-range filtering. ✅
  • Test evidence:
  • Endpoint: GET /api/v1/audit/export?since=<ISO>&until=<ISO>&event_types=<csv> → application/x-ndjson StreamingResponse. Files: src/pycharter/api/routes/v1/audit.py (261 lines) + _audit_streams.py (271 lines, split to honour 400-line cap) + src/pycharter/api/models/audit.py (62 lines).
  • Line shape: {"event_type": "...", "event_id": "<uuid>", "timestamp": "<iso>", "actor": "<str|null>", "workspace_id": "<scheme_id|null>", "payload": {...}}.
  • Event-type coverage: | event_type | source model | timestamp column | | --- | --- | --- | | review | SemanticReviewEventModel | timestamp | | binding | ConceptBindingEventModel | created_at | | accepted_suggestion | SuggestionModel (where accepted_at IS NOT NULL) | accepted_at | | proposal_link | ProposalModel (where suggestion_id IS NOT NULL) | created_at | | health_snapshot | SemanticHealthIssueSnapshotModel | detected_at |
  • Streaming strategy: per-event-type generators run session.query(...).yield_per(500). heapq.merge(*streams, key=lambda x: x[0]) interleaves chronologically. Memory O(num_event_types) regardless of total row count.
  • Auth + workspace scoping: require_role(Role.ADMIN) gates the route. Workspace-scoped streams (review, accepted_suggestion) filter by requester.workspace_id for non-superuser admin (via SuggestionModel.scheme_id join for review; direct on scheme_id for accepted). Global streams (binding, proposal_link, health_snapshot) lack scheme_id columns — rows pass through for any admin (gap documented in residual risks).
  • Resumption semantics: chronological ordering (oldest first), since is inclusive → consumers should dedupe by event_id on replay.
  • PII / sensitive-data check: payloads carry only domain columns (ids, decisions, comments, contract/field paths, concept ids, confidence scores, evaluation/fingerprint, severity, summary, suggested-fix, owner). No password hashes, JWTs, or raw secrets.
  • Tests (17 new, all passing): TestAuthGate (4), TestHappyPath (2), TestDateRangeFilter (4), TestEventTypesFilter (3), TestWorkspaceScoping (2), TestStreamContent (2).
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff clean; mypy clean; uv run --frozen pytest tests/unit/ -q --timeout=60 — 2935 passed.
  • Review notes:
  • Approved on orchestrator independent re-run. heapq.merge + yield_per is the right shape for memory-bounded streaming. Admin-gated workspace scoping is correct.
  • Residual risks:
  • Payload schema stability: each event_type's payload is the raw row shape. Migrations adding columns are additive-safe; renames/drops break consumers. Future card documents per-table payload contract.
  • Resumption with since inclusive: consumers should dedupe by event_id when replaying from last_timestamp_seen.
  • Memory pressure on huge exports: yield_per(500) guards memory; future change replacing yield_per with .all() would break the guarantee.
  • Workspace coverage gap: binding / proposal_link / health_snapshot events lack scheme_id → cross-workspace visibility for non-superuser admin. If multi-tenant tightens, those tables need a scheme_id column (separate card).

P6-005: Sample domain with contracts, docs, DDL, suggestions, expected outputs

  • Phase: Phase 6
  • Area: DevRel
  • Owner: DevRel Agent (general-purpose, agentId ac86706d5f0a8b669)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: High
  • Risk: Low
  • Source: Proposal §"Phase 6" task P6-005.
  • Dependencies: None (additive samples).
  • Write scope:
  • New samples/revenue_billing/ directory tree.
  • Add a make sample-revenue-billing target.
  • Smoke test under tests/integration/.
  • Acceptance criteria:
  • User runs the make target → pipeline produces the expected outputs. ✅
  • Sample is self-contained (no external network access). ✅
  • expected/*.yaml asserted in tests/integration/test_sample_revenue_billing.py. ✅
  • Test evidence:
  • Directory tree:
    • samples/revenue_billing/README.md — walkthrough.
    • contracts/{invoice,customer,line_item}.yaml — 3 data contracts, matches template_contract.yaml shape.
    • docs/{glossary,invoice_overview,billing_lifecycle}.md — 3 reference docs.
    • ddl/{invoices,customers,line_items}.sql — 3 Postgres DDL files.
    • expected/{concepts,relationships,suggestions,bindings}.yaml — 4 shape-contract fixtures.
    • scripts/run_pipeline.py — argparse driver (399 lines).
  • Pipeline driver builds 3 sets of ExtractionResult (contract / doc / DDL) with origins IMPORTED / DOC / STRUCTURAL, runs each through extraction_result_to_drafts, stamps ingest_run_id, persists via SuggestionManager.create_batch(..., require_evidence=True). Creates the concept scheme + SemanticIngestRun + 9 SemanticAsset rows upfront.
  • Makefile target sample-revenue-billing runs the driver against $${PYCHARTER_DATABASE_URL:-sqlite:///sample.db} with scheme revenue-billing and actor sample-importer@example.com. Logs "Ingest run <id> created N suggestions across M assets".
  • Expected-outputs assertion strategy: shape over exact counts.
    • concepts.yaml / relationships.yaml: subset checks (every listed item must appear at least once; extra extractor noise allowed).
    • suggestions.yaml: bands (min_total / max_total, per-kind / per-action bands, allowed origins, confidence range).
    • bindings.yaml: reviewer-intent (entries reference real candidate concepts + real contract filenames; binding row not required).
  • Smoke test: tests/integration/test_sample_revenue_billing.py (@pytest.mark.integration). On-disk SQLite under tmp_path via _alias_sqlite_jsonb (matches P2-009 pattern). pytest tests/integration/test_sample_revenue_billing.py -m integration -v → 1 passed.
  • Quality gates (orchestrator independent re-run on 2026-05-19): ruff clean; smoke test 1 passed; unit regression 2872 (no impact — samples touch zero src/pycharter/ files).
  • Review notes:
  • Approved on orchestrator independent re-run. Shape-based assertions are the right call for extractor-noise robustness. Driver reuses the production extraction_result_to_drafts + SuggestionManager paths so the sample exercises the real pipeline.
  • Residual risks:
  • Contract shape assumption: modeled on template_contract.yaml. If a future contract loader requires a contract: wrapper (some orders examples use one), YAMLs need the wrapper. Current pipeline does not load these through parse_contract, so risk is dormant.
  • Extractor-noise robustness: band tops (e.g. concept count 3..24) may need to grow when a future extractor adds significantly more candidates.
  • SQLite vs Postgres: sample defaults to SQLite via _alias_sqlite_jsonb. PYCHARTER_DATABASE_URL lets users redirect to Postgres; smoke test always uses SQLite under tmp_path.

P6-006: User guide for end-to-end workflow

  • Phase: Phase 6
  • Area: Docs
  • Owner: Docs Agent (general-purpose, agentId ae815b939cc5f8ed6)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: High
  • Risk: Low
  • Source: Proposal §"Phase 6" task P6-006.
  • Dependencies: P6-005 (sample domain) for the walkthrough.
  • Write scope:
  • New docs/guides/user-guide-end-to-end.md.
  • Link from docs/index.md and README.md + MkDocs nav.
  • Acceptance criteria:
  • A new user can follow the guide top-to-bottom and reproduce the sample. ✅
  • All commands verified against current API + UI. ✅
  • Linked from top-level docs. ✅
  • Test evidence:
  • docs/guides/user-guide-end-to-end.md — 474 lines, 8 top-level sections (Prerequisites, Workspace, Import, Review, Bind, Health, Context, Troubleshooting, Next steps).
  • Sample anchor verified: ran uv run --frozen --extra ontology python samples/revenue_billing/scripts/run_pipeline.py --db-url sqlite:////tmp/sample-run.db --scheme-id revenue-billing --actor sample-importer@example.com. Captured real log Ingest run <id> created 6 suggestions across 9 assets. Smoke test tests/integration/test_sample_revenue_billing.py passes.
  • Cross-references (11 + 7 image placeholders): samples README, expected/bindings.yaml, proposal, kanban, vocabulary, role-permissions-matrix, semantic-context-roles, pagination-and-filtering, configuration, building, production docs.
  • Top-level linking: README.md line 983; docs/index.md new Guides section; mkdocs.yml line 148 under How-To Guides.
  • Screenshot placeholders: 7 <!-- TODO: screenshot --> markers at docs/guides/images/user-guide-*.png paths (workspace-selector, import-wizard-step-1, import-wizard-step-6, suggestions-inbox, mapping-workbench, health-tab, context-tab).
  • Verification: mkdocs build succeeded; smoke test passed; commands run end-to-end on a fresh SQLite.
  • Makefile target gap closed by orchestrator: added sample-revenue-billing Make target after this card (P6-005 reported it but didn't ship it).
  • Review notes:
  • Approved on orchestrator independent re-run. 8-step structure mirrors the proposal's user-workflow section; cross-references are complete; sample log verified.
  • Residual risks:
  • Screenshot capture deferred — 7 <!-- TODO: screenshot --> markers need real images in a follow-up.
  • Commands may drift if UI/API surface changes; verified against develop @ 2026-05-19.
  • Sample-output drift if extractor adapter changes draft emission (log line shows 6 suggestions across 9 assets).
  • mkdocs --strict warnings: 7 image placeholders + cross-tree samples links. CI deferred until images land.

P6-007: Admin guide for governance policy and permissions

  • Phase: Phase 6
  • Area: Docs
  • Owner: Docs Agent (general-purpose, agentId adda854f6d79e7135)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Medium
  • Risk: Low
  • Source: Proposal §"Phase 6" task P6-007.
  • Dependencies: P6-001 (auth subsystem live).
  • Write scope:
  • New docs/guides/admin-guide-governance.md.
  • Acceptance criteria:
  • Admin can configure roles + policies from this document alone. ✅
  • All commands + endpoints verified. ✅
  • Test evidence:
  • docs/guides/admin-guide-governance.md — 640 lines, 11 sections (Role model, Configuring roles, Concept tier policy, Proposal workflow, Deprecation, Review audit, Semantic health monitoring, Backup/restore, Performance/scale, Security, Cross-references).
  • Verified env vars + commands: PYCHARTER_AUTH_USERS, PYCHARTER_AUTH_JWT_SECRET, PYCHARTER_AUTH_DISABLED, PYCHARTER_AUTH_SERVICE_URL, PYCHARTER_AUTH_ADMIN_ROLE_CLAIMS, PYCHARTER_AUTH_ADMIN_GROUP_CLAIMS, PYCHARTER_REQUESTER_CONTEXT_FROM_HEADERS, PYCHARTER_SEMANTIC_REVIEW_EVENT_BACKEND, PYCHARTER_SEMANTIC_HEALTH_BACKEND — all match source.
  • Verified endpoints: POST /api/v1/semantic/proposals/link (LinkProposalRequest shape), GET /api/v1/audit/export (since/until/event_types params, 7-day default), GET /api/v1/semantic/health filter shape + four kinds.
  • Cross-references: user-guide, migration-guide, samples README, role-permissions-matrix (×2 incl. anchor #operator-migration-plan), semantic-context-roles, pagination-and-filtering (×2).
  • Top-level linking: README.md line 984; docs/index.md line 228; mkdocs.yml line 149.
  • Known limitations flagged: plaintext password storage (§10.3), header-fallback removal horizon (§10.2 + §2), snapshot TTL absence (§7.4 — references P4-001-FU-D), legacy payload["proposal_id"] deprecation (§4.1), no rolling JWT secret (§10.1), audit export internal-only (§10.5), no HTTP deprecation endpoint (§5.1).
  • Review notes:
  • Approved on orchestrator independent re-run. Configuration matrix is comprehensive; known-limitations section is honest about plaintext password storage + bootstrap gaps; cross-references chain together cleanly.
  • Residual risks:
  • mkdocs build --strict not run locally (env: griffe missing). CI will run it; warnings expected only in P6-006 (image placeholders) and not in this doc.
  • Forward references to peer-agent docs (user-guide, migration-guide) — broken until those land in the same merge train.
  • HTTP-endpoint-driven concept deprecation does not exist: doc routes operators through the proposal workflow + SQL backfill. Follow-up card to add PATCH /api/v1/semantic/concepts/{id}/deprecate would close the gap.
  • Future cards referenced: P4-001-FU-D (snapshot TTL), DB-backed user table (replaces PYCHARTER_AUTH_USERS).

P6-008: Playwright coverage for core workflow

  • Phase: Phase 6
  • Area: QA
  • Owner: QA Agent (general-purpose, agentId aaaf0d0de082fc5e8)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: High
  • Risk: Medium
  • Source: Proposal §"Phase 6" task P6-008.
  • Dependencies: P6-005 (sample domain for test data), P6-001 (auth).
  • Write scope:
  • New src/pycharter/ui/e2e/ Playwright suite.
  • CI job in .github/workflows/ci.yml.
  • Acceptance criteria:
  • Golden-path tests pass against a freshly seeded sample. ✅
  • CI job runs in <10 minutes. ✅
  • Failure screenshots + videos uploaded on CI failure. ✅
  • Test evidence:
  • Files added (13 / 561 lines under src/pycharter/ui/e2e/): README.md, playwright.config.ts, tsconfig.json, fixtures/test-users.ts, helpers/auth.ts, helpers/seed.ts, 7 spec files (login, import-wizard, suggestions-inbox, mapping-workbench, context-preview, semantic-health, role-smoke).
  • CI job: new playwright step in .github/workflows/ci.yml. needs: [test], 10-min timeout, SQLite only (no Postgres service required), env PYCHARTER_AUTH_JWT_SECRET, PYCHARTER_AUTH_USERS (4 fixture roles), PYCHARTER_DATABASE_URL=sqlite:///playwright-sample.db, NEXT_PUBLIC_API_URL=http://localhost:8002, PLAYWRIGHT_BASE_URL=http://localhost:3002, PLAYWRIGHT_SKIP_SEED=1. Boots uvicorn + Next dev; polls /openapi.json + / until ready. Failure artefact: actions/upload-artifact@v4 uploads test-results/ + playwright-report/ (7-day retention).
  • Test specs (11 tests, all passing locally in 16.6 s): API + UI login round-trip; reviewer reaches import-wizard / suggestions-inbox / editor; companion tabs Context + Health switch; per-role auth round-trips (4 roles × GET /auth/me).
  • A11y selector strategy: all UI tests use getByRole / getByLabel. Zero CSS selectors, no data-testid, no console.log, no any. Single non-a11y selector pattern is the <main> landmark — also via getByRole.
  • Failure artefacts: playwright.config.ts sets video: 'retain-on-failure', screenshot: 'only-on-failure', trace: 'retain-on-failure'.
  • CI runtime budget: local 16.6 s + CI overhead (4-5 min for Node/Python setup + browser download + service boot). Well under 10-min cap.
  • Local verification: suite verified end-to-end against locally-booted backend + Next dev server with revenue/billing sample seeded into SQLite — 11/11 passing.
  • Minor adjacent edits to keep toolchain green: tsconfig.json excludes e2e/; eslint.config.mjs ignores e2e/** globally.
  • Review notes:
  • Approved on orchestrator independent re-run. A11y-first selectors are the right call. SQLite-only setup keeps the CI footprint small. Per-role smoke covers the hierarchy without depending on UI affordance changes that don't exist yet.
  • Residual risks:
  • No make sample-revenue-billing target at the time the card was written (orchestrator added it after). E2e invokes the Python script directly.
  • Mapping-workbench deep path: contract name + version dynamic per seed; spec smokes /editor only. Follow-up: call catalog API to discover a real contract URL.
  • Per-role UI affordance assertions (viewer can't see Accept; admin can submit proposal-link) deferred — inbox UI lacks stable a11y labels on per-row action menus.
  • Chromium download (~113 MiB) on cold CI run. Cache step keyed on Playwright version is the standard fix if first-run time hurts.
  • Dev-server cold-boot: Next dev was ready in 1 s locally; CI may take longer. 90 s poll budget is conservative.
  • Flake tolerance: retries: 1 on CI only; healthy locally with zero retries.

P6-009: Migration guide for existing ontology users

  • Phase: Phase 6
  • Area: Docs / Engineering
  • Owner: Docs/Eng Agent (general-purpose, agentId a7c87ab070343284d)
  • Reviewer: Orchestrator (independent re-run)
  • Status: Done
  • Priority: Medium
  • Risk: Low
  • Source: Proposal §"Phase 6" task P6-009.
  • Dependencies: P6-001..005.
  • Write scope:
  • New docs/guides/migration-from-legacy-ontology.md.
  • Optional service-level remap reconciliation workflow.
  • Acceptance criteria:
  • Migration path documented for each breaking change observed in this program. ✅
  • Sample migration script under service-level remap reconciliation workflow if applicable. ✅
  • Test evidence:
  • docs/guides/migration-from-legacy-ontology.md — 458 lines.
  • service-level remap reconciliation workflow — 222 lines. Scope: REMAP projection retirement (P3-008-FU-D). Scans concept_binding_events for action='remap' AND from_concept_id IS NOT NULL and re-fires MappingProjectionLifecycleUpdater.on_concept_detached against SqlAlchemyMappingProjectionRepository. Supports --dry-run and --verbose. Honours PYCHARTER_DATABASE_URL. Idempotent.
  • Breaking changes covered (11): POST /bindings/remap response shape (P3-008-FU-B); reason-prefix markers → FSM (P3-008-FU-A); payload["proposal_id"] legacy fallback (P3-006-FU-A); review-event repo default flip (P3-009-FU-B); health persist_history default (P4-001-FU-B); RequesterContext header→JWT (P6-001); pagination convention (P6-002); large-import async (P6-003); audit export (P6-004); REMAP projection retirement (P3-008-FU-D); migration chain (all phases).
  • Verified against source: every endpoint + env var + Pydantic shape cited in the guide cross-checked. mkdocs build succeeded; new page emits zero warnings.
  • Cross-references: kanban (×2), role-permissions-matrix (×3 incl. #operator-migration-plan anchor), pagination-and-filtering (×2), user-guide, admin-guide, database-erd (×2).
  • Top-level linking: README.md line under Documentation; docs/index.md Guides section; mkdocs.yml under How-To Guides.
  • Review notes:
  • Approved on orchestrator independent re-run. 11 breaking changes is the complete set from the kanban backlog scan. Script scope (REMAP retirement only) is the right call — silent backfilling of proposals.suggestion_id from payload["proposal_id"] would auto-link suggestions without human review.
  • Residual risks:
  • Script scope is REMAP only. Other potential backfills (proposals.suggestion_id from legacy payload["proposal_id"]) deliberately deferred to human review.
  • Data migration binding_events_actions is reversible but not symmetric. downgrade() does not restore marker prefixes for already-rewritten rows. Documented in rollback section.
  • Frontend client migration is descriptive only — TypeScript symbol enumeration deferred; external UI authors pointed at lib/api/ modules.
  • mkdocs build --strict red on screenshot placeholders from other agents' files, not from this doc.

Phase 6 Summary

Phase 6 (Production Hardening and Adoption) complete. All 9 cards Done (P6-001..009). Three waves shipped: - Wave 1 (PR #198): P6-001 auth + role gates, P6-005 revenue/billing sample. - Wave 2 (PR #199): P6-002 pagination convention, P6-003 FastAPI BackgroundTasks for large imports, P6-004 JSONL audit export. - Wave 3 (PR #200): P6-006 user guide, P6-007 admin guide, P6-008 Playwright coverage, P6-009 migration guide.

Definition of done met: - The workflow is usable by a new user following docs ✅ - Large imports and review queues remain responsive ✅ - Governance/audit requirements are covered ✅

Program Close-out

All 7 phases (0–6) of the KB/Ontology Workflow Proposal complete. All proposal cards Done. Deferred items remain in the backlog as named follow-ups: - P5-007 — MCP server / tool surface (deferred per program decision 2026-05-19). - P5-008 — PyGubernator integration tests (deferred per program decision 2026-05-19). - P4-001-FU-D — TTL/compaction strategy for snapshots (needs PM input on retention policy). - P3-006-FU-B — Remove legacy payload["proposal_id"] fallback (scheduled for next minor release; DeprecationWarning already live).

Ready

No active cards.

In Progress

No active cards.

Review

No cards awaiting review.

QA

No cards awaiting QA. Phase 0 audit batch + first Phase 1 card closed; see Done.

Blocked

No blocked cards.

Done

QA evidence (applies to all five cards): docs-only audits, no source code modified. Markdown integrity verified — all fenced code blocks balanced; cross-document references resolve to existing files or are deliberate forward references to in-flight cards (kb-phase-0-vocabulary.md for P0-005, kb-phase-1-route-mapping.md for a P1 follow-up). Reviewer spot-checked 100+ cited paths across the four audits. Out-of-scope changes: AGENTS.md was edited (added /orchestrate and /release to the skills table) and .claude/skills/orchestrate/ directory was added — both pre-existing on the working tree and not modified by any audit agent; flagged to the orchestrator for separate triage.

KAN-001: Create board and convert proposal backlog into cards

  • Phase: Phase 0
  • Area: Process
  • Owner: Orchestrator Agent
  • Reviewer: Reviewer/Integrator Agent (general-purpose, agentId ae3ab64b4cc034453)
  • Status: Done
  • Priority: High
  • Risk: Low
  • Source: Multi-Agent Implementation Orchestration Plan
  • Dependencies: None
  • Write scope: docs/guides/kb-ontology-workflow-kanban.md
  • Acceptance criteria:
  • Board exists with standard columns, card template, seed cards, and board rules. ✅
  • Locked program decisions are recorded. ✅
  • Agent role bindings are recorded. ✅
  • First parallel batch (UX0-001, UX0-002, P0-003, QA0-001) is staffed and dispatched with non-overlapping write scopes. ✅
  • Test evidence:
  • 2026-05-18 — Branch feat/kb-phase-0 cut from develop (086c46c4); proposal + kanban brought onto the branch as untracked working files.
  • 2026-05-18 — Four parallel general-purpose agents dispatched and returned; each wrote one disjoint audit file under docs/guides/kb-phase-0-*.md.
  • Review notes:
  • APPROVE (kb-phase-0-review.md §KAN-001): board complete, decisions locked, role bindings recorded.
  • Residual risks:
  • None.

UX0-001: Audit current semantic/ontology screens

  • Phase: UX Phase 0
  • Area: Frontend/UX
  • Owner: Frontend Agent A (general-purpose, agentId ab2ed841f267066d4)
  • Reviewer: Reviewer/Integrator Agent (general-purpose, agentId ae3ab64b4cc034453)
  • Status: Done
  • Priority: High
  • Risk: Medium
  • Source: Surgical UI/UX Enhancement Plan; Proposal §"UI/UX Implementation Phases — UX Phase 0"
  • Dependencies: None
  • Write scope: docs/guides/kb-phase-0-ux-audit.md (new) only. No source-code edits. ✅
  • Acceptance criteria:
  • Every semantic/ontology screen, route, and editor surface is listed by file path. ✅
  • Each is classified as keep, merge into unified editor, deprecate, or internal-only. ✅
  • Navigation impact (current nav entries, redirects needed) is recorded. ✅
  • Findings reference the unified editor shell, workspace store, and editor architecture ADR. ✅
  • Test evidence:
  • 2026-05-18 — docs/guides/kb-phase-0-ux-audit.md (345 lines) on feat/kb-phase-0.
  • Classification totals: keep 8 (/editor, /editor/concepts, /editor/concepts/[conceptKey], /editor/concepts/[conceptKey]/lineage, /editor/contracts/[name]/[version], /editor/ontology/[name]/[version], /editor/drafts, /editor/drafts/[draftId]); merge 8 (/ontology + nested concepts, search, lineage, discovery, history, suggestions); deprecate 3 (/knowledge-base, /editor/flow, /editor/kb); internal-only 3 (/ontology/configuration, /ontology/proposals, /ontology/proposals/detail).
  • Component-family classification: components/editor/shell|panels|bindings|toolbar keep; components/semantic keep-shared; components/knowledge-base deprecate.
  • Two doc-only follow-up cards (UX0-AUDIT-A, UX0-AUDIT-B) proposed at end of audit file for orchestrator triage.
  • Review notes:
  • APPROVE (kb-phase-0-review.md §UX0-001): 26/26 spot-checked paths exist; classification consistent; nav-impact table complete.
  • Residual risks:
  • Two parallel suggestion UIs (SuggestionPanel.tsx in components/editor/panels vs. SuggestionsQueue.tsx in components/semantic) must be reconciled in Phase 2 to avoid duplication.
  • Wholesale deletion of components/knowledge-base/** is unsafe; several leaves (ImportSchemaDialog, ReconciliationWizard, etc.) are reused inside the editor — needs per-file move-vs-delete pass.
  • Folding /editor/flow and /ontology/lineage into a companion bottom-panel tab requires a BottomPanelTabs.tsx slot that does not exist yet; route deletions must not precede that shell change.
  • Open questions (in audit doc): canonical relabel of "Ontology" → "Domain workspace" scope; fate of /ontology (redirect vs. thin Govern/Configure shell); canonical Suggestions Inbox surface.

UX0-002: Document existing UI patterns to reuse

  • Phase: UX Phase 0
  • Area: Frontend/UX
  • Owner: Frontend Agent B (general-purpose, agentId a54bf7aa3afc6b821)
  • Reviewer: Reviewer/Integrator Agent (general-purpose, agentId ae3ab64b4cc034453)
  • Status: Done
  • Priority: High
  • Risk: Low
  • Source: Surgical UI/UX Enhancement Plan; Proposal §"UI/UX Principles"
  • Dependencies: None
  • Write scope: docs/guides/kb-phase-0-ui-patterns.md (new) only. No source-code edits. ✅
  • Acceptance criteria:
  • Existing editor shell, left panel, inspector, modal, tabs, toolbar, badges, and semantic visual patterns are documented with file paths. ✅
  • Recommended reuse points are mapped to the proposed KB workflow steps. ✅
  • Any genuinely missing UI primitive is listed as a small additive component (not a new design system). ✅
  • Test evidence:
  • 2026-05-18 — docs/guides/kb-phase-0-ui-patterns.md (365 lines) on feat/kb-phase-0.
  • 12 pattern families documented: Shell (7), Left rail (7), Right rail / inspector (13), Bottom companion (9), Overlays (12), Toolbar / layer controls (10), Status / badges / chips (10), Semantic visual (16), Bindings (8), Forms (7), Empty/Loading/Error (7), Workspace store + API client (11). Every entry cites a verified file path under src/pycharter/ui/src/.
  • Reuse coverage per KB step — strong: Semantic Home, Suggestions Inbox, Mapping Workbench (per-field editing), Semantic Health; thin: Import Wizard (no stepper host), Mapping side-by-side picker (no candidate list paired with bindings list), Context Preview (no composed envelope view).
  • Missing additive primitives (3, justified): Stepper / WizardHost, MappingPairList, ContextPackPreview.
  • Review notes:
  • APPROVE (kb-phase-0-review.md §UX0-002): 100+ cited paths verified; reuse map covers all six KB steps; three additive primitives well-justified.
  • Residual risks:
  • Status colour overlap between StatusBadge, MappingDot, BindingHistoryDrawer — needs canonical mapping in P0-005 before new screens reuse badges.
  • Inspector tab target ambiguous: WorkspaceInspectorTabs (per ADR Phase 4) vs. legacy InspectorPanel — affects where KB tabs mount.
  • Import Wizard mount point not decided (toolbar dialog vs. left-rail Browse vs. EditorDashboard quick action).

P0-003: Audit existing semantic routes and backend endpoints

  • Phase: Phase 0
  • Area: Backend/API
  • Owner: Backend Agent (general-purpose, original audit agentId aaee5a6bd795a7638; revision pass agentId acc331a7998ffe277)
  • Reviewer: Reviewer/Integrator Agent (general-purpose, agentId ae3ab64b4cc034453)
  • Status: Done
  • Priority: High
  • Risk: Medium
  • Source: Product alignment and workflow contract; Proposal §"HTTP API Proposal", §"Phase 0"
  • Dependencies: None
  • Write scope: docs/guides/kb-phase-0-api-audit.md (new) only. No source-code edits. ✅
  • Acceptance criteria:
  • Every semantic/ontology/knowledge-base route, service, schema, and test is inventoried with file paths. ✅
  • Each is classified as active / replace / deprecate / internal-only. ✅
  • Gaps versus the consolidated HTTP API proposal are listed. ✅
  • Compatibility note: no breaking changes during Phase 1. ✅
  • Reviewer's four minor revisions applied. ✅
  • Test evidence:
  • 2026-05-18 (initial) — docs/guides/kb-phase-0-api-audit.md (611 lines) on feat/kb-phase-0.
  • 2026-05-18 (revision) — file grew to 619 lines (+8) with four corrections + Revision Log appended; only file modified per git status --short.
    • Fix 1: suggestion-accept row reclassified deferred-replace → keep after verifying src/pycharter/api/routes/v1/semantic_suggestions.py:98 already exposes POST /{suggestion_id}/accept; four call-sites in audit updated (route table, §Suggestions gap, §Specific compatibility shims, Open Question 3 now tracks missing POST /{id}/request-changes).
    • Fix 2: semantic/completeness.py → semantic/knowledge/completeness.py (rows L351, L390) verified with test -f.
    • Fix 3: semantic/lifecycle repointed to the package directory with explicit submodule citations _events.py (BindingLifecycle) and _derived_from.py (FieldLineageRecorder) (rows L102, L367) verified with ls.
    • Fix 4: test_lineage_roundtrip.py → tests/unit/api/test_lineage_roundtrip.py (row L99) verified with test -f.
  • Route counts (revised totals): keep ~58, deferred-replace ~28, deprecate ~4, internal-only ~14, missing 1 explicit (POST /suggestions/{id}/request-changes).
  • Persistence summary unchanged: only 2 in-memory semantic services (KBManagementService, NamespaceRegistry); semantic_context emits unpersisted output.
  • Top gaps vs. consolidated proposal unchanged: Imports fragmented, Assets entirely missing, Health & Context computed inline (no snapshot, no pack ID).
  • Five follow-up cards proposed (P0-006, P1-010, P1-011, P2-010, P2-011) — note P0-006's "compatibility shims" section is slightly stale post-fix and may need a one-line retitle when promoted.
  • Review notes:
  • APPROVE-WITH-MINOR-REVISIONS (kb-phase-0-review.md §P0-003) — all four required revisions applied verbatim per agent acc331a7998ffe277 report.
  • Residual risks:
  • Three overlapping concept-create paths (semantic_rdf, editor_concepts, editor_concept_graphs) — consolidating without behaviour drift requires parity tests.
  • editor_knowledge_base in-memory; any frontend already using it must tolerate empty state after Phase 1 swaps in DB-backed storage.
  • Open questions (in audit doc): is /semantic/workspaces alias a Phase 1 deliverable; which existing concept-create path is canonical; missing POST /suggestions/{id}/request-changes route shape; where do persisted context packs live (new table vs. existing artifact store).

QA0-001: Define workflow verification checklist and test matrix

  • Phase: Phase 0
  • Area: QA
  • Owner: QA Agent (general-purpose, agentId a07dc71a587c013b4)
  • Reviewer: Reviewer/Integrator Agent (general-purpose, agentId ae3ab64b4cc034453)
  • Status: Done
  • Priority: High
  • Risk: Low
  • Source: Quality Gates and UI/UX QA Checklist; Proposal §"Quality Gates", §"UI/UX QA Checklist"
  • Dependencies: None for first draft; will be refined after UX0-001 and P0-003 land.
  • Write scope: docs/guides/kb-phase-0-qa-checklist.md (new) only. No source-code edits. ✅
  • Acceptance criteria:
  • Checklist covers backend persistence, API/Python parity, UI workflow, accessibility, browser checks, traceability. ✅
  • Test matrix maps proposal phases 0–6 to minimum test commands and manual verification. ✅
  • Evidence format for kanban cards is documented. ✅
  • References editor-verification-checklist.md for consistency. ✅
  • Test evidence:
  • 2026-05-18 — docs/guides/kb-phase-0-qa-checklist.md (467 lines) on feat/kb-phase-0.
  • Sections: Purpose; Evidence Format (with revenue/billing example); Pre-Commit & Lint Gates (ruff, mypy, pytest, make, pre-commit run --all-files, UI npm run lint|type-check|test, mkdocs build --strict); 7-section Verification Checklist; Phase 0–6 Test Matrix; Revenue Agent Readiness worked example (fixture plan + CLI + screen-by-screen manual journey); 7 Open Questions.
  • Playwright status: not present at repo level. Interim browser-check protocol — npm run dev on port 3002, screenshots under docs/screenshots/kb-phase-<N>/, zero console errors, keyboard-only nav, ?workspace-canvas=0 flag exercised. Replace once P6-008 lands Playwright.
  • Review notes:
  • APPROVE (kb-phase-0-review.md §QA0-001): 35/35 cited paths verified; correctly flagged port 3000/3002 mismatch in editor-verification-checklist.md.
  • Residual risks:
  • Accessibility section is purely manual today (no axe / jest-axe); easy to skip under time pressure — consider promoting P6-008 earlier.
  • Worked-example commands (pycharter kb suggest|health|context) and revenue fixtures don't all exist yet — checklist may mark skipped until Phases 2/⅘ land. Reviewer confirmed: current pycharter kb CLI only has ingest|export|cache|status (positional path arg, no --scheme/--asset flags) — worked example needs a Phase 2/⅘ follow-up pass.
  • Backward-compat gate text was authored assuming the API audit's keep/replace/deprecate taxonomy; now that the API audit landed with that taxonomy, a follow-up pass should cross-link the two docs.

P0-005: Define shared user-facing vocabulary and status labels

  • Phase: Phase 0
  • Area: UX/Docs
  • Owner: Docs/DevRel Agent (general-purpose, agentId a9e618bf53f97b55d)
  • Reviewer: Reviewer/Integrator Agent (general-purpose, agentId a8fa1db9b984181d9)
  • Status: Done
  • Priority: Medium
  • Risk: Low
  • Source: Surgical UI/UX Enhancement Plan; Phase 0 audits kb-phase-0-ux-audit.md and kb-phase-0-api-audit.md.
  • Dependencies: UX0-001 (Done), P0-003 (Done).
  • Write scope: docs/guides/kb-phase-0-vocabulary.md (new) only. No source-code edits. ✅
  • Acceptance criteria:
  • User-facing labels avoid RDF/ontology jargon where possible. ✅
  • Backend status values map cleanly to UI labels. ✅
  • Vocabulary covers concept, suggestion, source asset, field binding, evidence, health issue, context pack, workspace, domain, mapping, ingest run, review decision. ✅ (12-entry table)
  • Status-label canon resolves the StatusBadge / MappingDot / BindingHistoryDrawer colour overlap. ✅
  • Test evidence:
  • 2026-05-18 — docs/guides/kb-phase-0-vocabulary.md (354 lines).
  • 12 Core Vocabulary entries with revenue/billing examples; 5 status-label families with canonical colours.
  • 3 cross-doc conflicts resolved: BindingHistoryDrawer.mapped realigned to MappingDot.green-500; StatusBadge.closed moved from red to grey (red reserved for reject/error/failed); SHACL severity normalised to Error/Warning/Info in UI prose.
  • Review notes:
  • APPROVE (kb-phase-1-review.md §P0-005): 14 spot-checked enum/file citations verify exactly. Single nit: data/seed/context_pack_schema.yaml path should be prefixed with src/pycharter/ — fold in next time the file is touched, not blocking.
  • Residual risks:
  • Open question (PM): does "request_changes" need a fourth SuggestionStatus value, or always route through proposal/review?
  • Open question (PM): does the "Domain workspace" relabel apply to all ontology surfaces or only Semantic Home? (Echoes UX0-001 Q1.)
  • Open question (PM): how should MaturityLevel (unmapped/partial/complete/approved) be surfaced when it overlaps three BindingState values?
  • Phase: Phase 1
  • Area: Backend
  • Owner: Backend Agent (general-purpose, agentId a94e99d4194891593)
  • Reviewer: Reviewer/Integrator Agent (general-purpose, agentId a8fa1db9b984181d9)
  • Status: Done
  • Priority: High
  • Risk: High
  • Source: Durable backend foundation; Phase 0 API audit (kb-phase-0-api-audit.md).
  • Dependencies: P0-003 (Done).
  • Write scope: As declared in the original card — observed clean. No API route file, no UI file, no other in-memory service modified.
  • Acceptance criteria:
  • KB versions, KB links, lineage records stored durably via SQLAlchemy. ✅
  • Review decisions transitively satisfied (already DB-backed via semantic_proposals / semantic_reviews); not touched in this card. ✅
  • In-memory implementation remains the default until opt-in. ✅
  • Migration reversible. ✅ (Alembic kb_management_tables; down_revision = widen_pipeline_runs_run_id; downgrade() drops all three tables.)
  • Restart-survival test passes against on-disk SQLite. ✅
  • Parity test proves SQLAlchemy and in-memory return identical results for the revenue/billing scenario. ✅
  • ruff check, ruff format, mypy, pytest pass locally. ✅
  • Test evidence:
  • Files added (10):
    • src/pycharter/db/models/semantic/kb_version.py (57) — KBVersionModel.
    • src/pycharter/db/models/semantic/kb_link.py (50) — KBLinkModel.
    • src/pycharter/db/models/semantic/kb_lineage.py (69) — KBLineageRecordModel.
    • src/pycharter/db/migrations/versions/20260518000000_add_kb_management_tables.py (88) — Alembic revision.
    • src/pycharter/semantic/knowledge_base/_types.py (113) — KBManagementServiceProtocol (runtime-checkable).
    • src/pycharter/semantic/knowledge_base/_sqlalchemy_kb_management.py (351) — SqlAlchemyKBManagementRepository.
    • src/pycharter/semantic/knowledge_base/_kb_row_mappers.py (96) — Row mappers + UUID parser.
    • src/pycharter/semantic/knowledge_base/factory.py (120) — make_kb_management_service() + resolve_backend(); env var PYCHARTER_KB_MANAGEMENT_BACKEND (default "memory").
    • tests/unit/test_sqlalchemy_kb_management.py (335) — 14 tests.
    • tests/integration/test_kb_management_restart.py (122) — restart-survival test (@pytest.mark.integration).
  • Files modified (3):
    • src/pycharter/db/models/semantic/__init__.py, src/pycharter/db/migrations/env.py, src/pycharter/semantic/knowledge_base/__init__.py.
  • Protocol match: 11 public methods matched signature-for-signature against in-memory KBManagementService.
  • Quality gates (orchestrator + reviewer independent re-runs on 2026-05-18):
    • ruff check <all P1-001 paths> — All checks passed.
    • pytest tests/unit/test_sqlalchemy_kb_management.py -q — 14 passed in 0.33s.
    • pytest tests/integration/test_kb_management_restart.py -q -m integration — 1 passed in 0.33s.
    • pytest tests/unit/test_kb_management_service.py -q (regression) — 9 passed in 0.10s (no in-memory regression).
    • mypy src/pycharter/semantic/knowledge_base/ — Success: no issues found in 7 source files (reviewer independent re-run).
  • Review notes:
  • APPROVE (kb-phase-1-review.md §P1-001): regression suite green; new tests green; integration test green; ruff clean; mypy clean; protocol signatures match; migration chain linear; default backend remains in-memory; editor route still uses KBManagementService() directly; no out-of-scope edits.
  • Residual risks:
  • Default-backend coverage gap: routes still construct KBManagementService directly; factory not yet wired into api/routes/v1/editor_knowledge_base.py. Follow-up tracked in P1-001-FU-A (Backlog).
  • JSON vs JSONB column choice: new tables use SQLAlchemy JSON for Postgres+SQLite compatibility. If product tooling expects JSONB indexing on Postgres, alteration needed. Follow-up tracked in P1-001-FU-B (Backlog).
  • Transaction boundary: when constructed with a session_factory, the repository commits per write call (no batched UoW). Callers wanting multi-method atomicity must pass a live Session.
  • SQLite JSONB shim in unit fixtures (same shim already used by test_sqlalchemy_concept_graph_repository.py).
  • Deploy path: Alembic head moves to kb_management_tables; confirm pycharter db upgrade (or CI equivalent) picks up the new revision without operator action.
  • Open question 4 (workspaces alias) still deferred to Phase 2.

P1-002: Add migrations for semantic assets and ingest runs

  • Phase: Phase 1
  • Area: Backend
  • Owner: Backend Agent (general-purpose, agentId a961686fc8ae1d130)
  • Reviewer: Reviewer/Integrator Agent (general-purpose, agentId a9b0cee38dd50bbd8)
  • Status: Done
  • Priority: High
  • Risk: High
  • Source: Durable backend foundation; Proposal §"Phase 1" tasks P1-002, P1-003, P1-004.
  • Dependencies: P1-001 (Done; commit 34a2f823).
  • Write scope: As declared — observed clean per git status --short (only the 5 declared new files + 2 declared modified files + pre-existing untracked entries).
  • Acceptance criteria:
  • semantic_assets table with required columns. ✅
  • semantic_ingest_runs table with required columns. ✅
  • Migration reversible: down_revision = "kb_management_tables"; downgrade() drops both tables in safe order. ✅
  • Tests cover create, list, lookup, status transitions, JSON round-trip, restart-survival. ✅
  • ruff check, ruff format, mypy, pytest pass locally. ✅
  • Test evidence:
  • Files added (5):
    • src/pycharter/db/models/semantic/semantic_asset.py (86) — SemanticAssetModel.
    • src/pycharter/db/models/semantic/semantic_ingest_run.py (79) — SemanticIngestRunModel.
    • src/pycharter/db/migrations/versions/20260518000001_add_semantic_asset_and_ingest_run_tables.py (90) — Alembic revision semantic_asset_ingest_run_tables.
    • tests/unit/test_semantic_asset_ingest_run_models.py (334) — 14 tests, revenue/billing fixtures.
    • tests/integration/test_semantic_asset_ingest_run_restart.py (134) — restart-survival test (@pytest.mark.integration).
  • Files modified (2): src/pycharter/db/models/semantic/__init__.py and src/pycharter/db/migrations/env.py — additive only.
  • Indexes added: ix_semantic_assets_scheme_status (composite on assets); ix_semantic_ingest_runs_status_started (composite on ingest runs).
  • Quality gates (orchestrator + reviewer independent re-runs on 2026-05-18):
    • ruff check / format on all P1-002 paths — clean.
    • mypy src/pycharter/db/models/semantic/ — Success: no issues found in 20 source files.
    • pytest tests/unit/test_semantic_asset_ingest_run_models.py -q — 14 passed in 0.37s.
    • pytest tests/integration/test_semantic_asset_ingest_run_restart.py -q -m integration — 1 passed in 0.33s (engine genuinely disposed and reopened).
    • pytest tests/unit/test_sqlalchemy_kb_management.py tests/unit/test_kb_management_service.py tests/integration/test_kb_management_restart.py -q (P1-001 regression) — 24 passed in 0.53s (zero regression).
  • Review notes:
  • APPROVE (kb-phase-1-p1-002-review.md): all independent re-runs green; models + migration mirror P1-001's schema-aware pattern; __init__.py and env.py edits additive only; no out-of-scope writes.
  • Residual risks:
  • Soft scheme_name reference — no FK by design (locked). Reviewer folded scheme-existence validation into P1-003's acceptance criteria.
  • JSONB vs JSON — same as P1-001; tracked in P1-001-FU-B.
  • No live alembic upgrade head smoke — operator responsibility post-merge.
  • Status values are free strings (not Python enums) — matches PipelineRunModel precedent.
  • No repository / service yet by scope. P1-003 (asset lineage repo) and P2-001 (import service) will consume these tables.

P1-001-FU-A: Wire editor_knowledge_base.py through make_kb_management_service

  • Phase: Phase 1
  • Area: Backend / API
  • Owner: Backend Agent (general-purpose, agentId a2c52e11405d4e1d5)
  • Reviewer: Reviewer/Integrator Agent (general-purpose, agentId a36f3aa452c116ec8)
  • Status: Done
  • Priority: Medium
  • Risk: Medium
  • Source: P1-001 reviewer report (kb-phase-1-review.md).
  • Dependencies: P1-001 (Done; commit 34a2f823), P1-002 (Done; commit 8d80af4e).
  • Write scope: As declared — observed clean.
  • Acceptance criteria:
  • Route uses make_kb_management_service() rather than KBManagementService() directly. ✅
  • Default backend remains "memory". ✅
  • In-memory mode functionally identical for existing callers. ✅
  • SQLAlchemy mode reachable via single PYCHARTER_KB_MANAGEMENT_BACKEND=sqlalchemy env-var flip. ✅
  • Tests verify both backends through the route layer. ✅ (12 parametrised + wiring-contract tests.)
  • ruff check, ruff format, mypy, pytest pass locally. ✅
  • Test evidence:
  • Files modified (1): src/pycharter/api/routes/v1/editor_knowledge_base.py — replaced module-level _service = KBManagementService() with a FastAPI dependency _get_kb_management_service. Yields the memory singleton in "memory" mode; constructs a request-scoped SqlAlchemyKBManagementRepository over get_db_session in "sqlalchemy" mode. Dependency commits on success and rolls back on exception (matches the repo's "live-session leaves commit to caller" contract).
  • Files added (1): tests/unit/test_editor_knowledge_base_route_factory.py (327 lines) — 12 tests.
  • Wiring approach: FastAPI dependency (not module-level swap) — preserves memory singleton, plumbs request-scoped sessions for SQLAlchemy mode, matches editor_lineage.py's pattern.
  • Quality gates (orchestrator + reviewer independent re-runs on 2026-05-18):
    • ruff check / format on touched paths — clean.
    • mypy src/pycharter/api/routes/v1/editor_knowledge_base.py src/pycharter/semantic/knowledge_base/ — Success: no issues found in 8 source files.
    • pytest tests/unit/test_editor_knowledge_base_route_factory.py -q — 12 passed in 1.37s.
    • pytest tests/unit/test_sqlalchemy_kb_management.py tests/unit/test_kb_management_service.py tests/unit/test_semantic_asset_ingest_run_models.py -q — 37 passed in 0.66s (P1-001 + P1-002 regression clean).
    • Reviewer additionally ran pytest tests/unit/api/test_editor_lineage.py -q (10 passed) and pytest tests/unit/test_public_api_surface.py -q (17 passed) — no adjacent-route regression and no public-API surface drift.
  • Review notes:
  • APPROVE (kb-phase-1-p1-001-fu-a-review.md): _DEFAULT_BACKEND = "memory" unchanged; transaction boundary correctly delegated to the dependency. Reviewer concurs the pattern is right for this codebase but recommends documenting it before P1-003 inherits it.
  • Residual risks:
  • Pattern documentation gap: first KB editor route delegating commit to the FastAPI dependency. Reviewer recommends a short "Transaction boundary in KB routes" note in knowledge_base/__init__.py or a docs guide before P1-003 starts.
  • Minor overhead: Depends(get_db_session) fires unconditionally even in memory mode. Tiny behavioural delta; not blocking.
  • No auth coverage in the new tests: mirrors test_editor_lineage.py posture. Orthogonal gap.

P1-003: Add repository methods for semantic asset lineage

  • Phase: Phase 1
  • Area: Backend
  • Owner: Backend Agent (general-purpose, agentId a0754fdba2cc00ba8)
  • Reviewer: Reviewer/Integrator Agent (general-purpose, agentId a23215bd5c428fd4e)
  • Status: Done
  • Priority: High
  • Risk: Medium
  • Source: Proposal §"Phase 1" (P1-005 + P1-006); P1-002 reviewer (scheme_name validation); P1-001-FU-A reviewer (transaction-boundary pattern).
  • Dependencies: P1-001 (Done, 34a2f823), P1-002 (Done, 8d80af4e), P1-001-FU-A (Done, 871583ca).
  • Write scope: As declared — observed clean.
  • Acceptance criteria:
  • Assets ↔ ingest runs linkage via new ingest_run_id column. ✅
  • Lookup surface (by scheme / type / ingest run / id, plus ingest-run filtering). ✅
  • scheme_name validation raises UnknownConceptSchemeError (subclass of PyCharterSemanticError), not bare ValueError. ✅
  • Transaction-boundary contract honoured — live session never commits. ✅
  • Restart-survival proved with SQLAlchemy backend. ✅
  • Full regression clean (P1-001/002/FU-A + 2339-test unit suite). ✅
  • Test evidence:
  • Files added (11): new src/pycharter/semantic/asset_lineage/ package (8 files, ~1080 lines) — __init__.py, _types.py (Protocol), models.py (frozen Pydantic value objects), errors.py, _in_memory.py, _sqlalchemy_asset_lineage.py (336 lines, live-session-leaves-commit-to-caller), _row_mappers.py, factory.py. Plus migration 20260518000002_add_asset_ingest_run_link.py (79 lines, revision asset_ingest_run_link → down_revision = semantic_asset_ingest_run_tables, idempotent + reversible). Plus tests: tests/unit/test_asset_lineage_repository.py (455 lines, 20 tests) + tests/integration/test_asset_lineage_restart.py (122 lines, 1 test marked @pytest.mark.integration).
  • Files modified (1): src/pycharter/db/models/semantic/semantic_asset.py — added ingest_run_id = Column(String(36), nullable=True, index=True) + docstring entry. No API route, UI file, or P1-001/002/FU-A file touched.
  • Public surface (11 methods): list_assets_by_scheme, list_assets_by_type, list_assets_by_ingest_run, get_asset, create_asset, link_asset_to_ingest_run, list_ingest_runs, get_ingest_run, record_ingest_run, update_ingest_run_status (+ value-object exports).
  • Quality gates (orchestrator + reviewer independent re-runs on 2026-05-18):
    • ruff check / format on all touched paths — clean.
    • mypy src/pycharter/semantic/asset_lineage/ src/pycharter/db/models/semantic/ — Success: no issues found in 28 source files.
    • pytest tests/unit/test_asset_lineage_repository.py tests/integration/test_asset_lineage_restart.py -q -m "integration or not integration" — 21 passed in 0.61s.
    • P1-001/002/FU-A regression — 49 passed in 1.70s (zero regression).
    • Full unit suite — 2339 passed, 1 skipped, 8 deselected.
  • Review notes:
  • APPROVE (kb-phase-1-p1-003-review.md): all locked decisions honoured; transaction-boundary contract correctly implemented (session.flush() only in live-session mode; _commit_and_close no-ops when caller owns the session; test_live_session_does_not_commit asserts db_session.in_transaction() post-write). Migration chain wires cleanly. Model diff +10/-3 additive. Reviewer recommended two follow-up cards (P1-003-FU-A tightens link_asset_to_ingest_run contract; P1-003-FU-B wires routes).
  • Residual risks:
  • ingest_run_id is a soft reference (String, no FK) — matches locked decision for scheme_name. Deliberate.
  • In-memory known_schemes per-instance and not thread-safe — production wiring needs a seeding strategy. The Protocol exposes register_scheme for that purpose.
  • link_asset_to_ingest_run no-op on missing asset — reviewer flagged as the weakest miss-vs-success contract. Tracked in P1-003-FU-A (Backlog).
  • Route wiring deferred — mirrors P1-001 → P1-001-FU-A pattern. Tracked in P1-003-FU-B (Backlog).

Phase 0 Summary

Closed 2026-05-18 on branch feat/kb-phase-0. All five Phase 0 cards (KAN-001, UX0-001, UX0-002, P0-003, QA0-001) reached Done. One reviewer pass produced docs/guides/kb-phase-0-review.md (415 lines, agentId ae3ab64b4cc034453).

Deliverables landed

File Lines Owner agentId Purpose
docs/guides/kb-ontology-workflow-kanban.md (this file) Orchestrator Board + program decisions + agent role bindings
docs/guides/kb-phase-0-ux-audit.md 345 ab2ed841f267066d4 UX surface inventory + keep/merge/deprecate/internal-only classification
docs/guides/kb-phase-0-ui-patterns.md 365 a54bf7aa3afc6b821 Reusable editor UI patterns + reuse map for KB workflow steps
docs/guides/kb-phase-0-api-audit.md 619 aaee5a6bd795a7638 (initial) + acc331a7998ffe277 (revision) Backend route/service/schema inventory + classification + gap analysis
docs/guides/kb-phase-0-qa-checklist.md 467 a07dc71a587c013b4 Evidence format + verification checklist + Phase 0–6 test matrix
docs/guides/kb-phase-0-review.md 415 ae3ab64b4cc034453 Independent reviewer / integrator report across the four audits

Locked decisions confirmed by Phase 0 evidence

  • Unified editor is the only forward UI path. UX0-001 classifies 8 editor routes as keep; 8 ontology routes as merge into the editor; 3 standalone routes as deprecate.
  • Workspace = UI label over concept schemes. No new persisted model needed; UX0-001 + UX0-002 do not require one; P0-003 confirms ConceptSchemeModel is sufficient as the underlying entity.
  • API compatibility: no breaking changes during Phase 1. P0-003 classifies overlapping/duplicate routes as deferred-replace (post-Phase 1); the single planned breaking change in the proposal (POST /{id} → POST /{id}/accept) turned out to be unnecessary — the slash-accept route already exists.
  • Sample domain: revenue / billing. QA0-001 worked example uses it; downstream fixtures will follow.

Top open questions surfaced to PM

  1. Does the workflow uniformly relabel "Ontology" → "Domain workspace", or only inside Semantic Home? (UX0-001)
  2. Does /ontology become a redirect to /editor, or stay as a thin shell for Govern + Configure? (UX0-001)
  3. Canonical Suggestions Inbox surface — editor panel vs. semantic queue? (UX0-001, UX0-002, P0-003)
  4. Is GET /api/v1/semantic/workspaces (read-only alias over ConceptSchemeModel) a Phase 1 deliverable or Phase 2? (P0-003)
  5. Which existing concept-create path is canonical among semantic_rdf, editor_concepts, editor_concept_graphs? (P0-003)
  6. Where do persisted context packs live — new semantic_context_packs table or reuse an existing artifact store? (P0-003)
  7. Pull Playwright coverage (P6-008) earlier as QA0-002, or stay manual through Phase 5? (QA0-001)

Coordination findings carried forward

  • Two parallel suggestion UIs (editor/panels/SuggestionPanel.tsx vs. semantic/SuggestionsQueue.tsx) must be reconciled before P3-001.
  • components/knowledge-base/** cannot be deleted wholesale; per-file move-vs-delete pass needed.
  • BottomPanelTabs.tsx does not yet have a slot for /ontology/lineage companion view; route deletions wait on that shell change.
  • Status colour overlap (StatusBadge / MappingDot / BindingHistoryDrawer) must be canonicalised in P0-005 before new screens reuse badges.
  • QA worked-example CLI commands (pycharter kb suggest|health|context) don't exist yet; checklist marks them skipped until Phases 2/⅘ land.

Outstanding orchestrator tasks before Phase 1 kicks off

  1. Decide how to handle the unrelated AGENTS.md + .claude/skills/orchestrate/ working-tree changes (separate commit vs. fold into Phase 0 commit vs. revert).
  2. Commit Phase 0 audit batch on feat/kb-phase-0 (six new docs + kanban updates). Per CLAUDE.md, the Orchestrator does not commit unprompted; awaiting user direction.
  3. Promote P0-005 (vocabulary) and P1-001 (DB-backed KB versions/links/lineage repository) from Ready to In Progress with tightly scoped agent briefs.