Skip to content

refactor(framework-phase): read a model-authored payload through one coercion module - #1997

Merged
chaojhou merged 30 commits into
mainfrom
refactor/chenyi/cc20-phases-1
Oct 22, 2026
Merged

chaojhou merged 30 commits into
mainfrom
refactor/chenyi/cc20-phases-1

Conversation

@chennyiiis

@chennyiiis chennyiiis commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

What changed

Refactor FrameworkPhase payload reads through hyperloom.common.coerce and name the authoring, retry, review-submission and attempt-recording operations in framework.py. The coercion module adds first_str, to_mapping and to_list beside its existing readers.

The diff against main has 67 fewer production Python lines (framework.py +561/-647 and coerce.py +19). No module is added or removed. test_coerce.py adds 11 tests: five for first_str, two for to_mapping, and four for to_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 in framework.py is 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_str preserves the former first-truthy-value rule, including skipping numeric zero.

The discovery-proposal gap_keywords and changed_files reads 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 main at bb2da63332dcbc46bdf10702c516ef43eb80a016; there are no missing main commits.

Chenyi Yan and others added 3 commits October 19, 2026 16:42
…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>
@chennyiiis
chennyiiis requested a review from a team as a code owner October 19, 2026 08:49
@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Code metrics gate: PASSED

Judged: 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.

Dimension Limit New Worse Baseline out of date Baseline entries vs base
Cyclomatic complexity fails > 20 0 0 0 45 +0
Cognitive complexity fails > 30 0 0 0 165 -5
Function length (lines) fails > 80 0 0 0 305 -10
Nested block depth fails > 5 0 0 0 19 +0
Parameter count fails > 5 0 0 0 89 +0
Module length (lines) warns > 800, fails > 1200 0 0 0 47 +0
Duplicated lines per file fails > 0 0 0 0 96 +0
Dead code (vulture) fails > 0 0 0 0 2 +0
Check Findings Effect
Comments 0
English only 0
Production code imports test code 0
Repeated literals 0
A1 Private name of another package 0
A8 Flag argument 0
A9 Tuple return 0
A10 Raw vocabulary comparison 0
A11 Hidden global write 0
A12 Pure forwarder 0
A12 Single-implementation abstraction 0
85 findings in files this change does not touch

Outside this change (informational): 85

Unit Dimension Baseline Now
src/hyperloom/orchestrator/state/shared_state.py resolve_graded_comparison Cognitive complexity absent 34
src/hyperloom_kb/collect/mapping.py _experience Cognitive complexity absent 31
src/kernelforge/cli/forge_loop_command.py forge_loop Cognitive complexity absent 31
src/kernelforge/loop/runner.py IterationLoop._collect_session_candidate Cognitive complexity absent 44
src/kernelforge/loop/runner.py IterationLoop._rule_on_attempt Cognitive complexity absent 38
src/hyperloom/inference_optimizer/cli/__init__.py _reexport_session_environment Function length (lines) absent 107
src/hyperloom/inference_optimizer/cli/__init__.py _resolve_agent_lineup Function length (lines) absent 89
src/hyperloom/inference_optimizer/cli/__init__.py _resolve_run_topology Function length (lines) absent 92
src/hyperloom/inference_optimizer/multi_node/scripts/launch_multinode.py _build_parser Function length (lines) absent 84
src/hyperloom/inference_optimizer/multi_node/scripts/launch_multinode.py _spawn_rank_actors Function length (lines) absent 84
src/hyperloom_kb/sync.py GlobalSync.pull Function length (lines) absent 84
src/kernelforge/loop/runner.py IterationLoop._close_iteration Function length (lines) absent 117
src/kernelforge/loop/runner.py IterationLoop._collect_session_candidate Function length (lines) absent 185
src/kernelforge/loop/runner.py IterationLoop._implementer_history Function length (lines) absent 112
src/kernelforge/loop/runner.py IterationLoop._rule_on_attempt Function length (lines) absent 102
src/kernelforge/loop/runner.py IterationLoop._source_attempt Function length (lines) absent 175
src/hyperloom_kb/http_service.py Module length (lines) absent 1525
src/kernelforge/cli/forge_loop_command.py Module length (lines) absent 2003
src/hyperloom/orchestrator/actions/executors/_geak_sweep.py sweep_via_geak Cognitive complexity 67 70
src/hyperloom/orchestrator/actions/executors/_grid_runner.py run_grid Cognitive complexity 177 180
src/hyperloom/orchestrator/actions/executors/explore.py ExploreExecutor.__call__ Cognitive complexity 369 383
src/hyperloom/orchestrator/kernel/request_handlers.py _run_forge_gemm_tuning Cognitive complexity 124 125
src/hyperloom/orchestrator/phases/kernel.py KernelPhase._run_geak_kernel_phase Cognitive complexity 122 130
src/hyperloom/orchestrator/phases/prelude.py PreludePhase.maybe_enqueue_warm_replay Cognitive complexity 190 191
src/hyperloom/orchestrator/actions/executors/_grid_runner.py run_grid Cyclomatic complexity 55 56

