Linting - Featurevisor
Featurevisor

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

Command
npx featurevisor lint

Misspell the plan attribute in the payingAccounts segment, and lint stops with exit code 1:

Output
segments/payingAccounts.yml
=> Error: Unknown attribute "plann". Did you mean "plan"?
Path: conditions.0.attribute

Fix 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 their enum and const values must be timestamps. String schemas with format: date-time validate 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:

Commands
npx featurevisor lint
npx featurevisor test

In 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:

Command
npx featurevisor lint \
--key-pattern=checkout \
--entity-type=feature

The 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:

Command
npx featurevisor lint --json --pretty
Output
{
"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:

CodeCauseFix
invalid_typeA property is missing or has the wrong typeUse the type the message names
invalid_valueA value is not one of the permitted choicesUse one of the listed values
invalid_unionA value matches none of the allowed shapesUse one of the shapes the page describes
unrecognized_keysAn object has properties it does not allowRemove or rename them
too_smallA value is below its minimumMeet the minimum
too_bigA value is above its maximumStay within the maximum
invalid_formatA string does not match its formatFix the string
invalid_keyA key of a collection is invalidFix the key at the path
invalid_elementAn item of a collection is invalidFix the item at the path
not_multiple_ofA number is not a multiple of the factorUse a multiple of it
customA contextual rule failed, such as an unknown referenceFollow the message
invalid_nameA key breaks the naming rulesRename it and its references
invalid_attribute_namespaceAn attribute definition is nested or its name contains a dotMove or rename it, or declare an object attribute
reserved_keyA key is reservedChoose another key
feature_variable_key_collisionA feature and a global variable share a keyRename one of them
reserved_nameAn attribute or property name is inherited by JavaScript objectsChoose another name
duplicate_tagA feature lists the same tag twiceRemove the repeated tag
removed_condition_fieldA v3 segments or required field remainsMove it into conditions
removed_schema_typeA v3 double, date or semver type remainsUse number, or a string with a format
removed_test_fieldA feature spec still uses stickyRename it to stickyFeatures
archived_referenceAn active definition or spec references an archived oneRemove the reference, or unarchive the definition
disallowed_conditionA location uses a kind of condition allowedConditions excludesMove the condition, or allow its kind
schema_v2_unsupportedA schema "2" target carries v4 only syntaxKeep it out of the target, or use the v3 shape
deprecated_referenceA warning about a reference to a deprecated definitionMove to its replacement before it is archived
missing_conditionsA selector has no conditionsAdd conditions, or '*' to match everything
unportable_regexA pattern uses syntax outside the portable subsetRewrite it with portable syntax
regex_backtrackingA pattern can take exponential time on short inputMake each repetition start or end distinctly
unknown_evaluation_reasonA test expects a reason no SDK returnsUse one of the listed reasons
removed_evaluation_fieldA test expects a v3 evaluation fieldAssert reason: conditions_unmet instead
circular_dependencyConditions form a dependency cycleBreak the cycle
dependency_depth_exceededA dependency chain exceeds the runtime limit of 256Shorten the chain
dependency_depthA warning about a chain deeper than 32Shorten the chain
dependency_validation_failedA dependency could not be checkedFix the reference in the message
dependency_expansion_costA warning about a large dependency graphReview the graph
mutation_order_changedA warning from --v3-mutation-orderCheck which value you meant
context_value_mismatchA warning about context outside an attribute schemaCheck the supplied value
composition_experiment_overrideA composed override may change an experiment source valueChange targeting on the source experiment
composition_nullable_mutationA mutation can traverse a source that normally returns nullAdd a fallback or a guarding requirement
EntityParseErrorA file could not be parsedFix the syntax at the line given
ErrorLoading or validation threw an errorFix what the message names
errorA mutation comparison failedFix the mutation in the message

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:

  • notNull on the whole source, with no path or path: []
  • not containing exactly one isNull check on the whole source, with no path or path: []
  • exists or notNull on 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:

Output
=> 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.

Warning
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:

Command
npx featurevisor lint --v3-mutation-order

Each 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.