From 6230d92f8510011125e6e38bfd1c733baf859922 Mon Sep 17 00:00:00 2001 From: Bart Waardenburg Date: Mon, 13 Jul 2026 20:34:07 +0200 Subject: [PATCH] fix(output): conform feature-flags _meta to the published schema `fallow flags --format json` without `--explain` injects a `_meta.telemetry` block for run correlation (via the post-serialization attach_telemetry_meta pass), but the schema modeled `FeatureFlagsMeta` as requiring the explain-only `feature_flags` field and did not model `telemetry`. The emitted default-path document therefore failed validation against docs/output-schema.json: `_meta` was neither null nor a valid FeatureFlagsMeta. The MCP `feature_flags` tool and Code Mode emit the same shape, so agents validating against the published schema saw invalid output. FeatureFlagsMeta now models both `feature_flags` and `telemetry` as optional, mirroring the `Meta` and `CombinedMeta` envelopes whose telemetry is likewise an optional, never-required property injected by the same post-pass. The wire bytes are unchanged (this is a schema-correctness fix, not a wire change), so there is no schema_version bump. Regenerated docs/output-schema.json and the TS contracts; added a regression test pinning the telemetry-only default-path _meta shape. Found by the plan-028 agent-surface instance-validation probe. --- CHANGELOG.md | 12 +++++ crates/output/src/feature_flags.rs | 50 +++++++++++++++++-- docs/output-schema.json | 26 ++++++++-- .../vscode/src/generated/output-contract.d.ts | 16 +++++- npm/fallow/types/output-contract.d.ts | 16 +++++- 5 files changed, 106 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d00ae3f0..5211e53da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 older VS Code extension paired with a newer npm package, keep working via that path. +### Fixed + +- **`fallow flags --format json` now conforms to the published output schema.** + The default (no `--explain`) feature-flags document injects a + `_meta.telemetry` block for run correlation, but the schema modeled + `_meta` as requiring the explain-only `feature_flags` field and did not model + `telemetry`, so the emitted document failed validation against + `docs/output-schema.json`. This affected the MCP `feature_flags` tool and Code + Mode, which emit the same shape. `FeatureFlagsMeta` now models both fields as + optional, mirroring the `Meta` and `CombinedMeta` envelopes. The wire output + is unchanged (this is a schema-correctness fix); no `schema_version` bump. + ## [3.4.2] - 2026-07-13 ### Added diff --git a/crates/output/src/feature_flags.rs b/crates/output/src/feature_flags.rs index b13ccbdd7..df3b482ba 100644 --- a/crates/output/src/feature_flags.rs +++ b/crates/output/src/feature_flags.rs @@ -3,7 +3,7 @@ use std::path::Path; use std::time::Duration; -use fallow_types::envelope::{ElapsedMs, SchemaVersion, ToolVersion}; +use fallow_types::envelope::{ElapsedMs, SchemaVersion, TelemetryMeta, ToolVersion}; use fallow_types::results::{FeatureFlag, FlagConfidence, FlagKind}; use serde::Serialize; @@ -100,11 +100,21 @@ pub struct FeatureFlagDeadCodeOverlap { pub dead_exports: Vec, } -/// `_meta.feature_flags` details emitted with `--explain`. +/// Optional `_meta` block for [`FeatureFlagsOutput`]. Both fields are optional +/// because the two contributors are independent: `feature_flags` details are +/// present only with `--explain`, and `telemetry` is injected post-pass by +/// [`attach_telemetry_meta`] whenever an analysis run id is available (which is +/// the default path). Mirrors `Meta` / `CombinedMeta`, which also model +/// `telemetry` as an optional, never-required property. #[derive(Debug, Clone, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct FeatureFlagsMeta { - pub feature_flags: FeatureFlagsMetaDetails, + /// Feature-flag detection explanations, emitted only with `--explain`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub feature_flags: Option, + /// Local telemetry correlation metadata for agent follow-up runs. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub telemetry: Option, } /// Feature flag explanatory metadata. @@ -173,7 +183,8 @@ pub fn serialize_feature_flags_json_output( #[must_use] pub const fn feature_flags_meta() -> FeatureFlagsMeta { FeatureFlagsMeta { - feature_flags: FeatureFlagsMetaDetails { + telemetry: None, + feature_flags: Some(FeatureFlagsMetaDetails { description: "Feature flag patterns detected via AST analysis", kinds: FeatureFlagsKindMeta { environment_variable: "process.env.FEATURE_* pattern (high confidence)", @@ -186,7 +197,7 @@ pub const fn feature_flags_meta() -> FeatureFlagsMeta { low: "Heuristic match (config objects), may produce false positives", }, docs: "https://docs.fallow.tools/cli/flags", - }, + }), } } @@ -310,4 +321,33 @@ mod tests { ); assert_eq!(value["_meta"]["telemetry"]["analysis_run_id"], "run-flags"); } + + #[test] + fn feature_flags_json_output_without_explain_emits_telemetry_only_meta() { + // The default path (no --explain) leaves `meta` as None, so the only + // `_meta` contributor is the post-pass telemetry injection. The typed + // `FeatureFlagsMeta` must model this telemetry-only shape (both fields + // optional) so the emitted document conforms to the published schema. + let output = build_feature_flags_output(FeatureFlagsOutputInput { + schema_version: 7, + version: "0.0.0".to_string(), + elapsed: Duration::from_millis(4), + flags: &[flag()], + root: Path::new("/repo"), + meta: None, + }); + + let value = serialize_feature_flags_json_output( + output, + RootEnvelopeMode::Tagged, + Some("run-flags"), + ) + .expect("feature flags output should serialize"); + + assert_eq!(value["_meta"]["telemetry"]["analysis_run_id"], "run-flags"); + assert!( + value["_meta"].get("feature_flags").is_none(), + "feature_flags details are absent without --explain" + ); + } } diff --git a/docs/output-schema.json b/docs/output-schema.json index a5d608c63..ee4f76891 100644 --- a/docs/output-schema.json +++ b/docs/output-schema.json @@ -17666,13 +17666,29 @@ "type": "object", "properties": { "feature_flags": { - "$ref": "#/definitions/FeatureFlagsMetaDetails" + "anyOf": [ + { + "$ref": "#/definitions/FeatureFlagsMetaDetails" + }, + { + "type": "null" + } + ], + "description": "Feature-flag detection explanations, emitted only with `--explain`." + }, + "telemetry": { + "anyOf": [ + { + "$ref": "#/definitions/TelemetryMeta" + }, + { + "type": "null" + } + ], + "description": "Local telemetry correlation metadata for agent follow-up runs." } }, - "required": [ - "feature_flags" - ], - "description": "`_meta.feature_flags` details emitted with `--explain`." + "description": "Optional `_meta` block for [`FeatureFlagsOutput`]. Both fields are optional\nbecause the two contributors are independent: `feature_flags` details are\npresent only with `--explain`, and `telemetry` is injected post-pass by\n[`attach_telemetry_meta`] whenever an analysis run id is available (which is\nthe default path). Mirrors `Meta` / `CombinedMeta`, which also model\n`telemetry` as an optional, never-required property." }, "FeatureFlagsMetaDetails": { "type": "object", diff --git a/editors/vscode/src/generated/output-contract.d.ts b/editors/vscode/src/generated/output-contract.d.ts index 30e13de93..0f66a16d3 100644 --- a/editors/vscode/src/generated/output-contract.d.ts +++ b/editors/vscode/src/generated/output-contract.d.ts @@ -9352,10 +9352,22 @@ dead_export_count: number dead_exports: string[] } /** - * `_meta.feature_flags` details emitted with `--explain`. + * Optional `_meta` block for [`FeatureFlagsOutput`]. Both fields are optional + * because the two contributors are independent: `feature_flags` details are + * present only with `--explain`, and `telemetry` is injected post-pass by + * [`attach_telemetry_meta`] whenever an analysis run id is available (which is + * the default path). Mirrors `Meta` / `CombinedMeta`, which also model + * `telemetry` as an optional, never-required property. */ export interface FeatureFlagsMeta { -feature_flags: FeatureFlagsMetaDetails +/** + * Feature-flag detection explanations, emitted only with `--explain`. + */ +feature_flags?: (FeatureFlagsMetaDetails | null) +/** + * Local telemetry correlation metadata for agent follow-up runs. + */ +telemetry?: (TelemetryMeta | null) } /** * Feature flag explanatory metadata. diff --git a/npm/fallow/types/output-contract.d.ts b/npm/fallow/types/output-contract.d.ts index 30e13de93..0f66a16d3 100644 --- a/npm/fallow/types/output-contract.d.ts +++ b/npm/fallow/types/output-contract.d.ts @@ -9352,10 +9352,22 @@ dead_export_count: number dead_exports: string[] } /** - * `_meta.feature_flags` details emitted with `--explain`. + * Optional `_meta` block for [`FeatureFlagsOutput`]. Both fields are optional + * because the two contributors are independent: `feature_flags` details are + * present only with `--explain`, and `telemetry` is injected post-pass by + * [`attach_telemetry_meta`] whenever an analysis run id is available (which is + * the default path). Mirrors `Meta` / `CombinedMeta`, which also model + * `telemetry` as an optional, never-required property. */ export interface FeatureFlagsMeta { -feature_flags: FeatureFlagsMetaDetails +/** + * Feature-flag detection explanations, emitted only with `--explain`. + */ +feature_flags?: (FeatureFlagsMetaDetails | null) +/** + * Local telemetry correlation metadata for agent follow-up runs. + */ +telemetry?: (TelemetryMeta | null) } /** * Feature flag explanatory metadata.