Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 45 additions & 5 deletions crates/output/src/feature_flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -100,11 +100,21 @@ pub struct FeatureFlagDeadCodeOverlap {
pub dead_exports: Vec<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.
#[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<FeatureFlagsMetaDetails>,
/// Local telemetry correlation metadata for agent follow-up runs.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub telemetry: Option<TelemetryMeta>,
}

/// Feature flag explanatory metadata.
Expand Down Expand Up @@ -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)",
Expand All @@ -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",
},
}),
}
}

Expand Down Expand Up @@ -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"
);
}
}
26 changes: 21 additions & 5 deletions docs/output-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
16 changes: 14 additions & 2 deletions editors/vscode/src/generated/output-contract.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 14 additions & 2 deletions npm/fallow/types/output-contract.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading