Skip to content

refactor(recall): split pg_recall.py to the local 300-line/40-line caps - #411

Merged
cdeust merged 1 commit into
mainfrom
worktree-agent-a2199eda766d9b35a
Aug 9, 2026
Merged

refactor(recall): split pg_recall.py to the local 300-line/40-line caps#411
cdeust merged 1 commit into
mainfrom
worktree-agent-a2199eda766d9b35a

Conversation

@cdeust

@cdeust cdeust commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Summary

pg_recall.py stood at 494 lines against the repo's local 300-line file cap (CLAUDE.md/CONTRIBUTING.md § Code Style, a tightening of coding-standards.md §4.1) — the prior #368 split had targeted the global 500-line limit instead and stopped short. recall() itself was a single 318-line, 15-parameter orchestration function, far over the 40-line method cap (§4.2).

Continues the two documented seams from #368 (pg_recall_weights.py, pg_recall_assembly.py) with two more, following the facade re-export pattern already established there (and matching pg_store.py's mixin-over-typed-host split, context_assembly/condensers.py's facade):

  • pg_recall_signals.py (82 l.) — store-duck-typed mood/goal/Titans signal readers.
  • pg_recall_context.py (130 l.) — RecallContext (Introduce Parameter Object, §4.4) bundling the 17 invariants of one recall() call, plus fetch_and_triage/_wrrf_fetch (steps 1-4·C2).
  • pg_recall_stages.py (247 l.) — the post-WRRF recollection/rerank/typed-pool/final stages plus run_recall_pipeline(ctx), the pipeline driver.
  • pg_recall.py (100 l.) — recall() (39 lines) is now a thin public-API wrapper; facade re-exports every name existing callers/tests reach as pg_recall.<name>.

Public API unchanged — recall()'s signature, defaults, and return contract are identical, no caller or test moved. Every value moved unchanged; no constant retuned. Also fixed two ruff RET504 findings the extraction surfaced (boy-scout, same commit).

Measured:

  • File sizes: pg_recall.py 100, pg_recall_context.py 130, pg_recall_stages.py 247, pg_recall_signals.py 82 — all ≤300.
  • Method sizes: every method ≤40 lines (max: recall() 39, apply_recollection_pipeline 36).

Test plan

  • uv run --no-sync pytest -q -p no:randomly — 7417 passed, 123 subtests passed, 0 failed (406s); identical count to pre-refactor, 0 tests modified.
  • ruff check . — all checks passed.
  • ruff format --check . — all files already formatted.
  • .venv/bin/python -m pyright mcp_server/ (uv sync'd from uv.lock per CONTRIBUTING.md's pyright-gate recipe) — 0 errors, 0 warnings, 0 informations.
  • No benchmarks run (machine shared with other agents, per instruction) — no ranking/weights logic changed, pure code motion.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

…ee seams

CLAUDE.md/CONTRIBUTING.md tighten the repo-wide 500-line file cap
(coding-standards.md §4.1) to 300 lines, and enforce a 40-line method cap
(§4.2). pg_recall.py stood at 494 lines against that local 300-line limit
after the #368 split stopped at the global 500-line threshold instead —
and recall() itself was a single 318-line, 15-parameter orchestration
function, far over the 40-line method cap.

