Skip to main content

Diagnostics

Every issue the linter can report, with its trigger, an example, and the remedy. Codes are stable identifiers: once allocated, a code's meaning is immutable. New conditions get new codes.

If you are running exd lint and want to know what a code means, this is the page. If you are debugging an upload that failed at the server, the server returns the same codes in its manifest_lint_failed response.


Severity

SeverityCodesEffect on upload
ErrorE0xxSchema violation. The manifest is invalid. Upload rejected with manifest_lint_failed.
WarningW0xxLikely problem. The manifest is valid but suspicious. Upload accepted. CI MAY treat warnings as blocking via --deny-warnings.
InfoI0xxInformational hint. The manifest is fine. Upload accepted.

LintReport.passed is true iff the error bucket is empty. Warnings and infos do not affect it.


Conformance levels

Every requirement in the manifest format applies at one of three levels:

LevelEnforced byEffect of violation
SchemaTOML parser + linter (E0xx codes)Manifest is rejected. Server upload returns manifest_lint_failed (HTTP 422).
LintLinter only (W0xx and I0xx codes)Manifest is accepted, but the linter emits a warning or info. CI MAY treat warnings as errors via --deny-warnings.
ConventionReviewers, dashboards, documentationNo automated enforcement. Affects readability, ownership, and team workflow.

Schema-level rules use MUST in the reference text. Lint-level rules use SHOULD for recommendations and MUST for the linter's behavior itself ("the linter MUST emit W004 when …"). Convention-level rules use prose without normative keywords.

The buckets are mutually exclusive: an error never appears in the warning or info bucket and vice versa. Diagnostic codes are stable identifiers — once allocated, a code's meaning is immutable. New conditions get new codes.


Numeric index

Errors

CodeOne-line summary
E001TOML parse failure, or schema_version missing / invalid
E004Undeclared variant on an env block or rule
E005Missing segment reference
E006Invalid bucket range or entity_id_attribute
E009Rule missing required field
E010Unknown environment (typed mode)
E011Segment lacks predicate and bucket
E012Circular segment reference
E013Deprecated inline-targeting field
E014flag.type invalid, or variant value type mismatch
E015Predicate shape error
E016Unknown field on a structural table
E017Namespace slug mismatched with directory
E018Symlink in manifest tree
E019File size exceeds 256 KB
E020Empty or missing [flag.variants]
E021Invalid variant key
E022Invalid flag.lifecycle value
E023[namespace.environments] declared but empty
E024Invalid environment slug
E025[segment] table missing
E026Non-string segment or variant on a rule
E029Non-finite float in variant value
E030Invalid namespace slug
E031Invalid flag filename
E032Invalid segment filename
E033Empty values = [] on in / not_in
E034Attribute type conflict across predicates
E036Rule declares both segment and predicate
E037[flag.environments._] block missing
E038[flag.environments._] missing variant
E039testing = true without rules, or on _

Warnings

CodeOne-line summary
W002Retired flag still has rules
W003Flag has no rules anywhere
W004Bucket segment without explicit salt
W005Predicate nesting exceeds 5 levels
W007Empty compound predicate array
W008Minor schema_version mismatch across files
W009Subdirectory under flags/ or segments/
W010namespace.display_name empty when declared
W011flags/ or segments/ directory missing
W012Duplicate segment in rules within the same environment
W013Segment defined but never referenced
W014Variant defined but never used
W015Substring operator with empty-string operand
W016Empty per-env block

Infos

CodeOne-line summary
I001Flag has no owner
I002Flag has no description
I003Segment has no description

Reserved (no current emission)

CodeReason
W001Unreachable rule via subsumption — reachability analysis not implemented. Today, the simpler "duplicate segment in same env" case is handled by W012.
E002, E003Retired in schema 0.1. Were "missing or mismatched flag.key / segment.key". Both fields are gone — the key is the filename stem. Declaring key = "..." now raises E016.
E007, E008Reserved. Were "duplicate flag/segment key". Structurally unreachable — a filesystem cannot return two regular files with the same name in the same directory.
E027, E028, E035, W006Retired in schema 0.1, replaced by the per-env-complete resolution model. See resolution.
I004Reserved for a future environment-write-gating diagnostic.
E040+ / W017+ / I005+Future minor-version diagnostics.

