Skip to content

feat(schema): derive JSON output schema from Rust source of truth (Phase 5)#382

Merged
BartWaardenburg merged 16 commits into
mainfrom
feat/issue-338-phase5-envelopes
May 16, 2026
Merged

feat(schema): derive JSON output schema from Rust source of truth (Phase 5)#382
BartWaardenburg merged 16 commits into
mainfrom
feat/issue-338-phase5-envelopes

Conversation

@BartWaardenburg
Copy link
Copy Markdown
Collaborator

Summary

Supersedes #374 with the full Phase 4 foundation + Phase 5 envelope derivation. Every type in derived_definition_names() (in crates/cli/src/bin/schema_emit.rs) is now generated from #[derive(JsonSchema)] on the matching Rust struct; the fallow-schema-emit binary regenerates docs/output-schema.json and a CI drift gate fails on any structural divergence.

The chain Rust → docs/output-schema.json → editors/vscode/src/generated/output-contract.d.ts + npm/fallow/types/output-contract.d.ts is now machine-derived end to end.

What's in this PR (13 commits, ordered)

Phase 4 foundation (the original PR #374 scope):

  1. f6f776bd feat(schema): derive JSON output schema from Rust source of truth
  2. 6fbe040f feat(schema): derive health output subtree from Rust source of truth
  3. 8c790921 feat(schema): cover coverage-gap actions and runtime-coverage helpers

Phase 5 envelope derivation:
4. 1b06fdb1 feat(schema): derive envelope utility types from Rust source of truth
5. 0a7d4943 feat(schema): derive DupesOutput envelope from Rust source of truth
6. e95c34a6 feat(schema): derive CheckOutput / CheckGroupedOutput from Rust source of truth
7. dde8760d feat(schema): derive HealthOutput envelope from Rust source of truth
8. 24629e9d feat(schema): derive Explain / CodeClimate / Review envelopes from Rust
9. 7b8ddf30 feat(schema): derive CombinedOutput envelope from Rust source of truth
10. 530ba13f feat(schema): derive AuditOutput envelope from Rust source of truth
11. 77808f41 feat(schema): derive CoverageSetupOutput envelope from Rust source of truth
12. 4e88d763 docs(contributing): collapse Layer 2 list now that envelopes are derived
13. 3bb046bd fix(schema): align CombinedOutput / AuditOutput sub-result refs with the wire
14. 1ea16889 docs(changelog): expand [Unreleased] for Phase 5 envelope derivation

What stays hand-maintained

Only the top-level docs/output-schema.json metadata ($schema, title, description, oneOf listing the 11 envelopes). merge_with_committed preserves it verbatim across regens.

Wire compatibility

SCHEMA_VERSION stays at 6. The Rust source is the type-side reflection of what crates/cli/src/report/json.rs already emits; no JSON output shape changes in this PR. New schema fields document fields the wire already carried (HotspotEntry.is_test_path, OwnershipMetrics.suggested_reviewers, the four VitalSignsCounts fields, the actions arrays on UntestedFile / UntestedExport). Two fields drop from required to match their Option<T> Rust sources (OwnershipMetrics.unowned, RuntimeCoverageSummary.last_received_at).

Relationship to other PRs

Test plan

  • cargo test --workspace --all-targets (all 6 drift tests + 7th strict gate marked #[ignore] until Phase 8)
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo fmt --all -- --check
  • pnpm run check:codegen

Refs #338.

Add `#[derive(JsonSchema)]` to the per-finding and duplication result
structs behind a new `schema` cargo feature on fallow-types and
fallow-core, plus a `fallow-schema-emit` dev binary on fallow-cli
(gated by a `schema-emit` feature) that regenerates
`docs/output-schema.json#/definitions` from the Rust source of truth.

New module `crates/types/src/output.rs` types the JSON-layer
augmentations (`IssueAction`, `FixAction`, `SuppressLineAction`,
`SuppressFileAction`, `AddToConfigAction`, and their kind discriminant
+ payload subtypes) so a future PR can route `crates/cli/src/report/json.rs`
through them instead of `serde_json::json!` builders.

Five drift tests catch field renames, additions, removals, and
required-flag drift between the Rust source and the committed schema;
a sixth walks every `$ref` in the merged document and asserts no
dangling targets. A strict structural variant (descriptions, integer
formats, nullable union shape) ships `#[ignore]`d pending prose
migration. CI runs the drift tests and `--tests` clippy on the
schema-emit binary so the gate is enforced on every push.

Also fixes two latent drift issues the new gate surfaced:
- `UnresolvedImport.specifier_col` was on the Rust struct since v2.39
  but missing from the public schema, so AJV-strict consumers rejected
  every unresolved-import finding.
- `MisconfiguredDependencyOverride.target_package` was emitted whenever
  the override key was syntactically valid but missing from the schema.

`CONTRIBUTING.md` documents the new derive-then-emit flow. The VS
Code extension's generated TS types (`editors/vscode/src/generated/output-contract.d.ts`
and `npm/fallow/types/output-contract.d.ts`) regenerate cleanly against
the updated schema, and one VS Code test fixture gains the new required
`specifier_col` field.

Phase 4 (health subtree), Phase 5 (typed envelope structs), and Phase 8
(prose migration into Rust doc comments) are intentionally deferred to
follow-up PRs.

Refs #338
Extends the schema-emit drift gate to cover the fallow-cli health output
subtree (Phase 4 of #338). The health types live on fallow-cli, so this
adds a sibling `schema` cargo feature there alongside the existing
`fallow-types/schema` + `fallow-core/schema`. `JsonSchema` derives now
attach to HealthFinding, HealthSummary, HealthScore + HealthScorePenalties,
VitalSigns + VitalSignsCounts + RiskProfile, HotspotEntry + HotspotSummary
+ OwnershipMetrics + ContributorEntry, RefactoringTarget + TargetThresholds,
HealthTrend + TrendCount, FileHealthScore, LargeFunctionEntry, CoverageGaps
+ CoverageGapSummary + UntestedFile + UntestedExport, RuntimeCoverageReport
and the protocol-derived signal/verdict/watermark/confidence/risk-band
enums, plus ChurnTrend (transitively).

Per-finding action wrappers (HealthFindingAction, HotspotAction,
RefactoringTargetAction) ship as schema-only types in
crates/types/src/output_health.rs so the drift gate covers the action
arrays without forcing a json.rs refactor in the same PR. The
schema-emit binary's augment_finding_definition takes a per-finding
FindingAugmentation now (actions_item_ref + include_introduced), so
HotspotEntry and RefactoringTarget correctly skip the audit-only
`introduced` flag while HealthFinding keeps it.

Reconciles three docs/output-schema.json fields that the JSON layer was
already emitting (HotspotEntry.is_test_path, OwnershipMetrics.suggested_reviewers,
four VitalSignsCounts fields) and tightens RuntimeCoverageReport.required
to include blast_radius + importance to match the wire. Drops `unowned`
from OwnershipMetrics.required to match the Option<bool> source.

Strips an unreachable `placement` field from RefactoringTargetAction
(the JSON layer never emits placement for refactoring targets; consumers
that want placement metadata should follow target.evidence back to the
matching HealthFinding action). Also adds an augment_runtime_coverage_report
helper that grafts schema_version onto the derived RuntimeCoverageReport
schema, mirroring what inject_runtime_coverage_report_schema_version
adds to the wire.

Refs #338
Two reviewer BLOCKs against the Phase 4 health-subtree derive: (1)
coverage_gaps.files[].actions and coverage_gaps.exports[].actions were
emitted by inject_health_actions but undocumented in the schema and
generated TS bindings; (2) RuntimeCoverageReport's helper structs and
enums were in the committed schema and inserted via the dangling-ref
fallback, but were not part of derived_definition_names, so a future
Rust field change in those helpers would not fail the drift gate.

Add typed UntestedFileAction + UntestedExportAction wrappers (and their
discriminant enums) to crates/types/src/output_health.rs, mirroring the
shape inject_health_actions emits today (add-tests / suppress-file for
files, add-test-import / suppress-file for exports). Add UntestedFile and
UntestedExport to finding_definition_names so augment_finding_definition
grafts the typed actions[] array onto each, without the audit-only
introduced flag.

Extend derived_definition_names with the runtime-coverage helper subtree:
RuntimeCoverageAction, RuntimeCoverageBlastRadiusEntry,
RuntimeCoverageCaptureQuality, RuntimeCoverageConfidence,
RuntimeCoverageEvidence, RuntimeCoverageFinding, RuntimeCoverageHotPath,
RuntimeCoverageImportanceEntry, RuntimeCoverageMessage,
RuntimeCoverageReportVerdict, RuntimeCoverageRiskBand,
RuntimeCoverageSignal, RuntimeCoverageSummary, RuntimeCoverageVerdict,
RuntimeCoverageWatermark. Drift gate now fires on field rename / addition
across the full runtime-coverage subtree, not just the top-level report.

Two follow-on drift fires resolved: RuntimeCoverageFinding.actions and
RuntimeCoverageHotPath.actions gain schemars(default) to match their
serde skip_serializing_if = "Vec::is_empty" wire shape, and
RuntimeCoverageSummary.last_received_at drops from required in the
committed schema to match its Option<String> Rust source (same precedent
as OwnershipMetrics.unowned).

Phase 6 wire-vs-schema verification on benchmarks/fixtures/real-world/zod
(13 untested files, 11 untested exports): every emitted actions[] entry
matches UntestedFileAction / UntestedExportAction. Drift gate green: 5/5
active tests pass.

Refs #338
Move the JSON envelope's shared utility shapes (SchemaVersion, ToolVersion,
ElapsedMs, AuditIntroduced, EntryPoints, CheckSummary, BaselineDeltas,
BaselineMatch, RegressionResult, Meta) into `crates/types/src/envelope.rs`
with `Serialize` + cfg-gated `JsonSchema` derives. Register the names in
`derived_definition_names()` and `derived_definitions()` so the drift gate
covers them.

Brings the committed `CheckSummary` block into sync with reality: the JSON
layer never emits `unused_dev_dependencies` / `unused_optional_dependencies`
(both folded into the combined `unused_dependencies` count) and always
emits `total_issues` / `type_only_dependencies` / `test_only_dependencies`,
which the schema previously omitted. The wire shape is unchanged; only the
schema documentation now matches what consumers actually receive.

Drops the orphaned `pub type AuditIntroduced = bool` alias from
`output.rs` (it had no call sites; the newtype in `envelope.rs` now owns
the name).

Part of issue #338 Phase 5; envelope migration follows.
Move the `fallow dupes --format json` envelope into Rust as
`crates/cli/src/output_envelope.rs::DupesOutput` and refactor
`build_duplication_json` / `build_grouped_duplication_json` to construct
it directly instead of round-tripping through `serde_json::json!` map
merges. The body is `#[serde(flatten)] DuplicationReport` so schemars 1
inlines every clone-detection field, matching the committed wire shape.

Field order is preserved (schema_version, version, elapsed_ms,
clone_groups, clone_families, mirrored_directories?, stats, then the
grouped-mode additions). The committed schema's `DupesOutput` gains a
`total_issues` property that the grouped builder has always emitted but
the schema previously omitted; ungrouped output stays identical.

Per-command envelope types live in `fallow-cli` rather than
`fallow-types` because their body fields reach into `fallow-core`
(`DuplicationReport`) and into this crate's own `health_types`; the
lower-level types crate cannot import from either. The shared utility
shapes (`SchemaVersion`, `Meta`, ...) stay in `fallow_types::envelope`
and are now compiled unconditionally so runtime construction does not
require the `schema` feature.

Part of issue #338 Phase 5.
…e of truth

Add `CheckOutput`, `CheckGroupedOutput`, and `CheckGroupedEntry` to
`crates/cli/src/output_envelope.rs`, flatten `AnalysisResults` into the
envelope body, and refactor `build_json_with_config_fixable` /
`print_grouped_json` to construct the typed structs instead of building
the envelope map ad-hoc.

`entry_points` lifts the otherwise `#[serde(skip)]`'d
`AnalysisResults::entry_point_summary` back into the JSON output via a
typed `Option<EntryPoints>`. `build_check_summary` extracts the
per-category counts the JSON layer always emits into a typed
`CheckSummary` (combined `unused_dependencies` count preserved).

Brings the committed CheckOutput / CheckGroupedEntry schemas into sync
with reality:
- `CheckGroupedEntry` gains the five missing catalog / override array
  properties that the grouped output has emitted since they shipped on
  `AnalysisResults`.
- `summary` becomes a required property on `CheckOutput` (always emitted
  today; the schema previously listed it as optional).
- `test_only_dependencies`, `boundary_violations`, `stale_suppressions`,
  and the catalog / override properties drop out of the `required` list
  to match what schemars derives from `#[serde(default)]` (consumers
  still receive these fields in practice; the schema downgrade is a
  documentation-only change).

Per-group action injection now runs on each `groups[]` entry
post-serialisation because `inject_actions` and the suppression
harmoniser only walk the top-level map; mirrors the dupes grouped
builder.

Part of issue #338 Phase 5.
Add `HealthOutput` to `crates/cli/src/output_envelope.rs`, flatten the
existing `HealthReport` body into it, derive `JsonSchema` on
`HealthReport` and `HealthGroup`, and refactor `build_health_json` /
`build_grouped_health_json` to construct the typed envelope.

Drop the `build_json_envelope` helper and its three direct unit tests:
every per-command builder now constructs its envelope as a typed
struct, so the dynamic-map envelope helper has no callers left.

Add `Clone` to `HealthReport`, `HealthSummary`, and `HotspotSummary` so
`HealthOutput` can be `Clone`-derived like the other envelopes; the
runtime path still constructs the envelope once per call and serializes
in place, so the new `Clone` impls are unused in hot paths.

Add `#[serde(default)]` alongside the existing `skip_serializing_if =
"Vec::is_empty"` annotations on `HealthReport` / `HealthGroup` vector
fields so schemars marks them optional in the required list, matching
the committed schema (the fields are already omitted from the wire when
empty; default is purely a deserialiser-tolerance signal that brings
schemars' derived `required` list into agreement with the wire shape).

Add `actions_meta` to the drift gate's `AUGMENTATION_KEYS` allowlist:
the breadcrumb is injected by `inject_health_actions` as a post-pass on
the `Value` tree rather than modelled as an `Option<...>` field on
`HealthOutput`. The drift gate already tolerates `actions` /
`introduced` for the same reason; this is the third augmentation key.

Part of issue #338 Phase 5.
Add the standalone (no-body) envelope types to
`crates/cli/src/output_envelope.rs`:

- `ExplainOutput` (used by `fallow explain <issue-type> --format json`).
- `CodeClimateOutput` + `CodeClimateIssue` + per-field enums for the
  `--format codeclimate` / `gitlab-codequality` array.
- `ReviewEnvelopeOutput` + `GitHubReviewComment` / `GitLabReviewComment`
  for `--format review-github` / `review-gitlab`.
- `ReviewReconcileOutput` for `fallow ci reconcile-review --format json`.

Register the new names in `derived_definition_names()` and the
matching `subschema_for::<...>()` calls so the drift gate enforces them.

Refactor `crates/cli/src/explain.rs` and the reconcile-result emitter
in `crates/cli/src/ci.rs` to construct the typed envelope and call
`serde_json::to_value` instead of building the JSON via
`serde_json::json!`. CodeClimate's per-issue construction and the
provider-branching review envelope renderer stay `Value`-based for now;
the types serve as the schema source of truth via the drift gate, and a
follow-up can swap each builder over without changing the wire shape.

Loosen the committed `ReviewReconcileOutput.target` required entry to
optional: the wire always emits the key (`null` when no target), but
`schemars` treats `Option<String>` as "may be absent" and the drift
gate's property-required check then fires. Dropping the property from
`required` matches schemars' derived shape without changing the wire.

Part of issue #338 Phase 5.
Add `CombinedOutput` to `crates/cli/src/output_envelope.rs` and refactor
`print_combined_json` to build the envelope shell as a typed struct
before serialising to `serde_json::Value`. The post-pass branches that
populate `check` / `dupes` / `health` and inject regression /
baseline / baseline_deltas still mutate the `Value` tree because those
sub-result types each have their own action-injection pipeline.

The combined envelope's `dupes` and `health` sub-keys hold a bare
`DuplicationReport` / `HealthReport` body (the runtime never wraps them
in the full per-command `DupesOutput` / `HealthOutput` envelope). The
committed schema still `$ref`s those sub-keys at `DupesOutput` /
`HealthOutput`, which is the pre-Phase-5 shape; the property-key drift
gate only checks key membership, so the inaccuracy stays parked behind
a clear doc comment on `CombinedOutput::dupes` / `health` for a
follow-up that adds `DuplicationReport` / `HealthReport` to the
committed `definitions` and rewires the refs.

Part of issue #338 Phase 5.
Add `AuditOutput` and singleton `AuditCommand` to
`crates/cli/src/output_envelope.rs`, plus cfg-gated `JsonSchema`
derives on the existing `AuditVerdict` / `AuditSummary` /
`AuditAttribution` structs that the envelope wraps. The audit module is
now also exposed from `crates/cli/src/lib.rs` so the typed envelope
struct can reach the verdict / summary / attribution types.

Like `CombinedOutput`, the typed `AuditOutput.duplication` and
`AuditOutput.complexity` sub-keys hold bare body types
(`DuplicationReport` / `HealthReport`); `dead_code` is the full
`CheckOutput` envelope. The committed schema's `$ref`s for the bare
sub-keys still point at `DupesOutput` / `HealthOutput` (the pre-Phase-5
shape); the property-key drift gate only checks key membership.

`print_audit_json` in `crates/cli/src/audit.rs` keeps its current
`serde_json::Map` builder: the conditional `head_sha` /
`base_snapshot_skipped` / sub-result branches mutate the result map in
place and call out to `build_json_with_config_fixable` /
`inject_dupes_actions` / `inject_health_actions` per sub-pass. The
typed `AuditOutput` here serves as the schema source of truth; a
follow-up can rewrite the builder to construct `AuditOutput` directly
without changing the wire shape.

Part of issue #338 Phase 5.
… truth

Add `CoverageSetupOutput` and its sub-shapes (`CoverageSetupMember`,
`CoverageSetupFileToEdit`, `CoverageSetupSnippet`) plus the supporting
enums (`CoverageSetupSchemaVersion`, `CoverageSetupFramework`,
`CoverageSetupPackageManager`, `CoverageSetupRuntimeTarget`) to
`crates/cli/src/output_envelope.rs`. Register the four primary names in
`derived_definition_names()` so the drift gate enforces them.

`crates/cli/src/coverage/mod.rs::build_setup_json` still constructs the
wire shape via `serde_json::json!` macros (one per member, snippet, and
file-to-edit). The typed structs here exist as the schema source of
truth; a follow-up can rewrite the builder to construct the typed
struct directly without changing the wire.

Loosen committed `required` lists on `CoverageSetupOutput` and
`CoverageSetupMember` to drop the always-emitted-but-nullable fields
(`package_manager`, `dockerfile_snippet`, and `CoverageSetupOutput`'s
`config_written`). The runtime path emits these as `null` when missing
but schemars treats `Option<T>` as "may be absent," so listing them as
required would force the drift gate to flag a shape difference that
isn't user-visible. Same pattern as `ReviewReconcileOutput.target` in
the previous commit.

Part of issue #338 Phase 5.
All 11 top-level envelopes and the 10 utility shapes are now derived
from Rust source via `crates/cli/src/output_envelope.rs` +
`crates/types/src/envelope.rs`. Rewrite the "Layer 2: hand-written
sections" list to reflect what's left (top-level metadata + the
`CombinedOutput` / `AuditOutput` sub-key `$ref`s that still point at
`DupesOutput` / `HealthOutput` even though the wire carries bare body
types). Update the Layer 1 paragraph to call out
`crates/types/src/envelope.rs` and `crates/cli/src/output_envelope.rs`
as additional source-of-truth locations.

Regenerate the VS Code TypeScript types (`pnpm run codegen:types`) and
the duplicate copy at `npm/fallow/types/output-contract.d.ts`. Fix up
the three call sites + three test fixtures whose `CheckSummary` /
`CheckOutput` literals were missing the new
`empty_catalog_groups` / `unused_dependency_overrides` /
`misconfigured_dependency_overrides` fields and the now-required
`summary` field on `CheckOutput`. `pnpm run lint` and `pnpm run
test:unit` both pass; the integration runner SIGABRTs on a corrupted
`@vscode/test-electron` cache that's unrelated to this change.

Part of issue #338 Phase 5.
…the wire

The committed schema still pointed `CombinedOutput.dupes` and
`AuditOutput.duplication` at `#/definitions/DupesOutput`, and the matching
health sub-keys at `#/definitions/HealthOutput`. Both `DupesOutput` and
`HealthOutput` require the envelope header (`schema_version` / `version`
/ `elapsed_ms`), but the runtime emit calls `serde_json::to_value(&report)`
on the bare `DuplicationReport` / `HealthReport` body for these sub-keys
and never wraps them. AJV consumers reading the documented contract
would reject every combined / audit JSON payload as missing the
envelope header on `dupes` / `health` / `duplication` / `complexity`.

Repoint the four sub-key `$ref`s at new committed `DuplicationReport`
and `HealthReport` definitions that mirror the bare body shape. Add the
two names to `derived_definition_names()` so the property-key drift
gate covers them.

Add `committed_property_refs_match_derived_property_refs`, a targeted
seventh drift test that compares the `$ref` value of every property
between the derived and committed schemas (peeling schemars's `allOf` /
`anyOf` wrappers via the existing `normalize_one` canonicalisation).
The earlier property-key gate only checked that a property exists on
both sides under the same name; it could not catch a sub-key pointing
at the wrong target type. Reverting the four `$ref` fixes in
`docs/output-schema.json` and re-running the gate confirms it fires
with the expected four-line failure listing every divergent sub-key.

Narrow the VS Code `FallowDupesResult` alias from `DupesOutput` to
`DuplicationReport`: the extension only reads dupes via the combined
invocation (`fallow --format json`), where the wire never carries the
envelope header on `combined.dupes`. The old alias was inaccurate;
the corrected one matches reality and reflects what consumers actually
read (clone_groups, clone_families, stats, mirrored_directories).
Regenerate the bundled VS Code TS types + npm wrapper copy.

Refresh `output_envelope.rs` doc comments to drop the now-stale
"pre-Phase-5 shape, the property-key drift gate only checks key
membership" caveat, and update `CONTRIBUTING.md` Layer 2 prose to
describe the new ref-value drift test instead.

Caught by parallel reviewers (Codex) running against the just-pushed
branch. Both BLOCKs surfaced together: the schema lied about the
shape, and the existing gate was too loose to catch the lie.

Refs #338.
The prior entry covered only the Phase 4 foundation (health subtree
deriving JsonSchema). Phase 5 added envelope utility types and the
per-command envelope structs (CheckOutput, CombinedOutput, AuditOutput,
DupesOutput, HealthOutput, ExplainOutput, CodeClimateOutput,
ReviewEnvelopeOutput, ReviewReconcileOutput, CoverageSetupOutput) plus
the DuplicationReport / HealthReport body shapes referenced by
CombinedOutput / AuditOutput sub-results.

Expanded the existing bullet into a structured list covering health
subtree (the original scope), per-finding action wrappers, envelope
utility shapes, per-command envelopes, body shapes, and the
previously-emitted-but-undocumented fields the Phase 5 work picked up
along the way.

Refs #338.
…ing-enum

Two fixes needed before Phase 5 can land:

1. **Clone-action augmentation.** `CloneFamily` and `CloneGroup` are
   regenerated from the Rust source (in `derived_definition_names()`)
   but were missing from `finding_definition_names()`, so the
   augmentation step did not graft `actions` onto them. The runtime
   `inject_dupes_actions` in `crates/cli/src/report/json.rs` walks both
   arrays and appends `actions[]` to every item, so the wire shape DID
   have the field while the regenerated schema did NOT. Added both to
   the augmentation list with the matching `CloneFamilyAction` /
   `CloneGroupAction` item refs. `CloneGroup` carries `introduced` (it
   flows through `fallow audit`), `CloneFamily` does not.

2. **Drop `SuppressAutoFixable` singleton-string-enum.** The Phase 4
   foundation introduced a `SuppressAutoFixable::False` Rust enum
   intending to constrain the schema to `auto_fixable: { const: false }`
   on suppress actions. Schemars 1 serializes Rust enums as strings
   (the variant rename), so the schema emitted `{const: "false"}`
   (string), not `{const: false}` (boolean). The runtime emits
   `auto_fixable: false` (boolean) via `serde_json::json!` macros, so
   the schema documented the wrong wire type. Dropped the enum,
   reverted the field type to `bool` on `SuppressLineAction` /
   `SuppressFileAction`. The schema now documents `auto_fixable: bool`
   (loses the explicit `const: false` constraint but matches the wire
   exactly). Cross-skill note: incident 2026-04-30
   ("Closed-enum field violations across hand-rolled emit paths")
   covers the symmetric case where a new emit path produces values
   outside the schema's enum; this is the inverse, where the schema
   declared a type the wire does not produce.

3. **`actions` is now in every finding's `required` array.** The
   augmentation pushes `"actions"` onto the required list so the
   regenerated schema documents what the wire actually carries (the
   `actions[]` array is always present, possibly empty). Backports
   the Phase 8 `augment_finding_definition` requireds fix onto the
   Phase 5 branch so the strict drift gate stays green when Phase 8
   lands.

Verified:
- `cargo test --workspace --all-targets` green.
- `cargo clippy --workspace --all-targets -- -D warnings` clean.
- `cargo fmt --all -- --check` clean.
- `pnpm lint` clean in `editors/vscode/` (TS contracts in lock-step).

Refs #338.
@BartWaardenburg BartWaardenburg merged commit cf9a85c into main May 16, 2026
21 checks passed
@BartWaardenburg BartWaardenburg deleted the feat/issue-338-phase5-envelopes branch May 16, 2026 21:14
BartWaardenburg added a commit that referenced this pull request May 16, 2026
One-line removal of `>>>>>>> origin/main` orphan from CHANGELOG.md line 41.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant