Skip to content

Navigation Menu

Sign in
Sign up

docs: document missing-value semantics and override handling in environment policies - #386

Open
dangrondahl wants to merge 2 commits into
main from
docs/policy-expression-missing-values
Open

docs: document missing-value semantics and override handling in environment policies #386
dangrondahl wants to merge 2 commits into
main from
docs/policy-expression-missing-values

Conversation

@dangrondahl

@dangrondahl dangrondahl commented Sep 7, 2026
edited
Loading

Copy link
Copy Markdown
Contributor

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:

  1. New ### Missing values section 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 (flow absent without provenance, flow.tags.<key> absent for a tag the flow lacks), and states the rule for the comparison operators, in, exists(), and matches().

  2. Override handling on attestation rules. A rule is evaluated against the newest attestation whose name and type match 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-values anchor, 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 both matches(flow.name, "^snyk-.*") and not matches(flow.name, "^snyk-.*") evaluate to false for an artifact with no provenance. That does not follow from the rule: if matches() is false, ordinary boolean semantics make not matches() true. Asserting otherwise requires missingness to propagate through not, 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:

  • The rule covers exactly the comparison operators, in, exists(), and matches(), and no other form.
  • "Newest attestation whose name and type match, including overrides" is the precise rule.
  • Whether an explicitly empty tag value (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 override command (referenced in shipped changelog entries), but client_reference/ has no page for it and config/navigation.json has no entry. Since client_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-accounts break in tutorials/working_with_controls.mdx, which is on main and untouched here.
  • No new or moved pages, so no config/navigation.json or config/redirects.json change.

...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.
dangrondahl requested a review from a team as a code owner September 7, 2026 08:08

mintlify Bot commented Sep 7, 2026
edited
Loading

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
kosli 🟢 Ready View Preview Sep 7, 2026, 8:09 AM

claude Bot commented Sep 7, 2026
edited
Loading

Copy link
Copy Markdown
Contributor

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, and in is 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.

Fix this →

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

Comment thread policy-reference/environment_policy.mdx Outdated
Comment on lines +159 to +166
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()`:

@claude claude Bot Sep 7, 2026

Copy link
Copy Markdown
Contributor

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-.*")truenot 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 to false when either side is missing... Previously an expression like not 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; not does not turn it into true"), 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.

Fix this →

@dangrondahl dangrondahl Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

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.

Comment thread policy-reference/environment_policy.mdx Outdated
</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.

@claude claude Bot Sep 7, 2026

Copy link
Copy Markdown
Contributor

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.

  1. "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.

  2. grep -rn -i override over .md/.mdx finds no page that defines an override attestation — the only explanations on the site are changelog/index.mdx:44 and :157, and client_reference/ has no override command 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.

Suggested change
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.

@dangrondahl dangrondahl Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

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.

  1. The match is now pinned: "evaluated against the newest attestation whose name and type match 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.

  2. 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_reference page 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.

  1. 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 other ParamField on 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.

@claude claude Bot Sep 7, 2026

Copy link
Copy Markdown
Contributor

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, and in is 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": in returns false unless the right-hand side is a list containing the value. Worth stating, since flow.name in flow.tags.teams fails for a reason this page doesn't cover.
  • exists() — returning false for a missing operand is exists()'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 with exists()". Pulling it out of the list would make line 166 land harder.

Fix this →

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@claude claude[bot] claude[bot] left review comments

At least 1 approving review is required to merge this pull request.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

1 participant

AltStyle によって変換されたページ (->オリジナル) /