ADR-0010: Layer A slices declared optional at the envelope's probes.* level¶
Status: Accepted Date: 2026-05-12 Tags: schema · multi-language · extension · envelope-shape Related: ADR-0004, Phase 0 ADR-0013
Context¶
The five new Phase 1 probes (NodeBuildSystem, NodeManifest, and TestInventory declare applies_to_languages = ["javascript", "typescript"]; CI and Deployment declare ["*"]). LanguageDetectionProbe runs on every repo. The Phase 0 Registry.for_task filter correctly skips probes whose languages don't match the detected stack.
But the envelope's schema has to validate either way. A Go-only repo gathered through codegenie gather produces a repo-context.yaml with only the language_stack slice populated (the three Node-only Phase 1 probes are filtered out; CI and Deployment run but may produce empty slices). If Phase 1 sub-schemas declare any Layer A slice as required at the envelope's probes.* level, the envelope fails schema validation on every non-Node repo.
The best-practices lens (design-best-practices.md) surfaced three options — "nullable variants" / "conditional branches" / "separate envelope" — without picking one. The synthesizer (final-design.md "Failure modes & recovery" row 14) picked: declare each Layer A slice as optional at the envelope's probes.* level. The cleanest of the three options; minimum-friction path forward.
The arch doc (phase-arch-design.md "Edge cases" row 11, Scenario 4) reinforces: tested by tests/integration/probes/test_non_node_repo.py.
Options considered¶
- Required slices, nullable shape. Slices are
requiredin the envelope; non-applicable slices ship asnull. Cluttered envelope; every consumer checksif slice is not Noneeverywhere; every probe emits a stub slice when filtered out. - Conditional branches via
oneOf(Node-envelope vs. Go-envelope vs. Python-envelope, etc.). Type-correct but the envelope schema grows combinatorially with language coverage; future Layer B+ probes touching multi-language repos make the schema explode. - Separate envelope per language. Different artifact shapes per language. Consumer complexity multiplies; the
repo-context.yamlcross-phase contract becomes language-keyed. - Optional slices at the envelope's
probes.*level. A non-Node repo produces an envelope with the slice key absent. Consumers handleOptional[Slice]. Schema validates cleanly. Composes with ADR-0004's per-probe sub-schema strictness (the slice that IS present is strict; the slice that's absent is fine).
Decision¶
Each Phase 1 sub-schema declares its slice as optional at the envelope's probes.* level. The envelope's properties.probes does not list any Phase 1 slice in its required array. The slice's key is absent (not null) from the YAML when its probe was filtered out by Registry.for_task.
This composes with ADR-0004's per-probe sub-schema strictness: if the slice is present, every key inside it conforms to the sub-schema's additionalProperties: false root. If the slice is absent, no validation runs against it. Optional fields inside a sub-schema continue to use null for not-present rather than field-absence (per ADR-0004).
Downstream consumers are obligated to treat every Layer A slice as Optional[Slice] — codified as a Phase 1 → Phase 2 implicit guarantee (phase-arch-design.md "Integration with Phase 2 (next phase)").
The for_task filter (Phase 0 Registry) is what determines which probes run on a given repo; the schema's optionality just admits the resulting envelope shape.
Tradeoffs¶
| Gain | Cost |
|---|---|
| Non-Node repos (Go-only, Python-only, etc.) validate cleanly — no special-casing in the envelope | Every downstream consumer must defensive-check Optional[Slice] access |
| Composes with ADR-0004 — per-probe strictness lives at the slice root; absence is not malformedness | "Present-but-empty" vs. "absent" must be distinguishable; the convention is absent-when-filtered, present-but-degraded when ran-but-failed |
| The envelope shape doesn't grow combinatorially with language coverage — Phase 2 adds Layers B–G probes with the same pattern | Reading the YAML requires understanding "what's missing means what didn't apply" — documented in final-design.md row 14 |
Registry.for_task (Phase 0) carries the load-bearing filter; schema admits the result |
Two layers of optionality — the schema permits absence; the registry decides absence — easier to reason about than oneOf-branching schemas |
| Phase 8 hot views (when they land) project from present slices; absence is a natural skip | Hot views must handle slice absence; tested when Phase 8 lands |
Adds no envelope-edit churn — Phase 0's envelope probes.* policy from ADR-0013 (additionalProperties: true) is preserved |
A future "Node is required for this kind of task" enforcement (e.g., a Phase-3 task class for Node migrations) must enforce at the planner level, not at the schema |
Consequences¶
- The envelope's
properties.probes.requiredarray does NOT listnode_build_system,node_manifest,ci,deployment,test_inventory, or the extended fields oflanguage_detection. The slices are admitted but not required. tests/integration/probes/test_non_node_repo.pyis the load-bearing regression — a Go-only fixture undertests/fixtures/non_node_go/gathers cleanly; envelope validates withlanguage_stackonly.tests/unit/test_sub_schemas.pyasserts each Phase 1 sub-schema declares optionality at the envelope level (norequiredreference to its slice key in the envelope).- Phase 2's Layer B/C/D/G probes follow the same convention. The "all Layer A slices present means a Node repo" inference is a planner-level decision, never schema-enforced.
- Phase 3+ task-class consumers handle
Optional[Slice]— ifmanifestsis absent, the task isn't applicable, full stop. - The Phase 0 → Phase 1 invariant — "the envelope shape is forward-compatible" — is honored by addition only.
Reversibility¶
Low. Flipping to required retroactively breaks every non-Node repo's cached envelope. Callers (Phase 3+ recipes, the planner, Phase 8 hot views) have come to depend on absence semantics; making slices required mid-stream breaks them. The forward direction (more optional slices in future phases) is symmetric and additive. The reverse direction (tighter language-conditional schemas) requires either a major version bump on the envelope (Phase 0 ADR-0003's schema versioning) or coordinated cache invalidation across all repos.
Evidence / sources¶
../final-design.md "Failure modes & recovery"row 14 — non-Node repo path../final-design.md "Risks" #5— the framing../final-design.md "Tests explicitly not in Phase 1"— non-Node fixture is in scope../phase-arch-design.md "Component design" #11— per-probe sub-schemas declare slices as optional../phase-arch-design.md "4+1 architectural views" "Scenarios" "Scenario 4"— Go-only fixture flow../phase-arch-design.md "Edge cases"row 11 — non-Node repo edge case../phase-arch-design.md "Integration with Phase 2"— implicit guarantee- ADR-0004 — the per-probe sub-schema strictness this composes with
- Phase 0 ADR-0013 — the envelope policy this preserves