Skip to content

core: let a subject declare parts with has: - #247

Draft
jbpros wants to merge 1 commit into
mainfrom
core/subject-parts
Draft

jbpros wants to merge 1 commit into
mainfrom
core/subject-parts

Conversation

@jbpros

@jbpros jbpros commented Oct 17, 2026

Copy link
Copy Markdown
Contributor

Third step of #244. A subject can declare parts: named paths inside it, which its checks read by their alias.

With this input:

{
  "trail": {
    "name": "pr-491-r1",
    "compliance_status": {
      "attestations_statuses": {
        "findings": {"attestation_data": {"truncated": false}},
        "coverage-verification": {
          "attestation_id": "att-7",
          "attestation_data": {"hunks_total": 25, "hunks_acknowledged": 25}
        }
      }
    }
  }
}

a policy can say:

subjects:
  round:
    from: trail
    identified_by: name
    has:
      findings: compliance_status.attestations_statuses.findings.attestation_data
      coverage:
        from: compliance_status.attestations_statuses.coverage-verification
        identified_by: attestation_id
requirements:
  review_round:
    subject: round
    checks:
      complete: findings.truncated is false
      every_hunk_acknowledged: coverage.attestation_data.hunks_acknowledged equals coverage.attestation_data.hunks_total
  • Two forms: a part is one line (a path), or from with an optional identified_by, read inside the part. Nothing uses identified_by yet. The fourth step tags inputs with it.
  • The swap rule: a part's alias can be replaced by its path and every row stays the same: passed, cause and values. Only the names in expression and inputs keep the alias. The alias is expanded into the path when the policy is read, so this holds by construction.
  • Where it works: in the subject's checks, its from line conditions, and the whose of a subject that narrows it. It doesn't apply where a bare word reads an item instead, in the whose and the argument of a one-line every/some.
  • Clashes: an alias shadows a field. Two aliases with the same name are alias given twice. .name doesn't get around an alias.
  • Narrowing and chains: a subject that narrows another keeps its parts. A chained child reads its parent's part as artifact.pr_attestation.x, and its from can start there.
  • Old syntax: a part is read as $coverage.
  • The report: parts in the subjects section lists every part as {"from": …, "identified_by": … or null}, kept ones included.

Calls made while building it

  1. named by <part alias> is written wrong, as not supported yet. Otherwise it would silently read the subject's field with that name.
  2. A part's path can't start at $params or $input: parts are paths inside the subject.
  3. ergo.as_sentences leaves an old check as it was when any string in it equals one of its subject's part aliases, since turning ["cov", "x"] into cov.x would change its meaning.
  4. Two places where names are imperfect, both in spec/OPEN.md. A walk that enters a list in the middle of a part's path names failed items like x.sub[1].y. And a chained subject whose from starts at a parent's part shows the full path in $min_subjects and $unique_ids. Rows are correct in both cases.

Checks

  • Every check in AGENTS.md passes: 1355 tests, no uncovered lines, 1141 spec cases. spec/check_grammar.py finds 0 disagreements.
  • The swap rule is tested on about 60 checks: every phrase kind, quantifiers, for lines, derived values, whose, and the old syntax's all/any/any_of/substitutes. They run against 8 groups of bad inputs, and the rows must match.
  • Reports from main and from this branch, on every spec case from main and every corpus line on two inputs (1740 reports), are byte-identical. Policies without has: don't change.
  • I rewrote pr-reviewer's policy.yaml with 12 parts, and the "round whose …" subjects narrowing review_round. All 106 rows of the fixture round's report are identical to the original's, apart from the order of $applies inputs, which are sorted by their shorter names. pr-reviewer's 131 Rego tests pass. One report takes 0.40 s, against 0.33 s without parts.
  • The JS Wasm runtime passes. Memory is 12.7 MB and 10.9 MB, from 12.4 and 10.6 on main, so the sentence report has about 1 MB left under its 12 MB budget.

🤖 Generated with Claude Code

Kosli policies repeat long paths: every attestation sits at
`compliance_status.attestations_statuses.<name>.attestation_data`. A
subject can now name a path inside itself once, under `has:`, and its
checks use the name. Each entry is a part. Its key is the part's alias,
and its value is a path, or an object with `from` and `identified_by`.
Nothing reads `identified_by` yet, but it is checked and shown.

A part's alias stands for its path. `coverage.attestation_data.hunks_total`
gives the same passed, cause and values as the path written out, whether
the part is there, missing, `null` or the wrong type. Only the names in the
row's `expression`, `inputs` and `failed_items` keep the alias. This holds
because ergo puts the part's steps into the check's path before it reads
anything, and shows the alias in place of them. A test runs about 60 checks,
in sentences and in the old syntax, with the alias and with the full path,
on every kind of bad input, and compares the rows.

An alias is read wherever a bare first key reads the subject: in checks, in
`for` assertions, in the conditions of the subject's `from` line and of a
subject that narrows it. It isn't read in a `whose` or in the argument of a
one-line `every` or `some`. A subject that narrows another keeps its parts
and can add its own. A chained subject reads its parent's parts as
`artifact.pr_attestation.approvers`, and its `from` can start from one.
The old syntax reads a part as `$coverage`.

Written wrong, with the part's alias in the `$well_formed` name:
- `has` that isn't an object, a part that isn't a string or an object, no
  `from`, a field other than `from` and `identified_by`
- a path that doesn't parse, has `whose`, `count of` or `named by each of`,
  or starts at `$params` or `$input`
- an alias that isn't a bare word, or is a word of the grammar, `params` or
  `input`
- an alias given twice: two parts, one kept from a narrowed subject, a `for`
  alias, a chained subject's parent name, an old `as` or `each_as`
- `named by` an alias that is a part, which isn't supported yet because it
  would read the subject's field of that name

`as_sentences` leaves a check that names an alias as written, because a
sentence would read the part where the old check read the field.

The report's `subjects` section lists every part, kept ones included, as
`{"from": ..., "identified_by": ...}`. The spec has the rules (`parts.*`,
`name.part`, `sentence.path.part`), 23 cases in `spec/cases/parts`, both
JSON Schemas, and `REFERENCE.md` has a Parts section. Open questions are in
`spec/OPEN.md`.

Changes the report: only for a policy that uses `has:`, and the `parts`
entry of each subject in `subjects`, which was always `{}`. A policy
without `has:` gives the same report: checked on every spec case and every
line of the sentence corpus, as a check, a from line and a for line (3008
reports).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@jbpros jbpros added enhancement New feature or request 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. labels Oct 17, 2026

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. enhancement New feature or request 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.

1 participant