# Seam — Progress Tracker
# Format: [x] = done, [>] = in progress, [ ] = not started
# Update this EVERY session. It is the first thing the agent reads.
#
# 2026-06-08: CLI-first agent guidance for `seam install` (#106, PR #107 merged to main)
#   (branch feat/cli-first-agent-guidance → rebase-merged; commit 11cc101 on main).
#   PROBLEM: `seam install` wired up MCP — the MOST expensive way for an agent to USE Seam.
#     Measured on this repo: the 12 MCP tool schemas cost ~6,060 tokens of standing context
#     per session and the MCP tools bottom out at lean-JSON. The CLI's --quiet mode is
#     ~14-17x leaner (seam impact connect --quiet = 170 tok vs 2,324 for the leanest MCP
#     call), but nothing told the agent the CLI existed or when to escalate verbosity. The
#     CLI is also Seam's agentic-first identity — yet it wasn't the install default.
#   FIX (CLI-first default, MCP opt-in): bare `seam install` now writes a token-lean CLI
#     guidance playbook into the repo (escalation ladder --quiet -> --json --lean -> --json,
#     seam init/sync freshness, when-to-use triggers, MCP-as-alternative note). `--with-mcp`
#     ALSO writes the MCP config (the old behavior). `seam uninstall` reverses BOTH.
#   PER-AGENT RENDERING (token principle: tiny always-loaded + progressive detail):
#     claude -> project skill .claude/skills/seam/SKILL.md (body loads only on invocation) +
#       a thin CLAUDE.md discovery hook (~95% reliable vs ~70-85% description-only).
#     cursor -> .cursor/rules/seam.mdc "Agent Requested" rule (alwaysApply:false + description
#       + empty globs = progressive). doc-confirmed format.
#     codex  -> inline AGENTS.md block (Codex has no progressive mechanism).
#     Guidance is PROJECT-SCOPED (lives in the repo), independent of MCP --location — so Codex
#       gets repo guidance despite its user-only MCP.
#   NEW LEAF: seam/installer/guide.py — ONE guide template + 4 renderers (skill / .mdc / Codex
#     AGENTS block / thin CLAUDE.md hook). Single source so the formats never drift. Pure.
#   NEW LEAF: seam/installer/markdownfile.py (stdlib) — owned-file write (skill/.mdc) + marker-
#     delimited block upsert/remove (<!-- seam:start/end -->) for shared AGENTS.md/CLAUDE.md.
#     Atomic; preserves foreign content; replaces in place, never duplicates. Pure.
#   ABC CHANGE: AgentTarget gains install_guidance/uninstall_guidance/guidance_previews; the 3
#     target modules implement them. CLI adds --with-mcp; upfront MCP-location validation so a
#     bad --with-mcp location fails BEFORE any write (no partial state).
#   DOCS: README "Wire it to an AI agent" rewritten CLI-first; CLAUDE.md command + installer
#     layout + Current Phase (this feature; P2 -> Prior phase); plus the documentation overhaul
#     (docs/CONCEPTS.md, docs/CONFIGURATION.md, docs/architecture.html, ARCHITECTURE current-
#     state overview) landed in the same PR.
#   NO new config knobs, NO schema change, NO re-index; MCP tool count stays 12.
#   GATE: ruff + mypy clean (100 files), full suite + 47 installer tests pass.
#   SHIPPED: PR #107 rebase-merged to main; issue #106 closed; remote+local branch deleted.
#   NEXT: v0.1.0 PyPI release (seam-mcp).
#
# 2026-06-08: P2 — index staleness banner + tools.py split (#103)
#   (branch feat/p2-staleness-banner-tools-split, 3 commits: 7bbbd0f staleness,
#   c06cc3e split, bac23d4 review/taste remediation).
#   PROBLEM: agents querying a stale index (file edited since seam init) received
#     incorrect blast-radius and change-risk answers with no in-band signal. seam status
#     had a freshness field but it wasn't surfaced on every graph query.
#   FIX 1 (staleness banner): after the 5 graph-traversal handlers (seam_impact,
#     seam_changes, seam_affected, seam_context, seam_trace) produce their core result,
#     _maybe_attach_staleness() appends index_status = {stale: true, reason, hint} when
#     the index is stale. ABSENT when fresh — presence = stale signal. Byte-identical
#     when fresh or when SEAM_STALENESS_CHECK=off.
#   FIX 2 (watcher-aware staleness): live watcher → file-drift NOT reported stale
#     (watcher self-heals). But a watched index WITH synthesized edges IS stale (watcher
#     never recomputes synth edges or clusters). `seam status` freshness now delegates to
#     staleness.py with respect_knob=False — always computed, independent of the MCP
#     banner knob.
#   FIX 3 (tools.py split): tools.py was 1671 lines (over 1000-line limit). Sliced into:
#     handler_common.py (323 lines) — shared helpers + _maybe_attach_staleness
#     impact_handler.py (615 lines) — all impact shaping logic
#     trace_handler.py  (184 lines) — seam_trace logic
#     tools.py          (697 lines) — thin facade re-exporting all handlers unchanged
#     All files < 1000 lines; mcp.py and all imports byte-identical.
#   NEW LEAF: seam/analysis/staleness.py (stdlib-only: logging, os, sqlite3, time, pathlib)
#     check_staleness(conn, *, root, watcher_alive, scan_cap, respect_knob) → StalenessVerdict
#     Bounded-scan (SEAM_STALENESS_SCAN_CAP newest files by indexed_at DESC) for mtime drift.
#     Per-process TTL cache: ONLY stale verdicts cached (fresh never cached; safe asymmetry).
#     Never raises; conservative stale=False on any IO/DB error.
#   NEW HELPER: seam/server/handler_common.py::_maybe_attach_staleness (last step in 5 handlers)
#   3 new config knobs: SEAM_STALENESS_CHECK (on/off, default on), SEAM_STALENESS_SCAN_CAP
#     (int, default 200), SEAM_STALENESS_TTL_SECONDS (int, default 5).
#   REVIEW/TASTE STOP FIX (bac23d4): seam status freshness decoupled from SEAM_STALENESS_CHECK
#     via respect_knob=False — the knob gates the MCP BANNER only, not the CLI freshness field.
#   QA VERDICT: seam_changes/seam_affected risk verdicts confirmed byte-stable (regression
#     tests pass; analysis layer called directly below the handler). QA-OBS-001 (pure
#     mtime-only change triggers banner but seam sync does not clear it) documented as a
#     Known Gotcha — pre-existing limitation, harmless (index is correct; only banner is off).
#   DOCS: CLAUDE.md (P2 as Current Phase, E4 → Prior phase; 3 new config knobs; package layout
#     with staleness.py + 4 new server modules; MCP Tools section updated for 5 tools; 5 new
#     Known Gotchas + superseded seam sync freshness gotcha corrected),
#     docs/api-contracts/mcp-tools.yaml (index_status field on 5 tools + header note),
#     progress.txt (this entry).
#   GATE: ruff + mypy clean, 3055 tests pass.
#   NEXT: push/PR/merge feat/p2-staleness-banner-tools-split → main, then v0.1.0 PyPI release.
#
# 2026-06-08: E4 — edge provenance on seam_impact entries + seam_trace hops, and
#   actionable truncation steer (branch feat/e4-edge-provenance-steer).
#   PROBLEM 1: seam_impact entries were edge-kind-blind — a WILL_BREAK dependent could
#     arrive via a hard 'call', a data-coupling 'reads', a composition 'holds', or a
#     heuristic synthesized edge. The edges.synthesized_by column (v12) existed precisely
#     to surface this but was never plumbed to the output. Two CLAUDE.md "Known Gotchas"
#     explicitly called this out as open follow-ups: "synthesized_by NOT surfaced in
#     impact/trace output" and "read/write kind NOT surfaced in impact/trace output".
#   PROBLEM 2: when entries were dropped by the per-tier count cap or the byte ceiling,
#     the response carried honest truncated/byte_capped counts but no remedy hint —
#     agents had to guess whether to raise limit or max_bytes.
#   FIX 1 (edge provenance): each seam_impact tier entry now carries:
#     kind          — edge kind of the final hop (full 9-kind vocab;
#                     call|import|extends|implements|instantiates|holds|reads|writes|uses)
#                     always present; kept in lean mode (core field, not heavy field)
#     synthesized_by — synthesis channel name when heuristic, null for static edges;
#                     stripped in lean mode like resolved_by; null RETAINED in verbose
#                     (null = static edge = the common informative value)
#   Each seam_trace hop gains synthesized_by (hops already carried kind; docstrings
#   corrected to the full 9-kind vocabulary).
#   Gated by SEAM_EDGE_PROVENANCE ("on" default; "off" = byte-identical pre-E4).
#   FIX 2 (truncation steer): a top-level next_actions: list[str] is attached to
#   seam_impact when >=1 entry was trimmed. Hints name the exact remedy, e.g.:
#     "Raise limit to 17 to see 12 more WILL_BREAK upstream dependents."
#     "Pass max_bytes=0 for the full untrimmed blast radius."
#   ABSENT when nothing was trimmed (presence = unambiguous "there is more" signal).
#   All-trimmed case emits explicit anti-false-safe warning.
#   Steer stays WITHIN max_bytes: _attach_steer reserves room and re-trims from the
#   PRE-ceiling response when appending would breach the budget (single pass, converges).
#   Gated by SEAM_IMPACT_STEER ("on" default; "off" = no next_actions key ever).
#   IMPLEMENTATION:
#     New pure leaf seam/analysis/steer.py (stdlib-only: logging, typing):
#       generate_steer(...) → list[str]; never raises ([] on any error, logged).
#       Injected with canonical TIER_* names from impact.py via tier_order param
#       (leaf stays standalone-testable; no impact.py import at module scope).
#     Traversal threading (seam/analysis/flows.py + impact.py):
#       _fetch_outgoing_edges / _fetch_incoming_edges: synthesized_by added to SELECT.
#       Reached TypedDict, Hop TypedDict, EdgeHop TypedDict: synthesized_by field added.
#       BFS propagates winning hop's kind/synthesized_by alongside resolved_by.
#       _build_tier_group: copies kind/synthesized_by from walk()'s Reached result.
#     Handler wiring (seam/server/tools.py):
#       _HEAVY_FIELDS: synthesized_by added (stripped in lean mode).
#       _serialize_hop / _serialize_edge_hop / _serialize_tier_entry: emit kind +
#         synthesized_by when SEAM_EDGE_PROVENANCE="on".
#       _apply_byte_ceiling: extra_reserve param (E4) holds back room for next_actions.
#       _attach_steer: new helper; generates steer, re-trims if budget breached.
#       handle_seam_impact: wires _attach_steer after _apply_byte_ceiling.
#     Config (seam/config.py): SEAM_EDGE_PROVENANCE + SEAM_IMPACT_STEER knobs.
#     CLI (seam/cli/main.py): kind_marker + synth_marker per impact entry; trace hops
#       show synth:<channel> marker; _render_next_actions footer (Rich); _IMPACT_META_KEYS
#       updated with 'next_actions'.
#   REVIEW/TASTE REMEDIATIONS (from /review + /backend-taste passes):
#     STOP (byte-ceiling breach): naively appending the steer after _apply_byte_ceiling
#       would push the response past max_bytes. _attach_steer now re-trims from the
#       PRE-ceiling response with extra_reserve=steer_bytes+_STEER_RESERVE_MARGIN.
#     WATCH-3 (CLI all-trimmed anti-false-safe): the early-return "total==0" branch in
#       impact_cmd now also calls _render_next_actions so the CLI surface gets the same
#       anti-false-safe warning that JSON/MCP consumers receive.
#     Logging fix: steer generation failure now logs a warning (not silent drop) so a
#       malformed-input bug that silently drops the all-trimmed warning is observable.
#   QA: seam_changes and seam_affected output confirmed byte-stable (regression tests
#     pass; they call the analysis layer directly, below the handler).
#   GATE: ruff + mypy clean, 3008 tests pass.
#
# 2026-06-08: E1-FULL — opt-in byte ceiling for seam_impact (SEAM_IMPACT_MAX_BYTES)
#   (branch feat/e1-full-impact-byte-ceiling). 3 build slices + review/taste/QA remediation.
#   PROBLEM: the E1 null-candidate omission brought the benchmark window to 5/5, but a hub
#   symbol or large codebase can still overflow an agent's context window. The per-tier COUNT
#   cap (SEAM_IMPACT_MAX_RESULTS) is a poor proxy: entry byte size varies widely. Agents
#   budget in tokens (chars), not entry counts.
#   FIX (opt-in hard character budget on the response body):
#     New SEAM_IMPACT_MAX_BYTES config knob (int, default 0 = unlimited = byte-identical
#     pre-feature). max_bytes param on seam_impact MCP tool + --max-bytes on CLI.
#     Runs AFTER the per-tier count cap and E2/E3 relevance ordering.
#     Trims entries from the least-valuable end (downstream-before-upstream,
#     MAY_NEED_TESTING-before-WILL_BREAK, intra-tier tail-before-front).
#     O(n) running-total prefix walk (no full re-serialization per placement).
#     Hard-ceiling guarantee: +1 comma over-estimate → running <= budget is a proven
#     upper bound; reserve in _apply_byte_ceiling accounts for trailing byte_capped +
#     truncated metadata so the FINAL response still fits.
#   NEW OUTPUT KEY: byte_capped = {"limit": int, "omitted": int} — present ONLY when the
#     ceiling fired (absent when max_bytes=0 or everything fit).
#     Dropped counts merged INTO truncated ADDITIVELY so the invariant
#     risk_summary[dir][tier] - shown == truncated[dir][tier] holds end-to-end.
#     risk_summary stays the honest full pre-cap total.
#   QA REMEDIATION (commits f0533b4 + 8007538):
#     Hard-ceiling fix: original O(n²) full-re-serialization-per-placement replaced
#     with the O(n) running-total prefix walk; reserve added so FINAL response fits budget.
#     Rich false-safe fix: total==0 branch now checks byte_capped FIRST and prints
#     "trimmed to fit --max-bytes" instead of "no dependents found" (dangerous false-safe
#     that would let an agent conclude a symbol is safe to delete when entries were merely
#     trimmed to nothing by the budget).
#     Footer misattribution fix: quiet/Rich --limit footer now subtracts byte_omitted so
#     byte-ceiling drops are not misattributed to --limit (a "use --limit 0" suggestion
#     is nonsense when limit=0 was already passed).
#   NEW LEAF: seam/analysis/byte_budget.py (stdlib-only: json, typing) —
#     fit_to_byte_budget(response, *, budget, ...) + serialized_size(obj).
#     Pure, no DB, no config, never raises; mirrors the leaf discipline of relevance.py.
#   KNOB: SEAM_IMPACT_MAX_BYTES (int, default 0; opt-in; follows the =off-revert
#     discipline of the E series). MCP tool count stays 12. No schema change, no re-index.
#   Tests: tests/unit/test_byte_budget.py + tests/integration/test_impact_byte_ceiling.py.
#   GATE: ruff + mypy clean, 2890 tests pass.
# Next: backlog open items — E4 actionable truncation steer (S); surface edge kind /
#   synthesized_by in impact/trace output (S); P2 staleness banner (M);
#   A4 framework resolvers (L). OR pivot to v0.1.0 PyPI release (seam-mcp).
#
# 2026-06-07: E3 — neighbor relevance ranking for seam_context_pack (RWR / personalized PageRank)
#   (branch feat/e3-neighbor-ranking). The most under-delivered CodeGraph-gap backlog item.
#   PROBLEM: context_pack capped callers/callees to SEAM_PACK_NEIGHBOR_LIMIT (10) in min_id order
#   (arbitrary insertion order) → kept 10 neither relevant nor alphabetical; a hot symbol could
#   drop its most important caller just because it was indexed late. (The prior "E2/E3" #94 ranked
#   impact TIERS; this is the SEPARATE neighbor-ranking gap — context() callers were still sorted()
#   alphabetical.)
#   FIX (full RWR, the user-chosen principled option, not the degree MVP): rank neighbors by
#   personalized PageRank from the seed over a bounded local subgraph BEFORE the per-file + global
#   caps. Restart-at-seed → a neighbor woven into the seed's neighborhood (shares callers/callees =
#   same functional cluster) outranks a globally-popular-but-distant one; raw degree can't express
#   that. Key=(-ppr_score, is_test, min_id), stable → off = byte-identical min_id revert.
#   NEW pure leaf seam/analysis/rwr.py (mirrors clustering.py): personalized_pagerank(adjacency,
#   seeds, ...) — graph + seed SET in → scores out; no DB/IO/config; never raises. seeds is a SET
#   (edge_match_names: qualified+bare) → handles the qualified/bare asymmetry. Subgraph BFS +
#   scoring in pack.py (_fetch_local_subgraph bounded, _neighbor_scores), computed ONCE per pack,
#   reused for callers+callees. Fully defensive (degrades to min_id order on any failure).
#   3 knobs: SEAM_PACK_RELEVANCE_RANK (on/off default on), SEAM_RWR_MAX_NODES (500),
#   SEAM_RWR_MAX_DEPTH (3). Read-path/MCP-only; no schema change, no re-index; tool count stays 12.
#   SMOKE (clicky, CompanionManager): ranking changes which callers survive the cap-10 (e.g.
#   sendTranscriptToClaudeWithScreenshot, the highest-PPR self-method, floats to #1), truncated
#   counts identical, never crashed. SCOPE: context_pack is MCP-only (NOT in the neutral benchmark
#   capture, which uses `seam context`/engine context()) → PRODUCT-quality win, NOT a benchmark-score
#   change (the benchmark frontier is tapped; structure-synthesis is the remaining qualitative gap).
#   context()'s uncapped lists stay alphabetical (out of scope — no info lost there).
#   Tests: tests/unit/test_rwr.py (9 pure PPR) + tests/unit/test_pack_relevance_rank.py (4: high-id
#   relevant caller survives cap, knob-off min_id revert, production-before-test tie, never-raises).
#   GATE: ruff + mypy clean (92 files), 2890 tests pass.
# Next: backlog open items after E3 — E4 actionable truncation steer (S) + surface edge kind/
#   synthesized_by in impact/trace output (S); E1-real full project-adaptive byte ceiling (M);
#   P2 staleness banner (M); A4 framework resolvers (L). OR pivot to v0.1.0 PyPI release (seam-mcp).
#
# 2026-06-07: E1 — leaner default seam_impact output (omit null best_candidate) complete
#   (branch feat/e1-lean-impact-output). The FINAL clicky impact-B lever — campaign done.
#   After 'uses' edges, all 5 truth-dependents resolve at d=1 and --lean showed 5/5, but the
#   DEFAULT (verbose) 1500-char window held only 4/5. MEASURED cause on the re-indexed clicky
#   index: every WILL_BREAK entry has best_candidate=null (the AMBIGUOUS proximity pick — null
#   for all EXTRACTED/INFERRED entries) and resolved_by non-null. The ', "best_candidate": null'
#   suffix (~25 B/entry) was the only thing pushing the 5th truth-dependent past the window.
#   FIX (lossless, NOT a byte-ceiling): drop best_candidate from impact entries when null
#   (null == absent per the contract). A non-null best_candidate (AMBIGUOUS entries) is kept.
#   resolved_by kept ALWAYS (genuine provenance; several tests rely on it). A token-budget /
#   byte-ceiling machine was deliberately NOT built — field omission already hits 5/5.
#   New knob SEAM_IMPACT_OMIT_NULL_CANDIDATE (on/off, default on; off = byte-identical revert
#   keeping best_candidate:null). Handler-layer only (_serialize_tier_entry threaded through
#   _shape_tier_group); no schema change, no re-index. seam_changes/affected byte-stable (they
#   never carried best_candidate); /api/impact already lean -> unaffected; trace out of scope.
#   DETERMINISTIC clicky impact-B (default, no --lean): 4/5 -> 5/5 (6379 B -> 5803 B). Campaign
#   COMPLETE: 1/5 (A3) -> 3/5 (E2/E3) -> 4/5 (qualified seeds) -> 4/5-default/5/5-lean (uses)
#   -> 5/5 DEFAULT (E1).
#   Tests: tests/unit/test_impact_omit_null_candidate.py (4: omit-default, non-null-kept,
#   knob-off-revert, lean-no-op) + updated test_impact_summary.py::test_verbose_mode_with_cap.
#   GATE: ruff + mypy clean (91 files), 2877 tests pass.
# Next: campaign on impact-B is done at structure+output level. Open levers — structure cells
#   (qualitative naming floor, untouched by edges) or a full blind-panel re-benchmark for fresh
#   LLM usability numbers post-campaign. Await direction before starting either.
#
# 2026-06-07: method-param composition edges ('uses') — all 12 languages complete
#   (branch feat/param-uses-edges). Edge-kind vocab 8 -> 9: +uses.
#   Closes the last clicky impact-B miss: OverlayWindowManager.showOverlay sat at d=2
#   because it RECEIVES companionManager as a method PARAM (not a stored field), so holds
#   never captured it. 'uses' = function/method -> every plain user type referenced as a
#   parameter, making param-injected deps DIRECT (d=1) upstream dependents of the type.
#   Per-language collectors reuse the receiver-inference machinery (record_<lang>_param_types
#   / plain-type helpers) so 'uses' and 'holds' share identical conservatism:
#     - Swift/Python/TS+JS: dedicated collect_param_types_* (precise per-param lines).
#     - Go/Rust/Java/C#/C++/PHP: shared param_types_via_recorder over each recorder.
#     - C: small local typedef/named-struct param extractor (no recorder exists).
#     - Ruby: untyped -> naturally emits none.
#   Conservatism (= holds): only plain user types bind; optionals/generics/containers/
#   builtins/pointers-stripped refused. Never a wrong edge; never raises.
#   New knob SEAM_PARAM_EDGES (on/off, default on; off = byte-identical). Extraction-time;
#   requires seam init re-index. Kind-agnostic traversal -> impact/context/trace auto-pick-up.
#   Higher-volume than holds -> impact/changes/affected verdicts widen (documented gotcha).
#   DETERMINISTIC clicky impact-B: showOverlay promoted d=2 -> d=1; ALL 5 truth-dependents
#   now d=1. Campaign: 1/5 -> 3/5 (E2/E3) -> 4/5 (qualified seeds) -> 5/5 (lean window).
#   Default-verbose window holds 4/5 — residual is now PURELY the E1 byte-ceiling lever
#   (--lean already shows 5/5). Edge-recall/structure for this cell is fully solved.
#   Tests: tests/unit/test_param_uses_edges.py (13: 10 langs + ruby no-op + knob-off +
#   free-fn). GATE: ruff + mypy clean (91 files), 2873 tests pass.
# Next: E1 byte-ceiling / leaner default output is the only remaining lever for impact-B
#   default-window 5/5. Or move to the structure cells (qualitative naming floor).
#
# 2026-06-07: impact qualified-member seeds (Tier-B asymmetry fix) complete
#   (branch feat/impact-qualified-seeds). Follow-on from the E2/E3 cell verification.
#   Root cause found by digging into why clicky impact-B truth-dependents landed at d=2:
#   expand_impact_seeds emitted only BARE member names ("start"), but Tier B made
#   member-call edge targets QUALIFIED ("CompanionManager.start") — so a direct injector
#   (CompanionAppDelegate.applicationDidFinishLaunching -> CompanionManager.start) MISSED
#   at d=1 and fell to d=2. Tier A bridged this for context() via edge_match_names but the
#   impact/trace seed path never got the both-forms treatment.
#   FIX: expand_impact_seeds container case now emits BOTH "method" and "Class.method" per
#   member (deduped; cap governs member count, not raw seeds). Bonus: within-cap members
#   become seeds -> excluded from their own blast radius (self-ref flood shrinks on its own).
#   DETERMINISTIC RESULT on clicky impact-B: external truth-dependents surviving the
#   1500-char cap went 1/5 (pre-E2/E3) -> 3/5 (E2/E3) -> 4/5 (+qualified seeds).
#   CompanionAppDelegate promoted d=2 -> d=1. Only remaining miss is
#   OverlayWindowManager.showOverlay, genuinely d=2 (indirect via instantiation + a method
#   PARAM, not a stored field) — defensibly not a direct dependent.
#   Tests: tests/unit/test_names.py S6 updated + S7 added (both forms);
#   tests/integration/test_impact_relevance.py fixture updated (residual self-refs as
#   non-symbol intra-class call sources). GATE: ruff+mypy clean, 2860 tests pass.
#   Verification report: .claude/eval/seam-eval-e2e3-verification-2026-06-07.md.
# Next: OverlayWindowManager.showOverlay → consider method-param composition edges (bigger
#   feature) OR accept d=2 as correct. Optional full blind-panel re-score of impact-B.
#
# 2026-06-07: E2/E3 seam_impact output relevance shaping complete
#   (branch feat/impact-relevance-ranking, issue #93, PR pending).
#   The usability gap closed: the 2026-06-07 neutral re-benchmark showed holds+synthesis+A3
#   edges improved RECALL (CompanionManager upstream 8→21) but NOT usability — a class seed
#   fans out to all members, so the upstream walk surfaced the class's OWN sibling methods as
#   direct dependents. Alphabetical ordering floated these self-refs above EXTERNAL truth-
#   dependents; under the byte cap only 1 of 5 externals survived. E2/E3 fix the SHAPE so the
#   recall gains become usable output.
#   E3 — relevance ranking BEFORE the per-tier cap: external dependents rank ahead of the
#     target's own container-members (production ahead of test as secondary key), stable sort,
#     so entries[:limit] keeps the closest/highest-signal externals.
#   E2 — self-reference handling: default "rank" keeps self-refs but sorts them last (lossless;
#     risk_summary still counts the full blast radius). Opt-in "hide" drops them + surfaces
#     hidden_self_refs (mirrors hidden_tests).
#   New deep leaf seam/analysis/relevance.py (pure, no DB, never raises): owning_container,
#     classify_self_ref, relevance_key, order_by_relevance, partition_self_refs.
#     Wired into handle_seam_impact via _compute_self_context + _shape_tier_group helpers.
#   2 new config knobs: SEAM_IMPACT_RELEVANCE_SORT (on/off, default on; off = byte-identical
#     revert), SEAM_IMPACT_SELF_REF (rank/hide/show, default rank).
#   Handler-layer ONLY → seam_changes/seam_affected byte-stable (call analysis impact() directly).
#   No schema change, no migration, no re-index. MCP tool count stays 12.
#   Tests: tests/unit/test_relevance.py (16) + tests/integration/test_impact_relevance.py (7,
#     incl. byte-stability + benchmark-shaped CompanionManager regression).
#   GATE: ruff + mypy clean (91 files), 2859 tests pass.
# Next: neutral re-benchmark the clicky impact-B cell to confirm the recall gains now surface.
#
# 2026-06-07: A3 field-access edges + field symbols complete
#   (branch feat/field-access-edges, all 12 languages, extraction-time, watcher-compatible).
#   The visibility gap closed: field reads/writes were invisible to seam_impact — renaming a
#   field or changing its type produced zero upstream results. A3 adds per-access-site edges
#   so data-flow through stored fields is as visible as control-flow through calls.
#   New edge kinds:
#     'reads'  — field/property read (obj.field as rvalue)
#     'writes' — field/property write or delete (LHS of assignment, augmented-assign, del)
#   Edge kind vocabulary: 6 → 8: call|import|extends|implements|instantiates|holds|reads|writes
#   All reads/writes edges carry confidence='INFERRED'.
#   New symbol kind:
#     'field' — first-class indexed field/property (qualified_name='Type.field', additive TEXT
#     value; no schema migration required).
#   New config knob: SEAM_FIELD_ACCESS_EDGES ("on"|"off", default "on"; extraction-time;
#     "off" = byte-identical pre-A3; toggling requires seam init re-index).
#   New read-path view (seam/query/context.py):
#     field_readers — symbols with 'reads' edges to this symbol (typed complement to callers)
#     field_writers — symbols with 'writes' edges to this symbol
#     handle_seam_context in seam/server/tools.py surfaces both lists.
#   New leaf modules (1000-line split across 7 files):
#     seam/indexer/field_access.py           — Python extractor + facade re-exports
#     seam/indexer/field_access_ts.py        — TypeScript/JS
#     seam/indexer/field_access_go_rust.py   — Go/Rust
#     seam/indexer/field_access_ext.py       — Java/C#
#     seam/indexer/field_access_c_cpp.py     — C/C++
#     seam/indexer/field_access_ext2.py      — Ruby/PHP
#     seam/indexer/field_access_php_swift.py — PHP emission + Swift
#   Conservatism contract: self/this/cls → enclosing class; typed receiver via
#     resolve_receiver_type (Tier B inference, same leaf); unresolvable → bare name;
#     NEVER emit a wrong edge; never raises.
#   BFS traversal kind-agnostic → seam_impact/seam_trace/seam_context pick up reads/writes
#     automatically (no per-tool change). MCP tool count stays 12.
#   Known tradeoffs (documented in CLAUDE.md Known Gotchas):
#     - impact/changes/affected verdicts WIDEN (one edge per access site — higher volume
#       than holds); gate with SEAM_FIELD_ACCESS_EDGES=off for byte-stable verdicts.
#     - requires seam init re-index; no migration (additive TEXT values).
#     - 'field' is a new symbol kind — tooling assuming closed enum must be updated.
#     - seam_context callers is a superset of field_readers ∪ field_writers (kind-agnostic).
#     - reads/writes provenance NOT yet surfaced in seam_impact/seam_trace output
#       (documented follow-up, same pattern as synthesized_by).
#   Docs: CLAUDE.md (new Current Phase; package layout + SEAM_FIELD_ACCESS_EDGES knob;
#     edge-kind vocabulary 6→8; symbols.kind +field; seam_context field_readers/field_writers;
#     MCP Tools section; 5 new Known Gotchas), docs/ARCHITECTURE.md (new A3 section at end),
#     docs/api-contracts/mcp-tools.yaml (header note; seam_context field_readers/field_writers
#     + kind enum; seam_trace hop kind; seam_impact A3 note).
# Next: surface reads/writes kind in seam_impact/seam_trace output (the documented follow-up).
#
# 2026-06-07: Edge-synthesis whole-graph post-pass + P1 recall harness complete
#   (branch feat/edge-synthesis-recall, PRD #83, 4 commits). The recall gap closed: static
#   extraction can't see runtime polymorphism, so seam_impact on an implementation/handler showed
#   empty upstream. Synthesis is a deliberate OVER-APPROXIMATION run once over the whole indexed
#   graph that writes the edges parsing structurally can't infer (false-positive edge << missed dep).
#   New leaf modules:
#     seam/analysis/synthesis.py          — A2 interface→implementation override fan-out: link every
#       base/interface method to EVERY same-name impl as a synthesized 'call' edge (NOT MRO — fans
#       out to all candidates, bounded by fanout cap). Pure; never raises.
#     seam/analysis/synthesis_channels.py — A1a closure-collection (collection iterated AND element
#       invoked, paired by field name to append sites) + A1b event-emitter (registrar verbs ↔
#       dispatcher verbs keyed by event-string literal). Pairs field-names/event-keys GLOBALLY
#       (cross-file) → bounded by fanout cap + requires both invocation and append/registration sites.
#   New bridge:
#     seam/indexer/synthesis_index.py     — mirrors cluster_index.py/embedding_index.py: reads
#       symbols+edges, runs the engine, writes synthesized edges in ONE transaction under a synthetic
#       ':synthesis:' files row. Idempotent (delete-then-insert). Never raises; -1 on error.
#   Schema v11 → v12: additive edges.synthesized_by TEXT NULL (_run_migration_v11_to_v12; auto-runs
#     on connect(); idempotent; never raises). NULL = statically extracted; channel-name = synthesized;
#     provenance derived (synthesized_by IS NOT NULL ⟹ heuristic). NOT backfilled — needs re-index.
#   Synthesized edges: kind='call', confidence='INFERRED', synthesized_by=<channel>. Read-path
#     traversal is KIND-AGNOSTIC → seam_impact/seam_context/seam_trace traverse them automatically
#     (like 'holds'). NO new MCP tool — count stays 12.
#   Gated like clusters: runs in `seam init` (always) + `seam sync` (on graph_changed or
#     --force-synthesis); NOT the per-file watcher.
#   P1 recall harness (tests/eval/): fixture repo + SHA-stamped golden.json + recall@K/MRR metric;
#     gate-wired test_recall_regression.py; `make eval` + `make eval-generate`.
#   3 new config knobs: SEAM_EDGE_SYNTHESIS (on/off, default on; off=byte-identical pre-synthesis),
#     SEAM_SYNTHESIS_FANOUT_CAP (default 40), SEAM_SYNTHESIS_MAX_SOURCE_BYTES (default 50MB; 0=unlimited).
#   REVIEW/TASTE fixes (commit 20bcde7) — documented as Known Gotchas in CLAUDE.md:
#     - Clustering EXCLUDES synthesized edges (synthesized_by IS NOT NULL) to avoid feedback pollution
#       (synthesis runs after clustering; its edges persist across runs).
#     - Synthesized edges go STALE after watcher edits (recomputed only by init/sync) — same trade-off
#       as clusters, but higher-stakes (a stale synthesized 'call' edge feeds seam_impact/seam_changes).
#     - seam_changes/seam_affected risk verdicts WIDEN after a synthesis-enabled re-index (more edges);
#       gate with SEAM_EDGE_SYNTHESIS=off for byte-stable verdicts.
#     - FANOUT_CAP semantics diverge per channel: A2/closure-collection TRUNCATE to N; event-emitter
#       SKIPS the whole event when handlers > N (generic high-fanout event = likely false-positive).
#     - synthesized_by stored in DB but NOT YET surfaced in seam_impact/seam_trace OUTPUT (agents can't
#       yet tell synthesized INFERRED from real) — documented follow-up; the DB tag is delivered.
#     - status-accuracy + total source-load cap (SEAM_SYNTHESIS_MAX_SOURCE_BYTES) added in the same fix.
#   Docs: CLAUDE.md (Current Phase replaced; Tier B → Prior phase; package layout + 3 knobs + MCP note +
#     Known Gotchas), docs/ARCHITECTURE.md (synthesis post-pass in write-path data-flow, after clustering),
#     docs/api-contracts/mcp-tools.yaml (header note: synthesized edges traversed; no new tool).
#   No new deps. MCP tool count stays 12. Test count: 2498. Gate green.
# Next: surface synthesized_by in seam_impact/seam_trace output (the documented follow-up).
# 2026-06-02: Phase 8 — Lean output + seam_impact summary tier complete
#   (branch feat/phase8-lean-output, issue #33, PR #34). Implemented via a Sonnet-agent
#   WORKFLOW (2 sequential TDD impl agents → parallel /review + /backend-taste).
#   #1 Lean output: verbose flag (default True) on seam_context/trace/impact/context_pack;
#      shared _apply_verbosity helper strips 6 heavy fields (decorators/is_exported/visibility/
#      qualified_name/resolved_by/best_candidate), keeps signature+core. CLI --lean on
#      impact/trace/pack. seam_query+seam_search enrichment-free → NO verbose flag.
#   #2 Impact summary: risk_summary {dir:{tier:count}} (full pre-cap), per-tier cap
#      SEAM_IMPACT_MAX_RESULTS=25 (config), truncated {dir:{tier:omitted}}, limit param (0=unlimited).
#      Cap applies by default → fixes hub-symbol blast (init_db 30k→4.5k tokens).
#   Review STOP (both reviewers): Rich CLI impact ignored --limit/--lean (called impact() directly).
#      Fixed: all 3 modes route through handle_seam_impact; _IMPACT_META_KEYS guards every iterator;
#      Rich truncation footer; quiet stderr signal. Also removed no-op verbose from seam_query;
#      corrected 3 misleading comments/docstrings (byte-identity, tier-distance, _apply_verbosity).
#   Benchmark re-run: 83.4%/77.6% → 91.8%/88.7% (Seam output 51k→26k tokens; row #2 -1.3%→+85.0%).
#   No schema change, no migration, no new deps, no new tool (count stays 10). 1 new config knob.
#   Test count: 1107 (1031 baseline + 74 Phase 8 net, incl. review-fix tests). Gate green.
# 2026-06-02: Phase 9 — Language expansion (5 → 11 languages) complete
#   (branch feat/phase9-language-expansion). Implemented via a scaffold + 3 parallel TDD
#   agents (Java/C# | C/C++ | Ruby/PHP), 6 review fixes + file-split (graph_ruby.py /
#   graph_php.py split from an oversized graph_ruby_php.py), and observability pass.
#   New grammar packages (6): tree-sitter-java 0.23.5, tree-sitter-c-sharp 0.23.5,
#     tree-sitter-ruby 0.23.1, tree-sitter-c 0.24.2, tree-sitter-cpp 0.23.4, tree-sitter-php 0.24.1
#   New extractor modules (4):
#     seam/indexer/graph_java_csharp.py — Java + C# symbol/edge/comment extractors
#       Java: class/interface/enum/record/method; annotations from modifiers
#       C#: class/struct/record/interface/enum/delegate/method; attributes; namespace traversed not emitted
#     seam/indexer/graph_c_cpp.py       — C + C++ symbol/edge/comment extractors
#       C: function/struct/union/enum/typedef; static→visibility=private
#       C++: class/struct/union/enum/function/method (Class.method); _dedup_cpp_symbols for in-class+out-of-line
#     seam/indexer/graph_ruby.py        — Ruby symbol/edge/comment extractors
#       class/module→class; def inside class→method; def self.x→method(Class.x); top-level def→function
#     seam/indexer/graph_php.py         — PHP symbol/edge/comment extractors
#       class/interface/trait/enum/function/method; grouped-use descent; enum method recursion
#   New leaf modules (2):
#     seam/indexer/signatures_ext.py   — Phase 4 enrichment for the 6 new langs (NodeFields re-declared;
#       drift test asserts key-parity with signatures.py:NodeFields)
#     seam/analysis/imports_ext.py     — Phase 5 import-mapping extraction for the 6 new langs
#       (_ImportMapping re-declared; drift test asserts key-parity with imports.py:_ImportMapping)
#       resolution returns [] for Java/C#/PHP qualified paths (classpath out of scope)
#   Builtins extended: seam/analysis/builtins.py gained 6 conservative per-language frozensets
#   Parser extended: seam/indexer/parser.py gained parse_java/parse_csharp/parse_ruby/parse_c/
#     parse_cpp/parse_php (PHP: language_php() factory); _dispatch_parser routes new language strings
#   Config: SEAM_LANGUAGE_MAP extended (.java, .cs, .rb, .c, .h, .cpp/.cc/.cxx/.c++, .hpp/.hh/.hxx, .php)
#     .h → c (deliberate MVP; see ADR-008 limitation a)
#   Docs: docs/adr/008-language-expansion.md created; ADR-005 updated with Phase 9 pointer;
#     CLAUDE.md, README.md, docs/ARCHITECTURE.md updated; code-comments WHY pass on new modules
#   MVP limitations (all documented in ADR-008 and CLAUDE.md Known Gotchas):
#     a) .h → C (not C++) — use .hpp/.hh/.hxx for C++ headers
#     b) nested classes have flat qualified names (Inner, not Outer.Inner) — matches Go/Rust precedent
#     c) C++ pure-virtual declarations (=0) NOT extracted (parse as field_declaration, not function_definition)
#     d) C function-pointer typedefs (typedef int(*Cb)(int)) NOT extracted
#     e) Java/C#/PHP import resolution returns [] (classpath/NuGet/Composer out of scope)
#     f) C/C++ system #include <...> resolution returns [] (no repo file found)
#   No schema change, no migration, MCP tool count stays 10, no new config knobs.
#   Test count: 1395 (1107 baseline + 288 Phase 9 net, incl. review-fix tests). Gate green.
# Next: roadmap item 8 (`seam install`) / v0.1.0 release prep.

# ── Phase 0: Foundation ────────────────────────────
[x] 1.1 Repository structure created
[x] 1.2 DISCOVERY.md written and approved
[x] 1.3 CLAUDE.md skeleton created
[x] 1.4 PRD, APP_FLOW, TECH_STACK, BACKEND_STRUCTURE docs written
[x] 1.5 Architecture docs + ADRs created
[x] 1.6 Python package scaffold created (empty modules)
[x] 1.7 Makefile + pyproject.toml + gate passing
[x] 1.8 .gitignore, .env.example committed
[x] 1.9 GitNexus indexed + CLAUDE.md finalized

# ── Phase 0: Implementation ────────────────────────
[x] 2.1 db.py — schema + upsert + delete
[x] 2.2 db.py tests
[x] 3.1 Python parser
[x] 3.2 TypeScript/JS parser
[x] 3.3 Parser tests with fixtures
[x] 4.1 Symbol extraction
[x] 4.2 Edge extraction
[x] 4.3 Graph tests
[x] 5.1 FTS5 search
[x] 5.2 Concept query
[x] 5.3 Context lookup
[x] 5.4 Query engine tests
[x] 6.1 seam init command
[x] 6.2 seam status command
[x] 6.3 CLI integration tests
[x] 7.1 File watcher daemon
[x] 7.2 Watcher integration test
[x] 8.1 MCP server setup
[x] 8.2 MCP tool handlers
[x] 8.3 seam start command
[x] 9.1 Benchmark baseline (grep+read proxy — live Bach session deferred, see docs/benchmark.md)
[x] 9.2 Benchmark with Seam (MCP tool-handler outputs)
[x] 9.3 Benchmark report (docs/benchmark.md — 88.7%/85.8% reduction, target was >=30%)
[ ] 10.1 README.md + quickstart
[ ] 10.2 uv.lock committed
[ ] 10.3 Gate passes in clean env
[ ] 10.4 Tag v0.1.0

# ── Last Session ─────────────────────────────────
# 2026-06-01: Day 0 — Full 101 initialization complete
# Next: Start with 2.1 (db.py schema implementation)
# 2026-06-01: Track A complete — db.py + engine.py implemented; 48 tests passing; gate green
# Next: Track B (parser.py + graph.py) and convergence (CLI seam init)
# 2026-06-01: Track B — Parser + Graph Extraction complete
#   - parser.py: _parse helper + parse_python/typescript/javascript
#   - graph.py: extract_symbols + extract_edges for Python + TypeScript
#   - 38 tests passing, make gate green
# 2026-06-01: Convergence — Tracks A+B merged into main; all steps 2.1-5.4 complete
# Next: Step 6.1 (seam init command)
# 2026-06-01: CONVERGE — CLI integration complete; steps 6.1-6.3 done
#   - seam/cli/main.py: index_one_file(), init, status commands implemented
#   - tests/integration/test_indexer.py: 6 integration tests; all passing
#   - 92 total tests; make gate green (lint + typecheck + pytest)
#   - --db-dir option on init/status for test isolation
# 2026-06-01: Step 7.1-7.2 — File watcher daemon complete
#   - seam/watcher/daemon.py: SeamWatcher (FileSystemEventHandler) with per-file debounce
#   - on_created/on_modified: debounced by SEAM_DEBOUNCE_MS -> index_one_file
#   - on_deleted: db.delete_file (immediate, no debounce)
#   - start()/stop(): Observer lifecycle + .seam/watcher.pid management
#   - tests/integration/test_watcher.py: 4 integration tests; all passing
#   - 96 total tests; make gate green (lint + typecheck + pytest)
# 2026-06-01: Steps 8.1-8.4 — MCP server complete
#   - seam/server/tools.py: handle_seam_query/context/search — validation, clamping, relativization
#   - seam/server/mcp.py: create_server(conn, root) — FastMCP stdio, three tools registered
#   - seam/cli/main.py: `seam start` — watcher subprocess + MCP foreground + SIGTERM cleanup
#   - tests/integration/test_mcp_tools.py: 15 integration tests; all passing
#   - 111 total tests; make gate green (ruff + mypy + pytest)
# 2026-06-01: REVIEW + HARDENING + QA pass (commit e27f349)
#   - /backend-taste + /review (high) found index-corruption + extraction bugs.
#   - Fixed (Opus): per-connection foreign_keys + busy_timeout (db.connect());
#     stable-id upsert (ON CONFLICT) + explicit edge delete; new indexer/pipeline.py
#     (kills watcher->cli layer violation); watcher resolves root (macOS /private);
#     guarded background-thread callbacks; `python -m seam.watcher` entry + PID
#     liveness; status takes a path arg; engine score sign; query maps malformed-FTS
#     to INVALID_QUERY; graph nested-fn mis-tag + decorated-class drop + docstring fix.
#     +8 regression tests (test_hardening.py).
#   - QA (no HITL): 119 tests green; init/status/query/context/search verified;
#     TS indexing works; live watcher re-index 0.55s (<1s PRD target).
#   - 9.x benchmark (Bach): DROPPED per user.
# Next: Step 10.x release — README quickstart, commit uv.lock, tag v0.1.0 (HOLD tag for user go).
# 2026-06-01: Phase 2 — Graph community detection (clustering) complete (branch feat/phase2-clustering)
#   New modules:
#     seam/analysis/clustering.py       — pure-Python Louvain (no deps)
#     seam/analysis/cluster_naming.py   — deterministic + opt-in LLM labeling (stdlib urllib)
#     seam/indexer/cluster_index.py     — orchestration bridge; writes clusters + symbols.cluster_id
#     seam/query/clusters.py            — read-only: list_clusters, cluster_members, cluster_peers
#   Updated:
#     seam/query/engine.py              — context() enriched with cluster_id/label/peers
#     seam/server/tools.py              — handle_seam_clusters + handle_seam_context cluster fields
#     seam/server/mcp.py                — seam_clusters tool registered (8 tools total)
#     seam/cli/main.py                  — `seam clusters` command + clustering post-pass in init
#                                         + cluster count in `seam status`
#     seam/config.py                    — SEAM_CLUSTER_NAMING, SEAM_LLM_API_KEY,
#                                         SEAM_LLM_MODEL, SEAM_CLUSTER_MIN_SIZE
#     docs/database/schema.sql          — v4: clusters table + symbols.cluster_id column
#     docs/adr/003-heuristic-execution-flows.md — Phase 2 update note added
#     docs/adr/ADR-007-louvain-clustering.md    — new ADR
#   Test count: 517 (gate green — ruff + mypy + pytest)
#   Schema: v4 (guarded migration from v3; fresh DBs seeded at v4 directly)
#   Config defaults: SEAM_CLUSTER_NAMING=deterministic, SEAM_CLUSTER_MIN_SIZE=2
#   Known limitation: clusters keyed on symbol name → cross-file homonyms collapse (ADR-007)
# Next: docs sync (this session), then benchmark / v0.1.0 release prep
# 2026-06-02: Phase 4 — Node-field enrichment complete (branch feat/phase4-node-enrichment)
#   New modules:
#     seam/indexer/signatures.py    — LEAF: extract_node_fields() → NodeFields
#                                     per-language extraction for Python/TS/JS/Go/Rust
#                                     five fields: signature, decorators, is_exported, visibility, qualified_name
#   Updated:
#     seam/indexer/graph_common.py  — Symbol TypedDict gains the five enrichment fields
#     seam/indexer/graph.py         — Python/TS extractors call extract_node_fields()
#     seam/indexer/graph_go_rust.py — Go/Rust extractors call extract_node_fields()
#     seam/indexer/db.py            — upsert_file writes enrichment cols + _run_migration_v4_to_v5()
#                                     connect() auto-runs pending migrations on open
#     seam/query/engine.py          — context/search/query SELECT the five new columns + decode JSON decorators
#     seam/query/fts.py             — rescore() Signal-6: +15/term for signature matches
#     seam/server/tools.py          — handle_seam_context passes through the five enrichment fields
#     seam/config.py                — SEAM_MAX_SIGNATURE_LEN (default 300)
#     docs/database/schema.sql      — v5: five enrichment cols + FTS5 rebuilt for (name, docstring, signature)
#   Test count: 741 (gate green — ruff + mypy + pytest)
#   Schema: v5 (guarded migration from v4; fresh DBs seeded at v5 directly)
#   Key notes: enrichment cols NULL until full seam init; connect() auto-migrates; signature FTS-searchable
# 2026-06-02: Phase 5 — Import resolution & confidence promotion complete (branch feat/phase5-import-resolution)
#   New modules:
#     seam/analysis/imports.py      — LEAF: extract_import_mappings + resolve_import_source + compute_path_proximity
#                                     per-language import extraction for Python/TS/JS/Go/Rust; never raises
#                                     extension-order resolution: Python[.py/__init__.py], Rust[.rs/mod.rs],
#                                     TS[.ts/.tsx/.d.ts/.js], JS[.js/.mjs/.cjs/index.js], Go[package dir]
#     seam/analysis/builtins.py     — LEAF: is_builtin(name, language) over static frozensets
#                                     conservative vocabulary covering Python/TS/JS/Go/Rust
#   Updated:
#     seam/analysis/confidence.py   — resolve_edge() → Resolution{confidence, resolved_by, best_candidate}
#                                     load_import_mappings(conn, file_path) → list[ImportMapping]
#                                     resolve() kept as backward-compat thin shim
#                                     Resolution order: import-promotion → name-count → builtin → unresolved
#     seam/indexer/pipeline.py      — extract_import_mappings() called per file; upserts import_mappings
#     seam/indexer/db.py            — _run_migration_v5_to_v6() (adds import_mappings table)
#                                     connect() auto-runs pending migrations (now covers v5→v6)
#     seam/query/engine.py          — resolve_edge() threaded through traversal; resolved_by/best_candidate
#                                     added to impact + trace output
#     seam/server/tools.py          — handle_seam_impact + handle_seam_trace pass through resolved_by/best_candidate
#     seam/config.py                — SEAM_BUILTIN_FILTERING, SEAM_IMPORT_RESOLUTION,
#                                     SEAM_MAX_IMPORT_CANDIDATES, SEAM_PROXIMITY_MAX_CANDIDATES
#     docs/database/schema.sql      — v6: import_mappings table + two indexes
#   Test count: 939 (gate green — ruff + mypy + pytest)
#   Schema: v6 (guarded additive migration from v5; fresh DBs seeded at v6 directly)
#   Key notes:
#     - import_mappings NOT backfilled by migration — run seam init to enable Phase 5 resolution
#     - connect() auto-migrates v5→v6 on open (additive: new table only, no column changes)
#     - resolved_by computed at read time — always fresh after per-file watcher re-index
#     - seam_changes + seam_affected deliberately DO NOT use import promotion (byte-stable verdicts)
#     - Go module-qualified imports (github.com/...) out of scope — Go homonyms stay AMBIGUOUS
# Next: benchmarking / v0.1.0 release prep
# 2026-06-02: Phase 6 — Context-Pack Primitive complete (branch feat/phase6-context-pack)
#   New modules:
#     seam/query/pack.py            — LEAF: context_pack(conn, symbol_name) → ContextPack | None
#                                     orchestrates engine.context() + comments.why() into one enriched bundle
#                                     ContextPack: target, callers, callees (NeighborRef), why, cluster_peers, truncated
#                                     batched enrichment: single WHERE name IN (...) — O(1) DB round-trips
#                                     per-file diversity cap (SEAM_PACK_PER_FILE_CAP) applied before global limit
#   Updated:
#     seam/config.py                — SEAM_PACK_NEIGHBOR_LIMIT (default 10)
#                                     SEAM_PACK_PER_FILE_CAP (default 3)
#                                     SEAM_PACK_MAX_COMMENTS (default 10)
#     seam/server/tools.py          — handle_seam_context_pack (thin adapter)
#     seam/server/mcp.py            — seam_context_pack tool registered (10 tools total)
#     seam/cli/main.py              — `seam pack <symbol>` command (--json / --quiet)
#     CLAUDE.md                     — new config knobs documented, pack.py in package layout
#   Test files:
#     tests/unit/test_pack.py               — 14 unit tests (orchestration, enrichment, caps, graceful degradation)
#     tests/integration/test_pack_parity.py — 11 integration tests (handler vs pack parity, JSON envelope, INVALID_INPUT)
#     tests/integration/test_pack_homonym.py— 9 integration tests (per-file cap, diversity, ambiguous target)
#   Test count: 975 (939 baseline + 36 Phase 6, incl. review-fix tests) (gate green — ruff + mypy + pytest)
#   No schema change, no migration, no new deps
# 2026-06-02: Phase 7 — One-Shot `seam sync` complete (branch feat/phase7-seam-sync, issue #31, PR #32)
#   New module:
#     seam/indexer/sync.py          — LEAF: sync(conn, root, *, recompute_clusters, force_clusters,
#                                     naming_mode, llm_api_key, llm_model, min_size) → SyncResult
#                                     filesystem reconcile: mtime pre-filter → SHA-1 confirm
#                                     reuses walk_project + index_one_file + sha1 + delete_file + index_clusters
#                                     delete is existsSync-guarded (roadmap §6.1) — only remove if file truly gone
#                                     FULL cluster recompute gated on graph_changed (or force_clusters)
#                                     SyncResult: added, modified, removed, unchanged, skipped, graph_changed,
#                                       clusters_recomputed, cluster_count (None=skipped / -1=failed / ≥0=ok)
#   Updated:
#     seam/cli/main.py              — `seam sync [path]` command (--json / --quiet / --force-clusters)
#                                     routes through sync.py:sync(); NO MCP tool (CLI-only, server read-only)
#                                     cluster-failure (-1) surfaced as "failed" + warning (mirrors seam init)
#     CLAUDE.md / README.md / docs/ARCHITECTURE.md — Phase 7 documented; stale-clusters gotcha updated
#   Test files:
#     tests/unit/test_sync.py                  — reconcile + gating + cluster-failure (S13) + delete-safety (S14)
#     tests/integration/test_sync_cli.py       — CLI envelope, NO_INDEX, mutual-excl, cluster-failure (SC11)
#     tests/integration/test_sync_clusters.py  — end-to-end cluster freshness + gating + --force-clusters
#   Review: /review + /backend-taste converged on the index_clusters -1 sentinel (surfaced as healthy) —
#     fixed: clusters_recomputed=(count>=0) + CLI "failed" warning; added existsSync delete guard;
#     narrowed CLI except to sqlite3.Error→DB_ERROR (documented catch-all fallback preserves --json envelope)
#   Test count: 1031 (975 baseline + 56 Phase 7, incl. 6 review-fix tests) (gate green — ruff + mypy + pytest)
#   No schema change, no migration, no new deps, no new config knobs (reuses SEAM_CLUSTER_*)
# Next: roadmap item 8 (`seam install`) / v0.1.0 release prep

# ─────────────────────────────────────────────────────────────────────────────
# Phase 10 — Swift support (11 → 12 languages); Kotlin deferred         [#37]
# ─────────────────────────────────────────────────────────────────────────────
#   Approach: same pipeline as Phase 9 — /to-prd → single TDD Sonnet agent (one
#     language = no inter-agent contention) → /review + /backend-taste → fix → /doc → PR+merge.
#   Grammar evidence (the go/no-go step): probed BOTH Swift + Kotlin against ts 0.25.2.
#     Swift  (tree-sitter-swift 0.7.3)  — clean, has_error=False on idiomatic code → ADD.
#     Kotlin (tree-sitter-kotlin 1.1.0) — ERROR nodes on interfaces/objects/ctor-classes;
#       recovered ~1 of 6 symbols on a realistic file → DEFERRED (would silently drop code).
#       No tree-sitter-kotlin-ng on PyPI. See ADR-009.
#   New:
#     seam/indexer/graph_swift.py   — symbol/edge/comment extractors (mirrors graph_go_rust.py)
#                                     class/struct/actor/extension→class, enum→type, protocol→interface
#                                     methods→Type.method; bare-identifier calls only; /// + /** */ docstrings
#     tests/fixtures/sample.swift, tests/unit/test_swift.py (58 tests)
#     docs/adr/009-swift-support.md — Swift added + explicit Kotlin deferral with evidence
#   Updated: config.py (.swift), parser.py (parse_swift via language()), pipeline._dispatch_parser,
#     graph.py (3 dispatch branches), graph_common._find_enclosing_function (Swift branch),
#     signatures_ext.py (_extract_swift), imports_ext.py (_extract_swift/_resolve_swift→[]),
#     builtins.py (_SWIFT_BUILTINS, trimmed to stay <1000 lines)
#   Review (/review + /backend-taste): cross-cutting = 0 findings; correctness = 1 — /** */ block
#     doc-comments dropped (only 'comment'///walked, not 'multiline_comment'). Fixed + regression test.
#   Test count: 1454 (1395 Phase-9 baseline + 58 Swift + 1 review-fix regression). Gate green.
#   No schema change, no migration, MCP tool count stays 10.
# Next: roadmap item 8 (`seam install`) / v0.1.0 release prep. (Kotlin: revisit on robust grammar.)

# ─────────────────────────────────────────────────────────────────────────────
# Agentic-readiness hardening (post-Phase-10) — 3 critical audit fixes; NO installer
# ─────────────────────────────────────────────────────────────────────────────
#   Trigger: end-to-end agentic-readiness audit. Method: built a fresh multi-language
#     repo, ran `seam init`, connected a REAL MCP stdio client (mcp lib, as Claude does),
#     drove all 10 tools + sad paths. Verdict: tools excellent; the edges around them weren't.
#   Findings fixed (critical-only; installer deferred):
#   #1 PyPI name collision — `seam` on PyPI is Seam Labs' smart-lock SDK (v1.145.0); README's
#      `pip install seam` installs the WRONG package. → renamed distribution to `seam-mcp` in
#      pyproject (import pkg + console command stay `seam`); README install now from-source
#      (not published yet); also fixed the bogus `["start","--stdio"]` MCP config (no such flag).
#   #2 MCP error contract — app-level rejections returned {"error","message"} with isError=FALSE
#      (a protocol-compliant agent reads a rejection as success); not-found returned empty content.
#      → new module-level `_finalize(result)` in seam/server/mcp.py, applied to all 10 closures:
#        error-dict sentinel → raise ToolError → FastMCP sets isError=True ("CODE: message");
#        None → {"found": false}; success dict/list pass through unchanged. Handlers/CLI/output.py
#        UNTOUCHED (CLI keeps {ok:false,error:{code,message}}; same code+message, native signal each).
#        FastMCP fact (probed): isError flips ONLY on raise, and content gets an
#        "Error executing tool <name>: " prefix — so clean JSON can't survive a raise; the right
#        cross-transport contract is same-code+message, each transport's native error signal.
#   #3 seam_changes polluted by its own index — init created `.seam/` but no gitignore, so
#      `.seam/seam.db`/-shm/-wal showed as changed "modules". → `seam init` writes `.seam/.gitignore`
#      containing `*` (probed: git stops reporting `.seam/`; written INSIDE `.seam/` so the
#      "nothing outside `.seam/`" property holds). Idempotent (exists() guard).
#   New tests: tests/integration/test_mcp_error_contract.py (8: _finalize unit + in-memory MCP
#     client wiring), tests/integration/test_init_gitignore.py (3: written/hides-from-git/idempotent).
#   Docs: CLAUDE.md (Current Phase + 2 new gotchas), README (from-source install + fixed MCP config),
#     docs/api-contracts/mcp-tools.yaml (error-contract header note).
#   Live re-verify (fresh repo, real MCP client): app-error isError=True ✓, not-found {found:false} ✓,
#     seam_changes excludes .seam ✓, success path intact ✓.
#   Test count: 1465 (1454 + 11). Gate green (ruff + mypy 46 files + pytest). No schema change,
#     no migration, MCP tool count stays 10. Plan: .claude/tasks/agentic-readiness-hardening.md.
# Next: roadmap item 8 (`seam install` / AgentTarget) + actually publish to PyPI as `seam-mcp`.

