From a1339f2dabf9156cc8c350c4ef9ae1075d8175ce Mon Sep 17 00:00:00 2001 From: 0xGaspar Date: Thu, 30 Jul 2026 19:08:36 +0000 Subject: [PATCH] refactor: name the two remaining --json schema literals (#444) Every outer `--json` envelope in the tree already routes through `crate::envelope::{resource,action,versioned}`; an audit of all 54 call sites turned up exactly two that still passed a bare `1` for `schema_version` instead of a named per-command constant: - `plugins validate` (src/plugin/validate.rs) - `lanes validate` (src/lanes/validate.rs) Every sibling envelope in those two modules uses a `*_SCHEMA` const declared next to its peers, so a future shape bump for validate would have had to be found by grepping for a magic number. Add `PLUGINS_VALIDATE_SCHEMA` / `LANES_VALIDATE_SCHEMA` alongside the existing constants and use them. Both are `1`, so the emitted bytes are unchanged. Also close two gaps in the envelope test module: - `resource` had no byte-identity test, unlike `action` and `versioned`. - Nothing covered the `versioned(v, to_value(struct))` shape that the two validate commands use, where the struct's fields must flatten to the top level next to `schema_version` rather than nest under a key. No public JSON surface changes. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01KuhwrJF2XFDVHhX7qzDuyy --- src/envelope.rs | 54 ++++++++++++++++++++++++++++++++++++++++++ src/lanes/mod.rs | 1 + src/lanes/validate.rs | 6 ++++- src/plugin/mod.rs | 1 + src/plugin/validate.rs | 8 ++++++- 5 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/envelope.rs b/src/envelope.rs index 05bc841..dfa5b06 100644 --- a/src/envelope.rs +++ b/src/envelope.rs @@ -107,6 +107,60 @@ mod tests { assert_eq!(typed["results"], serde_json::json!(["x", "y"])); } + #[test] + fn resource_is_byte_identical_to_hand_rolled_json() { + // Same migration guarantee the action/versioned dialects assert: the + // resource dialect must serialize identically to the hand-rolled form + // it replaced, so adopting the helper is never a wire change. + let built = resource( + 1, + "plugins", + vec![serde_json::json!({ "name": "github", "version": "0.4.0" })], + ); + let hand = serde_json::json!({ + "schema_version": 1, + "plugins": [{ "name": "github", "version": "0.4.0" }], + }); + assert_eq!( + serde_json::to_string_pretty(&built).unwrap(), + serde_json::to_string_pretty(&hand).unwrap() + ); + } + + #[test] + fn versioned_flattens_a_serialized_struct_alongside_schema_version() { + // The shape `plugins validate` / `lanes validate` emit: a report struct + // is serialized to a Value, then wrapped. The report's own fields must + // stay at the top level next to `schema_version`, not nested under a + // key, and the result must match the hand-rolled equivalent. + #[derive(Serialize)] + struct Report { + path: String, + lane_count: usize, + errors: Vec, + warnings: Vec, + } + let report = Report { + path: "fledge.toml".to_string(), + lane_count: 3, + errors: vec![], + warnings: vec!["unpinned step".to_string()], + }; + + let built = versioned(1, serde_json::to_value(&report).unwrap()); + let hand = serde_json::json!({ + "schema_version": 1, + "path": "fledge.toml", + "lane_count": 3, + "errors": [], + "warnings": ["unpinned step"], + }); + assert_eq!( + serde_json::to_string_pretty(&built).unwrap(), + serde_json::to_string_pretty(&hand).unwrap() + ); + } + #[test] fn action_leads_with_schema_version_and_action_then_merges_fields() { let out = action( diff --git a/src/lanes/mod.rs b/src/lanes/mod.rs index 0de4771..f0df252 100644 --- a/src/lanes/mod.rs +++ b/src/lanes/mod.rs @@ -40,6 +40,7 @@ pub(super) const LANES_SEARCH_SCHEMA: u32 = 1; pub(super) const LANES_IMPORT_SCHEMA: u32 = 1; pub(super) const LANES_CREATE_SCHEMA: u32 = 1; pub(super) const LANES_PUBLISH_SCHEMA: u32 = 1; +pub(super) const LANES_VALIDATE_SCHEMA: u32 = 1; #[derive(Debug, Deserialize)] pub(super) struct FledgeFileWithLanes { diff --git a/src/lanes/validate.rs b/src/lanes/validate.rs index 575382f..c597528 100644 --- a/src/lanes/validate.rs +++ b/src/lanes/validate.rs @@ -174,7 +174,11 @@ pub(crate) fn print_lane_report( ) -> Result<()> { if json { // Wrap with schema_version envelope (matches lanes list/run/search shape). - let value = crate::envelope::versioned(1, serde_json::to_value(report)?); + // The version comes from the named per-command constant rather than a + // literal so a future shape change bumps it in the same place as every + // other `lanes` envelope. + let value = + crate::envelope::versioned(super::LANES_VALIDATE_SCHEMA, serde_json::to_value(report)?); println!("{}", serde_json::to_string_pretty(&value)?); } else if report.errors.is_empty() && report.warnings.is_empty() { println!( diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs index beb066b..75afef7 100644 --- a/src/plugin/mod.rs +++ b/src/plugin/mod.rs @@ -71,6 +71,7 @@ const PLUGINS_SEARCH_SCHEMA: u32 = 1; const PLUGINS_RECOMMEND_SCHEMA: u32 = 1; const PLUGINS_CREATE_SCHEMA: u32 = 1; const PLUGINS_PUBLISH_SCHEMA: u32 = 1; +const PLUGINS_VALIDATE_SCHEMA: u32 = 1; // ─── Types ─────────────────────────────────────────────────────────────────── diff --git a/src/plugin/validate.rs b/src/plugin/validate.rs index 8845506..eb5ddcf 100644 --- a/src/plugin/validate.rs +++ b/src/plugin/validate.rs @@ -166,7 +166,13 @@ pub(crate) fn print_plugin_report( // schema_version (matches plugins list/audit/search shape). // The full report is flattened so existing fields (path, // plugin_name, errors, warnings) sit at the same level. - let value = crate::envelope::versioned(1, serde_json::to_value(report)?); + // The version comes from the named per-command constant rather than a + // literal so a future shape change bumps it in the same place as every + // other `plugins` envelope. + let value = crate::envelope::versioned( + super::PLUGINS_VALIDATE_SCHEMA, + serde_json::to_value(report)?, + ); println!("{}", serde_json::to_string_pretty(&value)?); } else if report.errors.is_empty() && report.warnings.is_empty() { let name = if report.plugin_name.is_empty() {