The numbering is sparse on purpose so future minor versions can introduce diagnostics without renumbering existing ones.


Errors by author intent

When editing namespace.toml

E001, E016, E017, E023, E024, E030, W010

When editing flags/<key>.toml

E001, E004, E005, E009, E010, E013, E014, E016, E019, E020, E021, E022, E026, E029, E031, E036, E037, E038, E039, W002, W003, W012, W014, W016

When editing segments/<key>.toml

E001, E005, E006, E011, E012, E015, E016, E019, E025, E032, E033, W004, W005, W007, W013, W015

Manifest-wide (cross-file)

E018 (symlinks), E034 (attribute type conflict), W008 (minor version mismatch), W009 (subdirectory), W011 (missing directories)


Error catalog (E0xx)

E001 — TOML parse failure, or schema_version malformed

Triggers when a file is empty, contains invalid TOML, or its schema_version field is absent, not a string, or does not match <u64>.<u64> shape. Examples that fail: "1", "1.0.0", "abc", "v1.0", "1.x", "x.0", "".

Only shape is checked — "99.0" passes lint but the server later rejects it with schema_version_mismatch.

Remedy. Add or correct schema_version = "0.1" at the top of the file; fix any TOML syntax error in the message.

E004 — Undeclared variant

Triggers when a [flag.environments.<env>].variant or a rule's variant references a key not declared in [flag.variants] (any env, including _).

Variants are per-flag — a variant declared in another flag does NOT satisfy this one.

Remedy. Add the variant to [flag.variants], or correct the typo.

E005 — Missing segment reference

Triggers when a flag rule's segment field, or a { segment = "..." } reference in a predicate (top-level or nested in and / or / not), names a segment with no corresponding segments/<key>.toml. A rule's segment = "" (empty string) also fails the lookup and raises E005.

Remedy. Create the missing segment file, fix the typo, or remove the reference.

E006 — Invalid bucket range

Triggers when a [segment.bucket] table has any of: start missing, end missing, entity_id_attribute missing or empty, start < 0, end > 9999, or start > end.

Boundary values that do NOT trigger: start = 0, end = 0; start = 0, end = 9999; start = 9999, end = 9999; start = end anywhere in range.

Remedy. Set start and end such that 0 ≤ start ≤ end ≤ 9999; set a non-empty entity_id_attribute. See buckets.

E009 — Rule missing required field

Triggers when a rule table omits variant, or declares neither segment nor predicate. Two malformed rules in the same array produce two E009 diagnostics.

A rule with segment = "" does NOT trigger E009 — the field is present but empty (lookup fails as E005). A rule with both segment and predicate triggers E036, not E009.

Remedy. Add the missing field. See flag § Rule fields.

E010 — Unknown environment (typed mode)

Triggers when typed-env mode is active ([namespace.environments] declared) AND a flag declares [flag.environments.<env>] for a slug not present in [namespace.environments]. Matching is case-sensitive.

The reserved name _ is always accepted and never reports E010. In untyped-env mode, E010 does not fire (any pattern-valid env slug is accepted).

Remedy. Add the env to namespace.toml or correct the slug in the flag.

E011 — Segment lacks predicate and bucket

Triggers when a [segment] table is present but neither [segment.predicate] nor [segment.bucket] is declared.

Remedy. Add a [segment.predicate] (for an audience) or [segment.bucket] (for a percentage rollout), or both.

E012 — Circular segment reference

Triggers when segment predicates form a cycle: self-loops (a → a), two-cycles (a → b → a), longer cycles, cycles through nested compounds, or multiple disjoint cycles. A cycle that goes through a missing segment produces E005 only — the broken edge is not a cycle.

Remedy. Break the cycle by extracting a shared sub-predicate into a leaf segment, or by replacing one of the segment references with an inline attribute predicate.

E013 — Deprecated inline-targeting field

Triggers when a rule contains any of the legacy fields condition, rollout, or percentage. These predate the segment-based targeting model.

Multiple deprecated fields in a single rule produce one E013 listing all offending field names.

Remedy. Rewrite the rule to reference a segment. Express predicates as [segment.predicate] and percentage rollouts as [segment.bucket].

E014flag.type invalid, or variant value type mismatch

Triggers when flag.type is missing, set to an unrecognized value, when a variant value's TOML type does not match the declared flag.type, or when a variant uses the reserved table-form ([flag.variants.<key>] with nested value / description).

Remedy. Set flag.type to one of the five permitted values; correct the variant value's TOML type. See flag § Per-type variant rules.

E015 — Predicate shape error

Triggers when a predicate atom's operator/operand shape is malformed, or a compound's value has the wrong TOML shape. The full trigger list is in predicates § Common diagnostics.

Remedy. Use the operator's documented operand shape. See predicates § Operator quick reference.

E016 — Unknown field on a structural table

Triggers when a closed-field table contains a field outside its recognized set. The closed-field tables in schema 0.1:

TableRecognized fields
[namespace]slug, display_name, description, telemetry_enabled, raw_entity_ids, private_attributes
[flag]type, description, owner, lifecycle, tags, private_attributes, variants, environments
[flag.environments.<env>]variant, rules, testing
[[flag.environments.<env>.rules]] entrysegment, predicate, variant, description (legacy condition / rollout / percentageE013)
[segment]description, predicate, bucket
[segment.bucket]entity_id_attribute, salt, start, end
Predicate atom tableattribute, op, value, values
Predicate compound tableexactly one of and, or, not
Predicate segment-reference tablesegment

Env values under [namespace.environments] are the only forward-compatible site and silently accept unknown fields.

Common occurrences:

  • Retired field names. value, enabled, key, top-level [[flag.rules]] from the pre-0.1 layered model.
  • default_variant typo. On [flag.environments.<env>], E016 carries a hint pointing at variant. (This is the one rename hint E016 carries; other unknown-field cases leave the remedy to the author.)
  • Rule typos. segments / varient, env-block keys lifted onto a rule (enabled, value), embedded predicate fragments (attribute, op, and, or, not) all produce E016.

Remedy. Remove the unknown field, or correct the typo.

E017 — Namespace slug mismatched with directory

Triggers when namespace.toml is present, [namespace] is present, namespace.slug is declared, AND the declared slug does not equal the directory name. A missing namespace.toml or missing slug field falls back to the directory-name slug; E017 fires only when a slug has been declared and it disagrees with the on-disk shape (almost always a stale rename).

Remedy. Set namespace.slug to the directory name, rename the directory, or remove the slug field.

Triggers when any path inside the flag-namespace directory tree is a symbolic link rather than a regular file or directory. Symlinks compromise reproducibility because the same tree may resolve differently on different machines.

Remedy. Replace the symlink with a regular file (or, if the target is in the same flag namespace, copy the contents).

E019 — File size exceeds 256 KB

Triggers when a .toml file inside the manifest is larger than 256 KB. The cap exists to prevent runaway JSON variant payloads.

Remedy. Slim down the file. If a single flag genuinely needs more than 256 KB of variant payload, split the configuration into multiple flags or store the bulk data outside exd.

E020 — Empty or missing [flag.variants]

Triggers when the flag file has no [flag.variants] table, or the table is present but empty.

Remedy. Declare at least one variant. A flag with zero variants cannot evaluate.

E021 — Invalid variant key

Triggers when a key inside [flag.variants] does not match [a-z][a-z0-9_-]* (max 63 chars).

Remedy. Rename the variant to satisfy the pattern.

E022 — Invalid flag.lifecycle value

Triggers when flag.lifecycle is set to a string other than "development", "active", or "retired".

Remedy. Use one of the three permitted values, or omit the field to default to "active".

E023[namespace.environments] declared but empty

Triggers when namespace.toml is present AND declares [namespace.environments] with zero entries. A missing [namespace.environments] (or missing namespace.toml) is not E023 — it selects untyped-env mode.

Remedy. Either declare at least one environment, or delete the empty table to opt into untyped-env mode.

E024 — Invalid environment slug

Triggers when a key in [namespace.environments] does not match [a-z][a-z0-9-]* (max 63 chars; underscores forbidden).

Remedy. Rename the environment.

E025[segment] table missing

Triggers when a segment file parses cleanly but does not declare a [segment] table.

Remedy. Add [segment] with at least a description plus either predicate or bucket.

E026 — Non-string segment or variant on a rule

Triggers when a rule's segment or variant field is present but is not a TOML string (e.g., segment = 42, variant = true, an array, or a table).

Remedy. Quote the value as a TOML string.

E029 — Non-finite float in variant value

Triggers when a variant value contains nan, inf, or -inf. The check covers:

  • A flag.type = "float" variant value that is non-finite at the top level.
  • A flag.type = "json" variant whose inline table or array contains a non-finite float at any nesting depth.

TOML 1.0 syntactically permits these literals, but JSON has no native representation for non-finite numbers; permitting them in variant values would force every SDK to invent a wire encoding.

Remedy. Use only finite float values. If a sentinel for "no limit" is needed, declare a documented large finite number (e.g., f64::MAX) or use an integer/string flag.

E030 — Invalid namespace slug

Triggers when namespace.slug is present but does not match [a-z][a-z0-9-]* (max 63 chars, no underscores, lowercase first character).

E017 (missing [namespace] / missing slug / slug-vs-directory mismatch) takes priority when the slug field is absent. E030 fires when a slug is present but malformed; if it also fails to match the directory, both E017 and E030 fire.

Remedy. Rename the slug to satisfy the pattern; rename the manifest directory to match.

E031 — Invalid flag filename

Triggers when a .toml file in flags/ has a stem that does not match [a-z][a-z0-9_-]* (max 63 chars). The stem is the flag key (no flag.key field).

The file is dropped before per-file lint, so an invalid filename produces exactly one E031 and no follow-on diagnostics from its TOML contents.

Remedy. Rename the file.

E032 — Invalid segment filename

Mirror of E031 for segment files in segments/.

Remedy. Rename the file.

E033 — Empty values = [] on in / not_in

Triggers when a predicate atom whose operator is in or not_in declares values = []. An empty in would always be false; an empty not_in would always be true. Both forms are degenerate.

Remedy. Either populate values with at least one scalar operand, or replace the atom with a tautology ({ attribute = "<always-set>", op = "is_set" }).

E034 — Attribute type conflict across predicates

Triggers when the same attribute name is used in two or more predicate atoms (across segments, inline flag rules, and entity_id_attribute on bucket segments) under operators / operand shapes that imply incompatible types. The linter walks every well-formed atom and infers a type per attribute:

OperatorInferred type for the attribute
eq, neqtype of the scalar operand (bool / integer / float / string)
lt, lte, gt, gtenumber (matches integer and float)
in, not_inelement type of values
contains, not_contains, starts_with, ends_withstring
semver_*semver (string-shaped, narrower than string)
is_set, is_not_set(no constraint)
[segment.bucket].entity_id_attributestring

The first site to fire fixes the inferred type; later disagreements emit E034 against the second site, with a message naming the first.

Compatible pairings do NOT fire: stringsemver, integerfloat under numeric operators.

Remedy. Pick a single canonical type for the attribute. Rename one site or rewrite an operand. Run exd schema to see the inferred type per attribute and the source sites.

E036 — Rule declares both segment and predicate

Triggers when a flag rule declares both segment = "..." and predicate = { ... }. The two are mutually exclusive — a rule MUST identify its audience exactly one way.

Remedy. Pick one. Use segment = "..." when the audience is named and reusable; use inline predicate = { ... } for one-off targeting.

E037[flag.environments._] block missing

Triggers when a flag file declares neither [flag.environments._] nor any block under [flag.environments]. The _ catch-all is mandatory — without it the flag has no fallback for environments that don't declare their own block.

A flag that declares named env blocks but no _ block also raises E037. Named envs cannot serve as the fallback.

Remedy. Add [flag.environments._] with at least a variant field. See resolution § The catch-all _.

E038[flag.environments._] missing variant

Triggers when a flag declares [flag.environments._] but the block does not declare variant. The catch-all MUST declare a fallback variant so resolution always terminates.

E004 (undeclared variant) takes priority when variant IS declared but references a key not in [flag.variants].

