Follow-up to #338. #338 made the in-scope definitions/ block in docs/output-schema.json derive from Rust schemars and gated drift on every cargo test. A handful of seams in the Rust to schema to wire chain are still hand-synced; this issue tracks closing them.
Sequencing
Dependency-first, not size-first. Item 5 is the structural backstop that lets every later refactor land safely (no silent shape regression on any of the 182 definitions). The hard chain is:
5 -> 1 -> 6
Item 5 (tighten drift gate) must precede item 1 (retire augment_finding_definition) because item 1 touches every finding definition and we want the gate to fire on every shape change. Item 1 must precede item 6 (document-root oneOf from a Rust FallowOutput enum) because item 6 wants actions modeled on the typed structs, not grafted on after derivation.
Item 6 is the strategic close: a typed FallowOutput enum with a kind discriminator makes fallow's JSON output machine-discriminable in O(1) without try-parsing 11 variants. That is the headline win for AI / agent consumers; it ships last only because it depends on items 1 and 5.
Item 4 is a tiny artifact-level change that opens the PR with the headline item visible.
Open seams
1. Retire augment_finding_definition
crates/cli/src/bin/schema_emit.rs::augment_finding_definition grafts an actions array and optional introduced flag onto every finding definition after derivation, because the Rust source structs do not carry those fields. The mapping from finding type to action $ref (finding_augmentation()) is hand-coupled to actions_for_issue_type / inject_health_actions / inject_dupes_actions in crates/cli/src/report/json.rs. Moving the runtime over to typed wrappers retires the post-pass and the coupling.
2. Retire augment_runtime_coverage_report
augment_runtime_coverage_report in schema_emit.rs hard-codes the runtime-coverage schema_version: "1" property onto the derived RuntimeCoverageReport definition. Bumping RUNTIME_COVERAGE_SCHEMA_VERSION in report/json.rs requires hand-editing the augmentation in the same PR (called out as MAINTENANCE: in source).
3. Migrate dynamic envelope builders to typed structs
The typed envelope structs (CodeClimateOutput, ReviewEnvelopeOutput, CoverageSetupOutput) lock the schema shape via the drift gate, but the runtime emit still builds the wire as serde_json::Value via json! macros. Adding a field to the struct does not flow to the wire automatically; the schema gate stays green while the wire silently misses it. The dead_code allow at crates/cli/src/output_envelope.rs:34-37 is the breadcrumb.
Three independent migrations; each can land as its own PR.
3a. CodeClimateOutput
3b. ReviewEnvelopeOutput
3c. CoverageSetupOutput
4. Add $id URL for SHA-pinning
Consumers that vendor docs/output-schema.json for runtime validation cannot SHA-pin a specific schema revision today.
5. Tighten drift gate to cover every committed definition
derived_definition_names() enumerates ~108 entries; the committed schema carries 182 definitions. The other ~74 (transitive helpers like AnalysisResults, AttributedCloneGroup, AuditSummary, TrendDirection, every kebab-case enum) are overwritten on regen and only protected by git diff --exit-code in CI, not by structural normalization. Schemars-version churn there can land silently if someone runs the regen without inspecting the diff.
6. Derive top-level oneOf / title / description from Rust
The document-root $schema, title, description, and the 11-entry oneOf discriminator at docs/output-schema.json are hand-maintained. The branches $ref typed envelopes, but the array structure and ordering are not generated. A typed top-level FallowOutput enum with #[serde(tag = "kind")] would let consumers discriminate on a single field rather than try-parsing every variant; this is the agent-discriminability headline.
Why one meta-issue
The seams are independent and can be picked up in any order along the dependency chain, but each one is a small focused change rather than a multi-day migration. Tracking them as checkboxes here keeps the issue list tight and lets the next contributor pick whichever ladder rung is unblocked.
Follow-up to #338. #338 made the in-scope
definitions/block indocs/output-schema.jsonderive from Rust schemars and gated drift on everycargo test. A handful of seams in the Rust to schema to wire chain are still hand-synced; this issue tracks closing them.Sequencing
Dependency-first, not size-first. Item 5 is the structural backstop that lets every later refactor land safely (no silent shape regression on any of the 182 definitions). The hard chain is:
5 -> 1 -> 6
Item 5 (tighten drift gate) must precede item 1 (retire
augment_finding_definition) because item 1 touches every finding definition and we want the gate to fire on every shape change. Item 1 must precede item 6 (document-rootoneOffrom a RustFallowOutputenum) because item 6 wantsactionsmodeled on the typed structs, not grafted on after derivation.Item 6 is the strategic close: a typed
FallowOutputenum with akinddiscriminator makes fallow's JSON output machine-discriminable in O(1) without try-parsing 11 variants. That is the headline win for AI / agent consumers; it ships last only because it depends on items 1 and 5.Item 4 is a tiny artifact-level change that opens the PR with the headline item visible.
Open seams
1. Retire
augment_finding_definitioncrates/cli/src/bin/schema_emit.rs::augment_finding_definitiongrafts anactionsarray and optionalintroducedflag onto every finding definition after derivation, because the Rust source structs do not carry those fields. The mapping from finding type to action$ref(finding_augmentation()) is hand-coupled toactions_for_issue_type/inject_health_actions/inject_dupes_actionsincrates/cli/src/report/json.rs. Moving the runtime over to typed wrappers retires the post-pass and the coupling.actions: Vec<IssueAction>(or per-finding action wrapper) to the Rust finding structs.introduced: Option<AuditIntroduced>where applicable.crates/cli/src/report/json.rsto serialize through the typed wrappers instead of post-pass injection.augment_finding_definitionand thefinding_definition_names()/finding_augmentation()lists.2. Retire
augment_runtime_coverage_reportaugment_runtime_coverage_reportinschema_emit.rshard-codes the runtime-coverageschema_version: "1"property onto the derivedRuntimeCoverageReportdefinition. BumpingRUNTIME_COVERAGE_SCHEMA_VERSIONinreport/json.rsrequires hand-editing the augmentation in the same PR (called out asMAINTENANCE:in source).schema_version: RuntimeCoverageSchemaVersionfield toRuntimeCoverageReport.3. Migrate dynamic envelope builders to typed structs
The typed envelope structs (
CodeClimateOutput,ReviewEnvelopeOutput,CoverageSetupOutput) lock the schema shape via the drift gate, but the runtime emit still builds the wire asserde_json::Valueviajson!macros. Adding a field to the struct does not flow to the wire automatically; the schema gate stays green while the wire silently misses it. Thedead_codeallow atcrates/cli/src/output_envelope.rs:34-37is the breadcrumb.Three independent migrations; each can land as its own PR.
3a. CodeClimateOutput
crates/cli/src/report/codeclimate.rs::cc_issueto constructCodeClimateIssue.dead_codeallow list accordingly.3b. ReviewEnvelopeOutput
crates/cli/src/report/ci/review.rsto constructReviewEnvelopeOutput/GitHubReviewComment/GitLabReviewComment.dead_codeallow list accordingly.3c. CoverageSetupOutput
crates/cli/src/coverage/mod.rs::build_setup_jsonto constructCoverageSetupOutput.dead_codeallow onoutput_envelope.rsonce 3a + 3b + 3c are all in.4. Add
$idURL for SHA-pinningConsumers that vendor
docs/output-schema.jsonfor runtime validation cannot SHA-pin a specific schema revision today.$idto the document root pointing at the canonical raw GitHub URL.docs/backwards-compatibility.md(replacemainwith a tag for stability; ajv does NOT fetch$idover the network by default).5. Tighten drift gate to cover every committed definition
derived_definition_names()enumerates ~108 entries; the committed schema carries 182 definitions. The other ~74 (transitive helpers likeAnalysisResults,AttributedCloneGroup,AuditSummary,TrendDirection, every kebab-case enum) are overwritten on regen and only protected bygit diff --exit-codein CI, not by structural normalization. Schemars-version churn there can land silently if someone runs the regen without inspecting the diff.derived_definitions()indrift_tests::committed_definitions_match_derived_structurally, not just the explicit allow-list.6. Derive top-level
oneOf/title/descriptionfrom RustThe document-root
$schema,title,description, and the 11-entryoneOfdiscriminator atdocs/output-schema.jsonare hand-maintained. The branches$reftyped envelopes, but the array structure and ordering are not generated. A typed top-levelFallowOutputenum with#[serde(tag = "kind")]would let consumers discriminate on a single field rather than try-parsing every variant; this is the agent-discriminability headline.JsonSchemato a top-levelFallowOutputenum whose variants are the existing 11 envelope shapes.kinddiscriminator (or equivalent tag) so AI / agent consumers can route in O(1).Why one meta-issue
The seams are independent and can be picked up in any order along the dependency chain, but each one is a small focused change rather than a multi-day migration. Tracking them as checkboxes here keeps the issue list tight and lets the next contributor pick whichever ladder rung is unblocked.