-
Notifications
You must be signed in to change notification settings - Fork 1
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 review
Both findings from the previous pass are resolved at head: the not matches(...) claim is gone, the enumeration no longer says "every operator and function", the override rule names the fields it matches on (name and type), and the override prose now sits before <Expandable> like every other ParamField on the site. What follows is new.
Answering the two questions in the PR description directly, because the site already contains a partial answer: schemas/policy/v1.json:72 is generated from the API's Pydantic models, and its PolicyExpression description is the missing-value rule in the server's own words —
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 covers question 1 better than the changelog does, and it does not support the ==/!= part of line 164. Question 2 (override participation) has no counterpart anywhere in the schema, the CLI reference, or navigation — see finding 3.
policy-reference/environment_policy.mdx
1. Improvement — line 164: ==/!= are broader than the schema, and != inverts an exception. Posted inline. Under ordinary inequality semantics ${{ flow.tags.risk-level != "high" }} is true for a flow with no risk-level tag, so as an exception it waives the requirement for every untagged flow — the opposite of what this line promises. The inline comment also notes that the schema's in rule is stricter than "either operand is missing", and that exists() fits awkwardly in the list.
2. Suggestion — lines 164–166: not is still the unanswered case, and it's the one authors write. The section enumerates comparison, membership and function forms and says nothing about not, while the Operators table at line 115 documents not flow.tags.risk-level == "high" and changelog/index.mdx names not matches(flow.name, "^snyk-.*") as the expression that used to crash. A reader applies line 164 and concludes not matches(...) → true for a missing value. If that's correct, one sentence saying so turns line 166's exists() advice from an unexplained recommendation into a consequence; if it isn't, the section is silent on its most important case. Either way the fix is one sentence — but it needs the same server confirmation as finding 1, so it belongs in the same round trip.
3. Suggestion — line 63: "override attestations" now has its only definition on the site inside a policy field reference. grep -rn -i override across .md/.mdx finds no page that documents them: only changelog/index.mdx:44 and :157, nothing in client_reference/, nothing in config/navigation.json, and no mention in understand_kosli/glossary.md (whose one "override" hit is about trail templates). The defining clause added here is a reasonable stopgap, but a policy author who wants to create one has nowhere to go — a glossary entry, or a how-to under getting_started/, would give the concept a home and this line a link target. Worth a follow-up issue rather than growth in this PR.
Related and out of scope: changelog/index.mdx:44 refers to a CLI attest override --commit, and no client_reference/kosli_attest_override.md exists. If the command ships in kosli-dev/cli and is simply hidden from kosli docs, that's an upstream gap in the CLI reference, not something this PR can fix.
Links
/policy-reference/environment_policy#policy-expressions and #missing-values both resolve — the second to the ### Missing values heading added in this PR. I could not run gh pr checks, so I have not seen what link-rot reported; the two links added here are verified by hand.
1 Improvement, 2 Suggestions, 0 Critical. Merge verdict: mergeable — but finding 1 changes what the page asserts, so it's worth settling the != behavior with kosli-dev/server before this becomes the canonical statement.
· branch docs/policy-expression-missing-values
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Uh oh!
There was an error while loading. Please reload this page.
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.