Skip to content

Invalid Combination Linter

A running tool — instantiates Grammar-Guided Structure Recovery

A running tool that scans a recovered structure for forbidden co-occurrences — elements each legal alone but illegal together — and dispatches a handled violation when it finds one.

The Invalid Combination Linter is the runtime that catches the errors no single node-local check can see: forbidden co-occurrences — two or more elements that are each perfectly well-formed on their own but must not appear together. Where a single validation rule inspects one node, this tool inspects relationships — mutual exclusions, required-together pairs, ordering and cardinality constraints across the structure — and it owns what happens when one fires. Its defining move is twofold: it operates on the class of errors that live only in the interaction of parts, and, as the running engine, it turns a detected conflict into a deterministic consequence rather than a passive note.

Example

A CI/CD pipeline config is written in YAML and has already parsed into a clean tree. A job specifies both image: and machine:. Each key is individually valid — plenty of jobs use one or the other — but together they are contradictory: you cannot run on a prebuilt image and a raw machine at once. A per-key rule sees nothing wrong; the Invalid Combination Linter, scanning the parsed structure, detects the forbidden pair within one job and dispatches a structured violation — which two keys, which job, why they conflict — and fails the run. That last part is the point: this is an error, not a style nit, so the linter's handling policy raises rather than warns.

The same tool enforces the mirror constraint elsewhere: cache: requires steps:, so a job with caching but no steps is flagged as an incomplete combination. In both cases the conflict exists only in the co-occurrence, and the linter is the thing that both finds it and decides the consequence.

How it works

The linter runs over the recovered structure — not the raw text — and evaluates cross-element constraints a node-local rule cannot express: mutual exclusion, required-together, ordering, and cardinality across nodes. On a hit it constructs a violation carrying the offending elements, their location, and a rationale, then dispatches it through a handling policy: raise as an exception and fail, block the operation, downgrade to a warning, or apply a mechanical fix. What distinguishes it from a single validation rule is both halves of that description — it finds combinations, and it owns the consequence of finding one.

Tuning parameters

  • Combination arity — pairwise conflicts vs. n-way constraints spanning several co-occurring elements. Wider constraints catch subtler conflicts at higher cost.
  • Handling policy — raise/fail vs. warn vs. auto-resolve — the exception behavior on a violation, and the tool's most consequential dial.
  • Locality window — evaluate within a node, a subtree, or the whole document. Wider windows catch cross-cutting conflicts but cost more to check.
  • Precedence — which constraint wins when several fire on overlapping elements.
  • Fail-fast vs. collect-all — stop at the first violation, or gather every conflict before reporting.

When it helps, and when it misleads

Its strength is that it catches the invalid-combination class that node-local validation structurally cannot see, and — by owning the handling policy — converts a detected conflict into a deterministic outcome: a failed build, a blocked publish, a rejected config. Its failure modes cluster where combinations are subtle. False positives bite hardest here, because a flagged "conflict" is sometimes a valid advanced usage the rule author didn't anticipate; an over-eager fail-fast policy can mask later conflicts behind the first; and exhaustive n-way checking gets expensive. The classic misuse is encoding a team's current convention as a hard combination-error, so any novel-but-legitimate structure is rejected as malformed. The discipline that guards against it is to reserve raised exceptions for true contradictions, downgrade merely-stylistic co-occurrence preferences to warnings, and test every combination rule against real, valid inputs before it can fail a run.[1]

How it implements the components

The Invalid Combination Linter realizes the consequence-handling component of the archetype — deciding and dispatching what happens when a recovered structure is malformed by combination:

  • exception_handling — on a detected forbidden co-occurrence it constructs a structured violation and dispatches it through a handling policy (raise, block, warn, or fix), owning the deterministic response to a malformed structure.

It does not author the atomic checks it runs (those are individual Linting or Validation Rules), does not validate a structure against a versioned semantic schema (that is Semantic Schema Validation), and does not repair malformed input so parsing can continue (that is Error-Tolerant Parsing).

Notes

The exception handling here is the consequence of a validation failure on an already-recovered structure — raise, block, or warn — and is a different thing from parse-error recovery, which keeps a parser running through malformed input. Error-Tolerant Parsing owns the second; this linter owns the first. Conflating them leads to the trap of trying to "recover" from a genuine contradiction that should simply have failed the run.

References

[1] A mutual-exclusivity constraint ("at most one of these may be present") and its partner the co-requirement ("if one, then the other") are the canonical forbidden-combination shapes. They cannot be expressed as a property of any single node — only of a set of nodes — which is exactly why they need a combination linter rather than an atomic validation rule.