docs: document missing-value semantics and override handling in environment policies - #386
docs: document missing-value semantics and override handling in environment policies#386dangrondahl wants to merge 2 commits into
Conversation
…onment policies Two behaviors were described only in the changelog, so a policy author looking them up on the reference page found nothing: - Comparison operators and matches() evaluate to false when either operand is missing, and missing values are the common case (no provenance, absent tag). Adds a "Missing values" section under Functions, including the negation pitfall and the exists() workaround. - Attestation rules evaluate the newest attestation including overrides. Raised in review on #376.
|
Preview deployment for your docs. Learn more about Mintlify Previews.
|
|
Claude finished @dangrondahl's task in 2m 50s —— View job Doc reviewBoth findings from the previous pass are resolved at head: the Answering the two questions in the PR description directly, because the site already contains a partial answer:
That covers question 1 better than the changelog does, and it does not support the
|
| Every operator and function evaluates to `false` when either operand is missing. This holds for `==`, `!=`, `in`, `exists()`, `matches()`, and the ordering comparisons `<`, `>`, `<=`, and `>=`. | ||
|
|
||
| Missing values are common rather than exceptional: | ||
|
|
||
| - `flow` is empty for any artifact reported without provenance. | ||
| - `flow.tags.<key>` is empty for a tag the flow does not have. | ||
|
|
||
| A missing value never matches, so negating a comparison does not make it match either: for an artifact with no provenance, both `${{ matches(flow.name, "^snyk-.*") }}` and `${{ not matches(flow.name, "^snyk-.*") }}` evaluate to `false`. To branch on presence, test it explicitly with `exists()`: |
There was a problem hiding this comment.
Improvement — the not matches(...) claim doesn't follow from the rule stated two paragraphs above, and needs a server check before it becomes canonical.
Line 159 says matches() evaluates to false when an operand is missing. Applying only that rule, a reader computes matches(flow.name, "^snyk-.*") → false, therefore not matches(flow.name, "^snyk-.*") → true — not is being applied to a present boolean, not to a missing operand. For both to be false as line 166 asserts, missingness has to propagate through not, which is a second, stronger rule the page never states.
The source entry in #376 (changelog/index.mdx, still unmerged) doesn't assert it either:
matches()and the ordering comparisons (<,>,<=,>=) now evaluate tofalsewhen either side is missing… Previously an expression likenot matches(flow.name, "^snyk-.*")crashed snapshot compliance evaluation
That names the negation as the expression that used to crash, not as one that now returns false. So one of two things is true, and they need different text:
- Missingness propagates through
not→ say so explicitly ("a missing operand makes the comparison unsatisfiable;notdoes not turn it intotrue"), because it is the whole point of the section and no reader will infer it. - It does not propagate → line 166 is wrong and the
exists()workaround at line 169 is unnecessary, which sends policy authors to write a guard they don't need.
Also on line 159: "Every operator and function" is broader than both the enumeration that follows it and the source. The Operators table at lines 113–116 includes and, or, and not, which the enumeration omits — those combine boolean results and aren't covered. Scope the sentence to the comparison, membership, and function forms.
There was a problem hiding this comment.
You are right, and this was my error rather than an ambiguity in the source. Fixed in ba9464b.
The claim was an extrapolation. The changelog entry says matches() is false when an operand is missing, and separately that not matches(flow.name, "^snyk-.*") previously crashed evaluation. I read the second as describing current behavior and wrote that the negation now returns false — which, as you show, requires missingness to propagate through not, a rule nothing states.
I removed the claim rather than replacing it with the opposite, because both readings are unverified and I cannot reach the server either. The exists() guard example went with it, since it only made sense under the propagation reading. It is now the one open question in the PR description, so whoever verifies knows it is the thing to settle; if propagation is real it needs its own explicit sentence, since no reader will infer it.
Worth noting I nearly repeated the mistake in the fix: my first pass replaced it with "the logical operators and, or, and not combine the boolean results of these comparisons, so they are unaffected" — which asserts non-propagation just as baselessly. Removed before pushing.
On the scoping half: also fixed. The sentence now names the comparison operators, in, exists(), and matches() explicitly and says nothing about and/or/not. Your finding 5 is in too — the section had "missing", "empty", and the table's "not null" for one state; it now uses "missing" throughout. I did not add anything about an explicitly empty tag value, since that is also unverified; it is listed in the PR description instead.
| </ParamField> | ||
| </Expandable> | ||
|
|
||
| A rule is satisfied by the newest matching attestation, including overrides. Overriding a non-compliant attestation can therefore bring an environment back to compliant, and overriding a compliant one down to non-compliant makes the environment non-compliant. |
There was a problem hiding this comment.
Improvement — "newest matching attestation" is ambiguous in a way that inverts the rule, and "overrides" is undefined anywhere on the site.
-
"matching" can be read two ways: matching the rule's
name/type, or satisfying the rule. Under the second reading a reader concludes an older passing attestation still satisfies the rule after a newer one fails — the opposite of this sentence's own second half. chore: update changelog CLI v2.39.0/v2.39.1 and Platform updates #376's entry says "evaluate the newest attestation including overrides", with no "matching", which is unambiguous. Name the fields the match is against. -
grep -rn -i overrideover.md/.mdxfinds no page that defines an override attestation — the only explanations on the site arechangelog/index.mdx:44and:157, andclient_reference/has nooverridecommand page (so it looks API/UI-only). A policy author who hits "including overrides" here has nowhere to go. Add a defining clause, and ideally a link to wherever an override is created.
| A rule is satisfied by the newest matching attestation, including overrides. Overriding a non-compliant attestation can therefore bring an environment back to compliant, and overriding a compliant one down to non-compliant makes the environment non-compliant. | |
| A rule is evaluated against the newest attestation matching its `name` and `type`, including override attestations — an override supersedes the status of the attestation it overrides. Overriding a non-compliant attestation can therefore bring an environment back to compliant, and overriding a compliant one down to non-compliant makes the environment non-compliant. |
Separately: this is the only place in the site where prose follows </Expandable> inside a ParamField — every other one puts its prose before the <Expandable>. Worth confirming on the deploy preview that it renders where you intend rather than below the collapsed toggle.
There was a problem hiding this comment.
Both taken, in ba9464b — I used your suggestion close to verbatim.
-
The match is now pinned: "evaluated against the newest attestation whose
nameandtypematch the rule". Your reading of the ambiguity is the one that matters — "matching" as "satisfying" inverts the rule, and against a reference page that is worse than saying nothing. -
Override now carries a defining clause ("an override supersedes the status of the attestation it overrides"), since there is nowhere to link. I confirmed your grep: no page defines the term, and there is no
client_referencepage for the command either.
That last point turned up something separate. The CLI clearly has an attest override command — shipped changelog entries reference attest override --commit — but there is no client_reference/kosli_attest_override.md and no config/navigation.json entry. Since client_reference/ is generated from the CLI, that looks like a generator gap rather than a page nobody wrote. Noted in the PR description; not chasing it here.
- Placement fixed. Rather than confirm on the preview that prose after
</Expandable>renders where I intended, I moved it above the<Expandable>to match every otherParamFieldon the site — the convention is the safer answer regardless of how it renders.
Review found the negation claim was an extrapolation, not something the source supports: from 'matches() is false when an operand is missing', a reader computes 'not matches(...)' as true, so the page needed a stronger propagation rule it never stated. Removed rather than replaced -- neither direction is verified, and the exists() workaround that depended on it went with it. - Scope the rule to the comparison, membership, and function forms; drop the 'every operator and function' over-claim. - Unify on 'missing' instead of mixing missing/empty/not null. - Pin the attestation match to name and type, and define what an override does, since no page on the site defines the term. - Move that prose above <Expandable>, matching every other ParamField. - Link the new anchor from the Exceptions section of getting_started/policies.md.
| - `flow` is missing for any artifact reported without provenance. | ||
| - `flow.tags.<key>` is missing for a tag the flow does not have. | ||
|
|
||
| The comparison operators (`==`, `!=`, `<`, `>`, `<=`, `>=`), the membership operator `in`, and the functions `exists()` and `matches()` all evaluate to `false` when either operand is missing. A missing operand is not an error — the comparison is simply not satisfied. |
There was a problem hiding this comment.
Improvement — == and != are in this list, but the generated schema doesn't put them there, and != is the one that inverts a policy.
schemas/policy/v1.json:72 is generated from the API's Pydantic models (per CLAUDE.md's Schemas section, kosli-dev/server is the source of truth) and states the rule as:
A missing (None) context value never matches:
matches()and the ordering comparisons (<,>,<=,>=) return false if either operand is missing, andinis a list-membership test that returns false unless its right-hand side is a list containing the left-hand value.
That enumeration is matches() + the four ordering comparisons, with in described separately. This line adds == and !=. == is harmless either way, but != is not: under ordinary inequality semantics flow.tags.risk-level != "high" on a flow with no risk-level tag evaluates to true, not false. Written as an exception —
trail-compliance:
required: true
exceptions:
- if: ${{ flow.tags.risk-level != "high" }}— that waives trail compliance for every untagged flow, which is the opposite of what this page tells the author to expect. Since the section is meant to become the canonical statement, it shouldn't be broader than the source it's transcribed from.
Recommendation: mirror the schema's enumeration (matches(), the ordering comparisons, and in), and add ==/!= back only once someone confirms them against kosli-dev/server. If != really does return false for a missing operand, that's the single most surprising fact in this section and deserves its own sentence rather than a slot in a list.
Two smaller things on the same line:
in— the schema's rule is stricter than "either operand is missing":inreturnsfalseunless the right-hand side is a list containing the value. Worth stating, sinceflow.name in flow.tags.teamsfails for a reason this page doesn't cover.exists()— returningfalsefor a missing operand isexists()'s contract, not an instance of the missing-value rule, and grouping it under "the comparison is simply not satisfied" reads oddly two lines above "test it withexists()". Pulling it out of the list would make line 166 land harder.
Two environment-policy behaviors were documented only in the changelog, so a policy author who went to the reference page to look them up found nothing. Split out of review feedback on #376, which announced both.
Changes
policy-reference/environment_policy.mdx:New
### Missing valuessection under Functions. The Operators and Functions tables said nothing about what happens when an operand is absent. The section defines what "missing" means, notes that it is the common case (flowabsent without provenance,flow.tags.<key>absent for a tag the flow lacks), and states the rule for the comparison operators,in,exists(), andmatches().Override handling on attestation rules. A rule is evaluated against the newest attestation whose
nameandtypematch it, including override attestations, with a clause defining what an override does — no page on the site defines the term today.getting_started/policies.md: the Exceptions section now links the new#missing-valuesanchor, since that is where a reader meets expressions first.One open question for the author
Does a missing operand propagate through
not? The first version of this PR claimed that bothmatches(flow.name, "^snyk-.*")andnot matches(flow.name, "^snyk-.*")evaluate tofalsefor an artifact with no provenance. That does not follow from the rule: ifmatches()isfalse, ordinary boolean semantics makenot matches()true. Asserting otherwise requires missingness to propagate throughnot, which the source changelog entry never claims — it says only that the negated form previously crashed evaluation.Both readings are plausible and they need different text, so this revision states neither and drops the
exists()guard example that depended on it. If missingness does propagate, that deserves its own explicit sentence, because no reader will infer it. If it does not, the page is correct as it stands.Verification status
Both behaviors are transcribed from changelog prose in #376, not verified against the server, and this page becomes the canonical statement once merged. Still worth confirming:
in,exists(), andmatches(), and no other form.nameandtypematch, including overrides" is the precise rule.risk-level: "") behaves the same as an absent one. The page does not currently say.Also found, not fixed here
The CLI has an
attest overridecommand (referenced in shipped changelog entries), butclient_reference/has no page for it andconfig/navigation.jsonhas no entry. Sinceclient_reference/is generated, that looks like a generator gap rather than a missing hand-written page. Out of scope for this PR.Checks
mint broken-links— clean apart from the pre-existing/getting_started/service-accountsbreak intutorials/working_with_controls.mdx, which is onmainand untouched here.config/navigation.jsonorconfig/redirects.jsonchange.