Skip to content

core: let requirements and checks carry a description and meta - #151

Merged
jbpros merged 5 commits into
mainfrom
core/description-and-meta
Oct 7, 2026
Merged

jbpros merged 5 commits into
mainfrom
core/description-and-meta

Conversation

@jbpros

@jbpros jbpros commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

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:

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

and this requirement:

{"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": {}):

{
  "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

A requirement can now say what it asks for in `description`, and keep
anything else, like a control id or an owner, in `meta`. Checks can have
`meta` too. ergo copies both into the report and never reads `meta`.

`meta` only holds strings and lists of strings, so every implementation
writes the same report.

Changes the report: every requirement entry and every check in it now has
a `description`, `""` when none is written, and `meta` when one is. A
check whose `description` isn't a string is now written wrong, where ergo
used to echo it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jbpros jbpros added enhancement New feature or request core Relates to the ergo library or format themselves. labels Oct 7, 2026
jbpros and others added 2 commits October 7, 2026 07:43
A missing `meta` makes `meta.control` throw in JavaScript, where a
missing key in `meta` just reads as undefined. So every requirement and
every check in the report has a `meta`, ergo's own checks included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Strings and lists of strings only was too strict: in YAML, `version: 2026`
or `reviewed: yes` failed the requirement. meta now takes any JSON value.
Its numbers follow the range rule numbers follow elsewhere, and a key that
isn't a string or a set, which only Rego can write, is written wrong.

The report writes meta's numbers in their plain form, the way expressions
already do, so `1.50` and `1.5` give the same bytes, in OPA, Wasm and the
JavaScript runtime alike.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jbpros and others added 2 commits October 7, 2026 08:11
A filter can have them, and ergo checks them, but every filter shares the
one `$applies` entry, which keeps ergo's own description. Also build the
requirement entry as one object rather than a union.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
YAML reads an empty `description:` or `meta:` as null, and failing the
requirement for a blank field punishes a gap, not a mistake. ergo only
copies these two fields, so null can't change a verdict. Every other field
still counts null as the wrong type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jbpros
jbpros marked this pull request as ready for review October 7, 2026 06:38
@jbpros
jbpros merged commit 1d92667 into main Oct 7, 2026
6 checks passed
@jbpros
jbpros deleted the core/description-and-meta branch October 7, 2026 06:38
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. enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant