Repository navigation
refactor(framework-phase): read a model-authored payload through one coercion module - #1997
Conversation
…ed reader A specialist deliverable, an executor result and a task's dispatch params are model-authored JSON: any key may be absent, null or of another type than the phase expects. Each call site coerced that itself, so the rule was restated wherever a field was read. No observable change: internal structure only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… module and name each routing decision The FRAMEWORK_AGENT phase handler held both the decisions it makes and the breakdown-timeline writes that record them. The timeline writers move to ``_framework_timeline``, which is the only place that touches the recorder, and the phase keeps the decisions. Inside the phase, each read of a model-authored payload now goes through the shared reader, and the conditions the routing turns on are named: whether a deliverable routes to integrate_patch, whether an ENABLEMENT round only asks for a targeted build, which candidate an authored patch belongs to, which batch an outcome is attributed to, and whether an adoption has already had the last word on a candidate. No observable change: internal structure only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Code metrics gate: PASSEDJudged: the 5 files this change touches. Files it does not touch never fail it; anything found there is listed under Outside this change for information.
85 findings in files this change does not touchOutside this change (informational): 85
...and 60 more; the job log lists every row.
Thresholds and sources: |
… mapping hands back Keeps the key at the ``.get`` that fetches it, and lets the caller pick the basis an adoption is recorded under instead of the recorder branching on a flag. Splits the repeated-review guard and the pre-screen dedup out of candidate submission. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…on the stand-in that drives it Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
chaojhou
left a comment
There was a problem hiding this comment.
PR #1997 -- refactor(framework-phase): split the FRAMEWORK timeline writes out and name each routing decision
What it does: PR #1997 is meant as a pure refactor. It splits the FRAMEWORK_AGENT phase handler into named helpers, moves the breakdown-timeline writers into a new phases/_framework_timeline.py, and adds phases/_payload.py so model-authored fields are read through one rule (as_text/as_mapping/as_entries/...). Most of the split is equivalent to the base, but the PR conflicts semantically with main because #1997 moved _record_source_attempt while 00422ae edited it. One payload reader also drops string-valued changed_files/gap_keywords that the base kept.
Blocking issues: 1
- [conflict-benched-lost] Merging main by taking the PR side silently drops 00422ae's
benchedfield from source-attempt rows [verified]
Problem: mergeable=CONFLICTING. The PR moved _record_source_attempt out of framework.py. Main commit 00422ae addedbenched=bool(result.get(RESULT_BENCHED))to the record_attempt call in the old location. The moved copy in _framework_timeline.py (record_attempt kwargs around L340-376) does not have it, and git only flags the deleted block in framework.py, not the new file. src/hyperloom/orchestrator/phases/_framework_timeline.py:370
Impact: Every source-arm attempt row losesbenched, and the breakdown variant funnel reverts to outcome-independent fallback reading for those rows. That undoes 00422ae's fix: a source patch that failed before the bench is counted as a failed bench run again. RESULT_BENCHED is left as an unused import in framework.py.
Action: Merge origin/main and portbenched=bool(result.get(RESULT_BENCHED))into _framework_timeline._record_source_attempt (importing RESULT_BENCHED there). Move the import out of framework.py if nothing else uses it.
Checked: _framework_config_levers_from_done (+_lever_envs, _lever_server_args) (yes); FrameworkPhase._candidates_from_discovery_proposals (no); FrameworkPhase._enqueue_framework_agent_authoring_specialist (+_critic_feedback_lines) (yes); FrameworkPhase._maybe_rearm_authored_lane (+_count_apply_failure, _queue_apply_fail_retry) (yes); FrameworkPhase._enqueue_author_specialist (+_apply_failure_feedback_lines, _specialist_params, _reauthor_framework_candi (yes); FrameworkPhase._drain_apply_fail_retry_pending (yes); FrameworkPhase._enqueue_framework_agent_local_explore_specialist (+_next_local_explore_candidate_id, _prior_framework_at (yes); FrameworkPhase._submit_framework_agent_candidate_for_review (+_review_already_pending, _charge_review_submission) (yes); FrameworkPhase._stamp_framework_progress / ProgressStamp.as_row / disposition (yes); FrameworkPhase.maybe_reauthor_from_critic_feedback (+_reauthor_target, _candidate_is_materializing, _close_out_spent_rea (yes); FrameworkPhase._record_framework_agent_authored_outcome (+_authored_candidate_id, _latest_batch_id, _clear_superseded_pr (yes); FrameworkPhase._recover_framework_agent_authoring_outcome (+_replay_specialist_deliverable, _replay_authored_integrates, (yes); FrameworkPhase._record_framework_agent_authoring_empty_outcome (+_routes_to_integrate_patch, _empty_outcome_status) (yes); FrameworkPhase.maybe_autosubmit_specialist_patches (+_specialist_file_bases, _patches_on_disk, _round_deliverable, _with (yes); FrameworkPhase.maybe_autosubmit_framework_config (+_asks_only_for_a_targeted_build) (yes); _record_run/_record_step/_record_review_outcome/_settle/_patch_material/_source_action_reasoning (moved) (yes); _record_source_attempt (+_patch_trail, _attempt_measurement, _attempt_accuracy, _attempt_failure, _adoption_basis, _reco (unsure); _record_discovered, _record_kb_exposure (moved) (unsure) | Conflict with main semantic | Base: ef93838 | Head: 9c72047
…phases-1 # Conflicts: # scripts/code_metrics_baseline.txt # src/hyperloom/orchestrator/phases/framework.py
|
Requesting changes: this conflicts with main in a way that would silently drop a field, it changes behaviour despite "No observable change", and it adds a third copy of the coercion helpers. Production code: +1291/−1038 (net +253). A complexity refactor should shrink production code. Bring this to net-zero or negative by deleting mechanisms (below), not by moving them. Blocking
Simplify instead of relocate
Tests: drive a real Metrics: |
…ged tree Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ine alias of it Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e, not a third copy of it Delete `phases/_payload.py`. Its readers duplicated `common.coerce.to_str_list` and `breakdown/recorder/event_fields.as_dict`/`as_list`, and its list reader handled a bare string the opposite way: a specialist that writes `gap_keywords: "fp8"` had the keyword dropped instead of wrapped, so the upstream-PR KB record lost the term it is later searched by. `coerce` gains the three readers it was missing beside `to_str_list` and `first_float`/`first_int` -- `first_str`, `to_mapping`, `to_list` -- and the phase reads every model-authored field through them. `task_id_of` / `params_of` / `payload_of` go with the module: they were attribute reads wearing a name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…at defines them The gate caught the inlining: spreading ``getattr(task, "task_id", "")`` across the phase wrote the attribute name three times in one new module and seventeen across the two. The registry owns ``Task``, so it answers for how to read a row a lookup may not have returned, and the name is written once. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Thanks — all three blocking items are fixed at 1. 2. String-valued 3. Not folded in yet: Deletion, not relocation. The number I should have led with: Metrics. Net production lines. Still positive: Where I have not acted, and why:
CodeQL's cyclic-import alert on |
|
Follow-up: CI is green at One more pass I owed this diff, since a move refactor's characteristic defect is not a logic error but a statement that ends up on the other side of the check that was protecting it. For each of the 13 functions in the No effect is less guarded than it was on
Two statements genuinely did move, and both are safe for a reason rather than by inspection:
I also checked the two idempotency keys this area builds ( |
|
Production code is still too large. The net growth is +254 lines (it was +253), 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 117 to 163, record types from 6 to 8, and total parameters from 203 to 277. Of the 48 new definitions, 38 are referenced exactly once in The logic barely changed (+12). The growth is the scaffolding around single-use units: their signatures, docstrings, fields and spacing (about +297 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. |
…phases-1 # Conflicts: # src/hyperloom/orchestrator/phases/framework.py
…phases-1 # Conflicts: # src/hyperloom/orchestrator/phases/framework.py
… line The restructuring added helpers, and each arrived with an Args/Returns-style 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>
…seed and an apply-retry lane through one reader each Collapses the three duplicated "args cannot be transported" warning paths in the config-lever reader into one, routes an apply-failure retry to the lane that owns it instead of branching inside one body, and reads an untrusted mapping/list/string field through hyperloom.common.coerce rather than a hand copy of the same guard at each site. Adds first_str, to_mapping and to_list to hyperloom.common.coerce beside the existing first_float/first_int/to_str_list. No observable change except the three config-lever warnings, which now render from one format string with the same text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…come and a recovery replay through one reader each Splits the needs_review re-author path into the target it resolves, the cap it settles and the dispatch it records; splits the delegated-result recovery into the specialist deliverable it replays and the integrate_patch outcomes it replays; and drops the progress-list normalisation the stamp already performs. No observable change: internal structure only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ates, and surface a specialist's patches, through one reader each Splits the source-arm attempt recorder into the attempt record it writes and the gate verdicts its measurement settled, drops the duplicated "claimed patches" list in the autosubmit observation, and removes the third copy of the companion-lever rule that `_framework_config_levers_from_done` already owns. Retires all fifteen of framework.py's code-metrics baseline entries and lowers its module-lines entry to 3067. No observable change: internal structure only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…phases-1 # Conflicts: # scripts/code_metrics_baseline.txt
…s through ``first_str``, ``to_mapping`` and ``to_list`` are exported from ``hyperloom.common.coerce`` alongside readers that each have a test; these had none. Pin the cases a caller depends on: ``first_str`` skips a falsy ``0`` rather than returning it, ``to_mapping`` and ``to_list`` copy rather than alias their input, and ``to_list`` never explodes a string into characters. Lower the ``framework.py`` module-lines baseline entry to the length the merged tree actually has. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ZhengGong-amd
left a comment
There was a problem hiding this comment.
PR #1997 -- refactor(framework-phase): read a model-authored payload through one coercion module
What it does: Restructures FrameworkPhase in phases/framework.py so model-authored payload fields are read through hyperloom.common.coerce, which gains first_str, to_mapping and to_list, and splits the long routing functions into named helpers. Production code is net -67 lines, behaviour is unchanged, and 15 framework.py entries leave the complexity baseline.
Blocking issues: 2
-
[X2] The Observable effect section describes a fix and a test that are not in the diff [verified]
Problem: The body says a bare-stringgap_keywords/changed_files"now keeps that value", is "read as a list of text where the candidate is built", is pinned bytest_a_discovery_proposal_may_name_its_keywords_and_files_as_one_string, and that "Readinggap_keywordsthroughto_str_list... fixes the observable effect above". At the head:git grep may_name_its_keywords d048533bereturns nothing; the test does not exist.git diff 3a548190f...d048533be -- src/hyperloom/orchestrator/phases/framework.pyhas no +/- line containinggap_keywords,changed_filesorto_str_list.- src/hyperloom/orchestrator/phases/framework.py:15 imports only
first_str, to_float, to_list, to_mappingfrom coerce, notto_str_list. - src/hyperloom/orchestrator/phases/framework.py:2736 and :2738 still read
entry.get("changed_files") or []/entry.get("gap_keywords") or [], and :745 still readscand.get("changed_files") or [], identical to main. - Accurate fact: the PR is an internal refactor with no operator-observable change.
Impact: The release notes would tell operators that a discovery specialist's bare-stringgap_keywords/changed_filesis now normalized on the candidate and the KB record is searchable by that keyword; that code path is byte-for-byte unchanged from main, so operators would rely on a fix that did not ship.
Action: Replace the Observable effect section with "None: internal refactor, no operator-observable change", and delete the What changed sentence claiming that readinggap_keywordsthroughto_str_listfixes it. Write the body against git diff origin/main...HEAD only; every name and number must be reproducible from that diff.
-
[X2] Five numbers in What changed and Code metrics do not match the diff [verified]
Problem: Each quoted figure against what the head diff shows:- "
test_coerce.pyadds 13 tests": it adds 11 (TestFirstStr 5, TestToMapping 2, TestToList 4), src/hyperloom/inference_optimizer/tests/test_coerce.py:172, :190, :202. - "The baseline loses 16 entries": 15 are removed (5 cognitive-complexity, 10 function-lines) and 1 is lowered (module-lines framework.py 3139 -> 3062), scripts/code_metrics_baseline.txt:749.
- "brought thirteen functions off the complexity baseline": those 15 entries cover 11 distinct functions.
- "written 270 times ... it is now written 111. The 159 that went": in framework.py
or ""/or []/or {}goes from 277 to 171 occurrences (106 removed, 0 added). - "except for two already on the baseline at their recorded 85 and 87 lines": the head baseline has no function-lines entry for framework.py; its longest function is
_record_framework_agent_authoring_empty_outcomeat 80 lines (src/hyperloom/orchestrator/phases/framework.py:2501).
Impact: The release notes would report 13 new tests, 16 baseline entries, 13 functions, a 270 -> 111 idiom count and two framework.py functions still over the length limit; none can be reproduced from the diff, and the last one tells operators framework.py still carries function-length debt it no longer has.
Action: Correct the five figures to 11 tests, 15 entries removed plus 1 lowered, 11 functions, 277 -> 171 (106 removed), and "every function in framework.py is at or under the 80-line limit". Write the body against git diff origin/main...HEAD only; every name and number must be reproducible from that diff.
- "
Checked: PR body (live), src/hyperloom/orchestrator/phases/framework.py (imports, :745, :2736, :2738, function lengths), src/hyperloom/common/coerce.py, src/hyperloom/inference_optimizer/tests/test_coerce.py, scripts/code_metrics_baseline.txt | Ran: git diff 3a54819...d048533; git grep for the cited test; trial merge with origin/main b772e94 (clean); code_metrics --base-ref origin/main on the merged tree (PASSED); pytest on 20 related test files (758 passed); ruff check, ruff format --check | Base: 3a54819 | Head: d048533
Resolve the scripts/code_metrics_baseline.txt ratchet by keeping the lower value on each side. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Merging main shortened the module; the ratchet entry has to follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
chaojhou
left a comment
There was a problem hiding this comment.
PR #1997 -- refactor(framework-phase): read a model-authored payload through one coercion module
What it does: Moves the FRAMEWORK phase's reads of model-authored payload fields onto hyperloom.common.coerce, which gains first_str, to_mapping and to_list. It also splits the 11 framework.py functions that were over the complexity and length baseline into named helpers. For schema-conforming payloads, routing, retries, idempotency keys and recorded attempt rows are unchanged. The only difference is that a wrong-typed array or mapping field is read as empty, as the description states.
Blocking issues: 1
- [X2] The description gives the wrong module-length baseline number for the current head [verified]
Problem: The body says the baseline "lowers the module-length entry from 3139 to 3062". The head commit ec8b8c1 changed that entry to 3059 after the body was written, and framework.py is 3059 lines at head. scripts/code_metrics_baseline.txt:731
Impact: The release cut copies this description, so it would publish a figure that matches neither the baseline nor the file. Every other number in the body checks out against the diff.
Action: Edit the body to say "from 3139 to 3059".
SKIPPED: V4 -- "# PARTIAL: #1869 / #1276 / #1154 / #1032 ... only its first 100 were compared"; overlap with those PRs was not checked
Checked: framework.py base vs head, unit by unit: _framework_config_levers_from_done/_config_lever_from_proposal, _enqueue_framework_agent_authoring_specialist/_critic_feedback_lines, _maybe_rearm_authored_lane, _enqueue_author_specialist/_apply_failure_feedback_lines/_authoring_specialist_params/_reauthor_framework_candidate/_reauthor_explore_patch, _enqueue_framework_agent_local_explore_specialist/_record_local_explore_proposal/_claim_local_explore_specialist, _submit_framework_agent_candidate_for_review/_review_already_pending/_charge_review_submission, maybe_reauthor_from_critic_feedback/_reauthor_target/_candidate_is_materializing/_close_out_spent_reauthor_budget, _record_framework_agent_authored_outcome/_clear_superseded_progress/_record_authored_attempt, _record_source_attempt/_attempt_outcome_fields/_record_attempt_gates, _recover_framework_agent_authoring_outcome/_replay_specialist_deliverable/_replay_authored_integrates, maybe_autosubmit_specialist_patches/_patches_on_disk. Also checked common/coerce.py, test_coerce.py, scripts/code_metrics_baseline.txt, and loop/verdicts.py serialize_verdict_advisory (the advisory producer) | Ran: a base-vs-head differential of _framework_config_levers_from_done (104 input combos, outputs and log lines identical); a base-vs-head differential of _record_source_attempt with a stub recorder (40 combos, recorder calls identical); an ast function-length scan; gh pr view 1997 (live body and head). Local pytest could not run on this Windows host (POSIX-only fcntl and signal.SIGHUP), so the CI test shards green at head are relied on | Base: bb2da63 | Head: ec8b8c1
chaojhou
left a comment
There was a problem hiding this comment.
PR #1997 -- refactor(framework-phase): read a model-authored payload through one coercion module
What it does: Moves the FRAMEWORK phase's reads of model-authored payload fields onto hyperloom.common.coerce, which gains first_str, to_mapping and to_list. It also splits the 11 framework.py functions that were over the complexity and length baseline into named helpers. For schema-conforming payloads, routing, retries, idempotency keys and recorded attempt rows are unchanged. The only difference is that a wrong-typed array or mapping field is read as empty, as the description states.
Blocking issues: none
SKIPPED: V4 -- "# PARTIAL: #1869 / #1276 / #1154 / #1032 ... only its first 100 were compared"; overlap with those PRs was not checked
Checked: framework.py base vs head, unit by unit: _framework_config_levers_from_done/_config_lever_from_proposal, _enqueue_framework_agent_authoring_specialist/_critic_feedback_lines, _maybe_rearm_authored_lane, _enqueue_author_specialist/_apply_failure_feedback_lines/_authoring_specialist_params/_reauthor_framework_candidate/_reauthor_explore_patch, _enqueue_framework_agent_local_explore_specialist/_record_local_explore_proposal/_claim_local_explore_specialist, _submit_framework_agent_candidate_for_review/_review_already_pending/_charge_review_submission, maybe_reauthor_from_critic_feedback/_reauthor_target/_candidate_is_materializing/_close_out_spent_reauthor_budget, _record_framework_agent_authored_outcome/_clear_superseded_progress/_record_authored_attempt, _record_source_attempt/_attempt_outcome_fields/_record_attempt_gates, _recover_framework_agent_authoring_outcome/_replay_specialist_deliverable/_replay_authored_integrates, maybe_autosubmit_specialist_patches/_patches_on_disk. Also checked common/coerce.py, test_coerce.py, scripts/code_metrics_baseline.txt, and loop/verdicts.py serialize_verdict_advisory (the advisory producer) | Ran: a base-vs-head differential of _framework_config_levers_from_done (104 input combos, outputs and log lines identical); a base-vs-head differential of _record_source_attempt with a stub recorder (40 combos, recorder calls identical); an ast function-length scan; gh pr view 1997 (live body and head); re-checked the body at the same head after the X2 finding: it now reads "from 3139 to 3059", matching scripts/code_metrics_baseline.txt:731. Local pytest could not run on this Windows host (POSIX-only fcntl and signal.SIGHUP), so the CI test shards green at head are relied on | Base: bb2da63 | Head: ec8b8c1
LGTM
What changed
Refactor
FrameworkPhasepayload reads throughhyperloom.common.coerceand name the authoring, retry, review-submission and attempt-recording operations inframework.py. The coercion module addsfirst_str,to_mappingandto_listbeside its existing readers.The diff against
mainhas 67 fewer production Python lines (framework.py+561/-647 andcoerce.py+19). No module is added or removed.test_coerce.pyadds 11 tests: five forfirst_str, two forto_mapping, and four forto_list. The framework rewrite dispatch stub binds the two extracted methods. The metrics baseline removes 15 entries covering 11 functions and lowers the module-length entry from 3139 to 3059; every function inframework.pyis within the 80-line gate.Observable effect
Supported, schema-conforming payloads keep the existing routing, retry limits, candidate identity, measurement and accuracy fields. Source-attempt rows retain
benched,measured_against, configuration deltas, and outcome-independent attribution.Optional array reads now accept lists/tuples and otherwise use an empty list. At affected reads such as critic
required_evidence/risks, applied/reverted patch lists and retry feedback, malformed scalar values no longer flow through as an iterable of characters (or raise because they are not iterable). Mapping reads copy a mapping and otherwise use an empty mapping.first_strpreserves the former first-truthy-value rule, including skipping numeric zero.The discovery-proposal
gap_keywordsandchanged_filesreads are unchanged by this PR; this change does not normalize a bare string in either field.Size-exception rationale: the churn comes from applying the coercion readers and extracting branches across one phase implementation. Keeping the new readers, their callers, and the phase's retry/recording helpers together makes the complete candidate lifecycle reviewable without an intermediate duplicate implementation.
Validation
163 targeted Linux tests passed covering coercion, framework rewrite specialists, critic reauthoring, discovery retry and the framework timeline. The PR head contains
mainatbb2da63332dcbc46bdf10702c516ef43eb80a016; there are no missing main commits.