diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index fa032d6621..852b366834 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -79,7 +79,6 @@ {"_type":"issue","id":"polylogue-lzh8","title":"Declare SEMANTIC_REPARSE index bump for Claude Workflow artifact classification (PR #3088)","description":"Investigation 2026-07-31 (worktree agent-a7335b82eed35c7cf), triggered by\noperator report that Claude Code Workflow artifacts appear BOTH normalized\nAND independently ingested raw as empty sessions.\n\nFINDING: the classification code is already correct. polylogue/archive/\nartifact_taxonomy/runtime.py:classify_artifact_path consults OriginSpec's\nartifact_rules (polylogue/sources/origin_specs.py, added by 1e0246d77 / PR\n#3088, \"admit Claude Workflow artifacts through OriginSpec\", 2026-07-18) and\ncorrectly returns parse_as_session=False for workflow_run_snapshot,\nworkflow_journal, agent_sidecar_meta, and adopt_manifest artifact kinds.\nVerified directly against the live paths (python3 -c\n\"classify_artifact_path(...)\") -- current code classifies them correctly.\n\nBut 1e0246d77 changed session/fact classification semantics for an already-\nrunning archive WITHOUT declaring an INDEX_SCHEMA_VERSION bump in\npolylogue/storage/sqlite/lifecycle.py (checked: no lifecycle.py/index.py\nchange in that commit, and no v33-v47 IndexDeltaDeclaration references\npolylogue-2qx.2 or the Workflow admission PR). Per docs/architecture (\"Schema\nregimes\"), only a declared SEMANTIC_REPARSE delta routes an index.db through\n`polylogue ops reset --index \u0026\u0026 polylogued run`; a semantic parser change\nwith no declared bump leaves already-materialized wrong-classification rows\nuntouched forever, because the daemon's fast-forward convergence has no\nsignal that anything changed.\n\nMEASURED LIVE IMPACT (index.db read-only query, 2026-07-31):\n zero-message claude-code-session rows total: 5,193\n of these, joined to a source_path under a `workflows/` artifact family: 172\n agent_sidecar_meta (subagents/workflows/*/agent-*.meta.json): 164\n workflow_run_snapshot (workflows/wf_*.json): 7\n other (workflow_journal / adopt_manifest): 1\n acquired_at_ms range for these 172: 2026-07-14 10:52 UTC .. 2026-07-26\n 19:18 UTC -- i.e. ALL acquired while the deployed daemon build predated\n the fix. The sinnix flake's polylogue input only advanced to a revision\n containing 1e0246d77 on 2026-07-29 (flake.lock lastModified\n 1785367887 = 2026-07-29 23:31 UTC; `git merge-base --is-ancestor` confirms\n 1e0246d77 is an ancestor of the pinned rev 5e23e6a). So this is deploy-lag\n contamination the fix code cannot self-heal without a reparse trigger, not\n a currently-active defect in the shipped classification logic.\n\nSeparately, polylogue-omsw's tool-result-sidecar and file-history-snapshot\npopulations are a DIFFERENT, still-open acquisition-scope gap (not covered\nby this bead) -- do not conflate the two when scoping remediation.\n\nDO NOT execute the reset live from this investigation; this bead exists to\nmake the repair describable and consented rather than silent. Per this\nrepo's ops.db/index.db durability rules, `polylogue ops reset --index` is a\ndisposable-tier rebuild, not durable-data loss, but it is still a\nconsequential live-daemon action (extended downtime rebuilding ~20K\nsessions) that needs explicit operator scheduling, not an agent-triggered\nversion bump buried in an unrelated PR.\n","acceptance_criteria":"1. polylogue/storage/sqlite/lifecycle.py gets a new IndexDeltaDeclaration bumping INDEX_SCHEMA_VERSION with classes=(SEMANTIC_REPARSE,), whose comment names 1e0246d77/#3088 as the retroactive semantic change being captured and cites the measured live-impact counts. 2. The bump lands in a PR whose body explicitly tells the operator a 'polylogue ops reset --index \u0026\u0026 polylogued run' is now required, so it is scheduled deliberately (not silently triggered by routine deploy). 3. After the rebuild, the 172+ contaminated sessions reclassify to their correct non-session disposition (verified by re-running the same index.db query this bead's evidence used and confirming zero remain). 4. devtools lab policy schema-versioning stays green.","status":"open","priority":1,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T05:59:40Z","created_by":"Sinity","updated_at":"2026-07-31T05:59:40Z","dependencies":[{"issue_id":"polylogue-lzh8","depends_on_id":"polylogue-2qx.2","type":"related","created_at":"2026-07-31T07:59:51Z","created_by":"Sinity","metadata":"{}"},{"issue_id":"polylogue-lzh8","depends_on_id":"polylogue-9ykn","type":"related","created_at":"2026-07-31T07:59:50Z","created_by":"Sinity","metadata":"{}"},{"issue_id":"polylogue-lzh8","depends_on_id":"polylogue-omsw","type":"related","created_at":"2026-07-31T07:59:50Z","created_by":"Sinity","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-roax","title":"FTS invariant violated: ops status says 100% indexed while queries fail as incomplete","description":"MEASURED 2026-07-31 on the live archive.\n\nCONTRADICTION between two surfaces:\n polylogue ops status -\u003e 'FTS: 100.0% indexed'\n polylogue find \u003canything\u003e -\u003e exit 1, DatabaseError,\n 'Search index is incomplete. Run polylogued run.'\nBoth were run minutes apart against /realm/db/polylogue with the daemon RUNNING.\nSo either the status surface measures something the query path does not require,\nor one of them is wrong. A user-facing error telling the operator to run a daemon\nthat is already running is itself a broken contract.\n\nWHY THIS IS AN INVARIANT VIOLATION, not just a bug: the automagic-invariants\ndoctrine (bd memory 'automagic-invariants') states that FTS coherence belongs to\ndaemon convergence/startup/write-path invariant enforcement, NOT to routine\noperator maintenance commands. Search being degraded while the daemon runs means\nthe convergence path either is not running the FTS stage, is failing it silently,\nor completed it against a different index generation than the query path opens.\n\nCONTEXT that may be causal, all measured tonight:\n- The daemon was livelocked for hours (raw materialization yielding to a pending\n browser-capture spool every 60s while ingesting nothing) and was restarted\n around 06:20. The index may have been left mid-convergence.\n- An index-generation swap happened 2026-07-30 (.index-generations/, active\n pointer gen-1785377665711-06297b00). A dataset lane separately measured 4,186\n embeddings rows (2.2%) pointing at message_ids no longer in index.db, which it\n attributed to that swap with no cross-tier reconciliation (bead polylogue-feu0).\n An FTS table left behind by the same swap would present exactly this way.\n- A dataset lane also measured 10,837 blocks with real text missing from\n messages_fts (down from 36,757), spot-checked directly (appended to\n polylogue-5vbs). That is a real gap, but 'incomplete' as a hard query-path\n failure is a different symptom from 'partially indexed'.\n- Concurrent stderr warning on every CLI call: 'format drift: origin\n aistudio-drive 100% of 302 records since 2026-07-01 carry unseen shapes'.\n\nAC: the two surfaces agree; a degraded FTS either self-heals via convergence or\nreports the SAME state through both surfaces; and the error message does not\ninstruct the operator to start a daemon that is already running.","status":"closed","priority":1,"issue_type":"task","assignee":"Sinity","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T05:26:15Z","created_by":"Sinity","updated_at":"2026-07-31T06:33:32Z","started_at":"2026-07-31T06:33:11Z","closed_at":"2026-07-31T06:33:32Z","close_reason":"Root cause: daemon/convergence_stages.py::repair_messages_fts_surface recorded state=ready with a fabricated source_rows=1,indexed_rows=1 placeholder (detail='bounded global messages_fts repair completed; exact counts skipped') after its exhaustive (not partial) reconcile pass, purely to dodge two cheap COUNT(*) probes. cli/commands/status.py then defaulted the resulting None coverage_pct to a hard-coded 100.0% whenever messages_ready was true -- the '100% indexed' the operator saw was never a measurement. The query path (storage/fts/freshness.py) independently trusts/distrusts the same ledger row via freshness_ready_record_trusted with no knowledge of the placeholder, so the two surfaces could show different confidence for the same state. Live evidence: /realm/db/polylogue/index.db carried exactly this poisoned row at investigation time; live messages_fts_docsize already matched the real indexable block count (0 missing) -- convergence HAD actually finished, it just lied about verifying it. Fix (PR #3429): repair_messages_fts_surface now records real post-repair counts via two plain COUNT(*) probes instead of the placeholder; removed the now-dead BOUNDED_MESSAGE_FTS_REPAIR_DETAIL/counts_available special-casing in fts_status.py; CLI no longer defaults an unmeasured coverage_pct to a fabricated percentage (prints 'coverage unknown'); centralized and reworded the FTS repair-hint text so it never tells the operator to start a daemon that might already be running. New regression test proves status and query-path readiness agree post-repair (verified it fails against the pre-fix code). All three AC items satisfied: surfaces derive from the same ledger check; repair now honestly self-heals (real counts recorded, not a lie); error text no longer presumes the daemon is down. devtools verify --quick green; devtools test on all touched/adjacent modules green (44+181+23 tests).","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-gvgi","title":"Non-transcript JSONL under ~/.claude/projects/ ingested as claude-code-session: 96,748 empty phantom messages","description":"Adversarial dataset investigation (H7) found a single phantom claude-code-session with native_id literally 'conversation_relationships' and message_count=96,748, all zero-block/zero-word (role=user, material_origin=human_authored, message_type=message, no user_context_text). It accounts for 96,748 of the archive's 101,765 total zero-block messages (95.1%).\n\nTraced to source: raw_sessions.raw_id=aa5e35075a0c0b809ae70811c2e5515a4b02e1890518078028149c4258ea3e93, source_path=/home/sinity/.claude/projects/-realm-project-sinex/analysis/index/conversation_relationships.jsonl (251,568 lines, 52MB blob). This file is NOT a Claude Code transcript -- it is a sinex analysis-index artifact recording parent/child/conversation graph edges (each line: conversation/parent/child/type/timestamp keys, type is assistant or user). It happens to live under a directory tree shaped like ~/.claude/projects/PROJECT/... and its per-line type field was apparently enough to satisfy a loose provider-shape check, causing dispatch to lower it as a claude-code-session with one empty message per JSONL line.\n\nDistinct root cause from the already-tracked polylogue-b508 (agent-star.meta.json sidecars, fixed PR 3403): that class is Claude Code own sidecar files; this is a third-party tool artifact that merely sits in the scanned directory tree and pattern-matches a provider detector.\n\nBlast radius (verified 2026-07-31 on live archive): 1 phantom session, 96,748 phantom messages (about 2 percent of the archive total 4,900,553 messages), 52MB wasted raw blob. Also the leading contributor to the C4 metric (sessions with created_at_ms NULL) growing from 1,117 (post-de-inflation) to 5,382 -- 97.8 percent of those NULL-created_at_ms sessions have word_count=0, consistent with this and similar phantom-ingestion artifacts accumulating.","acceptance_criteria":"1. Root-cause: identify the exact detector/heuristic that accepted this file as a claude-code-session, tighten it to require genuine Claude Code transcript shape evidence (sessionId/uuid/message envelope), not just a bare type key. 2. Purge the phantom session and its 96,748 messages/blocks from index.db via targeted delete, not full rebuild (rebuild would recreate it per the b508 lesson about the parse chokepoint in sources/revision_backfill.py). 3. Quarantine or reclassify the source raw so ops reset --index does not resurrect it. 4. Add a regression test: a JSONL file with type-assistant/user shaped lines but no session/message envelope must not be classified as any chat-transcript origin.","status":"open","priority":1,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T04:56:32Z","created_by":"Sinity","updated_at":"2026-07-31T04:56:32Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"polylogue-oqib","title":"Wire root: session filter end-to-end; default find to top-level sessions","description":"Split from polylogue-cijx.4 decision 4 (\"default result unit is the\ntop-level session\"). That bead's other three decisions (repo identity,\nrepo-relative paths, structural-label projection) landed; this one didn't,\nbecause it turned out to be a separate-shaped, higher-blast-radius change.\n\nWHAT EXISTS TODAY: `sessions.parent_session_id` and `Session.is_root`\n(`parent_id is None`) are correct and already used by a plan-level `root:\nbool | None` field (`archive/query/plan.py`) plus a fluent\n`.is_root(True)` builder method (`archive/filter/builder.py`). But `root`\nis completely unreachable from every actual query surface:\n\n - No `spec_attr` on its `QueryFieldDescriptor` in\n `archive/query/fields.py` (unlike `origin`/`repo`/`tag`/etc, which do\n have one) -- so `SessionQuerySpec.from_params`/`from_expression` can\n never set it.\n - No case in the Lark DSL transformer in `archive/query/expression.py`\n (`repo:`/`origin:`/`tag:`/... all have an explicit `fname == \"...\"`\n branch there; `root` has none). `continuation`/`sidechain`/\n `has_branches` are in the identical unreachable state -- this isn't\n unique to `root`.\n - No CLI flag anywhere (`rg` for `--root`/`--continuation`/`--sidechain`\n across `cli/*.py` returns nothing).\n\nLive measurement (read-only, `/realm/db/polylogue/index.db`, 2026-07-31):\n15,401 of 23,296 sessions (66.1%) are root/top-level; the other 33.9% are\nsubagent/branch children. A default `find` with no filters returns both,\nunlabeled as to which is which.\n\nSCOPE for whoever picks this up:\n 1. Add `root: bool | None = None` to `SessionQuerySpec`\n (`archive/query/spec.py`) and wire it through `query_spec_to_plan`.\n 2. Add a `root` case to the Lark DSL transformer\n (`archive/query/expression.py`) -- decide the value syntax (`root:true`\n /`root:false` to match other boolean-flavored fields, or a bare\n `root`/`-root` token; there's no existing precedent to copy since\n `continuation`/`sidechain` never got wired either -- worth deciding\n the pattern once for all three rather than one-off for `root`).\n 3. Register field metadata/discovery docs\n (`archive/query/metadata.py`/`discovery.py`) and regenerate CLI/MCP/\n OpenAPI docs (`devtools render all`).\n 4. DEFAULT-BEHAVIOR DECISION (the actual design call, not just plumbing):\n cijx.4's decision 4 wants the *default* list to be top-level-only,\n with children reachable only via an explicit `root:false` (or\n equivalent). That changes the result set of every unfiltered `find`/\n `list()`/MCP `query` call across CLI, Python API, MCP, and daemon HTTP\n -- audit existing callers/tests that assume today's \"everything\"\n default before flipping it, or scope the default change to the CLI\n `find` verb specifically (the interactive surface AC4's own proof\n text names: \"re-running `polylogue find repo:polylogue` and showing\n named non-fanout rows\") and leave the Python API/MCP defaults\n unfiltered for programmatic composability. Either choice needs to be\n made explicit and stated in the PR, not left implicit.\n\nACCEPTANCE CRITERIA (carried from polylogue-cijx.4 AC4, unchanged):\nDefault result unit is the top-level session, proven by re-running\n`polylogue find repo:polylogue` and showing named non-fanout rows; children\nremain reachable through an explicit filter, never silently filling the\ndefault list.\n","status":"open","priority":1,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T04:41:40Z","created_by":"Sinity","updated_at":"2026-07-31T04:41:40Z","dependencies":[{"issue_id":"polylogue-oqib","depends_on_id":"polylogue-cijx.4","type":"discovered-from","created_at":"2026-07-31T06:41:39Z","created_by":"Sinity","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-qj5x","title":"Decision: remove Origin.BEADS_ISSUE — Beads data belongs in the work-evidence graph, not sessions","description":"DESIGN INVESTIGATION VERDICT (2026-07-31, design doc: .agent/scratch/live/beads-handling-design-2026-07-31.html). The operator challenged BEADS_ISSUE-as-Origin (\"beads is not a chatlog\"). Investigation confirms the doubt with measurements:\n\n1. interactions.jsonl is 100% field_change rows (polylogue: 2,249 rows / 862 issues = priority 1124 + status 1071 + assignee 54), actor constant \"Sinity\" in 2,249/2,249. The parser synthesizes English prose from these (\"Sinity changed priority from 3 to 2\") into Role.USER messages with MaterialOrigin.RUNTIME_PROTOCOL — ~924 projected sessions containing zero human or assistant content. Same structural shape as the hook-event inflation incident (polylogue-31r1, 83,286→18,391 sessions).\n2. The rich Beads artifact — issues.jsonl (1,260 issues, 907 with notes, 1,857 dependency edges, descriptions/design/AC) — is NOT ingested by the Origin route at all. The Origin captures the least informative beads file.\n3. The architecturally correct home already exists in code: insights/work_effects.py BeadsIssueEffectAdapter reads the SAME interactions.jsonl as ObservedRepositoryEffect facts, and devtools/mandate_continuity_replay.py build_repository_claim_graph builds claim nodes from it. docs/internals.md 688-733 documents both. BEADS_ISSUE-as-Origin is a redundant second representation of data the archive already models correctly as effects/claims.\n4. Revealed preference: fully wired for months (parser/detector/dispatch/OriginSpec), acquired nothing, nobody noticed. #3416's sources.beads_roots defaults to () — still zero ingested (measured: 0 beads-issue sessions among 23,296 in the live index).\n5. Scaffolding rot: origin_specs.py:796 references stream_parser_path \"beads.py:parse_beads_stream\" — that function does not exist anywhere (dangling reference). Completeness mode is \"proposed\", never harvested from a real sample.\n\nREMOVAL PATH (no shims, no deprecation theater — nothing ingested, zero migration risk): delete Origin.BEADS_ISSUE + Provider.BEADS, sources/parsers/beads.py + its tests, dispatch branches (dispatch.py 44/46/56/198/239/1033/1159/1260), _beads_spec + completeness mode (origin_specs.py 787-805, 997-1030), core/sources.py mappings (126-129, 158, 236, 254, 300); drop \"beads-issue\" from session_links dst_origin CHECK (derived-tier index bump, declare delta class — 0 affected rows measured, in-place fast-forward safe); remove #3416 beads_roots acquisition wiring (no users exist; hard removal is policy-compliant per no-compat-pre-adoption). Keep artifact-taxonomy shape classification (looks_like_beads_interaction) keyed off shape, so a stray uploaded ledger classifies as a non-session artifact instead of unknown-export sessions — same treatment hook events got in 31r1. BeadsIssueEffectAdapter and the claim-graph builder are untouched and become the sole consumers of the ledger.\n\nWHAT IS NOT LOST: ledgers are git-tracked in their repos (durability is git's, not polylogue's); issue state-transition evidence (timestamps, old→new, close reasons carrying commit hashes) stays reachable via the effect adapter for 1vpm.6 reconciliation; bead ids in real sessions remain FTS-searchable (phrase \"polylogue-x4s\" already matches 248 real messages). What ingestion WOULD have added: +4% sessions, all synthetic protocol prose polluting exactly the FTS queries used to find real work on a bead.\n","notes":"Follow-on filed: polylogue-5jnq (issues.jsonl as work-evidence issue nodes, 1vpm.6 adapter). Related open beads: polylogue-37t.13 (beads\u003c-\u003eassertions boundary revisit — its premise 'beads-history ingestion landed (#2800)' refers to the Origin route this decision removes; re-anchor it on the work-evidence graph), polylogue-pbuh (typed pr-link records = the session↔PR leg of the three-way join).","status":"open","priority":1,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T04:36:56Z","created_by":"Sinity","updated_at":"2026-07-31T04:37:54Z","dependency_count":0,"dependent_count":2,"comment_count":0} {"_type":"issue","id":"polylogue-l9su","title":"session_commit.py ignores typed claude_pr_link/claude_bridge_session events and Claude-Session git trailers, regex-scans instead","description":"Two independent typed-signal-ignored gaps in polylogue/insights/session_commit.py, found during the 2026-07-31 heuristics audit (parallel to polylogue-pbuh/polylogue-1vpm.7's exemplars).\n\nGAP 1 -- GitHub PR/issue refs. extract_github_refs() (session_commit.py:26-142) regexes raw session message text for https://github.com/.../pull/N, owner/repo#N, and bare #N -- acknowledged in its own comments as a false-positive-prone heuristic (bare #N can match heading anchors / arbitrary numbers). Meanwhile polylogue-pbuh's fix (already landed on this branch, commit 5e23e6abf / index v46) now persists the Claude Code pr-link sidecar record as a typed claude_pr_link session_event, and bridge-session as claude_bridge_session. VERIFIED live: sqlite3 index.db \"SELECT COUNT(*) FROM session_events WHERE event_type='claude_pr_link'\" -\u003e 18,967 rows (167 distinct sessions); claude_bridge_session -\u003e 12,154 rows. VERIFIED zero readers: grep -rn claude_pr_link polylogue/ (excluding the writer in code_parser.py) returns nothing -- session_commit.py, correlation_view.py, and every consumer of build_correlation_result still regex-scan text instead of reading these typed events. This is a fresh instance of the pbuh pattern that survived the pbuh fix landing: the parse-side fix shipped, the read side never got updated to use it.\n\nGAP 2 -- session-to-commit attribution. detect_session_commits() (session_commit.py:256-363) attributes a git commit to an authoring session via time-window scan (+-2h around session timestamps) plus file-overlap scoring (score_file_overlap, confidence thresholded at 0.3) or an in-text commit-SHA regex match (explicit_ref, confidence 0.95 hardcoded). It never reads git commit trailers. This repo's own commit convention (CLAUDE.md, global agent instructions) appends 'Co-Authored-By: Claude ... ' plus 'Claude-Session: https://claude.ai/code/session_\u003cid\u003e' to every agent-authored commit -- a typed, zero-ambiguity session-authorship signal. VERIFIED: git log --all --format=%B | grep -oE 'Claude-Session: [^ ]+' | wc -l -\u003e 116 commits in this repo alone carry the trailer; grep -rn 'Claude-Session\\|Co-Authored-By' polylogue/ --include='*.py' returns zero hits anywhere in the codebase. Stronger evidence the fix was anticipated but never wired: the session_commits table schema itself (storage/sqlite/archive_tiers/index.py:922) already declares detection_type TEXT CHECK(... IN ('time_window','file_overlap','explicit_ref','origin_reported')) -- 'origin_reported' is a live CHECK-constraint value with ZERO rows using it (VERIFIED: sqlite3 index.db \"SELECT detection_type, COUNT(*) FROM session_commits GROUP BY detection_type\" -\u003e only explicit_ref, 2,990 rows). The schema slot for a typed session-commit link has existed, unused, while a scored heuristic fills the table instead.\n\nNOT EVALUATED: no test in tests/unit/insights/test_session_commit.py asserts accuracy of file_overlap/time_window scoring against ground truth -- only the arithmetic of score_file_overlap() itself is unit-tested (confidence math, not hit-rate).\n\nBLAST RADIUS: session_commits backs the PF-D1 receipts demo (polylogue-212.2/xyel), the provenance-carrying-PRs bead (polylogue-kph), and the Hermes forensics report (polylogue-fs1.4) -- all four read session-to-PR/commit linkage through this exact machinery. 2,990 live session_commits rows, all detection_type=explicit_ref (VERIFIED); repo breakdown polylogue=1,060, sinex=879, sinnix=495, sinity-lynchpin=104 (VERIFIED).","acceptance_criteria":"1. detect_session_commits (or a new higher-priority step ahead of it) parses git commit trailers (Co-Authored-By: Claude / Claude-Session: \u003curl\u003e) via git log --format=%B%n---%n and, when a trailer's session id matches an archived session, records a session_commits row with detection_type='origin_reported' and confidence=1.0, superseding time_window/file_overlap for that pair. 2. build_correlation_result (or its caller) reads claude_pr_link/claude_bridge_session typed session_events before falling back to extract_github_refs' text regex; the regex path is kept only as a fallback for sessions with no typed event, and its results are labeled distinctly from typed results in the output payload. 3. A live re-measure reports the before/after split of session_commits by detection_type, and the before/after count of PR/issue refs sourced from typed events vs regex. 4. tests/unit/insights/test_session_commit.py gains a fixture asserting the trailer-parse path takes priority over file_overlap/time_window for a commit carrying a matching Claude-Session trailer.","notes":"CORRECTION 2026-07-31 (self-correction, keep both versions visible per audit discipline): the original description implied the PERSISTED session_commits table (2,990 rows, all detection_type='explicit_ref') is filled by detect_session_commits()'s file-overlap/time-window scoring. VERIFIED that is wrong -- storage/sqlite/archive_tiers/write.py:4039-4063 shows session_commits is actually populated straight from session.git_commit_hash (a typed field the agent-runtime parser already reports, method='parser-git-meta', confidence hardcoded 1.0). This is a narrow but honest fact (HEAD at session capture time, not 'commit this session produced') and is NOT itself an instance of the audited pattern -- it already prefers a typed field.\n\nThe real, still-live gap is the ON-DEMAND correlation surface: build_correlation_result (session_commit.py:387-449) IS wired live -- api/archive.py:5406 and insights/correlation_view.py:60 both call it, reachable via the 'analyze correlation' CLI/API path (VERIFIED via grep, both call sites exist outside session_commit.py/its tests). THIS is where detect_session_commits' file-overlap/time-window scoring and extract_github_refs' text regex actually run, live, on every invocation -- and neither reads git commit trailers nor the typed claude_pr_link/claude_bridge_session session_events. The bead's AC1-AC4 stand unchanged: they target this on-demand path, not the persisted table. cijx.1's own notes (read after filing this bead) independently confirm session_commits has 0 readers and stores a different, narrower fact than commit attribution -- consistent with this correction, not contradicting it.","status":"closed","priority":1,"issue_type":"task","assignee":"Sinity","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T04:32:05Z","created_by":"Sinity","updated_at":"2026-07-31T05:39:45Z","started_at":"2026-07-31T05:39:19Z","closed_at":"2026-07-31T05:39:45Z","close_reason":"Fixed in PR #3425 (fix/insights/session-commit-typed-evidence). Ref polylogue-l9su.\n\nAC1 (trailer parsing, origin_reported): satisfied for the on-demand correlation\npath -- detect_session_commits now parses git commit Claude-Session trailers\nvia a second git-log pass and, when a trailer token matches one of the\nsession's own bridge_session_ids (from its claude_bridge_session events),\nrecords detection_method=\"origin_reported\", confidence=1.0, superseding\nfile_overlap/time_window/explicit_ref for that commit. NOT done: writing\norigin_reported rows into the persisted session_commits SQLite table --\nthat table is populated only at batch-ingest time from\nsession.git_commit_hash (storage/sqlite/archive_tiers/write.py, explicitly\nout of this lane's declared surface) and is a different, narrower fact (repo\nHEAD at session-capture time), matching the bead's own self-correction note.\nIf the operator wants the durable table to carry this fact too, that is a\nseparate follow-up against write.py.\n\nAC2 (typed session_refs/session_events before regex fallback): satisfied.\nbuild_correlation_result now accepts typed_pr_refs/typed_issue_refs (built\nfrom session_refs via new typed_refs_from_session_refs helper) and uses them\nas authoritative; the regex scan still runs (needed for file_paths\nregardless) but is used only as a fallback for sessions with no typed\nevidence for that ref kind, and to detect disagreement.\n\nAC3 (live re-measure): done, read-only against /realm/db/polylogue/index.db.\n167 sessions carry typed pull_request session_refs (1,690 PR-number rows).\nOld regex-only extraction over the same sessions' text finds 1,934 PR\nmentions: 102 sessions agree exactly with typed evidence, 65 would have\nsurfaced extra/different numbers (the silent-disagreement class this fix\nnow surfaces). Trailer side: 8 of 9 distinct Claude-Session trailer tokens\nin this repo's own git history resolve to a real archived session via\nclaude_bridge_session (9 sessions total, one token maps to 2). session_commits\ntable unaffected (still 2,990 rows, all explicit_ref) since write.py is out\nof scope.\n\nAC4 (surface disagreements, fail loud): satisfied. New CorrelationDisagreement\ndataclass + SessionCorrelationResult.disagreements list, populated for both\ncommit-trailer conflicts and PR/issue-ref conflicts; rendered in the CLI\n(read --view correlation) and included in the JSON payload. GitHubRef gained\na `source` field (typed_session_ref vs heuristic_regex) so which mechanism\nresolved each ref is visible per-row, not just in the disagreements list.\n\nPoint 5 (read/query surface for cijx.1 dependents): the surface already\nexisted (`read --view correlation`, Polylogue.session_correlation_payload) --\nthe blocker was purely that it ignored typed evidence it already had access\nto. No new CLI/MCP surface was needed; both existing entrypoints were wired\nto fetch session_refs + bridge_session_ids and pass them through. cijx.1 and\ndependents (212.2, xyel, kph, fs1.4) can now read session-\u003ePR linkage through\nthis path with typed-evidence priority instead of pure heuristic guessing --\nwhether that fully unblocks each of those beads is for their own owners to\nre-triage against their specific AC, not asserted here.\n\nAlso fixed in passing (required for the fallback path to work at all):\n_parse_git_log_blocks had a latent bug where splitting git log output on a\nliteral \"\\n---\\n\" token left every commit's changed-file set permanently\nempty (file_overlap detection never worked against a real repo, only\nexercised in tests against nonexistent paths). Switched to %x1e/%x1f\nASCII field/record separators.\n\nVerification: devtools test tests/unit/insights/test_session_commit.py\ntests/unit/cli/test_correlate_view.py (45 passed, new fixtures build a real\ngit repo via subprocess); devtools verify --quick (exit 0); mypy --strict\non the three touched modules (no issues).","labels":["area:insights","lane:read-contracts"],"dependencies":[{"issue_id":"polylogue-l9su","depends_on_id":"polylogue-1vpm.7","type":"related","created_at":"2026-07-31T06:32:05Z","created_by":"Sinity","metadata":"{}"},{"issue_id":"polylogue-l9su","depends_on_id":"polylogue-pbuh","type":"related","created_at":"2026-07-31T06:32:05Z","created_by":"Sinity","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-swqu","title":"Update sinnix Claude Code hook settings template to stop baking a stale --sidecar-dir","description":"Root cause of the 2026-07-31 hook-spool backlog (polylogue-k8wv): sinnix's\n/realm/project/sinnix/dots/claude/settings.json template (rendered to\n~/.claude/settings.json) has polylogue-hook commands with a literal\n`--sidecar-dir /home/sinity/.local/share/polylogue/hooks` baked in from an\ninstall that predates the archive root's move to /realm/db/polylogue. This\nis a sinnix-repo fix, not polylogue (out of scope for the polylogue PR that\nfiles this bead).\n\nTwo options, either acceptable:\n1. Re-run `polylogue hooks install` against the live settings.json and copy\n the regenerated hooks.* block back into the sinnix dotfiles template, OR\n2. Add a periodic/activation-time check (Home Manager activation script or a\n sinnix service) that re-runs `polylogue hooks install` whenever\n $HOME/.config/polylogue/polylogue.toml's archive root changes, so this\n class of drift cannot recur silently.\n\npolylogue now ships `polylogue.hooks.hook_install_sidecar_drift()` and a\ndaemon-heartbeat warning that logs when the installed command's baked path\ndiverges from the live-resolved one -- use that as the detection signal\nduring the sinnix-side fix.","notes":"Filed alongside PR https://github.com/Sinity/polylogue/pull/3418 which adds hook_install_sidecar_drift() detection to make this class of drift loud.","status":"open","priority":1,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T04:16:15Z","created_by":"Sinity","updated_at":"2026-07-31T04:22:29Z","dependency_count":0,"dependent_count":0,"comment_count":0} @@ -476,6 +475,7 @@ {"_type":"issue","id":"polylogue-upbv","title":"Temporary-chat tabs never show accurate archive-state (always 'missing')","description":"browser-extension: background.js's conversationIdForUrl returns TEMPORARY_CHAT_SENTINEL for a ChatGPT temporary-chat URL (fixed in PR #3411 to unblock automatic capture at all). Multiple call sites (refreshActiveTabArchiveState, captureTab's pageSessionId derivation) query /v1/archive-state and log ledger/UI state keyed by that sentinel rather than the conversation's real ephemeral provider_session_id (only known after a successful capture's envelope). Net effect: a temporary chat's popup/badge 'captured' indicator never turns accurate, and refreshActiveTabArchiveState's auto_capture_missing branch re-fires every ~30s (throttled) treating an already-captured temporary chat as missing. Not a data-loss bug (content-hash dedup makes the redundant re-captures cheap/idempotent), but real UI inaccuracy and wasted background work. Fix requires giving background.js a per-tab 'last known real captured id' to prefer over the sentinel at every archive-state query site, not just the ones fixed in #3411 (freshness-hint mismatch, captureTab's own pageSessionId). Found during PR #3411 Codex review (P1 finding), partially fixed there (freshness-hint rejection, which WAS a real data-loss bug, and captureTab's own log/state precedence); this bead tracks the remaining archive-state-query-site work.","status":"open","priority":2,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T05:24:44Z","created_by":"Sinity","updated_at":"2026-07-31T05:24:44Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-qqi1","title":"read --view summary silently falls through to transcript","description":"MEASURED 2026-07-31 while rendering sessions to /realm/inbox/polylogue_renders/.\n\nFor every session rendered, summary.md is BYTE-IDENTICAL to transcript.md:\n conversation_relationships summary 967,558 B == transcript 967,558 B\n 019f12b5-1a85 (135k msgs) summary 190,075,729 B == transcript 190,075,729 B\n 019ce460-6914 (175 msgs) summary 406,924 B == transcript 406,924 B\n\nread --views documents summary as: 'Compact human browse view for matched\nsessions', projection=sessions, body=full. A 190 MB 'compact browse view' is\nnot compact -- the view is silently falling through to the transcript renderer\nrather than producing a session-level summary.\n\nReproduce:\n env -u POLYLOGUE_ARCHIVE_ROOT polylogue --id \u003csession_id\u003e read --view summary --format markdown --to stdout\n\nNote this is the same defect FAMILY as the rest of tonight's findings: a\ndeclared behaviour silently degrading to a different one with no error. The\ncaller cannot tell the summary view did not run.\n\nAC: summary renders a session-level summary distinct from transcript, or the\nview is removed; a test pins that summary output is materially smaller than\ntranscript for a multi-message session.","status":"open","priority":2,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T05:19:52Z","created_by":"Sinity","updated_at":"2026-07-31T05:19:52Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-feu0","title":"embeddings.db has 4,186 message_embedding_refs pointing to messages no longer in index.db","description":"Adversarial dataset investigation (H10) cross-checked embeddings.db against the live index.db and found stale references left behind by index-tier changes (the tiers are independently rebuildable; embeddings.db is not automatically pruned when index.db loses rows).\n\nMeasured 2026-07-31 on live archive: 187,888 total message_embedding_refs. Of these, 4,186 (2.2%) reference a message_id absent from index.db messages, and 4,076 (2.2%) reference a session_id absent from index.db sessions. All sampled orphans are claude-code-session; the largest single orphaned session contributed 713 refs. Every embedding_input_hash in message_embedding_refs does have a matching message_embeddings_meta row (0/187,888 missing) -- the break is specifically refs-to-index, not refs-to-vectors.\n\nLikely cause: a session/message set was deleted or replaced in index.db (targeted repair, de-inflation cleanup, or the 2026-07-30 08:36 index generation swap) without a corresponding embeddings.db cleanup pass. Related but not identical to polylogue-wmsc (embedding freshness/staleness invariant, about content-hash staleness not deletion) and polylogue-8jg9.6 (persistent lineage identity across tier generations, about archive-level identity not per-row cleanup).","acceptance_criteria":"1. Quantify whether this is a one-time backlog (e.g. from the 2026-07-30 index generation swap or a prior de-inflation pass) or an ongoing leak with no GC path -- check whether any current write path deletes index.db session/message rows without emitting a corresponding embeddings.db cleanup instruction. 2. Add a GC/reconciliation pass (startup check, convergence stage, or explicit devtools command) that removes message_embedding_refs (and any orphaned message_embeddings/message_embeddings_meta rows once refcounted) whose message_id/session_id no longer resolves in index.db. 3. Re-run the H10 measurement after the fix lands; both counts should be 0 on a quiescent archive.","status":"open","priority":2,"issue_type":"bug","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T04:56:55Z","created_by":"Sinity","updated_at":"2026-07-31T04:56:55Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"polylogue-oqib","title":"Wire root: session filter end-to-end; default find to top-level sessions","description":"Split from polylogue-cijx.4 decision 4 (\"default result unit is the\ntop-level session\"). That bead's other three decisions (repo identity,\nrepo-relative paths, structural-label projection) landed; this one didn't,\nbecause it turned out to be a separate-shaped, higher-blast-radius change.\n\nWHAT EXISTS TODAY: `sessions.parent_session_id` and `Session.is_root`\n(`parent_id is None`) are correct and already used by a plan-level `root:\nbool | None` field (`archive/query/plan.py`) plus a fluent\n`.is_root(True)` builder method (`archive/filter/builder.py`). But `root`\nis completely unreachable from every actual query surface:\n\n - No `spec_attr` on its `QueryFieldDescriptor` in\n `archive/query/fields.py` (unlike `origin`/`repo`/`tag`/etc, which do\n have one) -- so `SessionQuerySpec.from_params`/`from_expression` can\n never set it.\n - No case in the Lark DSL transformer in `archive/query/expression.py`\n (`repo:`/`origin:`/`tag:`/... all have an explicit `fname == \"...\"`\n branch there; `root` has none). `continuation`/`sidechain`/\n `has_branches` are in the identical unreachable state -- this isn't\n unique to `root`.\n - No CLI flag anywhere (`rg` for `--root`/`--continuation`/`--sidechain`\n across `cli/*.py` returns nothing).\n\nLive measurement (read-only, `/realm/db/polylogue/index.db`, 2026-07-31):\n15,401 of 23,296 sessions (66.1%) are root/top-level; the other 33.9% are\nsubagent/branch children. A default `find` with no filters returns both,\nunlabeled as to which is which.\n\nSCOPE for whoever picks this up:\n 1. Add `root: bool | None = None` to `SessionQuerySpec`\n (`archive/query/spec.py`) and wire it through `query_spec_to_plan`.\n 2. Add a `root` case to the Lark DSL transformer\n (`archive/query/expression.py`) -- decide the value syntax (`root:true`\n /`root:false` to match other boolean-flavored fields, or a bare\n `root`/`-root` token; there's no existing precedent to copy since\n `continuation`/`sidechain` never got wired either -- worth deciding\n the pattern once for all three rather than one-off for `root`).\n 3. Register field metadata/discovery docs\n (`archive/query/metadata.py`/`discovery.py`) and regenerate CLI/MCP/\n OpenAPI docs (`devtools render all`).\n 4. DEFAULT-BEHAVIOR DECISION (the actual design call, not just plumbing):\n cijx.4's decision 4 wants the *default* list to be top-level-only,\n with children reachable only via an explicit `root:false` (or\n equivalent). That changes the result set of every unfiltered `find`/\n `list()`/MCP `query` call across CLI, Python API, MCP, and daemon HTTP\n -- audit existing callers/tests that assume today's \"everything\"\n default before flipping it, or scope the default change to the CLI\n `find` verb specifically (the interactive surface AC4's own proof\n text names: \"re-running `polylogue find repo:polylogue` and showing\n named non-fanout rows\") and leave the Python API/MCP defaults\n unfiltered for programmatic composability. Either choice needs to be\n made explicit and stated in the PR, not left implicit.\n\nACCEPTANCE CRITERIA (carried from polylogue-cijx.4 AC4, unchanged):\nDefault result unit is the top-level session, proven by re-running\n`polylogue find repo:polylogue` and showing named non-fanout rows; children\nremain reachable through an explicit filter, never silently filling the\ndefault list.\n","notes":"REACHABILITY DONE 2026-07-31 (this pass, worktree agent-aaffe89902b670d4b, PR pending). `root` is now fully reachable and SQL-pushed end-to-end:\n\n- SessionQuerySpec.root: bool | None (archive/query/spec.py), wired through build_query_spec_from_params (new optional_bool tri-state parser) and query_spec_to_plan.\n- DSL: root:true / root:false field clause in the compact-query transformer (archive/query/expression.py); -root: negation is rejected with a message pointing at root:false (the value already carries polarity, unlike origin:/tag:'s inclusion-vs-exclusion split).\n- CLI: --root/--no-root flag (cli/click_option_groups.py + cli/click_app.py's cli() signature, added last per this repo's \"new Click params go last\" convention).\n- EXPRESSION_FIELD_REGISTRY[\"root\"] + regenerated docs/cli-reference.md, docs/search.md (devtools render all).\n\nDEEPER BUGS FOUND AND FIXED while making this reachable (both were silent no-ops before this pass, not merely \"unreachable\" -- worth recording since a future root:/continuation:/sidechain: wiring pass will hit the identical shape):\n\n1. The CLI's actual browse/search path (cli/archive_query.py's _ArchiveFilterKwargs -\u003e ArchiveStore.list_summaries/search_summaries/count_sessions/count_search_sessions/search_session_ids/semantic_summaries/stats/stats_by) is a SQL-level filter path entirely separate from the SessionQueryPlan/apply_common_filters post-filter machinery this field's descriptor (requires_post_filter=True) was designed against. None of those eight ArchiveStore methods accepted a root kwarg. Fixed by pushing root into _session_filter_clause as a direct SQL predicate (sessions.parent_session_id IS [NOT] NULL -- trivially SQL-pushable, unlike continuation/sidechain which derive from branch_type) and threading it through all eight methods + _ArchiveFilterKwargs.\n\n2. Even the SessionQueryPlan post-filter path (Python API's list_summaries_archive/list_archive) was silently broken independent of reachability: ArchiveSessionSummary never carried parent_id (the SELECT never projected sessions.parent_session_id, _summary_from_row never read it), so is_root was True for every summary row regardless of actual parent -- a root:true filter would have silently returned everything even once reachable. Fixed by adding parent_id to ArchiveSessionSummary, projecting s.parent_session_id in both read_summary and list_summaries' SELECTs, and threading it through _summary_to_domain.\n\nVERIFIED LIVE (read-only, /realm/db/polylogue/index.db): `find repo:polylogue --root` -\u003e total 1906; `find repo:polylogue --no-root` -\u003e total 3206; 1906+3206=5112, the unfiltered total -- the SQL pushdown partitions the real archive exactly.\n\nNOT DONE -- the bead's own AC literally asks for the DEFAULT to change (\"Default result unit is the top-level session ... re-running polylogue find repo:polylogue and showing named non-fanout rows\"), not merely reachability. This pass deliberately did NOT flip any surface's default: find / Python API list() / MCP query / daemon HTTP all continue to return every session (root and child) unless root:/--root/.is_root() is given explicitly. Justification (per this pass's own operator instruction to propose-and-justify rather than silently flip): even the narrower option this bead's own scope note floated -- flipping only the CLI find verb's default -- still has real blast radius (every existing test/saved-query/demo-script assuming today's \"everything\" default needs re-auditing), and reachability is the load-bearing wedge that unblocks a cold-reader from getting the non-fanout view AT ALL via root:true; the default question is a separable, deliberately deferred design decision.\n\nAlso NOT wired (explicitly out of scope, unchanged from before this pass): query_unit_session_filters (the `with \u003cunits\u003e` projection's separate session-filter adapter) does not read root; daemon HTTP's _build_query_spec_params named-param allowlist has no dedicated ?root= query param (the existing ?query=root:true DSL path already covers it via compile_expression_into). continuation/sidechain/has_branches remain exactly as unreachable as before -- this pass did not touch them, though the same two deeper-bug shapes above almost certainly apply to them too if/when someone wires them next.\n\nRecommend: keep this bead open, narrowed to just the default-behavior decision (CLI find verb default, or a broader default across every surface) -- that is now the only remaining piece of the original scope, and it is a design decision + blast-radius audit, not more plumbing.\n","status":"open","priority":2,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T04:41:40Z","created_by":"Sinity","updated_at":"2026-07-31T06:57:04Z","dependencies":[{"issue_id":"polylogue-oqib","depends_on_id":"polylogue-cijx.4","type":"discovered-from","created_at":"2026-07-31T06:41:39Z","created_by":"Sinity","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-5jnq","title":"Work-evidence adapter: Beads issues.jsonl as issue nodes + dependency edges (1vpm.6 adapter)","description":"Follow-on from the polylogue-qj5x decision (design doc: .agent/scratch/live/beads-handling-design-2026-07-31.html). The Origin route ingested only interactions.jsonl (100% field_change audit rows). The genuinely informative Beads artifact is issues.jsonl: measured in the polylogue workspace, 1,260 issues, 907 with notes, 1,857 dependency edges, plus descriptions/design/acceptance-criteria — none of it currently represented anywhere in the archive.\n\nTARGET: a work-evidence adapter (sibling of BeadsIssueEffectAdapter in insights/work_effects.py) that reads a workspace's .beads/issues.jsonl and emits Beads-issue NODES for the 1vpm.6 work-evidence graph:\n- node ref keyed by the bead id itself (e.g. beads:polylogue-x4s) — the workspace prefix already provides global uniqueness; do NOT reintroduce the removed parser's sha256(workspace-root) key, which splits worktrees exactly like the cijx.1 repo-identity defect.\n- issue node carries title, status, priority, created/updated, and evidence_refs to the ledger lines; dependency edges become typed issue→issue edges (blocks/discovered-from/...), 1,857 measured in polylogue alone.\n- interactions.jsonl rows remain ObservedRepositoryEffect facts (existing adapter) and attach to these nodes as observed_effect edges with occurred_at, old→new, and close reasons (which carry commit hashes — join material for claim reconciliation).\n- CAVEAT measured 2026-07-31: interactions.jsonl actor is constant per repo (\"Sinity\" 2,249/2,249 in polylogue) — it is the git user, not real actor attribution. Session attribution must come from the session side (bd tool_use commands in action blocks), never from the ledger actor field.\n\nThis is an adapter of 1vpm.6's core graph per its 2026-07-15 invariant-collapse note (\"Complete Beads baseline/history acquisition is a required adapter of the core work-evidence graph, not an independently valuable product surface\"). It should also give 1vpm.6 the issue side of the session↔PR↔issue three-way join (session↔PR from pbuh's typed pr-link records; issue↔PR from exact-id tokens in PR bodies/close reasons).\n","status":"open","priority":2,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T04:37:35Z","created_by":"Sinity","updated_at":"2026-07-31T04:37:35Z","dependencies":[{"issue_id":"polylogue-5jnq","depends_on_id":"polylogue-qj5x","type":"blocks","created_at":"2026-07-31T06:37:35Z","created_by":"Sinity","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-0pyp","title":"Work-evidence adapter: Beads issues.jsonl as issue nodes + dependency edges (1vpm.6 adapter)","description":"Follow-on from the polylogue-qj5x decision (design doc: .agent/scratch/live/beads-handling-design-2026-07-31.html). The Origin route ingested only interactions.jsonl (100% field_change audit rows). The genuinely informative Beads artifact is issues.jsonl: measured in the polylogue workspace, 1,260 issues, 907 with notes, 1,857 dependency edges, plus descriptions/design/acceptance-criteria — none of it currently represented anywhere in the archive.\n\nTARGET: a work-evidence adapter (sibling of BeadsIssueEffectAdapter in insights/work_effects.py) that reads a workspace's .beads/issues.jsonl and emits Beads-issue NODES for the 1vpm.6 work-evidence graph:\n- node ref keyed by the bead id itself (e.g. beads:polylogue-x4s) — the workspace prefix already provides global uniqueness; do NOT reintroduce the removed parser's sha256(workspace-root) key, which splits worktrees exactly like the cijx.1 repo-identity defect.\n- issue node carries title, status, priority, created/updated, and evidence_refs to the ledger lines; dependency edges become typed issue→issue edges (blocks/discovered-from/...), 1,857 measured in polylogue alone.\n- interactions.jsonl rows remain ObservedRepositoryEffect facts (existing adapter) and attach to these nodes as observed_effect edges with occurred_at, old→new, and close reasons (which carry commit hashes — join material for claim reconciliation).\n- CAVEAT measured 2026-07-31: interactions.jsonl actor is constant per repo (\"Sinity\" 2,249/2,249 in polylogue) — it is the git user, not real actor attribution. Session attribution must come from the session side (bd tool_use commands in action blocks), never from the ledger actor field.\n\nThis is an adapter of 1vpm.6's core graph per its 2026-07-15 invariant-collapse note (\"Complete Beads baseline/history acquisition is a required adapter of the core work-evidence graph, not an independently valuable product surface\"). It should also give 1vpm.6 the issue side of the session↔PR↔issue three-way join (session↔PR from pbuh's typed pr-link records; issue↔PR from exact-id tokens in PR bodies/close reasons).\n","status":"open","priority":2,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T04:37:25Z","created_by":"Sinity","updated_at":"2026-07-31T04:37:25Z","dependencies":[{"issue_id":"polylogue-0pyp","depends_on_id":"polylogue-qj5x","type":"blocks","created_at":"2026-07-31T06:37:24Z","created_by":"Sinity","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-zc4a","title":"otlp_correlation.py queries columns that don't exist in the live otlp_spans schema, and ignores typed parent_span_id in favor of time-overlap heuristics","description":"Found during the 2026-07-31 heuristics audit (parallel to polylogue-pbuh/polylogue-1vpm.7).\n\nSCHEMA DRIFT (the more severe defect): _query_spans_for_session (polylogue/insights/otlp_correlation.py:135-150) selects columns session_id, agent_id, operation_name, start_time_unix_ns, end_time_unix_ns, duration_ms, status_code, status_message from otlp_spans. VERIFIED against both source.db and index.db live schema (sqlite3 ... .schema otlp_spans): the real DDL (storage/sqlite/archive_tiers/source.py:309-322, mirrored ops.py:140-154) has session_native_id, name, kind, started_at_ms, ended_at_ms, attributes_json, events_json -- none of the queried column names exist. Any real call to this path raises sqlite3.OperationalError: no such column, caught generically in _print_otlp_evidence (correlation_view.py:87-107) and printed as a bare 'query failed' -- the CLI surface (analyze correlation --otlp) is silently broken end to end, not merely heuristic. VERIFIED root cause of the miss: tests/unit/insights/test_otlp_correlation.py's _init_db_with_otlp_table (lines 18-42) hand-builds its own toy schema matching the CODE's imagined columns rather than the production DDL -- a self-authored replica that validates the module against itself and can never catch this drift.\n\nHEURISTIC-OVER-TYPED-FIELD (the pattern this audit is hunting): even once the schema bug is fixed, correlate_spans_to_work_events (otlp_correlation.py:193-264) joins spans to session_work_events by wall-clock time-range overlap, and _is_tool_span/_is_llm_span (otlp_correlation.py:446-481) classify spans by string-prefix matching on operation_name -- while the real otlp_spans schema carries parent_span_id (an exact typed parent/child edge) and kind (a typed span-kind enum), both unused by the correlation logic. Not evaluated: no test compares the overlap-matching heuristic's hit rate against what parent_span_id would give directly.\n\nBLAST RADIUS: VERIFIED currently 0 -- sqlite3 source.db \"SELECT COUNT(*) FROM otlp_spans\" -\u003e 0 rows live. OTLP ingestion is not yet populating this table, so today this is dead/unexercised code, not a live-data-corrupting bug. It will misbehave immediately (OperationalError on every call) the moment OTLP ingestion starts writing spans, unless fixed first.","acceptance_criteria":"1. _query_spans_for_session's column list matches the live otlp_spans DDL exactly (session_native_id/name/kind/started_at_ms/ended_at_ms/attributes_json/events_json, or the DDL is changed to match the code's intent -- pick one and align both). 2. The test fixture in test_otlp_correlation.py builds its table via the production DDL helper (e.g. importing the real CREATE TABLE from archive_tiers/source.py or ops.py) rather than a hand-authored replica schema, so schema drift is caught automatically. 3. correlate_spans_to_work_events joins on parent_span_id where present before falling back to time-overlap; _is_tool_span/_is_llm_span read the typed kind field before falling back to operation_name string-prefix matching. 4. A smoke test seeds \u003e=1 real-shaped otlp_spans row and exercises analyze correlation --otlp end to end without OperationalError.","status":"open","priority":2,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T04:32:29Z","created_by":"Sinity","updated_at":"2026-07-31T04:32:29Z","labels":["area:daemon","area:insights"],"dependencies":[{"issue_id":"polylogue-zc4a","depends_on_id":"polylogue-pbuh","type":"related","created_at":"2026-07-31T06:32:28Z","created_by":"Sinity","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} @@ -856,6 +856,7 @@ {"_type":"issue","id":"polylogue-rii.1","title":"Agent work-event write-leg -\u003e session_events -\u003e materialized read-models","description":"record_work_event/emit_decision write surface routed through the existing idempotent ingest seam (no parallel writer); flows into the run-projection read models. Today agents can only record_correction/blackboard_post/tag — there is no 'I ran this tool / spawned this subagent / decided X' write. GH issue thread (body + comments) is input, not authority; this bead's scope statement wins where they conflict.","design":"Route through the existing idempotent ingest seam (write_raw_and_parsed / the daemon ingest path) — no parallel writer (gh#2459 body is code-grounded here). Surface: MCP tools record_work_event/emit_decision (mutation role) accepting typed events (tool run, subagent spawn, decision, artifact change) with evidence/session refs; land in session_events; run-projection read models pick them up through the normal materializer. MCP registration trap: EXPECTED_TOOL_NAMES + TOOL_CONTRACT + role gating + render openapi/cli-output-schemas regen (see bd memories). Acceptance: an agent posts a work event mid-session; it is queryable via observed-events within one convergence cycle; re-posting is idempotent.","acceptance_criteria":"- MCP tools record_work_event / emit_decision are registered with the mutation role: EXPECTED_TOOL_NAMES + TOOL_CONTRACT updated, role gating enforced, and `devtools render openapi \u0026\u0026 devtools render cli-output-schemas` regenerated with `devtools render all --check` clean.\n- Typed events (tool run, subagent spawn, decision, artifact change) with evidence/session refs route through the existing idempotent ingest seam (write_raw_and_parsed / the daemon ingest path) into session_events — no parallel writer (grep confirms reuse).\n- Behavior test: an agent posts a work event mid-session and it is queryable via observed-events (session_work_events / DSL) within one convergence cycle; re-posting the same event is idempotent (no duplicate row). `devtools test \u003cmcp work-event test\u003e` green.","notes":"[Delivery upgrade 2026-07-07T00:05:00Z] Release=D-agent-context-coordination; lane=agent-coordination; readiness=A-implementation-ready; proof=two-agent separate-worktree proof with before/after coordination envelopes. Original readiness=A-implementation-ready.\n[Prework packet 2026-07-07] Static execution packet (anchors, mechanism, plan, tests, verification): .agent/handoffs/polylogue-gpt-pro-2026-07-07/prework-v2/task_packets/071_polylogue_rii_1.md (depth: bead-localized-from-export; urgency: T2-foundation-before-feature-proof). Generated from master @ 8a975a40 2026-07-06 — verify source anchors before coding; line numbers are snapshot-relative.\nRECONCILED 2026-07-13 with 37t.2 inline protocol: the agent work-event write-leg and the marker channel are ONE channel with two encodings (structured MCP writes; prose markers extracted at enrichment). Unify vocabularies — work-event kinds and marker kinds must share the registry (a ::phase marker IS a work event). Do not build parallel event taxonomies.","status":"open","priority":2,"issue_type":"feature","owner":"ezo.dev@gmail.com","created_at":"2026-07-03T04:31:43Z","created_by":"Sinity","updated_at":"2026-07-13T04:00:08Z","external_ref":"gh-2459","labels":["area:substrate","delivery:D-agent-context-coordination","horizon:frontier","lane:agent-coordination"],"dependencies":[{"issue_id":"polylogue-rii.1","depends_on_id":"polylogue-rii","type":"parent-child","created_at":"2026-07-03T06:31:43Z","created_by":"Sinity","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"polylogue-fs1.3","title":"Per-source coverage/fidelity declaration for Hermes imports","description":"Every Hermes acquisition tier and schema version needs a machine-readable fidelity declaration that distinguishes what is exact, absent, redacted, degraded, or inferred. The declaration is the guard against a parser test going green while silently dropping forensic history or cost/addressing provenance.","design":"Extend the OriginSpec/fidelity surface with: producer/schema version; installation/profile namespace; acquisition method (sqlite_backup, stable export, JSON fallback, runtime spans); exact retained-blob-to-normalized reproducibility verdict; counts and coverage for active, rewound, compacted, and observed messages; addressing/material-origin semantics; actual/estimated cost with status/source/pricing/billing provenance; lifecycle/relationship coverage; runtime-span coverage and explicit missingness. The snapshot and span lanes may enrich one logical session revision only with per-field provenance; they may not double-count or silently prefer a lower-fidelity tier.","acceptance_criteria":"explain-import on Hermes v16, a later schema, JSON fallback, and a spans-plus-snapshot merge names every capability as exact, absent, redacted, degraded, or inferred; exact-blob reproducibility is stated and verified; the same logical session from two tiers remains one revision with field-level provenance; message-state/addressing and cost-provenance counts reconcile to fixtures; deliberately dropping observed mapping, cost provenance, snapshot proof, or an unpaired span changes the declared fidelity and surfaces a downstream forensics caveat. OriginSpec fixtures and mutation-style negative tests pass.","notes":"[Delivery upgrade 2026-07-07T00:05:00Z] Release=K-interop-origin-export; lane=origin-interop-export; readiness=D-horizon-ready; proof=OriginSpec detector/parser/fixture/fidelity suite and content-hash export/import roundtrip. Original readiness=E-spec-needed.\n2026-07-12 fanout lane finding: blocked as scoped — explain-import cannot inspect SQLite Hermes state DBs and its payload lacks a fidelity-declaration field; both surfaces (import_explain.py + payload schema) must be in scope to implement. Evidence: 37bdfa04c; import_explain.py decodes JSON/JSONL only.","status":"closed","priority":2,"issue_type":"feature","owner":"ezo.dev@gmail.com","created_at":"2026-07-03T04:31:40Z","created_by":"Sinity","updated_at":"2026-07-12T23:15:18Z","closed_at":"2026-07-12T23:15:18Z","close_reason":"PR #2789 merged: Hermes per-source coverage/fidelity declaration shipped (import_explain.py, hermes_state.py, generated CLI-output schema regenerated)","labels":["area:ingest","area:substrate","delivery:K-interop-origin-export","delivery:ac-patched","horizon:frontier","lane:origin-interop-export"],"dependencies":[{"issue_id":"polylogue-fs1.3","depends_on_id":"polylogue-fs1","type":"parent-child","created_at":"2026-07-03T06:31:40Z","created_by":"Sinity","metadata":"{}"}],"dependency_count":0,"dependent_count":3,"comment_count":0} {"_type":"issue","id":"polylogue-tf2.2","title":"Fold agent_forensics.py into polylogue analyze","description":"~70% already materialized (cost_rollups, archive_coverage, total_credit_cost, portfolio, cost_outlook). Real gaps: reasoning-token lane on SessionProfile; usage_timeline archive insight (tokens/cost per month per model) registered in insights/registry.py; optional markdown forensics renderer. Drop the script's hand-rolled _CREDIT_RATES; delete the script. Sequenced AFTER the campaign regen (the campaign uses the script one last time). GH issue thread (body + comments) is input, not authority; this bead's scope statement wins where they conflict.","status":"closed","priority":2,"issue_type":"feature","assignee":"Sinity","owner":"ezo.dev@gmail.com","created_at":"2026-07-03T04:31:34Z","created_by":"Sinity","updated_at":"2026-07-03T11:54:39Z","started_at":"2026-07-03T11:31:18Z","closed_at":"2026-07-03T11:54:39Z","close_reason":"Completed: usage forensics is no longer a standalone script surface. Added registered usage_timeline archive insight with CLI/API/MCP registry coverage, reused the shared subscription-pricing catalog for credit estimates, deleted scripts/agent_forensics.py and its private-helper tests, and rewrote README/docs around polylogue analyze insights coverage/cost-rollups/usage-timeline plus devtools workspace claim-vs-evidence. Verification: focused claim-vs-evidence/insights tests passed, render all --check passed, devtools verify --quick passed, and live active-archive usage-timeline smoke returned valid JSON. Follow-up polylogue-5nn tracks the observed 18s whole-archive aggregation latency for unfiltered month-origin-model usage-timeline.","external_ref":"gh-2480","labels":["area:usage","campaign"],"dependencies":[{"issue_id":"polylogue-tf2.2","depends_on_id":"polylogue-tf2","type":"parent-child","created_at":"2026-07-03T06:31:34Z","created_by":"Sinity","metadata":"{}"},{"issue_id":"polylogue-tf2.2","depends_on_id":"polylogue-tf2.1","type":"blocks","created_at":"2026-07-03T06:31:34Z","created_by":"Sinity","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"polylogue-trjb","title":"bead-landing-check sweep abandoned: 6% precision even after live-consumer fix; note-staleness may be the better angle","notes":"CLOSED PR #3424 unmerged (2026-07-31) after measuring the tool against a\n190-bead human-verified ground truth (5 independent review groups, complete\nSTALE/PARTIAL/LIVE verdict set recorded as bd notes on the reviewed beads\nthemselves -- durable, queryable via `bd sql \"SELECT id, notes FROM issues\nWHERE notes LIKE '%VERDICT%'\"`).\n\nWHAT WAS BUILT: `devtools workspace bead-landing-check` (code still exists on\nbranch feature/devtools/bead-landing-check, not merged) -- extracts cited\ncommit hashes/PR numbers from bead text, cherry-picks commits onto master in\na reused throwaway worktree to detect empty-diff landings (survives\nsquash-merge id rewriting, unlike git log --is-ancestor or issue-id grep),\nchecks PR merge state via gh, and after a first sweep's ~5% precision was\nfound (95% false-positive on 114 human-checked beads), added three\ndowngrade-only fixes: (1) require a live production consumer for a landed\ncommit via git grep outside tests, (2) suppress verdicts for beads with open\nparent-child dependents, (3) suppress verdicts when the bead's own text\ncontains an explicit not-done phrase (deferred/xfail/not wired/etc).\n\nRESULT AFTER THE FIXES: precision 6.1% overall (66 flagged beads), 20.0% at\nstrong confidence (10 beads), 3.6% at weak (56 beads). Recall 4/7 confirmed\nSTALE beads still flagged (57.1%; 44.4% against the reported 9 -- 2 STALE\nbeads' notes used phrasing my regex could not match). The three fixes\nprovably removed genuine STALE beads along with false positives:\npolylogue-4fm3 (consumer check inconclusive on a non-Python change),\npolylogue-6pii (consumer check found no grep-visible caller despite a\nconfirmed-safe closable chore), polylogue-7mtf (the suppression check's\n\"xfail\" keyword, added to catch polylogue-hg97's genuine incompleteness\nadmission, fired on 7mtf's OWN unrelated use of the word describing a\nregression-guard the fix itself added -- same word, opposite meaning).\n\nWHY IT DOESN'T WORK: \"is this work done\" is a question about whether\nacceptance criteria are semantically satisfied; a git/text query can only\ncheck whether artifacts exist or specific phrases are present/absent.\npolylogue-aggz is the clearest illustration: two directly-matching MERGED\nPRs, and the PR bodies themselves state 2 of 3 declared invariants are\nuntouched -- no commit-graph query reaches that.\n\nTHE MORE PROMISING ANGLE, per the coordinator's read (which the data\nsupports): the suppression-phrase check reads the bead's OWN MOST RECENT\nNOTE, not the commit graph -- that's the signal the human reviewers actually\nused. A future tool aimed at NOTE STALENESS (has this bead's own\nmost-recent-note-implied status been contradicted by newer master state?)\nrather than commit archaeology might do better, but the 7mtf false negative\nshows a bare lexical keyword match isn't safe as-is -- it would need to\ndistinguish \"this note admits incompleteness\" from \"this note happens to\nmention a word like xfail/deferred/stale in an unrelated, completed\ncontext.\" Likely needs something closer to reading the note's actual claim\nsentence-by-sentence (an LLM-judge pass per candidate bead, not a sweep-scale\nregex) rather than a cheap grep-shaped heuristic.\n\nDo not resurrect the sweep-shaped tool as-is. If revisited, scope it as a\nper-bead check invoked when a human already suspects ONE bead is stale\n(narrower claim, human still reads the evidence), never a sweep that\nproduces a headline count -- per the coordinator's original framing of the\none outcome that would have kept a role for it, which this data did not\nreach.\n","status":"open","priority":3,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T06:39:57Z","created_by":"Sinity","updated_at":"2026-07-31T06:41:12Z","external_ref":"gh-3424","labels":["area:beads","area:devtools"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-6tue","title":"derive Claude Design chat titles instead of the literal 'Chat' placeholder","description":"Every Claude Design chat title observed in the 2026-07-30 sample is literally 'Chat' -- the same class of gap as the claude-code raw-UUID title problem (bd polylogue-6e7m territory). ai_parser.py's parse_design() currently sets title_source=TitleSource.ORIGIN whenever payload['title'] is present and non-empty, which is technically honest (the provider did assert this string) but useless for browsing/search. Follow-up: derive a HEURISTIC title from the first user message text or the project name (payload['project']['name']) when title == 'Chat', the same way other providers fall back past a generic provider title.","status":"open","priority":3,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T04:50:59Z","created_by":"Sinity","updated_at":"2026-07-31T04:50:59Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-iv3v","title":"Verify grok.py export field coverage against a real xAI GDPR export (unverified, no sample corpus available)","description":"Surfaced during the 2026-07-31 heuristics/discard-site audit as a low-confidence, UNVERIFIED lead -- filed as a follow-up investigation, not a confirmed finding, per the audit's evidence discipline.\n\npolylogue/sources/parsers/grok.py (178 lines) extracts only conversation.title, create_time, and per-response sender/message/create_time (grok.py:122-171). It documents itself as reverse-engineered from three third-party sources (a GitHub viewer, a blog post, a userscript) because 'no official xAI schema publication exists' (grok.py:1-38), and asserts the export has 'no native conversation id or attachment/image data.'\n\nThis audit could NOT verify that claim either way: no real Grok GDPR export exists under /realm/data/exports/chatlog, /realm/data/exports, or elsewhere searched (checked at audit time, 2026-07-31). The only grok-adjacent artifact found is a browser-capture DOM dump (/realm/inbox/polylogue-browser-spool-2026-07-10/grok/dom-e4e24461-4b1f7d02f3c4.json), which is a different capture path (live DOM scrape, not the GDPR export grok.py parses) and cannot substitute.\n\nEvery other provider audited this session (Claude Code via polylogue-pbuh/cgfy, ChatGPT, Codex, Hermes) turned out to have MORE typed fields in the real wire format than the parser initially read -- structuredPatch, patch_apply changes, reasoning traces, thread titles. Given that pattern, grok.py's self-reported 'no attachments, no conversation id' claim deserves the same corpus-diff treatment cgfy applied to Claude Code, but doing so requires acquiring one real xAI GDPR export first.","acceptance_criteria":"1. Acquire (or obtain from the operator) one real xAI/Grok GDPR export. 2. Run cgfy's key-enumeration method: list every top-level/response/message key present in the real export, diff against what grok.py currently reads. 3. Classify each unread key as read / deliberately-dropped-with-reason / to-acquire, same as cgfy's disposition table. 4. If grok.py's self-reported field coverage turns out accurate, close as verified-clean; if gaps are found, file follow-up beads per gap with corpus counts.","status":"open","priority":3,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T04:33:13Z","created_by":"Sinity","updated_at":"2026-07-31T04:33:13Z","labels":["area:ingest","lane:origin-interop-export"],"dependencies":[{"issue_id":"polylogue-iv3v","depends_on_id":"polylogue-cgfy","type":"related","created_at":"2026-07-31T06:33:13Z","created_by":"Sinity","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-mgf6","title":"Query DSL: float-literal numeric predicates for JSON-extracted fields (run_settings temperature/topP)","description":"Follow-up from polylogue-o4j2 (AC2, deferred).\n\naistudio-drive's runSettings (temperature/topP/topK/maxOutputTokens/\nthinkingLevel/safetySettings/enable* flags) is parsed and stored verbatim as\nsessions.run_settings_json (polylogue-2qx.4/cgfy, index v46). It is not\nexposed to the query DSL, so \"sessions where temperature \u003e 0.5\" is not\nexpressible. Two independent gaps block it:\n\n1. Grammar: the boolean-query numeric-comparison rule only accepts integer\n literals (`COUNT_FIELD COMP_OP INT` in archive/query/expression.py) --\n temperature/topP are floats (0.0-2.0 / 0.0-1.0 range).\n2. SQL builder: NUMERIC_QUERY_FIELD_REGISTRY's NumericQueryFieldInfo.unit_columns\n values are treated as plain column names (`f\"{table_alias}.{column}\"` in\n storage/sqlite/archive_tiers/archive.py, two call sites) -- there is no\n path for a computed/JSON-extract expression like\n `json_extract(run_settings_json, '$.temperature')`.\n\nScope: extend the grammar to accept decimal literals for numeric predicates\n(without breaking existing integer-only fields), and extend the SQL-builder\ncall sites (and NumericQueryFieldInfo, if needed) to support an expression\ncolumn alongside plain columns. Consider starting with the integer-typed\nrun_settings fields (topK, maxOutputTokens) which fit the existing INT-only\ngrammar and only need the SQL-builder JSON-extract half, then float support\n(temperature, topP) as a second phase needing the grammar change too.\n\nNot urgent: run_settings is durably stored and readable via `read --view`\nalready; this is about ergonomic filtering, not data loss.","status":"open","priority":3,"issue_type":"task","owner":"ezo.dev@gmail.com","created_at":"2026-07-31T04:03:16Z","created_by":"Sinity","updated_at":"2026-07-31T04:03:16Z","dependency_count":0,"dependent_count":0,"comment_count":0} @@ -951,11 +952,11 @@ {"_type":"issue","id":"polylogue-uh6c","title":"Tags redesign: namespaces with separate membership, affinity, and confidence axes","description":"Informal tags remain namespaced, plural, and non-hierarchical by default, but one scalar cannot\nrepresent asserted membership, semantic affinity, and classification confidence. These are three\nindependent constructs with axis-specific queries and provenance. Affinity never grants membership;\nconfidence measures an assertion/judgment, not similarity.","design":"tagged(item, tag) is asserted boolean/qualified membership with author/evidence/status.\ntag_affinity(item, prototype) is embedding/model-derived similarity tied to a versioned prototype,\nmodel, and evaluation world. tag_confidence(assertion) is calibrated uncertainty of a classifier or\njudgment tied to its actor/execution context and definition. Comparisons or conversions across axes\nfail closed unless an operation declares a versioned conversion. Informal membership may remain\ninformal forever. A prototype is a resource/definition used to calculate affinity, not tag identity.\nDSL predicates and renderers name the axis (`tagged:`, `tag-affinity:`, `tag-confidence:`); the\nambiguous scalar `tag:x\u003e0.7` is rejected.","acceptance_criteria":"Seed and query: high affinity without membership; asserted membership with unknown affinity; and a\nlow-confidence classifier assertion without changing membership or affinity. Axis-mixing operations\nfail with a named conversion requirement. Prototype/model changes alter affinity receipts, not tag\nidentity. Cross-surface outputs preserve axis and provenance. Verify through production DSL lowering,\nstorage/read paths, and renderer tests—not a test-only replica.\nAgent-authored asserted membership is always a candidate with inject:false and flows through the canonical 37t.12 judgment transaction; operator-authored membership may use the declared direct-authority path. The existing-row short-circuit compares axis, actor authority, and judgment state rather than treating any same-name row as active. A regression proves agent add_tag/bulk_tag cannot become query-visible membership before judgment, while affinity and classifier confidence never grant membership.","notes":"Invariant consolidation 2026-07-15: absorbs polylogue-ldau. Its bypass is the concrete asserted-membership authority regression for the three-axis model, and judgment is supplied by 37t.12 rather than a tag-specific queue.\nPriority calibration 2026-07-15: P2 to P3. This is a valuable query-language extension, interaction refinement, presentation improvement, or convenience surface, but the truthful bounded core and model-facing discovery contract precede it. The capability remains in scope and at the same horizon.","status":"open","priority":3,"issue_type":"spike","owner":"ezo.dev@gmail.com","created_at":"2026-07-13T02:07:40Z","created_by":"Sinity","updated_at":"2026-07-15T20:07:33Z","labels":["area:annotations","horizon:frontier"],"dependencies":[{"issue_id":"polylogue-uh6c","depends_on_id":"polylogue-1vpm","type":"parent-child","created_at":"2026-07-15T19:13:00Z","created_by":"Sinity","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-dve1","title":"Seed annotation ontologies + autonomous per-archive ontology bootstrap","description":"Ship versioned seed annotation schemas over the existing annotation/batch/judgment substrate and bootstrap archive-specific ontologies as governed candidates. Seed families are activity, prospective goal events, observed outcome evidence, knowledge artifacts, and reusability. `unresolved_inactive(H)` is derived by the goal graph; abandonment is never inferred as a timeless annotation. Informal tags/affinity may nominate candidates but cannot become formal ontology facts without a judged annotation batch.","design":"SEED SCHEMAS.\n- activity at declared session/segment grain: debugging, design, implementation, research, writing, ideation, ops, procurement;\n- goal events: opened, blocked, resumed, declared-resolved, superseded, and explicitly-abandoned only when an actor actually declares it;\n- outcome evidence: test passed, commit observed, deployment observed, user accepted, answer declared, unknown, with structural/rule/judged authority preserved;\n- knowledge artifact: decision, lesson, preference, fact candidate/established under named authority, commitment;\n- reusability: snippet-, recipe-, and demo-worthy as purpose-specific judgments.\nAffect/stance remains opt-in. Goal state belongs to 7yk5. `unresolved_inactive(H)` is a horizon/evaluation-world derivation with censoring, never a label this schema guesses. Historical solved/partial/abandoned labels are backfill candidates or audit judgments, not primary prospective truth.\n\nBOOTSTRAP. Sample and cluster multiple declared views: content embeddings, action-pattern signatures, temporal/cost shapes, and outcome evidence. Cross-view agreement is validation evidence, not automatic truth. An agent proposes labels grounded in exemplars; active elicitation routes boundary cases; the operator/shared judgment lifecycle accepts, renames, splits, or rejects; a versioned schema and annotation batch record the result. Query utility, residue, epoch drift, and precision may propose revisions. Autonomous work stops at candidates.\n\nTAG/ONTOLOGY BOUNDARY. Candidate generation records source tag membership, affinity, classifier definition, confidence, frame, and evidence refs separately. Promotion creates new formal annotation assertions under a schema/batch; it never mutates an informal tag into a formal fact. Version crosswalks, an unclassified residue bucket, rare-category exploration, privacy/excision, and rejection history remain visible.","acceptance_criteria":"1. Prospective open/close/block events and structural outcome evidence remain distinct; absence of closure does not create an abandonment annotation.\n2. The same open goal can become unresolved_inactive(H) only through 7yk5 with a named horizon/frame/evaluation receipt and right-censoring.\n3. A high-affinity informal tag produces at most a candidate; formal ontology queries remain empty until a judged schema/batch assertion exists.\n4. A rejected candidate leaves informal tags intact and preserves the rejection/evidence trail.\n5. Multi-view bootstrap fixtures preserve cross-view disagreement, residue, rare samples, epoch scope, and privacy/excision behavior.\n6. Historical outcome backfill renders its proxy/judged authority and cannot overwrite prospective goal events.","notes":"[LEGACY FIELDS PRESERVED BY FINAL CORRECTIVE PASS 2026-07-13]\n\nORIGINAL DESCRIPTION:\nThread 2 of archive-intelligence-design-2026-07-13.md. SHIP packaged annotation schemas on the EXISTING polylogue/annotations machinery: activity, outcome (solved/partial/abandoned/question-opened/closed), knowledge-artifact (decision/lesson/preference/fact/commitment-made), reusability (snippet/recipe/demo-worthy). Affect/stance NOT default (privacy: opt-in only). BUILD the bootstrap loop for archive-specific topics: embed+cluster sample -\u003e agent proposes labels grounded in exemplars -\u003e operator judges via elicitation session (rxdo.9.14) -\u003e schema v1 registered -\u003e agent batch backfill as candidates -\u003e standing query watches cluster drift, proposes v2. Productizes the data-cartography workflow. DEP: rxdo.4 labels, rxdo.9.14 elicitation.\n\n## Authoritative corrective scope (2026-07-13)\n\nFormal ontology membership is not the high end of an informal scalar tag ladder. Informal tags and\naffinity may nominate ontology candidates, but only schema/version/batch-governed judgment can\ncreate ontology facts.\n\nORIGINAL DESIGN:\n## Thread 2 — Ontologies: ship seeds, derive the rest\n\nThe machinery already exists (annotation schemas + batches + judge\nlifecycle + rxdo.4 labels). What to SHIP is seed schema definitions, and\nwhat to BUILD is the bootstrap loop.\n\nSeed ontologies (packaged annotation schemas, versioned, judge-gated):\n- **activity**: debugging | design | implementation | research | writing |\n ideation | ops | procurement — session/segment grain.\n- **outcome**: solved | partial | abandoned | superseded | question-opened |\n question-closed — the基basis for D1/D3 and success analytics.\n- **knowledge-artifact**: decision | lesson | preference | fact-established |\n commitment-made — feeds existing AssertionKinds; \"commitment-made\" is the\n sleeper (promises you made and forgot).\n- **reusability**: snippet-worthy | recipe-worthy | demo-worthy — the\n curation feeder.\n- Affect/stance deliberately NOT in the default seed set (privacy posture:\n private-by-default archives may opt in; never ship as silently-on).\n\nDomain topics are NOT built-in — they are derived per-archive by the\n**ontology bootstrap loop** (the productization of tonight's\ndata-cartography prompt): (1) embed + cluster a sample; (2) an agent\nproposes taxonomy labels grounded in exemplar sessions per cluster; (3) the\noperator judges/renames via an elicitation session (N — this is a judgment\nworkflow, blinding optional); (4) schema v1 registered in\nannotation_schemas; (5) agent batch backfills as candidate annotations;\n(6) a standing query watches cluster drift and proposes v2 when the space\nmoves. Custom ontology as a DERIVED, versioned, judged object — the\nscaffolding universal, the taxonomy personal.\n\n## Authoritative corrective contract (2026-07-13)\n\nCandidate generation records source tag membership, affinity, classifier definition, confidence,\nand evidence refs separately. Promotion creates a new annotation-batch assertion under a versioned\nontology schema after judgment; it never mutates an informal tag into a formal fact. Autonomous work\nstops at candidate generation until governed experiment receipts justify more authority.\n\nORIGINAL ACCEPTANCE_CRITERIA:\n## Corrective acceptance criteria (2026-07-13)\n\nA high-affinity informal tag produces at most a candidate. Without an explicit judged batch it does\nnot appear in formal ontology queries. Promotion preserves the source axes and schema/batch refs;\nrejection leaves informal tagging intact.\n\nORIGINAL NOTES:\nBOOTSTRAP LOOP EXPANSIONS (operator: 'expand this idea'): (1) MULTI-VIEW — cluster on content embeddings AND behavioral signatures (action patterns via avna row-patterns, temporal rhythms, cost shapes) — different views yield different ontologies (topic vs workflow vs collaboration-style); (2) CROSS-VIEW AGREEMENT as taxonomy validation — a category isolated independently by text AND behavior views is real, reducing LLM-label hallucination; (3) UTILITY-DRIVEN REFINEMENT — rxdo.3 query-run telemetry shows which labels are actually QUERIED; unused labels decay, heavily-queried low-precision labels get split proposals — ontology optimizes for query utility (closed loop unique to polylogue: we log our own query usage); (4) ACTIVE EXEMPLAR SOLICITATION — route max-entropy boundary sessions to judgment first (active learning, resorter economics); (5) definitions-as-hypotheses — each label carries prototype + criterion, backfill measures precision, bad labels pruned; (6) EPOCH-SCOPED labels — categories can have validity windows ('your 2025 obsession'); (7) MATURATION PATH — stable categories upgrade from labels to structured extraction schemas (annotation machinery already supports fields: procurement -\u003e {vendor, decision, price}); (8) subdivision on demand — split a cluster only when query traffic + internal variance justify it, namespaces emerge.\nPriority calibration 2026-07-15: P2 to P3. This remains part of the full project ambition, but it is a sequenced demo, experiment, governed analytic extension, or evaluation layer rather than a present failure of archive truth, bounded queryability, durability, or source fidelity. Priority is urgency, not deletion or scope reduction; horizon is unchanged.\n\n[2026-07-18] External-agent packet ann-01-ontology-seed-r01 (GPT Pro wave 2, snapshot 536a53efac0, unreviewed) implemented and landed via PR #3059 (feature/annotations/seed-ontology-dve1) after independent adversarial verification (git apply --check against current master, live-schema claim verification, real devtools test/verify runs -- not the packet's own claimed results).\n\nLanded: five immutable v1 seed schemas (seed.activity, seed.goal-event, seed.outcome-evidence, seed.knowledge-artifact, seed.reusability) registered in BUILTIN_ANNOTATION_SCHEMAS and data-replayed into user.db with no USER_SCHEMA_VERSION bump (confirmed live: annotation_schemas is already the immutable versioned registry, assertions.kind is unconstrained TEXT). OntologyCandidateNomination/OntologyCandidateGovernance state machine (nominate -\u003e accept/rename/split/reject) running the full read/judge/register/receipt sequence in one BEGIN IMMEDIATE, reusing judge_assertion_candidate(). Two new AssertionKind values (ONTOLOGY_CANDIDATE, ONTOLOGY_GOVERNANCE) wired through enums/user_write/user_audit/OpenAPI/CLI-schema. Archive-local schema resolution added to the existing AnnotationBatchImportRequest route.\n\nFixed two real defects the packet's dependency-starved sandbox couldn't catch: docs/plans/layering.yaml was missing the new persist_builtin_annotation_schemas entrypoint declaration (verify layering failed clean, confirmed via stash-testing the master baseline), and mypy --strict found 17 real errors (redundant JSONValue casts, a list[str]-into-dict[str,JSONValue] mismatch, 8 test call sites indexing JSONValue|None with a type:ignore comment whose error code didn't even match this mypy version) -- fixed via require_json_document narrowing.\n\nAC review: 1 (goal/outcome distinctness), 3 (tag-\u003ecandidate-only), 4 (rejection preserves source), 6 (historical backfill authority) are satisfied by the landed seed schemas and governance state machine. AC 5 (multi-view bootstrap fixtures) is satisfied structurally -- OntologyCandidateNomination captures content/action-pattern/temporal-cost/outcome view proposals, cross-view agreement state, residue, rare-category refs -- but this PR does NOT implement the actual autonomous bootstrap loop (embed+cluster a sample -\u003e agent proposes labels grounded in exemplars -\u003e active elicitation of boundary cases -\u003e standing query watches drift). That remains open, substantial, separate scope: an agent/pipeline that actually samples the archive, clusters it across the declared views, and calls nominate_ontology_candidate() with real proposals. This bead should stay open for that work; the substrate/governance/seed-vocabulary layer it depends on is now landed.\n\nVerification: devtools test tests/unit/annotations/ tests/unit/storage/test_archive_tiers_assertions.py -\u003e 155 passed; devtools test (schema/write/user_audit/render_openapi/cli_output_schemas/archive_tiers_ddl) -\u003e 154 passed (including the two sqlite-vec round-trip tests the packet's bare container could not run); devtools verify --quick -\u003e exit 0.\n\n[2026-07-18 addendum] ann-03-batch-runbook-r01's ranked annotation-campaign launch order (full decision recorded on polylogue-rxdo) ranks the five seed schemas landed here by campaign priority: failure.acknowledgment first (D1, uses seed.outcome-evidence-adjacent structural failure framing), task-completion-vs-claimed second (D2), pathology-detector validation fourth (D4), session-quality/derailment deferred (D5, needs comparative-dimension design first), and title/topic quality blocked on polylogue-ih67 (D6). Terminal-state/outcome-evidence auditing (which would use seed.outcome-evidence) is explicitly blocked (D3) on polylogue-vhjs/polylogue-wofr repair before any mass-annotation of terminal states.\nVERIFICATION (group3 sweep): PARTIAL, per own notes. Substrate/governance/seed-vocabulary layer (5 seed schemas, judgment substrate) confirmed landed: devtools test tests/unit/annotations/ + test_archive_tiers_assertions.py -\u003e 155 passed (own note). Remaining, explicitly still open per own note: the actual sampling/clustering agent/pipeline that calls nominate_ontology_candidate() with real proposals -- 'that remains open, substantial, separate scope'. Bead correctly stays open for that. Not stale.","status":"open","priority":3,"issue_type":"feature","owner":"ezo.dev@gmail.com","created_at":"2026-07-13T02:07:34Z","created_by":"Sinity","updated_at":"2026-07-31T05:53:33Z","labels":["area:annotations","horizon:frontier"],"dependencies":[{"issue_id":"polylogue-dve1","depends_on_id":"polylogue-37t","type":"parent-child","created_at":"2026-07-15T18:54:43Z","created_by":"Sinity","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-rxdo.9.15","title":"Judge cascades: agent screens route to sparse operator gold","description":"Rigor mechanism O. Routing policy: agent judges screen all judgment demand cheaply/parallel; disagreement, uncertainty, or declared stakes route to the operator; operator verdicts double as calibration gold (L). Existing accept/reject lifecycle unchanged as the durable-claim acceptance gate — a calibrated agent layer feeds it and most volume never needs a human. Recursive-safety spine intact: agent judgments stay candidates; promotion still gated. DEP: L + N.","design":"## Authoritative corrective contract (2026-07-13)\n\nCascades route agent screens to sparse operator gold using calibrated actor+execution-context strata.\nTie, incomparable, abstain, and insufficient-evidence outcomes are first-class routing signals.\nSparse-gold claims name coverage and cannot calibrate unseen contexts by model name alone.","acceptance_criteria":"## Corrective acceptance criteria (2026-07-13)\n\nLow calibration, context drift, disagreement, abstention, incomparability, and quota-selected cases\nroute to operator review. Well-calibrated covered cases may stop at the agent screen with a receipt.\nAn unseen execution context never inherits a confident pass.","notes":"Implemented: cascades.py -- route_judgment() routing policy. Non-decisive verdicts (tie/incomparable/abstain/insufficient_evidence), disagreement, quota-selected items, and unseen/low-calibration execution contexts all route to the operator; a well-calibrated, covered, decisive verdict stops at the agent screen with a receipt. Consumes calibration.py's agreement_rate=None-for-unseen-context semantics so an unseen execution context never inherits a confident pass from a sibling context (matches the AC directly). Existing accept/reject lifecycle (37t.12) unchanged as the durable-claim promotion gate -- agent verdicts still land CANDIDATE via the upsert_assertion chokepoint, recursive-safety spine intact. Verification: devtools test tests/unit/insights/judgment/test_cascades.py -\u003e passed. PR: https://github.com/Sinity/polylogue/pull/2889 (open, not merged).","status":"closed","priority":3,"issue_type":"feature","owner":"ezo.dev@gmail.com","created_at":"2026-07-13T01:05:54Z","created_by":"Sinity","updated_at":"2026-07-15T00:01:19Z","closed_at":"2026-07-15T00:01:19Z","close_reason":"Satisfied by PR #2889 (cascades.py): route_judgment sends non-decisive/disagreement/quota-selected/unseen-low-calibration cases to operator; well-calibrated covered decisive verdicts stop at agent screen. Independently reviewed round-4 (approved).","labels":["area:substrate","delivery:C-read-evidence-contract","horizon:mid","lane:read-contracts","tech-tree"],"dependencies":[{"issue_id":"polylogue-rxdo.9.15","depends_on_id":"polylogue-rxdo.9","type":"parent-child","created_at":"2026-07-13T03:05:54Z","created_by":"Sinity","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"polylogue-rxdo.9.12","title":"Judges as actors (human or agent) with measured calibration","description":"Rigor mechanism L. judge_ref identifies WHO judged: operator, or agent as (model + prompt hash) — a judge is a program. Nothing in the lifecycle assumes human (operator: 'judgments solely human is pointlessly limiting'). Per-judge, per-dimension calibration = agreement with consensus/gold on overlap items (Dawid-Skene framing: agent judges are noisy raters weighted by MEASURED agreement). DEP: K.","design":"## Authoritative corrective contract (2026-07-13)\n\nJudge identity is ActorRef plus separate ExecutionContextRef, never a model-name scalar. Calibration\nis stratified by stable actor/model family and exact prompt/tools/runtime/config context, with gold\ncoverage, abstention, tie/incomparable behavior, and drift windows visible. No universal JudgeSpec\ntable; a routing policy becomes a definition only after independent reuse/lifecycle appears.","acceptance_criteria":"## Corrective acceptance criteria (2026-07-13)\n\nThe same actor under two execution contexts has separable calibration. Missing gold or context\nproduces unknown calibration, not inherited confidence. Reports preserve abstain/tie/incomparable\nrates and refuse unsupported cross-context pooling.","notes":"Implemented: calibration.py -- per-(actor_ref, execution_context_id, dimension) agreement-with-gold report. Zero gold overlap yields agreement_rate=None (unknown), never inherited from a sibling execution context (matches rxdo.9.15's AC 'an unseen execution context never inherits a confident pass'). JudgeIdentity (types.py) is documented as an interim stand-in for the not-yet-built h6r ActorRef/ExecutionContextRef pair -- same two-field shape so it re-points cleanly when h6r lands. Verification: devtools test tests/unit/insights/judgment/test_calibration.py -\u003e passed. PR: https://github.com/Sinity/polylogue/pull/2889 (open, not merged).\nFix round 2026-07-14 (post-review, commit bb3d3b8a7): fixed a third calibration corruption case -- _winner_identity mismatched types (tuple vs bare str/ComparativeVerdict) when a gold judgment is a 2-item n-wise ORDERING and a candidate judgment on the same pair is a pairwise PREFER_LEFT/RIGHT, both legal per ComparativeJudgment's own validation. Confirmed by repro: same real-world winner scored agreement_rate=0.0 because the two verdict shapes never compared equal. Fix: _winner_identity now resolves EVERY directed verdict (ordering or pairwise) to the same representation -- frozenset of (winner_ref, loser_ref) edges via decompose_to_pairwise -- so cross-representation overlaps compare correctly in both directions (agree when same winner, disagree when different). 2 new regression tests added (agreement + disagreement cases) alongside the 7 existing. Verification: devtools test tests/unit/insights/judgment/test_calibration.py -\u003e 9 passed. PR #2889 (open).\nPriority calibration 2026-07-15: P2 to P3. This remains part of the full project ambition, but it is a sequenced demo, experiment, governed analytic extension, or evaluation layer rather than a present failure of archive truth, bounded queryability, durability, or source fidelity. Priority is urgency, not deletion or scope reduction; horizon is unchanged.\nVERDICT: PARTIAL — calibration.py (PR #2889, merged) correctly implements the corrective AC as literally written (separable per-context calibration, unknown-not-inherited on missing gold, no cross-context pooling) and is tested. However it is called only by cascades.py within the same judgment package, and the entire polylogue/insights/judgment package has zero callers from CLI/MCP/daemon — same unwired-primitive pattern as sibling beads 9.6/9.7, just not flagged in this bead's own notes. Evidence: git grep -ln 'insights.judgment.calibration' origin/master -- '*.py' | grep -v tests/ -\u003e judgment/__init__.py + cascades.py only; git grep -ln 'insights.judgment' origin/master -- polylogue/cli/*.py polylogue/mcp/*.py polylogue/daemon/*.py -\u003e empty.","status":"open","priority":3,"issue_type":"feature","owner":"ezo.dev@gmail.com","created_at":"2026-07-13T01:05:40Z","created_by":"Sinity","updated_at":"2026-07-31T05:46:46Z","labels":["area:substrate","delivery:C-read-evidence-contract","horizon:mid","lane:read-contracts","tech-tree"],"dependencies":[{"issue_id":"polylogue-rxdo.9.12","depends_on_id":"polylogue-h6r","type":"blocks","created_at":"2026-07-13T05:58:42Z","created_by":"Sinity","metadata":"{}"},{"issue_id":"polylogue-rxdo.9.12","depends_on_id":"polylogue-rxdo.9","type":"parent-child","created_at":"2026-07-13T03:05:39Z","created_by":"Sinity","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"polylogue-rxdo.9.12","title":"Judges as actors (human or agent) with measured calibration","description":"Rigor mechanism L. judge_ref identifies WHO judged: operator, or agent as (model + prompt hash) — a judge is a program. Nothing in the lifecycle assumes human (operator: 'judgments solely human is pointlessly limiting'). Per-judge, per-dimension calibration = agreement with consensus/gold on overlap items (Dawid-Skene framing: agent judges are noisy raters weighted by MEASURED agreement). DEP: K.","design":"## Authoritative corrective contract (2026-07-13)\n\nJudge identity is ActorRef plus separate ExecutionContextRef, never a model-name scalar. Calibration\nis stratified by stable actor/model family and exact prompt/tools/runtime/config context, with gold\ncoverage, abstention, tie/incomparable behavior, and drift windows visible. No universal JudgeSpec\ntable; a routing policy becomes a definition only after independent reuse/lifecycle appears.","acceptance_criteria":"## Corrective acceptance criteria (2026-07-13)\n\nThe same actor under two execution contexts has separable calibration. Missing gold or context\nproduces unknown calibration, not inherited confidence. Reports preserve abstain/tie/incomparable\nrates and refuse unsupported cross-context pooling.","notes":"Implemented: calibration.py -- per-(actor_ref, execution_context_id, dimension) agreement-with-gold report. Zero gold overlap yields agreement_rate=None (unknown), never inherited from a sibling execution context (matches rxdo.9.15's AC 'an unseen execution context never inherits a confident pass'). JudgeIdentity (types.py) is documented as an interim stand-in for the not-yet-built h6r ActorRef/ExecutionContextRef pair -- same two-field shape so it re-points cleanly when h6r lands. Verification: devtools test tests/unit/insights/judgment/test_calibration.py -\u003e passed. PR: https://github.com/Sinity/polylogue/pull/2889 (open, not merged).\nFix round 2026-07-14 (post-review, commit bb3d3b8a7): fixed a third calibration corruption case -- _winner_identity mismatched types (tuple vs bare str/ComparativeVerdict) when a gold judgment is a 2-item n-wise ORDERING and a candidate judgment on the same pair is a pairwise PREFER_LEFT/RIGHT, both legal per ComparativeJudgment's own validation. Confirmed by repro: same real-world winner scored agreement_rate=0.0 because the two verdict shapes never compared equal. Fix: _winner_identity now resolves EVERY directed verdict (ordering or pairwise) to the same representation -- frozenset of (winner_ref, loser_ref) edges via decompose_to_pairwise -- so cross-representation overlaps compare correctly in both directions (agree when same winner, disagree when different). 2 new regression tests added (agreement + disagreement cases) alongside the 7 existing. Verification: devtools test tests/unit/insights/judgment/test_calibration.py -\u003e 9 passed. PR #2889 (open).\nPriority calibration 2026-07-15: P2 to P3. This remains part of the full project ambition, but it is a sequenced demo, experiment, governed analytic extension, or evaluation layer rather than a present failure of archive truth, bounded queryability, durability, or source fidelity. Priority is urgency, not deletion or scope reduction; horizon is unchanged.\nVERDICT: PARTIAL — calibration.py (PR #2889, merged) correctly implements the corrective AC as literally written (separable per-context calibration, unknown-not-inherited on missing gold, no cross-context pooling) and is tested. However it is called only by cascades.py within the same judgment package, and the entire polylogue/insights/judgment package has zero callers from CLI/MCP/daemon — same unwired-primitive pattern as sibling beads 9.6/9.7, just not flagged in this bead's own notes. Evidence: git grep -ln 'insights.judgment.calibration' origin/master -- '*.py' | grep -v tests/ -\u003e judgment/__init__.py + cascades.py only; git grep -ln 'insights.judgment' origin/master -- polylogue/cli/*.py polylogue/mcp/*.py polylogue/daemon/*.py -\u003e empty.","status":"in_progress","priority":3,"issue_type":"feature","assignee":"Sinity","owner":"ezo.dev@gmail.com","created_at":"2026-07-13T01:05:40Z","created_by":"Sinity","updated_at":"2026-07-31T06:48:54Z","started_at":"2026-07-31T06:48:54Z","labels":["area:substrate","delivery:C-read-evidence-contract","horizon:mid","lane:read-contracts","tech-tree"],"dependencies":[{"issue_id":"polylogue-rxdo.9.12","depends_on_id":"polylogue-h6r","type":"blocks","created_at":"2026-07-13T05:58:42Z","created_by":"Sinity","metadata":"{}"},{"issue_id":"polylogue-rxdo.9.12","depends_on_id":"polylogue-rxdo.9","type":"parent-child","created_at":"2026-07-13T03:05:39Z","created_by":"Sinity","metadata":"{}"}],"comments":[{"id":"019fb6ee-dbbe-7a2b-86f4-78ffc64f620e","issue_id":"polylogue-rxdo.9.12","author":"Sinity","text":"WIRED (this session): 'polylogue compare --calibration --gold-actor \u003cref\u003e' calls the real compute_calibration() over judgments read back through the (also newly wired) list_comparative_judgments storage reader, splitting recorded ComparativeJudgment rows into gold (matching --gold-actor) vs candidate and reporting per-(actor_ref, execution_context_id, dimension) agreement_rate/tie_rate/abstain_rate/etc. Previously calibration.py was called only by cascades.py within the same judgment package, itself unreachable from CLI/MCP/daemon -- same unwired-primitive pattern as sibling beads 9.6/9.7. Verified end-to-end in devtools test tests/unit/cli/test_compare_command.py::test_compare_with_verdict_records_and_is_readable_via_calibration: records a worker verdict + a gold verdict on the same comparison through separate real CLI invocations, then confirms --calibration reports agreement_rate=1.0 with n_gold_overlap=1 for the worker actor -- a real end-to-end round trip (write -\u003e storage -\u003e calibration compute -\u003e render), no mocks. Remaining open: judge-as-actor identity properly sourced from h6r ActorRef/ExecutionContextRef (JudgeIdentity already documented as an interim stand-in with the same two-field shape) and any richer surface than this CLI command. Commit e69b54df9.","created_at":"2026-07-31T06:48:55Z"}],"dependency_count":1,"dependent_count":0,"comment_count":1} {"_type":"issue","id":"polylogue-rxdo.9.10","title":"Experiment analysis projection over stc definitions and cohort relations","description":"Rigor mechanism J analyzes experiments but does not own a second experiment identity. It consumes a\nversioned typed ExperimentDefinition assertion from stc, cohort/result relation refs, canonical\nMetricDefinition refs, and assignment/exposure/outcome receipts. A pair of cohorts alone is an\nobservational comparison unless the experiment lifecycle proves otherwise.","design":"Lower ExperimentDefinition arms/assignment/exposure/frame/exclusions/stopping/analysis plan into\ncohort/result selections, then compute the preregistered registered metrics through 9l5.7. Preserve\npaired/unpaired design, confirmatory versus exploratory metrics, attrition/exclusion, leakage, exact\nevaluation worlds, and claim class. Emit an analysis artifact/receipt linked to the definition; do\nnot create an experiment table, registry, or separate lifecycle. Context PROMPT_EVAL, curriculum A/B,\nrouting/harness comparisons, PF-D8 matched resume treatments, and AI-D9 causal savings use this same path.","acceptance_criteria":"A stc two-arm fixture with assignments/exposures/outcomes analyzes end to end and reproduces declared\npaired metrics. An otherwise identical cohort-pair fixture without assignment/exposure renders\nobservational and cannot emit a causal claim. Post-exposure metric changes render exploratory/new-\nversion. At least two different consumers share the path without a second ExperimentDefinition or\nspecial-case executor.","notes":"[LEGACY FIELDS PRESERVED BY CORRECTIVE FOLLOW-UP 2026-07-13]\n\nORIGINAL DESCRIPTION:\nRigor mechanism J. Two cohorts + one metric_ref + a pre-registered comparison = an experiment object; thin composition over mechanisms A+B+C; bridge to the e5b5 eval harness. DEP: mechanisms A, B, C landed.\nPARTIAL: experiments.py -- analyze_experiment() implements the projection/analysis logic against a structural ExperimentDefinitionLike Protocol (cohort refs, registered metric refs, assignment/exposure receipts, exclusions, stopping rule). Design refuses a causal verdict without BOTH assignment and exposure receipts, downgrading to 'observational' otherwise -- matches rxdo.9's program-level AC 4. NOT satisfied: there is no live stc ExperimentDefinition producer to wire against yet (stc hasn't landed a concrete assertion), so this is tested only against the structural contract, not a real experiment object end-to-end. Remaining scope: wire against stc's concrete ExperimentDefinition once it lands. Verification: devtools test tests/unit/insights/judgment/test_experiments.py -\u003e passed (71/71 in the full judgment+storage test run). PR: https://github.com/Sinity/polylogue/pull/2889 (open, not merged).\nExperiment-record consolidation 2026-07-15: absorbs polylogue-wnse. The analysis artifact/receipt must reference arms, prompts/context packs, models, budgets, query/result refs, assignments/exposures/outcomes, ground truth, judge assertion refs, scores, exclusions, and caveats without creating a second eval_run identity/table.\nPriority correction 2026-07-15: promoted P4 to P3 after consolidating eval-run records here. The implementation already exists partially; completing one real ExperimentDefinition-to-analysis receipt is useful rigor infrastructure but remains sequenced behind current query/evidence correctness.","status":"open","priority":3,"issue_type":"feature","owner":"ezo.dev@gmail.com","created_at":"2026-07-13T00:56:22Z","created_by":"Sinity","updated_at":"2026-07-15T19:56:03Z","labels":["area:substrate","delivery:C-read-evidence-contract","horizon:mid","lane:read-contracts","tech-tree"],"dependencies":[{"issue_id":"polylogue-rxdo.9.10","depends_on_id":"polylogue-rxdo.9","type":"parent-child","created_at":"2026-07-13T02:56:22Z","created_by":"Sinity","metadata":"{}"},{"issue_id":"polylogue-rxdo.9.10","depends_on_id":"polylogue-stc","type":"related","created_at":"2026-07-13T07:55:17Z","created_by":"Sinity","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"polylogue-rxdo.9.8","title":"Bootstrap CIs for sampled results only; exactness propagation everywhere","description":"Rigor mechanism H distinguishes enumeration uncertainty from frame and measurement uncertainty.\nAn exact enumeration over stored rows receives no sampling confidence interval, but may still carry\nincomplete-frame coverage, classifier/judgment uncertainty, or other named measurement error.","design":"Consume rxdo.3's three-part result contract. Bootstrap/Wilson/order-statistic intervals are allowed\nonly for the uncertainty source they actually estimate. Bootstrap over members does not repair\nparser bias, missing capture, construct invalidity, or classifier error. Every rendered interval\nnames its source and method. Exact enumeration renders n/frame/authority without inferential\ndecoration; frame and measurement uncertainty remain visible alongside it.","acceptance_criteria":"Seed four cases: exact+complete+structural, exact+frame-incomplete, exact+model-derived, and sampled.\nOnly the sampled case receives sampling CI. The two exact-but-uncertain cases retain coverage or\nmodel/judgment uncertainty with named sources. A bootstrap request over missing capture or parser\ndisagreement is refused with an actionable explanation. Verify with focused renderer and\nstatistical-property tests.","notes":"[LEGACY FIELDS PRESERVED BY CORRECTIVE PASS 2026-07-13]\nORIGINAL DESCRIPTION:\nRigor mechanism H. Rule: exact population counts get NO inferential dressing (anti-theater); sampled/estimated results (exactness != exact) MAY carry a bootstrap CI over result-set members. Small, assumption-light. DEP: rxdo.3 exactness field.\nImplemented: polylogue/insights/measurement/uncertainty.py -- resolve_uncertainty() refuses a bootstrap sampling interval for exact/capped enumeration (would misrepresent enumeration certainty as inferential uncertainty) while independently rendering frame-coverage and measurement-authority uncertainty regardless of exactness, so an exact+frame-incomplete or exact+model-derived count still surfaces those facts without ever getting a sampling CI. Sampled/estimated enumeration is the only case that receives a bootstrap CI. A bootstrap request over missing capture or parser disagreement is refused with an actionable explanation (not silently computed). Verification: devtools test tests/unit/insights/measurement/test_uncertainty.py -\u003e passing (covers the four seeded cases: exact+complete+structural, exact+frame-incomplete, exact+model-derived, sampled -- only the last gets a CI). devtools verify --quick -\u003e exit 0. PR: https://github.com/Sinity/polylogue/pull/2888 (open, not merged).","status":"open","priority":3,"issue_type":"feature","owner":"ezo.dev@gmail.com","created_at":"2026-07-13T00:56:11Z","created_by":"Sinity","updated_at":"2026-07-14T14:33:03Z","labels":["area:substrate","delivery:C-read-evidence-contract","horizon:mid","lane:read-contracts","tech-tree"],"dependencies":[{"issue_id":"polylogue-rxdo.9.8","depends_on_id":"polylogue-rxdo.3","type":"blocks","created_at":"2026-07-13T07:47:45Z","created_by":"Sinity","metadata":"{}"},{"issue_id":"polylogue-rxdo.9.8","depends_on_id":"polylogue-rxdo.9","type":"parent-child","created_at":"2026-07-13T02:56:11Z","created_by":"Sinity","metadata":"{}"}],"comments":[{"id":"019fb6d6-1e59-7947-900f-217ae6b8b0d2","issue_id":"polylogue-rxdo.9.8","author":"Sinity","text":"Confirmed unstarted (this session, unwired-primitives sweep, per operator instruction to identify never-started work rather than manufacture an implementation to close it): rg -ln 'analysis_recipes|analysis_runs' polylogue/ still returns zero hits. No schema, no runtime code exists -- this is not an unwired primitive (nothing was built), it is blocked on polylogue-60i5 declaring a user-tier v6 window per the bead's own 2026-07-14 note. Leaving open, untouched. Not in scope for this pass (avoids manufacturing a schema addition against an undeclared migration window, which 60i5 exists specifically to prevent).","created_at":"2026-07-31T06:21:54Z"}],"dependency_count":1,"dependent_count":0,"comment_count":1} -{"_type":"issue","id":"polylogue-rxdo.9.7","title":"Render paired negative controls on live findings","description":"The existing `NegativeControl`, validation, and `ClaimWithControls` primitives correctly reject unmatched/confounded controls and downgrade a claim when a control fails. They currently have no production finding/view caller. Complete the mechanism by binding control query/result refs to canonical findings and rendering claim versus control together through the 37t.12/7ome judgment/read surfaces.","design":"Controls are declared comparison definitions and evidence refs on the canonical finding/judgment transaction, not ad-hoc baselines or a parallel result model. A live finding projection resolves matched-task, shifted-window, permuted-label, or justified unrelated-cohort controls, validates frame variables, expected-null behavior, confounds, and definition receipts, then renders the claim and controls together. Control failure visibly downgrades the claim/rank tier. Mechanically suggested controls remain candidates until accepted. Reuse `validate_control` and `ClaimWithControls`; do not leave the composition in a test-only helper.","acceptance_criteria":"1. A production finding with a preregistered matched control is readable through the canonical CLI/MCP and enabled web projection with claim and control shown together, exact query/result/definition refs, and expected-null outcome. 2. A deliberately divergent baseline is rejected as confounded; shifted/permuted controls preserve frame and definition receipts. 3. Control failure visibly downgrades the same claim and ranking projection rather than only a test-local dataclass property. 4. Missing, stale, unauthorized, or unresolved control evidence degrades explicitly without fabricating a pass. 5. Removing matching/confound validation, the production composition call, or downgrade propagation makes a focused real-route fixture fail.","notes":"Implemented: controls.py -- NegativeControl convention + validation. Matched-shape controls (shifted_window/permuted_label/matched_task) require declared matching variables that are a subset of the claim's frame variables; unrelated_cohort controls are rejected unless every frame variable is declared a checked confound (this is the 'deliberately divergent baseline is rejected as confounded' AC). ClaimWithControls renders claim-vs-control side by side and downgrades the claim on control failure. Verification: devtools test tests/unit/insights/judgment/test_controls.py -\u003e passed. PR: https://github.com/Sinity/polylogue/pull/2889 (open, not merged).\nCORRECTIVE (review fix round, 2026-07-14): the mechanism itself (NegativeControl validation, ClaimWithControls rendering, tested) is correctly implemented and verified, but disclosure was incomplete -- the original \"Implemented\" note did not flag that ClaimWithControls has zero callers anywhere in the product (CLI/MCP/daemon) outside tests/unit/insights/judgment/test_controls.py. Nothing today calls this to actually render a claim beside its control in a live surface, so \"renders beside the finding\" is satisfied at the type/rendering-function level but not end-to-end. Wiring a live consumer (findings/read-view surface that constructs ClaimWithControls) is not yet scoped to a specific bead; tracked as remaining scope under polylogue-7ome's judgment-UX-surface umbrella (or a narrower follow-up if that turns out too broad). Not fixed in this PR -- would be scope expansion beyond the mechanism-core pass; flagging honestly per review finding (PR #2889).\n2026-07-16 closure correction: PR #2889 delivered and mutation-tested the control primitives, but its own corrective note acknowledged that no product surface constructs `ClaimWithControls`. Reopened for the missing canonical finding/read integration.\nVERDICT: LIVE — ClaimWithControls (PR #2889, merged) has zero callers anywhere outside tests/unit/insights/judgment/test_controls.py; matches bead's own 2026-07-16 reopen note ('no product surface constructs ClaimWithControls'). Evidence: git grep -n 'ClaimWithControls(' origin/master -- '*.py' | grep -v tests/ -\u003e empty.","status":"open","priority":3,"issue_type":"feature","owner":"ezo.dev@gmail.com","created_at":"2026-07-13T00:56:06Z","created_by":"Sinity","updated_at":"2026-07-31T05:46:46Z","labels":["area:substrate","delivery:C-read-evidence-contract","horizon:mid","lane:read-contracts","tech-tree"],"dependencies":[{"issue_id":"polylogue-rxdo.9.7","depends_on_id":"polylogue-37t.12","type":"blocks","created_at":"2026-07-16T19:15:48Z","created_by":"Sinity","metadata":"{}"},{"issue_id":"polylogue-rxdo.9.7","depends_on_id":"polylogue-7ome","type":"relates-to","created_at":"2026-07-16T19:15:52Z","created_by":"Sinity","metadata":"{}"},{"issue_id":"polylogue-rxdo.9.7","depends_on_id":"polylogue-rxdo.9","type":"parent-child","created_at":"2026-07-13T02:56:06Z","created_by":"Sinity","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"polylogue-rxdo.9.6","title":"Wire blinded judgment into live judgment surfaces","description":"The mutation-tested blinding primitive already exists in `polylogue/insights/judgment/blinding.py`, and `ElicitationSession` uses it to receipt randomized item order. The missing capability is production consumption: no canonical judgment queue/read surface applies the blinded projection before verdict, prevents adjacent payload leakage, or performs receipted reveal afterward. Wire the existing primitive through the 37t.12 judgment transaction and 7ome experience surfaces; do not create another queue or blind-data store.","design":"Keep blinding as a projection policy over the canonical 37t.12 transaction. Before verdict, every CLI/MCP/web judgment view obtains candidates through one projection that masks actor, model/provider, detector, arm, prior score/rank, judge, and execution-context provenance while retaining rubric-required evidence. Bind randomized order and the projection hash to the judgment transaction. Reveal the same underlying evidence only after verdict, abstention, or an explicit receipted policy exception. Adjacent list/detail/export/error payloads must consume the same disclosure policy. Reuse the existing `blind_items`, `assert_no_leak`, and `reveal` primitives.","acceptance_criteria":"1. A real judgment submitted and read through the canonical 37t.12/7ome production route is blinded before verdict across primary and adjacent list/detail/export/error payloads. 2. Item order, projection hash, rubric, ActorRef, and ExecutionContextRef are receipted on that same transaction; no second queue or blind store exists. 3. Verdict or abstention reveals the same evidence refs, while an unauthorized early reveal fails closed. 4. CLI, MCP, and any enabled web projection agree on masked/revealed fields. 5. Existing independent mask-checklist tests remain mutation-sensitive, and removing the production projection call or one mask leaks a sentinel and fails a focused real-route test.","notes":"Implemented: blinding.py -- blind_items() projects raw candidate records into a masked, order-bound BlindedItem view (DEFAULT_MASKED_PROVENANCE_FIELDS: actor_ref/author_ref/author_kind/model/provider/detector_ref/arm/prior_score/prior_rank/judge_ref/execution_context_id), receipted via BlindingReceipt (item_order_hash, masked_fields, rubric_ref, sealed_at_ms). reveal() only authorizes exposure once verdict_recorded=True. assert_no_leak() is a defense-in-depth check raised if a masked field survives projection -- test_blinding.py includes the 'remove one production mask leaks a sentinel and fails the test' AC via this function. No new store: pure projection over existing candidate records, evidence refs stay intact. Verification: devtools test tests/unit/insights/judgment/test_blinding.py -\u003e passed. PR: https://github.com/Sinity/polylogue/pull/2889 (open, not merged).\nCORRECTIVE (review fix round, 2026-07-14): the mechanism itself (blind_items/reveal/assert_no_leak, receipted, tested) is correctly implemented and verified, but disclosure was incomplete -- the original \"Implemented\" note did not flag that blind_items() has zero callers anywhere in the product (CLI/MCP/daemon) outside tests/unit/insights/judgment/test_blinding.py. Nothing today constructs a judge surface that calls this projection, so the mechanism is currently unreachable end-to-end even though its unit-level correctness is real and tested. Wiring a live judge surface (p5g interactive judge, MCP judgment tools) that actually calls blind_items() is scoped to polylogue-7ome (judgment UX surface: inbox, micro-moments, deliberate sessions), which is itself blocked on p5g landing. Not fixed in this PR -- would be scope expansion beyond the mechanism-core pass; flagging honestly per review finding (PR #2889).\nFIX ROUND (PR #2889 review, 2026-07-14): reviewer mutation-tested the blinding leak AC and found 9 of 11 DEFAULT_MASKED_PROVENANCE_FIELDS had zero regression coverage -- test_removing_one_production_mask_leaks_a_sentinel_and_fails derived its \"weakened\" mask set by subtracting from the live production constant and test_masked_fields_are_not_recoverable_from_the_visible_projection iterated that same constant to build its checklist, so a shrunk production set silently shrank both tests' expectations. Confirmed correct via mutation test (deleted detector_ref from production, reran, 7 tests still green).\n\nFixed in tests/unit/insights/judgment/test_blinding.py (commit 9a62ed535, pushed to feature/analysis/rxdo9-comparative-judgment): added a hardcoded 11-field checklist (_EXPECTED_MASKED_PROVENANCE_FIELDS) independent of the production constant, mirroring test_controls.py's rxdo.9.7 pattern; added test_default_masks_match_the_frozen_checklist to catch drift between the two directly; parametrized test_removing_one_production_mask_leaks_a_sentinel_and_fails over all 11 fields instead of hardcoding only actor_ref.\n\nVerification: mutation-tested by deleting detector_ref from blinding.py -- 3 tests now fail (field-specific parametrized case, checklist-match guard, leak-free-projection test); restored production code (git diff clean) and reran green -- devtools test tests/unit/insights/judgment/test_blinding.py -\u003e 18 passed; devtools verify --quick -\u003e exit_code 0; pre-push quick baseline also green on push.\n2026-07-16 closure correction: mechanism-level unit coverage was real, but the original AC also required canonical judgment consumption. The only current non-test caller is the internal ElicitationSession order receipt; there is still no live judgment surface. Reopened for 37t.12/7ome integration.\nVERDICT: LIVE — blind_items() (PR #2889, merged) is called only from ElicitationSession in the same package (elicitation.py); ElicitationSession itself has zero callers anywhere in CLI/MCP/daemon. No canonical judgment surface applies the blinded projection in production — exactly matches the bead's own 2026-07-14 corrective note ('missing capability is production consumption'). Evidence: git grep -n 'blind_items(' origin/master -- '*.py' | grep -v tests/ -\u003e only elicitation.py; git grep -ln 'ElicitationSession' origin/master -- '*.py' | grep -v tests/ -\u003e only elicitation.py itself; git grep -ln 'insights.judgment' origin/master -- polylogue/cli/*.py polylogue/mcp/*.py polylogue/daemon/*.py -\u003e empty.","status":"open","priority":3,"issue_type":"feature","owner":"ezo.dev@gmail.com","created_at":"2026-07-13T00:56:01Z","created_by":"Sinity","updated_at":"2026-07-31T05:46:44Z","labels":["area:substrate","delivery:C-read-evidence-contract","horizon:mid","lane:read-contracts","tech-tree"],"dependencies":[{"issue_id":"polylogue-rxdo.9.6","depends_on_id":"polylogue-37t.12","type":"blocks","created_at":"2026-07-16T19:15:42Z","created_by":"Sinity","metadata":"{}"},{"issue_id":"polylogue-rxdo.9.6","depends_on_id":"polylogue-7ome","type":"relates-to","created_at":"2026-07-16T19:15:45Z","created_by":"Sinity","metadata":"{}"},{"issue_id":"polylogue-rxdo.9.6","depends_on_id":"polylogue-rxdo.9","type":"parent-child","created_at":"2026-07-13T02:56:00Z","created_by":"Sinity","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"polylogue-rxdo.9.7","title":"Render paired negative controls on live findings","description":"The existing `NegativeControl`, validation, and `ClaimWithControls` primitives correctly reject unmatched/confounded controls and downgrade a claim when a control fails. They currently have no production finding/view caller. Complete the mechanism by binding control query/result refs to canonical findings and rendering claim versus control together through the 37t.12/7ome judgment/read surfaces.","design":"Controls are declared comparison definitions and evidence refs on the canonical finding/judgment transaction, not ad-hoc baselines or a parallel result model. A live finding projection resolves matched-task, shifted-window, permuted-label, or justified unrelated-cohort controls, validates frame variables, expected-null behavior, confounds, and definition receipts, then renders the claim and controls together. Control failure visibly downgrades the claim/rank tier. Mechanically suggested controls remain candidates until accepted. Reuse `validate_control` and `ClaimWithControls`; do not leave the composition in a test-only helper.","acceptance_criteria":"1. A production finding with a preregistered matched control is readable through the canonical CLI/MCP and enabled web projection with claim and control shown together, exact query/result/definition refs, and expected-null outcome. 2. A deliberately divergent baseline is rejected as confounded; shifted/permuted controls preserve frame and definition receipts. 3. Control failure visibly downgrades the same claim and ranking projection rather than only a test-local dataclass property. 4. Missing, stale, unauthorized, or unresolved control evidence degrades explicitly without fabricating a pass. 5. Removing matching/confound validation, the production composition call, or downgrade propagation makes a focused real-route fixture fail.","notes":"Implemented: controls.py -- NegativeControl convention + validation. Matched-shape controls (shifted_window/permuted_label/matched_task) require declared matching variables that are a subset of the claim's frame variables; unrelated_cohort controls are rejected unless every frame variable is declared a checked confound (this is the 'deliberately divergent baseline is rejected as confounded' AC). ClaimWithControls renders claim-vs-control side by side and downgrades the claim on control failure. Verification: devtools test tests/unit/insights/judgment/test_controls.py -\u003e passed. PR: https://github.com/Sinity/polylogue/pull/2889 (open, not merged).\nCORRECTIVE (review fix round, 2026-07-14): the mechanism itself (NegativeControl validation, ClaimWithControls rendering, tested) is correctly implemented and verified, but disclosure was incomplete -- the original \"Implemented\" note did not flag that ClaimWithControls has zero callers anywhere in the product (CLI/MCP/daemon) outside tests/unit/insights/judgment/test_controls.py. Nothing today calls this to actually render a claim beside its control in a live surface, so \"renders beside the finding\" is satisfied at the type/rendering-function level but not end-to-end. Wiring a live consumer (findings/read-view surface that constructs ClaimWithControls) is not yet scoped to a specific bead; tracked as remaining scope under polylogue-7ome's judgment-UX-surface umbrella (or a narrower follow-up if that turns out too broad). Not fixed in this PR -- would be scope expansion beyond the mechanism-core pass; flagging honestly per review finding (PR #2889).\n2026-07-16 closure correction: PR #2889 delivered and mutation-tested the control primitives, but its own corrective note acknowledged that no product surface constructs `ClaimWithControls`. Reopened for the missing canonical finding/read integration.\nVERDICT: LIVE — ClaimWithControls (PR #2889, merged) has zero callers anywhere outside tests/unit/insights/judgment/test_controls.py; matches bead's own 2026-07-16 reopen note ('no product surface constructs ClaimWithControls'). Evidence: git grep -n 'ClaimWithControls(' origin/master -- '*.py' | grep -v tests/ -\u003e empty.","status":"in_progress","priority":3,"issue_type":"feature","assignee":"Sinity","owner":"ezo.dev@gmail.com","created_at":"2026-07-13T00:56:06Z","created_by":"Sinity","updated_at":"2026-07-31T06:48:37Z","started_at":"2026-07-31T06:48:37Z","labels":["area:substrate","delivery:C-read-evidence-contract","horizon:mid","lane:read-contracts","tech-tree"],"dependencies":[{"issue_id":"polylogue-rxdo.9.7","depends_on_id":"polylogue-37t.12","type":"blocks","created_at":"2026-07-16T19:15:48Z","created_by":"Sinity","metadata":"{}"},{"issue_id":"polylogue-rxdo.9.7","depends_on_id":"polylogue-7ome","type":"relates-to","created_at":"2026-07-16T19:15:52Z","created_by":"Sinity","metadata":"{}"},{"issue_id":"polylogue-rxdo.9.7","depends_on_id":"polylogue-rxdo.9","type":"parent-child","created_at":"2026-07-13T02:56:06Z","created_by":"Sinity","metadata":"{}"}],"comments":[{"id":"019fb6ee-9926-7081-a3e4-45dd9691fccd","issue_id":"polylogue-rxdo.9.7","author":"Sinity","text":"WIRED (this session): FindingAssertion gained an optional 'controls' field (polylogue/storage/sqlite/archive_tiers/user_write.py). _finding_value validates each declared control via the real validate_control() (reused, not re-derived) and rejects the whole finding write on a confounded/unmatched control -- fails closed, matching AC2. Polylogue.resolve_ref's finding-provenance path (_resolve_finding_object_ref in polylogue/api/archive.py) now constructs ClaimWithControls from the stored controls and merges rank_tier/downgraded/controls into the real finding:\u003cid\u003e ref-resolution payload, adding a caveat when a control failed -- so 'polylogue find ... then read' / MCP get(ref='finding:\u003cid\u003e') now render claim-vs-control together for real, matching AC1/AC3. Not built: automatic re-execution of the control's query_ref/result_ref at read time (the detector/analyst records its own observed_null_held at write time instead) -- building a query re-execution engine is rxdo.6 scope, out of bounds here. Verified: devtools test tests/unit/api/test_facade_contracts.py::test_resolve_ref_renders_finding_claim_with_controls tests/unit/storage/test_archive_tiers_assertions.py -k control -\u003e passed, real storage writer + real Polylogue.resolve_ref facade, no mocks on ClaimWithControls/validate_control. Commit e69b54df9.","created_at":"2026-07-31T06:48:38Z"}],"dependency_count":1,"dependent_count":0,"comment_count":1} +{"_type":"issue","id":"polylogue-rxdo.9.6","title":"Wire blinded judgment into live judgment surfaces","description":"The mutation-tested blinding primitive already exists in `polylogue/insights/judgment/blinding.py`, and `ElicitationSession` uses it to receipt randomized item order. The missing capability is production consumption: no canonical judgment queue/read surface applies the blinded projection before verdict, prevents adjacent payload leakage, or performs receipted reveal afterward. Wire the existing primitive through the 37t.12 judgment transaction and 7ome experience surfaces; do not create another queue or blind-data store.","design":"Keep blinding as a projection policy over the canonical 37t.12 transaction. Before verdict, every CLI/MCP/web judgment view obtains candidates through one projection that masks actor, model/provider, detector, arm, prior score/rank, judge, and execution-context provenance while retaining rubric-required evidence. Bind randomized order and the projection hash to the judgment transaction. Reveal the same underlying evidence only after verdict, abstention, or an explicit receipted policy exception. Adjacent list/detail/export/error payloads must consume the same disclosure policy. Reuse the existing `blind_items`, `assert_no_leak`, and `reveal` primitives.","acceptance_criteria":"1. A real judgment submitted and read through the canonical 37t.12/7ome production route is blinded before verdict across primary and adjacent list/detail/export/error payloads. 2. Item order, projection hash, rubric, ActorRef, and ExecutionContextRef are receipted on that same transaction; no second queue or blind store exists. 3. Verdict or abstention reveals the same evidence refs, while an unauthorized early reveal fails closed. 4. CLI, MCP, and any enabled web projection agree on masked/revealed fields. 5. Existing independent mask-checklist tests remain mutation-sensitive, and removing the production projection call or one mask leaks a sentinel and fails a focused real-route test.","notes":"Implemented: blinding.py -- blind_items() projects raw candidate records into a masked, order-bound BlindedItem view (DEFAULT_MASKED_PROVENANCE_FIELDS: actor_ref/author_ref/author_kind/model/provider/detector_ref/arm/prior_score/prior_rank/judge_ref/execution_context_id), receipted via BlindingReceipt (item_order_hash, masked_fields, rubric_ref, sealed_at_ms). reveal() only authorizes exposure once verdict_recorded=True. assert_no_leak() is a defense-in-depth check raised if a masked field survives projection -- test_blinding.py includes the 'remove one production mask leaks a sentinel and fails the test' AC via this function. No new store: pure projection over existing candidate records, evidence refs stay intact. Verification: devtools test tests/unit/insights/judgment/test_blinding.py -\u003e passed. PR: https://github.com/Sinity/polylogue/pull/2889 (open, not merged).\nCORRECTIVE (review fix round, 2026-07-14): the mechanism itself (blind_items/reveal/assert_no_leak, receipted, tested) is correctly implemented and verified, but disclosure was incomplete -- the original \"Implemented\" note did not flag that blind_items() has zero callers anywhere in the product (CLI/MCP/daemon) outside tests/unit/insights/judgment/test_blinding.py. Nothing today constructs a judge surface that calls this projection, so the mechanism is currently unreachable end-to-end even though its unit-level correctness is real and tested. Wiring a live judge surface (p5g interactive judge, MCP judgment tools) that actually calls blind_items() is scoped to polylogue-7ome (judgment UX surface: inbox, micro-moments, deliberate sessions), which is itself blocked on p5g landing. Not fixed in this PR -- would be scope expansion beyond the mechanism-core pass; flagging honestly per review finding (PR #2889).\nFIX ROUND (PR #2889 review, 2026-07-14): reviewer mutation-tested the blinding leak AC and found 9 of 11 DEFAULT_MASKED_PROVENANCE_FIELDS had zero regression coverage -- test_removing_one_production_mask_leaks_a_sentinel_and_fails derived its \"weakened\" mask set by subtracting from the live production constant and test_masked_fields_are_not_recoverable_from_the_visible_projection iterated that same constant to build its checklist, so a shrunk production set silently shrank both tests' expectations. Confirmed correct via mutation test (deleted detector_ref from production, reran, 7 tests still green).\n\nFixed in tests/unit/insights/judgment/test_blinding.py (commit 9a62ed535, pushed to feature/analysis/rxdo9-comparative-judgment): added a hardcoded 11-field checklist (_EXPECTED_MASKED_PROVENANCE_FIELDS) independent of the production constant, mirroring test_controls.py's rxdo.9.7 pattern; added test_default_masks_match_the_frozen_checklist to catch drift between the two directly; parametrized test_removing_one_production_mask_leaks_a_sentinel_and_fails over all 11 fields instead of hardcoding only actor_ref.\n\nVerification: mutation-tested by deleting detector_ref from blinding.py -- 3 tests now fail (field-specific parametrized case, checklist-match guard, leak-free-projection test); restored production code (git diff clean) and reran green -- devtools test tests/unit/insights/judgment/test_blinding.py -\u003e 18 passed; devtools verify --quick -\u003e exit_code 0; pre-push quick baseline also green on push.\n2026-07-16 closure correction: mechanism-level unit coverage was real, but the original AC also required canonical judgment consumption. The only current non-test caller is the internal ElicitationSession order receipt; there is still no live judgment surface. Reopened for 37t.12/7ome integration.\nVERDICT: LIVE — blind_items() (PR #2889, merged) is called only from ElicitationSession in the same package (elicitation.py); ElicitationSession itself has zero callers anywhere in CLI/MCP/daemon. No canonical judgment surface applies the blinded projection in production — exactly matches the bead's own 2026-07-14 corrective note ('missing capability is production consumption'). Evidence: git grep -n 'blind_items(' origin/master -- '*.py' | grep -v tests/ -\u003e only elicitation.py; git grep -ln 'ElicitationSession' origin/master -- '*.py' | grep -v tests/ -\u003e only elicitation.py itself; git grep -ln 'insights.judgment' origin/master -- polylogue/cli/*.py polylogue/mcp/*.py polylogue/daemon/*.py -\u003e empty.","status":"in_progress","priority":3,"issue_type":"feature","assignee":"Sinity","owner":"ezo.dev@gmail.com","created_at":"2026-07-13T00:56:01Z","created_by":"Sinity","updated_at":"2026-07-31T06:48:19Z","started_at":"2026-07-31T06:48:19Z","labels":["area:substrate","delivery:C-read-evidence-contract","horizon:mid","lane:read-contracts","tech-tree"],"dependencies":[{"issue_id":"polylogue-rxdo.9.6","depends_on_id":"polylogue-37t.12","type":"blocks","created_at":"2026-07-16T19:15:42Z","created_by":"Sinity","metadata":"{}"},{"issue_id":"polylogue-rxdo.9.6","depends_on_id":"polylogue-7ome","type":"relates-to","created_at":"2026-07-16T19:15:45Z","created_by":"Sinity","metadata":"{}"},{"issue_id":"polylogue-rxdo.9.6","depends_on_id":"polylogue-rxdo.9","type":"parent-child","created_at":"2026-07-13T02:56:00Z","created_by":"Sinity","metadata":"{}"}],"comments":[{"id":"019fb6ee-51f8-7b0f-8e0f-40c505be5267","issue_id":"polylogue-rxdo.9.6","author":"Sinity","text":"WIRED (this session): new 'polylogue compare' CLI command (polylogue/cli/commands/compare.py) calls blind_items()/BlindingReceipt/reveal() for real on every invocation -- masks --left-field/--right-field provenance (e.g. model=) before verdict, receipts item order, and reveals only after a verdict is recorded via the (also newly wired) upsert_comparative_judgment_assertion storage chokepoint. This is deliberately NOT the full 37t.12/7ome canonical judgment-queue UX (inbox, micro-moments, resorter sessions) named in this bead's corrective scope -- building that epic is explicitly out of bounds for this pass. It IS a real, tested, reachable production caller where none existed (previously only ElicitationSession called blind_items, and ElicitationSession itself had zero callers). DELETE was rejected: polylogue-7ome's own design explicitly plans to reuse BlindedItem/BlindingReceipt as its rendering primitives. Verified: devtools test tests/unit/cli/test_compare_command.py -\u003e 2 passed, real CLI invocation via CliRunner against a real archive, no mocks on the blinding/storage machinery. Remaining open: the canonical judgment-queue surface (37t.12/7ome) itself. Commit e69b54df9.","created_at":"2026-07-31T06:48:20Z"}],"dependency_count":1,"dependent_count":0,"comment_count":1} {"_type":"issue","id":"polylogue-rxdo.9.4","title":"Holdout cohorts: persistence class + planner enforcement","description":"Rigor mechanism D. persistence_class='holdout' on cohorts/result_sets: excluded from exploratory queries by default; the RefOperand planner layer (rxdo.6) warns-or-fails on exploratory references; confirmation runs self-declare and are marked in query_runs. Gives demo claims the 'held on untouched data' leg. DEP: rxdo.6 planner + rxdo.2 persistence classes.","design":"Holdout is a persistence/access policy on a CohortDefinition or promoted relation manifest, not a second\ncohort type. It records frame, selection definition, creation epoch, intended confirmation use, authority,\nprivacy/excision, and contamination events. Exploratory planning excludes holdouts by default; explicit\nconfirmation access emits a receipt and cannot retroactively restore a contaminated holdout.","acceptance_criteria":"Exploratory queries cannot read a seeded holdout. A declared confirmation run can, with a visible access\nreceipt. Accidental/unauthorized access marks contamination and prevents an untouched-holdout claim.\nReset/excision preserve the declared durability semantics, and cohort/result relation identities remain\ndistinct while sharing RelationManifest.","notes":"Implemented: polylogue/storage/sqlite/holdout_cohorts.py + migration user/009_result_set_holdouts.sql (user.db schema v9) -- holdout is an access POLICY layered on an existing rxdo.2 result_sets manifest row, not a second cohort type. mark_holdout() records frame/selection/creation epoch/intended confirmation use/authority; require_non_holdout_access() refuses exploratory reads by default; record_holdout_access() gives a declared confirmation run an explicit pass + visible receipt, and an undeclared access a permanent contamination marker no later declared access can clear. Deferred (explicit, not silently dropped): rxdo.6 planner wiring for require_non_holdout_access -- the planner itself doesn't exist yet in this tree, so there is no RefOperand layer to wire into; tracked as open scope on this bead rather than closed. Reset/excision AC covered only at the DB layer: migration's ON DELETE RESTRICT FK on the holdout marker blocks a raw DELETE of a holdout-marked result_sets row with sqlite3.IntegrityError (test_deleting_a_holdout_marked_result_set_is_blocked_by_the_durable_fk) -- no excision/reset mechanism exists for result_sets in this tree at all yet, so there's nothing more to integration-test; no design exists yet for how a future excision path should unmark before deleting. Verification: devtools test tests/unit/storage/test_holdout_cohorts.py tests/unit/storage/test_durable_migrations.py -\u003e passing (includes fresh-DDL-vs-migrated-DDL fast-forward comparison extended for the new tables). devtools verify --quick -\u003e exit 0. PR: https://github.com/Sinity/polylogue/pull/2888 (open, not merged). Status: partial -- core mechanism (persistence-class marking, access refusal, contamination) is real and tested; planner enforcement is deferred pending rxdo.6 existing.\nPriority calibration 2026-07-15: P2 to P3. This remains part of the full project ambition, but it is a sequenced demo, experiment, governed analytic extension, or evaluation layer rather than a present failure of archive truth, bounded queryability, durability, or source fidelity. Priority is urgency, not deletion or scope reduction; horizon is unchanged.\n[2026-07-29, dead-code purge] Removed polylogue/storage/sqlite/holdout_cohorts.py\nand its test (tests/unit/storage/test_holdout_cohorts.py) from the tree.\nVerified by whole-tree grep: nothing outside the module's own test called\nHoldoutPolicy/mark_holdout/is_holdout/record_holdout_access/\nlist_holdout_access_receipts/has_holdout_contamination/\nrequire_non_holdout_access -- this bead's own notes already say the real\nenforcement point (the rxdo.6 RefOperand planner) doesn't exist yet, so the\nguard was reachable from no code path at all: \"protection\" that nothing\ncalls. Left the durable migration (storage/sqlite/migrations/user/\n009_result_set_holdouts.sql, user.db schema v9) and its tables\n(result_set_holdout_policies, holdout_access_receipts) untouched -- durable\ntiers are additive-only per this repo's schema regime, and\ntest_durable_migrations.py enumerates them structurally, independent of\nthis Python module. Holdout protection does NOT exist on any access path\ntoday. When rxdo.6 (or any other planner) actually lands, this design\n(persistence-class marking, typed contamination, access receipts) is\npreserved verbatim in git history at 0a464db08 and this commit; re-add it\nalongside its first real caller rather than ahead of one.\nVERDICT: LIVE — holdout mechanism was implemented (PR #2888) then deliberately PURGED as dead code on 2026-07-29 (nothing called it; rxdo.6 planner enforcement point still doesn't exist). Confirmed absent from current master. The tool's citation of PR #2888 is stale/wrong since the code it points to no longer exists. Evidence: git show origin/master:polylogue/storage/sqlite/holdout_cohorts.py -\u003e fatal (path does not exist); bead's own 2026-07-29 note documents the purge.","status":"open","priority":3,"issue_type":"feature","owner":"ezo.dev@gmail.com","created_at":"2026-07-13T00:55:49Z","created_by":"Sinity","updated_at":"2026-07-31T05:46:43Z","labels":["area:substrate","delivery:C-read-evidence-contract","horizon:mid","lane:read-contracts","tech-tree"],"dependencies":[{"issue_id":"polylogue-rxdo.9.4","depends_on_id":"polylogue-rxdo.9","type":"parent-child","created_at":"2026-07-13T02:55:49Z","created_by":"Sinity","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"polylogue-rxdo.9.1","title":"metric:\u003chash\u003e — content-addressed metric definitions","description":"Rigor mechanism A (highest leverage; docs/design/analysis-rigor.md). Metric definitions (unit source, filters, material_origin mask, aggregation, exclusions) canonicalize+hash exactly like query:\u003chash\u003e, riding the rxdo.2 canonicalizer. Findings carry metric_ref; comparing claims with different metric hashes is VISIBLY invalid; definition drift becomes a diffable event. Evidence this matters: the 7.69x Codex cost inflation and the 376.6B-token figure were both DEFINITION errors computed exactly. DEP: rxdo.2 substrate merge (PR #2813 lineage).\n\n## Authoritative corrective scope (2026-07-13)\n\nThis bead is the sole canonical identity/schema owner for metric:\u003chash\u003e. The statistics registry in\n9l5.7 consumes MetricDefinition; it must not create a competing MeasureSpec identity.","design":"## Authoritative corrective contract (2026-07-13)\n\nMetricDefinition carries construct, formula/component refs, unit, grain, denominator/null policy,\nrequired enumeration/frame/authority, confounds, provenance mixing, and output schema. 9l5.7 may\nprovide a Python declaration type named for implementation convenience, but serialization, hash,\nrefs, registry identity, and versioning are this MetricDefinition protocol.","acceptance_criteria":"## Corrective acceptance criteria (2026-07-13)\n\nOne hash/ref resolves through both query/analysis and statistical-registry paths. Creating an\nequivalent second MeasureSpec identity is impossible or rejected by the completeness audit.","notes":"Implemented: polylogue/insights/measurement/canon.py (canonicalize()+content_ref(), rides core.hashing.hash_payload, NFC-normalizes every string scalar/key so Unicode-equivalent strings hash identically) + metric.py (MetricDefinition dataclass: construct, formula/component refs, unit, grain, denominator/null policy, required enumeration/frame/measurement_authority as an order-independent set, confounds, provenance_mixing; MetricRegistry rejects a second definition bound to the same friendly name, satisfying the 'one hash resolves through both paths, second identity impossible' AC at the identity layer). Fixed post-review: measurement_authority order-independence (was list(), now canonical_payload sorts it) + NFC normalization in canon.py (both were CodeRabbit/Codex P2 findings, fixed in 528f94c46). 9l5.7 (statistics registry) itself is explicitly out of scope -- this bead is the identity/schema owner only. Verification: devtools test tests/unit/insights/measurement/test_canon.py tests/unit/insights/measurement/test_metric.py -\u003e passing (part of 126 passed in the full measurement+holdout run). devtools verify --quick -\u003e exit 0. PR: https://github.com/Sinity/polylogue/pull/2888 (open, not merged).\nPriority calibration 2026-07-15: P2 to P3. This remains part of the full project ambition, but it is a sequenced demo, experiment, governed analytic extension, or evaluation layer rather than a present failure of archive truth, bounded queryability, durability, or source fidelity. Priority is urgency, not deletion or scope reduction; horizon is unchanged.\nVERDICT: PARTIAL — MetricDefinition identity/schema (canon.py + metric.py, PR #2888 merged) is real and tested at its own narrow scope (sole identity/schema owner). But the corrective AC 'one hash resolves through both query/analysis AND statistical-registry paths' is unverifiable: the second path (9l5.7 statistics registry) is still open/unimplemented on master, and grep confirms zero consumers of MetricDefinition outside its own module/tests. Evidence: gh pr view 2888 (MERGED); bd show polylogue-9l5.7 --json (status open); git grep -ln MetricDefinition origin/master -- '*.py' excluding tests/insights/measurement (empty).","status":"in_progress","priority":3,"issue_type":"feature","assignee":"Sinity","owner":"ezo.dev@gmail.com","created_at":"2026-07-13T00:55:34Z","created_by":"Sinity","updated_at":"2026-07-31T06:27:17Z","started_at":"2026-07-31T06:27:17Z","labels":["area:substrate","delivery:C-read-evidence-contract","horizon:mid","lane:read-contracts","tech-tree"],"dependencies":[{"issue_id":"polylogue-rxdo.9.1","depends_on_id":"polylogue-9l5.7","type":"relates-to","created_at":"2026-07-15T20:53:17Z","created_by":"Sinity","metadata":"{}"},{"issue_id":"polylogue-rxdo.9.1","depends_on_id":"polylogue-rxdo.9","type":"parent-child","created_at":"2026-07-13T02:55:33Z","created_by":"Sinity","metadata":"{}"}],"comments":[{"id":"019fb6db-12fe-7813-9c29-f3fd2f1f2110","issue_id":"polylogue-rxdo.9.1","author":"Sinity","text":"PARTIAL WIRE (this session, unwired-primitives sweep): added polylogue/insights/measurement/registered_metrics.py -- a process-wide DEFAULT_METRIC_REGISTRY with one real registered MetricDefinition (session_cost_usd), resolvable through the real MCP get() tool (get(ref=\"metric:session_cost_usd\") or get(ref=\"metric:\u003chash\u003e\")). This is deliberately NOT the corrective AC's full second-consumer path (9l5.7's statistics registry remains unstarted, and building that composition/aggregation epic here would be exactly the rxdo-epic scope expansion this pass avoids) -- it proves the identity/registry machinery is reachable from a real production surface rather than only its own unit tests. DELETE was rejected: 9l5.7.2 and polylogue-stc both explicitly depend on MetricDefinition existing as their foundation. Remaining open, honestly not claimed: metric execution/aggregation (9l5.7), metric_ref attachment to any computed value, the corrective AC's literal 'one hash resolves through both paths' (still blocked on 9l5.7). Verified: devtools test tests/unit/mcp/test_metric_ref_resolution.py tests/unit/insights/measurement/test_registered_metrics.py -\u003e 6 passed, real MCP get() route, no mocks on the registry. Commit 30cdd0538.","created_at":"2026-07-31T06:27:18Z"}],"dependency_count":0,"dependent_count":2,"comment_count":1} {"_type":"issue","id":"polylogue-rxdo.9","title":"Analysis rigor program: frame-exact validity, judgments, and experiments","description":"Rigor is encoded in the provenance graph and result contracts, not added as statistical decoration.\nArchive counts are exact only over their declared frame under named definitions; frame coverage and\nmeasurement/classifier/judgment uncertainty remain independent. The program owns canonical metrics,\nderived ratios, preregistration, holdouts, alert budgets, blinding, negative controls, uncertainty by\nsource, evidence ancestry, ExperimentDefinition consumption, comparative judgments, calibrated\nactors, rankers, active elicitation, and cascades. Every mechanism must change what may be claimed or\nwhat action may fire.","design":"Mechanisms A-I remain typed children over the rxdo graph. Metric identity is rxdo.9.1 and statistical\nenforcement is 9l5.7; no competing MeasureSpec. Result rigor consumes rxdo.3's enumeration/frame/\nmeasurement-authority axes. Evidence ancestry is rxdo.9.9. Mechanism J does not define another\nexperiment object: rxdo.9.10 is an analysis/projection over stc ExperimentDefinition, cohort/result\nrefs, registered metrics, assignments/exposures, and outcomes. Comparative judgment extensions\nrxdo.9.11-.15 preserve tie/incomparable/abstain/insufficient evidence, partial orders, actor plus\nexecution-context calibration, exploration quotas, blinding, and sparse operator gold.\n\nAnti-goals: no p-values or sampling CI on enumeration-exact census counts; no bootstrap repair of\nmissing capture/parser bias; no auto-injected findings; no dashboard-first mechanism; no causal\nclaim without assignment/exposure; no universal JudgeSpec or receipt table. Exactness is always\nframe-exact, never an unqualified population claim.","acceptance_criteria":"1. Every child has execution-grade ACs and consumes canonical definition/evaluation refs rather than\n parallel identities.\n2. An exact enumeration with incomplete capture and model-derived measurement renders all three\n facts; no sampling CI appears.\n3. Evidence circularity/staleness/expired refs block current-supported claims and cold-reader export.\n4. Experiment analysis refuses a causal result without preregistration, assignment, exposure, frame,\n exclusions, stopping, and outcome receipts from stc.\n5. Judgment aggregation preserves partial-order ambiguity and exact actor/execution-context\n calibration.\n6. Closing the program requires every child reconciled satisfied/deferred/misframed with its named\n falsification proof; design adoption alone is not implementation completion.","notes":"Full design proposal written: .agent/scratch/rigor-mechanisms-proposal-2026-07-13.md. Core frame: the archive is a POPULATION, not a sample — rigor = validity mechanisms on the provenance graph, not inference machinery. Ranked mechanisms: (A) metric:\u003chash\u003e content-addressed metric definitions [new, highest leverage — both cost fiascos were definition bugs]; (B) ratios as numerator_ref+denominator_ref derived objects; (C) pre-registration with graph-provable ordering (registered badge); (D) holdout cohorts as persistence class + planner rule; (E) standing-query alert budget/cooldowns (multiple-looks guard for rxdo.5); (F) blinded judgment view; (G) paired negative controls; (H) uncertainty only where sampling exists — NO p-values on exact counts; (I) evidence ancestry circularity/freshness walker; (J) A/B as cohort pairs. Anti-goals: no stats library, no theater, no auto-inject, no dashboards-first. Phase-2 implementation after the three rxdo lanes merge; current lane schemas confirmed forward-compatible.\nADOPTED by operator 2026-07-13 ('do adopt that rigor proposal'). This bead is now the program tracker; the ten mechanisms are materialized as children rxdo.9.1 (metric:\u003chash\u003e) through rxdo.9.10 (experiments), priorities/deps encoded per the proposal's phase-2/phase-3 split. Design doc moves to docs/design/analysis-rigor.md (PR pending). BINDING ANTI-GOALS: no general statistics library; no p-values/significance on exact population counts; no auto-injected findings; no dashboards-first — every mechanism must change what a claim looks like or when an alert fires. Core frame: the archive is a population, not a sample; rigor = validity mechanisms as properties of the provenance graph.\nPR #2818 merged: design doc adopted (docs/design/analysis-rigor.md + docs-surface registration) for the analysis-rigor program (structured calibration, ranker:\u003chash\u003e aggregation, active elicitation, judge cascades). This is program adoption only — mechanisms materialize as rxdo.9.1-.10 (Part II beads to follow), not implemented by this PR.\n\n[LEGACY FIELDS PRESERVED BY CORRECTIVE FOLLOW-UP 2026-07-13]\n\nORIGINAL DESCRIPTION:\nOperator direction (2026-07-13, while sequencing rxdo before demo work): 'we want to design some more useful abstractions/mechanisms here. maybe stats adjacent... maybe experimental methods, blinding and such.' Timeboxed design spike over the rxdo object graph deciding which rigor mechanisms become first-class: (1) STATS-ADJACENT: denominators/n as mandatory finding fields (finding.v1 already has n — extend?), uncertainty (CIs / exactness propagation from result_sets), effect sizes vs raw counts, multiple-comparisons discipline for standing queries (rxdo.5 will re-test many watched queries continuously — naive alerting = guaranteed false discoveries; consider baseline windows, FDR-style throttles, or explicit expected-drift bands); (2) EXPERIMENTAL METHODS: pre-registration (finding.expected BEFORE running — rxdo.4's expected field + rxdo.5's findings-as-tests are the hooks; e5b5 pre-registered micro-evals is prior art), BLINDING for judge flows (judge sees claim+evidence with source/model/actor identity masked until verdict — candidate-\u003ejudge lifecycle is the natural place), holdout corpora (sessions excluded from exploratory queries, reserved for confirmation), A/B over archive slices. (3) OUTPUT: a design doc ranking mechanisms by leverage/cost, which land in finding.v1 schema NOW vs post-substrate; explicit anti-goals (no stats theater — every mechanism must change a real decision). Related prior art: polylogue-e5b5, polylogue-67ac, #2783 rigor contracts (refuse ungrounded quantitative claims), insight_rigor_audit MCP tool. Sequenced AFTER the three rxdo build lanes land their substrate; the lifecycle lane must design finding.v1 so these mechanisms remain addable without schema breaks.\n\nFRAME CORRECTION 2026-07-13: earlier notes saying \"the archive is a population, not a sample\" are\nsuperseded. Enumeration may be exact over stored rows, while source-frame coverage and measurement\nvalidity remain uncertain. Use \"frame-exact under named definitions,\" not population-exact.\n\n[2026-07-14, Wave 2 merge-train independent review of PR #2888 (rxdo.9 measurement substrate primitives, merged)] MAJOR finding, survived merge (approved=True, tracked as debt not a blocker): polylogue/insights/measurement/registration.py's registration_status()/render_badge() prove only timing/epoch ORDERING and ref stability between a PreRegistration and its RegistrationEvaluation -- they do not verify the evaluation actually ran against the registered/frozen analysis definition (no hash/digest comparison between what was pre-registered and what was evaluated). A badge could read \"registered\" even if the evaluation silently diverged from the frozen spec. Needs a content-identity check (digest of the frozen analysis definition compared at evaluation time), not just ordering/ref checks, before this substrate can be trusted as genuine pre-registration evidence.\nPriority calibration 2026-07-15: P2 to P3. This remains part of the full project ambition, but it is a sequenced demo, experiment, governed analytic extension, or evaluation layer rather than a present failure of archive truth, bounded queryability, durability, or source fidelity. Priority is urgency, not deletion or scope reduction; horizon is unchanged.\nVERDICT: LIVE — this is the program-tracker epic for the analysis-rigor mechanisms (children rxdo.9.1-.9.15). At least 7 of 15 children remain open (9.1, 9.4, 9.6, 9.7, 9.8, 9.10, 9.12), including 9.1 (metric:\u003chash\u003e canonical identity, PR #2888 open-not-merged per its notes) and 9.10 (experiments), both load-bearing per this bead's own AC #1/#4. Clearly not stale — substantial open child work remains. — evidence: bd show polylogue-rxdo.9.{1..15} --json status field: 7 open, 8 closed.","status":"open","priority":3,"issue_type":"spike","owner":"ezo.dev@gmail.com","created_at":"2026-07-13T00:26:08Z","created_by":"Sinity","updated_at":"2026-07-31T05:48:00Z","labels":["area:substrate","delivery:C-read-evidence-contract","horizon:mid","lane:read-contracts","tech-tree"],"dependencies":[{"issue_id":"polylogue-rxdo.9","depends_on_id":"polylogue-rxdo","type":"parent-child","created_at":"2026-07-13T02:26:08Z","created_by":"Sinity","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/docs/cli-reference.md b/docs/cli-reference.md index c09b6a995c..9c9875113f 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -127,6 +127,10 @@ Options: Sort by field --reverse Reverse sort order --sample INTEGER Random sample of N sessions + --root / --no-root Only top-level sessions (--root) or only + subagent/branch children (--no-root). Unset + (default) selects both, unfiltered by + structure. -o, --output TEXT Output destinations: browser, clipboard, stdout (comma-separated) --json Shortcut for --format json. Disables color diff --git a/docs/search.md b/docs/search.md index 58dd10211b..ec33c68716 100644 --- a/docs/search.md +++ b/docs/search.md @@ -132,7 +132,7 @@ Invalid examples found in shipped teaching surfaces at the snapshot boundary: |---|---|---|---| | `polylogue/mcp/server_prompts.py:509` | `actions where session.repo:example-repo since:7d AND output:failed` | invalid query expression near column 27 | `actions where session.repo:example-repo AND session.since:7d AND output:failed` | | `polylogue/mcp/server_prompts.py:524` | `files where repo:example-repo AND path:src/mcp/server.py` | field 'repo' is not supported for file predicates | `files where session.repo:example-repo AND path:src/mcp/server.py` | -| `docs/search.md:924` | `text:css {session_id example}: refactor` | unknown query field 'text'; recognized fields: action, assistant_messages, assistant_words, authored_user_messages, authored_user_words, contains, cwd, duration_ms, has, id, lane, lineage, messages, near, origin, paste_messages, path, project, repo, session, since, system_messages, tag, thinking_messages, title, tool, tool_messages, tool_use_messages, until, user_messages, user_words, words | `contains:"css refactor"` | +| `docs/search.md:924` | `text:css {session_id example}: refactor` | unknown query field 'text'; recognized fields: action, assistant_messages, assistant_words, authored_user_messages, authored_user_words, contains, cwd, duration_ms, has, id, lane, lineage, messages, near, origin, paste_messages, path, project, repo, root, session, since, system_messages, tag, thinking_messages, title, tool, tool_messages, tool_use_messages, until, user_messages, user_words, words | `contains:"css refactor"` | Machine clients can request parser-gated positives with MCP/CLI `query_completions(kind="example")` and real diagnostics/corrections with `query_completions(kind="error")`. The query capability resource carries corpus counts and the six shared semantics contracts. diff --git a/polylogue/archive/query/archive_execution.py b/polylogue/archive/query/archive_execution.py index 4263ccec46..d4193edfac 100644 --- a/polylogue/archive/query/archive_execution.py +++ b/polylogue/archive/query/archive_execution.py @@ -192,6 +192,7 @@ def _summary_to_domain(summary: ArchiveSessionSummary) -> SessionSummary: provider_project_ref=summary.provider_project_ref, message_count=summary.message_count, tags_m2m=summary.tags, + parent_id=SessionId(summary.parent_id) if summary.parent_id else None, ) diff --git a/polylogue/archive/query/discovery.py b/polylogue/archive/query/discovery.py index d4276a7e42..db92ad2e6d 100644 --- a/polylogue/archive/query/discovery.py +++ b/polylogue/archive/query/discovery.py @@ -1399,7 +1399,7 @@ def _example( diagnostic=( "unknown query field 'text'; recognized fields: action, assistant_messages, assistant_words, " "authored_user_messages, authored_user_words, contains, cwd, duration_ms, has, id, lane, lineage, " - "messages, near, origin, paste_messages, path, project, repo, session, since, system_messages, tag, " + "messages, near, origin, paste_messages, path, project, repo, root, session, since, system_messages, tag, " "thinking_messages, title, tool, tool_messages, tool_use_messages, until, user_messages, user_words, words" ), field="text", diff --git a/polylogue/archive/query/expression.py b/polylogue/archive/query/expression.py index d0222f02af..837600e233 100644 --- a/polylogue/archive/query/expression.py +++ b/polylogue/archive/query/expression.py @@ -156,8 +156,10 @@ ) from polylogue.archive.query.spec import ( QUERY_ACTION_TYPES, + QuerySpecError, SessionQuerySpec, normalize_retrieval_lane, + optional_bool, ) from polylogue.core.enums import Origin from polylogue.core.errors import PolylogueError @@ -3024,6 +3026,7 @@ class _SpecAccumulator: max_messages: int | None = None min_words: int | None = None max_words: int | None = None + root: bool | None = None def apply_token(self, tok: _LexToken) -> None: """Apply one token to the accumulator.""" @@ -3250,6 +3253,21 @@ def apply_token(self, tok: _LexToken) -> None: field="lane", ) from exc + elif fname == "root": + if tok.negated: + raise ExpressionCompileError( + "use root:false instead of -root: to select non-root (subagent/branch) sessions", + field=fname, + ) + if values: + try: + self.root = optional_bool("root", values[-1]) + except QuerySpecError as exc: + raise ExpressionCompileError( + f"invalid root value {values[-1]!r}; expected root:true or root:false", + field="root", + ) from exc + elif fname in COUNT_QUERY_FIELD_REGISTRY or fname in NUMERIC_QUERY_FIELD_REGISTRY: # Already handled via _CountToken; field:value form without op is an error raise ExpressionCompileError( @@ -3295,6 +3313,7 @@ def to_spec(self) -> SessionQuerySpec: max_messages=self.max_messages, min_words=self.min_words, max_words=self.max_words, + root=self.root, ) def merge_from_spec(self, other: SessionQuerySpec) -> None: @@ -3344,6 +3363,8 @@ def merge_from_spec(self, other: SessionQuerySpec) -> None: self.min_words = other.min_words if other.max_words is not None: self.max_words = other.max_words + if other.root is not None: + self.root = other.root # --------------------------------------------------------------------------- diff --git a/polylogue/archive/query/fields.py b/polylogue/archive/query/fields.py index 3997a70f3f..a589e86618 100644 --- a/polylogue/archive/query/fields.py +++ b/polylogue/archive/query/fields.py @@ -734,8 +734,11 @@ def sql_plan_value(self, plan: object) -> object: ), QueryFieldDescriptor( name="root", + spec_attr="root", plan_attr="root", + spec_active=_not_none, plan_active=_not_none, + spec_description=lambda value: "root" if value is True else "not root", plan_description=lambda value: "root" if value is True else "not root", requires_post_filter=True, blocks_sql_count=True, diff --git a/polylogue/archive/query/metadata.py b/polylogue/archive/query/metadata.py index 4d7420448d..3a491b27f9 100644 --- a/polylogue/archive/query/metadata.py +++ b/polylogue/archive/query/metadata.py @@ -133,6 +133,16 @@ class QueryPipelineStageInfo: "negatable": "no", "example": "title:refactor", }, + "root": { + "description": ( + "Filter by top-level-vs-child session structure. root:true keeps only " + "top-level sessions; root:false keeps only subagent/branch children. " + "Unset (default) selects both." + ), + "spec_field": "root", + "negatable": "no", + "example": "root:true", + }, "since": { "description": "Filter sessions after date (ISO or relative: 7d, 2w)", "spec_field": "since", diff --git a/polylogue/archive/query/spec.py b/polylogue/archive/query/spec.py index 8a326bb6d5..0910986d53 100644 --- a/polylogue/archive/query/spec.py +++ b/polylogue/archive/query/spec.py @@ -197,6 +197,28 @@ def optional_int(value: object) -> int | None: return int(str(value)) +def optional_bool(field: str, value: object) -> bool | None: + """Parse a tri-state boolean param: ``None`` means "unset", not ``False``. + + Accepts native ``bool`` (from Click ``--flag/--no-flag`` pairs), and the + string forms ``true``/``false`` (case-insensitive, as used by the ``root:`` + query-DSL field and JSON/MCP params). *field* names the caller's param for + the raised error. + """ + if value is None: + return None + if isinstance(value, bool): + return value + text = str(value).strip().lower() + if text == "": + return None + if text in {"true", "1", "yes"}: + return True + if text in {"false", "0", "no"}: + return False + raise QuerySpecError(field, str(value)) + + # Set of all recognized query-spec parameter names (drives strict-param mode). _RECOGNIZED_PARAMS: frozenset[str] = frozenset( { @@ -241,6 +263,7 @@ def optional_int(value: object) -> int | None: "message_type", "offset", "cursor", + "root", } ) @@ -359,6 +382,7 @@ def build_query_spec_from_params( message_type=optional_message_type(params.get("message_type")), offset=optional_int(params.get("offset")) or 0, cursor=optional_text(params.get("cursor")), + root=optional_bool("root", params.get("root")), ) @@ -425,6 +449,7 @@ def query_spec_to_plan( offset=spec.offset, cursor=spec.cursor, boolean_predicate=spec.boolean_predicate, + root=spec.root, vector_provider=vector_provider, ) if spec.latest: @@ -485,6 +510,11 @@ class SessionQuerySpec: offset: int = 0 cursor: str | None = None boolean_predicate: QueryPredicate | None = None + #: Restrict to top-level sessions (``True``) or subagent/branch children + #: only (``False``); ``None`` (the default) selects both, unchanged from + #: historical behavior. See ``root:`` in the query DSL and ``--root/--no-root`` + #: on the CLI (polylogue-oqib). + root: bool | None = None #: Canonical query units to attach to each selected session as a #: post-selection projection (the DSL ``with `` clause). This is a #: projection, not a filter/sort/limit, so it is deliberately absent from diff --git a/polylogue/cli/archive_query.py b/polylogue/cli/archive_query.py index 2de8bf49c0..e5fd965d4c 100644 --- a/polylogue/cli/archive_query.py +++ b/polylogue/cli/archive_query.py @@ -125,6 +125,7 @@ class _ArchiveFilterKwargs(TypedDict): since_ms: int | None until_ms: int | None since_session_id: str | None + root: bool | None boolean_predicate: NotRequired[QueryPredicate] @@ -304,6 +305,7 @@ def _execute_archive_query_stdout(env: AppEnv, request: RootModeRequest) -> None "since_ms": since_ms, "until_ms": until_ms, "since_session_id": since_session_id, + "root": compiled_spec.root, } if compiled_spec.boolean_predicate is not None: filter_kwargs["boolean_predicate"] = compiled_spec.boolean_predicate diff --git a/polylogue/cli/click_app.py b/polylogue/cli/click_app.py index f912611dd9..53fdb94614 100644 --- a/polylogue/cli/click_app.py +++ b/polylogue/cli/click_app.py @@ -395,6 +395,7 @@ def cli( sort: str | None, reverse: bool, sample: int | None, + root: bool | None, # Output output: str | None, output_format: str | None, diff --git a/polylogue/cli/click_option_groups.py b/polylogue/cli/click_option_groups.py index aa57bf7009..7f1f7c4ac5 100644 --- a/polylogue/cli/click_option_groups.py +++ b/polylogue/cli/click_option_groups.py @@ -275,6 +275,15 @@ def _validate_origin_tokens( ), click.option("--reverse", is_flag=True, help="Reverse sort order"), click.option("--sample", type=int, help="Random sample of N sessions"), + click.option( + "--root/--no-root", + "root", + default=None, + help=( + "Only top-level sessions (--root) or only subagent/branch children " + "(--no-root). Unset (default) selects both, unfiltered by structure." + ), + ), ) OUTPUT_OPTION_DECORATORS: tuple[Callable[[ClickCallable], ClickCallable], ...] = ( diff --git a/polylogue/insights/correlation_view.py b/polylogue/insights/correlation_view.py index 48d796816a..e9c63e87a3 100644 --- a/polylogue/insights/correlation_view.py +++ b/polylogue/insights/correlation_view.py @@ -135,7 +135,7 @@ def _print_otlp_evidence(env: AppEnv, session_id: str, output_format: str | None def _enrich_with_github_api(result: SessionCorrelationResult) -> SessionCorrelationResult: """Cross-reference issue/PR refs against the GitHub API via gh CLI.""" - from polylogue.insights.session_commit import GitHubRef + from polylogue.insights.session_commit import GitHubRef, SessionCorrelationResult all_refs: list[tuple[GitHubRef, str]] = [] for ref in result.issue_refs: diff --git a/polylogue/storage/sqlite/archive_tiers/archive.py b/polylogue/storage/sqlite/archive_tiers/archive.py index c46537d3d9..a04a4d2153 100644 --- a/polylogue/storage/sqlite/archive_tiers/archive.py +++ b/polylogue/storage/sqlite/archive_tiers/archive.py @@ -371,6 +371,7 @@ class ArchiveSessionSummary: message_count: int word_count: int tags: tuple[str, ...] + parent_id: str | None = None session_kind: str = "standard" reported_duration_ms: int | None = None tool_use_count: int = 0 @@ -3555,6 +3556,7 @@ def read_summary(self, session_id: str) -> ArchiveSessionSummary: row = self._conn.execute( f""" SELECT s.session_id, s.native_id, s.origin, s.title, s.created_at_ms, s.updated_at_ms, + s.parent_session_id, s.session_kind, s.message_count, s.word_count, s.reported_duration_ms, s.tool_use_count, s.thinking_count, s.paste_count, @@ -5378,6 +5380,7 @@ def count_sessions( until_ms: int | None = None, since_session_id: str | None = None, boolean_predicate: QueryPredicate | None = None, + root: bool | None = None, ) -> int: """Count sessions in the archive index.""" where, params = _session_filter_clause( @@ -5411,6 +5414,7 @@ def count_sessions( since_ms=since_ms, until_ms=until_ms, boolean_predicate=boolean_predicate, + root=root, tags_relation=self._tags_relation, ) where, params = _with_since_session_filter(self._conn, where, params, "s", since_session_id=since_session_id) @@ -5849,6 +5853,7 @@ def list_summaries( until_ms: int | None = None, since_session_id: str | None = None, boolean_predicate: QueryPredicate | None = None, + root: bool | None = None, sample: bool = False, sort: str | None = None, reverse: bool = False, @@ -5885,6 +5890,7 @@ def list_summaries( since_ms=since_ms, until_ms=until_ms, boolean_predicate=boolean_predicate, + root=root, tags_relation=self._tags_relation, ) where, params = _with_since_session_filter(self._conn, where, params, "s", since_session_id=since_session_id) @@ -5900,6 +5906,7 @@ def list_summaries( rows = self._conn.execute( f""" SELECT s.session_id, s.native_id, s.origin, s.title, s.created_at_ms, s.updated_at_ms, + s.parent_session_id, s.session_kind, s.message_count, s.word_count, s.reported_duration_ms, s.tool_use_count, s.thinking_count, s.paste_count, @@ -5973,6 +5980,7 @@ def search_summaries( until_ms: int | None = None, since_session_id: str | None = None, boolean_predicate: QueryPredicate | None = None, + root: bool | None = None, ) -> list[ArchiveSessionSearchHit]: """Search archive block text and return session-level hits with snippets.""" match_query = normalize_fts5_query(query) @@ -6016,6 +6024,7 @@ def search_summaries( since_ms=since_ms, until_ms=until_ms, boolean_predicate=boolean_predicate, + root=root, tags_relation=self._tags_relation, prefix="AND", ) @@ -6101,6 +6110,7 @@ def count_search_sessions( until_ms: int | None = None, since_session_id: str | None = None, boolean_predicate: QueryPredicate | None = None, + root: bool | None = None, ) -> int: """Count distinct sessions matching the archive block FTS search.""" match_query = normalize_fts5_query(query) @@ -6137,6 +6147,7 @@ def count_search_sessions( since_ms=since_ms, until_ms=until_ms, boolean_predicate=boolean_predicate, + root=root, tags_relation=self._tags_relation, prefix="AND", ) @@ -6201,6 +6212,7 @@ def search_session_ids( until_ms: int | None = None, since_session_id: str | None = None, boolean_predicate: QueryPredicate | None = None, + root: bool | None = None, ) -> tuple[str, ...]: """Return distinct sessions matching the archive block FTS search.""" match_query = normalize_fts5_query(query) @@ -6238,6 +6250,7 @@ def search_session_ids( since_ms=since_ms, until_ms=until_ms, boolean_predicate=boolean_predicate, + root=root, tags_relation=self._tags_relation, prefix="AND", ) @@ -6306,6 +6319,7 @@ def semantic_summaries( until_ms: int | None = None, since_session_id: str | None = None, boolean_predicate: QueryPredicate | None = None, + root: bool | None = None, ) -> list[ArchiveSessionSearchHit]: """Resolve vector-ranked message ids into filtered session-level hits.""" if not scored_message_ids: @@ -6342,6 +6356,7 @@ def semantic_summaries( since_ms=since_ms, until_ms=until_ms, boolean_predicate=boolean_predicate, + root=root, tags_relation=self._tags_relation, ) where, params = _with_since_session_filter(self._conn, where, params, "s", since_session_id=since_session_id) @@ -8070,6 +8085,7 @@ def stats( until_ms: int | None = None, since_session_id: str | None = None, session_ids: tuple[str, ...] = (), + root: bool | None = None, ) -> ArchiveStats: """Return archive-level stats from filtered archive index sessions.""" where, params = _session_filter_clause( @@ -8102,6 +8118,7 @@ def stats( max_words=max_words, since_ms=since_ms, until_ms=until_ms, + root=root, tags_relation=self._tags_relation, ) where, params = _with_since_session_filter(self._conn, where, params, "s", since_session_id=since_session_id) @@ -8251,6 +8268,7 @@ def stats_by( until_ms: int | None = None, since_session_id: str | None = None, session_ids: tuple[str, ...] = (), + root: bool | None = None, ) -> dict[str, int]: """Return filtered session counts grouped by a archive dimension.""" where, params = _session_filter_clause( @@ -8283,6 +8301,7 @@ def stats_by( max_words=max_words, since_ms=since_ms, until_ms=until_ms, + root=root, tags_relation=self._tags_relation, ) where, params = _with_since_session_filter(self._conn, where, params, "s", since_session_id=since_session_id) @@ -8338,12 +8357,24 @@ def row_int(key: str) -> int: provider_title=None, ) title_source = "path" + parent_id: str | None + try: + raw_parent_id = row["parent_session_id"] + except IndexError: + # Not every caller's SELECT projects parent_session_id (e.g. rows built + # for contexts that never need root/child filtering); treat absence as + # unknown rather than raising, matching row_int's IndexError handling + # above for other optional columns. + parent_id = None + else: + parent_id = str(raw_parent_id) if raw_parent_id else None return ArchiveSessionSummary( session_id=session_id, native_id=str(row["native_id"]), origin=origin, title=title, title_source=title_source, + parent_id=parent_id, title_ref=str(row["title_ref"]) if row["title_ref"] is not None else None, title_confidence=(float(row["title_confidence"]) if row["title_confidence"] is not None else None), session_kind=str(row["session_kind"] or "standard"), @@ -10239,6 +10270,7 @@ def _session_filter_clause( since_ms: int | None = None, until_ms: int | None = None, boolean_predicate: QueryPredicate | None = None, + root: bool | None = None, tags_relation: str = "session_tags", prefix: str = "WHERE", ) -> tuple[str, list[object]]: @@ -10494,6 +10526,10 @@ def _session_filter_clause( if until_ms is not None: clauses.append(f"COALESCE({table_alias}.updated_at_ms, {table_alias}.created_at_ms) <= ?") params.append(until_ms) + if root is True: + clauses.append(f"{table_alias}.parent_session_id IS NULL") + elif root is False: + clauses.append(f"{table_alias}.parent_session_id IS NOT NULL") if boolean_predicate is not None: boolean_clause, boolean_params = _boolean_predicate_clause( table_alias, diff --git a/tests/unit/cli/test_correlate_view.py b/tests/unit/cli/test_correlate_view.py index d53c5a37c8..c05542a29d 100644 --- a/tests/unit/cli/test_correlate_view.py +++ b/tests/unit/cli/test_correlate_view.py @@ -84,6 +84,28 @@ def test_run_correlation_view_missing_session_exits() -> None: env.ui.error.assert_called_once() +def test_run_correlation_view_github_enrichment_does_not_crash() -> None: + """Regression: ``_enrich_with_github_api`` used ``SessionCorrelationResult`` + at runtime while only importing it under ``TYPE_CHECKING`` -- the default + ``github_api=True`` path raised ``NameError`` on every session carrying + any issue/PR ref, silently breaking ``read --view correlation`` for the + live archive (verified against /realm/db/polylogue/index.db).""" + env = MagicMock() + env.polylogue.get_session = AsyncMock(return_value=_session()) + env.polylogue.repository.get_session_refs = AsyncMock(return_value=[]) + + with ( + patch("polylogue.insights.session_commit.build_correlation_result", return_value=_result()), + patch("subprocess.run", side_effect=FileNotFoundError("gh not installed")), + ): + run_correlation_view(env, session_id="target", output_format="json", github_api=True) + + printed = "".join(str(call.args[0]) for call in env.ui.console.print.call_args_list if call.args) + payload = json.loads(printed) + assert payload["pr_refs"][0]["number"] == 2149 + assert payload["issue_refs"][0]["number"] == 1845 + + def test_run_correlation_view_plain_renders_window() -> None: env = MagicMock() env.polylogue.get_session = AsyncMock(return_value=_session()) diff --git a/tests/unit/cli/test_query_exec_laws.py b/tests/unit/cli/test_query_exec_laws.py index 294a1ae595..4756d63f8f 100644 --- a/tests/unit/cli/test_query_exec_laws.py +++ b/tests/unit/cli/test_query_exec_laws.py @@ -426,6 +426,7 @@ def list_summaries( until_ms: int | None, since_session_id: str | None, sample: bool, + root: bool | None = None, ) -> list[ArchiveSessionSummary]: assert limit == 3 assert offset == 0 @@ -1559,6 +1560,7 @@ def search_summaries( since_ms: int | None, until_ms: int | None, since_session_id: str | None, + root: bool | None = None, ) -> list[ArchiveSessionSearchHit]: assert query == "needle" assert limit == 6 diff --git a/tests/unit/cli/test_query_expression.py b/tests/unit/cli/test_query_expression.py index dc80b210c4..d13367635b 100644 --- a/tests/unit/cli/test_query_expression.py +++ b/tests/unit/cli/test_query_expression.py @@ -6078,6 +6078,7 @@ class TestFieldRegistry: QueryFieldPredicate(field="assistant_words", values=("500",), op=">="), ), "lane": ("lane:dialogue", "retrieval_lane", "dialogue"), + "root": ("root:true", "root", True), "lineage": ( "lineage:id:chatgpt-export:ext-root", "boolean_predicate", diff --git a/tests/unit/storage/test_archive_tiers_archive.py b/tests/unit/storage/test_archive_tiers_archive.py index 2e9c340550..2ce17a83de 100644 --- a/tests/unit/storage/test_archive_tiers_archive.py +++ b/tests/unit/storage/test_archive_tiers_archive.py @@ -1752,3 +1752,86 @@ def test_list_archive_debt_insights_correct_while_main_connection_holds_transact assert facade._conn.in_transaction finally: facade._conn.rollback() + + +def test_root_filter_partitions_top_level_and_subagent_sessions(tmp_path: Path) -> None: + """``root`` filters/counts sessions by whether they have a parent (polylogue-oqib). + + Regression coverage for two bugs found while wiring the ``root:`` query + field end to end: (1) ``ArchiveStore.list_summaries``/``search_summaries``/ + ``count_sessions``/``count_search_sessions``/``search_session_ids``/ + ``semantic_summaries``/``stats``/``stats_by`` never accepted a ``root`` + kwarg or pushed it into ``_session_filter_clause`` at all, so a top-level- + only query returned everything unfiltered; (2) even where the plan-level + ``is_root`` post-filter existed, ``ArchiveSessionSummary``/``SessionSummary`` + never carried ``parent_id`` from ``sessions.parent_session_id`` (the SELECT + never projected the column and the row builder never read it), so every + summary row silently reported ``is_root=True`` regardless of its actual + parent. + """ + parent = ParsedSession( + source_name=Provider.CLAUDE_CODE, + provider_session_id="root-parent", + title="Parent session", + messages=[ + ParsedMessage( + provider_message_id="m1", + role=Role.USER, + blocks=[ParsedContentBlock(type=BlockType.TEXT, text="alpha parent token")], + ) + ], + ) + child = ParsedSession( + source_name=Provider.CLAUDE_CODE, + provider_session_id="root-parent:agent-child", + title="Subagent session", + messages=[ + ParsedMessage( + provider_message_id="m1", + role=Role.USER, + blocks=[ParsedContentBlock(type=BlockType.TEXT, text="alpha child token")], + ) + ], + ) + root = tmp_path / "archive" + with ArchiveStore(root) as facade: + parent_id = facade.write_parsed(parent) + child_id = facade.write_parsed(child) + facade._conn.execute( + "UPDATE sessions SET parent_session_id = ? WHERE session_id = ?", + (parent_id, child_id), + ) + facade._conn.commit() + + with ArchiveStore.open_existing(root) as facade: + assert facade.count_sessions() == 2 + assert facade.count_sessions(root=True) == 1 + assert facade.count_sessions(root=False) == 1 + + root_summaries = facade.list_summaries(limit=5, root=True) + child_summaries = facade.list_summaries(limit=5, root=False) + all_summaries = facade.list_summaries(limit=5) + + root_hits = facade.search_summaries("alpha", limit=5, root=True) + child_hits = facade.search_summaries("alpha", limit=5, root=False) + assert facade.count_search_sessions("alpha", root=True) == 1 + assert facade.count_search_sessions("alpha", root=False) == 1 + assert facade.search_session_ids("alpha", root=True) == (parent_id,) + assert facade.search_session_ids("alpha", root=False) == (child_id,) + + root_stats = facade.stats(root=True) + child_stats = facade.stats(root=False) + root_stats_by_origin = facade.stats_by("origin", root=True) + + assert [summary.session_id for summary in root_summaries] == [parent_id] + assert [summary.session_id for summary in child_summaries] == [child_id] + assert {summary.session_id for summary in all_summaries} == {parent_id, child_id} + assert root_summaries[0].parent_id is None + assert child_summaries[0].parent_id == parent_id + + assert [hit.session_id for hit in root_hits] == [parent_id] + assert [hit.session_id for hit in child_hits] == [child_id] + + assert root_stats.total_sessions == 1 + assert child_stats.total_sessions == 1 + assert root_stats_by_origin == {"claude-code-session": 1}