assent lint hard-error list (ADR-0010 amendment)¶
This is the consolidated, authoritative list ADR-0010's amendment promised ("The lint
hard-error list … is consolidated in the Phase 3 spec for assent lint") and
openspec/specs/p3-e1-schemas-fixture/spec.md §P3-E1-S06 requires. Each row names the
triggering condition and the ADR/decision that mandates it, so a future assent lint
implementation (Phase 5) has one place to enumerate against — and so later epics that add
their own hard error (e.g. P3-E4's no-implicit-enforce-phase, single-writer-profile) extend this table instead of
inventing a second list.
A hard error fails assent lint (and therefore CI) unconditionally — it is never a warning,
and it is never contingent on what a predicate evaluates to at decision time. Several of
these exist specifically because ADR-0017's adversarial reviews found that letting the
decision layer catch a problem left a window where the policy itself was already unsafe
in a way lint could have caught before any MR triggered evaluation.
| Hard error | Triggering condition | Mandated by |
|---|---|---|
| Obligation coverage | A RulesetBinding's require: list names an obligation that no rule in the bound packs proves (prove.obligation) — a required safety property with zero rules proving it. |
ADR-0017 §2 (required obligations replace anonymous vouch coverage); schemas/policy/v1alpha1/merge-policy.schema.json's prove.obligation description |
| Reserved-class violation | An MR that touches .assent/** carries a pack rule routing the built-in assent-policy meta-class to anything other than block/challenge — i.e. to vouch or to a prove/onFailure obligation-satisfying outcome — independent of what the rule's predicate evaluates to. |
ADR-0015 §1 (policy MRs are block-by-default and may only relax to challenge, never vouch) |
| Fail-open restriction | A config.yaml providers entry backing a controlling or authorization fact (e.g. an entries-identity, ownership, or approval-eligibility provider) declares failure: open. |
ADR-0004 amendment; ADR-0017 §6 ("Controlling facts never fail open") |
| Tests-per-rule | A rule has zero cases exercising it under assent test --coverage (directory-form .assent/tests/** or inline cases.yaml). |
ADR-0010 ("assent lint fails packs without tests"); schemas/testfixture/v1alpha1/test-expectation.schema.json |
| Unkeyed lists | A class's entries: {mode: list} declaration has no identity.pointer — an unkeyed list collection, rejected at lint rather than guessed. |
ADR-0017 §5; REQ-P3-E1-S01-03 (merge-policy.schema.json's entriesSpec.allOf already enforces this at the schema level — lint surfaces the same rule with an actionable message before evaluation) |
| Undeclared predicate-scope fields | An assert/when/cel leaf references a top-level identifier outside the closed set frozen in docs/planning/predicate-scope.md (old, new, path, kind, file, entry, oldEntry, changes, facts, mr, env). |
ADR-0016 §2 (unknown fields are load-time errors, never <no value>); docs/planning/predicate-scope.md |
no-implicit-enforce-phase |
A rule or pack manifest (pack.yaml) omits an explicit phase field (off/observe/enforce) — rollout phase has no default, so an undecorated rule/pack is rejected rather than silently defaulting to one phase or the other. Named specifically so an author who edits effect/onFailure to approximate a rollout instead of using phase gets pointed at the sanctioned mechanism. |
D-017 (B2); P3-E4-S01; schemas/policy/v1alpha1/merge-policy.schema.json (rule.phase required); schemas/policy/v1alpha1/pack.schema.json (spec.phase required ceiling); docs/planning/policy-lifecycle-phase.md |
single-writer-profile |
For any given (environment, class) binding, zero or more than one active PolicyProfile resolves writes: true (overlapping unconditional writers, or a binding with no writer when Config.profiles is present). Never last-one-wins — lint fails closed. |
D-017 (B3); P3-E4-S02; schemas/policy/v1alpha1/profile.schema.json (spec.writes required); schemas/policy/v1alpha1/config.schema.json (profiles[] precedence table); docs/planning/policy-lifecycle-profiles.md; docs/architecture/policy-profiles.md |
Notes¶
- This table is additive: a later epic that introduces a new safety-bearing construct adds its
hard error here (with a citation) rather than starting a second list — P3-E4's
no-implicit-enforce-phaseandsingle-writer-profilerows above are examples of that pattern (schema-level fields; this row is the lint cross-reference). - Hard errors are distinct from
assent lintadvisories (e.g. missingdocs.urlon achallenge/blockrule) — advisories are out of scope for this table; only errors that fail the run belong here. - Several rows are already enforced at the JSON Schema level (unkeyed lists, reserved-class routing's enum shape) — lint's job is to catch the same violation earlier, with a message that names the offending rule/binding, not to duplicate schema validation for its own sake.