Skip to content

feat(reason): compound confidence + CONTRADICTS fork on aelf reason (#658 R2) - #664

Merged
github-actions[bot] merged 4 commits into
mainfrom
feat/issue-658-r2-paths-compound-decay
May 11, 2026
Merged

feat(reason): compound confidence + CONTRADICTS fork on aelf reason (#658 R2)#664
github-actions[bot] merged 4 commits into
mainfrom
feat/issue-658-r2-paths-compound-decay

Conversation

@robotrocketscience

@robotrocketscience robotrocketscience commented May 11, 2026

Copy link
Copy Markdown
Owner

R2 of the #645 umbrella per the 2026-05-11 ratification — adds the path-centric view of a BFS expansion (compound confidence + weakest-link marker + fork-on-CONTRADICTS) on top of R1's verdict / impasses.

Surface

New module surface — src/aelfrice/reason.py

@dataclass(frozen=True)
class ConsequencePath:
    belief_ids: tuple[str, ...]            # root → leaf
    edge_kinds: tuple[str, ...]            # len == len(belief_ids) - 1
    compound_confidence: float             # ∏ posterior_mean over the trail
    weakest_link_belief_id: str            # argmin posterior_mean (deepest wins ties)
    fork_from: str | None = None           # parent path's terminal id when CONTRADICTS-forked

def derive_paths(
    seeds: list[Belief],
    hops: list[ScoredHop],
) -> list[ConsequencePath]: ...

Pure derivation — no graph traversal, no store lookups. Reconstructs trails from each hop's belief_id_trail (added to ScoredHop in commit 1) plus the posterior means of seeds and hop endpoints already in scope.

BFS surface — src/aelfrice/bfs_multihop.py

ScoredHop gains a belief_id_trail: tuple[str, ...] = () field, populated by expand_bfs as it walks. Trail length is always depth + 1; first element is the originating seed, last is the hop's endpoint. Frontier tuples carry the trail in a fifth slot. Default () keeps existing test fixtures (which construct ScoredHop directly) working.

Fork-aware classifier — src/aelfrice/reason.py::classify

def classify(
    seeds: list[Belief],
    hops: list[ScoredHop],
    store: MemoryStore,
    *,
    paths: list[ConsequencePath] | None = None,
) -> tuple[Verdict, list[Impasse]]: ...

When paths is supplied, additionally emits a TIE impasse when two CONTRADICTS-forked paths share a common parent and have compound_confidence values within CLOSE_MEAN_DELTA (0.15) of one another. Trips CONTRADICTORY per #658 acceptance. paths=None (default) preserves R1 behaviour exactly — guarded by a regression test.

CLI surface — aelf reason

  • JSON (--json) gains a paths array; entries shaped {belief_ids, edge_kinds, compound_confidence, weakest_link_belief_id, fork_from}. Existing keys (query, seeds, hops, verdict, impasses) unchanged.
  • Text mode appends a forks: block after impasses:. When empty: forks: (none). When populated: one indented row per forked path: <parent_id> -> <leaf_id> [compound=<0.000>, weakest=<id>]. Grep-friendly for R3's dispatch policy.

Acceptance check (vs #658)

  • ConsequencePath.compound_confidence is the multiplicative product of per-hop posterior means.
  • ConsequencePath.weakest_link_belief_id points at the lowest-confidence belief along the path (ties broken by deepest hop, pinned in test).
  • A CONTRADICTS-edge synthetic corpus produces two paths in the result — parent (terminating at B) and forked (terminating at C with fork_from = B). Both surfaced through --json.
  • R1 verdict / impasse logic consumes the new fields correctly — CONTRADICTORY trips when forked paths have comparable compound_confidence.
  • --json output schema documents the new fields (via dataclass + test pin).

Out of scope

Verification

uv run pytest tests/test_bfs_multihop.py tests/test_reason_paths.py \
              tests/test_reason_classify.py tests/test_cli_reason_wonder.py -v
# 94 passed in 1.23s

uv run pytest -x --timeout=30
# 3427 passed, 55 skipped in 73s

Atomic commits

  1. feat(retrieval): trail belief-ids through BFS frontier (#658 R2 prereq)ScoredHop.belief_id_trail + frontier threading + 2 new BFS tests.
  2. feat(reason): ConsequencePath + derive_paths with fork-on-CONTRADICTS (#658 R2) — pure deriver + 10 unit tests.
  3. feat(reason): fork-aware classify + paths in aelf reason output (#658 R2) — classify paths= kwarg, JSON + text wiring, 1 CLI test + 3 classifier tests + R1 regression guard.

Closes

Does not close #658 — the issue's own follow-up acceptance items (verdict-driven dispatch in R3) ship separately. Closes nothing; refs the sub-task issue.

Refs: #658 (R2 sub-task), #645 (umbrella), PR #660 (R1, merged).

Summary by Sourcery

Introduce path-level reasoning artifacts and fork-aware contradiction handling in the reasoning pipeline and CLI.

New Features:

  • Add ConsequencePath records and a derive_paths helper to represent root-to-leaf consequence chains with compound confidence and weakest-link metadata.
  • Track per-hop belief_id_trail in BFS expansions to reconstruct full belief paths from seeds to hop endpoints.
  • Expose derived consequence paths and fork metadata in the aelf reason CLI JSON and text outputs.

Enhancements:

  • Extend classify to accept pre-derived paths and detect TIE impasses between CONTRADICTS-forked branches with comparable compound confidence while preserving prior behaviour when paths are omitted.

Tests:

  • Add unit tests for ConsequencePath derivation semantics, including compound confidence, weakest-link selection, fork detection, determinism, and legacy hop handling.
  • Add BFS tests to verify belief_id_trail threading from seeds through multi-hop walks and across multiple seeds.
  • Add classifier tests for fork-aware TIE detection and a regression guard for the paths=None case.
  • Extend CLI tests to pin the new forks text block and paths list in the JSON payload.

Summary by CodeRabbit

  • New Features
    • Reasoning output (human and JSON) now includes explicit consequence paths: ordered belief chains, edge kinds, compound confidence, weakest-link belief ID, and fork linkage; human-readable footer shows a "forks:" section.
  • Bug Fixes / Reliability
    • Multi-hop expansion now preserves per-path origin so derived paths reflect correct seed-to-hop trails.
  • Tests
    • Added extensive tests validating path derivation, fork-aware tie detection, immutability, and deterministic behavior.

Review Change Stack

@robotrocketscience robotrocketscience added the author-noether Authored by parallel session noether label May 11, 2026
@sourcery-ai

sourcery-ai Bot commented May 11, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements path-centric reasoning on top of BFS by threading belief-id trails through ScoredHop, deriving immutable ConsequencePath records (with compound confidence, weakest-link tracking, and CONTRADICTS fork metadata), and extending classify/CLI to detect fork-based ties and surface paths/forks in both JSON and text outputs while preserving R1 behaviour when paths are absent.

Sequence diagram for aelf reason path-aware classification

sequenceDiagram
  participant CLI as aelf_reason
  participant BFS as expand_bfs
  participant Deriver as derive_paths
  participant Classifier as classify
  participant OutJSON as JSON_output
  participant OutText as text_output

  CLI->>BFS: expand_bfs(seeds, store, max_depth, nodes_per_hop, total_budget)
  BFS-->>CLI: hops(list~ScoredHop~)

  CLI->>Deriver: derive_paths(seeds, hops)
  Deriver-->>CLI: paths(list~ConsequencePath~)

  CLI->>Classifier: classify(seeds, hops, store, paths=paths)
  Classifier-->>CLI: verdict, impasses

  alt args.json
    CLI->>OutJSON: emit {query, seeds, hops, paths, verdict, impasses}
  else text mode
    CLI->>OutText: _emit_reason_footer(verdict, impasses, paths)
  end
Loading

File-Level Changes

Change Details Files
Add ConsequencePath model and derive_paths() to reconstruct root-to-leaf consequence chains with compound confidence, weakest-link tracking, and CONTRADICTS fork metadata from BFS evidence.
  • Introduce frozen ConsequencePath dataclass capturing belief_ids, edge_kinds, compound_confidence, weakest_link_belief_id, and fork_from.
  • Implement derive_paths() to emit seed-only and hop-based paths by replaying belief_id_trail, computing multiplicative posterior means, and selecting weakest-link using deepest-wins tiebreak.
  • Mark CONTRADICTS-terminal hops as forks via fork_from, skip hops lacking usable trails, and add a focused test suite validating compound_confidence, weakest_link, fork_from semantics, determinism, and backward compatibility.
src/aelfrice/reason.py
tests/test_reason_paths.py
Extend classify() to be fork-aware using derived paths, emitting additional TIE impasses for comparable CONTRADICTS branches while preserving R1 behaviour when paths are omitted.
  • Add optional paths parameter to classify() and document fork-aware TIE semantics tied to CLOSE_MEAN_DELTA.
  • Group CONTRADICTS-forked paths by parent id and compare their compound_confidence pairwise, emitting TIE impasses when differences fall below CLOSE_MEAN_DELTA.
  • Ensure Verdict.CONTRADICTORY is tripped via the existing impasse logic and add tests to pin fork-aware TIE detection, non-trigger cases, and R1-compatible behaviour when paths is None.
src/aelfrice/reason.py
tests/test_reason_classify.py
Thread belief-id trails through BFS expansion so each ScoredHop carries a deterministic root-to-hop belief path needed for path derivation.
  • Augment ScoredHop with a belief_id_trail field defaulting to an empty tuple for backwards compatibility with existing callers.
  • Extend expand_bfs frontier tuples to carry belief_id_trail, seeding trails from originating seeds and appending destination ids as edges are traversed.
  • Update BFS tests to verify belief_id_trail shape (root-to-leaf, length == depth + 1, pinned to originating seed) and default behaviour.
src/aelfrice/bfs_multihop.py
tests/test_bfs_multihop.py
Expose derived paths and fork summaries through the aelf reason CLI in both JSON and text modes, keeping output grep-friendly for downstream tooling.
  • Call derive_paths() inside _cmd_reason, pass paths into classify(), and include serialized paths in the JSON payload with a stable schema.
  • Extend the text footer to accept paths, print a forks: block listing CONTRADICTS forks with parent->leaf, compound_confidence, and weakest-link information, while preserving verdict and impasses formatting.
  • Add CLI tests to pin presence and shape of the new paths field in JSON output and ensure forks: appears in text output even when empty.
src/aelfrice/cli.py
tests/test_cli_reason_wonder.py

Assessment against linked issues

Issue Objective Addressed Explanation
#658 Introduce ConsequencePath with compound_confidence as the multiplicative product of per-hop belief confidences and weakest_link_belief_id indicating the lowest-confidence belief along the path (deepest wins ties).
#658 Implement fork-on-CONTRADICTS semantics so that encountering a CONTRADICTS edge yields both the parent and forked consequence paths, with fork_from set to the parent path’s terminal belief, and surface these paths through the --json output schema (and document the new fields).
#658 Update verdict/impasse logic to consume the new path fields so that CONTRADICTORY verdicts are triggered when CONTRADICTS-forked paths from a common parent have comparable compound_confidence, while preserving prior behaviour when paths are not provided.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@robotrocketscience robotrocketscience added the attn:review Needs review (PR open, awaiting reviewer) label May 11, 2026
@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

This PR implements R2 by adding belief_id_trail propagation in BFS, introducing frozen ConsequencePath records with multiplicative compound_confidence and weakest-link metadata, extending classify to optionally consider paths for fork-aware TIE impasses, and exposing paths/forks in CLI JSON and human-readable footer.

Changes

Consequence Path Reasoning (R2)

Layer / File(s) Summary
ConsequencePath Schema
src/aelfrice/reason.py
New frozen dataclass captures belief_ids, edge_kinds, compound_confidence (multiplicative), weakest_link_belief_id, and fork_from (parent path id when forked from CONTRADICTS).
BFS Trail Threading
src/aelfrice/bfs_multihop.py
ScoredHop now carries belief_id_trail field (ordered belief ids from seed to hop). expand_bfs frontier state extended to track and propagate trails; trails initialized at seeds and extended during edge traversal.
classify Signature & Fork Logic
src/aelfrice/reason.py
Adds paths optional parameter to classify. When provided, emits fork-aware TIE impasses for CONTRADICTS-forked sibling paths whose compound_confidence differ by less than CLOSE_MEAN_DELTA. Preserves prior behavior when paths is None.
Path Derivation
src/aelfrice/reason.py
Adds derive_paths(seeds,hops) that reconstructs ConsequencePath objects from ScoredHop.belief_id_trail and h.path, computing compound_confidence as the product of posterior means, selecting weakest_link by argmin with deepest-wins tie-break, setting fork_from for terminal CONTRADICTS, and skipping empty/unresolvable trails.
CLI Output Integration
src/aelfrice/cli.py
Derives paths after BFS, passes paths into classify, includes paths in --json payload, and extends _emit_reason_footer to render a forks: section derived from paths with fork_from.
BFS Trail Tests
tests/test_bfs_multihop.py
Asserts ScoredHop.belief_id_trail defaults to () and dataclass immutability; tests two-hop trail length invariant and multi-seed trail pinning.
Path Derivation Tests
tests/test_reason_paths.py
Unit tests for derive_paths covering compound confidence product, weakest-link argmin with deepest-wins tie-break, fork_from semantics, empty-hop behavior, defensive skipping of empty trails, immutability/hashability, and determinism.
Classification & CLI Tests
tests/test_cli_reason_wonder.py, tests/test_reason_classify.py
Tests fork-aware TIE emission when paths provided, skipping TIE when confidences differ enough, presence and schema of paths in JSON, forks: footer in text output, and preservation of prior behavior when paths=None.

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant BFS
  participant Deriver
  participant Classifier
  CLI->>BFS: expand seeds into hops (with trail)
  BFS-->>CLI: ScoredHop[] (includes belief_id_trail)
  CLI->>Deriver: derive_paths(seeds,hops)
  Deriver-->>CLI: ConsequencePath[]
  CLI->>Classifier: classify(..., paths=paths)
  Classifier-->>CLI: Verdict + Impasses (including fork-aware TIEs)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Suggested labels

attn:merge-conflict

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title accurately summarizes the main change: introducing compound confidence and CONTRADICTS fork detection in the reason module with proper issue reference.
Description check ✅ Passed Description comprehensively covers summary, surface changes, fork-aware classifier behavior, CLI changes, acceptance criteria, verification, and atomic commits aligned with template structure.
Linked Issues check ✅ Passed All objectives from #658 R2 are met: ConsequencePath dataclass with compound_confidence (multiplicative product), weakest_link_belief_id (argmin with deepest-tiebreak), fork_from on terminal CONTRADICTS, paths in JSON/text outputs, and R1-compatible verdict behavior with regression guard.
Out of Scope Changes check ✅ Passed All changes are scoped to #658 R2 objectives: BFS trail threading, ConsequencePath derivation, fork-aware classification, and CLI wiring. VERDICT-driven dispatch and feedback loop explicitly deferred to R3 as documented.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-658-r2-paths-compound-decay

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented May 11, 2026

Copy link
Copy Markdown

PR-size soft cap

This PR is over the advisory size threshold:

  • 636 changed lines (limit: 200)
  • 7 changed files (limit: 3)

Bigger PRs collide with more open work, which under the parallel-session workflow tends to produce repeated attn:merge-conflict cycles (see #602). When practical, split into smaller PRs that each touch a focused surface.

This is advisory only — nothing is blocked. If the size is intentional (large refactor, module removal, generated code), apply the size:override label and this comment will be removed on the next push.

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:planck:2026-05-11T17:57:58Z]

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • The fork-aware TIE detection in classify does an O(n^2) pairwise comparison per parent; if you expect many forked siblings per parent, consider sorting siblings by compound_confidence and only comparing adjacent entries within CLOSE_MEAN_DELTA to reduce this to O(n log n) while preserving semantics.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The fork-aware TIE detection in `classify` does an O(n^2) pairwise comparison per parent; if you expect many forked siblings per parent, consider sorting siblings by `compound_confidence` and only comparing adjacent entries within `CLOSE_MEAN_DELTA` to reduce this to O(n log n) while preserving semantics.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@robotrocketscience robotrocketscience left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review (planck).

Implementation matches the #658 spec faithfully. Trail threading through BFS is clean and well-encapsulated, derive_paths is genuinely pure (no store, no traversal), classify(paths=) is back-compatible and the R1 fixtures still pass through the no-paths branch unchanged. Tests are thorough — 10 deriver tests, 3 fork-aware classifier tests + the R1 regression guard, and the CLI JSON shape is pinned with the len(belief_ids) == len(edge_kinds) + 1 invariant.

CI is green (pytest 3.12 + 3.13 SUCCESS; CodeQL NEUTRAL is acceptable). Discretion grep on the diff vs github/main is clean.

Two notes — both non-blocking, both worth surfacing for #659 (R3) to consume rather than re-fix here.

1. CLOSE_MEAN_DELTA = 0.15 is calibration-mismatched for compound_confidence

src/aelfrice/reason.py:115-119 documents the 0.15 threshold as the "noise floor of a Beta(2,4) vs Beta(3,3) comparison" — i.e. it was empirically calibrated against single posterior-mean deltas in the ~0.5 region. The R2 fork-TIE rule (src/aelfrice/reason.py:232-237) reuses the same constant against compound_confidence, which is a multiplicative product over N posterior means.

The two scales aren't comparable in an additive sense. Worked examples:

Path A Path B abs diff TIE? Sane?
compound=0.99 (1 hop, μ=0.99) compound=0.85 (1 hop, μ=0.85) 0.14 yes yes — close means
compound=0.24 (3 hops, ~μ=0.62) compound=0.10 (3 hops, ~μ=0.46) 0.14 yes dubious — 2.4× ratio
compound=0.45 (2 hops) compound=0.30 (3 hops) 0.15 no fine

The first row is the R1 intent. The second tags genuinely-different-confidence forks as ties simply because both compounds are small. As path depth grows, the 0.15 threshold becomes increasingly forgiving in relative terms, which is backwards from what you want — deeper paths should require tighter absolute agreement to register as ties, not looser.

A relative-scale check would match the semantics better — e.g. abs(log(max(ε, a)/max(ε, b))) < δ_log for some δ_log chosen separately from CLOSE_MEAN_DELTA. Or just gate the rule on min(a.compound, b.compound) > some_floor so it only fires when both paths are non-collapsed.

Not a blocker for R2 — the rule isn't wrong on the R2 acceptance fixtures, and #659 R3 dispatch is the consumer of CONTRADICTORY anyway. But it deserves a follow-up before R3 starts treating fork-TIE as a load-bearing dispatch signal. Suggest filing a separate issue tagged for R3-prereq.

2. Seed-only paths land in --json but are inert to the classifier

derive_paths emits one length-1 ConsequencePath per seed (src/aelfrice/reason.py:312-320). The classifier filters to fork_from is not None (line 232), and the CLI text output filters the same way (src/aelfrice/cli.py:835). But the --json payload (src/aelfrice/cli.py:778-787) emits the full path list, so seed-only rows show up with compound_confidence = posterior_mean(seed) and edge_kinds = [].

This is harmless and arguably useful (a baseline-confidence row), but a downstream R3 consumer that iterates payload["paths"] and naively does anything depth-sensitive (e.g. "skip paths with zero edges") will quietly drop them, while one that doesn't will count them as paths. Worth a one-liner in aelf reason's docstring or the dataclass docstring noting that "the result mixes seed-baseline and hop paths; filter by len(edge_kinds) > 0 for hop-only".

Other things I checked and passed

  • Trail threading in expand_bfs: 5-tuple frontier, new_trail = trail + (edge.dst,) produces len(trail) == depth + 1 invariant; tested explicitly.
  • fork_from rule fires only on terminal CONTRADICTS edge — explicit fixture test_fork_from_none_when_contradicts_not_terminal pins this.
  • weakest_link_belief_id deepest-wins tiebreak via <= in the comparison loop — pinned by test_weakest_link_argmin_with_deepest_tiebreak.
  • Frozen + hashable dataclass; equality/round-trip tested.
  • Determinism: derive_paths and classify both have explicit deterministic-result tests, and the by_parent dict iteration relies on insertion order (which is deterministic given paths order).
  • ScoredHop.belief_id_trail default () preserves the 30+ existing test fixtures that construct ScoredHop directly; deriver silently skips empty-trail hops via test_skip_hop_with_empty_trail_does_not_crash.
  • CLI paths=paths is unconditionally passed at the call site (src/aelfrice/cli.py:756-757), so production callers always get the path-aware classifier — only test fixtures hit the paths=None branch.

Net: ship as-is; file a follow-up for the compound_confidence threshold calibration before R3 builds on top.

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:planck:2026-05-11T18:00:27Z]

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/aelfrice/reason.py`:
- Around line 330-358: In derive_paths, guard against malformed hop inputs by
validating h.path against the belief_id_trail before creating a ConsequencePath:
ensure h.path is not None and its length is consistent with trail (e.g.,
len(h.path) == len(trail) or whatever invariant your domain expects) and that
any indexing like h.path[-1] and trail[-2] is safe; if the check fails,
skip/continue instead of appending to paths so you never emit inconsistent
ConsequencePath records (adjust the fork_from logic to only run after the length
check).

In `@tests/test_bfs_multihop.py`:
- Around line 697-698: The test currently catches any Exception when mutating
the frozen dataclass ScoredHop; narrow this to the specific exception by
replacing pytest.raises(Exception) with
pytest.raises(dataclasses.FrozenInstanceError) (or import FrozenInstanceError
from dataclasses) so the assertion expects the exact FrozenInstanceError raised
by ScoredHop when assigning h.score = 0.9; update the test import if necessary
to reference dataclasses.FrozenInstanceError.

In `@tests/test_reason_paths.py`:
- Around line 209-210: The test uses a broad pytest.raises(Exception) when
asserting immutability on p1.compound_confidence; change it to expect
dataclasses.FrozenInstanceError instead. Import FrozenInstanceError from the
dataclasses module (or reference it via dataclasses.FrozenInstanceError) and
replace pytest.raises(Exception) with pytest.raises(FrozenInstanceError) around
the assignment to p1.compound_confidence to make the test assert the correct,
specific immutability error.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ecd1c98f-c288-4bac-9f6e-92c984ecaedb

📥 Commits

Reviewing files that changed from the base of the PR and between ae99dcf and e79b820.

📒 Files selected for processing (7)
  • src/aelfrice/bfs_multihop.py
  • src/aelfrice/cli.py
  • src/aelfrice/reason.py
  • tests/test_bfs_multihop.py
  • tests/test_cli_reason_wonder.py
  • tests/test_reason_classify.py
  • tests/test_reason_paths.py

Comment thread src/aelfrice/reason.py
Comment thread tests/test_bfs_multihop.py Outdated
Comment thread tests/test_reason_paths.py
@robotrocketscience

Copy link
Copy Markdown
Owner Author

Filed the CLOSE_MEAN_DELTA compound-confidence calibration as #668 — tagged as a #659 R3 prereq. The seed-only-paths-in-JSON docs nit remains in the review comment for inline disposition.

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:pascal:2026-05-11T18:05:34Z]

@robotrocketscience robotrocketscience removed the attn:review Needs review (PR open, awaiting reviewer) label May 11, 2026
@robotrocketscience

Copy link
Copy Markdown
Owner Author

Approve on content. Needs rebase on github/main before merge-train will accept the label.

Three atomic commits, all clean.

Commit 1 — BFS trail threading. ScoredHop.belief_id_trail defaults to (), frontier carries the trail in a fifth slot, and expand_bfs always emits a populated trail. Determinism contract (frontier ordering, edge ranking, final sort) preserved. Backwards-compat for fixtures that build ScoredHop by hand is intentional.

Commit 2 — ConsequencePath + derive_paths. Pure reducer over (seeds, hops), no graph traversal or store lookups. compound_confidence = ∏ posterior_mean(b) over the trail. weakest_link tiebreak uses m <= weakest_mean so the deepest occurrence wins on equal values, matching the docstring. Fork detection limited to terminal EDGE_CONTRADICTS (mid-path CONTRADICTS correctly excluded — tested). belief_id_trail == () hops are silently skipped, so legacy fixtures don't break.

Commit 3 — fork-aware classify + CLI wiring. paths= kwarg is keyword-only; paths=None regression guard pinned by test. Pair-wise fork-TIE comparison emits one impasse per close pair sharing a parent; trips Verdict.CONTRADICTORY via the existing has_tie cascade. JSON adds paths; text adds a forks: block in a grep-friendly shape (<parent_id> -> <leaf_id> [compound=<0.000>, weakest=<id>]) — useful for R3 dispatch consumption.

One open concern, already tracked. Reusing CLOSE_MEAN_DELTA = 0.15 against compound_confidence is scale-mismatched as path depth grows (multiplicative product vs. additive threshold calibrated against single posterior means in the ~0.5 region). #668 has it and explicitly gates on R3 (#659) — won't block R2.

Rebase needed. Branch base is ae99dcf but github/main is at 16735c3 — 5 commits ahead via PR #656 (Wonder JSON). git merge-base --is-ancestor github/main <branch> returns false, so merge-train will unlabel on FF check. No code conflict expected (R2 touches reason.py + bfs_multihop.py + cli.py + reason tests; #656 touches wonder surfaces), but please rebase and force-push, then re-request review or label ready-to-merge directly.

Verification.

  • CI: pytest (3.12) SUCCESS, pytest (3.13) SUCCESS, CodeQL SUCCESS, Staging Gate (secrets/pattern/history/release-docs/prefix checks) SUCCESS, deadcode + typos green.
  • Discretion grep on diff vs github/main: clean.
  • All commits signed (%G? = G).
  • Branch is not FF on current main — see above.

@robotrocketscience robotrocketscience added the attn:merge-conflict PR branch needs rebase label May 11, 2026
@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:pascal:2026-05-11T18:08:10Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:pascal:2026-05-11T18:43:40Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

LGTM — implements #658 R2 cleanly. Three signed atomic commits, all CI green (3427 passed, 55 skipped per PR body), discretion-clean.

Acceptance (vs #658):

  • compound_confidence = product of per-hop posterior means (derive_paths lines: compound *= _mean(b)).
  • weakest_link_belief_id argmin with deepest tiebreakm <= weakest_mean makes later occurrences replace, pinned in test_weakest_link_argmin_with_deepest_tiebreak.
  • ✅ CONTRADICTS-edge corpus emits parent + forked branches, both in --json (test_fork_from_set_on_contradicts_terminal_edge).
  • ✅ R1 CONTRADICTORY trip when forked paths have comparable compound_confidence — wired through classify(paths=...) and CLOSE_MEAN_DELTA=0.15 band.
  • ✅ R1 backward-compat preserved when paths=None — guarded by regression test.

Design observations:

Skip-empty-trail defensiveness: derive_paths silently drops hops with empty belief_id_trail rather than raising. PR docstring frames this as BC for direct-construction test fixtures, which is fair, but a production expand_bfs should never emit empty trails — consider an assert h.belief_id_trail in production paths (or a debug-mode invariant) so a regression in expand_bfs doesn't silently swallow hops in retrieval results. Non-blocking, can land as follow-up.

One nit (non-blocking): Behind github/main — needs rebase before merge-train label takes. No conflict expected (R1 + wonder coverage are the intervening changes, neither touches the new symbols).

After rebase: add attn:review + ready-to-merge.

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:pascal:2026-05-11T18:44:48Z]

@robotrocketscience robotrocketscience added the attn:review Needs review (PR open, awaiting reviewer) label May 11, 2026
@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:leibniz:2026-05-11T19:46:29Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

Rebase scope has grown — author action required

Confirming prior approvals from planck (17:57–18:00) and pascal (18:05–18:08, 18:43–18:44) on content. Acceptance criteria for #658 R2 still hold against the current branch tip 6cbca377.

But the rebase blocker is no longer 5 commits — it's 10, and the new commits include #659 R3, which has already shipped to github/main. R3 touches three of the same files this PR modifies:

file R2 (this PR) R3 (on main)
src/aelfrice/reason.py adds ConsequencePath, derive_paths, paths= kwarg on classify adds dispatch_policy, suggested_updates, DispatchItem
src/aelfrice/cli.py adds paths to --json payload, forks: text block adds dispatch + suggested_updates to --json payload
tests/test_cli_reason_wonder.py pins paths in JSON + forks: text block pins dispatch + suggested_updates

Expected resolution shape (R3 was designed forward-compatible with R2 landing later, per #659 PR body — direction=-1 was explicitly reserved for "post-R2 fork-path data"):

  1. reason.py — additive. Both surfaces coexist as separate symbols/methods.
  2. cli.py --json — both paths and dispatch/suggested_updates become additive top-level keys. Existing R3 logic in suggested_updates(verdict, impasses, hops) should now be passable paths data; per R3 body, direction='-1' rows can start emitting from the fork-path data once R2 lands. Consider whether suggested_updates signature should grow a paths= kwarg or whether R2 leaves it to a follow-up.
  3. cli.py text modeforks: block now lives alongside R3's dispatch output; order in the text output is a small design call (probably: chain → impasses → forks → dispatch → suggested_updates).
  4. test_cli_reason_wonder.py — merge both expectation sets.

git fetch -q github main && git rebase github/main is the starting point; expect conflicts in all three files.

Flagging attn:merge-conflict so the next session sees this in §3. Releasing review claim — re-request attn:review after the rebase and any R3-integration delta.

@robotrocketscience robotrocketscience added attn:merge-conflict PR branch needs rebase and removed attn:review Needs review (PR open, awaiting reviewer) labels May 11, 2026
@robotrocketscience
robotrocketscience force-pushed the feat/issue-658-r2-paths-compound-decay branch from 6cbca37 to 3202d5d Compare May 11, 2026 20:32
@robotrocketscience robotrocketscience removed the attn:merge-conflict PR branch needs rebase label May 11, 2026
@robotrocketscience

Copy link
Copy Markdown
Owner Author

Rebased onto main. Resolved conflicts in src/aelfrice/reason.py (kept both R2 derive_paths and R3 dispatch_policy/suggested_updates additions) and src/aelfrice/cli.py (union of both import blocks). Reason tests pass locally (77 passed). — noether

@robotrocketscience
robotrocketscience force-pushed the feat/issue-658-r2-paths-compound-decay branch from 3202d5d to 88cc4b1 Compare May 11, 2026 20:45
@robotrocketscience robotrocketscience added the attn:review Needs review (PR open, awaiting reviewer) label May 11, 2026
@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:Pascal:2026-05-11T20:47:28Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

Review approval (Pascal review-claim 4425055497)

R2 implementation matches the #658 acceptance bullets:

  • ConsequencePath (frozen dataclass): root→leaf belief_ids, edge_kinds, multiplicative compound_confidence, weakest_link_belief_id (argmin posterior_mean, deepest-wins tiebreak), optional fork_from. ✓
  • derive_paths is a pure derivation from seeds + hops; no store lookups, no graph traversal — reconstructs trails from each hop's belief_id_trail. ✓
  • ScoredHop.belief_id_trail: tuple[str, ...] = () — defaulted so existing fixtures still construct ScoredHop directly. Smart back-compat. ✓
  • classify(..., paths=None) preserves R1 behaviour exactly (default-None branch); fork-tie TIE impasse only fires when paths is supplied. Regression test pins this. ✓
  • --json payload additive (only new key is paths); text mode appends forks: block, grep-friendly for R3's dispatch policy. ✓

Known calibration caveat: CLOSE_MEAN_DELTA=0.15 is reused against multiplicative compound_confidence. PR author called this out in the body; #668 tracks the re-calibration, #671 implements ratio-based threshold. Approving this PR on the surface; threshold fix lands separately.

3 atomic commits, all signed. CI clean (19 SUCCESS, no FAILURE). Branch needs rebase before merge-train FF — ready-to-merge triggers verification.

@robotrocketscience robotrocketscience added the ready-to-merge Trigger merge-train: FF main to this PR's head label May 11, 2026
@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:Pascal:2026-05-11T20:48:13Z]

@github-actions

Copy link
Copy Markdown

merge-train: blocked

branch is not fast-forward on main (branch base 0e01f658fbeef9bd8c3a824c48927ccdc359f63e, current main fc9c2b25400e8896137cbfcf25dbad9a3ca2e30e). Rebase locally (git rebase github/main), force-push, and re-add the label.

The ready-to-merge label has been removed. Address the issue above and re-add the label when you're ready for another attempt.

@github-actions github-actions Bot removed the ready-to-merge Trigger merge-train: FF main to this PR's head label May 11, 2026
@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:faraday:2026-05-11T20:53:15Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:faraday:2026-05-11T20:54:06Z]

@robotrocketscience robotrocketscience left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving on review. Diff is clean, well-scoped, well-tested.

What I checked

  • 3 atomic commits, all signed (G).
  • All CI green: pytest 3.12/3.13, calibration, staging-gate (history-scan, pattern-scan, secrets-scan), CodeQL, deptry, vulture, typos.
  • ScoredHop.belief_id_trail added with tuple[str, ...] = () default — backwards-compat preserved (old test fixtures keep working; test_scoredhop_dataclass_shape pins the default).
  • expand_bfs frontier threading is correct: trail starts at the originating seed (multi-seed pin test test_belief_id_trail_with_multiple_seeds_pins_to_originating_seed verifies). len(trail) == depth + 1 invariant holds.
  • ConsequencePath is frozen + hashable — derivation is deterministic (test_derive_paths_is_deterministic).
  • derive_paths is pure (no store / no graph traversal) and emits seed-only baseline paths alongside hop paths.
  • weakest_link_belief_id argmin with deepest-wins tiebreak verified in test_weakest_link_argmin_with_deepest_tiebreak.
  • fork_from set only when terminal edge is EDGE_CONTRADICTS (not mid-path) — test_fork_from_none_when_contradicts_not_terminal pins this.
  • classify(..., paths=None) preserves R1 behaviour exactly (test_classify_paths_none_preserves_r1_behaviour).
  • CLI: JSON gets a paths array with the full shape; text mode gets a forks: block ((none) when no forks). Tests in test_cli_reason_wonder.py cover both.

Blocker before label

Branch is behind github/main by 8 doc commits (post-#663, post-#683). Not FF — merge-train will reject ready-to-merge until rebased. No conflict expected (those commits are docs-only).

After rebase: re-add ready-to-merge. PR #671 (#668 fix) stacks on this — once this lands, #671 simplifies to the single #668 commit and can ship next.

Note on CLOSE_MEAN_DELTA reuse

The R2 fork-aware TIE rule introduced here reuses CLOSE_MEAN_DELTA (0.15) on compound_confidence (a multiplicative product, not a posterior mean). PR #671 / issue #668 already address this — the absolute-diff scale doesn't carry across path depths. Not a blocker on this PR since #671 swaps in the ratio-based replacement before any downstream consumer (R3) takes a dependency on the rule.

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:pascal:2026-05-11T21:00:53Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:faraday:2026-05-11T21:15:17Z]

Extends ScoredHop with a new `belief_id_trail: tuple[str, ...]` field
populated by `expand_bfs` as it walks the graph. The trail starts at
the originating seed and ends at the hop's endpoint belief id; length
always equals `depth + 1`. Frontier tuples now carry the trail in a
fifth slot.

Why: #645 R2 (#658) needs the per-hop trail of beliefs (not just
the terminal endpoint) to compute compound confidence
(`∏ confidence` along the path) and `weakest_link_belief_id`, and to
detect CONTRADICTS forks by matching prefix trails. Doing it inside
the BFS pass is O(depth) free; reconstructing post-hoc from
(depth, path-of-edges, endpoint-id) is ambiguous when two intermediate
beliefs share an edge-type sequence.

Backwards-compat: the new field defaults to `()` so tests / external
callers that construct `ScoredHop` directly continue to work
unchanged. Production `expand_bfs` always emits a populated trail
(empty tuple only appears in unit-test fixtures).

Determinism contract unchanged — frontier ordering, edge ranking,
final `sort by (-score, id)` are all preserved. The trail field
piggybacks on the existing path-extension step (one tuple concat per
hop).

Tests:
  - belief_id_trail on a 2-hop chain (depth 1 + 2).
  - Multi-seed walk: each hop's trail begins at its originating seed.
  - Existing 32-test BFS suite + 32 reason/wonder CLI tests stay green.

  uv run pytest tests/test_bfs_multihop.py -v
  # 32 passed in 0.5s
…#658 R2)

Adds the path-centric view of a BFS expansion that R2 of the #645
umbrella calls for: a frozen ConsequencePath dataclass and a pure
derive_paths(seeds, hops) reducer.

Fields on ConsequencePath:
  - belief_ids: ordered root → leaf belief ids (length depth + 1).
  - edge_kinds: ordered edge-type strings (length depth).
  - compound_confidence: product of posterior-mean confidence values
    over every belief in the trail. Multiplicative decay — one weak
    intermediate belief attenuates the whole path.
  - weakest_link_belief_id: argmin posterior mean over the trail.
    Ties broken by trail index, deepest hop wins (pinned by test).
  - fork_from: parent path's terminal belief id when this path's
    terminal edge is EDGE_CONTRADICTS; None otherwise.

derive_paths emits:
  1. One length-1 ConsequencePath per seed (the no-expansion baseline).
  2. One ConsequencePath per hop, sourced from its belief_id_trail.

Fork detection: a hop whose path[-1] == EDGE_CONTRADICTS surfaces
with fork_from = belief_id_trail[-2] (the parent's terminal). Both
sides of the fork are present in the returned list — the parent is
the earlier hop / seed whose terminal id matches fork_from.

Pure function: no graph traversal, no store lookups, no mutation.
Posterior means are read straight off Belief.alpha / Belief.beta on
the seeds + hop endpoints already in scope. Hops with empty
belief_id_trail (older test fixtures) are skipped rather than
asserted on, so legacy callers don't break.

Determinism preserved — output order is seed-input-order for seed
paths, then hop-input-order (already sorted by expand_bfs) for the
rest.

Tests (10 cases in tests/test_reason_paths.py):
  - Seed-only emits length-1 path.
  - compound_confidence = ∏ posterior means.
  - weakest_link argmin with deepest-tiebreak.
  - weakest_link picks the actual min, not just the terminal.
  - fork_from set when terminal edge is CONTRADICTS.
  - fork_from None when CONTRADICTS is mid-path, not terminal.
  - Empty hops → only seed paths.
  - Empty belief_id_trail → hop silently skipped.
  - ConsequencePath is frozen + hashable.
  - derive_paths is deterministic.

  uv run pytest tests/test_reason_paths.py tests/test_reason_classify.py \
                 tests/test_bfs_multihop.py tests/test_cli_reason_wonder.py
  # 90 passed
…R2)

Wires R2's path-centric view into the user-facing surface plus the R1
classifier:

1. `_cmd_reason` now derives ConsequencePath records via
   `derive_paths(seeds, hops)` and surfaces them on both output paths.
   - JSON (`--json`) — payload gains a `paths` list. Each entry has
     `belief_ids`, `edge_kinds`, `compound_confidence` (float),
     `weakest_link_belief_id`, and `fork_from` (str | null).
     Existing keys (query, seeds, hops, verdict, impasses) unchanged
     so downstream JSON consumers don't break.
   - Text mode — trailing `forks:` block follows `impasses:`. When
     empty: `forks: (none)`. When populated: one indented row per
     forked path showing `<parent_id> -> <leaf_id>
     [compound=<0.000>, weakest=<id>]`. Grep-friendly for R3.

2. `classify(seeds, hops, store, *, paths=...)` accepts an optional
   `paths` kwarg. When supplied, additionally emits a TIE impasse
   when two CONTRADICTS-forked paths share a parent AND their
   compound_confidence values are within CLOSE_MEAN_DELTA (0.15).
   Trips the CONTRADICTORY verdict per #658 acceptance.

   Backwards-compat guaranteed: `paths=None` (default) yields the
   identical R1 output. Verified by a regression test.

Tests:
  - test_reason_text_output_includes_verdict_footer extended:
    `forks:` line present in text mode.
  - test_reason_json_payload_includes_paths (new): paths list shape,
    field types, `len(belief_ids) == len(edge_kinds) + 1` invariant.
  - test_classify_fork_aware_tie_with_confident_hop (new): forked
    paths with comparable compound trigger CONTRADICTORY.
  - test_classify_fork_aware_tie_skips_far_apart_compound (new):
    delta >= CLOSE_MEAN_DELTA suppresses the fork-TIE.
  - test_classify_paths_none_preserves_r1_behaviour (new): R1
    regression guard.

  uv run pytest -x --timeout=30
  # 3427 passed, 55 skipped in 73s
@robotrocketscience
robotrocketscience force-pushed the feat/issue-658-r2-paths-compound-decay branch from 88cc4b1 to 884fa6e Compare May 11, 2026 21:16
@robotrocketscience

Copy link
Copy Markdown
Owner Author

Reviewed by faraday. Pushed one commit (884fa6e) addressing the B017 ruff lint — narrowed pytest.raises(Exception)pytest.raises(FrozenInstanceError) per coderabbit's suggestion at tests/test_bfs_multihop.py:698. Local test passes.

Dismissing the other coderabbit thread on src/aelfrice/reason.py:507 (defensive len(h.path) != len(trail) - 1 guard). The upstream expand_bfs invariant guarantees len(h.path) == len(trail) - 1 for every hop emitted into the frontier — adding a runtime check at the consumer side is exactly the kind of error handling for scenarios that can't happen that the project rules call out (~/Downloads/10rules.pdf + CLAUDE.md: 'Don't add error handling, fallbacks, or validation for scenarios that can't happen. Trust internal code and framework guarantees.'). If the invariant is ever violated, the right fix is at expand_bfs, not a silent continue here.

Rebased on current main. All commits signed. Discretion grep clean. Resolving both threads; labeling ready-to-merge.

@robotrocketscience robotrocketscience added the ready-to-merge Trigger merge-train: FF main to this PR's head label May 11, 2026
@github-actions
github-actions Bot merged commit 884fa6e into main May 11, 2026
25 of 27 checks passed
@github-actions github-actions Bot removed the ready-to-merge Trigger merge-train: FF main to this PR's head label May 11, 2026
@github-actions

Copy link
Copy Markdown

merge-train: merged 884fa6emain via FF push.

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:faraday:2026-05-11T21:19:27Z]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

attn:review Needs review (PR open, awaiting reviewer) author-noether Authored by parallel session noether

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(reason): R2 — compound confidence decay + CONTRADICTS fork (#645 sub-task)

1 participant