Skip to content

core: let a selector tag the values it reads with labels - #34

Draft
pbeckham wants to merge 1 commit into
mainfrom
core/selector-source
Draft

pbeckham wants to merge 1 commit into
mainfrom
core/selector-source

Conversation

@pbeckham

@pbeckham pbeckham commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

A selector can now define labels, a map of names to paths inside the item it picked. Every value read through that selector is tagged with those fields, so a report shows where each value came from. Kosli wants this so a row can name the attestation a check read.

"path": ["attestations", {
	"where": {"type": "pull_request"},
	"labels": {"evidence_id": ["id"], "url": ["url"]},
}, "state"]

gives this input on the row:

{
  "name": "attestations.[type==pull_request].state",
  "value": "MERGED",
  "labels": { "evidence_id": "a1b2c3", "url": "https://example.com/prs/42" }
}

How it behaves:

  • Labels go on each input, not on the row, because one row can read through several selectors: both sides of a compare, the options of an any_of, a substitute. A row-level tag would need a rule for which value wins when two selectors use the same name.
  • Labels never change whether a check passes, its cause or its expression. If the selector matches nothing or more than one item, every label is null, and so is a label whose path leads nowhere. If labels isn't an object, the input's labels is null.
  • Inputs read through a selector with no labels are unchanged, so existing reports are byte-identical.
  • For all and any, labels come from the selector in the check's path. A selector in the element check adds none, and a path still holds only one selector.

Known gap: if two any_of options read the same path with the same where but different labels, OPA errors instead of writing a failing row. The same error already happens today when two different paths render to the same name, so I've left both alone.

kosli-dev/sdlc-policies#19 tries this in SDLC-CTRL-0007, with the pinned ergo plus this change.

Ran opa check --strict . --ignore .github, opa fmt --list . and opa test . --ignore .github (336/336), and the coverage command lists no unreached lines.

🤖 Generated with Claude Code

A selector can define labels, a map of names to paths inside the item
it picked. Every input read through that selector gets a labels field
with those values, so a row shows which item each value came from, such
as the id of the attestation a check read.

Labels go on each input rather than the row, because one row can read
through several selectors. They never change whether a check passes,
its cause or its expression. Inputs read through a selector without
labels are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jbpros jbpros added impacts-ergo-policies Impacts ergo policy schema. Careful design attention needed. impacts-ergo-reports Impacts ergo report schema. Careful design attention needed. core Relates to the ergo library or format themselves. labels Oct 1, 2026
jbpros added a commit that referenced this pull request Oct 2, 2026
…are clear (#98)

Closes #27.

Eight rules keep their names, because the docs tell people to call or
define them: `report`, `report_with_params`, `violations`, `operators`,
`op_passed`, `value_at`, `arg` and `leaf_passed`. The other 227 now
start with `_`.

This breaks any policy that calls a helper directly, like
`ergo.row_cause`.

## How the rename was done

OPA lets a local variable or parameter share a rule's name, so a plain
find-and-replace wasn't safe. A script renamed identifiers outside
strings and field reads (`.x`), and would have skipped any rule body
that declared a local with the same name. It found none. Calls from
other packages were renamed only where written as `ergo.x`.

## Other changes

- Four tests in `ergo_test.rego` call `_is_violation`,
`_definition_field` and `_rfc3339_shaped` directly. Two pin that
`violations` reads only the requirements, and two test the RFC 3339 gate
on its own (`compare_time` accepts numbers through another branch), so
neither can be checked through the report. They stay where they are:
Regal's `leaked-internal-reference` doesn't apply to `_test.rego` files,
and moving them into `package ergo` would put test rules into
`data.ergo`.
- REFERENCE.md now names exactly the rules a custom operator may call,
says what each does, and says the rest can change. It also drops
`check_passed` from the list of rules that cause a loop, since it's
private now, and fixes "Two rules:" over a list of three.

## Checks

- `opa check --strict . --ignore .github`, `opa fmt --list .` (prints
nothing), `opa test . --ignore .github` (642/642, the same count as
main), and the `--strict-builtin-errors` run prints `true`.
- The coverage command lists no unreached lines.
- Only `ergo_test.rego` calls a `_` rule from outside `package ergo`.
- The baking example's report is byte-identical to main's.
- `regal lint --disable-all --enable leaked-internal-reference
--ignore-files '.github/**' .` finds nothing.
- 1,000 random policies and inputs give byte-identical reports and
violations on main and this branch.

#34 is the only open PR touching `ergo.rego`, so it will need its helper
calls renamed when rebased.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
jbpros added a commit that referenced this pull request Oct 7, 2026
Part of #114. Since #148, a requirement can't carry any wording:
`description` fails as an unknown field. This adds two fields, and
answers #114's questions about notes in the policy: one agreed field,
`meta`, on checks and requirements, kept in the definitions in
`requirements` rather than on the rows, and type-checked. The labels
half of #114 (#34) is untouched.

- **`description`** on a requirement: what it asks for, in plain words.
Checks already had it.
- **`meta`** on a requirement and on any check: anything else to keep
with it, like a control id, an owner or a framework. ergo copies it into
the report and never reads it.

With this input:

```json
{"deployments": [
  {"id": "d-1", "environment": "prod", "approved_by": "alice"},
  {"id": "d-2", "environment": "prod"}
]}
```

and this requirement:

```json
{"prod_deploy": {
  "description": "Every production deployment is approved",
  "meta": {"control": "SDLC-CTRL-0007", "frameworks": ["SOC 2"]},
  "subject_type": "deployment",
  "from": ["deployments"],
  "id": ["id"],
  "checks": {"approved": {
    "description": "Someone approved the deployment",
    "meta": {"severity": "high"},
    "op": "non_empty_string",
    "path": ["approved_by"]
  }}
}}
```

the requirement's entry in the report starts like this (`$` checks left
out, each of them has `"meta": {}`):

```json
{
  "checks": {
    "approved": {
      "description": "Someone approved the deployment",
      "expression": "approved_by is a non-empty string",
      "meta": {"severity": "high"},
      "op": "non_empty_string",
      "path": ["approved_by"]
    }
  },
  "description": "Every production deployment is approved",
  "meta": {"control": "SDLC-CTRL-0007", "frameworks": ["SOC 2"]},
  "require": "every",
  "status": "not_met",
  ...
}
```

The rules:

- Both are optional, but the report always has them, `""` and `{}` when
not written, on every requirement and every check, ergo's own `$` checks
included. So `meta.control` never throws in JavaScript: a missing key
reads as `undefined`, where a missing `meta` would throw.
- `meta` holds anything JSON can, so YAML like `version: 2026` or
`reviewed: yes` just works. Its numbers follow the range rule numbers
follow everywhere else in a policy (`1e400` is written wrong), and a key
that isn't a string or a set, which only a Rego-written policy can hold,
is written wrong too.
- The report writes `meta`'s numbers in their plain form, the way
expressions already do: `1.50`, `1e2` and `-0` come out as `1.5`, `100`
and `0`. OPA otherwise keeps a number exactly as it was written, so the
same `meta` would give different bytes depending on how it was typed,
and differ from a JavaScript implementation. A test compares the
marshalled reports of two policies that write the same numbers
differently, and it passes under OPA, Wasm and the JS runtime. It fails
if `meta` is copied as written.
- A `null` `description` or `meta` counts as not written, since YAML
reads an empty `description:` as `null`. ergo only copies these two
fields, so this can't change a verdict. Every other field still counts
`null` as the wrong type.
- Any other `description` that isn't a string, or `meta` of the wrong
shape, fails `$well_formed`, on a requirement or a check (custom
operators included). The report shows `""` or `{}` in its place.
- Violations are unchanged. The requirement's description stays in
`report.requirements[row.requirement]` rather than being copied onto
every row.

A filter in `applies_to` can have both, and ergo checks them, but they
don't reach the report, because every filter shares the one `$applies`
entry.

The policy-level `description` and `meta` wait for the shape of a policy
to be settled in #113, since today a policy is a bare map of
requirements and has nowhere to put them.

Changes the report: every requirement entry and every check in it gets
`description` and `meta`, `""` and `{}` when not written. A check whose
`description` isn't a string used to be echoed and is now written wrong.

### Checked

- `opa check --strict`, `opa fmt --list`, `opa test` (859/859),
`--strict-builtin-errors`, Regal and the comment check, all clean. Full
coverage.
- OPA 1.20.2 in Docker (859/859), Wasm with OPA 1.19.0 (859/859), and
the JS Wasm runtime script (859/859).
- The README example run as Rego and as YAML gives the same report, and
the reference's report sample is from that run.
- Downstream, against `main` and this branch: sdlc-policies (95/95
across four policies), server npm-bump (6/6) and its 11 demo policies
compile, pr-reviewer review-controls (97/97). None uses a non-string
description or a `meta` field. I didn't run sdlc-policies' Python system
tests, which may compare whole reports.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

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

Labels

core Relates to the ergo library or format themselves. impacts-ergo-policies Impacts ergo policy schema. Careful design attention needed. impacts-ergo-reports Impacts ergo report schema. Careful design attention needed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants