feat(promotion): phantom promotion trigger — Surfaces A+B (closes #550) - #616
Conversation
Reviewer's GuideImplements the v2.1 phantom promotion trigger (#550) by allowing speculative-origin beliefs to be promoted and by wiring an auto-promotion path from Sequence diagram for Surface_B phantom promotion via aelf_locksequenceDiagram
actor User
participant CLI as CLI_aelf_lock
participant Store as MemoryStore
participant Promotion as promotion_module
User->>CLI: run aelf lock <text>
CLI->>Store: open()
CLI->>Store: write_lock(statement)
Note over Store: lock row persisted
CLI->>Promotion: find_phantom_lock_matches(Store, statement)
Promotion->>Store: list_active_speculative_beliefs()
Store-->>Promotion: list of speculative beliefs
loop for each belief
Promotion->>Promotion: compute lock_hash
Promotion->>Promotion: _normalize_tokens(lock_text)
Promotion->>Promotion: _normalize_tokens(belief.content)
Promotion->>Promotion: _jaccard(lock_tokens, belief_tokens)
Promotion-->>CLI: matching phantom_ids (after scan)
end
loop for each phantom_id
CLI->>Promotion: promote(Store, phantom_id, SOURCE_PROMOTE_PHANTOM_LOCK_MATCH, now)
Promotion->>Store: update belief origin to user_validated
Promotion->>Store: insert audit_row(source_label=promotion:phantom_lock_match)
Store-->>Promotion: commit
Promotion-->>CLI: PromoteResult
CLI->>User: print promoted phantom: <id>
end
CLI->>Store: close()
CLI-->>User: exit code 0
Class diagram for promotion helpers and speculative_belief_listingclassDiagram
class MemoryStore {
+list_active_speculative_beliefs() Belief[]
}
class Belief {
+id str
+type str
+origin str
+valid_to str
+created_at str
+content str
+content_hash str
+corroboration_count int
}
class PromotionModule {
<<module>>
+PHANTOM_LOCK_JACCARD_THRESHOLD float
+SOURCE_PROMOTE_PHANTOM_LOCK_MATCH str
+_normalize_tokens(text str) frozenset~str~
+_jaccard(a frozenset~str~, b frozenset~str~) float
+find_phantom_lock_matches(store MemoryStore, lock_text str, jaccard_threshold float) list~str~
+promote(store MemoryStore, belief_id str, source_label str, now str) PromoteResult
}
class PromoteResult {
+already_validated bool
+audit_event_id int
}
class CLI_LockCommand {
<<function>>
+_cmd_lock(args Namespace, out object) int
}
MemoryStore "*" --> "*" Belief : returns
CLI_LockCommand --> MemoryStore : uses
CLI_LockCommand --> PromotionModule : imports
PromotionModule --> MemoryStore : depends_on
PromotionModule --> PromoteResult : returns
PromotionModule --> Belief : reads_fields
class Namespace {
+statement str
}
CLI_LockCommand --> Namespace : reads_statement
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (5)
📝 WalkthroughWalkthroughThis PR implements Surface B phantom promotion via lock-text matching. When ChangesPhantom Lock-Match Promotion
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested labels
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsGit: Failed to clone repository. Please run the 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 |
PR-size soft capThis PR is over the advisory size threshold:
Bigger PRs collide with more open work, which under the parallel-session workflow tends to produce repeated This is advisory only — nothing is blocked. If the size is intentional (large refactor, module removal, generated code), apply the |
There was a problem hiding this comment.
Hey - I've left some high level feedback:
- The
_PHANTOM_STOPWORDSset is currently hardcoded inpromotion.pyeven though the comment says it mirrorsrelationship_detector; consider centralizing this stopword list in a shared module to avoid subtle divergence over time. - The
_jaccardhelper treats two empty token sets as 1.0 similarity; if speculative beliefs or lock statements can ever normalize to empty, you may want to explicitly guard against promoting such degenerate cases or document that behavior as intentional.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The `_PHANTOM_STOPWORDS` set is currently hardcoded in `promotion.py` even though the comment says it mirrors `relationship_detector`; consider centralizing this stopword list in a shared module to avoid subtle divergence over time.
- The `_jaccard` helper treats two empty token sets as 1.0 similarity; if speculative beliefs or lock statements can ever normalize to empty, you may want to explicitly guard against promoting such degenerate cases or document that behavior as intentional.Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
|
[claim:review:Maxwell:2026-05-11T00:25:20Z] |
|
Reviewed by Maxwell. Four atomic signed commits, FF on main, all checks green, discretion clean. Approving for Observations (non-blocking)
None block merge. Adding |
|
[release:review:Maxwell:2026-05-11T00:28:05Z] |
|
merge-train: blocked one or more commits between main and The |
|
This PR is now behind Auto-rebase was removed because the bot has no signing key; rebasing as the bot strips author signatures and the |
|
[claim:review:noether:2026-05-11T00:30:12Z] |
|
Substance LGTM — flagging Things I checked:
After rebase + green CI, this is FF-mergeable. Re-flag |
|
[release:review:noether:2026-05-11T00:31:48Z] |
e0cc8cf to
2c1e198
Compare
|
Rebased on github/main. 3393 passed / 52 skipped. All 4 commits signed. Discretion grep clean. Removed attn:merge-conflict, flagged attn:review. |
|
[claim:review:faraday:2026-05-11T15:46:41Z] |
|
[claim:review:einstein:2026-05-11T15:46:45Z] |
|
[release:review:einstein:2026-05-11T15:46:50Z] |
|
[claim:review:Leibniz:2026-05-11T15:47:25Z] |
|
[claim:review:faraday:2026-05-11T23:05:54Z] |
Iterator, LOCK_USER, ORIGIN_USER_STATED, PHANTOM_LOCK_JACCARD_THRESHOLD are imported but never referenced. Removed to clear CodeQL findings 341–343 blocking merge-train on #616.
Extends promote() to treat speculative phantom beliefs as a valid promotion source (Surface A of #550). Adds SOURCE_PROMOTE_PHANTOM_LOCK_MATCH constant for Surface B callers. Updates docstring to document both surfaces. Tests cover: provenance flip, alpha/beta preservation, audit row label, custom source_label, and idempotency on the speculative path.
Adds find_phantom_lock_matches() to promotion.py — a pure function that scans active speculative beliefs for a content_hash exact match (sha256 of lock text) followed by a normalized-text Jaccard ≥ 0.9 pass. Adds list_active_speculative_beliefs() to MemoryStore to support the scan. Adds PHANTOM_LOCK_JACCARD_THRESHOLD constant (0.9) per docs/v2_phantom_promotion_trigger.md § Surface B.
After the lock write succeeds, calls find_phantom_lock_matches() and promotes each matched speculative belief via promote(..., source_label=SOURCE_PROMOTE_PHANTOM_LOCK_MATCH). Prints one "promoted phantom: <id>" line per match. Uses the same `now` timestamp as the lock write for audit-row coherence.
17 tests covering: Surface A explicit promote on a phantom, Surface B exact content_hash match, Surface B Jaccard ≥ 0.9 match, Surface B no-match (unrelated lock leaves phantom unpromoted), idempotency on both surfaces, audit_log row shape per trigger (source label, valence=0.0, created_at), and a wonder_ingest integration path.
Adds Unreleased Added entry per the owner review on PR #616. Documents both surfaces (aelf validate accepting speculative origin; aelf lock auto-promote via content_hash exact + Jaccard >= 0.9), the new public helpers (find_phantom_lock_matches, list_active_speculative_beliefs), the new audit label promotion:phantom_lock_match, idempotency-based Surface B recovery, and the explicit rejection of the count-threshold trigger per spec / #229 ratification.
Iterator, LOCK_USER, ORIGIN_USER_STATED, PHANTOM_LOCK_JACCARD_THRESHOLD are imported but never referenced. Removed to clear CodeQL findings 341–343 blocking merge-train on #616.
8a19319 to
55418a7
Compare
|
Unblock for merge-train:
New head: 55418a7. Discretion grep clean. Local pytest 69 passed + 75 xfailed against the rebased branch. Re-add 'ready-to-merge' once CI lands green. |
|
merge-train: merged 55418a7 → |
|
[release:review:faraday:2026-05-11T23:19:16Z] |
Bump pyproject version 2.1.0 → 3.0.0. uv.lock refreshed. Substrate landed across the v3.0 cut: - Wonder consolidation #542 (all sub-issues closed) - Wonder/reason agentmemory parity #645 - HRR persistence umbrella #553 (PR #714) - Type-aware compression #434 - Federation read-only mechanics #650 (scope #688, promote/demote #689, peer-aware reason #690, transport #655) - Phantom promotion #550 (PR #616), wonder dispatch #552 (PR #644) - v3.0 design decisions ratified: PHILOSOPHY #605, sentiment-hook #606, multimodel defer #607, federation read-only #661 - Eval-judge κ calibration #687 (judge-driven; bench captures deferred under attn:bench-needed) Bench-only items #152, #592, #697 remain `attn:bench-needed` and are ratified-deferred per the milestone-tracker DoD. Refs #608.
Surface B (`find_phantom_lock_matches`) shipped via #616. The skipif predicate still works correctly at runtime — `_surface_b_available()` returns True on current main, so 27 tests pass + 75 xfail per the documented edge-case fixture. Only the reason text was stale, framing the skip as "not yet on main — gated on #616 merge" when in fact the predicate is defensive scaffolding against back-revision benches that still target pre-#616 trees. Updated reason: "Surface B (find_phantom_lock_matches) not importable — shipped via #616 but the predicate stays defensive for back-revision benches against pre-#616 trees". Verified locally: 27 passed, 75 xfailed (unchanged from pre-edit). Audit row: [MED-2] in ~/.claude/handoffs/audit-2026-05-21-aelfrice-v3.3.0.md.
Surface B (`find_phantom_lock_matches`) shipped via #616. The skipif predicate still works correctly at runtime — `_surface_b_available()` returns True on current main, so 27 tests pass + 75 xfail per the documented edge-case fixture. Only the reason text was stale, framing the skip as "not yet on main — gated on #616 merge" when in fact the predicate is defensive scaffolding against back-revision benches that still target pre-#616 trees. Updated reason: "Surface B (find_phantom_lock_matches) not importable — shipped via #616 but the predicate stays defensive for back-revision benches against pre-#616 trees". Verified locally: 27 passed, 75 xfailed (unchanged from pre-edit). Audit row: [MED-2] in ~/.claude/handoffs/audit-2026-05-21-aelfrice-v3.3.0.md.
Surface B (`find_phantom_lock_matches`) shipped via #616. The skipif predicate still works correctly at runtime — `_surface_b_available()` returns True on current main, so 27 tests pass + 75 xfail per the documented edge-case fixture. Only the reason text was stale, framing the skip as "not yet on main — gated on #616 merge" when in fact the predicate is defensive scaffolding against back-revision benches that still target pre-#616 trees. Updated reason: "Surface B (find_phantom_lock_matches) not importable — shipped via #616 but the predicate stays defensive for back-revision benches against pre-#616 trees". Verified locally: 27 passed, 75 xfailed (unchanged from pre-edit). Audit row: [MED-2] in ~/.claude/handoffs/audit-2026-05-21-aelfrice-v3.3.0.md.
Surface B (`find_phantom_lock_matches`) shipped via #616. The skipif predicate still works correctly at runtime — `_surface_b_available()` returns True on current main, so 27 tests pass + 75 xfail per the documented edge-case fixture. Only the reason text was stale, framing the skip as "not yet on main — gated on #616 merge" when in fact the predicate is defensive scaffolding against back-revision benches that still target pre-#616 trees. Updated reason: "Surface B (find_phantom_lock_matches) not importable — shipped via #616 but the predicate stays defensive for back-revision benches against pre-#616 trees". Verified locally: 27 passed, 75 xfailed (unchanged from pre-edit). Audit row: [MED-2] in ~/.claude/handoffs/audit-2026-05-21-aelfrice-v3.3.0.md.
Closes #550 (C4 sub-task of wonder umbrella #542).
What ships
The phantom promotion trigger per
docs/v2_phantom_promotion_trigger.md(ratified by closed issue #229). Two explicit surfaces, no count-threshold trigger:aelf promote <id>.promote()now acceptsorigin=ORIGIN_SPECULATIVEas a valid promotion source (was previously refused alongsideuser_stated). Speculative →user_validatedwith the existingpromotion:user_validatedaudit label.aelf lock <text>auto-promotion. After a lock write succeeds,_cmd_lockscans active speculative beliefs for acontent_hashexact match (sha256 of lock text) followed by a normalized-text Jaccard ≥ 0.9 pass. Matches promote with new audit labelpromotion:phantom_lock_match. Threshold lives inPHANTOM_LOCK_JACCARD_THRESHOLD(tunable per spec § Labeled-corpus benchmark).Spec vs issue-body conflict (resolved per spec)
The #550 body lists "corroboration row count crosses gate" as a trigger. The spec doc (and #229's ratified decision) explicitly rejects this as one of the three rejected naive triggers ("conflates posterior movement with discrete origin promotion; threshold has no benchmark"). This PR implements the spec; the count-threshold path is not added.
Implementation notes
promote()commits per call, so this PR commits the lock first then each promotion. Idempotency makes this recoverable: a crash between lock and promote leaves the phantom unpromoted; re-issuing the same lock re-matches and promotes. If strict atomicity is required, that's a follow-up that refactorspromote()to take an open connection.wonder_ingestderives content_hash from constituent IDs, not from content text, so the sha256(lock_text) collision will never fire for wonder-generated phantoms. The Jaccard pass is the live path. Kept for non-wonder phantoms or future content-keyed phantoms.devalidate()flips a promoted phantom toagent_inferred, not back tospeculative. Spec § Out of scope confirms demotion is not currently planned.Acceptance check
aelf promote <phantom_id>flips speculative → user_validated, writes audit row.aelf lock <text>matching (content_hash or Jaccard ≥ 0.9) promotes the phantom, writes audit row taggedpromotion:phantom_lock_match.source_label.promote()already had the already-validated short-circuit; Surface B inherits it).Tests
tests/test_promotion.py— 5 new tests cover the speculative-source path onpromote().tests/test_phantom_promotion_trigger.py— new file, 17 end-to-end tests covering Surface A, Surface B exact + Jaccard, no-match cases, idempotency on both surfaces, and audit-row content per trigger.Summary by Sourcery
Introduce phantom promotion triggers for speculative beliefs via explicit promotion and lock-based auto-promotion surfaces.
New Features:
Tests:
Summary by CodeRabbit
Release Notes
New Features
aelf lockcommand now automatically identifies and promotes speculative beliefs matching your lock statement using exact matching and text similarity detection.Tests