Problem / why
The single in-memory component-major model must serialize back to whichever shape the manifest
declared. A single-component manifest (no components: block, one synthesized default
component) must serialize to the COLLAPSE form state.<env> so its on-disk bytes never change.
A multi-component manifest (a components: block) must serialize to the EXPAND form
state.components.<name>.<env>. The serializer picks the form from whether components are
declared.
Scope
This PR does:
- Implement the dual serializer: collapse to
state.<env> and latest_release.<single> when
the model holds exactly the one default component (no components: block); expand to
state.components.<name>.<env> and latest_release.components.<name> when the manifest
declares components.
- Make the statewrite write path carry the chosen form. The
internal/statewrite overlay
re-marshals via the model's serializer, so the form is selected once and applied on every
retry attempt; the overlay logic itself is unchanged.
- Guarantee byte-stable round-trips for both forms: a single-component manifest writes back
identical state.<env> bytes; a multi-component manifest writes back stable
state.components.<name>.<env>.
This PR does NOT:
- Change
CommitWithRetry semantics (internal/statewrite/apiwrite.go); only the Mutate
closures select the serializer form.
- Add per-component fan-out of promote / hotfix / rollback writes (that is P1 / R1 / R2).
Files / areas touched
internal/config/types.go - the dual-form serializer on the component-major model.
internal/config/parse.go - the serialize-back entry the statewrite path uses.
internal/promote/finalize.go, internal/hotfix/finalize.go,
internal/rollback/rollback.go - select the serializer form (still single-component here).
Tests and coverage required
- Round-trip unit tests: single-component in to component-major to single-component out is
byte-identical; multi-component in to component-major to multi-component out is byte-stable.
- A finalize-overlay unit test proving the single-component collapse form stays byte-stable
through the statewrite retry loop.
Acceptance criteria
- A single-component manifest serializes back to
state.<env> byte-identically.
- A multi-component manifest serializes to
state.components.<name>.<env>.
- The serializer chooses the form from whether components are declared; the statewrite overlay
is unchanged.
Dependencies
Blocked by F1. Best landed with or right after F2 (so both forms have parse coverage).
Problem / why
The single in-memory component-major model must serialize back to whichever shape the manifest
declared. A single-component manifest (no
components:block, one synthesizeddefaultcomponent) must serialize to the COLLAPSE form
state.<env>so its on-disk bytes never change.A multi-component manifest (a
components:block) must serialize to the EXPAND formstate.components.<name>.<env>. The serializer picks the form from whether components aredeclared.
Scope
This PR does:
state.<env>andlatest_release.<single>whenthe model holds exactly the one
defaultcomponent (nocomponents:block); expand tostate.components.<name>.<env>andlatest_release.components.<name>when the manifestdeclares components.
internal/statewriteoverlayre-marshals via the model's serializer, so the form is selected once and applied on every
retry attempt; the overlay logic itself is unchanged.
identical
state.<env>bytes; a multi-component manifest writes back stablestate.components.<name>.<env>.This PR does NOT:
CommitWithRetrysemantics (internal/statewrite/apiwrite.go); only theMutateclosures select the serializer form.
Files / areas touched
internal/config/types.go- the dual-form serializer on the component-major model.internal/config/parse.go- the serialize-back entry the statewrite path uses.internal/promote/finalize.go,internal/hotfix/finalize.go,internal/rollback/rollback.go- select the serializer form (still single-component here).Tests and coverage required
byte-identical; multi-component in to component-major to multi-component out is byte-stable.
through the statewrite retry loop.
Acceptance criteria
state.<env>byte-identically.state.components.<name>.<env>.is unchanged.
Dependencies
Blocked by F1. Best landed with or right after F2 (so both forms have parse coverage).