# ─────────────────────────────────────────────────────────────────────────────
# `seam install` (roadmap item 8) — one-command MCP wiring; Claude + Cursor + Codex
# ─────────────────────────────────────────────────────────────────────────────
#   Why now: the readiness audit's last gap was onboarding — users had to hand-write MCP config.
#   Scope (user-chosen): 3 agents, 2 config formats. Verified each format from docs before coding
#     (same discipline as the Claude probe): Claude .mcp.json needs type:"stdio"; Cursor omits type;
#     Codex is TOML [mcp_servers.<name>] in ~/.codex/config.toml.
#   New package seam/installer/:
#     __init__.py  — TARGETS {claude,cursor,codex} registry + get_target + resolve_seam_command()
#                    (resolves the ABSOLUTE seam path via sys.argv[0] — cwd/PATH-independent at agent launch)
#     core.py      — AgentTarget ABC + InstallResult + shared idempotent JSON install/uninstall_entry
#     jsonfile.py  — LEAF (stdlib json): load/atomic-write/get_in/set_in/delete_in — Claude + Cursor
#     tomlfile.py  — LEAF (tomlkit): load/atomic-write/get/set/delete [mcp_servers.<name>] — Codex
#     claude.py    — .mcp.json (project) / ~/.claude.json projects.<abs-root> (user); entry has type:stdio
#     cursor.py    — .cursor/mcp.json (project) / ~/.cursor/mcp.json (user); no type field
#     codex.py     — ~/.codex/config.toml [mcp_servers.seam] (user scope ONLY); tomlkit round-trip
#   New dep: tomlkit 0.15.0 (stdlib tomllib is read-only; tomlkit preserves user comments/formatting).
#   seam/cli/install.py — `seam install` + `seam uninstall` Typer commands (registered onto app in
#     main.py with 2 lines — main.py is already >1000 lines so logic lives in its own module).
#     Flags: --target claude|cursor|codex|all, --location project|user, --print-config, --json.
#     Idempotent (deep-equal→unchanged), atomic temp+rename, .backup on corrupt config, preserves
#     other servers. Codex+project (or any unsupported target,location): explicit single target →
#     INVALID_INPUT; under --target all → skipped-with-reason (not an error).
#   Review (self, /backend-taste lens): removed an unused detect() method (YAGNI); tightened
#     _select_targets typing to list[AgentTarget]|None.
#   New tests: tests/unit/test_installer.py (18: jsonfile/tomlfile/core/claude/cursor/codex/registry),
#     tests/integration/test_install_cli.py (9: create/idempotent/print-config/all-user/all-project-skips-
#     codex/no-index-warn/unknown-target/codex-project-invalid/uninstall). ALL use tmp paths + tmp HOME —
#     never the real ~/.claude.json/~/.cursor/~/.codex.
#   Live verify (real seam binary, fake HOME): all 3 configs written correctly; server BOOTS from the
#     written .mcp.json command+args (10 tools, seam_search ok); re-run → unchanged; uninstall → removed.
#   Test count: 1492 (1465 + 27). Gate green (ruff + mypy 54 files + pytest). CLI-only — NO new MCP tool;
#     tool count stays 10. No schema change, no migration. Plan: .claude/tasks/seam-install.md.
# Next: v0.1.0 — publish to PyPI as `seam-mcp`; add more agent targets (one file each) as needed.

# ─────────────────────────────────────────────────────────────────────────────
# CLI-only completion + optional-MCP install profile
# ─────────────────────────────────────────────────────────────────────────────
#   User goal: use Seam fully from the terminal without the MCP server, and an install
#   profile that doesn't pull the mcp stack. Finding: the CLI already ran without MCP for
#   11/13 commands (they query .seam/seam.db directly). Only query/search/context were MCP-only.
#   New: seam/cli/read.py — `seam query` / `search` / `context` over the transport-agnostic
#     handle_seam_query/_search/_context (tools.py is MCP-free). Flags mirror the other read
#     commands: --json/--quiet; context has --lean (verbose=False). Registered onto app in main.py.
#     context not-found → {found:false} success (matches the MCP _finalize + impact contract).
#   Optional MCP:
#     - pyproject: `mcp` moved OUT of [project] dependencies INTO [project.optional-dependencies]
#       server=["mcp>=1.0.0"]; also added to [dependency-groups].dev so tests still exercise it.
#       (dependency-groups don't ship to pip users → `pip install seam-mcp` is mcp-free.)
#     - main.py: `from seam.server.mcp import create_server` is now LAZY inside start() via
#       _load_create_server(); missing mcp → friendly install hint + exit 1 (fails fast, before
#       spawning the watcher). mcp is imported at top level in exactly ONE module (server/mcp.py),
#       tools.py + the whole read path are mcp-free — so the CLI imports cleanly without mcp.
#   DISTRIBUTION BUG FIXED (surfaced by a real wheel install, not the editable dev install):
#     seam init read docs/database/schema.sql (OUTSIDE the seam/ package) → FileNotFoundError on
#     ANY `pip install` (docs/ isn't packaged). Fix: hatch force-include docs/database/schema.sql →
#     seam/_data/schema.sql; db.py loads packaged-first (_PACKAGED_SCHEMA_PATH) with dev fallback
#     (_DEV_SCHEMA_PATH). docs/ stays the single canonical source. Guard test added.
#   New tests: tests/integration/test_cli_read.py (8), tests/integration/test_no_mcp_profile.py (2:
#     read works w/ mcp poisoned; start exits 1), tests/unit/test_schema_packaging.py (2: schema
#     resolves + force-include configured).
#   Live verify (fresh venv, `uv pip install .` core-only → NO mcp): `import mcp` fails; seam init/
#     search/query/context all WORK; seam start → install hint + exit 1. (This is what caught the
#     schema-packaging bug — the editable dev install never would.)
#   Test count: 1504 (1492 + 12). Gate green (ruff + mypy 55 files + pytest). MCP tool count stays 10.
#     Plan: .claude/tasks/cli-query-context-search.md.
# Next: v0.1.0 — publish to PyPI as `seam-mcp` (CLI: `pip install seam-mcp`; agents: `[server]` extra).

# ─────────────────────────────────────────────────────────────────────────────
# Frontend Explorer — Tasks F5+F6: DetailPanel, ClusterLegend, build wiring
# ─────────────────────────────────────────────────────────────────────────────
#   TDD: wrote failing tests in web/src/__tests__/DetailPanel.test.tsx (21 tests)
#     confirmed failures (import error — components didn't exist), then implemented.
#   New frontend components:
#     web/src/components/DetailPanel.tsx  — right-side detail panel driven by useSymbol():
#       renders name (heading), all definitions (file:line each — homonym support),
#       signature + docstring from first def, WHY/HACK/NOTE comment rows with kind badges,
#       callers/callees counts, cluster label + colour stripe, and the ClusterLegend at the
#       bottom (via useClusters — TanStack Query caches it from the landing page call).
#     web/src/components/ClusterLegend.tsx — standalone colour-swatch legend:
#       maps all cluster_ids → label + colour via clusterColor() (same hash as SymbolNode
#       stripe and LandingPage dots — NO duplication). Empty list renders gracefully.
#   App.tsx updated: replaced the F4 "detail panel coming in F5" placeholder toast with
#     the real <DetailPanel selectedSymbol={selectedSymbol} />, shown when canvas is active.
#   Makefile: added `build-web:` target (cd web && npm ci && npm run build).
#     .PHONY updated to include build-web.
#   README.md: added `## Seam Explorer` section (seam serve quickstart, feature list)
#     and `## Development` now documents `make build-web`. Release ritual documented:
#     make build-web → uv build → uv publish.
#   Frontend tests: 58 passing (37 baseline + 21 new DetailPanel+ClusterLegend tests).
#   npm run typecheck: clean. npm run build: succeeds (seam/_web/ ~530 kB bundle).
#   Python gate: unchanged (1538+ passed — no Python files modified).
#   Plan: .claude/tasks/seam-explorer-frontend.md (tasks F5+F6 complete).
# Next: v0.1.0 — make build-web → uv build → uv publish (release ritual now documented).

# ─────────────────────────────────────────────────────────────────────────────
# Seam Explorer — Phase 2: impact / trace / changes / constellation + polish
# ─────────────────────────────────────────────────────────────────────────────
#   Branch: feat/explorer-phase2 (one PR, inline build, second-search-box trace).
#   Plan: .claude/tasks/seam-explorer-phase2.md.
#
#   BACKEND (4 HTTP routes over existing handlers — ZERO query-logic dup):
#     GET /api/impact        — handle_seam_impact (verbose=False → lean entries)
#     GET /api/trace         — handle_seam_trace (paths only)
#     GET /api/changes       — handle_seam_changes (NOT_A_GIT_REPO → 400)
#     GET /api/constellation — NEW pure leaf graph_api.build_constellation():
#       clusters + weighted inter-cluster links; homonym-safe name→cluster map;
#       never raises; pre-v4/empty → {clusters:[],links:[]}.
#     Pydantic models added to web.py (TS codegen source). 19 new py tests.
#     No schema change, no migration, MCP tool count stays 10.
#
#   FRONTEND (web/): all overlays derive from the base graph via useMemo —
#     toggling an overlay off restores the original view byte-for-byte.
#     F1 hooks: useImpact/useTrace/useChanges/useConstellation (opt-in enabled).
#     F2 polish: SymbolNode exported badge + visibility chip + tier ring; Legend
#       (confidence/clusters/risk); FilterBar (kind + confidence toggles).
#     F3 impact overlay: paint blast radius by tier, dim rest, off-canvas cards,
#       risk-summary chip. F4 trace: second "Trace to…" box → bold shortest path.
#     F5 ChangesDrawer: changed symbols + risk badge + NOT_A_GIT_REPO notice.
#     F6 ConstellationCanvas: Overview⇄Neighborhood toggle; region click drills in.
#     Pure helpers (riskTier/edgeFilter/impactOverlay/tracePath/constellationLayout)
#       unit-tested in isolation.
#
#   GATES: python 1558 passed, ruff + mypy clean. web 92 passed, tsc clean,
#     production build OK (seam/_web ~551 kB). Live smoke (4853-symbol index):
#     impact/trace/constellation/changes/SPA all return correct data.
# Next: /review + manual QA, then merge feat/explorer-phase2.
