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 constructor — ModuleManifest::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.
ModuleManifesthas noDefaultimpl 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_signalswaveself_signalsis 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:Nine consumers, and no revision of
subc-protocolcompiles 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 noDefaultimpl exists to make one possible.Why this is structural rather than a one-off
ModuleManifesthas 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 toModuleManifest { 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 constructor —ModuleManifest::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_signalswave 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.