Skip to content

feat(schema): derive JSON output schema from Rust source of truth#374

Closed
BartWaardenburg wants to merge 3 commits into
mainfrom
feat/issue-338-schemars-derive
Closed

feat(schema): derive JSON output schema from Rust source of truth#374
BartWaardenburg wants to merge 3 commits into
mainfrom
feat/issue-338-schemars-derive

Conversation

@BartWaardenburg
Copy link
Copy Markdown
Collaborator

Summary

Phase 1+2+3+6+7+9 of #338: the per-finding and duplication result structs now derive JsonSchema (behind a new schema cargo feature on fallow-types + fallow-core), and a new fallow-schema-emit dev binary regenerates docs/output-schema.json#/definitions from the Rust source of truth.

  • Drift gate (5 active + 1 strict ignored). cargo test -p fallow-cli --features schema-emit --bin fallow-schema-emit catches Rust → schema drift: property renames, additions, removals, required-flag changes, and dangling $ref targets in the merged document. CI runs the gate + a --tests clippy on the schema-emit binary so the contract is enforced on every push.
  • IssueAction enum hierarchy in new module crates/types/src/output.rs (IssueAction, FixAction, FixActionType, SuppressLineAction, SuppressFileAction, AddToConfigAction, plus the singleton kind discriminants). Schema-only today; a follow-up will route crates/cli/src/report/json.rs through them instead of serde_json::json! builders.
  • Two latent schema-drift fixes 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 on empty-value findings but had no schema declaration.
  • VS Code codegen regenerated cleanly (editors/vscode/src/generated/output-contract.d.ts + npm/fallow/types/output-contract.d.ts); one test fixture gained the new required specifier_col field.
  • CONTRIBUTING.md documents the new derive-then-emit flow; .claude/rules/vscode-extension.md updated with the Rust → schema → TS chain.

Phase 4 (health subtree), Phase 5 (typed envelope structs replacing the serde_json::json! builders), and Phase 8 (prose migration into Rust doc comments) are intentionally deferred to follow-up PRs and listed under "Layer 2: hand-written sections" in CONTRIBUTING.md.

Test plan

  • cargo check --workspace clean
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • cargo clippy -p fallow-cli --features schema-emit --bin fallow-schema-emit --tests -- -D warnings clean
  • cargo clippy -p fallow-types --features schema -- -D warnings and cargo clippy -p fallow-core --features schema -- -D warnings clean
  • cargo test --workspace --lib 5959 tests pass, 0 fail
  • cargo test -p fallow-cli full integration suite green
  • cargo test -p fallow-cli --features schema-emit --bin fallow-schema-emit 5 active drift tests pass, 1 strict variant ignored
  • cargo fmt --all -- --check clean, typos . clean, cargo doc --workspace --no-deps --document-private-items no warnings
  • cargo run -p fallow-cli --features schema-emit --bin fallow-schema-emit produces a self-consistent schema (128 definitions, 119 refs, 0 dangling)
  • cd editors/vscode && pnpm run check:codegen clean; pnpm run lint clean; pnpm run test 6/6 pass; pnpm run build rebuilds the committed bundle without further dist/ diff
  • python3 -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml'))" parses the new CI steps

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
@BartWaardenburg
Copy link
Copy Markdown
Collaborator Author

Superseded by #382, which contains this PR's 3 foundation commits (f6f776bd, 6fbe040f, 8c790921) at the base of the Phase 5 envelope-derivation stack plus 10 additional commits that finish the Rust-as-source-of-truth migration.

The new PR rebases cleanly on origin/main and unblocks #380 (Phase 8: close the strict drift gate), which is stacked on it.

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