Ship it
Linting
featurevisor lint checks every definition and test spec before anything is built. It catches a misspelled attribute, a value outside its schema or a feature that requires itself, and names the file, the path and usually the fix. Run it on every pull request, so a mistake is caught in review rather than by users.
Run it#
npx featurevisor lintMisspell the plan attribute in the payingAccounts segment, and lint stops with exit code 1:
segments/payingAccounts.yml => Error: Unknown attribute "plann". Did you mean "plan"? Path: conditions.0.attributeFix every error it reports, then run it again until it passes. A file that cannot be parsed is reported with its line and column. Add --verbose to see the parser's stack while investigating.
What it checks#
- Structure: every definition and spec has the fields its kind allows, of the right type.
- References: attributes, segments, features, global variables and schemas exist, with a suggestion when a name is close to one that does.
- Values: defaults, overrides, mutations and test expectations match their schemas.
- Operators: each operator suits the declared type of what it compares.
- Dates: timestamp attributes and variables, declared with
format: date-time, accept only the six date and presence operators, and theirenumandconstvalues must be timestamps. String schemas withformat: date-timevalidate timestamp values. - Context: test values that do not match declared attributes produce warnings, not errors.
- Dependencies: conditions do not form a cycle or a chain too deep to evaluate.
- Archived definitions: no active definition, spec or target references an archived feature, global variable, segment or attribute, including the keys of sticky values in specs. Archived definitions are left out of datafiles, so such a reference would silently stop matching. An archived definition is checked for its shape only. Its own references are not resolved, so what it used can change or be deleted.
- Deprecated definitions: a reference to a deprecated feature, feature variable or global variable from an active definition is a warning, not an error.
- Patterns: regular expressions stay in the portable subset and cannot backtrack exponentially.
- Project rules: tags, reserved keys and the optional lint rules in your configuration, such as the kinds of condition each location accepts.
Lint does not run your tests, and test does not lint first. Run both, lint first, so a broken spec is reported as a broken spec rather than as a failing assertion:
npx featurevisor lintnpx featurevisor testIn CI, run them as separate steps that stop on failure, or join them with &&.
build does not lint either. A build can succeed with definitions lint would reject, such as an invalid date operand, so a successful build does not mean the project is valid. Lint before you build too.
Lint part of a project#
--key-pattern takes a regular expression matched against keys, case sensitively, and --entity-type narrows it to one kind:
npx featurevisor lint \ --key-pattern=checkout \ --entity-type=featureThe pattern is not anchored, so checkout also matches checkoutSettings. Use ^checkout$ for exactly one key.
The entity types are attribute, segment, feature, variable, schema, target, test and scenario. With npm scripts, pass options after --, as in npm run lint -- --key-pattern=checkout.
JSON output#
Pass --json for output a script can read, and --pretty to indent it:
npx featurevisor lint --json --pretty{ "errors": [ { "filePath": "segments/payingAccounts.yml", "entityType": "segment", "key": "payingAccounts", "message": "Unknown attribute \"plann\". Did you mean \"plan\"?", "path": ["conditions", 0, "attribute"], "code": "custom" } ], "warnings": []}The exit code is the same as without --json: 1 when there are errors, 0 when there are none. Warnings alone never fail.
Issue codes#
Each issue has a code:
The message beside each code says what to change. Parser and plugin failures may use their own error class name as the code. Errors in the command's arguments use the CLI error output instead.
Composition warnings#
These advisory warnings do not reject a definition or change the exit status. Lint checks them after the variable passes schema validation.
composition_experiment_override means a mutation or replacement may change a variation or feature variable from a feature declaring variations, including through nested compositions. The final value can differ from the assignment or value recorded by the source evaluation. Change the original experiment when the change belongs to it, and follow the composition exposure guidance.
composition_nullable_mutation means a mutation may traverse a member whose source can normally return null, such as a prerequisite without a fallback or a feature variable without a disabled fallback. A feature variable without that fallback still warns when its feature has an unconditional 100% rule. Add a source fallback or guard the override with conditions on the same source:
notNullon the whole source, with nopathorpath: []notcontaining exactly oneisNullcheck on the whole source, with nopathorpath: []existsornotNullon a source path, since a path cannot exist inside a null source
Checks inherited from parent overrides also guard children. In an and group, one guarding branch is enough. In an or group, every branch must guard the same source. Checks on a different source and not groups with multiple entries do not establish the guard.
The warning does not cover root replacements or every defensive getter null possibility. Append operations that create a null final property as an array do not warn, but traversing a null ancestor still can. A mutation through null otherwise follows normal missing path behaviour and leaves the value unchanged. A matched variable_override_rule does not prove a value changed, so test the final value. Guards do not narrow generated nullable types.
Deprecated references#
This advisory warning does not reject a definition or change the exit status. It marks each place where an active segment, feature or global variable refers to a deprecated feature, feature variable or global variable:
=> Warning: Deprecated feature "liveChat" is referenced. Remove or replace the reference before the feature is archived. (feature chatFileUpload: conditions.0.feature)A deprecated definition still evaluates, so nothing breaks yet. Once it is archived, the same reference fails lint with archived_reference. Definitions that are deprecated or archived themselves do not get the warning, and specs never do, since a deprecated definition still needs its tests.
Context warnings#
Lint checks test contexts against every declared attribute type and its constraints, including object paths. It covers feature, segment and variable assertions, expanded matrices, scenarios and child contexts. Unknown context keys are ignored, and values with unexpanded matrix placeholders are skipped.
Mismatches produce context_value_mismatch with the path and received value. Each distinct invalid matrix value gets its own warning. Values longer than 80 characters are shortened in the message, while the JSON lint report retains the full value. They do not change the exit status, so tests can deliberately exercise malformed input. A date only value such as now: '2026-11-27' warns because it lacks a time and timezone. Use a complete timestamp.
Context value "2026-11-27" for "now" is not a valid date. The before and after operators do not match this value. Use YYYY-MM-DDTHH:mm:ss with Z or ±HH:mm.CLI evaluate uses the same checks for supplied context without changing its results. Text output lists warnings. JSON output adds a warnings array of { attribute, message } only when there are warnings. SDKs and test runners do not validate context or emit these warnings. A warning does not guarantee a condition is false: not can invert a failed date comparison.
Large dependency graphs#
Lint warns with dependency_expansion_cost when a connected group of requirements could reach more than 1,000 distinct prerequisite edges. Shared paths are counted once. It is a prompt to review the graph, not a limit: what runs depends on short circuiting, the context and any modules. Review the conditions and test representative contexts.
Upgrading from v3#
--v3-mutation-order compares feature variable mutations with v3's ordering, which applied the shortest paths first. v4 applies them in the order they are written:
npx featurevisor lint --v3-mutation-orderEach difference is reported as a mutation_order_changed warning with both values, so you can check which one you meant and test it. Normal linting skips this comparison. See the v4 migration guide.