Continues the "two documented seams" from #368 (pg_recall_weights.py,
pg_recall_assembly.py) with two more, following the same facade
re-export pattern (mirrors pg_store.py's mixin-over-typed-host split and
context_assembly/condensers.py's facade):

- pg_recall_signals.py (82 l.) — the store-duck-typed mood/goal/Titans
  signal readers (_get_user_mood, _get_active_goal, _get_titans).
- pg_recall_context.py (130 l.) — RecallContext (Introduce Parameter
  Object, coding-standards.md §4.4: bundles the 17 invariants of one
  recall() call so every stage takes exactly (candidates, ctx) instead of
  8-13 positional params) plus fetch_and_triage/_wrrf_fetch (steps 1-4·C2).
- pg_recall_stages.py (247 l.) — the post-WRRF recollection/rerank/
  typed-pool/final stages, plus run_recall_pipeline(ctx) — the pipeline
  driver, kept out of pg_recall.py so recall() stays a stable, thin
  public-API wrapper regardless of how many stages the pipeline grows.
- pg_recall.py (100 l.) — recall() (39 lines) builds RecallContext and
  forwards to run_recall_pipeline; facade re-exports every name existing
  callers/tests reach as pg_recall.<name> (_get_active_goal, _get_user_mood,
  _chronological_rerank, RecallContext, compute_pg_weights,
  assemble_context) so no caller or test moved.

Public API unchanged: recall()'s signature, defaults, and return contract
are identical. Every value moved unchanged; no constant retuned. Also
fixed two ruff RET504 (unnecessary assignment before return) findings the
extraction surfaced in the moved stage functions (boy-scout, same commit).

Before: pg_recall.py 494 lines (1 file over cap), recall() 318 lines (1
method over cap).
After: pg_recall.py 100, pg_recall_context.py 130, pg_recall_stages.py
247, pg_recall_signals.py 82 — all ≤300. Every method ≤40 lines (max:
recall() 39, apply_recollection_pipeline 36).
Tests: uv run --no-sync pytest -q -p no:randomly — 7417 passed, 123
subtests passed, 0 failed (406s), identical count to pre-refactor,
0 modified. ruff check/format --check green. pyright mcp_server/ —
0 errors, 0 warnings, 0 informations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01263uv1QqR8TVzw2jXYUrXn
@cdeust

cdeust commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

ZETETIC-REVIEW: APPROVE

Reviewed from a fresh scratch clone (gh pr diff 411 + git fetch origin pull/411/head into an isolated worktree, not the shared clone), reading the actual post-split tree rather than trusting the PR body's numbers. All measured claims independently re-derived and confirmed exact.

Move 0 — ledger/seen-defect check. Pure code-motion refactor, no branches/error-arms to reconcile against a ledger. No "unrelated/pre-existing/out of scope" rationalization in the report; the RET504 fixes are proactive boy-scout, not a dismissal. Passes.

Stakes: Medium. Core recall pipeline, single author session, no schema/API/concurrency change, but load-bearing (imported by the production handler). Full Moves 1-4 applied.

1. "Aucun test modifié" — force ou angle mort?

Both, precisely characterized. It's a real strength for the wiring (facade re-exports are exactly what every existing test imports through — grep-confirmed zero test references any of the 3 new module names directly; they all go through pg_recall.<name>), so a broken re-export would fail loudly. But it is a blind spot for one specific path: no test anywhere passes momentum_state to recall() (only the production caller, mcp_server/handlers/recall.py:597, does) — the Titans-update branch inside apply_final_stages (pg_recall_stages.py:205-213) has zero coverage before and after this split. This is pre-existing (same gap existed in the monolithic recall() on main), not introduced by #411, so non-blocking — but worth a follow-up issue since the refactor was the natural moment to notice it. By contrast, EVENT_ORDER/_chronological_rerank is a real regression guard: test_spread_activation_candidate_contract.py imports it via the pg_recall.<name> facade, so a dropped/renamed re-export would break that test immediately.

2. RecallContext — value object or disguised shared state?

Verified genuine: @dataclass(frozen=True, slots=True). fetch_and_triage uses dataclasses.replace() to produce a new context carrying resolved intent/q_emb — never a field assignment on the original. Grepped for ctx\.\w+ *= across both new files: zero field mutations (only == comparisons matched, false positive from the regex). The one dict mutation (ctx.momentum_state["momentum"] = surprise) is on a caller-owned mutable dict — byte-identical to the pre-refactor line, not new coupling. This is a legitimate Introduce Parameter Object (§4.4), not a state bag.

3. run_recall_pipeline / fetch_and_triage boundary — real seam or size-driven cut?

Real. Cross-checked against the earlier #368 review already in memory: that review flagged pg_recall.py at 494 lines (post-#368) as missing this repo's local 300-line cap and explicitly named recall() (~265 lines then) as "the obvious next seam" — #411 is exactly that follow-up, not an invented justification. The cut lines up with an actual phase boundary (fetch+triage vs. the post-WRRF recollection chain), matching the numbered comments (1-4·C2 vs. 4a-11) in the original monolith.

4. Execution order / side effects

Traced stage-by-stage against the removed block: fetch_and_triage (intent -> weights -> encode -> WRRF fetch -> familiarity triage) -> apply_recollection_pipeline (hopfield -> hdc -> SA-augment -> dendritic -> emotional -> mood -> reconsolidation) -> apply_rerank_nudges (FlashRank -> value -> conflict -> goal -> attentional) -> reserve_typed_pool -> apply_final_stages (chronological -> Titans -> SA-tail). One-for-one match with the original's comments 1-11, no reordering, no stage moved earlier/later relative to another.

5. Layer rule + caps

grep -rn "from mcp_server.infrastructure" mcp_server/core/pg_recall*.py → zero hits, confirmed by hand not assumed. All imports are core-internal + mcp_server.observability (pre-existing, unchanged from main, moved not introduced — out of this diff's blast radius). No circular imports: pg_recall -> {signals, context, stages}, stages -> {context, signals}, context -> weights only — one-directional. File/method sizes independently measured via wc -l/grep -n "^def ": pg_recall.py 100, pg_recall_context.py 130, pg_recall_stages.py 247, pg_recall_signals.py 82, all ≤300; recall() 39 lines, apply_rerank_nudges 40 (tightest margin, still in-cap). One-language-per-file: English throughout, consistent. No new magic constants (every moved constant unchanged, # source: comments preserved verbatim). Bonus find: the split silently drops a genuinely dead duplicate _MIN_ENTITY_NAME_LEN = 3 that was leftover cruft in pg_recall.py on main (the real one lives in pg_recall_assembly.py since #368; the pg_recall.py copy had zero references) — accurate cleanup, unmentioned in the PR body but harmless and correct.

6. Two RET504 fixes — legitimate or noise?

Legitimate. In the monolith, candidates = reconsolidation_apply(...) needed the assignment because execution continued past it; in the split apply_recollection_pipeline/apply_rerank_nudges, that call is now the last statement, so return reconsolidation_apply(...) / return attentional_focus_rerank(...) directly is the correct fix, not incidental churn riding along in an already-large diff.

Summary: claims hold up against the actual tree. One MAJOR-severity, non-blocking test-adequacy gap (Titans/momentum_state branch, pre-existing) — recommend filing a follow-up issue. Everything else checks out at the evidence, not the assertion, level.

@cdeust
cdeust merged commit 9d8f437 into main Aug 9, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant