> Sections may be reordered when it improves clarity; this is a backend/systems (C daemon + capnp schema) change, not a web app — "Service Interface"/API/Event contract subsections are adapted to C function-signature contracts where used, and omitted where not applicable. ## Overview **Purpose**: Removes eight fields from the `ClusterConfig` capnp struct (`port`, `candidates`, `vcpus`, `jobmgr`, `workermgr`, `qmgr`, `resmgr`, `xstrings`) that are either already dead at runtime or (in `port`'s case) load-bearing for single-node startup but conceptually misplaced in cluster-shared config. `port`'s replacement source is the already-existing `--peers` candidate-match mechanism plus a hardcoded default — no new CLI flag. **Users**: XCE maintainers (schema/controller code owners) and, indirectly, XCE operators running single-node or `--peers`-mismatched controllers, whose startup behavior must not change. **Impact**: `ClusterConfig` shrinks from 21 to 13 fields. `cluster_config_t` (the generated C struct) loses the matching members. `cluster_open_listener`'s port-resolution fallback moves from a capnp-blob field to a two-tier shape: `--peers` candidate match (unchanged, including the already-supported single self-referencing `--peers =:` entry for single-node mode) → hardcoded constant when `--peers` was never passed at all. ### Goals - Remove the 8 fields and the `MasterCandidate` struct from `controller.capnp`, and their mirrors in `cluster_config_t`. - Replace `port`'s ClusterConfig-sourced fallback with the existing `--peers` candidate-match mechanism (unchanged) plus a hardcoded default for the true zero-`--peers` case, preserving single-node startup behavior exactly with no new CLI flag. - Prove behavior-preservation live (single-node startup, 3-node soak against a regenerated `ha/data/cluster.bin` fixture) — not just by unit tests, given the touched files (`cluster.c`, `ha.c`, `main.c`) are HA-critical. - (Nice-to-have) Add CLI flags for the hash-tuning tunables, closing the existing `cluster.c` TODOs. ### Non-Goals - `heartbeatInterval`/`inactiveHeartbeatCount` (controller→worker liveness) — unchanged, stay in `ClusterConfig`. - `fetchJobInterval`/`netioTimeout` — unresolved, left untouched, a candidate for a future pass. - A real HA/Raft election-heartbeat CLI parameter — separate roadmap item, not this spec. - Peer list replication protocol / joint consensus — future, unrelated to a field deletion. - Any change to `entity-bearing-snapshots` / `config-upgrade-snapshot`. ## Boundary Commitments ### This Spec Owns - `ClusterConfig` and `MasterCandidate` struct shape in `controller.capnp`. - `cluster_config_t`'s field list in `clusterconfig.h`. - The port-resolution fallback branch in `cluster_open_listener` (`cluster.c`): `--peers` candidate match (unchanged) → hardcoded default constant. - The dead-code removals that don't need a replacement: `candidates` KV-mirror loop (`cluster_build_config`), `SET_DEFAULT_IF_UNSET(c->port, ...)` call sites, and the (already-inert) `jobmgr`/`workermgr`/`qmgr`/`resmgr`/`xstrings`/`vcpus` blob reads. - The two known test-fixture updates (`test_commit_barrier_rollout.c`, `test_config_repl.c`) and any others a full-repo grep surfaces during implementation. - Regenerating capnp bindings. - (Optional, Requirement 6) CLI-configurable perf tunables — five new `OPT_*` flags mirroring the existing `--vcpus`, deferrable without blocking the rest of this spec. ### Out of Boundary - `heartbeatInterval`/`inactiveHeartbeatCount`, `fetchJobInterval`/`netioTimeout` — no field or code change. - A `ClusterConfig` field or CLI flag for the HA/Raft election heartbeat — structurally disallowed (`config-design.md` Rule 0); tracked as its own roadmap item. - Peer list replication / joint-consensus membership — unrelated, `candidates` is deleted, not replaced with a distributed mechanism. - `config-upgrade-snapshot`, `entity-bearing-snapshots` — no dependency either direction. - `admin-config-tool`'s `xceadmin` implementation — this spec closes the risk that tool depends on (per its requirements.md), but does not implement or block on any of that tool's own code. ### Allowed Dependencies - Existing build tooling: capnp compiler + `c-capnproto` codegen (schema → C bindings), already wired into the meson build. - `argp` CLI parsing (already used by every other controller flag, including `--vcpus`). - No new external dependencies, no new libraries. ### Revalidation Triggers - Any future proposal to add a field to `ClusterConfig` must be checked against `.kiro/steering/config-design.md`'s inclusion rules before landing. - `admin-config-tool`'s `xceadmin config build`/`push` implementation, when it lands, should target the post-split (13-field) schema — if it's implemented against the pre-split schema instead, that's a signal this spec's sequencing assumption was violated. - If the controller's invocation contract changes elsewhere (e.g., `--peers` becomes mandatory for another reason), revisit whether the hardcoded-default fallback added here is still needed. ## Architecture ### Existing Architecture Analysis `ClusterConfig` today mixes three concerns in one capnp struct: cluster-wide business config (queues, resources, groups — genuinely shared), HA peer membership (`candidates`, already superseded in practice by the `--peers` CLI flag into a separate `xce_cluster_config_t`/`peers_cfg` struct), and per-controller performance tunables (`jobmgr`/`workermgr`/`qmgr`/`resmgr`/`xstrings`/`vcpus`, already hardcoded in `cluster.c`'s `init_controller_tunables()` with TODOs acknowledging the blob values are dead). `port` sits in the same struct but, unlike the others, still has one live reader: `cluster_open_listener`'s fallback branch, reached whenever the local controller's name isn't matched in `--peers` (including single-node mode, which passes no `--peers` at all). ### Architecture Pattern & Boundary Map ```mermaid graph TB Schema[ClusterConfig capnp schema] Codegen[capnp codegen] ConfigStruct[cluster_config_t] Blob[cluster.bin blob] PeersFlag[peers CLI flag] PeersCfg[peers_cfg struct] Listener[cluster_open_listener] Tunables[init_controller_tunables] Business[cluster_init_phase2 business config] Schema --> Codegen --> ConfigStruct Blob --> ConfigStruct ConfigStruct --> Business PeersFlag --> PeersCfg --> Listener Listener --> DefaultConst[hardcoded default constant] Tunables --> HardcodedTunables[hardcoded tunable constants] ``` **Architecture Integration**: - Selected pattern: no new CLI surface — `port` resolution reuses the existing `--peers` candidate-match loop (a single self-referencing `--peers =:` entry already gives single-node mode a custom port today) plus a hardcoded fallback constant for the true zero-`--peers` case. - Domain/feature boundaries: `ClusterConfig` (schema) retains only fields that are genuinely cluster-shared (per `config-design.md`); `peers_cfg` (CLI-sourced, per-process, already existing) owns port resolution for everything this spec touches. - Existing patterns preserved: `--peers` remains the sole port/membership source; `init_controller_tunables()`'s hardcoded constants are the default values and reproduce today's exact behavior when no tunable flag is passed (Requirement 2.2 — no behavior change by default). (Note: the optional Requirement 6 CLI-sourcing was in fact implemented — `init_controller_tunables()` now reads the corresponding `myconf_t` field first, falling back to the hardcoded constant only when that field is 0/unset — so the function itself is no longer literally untouched, though default behavior is unchanged.) - New components: one new hardcoded constant (`cluster.c` or `clusterconfig.h`) — the minimum needed to satisfy Requirement 1.7 without any invocation-contract or CLI-surface change. - Steering compliance: enforces `.kiro/steering/config-design.md`'s independence/frequency/Rule-0 classification; follows `src/controller/CLAUDE.md`'s HA protocol verification checklist for the touched files. ### Technology Stack | Layer | Choice / Version | Role in Feature | Notes | |-------|------------------|------------------|-------| | Schema | Cap'n Proto (existing, `controller.capnp`) | Defines `ClusterConfig`/`MasterCandidate` shape | No version change | | Codegen | `c-capnproto` (existing) | Schema → C struct/reader/builder | Regenerated, not upgraded | | Backend | C (gnu17, existing) | `cluster.c`/`myconf.c`/`config_repl.c` changes | No new C dependency | | CLI | `argp` (existing) | No new flag for Req 1.7; reused unchanged for optional Req 6 tunables, same pattern as `--vcpus` | No new dependency | ## File Structure Plan ### Modified Files - `src/schema/controller.capnp` — remove `port` (@1), `candidates` (@2), `vcpus` (@7), `jobmgr` (@13), `workermgr` (@14), `qmgr` (@15), `resmgr` (@16), `xstrings` (@17) from `ClusterConfig`; remove the `MasterCandidate` struct entirely; remove the now-orphaned `HashZArrayConfig`/`ResmgrConfig` structs (used only by the removed fields — discovered during `/kiro-impl` task 2.1, their `$C.mapname` bindings broke the capnp-generated code once task 1.2 removed the C typedefs they referenced). Cap'n Proto requires gapless ordinals, so the 13 retained fields are renumbered to a contiguous `0..12` range as a direct consequence — see `research.md`'s ordinal-gap finding (Req 1.1, 1.2, 1.6, 2.1, 2.5, 3.1). - `ha/data/cluster.bin` (binary fixture, not source) — regenerated in place against the renumbered schema, preserving the retained fields' semantic values from the pre-spec blob (Req 3.2, 5.3). (Note: superseded by `admin-config-tool` (landed 2026-07-22, one day after this spec closed) — `ha/data/` was deleted entirely and `deploy-ha.sh` now builds `cluster.bin` fresh via `xceadmin config build` from `src/configs/*.yaml` on every deploy run, not from a checked-in regenerated blob. See that spec's design.md.) - `src/include/clusterconfig.h` — remove the matching `cluster_config_t` members: `port`, `ncandidates`, `candidates`/`master_candidate_t**`, `vcpus`, `jobmgr`, `workermgr`, `qmgr`, `resmgr`, `xstrings` (Req 1.5, 2.4). - `src/controller/cluster.c` — (a) remove the `candidates`/`ncandidates` KV-mirror loop in `cluster_build_config` (Req 1.4); (b) change `cluster_open_listener`'s fallback branch from reading `cluster_config->port` to a hardcoded default constant, reached only when `--peers` was never passed at all — the `--peers` candidate-match branch itself is unchanged (Req 1.3, 1.7); `init_controller_tunables()` itself is unchanged (Req 2.2). - `src/controller/myconf.c` — remove the `SET_DEFAULT_IF_UNSET(c->port, 0, 9990)` call (the field it defaults no longer exists) (part of Req 1.7's cleanup). - `src/controller/config_repl.c` — remove the equivalent `SET_DEFAULT_IF_UNSET(c->port, 0, 9990)` call (same reason). - `src/controller/testsuite/test_commit_barrier_rollout.c` — drop the `cfg.port = 9990;` literal at line 1359 (Req 5.1). - `src/controller/testsuite/test_config_repl.c` — drop the `cfg.port = 9991;` literal at line 983 (Req 5.1). - Capnp-generated bindings (build output, not hand-edited) — regenerated as part of the normal build after the schema change (Req 1.1-1.2, 1.6, 2.1). ### Verification Infrastructure (existing, no new files) - `controller_unit_test` (meson target, existing) — runs the full existing suite including the two updated test files above (Req 5.2). - A small throwaway capnp-encoding tool (written and discarded during implementation, not part of the shipped codebase) — decodes `ha/data/cluster.bin`'s retained-field values under the pre-spec schema and re-encodes them under the post-spec (renumbered) schema, overwriting the fixture (Req 3.2, 5.3). (Note: the fixture this tool targeted no longer exists — see the `ha/data/cluster.bin` note above.) - `/xce-deploy-ha` skill (existing, `src/tools/deploy-ha.sh`) — deploys the 3-node live cluster for the config-push cycle, using the regenerated `ha/data/cluster.bin` (Req 5.4). (Note: superseded by `admin-config-tool` — `deploy-ha.sh` now generates `cluster.bin` fresh via `xceadmin config build` instead of consuming this fixture. See that spec's design.md.) - Direct `xce_controller` invocation with no `--peers` flag (existing supported mode, `main.c:441-443` — line numbers shifted from the original `main.c:293-295` after this spec's own Requirement 6 CLI flags were added earlier in the file) — the single-node startup verification (Req 5.5). - Direct `xce_controller` invocation with a single self-referencing `--peers =:` entry (existing supported mode) — verifies the custom-port replacement mechanism itself (Req 5.5). ### Optional (Requirement 6, nice-to-have) - `src/controller/main.c` — five new `OPT_*` flags (`--jobmgr-nhash`, `--workermgr-nhash`, `--qmgr-nhash`, `--resmgr-nhash`, `--xstrings-nhash`, plus `-delta`/`-inst` variants where the tunable has more than one dimension), same pattern as the existing `--vcpus`. - `src/include/myconf.h` — matching `myconf_t` fields. - `src/controller/cluster.c` — `init_controller_tunables()` reads the new `myconf_t` fields with the existing hardcoded values as fallback (0 = unset), rather than always using the hardcoded constant. No new files are created by this spec — every change is to an existing file. ## System Flows Port resolution at controller startup — corrected during design review (see `research.md` Design Decisions for the two fixes below): ```mermaid flowchart TD Start[cluster_open_listener starts] HasPeers{peers_cfg candidates_count greater than 0} SearchName[search peers_cfg for local name] NameFound{local name found} PortValid{matched candidate port greater than 0} UseMatchedPort[use matched candidate port] UseFallback[use hardcoded default port] RefuseStart[log error and refuse to start] Bind[bind listener] Start --> HasPeers HasPeers -->|no, --peers never passed| UseFallback --> Bind HasPeers -->|yes| SearchName --> NameFound NameFound -->|no| RefuseStart NameFound -->|yes| PortValid PortValid -->|yes| UseMatchedPort --> Bind PortValid -->|no, malformed entry| UseFallback ``` **Key decisions** (corrected from the initial draft during `/kiro-validate-design`; the `--port` CLI flag was further revised away in favor of the mechanism below during `/kiro-impl` kickoff — see `research.md`'s "Design Revision" section): - **Fallback trigger is port value, not just match presence.** A matched candidate whose port field is `0` (a malformed `--peers` entry — already WARNed about at `main.c:434-439`, was `main.c:289-291`; line numbers shifted after this spec's own Requirement 6 CLI flags were added earlier in the file) falls through to the hardcoded-default tier exactly like today's `if (my_port == 0)` check, rather than binding to literal port 0. This preserves Requirement 1.3's "exactly as it does today" for the already-tolerated malformed-entry case. - **Error path is preserved, but only for one specific case.** `candidates_count == 0` can only happen when `--peers` was never passed at all — a passed-but-empty or malformed `--peers` string is rejected earlier by `xce_parse_peers` itself (`src/lib/cluster.c:59` returns `-1` unless `candidates_count > 0`; was cited as `:58`), which `main.c` treats as a startup error before `cluster_open_listener` is ever reached (corrected citation, 2026-07-20: this guarantee comes from `xce_parse_peers`'s contract, not `validate_peer_count`, which only rejects even non-zero counts) — that's genuine single-node mode, and gets the hardcoded-default fallback per Requirement 1.7. An operator who wants a non-default port in this mode passes a single self-referencing `--peers =:` entry instead — an already-existing, already-supported invocation (this resolves via the unchanged `NameFound`/`PortValid` branches above, not a new mechanism). But when `--peers` WAS passed and the local name simply isn't in it, that's a likely operator misconfiguration (own name typo'd or omitted) — this spec keeps today's `-EINVAL`/refuse-to-start behavior for that case rather than silently defaulting, per the project's fail-loud convention (`src/controller/CLAUDE.md` process-lifecycle invariant). Decided interactively during design review rather than defaulting to "simplest single code path." ## Requirements Traceability | Requirement | Summary | Components | Files | |-------------|---------|-------------|-------| | 1.1 | Remove `port` from schema | Schema Component | `controller.capnp` | | 1.2 | Remove `candidates` from schema | Schema Component | `controller.capnp` | | 1.3 | Listener uses `--peers` match when available | Listener/Port-Resolution Component | `cluster.c` | | 1.4 | Debug dump stops reading `candidates` | Debug-Dump Component | `cluster.c` | | 1.5 | `cluster_config_t` drops `port`/`candidates` members | C Struct Component | `clusterconfig.h` | | 1.6 | Remove `MasterCandidate` struct | Schema Component | `controller.capnp` | | 1.7 | Replacement port-sourcing mechanism (existing `--peers` self-reference + hardcoded default) | Listener/Port-Resolution Component | `cluster.c`, `myconf.c`, `config_repl.c` | | 2.1 | Remove 6 perf-tuning fields from schema | Schema Component | `controller.capnp` | | 2.5 | Remove orphaned `HashZArrayConfig`/`ResmgrConfig` structs | Schema Component | `controller.capnp` | | 2.2 | Hardcoded tunables unchanged (as defaults) | Tunables Component | `cluster.c` (values unchanged; function itself later extended by the now-implemented Req 6.2 CLI-sourcing wrapper) | | 2.3 | Controller stops attempting to read removed tunable fields | Tunables Component | `cluster.c` (implicit — field no longer exists) | | 2.4 | `cluster_config_t` drops tunable members | C Struct Component | `clusterconfig.h` | | 3.1 | Retained fields unchanged in type/number | Schema Component | `controller.capnp` (no change to retained fields) | | 3.2 | Renumbering breaks pre-spec `cluster.bin` binary-compat (accepted); fixture rebuilt | Schema Component, Verification | `controller.capnp`, `ha/data/cluster.bin` regeneration | | 3.3 | Worker-liveness behavior unaffected | Tunables/Business Component | `cluster.c` (no change) | | 4.1 | Discard removed-field values from legacy blobs silently | Schema Component | `controller.capnp` (capnp default behavior) | | 4.2 | No deprecate-in-place transition | Schema Component | `controller.capnp` | | 5.1 | Build + test-fixture compile cleanly | Test Fixture Updates | `test_commit_barrier_rollout.c`, `test_config_repl.c` | | 5.2 | No new unit test failures | Verification | controller unit test suite | | 5.3 | Regenerate `ha/data/cluster.bin` fixture against the new schema | Verification | `ha/data/cluster.bin` (Note: `ha/data/` was later deleted entirely by `admin-config-tool`; see the "Note" on this file in File Structure Plan above) | | 5.4 | 3-node live soak (config-push, peer/listener/worker-liveness) | Verification | live HA cluster | | 5.5 | Single-node + bare-peers port-override + `-EINVAL` live verification | Verification | live single-node controller | | 6.1 | (Nice-to-have) CLI tunable flags | CLI Tunables Component (optional) | `main.c`, `myconf.h` | | 6.2 | (Nice-to-have) Fallback to hardcoded default | Tunables Component (optional) | `cluster.c` | ## Components and Interfaces | Component | Domain | Intent | Req Coverage | Key Dependencies | Contracts | |-----------|--------|--------|---------------|-------------------|-----------| | Schema Component | capnp | Defines `ClusterConfig`/`MasterCandidate` shape | 1.1, 1.2, 1.6, 2.1, 3.1, 4.1, 4.2 | capnp compiler (P0) | State | | C Struct Component | C headers | `cluster_config_t` field list matches schema | 1.5, 2.4 | Schema Component (P0) | State | | Listener/Port-Resolution Component | C, `cluster.c` | Resolves listen port via `--peers` match (unchanged) with a hardcoded-default fallback | 1.3, 1.7 | C Struct Component (P0) | Service | | Debug-Dump Component | C, `cluster.c` | Internal status/debug KV dump, no longer mirrors `candidates` | 1.4 | C Struct Component (P0) | State | | Tunables Component | C, `cluster.c` | Hardcoded (or optionally CLI-sourced) perf tunables | 2.2, 2.3, 3.3, 6.2 | CLI Tunables Component (P1, optional path) | State | | CLI Tunables Component (optional) | C, `main.c`/`myconf.h` | Requirement 6 only: parses tunable flags, populates `myconf_t`, same pattern as the existing `--vcpus` | 6.1 | argp (P0) | Service | | Test Fixture Updates | C tests | Two known struct-literal sites updated to compile | 5.1 | C Struct Component (P0) | — | | Verification | Build + live cluster | Proves behavior preservation | 5.1, 5.2, 5.3, 5.4, 5.5, 3.2, 4.1 | All components above (P0) | — | Only the Listener/Port-Resolution Component introduces new logic (the two-tier fallback); every other component is either schema/struct shrinkage or already-hardcoded behavior left untouched, so only that one gets a full contract block below. ### Controller #### Listener/Port-Resolution Component | Field | Detail | |-------|--------| | Intent | Determine the controller's own listen port without depending on a `ClusterConfig` field | | Requirements | 1.3, 1.7 | **Responsibilities & Constraints** - Primary responsibility: resolve exactly one port value before `cluster_open_listener` binds. - Precedence is strict and non-configurable: `--peers` match (if local name found) > hardcoded default constant. No tier is skipped or reordered by configuration. No CLI flag mediates this — an operator who needs a non-default port in the no-other-peers case supplies it via a single self-referencing `--peers =:` entry, which resolves through the same match branch as any other `--peers` candidate. - Must not depend on `cluster_config_t` in any form (the field this component replaces no longer exists after this spec). **Dependencies** - Inbound: `main.c` startup sequence — invokes this resolution before/during `cluster_open_listener` (P0) - Outbound: `myconf->peers_cfg` (existing, unchanged parsing) (P0) - Outbound: hardcoded default constant (same file or `clusterconfig.h`) — no external dependency (P0) **Contracts**: Service [x] ##### Service Interface ```c /* cluster_open_listener — port resolution, updated contract */ /* Precondition: myconf->peers_cfg is populated (possibly empty, iff * --peers was never passed). */ /* Postcondition, --peers never passed (peers_cfg.candidates_count == 0): * returns 0 with the listener bound to the hardcoded default (9990). * Always succeeds. An operator wanting a non-default port here passes * a single self-referencing --peers entry instead (see below). * Postcondition, --peers given: if the local name is matched with * a nonzero port, binds to it (today's unchanged happy path — this * is also how a single-node operator gets a custom port). If * matched with a zero port (malformed entry), falls through to the * hardcoded-default resolution. If the local name is NOT matched at * all, returns -EINVAL and refuses to start — unchanged from today, * preserved deliberately (see System Flows "Error path is preserved" * decision). */ int cluster_open_listener(cluster_t *c); ``` - Preconditions: `myconf->peers_cfg` populated (empty iff `--peers` never passed). - Postconditions: see the branches above — no-`--peers` and matched-with-zero-port always resolve to a bound listener; `--peers`-given-but-unmatched still returns `-EINVAL`. - Invariants: precedence order (`--peers` match > hardcoded default) never changes based on runtime state; the `-EINVAL` case is determined solely by "was `--peers` passed AND local name absent," never by port value. **Implementation Notes** - Integration: the match/no-match branch structure is unchanged from today's `cluster_open_listener`; only the final fallback's source changed (from `cluster_config->port` to a hardcoded `XCE_CONTROLLER_DEFAULT_PORT` constant). No new CLI flag, no new `myconf_t` field — see `research.md`'s "Design Revision" section for why this replaced the originally-designed `--port` flag. - Validation: no new validation needed — the hardcoded constant requires no runtime validation, and `--peers` candidate validation is unchanged. - Risks: none identified beyond what Requirement 5.5's live single-node test is designed to catch — this is the one path that removes a previously-live capability, and this component IS the requirement's specified replacement. Requirement 5.5's live verification must cover BOTH the true zero-`--peers` case AND the self-referencing single-candidate `--peers` case (the latter is now the sole way to get a non-default single-node port, so it's load-bearing, not incidental). The preserved `-EINVAL` path for "peers given, no match" should also get its own live/unit test coverage during tasks (not separately required by requirements.md, but a natural extension of Requirement 5.5's spirit — flag during task generation). ## Error Handling ### Error Strategy No new error categories are introduced, and the existing `-EINVAL` error path (`cluster_open_listener`'s "no port could be determined," `cluster.c:1226` today) is **preserved**, not removed — corrected during design review, since single-code-path simplicity would have traded a clear startup error for a silent wrong-default in a likely-misconfiguration scenario (`--peers` given, local name absent). It now applies to exactly one case: `--peers` was passed and the local name isn't in it. The genuinely-new case (`--peers` never passed at all, i.e. single-node) and the pre-existing-tolerated case (name matched, that entry's port is `0`) both resolve via the hardcoded-default fallback and never error — unchanged from today's tolerance for the latter, newly-defined for the former. All other error handling (schema decode failures, capnp field-absence for legacy blobs) is capnp's existing default behavior: an old `cluster.bin` with data in now-unused field slots decodes without error, and those values are simply never read (Requirement 4.1) — no new error branch needed for that case either. ### Monitoring No new logging beyond what already exists for `--peers` parsing (`main.c:411-443`, was `main.c:284-291` — shifted after this spec's own Requirement 6 CLI flags were added earlier in the file) and controller startup (`INFO`/`WARN`/`ERROR` via the existing `mylog` macros). No new metrics. ## Testing Strategy - **Unit**: build-time enforcement is the primary "test" for Requirements 1.1-1.2, 1.5-1.6, 2.1, 2.4 — a removed field/struct member that's still referenced anywhere is a compile error, not a silent gap (this is why Requirement 5.1's test-fixture fix is explicit rather than assumed). - **Unit**: `controller_unit_test` full suite run (Requirement 5.2) — existing coverage for `cluster_build_config`, `init_controller_tunables`, and the two touched test files, now compiling against the shrunk struct. - **Integration**: regenerate the `ha/data/cluster.bin` fixture (Requirement 3.2, 5.3) — since retained-field renumbering breaks binary compatibility with any pre-spec blob, decode the current fixture's semantic field values under the pre-spec schema and re-encode them under the new schema with a small throwaway tool; confirm the regenerated blob decodes correctly under the post-spec schema and `deploy-ha.sh`'s verbatim-copy usage keeps working unmodified. (Note: superseded by `admin-config-tool` — `deploy-ha.sh` no longer does verbatim-copy of a fixture at all; it builds `cluster.bin` fresh via `xceadmin config build` from YAML on every run. See that spec's design.md.) - **Live/E2E**: 3-node HA cluster deploy + config-push cycle (Requirement 5.4) — using the regenerated fixture; peer discovery via `--peers`, listener binding, worker-liveness tracking all behave identically to pre-spec. - **Live/E2E**: single-node controller startup with no `--peers` flag, AND with a single self-referencing `--peers` entry for a custom port (Requirement 5.5) — the actual regression risk this spec's design resolves; must be verified live, not inferred from the 3-node soak (single-node never exercises the `--peers`-match branch at all). - **Live/E2E (optional, Requirement 6)**: if the nice-to-have CLI tunable flags are implemented, verify each flag overrides its corresponding hardcoded default and that omitting all of them reproduces today's exact hardcoded values. ## Migration Strategy No data migration is required for the fields this spec targets — Requirement 4.2 explicitly rules out a deprecate-in-place transition, and Requirement 4.1 confirms retained-field renumbering is an accepted, unguarded compatibility break for this pre-1.0 product: a `cluster.bin` built before this spec is not expected to be usable as-is against a controller built after it, and no backward-read path is supported or verified for such an artifact. The one thing this spec DOES migrate is the checked-in `ha/data/cluster.bin` test fixture (Requirement 3.2, 5.3): its retained-field values are decoded under the pre-spec schema and re-encoded under the new schema with a small throwaway tool, so `/xce-deploy-ha` and dependent live-verification tooling keep working. (Note: superseded by `admin-config-tool`, landed 2026-07-22 — `ha/data/cluster.bin` was deleted entirely and `/xce-deploy-ha` now generates `cluster.bin` fresh via `xceadmin config build` from YAML, so this migrated fixture no longer exists or is consumed. See that spec's design.md.) There is no forward path either (a post-spec `cluster.bin` is not expected to be readable by a pre-spec controller, but that direction isn't a supported downgrade path in this project regardless of this spec). ## Supporting References - `src/docs/cluster-config-split-analysis.md` — prior team analysis; primary design input for the dead-field classification. - `.kiro/specs/clusterconfig-schema-split/research.md` — port-replacement-mechanism investigation, alternatives considered, and the decision rationale in full. - `.kiro/steering/config-design.md` — the general shared-vs-local config rule this spec enforces.