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..fce4626 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,6 +1129,80 @@ _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 + 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 + m.n >= 0 +} + +_visible_root(m) := [concat("", ["$", m.of])] if { + m.of != null + m.n < 0 } _keys_of(path) := ks if { @@ -1304,7 +1397,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 +1422,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 +1543,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 +2802,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 @@ -3964,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 { @@ -3979,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 { @@ -4086,10 +4216,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 +4549,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 +4592,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 +5247,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 +6206,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 +6222,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) +} + +_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) +} + +_p_lines(subjects) := {name: _from_line(def.from) | some name, def in subjects; is_object(def); is_string(def.from)} + +_p_narrow(subjects, lines) := {name: _s_chain_parent(name, lines, subjects) | some name, _ in subjects} + +_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)} -_s_subjects(subjects, reads) := {name: _s_subject(name, def, reads) | some name, def in subjects} if is_object(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) -_s_subjects(subjects, _) := subjects if not is_object(subjects) +_p_members(name, narrow) := graph.reachable(narrow, [name]) -_s_subject(name, null, _) := {"from": [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, {})}} -_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_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) } @@ -6106,17 +6392,25 @@ _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 := [_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 { +_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(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 +6482,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) := {"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, _) := {"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) @@ -6202,13 +6496,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) + +_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) + +_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) -_s_requirements(reqs, _) := reqs if not is_object(reqs) +_p_group_e(group, pc) := [_p_old_e(c, pc) | some c in group] if is_array(group) -_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_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 +6785,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 +6803,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 +7209,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) @@ -6729,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)] } @@ -6743,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, _) := [] @@ -6892,9 +7448,46 @@ _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) := _walk_shown_name(_walk_unaliased(path, _walk_crossed(path, walked)), walked) -_walk_root_name(path) := [_plain_step_name(0, path[0])] if _named(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) + +_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 { + _part_start(path) + count(_visible_root(_part_of(path))) == 0 +} _walk_root_name(path) := [] if not _named(path) @@ -7010,7 +7603,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 +7796,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 +7816,7 @@ _has_name_step(p) if { } _for_named(p) if { - _named(p) + _off_subject(p) not _builtin(p) } @@ -7237,26 +7830,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_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_chains(policy) := _s_subject_sentences(policy.subjects, _s_from_reads(policy.subjects)).chains 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..9b0c1d2 100644 --- a/ergo_test.rego +++ b/ergo_test.rego @@ -10434,3 +10434,1111 @@ 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_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) > 10 +} + +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_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_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_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_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_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_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_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_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"}), {}) + 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 { + 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} + +_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 +} + +_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 842e9aa..edbfd6f 100644 --- a/spec/OPEN.md +++ b/spec/OPEN.md @@ -44,6 +44,13 @@ 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. + ## 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..ed4d4c4 --- /dev/null +++ b/spec/cases/parts/cases.json @@ -0,0 +1,2974 @@ +[ + { + "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 + } + } + ] + } + } + } + } + } + }, + "expressions": { + "r": { + "$min_subjects": "count(matching(artifact.pra.pull_requests)) >= 1", + "$unique_ids": "count(repeated(ids(artifact.pra.pull_requests))) == 0" + } + }, + "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" + } + } + ] + } + ] + }, + { + "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/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..8372a7a 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. 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 Each check's definition in the report has an `expression` that says what it checks. @@ -118,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 | | --------------- | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | 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")