From 0a941ed410465b77b824e20990b5df5945a857b4 Mon Sep 17 00:00:00 2001 From: Julien Biezemans Date: Sun, 18 Oct 2026 01:17:42 +0200 Subject: [PATCH 1/3] core: let a subject declare parts with has: Kosli policies repeat long paths: every attestation sits at `compliance_status.attestations_statuses..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 --- REFERENCE.md | 164 +- ergo.rego | 619 +++++++- ergo_test.rego | 852 +++++++++++ spec/OPEN.md | 9 + spec/cases/parts/cases.json | 2840 +++++++++++++++++++++++++++++++++++ spec/policy/schema.json | 77 + spec/report/schema.json | 14 +- spec/semantics.md | 16 +- spec/sentences.md | 6 +- spec/syntax.md | 30 +- spec_test.rego | 4 +- 11 files changed, 4552 insertions(+), 79 deletions(-) create mode 100644 spec/cases/parts/cases.json diff --git a/REFERENCE.md b/REFERENCE.md index f94a3de..299b288 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -130,11 +130,12 @@ deployment: identified_by: id ``` -| Field | Meaning | Default | -| --------------- | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | -| `description` | What the subject is, in words anyone can read. The report copies it into its [`subjects`](#subjects-in-the-report). | `""` | -| `from` | Where the subjects are in the input, and which of them are in scope, written as a [from line](#from-lines). | the input field named like the subject | -| `identified_by` | The path, inside one subject, to the value that identifies it. | the whole subject | +| Field | Meaning | Default | +| --------------- | ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | +| `description` | What the subject is, in words anyone can read. The report copies it into its [`subjects`](#subjects-in-the-report). | `""` | +| `from` | Where the subjects are in the input, and which of them are in scope, written as a [from line](#from-lines). | the input field named like the subject | +| `identified_by` | The path, inside one subject, to the value that identifies it. | the whole subject | +| `has` | Names for paths inside the subject, so a check can write a short alias where a long path would go. See [Parts](#parts). | none | `applies_to` and `of`, and a `from` or `identified_by` written as a list, are the [old syntax](#subjects-in-the-old-syntax). @@ -283,6 +284,111 @@ A from line that's another subject's name reads that subject's `from` and `ident - A problem fails `$well_formed` on every requirement that names a subject whose chain has it, and the requirement finds no subjects. - A requirement can name the subject others build on too, like `component` to check every component. +### Parts + +Data often keeps what a check needs deep inside the subject. Kosli, for one, puts every attestation at `compliance_status.attestations_statuses..attestation_data`, so a policy that reads three attestations repeats that path in every check. A subject can give a path a name once, with `has`, and the checks use the name. Each entry of `has` is a **part**, and its key is the part's alias. An alias is a short word that stands for something longer, as `as pr` does on a `for` line. + +```yaml +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 +``` + +With this input: + +```json +{ + "trail": { + "name": "round-1", + "compliance_status": { + "attestations_statuses": { + "findings": { "attestation_data": { "truncated": false } }, + "coverage-verification": { + "attestation_id": "cov-9", + "attestation_data": { "hunks_acknowledged": 25, "hunks_total": 28 } + } + } + } + } +} +``` + +`complete` passes and `every_hunk_acknowledged` fails with cause `value`: + +```json +{"check": "complete", "subject": {"type": "round", "id": "round-1"}, "passed": true, "cause": "satisfied", + "inputs": [{"name": "findings.truncated", "value": false}]} +{"check": "every_hunk_acknowledged", "subject": {"type": "round", "id": "round-1"}, "passed": false, "cause": "value", + "inputs": [{"name": "coverage.attestation_data.hunks_acknowledged", "value": 25}, + {"name": "coverage.attestation_data.hunks_total", "value": 28}]} +``` + +A part is a short name for its path and nothing more. `coverage.attestation_data.hunks_total` gives exactly the rows that `compliance_status.attestations_statuses.coverage-verification.attestation_data.hunks_total` gives: the same `passed`, the same `cause` and the same values. A part that is missing fails the check as `absent`, a `null` one as `absent` when a step follows it, and one of the wrong type as `unusable`, because the full path would. Only the names differ. With `coverage-verification` missing from the input, the row is + +```json +{"check": "every_hunk_acknowledged", "subject": {"type": "round", "id": "round-1"}, "passed": false, "cause": "absent", + "inputs": [{"name": "coverage.attestation_data.hunks_acknowledged", "value": null}, + {"name": "coverage.attestation_data.hunks_total", "value": null}]} +``` + +and written out in full, its inputs are named `compliance_status.attestations_statuses.coverage-verification.attestation_data.hunks_acknowledged` and `...hunks_total`, with the same `null`. The `expression` of a check and the names in its `inputs` and `failed_items` keep the alias, so a reader sees the words the policy was written in. + +- A part is one line, a path, or an object with `from`, a path, and `identified_by`, a path read inside the part. Nothing reads `identified_by` yet, but the report shows it, because it will say how to tell one part from another. +- A path is written as in a [from line](#from-lines): keys joined by `.`, keys in quotes, positions, and `named by` a key of `$params` or `$input`. It is a path inside the subject, so it can't start at `$params` or `$input`, and it can't have `whose`, `count of` or `named by each of`. +- A part can be any path, even one that ends at a single value: `ratio: compliance_status.attestations_statuses.coverage.attestation_data.ratio` makes `ratio is at least 0.8` read the same. +- Keep the data's own names, as `findings` and `coverage` do, because a reader who knows the data finds `coverage.attestation_data.hunks_total` where they'd look for it. +- An alias is read wherever a bare first key reads the subject: in the subject's checks, in the assertions of a `for` line, and 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`, where a bare first key is a field of the item. `named by` a part's alias is written wrong, with `named by coverage reads a part, which isn't supported yet`. +- An alias shadows a field of the subject with the same name, and `.coverage` doesn't get around it: that dot only marks a field named like a word of the grammar. To read the field, give the part another name. +- An alias is a bare word that isn't a word of the grammar, `params` or `input`, or it is written wrong with `invalid alias`. Two aliases with the same name are written wrong with `alias given twice`: two parts of a subject, a part and one a subject keeps from a subject it narrows, a part and an alias of a `for` line in a check of the subject, a part and the name of the subject a chained subject reads from, and in the [old syntax](#subjects-in-the-old-syntax) a part and an `as` or `each_as` alias. +- A subject that narrows another keeps its parts and can add its own. The report lists them all. +- A [chained subject](#from-lines) reads its parent's parts through the parent's name, and its `from` line can start from one. With `{"params": {"artifact_name": "web"}}` and this input: + + ```json + {"trail": {"artifacts": {"web": {"fingerprint": "sha-1", "attestations_statuses": { + "pull-request": {"attestation_data": {"approvers": ["bob"], "pull_requests": [{"url": "pr/7"}, {"url": "pr/8"}]}}}}}}} + ``` + + this policy reads the pull requests of the attestation, and checks each of them against its approvers: + + ```yaml + subjects: + artifact: + from: trail.artifacts named by $params.artifact_name + identified_by: fingerprint + has: + pr_attestation: attestations_statuses.pull-request.attestation_data + pull request: + from: artifact.pr_attestation.pull_requests + identified_by: url + requirements: + approved: + subject: pull request + checks: + approved: artifact.pr_attestation.approvers is not empty + ``` + + ```json + {"check": "approved", "subject": {"type": "pull request", "id": "pr/7", "in": {"artifact": "sha-1"}}, "passed": true, "cause": "satisfied", + "inputs": [{"name": "artifact.pr_attestation.approvers", "value": ["bob"]}]} + ``` + + A part is only reached through its subject, so a bare `pr_attestation` in a check of `pull request` is one of its own fields. +- In the old syntax, a check reads a part as `$alias` in the first step of a path, like `{"op": "equals", "path": ["$coverage", "attestation_data", "hunks_total"], "value": 28}`, and the name keeps the dollar: `$coverage.attestation_data.hunks_total`. The same rules apply, and a part's path is written as above whichever syntax the subject uses. +- A part that is written wrong fails `$well_formed` on every requirement that names the subject, with the problem under the part's alias, like `{"name": "subjects.round.has.coverage", "value": ["missing from"]}`. That is a `has` that isn't an object (`not an object`), a part that isn't a string or an object (`not a string or an object`), an object without `from` (`missing from`), with a field other than `from` and `identified_by` (`unknown field `), or with a `from` or `identified_by` that isn't a string (`invalid from`, `invalid identified_by`), and a path that can't be read (`from: a part is a path: whose and named by each of can't go in it`, `from: a part reads inside its subject, so it can't start at $params or $input`, or the problems a from line gives). +- The report lists every part under the subject's [`parts`](#subjects-in-the-report), including the ones it keeps from a subject it narrows. + ## Requirements ```yaml @@ -1030,7 +1136,16 @@ Each entry has: - `description` is the subject's, or `""` when it has none. A `description` that isn't a string is [written wrong](#subjects) and shows as `""`. - `from` says how the subject is found, as a [from line](#from-lines) is written. A `from` written as a line is printed the same way whatever spaces it had. One written as a list, in the [old syntax](#subjects-in-the-old-syntax), is printed as a sentence too: its path, then ` whose ` and its `applies_to` filters, sorted by name and joined with `and`. A [naming step](#naming-subjects) at the end of the path shows as `named by each of $params.suites` when its keys come from a ref, as `` when they're listed, and as `` when it has none, since a `from` line has no way to write those two. A filter that can't be written as a sentence, like one with a custom operator or a substitute, shows as `` so it's never left out, and an `applies_to` that isn't an object shows as ``. A subject built on another with `of` starts with that subject's name. A subject without `from` reads the input field named like it. A `from` that is written wrong shows as ``. - `identified_by` is the path of the subject's id, or `null` when it has none, and the whole subject is its id. A subject that narrows another has the id of the one it narrows. An id that is written wrong shows as ``. -- `parts` is always `{}` for now. +- `parts` lists every [part](#parts) the subject has, including the ones it keeps from a subject it narrows, by alias. Each is `{"from": ..., "identified_by": ...}`, with `from` written the way a sentence writes a path, so `" dd.b "` is `dd.b` and `x[$params.k]` is `x named by $params.k`, and `identified_by` the path of the part's id or `null` when it has none. For the policy under [Parts](#parts) it is + + ```json + { + "coverage": {"from": "compliance_status.attestations_statuses.coverage-verification", "identified_by": "attestation_id"}, + "findings": {"from": "compliance_status.attestations_statuses.findings.attestation_data", "identified_by": null} + } + ``` + + A part that is written wrong is listed as best it can be, with `` for a path that can't be read and `` for an id that can't. A subject that is written wrong still has its entry, so nothing disappears from the report. With this policy and the same input: @@ -1118,23 +1233,25 @@ The fields of the requirement and its subject get an input of the same form when ] ``` -| Field | What's wrong | -| ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -| any field ergo doesn't know | `unknown field` | -| `checks` | `not an object`, `holds a key that isn't a string`, `missing`, `empty` | -| `require` | `neither every nor some` | -| `min_subjects` | `not a whole number of 0 or more`, `number out of range` | -| `subject` | `missing`, `empty or not a string`, `not in subjects` | -| `subjects` | `not an object` | -| `subjects.deployment` | `not an object` | -| `subjects.deployment.applies_to` | `not an object`, `holds a key that isn't a string` | -| `subjects.deployment.from`, `...identified_by` | `not a list`, `step that can't be a key`, `number out of range`, `holds a set`, `holds a key that isn't a string` | -| `subjects.deployment.from` | `object step before the last`, `object step without each_as`, `invalid alias`, `invalid keys`, `unknown field foo in naming step` | -| `subjects.deployment.identified_by` | `ref inside where`, `literal inside where` | -| `subjects.deployment.description` | `not a string` | -| `subjects.deployment.of` | `empty or not a string`, `not in subjects`, `leads to a loop` | -| `subjects.deployment.from`, `...identified_by` | `not allowed with of` | -| `subjects.deployment.applies_to.locked` | `also a filter of a subject it builds on` | +| Field | What's wrong | +| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| any field ergo doesn't know | `unknown field` | +| `checks` | `not an object`, `holds a key that isn't a string`, `missing`, `empty` | +| `require` | `neither every nor some` | +| `min_subjects` | `not a whole number of 0 or more`, `number out of range` | +| `subject` | `missing`, `empty or not a string`, `not in subjects` | +| `subjects` | `not an object` | +| `subjects.deployment` | `not an object` | +| `subjects.deployment.applies_to` | `not an object`, `holds a key that isn't a string` | +| `subjects.deployment.from`, `...identified_by` | `not a list`, `step that can't be a key`, `number out of range`, `holds a set`, `holds a key that isn't a string` | +| `subjects.deployment.from` | `object step before the last`, `object step without each_as`, `invalid alias`, `invalid keys`, `unknown field foo in naming step` | +| `subjects.deployment.identified_by` | `ref inside where`, `literal inside where` | +| `subjects.deployment.description` | `not a string` | +| `subjects.deployment.of` | `empty or not a string`, `not in subjects`, `leads to a loop` | +| `subjects.deployment.from`, `...identified_by` | `not allowed with of` | +| `subjects.deployment.applies_to.locked` | `also a filter of a subject it builds on` | +| `subjects.deployment.has` | `not an object` | +| `subjects.deployment.has.coverage` | `not a string or an object`, `missing from`, `unknown field foo`, `invalid from`, `invalid identified_by`, `from: ...`, `identified_by: ...`, `invalid alias`, `alias given twice` | A policy written in Rego can use a key that isn't a string, like `true` or `1.5`. ergo names it `` or ``, so two such keys never share a name. A requirement whose own name isn't a string, like `1`, or every requirement of a policy written as a list, fails `$well_formed`, which then starts with `{"name": "requirement name", "value": ["not a string"]}`. The requirement is `not_met`, so the policy isn't compliant. @@ -1250,6 +1367,7 @@ ergo fails a check whenever it can't be sure, instead of letting it pass. Rego d - `compare` needs both sides to exist and have the same type. In plain Rego, `null < 5` is true, so a missing field would otherwise pass a `lt` check. Ordering objects, lists or booleans fails too. - `every`, `some` and `for` lines need non-empty lists, inner lists included. A policy that's fine with no items says so with `, if any,`. - An alias given twice, or badly written, fails the check, so an inner alias can't quietly hide an outer one. +- A [part](#parts) that is missing, `null` or the wrong type fails a check exactly as the full path it stands for would, and a part that is written wrong, or has an alias another one has, fails `$well_formed` and is never read as an alias. - A check that's written wrong, like an unknown `op` or `cmp`, fails `$well_formed`, and its rows fail with cause `ill_formed`, so a mistake in a subject's conditions can't rule every subject out. - A value of the wrong kind, like a string where `range` needs a number, or a param of the wrong type, fails with cause `unusable`, so a condition can't rule a subject out on it. - With several conditions, one that can't be read fails the requirement, even when another rules the subject out. The only condition that can rule a subject out regardless is an `exists` that found its field missing. diff --git a/ergo.rego b/ergo.rego index cb00281..f1fd7ca 100644 --- a/ergo.rego +++ b/ergo.rego @@ -230,7 +230,7 @@ _unknown_req_fields(req) := sort([f | _unknown_subject_fields(def) := sort([f | some f in object.keys(def) - not f in object.keys(_subject_wrong_type_problems) + not f in (object.keys(_subject_wrong_type_problems) | {"has"}) ]) _unknown_req_fields(req) := [] if not is_object(req) @@ -293,6 +293,10 @@ _member_problems(members) := union({ some f, ps in object.get(data.ergo_subject_sentences.problems, m, {}) some p in ps }, + {[array.concat(["subjects", m], p[0]), p[1]] | + some m in members + some p in object.get(object.get(data.ergo_subject_sentences, "has_problems", {}), m, set()) + }, {[["subjects", m, "applies_to", n], "also a filter of a subject it builds on"] | some m in members some n in object.keys(_object_or_empty(_filters_of(m))) @@ -667,7 +671,20 @@ _keyed(doc, req) := coll if { _keyed(doc, req) := {} if not is_object(_target(doc, req)) -_scope_of(req, subj) := {"ergo/names": object.union(names, _parent_names)} if names := _each_names(req, subj) +_scope_of(req, subj) := {"ergo/names": object.union(object.union(names, _parent_names), _subject_name(req, subj))} if names := _each_names(req, subj) + +_subject_name(req, subj) := {"ergo/subject": subj} if _has_parts(req) + +_subject_name(req, _) := {} if not _has_parts(req) + +_subjects_with_parts := {name | some name, parts in object.get(data.ergo_subject_sentences, "parts", {}); count(parts) > 0} + +default _has_parts(_) := false + +_has_parts(req) if { + is_string(object.get(req, "subject", null)) + req.subject in _subjects_with_parts +} _each_names(req, subj) := {_each_step(req).each_as: subj} if { _has_step(req) @@ -684,6 +701,8 @@ _scoped(req) if _has_step(req) _scoped(_) if _parent_names != {} +_scoped(req) if _has_parts(req) + _passes(req, check, flaw, subj) := _check_passed(check, subj, flaw) if not _scoped(req) else := v if { @@ -1110,8 +1129,67 @@ _start_of(_, path) := data.ergo_params if path[0] == "$$params" _start_of(_, path) := input["ergo/names"][substring(path[0], 1, -1)] if { _named(path) not _builtin(path) + not _part_start(path) +} + +_start_of(subj, path) := subj if { + _subject_part(path) + _part_of(path).old == false +} + +_start_of(_, path) := input["ergo/names"]["ergo/subject"] if { + _subject_part(path) + _part_of(path).old == true +} + +_start_of(_, path) := input["ergo/names"][_part_of(path).of] if { + _part_start(path) + not _subject_part(path) } +default _part_start(_) := false + +_part_start(path) if { + is_array(path) + is_string(path[0]) + startswith(path[0], "${\"alias\":") + json.is_valid(substring(path[0], 1, -1)) + is_object(json.unmarshal(substring(path[0], 1, -1))) + object.keys(json.unmarshal(substring(path[0], 1, -1))) == {"alias", "n", "of", "old"} +} + +_part_of(path) := json.unmarshal(substring(path[0], 1, -1)) + +default _subject_part(_) := false + +_subject_part(path) if { + _part_start(path) + _part_of(path).of == null +} + +default _off_subject(_) := false + +_off_subject(path) if { + _named(path) + not _subject_part(path) +} + +_visible(path) := array.concat(_visible_root(_part_of(path)), array.slice(path, 1 + _part_of(path).n, count(path))) if _part_start(path) + +_visible(path) := path if not _part_start(path) + +_visible_root(m) := [m.alias] if { + m.of == null + m.old == false +} + +_visible_root(m) := [concat("", ["$", m.alias])] if { + m.of == null + m.old == true +} + +_visible_root(m) := [concat("", ["$", m.of]), m.alias] if m.of != null + _keys_of(path) := ks if { is_array(path) not _named(path) @@ -1304,7 +1382,9 @@ _selector_matches(v, sel) if { _field_path_name(path) := concat(".", [_segment_name(path[i]) | some i in _names(path)]) -_path_name(path) := concat("", ["item ", _literal_text(_unliteral(path[count(path) - 1]) + 1), " of ", _steps_name(array.slice(path, 0, count(path) - 1))]) if { +_path_name(path) := _visible_path_name(_visible(path)) + +_visible_path_name(path) := concat("", ["item ", _literal_text(_unliteral(path[count(path) - 1]) + 1), " of ", _steps_name(array.slice(path, 0, count(path) - 1))]) if { is_array(path) count(path) > 1 _index_step(path[count(path) - 1]) @@ -1327,7 +1407,7 @@ _index_step(s) if { _is_key(s.literal) } -_steps_name(path) := concat("", [_step_name(i, path[i]) | some i in _names(path)]) +_steps_name(path) := concat("", [_step_name(i, visible[i]) | visible := _visible(path); some i in _names(visible)]) _plain_steps_name(path) := concat("", [_plain_step_name(i, path[i]) | some i in _names(path)]) @@ -1448,7 +1528,7 @@ _item_path_name(_, path) := _path_name(path) if path != [] _projection_name(path, each) := concat("", [_steps_name(path), "[]", _tail_name(each)]) -_tail_name(steps) := concat("", [_step_name(i + 1, steps[i]) | some i in _names(steps)]) if is_array(steps) +_tail_name(steps) := concat("", [_step_name(i + 1, visible[i]) | visible := _visible(steps); some i in _names(visible)]) if is_array(steps) _tail_name(steps) := "." if not is_array(steps) @@ -2707,6 +2787,18 @@ _unknown_name_problem(node, names) := {concat("", ["unknown alias ", p[0]]) | _name_known(start, _) if start in {"$$input", "$$params"} +_name_known(start, names) if { + _part_start([start]) + _part_of([start]).of == null + _part_of([start]).alias in names +} + +_name_known(start, names) if { + _part_start([start]) + _part_of([start]).of != null + _part_of([start]).of in names +} + _name_known(start, names) if { not startswith(start, "$$") substring(start, 1, -1) in names @@ -4086,10 +4178,13 @@ _checks_at(req, "applies_to", n) := ["subjects", _filter_owner(req, n), "applies _checks_at(_, "checks", n) := ["checks", n] -_from_names(req, here) := {n | +_from_names(req, here) := ({n | _from_well_formed(req) n := _each_step(req).each_as -} | {data.ergo_subject_sentences.chains[req.subject] | _chained(req); here} +} | {data.ergo_subject_sentences.chains[req.subject] | _chained(req); here}) | {a | + is_string(object.get(req, "subject", null)) + some a, _ in object.get(object.get(data.ergo_subject_sentences, "parts", {}), req.subject, {}) +} _well_formed_inputs(req) := array.concat( array.concat( @@ -4416,10 +4511,11 @@ _names(x) := sort(x) if is_set(x) report(doc, policy, params) := r if { raw := _section(policy, "requirements") written_subjects := _section(policy, "subjects") - reads := _s_from_reads(written_subjects) - subjects := _s_subjects(written_subjects, reads) - subject_sentences := _s_subject_sentences(written_subjects, reads) - reqs := _s_requirements(raw, subject_sentences.chains) + parts := _p_parts(written_subjects) + reads := _s_from_reads(written_subjects, parts) + subjects := _s_subjects(written_subjects, reads, parts) + subject_sentences := _s_subject_sentences(written_subjects, reads, parts) + reqs := _p_requirements(raw, subject_sentences.chains, parts) sentences := _s_sentences(raw, reqs) unreadable := _unreadable_inputs(doc, params) resolved := _resolved_subjects with data.ergo_subjects as subjects with data.ergo_subject_sentences as subject_sentences @@ -4458,7 +4554,28 @@ _reported_subject(name, def) := { "description": _reported_description(def), "from": _subject_from_text(name, def), "identified_by": _subject_id_text(name), - "parts": {}, + "parts": _reported_parts(name), +} + +_reported_parts(name) := {alias: {"from": _part_from_text(read.from), "identified_by": _part_id_text(read.identified_by)} | + some alias, read in object.get(object.get(data.ergo_subject_sentences, "parts", {}), name, {}) + is_string(alias) +} + +_part_from_text(read) := read.text if "text" in object.keys(read) + +_part_from_text(read) := "" if not "text" in object.keys(read) + +_part_id_text(null) := null + +_part_id_text(read) := read.text if { + read != null + "text" in object.keys(read) +} + +_part_id_text(read) := "" if { + read != null + not "text" in object.keys(read) } _subject_from_text(name, null) := _path_name([name]) @@ -5092,7 +5209,7 @@ _side_inputs(subj, side, item) := array.concat([_derived_input(subj, side, _side _tally(subj, side) := {"states": {"not_an_object"}, "list": _path_name(path), "values": []} if { path := side[_basic_form(side)] - not _named(path) + not _off_subject(path) not is_object(subj) } else := {"states": {_read_state(subj, path)}, "list": _path_name(path), "values": []} if { path := side[_basic_form(side)] @@ -6051,12 +6168,7 @@ _s_single_quoted(v) if { regex.match(`^[^\x00-\x1f]*$`, v) } -_s_from_reads(subjects) := {name: object.union(_s_from_part(name, lines, subjects, {"narrow": narrow, "descend": descend}), _s_id_part(def)) | some name, def in subjects; is_object(def)} if { - is_object(subjects) - lines := {name: _from_line(def.from) | some name, def in subjects; is_object(def); is_string(def.from)} - narrow := {name: _s_chain_parent(name, lines, subjects) | some name, _ in subjects} - descend := {name: lines[name].path.steps[0] | some name, _ in lines; "path" in object.keys(lines[name]); _s_descends(name, lines[name], subjects)} -} +_s_from_reads(subjects, parts) := {name: object.union(_s_from_part(name, parts.lines, subjects, {"narrow": parts.narrow, "descend": parts.descend, "parts": parts.effective}), _s_id_part(def)) | some name, def in subjects; is_object(def)} if is_object(subjects) _s_from_part(name, lines, subjects, chains) := _s_from_read(name, lines[name], subjects, chains) if name in object.keys(lines) @@ -6072,22 +6184,158 @@ _s_id_read(text) := {"path": _s_model_path(fl.path)} if { count(_s_from_paths_unsupported({"path": fl.path})) == 0 } else := {"problems": [`identified_by is a path, like "id" or "build.number"`]} -_s_from_reads(subjects) := {} if not is_object(subjects) +_s_from_reads(subjects, _) := {} if not is_object(subjects) + +_p_alias_ok(alias) if { + is_string(alias) + regex.match(`^[A-Za-z_][A-Za-z0-9_-]*$`, alias) + not _s_bad_name(alias) +} + +_p_own_reads(subjects) := {name: {alias: _p_part_read(alias, entry) | some alias, entry in def.has} | + some name, def in subjects + is_object(def) + is_object(object.get(def, "has", null)) +} if is_object(subjects) + +_p_part_read(alias, entry) := {"from": from, "identified_by": id, "problems": sort(problems)} if { + from := _p_field_read(entry, "from") + id := _p_field_read(entry, "identified_by") + problems := ((_p_alias_problems(alias) | _p_shape_problems(entry)) | _p_prefixed("from", from)) | _p_prefixed("identified_by", id) +} + +_p_alias_problems(alias) := {"invalid alias" | not _p_alias_ok(alias)} + +_p_shape_problems(entry) := {"not a string or an object"} if { + not is_string(entry) + not is_object(entry) +} + +_p_shape_problems(entry) := ({concat("", ["unknown field ", _text(f)]) | some f in object.keys(entry); not f in {"from", "identified_by"}} | {"missing from" | not "from" in object.keys(entry)}) | {concat("", ["invalid ", f]) | some f in ["from", "identified_by"]; f in object.keys(entry); not is_string(entry[f])} if is_object(entry) + +_p_shape_problems(entry) := set() if is_string(entry) + +_p_prefixed(f, read) := {concat("", [f, ": ", p]) | + is_object(read) + some p in object.get(read, "problems", []) +} + +_p_field_read(entry, "from") := _p_path_read(entry) if is_string(entry) + +_p_field_read(entry, f) := _p_path_read(entry[f]) if { + is_object(entry) + is_string(object.get(entry, f, null)) +} + +_p_field_read(entry, f) := {} if { + is_object(entry) + f in object.keys(entry) + not is_string(entry[f]) +} + +_p_field_read(entry, "identified_by") := null if { + is_object(entry) + not "identified_by" in object.keys(entry) +} -_s_subjects(subjects, reads) := {name: _s_subject(name, def, reads) | some name, def in subjects} if is_object(subjects) +_p_field_read(entry, "identified_by") := null if is_string(entry) + +_p_field_read(entry, "from") := {} if { + not is_string(entry) + not is_object(entry) +} + +_p_field_read(entry, "identified_by") := null if { + not is_string(entry) + not is_object(entry) +} + +_p_field_read(entry, "from") := {} if { + is_object(entry) + not "from" in object.keys(entry) +} + +_p_path_read(text) := {"path": path, "text": _path_name(path)} if { + fl := _from_line(text) + count(_p_path_problems(fl)) == 0 + path := _s_model_path(fl.path) +} + +_p_path_read(text) := {"problems": sort(_p_path_problems(_from_line(text)))} if count(_p_path_problems(_from_line(text))) > 0 + +_p_path_problems(fl) := {p | some p in object.get(fl, "problems", ["doesn't parse"])} if not "path" in object.keys(fl) + +_p_path_problems(fl) := ({"a part is a path: whose and named by each of can't go in it" | object.keys(fl) != {"path"}} | {"a part reads inside its subject, so it can't start at $params or $input" | "start" in object.keys(fl.path)}) | _s_from_paths_unsupported({"path": fl.path}) if "path" in object.keys(fl) + +_p_valid(read) if { + read.problems == [] + "path" in object.keys(read.from) +} -_s_subjects(subjects, _) := subjects if not is_object(subjects) +_p_lines(subjects) := {name: _from_line(def.from) | some name, def in subjects; is_object(def); is_string(def.from)} -_s_subject(name, null, _) := {"from": [name]} +_p_narrow(subjects, lines) := {name: _s_chain_parent(name, lines, subjects) | some name, _ in subjects} -_s_subject(name, def, reads) := object.union(_s_without_narrowed_from(def, reads[name]), object.union(_s_from_fields(name, def, reads), _s_id_field(reads[name]))) if is_object(def) +_p_descend(subjects, lines) := {name: lines[name].path.steps[0] | some name, _ in lines; "path" in object.keys(lines[name]); _s_descends(name, lines[name], subjects)} + +_p_parts(subjects) := {"own": own, "effective": {name: _p_merged(name, own, narrow) | some name, _ in subjects}, "problems": _p_problems(subjects, own, narrow, descend), "lines": lines, "narrow": narrow, "descend": descend} if { + is_object(subjects) + lines := _p_lines(subjects) + narrow := _p_narrow(subjects, lines) + descend := _p_descend(subjects, lines) + own := _p_own_reads(subjects) +} + +_p_parts(subjects) := _p_no_parts if not is_object(subjects) + +_p_members(name, narrow) := graph.reachable(narrow, [name]) + +_p_merged(name, own, narrow) := {a: own[_p_owner(a, name, own, narrow)][a] | some a in {a | some m in _p_members(name, narrow); some a, _ in object.get(own, m, {})}} + +_p_owner(a, name, own, narrow) := sort([m | some m in _p_members(name, narrow); a in object.keys(object.get(own, m, {}))])[0] + +_p_problems(subjects, own, narrow, descend) := {name: probs | + some name, def in subjects + is_object(def) + probs := _p_subject_problems(name, def, own, narrow, descend) + count(probs) > 0 +} + +_p_subject_problems(name, def, own, narrow, descend) := ({[["has"], "not an object"] | "has" in object.keys(def); not is_object(def.has)} | {[["has", alias], p] | some alias, read in object.get(own, name, {}); some p in read.problems}) | {[["has", alias], "alias given twice"] | + some alias, _ in object.get(own, name, {}) + _p_alias_taken(name, alias, def, own, narrow, descend) +} + +_p_alias_taken(name, alias, _, own, narrow, _) if { + some m in _p_members(name, narrow) + m != name + alias in object.keys(object.get(own, m, {})) +} + +_p_alias_taken(name, alias, _, _, _, descend) if descend[name] == alias + +_p_alias_taken(_, alias, def, _, _, _) if { + is_array(object.get(def, "from", null)) + count(def.from) > 0 + step := def.from[count(def.from) - 1] + is_object(step) + object.get(step, "each_as", null) == alias +} + +_s_subjects(subjects, reads, parts) := {name: _s_subject(name, def, reads, parts) | some name, def in subjects} if is_object(subjects) + +_s_subjects(subjects, _, _) := subjects if not is_object(subjects) + +_s_subject(name, null, _, _) := {"from": [name]} + +_s_subject(name, def, reads, parts) := object.union(_s_without_narrowed_from(_p_old_filters(def, _p_own_context(name, parts.effective)), reads[name]), object.union(_s_from_fields(name, def, reads), _s_id_field(reads[name]))) if is_object(def) _s_without_narrowed_from(def, read) := object.remove(def, ["from"]) if { "of" in object.keys(object.get(read, "fields", {})) _s_from_used(def, read) } else := def -_s_subject(_, def, _) := def if { +_s_subject(_, def, _, _) := def if { def != null not is_object(def) } @@ -6108,15 +6356,15 @@ _s_from_used(def, read) if { _s_from_read(name, fl, subjects, chains) := {"problems": problems, "fields": fields, "parent": parent} if { "path" in object.keys(fl) - conds := [_s_check([_s_cond_text(c)], {}, set()) | some c in object.get(fl, "where", [])] + conds := [_p_applied(_s_check([_s_cond_text(c)], {}, set()), _p_own_context(name, chains.parts)) | some c in object.get(fl, "where", [])] problems := sort(((((_s_chain_unsupported(name, fl, subjects, chains.descend) | _s_from_start_problems(fl)) | _s_from_paths_unsupported({"path": fl.path})) | _s_elapsed_unsupported({"where": object.get(fl, "where", [])})) | {concat("", ["whose: ", p]) | some c in conds; some p in object.get(c, "$sentence", [])})) - fields := object.union(_s_source(name, fl, subjects), _s_whose_filters(_s_filter_prefix(name, chains.narrow), conds)) + fields := object.union(_s_source(name, fl, subjects, chains.parts), _s_whose_filters(_s_filter_prefix(name, chains.narrow), conds)) parent := _s_descend_parent(name, fl, subjects) } else := {"problems": object.get(fl, "problems", ["doesn't parse"]), "fields": {}} -_s_source(name, fl, subjects) := {"of": fl.path.steps[0]} if { +_s_source(name, fl, subjects, eff) := {"of": fl.path.steps[0]} if { _s_narrows(name, fl, subjects) -} else := {"from": array.concat(_s_from_steps(array.slice(_s_model_path(fl.path), 1, count(fl.path.steps))), _s_each_step(fl))} if { +} else := {"from": array.concat(_s_from_steps(_p_descend_steps(array.slice(_s_model_path(fl.path), 1, count(fl.path.steps)), object.get(eff, fl.path.steps[0], {}))), _s_each_step(fl))} if { _s_descends(name, fl, subjects) } else := {"from": array.concat(_s_from_steps(_s_model_path(fl.path)), _s_each_step(fl))} @@ -6188,9 +6436,9 @@ _s_id_field(read) := {"identified_by": read.identified_by.path} if "path" in obj else := {} -_s_subject_sentences(subjects, reads) := {"chains": {name: p | some name, r in reads; _s_from_used(subjects[name], r); p := object.get(r, "parent", null); p != null}, "narrowed": {name | some name, r in reads; _s_from_used(subjects[name], r); "of" in object.keys(r.fields)}, "filters": {[name, n] | some name, r in reads; _s_from_used(subjects[name], r); some n, _ in object.get(r.fields, "applies_to", {})}, "problems": {name: probs | some name, def in subjects; probs := _s_subject_problems(name, def, reads); probs != {}}} if is_object(subjects) +_s_subject_sentences(subjects, reads, parts) := {"parts": parts.effective, "has_problems": parts.problems, "chains": {name: p | some name, r in reads; _s_from_used(subjects[name], r); p := object.get(r, "parent", null); p != null}, "narrowed": {name | some name, r in reads; _s_from_used(subjects[name], r); "of" in object.keys(r.fields)}, "filters": {[name, n] | some name, r in reads; _s_from_used(subjects[name], r); some n, _ in object.get(r.fields, "applies_to", {})}, "problems": {name: probs | some name, def in subjects; probs := _s_subject_problems(name, def, reads); probs != {}}} if is_object(subjects) -_s_subject_sentences(subjects, _) := {"chains": {}, "narrowed": set(), "filters": set(), "problems": {}} if not is_object(subjects) +_s_subject_sentences(subjects, _, _) := {"parts": {}, "has_problems": {}, "chains": {}, "narrowed": set(), "filters": set(), "problems": {}} if not is_object(subjects) _s_subject_problems(name, def, reads) := {f: ps | some f in ["from", "identified_by"]; ps := _s_field_problems(name, def, f, reads); ps != []} if is_object(def) @@ -6202,13 +6450,255 @@ _s_field_problems(name, def, "identified_by", reads) := object.get(reads[name].i _s_field_problems(_, def, f, _) := [] if not is_string(object.get(def, f, null)) -_s_requirements(reqs, chains) := {name: _s_requirement(req, chains) | some name, req in reqs} if is_object(reqs) +_p_no_parts := {"own": {}, "effective": {}, "problems": {}, "lines": {}, "narrow": {}, "descend": {}} + +_p_aliases(req, parts) := {alias | some alias, _ in object.get(parts.effective, req.subject, {})} if is_string(object.get(req, "subject", null)) + +_p_aliases(req, _) := set() if not is_string(object.get(req, "subject", null)) + +_p_named_by_problems(lines, ftext, aliases) := {concat("", ["named by ", ref.steps[0], " reads a part, which isn't supported yet"]) | + aliases != set() + some node in array.concat([n | some line in lines; is_string(line); some n in _s_nodes(_sentence(line))], [n | is_string(ftext); some n in _s_nodes(_for_line(ftext))]) + is_object(node) + ref := object.get(node, "named_by", {"start": ""}) + not "start" in object.keys(ref) + ref.steps[0] in aliases +} + +_p_named_by_problems(_, _, aliases) := set() if aliases == set() + +_p_for_clash(check, aliases) := {"alias given twice" | + is_string(check.for) + fl := _for_line(check.for) + some item in object.get(fl, "items", []) + item.as in aliases +} + +_p_for_clash(check, _) := set() if not is_string(object.get(check, "for", "")) + +_p_steps_of(parts) := {alias: read.from.path | some alias, read in parts; _p_valid(read)} + +_p_own_context(name, eff) := {"own": _p_steps_of(object.get(eff, name, {})), "parent": null, "pparts": {}} + +_p_context(req, chains, parts) := {"own": _p_steps_of(object.get(parts.effective, req.subject, {})), "parent": parent, "pparts": _p_steps_of(object.get(parts.effective, parent, {}))} if { + is_string(object.get(req, "subject", null)) + parent := chains[req.subject] +} else := {"own": _p_steps_of(object.get(parts.effective, req.subject, {})), "parent": null, "pparts": {}} if is_string(object.get(req, "subject", null)) + +else := {"own": {}, "parent": null, "pparts": {}} + +_p_idle(pc) if { + pc.own == {} + pc.pparts == {} +} + +_p_applied(m, pc) := m if _p_idle(pc) + +_p_applied(m, pc) := m if { + not _p_idle(pc) + "$sentence" in object.keys(m) +} + +_p_applied(m, pc) := _p_smodel(m, pc) if { + not _p_idle(pc) + not "$sentence" in object.keys(m) +} + +_p_applied_to(_, model, pc) := model if _p_idle(pc) + +_p_applied_to(written, model, pc) := _p_old_top(model, pc) if { + not _p_idle(pc) + not _s_written_as_sentence(written) +} + +_p_old_top(c, pc) := object.union(_p_old_a(c, pc), {"substitute": _p_old_a(c.substitute, pc) | is_object(object.get(c, "substitute", null))}) if is_object(c) + +_p_old_top(c, _) := c if not is_object(c) + +_p_applied_to(written, model, pc) := _p_applied(model, pc) if { + not _p_idle(pc) + _s_written_as_sentence(written) +} + +_p_old_filters(def, pc) := object.union(def, {"applies_to": {n: _p_old_top(f, pc) | some n, f in def.applies_to}}) if { + is_object(object.get(def, "applies_to", null)) + not _p_idle(pc) +} + +_p_old_filters(def, pc) := def if _p_idle(pc) + +_p_old_filters(def, pc) := def if { + not _p_idle(pc) + not is_object(object.get(def, "applies_to", null)) +} + +_p_descend_steps(rest, parts) := array.concat(_p_steps_of(parts)[rest[0]], array.slice(rest, 1, count(rest))) if { + count(rest) > 0 + is_string(rest[0]) + rest[0] in object.keys(_p_steps_of(parts)) +} else := rest + +_p_marker(alias, n, of, old) := concat("", ["${\"alias\":", json.marshal(alias), ",\"n\":", format_int(n, 10), ",\"of\":", json.marshal(of), ",\"old\":", json.marshal(old), "}"]) + +_p_item_mode(mode) := "o" if mode == "o" + +_p_item_mode(mode) := "i" if mode != "o" + +_p_path(path, pc, "s") := array.concat(array.concat([_p_marker(path[0], count(pc.own[path[0]]), null, false)], pc.own[path[0]]), array.slice(path, 1, count(path))) if { + is_array(path) + count(path) > 0 + is_string(path[0]) + not startswith(path[0], "$") + path[0] in object.keys(pc.own) +} else := _p_parent_path(path, pc) + +_p_path(path, pc, "i") := _p_parent_path(path, pc) + +_p_path(path, pc, "o") := array.concat(array.concat([_p_marker(alias, count(pc.own[alias]), null, true)], pc.own[alias]), array.slice(path, 1, count(path))) if { + is_array(path) + count(path) > 0 + is_string(path[0]) + startswith(path[0], "$") + not startswith(path[0], "$$") + alias := substring(path[0], 1, -1) + alias in object.keys(pc.own) +} else := path + +_p_parent_path(path, pc) := array.concat(array.concat([_p_marker(path[1], count(pc.pparts[path[1]]), pc.parent, false)], pc.pparts[path[1]]), array.slice(path, 2, count(path))) if { + pc.parent != null + is_array(path) + count(path) > 1 + path[0] == concat("", ["$", pc.parent]) + is_string(path[1]) + path[1] in object.keys(pc.pparts) +} else := path + +_p_smodel(m, pc) := object.union(m, {"checks": [_p_sleaf(c, pc) | some c in m.checks]}) if m.op == "all_of" + +_p_smodel(m, pc) := object.union(m, { + "checks": [_p_sleaf(c, pc) | some c in m.checks], + "items": [object.union(item, {"path": _p_path(item.path, pc, "s"), "whose": [_p_leaf(c, pc, "i") | some c in item.whose]}) | some item in m.items], +}) if m.op == "for" + +_p_smodel(m, pc) := _p_sleaf(m, pc) if not m.op in {"all_of", "for"} + +_p_sleaf(m, pc) := object.union(m, { + "path": _p_path(m.path, pc, "s"), + "whose": [_p_leaf(c, pc, "i") | some c in m.whose], + "check": _p_leaf(m.check, pc, "i"), +}) if m.op in {"every", "some"} + +_p_sleaf(m, pc) := _p_leaf(m, pc, "s") if not m.op in {"every", "some"} + +_p_leaf(m, pc, mode) := object.union(m, object.union({f: _p_side(m[f], pc, mode) | some f in ["path", "left", "right"]; f in object.keys(m)}, _p_extras(m, pc, mode))) + +_p_extras(m, pc, "o") := object.union({"each": _p_path(m.each, pc, "o") | is_array(object.get(m, "each", null))}, {"inputs": [_p_input(spec, pc) | some spec in m.inputs] | is_array(object.get(m, "inputs", null))}) + +_p_extras(_, _, "s") := {} + +_p_extras(_, _, "i") := {} + +_p_input(spec, pc) := _p_path(spec, pc, "o") if is_array(spec) -_s_requirements(reqs, _) := reqs if not is_object(reqs) +_p_input(spec, pc) := object.union(spec, {f: _p_path(spec[f], pc, "o") | some f in ["path", "each"]; is_array(object.get(spec, f, null))}) if is_object(spec) -_s_requirement(req, chains) := object.union(object.remove(req, ["checks"]), {"checks": {name: _s_parent_named(check, _s_written_check(check), _s_parents_of(req, chains)) | some name, check in req.checks}}) if { +_p_input(spec, _) := spec if { + not is_array(spec) + not is_object(spec) +} + +_p_side(side, pc, mode) := _p_path(side, pc, mode) if is_array(side) + +_p_side(side, pc, mode) := object.union(side, {"add": [_p_side_one(o, pc, mode) | some o in side.add]}) if { + is_object(side) + is_array(object.get(side, "add", null)) +} + +_p_side(side, pc, mode) := _p_side_one(side, pc, mode) if { + is_object(side) + not "add" in object.keys(side) +} + +_p_side(side, _, _) := side if { + not is_array(side) + not is_object(side) +} + +_p_side_one(side, pc, mode) := _p_path(side, pc, mode) if is_array(side) + +_p_side_one(side, pc, mode) := object.union(_p_tally(side, pc, mode), object.union({"whose": [_p_cond(c, pc, _p_item_mode(mode)) | some c in side.whose] | is_array(object.get(side, "whose", null))}, {"where": _p_cond(side.where, pc, _p_item_mode(mode)) | is_object(object.get(side, "where", null))})) if is_object(side) + +_p_side_one(side, _, _) := side if { + not is_array(side) + not is_object(side) +} + +_p_tally(side, pc, mode) := object.union(side, object.union({f: _p_path(side[f], pc, mode) | some f in ["count_of", "sum_of", "elapsed_from", "elapsed_to", "count", "sum"]; is_array(object.get(side, f, null))}, {"each": _p_path(side.each, pc, mode) | mode == "o"; is_array(object.get(side, "each", null))})) + +_p_cond(c, pc, mode) := object.union(c, object.union({f: _p_path(c[f], pc, mode) | some f in ["path", "left", "right"]; is_array(object.get(c, f, null))}, _p_extras(c, pc, mode))) if is_object(c) + +_p_cond(c, _, _) := c if not is_object(c) + +_p_old_a(c, pc) := object.union(_p_leaf(c, pc, "o"), object.union({"check": _p_old_b(c.check, pc) | is_object(object.get(c, "check", null))}, object.union({"options": _p_options_b(c.options, pc) | is_object(object.get(c, "options", null))}, {"options": _p_options_b(c.options, pc) | is_array(object.get(c, "options", null))}))) if is_object(c) + +_p_options_b(opts, pc) := {name: _p_group_b(group, pc) | some name, group in opts} if is_object(opts) + +_p_options_b(opts, pc) := [_p_group_b(group, pc) | some group in opts] if is_array(opts) + +_p_group_b(group, pc) := [_p_old_b(c, pc) | some c in group] if is_array(group) + +_p_group_b(group, _) := group if not is_array(group) + +_p_old_b(c, pc) := object.union(_p_leaf(c, pc, "o"), object.union({"check": _p_old_c(c.check, pc) | is_object(object.get(c, "check", null))}, object.union({"options": _p_options_c(c.options, pc) | is_object(object.get(c, "options", null))}, {"options": _p_options_c(c.options, pc) | is_array(object.get(c, "options", null))}))) if is_object(c) + +_p_old_b(c, _) := c if not is_object(c) + +_p_options_c(opts, pc) := {name: _p_group_c(group, pc) | some name, group in opts} if is_object(opts) + +_p_options_c(opts, pc) := [_p_group_c(group, pc) | some group in opts] if is_array(opts) + +_p_group_c(group, pc) := [_p_old_c(c, pc) | some c in group] if is_array(group) + +_p_group_c(group, _) := group if not is_array(group) + +_p_old_c(c, pc) := object.union(_p_leaf(c, pc, "o"), object.union({"check": _p_old_d(c.check, pc) | is_object(object.get(c, "check", null))}, object.union({"options": _p_options_d(c.options, pc) | is_object(object.get(c, "options", null))}, {"options": _p_options_d(c.options, pc) | is_array(object.get(c, "options", null))}))) if is_object(c) + +_p_old_c(c, _) := c if not is_object(c) + +_p_options_d(opts, pc) := {name: _p_group_d(group, pc) | some name, group in opts} if is_object(opts) + +_p_options_d(opts, pc) := [_p_group_d(group, pc) | some group in opts] if is_array(opts) + +_p_group_d(group, pc) := [_p_old_d(c, pc) | some c in group] if is_array(group) + +_p_group_d(group, _) := group if not is_array(group) + +_p_old_d(c, pc) := object.union(_p_leaf(c, pc, "o"), object.union({"check": _p_old_e(c.check, pc) | is_object(object.get(c, "check", null))}, object.union({"options": _p_options_e(c.options, pc) | is_object(object.get(c, "options", null))}, {"options": _p_options_e(c.options, pc) | is_array(object.get(c, "options", null))}))) if is_object(c) + +_p_old_d(c, _) := c if not is_object(c) + +_p_options_e(opts, pc) := {name: _p_group_e(group, pc) | some name, group in opts} if is_object(opts) + +_p_options_e(opts, pc) := [_p_group_e(group, pc) | some group in opts] if is_array(opts) + +_p_group_e(group, pc) := [_p_old_e(c, pc) | some c in group] if is_array(group) + +_p_group_e(group, _) := group if not is_array(group) + +_p_old_e(c, pc) := _p_leaf(c, pc, "o") if is_object(c) + +_p_old_e(c, _) := c if not is_object(c) + +_s_requirements(reqs, chains) := _p_requirements(reqs, chains, _p_no_parts) + +_p_requirements(reqs, chains, parts) := {name: _p_requirement(req, chains, parts) | some name, req in reqs} if is_object(reqs) + +_p_requirements(reqs, _, _) := reqs if not is_object(reqs) + +_p_requirement(req, chains, parts) := object.union(object.remove(req, ["checks"]), {"checks": {name: _p_applied_to(check, _s_parent_named(check, _p_written(check, aliases), _s_parents_of(req, chains)), _p_context(req, chains, parts)) | some name, check in req.checks}}) if { is_object(req) is_object(object.get(req, "checks", null)) + aliases := _p_aliases(req, parts) } else := req _s_parents_of(req, chains) := {chains[req.subject]} if { @@ -6249,14 +6739,16 @@ _s_named_model_leaf(m, names) := object.union(m, { _s_named_model_leaf(m, names) := _s_named(m, names) if not m.op in {"every", "some"} -_s_written_check(check) := _s_check([check], {}, set()) if is_string(check) +_s_written_check(check) := _p_written(check, set()) + +_p_written(check, aliases) := _s_check([check], {}, _p_named_by_problems([check], "", aliases)) if is_string(check) -_s_written_check(check) := _s_check(_s_lines(check.assert), object.filter(check, {"description", "meta"}), _s_form_problems(check)) if { +_p_written(check, aliases) := _s_check(_s_lines(check.assert), object.filter(check, {"description", "meta"}), _s_form_problems(check) | _p_named_by_problems(_s_lines(check.assert), "", aliases)) if { _s_assert_form(check) not "for" in object.keys(check) } -_s_written_check(check) := _s_check_for(_s_lines(check.assert), check.for, object.filter(check, {"description", "meta"}), _s_form_problems(check)) if { +_p_written(check, aliases) := _s_check_for(_s_lines(check.assert), check.for, object.filter(check, {"description", "meta"}), ((_s_form_problems(check) | _p_for_clash(check, aliases)) | _p_named_by_problems(_s_lines(check.assert), object.get(check, "for", ""), aliases))) if { _s_assert_form(check) "for" in object.keys(check) } @@ -6265,7 +6757,7 @@ _s_lines(a) := a if is_array(a) _s_lines(a) := [a] if not is_array(a) -_s_written_check(check) := check if not _s_written_as_sentence(check) +_p_written(check, _) := check if not _s_written_as_sentence(check) _s_written_as_sentence(check) if is_string(check) @@ -6671,7 +7163,7 @@ _unusable_mark := {"ergo/unusable": set()} _walk_row(check, subj) := object.union(_walk_read_row(check, subj), {"cause": "unusable", "failed": []}) if { _walk_flawed(check) } else := {"cause": "not_an_object", "inputs": [], "failed": []} if { - not _named(check.path) + not _off_subject(check.path) not is_object(subj) } else := _walk_read_row(check, subj) @@ -6892,9 +7384,28 @@ _walk_failed(cause, fails) := [{"path": _path_name(f.path), "item": _path_name(f _walk_inputs(check, w) := [{"name": _walk_name(check.path, w.walked), "value": [_walk_shown(e.v) | some e in w.items]}] -_walk_name(path, walked) := concat("", array.concat(array.concat(_walk_root_name(path), [concat("", [_walk_mark(i, walked), _step_name(i + count(_walk_root(path)), step)]) | some i, step in _walk_steps(path)]), [_walk_mark(count(_walk_steps(path)), walked)])) +_walk_name(path, walked) := concat("", array.concat(array.concat(_walk_root_name(path), [concat("", [_walk_mark(i + _walk_hidden(path), walked), _step_name(i + count(_walk_root_name(path)), step)]) | some i, step in array.slice(_walk_steps(path), _walk_hidden(path), count(_walk_steps(path)))]), [_walk_mark(count(_walk_steps(path)), walked)])) + +_walk_hidden(path) := _part_of(path).n if _part_start(path) + +_walk_hidden(path) := 0 if not _part_start(path) -_walk_root_name(path) := [_plain_step_name(0, path[0])] if _named(path) +_walk_root_name(path) := [_plain_step_name(0, path[0])] if { + _named(path) + not _part_start(path) +} + +_walk_root_name(path) := [_plain_step_name(0, root[0])] if { + _part_start(path) + root := _visible_root(_part_of(path)) + count(root) == 1 +} + +_walk_root_name(path) := [_plain_step_name(0, root[0]), _step_name(1, root[1])] if { + _part_start(path) + root := _visible_root(_part_of(path)) + count(root) == 2 +} _walk_root_name(path) := [] if not _named(path) @@ -7010,7 +7521,7 @@ _for_row(check, subj) := object.union(_for_outer(check, subj, _for_names), {"cau _walk_flawed(check) } else := {"cause": "not_an_object", "inputs": [], "failed": []} if { some item in check.items - not _named(item.path) + not _off_subject(item.path) not is_object(subj) } else := _for_outer(check, subj, _for_names) @@ -7203,7 +7714,7 @@ _for_list_name(path, walked) := substring(_walk_name(path, walked), 1, -1) if _s _for_list_name(path, walked) := _walk_name(path, walked) if not _starts_at_name(path) _starts_at_name(p) if { - _named(p) + _off_subject(p) not _builtin(p) } @@ -7223,7 +7734,7 @@ _has_name_step(p) if { } _for_named(p) if { - _named(p) + _off_subject(p) not _builtin(p) } @@ -7237,26 +7748,34 @@ as_sentences(policy) := object.union(object.remove(policy, ["subjects", "require as_sentences(policy) := policy if not is_object(policy) -_c_sections(policy) := {f: _c_section(f, policy[f], _c_chains(policy)) | some f in ["subjects", "requirements"]; f in object.keys(policy)} +_c_sections(policy) := {f: _c_section(f, policy[f], {"chains": _c_chains(policy), "aliases": _c_aliases(policy)}) | some f in ["subjects", "requirements"]; f in object.keys(policy)} -_c_chains(policy) := _s_subject_sentences(policy.subjects, _s_from_reads(policy.subjects)).chains if is_object(object.get(policy, "subjects", null)) +_c_aliases(policy) := {name: {a | some a, _ in parts} | some name, parts in _p_parts(policy.subjects).effective} if is_object(object.get(policy, "subjects", null)) + +_c_aliases(policy) := {} if not is_object(object.get(policy, "subjects", null)) + +_c_chains(policy) := _s_subject_sentences(policy.subjects, _s_from_reads(policy.subjects, _p_parts(policy.subjects)), _p_no_parts).chains if is_object(object.get(policy, "subjects", null)) _c_chains(policy) := {} if not is_object(object.get(policy, "subjects", null)) _c_section("subjects", section, _) := _c_subjects(section) if is_object(section) -_c_section("requirements", section, chains) := _c_requirements(section, chains) if is_object(section) +_c_section("requirements", section, ctx) := _c_requirements(section, ctx) if is_object(section) _c_section(_, section, _) := section if not is_object(section) -_c_requirements(reqs, chains) := {name: _c_requirement(req, chains) | some name, req in reqs} +_c_requirements(reqs, ctx) := {name: _c_requirement(req, ctx) | some name, req in reqs} -_c_requirement(req, chains) := object.union(object.remove(req, ["checks"]), {"checks": {name: _c_check(check) | some name, check in req.checks}}) if { +_c_requirement(req, ctx) := object.union(object.remove(req, ["checks"]), {"checks": {name: _c_check_of(check, object.get(ctx.aliases, object.get(req, "subject", null), set())) | some name, check in req.checks}}) if { is_object(req) is_object(object.get(req, "checks", null)) - not object.get(req, "subject", null) in object.keys(chains) + not object.get(req, "subject", null) in object.keys(ctx.chains) } else := req +_c_check_of(check, aliases) := check if { + aliases & {x | walk(check, [_, x]); is_string(x)} != set() +} else := _c_check(check) + _c_check(check) := _c_written(check, text) if { text := _c_sentence(_c_bare(check)) } else := check diff --git a/ergo_test.rego b/ergo_test.rego index c198a3c..76d8e74 100644 --- a/ergo_test.rego +++ b/ergo_test.rego @@ -10434,3 +10434,855 @@ test_a_filter_with_a_field_its_sentence_would_lose_is_shown_by_its_name if { } _listed({"d": {"from": ["ds"], "applies_to": filters}}, "d").d.from == "ds whose and and and and and and x exists" } + +_parts_policy(has, checks) := { + "subjects": {"round": {"from": "trail", "identified_by": "name", "has": has}}, + "requirements": {"r": {"subject": "round", "checks": checks}}, +} + +_parts_doc(d) := {"trail": {"name": "t1", "dd": d}} + +_parts_report(has, checks, d) := ergo.report(_parts_doc(d), _parts_policy(has, checks), {}) + +_parts_rows(rep, check) := [r | some r in rep.results; r.check == check] + +_parts_row(rep, check) := _parts_rows(rep, check)[0] + +_parts_problems(rep) := {i.name: i.value | some r in rep.results; r.check == "$well_formed"; some i in r.inputs} + +test_a_part_alias_stands_for_its_path_inside_the_subject if { + rep := _parts_report({"cov": "dd.b"}, {"c": "cov.x is 1"}, {"b": {"x": 1}}) + row := _parts_row(rep, "c") + row.passed + row.cause == "satisfied" + row.inputs == [{"name": "cov.x", "value": 1}] + rep.requirements.r.checks.c.expression == "cov.x is 1" +} + +test_a_part_can_be_a_path_to_one_value_and_a_check_can_read_the_alias_alone if { + rep := _parts_report({"ratio": "dd.b.ratio"}, {"c": "ratio is at least 2"}, {"b": {"ratio": 3}}) + row := _parts_row(rep, "c") + row.passed + row.inputs == [{"name": "ratio", "value": 3}] +} + +test_a_part_in_the_long_form_works_like_one_written_as_a_line if { + line := _parts_report({"cov": "dd.b"}, {"c": "cov.x is 1"}, {"b": {"x": 2}}) + long := _parts_report({"cov": {"from": "dd.b", "identified_by": "id"}}, {"c": "cov.x is 1"}, {"b": {"x": 2}}) + _parts_row(line, "c") == _parts_row(long, "c") + _parts_row(line, "c").cause == "value" +} + +test_a_missing_part_fails_as_absent_like_its_full_path if { + rep := _parts_report({"cov": "dd.b"}, {"c": "cov.x is 1", "f": "dd.b.x is 1"}, {}) + _parts_row(rep, "c").cause == "absent" + _parts_row(rep, "f").cause == "absent" + rep2 := _parts_report({"cov": "dd.b"}, {"c": "cov exists", "f": "dd.b exists"}, {}) + _parts_row(rep2, "c").cause == "value" + _parts_row(rep2, "f").cause == "value" +} + +test_a_null_part_fails_as_null_when_the_alias_stands_alone_and_absent_when_a_step_follows if { + rep := _parts_report({"cov": "dd.b"}, {"c": "cov.x is 1", "a": "cov is null", "f": "dd.b.x is 1"}, {"b": null}) + _parts_row(rep, "c").cause == "absent" + _parts_row(rep, "f").cause == "absent" + _parts_row(rep, "a").passed +} + +test_a_wrong_typed_part_fails_as_unusable_like_its_full_path if { + rep := _parts_report({"cov": "dd.b"}, {"c": "cov.x is 1", "f": "dd.b.x is 1"}, {"b": "text"}) + _parts_row(rep, "c").cause == "unusable" + _parts_row(rep, "f").cause == "unusable" + rep2 := _parts_report({"cov": "dd.b"}, {"c": "cov.x is 1", "f": "dd.b.x is 1"}, "text") + _parts_row(rep2, "c").cause == "unusable" + _parts_row(rep2, "f").cause == "unusable" +} + +test_a_part_of_a_subject_that_is_not_an_object_fails_as_not_an_object_like_its_full_path if { + policy := { + "subjects": {"s": {"from": "items", "has": {"cov": "dd.b"}}}, + "requirements": {"r": {"subject": "s", "checks": {"c": "cov.x is 1", "f": "dd.b.x is 1"}}}, + } + rep := ergo.report({"items": ["text", null, 5]}, policy, {}) + causes := [r.cause | some r in rep.results; r.check == "c"] + causes == ["not_an_object", "not_an_object", "not_an_object"] + [r.cause | some r in rep.results; r.check == "f"] == causes +} + +test_a_part_keeps_its_ref_step_so_an_unreadable_param_fails_it_like_its_full_path if { + has := {"by": "dd.m named by $params.k"} + alias := ergo.report(_parts_doc({"m": {"q": {"x": 1}}}), _parts_policy(has, {"c": "by.x is 1"}), {"k": 1}) + full := ergo.report(_parts_doc({"m": {"q": {"x": 1}}}), _parts_policy({}, {"c": "dd.m named by $params.k.x is 1"}), {"k": 1}) + _parts_row(alias, "c").cause == _parts_row(full, "c").cause + _parts_row(alias, "c").cause == "unusable" + alias.requirements.r.checks.c["$refs"] == full.requirements.r.checks.c["$refs"] + good := ergo.report(_parts_doc({"m": {"q": {"x": 1}}}), _parts_policy(has, {"c": "by.x is 1"}), {"k": "q"}) + _parts_row(good, "c").passed +} + +test_the_names_in_a_row_keep_the_alias_and_the_values_are_the_full_paths if { + has := {"lst": "dd.items", "pos": "dd.items[1]", "cov": "dd.b"} + checks := { + "every": "every lst.x is 1", + "count": "count of lst is 2", + "pos": "pos.x is 1", + "alone": "pos exists", + "two": "cov.x is at least cov.n", + } + d := {"items": [{"x": 1}, {"x": 2}], "b": {"x": 1, "n": 2}} + rep := _parts_report(has, checks, d) + _parts_row(rep, "every").inputs == [{"name": "lst[].x", "value": [1, 2]}] + _parts_row(rep, "every").failed_items == [{"cause": "value", "item": "item 2 of lst", "path": "lst[1].x", "value": 2}] + _parts_row(rep, "count").inputs == [{"name": "count of lst", "value": 2}, {"name": "lst[]", "value": [{"x": 1}, {"x": 2}]}] + _parts_row(rep, "pos").inputs == [{"name": "pos.x", "value": 2}] + _parts_row(rep, "alone").inputs == [{"name": "pos", "value": {"x": 2}}] + _parts_row(rep, "two").inputs == [{"name": "cov.x", "value": 1}, {"name": "cov.n", "value": 2}] + rep.requirements.r.checks.every.expression == "every lst.x is 1" + rep.requirements.r.checks.pos.expression == "pos.x is 1" +} + +test_an_alias_shadows_a_field_of_the_subject_with_the_same_name if { + policy := _parts_policy({"cov": "dd.b"}, {"c": "cov.x is 1"}) + rep := ergo.report({"trail": {"name": "t1", "cov": {"x": 2}, "dd": {"b": {"x": 1}}}}, policy, {}) + _parts_row(rep, "c").passed +} + +test_the_dot_in_front_of_a_field_does_not_get_around_an_alias if { + policy := _parts_policy({"cov": "dd.b"}, {"c": "cov.x is 1", "d": ".cov.x is 1", "e": "dd.b.x is 1 and .cov.x is 1"}) + rep := ergo.report({"trail": {"name": "t1", "cov": {"x": 2}, "dd": {"b": {"x": 1}}}}, policy, {}) + _parts_row(rep, "c").passed + _parts_row(rep, "d").passed + _parts_row(rep, "d").inputs == [{"name": "cov.x", "value": 1}] + _parts_row(rep, "e").passed +} + +test_a_field_of_the_subject_with_no_alias_in_the_way_is_read_as_before if { + rep := _parts_report({"cov": "dd.b"}, {"c": "name is \"t1\""}, {}) + _parts_row(rep, "c").passed +} + +test_an_alias_does_not_apply_to_the_whose_or_the_argument_of_a_one_line_every if { + has := {"cov": "dd.b"} + d := {"b": {"x": 1}, "items": [{"cov": 5, "x": 5}, {"cov": 7, "x": 6}]} + rep := _parts_report(has, {"w": "every dd.items.x whose cov is at least 5 is at least 5", "a": "every dd.items.x equals cov"}, d) + _parts_row(rep, "w").passed + _parts_row(rep, "a").cause == "value" + _parts_row(rep, "a").failed_items == [{"cause": "value", "item": "item 2 of dd.items", "path": "dd.items[1].x", "value": 6}] +} + +test_an_alias_works_on_the_subject_in_a_for_line_and_not_on_its_items if { + has := {"cov": "dd.b", "lst": "dd.items"} + checks := { + "c": {"for": "every lst as it", "assert": ["it.x is cov.x", "cov.s exists"]}, + "d": {"for": "some lst whose y is 2 as it", "assert": "it.x is 1"}, + "e": {"for": "every lst as it, every it.sub as s", "assert": "s is cov.x"}, + } + d := {"b": {"x": 1, "s": "a"}, "items": [{"x": 1, "y": 2, "sub": [1]}, {"x": 1, "y": 3, "sub": [1, 1]}]} + rep := _parts_report(has, checks, d) + every c in ["c", "d", "e"] { + _parts_row(rep, c).passed + } + rep.requirements.r.checks.c.expression == "for every lst as it: it.x equals cov.x and cov.s exists" + _parts_row(rep, "c").inputs == [{"name": "cov.s", "value": "a"}, {"name": "cov.x", "value": 1}, {"name": "lst[]", "value": [{"sub": [1], "x": 1, "y": 2}, {"sub": [1, 1], "x": 1, "y": 3}]}] +} + +test_a_for_line_alias_that_is_also_a_part_alias_is_written_wrong if { + has := {"cov": "dd.b"} + rep := _parts_report(has, {"c": {"for": "every dd.items as cov", "assert": "cov.x is 1"}}, {"items": [{"x": 1}]}) + _parts_problems(rep)["checks.c"] == ["alias given twice"] + _parts_row(rep, "c").cause == "ill_formed" +} + +test_a_whose_in_the_from_line_reads_the_subjects_parts if { + policy := { + "subjects": {"round": {"from": "rounds whose cov.x is 1", "identified_by": "id", "has": {"cov": "dd.b"}}}, + "requirements": {"r": {"subject": "round", "checks": {"c": "cov.n is 2"}}}, + } + rep := ergo.report({"rounds": [{"id": "a", "dd": {"b": {"x": 1, "n": 2}}}, {"id": "b", "dd": {"b": {"x": 2, "n": 2}}}, {"id": "c", "dd": {}}]}, policy, {}) + [[r.subject.id, r.cause, r.inputs] | some r in rep.results; r.check == "$applies"] == [ + ["a", "satisfied", [{"name": "cov.x", "value": 1}]], + ["b", "value", [{"name": "cov.x", "value": 2}]], + ["c", "absent", [{"name": "cov.x", "value": null}]], + ] + rep.requirements.r.checks["$applies"].expression == "cov.x is 1" + [r.subject.id | some r in rep.results; r.check == "c"] == ["a"] +} + +_parts_narrow := { + "subjects": { + "round": {"from": "rounds", "identified_by": "id", "has": {"cov": "dd.b"}}, + "good round": {"from": "round whose cov.x is 1", "has": {"extra": "dd.e"}}, + "old round": {"of": "round", "applies_to": {"f": {"op": "equals", "path": ["$cov", "x"], "value": 1}}, "has": {"extra": "dd.e"}}, + }, + "requirements": { + "r": {"subject": "good round", "checks": {"c": "cov.n is 2 and extra is 5"}}, + "o": {"subject": "old round", "checks": {"c": "cov.n is 2 and extra is 5", "d": {"op": "equals", "path": ["$extra"], "value": 5}}}, + }, +} + +_parts_rounds := {"rounds": [{"id": "a", "dd": {"b": {"x": 1, "n": 2}, "e": 5}}, {"id": "b", "dd": {"b": {"x": 2, "n": 2}, "e": 5}}]} + +test_a_subject_that_narrows_another_keeps_its_parts_and_can_add_its_own if { + rep := ergo.report(_parts_rounds, _parts_narrow, {}) + rows := [[r.requirement, r.subject.id, r.check, r.passed] | some r in rep.results; r.check in {"c", "d"}] + rows == [["o", "a", "c", true], ["o", "a", "d", true], ["r", "a", "c", true]] + rep.subjects["good round"].parts == { + "cov": {"from": "dd.b", "identified_by": null}, + "extra": {"from": "dd.e", "identified_by": null}, + } + rep.subjects.round.parts == {"cov": {"from": "dd.b", "identified_by": null}} +} + +test_re_adding_a_kept_alias_is_written_wrong_and_names_the_part if { + subjects := object.union(_parts_narrow.subjects, {"good round": {"from": "round whose cov.x is 1", "has": {"cov": "dd.q"}}}) + rep := ergo.report(_parts_rounds, {"subjects": subjects, "requirements": {"r": {"subject": "good round", "checks": {"c": "cov.n is 2"}}}}, {}) + _parts_problems(rep)["subjects.\"good round\".has.cov"] == ["alias given twice"] + rep2 := ergo.report(_parts_rounds, {"subjects": object.union(subjects, {"good round": {"of": "round", "has": {"cov": "dd.q"}}}), "requirements": {"r": {"subject": "good round", "checks": {"c": "cov.n is 2"}}}}, {}) + _parts_problems(rep2)["subjects.\"good round\".has.cov"] == ["alias given twice"] +} + +test_two_subjects_that_do_not_narrow_each_other_can_use_the_same_alias if { + subjects := { + "a": {"from": "xs", "has": {"cov": "p"}}, + "b": {"from": "ys", "has": {"cov": "q"}}, + } + policy := {"subjects": subjects, "requirements": {"ra": {"subject": "a", "checks": {"c": "cov is 1"}}, "rb": {"subject": "b", "checks": {"c": "cov is 2"}}}} + rep := ergo.report({"xs": [{"p": 1, "q": 2}], "ys": [{"p": 1, "q": 2}]}, policy, {}) + [r.passed | some r in rep.results; r.check == "c"] == [true, true] +} + +_parts_chain := { + "subjects": { + "artifact": {"from": "trail.artifacts named by $params.art", "identified_by": "fingerprint", "has": {"pra": "attestations.pr"}}, + "pull request": {"from": "artifact.pra.pull_requests", "identified_by": "url", "has": {"rev": "reviews"}}, + }, + "requirements": {"r": {"subject": "pull request", "checks": { + "parent": "artifact.pra.name is \"w\"", + "own": "rev.count is 2", + "field": "pra is 1", + "every": "every artifact.pra.pull_requests.url is not empty", + }}}, +} + +_parts_chain_doc := {"trail": {"artifacts": {"web": {"fingerprint": "sha-1", "pra": 1, "attestations": {"pr": {"name": "w", "pull_requests": [ + {"url": "pr/7", "reviews": {"count": 2}}, + {"url": "pr/8", "reviews": {"count": 1}}, +]}}}}}} + +test_a_chained_subject_reads_its_parents_parts_and_starts_from_one if { + rep := ergo.report(_parts_chain_doc, _parts_chain, {"art": "web"}) + [[r.subject.id, r.check, r.passed, r.cause] | some r in rep.results; r.check in {"parent", "own", "field", "every"}] == [ + ["pr/7", "every", true, "satisfied"], ["pr/7", "field", false, "absent"], ["pr/7", "own", true, "satisfied"], ["pr/7", "parent", true, "satisfied"], + ["pr/8", "every", true, "satisfied"], ["pr/8", "field", false, "absent"], ["pr/8", "own", false, "value"], ["pr/8", "parent", true, "satisfied"], + ] + _parts_row(rep, "parent").inputs == [{"name": "artifact.pra.name", "value": "w"}] + _parts_row(rep, "every").inputs == [{"name": "artifact.pra.pull_requests[].url", "value": ["pr/7", "pr/8"]}] + rep.subjects["pull request"].from == "artifact.pra.pull_requests" +} + +test_a_part_is_only_reached_through_its_subject_so_a_bare_alias_in_a_chained_child_is_a_field if { + rep := ergo.report(_parts_chain_doc, _parts_chain, {"art": "web"}) + _parts_row(rep, "field").inputs == [{"name": "pra", "value": null}] + _parts_row(rep, "field").cause == "absent" +} + +test_a_chained_subject_cannot_use_its_parents_name_as_a_part_alias if { + subjects := object.union(_parts_chain.subjects, {"pull request": {"from": "artifact.pra.pull_requests", "identified_by": "url", "has": {"artifact": "reviews"}}}) + rep := ergo.report(_parts_chain_doc, {"subjects": subjects, "requirements": _parts_chain.requirements}, {"art": "web"}) + _parts_problems(rep)["subjects.\"pull request\".has.artifact"] == ["alias given twice"] +} + +test_a_problem_in_a_parent_subjects_has_fails_the_requirements_of_its_chained_child if { + subjects := object.union(_parts_chain.subjects, {"artifact": {"from": "trail.artifacts named by $params.art", "identified_by": "fingerprint", "has": {"pra": 5}}}) + rep := ergo.report(_parts_chain_doc, {"subjects": subjects, "requirements": _parts_chain.requirements}, {"art": "web"}) + _parts_problems(rep)["subjects.artifact.has.pra"] == ["not a string or an object"] +} + +test_a_chained_child_cannot_read_a_part_the_parent_does_not_have if { + policy := object.union(_parts_chain, {"requirements": {"r": {"subject": "pull request", "checks": {"c": "artifact.nope.name is \"w\""}}}}) + rep := ergo.report(_parts_chain_doc, policy, {"art": "web"}) + _parts_row(rep, "c").cause == "absent" +} + +test_an_old_syntax_check_reads_a_part_as_a_dollar_alias_and_the_name_keeps_the_dollar if { + checks := { + "eq": {"op": "equals", "path": ["$cov", "x"], "value": 1}, + "pr": {"op": "present", "path": ["$cov"]}, + "ms": {"op": "missing", "path": ["$cov", "nope"]}, + "cmp": {"op": "compare", "left": ["$cov", "x"], "right": ["$cov", "n"], "cmp": "lt"}, + } + rep := _parts_report({"cov": "dd.b"}, checks, {"b": {"x": 1, "n": 2}}) + every c in ["eq", "pr", "ms", "cmp"] { + _parts_row(rep, c).passed + } + _parts_row(rep, "eq").inputs == [{"name": "$cov.x", "value": 1}] + rep.requirements.r.checks.eq.expression == "$cov.x == 1" +} + +test_an_old_syntax_check_reads_a_part_inside_all_any_any_of_and_a_substitute if { + has := {"cov": "dd.b", "lst": "dd.items"} + checks := { + "all": {"op": "all", "path": ["$lst"], "check": {"op": "equals", "path": ["x"], "value": 1}}, + "any": {"op": "any", "path": ["$cov", "items"], "as": "i", "check": {"op": "equals", "path": ["$i"], "value": 2}}, + "anyof": {"op": "any_of", "options": {"a": [{"op": "equals", "path": ["$cov", "x"], "value": 9}], "b": [{"op": "equals", "path": ["$cov", "x"], "value": 1}]}}, + "sub": {"op": "equals", "path": ["$cov", "x"], "value": 9, "substitute": {"op": "equals", "path": ["$cov", "n"], "value": 2}}, + "count": {"op": "compare", "left": {"count": ["$lst"]}, "right": {"literal": 2}, "cmp": "eq"}, + "sum": {"op": "compare", "left": {"sum": ["$lst"], "each": ["n"]}, "right": ["$cov", "n"], "cmp": "gt"}, + "add": {"op": "compare", "left": {"add": [["$cov", "x"], {"literal": 1}]}, "right": ["$cov", "n"], "cmp": "eq"}, + } + d := {"b": {"x": 1, "n": 2, "items": [1, 2]}, "items": [{"x": 1, "n": 2}, {"x": 1, "n": 3}]} + rep := _parts_report(has, checks, d) + every c in ["all", "any", "anyof", "sub", "count", "sum", "add"] { + _parts_row(rep, c).passed + } + _parts_row(rep, "sub").cause == "substituted" + _parts_row(rep, "all").inputs == [{"name": "$lst[].x", "value": [1, 1]}] +} + +test_an_old_syntax_custom_operator_reads_a_part_through_its_path_and_inputs if { + checks := {"c": {"op": "even", "path": ["$cov", "n"], "inputs": [["$cov", "n"]], "expression": "n is even"}} + rep := _parts_report({"cov": "dd.b"}, checks, {"b": {"n": 4}}) + _parts_row(rep, "c").passed + _parts_row(rep, "c").inputs == [{"name": "$cov.n", "value": 4}] +} + +test_an_old_syntax_alias_that_is_not_a_part_is_still_unknown if { + rep := _parts_report({"cov": "dd.b"}, {"c": {"op": "equals", "path": ["$other", "x"], "value": 1}}, {"b": {"x": 1}}) + _parts_problems(rep)["checks.c"] == ["unknown alias $other"] +} + +test_a_dollar_alias_in_a_check_of_a_subject_without_that_part_is_unknown if { + policy := { + "subjects": {"a": {"from": "xs", "has": {"cov": "p"}}, "b": {"from": "ys"}}, + "requirements": {"r": {"subject": "b", "checks": {"c": {"op": "present", "path": ["$cov"]}}}}, + } + rep := ergo.report({"xs": [], "ys": [{"p": 1}]}, policy, {}) + _parts_problems(rep)["checks.c"] == ["unknown alias $cov"] +} + +test_an_old_syntax_alias_given_by_as_or_each_as_cannot_also_be_a_part if { + checks := {"c": {"op": "all", "path": ["dd", "items"], "as": "cov", "check": {"op": "present", "path": ["x"]}}} + rep := _parts_report({"cov": "dd.b"}, checks, {"items": [{"x": 1}]}) + _parts_problems(rep)["checks.c"] == ["alias given twice"] + policy := { + "subjects": {"s": {"from": ["items", {"each_as": "cov", "keys": ["a"]}], "has": {"cov": "p"}}}, + "requirements": {"r": {"subject": "s", "checks": {"c": {"op": "present", "path": ["p"]}}}}, + } + _parts_problems(ergo.report({"items": {"a": {"p": 1}}}, policy, {}))["subjects.s.has.cov"] == ["alias given twice"] +} + +test_a_path_shaped_like_an_internal_part_marker_does_not_read_the_subject if { + forged := "${\"alias\":\"zzz\",\"n\":0,\"of\":null,\"old\":false}" + rep := _parts_report({"cov": "dd.b"}, {"c": {"op": "present", "path": [forged, "name"]}}, {"b": {"x": 1}}) + _parts_row(rep, "c").cause == "ill_formed" + _parts_row(rep, "c").passed == false +} + +test_a_subject_with_no_has_is_listed_with_no_parts if { + rep := ergo.report({"trail": {"name": "t1"}}, _parts_policy({}, {"c": "name is \"t1\""}), {}) + rep.subjects.round.parts == {} + _parts_row(rep, "c").passed +} + +test_the_report_lists_every_part_with_its_path_as_written_canonically_and_its_id if { + has := { + "a": " dd.b ", + "b": {"from": "dd.\"x y\".z", "identified_by": "attestation_id"}, + "c": {"from": "dd.m named by $params.k"}, + "d": "dd.items[1].y", + "e": "item 3 of dd.items", + "f": "dd.m[$params.k]", + } + rep := _parts_report(has, {"c": "name is \"t1\""}, {}) + rep.subjects.round.parts == { + "a": {"from": "dd.b", "identified_by": null}, + "b": {"from": "dd.\"x y\".z", "identified_by": "attestation_id"}, + "c": {"from": "dd.m named by $params.k", "identified_by": null}, + "d": {"from": "dd.items[1].y", "identified_by": null}, + "e": {"from": "item 3 of dd.items", "identified_by": null}, + "f": {"from": "dd.m named by $params.k", "identified_by": null}, + } +} + +test_the_report_shows_a_part_written_wrong_as_best_it_can if { + has := { + "bad_from": {"from": "dd.b whose x is 1", "identified_by": "id"}, + "bad_id": {"from": "dd.b", "identified_by": 5}, + "no_from": {"identified_by": "id"}, + "number": 5, + "bad alias": "dd.b", + } + rep := _parts_report(has, {"c": "name is \"t1\""}, {}) + rep.subjects.round.parts == { + "bad alias": {"from": "dd.b", "identified_by": null}, + "bad_from": {"from": "", "identified_by": "id"}, + "bad_id": {"from": "dd.b", "identified_by": ""}, + "no_from": {"from": "", "identified_by": "id"}, + "number": {"from": "", "identified_by": null}, + } +} + +test_a_has_that_is_not_an_object_is_written_wrong_and_lists_no_parts if { + rep := _parts_report("cov", {"c": "name is \"t1\""}, {}) + _parts_problems(rep)["subjects.round.has"] == ["not an object"] + rep.subjects.round.parts == {} +} + +test_every_way_a_part_can_be_written_wrong_is_named_with_its_alias if { + has := { + "num": 5, + "nofrom": {"identified_by": "id"}, + "extra": {"from": "x", "other": 1}, + "fromnum": {"from": 1}, + "idnum": {"from": "x", "identified_by": 1}, + "who": "x whose y is 1", + "eacha": "x named by each of $params.k", + "cnt": "count of x", + "prm": "$params.x", + "inp": "$input.x", + "nobare": "x named by foo", + "emp": "", + "idbad": {"from": "x", "identified_by": "y whose z is 1"}, + "$dollar": "x", + "with space": "x", + "plain": "x", + "item": "x", + } + problems := _parts_problems(_parts_report(has, {"c": "name is \"t1\""}, {})) + problems["subjects.round.has.num"] == ["not a string or an object"] + problems["subjects.round.has.nofrom"] == ["missing from"] + problems["subjects.round.has.extra"] == ["unknown field other"] + problems["subjects.round.has.fromnum"] == ["invalid from"] + problems["subjects.round.has.idnum"] == ["invalid identified_by"] + problems["subjects.round.has.who"] == ["from: a part is a path: whose and named by each of can't go in it"] + problems["subjects.round.has.eacha"] == ["from: a part is a path: whose and named by each of can't go in it"] + problems["subjects.round.has.cnt"] == ["from: doesn't parse"] + problems["subjects.round.has.prm"] == ["from: a part reads inside its subject, so it can't start at $params or $input"] + problems["subjects.round.has.inp"] == ["from: a part reads inside its subject, so it can't start at $params or $input"] + problems["subjects.round.has.nobare"] == ["from: unknown alias foo"] + problems["subjects.round.has.emp"] == ["from: doesn't parse"] + problems["subjects.round.has.idbad"] == ["identified_by: a part is a path: whose and named by each of can't go in it"] + problems["subjects.round.has.$dollar"] == ["invalid alias"] + problems["subjects.round.has.\"with space\""] == ["invalid alias"] + problems["subjects.round.has.item"] == ["invalid alias"] + not "subjects.round.has.plain" in object.keys(problems) +} + +test_a_part_alias_must_be_a_bare_word_that_is_not_a_word_of_the_grammar_or_params_or_input if { + bad := ["empty", "every", "params", "input", "true", "a", "x y", "$x", "", "1x", "x.y", "é"] + every alias in bad { + problems := _parts_problems(_parts_report({alias: "x"}, {"c": "name is \"t1\""}, {})) + some key, value in problems + contains(key, "subjects.round.has") + value == ["invalid alias"] + } + good := ["x", "_x", "x-y", "x_1", "Cov", "coverage-verification"] + every alias in good { + problems := _parts_problems(_parts_report({alias: "x"}, {"c": "name is \"t1\""}, {})) + count([k | some k, _ in problems; contains(k, "subjects.round.has")]) == 0 + } +} + +test_a_named_by_that_names_a_part_alias_is_written_wrong_because_it_would_read_the_subjects_field if { + has := {"cov": "dd.k"} + checks := { + "a": "$params.m named by cov is 1", + "b": "every dd.items.z whose $params.m named by cov is 1 is 2", + "c": {"for": "every dd.items as it", "assert": "$params.m named by it is 1"}, + } + problems := _parts_problems(_parts_report(has, checks, {"k": "q"})) + problems["checks.a"] == ["named by cov reads a part, which isn't supported yet"] + problems["checks.b"] == ["named by cov reads a part, which isn't supported yet"] + not "checks.c" in object.keys(problems) +} + +test_as_sentences_leaves_an_old_check_that_names_an_alias_as_written_because_a_sentence_would_read_the_part if { + policy := { + "subjects": {"s": {"from": ["items"], "has": {"cov": "dd.b"}}}, + "requirements": {"r": {"subject": "s", "checks": { + "a": {"op": "equals", "path": ["cov", "x"], "value": 1}, + "b": {"op": "equals", "path": ["other", "x"], "value": 1}, + "c": {"op": "equals", "path": ["$cov", "x"], "value": 1}, + }}}, + } + converted := ergo.as_sentences(policy) + converted.requirements.r.checks.a == {"op": "equals", "path": ["cov", "x"], "value": 1} + converted.requirements.r.checks.b == "other.x is 1" + converted.requirements.r.checks.c == {"op": "equals", "path": ["$cov", "x"], "value": 1} + converted.subjects.s.has == {"cov": "dd.b"} +} + +test_a_problem_in_has_names_the_subject_that_holds_it_for_every_requirement_that_builds_on_it if { + subjects := { + "round": {"from": "rounds", "has": {"cov": 5}}, + "good": {"from": "round whose x is 1"}, + } + rep := ergo.report({"rounds": [{"x": 1}]}, {"subjects": subjects, "requirements": {"r": {"subject": "good", "checks": {"c": "x is 1"}}}}, {}) + _parts_problems(rep)["subjects.round.has.cov"] == ["not a string or an object"] +} + +test_a_part_with_no_alias_clash_works_when_another_subject_of_the_policy_is_written_wrong if { + subjects := { + "round": {"from": "rounds", "has": {"cov": "dd.b"}}, + "bad": {"from": "rounds", "has": 5}, + } + policy := {"subjects": subjects, "requirements": {"r": {"subject": "round", "checks": {"c": "cov.x is 1"}}}} + rep := ergo.report({"rounds": [{"dd": {"b": {"x": 1}}}]}, policy, {}) + _parts_row(rep, "c").passed + not "subjects.bad.has" in object.keys(_parts_problems(rep)) +} + +_swap_has := { + "cov": "dd.b", + "lst": "dd.items", + "uno": "dd.b.x", + "by": "dd.m named by $params.k", + "pos": "dd.items[1]", +} + +_swap_full := { + "@cov": "dd.b", + "@lst": "dd.items", + "@uno": "dd.b.x", + "@by": "dd.m named by $params.k", + "@pos": "dd.items[1]", +} + +_swap_alias := { + "@cov": "cov", + "@lst": "lst", + "@uno": "uno", + "@by": "by", + "@pos": "pos", +} + +_swap_lines := [ + "@cov.x is 1", + "@cov.x is not 1", + "@cov.x exists", + "@cov.x does not exist", + "@cov.s is a non-empty string", + "@cov.x is one of 1, 2", + "@cov.x is not one of 1, 2", + "@cov.s matches \"^a\"", + "@cov.s does not match \"^b\"", + "@cov.s starts with \"a\"", + "@cov.x is at least 2", + "@cov.x is between 1 and 3", + "@cov.x equals @cov.n", + "@cov.x does not equal @cov.n", + "@cov.t is before @cov.t2", + "@cov.items is empty", + "@cov.items is not empty", + "@cov exists", + "@cov.x is a number", + "@cov.x is 1 and @cov.n is 2", + "@uno is 1", + "@uno exists", + "@by.x is 1", + "@by exists", + "@pos.x is 1", + "@pos exists", +] + +_swap_walks := [ + "every @lst.x is 1", + "some @lst.x is 1", + "every @lst.x whose y is 2 is 1", + "every @lst.x is at least 1", + "every @lst, if any, is not empty", + "some @lst.x whose y is 2 is 1", + "every @lst.sub is not empty", + "count of @lst is 2", + "count of @lst whose y is 2 is 1", + "sum of @lst.n is 5", + "sum of @lst.n whose y is 2 is 2", + "@cov.x plus @cov.n is 3", + "count of @lst plus @cov.x is 3", + "days elapsed from @cov.t to @cov.t2 is 1", + "count of @lst is @cov.x", + "sum of @lst.n equals @cov.n within 0.5", +] + +_swap_fors := [ + {"for": "every @lst as it", "assert": "it.x is 1"}, + {"for": "every @lst as it", "assert": ["it.x is @cov.x", "@cov.s exists"]}, + {"for": "some @lst whose y is 2 as it", "assert": ["it.x is 1", "@cov.s exists"]}, + {"for": "every @lst as it, every it.sub as s", "assert": "s is @cov.x"}, + {"for": "every @lst as it", "assert": "count of it.sub is @cov.x"}, + {"for": "every @lst as it", "assert": "it.x is @uno"}, + {"for": "every @cov.items as it", "assert": "it.x is @uno"}, +] + +_swap_old(p) := [ + {"op": "present", "path": array.concat(p.cov, ["x"])}, + {"op": "equals", "path": array.concat(p.cov, ["x"]), "value": 1}, + {"op": "compare", "left": array.concat(p.cov, ["x"]), "right": array.concat(p.cov, ["n"]), "cmp": "lt"}, + {"op": "compare", "left": {"count": p.lst}, "right": {"literal": 2}, "cmp": "eq"}, + {"op": "compare", "left": {"sum": p.lst, "each": ["n"]}, "right": array.concat(p.cov, ["n"]), "cmp": "eq"}, + {"op": "compare", "left": {"add": [array.concat(p.cov, ["x"]), {"literal": 1}]}, "right": {"literal": 2}, "cmp": "eq"}, + {"op": "all", "path": p.lst, "check": {"op": "equals", "path": ["x"], "value": 1}}, + {"op": "any", "path": p.lst, "as": "q", "check": {"op": "equals", "path": ["x"], "value": 1}}, + {"op": "all", "path": array.concat(p.cov, ["items"]), "check": {"op": "present", "path": ["x"]}, "substitute": {"op": "present", "path": p.uno}}, + {"op": "any_of", "options": {"a": [{"op": "present", "path": array.concat(p.cov, ["x"])}], "b": [{"op": "equals", "path": p.uno, "value": 1}]}}, + {"op": "even", "path": array.concat(p.cov, ["n"]), "inputs": [array.concat(p.cov, ["n"])], "expression": "n is even"}, +] + +_swap_old_alias := {"cov": ["$cov"], "lst": ["$lst"], "uno": ["$uno"]} + +_swap_old_full := {"cov": ["dd", "b"], "lst": ["dd", "items"], "uno": ["dd", "b", "x"]} + +_swap_checks(replacements, old, lines, walks, fors) := object.union( + {sprintf("l%02d", [i]): strings.replace_n(replacements, line) | some i, line in lines}, + object.union( + {sprintf("w%02d", [i]): strings.replace_n(replacements, line) | some i, line in walks}, + object.union( + {sprintf("f%02d", [i]): _swap_for(replacements, item) | some i, item in fors}, + {sprintf("o%02d", [i]): item | some i, item in old}, + ), + ), +) + +_swap_for(replacements, item) := {"for": strings.replace_n(replacements, item.for), "assert": _swap_assert(replacements, item.assert)} + +_swap_assert(replacements, a) := strings.replace_n(replacements, a) if is_string(a) + +_swap_assert(replacements, a) := [strings.replace_n(replacements, line) | some line in a] if is_array(a) + +_swap_policy(has, checks) := { + "subjects": {"round": _swap_subject(has)}, + "requirements": {"r": {"subject": "round", "checks": checks}}, +} + +_swap_subject(has) := {"from": "trail", "identified_by": "name", "has": has} if count(has) > 0 + +_swap_subject(has) := {"from": "trail", "identified_by": "name"} if count(has) == 0 + +_swap_view(rep) := { + "rows": [{ + "check": r.check, + "subject": r.subject, + "passed": r.passed, + "cause": r.cause, + "values": {i.value | some i in r.inputs}, + "failed": {[f.cause, f.value, [a.cause | some a in object.get(f, "failed_assertions", [])]] | some f in object.get(r, "failed_items", [])}, + "lines": [a.cause | some a in object.get(r, "failed_assertions", [])], + } | + some r in rep.results + ], + "requirement": {c: {x.value | some x in object.get(d, "$refs", [])} | some c, d in rep.requirements.r.checks}, + "status": rep.requirements.r.status, + "causes": rep.requirements.r.causes, +} + +_swap_item(x, y, n, sub) := {"x": x, "y": y, "n": n, "sub": sub} + +_swap_good := { + "b": {"x": 1, "s": "abc", "n": 2, "t": "2026-01-01T00:00:00Z", "t2": "2026-01-02T00:00:00Z", "items": [1, 2]}, + "items": [_swap_item(1, 2, 2, [1]), _swap_item(1, 3, 3, [1, 1])], + "m": {"m1": {"x": 1}}, +} + +_swap_same(d, params) if { + alias := ergo.report(_parts_doc(d), _swap_policy(_swap_has, _swap_checks(_swap_alias, _swap_old(_swap_old_alias), _swap_lines, _swap_walks, _swap_fors)), params) + full := ergo.report(_parts_doc(d), _swap_policy({}, _swap_checks(_swap_full, _swap_old(_swap_old_full), _swap_lines, _swap_walks, _swap_fors)), params) + _swap_view(alias) == _swap_view(full) + count(alias.results) > 60 +} + +_swap_same_doc(doc, params) if { + alias := ergo.report(doc, _swap_policy(_swap_has, _swap_checks(_swap_alias, _swap_old(_swap_old_alias), _swap_lines, _swap_walks, _swap_fors)), params) + full := ergo.report(doc, _swap_policy({}, _swap_checks(_swap_full, _swap_old(_swap_old_full), _swap_lines, _swap_walks, _swap_fors)), params) + _swap_view(alias) == _swap_view(full) + count(alias.results) > 60 +} + +test_a_part_gives_the_rows_of_its_full_path_when_everything_is_there if { + _swap_same(_swap_good, {"k": "m1"}) + _swap_same(_swap_good, {}) +} + +test_a_part_gives_the_rows_of_its_full_path_when_the_params_it_names_by_are_wrong if { + _swap_same(_swap_good, {"k": 1}) + _swap_same(_swap_good, {"k": null}) + _swap_same(object.union(_swap_good, {"m": {}}), {"k": "m1"}) +} + +test_a_part_gives_the_rows_of_its_full_path_when_an_object_part_is_missing_null_or_wrong_typed if { + _swap_same(object.remove(_swap_good, ["b"]), {"k": "m1"}) + _swap_same(object.union(_swap_good, {"b": null}), {"k": "m1"}) + _swap_same(object.union(_swap_good, {"b": "text"}), {"k": "m1"}) + _swap_same(object.union(_swap_good, {"b": [1]}), {"k": "m1"}) +} + +test_a_part_gives_the_rows_of_its_full_path_when_a_field_inside_it_is_null_or_wrong_typed if { + _swap_same(object.union(_swap_good, {"b": object.union(_swap_good.b, {"x": null})}), {"k": "m1"}) + _swap_same(object.union(_swap_good, {"b": {"x": "1", "s": 5, "n": "2", "t": "nope", "t2": 3, "items": "s"}}), {"k": "m1"}) +} + +test_a_part_gives_the_rows_of_its_full_path_when_the_list_is_missing_null_empty_or_a_string if { + _swap_same(object.remove(_swap_good, ["items"]), {"k": "m1"}) + _swap_same(object.union(_swap_good, {"items": null}), {"k": "m1"}) + _swap_same(object.union(_swap_good, {"items": []}), {"k": "m1"}) + _swap_same(object.union(_swap_good, {"items": "str"}), {"k": "m1"}) +} + +test_a_part_gives_the_rows_of_its_full_path_when_the_list_holds_odd_items if { + _swap_same(object.union(_swap_good, {"items": [_swap_item(1, 2, 2, [1]), 5, null]}), {"k": "m1"}) + _swap_same(object.union(_swap_good, {"items": [_swap_item(null, 2, "q", []), {"y": 2}]}), {"k": "m1"}) +} + +test_a_part_gives_the_rows_of_its_full_path_when_the_named_by_part_is_missing_or_wrong_typed if { + _swap_same(object.union(_swap_good, {"m": null}), {"k": "m1"}) + _swap_same(object.union(_swap_good, {"m": "s"}), {"k": "m1"}) + _swap_same(object.remove(_swap_good, ["m"]), {"k": "m1"}) +} + +test_a_part_gives_the_rows_of_its_full_path_when_the_subject_has_nothing_or_is_not_an_object if { + _swap_same_doc({"trail": {"name": "t1"}}, {"k": "m1"}) + _swap_same_doc({"trail": {"name": "t1", "dd": null}}, {"k": "m1"}) + _swap_same_doc({"trail": {"name": "t1", "dd": "s"}}, {"k": "m1"}) + _swap_same_doc({"trail": {"name": "t1", "dd": [1]}}, {"k": "m1"}) +} + +test_the_swap_comparison_would_notice_a_part_that_reads_the_wrong_path if { + alias := ergo.report(_parts_doc(_swap_good), _swap_policy({"cov": "dd.items"}, {"c": "cov.x is 1"}), {}) + full := ergo.report(_parts_doc(_swap_good), _swap_policy({}, {"c": "dd.b.x is 1"}), {}) + _swap_view(alias) != _swap_view(full) +} + +test_the_swap_comparison_sees_rows_of_every_kind_of_outcome if { + alias := ergo.report(_parts_doc(object.union(_swap_good, {"b": "text"})), _swap_policy(_swap_has, _swap_checks(_swap_alias, _swap_old(_swap_old_alias), _swap_lines, _swap_walks, _swap_fors)), {"k": "m1"}) + {r.cause | some r in alias.results; startswith(r.check, "l")} >= {"unusable", "satisfied"} + {r.cause | some r in alias.results} >= {"unusable", "satisfied", "value", "not_an_object"} +} + +_deep_all(p, check) := {"op": "all", "path": p.lst, "check": check} + +_deep_options(form, group) := {"g": group} if form == "object" + +_deep_options(form, group) := [group] if form == "list" + +_deep_leaf(p) := {"op": "present", "path": array.concat(p.cov, ["x"])} + +_deep_at(level, _, options) := {"op": "any_of", "options": options} if level == "a" + +_deep_at(level, p, options) := _deep_all(p, {"op": "any_of", "options": options}) if level == "b" + +_deep_at(level, p, options) := _deep_all(p, {"op": "all", "path": ["sub"], "check": {"op": "any_of", "options": options}}) if level == "c" + +_deep_at(level, p, options) := {"op": "any_of", "options": [[_deep_all(p, {"op": "all", "path": ["sub"], "check": {"op": "any_of", "options": options}})]]} if level == "d" + +_deep_row(doc, level, form) := _parts_row(ergo.report(doc, _swap_policy(_swap_has, {"c": _deep_at(level, _swap_old_alias, _deep_options(form, _deep_leaf_group(_swap_old_alias)))}), {}), "c") + +_deep_leaf_group(p) := [_deep_leaf(p)] + +test_an_old_syntax_part_is_read_from_the_subject_at_every_depth_a_check_can_be_nested_to if { + every level in ["a", "b", "c", "d"] { + every form in ["object", "list"] { + good := _deep_row(_parts_doc(_swap_good), level, form) + good.passed + good.cause == "satisfied" + bad := _deep_row(_parts_doc(object.remove(_swap_good, ["b"])), level, form) + bad.passed == false + bad.cause == "value" + } + } +} + +test_a_group_of_options_that_is_not_a_list_or_holds_a_non_object_is_written_wrong_at_every_depth if { + every level in ["a", "b", "c", "d"] { + every options in [{"g": "nope"}, ["nope"], {"g": [5]}, [[5]]] { + check := _deep_at(level, _swap_old_alias, options) + rep := ergo.report(_parts_doc(_swap_good), _swap_policy(_swap_has, {"c": check}), {}) + [r.cause | some r in rep.results; r.check == "c"] == ["ill_formed"] + } + } +} + +_deep_nest(n, leaf) := {"op": "all", "path": ["xs"], "check": leaf} if n == 1 + +_deep_nest(n, leaf) := {"op": "all", "path": ["xs"], "check": {"op": "all", "path": ["xs"], "check": leaf}} if n == 2 + +_deep_nest(n, leaf) := {"op": "all", "path": ["xs"], "check": {"op": "all", "path": ["xs"], "check": {"op": "all", "path": ["xs"], "check": leaf}}} if n == 3 + +_deep_nest(n, leaf) := {"op": "all", "path": ["xs"], "check": {"op": "all", "path": ["xs"], "check": {"op": "all", "path": ["xs"], "check": {"op": "all", "path": ["xs"], "check": leaf}}}} if n == 4 + +test_a_nested_check_that_is_not_an_object_is_written_wrong_at_every_depth_and_does_not_hide_the_row if { + every n in [1, 2, 3, 4] { + rep := ergo.report(_parts_doc(_swap_good), _swap_policy(_swap_has, {"c": _deep_nest(n, 5)}), {}) + [r.cause | some r in rep.results; r.check == "c"] == ["ill_formed"] + } +} + +test_a_check_or_a_substitute_or_a_filter_that_is_not_an_object_is_written_wrong_when_a_subject_has_parts if { + rep := ergo.report(_parts_doc(_swap_good), _swap_policy(_swap_has, {"c": 5, "d": {"op": "present", "path": ["$cov", "x"], "substitute": 5}}), {}) + [r.cause | some r in rep.results; r.check == "c"] == ["ill_formed"] + [r.cause | some r in rep.results; r.check == "d"] == ["ill_formed"] + policy := { + "subjects": {"s": {"from": ["trail"], "has": {"cov": "dd.b"}, "applies_to": {"f": 5, "g": {"op": "present", "path": ["$cov", "x"]}}}}, + "requirements": {"r": {"subject": "s", "checks": {"c": "name is \"t1\""}}}, + } + filters := ergo.report(_parts_doc(_swap_good), policy, {}) + [r.cause | some r in filters.results; r.check == "$applies"] == ["ill_formed"] + _parts_problems(filters)["subjects.s.applies_to.f"] == ["invalid check"] +} + +test_an_old_syntax_filter_and_substitute_read_a_part_through_its_alias if { + policy := { + "subjects": {"s": {"from": ["trail"], "has": {"cov": "dd.b"}, "applies_to": {"f": {"op": "present", "path": ["$cov", "x"], "substitute": {"op": "present", "path": ["$cov", "n"]}}}}}, + "requirements": {"r": {"subject": "s", "checks": {"c": {"op": "equals", "path": ["$cov", "x"], "value": 5, "substitute": {"op": "equals", "path": ["$cov", "n"], "value": 2}}}}}, + } + rep := ergo.report(_parts_doc({"b": {"n": 2}}), policy, {}) + [[r.check, r.cause] | some r in rep.results; r.check in {"$applies", "c"}] == [["$applies", "satisfied"], ["c", "substituted"]] +} + +test_an_old_syntax_derived_side_reads_a_part_from_the_subject_in_where_and_add if { + p := _swap_old_alias + checks := { + "count": {"op": "compare", "left": {"count": p.lst, "where": {"op": "equals", "path": ["y"], "value": 2}}, "right": {"literal": 1}, "cmp": "eq"}, + "sum": {"op": "compare", "left": {"sum": p.lst, "each": ["n"], "where": {"op": "present", "path": array.concat(p.cov, ["x"])}}, "right": {"literal": 5}, "cmp": "eq"}, + "add": {"op": "compare", "left": {"add": [{"count": p.lst}, array.concat(p.cov, ["n"])]}, "right": {"literal": 4}, "cmp": "eq"}, + } + rep := _parts_report(_swap_has, checks, _swap_good) + every c in ["count", "sum", "add"] { + _parts_row(rep, c).passed + } + _parts_row(rep, "sum").inputs[0].name == "sum($lst[].n where $cov.x is present)" + _parts_row(rep, "add").inputs[0] == {"name": "count($lst) + $cov.n", "value": 4} + missing := _parts_report(_swap_has, checks, object.remove(_swap_good, ["b"])) + _parts_row(missing, "sum").cause == "value" + _parts_row(missing, "add").cause == "absent" +} + +test_an_old_syntax_custom_operator_with_object_and_odd_inputs_still_reads_its_part_and_shows_no_marker if { + checks := {"c": {"op": "even", "path": ["$cov", "n"], "inputs": [{"path": ["$cov", "n"], "each": ["$cov", "x"]}, 5, ["$cov", "x"]], "expression": "n is even"}} + rep := _parts_report({"cov": "dd.b"}, checks, {"b": {"n": 4, "x": 1}}) + row := _parts_row(rep, "c") + row.passed + every i in row.inputs { + not contains(i.name, "alias") + } +} + +test_an_old_syntax_side_that_is_not_a_path_or_a_known_shape_is_left_as_written if { + checks := { + "a": {"op": "compare", "left": 5, "right": ["$cov", "x"], "cmp": "lt"}, + "b": {"op": "compare", "left": {"add": [5, ["$cov", "x"]]}, "right": {"literal": 1}, "cmp": "lt"}, + "c": {"op": "compare", "left": {"count_of": ["$lst"], "whose": [5], "text": "t"}, "right": {"literal": 1}, "cmp": "lt"}, + } + rep := _parts_report(_swap_has, checks, _swap_good) + every c in ["a", "b", "c"] { + [r.cause | some r in rep.results; r.check == c] == ["ill_formed"] + } +} + +test_a_part_with_a_key_that_is_not_a_string_is_written_wrong_and_is_not_listed if { + policy := _parts_policy({1: "dd.b", true: "dd.c", "ok": "dd.d"}, {"c": "ok exists"}) + rep := ergo.report(_parts_doc({"d": 1}), policy, {}) + problems := _parts_problems(rep) + problems["subjects.round.has."] == ["invalid alias"] + problems["subjects.round.has."] == ["invalid alias"] + rep.subjects.round.parts == {"ok": {"from": "dd.d", "identified_by": null}} + _parts_row(rep, "c").passed +} diff --git a/spec/OPEN.md b/spec/OPEN.md index 842e9aa..b555406 100644 --- a/spec/OPEN.md +++ b/spec/OPEN.md @@ -44,6 +44,15 @@ Derived values on a side of `compare` (`count`, `sum`, `add`, `literal`, and `wi A subject with `of` (see [REFERENCE.md](../REFERENCE.md#a-subject-built-on-another)) has an entry in `policy/schema.json`, but no rules and no cases yet. +## Parts + +A subject's parts (`has`, see [syntax.md](syntax.md#parts)) leave a few things open: + +- `named by` a bare word that is a part's alias is written wrong. It would have to read the part's value and use it as a key, as it does for a `for` alias, and nothing says yet whether a part that is a path with its own `named by` can be used that way. +- `identified_by` of a part is read and shown, but nothing uses it. A later change is meant to tag the values a check reads with the id of the part they sit in, so a report can say which one. +- When `every`, `some`, `count of` or `sum of` walks a list in the middle of a part's path, not at its end, the rows are the full path's, but the name of a failed item shows the alias and the steps after the walked list's position, like `x.sub[1].y` for a part `x` that stands for `items.sub`. The position in the hidden list isn't shown. A part that ends at its list, like `items`, names every item. +- A part's path in the `$min_subjects` and `$unique_ids` expressions of a chained subject whose `from` starts at a part of its parent is shown in full, because those expressions show the subject's `from` as a path and not as a line. + ## Time limits `time.format` only takes years 1678 to 2261 and only counts the first nine digits of a fraction. Both limits come from OPA, which holds a time as nanoseconds since 1970 in a 64-bit integer. A port in another language would have to add them on purpose. Either they stay, and the spec keeps them as a rule every implementation follows, or the spec drops them and `ergo.rego` has to compare times some other way outside that range. diff --git a/spec/cases/parts/cases.json b/spec/cases/parts/cases.json new file mode 100644 index 0000000..a6e30ea --- /dev/null +++ b/spec/cases/parts/cases.json @@ -0,0 +1,2840 @@ +[ + { + "description": "a subject with two parts, one written as a line and one with an id", + "policy": { + "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" + } + } + } + }, + "cases": [ + { + "description": "reads each alias as the path inside the subject, and the names in the rows and expressions keep the alias", + "rules": [ + "parts.syntax", + "parts.path", + "parts.swap", + "sentence.path.part", + "name.part" + ], + "input": { + "trail": { + "name": "t1", + "compliance_status": { + "attestations_statuses": { + "findings": { + "attestation_data": { + "truncated": false + } + }, + "coverage-verification": { + "attestation_data": { + "hunks_acknowledged": 3, + "hunks_total": 3 + } + } + } + } + } + }, + "expressions": { + "review_round": { + "complete": "findings.truncated is false", + "every_hunk_acknowledged": "coverage.attestation_data.hunks_acknowledged equals coverage.attestation_data.hunks_total" + } + }, + "report": { + "subjects": { + "round": { + "description": "", + "from": "trail", + "identified_by": "name", + "parts": { + "coverage": { + "from": "compliance_status.attestations_statuses.coverage-verification", + "identified_by": "attestation_id" + }, + "findings": { + "from": "compliance_status.attestations_statuses.findings.attestation_data", + "identified_by": null + } + } + } + } + }, + "results": [ + { + "cause": "satisfied", + "check": "complete", + "inputs": [ + { + "name": "findings.truncated", + "value": false + } + ], + "passed": true, + "requirement": "review_round", + "subject": { + "id": "t1", + "type": "round" + } + }, + { + "cause": "satisfied", + "check": "every_hunk_acknowledged", + "inputs": [ + { + "name": "coverage.attestation_data.hunks_acknowledged", + "value": 3 + }, + { + "name": "coverage.attestation_data.hunks_total", + "value": 3 + } + ], + "passed": true, + "requirement": "review_round", + "subject": { + "id": "t1", + "type": "round" + } + } + ] + }, + { + "description": "fails as value when a field of a part holds the wrong value", + "rules": [ + "parts.swap" + ], + "input": { + "trail": { + "name": "t1", + "compliance_status": { + "attestations_statuses": { + "findings": { + "attestation_data": { + "truncated": true + } + }, + "coverage-verification": { + "attestation_data": { + "hunks_acknowledged": 3, + "hunks_total": 3 + } + } + } + } + } + }, + "results": [ + { + "cause": "value", + "check": "complete", + "inputs": [ + { + "name": "findings.truncated", + "value": true + } + ], + "passed": false, + "requirement": "review_round", + "subject": { + "id": "t1", + "type": "round" + } + }, + { + "cause": "satisfied", + "check": "every_hunk_acknowledged", + "inputs": [ + { + "name": "coverage.attestation_data.hunks_acknowledged", + "value": 3 + }, + { + "name": "coverage.attestation_data.hunks_total", + "value": 3 + } + ], + "passed": true, + "requirement": "review_round", + "subject": { + "id": "t1", + "type": "round" + } + } + ] + }, + { + "description": "fails as absent when the part is missing, as the full path would", + "rules": [ + "parts.swap", + "path.absent" + ], + "input": { + "trail": { + "name": "t1", + "compliance_status": { + "attestations_statuses": { + "findings": { + "attestation_data": { + "truncated": false + } + } + } + } + } + }, + "results": [ + { + "cause": "satisfied", + "check": "complete", + "inputs": [ + { + "name": "findings.truncated", + "value": false + } + ], + "passed": true, + "requirement": "review_round", + "subject": { + "id": "t1", + "type": "round" + } + }, + { + "cause": "absent", + "check": "every_hunk_acknowledged", + "inputs": [ + { + "name": "coverage.attestation_data.hunks_acknowledged", + "value": null + }, + { + "name": "coverage.attestation_data.hunks_total", + "value": null + } + ], + "passed": false, + "requirement": "review_round", + "subject": { + "id": "t1", + "type": "round" + } + } + ] + }, + { + "description": "fails as absent when the part is null, because a step follows it", + "rules": [ + "parts.swap", + "path.absent" + ], + "input": { + "trail": { + "name": "t1", + "compliance_status": { + "attestations_statuses": { + "findings": { + "attestation_data": null + }, + "coverage-verification": { + "attestation_data": null + } + } + } + } + }, + "results": [ + { + "cause": "absent", + "check": "complete", + "inputs": [ + { + "name": "findings.truncated", + "value": null + } + ], + "passed": false, + "requirement": "review_round", + "subject": { + "id": "t1", + "type": "round" + } + }, + { + "cause": "absent", + "check": "every_hunk_acknowledged", + "inputs": [ + { + "name": "coverage.attestation_data.hunks_acknowledged", + "value": null + }, + { + "name": "coverage.attestation_data.hunks_total", + "value": null + } + ], + "passed": false, + "requirement": "review_round", + "subject": { + "id": "t1", + "type": "round" + } + } + ] + }, + { + "description": "fails as unusable when the part is a string, as the full path would", + "rules": [ + "parts.swap", + "path.unusable" + ], + "input": { + "trail": { + "name": "t1", + "compliance_status": { + "attestations_statuses": { + "findings": { + "attestation_data": "truncated" + }, + "coverage-verification": { + "attestation_data": "done" + } + } + } + } + }, + "results": [ + { + "cause": "unusable", + "check": "complete", + "inputs": [ + { + "name": "findings.truncated", + "value": null + } + ], + "passed": false, + "requirement": "review_round", + "subject": { + "id": "t1", + "type": "round" + } + }, + { + "cause": "unusable", + "check": "every_hunk_acknowledged", + "inputs": [ + { + "name": "coverage.attestation_data.hunks_acknowledged", + "value": null + }, + { + "name": "coverage.attestation_data.hunks_total", + "value": null + } + ], + "passed": false, + "requirement": "review_round", + "subject": { + "id": "t1", + "type": "round" + } + } + ] + }, + { + "description": "fails as unusable when something on the way to the part can't hold a key", + "rules": [ + "parts.swap", + "path.unusable" + ], + "input": { + "trail": { + "name": "t1", + "compliance_status": "ok" + } + }, + "results": [ + { + "cause": "unusable", + "check": "complete", + "inputs": [ + { + "name": "findings.truncated", + "value": null + } + ], + "passed": false, + "requirement": "review_round", + "subject": { + "id": "t1", + "type": "round" + } + }, + { + "cause": "unusable", + "check": "every_hunk_acknowledged", + "inputs": [ + { + "name": "coverage.attestation_data.hunks_acknowledged", + "value": null + }, + { + "name": "coverage.attestation_data.hunks_total", + "value": null + } + ], + "passed": false, + "requirement": "review_round", + "subject": { + "id": "t1", + "type": "round" + } + } + ] + } + ] + }, + { + "description": "the same check written with an alias and with the path it stands for", + "policy": { + "subjects": { + "s": { + "from": "items", + "identified_by": "id", + "has": { + "cov": "d.b" + } + } + }, + "requirements": { + "r": { + "subject": "s", + "checks": { + "alias": "cov.x is at least cov.n", + "full": "d.b.x is at least d.b.n" + } + } + } + }, + "cases": [ + { + "description": "gives the same row apart from the names of the inputs", + "rules": [ + "parts.swap", + "name.part" + ], + "input": { + "items": [ + { + "id": "a", + "d": { + "b": { + "x": 3, + "n": 2 + } + } + }, + { + "id": "b", + "d": { + "b": { + "x": 1, + "n": 2 + } + } + } + ] + }, + "expressions": { + "r": { + "alias": "cov.x is at least cov.n", + "full": "d.b.x is at least d.b.n" + } + }, + "results": [ + { + "cause": "satisfied", + "check": "alias", + "inputs": [ + { + "name": "cov.x", + "value": 3 + }, + { + "name": "cov.n", + "value": 2 + } + ], + "passed": true, + "requirement": "r", + "subject": { + "id": "a", + "type": "s" + } + }, + { + "cause": "satisfied", + "check": "full", + "inputs": [ + { + "name": "d.b.x", + "value": 3 + }, + { + "name": "d.b.n", + "value": 2 + } + ], + "passed": true, + "requirement": "r", + "subject": { + "id": "a", + "type": "s" + } + }, + { + "cause": "value", + "check": "alias", + "inputs": [ + { + "name": "cov.x", + "value": 1 + }, + { + "name": "cov.n", + "value": 2 + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": "b", + "type": "s" + } + }, + { + "cause": "value", + "check": "full", + "inputs": [ + { + "name": "d.b.x", + "value": 1 + }, + { + "name": "d.b.n", + "value": 2 + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": "b", + "type": "s" + } + } + ] + }, + { + "description": "gives the same rows when the part is missing, null or a string", + "rules": [ + "parts.swap" + ], + "input": { + "items": [ + { + "id": "a", + "d": {} + }, + { + "id": "b", + "d": { + "b": null + } + }, + { + "id": "c", + "d": { + "b": "text" + } + } + ] + }, + "results": [ + { + "cause": "absent", + "check": "alias", + "inputs": [ + { + "name": "cov.x", + "value": null + }, + { + "name": "cov.n", + "value": null + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": "a", + "type": "s" + } + }, + { + "cause": "absent", + "check": "full", + "inputs": [ + { + "name": "d.b.x", + "value": null + }, + { + "name": "d.b.n", + "value": null + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": "a", + "type": "s" + } + }, + { + "cause": "absent", + "check": "alias", + "inputs": [ + { + "name": "cov.x", + "value": null + }, + { + "name": "cov.n", + "value": null + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": "b", + "type": "s" + } + }, + { + "cause": "absent", + "check": "full", + "inputs": [ + { + "name": "d.b.x", + "value": null + }, + { + "name": "d.b.n", + "value": null + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": "b", + "type": "s" + } + }, + { + "cause": "unusable", + "check": "alias", + "inputs": [ + { + "name": "cov.x", + "value": null + }, + { + "name": "cov.n", + "value": null + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": "c", + "type": "s" + } + }, + { + "cause": "unusable", + "check": "full", + "inputs": [ + { + "name": "d.b.x", + "value": null + }, + { + "name": "d.b.n", + "value": null + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": "c", + "type": "s" + } + } + ] + }, + { + "description": "gives the same rows when the subject is not an object", + "rules": [ + "parts.swap", + "path.not_an_object" + ], + "input": { + "items": [ + "text", + null, + 5 + ] + }, + "results": [ + { + "cause": "not_an_object", + "check": "alias", + "inputs": [ + { + "name": "cov.x", + "value": null + }, + { + "name": "cov.n", + "value": null + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": "text", + "type": "s" + } + }, + { + "cause": "not_an_object", + "check": "full", + "inputs": [ + { + "name": "d.b.x", + "value": null + }, + { + "name": "d.b.n", + "value": null + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": "text", + "type": "s" + } + }, + { + "cause": "not_an_object", + "check": "alias", + "inputs": [ + { + "name": "cov.x", + "value": null + }, + { + "name": "cov.n", + "value": null + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": null, + "type": "s" + } + }, + { + "cause": "not_an_object", + "check": "full", + "inputs": [ + { + "name": "d.b.x", + "value": null + }, + { + "name": "d.b.n", + "value": null + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": null, + "type": "s" + } + }, + { + "cause": "not_an_object", + "check": "alias", + "inputs": [ + { + "name": "cov.x", + "value": null + }, + { + "name": "cov.n", + "value": null + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": 5, + "type": "s" + } + }, + { + "cause": "not_an_object", + "check": "full", + "inputs": [ + { + "name": "d.b.x", + "value": null + }, + { + "name": "d.b.n", + "value": null + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": 5, + "type": "s" + } + } + ] + } + ] + }, + { + "description": "where a part's alias is read", + "policy": { + "subjects": { + "round": { + "from": "rounds whose cov.x is 1", + "identified_by": "id", + "has": { + "cov": "d.b", + "lst": "d.items" + } + } + }, + "requirements": { + "r": { + "subject": "round", + "checks": { + "in_assertion": { + "for": "every lst as it", + "assert": [ + "it.x is cov.x", + "cov.s exists" + ] + }, + "item_level": "every lst.x whose cov is at least 5 is cov", + "shadows": "cov.n is 2", + "dot": ".cov.n is 2" + } + } + } + }, + "cases": [ + { + "description": "reads the alias in the conditions of the from line, in a for line's assertions, and not in a whose or in the argument of a one-line every", + "rules": [ + "parts.where", + "sentence.path.part", + "sentence.subject.from" + ], + "input": { + "rounds": [ + { + "id": "a", + "cov": { + "n": 9 + }, + "d": { + "b": { + "x": 1, + "s": "t", + "n": 2 + }, + "items": [ + { + "x": 1, + "cov": 5 + }, + { + "x": 1, + "cov": 1 + } + ] + } + }, + { + "id": "b", + "d": { + "b": { + "x": 2, + "s": "t", + "n": 2 + }, + "items": [] + } + } + ] + }, + "expressions": { + "r": { + "in_assertion": "for every lst as it: it.x equals cov.x and cov.s exists", + "item_level": "every lst.x whose cov is at least 5 equals cov", + "$applies": "cov.x is 1" + } + }, + "results": [ + { + "cause": "satisfied", + "check": "$applies", + "inputs": [ + { + "name": "cov.x", + "value": 1 + } + ], + "passed": true, + "requirement": "r", + "subject": { + "id": "a", + "type": "round" + } + }, + { + "cause": "value", + "check": "$applies", + "inputs": [ + { + "name": "cov.x", + "value": 2 + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": "b", + "type": "round" + } + }, + { + "cause": "satisfied", + "check": "dot", + "inputs": [ + { + "name": "cov.n", + "value": 2 + } + ], + "passed": true, + "requirement": "r", + "subject": { + "id": "a", + "type": "round" + } + }, + { + "cause": "satisfied", + "check": "in_assertion", + "failed_items": [], + "inputs": [ + { + "name": "cov.s", + "value": "t" + }, + { + "name": "cov.x", + "value": 1 + }, + { + "name": "lst[]", + "value": [ + { + "cov": 5, + "x": 1 + }, + { + "cov": 1, + "x": 1 + } + ] + } + ], + "passed": true, + "requirement": "r", + "subject": { + "id": "a", + "type": "round" + } + }, + { + "cause": "value", + "check": "item_level", + "failed_items": [ + { + "cause": "value", + "item": "item 1 of lst", + "path": "lst[0].x", + "value": 1 + } + ], + "inputs": [ + { + "name": "lst[].x", + "value": [ + 1, + 1 + ] + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": "a", + "type": "round" + } + }, + { + "cause": "satisfied", + "check": "shadows", + "inputs": [ + { + "name": "cov.n", + "value": 2 + } + ], + "passed": true, + "requirement": "r", + "subject": { + "id": "a", + "type": "round" + } + } + ] + }, + { + "description": "an alias shadows a field with the same name, and the dot in front doesn't get around it", + "rules": [ + "parts.shadow", + "sentence.path.part" + ], + "input": { + "rounds": [ + { + "id": "a", + "cov": { + "n": 3 + }, + "d": { + "b": { + "x": 1, + "s": "t", + "n": 2 + }, + "items": [ + { + "x": 1 + } + ] + } + } + ] + }, + "results": [ + { + "cause": "satisfied", + "check": "$applies", + "inputs": [ + { + "name": "cov.x", + "value": 1 + } + ], + "passed": true, + "requirement": "r", + "subject": { + "id": "a", + "type": "round" + } + }, + { + "cause": "satisfied", + "check": "dot", + "inputs": [ + { + "name": "cov.n", + "value": 2 + } + ], + "passed": true, + "requirement": "r", + "subject": { + "id": "a", + "type": "round" + } + }, + { + "cause": "satisfied", + "check": "in_assertion", + "failed_items": [], + "inputs": [ + { + "name": "cov.s", + "value": "t" + }, + { + "name": "cov.x", + "value": 1 + }, + { + "name": "lst[]", + "value": [ + { + "x": 1 + } + ] + } + ], + "passed": true, + "requirement": "r", + "subject": { + "id": "a", + "type": "round" + } + }, + { + "cause": "absent", + "check": "item_level", + "failed_items": [ + { + "cause": "absent", + "item": "item 1 of lst", + "path": "lst[0].cov", + "value": null + } + ], + "inputs": [ + { + "name": "lst[].x", + "value": [ + 1 + ] + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": "a", + "type": "round" + } + }, + { + "cause": "satisfied", + "check": "shadows", + "inputs": [ + { + "name": "cov.n", + "value": 2 + } + ], + "passed": true, + "requirement": "r", + "subject": { + "id": "a", + "type": "round" + } + } + ] + } + ] + }, + { + "description": "an alias that is also a for alias", + "policy": { + "subjects": { + "round": { + "from": "rounds", + "identified_by": "id", + "has": { + "cov": "d.b" + } + } + }, + "requirements": { + "r": { + "subject": "round", + "checks": { + "c": { + "for": "every d.items as cov", + "assert": "cov.x is 1" + } + } + } + } + }, + "cases": [ + { + "description": "is written wrong, and the check fails", + "rules": [ + "parts.alias", + "parts.written_wrong", + "sentence.for.alias" + ], + "input": { + "rounds": [ + { + "id": "a", + "d": { + "b": { + "x": 1 + }, + "items": [ + { + "x": 1 + } + ] + } + } + ] + }, + "results": [ + { + "cause": "value", + "check": "$well_formed", + "inputs": [ + { + "name": "count(checks)", + "value": 1 + }, + { + "name": "require", + "value": "every" + }, + { + "name": "checks.c", + "value": [ + "alias given twice" + ] + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": null, + "type": "round" + } + }, + { + "cause": "ill_formed", + "check": "c", + "inputs": [], + "passed": false, + "requirement": "r", + "subject": { + "id": "a", + "type": "round" + } + } + ] + } + ] + }, + { + "description": "named by an alias that is a part", + "policy": { + "subjects": { + "round": { + "from": "rounds", + "identified_by": "id", + "has": { + "cov": "d.k" + } + } + }, + "requirements": { + "r": { + "subject": "round", + "checks": { + "c": "$params.m named by cov is 1" + } + } + } + }, + "params": { + "m": { + "q": 1 + } + }, + "cases": [ + { + "description": "is written wrong, because it would otherwise read the field", + "rules": [ + "sentence.path.part", + "parts.written_wrong" + ], + "input": { + "rounds": [ + { + "id": "a", + "cov": "q", + "d": { + "k": "q" + } + } + ] + }, + "results": [ + { + "cause": "value", + "check": "$well_formed", + "inputs": [ + { + "name": "count(checks)", + "value": 1 + }, + { + "name": "require", + "value": "every" + }, + { + "name": "checks.c", + "value": [ + "named by cov reads a part, which isn't supported yet" + ] + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": null, + "type": "round" + } + }, + { + "cause": "ill_formed", + "check": "c", + "inputs": [], + "passed": false, + "requirement": "r", + "subject": { + "id": "a", + "type": "round" + } + } + ] + } + ] + }, + { + "description": "a subject that narrows another keeps its parts and adds its own", + "policy": { + "subjects": { + "round": { + "from": "rounds", + "identified_by": "id", + "has": { + "cov": "d.b" + } + }, + "good round": { + "from": "round whose cov.x is 1", + "has": { + "extra": "d.e" + } + } + }, + "requirements": { + "r": { + "subject": "good round", + "checks": { + "c": "cov.n is 2 and extra is 5" + } + } + } + }, + "cases": [ + { + "description": "reads the kept part in its conditions and in its checks, and its own part in its checks", + "rules": [ + "parts.narrow", + "parts.where", + "sentence.subject.narrow" + ], + "input": { + "rounds": [ + { + "id": "a", + "d": { + "b": { + "x": 1, + "n": 2 + }, + "e": 5 + } + }, + { + "id": "b", + "d": { + "b": { + "x": 2, + "n": 2 + }, + "e": 5 + } + } + ] + }, + "report": { + "subjects": { + "round": { + "description": "", + "from": "rounds", + "identified_by": "id", + "parts": { + "cov": { + "from": "d.b", + "identified_by": null + } + } + }, + "good round": { + "description": "", + "from": "round whose cov.x is 1", + "identified_by": "id", + "parts": { + "cov": { + "from": "d.b", + "identified_by": null + }, + "extra": { + "from": "d.e", + "identified_by": null + } + } + } + } + }, + "results": [ + { + "cause": "satisfied", + "check": "$applies", + "inputs": [ + { + "name": "cov.x", + "value": 1 + } + ], + "passed": true, + "requirement": "r", + "subject": { + "id": "a", + "type": "good round" + } + }, + { + "cause": "value", + "check": "$applies", + "inputs": [ + { + "name": "cov.x", + "value": 2 + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": "b", + "type": "good round" + } + }, + { + "cause": "satisfied", + "check": "c", + "failed_assertions": [], + "inputs": [ + { + "name": "cov.n", + "value": 2 + }, + { + "name": "extra", + "value": 5 + } + ], + "passed": true, + "requirement": "r", + "subject": { + "id": "a", + "type": "good round" + } + } + ] + } + ] + }, + { + "description": "a subject that narrows another and adds a part with a kept alias", + "policy": { + "subjects": { + "round": { + "from": "rounds", + "identified_by": "id", + "has": { + "cov": "d.b" + } + }, + "good round": { + "from": "round whose cov.x is 1", + "has": { + "cov": "d.q" + } + } + }, + "requirements": { + "r": { + "subject": "good round", + "checks": { + "c": "cov.n is 2" + } + } + } + }, + "cases": [ + { + "description": "is written wrong with alias given twice, under the part", + "rules": [ + "parts.alias", + "parts.written_wrong", + "parts.narrow" + ], + "input": { + "rounds": [ + { + "id": "a", + "d": { + "b": { + "x": 1, + "n": 2 + }, + "q": { + "n": 2 + } + } + } + ] + }, + "results": [ + { + "cause": "value", + "check": "$well_formed", + "inputs": [ + { + "name": "count(checks)", + "value": 1 + }, + { + "name": "require", + "value": "every" + }, + { + "name": "subjects.\"good round\".has.cov", + "value": [ + "alias given twice" + ] + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": null, + "type": "good round" + } + }, + { + "cause": "absent", + "check": "$applies", + "inputs": [ + { + "name": "cov.x", + "value": null + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": "a", + "type": "good round" + } + } + ] + } + ] + }, + { + "description": "a chained subject reads its parent's parts", + "policy": { + "subjects": { + "artifact": { + "from": "trail.artifacts named by $params.art", + "identified_by": "fingerprint", + "has": { + "pra": "attestations.pr" + } + }, + "pull request": { + "from": "artifact.pra.pull_requests", + "identified_by": "url", + "has": { + "rev": "reviews" + } + } + }, + "requirements": { + "r": { + "subject": "pull request", + "checks": { + "parent": "artifact.pra.name is \"w\"", + "own": "rev.count is 2", + "field": "pra is 1" + } + } + } + }, + "params": { + "art": "web" + }, + "cases": [ + { + "description": "starts from a part of the parent, reads a part of the parent by the parent's name, and reads a bare alias of the parent as a field", + "rules": [ + "parts.chain", + "subjects.chain", + "sentence.subject.chain" + ], + "input": { + "trail": { + "artifacts": { + "web": { + "fingerprint": "sha-1", + "pra": 1, + "attestations": { + "pr": { + "name": "w", + "pull_requests": [ + { + "url": "pr/7", + "reviews": { + "count": 2 + } + }, + { + "url": "pr/8", + "reviews": { + "count": 1 + } + } + ] + } + } + } + } + } + }, + "report": { + "subjects": { + "artifact": { + "description": "", + "from": "trail.artifacts named by $params.art", + "identified_by": "fingerprint", + "parts": { + "pra": { + "from": "attestations.pr", + "identified_by": null + } + } + }, + "pull request": { + "description": "", + "from": "artifact.pra.pull_requests", + "identified_by": "url", + "parts": { + "rev": { + "from": "reviews", + "identified_by": null + } + } + } + } + }, + "results": [ + { + "cause": "absent", + "check": "field", + "inputs": [ + { + "name": "pra", + "value": null + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": "pr/7", + "in": { + "artifact": "sha-1" + }, + "type": "pull request" + } + }, + { + "cause": "satisfied", + "check": "own", + "inputs": [ + { + "name": "rev.count", + "value": 2 + } + ], + "passed": true, + "requirement": "r", + "subject": { + "id": "pr/7", + "in": { + "artifact": "sha-1" + }, + "type": "pull request" + } + }, + { + "cause": "satisfied", + "check": "parent", + "inputs": [ + { + "name": "artifact.pra.name", + "value": "w" + } + ], + "passed": true, + "requirement": "r", + "subject": { + "id": "pr/7", + "in": { + "artifact": "sha-1" + }, + "type": "pull request" + } + }, + { + "cause": "absent", + "check": "field", + "inputs": [ + { + "name": "pra", + "value": null + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": "pr/8", + "in": { + "artifact": "sha-1" + }, + "type": "pull request" + } + }, + { + "cause": "value", + "check": "own", + "inputs": [ + { + "name": "rev.count", + "value": 1 + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": "pr/8", + "in": { + "artifact": "sha-1" + }, + "type": "pull request" + } + }, + { + "cause": "satisfied", + "check": "parent", + "inputs": [ + { + "name": "artifact.pra.name", + "value": "w" + } + ], + "passed": true, + "requirement": "r", + "subject": { + "id": "pr/8", + "in": { + "artifact": "sha-1" + }, + "type": "pull request" + } + } + ] + } + ] + }, + { + "description": "a chained subject with a part named like its parent", + "policy": { + "subjects": { + "artifact": { + "from": "trail.artifacts named by $params.art", + "identified_by": "fingerprint", + "has": { + "pra": "attestations.pr" + } + }, + "pull request": { + "from": "artifact.pra.pull_requests", + "identified_by": "url", + "has": { + "artifact": "reviews" + } + } + }, + "requirements": { + "r": { + "subject": "pull request", + "checks": { + "c": "url exists" + } + } + } + }, + "params": { + "art": "web" + }, + "cases": [ + { + "description": "is written wrong with alias given twice", + "rules": [ + "parts.alias", + "parts.written_wrong", + "parts.chain" + ], + "input": { + "trail": { + "artifacts": { + "web": { + "fingerprint": "sha-1", + "attestations": { + "pr": { + "pull_requests": [ + { + "url": "pr/7" + } + ] + } + } + } + } + } + }, + "results": [ + { + "cause": "value", + "check": "$well_formed", + "inputs": [ + { + "name": "count(checks)", + "value": 1 + }, + { + "name": "require", + "value": "every" + }, + { + "name": "subjects.\"pull request\".has.artifact", + "value": [ + "alias given twice" + ] + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": null, + "in": { + "artifact": "sha-1" + }, + "type": "pull request" + } + }, + { + "cause": "satisfied", + "check": "c", + "inputs": [ + { + "name": "url", + "value": "pr/7" + } + ], + "passed": true, + "requirement": "r", + "subject": { + "id": "pr/7", + "in": { + "artifact": "sha-1" + }, + "type": "pull request" + } + } + ] + } + ] + }, + { + "description": "a subject with parts and checks written with op", + "policy": { + "subjects": { + "s": { + "from": [ + "items" + ], + "identified_by": [ + "id" + ], + "has": { + "cov": "d.b", + "lst": "d.items" + }, + "applies_to": { + "has_cov": { + "op": "present", + "path": [ + "$cov", + "x" + ] + } + } + } + }, + "requirements": { + "r": { + "subject": "s", + "checks": { + "equals": { + "op": "equals", + "path": [ + "$cov", + "x" + ], + "value": 1 + }, + "all": { + "op": "all", + "path": [ + "$lst" + ], + "check": { + "op": "equals", + "path": [ + "x" + ], + "value": 1 + } + }, + "inside": { + "op": "all", + "path": [ + "$lst" + ], + "check": { + "op": "equals", + "path": [ + "$cov", + "x" + ], + "value": 1 + } + }, + "derived": { + "op": "compare", + "left": { + "count": [ + "$lst" + ] + }, + "right": { + "literal": 2 + }, + "cmp": "eq" + }, + "unknown": { + "op": "present", + "path": [ + "$other", + "x" + ] + } + } + } + } + }, + "cases": [ + { + "description": "reads $alias as the part in a path, in a filter, inside all, and in a derived value, and keeps the dollar in the names", + "rules": [ + "parts.old_syntax", + "name.part", + "parts.swap" + ], + "input": { + "items": [ + { + "id": "a", + "d": { + "b": { + "x": 1 + }, + "items": [ + { + "x": 1 + }, + { + "x": 2 + } + ] + } + } + ] + }, + "expressions": { + "r": { + "equals": "$cov.x == 1", + "all": "every $lst: x == 1", + "inside": "every $lst: $cov.x == 1", + "derived": "count($lst) eq 2", + "$applies": "$cov.x is present" + } + }, + "results": [ + { + "cause": "value", + "check": "$well_formed", + "inputs": [ + { + "name": "count(checks)", + "value": 5 + }, + { + "name": "require", + "value": "every" + }, + { + "name": "checks.unknown", + "value": [ + "unknown alias $other" + ] + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": null, + "type": "s" + } + }, + { + "cause": "satisfied", + "check": "$applies", + "inputs": [ + { + "name": "$cov.x", + "value": 1 + } + ], + "passed": true, + "requirement": "r", + "subject": { + "id": "a", + "type": "s" + } + }, + { + "cause": "value", + "check": "all", + "failed_items": [ + { + "cause": "value", + "item": "item 2 of $lst", + "path": "item 2 of $lst", + "value": { + "x": 2 + } + } + ], + "inputs": [ + { + "name": "$lst[].x", + "value": [ + 1, + 2 + ] + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": "a", + "type": "s" + } + }, + { + "cause": "satisfied", + "check": "derived", + "inputs": [ + { + "name": "count($lst)", + "value": 2 + }, + { + "name": "$lst", + "value": [ + { + "x": 1 + }, + { + "x": 2 + } + ] + } + ], + "passed": true, + "requirement": "r", + "subject": { + "id": "a", + "type": "s" + } + }, + { + "cause": "satisfied", + "check": "equals", + "inputs": [ + { + "name": "$cov.x", + "value": 1 + } + ], + "passed": true, + "requirement": "r", + "subject": { + "id": "a", + "type": "s" + } + }, + { + "cause": "satisfied", + "check": "inside", + "failed_items": [], + "inputs": [ + { + "name": "$lst[]", + "value": [ + { + "x": 1 + }, + { + "x": 2 + } + ] + }, + { + "name": "$cov.x", + "value": 1 + } + ], + "passed": true, + "requirement": "r", + "subject": { + "id": "a", + "type": "s" + } + }, + { + "cause": "ill_formed", + "check": "unknown", + "inputs": [ + { + "name": "$other.x", + "value": null + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": "a", + "type": "s" + } + } + ] + }, + { + "description": "fails as the path would when the part is missing", + "rules": [ + "parts.old_syntax", + "parts.swap" + ], + "input": { + "items": [ + { + "id": "a", + "d": {} + } + ] + }, + "results": [ + { + "cause": "value", + "check": "$applies", + "inputs": [ + { + "name": "$cov.x", + "value": null + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": "a", + "type": "s" + } + } + ] + } + ] + }, + { + "description": "a part with the alias of an as in the old syntax", + "policy": { + "subjects": { + "s": { + "from": [ + "items" + ], + "identified_by": [ + "id" + ], + "has": { + "cov": "d.b" + } + } + }, + "requirements": { + "r": { + "subject": "s", + "checks": { + "c": { + "op": "all", + "path": [ + "d", + "items" + ], + "as": "cov", + "check": { + "op": "present", + "path": [ + "x" + ] + } + } + } + } + } + }, + "cases": [ + { + "description": "is written wrong with alias given twice", + "rules": [ + "parts.alias", + "parts.written_wrong", + "parts.old_syntax" + ], + "input": { + "items": [ + { + "id": "a", + "d": { + "b": { + "x": 1 + }, + "items": [ + { + "x": 1 + } + ] + } + } + ] + }, + "results": [ + { + "cause": "value", + "check": "$well_formed", + "inputs": [ + { + "name": "count(checks)", + "value": 1 + }, + { + "name": "require", + "value": "every" + }, + { + "name": "checks.c", + "value": [ + "alias given twice" + ] + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": null, + "type": "s" + } + }, + { + "cause": "ill_formed", + "check": "c", + "failed_items": [ + { + "cause": "ill_formed", + "item": "item 1 of d.items", + "path": "item 1 of d.items", + "value": { + "x": 1 + } + } + ], + "inputs": [ + { + "name": "d.items[].x", + "value": [ + 1 + ] + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": "a", + "type": "s" + } + } + ] + } + ] + }, + { + "description": "a subject whose parts are written wrong", + "policy": { + "subjects": { + "round": { + "from": "rounds", + "identified_by": "id", + "has": { + "num": 5, + "nofrom": { + "identified_by": "id" + }, + "extra": { + "from": "x", + "other": 1 + }, + "fromnum": { + "from": 1 + }, + "idnum": { + "from": "x", + "identified_by": 1 + }, + "who": "x whose y is 1", + "each_of": "x named by each of $params.k", + "in_params": "$params.x", + "in_input": "$input.x", + "bare_by": "x named by foo", + "blank": "", + "idbad": { + "from": "x", + "identified_by": "y whose z is 1" + }, + "$dollar": "x", + "with space": "x", + "every": "x", + "ok": "x" + } + } + }, + "requirements": { + "r": { + "subject": "round", + "checks": { + "c": "id exists" + } + } + } + }, + "cases": [ + { + "description": "fails $well_formed with a problem under each part's alias", + "rules": [ + "parts.written_wrong", + "parts.alias", + "parts.path", + "parts.syntax" + ], + "input": { + "rounds": [ + { + "id": "a" + } + ] + }, + "report": { + "subjects": { + "round": { + "description": "", + "from": "rounds", + "identified_by": "id", + "parts": { + "$dollar": { + "from": "x", + "identified_by": null + }, + "bare_by": { + "from": "", + "identified_by": null + }, + "blank": { + "from": "", + "identified_by": null + }, + "each_of": { + "from": "", + "identified_by": null + }, + "every": { + "from": "x", + "identified_by": null + }, + "extra": { + "from": "x", + "identified_by": null + }, + "fromnum": { + "from": "", + "identified_by": null + }, + "idbad": { + "from": "x", + "identified_by": "" + }, + "idnum": { + "from": "x", + "identified_by": "" + }, + "in_input": { + "from": "", + "identified_by": null + }, + "in_params": { + "from": "", + "identified_by": null + }, + "nofrom": { + "from": "", + "identified_by": "id" + }, + "num": { + "from": "", + "identified_by": null + }, + "ok": { + "from": "x", + "identified_by": null + }, + "who": { + "from": "", + "identified_by": null + }, + "with space": { + "from": "x", + "identified_by": null + } + } + } + } + }, + "results": [ + { + "cause": "value", + "check": "$well_formed", + "inputs": [ + { + "name": "count(checks)", + "value": 1 + }, + { + "name": "require", + "value": "every" + }, + { + "name": "subjects.round.has.$dollar", + "value": [ + "invalid alias" + ] + }, + { + "name": "subjects.round.has.bare_by", + "value": [ + "from: unknown alias foo" + ] + }, + { + "name": "subjects.round.has.blank", + "value": [ + "from: doesn't parse" + ] + }, + { + "name": "subjects.round.has.each_of", + "value": [ + "from: a part is a path: whose and named by each of can't go in it" + ] + }, + { + "name": "subjects.round.has.every", + "value": [ + "invalid alias" + ] + }, + { + "name": "subjects.round.has.extra", + "value": [ + "unknown field other" + ] + }, + { + "name": "subjects.round.has.fromnum", + "value": [ + "invalid from" + ] + }, + { + "name": "subjects.round.has.idbad", + "value": [ + "identified_by: a part is a path: whose and named by each of can't go in it" + ] + }, + { + "name": "subjects.round.has.idnum", + "value": [ + "invalid identified_by" + ] + }, + { + "name": "subjects.round.has.in_input", + "value": [ + "from: a part reads inside its subject, so it can't start at $params or $input" + ] + }, + { + "name": "subjects.round.has.in_params", + "value": [ + "from: a part reads inside its subject, so it can't start at $params or $input" + ] + }, + { + "name": "subjects.round.has.nofrom", + "value": [ + "missing from" + ] + }, + { + "name": "subjects.round.has.num", + "value": [ + "not a string or an object" + ] + }, + { + "name": "subjects.round.has.who", + "value": [ + "from: a part is a path: whose and named by each of can't go in it" + ] + }, + { + "name": "subjects.round.has.\"with space\"", + "value": [ + "invalid alias" + ] + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": null, + "type": "round" + } + }, + { + "cause": "satisfied", + "check": "c", + "inputs": [ + { + "name": "id", + "value": "a" + } + ], + "passed": true, + "requirement": "r", + "subject": { + "id": "a", + "type": "round" + } + } + ] + } + ] + }, + { + "description": "a subject whose has is not an object", + "policy": { + "subjects": { + "round": { + "from": "rounds", + "identified_by": "id", + "has": [ + "x" + ] + } + }, + "requirements": { + "r": { + "subject": "round", + "checks": { + "c": "id exists" + } + } + } + }, + "cases": [ + { + "description": "fails $well_formed under has", + "rules": [ + "parts.written_wrong", + "parts.syntax" + ], + "input": { + "rounds": [ + { + "id": "a" + } + ] + }, + "report": { + "subjects": { + "round": { + "description": "", + "from": "rounds", + "identified_by": "id", + "parts": {} + } + } + }, + "results": [ + { + "cause": "value", + "check": "$well_formed", + "inputs": [ + { + "name": "count(checks)", + "value": 1 + }, + { + "name": "require", + "value": "every" + }, + { + "name": "subjects.round.has", + "value": [ + "not an object" + ] + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": null, + "type": "round" + } + }, + { + "cause": "satisfied", + "check": "c", + "inputs": [ + { + "name": "id", + "value": "a" + } + ], + "passed": true, + "requirement": "r", + "subject": { + "id": "a", + "type": "round" + } + } + ] + } + ] + }, + { + "description": "the parts a report lists", + "policy": { + "subjects": { + "round": { + "from": "rounds", + "identified_by": "id", + "has": { + "a": " d.b ", + "b": { + "from": "d.\"x y\".z", + "identified_by": "attestation_id" + }, + "c": "d.m named by $params.k", + "e": "item 3 of d.items", + "f": "d.items[1].y" + } + } + }, + "requirements": { + "r": { + "subject": "round", + "checks": { + "c": "id exists" + } + } + } + }, + "cases": [ + { + "description": "lists every part with its path written as a sentence writes it, and null for an id it doesn't have", + "rules": [ + "report.subjects", + "parts.syntax", + "parts.path" + ], + "input": { + "rounds": [ + { + "id": "a" + } + ] + }, + "report": { + "subjects": { + "round": { + "description": "", + "from": "rounds", + "identified_by": "id", + "parts": { + "a": { + "from": "d.b", + "identified_by": null + }, + "b": { + "from": "d.\"x y\".z", + "identified_by": "attestation_id" + }, + "c": { + "from": "d.m named by $params.k", + "identified_by": null + }, + "e": { + "from": "item 3 of d.items", + "identified_by": null + }, + "f": { + "from": "d.items[1].y", + "identified_by": null + } + } + } + } + }, + "results": [ + { + "cause": "satisfied", + "check": "c", + "inputs": [ + { + "name": "id", + "value": "a" + } + ], + "passed": true, + "requirement": "r", + "subject": { + "id": "a", + "type": "round" + } + } + ] + } + ] + } +] \ No newline at end of file diff --git a/spec/policy/schema.json b/spec/policy/schema.json index 257ced3..122ceae 100644 --- a/spec/policy/schema.json +++ b/spec/policy/schema.json @@ -93,6 +93,83 @@ "of": { "type": "string", "description": "The name of another subject this one builds on. A subject with of has no from or identified_by of its own." + }, + "has": { + "type": "object", + "description": "The subject's parts: named paths inside it, so a check can write a short alias where a long path would go. Each key is the alias.", + "propertyNames": { + "pattern": "^[A-Za-z_][A-Za-z0-9_-]*$", + "not": { + "enum": [ + "a", + "after", + "an", + "and", + "as", + "at", + "before", + "between", + "count", + "days", + "each", + "empty", + "equal", + "every", + "false", + "first", + "hours", + "in", + "input", + "item", + "last", + "less", + "minutes", + "more", + "named", + "not", + "null", + "of", + "on", + "one", + "or", + "params", + "plus", + "seconds", + "some", + "sum", + "to", + "true", + "weeks", + "whose", + "within" + ] + } + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "description": "A path inside the subject, written as in a from line, with no whose, count of or named by each of." + }, + { + "type": "object", + "properties": { + "from": { + "type": "string", + "description": "A path inside the subject, written as above." + }, + "identified_by": { + "type": "string", + "description": "A path inside the part, to the value that identifies it. Nothing reads it yet." + } + }, + "required": [ + "from" + ], + "additionalProperties": false + } + ] + } } }, "additionalProperties": false diff --git a/spec/report/schema.json b/spec/report/schema.json index 799cbbd..e5011f5 100644 --- a/spec/report/schema.json +++ b/spec/report/schema.json @@ -24,7 +24,19 @@ "description": {"type": "string", "description": "The subject's description, or \"\" when it has none."}, "from": {"type": "string", "description": "How the subject is found, as a from line is written, or when it is written wrong."}, "identified_by": {"type": ["string", "null"], "description": "The path of the subject's id, or null when it has none and the whole subject is its id."}, - "parts": {"type": "object", "description": "Always empty for now."} + "parts": { + "type": "object", + "description": "Every part the subject has, including the ones it keeps from a subject it narrows, by alias.", + "additionalProperties": { + "type": "object", + "properties": { + "from": {"type": "string", "description": "The part's path, written as a sentence writes it, or when it is written wrong."}, + "identified_by": {"type": ["string", "null"], "description": "The path of the part's id, when it is written wrong, or null when it has none."} + }, + "required": ["from", "identified_by"], + "additionalProperties": false + } + } }, "required": ["description", "from", "identified_by", "parts"], "additionalProperties": false diff --git a/spec/semantics.md b/spec/semantics.md index 6e2bb9c..446776c 100644 --- a/spec/semantics.md +++ b/spec/semantics.md @@ -389,6 +389,20 @@ A policy has two sections. `subjects` defines each kind of subject once, and `re **[well_formed]** The `$well_formed` row passes when the requirement can be read into the model, names a subject definition that can be read too, and has at least one check. Otherwise it fails with `value`, and the requirement is never met. Its inputs start with `count(checks)` and `require`. A requirement that's written wrong still gets its rows for its subjects. +## Parts + +A subject can have parts: named paths inside itself, each with an alias, written as [syntax.md](syntax.md#parts) says. A part is a short name for a long path, so a policy that reads the same attestation in six checks writes where it is once. + +**[parts.swap]** A part's alias stands for its path inside the subject. A check that starts a path with the alias gives exactly the rows that the same check gives with the path written out in its place: the same `passed`, the same `cause` and the same values. A part that is missing, `null` or the wrong type fails the check as the full path would, with `absent`, `null`, `unusable`, or `not_an_object` when the subject isn't an object, and a `named by` in the path that can't be read fails it as it would there. Only the names differ. A row's `expression`, its `inputs` and the paths of its `failed_items` keep the alias, see [name.part](syntax.md#naming-a-path). + +**[parts.where]** An alias is read wherever a bare first key reads the subject (see [sentence.path.start](sentences.md#paths)): in the subject's checks, in the assertions of a `for` line and in a later item of it whose path starts at the subject, in the conditions of the subject's own `from` line, and in the conditions of a subject that narrows it. It isn't read where a bare first key reads an item instead: in a `whose`, and in the argument of a one-line `every` or `some`, see [sentence.argument.item](sentences.md#every-some-and-whose). There `cov` is a field of the item, as it is for a subject with no part called `cov`. + +**[parts.shadow]** An alias shadows a field of the subject with the same name, so `cov.x` reads the part and not the field `cov`. The dot in front of a first key, like `.cov`, only marks a field that is named like a word of the grammar. It doesn't get around an alias, so a field named like a part of the same subject can't be read by a sentence: name the part something else. + +**[parts.narrow]** A subject that narrows another keeps its parts, and can add its own. Its checks and its conditions read them like parts of its own, and the report lists them (see [report.subjects](#statuses)). + +**[parts.chain]** The alias of a chained subject's parent gives everything the parent has, its fields and its parts, so `artifact.pr_attestation.approvers` reads the part `pr_attestation` of `artifact`, and a `from` line can start from one: `from: artifact.pr_attestation.pull_requests`. A part is only reached through its subject. A bare `pr_attestation` in a check of the chained subject is its own field, and a part of the chained subject isn't one of the parent's. + ## Scope **[scope.filters]** A subject is in scope when it passes every filter in its definition's `applies_to`. Each subject gets one `$applies` row when the definition has filters, and a subject that's out of scope gets no other rows, but its `$applies` row stays. @@ -416,7 +430,7 @@ A policy has two sections. `subjects` defines each kind of subject once, and `re - `description`, the subject's description, or `""` when it has none or it isn't a string - `from`, how the subject is found, written as a `from` line: the line itself, printed the same way whatever spaces it was written with, or for a subject written as a list, its path followed by `whose` and its conditions, which are sorted by name. A naming step at the end of the path is shown as `named by each of ` when its keys come from a ref, as `` when they're listed, and as `` when it has none. Anything with no sentence form is shown in angle brackets, never left out: a condition that can't be written as a sentence is shown as ``, and `applies_to` that isn't an object as ``. A subject without `from` reads the input field named like it. A `from` that is written wrong is ``. - `identified_by`, the path of the subject's id, or `null` when it has none, because the whole subject is its id. A subject that narrows another has the id of the one it narrows. An id that is written wrong is ``. -- `parts`, always `{}` for now +- `parts`, every part the subject has, including the ones it keeps from a subject it narrows, by alias. Each is `{"from": ..., "identified_by": ...}`: `from` is the part's path written the way a sentence writes it, so ` dd.b ` is `dd.b` and `x[$params.k]` is `x named by $params.k`, and `identified_by` is the path of the part's id, or `null` when it has none. A part written as one line has no id. A part written wrong is listed as best it can be: `` for a path that can't be read and `` for an id that can't, and a part that is neither a string nor an object has `` and `null`. A subject that is written wrong still has its entry. diff --git a/spec/sentences.md b/spec/sentences.md index 46d0d1e..dafe86e 100644 --- a/spec/sentences.md +++ b/spec/sentences.md @@ -69,7 +69,9 @@ peer_approval: **[sentence.path.string]** A path is keys joined by `.`, like `git_commit_info.sha1`. A key is written bare when it starts with an ASCII letter or `_` and the rest is ASCII letters, digits, `_` and `-`, so Kosli's `secrets-scan`, `pull-request` and `coverage-verification` need no quotes. Any other key is quoted as a JSON string, like `metadata.labels."app.kubernetes.io/name"`, and so is any key with a `$` in it, like `"$schema"` or `"foo$bar"`. The report prints keys by the same rule, so `$` only ever starts `$params` or `$input`, and a path from a row can be pasted into a policy. -**[sentence.path.start]** A bare first key is a field of the subject. Inside `whose`, and in the argument of a one-line `every` or `some`, it's a field of the item instead (see [sentence.argument.item](#every-some-and-whose)). An alias given by `as` on a `for` line is a bare word too, and it shadows a field with the same name. `$params` starts at the params and `$input` at the input. Any other `$` is written wrong, with `unknown name $`. +**[sentence.path.start]** A bare first key is a field of the subject. Inside `whose`, and in the argument of a one-line `every` or `some`, it's a field of the item instead (see [sentence.argument.item](#every-some-and-whose)). An alias given by `as` on a `for` line is a bare word too, and it shadows a field with the same name, and so does a part's alias (see [parts.shadow](semantics.md#parts)). `$params` starts at the params and `$input` at the input. Any other `$` is written wrong, with `unknown name $`. + +**[sentence.path.part]** A part of the subject, declared with `has` (see [parts.syntax](syntax.md#parts)), is read by its alias as a bare first key, wherever a bare first key reads the subject ([parts.where](semantics.md#parts)), and gives the rows of the path it stands for ([parts.swap](semantics.md#parts)). Inside `whose` and in the argument of a one-line `every` or `some` it isn't read, because there the first key is the item's. `named by` a bare word that is a part's alias is written wrong, with `named by reads a part, which isn't supported yet`, because it would otherwise read the subject's field of that name. A part's own path is written as a `from` line's path is, so it never starts with an alias. **[sentence.path.index]** `item 3 of items` reads the third item of `items`, counting from 1, and `first of items` and `last of items` read the ends. The path after `of` is the whole rest of the path, so `item 3 of pull_request.commits` picks the third commit, and a field of that item has no word form: write `pull_request.commits[2].sha` with brackets, which count from 0 as [sentence.path.brackets](#paths) says. `item 0 of items` is written wrong. The report prints the word form when the position ends the path, so `commits[2]` prints as `item 3 of commits`, and prints the brackets when a key follows them. No policy in the corpus needs a position. @@ -191,7 +193,7 @@ Items nest left to right, so a later one can use an earlier alias, as `pr.approv **[sentence.for.scope]** In `assert` and in a later item, a bare first key is the subject's field and an item is reached by its alias. Inside an item's own `whose`, a bare key is that item's field. So in `for: some approvals whose role is "qa" as approval` with `assert: approval.approved_at is before started_at`, `role` is the approval's and `started_at` the subject's. -**[sentence.for.alias]** An alias is a bare word that isn't `params`, `input` or a word of the grammar, like `every`, `as` or `empty`. For the rest of the check it shadows a field of the subject with the same name. An alias given twice is written wrong, and so is a bare word after `named by` that no `for` line gives, with `unknown alias `. +**[sentence.for.alias]** An alias is a bare word that isn't `params`, `input` or a word of the grammar, like `every`, `as` or `empty`. For the rest of the check it shadows a field of the subject with the same name. An alias given twice is written wrong. So is one that is also the alias of a part of the subject, with `alias given twice`, and so is a bare word after `named by` that no `for` line gives, with `unknown alias `. **[sentence.for.fail]** Each item fails closed as a one-line `every` or `some` does: `some` over an empty list fails, `every` over one fails unless the item says `if any`, a missing or non-list path fails as `absent`, `null` or `unusable`, and an item a `whose` can't decide fails the try with its cause. diff --git a/spec/syntax.md b/spec/syntax.md index 607f58d..4be8c15 100644 --- a/spec/syntax.md +++ b/spec/syntax.md @@ -6,12 +6,38 @@ Draft 0.1. This file covers the syntax policies use today, where a check is a JS ## Policies -**[policy.shape]** A policy is an object with two sections. `subjects` maps each subject's name to its definition, with `from`, `identified_by`, `applies_to` and `description`, or to `null` for a subject with none of them (see [subjects.no_from](semantics.md#requirements)). `requirements` maps each requirement's name to its definition, with `subject`, `checks`, `require`, `min_subjects`, `description` and `meta`. `subject` is the name of an entry in `subjects`. The policy can also have a `description` and a `meta` of its own, next to its sections. A subject definition can also build on another with `of`, which the spec doesn't cover yet (see [OPEN.md](OPEN.md)). +**[policy.shape]** A policy is an object with two sections. `subjects` maps each subject's name to its definition, with `from`, `identified_by`, `applies_to`, `has` and `description`, or to `null` for a subject with none of them (see [subjects.no_from](semantics.md#requirements)). `requirements` maps each requirement's name to its definition, with `subject`, `checks`, `require`, `min_subjects`, `description` and `meta`. `subject` is the name of an entry in `subjects`. The policy can also have a `description` and a `meta` of its own, next to its sections. A subject definition can also build on another with `of`, which the spec doesn't cover yet (see [OPEN.md](OPEN.md)). **[sentence_ops.syntax]** `empty_string_or_list`, `not_empty_string_or_list`, `not_in`, `has_type`, `all_of`, `every`, `some` and `for`, the comparisons `starts_with`, `ends_with`, `in`, `not_in`, `includes`, `excludes`, `includes_all`, `excludes_all`, `matches_any` and `not_matches_any` of `compare`, and the `compare` sides `count_of`, `sum_of` and an `add` with `text`, are only read from sentences. A check in this syntax with one of them as its `op` is written wrong, with `unknown op `, a `compare` with one of them as its `cmp` with `invalid cmp`, and one with one of them as a side with `invalid left` or `invalid right`, as a misspelt operator is, wherever it is and even when it's a copy of what a sentence in the same policy reads into. Whether a check was written as a sentence decides, not what it looks like. **[policy.old_fields]** A requirement that has `from`, `identified_by`, `applies_to` or `subject_type`, or any other field it doesn't take, is written wrong, and the `$well_formed` row lists the field with `["unknown field"]`. A subject definition with a field it doesn't take is written wrong the same way. So is one with `id`, the old name for `identified_by`. +## Parts + +A subject can have parts, named paths inside itself. What a part means is in [semantics.md](semantics.md#parts), and how a sentence reads one in [sentences.md](sentences.md#paths). + +**[parts.syntax]** `has` on a subject definition is an object. Each key is a part's alias, and each value is the part: a path in a string, or an object with `from`, a path in a string, and optionally `identified_by`, a path in a string that is read inside the part. Nothing reads `identified_by` yet, but the report shows it. A part is written the same way whichever syntax the subject's checks use. A subject that narrows another, with `of` or a from line, can have `has` too. + +```yaml +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 +``` + +**[parts.path]** A part's path is written as a path in a `from` line is (see [sentence.path.string](sentences.md#paths)): keys joined by `.`, keys in double quotes, positions, and `named by` a key of `$params` or `$input`. It is a path inside the subject, so it can't start at `$params` or `$input`, and it has no `whose`, `count of` or `named by each of`. A part is as often a path to one value as to a whole attestation. + +**[parts.alias]** An alias is a bare word that starts with an ASCII letter or `_` and goes on with ASCII letters, digits, `_` or `-`. It isn't a word of the grammar (see [sentence.words](sentences.md#words-a-field-cant-be-called)), `params` or `input`, or it's written wrong with `invalid alias`. Two aliases with the same name are written wrong with `alias given twice`: two parts of one subject, a part and one a subject keeps from a subject it narrows, a part and a `for` alias in a check of the subject, a part and the name of the subject a chained subject reads from, and in the old syntax a part and an `as` or `each_as` alias. + +**[parts.written_wrong]** A part written wrong makes its subject written wrong, so the `$well_formed` row of every requirement that uses the subject fails. It lists the problems under the part's alias, like `subjects.round.has.coverage`, and under `subjects.round.has` when `has` isn't an object, which is written `not an object`. A part that's neither a string nor an object is written `not a string or an object`, and an object without `from` `missing from`. A field other than `from` and `identified_by` is written `unknown field `, and a `from` or `identified_by` that isn't a string `invalid from` or `invalid identified_by`. A path that can't be read is written `from: ` or `identified_by: `, with the problems of a path in a `from` line, or `a part is a path: whose and named by each of can't go in it` and `a part reads inside its subject, so it can't start at $params or $input`. A bad alias is written `invalid alias` and a repeated one `alias given twice`. A part written wrong is never read as an alias, and the report still lists it, see [report.subjects](semantics.md#statuses). + +**[parts.old_syntax]** A check written with `op` reads a part as `$alias` in the first step of a path, like `["$coverage", "attestation_data", "hunks_total"]`, wherever it can read an alias given by `as`, including in the paths of a derived value and of a custom operator's `inputs`, in a substitute, and in the filters of `applies_to`. Inside an item of `all` or `any`, `$alias` still reads the subject, as any alias from outside the item does. Any other `$name` that nothing gave is `unknown alias $` as before. + ## Paths **[path.not_a_list]** A path is a list of steps. A path that isn't a list is written wrong, even a string that names one key. @@ -78,6 +104,8 @@ A row's `inputs`, a ref's entry in `$refs`, an item in `failed_items` and a path **[name.list]** Inside `all` or `any`, a path is named after the list: `cs[].s`, or `cs[]` for the items themselves. With `each`, the inner lists are `prs[].cs`. +**[name.part]** A path that starts with a part's alias is named with the alias, and the steps the alias stands for aren't named, so the full path `compliance_status.attestations_statuses.coverage-verification.attestation_data.hunks_total` is `coverage.attestation_data.hunks_total` when it was written with the alias. In the old syntax the alias keeps its `$`, like `$coverage.attestation_data.hunks_total`, as `$pr.author` does. A path read through a chained subject's parent, like `artifact.pr_attestation.approvers`, is named the same way, and a list the alias stands for is `coverage[]` when it's walked. A path written in full is named in full, whether or not a part stands for it. Only the names differ: a row's `expression`, `inputs` and the paths of its `failed_items` use the alias, and the values in them are the same. + ## Expressions Each check's definition in the report has an `expression` that says what it checks. diff --git a/spec_test.rego b/spec_test.rego index 08aa356..626e029 100644 --- a/spec_test.rego +++ b/spec_test.rego @@ -79,7 +79,7 @@ _case_names(topic) := {concat(" / ", [topic, group.description, c.description]) test_every_known_difference_names_a_case if known_differences - union({_case_names(topic) | some topic, _ in data.spec.cases}) == set() -_tested_topics := {"any_of", "compare", "compare_time", "contains", "custom", "derived", "empty", "equals", "for", "in", "lists", "missing", "non_empty_string", "patterns", "present", "range", "requirements", "sentences", "subjects", "substitutes", "walks"} +_tested_topics := {"any_of", "compare", "compare_time", "contains", "custom", "derived", "empty", "equals", "for", "in", "lists", "missing", "non_empty_string", "parts", "patterns", "present", "range", "requirements", "sentences", "subjects", "substitutes", "walks"} test_every_folder_of_cases_has_a_test_of_its_own if {t | some t, _ in data.spec.cases} == _tested_topics @@ -109,6 +109,8 @@ test_the_missing_cases_pass if _topic_failures("missing") == _known_in("missing" test_the_non_empty_string_cases_pass if _topic_failures("non_empty_string") == _known_in("non_empty_string") +test_the_parts_cases_pass if _topic_failures("parts") == _known_in("parts") + test_the_patterns_cases_pass if _topic_failures("patterns") == _known_in("patterns") test_the_present_cases_pass if _topic_failures("present") == _known_in("present") From 7adc92a90b066b8d1e784f5e81f69b718dfc094c Mon Sep 17 00:00:00 2001 From: Julien Biezemans Date: Sun, 18 Oct 2026 13:16:43 +0200 Subject: [PATCH 2/3] core: never hide a position in a part's path, and show a chained from as written A walk that goes through a list in the middle of a part's path named a failed item with the alias and dropped the position in that list, like `x.sub[1].y`. Now every name from such a walk uses the full path, like `pull_requests[2].reviews[0].state`. Rows are unchanged. A chained subject whose `from` starts at its parent's part showed the full path in its `$min_subjects` and `$unique_ids` expressions. It now shows the line as written, like `artifact.pr_attestation.pull_requests`. Changes the report: only for a policy that uses `has:`. Co-Authored-By: Claude Sonnet 5.5 --- ergo.rego | 104 +++++++++++++++++++++++++--- ergo_test.rego | 71 +++++++++++++++++++ spec/OPEN.md | 2 - spec/cases/parts/cases.json | 134 ++++++++++++++++++++++++++++++++++++ spec/syntax.md | 4 +- 5 files changed, 300 insertions(+), 15 deletions(-) diff --git a/ergo.rego b/ergo.rego index f1fd7ca..fce4626 100644 --- a/ergo.rego +++ b/ergo.rego @@ -1174,21 +1174,36 @@ _off_subject(path) if { not _subject_part(path) } -_visible(path) := array.concat(_visible_root(_part_of(path)), array.slice(path, 1 + _part_of(path).n, count(path))) if _part_start(path) +_visible(path) := array.concat(_visible_root(_part_of(path)), array.slice(path, 1 + max([_part_of(path).n, 0]), count(path))) if _part_start(path) _visible(path) := path if not _part_start(path) _visible_root(m) := [m.alias] if { m.of == null m.old == false + m.n >= 0 +} + +_visible_root(m) := [] if { + m.of == null + m.n < 0 } _visible_root(m) := [concat("", ["$", m.alias])] if { m.of == null m.old == true + m.n >= 0 } -_visible_root(m) := [concat("", ["$", m.of]), m.alias] if m.of != null +_visible_root(m) := [concat("", ["$", m.of]), m.alias] if { + m.of != null + m.n >= 0 +} + +_visible_root(m) := [concat("", ["$", m.of])] if { + m.of != null + m.n < 0 +} _keys_of(path) := ks if { is_array(path) @@ -4056,6 +4071,22 @@ _subject_item_name(req) := concat("", [_steps_name(_from_steps(_from_of(req))), not _stepped(req) _from_well_formed(req) _from_of(req) != [] + not _chain_part(req) +} + +_chain_part(req) if data.ergo_subject_sentences.from_parts[req.subject] + +_subject_item_name(req) := concat("", [_chain_part_name(req), "[]"]) if { + not _stepped(req) + _from_well_formed(req) + _from_of(req) != [] + _chain_part(req) +} + +_chain_part_name(req) := concat("", [_s_key_text(data.ergo_subject_sentences.chains[req.subject]), ".", _s_key_text(alias), _tail_after_part(req, alias)]) if alias := data.ergo_subject_sentences.from_parts[req.subject] + +_tail_after_part(req, alias) := concat("", [_step_name(i + 2, rest[i]) | some i in _names(rest)]) if { + rest := array.slice(_from_of(req), count(data.ergo_subject_sentences.parts[data.ergo_subject_sentences.chains[req.subject]][alias].from.path), count(_from_of(req))) } _subject_item_name(req) := "$input" if { @@ -4071,6 +4102,13 @@ _matching_count_name(req) := concat("", ["count(matching(", _from_text(req), ")) _from_text(req) := _from_name(_from_path(req)) if { _from_well_formed(req) _from_path(req) != [] + not _chain_part(req) +} + +_from_text(req) := _chain_part_name(req) if { + _from_well_formed(req) + _from_path(req) != [] + _chain_part(req) } _from_text(req) := "$input" if { @@ -6354,7 +6392,7 @@ _s_from_used(def, read) if { not "of" in object.keys(def) } -_s_from_read(name, fl, subjects, chains) := {"problems": problems, "fields": fields, "parent": parent} if { +_s_from_read(name, fl, subjects, chains) := object.union({"problems": problems, "fields": fields, "parent": parent}, _p_from_part(name, fl, subjects, chains.parts)) if { "path" in object.keys(fl) conds := [_p_applied(_s_check([_s_cond_text(c)], {}, set()), _p_own_context(name, chains.parts)) | some c in object.get(fl, "where", [])] problems := sort(((((_s_chain_unsupported(name, fl, subjects, chains.descend) | _s_from_start_problems(fl)) | _s_from_paths_unsupported({"path": fl.path})) | _s_elapsed_unsupported({"where": object.get(fl, "where", [])})) | {concat("", ["whose: ", p]) | some c in conds; some p in object.get(c, "$sentence", [])})) @@ -6362,6 +6400,14 @@ _s_from_read(name, fl, subjects, chains) := {"problems": problems, "fields": fie parent := _s_descend_parent(name, fl, subjects) } else := {"problems": object.get(fl, "problems", ["doesn't parse"]), "fields": {}} +_p_from_part(name, fl, subjects, eff) := {"part": rest[0]} if { + _s_descends(name, fl, subjects) + rest := array.slice(_s_model_path(fl.path), 1, count(fl.path.steps)) + count(rest) > 0 + is_string(rest[0]) + rest[0] in object.keys(_p_steps_of(object.get(eff, fl.path.steps[0], {}))) +} else := {} + _s_source(name, fl, subjects, eff) := {"of": fl.path.steps[0]} if { _s_narrows(name, fl, subjects) } else := {"from": array.concat(_s_from_steps(_p_descend_steps(array.slice(_s_model_path(fl.path), 1, count(fl.path.steps)), object.get(eff, fl.path.steps[0], {}))), _s_each_step(fl))} if { @@ -6436,9 +6482,9 @@ _s_id_field(read) := {"identified_by": read.identified_by.path} if "path" in obj else := {} -_s_subject_sentences(subjects, reads, parts) := {"parts": parts.effective, "has_problems": parts.problems, "chains": {name: p | some name, r in reads; _s_from_used(subjects[name], r); p := object.get(r, "parent", null); p != null}, "narrowed": {name | some name, r in reads; _s_from_used(subjects[name], r); "of" in object.keys(r.fields)}, "filters": {[name, n] | some name, r in reads; _s_from_used(subjects[name], r); some n, _ in object.get(r.fields, "applies_to", {})}, "problems": {name: probs | some name, def in subjects; probs := _s_subject_problems(name, def, reads); probs != {}}} if is_object(subjects) +_s_subject_sentences(subjects, reads, parts) := {"from_parts": {name: r.part | some name, r in reads; _s_from_used(subjects[name], r)}, "parts": parts.effective, "has_problems": parts.problems, "chains": {name: p | some name, r in reads; _s_from_used(subjects[name], r); p := object.get(r, "parent", null); p != null}, "narrowed": {name | some name, r in reads; _s_from_used(subjects[name], r); "of" in object.keys(r.fields)}, "filters": {[name, n] | some name, r in reads; _s_from_used(subjects[name], r); some n, _ in object.get(r.fields, "applies_to", {})}, "problems": {name: probs | some name, def in subjects; probs := _s_subject_problems(name, def, reads); probs != {}}} if is_object(subjects) -_s_subject_sentences(subjects, _, _) := {"parts": {}, "has_problems": {}, "chains": {}, "narrowed": set(), "filters": set(), "problems": {}} if not is_object(subjects) +_s_subject_sentences(subjects, _, _) := {"from_parts": {}, "parts": {}, "has_problems": {}, "chains": {}, "narrowed": set(), "filters": set(), "problems": {}} if not is_object(subjects) _s_subject_problems(name, def, reads) := {f: ps | some f in ["from", "identified_by"]; ps := _s_field_problems(name, def, f, reads); ps != []} if is_object(def) @@ -7221,7 +7267,7 @@ _walk_level(entries, keys, i, _) := {"entries": entries, "fails": [], "walked": _walk_level(entries, keys, i, if_any) := {"entries": [_walk_step(e, keys, i) | some e in expanded], "fails": fails, "walked": walked} if { i <= count(keys) walked := _walks_here(entries, keys, i) - expanded := [x | some e in entries; some x in _walk_expand(e, walked)] + expanded := [x | some e in entries; some x in _walk_expand(e, walked, i)] fails := [f | some e in entries; some f in _walk_expand_fail(e, walked, if_any)] } @@ -7235,11 +7281,29 @@ _walks_here(entries, keys, i) if { not is_number(keys[i]) } else := false -_walk_expand(e, false) := [e] +_walk_expand(e, false, _) := [e] + +_walk_expand(e, true, i) := [{"c": array.concat(_walk_unaliased(e.c, i), [j]), "v": x, "i": array.concat(_walk_unaliased(e.c, i), [j]), "iv": x, "r": []} | some j, x in e.v] if is_array(e.v) + +_walk_expand(e, true, _) := [] if not is_array(e.v) -_walk_expand(e, true) := [{"c": array.concat(e.c, [j]), "v": x, "i": array.concat(e.c, [j]), "iv": x, "r": []} | some j, x in e.v] if is_array(e.v) +_walk_unaliased(c, i) := array.concat([_p_marker(m.alias, -1, m.of, m.old)], array.slice(c, 1, count(c))) if { + _part_start(c) + m := _part_of(c) + m.n > 0 + i > 0 + i < m.n +} + +_walk_unaliased(c, i) := c if not _walk_inside_part(c, i) -_walk_expand(e, true) := [] if not is_array(e.v) +_walk_inside_part(c, i) if { + _part_start(c) + m := _part_of(c) + m.n > 0 + i > 0 + i < m.n +} _walk_expand_fail(_, false, _) := [] @@ -7384,9 +7448,22 @@ _walk_failed(cause, fails) := [{"path": _path_name(f.path), "item": _path_name(f _walk_inputs(check, w) := [{"name": _walk_name(check.path, w.walked), "value": [_walk_shown(e.v) | some e in w.items]}] -_walk_name(path, walked) := concat("", array.concat(array.concat(_walk_root_name(path), [concat("", [_walk_mark(i + _walk_hidden(path), walked), _step_name(i + count(_walk_root_name(path)), step)]) | some i, step in array.slice(_walk_steps(path), _walk_hidden(path), count(_walk_steps(path)))]), [_walk_mark(count(_walk_steps(path)), walked)])) +_walk_name(path, walked) := _walk_shown_name(_walk_unaliased(path, _walk_crossed(path, walked)), walked) -_walk_hidden(path) := _part_of(path).n if _part_start(path) +_walk_crossed(path, walked) := min({i | some i in walked; i > 0; i < _part_of(path).n}) if _part_start(path) + +_walk_crossed(path, walked) := 0 if not _walk_crosses(path, walked) + +_walk_crosses(path, walked) if { + _part_start(path) + some i in walked + i > 0 + i < _part_of(path).n +} + +_walk_shown_name(path, walked) := concat("", array.concat(array.concat(_walk_root_name(path), [concat("", [_walk_mark(i + _walk_hidden(path), walked), _step_name(i + count(_walk_root_name(path)), step)]) | some i, step in array.slice(_walk_steps(path), _walk_hidden(path), count(_walk_steps(path)))]), [_walk_mark(count(_walk_steps(path)), walked)])) + +_walk_hidden(path) := max([_part_of(path).n, 0]) if _part_start(path) _walk_hidden(path) := 0 if not _part_start(path) @@ -7407,6 +7484,11 @@ _walk_root_name(path) := [_plain_step_name(0, root[0]), _step_name(1, root[1])] count(root) == 2 } +_walk_root_name(path) := [] if { + _part_start(path) + count(_visible_root(_part_of(path))) == 0 +} + _walk_root_name(path) := [] if not _named(path) _walk_steps(path) := array.slice(path, count(_walk_root(path)), count(path)) diff --git a/ergo_test.rego b/ergo_test.rego index 76d8e74..db4eeda 100644 --- a/ergo_test.rego +++ b/ergo_test.rego @@ -11286,3 +11286,74 @@ test_a_part_with_a_key_that_is_not_a_string_is_written_wrong_and_is_not_listed i rep.subjects.round.parts == {"ok": {"from": "dd.d", "identified_by": null}} _parts_row(rep, "c").passed } + +_hidden_doc := {"pull_requests": [ + {"reviews": [{"state": "ok"}]}, + {"reviews": [{"state": "ok"}, {"state": "ok"}]}, + {"reviews": [{"state": "bad"}, {"state": "ok"}]}, +]} + +test_a_position_in_a_list_inside_a_part_is_never_hidden_so_the_failed_item_names_the_full_path if { + checks := { + "every": "every reviews.state is \"ok\"", + "count": "count of reviews is 4", + "for": {"for": "every reviews as r", "assert": "r.state is \"ok\""}, + } + full := { + "every": "every pull_requests.reviews.state is \"ok\"", + "count": "count of pull_requests.reviews is 4", + "for": {"for": "every pull_requests.reviews as r", "assert": "r.state is \"ok\""}, + } + alias := ergo.report(_hidden_doc, _swap_policy2({"reviews": "pull_requests.reviews"}, checks), {}) + plain := ergo.report(_hidden_doc, _swap_policy2({}, full), {}) + row := _parts_row(alias, "every") + row.failed_items == [{"cause": "value", "item": "item 1 of pull_requests[2].reviews", "path": "pull_requests[2].reviews[0].state", "value": "bad"}] + row.inputs == [{"name": "pull_requests[].reviews[].state", "value": ["ok", "ok", "ok", "bad", "ok"]}] + _parts_row(alias, "count").inputs[1].name == "pull_requests[].reviews[]" + _parts_row(alias, "for").failed_items[0].item == "item 1 of pull_requests[2].reviews" + _parts_row(alias, "for").inputs[0].name == "pull_requests[].reviews[]" + [[r.check, r.passed, r.cause] | some r in alias.results; r.check in {"every", "count", "for"}] == [[r.check, r.passed, r.cause] | some r in plain.results; r.check in {"every", "count", "for"}] + alias.requirements.r.checks.every.expression == "every reviews.state is \"ok\"" +} + +_swap_policy2(has, checks) := { + "subjects": {"round": _swap_subject2(has)}, + "requirements": {"r": {"subject": "round", "checks": checks}}, +} + +_swap_subject2(has) := {"from": [], "has": has} if count(has) > 0 + +_swap_subject2(has) := {"from": []} if count(has) == 0 + +test_a_position_in_a_list_inside_a_parents_part_is_never_hidden_either if { + subjects := { + "artifact": {"from": "trail.artifacts named by $params.art", "identified_by": "fingerprint", "has": {"rv": "pull_requests.reviews"}}, + "pull request": {"from": "artifact.items", "identified_by": "state"}, + } + doc := {"trail": {"artifacts": {"web": {"fingerprint": "sha-1", "items": [{"state": "s"}], "pull_requests": [{"reviews": [{"state": "ok"}]}, {"reviews": [{"state": "bad"}]}]}}}} + checks := {"c": "every artifact.rv.state is \"ok\"", "n": "count of artifact.rv is 2"} + rep := ergo.report(doc, {"subjects": subjects, "requirements": {"r": {"subject": "pull request", "checks": checks}}}, {"art": "web"}) + row := _parts_row(rep, "c") + row.failed_items == [{"cause": "value", "item": "item 1 of artifact.pull_requests[1].reviews", "path": "artifact.pull_requests[1].reviews[0].state", "value": "bad"}] + row.inputs == [{"name": "artifact.pull_requests[].reviews[].state", "value": ["ok", "bad"]}] +} + +test_a_chained_subject_that_starts_at_its_parents_part_shows_its_from_as_written_in_its_expressions if { + rep := ergo.report(_parts_chain_doc, _parts_chain, {"art": "web"}) + checks := rep.requirements.r.checks + checks["$min_subjects"].expression == "count(matching(artifact.pra.pull_requests)) >= 1" + checks["$unique_ids"].expression == "count(repeated(ids(artifact.pra.pull_requests))) == 0" + deeper := object.union(_parts_chain.subjects, {"pull request": {"from": "artifact.pra.pull_requests", "identified_by": "url"}}) + policy := {"subjects": deeper, "requirements": {"r": {"subject": "pull request", "checks": {"c": "url exists"}}}} + ergo.report(_parts_chain_doc, policy, {"art": "web"}).requirements.r.checks["$min_subjects"].expression == "count(matching(artifact.pra.pull_requests)) >= 1" +} + +test_a_chained_subject_that_starts_deeper_in_its_parents_part_keeps_the_rest_of_the_path_in_its_expressions if { + subjects := { + "artifact": {"from": "trail.artifacts named by $params.art", "identified_by": "fingerprint", "has": {"pra": "attestations.pr"}}, + "pull request": {"from": "artifact.pra.pull_requests[0].list", "identified_by": "url"}, + } + doc := {"trail": {"artifacts": {"web": {"fingerprint": "sha-1", "attestations": {"pr": {"pull_requests": [{"list": [{"url": "a"}]}]}}}}}} + rep := ergo.report(doc, {"subjects": subjects, "requirements": {"r": {"subject": "pull request", "checks": {"c": "url exists"}}}}, {"art": "web"}) + rep.requirements.r.checks["$min_subjects"].expression == "count(matching(artifact.pra.pull_requests[0].list)) >= 1" +} diff --git a/spec/OPEN.md b/spec/OPEN.md index b555406..edbfd6f 100644 --- a/spec/OPEN.md +++ b/spec/OPEN.md @@ -50,8 +50,6 @@ A subject's parts (`has`, see [syntax.md](syntax.md#parts)) leave a few things o - `named by` a bare word that is a part's alias is written wrong. It would have to read the part's value and use it as a key, as it does for a `for` alias, and nothing says yet whether a part that is a path with its own `named by` can be used that way. - `identified_by` of a part is read and shown, but nothing uses it. A later change is meant to tag the values a check reads with the id of the part they sit in, so a report can say which one. -- When `every`, `some`, `count of` or `sum of` walks a list in the middle of a part's path, not at its end, the rows are the full path's, but the name of a failed item shows the alias and the steps after the walked list's position, like `x.sub[1].y` for a part `x` that stands for `items.sub`. The position in the hidden list isn't shown. A part that ends at its list, like `items`, names every item. -- A part's path in the `$min_subjects` and `$unique_ids` expressions of a chained subject whose `from` starts at a part of its parent is shown in full, because those expressions show the subject's `from` as a path and not as a line. ## Time limits diff --git a/spec/cases/parts/cases.json b/spec/cases/parts/cases.json index a6e30ea..ed4d4c4 100644 --- a/spec/cases/parts/cases.json +++ b/spec/cases/parts/cases.json @@ -1681,6 +1681,12 @@ } } }, + "expressions": { + "r": { + "$min_subjects": "count(matching(artifact.pra.pull_requests)) >= 1", + "$unique_ids": "count(repeated(ids(artifact.pra.pull_requests))) == 0" + } + }, "report": { "subjects": { "artifact": { @@ -2836,5 +2842,133 @@ ] } ] + }, + { + "description": "a part whose path goes through a list", + "policy": { + "subjects": { + "s": { + "from": [], + "has": { + "reviews": "pull_requests.reviews" + } + } + }, + "requirements": { + "r": { + "subject": "s", + "checks": { + "all_ok": "every reviews.state is \"ok\"" + } + } + } + }, + "cases": [ + { + "description": "names a failed item with the full path, so no position is hidden", + "rules": [ + "name.part", + "parts.swap" + ], + "input": { + "pull_requests": [ + { + "reviews": [ + { + "state": "ok" + } + ] + }, + { + "reviews": [ + { + "state": "ok" + }, + { + "state": "ok" + } + ] + }, + { + "reviews": [ + { + "state": "bad" + }, + { + "state": "ok" + } + ] + } + ] + }, + "expressions": { + "r": { + "all_ok": "every reviews.state is \"ok\"" + } + }, + "results": [ + { + "cause": "value", + "check": "all_ok", + "failed_items": [ + { + "cause": "value", + "item": "item 1 of pull_requests[2].reviews", + "path": "pull_requests[2].reviews[0].state", + "value": "bad" + } + ], + "inputs": [ + { + "name": "pull_requests[].reviews[].state", + "value": [ + "ok", + "ok", + "ok", + "bad", + "ok" + ] + } + ], + "passed": false, + "requirement": "r", + "subject": { + "id": { + "pull_requests": [ + { + "reviews": [ + { + "state": "ok" + } + ] + }, + { + "reviews": [ + { + "state": "ok" + }, + { + "state": "ok" + } + ] + }, + { + "reviews": [ + { + "state": "bad" + }, + { + "state": "ok" + } + ] + } + ] + }, + "type": "s" + } + } + ] + } + ] } ] \ No newline at end of file diff --git a/spec/syntax.md b/spec/syntax.md index 4be8c15..8372a7a 100644 --- a/spec/syntax.md +++ b/spec/syntax.md @@ -104,7 +104,7 @@ A row's `inputs`, a ref's entry in `$refs`, an item in `failed_items` and a path **[name.list]** Inside `all` or `any`, a path is named after the list: `cs[].s`, or `cs[]` for the items themselves. With `each`, the inner lists are `prs[].cs`. -**[name.part]** A path that starts with a part's alias is named with the alias, and the steps the alias stands for aren't named, so the full path `compliance_status.attestations_statuses.coverage-verification.attestation_data.hunks_total` is `coverage.attestation_data.hunks_total` when it was written with the alias. In the old syntax the alias keeps its `$`, like `$coverage.attestation_data.hunks_total`, as `$pr.author` does. A path read through a chained subject's parent, like `artifact.pr_attestation.approvers`, is named the same way, and a list the alias stands for is `coverage[]` when it's walked. A path written in full is named in full, whether or not a part stands for it. Only the names differ: a row's `expression`, `inputs` and the paths of its `failed_items` use the alias, and the values in them are the same. +**[name.part]** A path that starts with a part's alias is named with the alias, and the steps the alias stands for aren't named, so the full path `compliance_status.attestations_statuses.coverage-verification.attestation_data.hunks_total` is `coverage.attestation_data.hunks_total` when it was written with the alias. In the old syntax the alias keeps its `$`, like `$coverage.attestation_data.hunks_total`, as `$pr.author` does. A path read through a chained subject's parent, like `artifact.pr_attestation.approvers`, is named the same way, and a list the alias stands for is `coverage[]` when it's walked. A path written in full is named in full, whether or not a part stands for it. A position is never hidden: when a walk (`every`, `some`, `count of`, `sum of`, a `for` line) goes through a list inside a part's path, not at its end, every name from that walk uses the full path, like `pull_requests[2].reviews[0].state` for a part `reviews` that stands for `pull_requests.reviews`. Only the names differ: a row's `expression`, `inputs` and the paths of its `failed_items` use the alias, and the values in them are the same. ## Expressions @@ -146,7 +146,7 @@ Each check's definition in the report has an `expression` that says what it chec ## Checks ergo adds -**[added.text]** The checks ergo adds have these descriptions and expressions, where `` is the name of the requirement's subject, and `` is the name of the subject's `from`, or `$input` when it has no steps: +**[added.text]** The checks ergo adds have these descriptions and expressions, where `` is the name of the requirement's subject, and `` is the name of the subject's `from`, or `$input` when it has no steps. A subject read from a part of its parent shows its `from` as written, with the parent's name and the part's alias, like `artifact.pr_attestation.pull_requests`: | Check | Description | Expression | | --------------- | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | From c0d6048136200eea936f66cf4d9d08d75dc9515d Mon Sep 17 00:00:00 2001 From: Julien Biezemans Date: Sun, 18 Oct 2026 13:44:19 +0200 Subject: [PATCH 3/3] core: split the swap-rule tests so each one stays small under Wasm Each swap test built two reports of about 60 checks for several inputs, and one of them peaked at 857 MB under Wasm. Now each test compares one input with one quarter of the checks (two halves of the one-line checks, the checks that walk lists, and the for line and old syntax checks), so there are 96 of them. The checks, the inputs and the fields compared are the same. Co-Authored-By: Claude Sonnet 5.5 --- ergo_test.rego | 291 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 238 insertions(+), 53 deletions(-) diff --git a/ergo_test.rego b/ergo_test.rego index db4eeda..9b0c1d2 100644 --- a/ergo_test.rego +++ b/ergo_test.rego @@ -11083,67 +11083,244 @@ _swap_good := { "m": {"m1": {"x": 1}}, } -_swap_same(d, params) if { - alias := ergo.report(_parts_doc(d), _swap_policy(_swap_has, _swap_checks(_swap_alias, _swap_old(_swap_old_alias), _swap_lines, _swap_walks, _swap_fors)), params) - full := ergo.report(_parts_doc(d), _swap_policy({}, _swap_checks(_swap_full, _swap_old(_swap_old_full), _swap_lines, _swap_walks, _swap_fors)), params) +_swap_group_checks("lines_a", replacements, _) := _swap_checks(replacements, [], array.slice(_swap_lines, 0, 13), [], []) + +_swap_group_checks("lines_b", replacements, _) := _swap_checks(replacements, [], array.slice(_swap_lines, 13, count(_swap_lines)), [], []) + +_swap_group_checks("walks", replacements, _) := _swap_checks(replacements, [], [], _swap_walks, []) + +_swap_group_checks("for_and_old", replacements, old) := _swap_checks(replacements, old, [], [], _swap_fors) + +_swap_good_with(f, v) := {"trail": {"name": "t1", "dd": object.union(_swap_good, {f: v})}} + +_swap_good_without(f) := {"trail": {"name": "t1", "dd": object.remove(_swap_good, [f])}} + +_swap_scenarios := { + "everything_is_there": {"doc": _parts_doc(_swap_good), "params": {"k": "m1"}}, + "everything_is_there_and_no_params": {"doc": _parts_doc(_swap_good), "params": {}}, + "the_named_by_param_is_a_number": {"doc": _parts_doc(_swap_good), "params": {"k": 1}}, + "the_named_by_param_is_null": {"doc": _parts_doc(_swap_good), "params": {"k": null}}, + "the_named_by_key_is_not_in_its_object": {"doc": _swap_good_with("m", {}), "params": {"k": "m1"}}, + "an_object_part_is_missing": {"doc": _swap_good_without("b"), "params": {"k": "m1"}}, + "an_object_part_is_null": {"doc": _swap_good_with("b", null), "params": {"k": "m1"}}, + "an_object_part_is_text": {"doc": _swap_good_with("b", "text"), "params": {"k": "m1"}}, + "an_object_part_is_a_list": {"doc": _swap_good_with("b", [1]), "params": {"k": "m1"}}, + "a_field_inside_a_part_is_null": {"doc": _swap_good_with("b", object.union(_swap_good.b, {"x": null})), "params": {"k": "m1"}}, + "the_fields_inside_a_part_have_the_wrong_types": {"doc": _swap_good_with("b", {"x": "1", "s": 5, "n": "2", "t": "nope", "t2": 3, "items": "s"}), "params": {"k": "m1"}}, + "the_list_is_missing": {"doc": _swap_good_without("items"), "params": {"k": "m1"}}, + "the_list_is_null": {"doc": _swap_good_with("items", null), "params": {"k": "m1"}}, + "the_list_is_empty": {"doc": _swap_good_with("items", []), "params": {"k": "m1"}}, + "the_list_is_a_string": {"doc": _swap_good_with("items", "str"), "params": {"k": "m1"}}, + "the_list_holds_a_number_and_a_null": {"doc": _swap_good_with("items", [_swap_item(1, 2, 2, [1]), 5, null]), "params": {"k": "m1"}}, + "the_list_holds_items_with_missing_and_wrong_fields": {"doc": _swap_good_with("items", [_swap_item(null, 2, "q", []), {"y": 2}]), "params": {"k": "m1"}}, + "the_named_by_part_is_null": {"doc": _swap_good_with("m", null), "params": {"k": "m1"}}, + "the_named_by_part_is_text": {"doc": _swap_good_with("m", "s"), "params": {"k": "m1"}}, + "the_named_by_part_is_missing": {"doc": _swap_good_without("m"), "params": {"k": "m1"}}, + "the_subject_has_no_part_field": {"doc": {"trail": {"name": "t1"}}, "params": {"k": "m1"}}, + "the_part_field_is_null": {"doc": {"trail": {"name": "t1", "dd": null}}, "params": {"k": "m1"}}, + "the_part_field_is_text": {"doc": {"trail": {"name": "t1", "dd": "s"}}, "params": {"k": "m1"}}, + "the_part_field_is_a_list": {"doc": {"trail": {"name": "t1", "dd": [1]}}, "params": {"k": "m1"}}, +} + +_swap_same(scenario, group) if { + s := _swap_scenarios[scenario] + alias := ergo.report(s.doc, _swap_policy(_swap_has, _swap_group_checks(group, _swap_alias, _swap_old(_swap_old_alias))), s.params) + full := ergo.report(s.doc, _swap_policy({}, _swap_group_checks(group, _swap_full, _swap_old(_swap_old_full))), s.params) _swap_view(alias) == _swap_view(full) - count(alias.results) > 60 + count(alias.results) > 10 } -_swap_same_doc(doc, params) if { - alias := ergo.report(doc, _swap_policy(_swap_has, _swap_checks(_swap_alias, _swap_old(_swap_old_alias), _swap_lines, _swap_walks, _swap_fors)), params) - full := ergo.report(doc, _swap_policy({}, _swap_checks(_swap_full, _swap_old(_swap_old_full), _swap_lines, _swap_walks, _swap_fors)), params) - _swap_view(alias) == _swap_view(full) - count(alias.results) > 60 -} +test_a_part_gives_the_rows_of_its_full_path_when_everything_is_there_in_the_first_one_line_checks if _swap_same("everything_is_there", "lines_a") -test_a_part_gives_the_rows_of_its_full_path_when_everything_is_there if { - _swap_same(_swap_good, {"k": "m1"}) - _swap_same(_swap_good, {}) -} +test_a_part_gives_the_rows_of_its_full_path_when_everything_is_there_in_the_other_one_line_checks if _swap_same("everything_is_there", "lines_b") -test_a_part_gives_the_rows_of_its_full_path_when_the_params_it_names_by_are_wrong if { - _swap_same(_swap_good, {"k": 1}) - _swap_same(_swap_good, {"k": null}) - _swap_same(object.union(_swap_good, {"m": {}}), {"k": "m1"}) -} +test_a_part_gives_the_rows_of_its_full_path_when_everything_is_there_in_the_checks_that_walk_lists if _swap_same("everything_is_there", "walks") -test_a_part_gives_the_rows_of_its_full_path_when_an_object_part_is_missing_null_or_wrong_typed if { - _swap_same(object.remove(_swap_good, ["b"]), {"k": "m1"}) - _swap_same(object.union(_swap_good, {"b": null}), {"k": "m1"}) - _swap_same(object.union(_swap_good, {"b": "text"}), {"k": "m1"}) - _swap_same(object.union(_swap_good, {"b": [1]}), {"k": "m1"}) -} +test_a_part_gives_the_rows_of_its_full_path_when_everything_is_there_in_the_for_line_and_old_syntax_checks if _swap_same("everything_is_there", "for_and_old") -test_a_part_gives_the_rows_of_its_full_path_when_a_field_inside_it_is_null_or_wrong_typed if { - _swap_same(object.union(_swap_good, {"b": object.union(_swap_good.b, {"x": null})}), {"k": "m1"}) - _swap_same(object.union(_swap_good, {"b": {"x": "1", "s": 5, "n": "2", "t": "nope", "t2": 3, "items": "s"}}), {"k": "m1"}) -} +test_a_part_gives_the_rows_of_its_full_path_when_everything_is_there_and_no_params_in_the_first_one_line_checks if _swap_same("everything_is_there_and_no_params", "lines_a") -test_a_part_gives_the_rows_of_its_full_path_when_the_list_is_missing_null_empty_or_a_string if { - _swap_same(object.remove(_swap_good, ["items"]), {"k": "m1"}) - _swap_same(object.union(_swap_good, {"items": null}), {"k": "m1"}) - _swap_same(object.union(_swap_good, {"items": []}), {"k": "m1"}) - _swap_same(object.union(_swap_good, {"items": "str"}), {"k": "m1"}) -} +test_a_part_gives_the_rows_of_its_full_path_when_everything_is_there_and_no_params_in_the_other_one_line_checks if _swap_same("everything_is_there_and_no_params", "lines_b") -test_a_part_gives_the_rows_of_its_full_path_when_the_list_holds_odd_items if { - _swap_same(object.union(_swap_good, {"items": [_swap_item(1, 2, 2, [1]), 5, null]}), {"k": "m1"}) - _swap_same(object.union(_swap_good, {"items": [_swap_item(null, 2, "q", []), {"y": 2}]}), {"k": "m1"}) -} +test_a_part_gives_the_rows_of_its_full_path_when_everything_is_there_and_no_params_in_the_checks_that_walk_lists if _swap_same("everything_is_there_and_no_params", "walks") -test_a_part_gives_the_rows_of_its_full_path_when_the_named_by_part_is_missing_or_wrong_typed if { - _swap_same(object.union(_swap_good, {"m": null}), {"k": "m1"}) - _swap_same(object.union(_swap_good, {"m": "s"}), {"k": "m1"}) - _swap_same(object.remove(_swap_good, ["m"]), {"k": "m1"}) -} +test_a_part_gives_the_rows_of_its_full_path_when_everything_is_there_and_no_params_in_the_for_line_and_old_syntax_checks if _swap_same("everything_is_there_and_no_params", "for_and_old") -test_a_part_gives_the_rows_of_its_full_path_when_the_subject_has_nothing_or_is_not_an_object if { - _swap_same_doc({"trail": {"name": "t1"}}, {"k": "m1"}) - _swap_same_doc({"trail": {"name": "t1", "dd": null}}, {"k": "m1"}) - _swap_same_doc({"trail": {"name": "t1", "dd": "s"}}, {"k": "m1"}) - _swap_same_doc({"trail": {"name": "t1", "dd": [1]}}, {"k": "m1"}) -} +test_a_part_gives_the_rows_of_its_full_path_when_the_named_by_param_is_a_number_in_the_first_one_line_checks if _swap_same("the_named_by_param_is_a_number", "lines_a") + +test_a_part_gives_the_rows_of_its_full_path_when_the_named_by_param_is_a_number_in_the_other_one_line_checks if _swap_same("the_named_by_param_is_a_number", "lines_b") + +test_a_part_gives_the_rows_of_its_full_path_when_the_named_by_param_is_a_number_in_the_checks_that_walk_lists if _swap_same("the_named_by_param_is_a_number", "walks") + +test_a_part_gives_the_rows_of_its_full_path_when_the_named_by_param_is_a_number_in_the_for_line_and_old_syntax_checks if _swap_same("the_named_by_param_is_a_number", "for_and_old") + +test_a_part_gives_the_rows_of_its_full_path_when_the_named_by_param_is_null_in_the_first_one_line_checks if _swap_same("the_named_by_param_is_null", "lines_a") + +test_a_part_gives_the_rows_of_its_full_path_when_the_named_by_param_is_null_in_the_other_one_line_checks if _swap_same("the_named_by_param_is_null", "lines_b") + +test_a_part_gives_the_rows_of_its_full_path_when_the_named_by_param_is_null_in_the_checks_that_walk_lists if _swap_same("the_named_by_param_is_null", "walks") + +test_a_part_gives_the_rows_of_its_full_path_when_the_named_by_param_is_null_in_the_for_line_and_old_syntax_checks if _swap_same("the_named_by_param_is_null", "for_and_old") + +test_a_part_gives_the_rows_of_its_full_path_when_the_named_by_key_is_not_in_its_object_in_the_first_one_line_checks if _swap_same("the_named_by_key_is_not_in_its_object", "lines_a") + +test_a_part_gives_the_rows_of_its_full_path_when_the_named_by_key_is_not_in_its_object_in_the_other_one_line_checks if _swap_same("the_named_by_key_is_not_in_its_object", "lines_b") + +test_a_part_gives_the_rows_of_its_full_path_when_the_named_by_key_is_not_in_its_object_in_the_checks_that_walk_lists if _swap_same("the_named_by_key_is_not_in_its_object", "walks") + +test_a_part_gives_the_rows_of_its_full_path_when_the_named_by_key_is_not_in_its_object_in_the_for_line_and_old_syntax_checks if _swap_same("the_named_by_key_is_not_in_its_object", "for_and_old") + +test_a_part_gives_the_rows_of_its_full_path_when_an_object_part_is_missing_in_the_first_one_line_checks if _swap_same("an_object_part_is_missing", "lines_a") + +test_a_part_gives_the_rows_of_its_full_path_when_an_object_part_is_missing_in_the_other_one_line_checks if _swap_same("an_object_part_is_missing", "lines_b") + +test_a_part_gives_the_rows_of_its_full_path_when_an_object_part_is_missing_in_the_checks_that_walk_lists if _swap_same("an_object_part_is_missing", "walks") + +test_a_part_gives_the_rows_of_its_full_path_when_an_object_part_is_missing_in_the_for_line_and_old_syntax_checks if _swap_same("an_object_part_is_missing", "for_and_old") + +test_a_part_gives_the_rows_of_its_full_path_when_an_object_part_is_null_in_the_first_one_line_checks if _swap_same("an_object_part_is_null", "lines_a") + +test_a_part_gives_the_rows_of_its_full_path_when_an_object_part_is_null_in_the_other_one_line_checks if _swap_same("an_object_part_is_null", "lines_b") + +test_a_part_gives_the_rows_of_its_full_path_when_an_object_part_is_null_in_the_checks_that_walk_lists if _swap_same("an_object_part_is_null", "walks") + +test_a_part_gives_the_rows_of_its_full_path_when_an_object_part_is_null_in_the_for_line_and_old_syntax_checks if _swap_same("an_object_part_is_null", "for_and_old") + +test_a_part_gives_the_rows_of_its_full_path_when_an_object_part_is_text_in_the_first_one_line_checks if _swap_same("an_object_part_is_text", "lines_a") + +test_a_part_gives_the_rows_of_its_full_path_when_an_object_part_is_text_in_the_other_one_line_checks if _swap_same("an_object_part_is_text", "lines_b") + +test_a_part_gives_the_rows_of_its_full_path_when_an_object_part_is_text_in_the_checks_that_walk_lists if _swap_same("an_object_part_is_text", "walks") + +test_a_part_gives_the_rows_of_its_full_path_when_an_object_part_is_text_in_the_for_line_and_old_syntax_checks if _swap_same("an_object_part_is_text", "for_and_old") + +test_a_part_gives_the_rows_of_its_full_path_when_an_object_part_is_a_list_in_the_first_one_line_checks if _swap_same("an_object_part_is_a_list", "lines_a") + +test_a_part_gives_the_rows_of_its_full_path_when_an_object_part_is_a_list_in_the_other_one_line_checks if _swap_same("an_object_part_is_a_list", "lines_b") + +test_a_part_gives_the_rows_of_its_full_path_when_an_object_part_is_a_list_in_the_checks_that_walk_lists if _swap_same("an_object_part_is_a_list", "walks") + +test_a_part_gives_the_rows_of_its_full_path_when_an_object_part_is_a_list_in_the_for_line_and_old_syntax_checks if _swap_same("an_object_part_is_a_list", "for_and_old") + +test_a_part_gives_the_rows_of_its_full_path_when_a_field_inside_a_part_is_null_in_the_first_one_line_checks if _swap_same("a_field_inside_a_part_is_null", "lines_a") + +test_a_part_gives_the_rows_of_its_full_path_when_a_field_inside_a_part_is_null_in_the_other_one_line_checks if _swap_same("a_field_inside_a_part_is_null", "lines_b") + +test_a_part_gives_the_rows_of_its_full_path_when_a_field_inside_a_part_is_null_in_the_checks_that_walk_lists if _swap_same("a_field_inside_a_part_is_null", "walks") + +test_a_part_gives_the_rows_of_its_full_path_when_a_field_inside_a_part_is_null_in_the_for_line_and_old_syntax_checks if _swap_same("a_field_inside_a_part_is_null", "for_and_old") + +test_a_part_gives_the_rows_of_its_full_path_when_the_fields_inside_a_part_have_the_wrong_types_in_the_first_one_line_checks if _swap_same("the_fields_inside_a_part_have_the_wrong_types", "lines_a") + +test_a_part_gives_the_rows_of_its_full_path_when_the_fields_inside_a_part_have_the_wrong_types_in_the_other_one_line_checks if _swap_same("the_fields_inside_a_part_have_the_wrong_types", "lines_b") + +test_a_part_gives_the_rows_of_its_full_path_when_the_fields_inside_a_part_have_the_wrong_types_in_the_checks_that_walk_lists if _swap_same("the_fields_inside_a_part_have_the_wrong_types", "walks") + +test_a_part_gives_the_rows_of_its_full_path_when_the_fields_inside_a_part_have_the_wrong_types_in_the_for_line_and_old_syntax_checks if _swap_same("the_fields_inside_a_part_have_the_wrong_types", "for_and_old") + +test_a_part_gives_the_rows_of_its_full_path_when_the_list_is_missing_in_the_first_one_line_checks if _swap_same("the_list_is_missing", "lines_a") + +test_a_part_gives_the_rows_of_its_full_path_when_the_list_is_missing_in_the_other_one_line_checks if _swap_same("the_list_is_missing", "lines_b") + +test_a_part_gives_the_rows_of_its_full_path_when_the_list_is_missing_in_the_checks_that_walk_lists if _swap_same("the_list_is_missing", "walks") + +test_a_part_gives_the_rows_of_its_full_path_when_the_list_is_missing_in_the_for_line_and_old_syntax_checks if _swap_same("the_list_is_missing", "for_and_old") + +test_a_part_gives_the_rows_of_its_full_path_when_the_list_is_null_in_the_first_one_line_checks if _swap_same("the_list_is_null", "lines_a") + +test_a_part_gives_the_rows_of_its_full_path_when_the_list_is_null_in_the_other_one_line_checks if _swap_same("the_list_is_null", "lines_b") + +test_a_part_gives_the_rows_of_its_full_path_when_the_list_is_null_in_the_checks_that_walk_lists if _swap_same("the_list_is_null", "walks") + +test_a_part_gives_the_rows_of_its_full_path_when_the_list_is_null_in_the_for_line_and_old_syntax_checks if _swap_same("the_list_is_null", "for_and_old") + +test_a_part_gives_the_rows_of_its_full_path_when_the_list_is_empty_in_the_first_one_line_checks if _swap_same("the_list_is_empty", "lines_a") + +test_a_part_gives_the_rows_of_its_full_path_when_the_list_is_empty_in_the_other_one_line_checks if _swap_same("the_list_is_empty", "lines_b") + +test_a_part_gives_the_rows_of_its_full_path_when_the_list_is_empty_in_the_checks_that_walk_lists if _swap_same("the_list_is_empty", "walks") + +test_a_part_gives_the_rows_of_its_full_path_when_the_list_is_empty_in_the_for_line_and_old_syntax_checks if _swap_same("the_list_is_empty", "for_and_old") + +test_a_part_gives_the_rows_of_its_full_path_when_the_list_is_a_string_in_the_first_one_line_checks if _swap_same("the_list_is_a_string", "lines_a") + +test_a_part_gives_the_rows_of_its_full_path_when_the_list_is_a_string_in_the_other_one_line_checks if _swap_same("the_list_is_a_string", "lines_b") + +test_a_part_gives_the_rows_of_its_full_path_when_the_list_is_a_string_in_the_checks_that_walk_lists if _swap_same("the_list_is_a_string", "walks") + +test_a_part_gives_the_rows_of_its_full_path_when_the_list_is_a_string_in_the_for_line_and_old_syntax_checks if _swap_same("the_list_is_a_string", "for_and_old") + +test_a_part_gives_the_rows_of_its_full_path_when_the_list_holds_a_number_and_a_null_in_the_first_one_line_checks if _swap_same("the_list_holds_a_number_and_a_null", "lines_a") + +test_a_part_gives_the_rows_of_its_full_path_when_the_list_holds_a_number_and_a_null_in_the_other_one_line_checks if _swap_same("the_list_holds_a_number_and_a_null", "lines_b") + +test_a_part_gives_the_rows_of_its_full_path_when_the_list_holds_a_number_and_a_null_in_the_checks_that_walk_lists if _swap_same("the_list_holds_a_number_and_a_null", "walks") + +test_a_part_gives_the_rows_of_its_full_path_when_the_list_holds_a_number_and_a_null_in_the_for_line_and_old_syntax_checks if _swap_same("the_list_holds_a_number_and_a_null", "for_and_old") + +test_a_part_gives_the_rows_of_its_full_path_when_the_list_holds_items_with_missing_and_wrong_fields_in_the_first_one_line_checks if _swap_same("the_list_holds_items_with_missing_and_wrong_fields", "lines_a") + +test_a_part_gives_the_rows_of_its_full_path_when_the_list_holds_items_with_missing_and_wrong_fields_in_the_other_one_line_checks if _swap_same("the_list_holds_items_with_missing_and_wrong_fields", "lines_b") + +test_a_part_gives_the_rows_of_its_full_path_when_the_list_holds_items_with_missing_and_wrong_fields_in_the_checks_that_walk_lists if _swap_same("the_list_holds_items_with_missing_and_wrong_fields", "walks") + +test_a_part_gives_the_rows_of_its_full_path_when_the_list_holds_items_with_missing_and_wrong_fields_in_the_for_line_and_old_syntax_checks if _swap_same("the_list_holds_items_with_missing_and_wrong_fields", "for_and_old") + +test_a_part_gives_the_rows_of_its_full_path_when_the_named_by_part_is_null_in_the_first_one_line_checks if _swap_same("the_named_by_part_is_null", "lines_a") + +test_a_part_gives_the_rows_of_its_full_path_when_the_named_by_part_is_null_in_the_other_one_line_checks if _swap_same("the_named_by_part_is_null", "lines_b") + +test_a_part_gives_the_rows_of_its_full_path_when_the_named_by_part_is_null_in_the_checks_that_walk_lists if _swap_same("the_named_by_part_is_null", "walks") + +test_a_part_gives_the_rows_of_its_full_path_when_the_named_by_part_is_null_in_the_for_line_and_old_syntax_checks if _swap_same("the_named_by_part_is_null", "for_and_old") + +test_a_part_gives_the_rows_of_its_full_path_when_the_named_by_part_is_text_in_the_first_one_line_checks if _swap_same("the_named_by_part_is_text", "lines_a") + +test_a_part_gives_the_rows_of_its_full_path_when_the_named_by_part_is_text_in_the_other_one_line_checks if _swap_same("the_named_by_part_is_text", "lines_b") + +test_a_part_gives_the_rows_of_its_full_path_when_the_named_by_part_is_text_in_the_checks_that_walk_lists if _swap_same("the_named_by_part_is_text", "walks") + +test_a_part_gives_the_rows_of_its_full_path_when_the_named_by_part_is_text_in_the_for_line_and_old_syntax_checks if _swap_same("the_named_by_part_is_text", "for_and_old") + +test_a_part_gives_the_rows_of_its_full_path_when_the_named_by_part_is_missing_in_the_first_one_line_checks if _swap_same("the_named_by_part_is_missing", "lines_a") + +test_a_part_gives_the_rows_of_its_full_path_when_the_named_by_part_is_missing_in_the_other_one_line_checks if _swap_same("the_named_by_part_is_missing", "lines_b") + +test_a_part_gives_the_rows_of_its_full_path_when_the_named_by_part_is_missing_in_the_checks_that_walk_lists if _swap_same("the_named_by_part_is_missing", "walks") + +test_a_part_gives_the_rows_of_its_full_path_when_the_named_by_part_is_missing_in_the_for_line_and_old_syntax_checks if _swap_same("the_named_by_part_is_missing", "for_and_old") + +test_a_part_gives_the_rows_of_its_full_path_when_the_subject_has_no_part_field_in_the_first_one_line_checks if _swap_same("the_subject_has_no_part_field", "lines_a") + +test_a_part_gives_the_rows_of_its_full_path_when_the_subject_has_no_part_field_in_the_other_one_line_checks if _swap_same("the_subject_has_no_part_field", "lines_b") + +test_a_part_gives_the_rows_of_its_full_path_when_the_subject_has_no_part_field_in_the_checks_that_walk_lists if _swap_same("the_subject_has_no_part_field", "walks") + +test_a_part_gives_the_rows_of_its_full_path_when_the_subject_has_no_part_field_in_the_for_line_and_old_syntax_checks if _swap_same("the_subject_has_no_part_field", "for_and_old") + +test_a_part_gives_the_rows_of_its_full_path_when_the_part_field_is_null_in_the_first_one_line_checks if _swap_same("the_part_field_is_null", "lines_a") + +test_a_part_gives_the_rows_of_its_full_path_when_the_part_field_is_null_in_the_other_one_line_checks if _swap_same("the_part_field_is_null", "lines_b") + +test_a_part_gives_the_rows_of_its_full_path_when_the_part_field_is_null_in_the_checks_that_walk_lists if _swap_same("the_part_field_is_null", "walks") + +test_a_part_gives_the_rows_of_its_full_path_when_the_part_field_is_null_in_the_for_line_and_old_syntax_checks if _swap_same("the_part_field_is_null", "for_and_old") + +test_a_part_gives_the_rows_of_its_full_path_when_the_part_field_is_text_in_the_first_one_line_checks if _swap_same("the_part_field_is_text", "lines_a") + +test_a_part_gives_the_rows_of_its_full_path_when_the_part_field_is_text_in_the_other_one_line_checks if _swap_same("the_part_field_is_text", "lines_b") + +test_a_part_gives_the_rows_of_its_full_path_when_the_part_field_is_text_in_the_checks_that_walk_lists if _swap_same("the_part_field_is_text", "walks") + +test_a_part_gives_the_rows_of_its_full_path_when_the_part_field_is_text_in_the_for_line_and_old_syntax_checks if _swap_same("the_part_field_is_text", "for_and_old") + +test_a_part_gives_the_rows_of_its_full_path_when_the_part_field_is_a_list_in_the_first_one_line_checks if _swap_same("the_part_field_is_a_list", "lines_a") + +test_a_part_gives_the_rows_of_its_full_path_when_the_part_field_is_a_list_in_the_other_one_line_checks if _swap_same("the_part_field_is_a_list", "lines_b") + +test_a_part_gives_the_rows_of_its_full_path_when_the_part_field_is_a_list_in_the_checks_that_walk_lists if _swap_same("the_part_field_is_a_list", "walks") + +test_a_part_gives_the_rows_of_its_full_path_when_the_part_field_is_a_list_in_the_for_line_and_old_syntax_checks if _swap_same("the_part_field_is_a_list", "for_and_old") test_the_swap_comparison_would_notice_a_part_that_reads_the_wrong_path if { alias := ergo.report(_parts_doc(_swap_good), _swap_policy({"cov": "dd.items"}, {"c": "cov.x is 1"}), {}) @@ -11152,9 +11329,17 @@ test_the_swap_comparison_would_notice_a_part_that_reads_the_wrong_path if { } test_the_swap_comparison_sees_rows_of_every_kind_of_outcome if { - alias := ergo.report(_parts_doc(object.union(_swap_good, {"b": "text"})), _swap_policy(_swap_has, _swap_checks(_swap_alias, _swap_old(_swap_old_alias), _swap_lines, _swap_walks, _swap_fors)), {"k": "m1"}) - {r.cause | some r in alias.results; startswith(r.check, "l")} >= {"unusable", "satisfied"} - {r.cause | some r in alias.results} >= {"unusable", "satisfied", "value", "not_an_object"} + text := _swap_scenarios.an_object_part_is_text + one := ergo.report(text.doc, _swap_policy(_swap_has, _swap_group_checks("lines_a", _swap_alias, [])), text.params) + {"unusable"} - {r.cause | some r in one.results; startswith(r.check, "l")} == set() + walks := ergo.report(_swap_scenarios.the_list_holds_a_number_and_a_null.doc, _swap_policy(_swap_has, _swap_group_checks("walks", _swap_alias, [])), {"k": "m1"}) + {"value", "satisfied"} - {r.cause | some r in walks.results; startswith(r.check, "w")} == set() +} + +test_the_swap_comparison_sees_a_subject_that_is_not_an_object if { + nothing := _swap_scenarios.the_part_field_is_text + rep := ergo.report(nothing.doc, _swap_policy(_swap_has, _swap_group_checks("for_and_old", _swap_alias, _swap_old(_swap_old_alias))), nothing.params) + {"unusable"} - {r.cause | some r in rep.results; startswith(r.check, "f")} == set() } _deep_all(p, check) := {"op": "all", "path": p.lst, "check": check}