Repository navigation
refactor(knowledge): build the recipe journal and KB writes from named parts - #2011
Conversation
…d parts recipe_journal.py carried its whole job in a handful of very large functions: the session's close path, the Recipe KB amend, the per-variant fact write and the configuration arm's timeline rows were each one long body threading dozens of defaulting expressions together. Two coherent groups move to sibling modules: - `_recipe_attrs.py` owns the recipe-shaped view of SharedState: the workload tags, the KEEP'd kernel optimizations joined to their end-to-end verdict, the attempt provenance and the recipe attrs themselves. These are pure projections of settled state and never needed the collaborator. - `_config_timeline.py` owns the configuration arm's run and attempt rows on the FRAMEWORK_AGENT event. What stays is split along the jobs it already did: the close path reads as its guards, the remote publish and the local amend, and the amend builds its `put_recipe` arguments from one table of the fields a recipe carries instead of repeating "the override if the caller set it, else the live value" for each of eleven fields. Three duplicated mechanisms go with it. The framework, version and precision identity was derived twice; the store's authority row was read through three near-identical `try`/`except` blocks; and the integrate ledger's float coercion was written out three times, now `to_float`. `_collect_workload_tags`, `_build_kernel_optimizations_from_state`, `_collect_attempt_provenance` and `_build_recipe_attrs_from_state` were private helpers on the collaborator and are now module functions in `_recipe_attrs`; their tests call them there, with every assertion carried across unchanged. `test_remote_read_is_wired_into_cli_t0_and_ close_write_remains` now inspects `_publish_remote_recipe`, which is where the close path's `HyperloomRemoteKB.from_env().write` lives. No observable change: internal structure only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…knowledge # Conflicts: # scripts/code_metrics_baseline.txt # src/hyperloom/inference_optimizer/tests/test_coordinator_sync_helpers_coverage_unit.py # src/hyperloom/orchestrator/knowledge/recipe_journal.py
Code metrics gate: PASSEDJudged: the 10 files this change touches. Files it does not touch never fail it; anything found there is listed under Outside this change for information.
59 findings in files this change does not touchOutside this change (informational): 59
...and 34 more; the job log lists every row.
Thresholds and sources: |
…w in its own module The read-modify-write an amend performs is one job: the caller's overrides win, each field it left alone keeps the value the live row carries, and the extras another session wrote survive untouched. That rule, its table of the fields a recipe carries and the row-supersession it shares with the close path now live in `_recipe_amend.py` rather than among the journal's own helpers. No observable change: internal structure only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
chaojhou
left a comment
There was a problem hiding this comment.
PR #2011 -- refactor(knowledge): build the recipe journal and KB writes from named parts
What it does: PR #2011 splits the over-complex functions in knowledge/recipe_journal.py (finalize_recipe_and_journal, kb_amend_recipe, _record_config_attempts, the recipe-attrs/workload-tag/kernel-opt/provenance projections, the explore-grid loop in fact_write_hook, _record_fact_per_variant) into named helpers and three new sibling modules: _recipe_attrs.py, _config_timeline.py and _recipe_amend.py. It also folds the three authority-row reads into one _authoritative_recipe_row helper. I ran a differential harness over 600 random states and found the extraction behaviour-equivalent apart from log text, error-dict key order and to_float edge cases. The PR still cannot merge: it conflicts semantically with main, because #1986 removed the swallow handlers that this PR consolidates and #1985 added benched=True to a function this PR moved.
Blocking issues: 2
-
[conflict-main-1986-1985] CONFLICTING with main: taking the PR side reverts #1986's no-swallow fix and silently drops #1985's benched=True [verified]
Problem: The PR's mergeable state is CONFLICTING (3 files), and the conflict is semantic. Main's 84bcdfb (#1986) deleted the broad except handlers around get_authoritative_recipe and put_recipe in read_local_recipe_row, kb_amend_recipe and the finalize local path. This PR collects those same handlers into a new swallowing helper, _authoritative_recipe_row (except Exception -> {} with an INFO log), and keeps the put_recipe try/except. Main's 00422ae (#1985) added benched=True to the config-arm record_attempt call, which this PR moved to _config_timeline.py. src/hyperloom/orchestrator/knowledge/recipe_journal.py:1229
Impact: Recipe store read and put failures are swallowed again, which undoes #1986 and fails its new propagation tests. LocalRecipeStoreError is no longer imported while _record_fact_impl's merged handler still names it, so the first store failure raises NameError. Config-arm attempts lose benched=True, which the #1985 variant funnel counts on (framework_event.py iterates adopted/attribution_eligible/benched). Nothing flags this because _config_timeline.py does not conflict.
Action: Rebase onto main. Remove the try/except from _authoritative_recipe_row (or inline the plain read), drop the put_recipe try/except in kb_amend_recipe, keep the LocalRecipeStoreError import, add benched=True in _config_timeline._attempt_fields, regenerate code_metrics_baseline.txt from main's numbers, then update the description, which still claims three try/except reads were unified. -
[desc-unstated-log-and-coercion] Description claims a single log-line difference; other operator-visible changes are unstated [verified]
Problem: The description says the only behavioural difference is a new INFO log in the KEEP path's read_local_recipe_row. The differential harness found more than that. The INFO messages operators may grep for, 'kb_amend_recipe: authority get_recipe failed (...); proceeding with empty live' and 'recipe read failed (...); finalize appends the current session only', are both replaced by 'recipe authority read failed for cid=... (...); proceeding without it'. The remote-finalize error outcome dict changes key order. The integrate-ledger float coercion now goes through to_float (_recipe_attrs.py:178/191/208, _recipe_amend.py:83), which maps bool and non-finite values to 0.0 where base kept them. The description also says two modules are added and the file goes from 1785 to 1485 lines; at head it adds three modules (_recipe_amend.py too) and goes from 1800 to 1376 lines. It also names a remaining _record_fact_impl baseline entry that does not exist at head. src/hyperloom/orchestrator/knowledge/recipe_journal.py:1238
Impact: An operator grepping the old log strings stops matching. The persisted micro_speedup, e2e_gain_pct and e2e_tput values differ for non-finite inputs. The description calls the change 'same fields with the same values' and so misstates it.
Action: Restate the Observable effect section: list the changed log texts and the to_float normalisation, and fix the module, line and baseline counts. After the rebase onto main, most of these log lines disappear (see the conflict finding), so rewrite the description after resolving it.
Checked: RecipeJournalCollaborator.fact_write_hook (+ _record_grid_facts, _grid_attempt_row, _result_payload) (yes); _record_config_run / _record_config_attempts -> _config_timeline.record_config_run / record_config_attempts (yes); RecipeJournalCollaborator._record_fact_per_variant (+ _variant_verdict/_variant_measurement/_variant_failure/_variant_ch (yes); _collect_workload_tags -> _recipe_attrs.workload_kb_tags (yes); _build_kernel_optimizations_from_state -> _recipe_attrs.kept_kernel_optimizations (no); _collect_attempt_provenance -> _recipe_attrs.attempt_provenance (yes); _build_recipe_attrs_from_state -> _recipe_attrs.recipe_attrs_from_state (yes); finalize_recipe_and_journal (+ _unvalidated_stack_outcome, _finalize_journal, _publish_remote_recipe, _final_recipe_over (unsure); kb_amend_recipe (+ _amends_local_recipe, _framework_identity, _recipe_amend.amended_recipe_fields/amended_recipe_extras/ (yes); read_local_recipe_row (no); workload_canonical_id (yes); improved_best_config (from finalize local path) (no) | Conflict with main semantic | Base: ef93838 | Head: 20d0823
…knowledge # Conflicts: # scripts/code_metrics_baseline.txt # src/hyperloom/inference_optimizer/tests/test_coordinator_kb_writes.py # src/hyperloom/orchestrator/knowledge/recipe_journal.py
|
Requesting changes: resolving the conflict with main as written silently reverts two merged fixes, the description does not match the diff, and production code grows by 23%. Production code: +1363/−944 (net +419), functions 31 → 83, 7 new classes. Most of the growth wraps redundant normalization in named helpers. Bring this to net-zero or negative by deleting those mechanisms, not by relocating them. Blocking
Simplify instead of relocate
|
…r the merge Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two wrappers whose whole body was a dict literal, one whose body was two recorder calls, a NamedTuple built in one place and unpacked in the next, and a classmethod constructor with one caller. Each is inlined at its only call site; no function crosses the complexity ceiling as a result. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Thanks — all three points addressed at 1. The merge silently reverted two merged fixes. Correct, and the resolution was wrong in both directions.
2. The description does not match the diff. Rewritten against the current head. It had said two new modules when there are three, carried line and complexity numbers from an earlier push, and claimed "no observable change" while two things had changed. Both are now stated and both are reverted:
3. Production code grows by 23%. Fair, and now 21% after deleting five single-use units rather than moving them: two wrappers whose whole body was a dict literal, one whose body was two recorder calls, a
The executable growth is the parameter lists and returns of the units the ceiling required; the rest is each unit's docstring and its two PEP 8 blank lines, plus three module headers. Three duplicated mechanisms are gone outright — the twice-derived framework/version/precision identity, the thrice-written float coercion, and the three near-identical authority-row handlers (that last one jointly with #1986). Nothing further can be inlined without putting a function back over the ceiling: The three new modules import only downward and none imports |
…ee on to_float is not a drop-in replacement for a guarded float(x or 0.0): it returns its default for inf, nan and bool. Pinning all three keeps the next swap from being silent, and says so in recorded_float's own docstring. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Follow-up on the It was not a rendering decision — it was a swap. The integrate ledger's four figures each carried their own
So Fixed by de-duplicating onto Same conclusion was reached independently on #1958 and #1995. |
PR #2011 -- Step 7 independent reader, runA second reader, given the diff, The card's two findings, re-tested by behaviour rather than ancestry. A statement that ended up on the other side of the check protecting it. The untouched regions A shared-helper swap that changes the domain. Checked: |
|
Production code is still too large. The net growth is +393 lines (it was +419), and it needs to keep shrinking before this can merge. I classified every changed line in the touched production files with an AST pass (merge-base vs head
Functions go from 39 to 86, record types from 2 to 8, and total parameters from 64 to 158. Of the 59 new definitions, 38 are referenced exactly once in The logic barely changed (+0). The growth is the scaffolding around single-use units: their signatures, docstrings, fields and spacing (about +390 lines). That is the same control flow in more places, not a simpler one (AGENTS.md Simplify by removing a mechanism). The parameter-count and tuple-return limits in Cut next:
Target: production net ≤ 0, or name each remaining new type in the description with what it removes. |
…knowledge # Conflicts: # src/hyperloom/orchestrator/knowledge/recipe_journal.py
…knowledge # Conflicts: # scripts/code_metrics_baseline.txt # src/hyperloom/orchestrator/knowledge/recipe_journal.py
…ary line The restructuring added helpers, and each arrived with a multi-paragraph block. Reduce every added helper to its summary line, keeping only the sentences that state an invariant or a constraint from outside the file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e made stale Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fact_write_hook, _record_fact_impl and _record_config_run were inside every repo threshold before this branch, so they read as they did: the grid loop and the provenance dict are back in their callers and the configuration arm's run row is back beside them. Four more single-use units -- a dict-literal wrapper, a two-call wrapper and two predicates -- go the same way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…knowledge # Conflicts: # scripts/code_metrics_baseline.txt
…scaffolding The split answered the complexity ceiling by relocation: `_config_timeline.py`, `_recipe_amend.py` and `_recipe_attrs.py` held 38 helpers, 29 of them with one caller, each carrying a docstring restating its name and two blank lines. All three are inlined back into `recipe_journal.py`, and what only existed to carry arguments across the module boundary is deleted with them: - `_ConfigAttemptContext`, `_attempt_accuracy` and `_attempt_artifacts`; - the caller-side `str(x or "")` on every field `record_attempt` already projects through `event_fields.text`, and the empty-gate skip `record_attempt_gate` already makes; - `_IntegrateIndex`, nine single-use constants, `_model_identity_tags`, `_last_stack_launch_args`, `_stack_action_names`, `_stack_entry_source`, `amend_provenance` and `improved_best_config`; - every docstring that restated a private helper's name; the invariants the longer ones carried are kept as comments. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…he length gate Its docstring restated each parameter name; only the no_promote rule said something the signature could not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…knowledge #2055 deletes state nothing reads, and this branch had relocated one of the readers: _build_kernel_optimizations_from_state became the module-level kept_kernel_optimizations with _index_integrate_attempts, _kernel_optimization_row and _last_rebenched_throughput beside it. A textual merge would have resurrected the whole join over the deleted kernel_opt_task_attempts ledger. Main's deletion wins and is re-applied to the branch's shape: those four units, the kernel_optimizations attrs key and the extras merge that carried it into the KB row are gone, and the config arm's attempt now copies the attribution explore stored instead of re-deriving it (_attempt_failure no longer takes the outcome). The two conflicted tests take main's side: it replaced the recipe-attrs kernel_optimizations assertions with one that pins an earlier session's rows being carried forward, and deleted the e2e_decision tests of the retired join. The branch's move from _build_recipe_attrs_from_state to recipe_attrs_from_state is carried across in the surviving cases. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…knowledge Only scripts/code_metrics_baseline.txt conflicted. Resolved with the ratchet rule: whichever side records the lower value wins, an entry the merge base had and a side dropped is retired, an entry absent from the base is kept. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…knowledge Brings the branch up to origin/main; no path conflicted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…knowledge Brings the branch up to origin/main; no path conflicted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…documented twice The module carried a second copy of several things it already had. The framework / version / precision identity was derived at two sites with the same fallback chain around each; `_extra_server_args` re-implemented `common.coerce.to_str_list`'s whole None / str / list / scalar ladder; four figures were parsed through their own `try: float(x or 0.0)` blocks, and the `gain_pct` / `delta_pct` fallback was an open-coded `first_float`. Deleted with them: the `isinstance` guards whose producers are all typed (`lever_kind_for_task`'s two dict arguments, the variant metrics checked twice, and the tuple shape of a cache this class is the sole writer of), and eleven docstring `Args:` / `Returns:` blocks that restated the parameter names and the body back to the reader. What those docstrings said that the code cannot is kept. The module goes from 1671 lines to 1654 and its baseline entry comes down with it; production lines across both changed files are -4 against main. No observable field, value or log line changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…es their own modules recipe_journal.py carried the FRAMEWORK_AGENT configuration-arm attempt rows and the session's recipe attributes alongside the journal itself. Each is a complete job with one entry point, so each becomes its own module and the journal keeps only what it owns. _NON_ATTEMPT_OUTCOMES travels with the attempt rows that read it, so the dependency points one way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…knowledge # Conflicts: # scripts/code_metrics_baseline.txt
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
xiaofei-zheng
left a comment
There was a problem hiding this comment.
What this PR does
Nine functions in src/hyperloom/orchestrator/knowledge/recipe_journal.py sat above the repo's complexity ceiling and the module was at its baselined length, so this names the parts each of them was already built from and moves two coherent groups sideways: _recipe_attrs.py (the recipe-shaped projections of SharedState — workload tags, attempt provenance, recipe attrs) and _config_timeline.py (the configuration arm's attempt rows on the FRAMEWORK_AGENT event). The close path splits into _publish_remote_recipe / _write_local_recipe / _final_recipe_overrides, the amend builds its put_recipe arguments from one field table, and recorded_float goes to hyperloom.common.coerce as the one home for the guarded float(x or 0.0) form that keeps inf, nan and bool as recorded.
Blocking issues (1)
1. The description still describes an earlier revision of this branch.
body.txt and the diff at feda1ad2 disagree on most of the structural facts:
- "Three coherent groups move to sibling modules" lists
_recipe_amend.py(141 lines). That file does not exist;rows_not_superseded,amended_recipe_fieldsandamended_recipe_extrasstayed inrecipe_journal.py:244-290. The later commitrefactor(recipe-journal): delete the three sibling modules and their scaffoldingundid that direction and the description was never updated. _recipe_attrs.pyis given as 408 lines and_config_timeline.pyas 252; they are 242 and 169.- The Tests section says
_build_kernel_optimizations_from_stateis now a module function in_recipe_attrs. No such symbol exists anywhere in the tree, and_recipe_attrscarries no kernel-optimization projection. - "The three integrate-ledger figures and the live-row throughput comparison use it" —
recorded_float's only call sites arerecipe_journal.py:1079,:1209and:1210, all throughput comparisons.micro_speedup,e2e_gain_pctande2e_tputare untouched by this diff, so thetry: float(x or 0.0)rationale the section is built on describes code that is not here. - Metrics:
recipe_journal.pyis given as 1786 → 1362 and the baseline entries as 13 → 1. This PR's ownscripts/code_metrics_baseline.txthunk says-module-lines ... 1671/+... 1269, and the entry count goes 12 → 1. - Line accounting: "the four production modules total 2163 lines against
main's 1786, which is +21%". Three production modules total 1680 against 1671 — the split is close to line-neutral, not +21%. The executable / docstring / blank breakdown is derived from the same stale figures.
A reviewer sizing the risk of this refactor is told about a module split and a line-growth profile that did not happen, and is pointed at coercion call sites the diff never touches. Please rewrite the description against the current head.
What was checked
recipe_journal.py compared field by field against the merge base e34bb7af: the close path, kb_amend_recipe (24-key reserved set, 21 put_recipe kwargs, the 11 per-field defaults), _record_fact_per_task / _record_fact_per_variant, extract_kept_best_config, workload_canonical_id, read_local_recipe_row; _recipe_attrs.py and _config_timeline.py against the helpers they were moved from; common/coerce.py (recorded_float, first_float, to_str_list, to_mapping); breakdown/recorder/framework_event.py (_ATTEMPT_FIELDS, _ATTEMPT_BLOCKS, record_attempt, record_attempt_gate); orchestrator/lever.py; session/optimization_journal.py.
The no-observable-change claim holds on every path read. In particular: the record_attempt fields that are now passed raw instead of str(x or "")-coerced are re-coerced identically by _ATTEMPT_FIELDS / _ATTEMPT_BLOCKS; the gate row the base skipped on an empty gate name is dropped by record_attempt_gate itself; _extra_server_args's switch to to_str_list strips a string payload that every call site already strips; amended_recipe_fields' type(default)(live_value) reproduces the base's per-field dict() / list() / str() / float() exactly; first_float(gain_pct, delta_pct) is the base's two-step fallback; narrowing the remote publish's try is safe because _record_remote_recipe_audit catches Exception itself. Dangling-reference check: the three deleted private helpers have no surviving caller outside the four migrated tests.
No local test run — all 12 test shards plus ruff, pylint, mypy, bandit, CodeQL, import-linter and code-metrics are green at this head.
Resolved: the description has been rewritten against the current head.
xiaofei-zheng
left a comment
There was a problem hiding this comment.
No blocking issues.
The one finding from my earlier review — the description describing an earlier revision of this branch — is resolved: the body now matches the diff at feda1ad2 (two sibling modules at their real line counts, 12 to 1 baseline entries, recipe_journal.py 1671 to 1269, production net +22, and recorded_float's three actual call sites). The earlier reviews' blocking items are resolved at this head too: _authoritative_recipe_row and the broad handlers it revived are gone, LocalRecipeStoreError is imported at recipe_journal.py:32 and caught at :559, #1985's benched=True is set in _config_timeline._attempt_fields, the baseline is regenerated, and _ConfigAttemptContext, _IntegrateIndex, _E2eVerdict and AttemptProvenance.unrecorded no longer exist.
Checked: recipe_journal.py compared field by field against the merge base e34bb7af — the close path (_unvalidated_stack_outcome / _publish_remote_recipe / _write_local_recipe / _final_recipe_overrides), kb_amend_recipe (the 24-key reserved set, the 21 put_recipe kwargs, the 11 per-field defaults), _record_fact_per_task / _record_fact_per_variant, extract_kept_best_config, workload_canonical_id, read_local_recipe_row; _recipe_attrs.py and _config_timeline.py against the helpers they were moved from; common/coerce.py (recorded_float, first_float, to_str_list, to_mapping); breakdown/recorder/framework_event.py (_ATTEMPT_FIELDS, _ATTEMPT_BLOCKS, record_attempt, record_attempt_gate); orchestrator/lever.py (lever_kind_for_task); session/optimization_journal.py (OUTCOME_KEEP / OUTCOME_REVERT).
The no-observable-change claim holds on every path read. Specifically: the record_attempt fields now passed raw instead of str(x or "")-coerced are re-coerced identically by the recorder's own projections; the gate row the base skipped on an empty gate name is dropped by record_attempt_gate itself; _extra_server_args's switch to to_str_list strips a string every call site already strips; amended_recipe_fields' type(default)(live_value) reproduces the base's per-field dict() / list() / str() / float(); first_float(gain_pct, delta_pct) is the base's two-step fallback; narrowing the remote publish's try is safe because _record_remote_recipe_audit carries its own except Exception. Dangling-reference check: the three deleted private helpers have no surviving caller outside the four migrated tests.
Ran: no local suite — all 12 test shards plus ruff, pylint, mypy, bandit, CodeQL, gitleaks, import-linter, REUSE and code-metrics are green at this head.
LGTM
src/hyperloom/orchestrator/knowledge/recipe_journal.pycarried twelve entries inscripts/code_metrics_baseline.txt: eleven cognitive-complexity and function-length exemptions across seven functions, plus the module-line count. The weight was in boolean operands and ternaries — long chains ofstr(x or "")defaulting andoverride if the caller set it else the live value— not in branching, so this names those idioms once instead of restructuring control flow. One entry remains, the module-line count, lowered to match.What the module no longer does twice:
workload_canonical_idandkb_amend_recipe;_framework_identityderives it once;PRESERVED_RECIPE_FIELDSstates each field with the empty valueput_recipefalls back to, andamended_recipe_fieldsreads it;_extra_server_argsreimplementedto_str_list's isinstance ladder;float(x or 0.0)coercions were written out inline.recorded_floatinhyperloom.common.coerceis that form named once, and it is notto_float: the two disagree oninf,nanandTrue, whereto_floatreturns the default.recorded_floathas three call sites, all throughput comparisons (recipe_journal.py:1079,:1209,:1210).TestRecordedFloatpins the three inputs they disagree on so a later swap is not silent.Two groups that never needed the collaborator move to sibling modules, because the module was already at its baselined length:
_recipe_attrs.py(242 lines) — the recipe-shaped projections ofSharedState: the workload tags, the attempt provenance, and the recipe attrs themselves;_config_timeline.py(169 lines) — the configuration arm's attempt rows on theFRAMEWORK_AGENTevent.Both depend only downward (
hyperloom.common,hyperloom.inference_optimizer, the knowledge package's ownworkload_tags) and neither importsrecipe_journal, so the split adds no cycle.recipe_journal.pygoes 1671 → 1269 lines; the three production modules total 1680 against main's 1671, and withcoerce.py's new primitive the production net is +22 lines.Upstream work this branch merges in and carries through the split unchanged: #1985's
benched=Trueon the configuration arm's attempt row, now set in_config_timeline._attempt_fields; and #1986's propagation of recipe-store read and write failures, including the narrowLocalRecipeStoreError/OSError/ValueErrorhandler on the per-variant KB write. The three broad handlers #1986 deleted are absent here.None.
workload_kb_tags,attempt_provenanceandrecipe_attrs_from_statewere private helpers on the collaborator and are now module functions in_recipe_attrs, re-exported throughrecipe_journal.__all__.test_coordinator_kb_writes.py,test_coordinator_sync_helpers_coverage_unit.pyandtest_recipe_sedimentation.pycall them there, with every assertion carried across unchanged.test_remote_read_is_wired_into_cli_t0_and_close_write_remainspins that the close path still publishes throughHyperloomRemoteKB.from_env().write; it now inspects_publish_remote_recipe, where that call lives.TestRecordedFloatintest_coerce.pyis new. #1986's own tests run unchanged against this tree. Full CI.This PR is the split. Seven functions were over the complexity or length trigger; the code-metrics gate reports 0 new and 0 worse across the ten touched files. Three record types are added:
AttemptProvenancereplaces a bare three-element tuple returned and unpacked across modules, and_FrameworkIdentityand_VariantMeasurementeach replace a group of locals threaded into one call.Removed: the duplicate identity derivation, the eleven-way field ternary, the inline
extra_server_argsladder and the four inline float coercions, and the five single-use units an earlier revision of this branch had added. Preserved: theput_recipekeyword set, theJournalEntryfield set and the 24record_attemptkeys are identical to main's, which the tests above exercise.None. Every journal row, KB lesson, KB pitfall, recipe amend and framework-event row carries the same fields with the same values, and no log line's wording, level or presence changes. The fields
_config_timelinenow passes torecord_attemptwithout a caller-sidestr(x or "")are coerced identically by that recorder's own_ATTEMPT_FIELDS/_ATTEMPT_BLOCKSprojections.Breaking changes: no
PR addresses single concern: yes
Root cause is upstream: no