Skip to content

ModuleManifest has no additive path: every new field is a fleet-wide compile break #84

Description

@iceteaSA

ModuleManifest has no Default impl and no #[non_exhaustive], so every field addition is a breaking change for every module that constructs one — and there is no ordering that lets a consumer stay green across the transition.

Measured now, on the self_signals wave

self_signals is correctly designed on the wire: Option<Vec<SelfSignalDeclaration>> with #[serde(default, skip_serializing_if = "Option::is_none")]. Purely additive, no protocol-version change, old and new decode each other cleanly.

The source side is the problem. Fleet census of who constructs ModuleManifest:

broca         sets self_signals      compiles ONLY against >= 40a4b88a
synapse       does not set it        E0063 missing field against >= 40a4b88a
astrocyte     does not set it        E0063
insula        does not set it        E0063
cerebellum    does not set it        E0063
claustrum     does not set it        E0063
prefrontal    does not set it        E0063
thalamus      does not set it        E0063
wernicke      does not set it        E0063

Nine consumers, and no revision of subc-protocol compiles all nine. Before the field lands, broca fails with E0560 (no such field). After it lands, the other eight fail with E0063 (missing field). The fleet cannot be green at any point during the transition.

I checked the usual escape hatch explicitly: zero struct-update (..Default::default()) occurrences in any consumer's constructor, and no Default impl exists to make one possible.

Why this is structural rather than a one-off

ModuleManifest has grown three fields in roughly six weeks — capabilities, provenance, self_signals — each one a fleet-wide compile break requiring a coordinated edit in nine repos. The waves themselves are fine; the type offers no way to land them additively.

The current cost per field is nine one-line edits, nine review cycles, and a window where the shared path-dep tree cannot serve every consumer. That is paid again on the next field.

Ask

Either:

(a) impl Default for ModuleManifest — consumers switch to ModuleManifest { module_id, .., ..Default::default() } and every future optional field is additive at the source level. Requires consumers to adopt struct-update syntax once, after which they never pay again.

(b) #[non_exhaustive] + a constructorModuleManifest::new(module_id, module_version, protocol_ver, trust_tier, provides) returning a value with optionals defaulted, and builder-style setters for the rest. Stricter: it also prevents out-of-crate construction entirely, so future fields cannot break anyone.

(b) is the more thorough fix and the more disruptive one to land. (a) is about four lines and solves the recurring cost immediately.

Either way the ordering matters: the Default (or constructor) must land BEFORE the next field, otherwise it is another coordinated nine-repo wave to adopt it.

Not urgent for correctness

Nothing is broken on the wire and no running module is affected — this is entirely about build coordination cost. Filing it now because the self_signals wave is mid-flight and the shared-tree fast-forward is being held on exactly this: whichever way it goes, someone's build breaks, and that is a property of the type rather than of the wave.

Happy to take the PR for (a). It would queue behind #82 and #83.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions