42. Unify the session extractors¶
Date: 2026-08-27
Status¶
Accepted
Supersedes 36. The two session extractors stay forked, for now
Context¶
ADR 0036 was accepted 2026-08-25. This ADR reverses it within weeks — worth saying plainly, and worth explaining why that is a change of decision, not a change of mind.
0036's own revisit trigger had already fired when it was written. 0036 named three conditions that would flip its "leave the shared-core question open" stance and demand a follow-up ADR. Trigger 2 — "any future port that itself ships an un-carried fix" — is not hypothetical: 0036's own Context section names #1994, the port that motivated writing 0036 in the first place, as already an instance of it. #1994 itself left four of #1991's fixes behind, caught only by review. 0036 was accepted with its own revisit condition standing, not merely at risk of it.
#2029 crossed the fork again, independently, while 0036 was still
current. Real per-agent context_tokens (#2010) landed only in the
shipped extract_session_report.py, leaving session_extract.py without
the field entirely — the identical duplication-cost shape 0036 was written
to characterize, recurring within the same short window 0036 was supposed
to hold the line for.
0036 explicitly left the shared-core question open, and said the evidence pointed the other way. Its own "Boundary of the evidence" section: "this ADR establishes only that unifying the whole extractors is wrong, and that porting-by-hand has failed twice. It does not establish that a shared classification core is unworkable — the review showed the opposite is plausible." This ADR does not merely answer that open question — it goes further than what 0036 contemplated: full unification into one script with two profiles, not just a shared classification-core module sitting beside two still-separate scripts. That wider scope is the part that genuinely supersedes 0036 rather than settling the question 0036 deliberately left for later.
What was measured, since 0036 noted nobody had. 0036: "Nor has anyone
measured the cost of building one against the cost of the next missed
port." Epic #2040's own slices (#2042–#2045) measured it before unifying:
17 shared function names existed across the two scripts, 2 with
byte-identical bodies; among the diverged ones, _is_subagent_transcript
scored 0.22 similarity and _accumulate_token_signals scored 0.10 — two
names, two very different implementations, exactly the silent-drift shape
0036 worried about. Nine modules across the repo read usage blocks, using
four distinct null-handling idioms between them. That is the cost side of
the ledger 0036 said was missing; #2050 (folding the three remaining
transcript parsers onto session_log.records) is this epic's answer to the
other side — the cost of the next missed port, paid down rather than
deferred again.
Decision¶
Unify scripts/session_extract.py and
plugins/dev-team/scripts/extract_session_report.py into one script,
plugins/dev-team/scripts/session_report.py, selected by --profile
maintainer|downstream. Both profiles ship together, under
plugins/dev-team/scripts/, closing #1779 at the root: the maintainer
profile — previously monorepo-only tooling, unreachable from an installed
plugin cache — is now shipped and runs from any install (#2046/#2047,
verified end-to-end against a simulated no-source-checkout install, not
merely assumed).
What 0036 got right is preserved, not erased. Divergence-by-consumer is
real and survives as profiles, not as forced erasure of the difference:
pricing, cost, rollup, --sync-out, --correlate, and --escalate remain
maintainer-profile-only, exactly as 0036 decided they should. The two
profiles' outputs are byte-equal to their predecessors' goldens modulo one
documented, deliberate difference: the schema-version bump (see below).
Two genuinely monorepo-only pieces of tooling that /session-review still
depends on — scripts/telemetry-sync.sh (--cross-machine sync) and
scripts/eval_rawlog.py (the raw-log semantic tier it gates) — are left
exactly where 0036's own ADR 0032 Category 2 reasoning puts them: not
shipped, both already opt-in and off by default.
Schema eras stay tellable apart, per 0036's own hard-won lesson. Both
profiles bump to v3 (session-digest/v3, downstream-session-report/v3)
— a version label on the new unified entry point, not a retroactive
rewrite of history. SYNC_SCHEMAS is the one exported constant naming
every schema a reader accepts (v1/v2/v3); no call site literal-matches
a schema string. This directly targets 0036's own recorded failure mode:
slim_record() kept stamping v1 onto v2-basis numbers, and
eval_rawlog.py exact-matched the old sync schema and silently returned an
empty ranking when the schema moved. eval_rawlog.py now imports
SYNC_SCHEMAS from session_report.py rather than carrying its own
literal, and a dedicated test
(test_v3_digest_records_are_ranked_not_silently_dropped_2047) asserts it
finds records in a v3 digest, not merely that it doesn't raise.
The classification core moved to a shared package, plugins/dev-team/
scripts/lib/session_log/ (classify.py, discovery.py, records.py,
signals.py, redact.py, epic #2040's #2042–#2045), exactly the shape 0036
said the evidence made plausible. session_report.py is the one sanctioned
entry point composing those primitives — not a third independent
reimplementation, which is the failure mode
skills/code-review/scripts/repo_invariants.py's
check_transcript_parsing_confined_to_session_log (#2048) now guards
against mechanically: no module outside session_log/ may parse a
transcript record or a usage block, session_report.py itself excepted
as the sanctioned composer, everything else allowlisted with a stated
reason or migrated.
Both old scripts are retired (#2048) once every real consumer was cut over (#2047) and their own test coverage was migrated onto the new script rather than deleted.
Consequences¶
/session-review's core extraction (Extract, Analyze, Suggest, Persist trend) no longer requires this monorepo's own dev checkout — the actual #1779 fix, not merely a guard around the absence of one.- A fix found in the shared classification core now has exactly one home
to land in, closing the two-scripts-drift class 0036 was written to
manage by discipline rather than mechanism.
check_transcript_parsing_ confined_to_session_logis the standing mechanism that replaces that discipline going forward. - Three transcript parsers still exist outside
session_log/(hooks/lib/cost_meter.py,hooks/context_ceiling_guard.py,scripts/measure_full_file_duplication.py), each carrying its own reason inrepo_invariants._TRANSCRIPT_PARSING_ALLOWLISTand each named for migration in #2050 — the epic is not yet fully discharged by this ADR alone. - A downstream user's
--profile downstreamoutput and a maintainer's--profile maintaineroutput now come from the same binary, so a bug fixed in one profile's shared logic (session_log/) is fixed in both simultaneously — the class of risk 0036 accepted as a known cost (a fix landing in one script and not the other) is structurally closed for everything routed throughsession_log/, and visibly tracked via the allowlist for what is not yet routed through it.
Reconciliation with standing ADRs¶
- ADR 0032 (shipped-script
path resolution):
session_report.pyis Category 1 (shipped and portable) in both profiles — every path it touches resolves relative to its own location insideplugins/dev-team/. The reclassification ADR 0032 anticipated ("INTENTIONAL_BARE_INVOCATIONmust be reclassified asKNOWN_BARE_INVOCATION… once the script ships") is discharged, not triggered as a defect: #2047 moved every/session-reviewreference to a resolved${CLAUDE_PLUGIN_ROOT}/scripts/session_report.pyform in the same slice that shipped the script, so the defect state was never observed live.tests/repo/test_shipped_script_refs.py'sESCAPE_ALLOWLISTentry forskills/session-review/is kept, not removed — the two remaining monorepo-only helpers still need it. - ADR 0014 / ADR
0015 (stdlib-only, cross-OS Python):
session_report.pyandsession_log/are stdlib-only Python, consistent throughout. - ADR 0031 (3.10 floor):
session_report.pydeliberately usestimezone.utc, neverdatetime.UTC(3.11+) — the one genuine floor-sensitive difference from its monorepo-only predecessor, verified by actually running the floor-slice tests under a real 3.10 interpreter (uv run --python 3.10), not inferred from a grep.
References¶
- Issues: #1779 (root cause closed), #1990/#1991/#1994 (the defect class 0036 was written to manage), #2010/#2029 (the class recurring while 0036 was current), #2040 (epic), #2042–#2050 (slices)
- Files:
plugins/dev-team/scripts/session_report.py,plugins/dev-team/scripts/lib/session_log/,plugins/dev-team/skills/code-review/scripts/repo_invariants.py - Superseded: ADR 0036
- Reconciled: ADR 0032, ADR 0014, ADR 0015, ADR 0031