refactor(harbor): split Mode A / Mode B into discriminated-union configs#31
Open
shehabyasser-scale wants to merge 1 commit into
Open
refactor(harbor): split Mode A / Mode B into discriminated-union configs#31shehabyasser-scale wants to merge 1 commit into
shehabyasser-scale wants to merge 1 commit into
Conversation
…igs (supersedes #20) BuildConfig and ServeConfig become discriminated unions on `mode`: a shared extra="forbid" base plus per-mode subclasses (BuildConfigA/B, ServeConfigA/B). A Mode-A config that sets a Mode-B-only field (feedback_transcripts, expose_attempt_detail, instruct_multifidelity, feedback_max_bytes, harbor, partition, inner_task) or a Mode-B config that sets a Mode-A-only field (sample_timeout, task, task_project, task_module, dataset) is now a load-time ValidationError instead of a silently-ignored no-op. A discriminated union is not a class, so the BuildConfig.from_file and ServeConfig.from_file classmethods become module-level loaders (load_build_config, load_serve_config) built on pydantic TypeAdapter, keeping the relative-path resolution and defaulting mode to "A" when omitted. Call sites in the CLI, compiler, and serve entrypoint updated; _serve_config and build_components narrow by isinstance / mode. Deletes PR #20's _warn_mode_b_sample_timeout and _warn_mode_a_ignores_feedback_levers (plus the compiler's Mode-A feedback-lever warning) and their tests: under the type split those conditions are structurally impossible. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes #20 by construction. Mode A (vero-native per-sample scoring) and Mode B (nested
harbor run) previously shared one flatBuildConfig/ServeConfig, so a field that only works in one mode silently no-opped in the other, and we kept papering over that with runtime warnings (#20'ssample_timeoutwarning, and the Mode-A feedback-lever warning added in #30).This splits both schemas into discriminated unions on
mode:BuildConfigA(mode A:task,task_project,task_module,dataset,sample_timeout) andBuildConfigB(mode B:harbor,partition,inner_task, and the feat(harbor): feedback levers: failure transcripts, multi-fidelity screening, per-attempt detail #30 feedback levers), over a sharedextra="forbid"base. Same forServeConfigA/ServeConfigB.ValidationErrorinstead of a silent no-op:sample_timeoutin a Mode-B yaml fails to parse,feedback_transcriptsin a Mode-A yaml fails to parse.from_fileclassmethods becomeload_build_config/load_serve_config(TypeAdapter loaders; a union is not a class). Missingmodestill defaults to A._warn_mode_b_sample_timeout(all of fix(harbor): warn that sample_timeout is not enforced in Mode B #20) and_warn_mode_a_ignores_feedback_levers, plus their tests. The conditions they warned about are now structurally impossible.model_fields_set, but the compiler bakessample_timeoutunconditionally into the serve JSON, so the "explicitly set" check would have fired on every compiled Mode-B task.Acceptance tests cover both wrong-mode rejections and valid round-trips through
compile_task. The three live Pawn lever configs parse and compile through the new loader (one of them ran a full trial through this code).Stacked on #30 (
harbor-3-feedback-levers).🤖 Generated with Claude Code
Greptile Summary
Replaces the single flat
BuildConfig/ServeConfigwith amode-discriminated union (BuildConfigA|BuildConfigB,ServeConfigA|ServeConfigB), so a wrong-mode field is now a load-timeValidationErrorrather than a silently-ignored no-op. Thefrom_fileclassmethods become module-levelload_build_config/load_serve_configTypeAdapter loaders, and the PR #20/#30 runtime warnings are deleted as structurally impossible.config.py/serve.py:_BuildConfigBase/_ServeConfigBasecarry shared fields withextra="forbid"; Mode-A subclasses addtask/dataset/sample_timeout, Mode-B subclasses addharbor/partition/feedback levers.compiler.py:_serve_configsplits its return into mode-specific branches so no wrong-mode key ever reaches the serialisedserve.json;compile_taskreplacesconfig.mode == "A"string checks withisinstanceguards.TestModeMismatchRejection, covering both wrong-mode directions, and add abuilt_bfixture for Mode-B compile round-trips.Confidence Score: 4/5
Safe to merge — the refactor is structurally sound and well-tested; both wrong-mode directions are rejected at load time, and three live configs have been validated end-to-end.
The core split logic and
_serve_configbranching are correct. Two style inconsistencies were found:getattrincompiler.pyforinner_task/instruct_multifidelityinstead of anisinstancecheck (inconsistent with the rest of the function), and theis_mode_aboolean guard inserve.pythat accesses Mode-A-only attributes via short-circuit evaluation rather than a narrowed block (safe at runtime, but static analysis tools will report errors on those lines).The
is_mode_aguard pattern invero/src/vero/harbor/serve.py(lines 238–251) and thegetattrcalls invero/src/vero/harbor/build/compiler.pyare worth a second look if the project adopts strict mypy/pyright checking.Important Files Changed
BuildConfig(flat class) → discriminated union ofBuildConfigA/BuildConfigBonmode;from_filereplaced byload_build_configwith TypeAdapter. Path-resolution loop is safe (operates on the raw dict before validation). No issues.ServeConfig→ServeConfigA/ServeConfigB;build_componentsswitches fromconfig.harbor is not Noneguards toisinstancechecks. Mode-A-only attributes (task_project,task_module,task,sample_timeout) are accessed viais_mode_aboolean short-circuit rather than type-narrowed blocks — safe at runtime but static analysis would flag it._serve_configsplit into mode-specific branches;compile_taskusesisinstancechecks throughout except forinner_taskandinstruct_multifidelitywhich usegetattrwith defaults — inconsistent with the rest of the function and bypasses type narrowing.TestModeAIgnoresFeedbackLeversreplaced byTestModeMismatchRejectioncovering both wrong-mode directions; newbuilt_bfixture exercises Mode-B compile path. Coverage is thorough.ServeConfigBinstance;test_mode_b_sample_timeout_warnsreplaced bytest_mode_mismatch_fields_rejectedverifying ValidationError in both directions. No issues.Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A["build.yaml / serve.json"] --> B{"mode present?"} B -- "no" --> C["inject mode='A'"] B -- "yes" --> D{"mode value"} C --> D D -- "A" --> E["BuildConfigA / ServeConfigA\n(task, task_project, task_module,\ndataset, sample_timeout)"] D -- "B" --> F["BuildConfigB / ServeConfigB\n(harbor, partition, inner_task,\nfeedback levers)"] D -- "wrong-mode field present" --> G["ValidationError\n(extra='forbid')"] E --> H["compile_task / build_components\n(Mode-A branch)"] F --> I["compile_task / build_components\n(Mode-B branch)"] H --> J["serve.json with mode='A'\n+ task/task_project/sample_timeout"] I --> K["serve.json with mode='B'\n+ harbor/feedback levers"] J --> L["load_serve_config → ServeConfigA"] K --> M["load_serve_config → ServeConfigB"]%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% flowchart TD A["build.yaml / serve.json"] --> B{"mode present?"} B -- "no" --> C["inject mode='A'"] B -- "yes" --> D{"mode value"} C --> D D -- "A" --> E["BuildConfigA / ServeConfigA\n(task, task_project, task_module,\ndataset, sample_timeout)"] D -- "B" --> F["BuildConfigB / ServeConfigB\n(harbor, partition, inner_task,\nfeedback levers)"] D -- "wrong-mode field present" --> G["ValidationError\n(extra='forbid')"] E --> H["compile_task / build_components\n(Mode-A branch)"] F --> I["compile_task / build_components\n(Mode-B branch)"] H --> J["serve.json with mode='A'\n+ task/task_project/sample_timeout"] I --> K["serve.json with mode='B'\n+ harbor/feedback levers"] J --> L["load_serve_config → ServeConfigA"] K --> M["load_serve_config → ServeConfigB"]Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "refactor(harbor): split Mode A / Mode B ..." | Re-trigger Greptile