refactor(retrieval): the notes arms run on the one pipeline - auto_inject, its reuse and lesson slots, the write path by meaning, and rule_via_lesson are specs (milestone 456 step 4, #4906)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 1m2s
CI & Build / Python tests (push) Successful in 1m52s
CI & Build / Build & push image (push) Successful in 35s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 1m2s
CI & Build / Python tests (push) Successful in 1m52s
CI & Build / Build & push image (push) Successful in 35s
retrieval_pipeline gains the notes half: NoteArm / NoteSlot / NoteMoment / NoteIO / NoteResult and run_note_arm, which writes once the stages both notes arms copied: search, withhold this response's own menu (#3739), fresh/repeat split, the call row before any return (#3497, #3752), the band, the reserved slots in their order (reuse evicts, lesson extends), and the surfacing rows. The note renderer (_record_kind, _menu_name, _menu_passage, the seen pointer, menu_entry) moves with it, and run_via_lesson_arm takes rule_via_lesson. Behaviour-preserving, with flags for today's differences: notes still log BEFORE the band and rules after it (step 7's question). One deliberate change: a notes arm now fails open like the rule arms, so a failing search costs its lines and no longer the whole hook response. The I/O is resolved from plugin_context at call time (_note_io), so the existing patches keep working. The review re-run reads the AUTO_INJECT spec instead of restating it, and its guard now compares the two live searches. The registry declares the pipeline's notes fan-out sites. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1727,16 +1727,28 @@ def test_every_hook_rule_search_says_which_project_it_is_for():
|
||||
f"{path}:{call.lineno} searches every project's rules from a hook"
|
||||
)
|
||||
|
||||
# The pipeline: exactly one search, in `_ranked`, whose keyword set is
|
||||
# built as a dict literal — so the keys are read from that literal.
|
||||
# The pipeline: exactly one RULE search, in `_ranked`, whose keyword set
|
||||
# is built as a dict literal — so the keys are read from that literal.
|
||||
# Since step 4 the module also searches the NOTES corpus (the two notes
|
||||
# arms, their slots, and the via-lesson arm's lesson search), so the
|
||||
# searches are counted per function: a rule search copied anywhere else
|
||||
# is a new function in this map, and fails it.
|
||||
pipeline = ast.parse(Path("src/scribe/services/retrieval_pipeline.py").read_text())
|
||||
searches = [
|
||||
n for n in ast.walk(pipeline)
|
||||
if isinstance(n, ast.Call) and getattr(n.func, "attr", None) == "search"
|
||||
]
|
||||
assert len(searches) == 1, (
|
||||
f"the pipeline has {len(searches)} rule searches, expected 1 — every "
|
||||
f"arm is meant to reach the ranker through `_ranked`"
|
||||
by_function = {}
|
||||
for fn in ast.walk(pipeline):
|
||||
if isinstance(fn, ast.AsyncFunctionDef):
|
||||
n = sum(1 for c in ast.walk(fn) if isinstance(c, ast.Call)
|
||||
and getattr(c.func, "attr", None) == "search")
|
||||
if n:
|
||||
by_function[fn.name] = n
|
||||
assert by_function == {
|
||||
"_ranked": 1, # the rule ranker, every rule arm
|
||||
"_reserve_note_slot": 1, # notes: a reserved slot
|
||||
"run_note_arm": 1, # notes: the arm itself
|
||||
"run_via_lesson_arm": 1, # notes: lessons, then their linked rules
|
||||
}, (
|
||||
f"the pipeline's searches moved: {by_function} — every rule arm is "
|
||||
f"meant to reach the ranker through `_ranked`"
|
||||
)
|
||||
ranked = next(n for n in ast.walk(pipeline)
|
||||
if isinstance(n, ast.AsyncFunctionDef) and n.name == "_ranked")
|
||||
|
||||
Reference in New Issue
Block a user