Remedy. Add variant = "<variant-key>" to the _ block.

E039testing = true misused

Triggers when either:

  • A [flag.environments.<env>] block declares testing = true but does not declare rules (or declares rules = []). The gate has nothing to gate.
  • A [flag.environments._] block declares testing = true. The catch-all is the resolver-of-last-resort for non-opted-in callers; gating it has no audience.

E001 takes priority when testing is non-bool. E039 does NOT fire when testing = false is paired with no rules (a no-op, harmless).

Remedy. Either remove testing = true, or add a rules array. For the _-block case, move the testing-gated rules into a named env block. See resolution § The rollout workflow.


Warning catalog (W0xx)

W002 — Retired flag still has rules

Triggers when flag.lifecycle = "retired" and any environment has rules. One W002 fires per flag regardless of how many environments still carry rules.

Remedy. Either move the flag back to lifecycle = "active" or remove the rules and proceed with retirement (audit usage in code, then delete the file).

W003 — Flag has no rules anywhere

Triggers when a flag declares no rules in [flag.environments._] AND no rules in any named env block. This may be intentional (kill-switch pattern). The warning surfaces it for visibility.

Remedy. Either accept the warning (kill-switch pattern) or add at least one rule.

W004 — Bucket segment without explicit salt

Triggers when a [segment.bucket] table omits salt or sets salt = "". The bucket still works (falls back to the segment key) but renaming the segment will rebucket every entity.

Remedy. Set an explicit salt, typically a stable rollout or experiment name.

W005 — Predicate nesting exceeds 5 levels

Triggers when a [segment.predicate] is nested more than 5 levels deep, counting the chain of and / or / not keys. Atom tables and segment-reference tables do not count.

Remedy. Extract inner predicates into named segments and reference them via { segment = "..." }.

W007 — Empty compound predicate array

Triggers when a predicate contains and = [] or or = []. Vacuous truth is well-defined (true for and, false for or) but the empty list is more often a mistake.

Remedy. Populate the array, or replace the compound with an explicit tautology / negation.

W008 — Minor schema_version mismatch across files

Triggers when two or more files inside the same flag namespace declare different minor versions within the same major (e.g., "1.0" and "1.2"). The upload is still accepted.

Remedy. Run exd manifest migrate --to <minor> to advance every file to the same minor.

W009 — Subdirectory under flags/ or segments/

Triggers when a subdirectory exists under flags/ or segments/. The linter does NOT descend; the warning surfaces the unexpected layout.

Remedy. Move the subdirectory's contents into the parent directory or out of the manifest tree entirely.

W010namespace.display_name empty when declared

Triggers when namespace.display_name is present and equal to "". The field is optional; omitting it defaults to the slug. Declaring it as empty is almost always an oversight.

Remedy. Set display_name to a human-readable string, or remove the field.

W011flags/ or segments/ directory missing

Triggers when the flag-namespace root does not contain a flags/ or segments/ directory. The linter accepts their absence during initialization; the warning surfaces the missing directory.

Remedy. Create the missing directory (it MAY be empty).

W012 — Duplicate segment in rules within the same environment

Triggers when two or more rules inside the same [flag.environments.<env>] block declare the same segment. Because the loop returns on the first match, every rule after the first against that segment is unreachable.

One W012 fires per shadowed rule. A rule in _ and a rule in a named env block are NOT in conflict — env rules completely replace _'s rules for that env.

Remedy. Remove the shadowed rule, or change one rule's segment.

W013 — Segment defined but never referenced

Triggers when a segments/<key>.toml file declares a segment whose key is not referenced by any flag rule or any other segment's predicate. The segment is dead data.

A self-reference does NOT count as "used" (and typically also fires E012).

Remedy. Reference the segment from at least one flag rule (or from another segment's predicate), or delete the file. Use --allow W013 during transitional periods.

W014 — Variant defined but never used

Triggers when a [flag.variants] entry declares a variant whose key is not referenced by any [flag.environments.<env>].variant (including _) and not by any rule's variant field across the entire flag. The variant is unreachable.

Remedy. Either add a rule that returns the variant, or remove the variant from [flag.variants].

W015 — Substring operator with empty-string operand

Triggers when a predicate atom uses contains, not_contains, starts_with, or ends_with with value = "". Mathematically degenerate (every string contains, starts with, and ends with the empty string).

Remedy. Replace value = "" with the substring/prefix/suffix the author actually meant. For an intentional always-true atom, use op = "is_set".

W016 — Empty per-env block

Triggers when a named [flag.environments.<env>] block (<env>_) declares neither variant nor rules. Every step of resolution falls through to _ for this env — the block is structurally redundant.

W016 does NOT fire on [flag.environments._] — an empty _ block already triggers E038, the more severe diagnostic.

Remedy. Add at least one of variant or rules, or delete the block.


Info catalog (I0xx)

I001 — Flag has no owner

flag.owner is absent or empty. Set it to a team handle.

I002 — Flag has no description

flag.description is absent or empty. Write a short prose description.

I003 — Segment has no description

segment.description is absent or empty.


Structural notes (no separate code)

  • Empty manifest directory (no namespace.toml, no flags/, no segments/): lint succeeds. Slug = directory name; untyped-env mode; empty flag and segment maps.
  • Missing flags/: lint succeeds, no flag diagnostics, segments still validated.
  • Missing segments/: lint succeeds, no segment diagnostics; flag rules referencing any segment all emit E005.
  • Missing namespace.toml: lint succeeds; untyped-env mode with all defaults applied.
  • Non-.toml files in flags/ and segments/: silently ignored.
  • Subdirectories under flags/ / segments/: silently skipped (the linter only reads regular files).
  • Invalid filenames are dropped before per-file lint: a .toml whose stem fails the key pattern produces exactly one E031 / E032; the file's contents are not parsed.
  • All diagnostics are collected: emitting E020 does NOT suppress E004 / E009 / E010 in the same file.
  • Deterministic traversal: flags/ and segments/ are walked in lexicographic order so cross-file diagnostics fire deterministically.
  • Diagnostic file paths are relative to the manifest root.

CLI flags that affect output

FlagEffect
--format jsonEmit JSON instead of human-readable form.
--deny-warningsExit non-zero if any warning is present (treat warnings as errors).
--quietSuppress per-diagnostic lines; print only the summary.
--allow <CODE>Suppress a specific code from the exit status. Repeatable. Useful during incremental migrations.
--schema-major <N>Reject any file whose schema_version major is not <N>.

These affect the CLI experience, not the wire format of the report. The diagnostic codes themselves are stable regardless of flags.


Output formats

Human-readable (default)

flags/express-checkout.toml:22 error E004 Variant 'maybe' is not declared in flag.variants
segments/checkout-redesign-rollout-10.toml:34 error E006 Bucket range must satisfy 0 <= start <= end <= 9999

2 errors, 0 warnings, 0 infos

JSON (--format json)

{
"namespace": "payments",
"manifest_version": null,
"errors": [
{
"code": "E006",
"severity": "error",
"file": "segments/checkout-redesign-rollout-10.toml",
"line": 34,
"message": "Bucket range must satisfy 0 <= start <= end <= 9999"
},
{
"code": "E004",
"severity": "error",
"file": "flags/express-checkout.toml",
"line": 22,
"message": "Variant 'maybe' is not declared in flag.variants"
}
],
"warnings": [],
"infos": [],
"passed": false
}

CI integrations should consume the JSON form. Severity is rendered as the lowercase strings "error", "warning", "info". Match on code only — message text is not stable across versions.

Report structure

A LintReport carries:

FieldTypeDescription
namespacestringThe flag-namespace slug. Falls back to the directory name when namespace.toml does not declare a slug.
manifest_versioninteger / nullServer-side only; null for a local lint.
errors, warnings, infosarrays of diagnosticsOne bucket per severity.
passedbooltrue iff errors is empty. Warnings and infos do not affect this.

Each diagnostic carries:

FieldTypeDescription
codestringThe stable code, e.g. "E001".
severitystring"error" / "warning" / "info".
filestringPath relative to the manifest root (e.g., flags/checkout-redesign.toml).
lineinteger1-based line number. Today the linter often reports 1; future versions will track exact lines.
messagestringHuman-readable description. Not stable across versions — match on code.

See also