[Config] Round 6.2: the field declarations move to their namespaces, and the record is assembled from them - #38047
Merged
Merged
Conversation
ch-wan
force-pushed
the
cheng/gc-r6-1-pilots
branch
from
September 5, 2026 08:10
736e6c1 to
604ff16
Compare
ch-wan
force-pushed
the
cheng/gc-r6-2-fields
branch
from
September 5, 2026 08:10
2607c62 to
3efd951
Compare
ch-wan
force-pushed
the
cheng/gc-r6-1-pilots
branch
from
September 6, 2026 03:52
604ff16 to
ca635af
Compare
ch-wan
force-pushed
the
cheng/gc-r6-2-fields
branch
from
September 6, 2026 03:52
3efd951 to
473d21c
Compare
ch-wan
added a commit
that referenced
this pull request
Sep 6, 2026
Not for merging. The work is #38046, #38047, #38048, #38049 and #38113, each stacked on the one before it, and each reviewable on its own terms. GitHub shows a stacked PR only against its parent, so there is nowhere to read the whole thing at once -- this branch is that view, and this empty commit is what lets it be a separate pull request from the same content.
ch-wan
added a commit
that referenced
this pull request
Sep 6, 2026
Not for merging. The work is #38046, #38047, #38048, #38049 and #38113, each stacked on the one before it, and each reviewable on its own terms. GitHub shows a stacked PR only against its parent, so there is nowhere to read the whole thing at once -- this branch is that view, and this empty commit is what lets it be a separate pull request from the same content.
ch-wan
added a commit
that referenced
this pull request
Sep 6, 2026
Not for merging. The work is #38046, #38047, #38048, #38049 and #38113, each stacked on the one before it, and each reviewable on its own terms. GitHub shows a stacked PR only against its parent, so there is nowhere to read the whole thing at once -- this branch is that view, and this empty commit is what lets it be a separate pull request from the same content.
ch-wan
added a commit
that referenced
this pull request
Sep 6, 2026
Not for merging. The work is #38046, #38047, #38048, #38049 and #38113, each stacked on the one before it, and each reviewable on its own terms. GitHub shows a stacked PR only against its parent, so there is nowhere to read the whole thing at once -- this branch is that view, and this empty commit is what lets it be a separate pull request from the same content.
ch-wan
added a commit
that referenced
this pull request
Sep 6, 2026
Not for merging. The work is #38046, #38047, #38048, #38049 and #38113, each stacked on the one before it, and each reviewable on its own terms. GitHub shows a stacked PR only against its parent, so there is nowhere to read the whole thing at once -- this branch is that view, and this empty commit is what lets it be a separate pull request from the same content.
ch-wan
force-pushed
the
cheng/gc-r6-1-pilots
branch
from
September 6, 2026 07:12
ca635af to
188a027
Compare
ch-wan
force-pushed
the
cheng/gc-r6-2-fields
branch
from
September 6, 2026 07:12
473d21c to
4f45882
Compare
ch-wan
added a commit
that referenced
this pull request
Sep 6, 2026
Not for merging. The work is #38046, #38047, #38048, #38049 and #38113, each stacked on the one before it, and each reviewable on its own terms. GitHub shows a stacked PR only against its parent, so there is nowhere to read the whole thing at once -- this branch is that view, and this empty commit is what lets it be a separate pull request from the same content.
ch-wan
added a commit
that referenced
this pull request
Sep 6, 2026
Not for merging. The work is #38046, #38047, #38048, #38049, #38113 and #38194, each stacked on the one before it, and each reviewable on its own terms. GitHub shows a stacked PR only against its parent, so there is nowhere to read the whole thing at once -- this branch is that view, and this empty commit is what lets it be a separate pull request from the same content.
ch-wan
force-pushed
the
cheng/gc-r6-1-pilots
branch
from
September 6, 2026 08:43
188a027 to
54b5114
Compare
ch-wan
force-pushed
the
cheng/gc-r6-2-fields
branch
from
September 6, 2026 08:43
4f45882 to
c37098c
Compare
ch-wan
added a commit
that referenced
this pull request
Sep 6, 2026
Not for merging. The work is #38046, #38047, #38048, #38049 and #38113, each stacked on the one before it, and each reviewable on its own terms. GitHub shows a stacked PR only against its parent, so there is nowhere to read the whole thing at once -- this branch is that view, and this empty commit is what lets it be a separate pull request from the same content.
ch-wan
added a commit
that referenced
this pull request
Sep 6, 2026
Not for merging. The work is #38046, #38047, #38048, #38049 and #38113, each stacked on the one before it, and each reviewable on its own terms. GitHub shows a stacked PR only against its parent, so there is nowhere to read the whole thing at once -- this branch is that view, and this empty commit is what lets it be a separate pull request from the same content.
ch-wan
force-pushed
the
cheng/gc-r6-2-fields
branch
from
September 6, 2026 09:00
c37098c to
3bfa93e
Compare
ch-wan
added a commit
that referenced
this pull request
Sep 6, 2026
Not for merging. The work is #38046, #38047, #38048, #38049 and #38113, each stacked on the one before it, and each reviewable on its own terms. GitHub shows a stacked PR only against its parent, so there is nowhere to read the whole thing at once -- this branch is that view, and this empty commit is what lets it be a separate pull request from the same content.
ch-wan
added a commit
that referenced
this pull request
Sep 6, 2026
Not for merging. The work is #38046, #38047, #38048, #38049 and #38113, each stacked on the one before it, and each reviewable on its own terms. GitHub shows a stacked PR only against its parent, so there is nowhere to read the whole thing at once -- this branch is that view, and this empty commit is what lets it be a separate pull request from the same content.
ch-wan
force-pushed
the
cheng/gc-r6-1-pilots
branch
from
September 6, 2026 10:18
54b5114 to
6206a5e
Compare
ch-wan
force-pushed
the
cheng/gc-r6-2-fields
branch
from
September 6, 2026 10:18
3bfa93e to
2f8f653
Compare
ch-wan
added a commit
that referenced
this pull request
Sep 6, 2026
Not for merging. The work is #38046, #38047, #38048, #38049 and #38113, each stacked on the one before it, and each reviewable on its own terms. GitHub shows a stacked PR only against its parent, so there is nowhere to read the whole thing at once -- this branch is that view, and this empty commit is what lets it be a separate pull request from the same content.
ch-wan
added a commit
that referenced
this pull request
Sep 6, 2026
Not for merging. The work is #38046, #38047, #38048, #38049 and #38113, each stacked on the one before it, and each reviewable on its own terms. GitHub shows a stacked PR only against its parent, so there is nowhere to read the whole thing at once -- this branch is that view, and this empty commit is what lets it be a separate pull request from the same content.
ch-wan
added a commit
that referenced
this pull request
Sep 6, 2026
Not for merging. The work is #38046, #38047, #38048, #38049 and #38113, each stacked on the one before it, and each reviewable on its own terms. GitHub shows a stacked PR only against its parent, so there is nowhere to read the whole thing at once -- this branch is that view, and this empty commit is what lets it be a separate pull request from the same content.
ch-wan
added a commit
that referenced
this pull request
Sep 6, 2026
Not for merging. The work is #38046, #38047, #38048, #38049 and #38113, each stacked on the one before it, and each reviewable on its own terms. GitHub shows a stacked PR only against its parent, so there is nowhere to read the whole thing at once -- this branch is that view, and this empty commit is what lets it be a separate pull request from the same content.
ch-wan
added a commit
that referenced
this pull request
Sep 7, 2026
Not for merging. The work is #38046, #38047, #38048, #38049 and #38113, each stacked on the one before it, and each reviewable on its own terms. GitHub shows a stacked PR only against its parent, so there is nowhere to read the whole thing at once -- this branch is that view, and this empty commit is what lets it be a separate pull request from the same content.
ch-wan
force-pushed
the
cheng/gc-r6-1-pilots
branch
from
September 7, 2026 04:25
6206a5e to
0bf74d6
Compare
ch-wan
force-pushed
the
cheng/gc-r6-2-fields
branch
from
September 7, 2026 04:25
2f8f653 to
b51a1c9
Compare
ch-wan
force-pushed
the
cheng/gc-r6-1-pilots
branch
from
September 7, 2026 04:36
0bf74d6 to
320e3ae
Compare
ch-wan
force-pushed
the
cheng/gc-r6-2-fields
branch
from
September 7, 2026 04:36
b51a1c9 to
df385fc
Compare
`ServerArgs` carried all 487 declarations in one 4,462-line file, each tagged
with an `NS("...")` marker naming the namespace it belongs to -- structure
supplied by annotation, in a file a namespace away from the
`arg_groups/*_hook.py` that resolves it.
The declarations move to `arg_groups/fields/`, one module per top-level
namespace and one class per leaf namespace (21 of them, `exec.moe` becomes
`exec_.py::ExecMoe`). The class carries the `_NS_PATH` it stands for, so the
module a field is declared in *is* its namespace and the marker is redundant:
`namespace_of` reads the declaring class off the MRO instead. `NS` stays for
the one case a class cannot express -- a single ad-hoc dataclass whose fields
span namespaces, which is what the config-bag tests build.
`ServerArgs` composes the 21. It is still one flat dataclass with 487
attributes, so `server_args.tp_size`, `ServerArgs(model_path=..., tp_size=8)`,
pickling to a subprocess and all 408 parameter sites are untouched. `Model` is
the last base because reverse-MRO order puts the last base's fields first, and
`model_path` -- the one field with no default -- has to stay the first
positional argument.
Two things travel with the declarations. The twelve `*_CHOICES` lists and the
`add_*_choices` adders that extend them move to `arg_groups/choices.py`, since
the fields naming them can no longer import from `server_args` without a
cycle; `server_args` re-exports them for the plugins that have always reached
them there. And five fields whose only annotation element was the namespace
marker become plain annotations -- `A` is `Annotated`, which needs two
arguments, so stripping the marker would have left them invalid.
Verified three ways: the 487-entry `namespace_of` map is identical field for
field, the resolution result is identical across 24 launch shapes x 485
fields, and the CLI registers the same 503 options with the same defaults,
choices and actions.
Ten places still told the reader that a field's namespace comes from an `NS(...)` marker on the field. Two of them are errors a developer reads when something is misconfigured -- "no NS namespace" would send them looking for a marker that no longer exists on any ServerArgs field. The namespace now comes from the class that declares the field, which is what `namespace_of` reads, so the messages name the namespace rather than the mechanism that used to supply it.
The split re-exported the names the extension-point block defines. That misses a list the block never mentions: `SAMPLING_BACKEND_CHOICES` is read only by the CLI section, so `add_cli_args` raised `NameError` on the first call -- which no import-time check catches. Re-export everything `choices.py` defines. All 33 used to be importable from `server_args`, and a rule narrower than that has no way to know which ones a plugin reaches for. The typing names the moved declarations were the last users of go the other way and are dropped.
Inheriting the namespace classes made the record's contents a property of which classes happen to appear in a base list. That is correct only while every namespace declares nothing but operator input, and it stops being correct the moment a derived field is declared -- `attn_tp_size` belongs in `parallel.py` next to the leaves it is derived from, and inheriting `Parallel` would put it on the record, where it is neither input nor safe: the record is the wire format, so a derived width pickled to a subprocess is a stamp, and a width has to stay a live read for elastic scale-up to see the group it has. `collect_input_fields` takes the classes that declare input and returns their annotations, defaults and namespaces. Each source's annotations are resolved in its own module and handed on as type objects -- carried across as text they would be re-evaluated where they land, and the composing module deliberately imports none of the names the declarations use. So a namespace can now declare both halves side by side, and which half reaches the record is one readable call instead of an invariant spread across a base-class list. Nothing is registered on the derived side yet; this is what makes it possible. The order is the old reverse-MRO order, so the fields, the CLI's 504 options and `model_path` as the first positional argument are all unchanged. Verified the four ways this refactor can be: the 491-entry namespace map and the CLI surface are identical to origin/main, the resolution result is identical across 24 launch shapes x 489 fields, and nothing that was importable from `server_args` stopped being.
ch-wan
force-pushed
the
cheng/gc-r6-2-fields
branch
from
September 7, 2026 04:40
df385fc to
945d9ad
Compare
ch-wan
added a commit
that referenced
this pull request
Sep 7, 2026
… sealed while resolution runs (#38048) Third of four; stacked on #38047. Two small changes, both about the same thing: the record holds the operator's input, and nothing else should be true of it. ## `/server_info` can answer what was actually typed It reports `resolved_dict()` -- what resolution decided. There was no way to ask the other question, and the two are not derivable from each other: a field nobody set reads the same as one set to the value resolution would have picked anyway. The launcher stores the arguments it parsed and the in-process `Engine` stores the call that built the record. All three readbacks report it beside the resolved values, so both surfaces come back in one request: HTTP `/server_info`, `Engine.get_server_info`, and the gRPC bridge's -- the last one builds from `resolved_dict()` and would otherwise have been the one surface of the three that answers only "what resolution decided". It rides on the record rather than in a field -- it describes how the configuration was asked for, so it is not part of the configuration: no CLI flag, no namespace, not in the bags. Being on the record is what lets a subprocess copy answer the same question the launcher can, and `replace_resolved` carries it because a copy was launched by whatever launched its parent. The crash dump already collected all four surfaces (`server_args`, `config_updates`, `resolved_config`, `launch_command`); this is the one that `/server_info` was missing. ## The record is sealed for the length of resolution The read-only guard armed on `_resolution_finished`, so for the whole run of the pipeline nothing stopped a resolver from assigning a field. Nothing in `srt/` does -- 0 assignments statically, and 0 writes observed across the launch-shape matrix with a watching `__setattr__` -- but that was a convention, and the defect it permits is invisible: a value a resolver wrote onto the record is indistinguishable from a value the operator typed, which is the one distinction the record exists to preserve. It now arms when resolution starts. A resolver that assigns a field fails at boot with a message naming `declare_resolution`, which is where the decision belongs: the stash carries a source and leaves the input intact. `declare_direct_writes` asks for the seal by name through `record_writable`. It hands the record to an out-of-tree platform plugin that sets fields on it; those implementations cannot be converted by editing a resolver here, so the write stays and the diff is captured into the stash afterwards. Naming the exception is the point -- an in-tree resolver reaching for it is doing something it should be declaring. ## Verification Costs nothing: the 211 test-side assignments all happen before `resolve_once`, which a post-resolution write already refused. A full registered-unit sweep (648 files) against the stack's merge-base: 19 failures on both sides, the same 19, none of them config. Driving a deliberate write into a real handler produces the new error, so the seal is tested by more than its own unit test.
SovietPower
added a commit
to SovietPower/sglang
that referenced
this pull request
Sep 7, 2026
…t/add_hrrn_scheduler_policy Resolve server_args.py conflict from PR sgl-project#38047 refactor: --schedule-policy hrrn choice moved to arg_groups/fields/schedule.py.
mqhc2020
pushed a commit
to mqhc2020/sglang
that referenced
this pull request
Sep 15, 2026
…and the record is assembled from them (sgl-project#38047) Second of four; stacked on sgl-project#38046. Mechanical relocation plus one design change that the relocation makes possible. **Review by checking the identity proofs at the bottom** -- nothing here is meant to change behaviour. ## The declarations move `ServerArgs` carried all 487 declarations in one 4,462-line file, each tagged with an `NS("...")` marker naming the namespace it belongs to -- structure supplied by annotation, in a file a namespace away from the `arg_groups/*_hook.py` that resolves it. They move to `arg_groups/fields/`: one module per top-level namespace, one class per leaf namespace (21 of them, `exec.moe` becomes `exec_.py::ExecMoe`). The class carries the `_NS_PATH` it stands for, so the module a field is declared in *is* its namespace and the marker is redundant -- `namespace_of` reads the declaring class instead. `NS` stays for the one case a class cannot express: a single ad-hoc dataclass whose fields span namespaces, which is what the config-bag tests build. Two things travel with the declarations. The `*_CHOICES` lists and the `add_*_choices` adders that extend them move to `arg_groups/choices.py`, since the fields naming them can no longer import from `server_args` without a cycle; `server_args` re-exports all of them, because out-of-tree plugins have always reached them there. And five fields whose only annotation element was the namespace marker become plain annotations -- `A` is `Annotated`, which needs two arguments, so stripping the marker would have left them invalid. `server_args.py` goes from 4,458 lines to about 1,000. ## The record is assembled, not inherited Inheriting the namespace classes would make the record's contents a property of which classes happen to appear in a base list. That is correct only while every namespace declares nothing but operator input, and it stops being correct the moment a derived field is declared: `attn_tp_size` belongs in `parallel.py` next to the leaves it is derived from, and inheriting `Parallel` would put it on the record -- where it is neither input nor safe, since the record is what crosses a process boundary and a derived width pickled to a subprocess is a stamp that elastic scale-up will not refresh. `collect_input_fields` takes the classes that declare input and returns their annotations, defaults and namespaces. Each source's annotations are resolved in its own module and handed on as type objects; carried across as text they would be re-evaluated where they land, and the composing module deliberately imports none of the names the declarations use. A namespace can now declare both halves side by side, and which half reaches the record is one readable call rather than an invariant spread across a base-class list. Nothing is registered on the derived side yet -- this is what makes it possible. `ServerArgs` is still one flat dataclass with 494 attributes, so `server_args.tp_size`, `ServerArgs(model_path=..., tp_size=8)`, pickling to a subprocess and every existing call site are untouched. ### Field order is a contract, so it is written down A dataclass turns field order into a positional constructor signature, and collecting whole namespaces groups fields that used to be interleaved. Keeping `model_path` first is not enough: `ServerArgs("dummy", "/tmp/tokenizer")` would set `load_format="/tmp/tokenizer"` and leave `tokenizer_path=None`, which then selects an invalid model loader -- silently, at a call site that did not change. So `arg_groups/field_order.py` records the order the record had before the split, and `collect_input_fields` orders what it collects by it. A field the record declares that the frozen order does not name goes after it, in declaration order -- the only backward-compatible place for a new field anyway, so a new declaration needs no edit there. The list is a compatibility record and nothing else reads it; the namespace a field belongs to is still the module it is declared in. ## Verification Four ways, all against the base commit: | check | result | |---|---| | `namespace_of` map, field by field | 494 / 494, **0 differences** | | CLI surface (options, defaults, choices, actions) | 507 / 507, **0 differences** | | field order, name by name | 494 / 494, **identical to the base** | | resolution result, 24 launch shapes x 489 fields | **0 differences** | | names importable from `sglang.srt.server_args` | nothing lost | Plus a full registered-unit sweep (648 files) against the stack's merge-base: 19 failures on both sides, the same 19, none of them config.
mqhc2020
pushed a commit
to mqhc2020/sglang
that referenced
this pull request
Sep 15, 2026
… sealed while resolution runs (sgl-project#38048) Third of four; stacked on sgl-project#38047. Two small changes, both about the same thing: the record holds the operator's input, and nothing else should be true of it. ## `/server_info` can answer what was actually typed It reports `resolved_dict()` -- what resolution decided. There was no way to ask the other question, and the two are not derivable from each other: a field nobody set reads the same as one set to the value resolution would have picked anyway. The launcher stores the arguments it parsed and the in-process `Engine` stores the call that built the record. All three readbacks report it beside the resolved values, so both surfaces come back in one request: HTTP `/server_info`, `Engine.get_server_info`, and the gRPC bridge's -- the last one builds from `resolved_dict()` and would otherwise have been the one surface of the three that answers only "what resolution decided". It rides on the record rather than in a field -- it describes how the configuration was asked for, so it is not part of the configuration: no CLI flag, no namespace, not in the bags. Being on the record is what lets a subprocess copy answer the same question the launcher can, and `replace_resolved` carries it because a copy was launched by whatever launched its parent. The crash dump already collected all four surfaces (`server_args`, `config_updates`, `resolved_config`, `launch_command`); this is the one that `/server_info` was missing. ## The record is sealed for the length of resolution The read-only guard armed on `_resolution_finished`, so for the whole run of the pipeline nothing stopped a resolver from assigning a field. Nothing in `srt/` does -- 0 assignments statically, and 0 writes observed across the launch-shape matrix with a watching `__setattr__` -- but that was a convention, and the defect it permits is invisible: a value a resolver wrote onto the record is indistinguishable from a value the operator typed, which is the one distinction the record exists to preserve. It now arms when resolution starts. A resolver that assigns a field fails at boot with a message naming `declare_resolution`, which is where the decision belongs: the stash carries a source and leaves the input intact. `declare_direct_writes` asks for the seal by name through `record_writable`. It hands the record to an out-of-tree platform plugin that sets fields on it; those implementations cannot be converted by editing a resolver here, so the write stays and the diff is captured into the stash afterwards. Naming the exception is the point -- an in-tree resolver reaching for it is doing something it should be declaring. ## Verification Costs nothing: the 211 test-side assignments all happen before `resolve_once`, which a post-resolution write already refused. A full registered-unit sweep (648 files) against the stack's merge-base: 19 failures on both sides, the same 19, none of them config. Driving a deliberate write into a real handler produces the new error, so the seal is tested by more than its own unit test.
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.
Second of four; stacked on #38046. Mechanical relocation plus one design change
that the relocation makes possible. Review by checking the identity proofs at
the bottom -- nothing here is meant to change behaviour.
The declarations move
ServerArgscarried all 487 declarations in one 4,462-line file, each taggedwith an
NS("...")marker naming the namespace it belongs to -- structuresupplied by annotation, in a file a namespace away from the
arg_groups/*_hook.pythat resolves it.They move to
arg_groups/fields/: one module per top-level namespace, one classper leaf namespace (21 of them,
exec.moebecomesexec_.py::ExecMoe). Theclass carries the
_NS_PATHit stands for, so the module a field is declared inis its namespace and the marker is redundant --
namespace_ofreads thedeclaring class instead.
NSstays for the one case a class cannot express: asingle ad-hoc dataclass whose fields span namespaces, which is what the
config-bag tests build.
Two things travel with the declarations. The
*_CHOICESlists and theadd_*_choicesadders that extend them move toarg_groups/choices.py, sincethe fields naming them can no longer import from
server_argswithout a cycle;server_argsre-exports all of them, because out-of-tree plugins have alwaysreached them there. And five fields whose only annotation element was the
namespace marker become plain annotations --
AisAnnotated, which needs twoarguments, so stripping the marker would have left them invalid.
server_args.pygoes from 4,458 lines to about 1,000.The record is assembled, not inherited
Inheriting the namespace classes would make the record's contents a property of
which classes happen to appear in a base list. That is correct only while every
namespace declares nothing but operator input, and it stops being correct the
moment a derived field is declared:
attn_tp_sizebelongs inparallel.pynext to the leaves it is derived from, and inheriting
Parallelwould put it onthe record -- where it is neither input nor safe, since the record is what
crosses a process boundary and a derived width pickled to a subprocess is a
stamp that elastic scale-up will not refresh.
collect_input_fieldstakes the classes that declare input and returns theirannotations, defaults and namespaces. Each source's annotations are resolved in
its own module and handed on as type objects; carried across as text they would
be re-evaluated where they land, and the composing module deliberately imports
none of the names the declarations use. A namespace can now declare both halves
side by side, and which half reaches the record is one readable call rather than
an invariant spread across a base-class list. Nothing is registered on the
derived side yet -- this is what makes it possible.
ServerArgsis still one flat dataclass with 494 attributes, soserver_args.tp_size,ServerArgs(model_path=..., tp_size=8), pickling to asubprocess and every existing call site are untouched.
Field order is a contract, so it is written down
A dataclass turns field order into a positional constructor signature, and
collecting whole namespaces groups fields that used to be interleaved. Keeping
model_pathfirst is not enough:ServerArgs("dummy", "/tmp/tokenizer")wouldset
load_format="/tmp/tokenizer"and leavetokenizer_path=None, which thenselects an invalid model loader -- silently, at a call site that did not change.
So
arg_groups/field_order.pyrecords the order the record had before thesplit, and
collect_input_fieldsorders what it collects by it. A field therecord declares that the frozen order does not name goes after it, in
declaration order -- the only backward-compatible place for a new field anyway,
so a new declaration needs no edit there. The list is a compatibility record and
nothing else reads it; the namespace a field belongs to is still the module it
is declared in.
Verification
Four ways, all against the base commit:
namespace_ofmap, field by fieldsglang.srt.server_argsPlus a full registered-unit sweep (648 files) against the stack's merge-base:
19 failures on both sides, the same 19, none of them config.
CI States
Latest PR Test (Base): ❌ Run #34022667423
Latest PR Test (Extra): ❌ Run #34022667349
Latest PR Test (AMD ROCm 7.2): ❌ Run #34022667436
CI States
Latest PR Test (Base): 🚫 Run #34083970723
Latest PR Test (Extra): 🚫 Run #34083970596
Latest PR Test (AMD ROCm 7.2): 🚫 Run #34083970711