...and 60 more; the job log lists every row.

Verdict computed by the gate scripts in /home/runner/work/_temp/code-metrics-gate, not by this tree's copy.

Thresholds and sources: [tool.hyperloom.code_metrics] in pyproject.toml. Tools: complexipy 8.0.1, jscpd 5.4.1, ruff 0.16.2, vulture 2.16.

@chennyiiis chennyiiis added the size-exception Waives the PR diff budget; added by a maintainer with a reason in the PR description label Oct 19, 2026
Chenyi Yan and others added 3 commits October 19, 2026 17:15
… 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 chaojhou left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

  1. [conflict-benched-lost] Merging main by taking the PR side silently drops 00422ae's benched field from source-attempt rows [verified]
    Problem: mergeable=CONFLICTING. The PR moved _record_source_attempt out of framework.py. Main commit 00422ae added benched=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 loses benched, 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 port benched=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
@ZhengGong-amd

Copy link
Copy Markdown
Collaborator

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

  1. Conflicts with main (framework.py, baseline). Main's 00422ae added benched=bool(result.get(RESULT_BENCHED)) to record_attempt(...) in _record_source_attempt. This PR moves and rewrites that function into _framework_timeline.py:338 without it. Git only reports the conflict on the deleted block, so resolving to this PR's side silently reverts 00422ae: every source-arm attempt row loses benched. Merge main and add the field.
  2. as_entries now turns string-valued changed_files/gap_keywords into [] (framework.py:2566,2568). Repro: gap_keywords: "fp8 moe" stays a string on base and becomes [] on head, so the upstream-PR KB writeback loses it (integrate_patch.py:4295-4300 explicitly wraps strings). Use hyperloom.common.coerce.to_str_list and add a test that "fp8" reaches the KB as ["fp8"].
  3. Delete phases/_payload.py. It duplicates common.coerce.to_str_list and breakdown/recorder/event_fields.as_dict/as_list, and it handles strings the opposite way, which is exactly what caused item 2.

Simplify instead of relocate

Tests: drive a real FrameworkPhase instead of adding private methods to _DispatchStub.

Metrics: framework.py was 3167 lines on base, not 3139.

…ged tree

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Test and others added 3 commits October 20, 2026 11:03
…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>
@chennyiiis

Copy link
Copy Markdown
Contributor Author

Thanks — all three blocking items are fixed at 554b385b4, and item 2 was a real regression I had wrongly cleared for myself.

1. benched (conflict with main). Merged origin/main; 00422ae09 is now an ancestor. _record_source_attempt carries benched=bool(result.get(RESULT_BENCHED)) — upstream's own expression, not a re-spelling — at _framework_timeline.py:378. #1985's own tests (test_sbd_v6_framework_wiring.py::test_a_source_attempt_says_whether_it_reached_the_bench, test_breakdown_variant_funnel.py) pass against the moved function. I also checked #1984, #1986, #1918 and #2021: #1984 touched framework.py and its removal of disable_run_eval survived the merge; the other three touch no file here and their files are byte-identical to origin/main on this branch.

2. String-valued changed_files / gap_keywords. Confirmed and fixed. You are right about the mechanism and I had refuted this wrongly: I enumerated the in-tree writers of those fields, found they all emit lists, and missed that the writer here is the discovery specialist. integrate_patch.py:4299-4304 wrapping a bare string is the in-tree evidence that a string is an expected shape. Both fields now read through to_str_list where the candidate is built (framework.py), so "fp8" arrives as ["fp8"] and the far-end wrap becomes a no-op. test_a_discovery_proposal_may_name_its_keywords_and_files_as_one_string pins it on the production entry point.

3. phases/_payload.py deleted. Its readers now come from hyperloom.common.coerce, which gains the three it was missing beside to_str_list / first_float / first_int: first_str, to_mapping, to_list (+22 lines). as_texts is gone in favour of to_str_list — that duplicate was the cause of item 2. task_id_of / params_of / payload_of are gone too: payload_of became to_mapping(getattr(x, "payload", None)), and the other two moved onto state/task_registry.py, which defines Task. That last one was not taste — inlining them scattered the "task_id" literal and the metrics gate failed on it; putting the reader on the owner writes the attribute name once (17 occurrences on base, 5 now).

Not folded in yet: breakdown/recorder/event_fields.as_dict / as_list are still a private copy of to_mapping / to_list. Replacing them changes identity-to-copy semantics for the recorder's callers and belongs to that module's owner, so I would rather not ride it in here — say the word and I will open it as its own PR.

Deletion, not relocation. The number I should have led with: str(x.get(k) or "") / or [] / or {} was written 270 times across these functions and is now written 111. Those 159 are deleted, not moved — the reads go through the canonical module. That is what took thirteen functions off the complexity baseline. The description now says so.

Metrics. framework.py measures 3165 lines at the merge base 11d07e05, not 3139 (the baseline entry was stale) and not 3167 — git show 11d07e05:src/hyperloom/orchestrator/phases/framework.py | wc -l. It is 2924 now. I have lowered the baseline entry to the measured value.

Net production lines. Still positive: framework.py −241, _framework_timeline.py +460, coerce.py +22, task_registry.py +13 = +254. The arithmetic is in the description now, and the "counted twice" claim is gone — you were right that it was wrong, the move is 460 of the 1841 changed lines, not most of them. The remainder is 35 decisions each paying for a def and a docstring. I do not think I can get this to net-zero without the split you propose below, so I have stated the cost rather than dressed it up.

Where I have not acted, and why:

  • Normalize the model payload once at the boundary. I agree this is the right end state and it is strictly bigger than this PR: the candidate boundary is only one of several (critic results, executor results, task params, bus payloads each enter elsewhere), and getting it wrong silently changes what the recorder writes. I would rather land it as its own change with the boundary enumerated, on top of a tree where every read already names the same reader. Happy to open it.
  • Delete _empty_outcome_status and the semantic_status mapping. refactor(framework): drop the semantic_status mapping nothing produces; correct the KB-ledger docstring and warning; fix three misspellings #2029 is open and does exactly this on this file. Doing it here would conflict with it for no gain; it should land there.
  • Keep _framework_timeline.py a pure move; split the _record_source_attempt rewrite. I have left it in, and the reason is concrete rather than preference: as a pure move the function is 150 lines and the gate fails it at >80, so the split would have to land before or with the move, not after. PatchTrail plus the five _attempt_* helpers are what bring it to 79. If you would still rather see it separately I will reorder the two PRs, but the move cannot land first on its own.
  • Drive a real FrameworkPhase instead of _DispatchStub. Agreed in principle. The stand-in predates this PR and binds 20-odd methods; my diff adds 4 names to that list because I extracted 4 methods. Rewriting that file onto a real phase is worth doing and is a bigger change than this one's test delta — I would rather not grow this PR into it.

CodeQL's cyclic-import alert on _framework_timeline.py is also fixed: RESULT_BENCHED is now read through a deferred import beside the recorder's own, with the reason in a comment.

@chennyiiis

Copy link
Copy Markdown
Contributor Author

Follow-up: CI is green at 554b385b4 — 40 checks, 0 failures, code-metrics PASSED (0 new, 0 worse on every dimension; the repeated-literal finding is gone), CodeQL's cyclic-import alert on _framework_timeline.py cleared.

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 _enqueue_*_specialist and _record_framework_agent_*_outcome families I compared, per persisted effect (state write, .save, upsert_gap, create_or_return_existing, record_*, settle_proposal, warm_specialist_params), the set of guard conditions enclosing it on main against the set enclosing it here, inlining the helpers this PR extracts.

No effect is less guarded than it was on main. Ten matched mechanically; three I read by hand because the inliner does not resolve their method chains:

  • _submit_framework_agent_candidate_for_review — the review counter is still charged only after the dedup loop declines to return, and the cap still stamps before returning; both now live in _charge_review_submission.
  • _record_framework_agent_authored_outcome — the progress write is still behind not isinstance(rows, list), and the any(row.get("kept")) early return still precedes rows[:] = [...], now in _clear_superseded_progress.
  • _recover_framework_agent_authoring_outcome — the replay helpers keep the payload-match guards and main's break after the first matching message.

Two statements genuinely did move, and both are safe for a reason rather than by inspection:

  1. _recover_framework_agent_authoring_outcome hoists if cand_id in self._framework_processed_candidate_keys(): return True out of the inner branch, so it runs where main fell through to the second loop. Its only caller, _settle_finished_authoring_specialist (framework.py:853), returns early when cand_id in already_rows, so cand_id is provably absent on entry and the check can only become true from the replay itself — which is main's condition.
  2. maybe_autosubmit_specialist_patches evaluates _resolvable_artifacts_from_done before the patch-on-disk scan. Both are pure resolve()/is_file() probes, both are always evaluated either way, and nothing mutates between them.

I also checked the two idempotency keys this area builds (perf_explore_authoring:{gap_cid}:retry:{attempt} and framework_agent_local_explore:{cand_id}): byte-identical to main, verified by extracting every string constant and f-string from both trees and diffing — only docstrings differ. A changed key would silently re-dispatch work on resume, which is the same defect class in a different costume.

@ZhengGong-amd

Copy link
Copy Markdown
Collaborator

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 e7228a59b):

net lines
executable logic +12
docstrings +96
def/class signatures +99
NamedTuple/dataclass fields +6
blank lines between new defs +71
imports / __all__ +25

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 src/.

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 [tool.hyperloom.code_metrics] are review triggers, not a reason to add a type or a helper. When a limit and the clearer code disagree, keep the clearer code and say so (Size is a design signal).

Cut next:

  • Normalize the model payload once at the boundary (discovery and critic parsing) instead of wrapping the read sites.
  • _specialist_params (framework.py:1113) reimplements params_of. Use event_fields.as_dict/as_list or coerce, not a fourth copy.
  • The _record_source_attempt rewrite (PatchTrail + 6 helpers): make it a pure move or drop it.
  • Inline the remaining helpers that have one caller and only thread arguments through, and drop the docstrings that restate a private helper's name (Comment below the local average).

Target: production net ≤ 0, or name each remaining new type in the description with what it removes.

Test and others added 6 commits October 20, 2026 16:53
…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>
Test and others added 9 commits October 21, 2026 14:35
…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>
@chennyiiis chennyiiis changed the title refactor(framework-phase): split the FRAMEWORK timeline writes out and name each routing decision refactor(framework-phase): read a model-authored payload through one coercion module Oct 21, 2026

@ZhengGong-amd ZhengGong-amd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

  1. [X2] The Observable effect section describes a fix and a test that are not in the diff [verified]
    Problem: The body says a bare-string gap_keywords / changed_files "now keeps that value", is "read as a list of text where the candidate is built", is pinned by test_a_discovery_proposal_may_name_its_keywords_and_files_as_one_string, and that "Reading gap_keywords through to_str_list ... fixes the observable effect above". At the head:

    • git grep may_name_its_keywords d048533be returns nothing; the test does not exist.
    • git diff 3a548190f...d048533be -- src/hyperloom/orchestrator/phases/framework.py has no +/- line containing gap_keywords, changed_files or to_str_list.
    • src/hyperloom/orchestrator/phases/framework.py:15 imports only first_str, to_float, to_list, to_mapping from coerce, not to_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 reads cand.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-string gap_keywords / changed_files is 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 reading gap_keywords through to_str_list fixes it. Write the body against git diff origin/main...HEAD only; every name and number must be reproducible from that diff.
  2. [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.py adds 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_outcome at 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

Test and others added 2 commits October 21, 2026 21:40
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 chaojhou left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

  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 chaojhou left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

@chaojhou
chaojhou merged commit 2d78349 into main Oct 22, 2026
46 checks passed
@chaojhou
chaojhou deleted the refactor/chenyi/cc20-phases-1 branch October 22, 2026 07:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size-exception Waives the PR diff budget; added by a maintainer with a reason in the PR description

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants