feat(lineage): estimate channel-fusion weights psychometrically (ADR 0145) - #499
Merged
seonghobae merged 1 commit intoAug 23, 2026
Conversation
…0145)
The convex weights reconstruct() fuses its evidence channels with were
hand-picked constants justified only by a code comment. ADR 0145
replaces assertion with estimation: channels are items observing the
latent "these posts are related" trait, scored candidate pairs are
respondents nested in reconstruction groups, and fast-mlsirm's
multilevel 2PL (MLS2PLM, mmle estimator with cluster random intercepts
per Fox & Glas 2001) estimates per-channel discriminations -- the
information-optimal weights per Birnbaum (1968). Fail-closed at every
step: no fast_mlsirm, too small a sample, a degenerate channel, or a
non-finite estimate produces no weights and rebuilds keep the (now
explicitly labeled) fallback constants; persisted weights are used only
on an exact active-channel-set match, never mixed.
Also lands the grouping-evidence backfill this estimation depends on:
the zcrht811 import stuffed per-row-unique identifiers into BOTH
grouping columns (thread_group_key = own record GUID on 43,811/43,839
rows; secondary_grouping_key = own document number), so _group_by saw
~43k singleton groups and no channel ever scored a real candidate pair
-- Event Lineage was silently non-functional corpus-wide.
backfill_thread_group_keys.py clears the placeholder signature rows to
'' (migration 0002's designed "no signal" value; grouping falls back to
process-unit/corporate-entity candidate pools) and routes
source_project_code to secondary_grouping_key -- the secondary-key
channel's documented signal ("e.g. project code") -- as fused evidence,
never a hard partition that would wall related posts apart. Fails
closed when an analysis_scope_thread_group run (TEPP/report scope keys
resolve live against thread_group_key) would be orphaned.
Verified: parameter-recovery test recovers planted discrimination
ordering (strong > mid > weak) via the real fast-mlsirm fit; estimation
script fails closed against the live un-backfilled corpus (105 pairs <
200 minimum, nothing written); a100-fork rebuild regression stays
green.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015HgzNGsCxqiTaT4YuJEb5J
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This was referenced Aug 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The standing product requirement — repeated across many sessions — is that scoring weights be grounded in published measurement methodology via the org's own psychometric libraries, never hand-picked.
reconstruct()'sDEFAULT_CHANNEL_WEIGHTS(0.15/0.15/0.30/0.40) had no citation; only a code comment. This PR replaces assertion with estimation:docs/adr/0145-psychometric-channel-weight-estimation.md): channels = items observing the latent "these posts are genuinely related" trait; scored candidate pairs = respondents; reconstruction groups = multilevel nesting. Weights = normalized natural-scale discriminations from fast-mlsirm's MLS2PLM (mmle estimator with per-group cluster random intercepts — Fox & Glas, 2001), the information-optimal weighting per Birnbaum (1968); multilevel to avoid the atomistic fallacy (Robinson, 1950). Full APA 7th references in the ADR.lineageweave/channel_weight_estimation.py: fail-closed at every step —fast_mlsirmunimportable, sample < 200 pairs, a degenerate channel (never/always clears the fusion floor), or any non-finite alpha →None, and rebuilds keep the fallback constants (now labeled as ungrounded fallback, not justified default). Dichotomization happens atDEFAULT_MIN_FUSED_SCOREso the measurement model observes the same binary event the fusion decision acts on.lineage_channel_weight(+ rollback + migrate.sh whitelist + test-harness registration): persisted, provenance-bearing weights (method code, sample size, timestamp).rebuild_lineageuses them only on an exact active-channel-set match — no partial mixing of estimated and hand-picked weights; a DB without the migration is treated as "no estimate persisted", not an error.scripts/estimate_channel_weights.py: samples pairs with reconstruct's exact candidate geometry (same grouping fallback, same trailing window), fits, persists. llm channel joins in a follow-up per ADR 0145 §5.scripts/backfill_thread_group_keys.py: the estimation's prerequisite. The zcrht811 import put per-row-unique identifiers in BOTH grouping columns (thread_group_key= own record GUID on 43,811/43,839 rows;secondary_grouping_key= own document number), so_group_bysaw ~43k singleton groups and no channel ever scored a real pair — Event Lineage silently non-functional corpus-wide. The backfill clears placeholder-signature rows to''(migration 0002's designed no-signal value → process-unit/corporate-entity candidate pools) and routessource_project_codeintosecondary_grouping_key— the secondary-key channel's own documented signal ("e.g. project code") — as fused evidence, never a hard partition that would wall related posts apart (the earlier draft made exactly that mistake; caught in design review). Fails closed if ananalysis_scope_thread_grouprun (TEPP/report scope keys resolve live againstthread_group_key) would be orphaned.Verification
pytest tests/test_channel_weight_estimation.py tests/test_estimate_channel_weights_script.py tests/test_backfill_thread_group_keys.py— 14 passed, 1 honest skip.backend/tests/test_api.py::test_rebuild_lineage_recovers_the_a100_fork+ lineage-ingestion suite — green after harness migration registration.resolved_date_text×1), none in files this PR touches.Operator sequence (not run here — mass rewrite of shared data needs explicit operator action)
python scripts/backfill_thread_group_keys.py --dry-run→ review counts → run without--dry-runpython scripts/estimate_channel_weights.py --dry-run→ review weights → run without--dry-runPOST /api/lineage/rebuild🤖 Generated with Claude Code
https://claude.ai/code/session_015HgzNGsCxqiTaT4YuJEb5J