feat(schema): derive JSON output schema from Rust source of truth (Phase 5)#382
Merged
Conversation
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.
11 tasks
# Conflicts: # CHANGELOG.md
…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
added a commit
that referenced
this pull request
May 16, 2026
One-line removal of `>>>>>>> origin/main` orphan from CHANGELOG.md line 41.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Supersedes #374 with the full Phase 4 foundation + Phase 5 envelope derivation. Every type in
derived_definition_names()(incrates/cli/src/bin/schema_emit.rs) is now generated from#[derive(JsonSchema)]on the matching Rust struct; thefallow-schema-emitbinary regeneratesdocs/output-schema.jsonand 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.tsis now machine-derived end to end.What's in this PR (13 commits, ordered)
Phase 4 foundation (the original PR #374 scope):
f6f776bd feat(schema): derive JSON output schema from Rust source of truth6fbe040f feat(schema): derive health output subtree from Rust source of truth8c790921 feat(schema): cover coverage-gap actions and runtime-coverage helpersPhase 5 envelope derivation:
4.
1b06fdb1 feat(schema): derive envelope utility types from Rust source of truth5.
0a7d4943 feat(schema): derive DupesOutput envelope from Rust source of truth6.
e95c34a6 feat(schema): derive CheckOutput / CheckGroupedOutput from Rust source of truth7.
dde8760d feat(schema): derive HealthOutput envelope from Rust source of truth8.
24629e9d feat(schema): derive Explain / CodeClimate / Review envelopes from Rust9.
7b8ddf30 feat(schema): derive CombinedOutput envelope from Rust source of truth10.
530ba13f feat(schema): derive AuditOutput envelope from Rust source of truth11.
77808f41 feat(schema): derive CoverageSetupOutput envelope from Rust source of truth12.
4e88d763 docs(contributing): collapse Layer 2 list now that envelopes are derived13.
3bb046bd fix(schema): align CombinedOutput / AuditOutput sub-result refs with the wire14.
1ea16889 docs(changelog): expand [Unreleased] for Phase 5 envelope derivationWhat stays hand-maintained
Only the top-level
docs/output-schema.jsonmetadata ($schema,title,description,oneOflisting the 11 envelopes).merge_with_committedpreserves it verbatim across regens.Wire compatibility
SCHEMA_VERSIONstays at 6. The Rust source is the type-side reflection of whatcrates/cli/src/report/json.rsalready 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 fourVitalSignsCountsfields, theactionsarrays onUntestedFile/UntestedExport). Two fields drop fromrequiredto match theirOption<T>Rust sources (OwnershipMetrics.unowned,RuntimeCoverageSummary.last_received_at).Relationship to other PRs
feat/issue-338-schemars-derive). Phase 5's branch contains feat(schema): derive JSON output schema from Rust source of truth #374's 3 commits as its base.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 warningscargo fmt --all -- --checkpnpm run check:codegenRefs #338.