@tiphys/kernel 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +56 -4
- package/assurance-modes.yaml +23 -2
- package/dist/bin/tiphys.js +86 -8
- package/dist/src/adapters/load.d.ts +202 -0
- package/dist/src/adapters/load.js +440 -0
- package/dist/src/brief.js +27 -20
- package/dist/src/checks.d.ts +720 -9
- package/dist/src/checks.js +1874 -163
- package/dist/src/cli.js +11 -0
- package/dist/src/commands/brief.js +27 -4
- package/dist/src/commands/cutover.d.ts +35 -0
- package/dist/src/commands/cutover.js +448 -0
- package/dist/src/commands/doctor.d.ts +229 -0
- package/dist/src/commands/doctor.js +968 -27
- package/dist/src/commands/init.d.ts +3 -3
- package/dist/src/commands/init.js +57 -8
- package/dist/src/commands/lock.d.ts +33 -0
- package/dist/src/commands/lock.js +117 -6
- package/dist/src/commands/next.d.ts +130 -0
- package/dist/src/commands/next.js +597 -0
- package/dist/src/commands/pool.js +12 -1
- package/dist/src/commands/resume.d.ts +1 -0
- package/dist/src/commands/resume.js +88 -0
- package/dist/src/commands/spawn.js +51 -2
- package/dist/src/commands/status.d.ts +6 -4
- package/dist/src/commands/status.js +6 -4
- package/dist/src/commands/sync.d.ts +47 -0
- package/dist/src/commands/sync.js +341 -0
- package/dist/src/commands/teardown.js +10 -2
- package/dist/src/commands/validate.js +70 -0
- package/dist/src/cutover.d.ts +584 -0
- package/dist/src/cutover.js +1444 -0
- package/dist/src/exclusion.d.ts +389 -0
- package/dist/src/exclusion.js +843 -0
- package/dist/src/exec/env.d.ts +152 -2
- package/dist/src/exec/env.js +146 -2
- package/dist/src/fleet.d.ts +172 -0
- package/dist/src/fleet.js +219 -1
- package/dist/src/gates/citations.js +7 -1
- package/dist/src/gates/coverage.d.ts +113 -22
- package/dist/src/gates/coverage.js +166 -31
- package/dist/src/gates/credentials.d.ts +159 -0
- package/dist/src/gates/credentials.js +221 -2
- package/dist/src/gates/gate-classes.d.ts +56 -0
- package/dist/src/gates/gate-classes.js +633 -0
- package/dist/src/gates/merge-preconditions.d.ts +319 -0
- package/dist/src/gates/merge-preconditions.js +932 -0
- package/dist/src/gates/red-witness.js +105 -13
- package/dist/src/gates/run.d.ts +49 -1
- package/dist/src/gates/run.js +83 -5
- package/dist/src/gates/schemas/phase-declaration.schema.json +45 -0
- package/dist/src/gates/suite.js +48 -7
- package/dist/src/hooks.d.ts +55 -3
- package/dist/src/hooks.js +69 -6
- package/dist/src/index.d.ts +31 -0
- package/dist/src/index.js +30 -0
- package/dist/src/lock.d.ts +82 -4
- package/dist/src/lock.js +314 -22
- package/dist/src/model-resolution.d.ts +159 -0
- package/dist/src/model-resolution.js +307 -0
- package/dist/src/path-identity.d.ts +32 -0
- package/dist/src/path-identity.js +38 -0
- package/dist/src/pool.d.ts +197 -1
- package/dist/src/pool.js +289 -22
- package/dist/src/roles.d.ts +31 -0
- package/dist/src/roles.js +42 -0
- package/dist/src/spawn.d.ts +307 -2
- package/dist/src/spawn.js +690 -19
- package/dist/src/status.d.ts +27 -2
- package/dist/src/status.js +34 -5
- package/dist/src/task.d.ts +295 -55
- package/dist/src/task.js +125 -123
- package/dist/src/teardown.d.ts +7 -0
- package/dist/src/teardown.js +120 -12
- package/dist/src/validate.d.ts +44 -11
- package/dist/src/validate.js +44 -34
- package/dist/src/watcher.js +1 -11
- package/dist/src/witness/run.d.ts +32 -7
- package/dist/src/witness/run.js +76 -30
- package/dist/src/witness/spec.d.ts +168 -0
- package/dist/src/witness/spec.js +240 -18
- package/dist/tsconfig.src.tsbuildinfo +1 -1
- package/gate-registry.yaml +136 -0
- package/gates.manifest.json +63 -1
- package/package.json +18 -3
- package/roles/implementer.md +3 -0
- package/schemas/README.md +1 -0
- package/schemas/assurance-modes.schema.json +1 -1
- package/schemas/charter.schema.json +19 -0
- package/schemas/cutover-state.schema.json +64 -0
- package/schemas/executor-record.schema.json +36 -0
- package/schemas/model-resolution.schema.json +362 -0
- package/schemas/verdict.schema.json +9 -3
- package/schemas/write-bypass.schema.json +69 -0
package/dist/src/checks.js
CHANGED
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
* D-M3-22: a check that belongs in section 2.3's table and is not in it is a
|
|
25
25
|
* PLAN DEFECT to escalate, not a script to add quietly.
|
|
26
26
|
*/
|
|
27
|
+
import { spawnSync } from "node:child_process";
|
|
28
|
+
import { createHash } from "node:crypto";
|
|
27
29
|
import { readdirSync } from "node:fs";
|
|
28
30
|
import { createRequire } from "node:module";
|
|
29
31
|
import { join } from "node:path";
|
|
@@ -2648,16 +2650,507 @@ export const DECORRELATION_DIMENSIONS = [
|
|
|
2648
2650
|
/** The merge-authority value that makes decorrelation a precondition of merge. */
|
|
2649
2651
|
export const DELEGATED_MERGE_AUTHORITY = "delegated-under-conditions";
|
|
2650
2652
|
/**
|
|
2651
|
-
*
|
|
2653
|
+
* SAY WHAT `produced-by` ACTUALLY COMPARED, ON THE GREEN LINE (CR-VS-003).
|
|
2654
|
+
*
|
|
2655
|
+
* The comparison is `canonicalScalar`: NFKC, whitespace collapse, lowercase,
|
|
2656
|
+
* then `!==`. Reproduced at the swept head with one variable changed: two
|
|
2657
|
+
* `produced-by` strings naming two different MODELS of one vendor, in the
|
|
2658
|
+
* vendor-plus-model-plus-organisation form this project's own reviews use, are
|
|
2659
|
+
* certified "distinct on produced-by", green, exit 0. Two models of ONE family
|
|
2660
|
+
* pass as decorrelated, which is T-001's own property failing inside the
|
|
2661
|
+
* kernel's decorrelation check. The two strings are quoted verbatim in the
|
|
2662
|
+
* sweep evidence rather than here, because no vendor model name may appear in
|
|
2663
|
+
* the kernel's shipped surface (test/schemas.test.ts:800).
|
|
2664
|
+
*
|
|
2665
|
+
* WHY THIS IS A SENTENCE AND NOT A FAMILY VOCABULARY, and the reason is a
|
|
2666
|
+
* settled one rather than an omission. M4-P10 deferred the family comparison to
|
|
2667
|
+
* M4-P11 (delivery/work-history/m4-p10.md:654); M4-P11 DECLINED the mechanism in
|
|
2668
|
+
* its own words, "a closed enum of family names was rejected: no such vocabulary
|
|
2669
|
+
* can be kept current" (delivery/work-history/m4-p11.md:142). That reason holds
|
|
2670
|
+
* and shipping the enum that phase rejected would be reopening it by the back
|
|
2671
|
+
* door. What was never done is the OTHER half of the reviewer's own proposal:
|
|
2672
|
+
* stop the green line reading as a cross-family assertion. A bundle-level green
|
|
2673
|
+
* saying "distinct on produced-by" is read as "two families reviewed this", and
|
|
2674
|
+
* nothing here establishes that. So the line now says what it measured.
|
|
2675
|
+
*
|
|
2676
|
+
* WHAT WOULD CLOSE IT, named rather than left open: a `produced-by-family`
|
|
2677
|
+
* field in `schemas/verdict.schema.json`, required, compared instead of the free
|
|
2678
|
+
* string, with the vocabulary OPEN (any two distinct values decorrelate) so no
|
|
2679
|
+
* list has to be kept current. That is a schema change and a decision record,
|
|
2680
|
+
* both outside this fix round's declared files.
|
|
2681
|
+
*/
|
|
2682
|
+
export function producedByCaveat(compared) {
|
|
2683
|
+
return compared.includes("produced-by")
|
|
2684
|
+
? "; produced-by was compared as a canonicalised STRING and not as a model FAMILY, so two models of one " +
|
|
2685
|
+
"family are distinct here and this line is not a cross-family assertion"
|
|
2686
|
+
: "";
|
|
2687
|
+
}
|
|
2688
|
+
export function readVerdictKind(value) {
|
|
2689
|
+
const record = asRecord(value);
|
|
2690
|
+
if (record === undefined) {
|
|
2691
|
+
return { kind: "other" };
|
|
2692
|
+
}
|
|
2693
|
+
const reading = establishField(record, "kind");
|
|
2694
|
+
if (reading.kind === "absent") {
|
|
2695
|
+
return { kind: "other" };
|
|
2696
|
+
}
|
|
2697
|
+
if (reading.kind === "unusable" || reading.kind === "uncanonical") {
|
|
2698
|
+
/* The vocabulary is the DOCUMENT's, as it is at `establishField`: a reader
|
|
2699
|
+
looking at their own YAML is helped by "a list" and by the codepoint and
|
|
2700
|
+
position of the character they cannot see, and not by "unestablished". */
|
|
2701
|
+
return { kind: "unreadable", found: reading.found };
|
|
2702
|
+
}
|
|
2703
|
+
if (reading.value !== "verdict") {
|
|
2704
|
+
return { kind: "other" };
|
|
2705
|
+
}
|
|
2706
|
+
return { kind: "verdict", record };
|
|
2707
|
+
}
|
|
2708
|
+
/** A verdict document is committed under one of these extensions. */
|
|
2709
|
+
const VERDICT_FILE_PATTERN = /\.(ya?ml|json)$/i;
|
|
2710
|
+
/**
|
|
2711
|
+
* The project's own paperwork root, and the widest set the FALSIFIERS look at.
|
|
2712
|
+
*
|
|
2713
|
+
* WHY THIS IS THE BOUNDARY AND NOT THE WHOLE TREE (CR-M4P11-002, and the
|
|
2714
|
+
* measurement that decided it). Falsifier 1 asks a question about A PROJECT'S
|
|
2715
|
+
* OWN RECORD, so scoping it to `delivery/review/` was defeated by FILE
|
|
2716
|
+
* PLACEMENT ALONE: the reviewer committed two `family-b` verdicts under
|
|
2717
|
+
* `delivery/evidence/`, which is where this repository actually keeps its only
|
|
2718
|
+
* two real verdicts, and the exception was granted anyway.
|
|
2719
|
+
*
|
|
2720
|
+
* The obvious repair, the whole tree, was tried and MEASURED WRONG. At
|
|
2721
|
+
* `122472b` a whole-subtree enumeration finds SEVEN verdict documents here and
|
|
2722
|
+
* FIVE of them are `witness/fixtures/dual-review/*.yaml`, deliberately
|
|
2723
|
+
* carrying `family-a` and `family-b` because they are the inputs to this very
|
|
2724
|
+
* check's own tests. A corpus that counts a test fixture as a project's record
|
|
2725
|
+
* would make the exception permanently unusable for any project that vendors
|
|
2726
|
+
* fixtures, and would say something false while doing it.
|
|
2727
|
+
*
|
|
2728
|
+
* `delivery/` is the kernel's OWN declared paperwork root, not a boundary
|
|
2729
|
+
* invented here: `REVIEW_DIRECTORY` is under it, `loadDeclaration`
|
|
2730
|
+
* (src/gates/release.ts:817) reads phase declarations from under it, and
|
|
2731
|
+
* src/gates/scope.ts:565 already treats `delivery/review/` and
|
|
2732
|
+
* `delivery/verification/` as the phase-evidence directories. Using it keeps
|
|
2733
|
+
* the rule mechanical and placement-based rather than a judgment about which
|
|
2734
|
+
* documents look real.
|
|
2735
|
+
*
|
|
2736
|
+
* WHAT IT STILL DOES NOT REACH, named rather than left to be found: a verdict
|
|
2737
|
+
* committed OUTSIDE `delivery/` is invisible to the falsifiers. That residue
|
|
2738
|
+
* is smaller than the one it replaces and it is stated here, in the file, so
|
|
2739
|
+
* the next reader does not have to re-derive it.
|
|
2740
|
+
*/
|
|
2741
|
+
const PAPERWORK_ROOT = "delivery";
|
|
2742
|
+
/**
|
|
2743
|
+
* How to name the set a check just looked at, in the check's own output.
|
|
2744
|
+
*
|
|
2745
|
+
* SC-011 one scope out: "every verdict this project has committed" and "every
|
|
2746
|
+
* file that happens to be sitting in one directory right now" are different
|
|
2747
|
+
* claims and must not print the same sentence. This renders a TRAILING
|
|
2748
|
+
* parenthetical rather than a clause in the middle of one, so a sentence that
|
|
2749
|
+
* already names its subject keeps its shape and gains a provenance tail.
|
|
2750
|
+
*/
|
|
2751
|
+
export function describeVerdictCorpusSource(source) {
|
|
2752
|
+
return source.kind === "commit"
|
|
2753
|
+
? `(corpus: ${source.scope} read from commit ${source.refSha}, resolved from ${source.ref})`
|
|
2754
|
+
: `(corpus: ${source.scope} read from the WORKING TREE because this context has no resolvable git ref: ${source.reason})`;
|
|
2755
|
+
}
|
|
2756
|
+
/**
|
|
2757
|
+
* How to name the source ONE context document was looked for in.
|
|
2758
|
+
*
|
|
2759
|
+
* FIX ROUND 2, DV-001. The regime report line used to say "no charter.yaml"
|
|
2760
|
+
* about a directory with a `charter.yaml` sitting in it, because the probe had
|
|
2761
|
+
* moved to the commit and the sentence had not. A record that names a document
|
|
2762
|
+
* and not the SOURCE it was looked for in is unfalsifiable by the person
|
|
2763
|
+
* reading it, which is the same SC-011 property `describeVerdictCorpusSource`
|
|
2764
|
+
* exists for one scope out.
|
|
2765
|
+
*/
|
|
2766
|
+
export function describeContextDocumentSource(source) {
|
|
2767
|
+
return source.kind === "commit"
|
|
2768
|
+
? `in commit ${source.refSha}, resolved from ${source.ref}`
|
|
2769
|
+
: `in the WORKING TREE, because this context has no resolvable git ref: ${source.reason}`;
|
|
2770
|
+
}
|
|
2771
|
+
/**
|
|
2772
|
+
* The verdict documents a PAIR decision is made over: `delivery/review/`.
|
|
2773
|
+
*
|
|
2774
|
+
* TWO ARMS, AND WHICH ONE RAN IS REPORTED RATHER THAN INFERRED.
|
|
2775
|
+
*
|
|
2776
|
+
* THE COMMIT ARM is taken whenever `<context>` resolves `ref`, and it reads
|
|
2777
|
+
* the directory's entries out of the git object database. The filesystem is
|
|
2778
|
+
* not consulted at all, so an uncommitted addition, deletion or edit cannot
|
|
2779
|
+
* change what this returns. That is the anti-widening rule
|
|
2780
|
+
* `readReviewFamilies` and `loadDeclaration` (src/gates/release.ts:817)
|
|
2781
|
+
* already apply to a DECLARATION, now applied to the evidence beside it. It
|
|
2782
|
+
* closes both directions of the same hole: an uncommitted DELETION can no
|
|
2783
|
+
* longer remove a verdict that contradicts a declaration, and an uncommitted
|
|
2784
|
+
* ADDITION can no longer manufacture the pair DR-0012 condition 2 requires.
|
|
2785
|
+
*
|
|
2786
|
+
* THE WORKTREE ARM is taken only when there is no resolvable ref, which is the
|
|
2787
|
+
* pre-existing behaviour for a context that is not a git repository at all,
|
|
2788
|
+
* and it SAYS SO in every sentence it produces. No exception can be granted on
|
|
2789
|
+
* this arm, because `readReviewFamilies` resolves the same ref and returns
|
|
2790
|
+
* absent or error when it cannot: with no git there is one source of truth and
|
|
2791
|
+
* nothing to disagree.
|
|
2792
|
+
*
|
|
2793
|
+
* THE SCOPE STAYS `delivery/review/` HERE, and widening it was measured wrong.
|
|
2794
|
+
* `headGroupFor` turns this set into the reviews of one `(phase, head)`, and
|
|
2795
|
+
* five of the seven verdict documents in this repository's tree are fixtures
|
|
2796
|
+
* for this check's own tests. See `PAPERWORK_ROOT` above; the widest set is
|
|
2797
|
+
* what the FALSIFIERS use, and it is a different question.
|
|
2798
|
+
*
|
|
2799
|
+
* A file that does not carry `kind: verdict` is SKIPPED rather than reported,
|
|
2800
|
+
* because that directory also holds this project's prose reviews and a check
|
|
2801
|
+
* that reddened on a markdown file would be unusable. What is NOT skipped is
|
|
2802
|
+
* the directory being unreadable, which the caller turns into a violation:
|
|
2803
|
+
* "nothing to compare" and "could not look" are different facts.
|
|
2804
|
+
*
|
|
2805
|
+
* AND A CANDIDATE THAT COULD NOT BE LOOKED AT IS THE SECOND HALF OF THAT SAME
|
|
2806
|
+
* SENTENCE, WHICH THE FIRST ROUND WROTE AND APPLIED AT ONE SITE ONLY. A
|
|
2807
|
+
* `.yaml`, `.yml` or `.json` file here has passed the only filter that
|
|
2808
|
+
* separates a candidate verdict from a prose review, so bytes that cannot be
|
|
2809
|
+
* READ and bytes that do not DECODE are not "this is not a verdict", they are
|
|
2810
|
+
* "nobody knows whether this is a verdict". Dropping such a file SHRINKS the
|
|
2811
|
+
* set the merge predicates reason over, which is the fail-open direction for a
|
|
2812
|
+
* predicate that approves when the set is clean: measured at the reviewed head,
|
|
2813
|
+
* a third review reading FIX-ROUND-NEEDED with one malformed line left
|
|
2814
|
+
* `verdict-pair-approves` printing that the pair approves. So they are returned
|
|
2815
|
+
* as diagnostics and every caller seeds its violation list with them, exactly
|
|
2816
|
+
* as `headGroupFor` already does for a sibling with no usable head.
|
|
2817
|
+
*/
|
|
2818
|
+
export function loadCommittedVerdicts(contextDirectory, source = resolveCorpusSource(contextDirectory)) {
|
|
2819
|
+
if (source.kind !== "commit") {
|
|
2820
|
+
return loadVerdictsFromWorktree(contextDirectory, source.reason);
|
|
2821
|
+
}
|
|
2822
|
+
const refSha = source.refSha;
|
|
2823
|
+
/* RECURSIVE SINCE THE DR-0047 SWEEP (CR-VS-002), AND THE ARGUMENT THAT USED
|
|
2824
|
+
TO SIT AT `loadPaperworkVerdicts` FOR WHY THIS ONE WAS FLAT IS WITHDRAWN.
|
|
2825
|
+
One function had two callers at two depths, so a committed document could
|
|
2826
|
+
be INSIDE the corpus that can contradict a single-family declaration and
|
|
2827
|
+
OUTSIDE the corpus that can refuse a merge. Measured before the change: two
|
|
2828
|
+
APPROVE verdicts at this directory's top level plus a committed THIRD
|
|
2829
|
+
verdict for the same phase and head reading FIX-ROUND-NEEDED one directory
|
|
2830
|
+
down gave `check-dual-review: green`, exit 0, with the refusal neither
|
|
2831
|
+
counted nor mentioned; and with BOTH verdicts one directory down the gate
|
|
2832
|
+
reported `0 verdict document(s)` and not-applicable while `git ls-files`
|
|
2833
|
+
listed them. `ls-tree` without `-r` yields the SUBTREE'S NAME, which
|
|
2834
|
+
`VERDICT_FILE_PATTERN` discards, so the drop was silent by construction,
|
|
2835
|
+
which is the fail-open direction for a predicate that approves when the set
|
|
2836
|
+
is clean. Depth is now a property of the LISTING FUNCTION's one contract
|
|
2837
|
+
rather than of which caller reached it. */
|
|
2838
|
+
const listed = listCommittedTree(contextDirectory, refSha, REVIEW_DIRECTORY, true);
|
|
2839
|
+
if (!listed.ok) {
|
|
2840
|
+
return { ok: false, reason: listed.reason };
|
|
2841
|
+
}
|
|
2842
|
+
return readCommittedVerdicts(contextDirectory, refSha, listed.paths, source);
|
|
2843
|
+
}
|
|
2844
|
+
/**
|
|
2845
|
+
* Every verdict document committed anywhere under `delivery/` at one commit.
|
|
2846
|
+
*
|
|
2847
|
+
* THE FALSIFIERS' CORPUS, AND A DIFFERENT QUESTION FROM THE PAIR'S. See
|
|
2848
|
+
* `PAPERWORK_ROOT` for why the boundary is the paperwork root rather than one
|
|
2849
|
+
* directory or the whole tree.
|
|
2850
|
+
*
|
|
2851
|
+
* COMMIT ONLY, WITH NO WORKTREE FALLBACK, and that is not an omission. This is
|
|
2852
|
+
* reached only from `singleFamilyException`, which is reached only when a
|
|
2853
|
+
* declaration was successfully read out of a commit. There is no arm where a
|
|
2854
|
+
* declaration exists and a commit does not, so a worktree fallback here would
|
|
2855
|
+
* be code that cannot run, which is the dead-arm shape this file already
|
|
2856
|
+
* refused once at `classifyEntry`.
|
|
2857
|
+
*/
|
|
2858
|
+
function loadPaperworkVerdicts(contextDirectory, ref, refSha) {
|
|
2859
|
+
const source = {
|
|
2860
|
+
kind: "commit",
|
|
2861
|
+
ref,
|
|
2862
|
+
refSha,
|
|
2863
|
+
scope: `every verdict document under ${PAPERWORK_ROOT}/`,
|
|
2864
|
+
};
|
|
2865
|
+
/* `recursive` HERE AND, SINCE THE DR-0047 SWEEP, ON THE PAIR CORPUS TOO. The
|
|
2866
|
+
paragraph that stood here said the pair's directory is flat by convention
|
|
2867
|
+
and that recursing it would be a silent behaviour change on the arm that
|
|
2868
|
+
already worked. CR-VS-002 measured what the asymmetry cost instead: a
|
|
2869
|
+
verdict one directory down was inside THIS corpus and outside that one, so
|
|
2870
|
+
a committed review refusing the head under audit could contradict a
|
|
2871
|
+
declaration and could not refuse a merge. Both corpora now read the same
|
|
2872
|
+
depth; the boundaries still differ, and that difference is the real one.
|
|
2873
|
+
THE LISTING
|
|
2874
|
+
ITSELF IS THE SAME FUNCTION the pair corpus uses (FIX ROUND 2, DV-002):
|
|
2875
|
+
two listing idioms maintained side by side is what let one of them be
|
|
2876
|
+
wrong in a nested context while the other was right. */
|
|
2877
|
+
const listed = listCommittedTree(contextDirectory, refSha, PAPERWORK_ROOT, true);
|
|
2878
|
+
if (!listed.ok) {
|
|
2879
|
+
return {
|
|
2880
|
+
ok: false,
|
|
2881
|
+
reason: `the verdict documents under ${PAPERWORK_ROOT}/ in ${refSha} could not be enumerated, so the ` +
|
|
2882
|
+
`record that would refute a single-family declaration could not be established: ${listed.reason}`,
|
|
2883
|
+
};
|
|
2884
|
+
}
|
|
2885
|
+
return readCommittedVerdicts(contextDirectory, refSha, listed.paths, source);
|
|
2886
|
+
}
|
|
2887
|
+
/**
|
|
2888
|
+
* List one committed directory, as paths relative to the CONTEXT DIRECTORY.
|
|
2889
|
+
*
|
|
2890
|
+
* ONE LISTING IDIOM FOR BOTH CORPORA (FIX ROUND 2, DV-002), AND THE PATHSPEC
|
|
2891
|
+
* FORM IS THE LOAD-BEARING HALF. Fix round 1 replaced one `readdirSync` with
|
|
2892
|
+
* TWO different git idioms: the falsifiers' corpus listed
|
|
2893
|
+
* `<sha> -- ./<dir>/` and the pair corpus listed the tree-ish `<sha>:./<dir>`.
|
|
2894
|
+
* Those are not two spellings of one question. `git ls-tree` applies the
|
|
2895
|
+
* CURRENT DIRECTORY as an implicit pathspec, so a tree-ish listing run from a
|
|
2896
|
+
* context directory that is a SUBDIRECTORY of its repository is filtered
|
|
2897
|
+
* against a prefix the named tree's own entries do not carry, and it returns
|
|
2898
|
+
* NOTHING with exit 0. Measured, one commit, cwd = a context directory nested
|
|
2899
|
+
* one level inside its repository, `delivery/review/` holding two committed
|
|
2900
|
+
* verdicts:
|
|
2901
|
+
*
|
|
2902
|
+
* git cat-file -t $S:./delivery/review -> tree, exit 0
|
|
2903
|
+
* git ls-tree -z --name-only $S:./delivery/review -> EMPTY, exit 0
|
|
2904
|
+
* git ls-tree -z --name-only $S -- ./delivery/review/
|
|
2905
|
+
* -> both names, exit 0
|
|
2906
|
+
*
|
|
2907
|
+
* An empty listing is then indistinguishable from an absent directory, so
|
|
2908
|
+
* "could not enumerate" became "there are none", the pair corpus came back
|
|
2909
|
+
* empty, and a committed pair sharing one `produced-by` reported
|
|
2910
|
+
* NOT-APPLICABLE on a conditional gate instead of red. The kernel's own
|
|
2911
|
+
* repository could not see it, because the registry command runs the script
|
|
2912
|
+
* with `.` at the repository root; every consumer whose tiphys context is not
|
|
2913
|
+
* its repository root does see it.
|
|
2914
|
+
*
|
|
2915
|
+
* The pathspec form's output is relative to the current directory, which is
|
|
2916
|
+
* the context directory, which is what `readCommittedVerdicts` then hands to
|
|
2917
|
+
* `git show ${refSha}:./${path}`. The listing and the read therefore resolve
|
|
2918
|
+
* against the SAME base, which is the property that broke.
|
|
2919
|
+
*
|
|
2920
|
+
* ABSENT, REGULAR AND UNLISTABLE ARE THREE ANSWERS, exactly as `classifyEntry`
|
|
2921
|
+
* gives three on the worktree arm. `git cat-file -t` is what separates them:
|
|
2922
|
+
* a missing path is an empty corpus, a `blob` where a directory was expected
|
|
2923
|
+
* is the same fact the worktree arm reports as "is a regular file, not a
|
|
2924
|
+
* directory", and a listing that fails for any other reason has not reached a
|
|
2925
|
+
* verdict and must not report one (M2-C-3).
|
|
2926
|
+
*/
|
|
2927
|
+
function listCommittedTree(contextDirectory, refSha, directory, recursive) {
|
|
2928
|
+
const typed = gitIn(["cat-file", "-t", `${refSha}:./${directory}`], contextDirectory);
|
|
2929
|
+
if (!typed.ok) {
|
|
2930
|
+
/* ABSENT, OR COULD NOT LOOK, AND THEY ARE NOT THE SAME ANSWER. Until this
|
|
2931
|
+
round a `cat-file -t` that failed for ANY reason returned an empty
|
|
2932
|
+
corpus, so an object database that could not be read reported the same
|
|
2933
|
+
thing as a project that keeps no `delivery/` at all, which is the
|
|
2934
|
+
"could not look" reported as "looked and found nothing" shape the
|
|
2935
|
+
sibling loader's comment already named. Absence is established by a
|
|
2936
|
+
SECOND probe that does not mention the path: if the commit object
|
|
2937
|
+
itself is readable, the only thing the first probe can have been
|
|
2938
|
+
reporting is that the path is not in it. This is the same fail-closed
|
|
2939
|
+
rule `readCommittedVerdicts` twenty lines down already applies to a
|
|
2940
|
+
blob it was told about and cannot read. */
|
|
2941
|
+
const commitReadable = gitIn(["cat-file", "-t", refSha], contextDirectory);
|
|
2942
|
+
if (!commitReadable.ok) {
|
|
2943
|
+
return {
|
|
2944
|
+
ok: false,
|
|
2945
|
+
reason: `${refSha} could not be read in ${contextDirectory}, so whether ${directory}/ is committed there ` +
|
|
2946
|
+
`was not established and an empty corpus must not be reported: ${commitReadable.reason}`,
|
|
2947
|
+
};
|
|
2948
|
+
}
|
|
2949
|
+
return { ok: true, paths: [] };
|
|
2950
|
+
}
|
|
2951
|
+
const type = typed.stdout.trim();
|
|
2952
|
+
if (type !== "tree") {
|
|
2953
|
+
return {
|
|
2954
|
+
ok: false,
|
|
2955
|
+
reason: `${refSha}:./${directory} is a ${type}, not a directory, so the committed verdicts cannot be enumerated`,
|
|
2956
|
+
};
|
|
2957
|
+
}
|
|
2958
|
+
const listed = gitIn([
|
|
2959
|
+
"ls-tree",
|
|
2960
|
+
...(recursive ? ["-r"] : []),
|
|
2961
|
+
"-z",
|
|
2962
|
+
"--name-only",
|
|
2963
|
+
refSha,
|
|
2964
|
+
"--",
|
|
2965
|
+
`./${directory}/`,
|
|
2966
|
+
], contextDirectory);
|
|
2967
|
+
if (!listed.ok) {
|
|
2968
|
+
return {
|
|
2969
|
+
ok: false,
|
|
2970
|
+
reason: `${refSha}:./${directory} could not be listed: ${listed.reason}`,
|
|
2971
|
+
};
|
|
2972
|
+
}
|
|
2973
|
+
return { ok: true, paths: listed.stdout.split("\0").filter((name) => name !== "") };
|
|
2974
|
+
}
|
|
2975
|
+
/**
|
|
2976
|
+
* Read a list of committed paths and keep the ones that are verdicts.
|
|
2977
|
+
*
|
|
2978
|
+
* `-z` ON EVERY LISTING THAT FEEDS THIS IS LOAD-BEARING. Without it git QUOTES
|
|
2979
|
+
* a path carrying a quote, a backslash or a non-ASCII byte, and the quoted
|
|
2980
|
+
* spelling is not the path `git show` wants, so exactly the documents whose
|
|
2981
|
+
* names are unusual would drop out of the corpus. Dropping a document from the
|
|
2982
|
+
* corpus is the fail-open direction.
|
|
2983
|
+
*
|
|
2984
|
+
* `${refSha}:./${path}` AND NOT `${refSha}:${path}`, for the reason
|
|
2985
|
+
* `readReviewFamilies` already gives further down: without the leading `./`
|
|
2986
|
+
* git resolves the path against the REPOSITORY ROOT, so a context directory
|
|
2987
|
+
* nested inside a larger repository would silently read the outer
|
|
2988
|
+
* repository's documents. With `./` the listing and the read are both relative
|
|
2989
|
+
* to the directory the caller named, so they cannot disagree about which tree
|
|
2990
|
+
* they are describing.
|
|
2991
|
+
*/
|
|
2992
|
+
function readCommittedVerdicts(contextDirectory, refSha, paths, source) {
|
|
2993
|
+
const documents = [];
|
|
2994
|
+
for (const path of [...paths].sort()) {
|
|
2995
|
+
if (!VERDICT_FILE_PATTERN.test(path)) {
|
|
2996
|
+
continue;
|
|
2997
|
+
}
|
|
2998
|
+
const shown = gitIn(["show", `${refSha}:./${path}`], contextDirectory);
|
|
2999
|
+
if (!shown.ok) {
|
|
3000
|
+
/* A path the same commit's own listing named and the same commit cannot
|
|
3001
|
+
produce is not a document to skip, it is a corpus that could not be
|
|
3002
|
+
read. M2-C-3: this has not reached a verdict, so it must not report
|
|
3003
|
+
one. */
|
|
3004
|
+
return {
|
|
3005
|
+
ok: false,
|
|
3006
|
+
reason: `${refSha}:./${path} is listed in ${refSha} and could not be read, so the committed verdict corpus is ` +
|
|
3007
|
+
`incomplete and no merge precondition can be decided over it: ${shown.reason}`,
|
|
3008
|
+
};
|
|
3009
|
+
}
|
|
3010
|
+
documents.push({ path: join(contextDirectory, path), body: shown.stdout });
|
|
3011
|
+
}
|
|
3012
|
+
const selected = selectVerdicts(documents);
|
|
3013
|
+
return { ok: true, verdicts: selected.verdicts, unexaminable: selected.unexaminable, source };
|
|
3014
|
+
}
|
|
3015
|
+
/**
|
|
3016
|
+
* Read a context document AT THE SOURCE THE DECISION IS BEING MADE FROM.
|
|
3017
|
+
*
|
|
3018
|
+
* FIX ROUND 1, AND IT IS THE FOURTH SITE OF THE MECHANISM the reviewers found
|
|
3019
|
+
* at the first. Their finding was that the CORPUS was read from disk while the
|
|
3020
|
+
* declaration was read from a commit. The derivation for it (D2 and D3 in the
|
|
3021
|
+
* work history) turned up the same split one document further out and WORSE:
|
|
3022
|
+
* `establishDelegatedRegime` decides whether a delegated merge grant is in
|
|
3023
|
+
* force at all, and it read `charter.yaml` and `assurance-modes.yaml` off
|
|
3024
|
+
* DISK, while `readReviewFamilies` read THE SAME `charter.yaml` out of the
|
|
3025
|
+
* object database.
|
|
3026
|
+
*
|
|
3027
|
+
* Measured, one commit, one working-tree edit of one word:
|
|
3028
|
+
*
|
|
3029
|
+
* committed `delivery-mode: full` (merge-authority delegated-under-conditions)
|
|
3030
|
+
* with a pair sharing produced-by -> red, exit 1
|
|
3031
|
+
* the SAME commit, `delivery-mode: direct-pr` written into the working tree
|
|
3032
|
+
* and never committed -> GREEN, exit 0, and the record
|
|
3033
|
+
* prints "mode direct-pr declares
|
|
3034
|
+
* merge-authority owner, which is
|
|
3035
|
+
* not a delegated grant"
|
|
3036
|
+
*
|
|
3037
|
+
* That is not the exception being bought, it is the ENTIRE decorrelation
|
|
3038
|
+
* requirement being switched off, by an edit no commit records and no diff
|
|
3039
|
+
* shows. It is the same mechanism as CR-M4P11-001 and it is why this round
|
|
3040
|
+
* fixes the mechanism rather than the corpus.
|
|
3041
|
+
*
|
|
3042
|
+
* The worktree arm is the pre-existing behaviour and is unchanged: with no
|
|
3043
|
+
* resolvable ref there is one source of truth and nothing to disagree.
|
|
3044
|
+
*/
|
|
3045
|
+
function readContextDocumentAt(contextDirectory, relativePath, source) {
|
|
3046
|
+
if (source.kind !== "commit") {
|
|
3047
|
+
return readContextDocument(contextDirectory, relativePath);
|
|
3048
|
+
}
|
|
3049
|
+
const path = join(contextDirectory, relativePath);
|
|
3050
|
+
const shown = gitIn(["show", `${source.refSha}:./${relativePath}`], contextDirectory);
|
|
3051
|
+
if (!shown.ok) {
|
|
3052
|
+
return {
|
|
3053
|
+
ok: false,
|
|
3054
|
+
reason: `${source.refSha}:./${relativePath} could not be read: ${shown.reason}`,
|
|
3055
|
+
};
|
|
3056
|
+
}
|
|
3057
|
+
const decoded = decodeDocument(shown.stdout, path);
|
|
3058
|
+
if (!decoded.ok) {
|
|
3059
|
+
return { ok: false, reason: decoded.reason };
|
|
3060
|
+
}
|
|
3061
|
+
return { ok: true, value: decoded.value, path };
|
|
3062
|
+
}
|
|
3063
|
+
/**
|
|
3064
|
+
* Is a context document present AT THE SOURCE the decision is read from?
|
|
3065
|
+
*
|
|
3066
|
+
* SEPARATE FROM READING IT, because "absent" and "present and unreadable" are
|
|
3067
|
+
* different facts with different verdicts one screen down, exactly as
|
|
3068
|
+
* `classifyEntry` keeps them apart on the worktree arm.
|
|
3069
|
+
*/
|
|
3070
|
+
function contextDocumentPresentAt(contextDirectory, relativePath, source) {
|
|
3071
|
+
if (source.kind !== "commit") {
|
|
3072
|
+
return classifyEntry(join(contextDirectory, relativePath)).kind !== "absent";
|
|
3073
|
+
}
|
|
3074
|
+
const typed = gitIn(["cat-file", "-t", `${source.refSha}:./${relativePath}`], contextDirectory);
|
|
3075
|
+
return typed.ok && typed.stdout.trim() === "blob";
|
|
3076
|
+
}
|
|
3077
|
+
/**
|
|
3078
|
+
* Resolve, ONCE, the source every document of one decision is read from.
|
|
3079
|
+
*
|
|
3080
|
+
* EXPORTED because the two merge-precondition checks each resolve it at the
|
|
3081
|
+
* top of their own run and hand the SAME value to the regime reader, the
|
|
3082
|
+
* declaration reader and both corpus loaders. One resolution is what makes
|
|
3083
|
+
* "the halves disagree" unrepresentable rather than merely unlikely.
|
|
3084
|
+
*/
|
|
3085
|
+
export function resolveCorpusSource(contextDirectory, ref = "HEAD") {
|
|
3086
|
+
const resolved = gitIn(["rev-parse", `${ref}^{commit}`], contextDirectory);
|
|
3087
|
+
return resolved.ok
|
|
3088
|
+
? { kind: "commit", ref, refSha: resolved.stdout.trim(), scope: REVIEW_DIRECTORY }
|
|
3089
|
+
: { kind: "worktree", reason: resolved.reason, scope: REVIEW_DIRECTORY };
|
|
3090
|
+
}
|
|
3091
|
+
/**
|
|
3092
|
+
* The one selection rule, applied to every document an arm produced.
|
|
2652
3093
|
*
|
|
2653
|
-
*
|
|
2654
|
-
*
|
|
2655
|
-
*
|
|
2656
|
-
*
|
|
2657
|
-
*
|
|
2658
|
-
* "could not look" are different facts.
|
|
3094
|
+
* MATERIALISED RATHER THAN STREAMED, and the bound is stated rather than left
|
|
3095
|
+
* to be discovered: the widest corpus is the candidate `.yaml`, `.yml` and
|
|
3096
|
+
* `.json` blobs under `delivery/`, measured at 103 files and 8953667 bytes in
|
|
3097
|
+
* this repository at `79ce63b`, and it is read only when a single-family
|
|
3098
|
+
* declaration has already been established.
|
|
2659
3099
|
*/
|
|
2660
|
-
function
|
|
3100
|
+
function selectVerdicts(documents) {
|
|
3101
|
+
const verdicts = [];
|
|
3102
|
+
const unexaminable = [];
|
|
3103
|
+
for (const { path, body } of documents) {
|
|
3104
|
+
const decoded = decodeDocument(body, path);
|
|
3105
|
+
if (!decoded.ok) {
|
|
3106
|
+
unexaminable.push({
|
|
3107
|
+
pointer: "#/kind",
|
|
3108
|
+
message: `${path} sits under ${REVIEW_DIRECTORY} and did not decode, so whether it is a verdict refusing this head could not be established, and a merge check that could not look at one document must not report the rest of them clean: ${decoded.reason}`,
|
|
3109
|
+
});
|
|
3110
|
+
continue;
|
|
3111
|
+
}
|
|
3112
|
+
/* CANONICAL HERE TOO, AND THE REASON IS THE SAME ONE ONE LAYER OUT. This
|
|
3113
|
+
`===` decides MEMBERSHIP OF THE GROUP the decorrelation decision is made
|
|
3114
|
+
over, so a lookalike character in `kind` does not produce a wrong
|
|
3115
|
+
comparison, it silently removes a document from the comparison. With
|
|
3116
|
+
three verdicts, two of them sharing a model family, dropping one of the
|
|
3117
|
+
correlated pair leaves two distinct ones and a green run. That is the
|
|
3118
|
+
same fail-open outcome as the reported finding, reached by making the
|
|
3119
|
+
check look at less rather than by making it compare wrongly.
|
|
3120
|
+
|
|
3121
|
+
Canonicalising ADMITS more documents, which is the fail-closed direction
|
|
3122
|
+
here: more verdicts in the group means more chances to find a shared
|
|
3123
|
+
value, never fewer. A file that is not a verdict at all still fails this
|
|
3124
|
+
test, because no canonical form turns a prose review into `verdict`. */
|
|
3125
|
+
/* THE ONE READER, AND FIX ROUND 2 IS THAT IT IS ONE READER WITH THREE
|
|
3126
|
+
OUTCOMES RATHER THAN A BOOLEAN. `readVerdictKind` is documented at its
|
|
3127
|
+
own definition; what matters here is that `unreadable` is a DIAGNOSTIC
|
|
3128
|
+
and `other` is a skip, because a document whose `kind` key is present and
|
|
3129
|
+
whose reading FAILED has not said it is not a verdict, it has said
|
|
3130
|
+
nothing that could be read. The skip below is now reached only by a
|
|
3131
|
+
document that answered. */
|
|
3132
|
+
const kindReading = readVerdictKind(decoded.value);
|
|
3133
|
+
if (kindReading.kind === "unreadable") {
|
|
3134
|
+
unexaminable.push({
|
|
3135
|
+
pointer: "#/kind",
|
|
3136
|
+
message: `${path} sits under ${REVIEW_DIRECTORY} and declares a kind field that could not be read as a word (it is ${kindReading.found}), so whether it is a verdict refusing this head could not be established, and a merge check that could not read one document's own type must not report the rest of them clean`,
|
|
3137
|
+
});
|
|
3138
|
+
continue;
|
|
3139
|
+
}
|
|
3140
|
+
if (kindReading.kind !== "verdict") {
|
|
3141
|
+
continue;
|
|
3142
|
+
}
|
|
3143
|
+
verdicts.push({ path, record: kindReading.record });
|
|
3144
|
+
}
|
|
3145
|
+
return { verdicts, unexaminable };
|
|
3146
|
+
}
|
|
3147
|
+
/** The pre-existing arm, for a context that is not a git repository. */
|
|
3148
|
+
function loadVerdictsFromWorktree(contextDirectory, why) {
|
|
3149
|
+
const source = {
|
|
3150
|
+
kind: "worktree",
|
|
3151
|
+
reason: why,
|
|
3152
|
+
scope: REVIEW_DIRECTORY,
|
|
3153
|
+
};
|
|
2661
3154
|
const directory = join(contextDirectory, REVIEW_DIRECTORY);
|
|
2662
3155
|
/* `classifyEntry` HAS NO `directory` KIND: a directory lands in `irregular`,
|
|
2663
3156
|
which is the kind that means "present and not safe to OPEN AS A FILE". So
|
|
@@ -2667,14 +3160,21 @@ function loadCommittedVerdicts(contextDirectory) {
|
|
|
2667
3160
|
exist would have been dead code that always took the error arm. */
|
|
2668
3161
|
const entry = classifyEntry(directory);
|
|
2669
3162
|
if (entry.kind === "absent" || entry.kind === "dangling") {
|
|
2670
|
-
return { ok: true, verdicts: [] };
|
|
3163
|
+
return { ok: true, verdicts: [], unexaminable: [], source };
|
|
2671
3164
|
}
|
|
2672
3165
|
if (entry.kind === "unexaminable") {
|
|
2673
3166
|
return { ok: false, reason: entry.reason };
|
|
2674
3167
|
}
|
|
2675
3168
|
let names;
|
|
2676
3169
|
try {
|
|
2677
|
-
|
|
3170
|
+
/* `recursive` SO THE TWO ARMS READ THE SAME DEPTH (CR-VS-002). The commit
|
|
3171
|
+
arm lists the whole subtree, and an arm that read one level would make
|
|
3172
|
+
WHICH ARM RAN decide whether a refusing verdict one directory down is
|
|
3173
|
+
part of the corpus. That is the same one-rule-two-readings shape this
|
|
3174
|
+
section already removed for the selection rule. `recursive` yields paths
|
|
3175
|
+
relative to `directory`, which is what `join` below already expects, and
|
|
3176
|
+
a nested name still has to pass `VERDICT_FILE_PATTERN`. */
|
|
3177
|
+
names = readdirSync(directory, { recursive: true }).map((name) => String(name));
|
|
2678
3178
|
}
|
|
2679
3179
|
catch (error) {
|
|
2680
3180
|
if (entry.kind === "regular") {
|
|
@@ -2685,44 +3185,37 @@ function loadCommittedVerdicts(contextDirectory) {
|
|
|
2685
3185
|
}
|
|
2686
3186
|
return { ok: false, reason: `${directory} could not be listed: ${String(error)}` };
|
|
2687
3187
|
}
|
|
2688
|
-
const
|
|
3188
|
+
const documents = [];
|
|
3189
|
+
const unreadable = [];
|
|
2689
3190
|
for (const name of names.sort()) {
|
|
2690
|
-
if (
|
|
3191
|
+
if (!VERDICT_FILE_PATTERN.test(name)) {
|
|
2691
3192
|
continue;
|
|
2692
3193
|
}
|
|
2693
3194
|
const path = join(directory, name);
|
|
2694
3195
|
const read = readOperatorPath(path);
|
|
2695
3196
|
if (!read.ok) {
|
|
3197
|
+
/* M4-P10 FIX ROUND 2, REAPPLIED ON THIS ARM. The commit arm one screen up
|
|
3198
|
+
refuses the whole corpus when a path its own listing named cannot be
|
|
3199
|
+
produced, which is stricter than a diagnostic and is right there: the
|
|
3200
|
+
same commit named it. Here the directory was listed from a working tree
|
|
3201
|
+
that can change under the read, so the fail-closed form is the one
|
|
3202
|
+
M4-P10 wrote, a named candidate carried out to every caller's violation
|
|
3203
|
+
list rather than a silent `continue`. */
|
|
3204
|
+
unreadable.push({
|
|
3205
|
+
pointer: "#/kind",
|
|
3206
|
+
message: `${path} sits under ${REVIEW_DIRECTORY} and could not be read, so whether it is a verdict refusing this head could not be established, and a merge check that could not look at one document must not report the rest of them clean: ${read.reason}`,
|
|
3207
|
+
});
|
|
2696
3208
|
continue;
|
|
2697
3209
|
}
|
|
2698
|
-
|
|
2699
|
-
if (!decoded.ok) {
|
|
2700
|
-
continue;
|
|
2701
|
-
}
|
|
2702
|
-
const record = asRecord(decoded.value);
|
|
2703
|
-
/* CANONICAL HERE TOO, AND THE REASON IS THE SAME ONE ONE LAYER OUT. This
|
|
2704
|
-
`===` decides MEMBERSHIP OF THE GROUP the decorrelation decision is made
|
|
2705
|
-
over, so a lookalike character in `kind` does not produce a wrong
|
|
2706
|
-
comparison, it silently removes a document from the comparison. With
|
|
2707
|
-
three verdicts, two of them sharing a model family, dropping one of the
|
|
2708
|
-
correlated pair leaves two distinct ones and a green run. That is the
|
|
2709
|
-
same fail-open outcome as the reported finding, reached by making the
|
|
2710
|
-
check look at less rather than by making it compare wrongly.
|
|
2711
|
-
|
|
2712
|
-
Canonicalising ADMITS more documents, which is the fail-closed direction
|
|
2713
|
-
here: more verdicts in the group means more chances to find a shared
|
|
2714
|
-
value, never fewer. A file that is not a verdict at all still fails this
|
|
2715
|
-
test, because no canonical form turns a prose review into `verdict`. */
|
|
2716
|
-
if (record === undefined) {
|
|
2717
|
-
continue;
|
|
2718
|
-
}
|
|
2719
|
-
const kindReading = establishField(record, "kind");
|
|
2720
|
-
if (kindReading.kind !== "established" || kindReading.value !== "verdict") {
|
|
2721
|
-
continue;
|
|
2722
|
-
}
|
|
2723
|
-
verdicts.push({ path, record });
|
|
3210
|
+
documents.push({ path, body: read.body });
|
|
2724
3211
|
}
|
|
2725
|
-
|
|
3212
|
+
const selected = selectVerdicts(documents);
|
|
3213
|
+
return {
|
|
3214
|
+
ok: true,
|
|
3215
|
+
verdicts: selected.verdicts,
|
|
3216
|
+
unexaminable: [...unreadable, ...selected.unexaminable],
|
|
3217
|
+
source,
|
|
3218
|
+
};
|
|
2726
3219
|
}
|
|
2727
3220
|
/**
|
|
2728
3221
|
* THE CANONICAL FORM OF A GOVERNANCE SCALAR, DECLARED HERE BECAUSE A
|
|
@@ -2899,6 +3392,881 @@ function decorrelationTriple(record) {
|
|
|
2899
3392
|
return reading.kind === "established" ? `=${reading.value}` : `<${reading.kind}>`;
|
|
2900
3393
|
}).join(" | ");
|
|
2901
3394
|
}
|
|
3395
|
+
/* ------------------------------------------------------------------ */
|
|
3396
|
+
/* The head, and what it is allowed to be (M4-P10) */
|
|
3397
|
+
/* ------------------------------------------------------------------ */
|
|
3398
|
+
/**
|
|
3399
|
+
* A commit sha as `schemas/verdict.schema.json` spells it.
|
|
3400
|
+
*
|
|
3401
|
+
* RESTATED HERE RATHER THAN BORROWED FROM THE SCHEMA, and that is not
|
|
3402
|
+
* duplication by accident. Nothing on the shipped path validates the SIBLING
|
|
3403
|
+
* documents this check loads, which is recorded at `dualReviewDecorrelation`
|
|
3404
|
+
* below, so a sibling carrying an abbreviated `head` reaches the grouping code
|
|
3405
|
+
* whatever the schema says. A check that trusted the schema for this would put
|
|
3406
|
+
* a short sha in its own group of one and never compare it to anything, which
|
|
3407
|
+
* is the fail-open direction.
|
|
3408
|
+
*/
|
|
3409
|
+
const FULL_SHA = /^[0-9a-f]{40}$/;
|
|
3410
|
+
/**
|
|
3411
|
+
* Establish the head a verdict claims to review.
|
|
3412
|
+
*
|
|
3413
|
+
* TWO REFUSALS, AND THEY ARE DIFFERENT FACTS. An UNESTABLISHED `head` is the
|
|
3414
|
+
* ordinary absent-or-unusable-or-uncanonical reading every other dimension in
|
|
3415
|
+
* this file gets, with its own sentence from `unestablishedReason`. A head that
|
|
3416
|
+
* IS established and is not forty hex digits is a SECOND SPELLING of a fact
|
|
3417
|
+
* some other document may state in full, and it is refused on its own terms,
|
|
3418
|
+
* because no canonical form reconciles an abbreviation with the forty-character
|
|
3419
|
+
* sha it abbreviates without resolving both against a repository this check is
|
|
3420
|
+
* never given.
|
|
3421
|
+
*
|
|
3422
|
+
* Case never reaches the pattern as a problem: `establishField` folds it, so an
|
|
3423
|
+
* upper-case sha and a lower-case one are already ONE key by the time the test
|
|
3424
|
+
* runs. That is the direction the hazard row asks for, two spellings of one
|
|
3425
|
+
* head becoming one group rather than two.
|
|
3426
|
+
*/
|
|
3427
|
+
function headKeyOf(record, where) {
|
|
3428
|
+
const reading = establishField(record, "head");
|
|
3429
|
+
if (reading.kind !== "established") {
|
|
3430
|
+
return {
|
|
3431
|
+
ok: false,
|
|
3432
|
+
message: `${where} ${unestablishedReason(reading, "head")}, so the reviews cannot be grouped by the head they reviewed, and a delegated grant is not satisfied by a review that does not say what it reviewed`,
|
|
3433
|
+
};
|
|
3434
|
+
}
|
|
3435
|
+
if (!FULL_SHA.test(reading.value)) {
|
|
3436
|
+
return {
|
|
3437
|
+
ok: false,
|
|
3438
|
+
message: `${where} declares head ${reading.value}, which is not forty lowercase hexadecimal digits; an abbreviated sha is a second spelling of one head and would form its own group of one, which is never compared to anything`,
|
|
3439
|
+
};
|
|
3440
|
+
}
|
|
3441
|
+
return { ok: true, value: reading.value };
|
|
3442
|
+
}
|
|
3443
|
+
/**
|
|
3444
|
+
* Select the verdicts for one `(phase, head)` out of a directory's committed set.
|
|
3445
|
+
*
|
|
3446
|
+
* WHY A SIBLING WITH NO USABLE HEAD BECOMES A VIOLATION RATHER THAN BEING
|
|
3447
|
+
* SKIPPED, which is the whole reason this is a function and not a `filter`.
|
|
3448
|
+
* Dropping such a sibling silently SHRINKS the group, and a shrinking group is
|
|
3449
|
+
* exactly the fail-open shape this file has already been bitten by twice, at
|
|
3450
|
+
* `loadCommittedVerdicts` and at the `phase` canonicalisation. With three
|
|
3451
|
+
* verdicts, two of them sharing a model family, giving the third an unreadable
|
|
3452
|
+
* head would leave a compared pair of two and a green run. So every same-phase
|
|
3453
|
+
* sibling that cannot be keyed is REPORTED as a violation and the remaining
|
|
3454
|
+
* members are still compared: a reader is owed both facts.
|
|
3455
|
+
*/
|
|
3456
|
+
function headGroupFor(verdicts, phaseKey, headKey) {
|
|
3457
|
+
const members = [];
|
|
3458
|
+
const unkeyed = [];
|
|
3459
|
+
for (const candidate of verdicts) {
|
|
3460
|
+
/* BOTH SIDES CANONICAL. `phaseKey` is already canonical; the sibling's is
|
|
3461
|
+
read through the same function so the two are compared in one form
|
|
3462
|
+
rather than one canonical value against one raw one.
|
|
3463
|
+
|
|
3464
|
+
AND THE TWO ARMS ARE SPLIT, WHICH THE FIRST ROUND LEFT JOINED. `phase` is
|
|
3465
|
+
half of the join key, so a sibling whose phase cannot be ESTABLISHED is
|
|
3466
|
+
unkeyable for exactly the reason a sibling with no usable head is, and
|
|
3467
|
+
the paragraph above says what that costs. It was folded into one `||`
|
|
3468
|
+
with the determinate case, so a verdict declaring no phase fell out
|
|
3469
|
+
silently while one declaring a DIFFERENT phase fell out correctly.
|
|
3470
|
+
Measured at the reviewed head through the shipped CLI: a refusing third
|
|
3471
|
+
review with its `phase:` line deleted left both merge checks printing
|
|
3472
|
+
their affirmative REPORT lines over the remaining two. */
|
|
3473
|
+
const phaseReading = establishField(candidate.record, "phase");
|
|
3474
|
+
if (phaseReading.kind !== "established") {
|
|
3475
|
+
unkeyed.push({
|
|
3476
|
+
pointer: "#/phase",
|
|
3477
|
+
message: `${candidate.path} ${unestablishedReason(phaseReading, "phase")}, so it cannot be placed in or out of the group for phase ${phaseKey}, and a sibling that cannot be keyed must not shrink the set the delegated grant is read off`,
|
|
3478
|
+
});
|
|
3479
|
+
continue;
|
|
3480
|
+
}
|
|
3481
|
+
if (phaseReading.value !== phaseKey) {
|
|
3482
|
+
continue;
|
|
3483
|
+
}
|
|
3484
|
+
const key = headKeyOf(candidate.record, candidate.path);
|
|
3485
|
+
if (!key.ok) {
|
|
3486
|
+
unkeyed.push({ pointer: "#/head", message: key.message });
|
|
3487
|
+
continue;
|
|
3488
|
+
}
|
|
3489
|
+
if (key.value === headKey) {
|
|
3490
|
+
members.push(candidate);
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
return { members, unkeyed };
|
|
3494
|
+
}
|
|
3495
|
+
/* ------------------------------------------------------------------ */
|
|
3496
|
+
/* ANCHORING THE CORPUS TO THE COMMIT UNDER AUDIT (CR-VS-001) */
|
|
3497
|
+
/* ------------------------------------------------------------------ */
|
|
3498
|
+
/**
|
|
3499
|
+
* THE MECHANISM THIS SECTION CLOSES, STATED ONE LEVEL UP FROM THE FINDING:
|
|
3500
|
+
* a gate whose verdict is about EVIDENCE THE CALLER SUPPLIED, with no check
|
|
3501
|
+
* that the evidence is about the thing being audited.
|
|
3502
|
+
*
|
|
3503
|
+
* `headGroupFor` above groups by the head THE VERDICT DOCUMENTS THEMSELVES
|
|
3504
|
+
* DECLARE. Before this section nothing compared that value to the commit the
|
|
3505
|
+
* gate was running against, and nothing established that it named a commit at
|
|
3506
|
+
* all. Both arms were reproduced end to end through the shipped script:
|
|
3507
|
+
*
|
|
3508
|
+
* ARM A two decorrelated APPROVE verdicts naming
|
|
3509
|
+
* deadbeefdeadbeefdeadbeefdeadbeefdeadbeef, which
|
|
3510
|
+
* `git cat-file -t` reports as not an object, in a repository whose
|
|
3511
|
+
* HEAD is a real commit -> GREEN, 2 units, exit 0
|
|
3512
|
+
* ARM B the same verdicts plus a further commit of work no verdict
|
|
3513
|
+
* mentions -> GREEN, exit 0
|
|
3514
|
+
*
|
|
3515
|
+
* So ONE committed approving pair turned this gate green for that phase at
|
|
3516
|
+
* every later head, indefinitely, on evidence about one old commit. That is
|
|
3517
|
+
* T-009's own rule ("a gate result is evidence only for the configuration it
|
|
3518
|
+
* ran under") failing inside the gate that carries DR-0012's merge grant.
|
|
3519
|
+
*
|
|
3520
|
+
* THE ASYMMETRY THAT MAKES THE FIX SOUND, and it is the whole of the design.
|
|
3521
|
+
* The AUDITED head comes from the CALLER: the gate runner passes `--head` from
|
|
3522
|
+
* the pull-request event (the registry entry now declares `parameters: [head]`,
|
|
3523
|
+
* the same mechanism `scope` has always used), and with no `--head` it is the
|
|
3524
|
+
* commit the context's own `HEAD` resolves to, which is what the checkout put
|
|
3525
|
+
* there. The DECLARED head comes from a document an agent under review wrote.
|
|
3526
|
+
* One of those two is a fact about the run and the other is a claim in the
|
|
3527
|
+
* evidence, and the claim must be measured against the fact rather than
|
|
3528
|
+
* replacing it.
|
|
3529
|
+
*/
|
|
3530
|
+
/** Resolve one ref to a full commit sha inside a context directory. */
|
|
3531
|
+
export function resolveCommitIn(contextDirectory, ref) {
|
|
3532
|
+
/* `^{commit}` AND NOT A BARE `rev-parse`. A bare `rev-parse` of a forty-hex
|
|
3533
|
+
string that is in no object database ECHOES IT BACK and exits 0, so it
|
|
3534
|
+
cannot tell a commit from a sha somebody typed. The peel is what makes
|
|
3535
|
+
this a question about the object database rather than about the syntax of
|
|
3536
|
+
the argument, and it also refuses a tag or a tree that is not a commit.
|
|
3537
|
+
`--end-of-options` keeps a ref that begins with `-` from being read as a
|
|
3538
|
+
flag. */
|
|
3539
|
+
const resolved = gitIn(["rev-parse", "--verify", "--quiet", "--end-of-options", `${ref}^{commit}`], contextDirectory);
|
|
3540
|
+
if (!resolved.ok) {
|
|
3541
|
+
return { ok: false, reason: resolved.reason };
|
|
3542
|
+
}
|
|
3543
|
+
const sha = resolved.stdout.trim();
|
|
3544
|
+
if (!FULL_SHA.test(sha)) {
|
|
3545
|
+
return {
|
|
3546
|
+
ok: false,
|
|
3547
|
+
reason: `git rev-parse ${ref}^{commit} in ${contextDirectory} produced ${sha}, which is not a full commit sha`,
|
|
3548
|
+
};
|
|
3549
|
+
}
|
|
3550
|
+
return { ok: true, sha };
|
|
3551
|
+
}
|
|
3552
|
+
export function resolveAuditedHead(contextDirectory, requested, source) {
|
|
3553
|
+
if (requested !== undefined) {
|
|
3554
|
+
const resolved = resolveCommitIn(contextDirectory, requested);
|
|
3555
|
+
if (!resolved.ok) {
|
|
3556
|
+
/* M2-C-3. A caller that named a head this repository cannot produce has
|
|
3557
|
+
not told the gate which commit to judge, and a gate that carried on
|
|
3558
|
+
would be judging whatever the documents felt like naming, which is the
|
|
3559
|
+
state this whole section exists to end. `error`, never green and never
|
|
3560
|
+
not-applicable: nothing has been evaluated. */
|
|
3561
|
+
return {
|
|
3562
|
+
kind: "error",
|
|
3563
|
+
reason: `--head ${requested} does not resolve to a commit in ${contextDirectory}, so the commit under audit ` +
|
|
3564
|
+
`was not established and no merge verdict can be reached over evidence that names its own subject: ${resolved.reason}`,
|
|
3565
|
+
};
|
|
3566
|
+
}
|
|
3567
|
+
return { kind: "anchored", head: resolved.sha, how: `--head ${requested}` };
|
|
3568
|
+
}
|
|
3569
|
+
if (source.kind === "commit") {
|
|
3570
|
+
return { kind: "anchored", head: source.refSha, how: `${source.ref}, resolved in ${contextDirectory}` };
|
|
3571
|
+
}
|
|
3572
|
+
return { kind: "unanchored", reason: source.reason };
|
|
3573
|
+
}
|
|
3574
|
+
/**
|
|
3575
|
+
* Ask git whether `candidate` is an ancestor of `descendant`.
|
|
3576
|
+
*
|
|
3577
|
+
* `merge-base --is-ancestor` ANSWERS WITH AN EXIT CODE, and 1 is an ANSWER
|
|
3578
|
+
* while anything else is a FAILURE. `gitIn` folds every nonzero status into
|
|
3579
|
+
* `ok: false`, which would make "no" indistinguishable from "git could not
|
|
3580
|
+
* run", and that collapse is the fail-open direction here: an unanswerable
|
|
3581
|
+
* question read as "not an ancestor" is merely noisy, but read as "ancestor"
|
|
3582
|
+
* it would admit anything. Three outcomes, never two.
|
|
3583
|
+
*/
|
|
3584
|
+
function isAncestorIn(contextDirectory, candidate, descendant) {
|
|
3585
|
+
const run = spawnSync("git", ["merge-base", "--is-ancestor", "--end-of-options", candidate, descendant], { cwd: contextDirectory, encoding: "utf8" });
|
|
3586
|
+
if (run.error !== undefined) {
|
|
3587
|
+
return {
|
|
3588
|
+
kind: "undetermined",
|
|
3589
|
+
reason: `git merge-base --is-ancestor ${candidate} ${descendant} could not be run: ${String(run.error)}`,
|
|
3590
|
+
};
|
|
3591
|
+
}
|
|
3592
|
+
if (run.status === 0) {
|
|
3593
|
+
return { kind: "yes" };
|
|
3594
|
+
}
|
|
3595
|
+
if (run.status === 1) {
|
|
3596
|
+
return { kind: "no" };
|
|
3597
|
+
}
|
|
3598
|
+
return {
|
|
3599
|
+
kind: "undetermined",
|
|
3600
|
+
reason: `git merge-base --is-ancestor ${candidate} ${descendant} exited ${String(run.status)}: ` +
|
|
3601
|
+
`${(run.stderr ?? "").replace(/\s+/g, " ").trim()}`,
|
|
3602
|
+
};
|
|
3603
|
+
}
|
|
3604
|
+
/**
|
|
3605
|
+
* The paperwork prefix a changed path must carry to count as evidence, spelled
|
|
3606
|
+
* for THIS context directory rather than for the repository root.
|
|
3607
|
+
*
|
|
3608
|
+
* `git diff --name-only` prints paths relative to the REPOSITORY ROOT, and the
|
|
3609
|
+
* context directory need not be that root. Reading `delivery/` out of a nested
|
|
3610
|
+
* context would then classify the repository root's `delivery/` as this
|
|
3611
|
+
* project's paperwork and a nested `sub/delivery/` as shipped content, which is
|
|
3612
|
+
* backwards. `rev-parse --show-prefix` gives the offset, so the comparison is
|
|
3613
|
+
* made in the repository's own spelling. The diff is deliberately NOT narrowed
|
|
3614
|
+
* to the context directory: a change outside it is still unreviewed content in
|
|
3615
|
+
* the audited commit, and narrowing would hide it.
|
|
3616
|
+
*/
|
|
3617
|
+
function evidencePrefixIn(contextDirectory) {
|
|
3618
|
+
const shown = gitIn(["rev-parse", "--show-prefix"], contextDirectory);
|
|
3619
|
+
if (!shown.ok) {
|
|
3620
|
+
return { ok: false, reason: shown.reason };
|
|
3621
|
+
}
|
|
3622
|
+
return { ok: true, prefix: `${shown.stdout.trim()}${PAPERWORK_ROOT}/` };
|
|
3623
|
+
}
|
|
3624
|
+
/**
|
|
3625
|
+
* Place one declared head against the commit under audit.
|
|
3626
|
+
*
|
|
3627
|
+
* EQUAL PASSES, unchanged, and it is checked first so a context git cannot be
|
|
3628
|
+
* questioned about still answers the one relation that needs no git at all.
|
|
3629
|
+
*
|
|
3630
|
+
* A DESCENDANT IS REFUSED, and it has its own sentence rather than being folded
|
|
3631
|
+
* into "not an ancestor". A verdict naming a commit BELOW the audited one is a
|
|
3632
|
+
* review of work the audited commit does not contain, which is the fail-open
|
|
3633
|
+
* direction stated backwards: the reviewers saw more than is being merged, and
|
|
3634
|
+
* nothing here establishes that what they approved about the extra work says
|
|
3635
|
+
* anything about the tree without it.
|
|
3636
|
+
*/
|
|
3637
|
+
export function relateDeclaredHead(contextDirectory, declared, auditedHead) {
|
|
3638
|
+
if (declared === auditedHead) {
|
|
3639
|
+
return { kind: "same" };
|
|
3640
|
+
}
|
|
3641
|
+
const resolved = resolveCommitIn(contextDirectory, declared);
|
|
3642
|
+
if (!resolved.ok) {
|
|
3643
|
+
return { kind: "unresolvable", reason: resolved.reason };
|
|
3644
|
+
}
|
|
3645
|
+
const ancestor = isAncestorIn(contextDirectory, declared, auditedHead);
|
|
3646
|
+
if (ancestor.kind === "undetermined") {
|
|
3647
|
+
return { kind: "undetermined", reason: ancestor.reason };
|
|
3648
|
+
}
|
|
3649
|
+
if (ancestor.kind === "no") {
|
|
3650
|
+
const other = isAncestorIn(contextDirectory, auditedHead, declared);
|
|
3651
|
+
if (other.kind === "undetermined") {
|
|
3652
|
+
return { kind: "undetermined", reason: other.reason };
|
|
3653
|
+
}
|
|
3654
|
+
return other.kind === "yes" ? { kind: "descendant" } : { kind: "unrelated" };
|
|
3655
|
+
}
|
|
3656
|
+
const prefix = evidencePrefixIn(contextDirectory);
|
|
3657
|
+
if (!prefix.ok) {
|
|
3658
|
+
return { kind: "undetermined", reason: prefix.reason };
|
|
3659
|
+
}
|
|
3660
|
+
/* `-z` IS LOAD-BEARING FOR THE SAME REASON `--no-renames` IS, and it was
|
|
3661
|
+
measured rather than reasoned. Without it git QUOTES any path outside the
|
|
3662
|
+
printable ASCII set, so a paperwork file whose name carries one non-ASCII
|
|
3663
|
+
character arrives wrapped in double quotes with octal escapes, which does
|
|
3664
|
+
not start with `delivery/` and is classified as shipped content. Measured
|
|
3665
|
+
on git 2.43.0, one repository, one commit, one flag changed: the default
|
|
3666
|
+
form printed the quoted spelling and the `-z` form printed the real path.
|
|
3667
|
+
|
|
3668
|
+
The failure that would cause is fail-CLOSED, so it admits nothing it should
|
|
3669
|
+
not; it is fixed anyway because refusing a green a project is entitled to is
|
|
3670
|
+
the cannot-go-green shape this whole section exists to end, one filename
|
|
3671
|
+
narrower. `-z` also makes the separator NUL rather than newline, which is
|
|
3672
|
+
why the split changed with it: a newline split over `-z` output would read
|
|
3673
|
+
the whole list as one path. */
|
|
3674
|
+
const diff = gitIn(["diff", "-z", "--no-renames", "--name-only", `${declared}..${auditedHead}`, "--"], contextDirectory);
|
|
3675
|
+
if (!diff.ok) {
|
|
3676
|
+
return { kind: "undetermined", reason: diff.reason };
|
|
3677
|
+
}
|
|
3678
|
+
const changed = diff.stdout.split("\0").map((line) => line.trim()).filter((line) => line !== "");
|
|
3679
|
+
const shipped = changed.filter((path) => !path.startsWith(prefix.prefix));
|
|
3680
|
+
return shipped.length === 0
|
|
3681
|
+
? { kind: "evidence-only-ancestor", changed }
|
|
3682
|
+
: { kind: "shipped-change", shipped };
|
|
3683
|
+
}
|
|
3684
|
+
/**
|
|
3685
|
+
* Split a loaded corpus into the verdicts that are about the audited commit
|
|
3686
|
+
* and the ones that are not.
|
|
3687
|
+
*
|
|
3688
|
+
* FIVE REFUSAL ROUTES, AND THEY ARE STRUCTURALLY DIFFERENT RATHER THAN ONE
|
|
3689
|
+
* SHAPE FIVE TIMES, which is what makes them members of a class instead of
|
|
3690
|
+
* instances of a finding:
|
|
3691
|
+
*
|
|
3692
|
+
* RESOLUTION the declared head is not a commit in this repository. The
|
|
3693
|
+
* document is evidence about an object nobody can produce.
|
|
3694
|
+
* SHIPPED GAP the declared head IS an ancestor, and shipped content changed
|
|
3695
|
+
* between it and the audited commit. Unreviewed work is riding
|
|
3696
|
+
* in on a review of something else.
|
|
3697
|
+
* DESCENDANT the declared head is BELOW the audited commit. The reviewers
|
|
3698
|
+
* read a tree the audited commit does not contain.
|
|
3699
|
+
* UNRELATED a real commit on neither side. A review of another line.
|
|
3700
|
+
* UNDETERMINED git could not place it. Never admitted, because a relation
|
|
3701
|
+
* nobody established must not read as the one that passes.
|
|
3702
|
+
*
|
|
3703
|
+
* ADMISSION IS TWO ROUTES AND THEY ARE ALSO PRINTED: the declared head IS the
|
|
3704
|
+
* audited commit, or it is an ancestor whose whole gap is paperwork. See
|
|
3705
|
+
* `relateDeclaredHead` for why the second is safe and for what it deliberately
|
|
3706
|
+
* does not refuse.
|
|
3707
|
+
*
|
|
3708
|
+
* Every refusal leaves the verdict out of the audited group and IS PRINTED with
|
|
3709
|
+
* the route it took, because a document silently dropped from a merge
|
|
3710
|
+
* corpus is the fail-open direction this file has already been bitten by at
|
|
3711
|
+
* `loadCommittedVerdicts`, at the `phase` canonicalisation and at
|
|
3712
|
+
* `headGroupFor`. A reader is owed the fact that the corpus holds two
|
|
3713
|
+
* approving reviews of something else.
|
|
3714
|
+
*/
|
|
3715
|
+
export function partitionByAuditedHead(contextDirectory, verdicts, auditedHead) {
|
|
3716
|
+
const onHead = [];
|
|
3717
|
+
const admitted = [];
|
|
3718
|
+
const offHead = [];
|
|
3719
|
+
const unkeyed = [];
|
|
3720
|
+
const unkeyedVerdicts = [];
|
|
3721
|
+
for (const candidate of verdicts) {
|
|
3722
|
+
const key = headKeyOf(candidate.record, candidate.path);
|
|
3723
|
+
if (!key.ok) {
|
|
3724
|
+
unkeyed.push({ pointer: "#/head", message: key.message });
|
|
3725
|
+
unkeyedVerdicts.push(candidate);
|
|
3726
|
+
continue;
|
|
3727
|
+
}
|
|
3728
|
+
const relation = relateDeclaredHead(contextDirectory, key.value, auditedHead);
|
|
3729
|
+
if (relation.kind === "same" || relation.kind === "evidence-only-ancestor") {
|
|
3730
|
+
onHead.push(candidate);
|
|
3731
|
+
admitted.push({ path: candidate.path, declared: key.value, relation });
|
|
3732
|
+
continue;
|
|
3733
|
+
}
|
|
3734
|
+
offHead.push({ path: candidate.path, declared: key.value, relation });
|
|
3735
|
+
}
|
|
3736
|
+
return { onHead, admitted, offHead, unkeyed, unkeyedVerdicts };
|
|
3737
|
+
}
|
|
3738
|
+
/** How many shipped paths to name before the sentence stops being readable. */
|
|
3739
|
+
const NAMED_SHIPPED_PATHS = 5;
|
|
3740
|
+
/** One operator-facing line per verdict the audit excluded, naming its route. */
|
|
3741
|
+
export function describeOffHeadVerdicts(offHead, auditedHead) {
|
|
3742
|
+
return [...offHead]
|
|
3743
|
+
.sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0))
|
|
3744
|
+
.map((entry) => {
|
|
3745
|
+
const head = `${entry.path} declares head ${entry.declared}, which`;
|
|
3746
|
+
const tail = `it is not evidence about the commit under audit ${auditedHead}`;
|
|
3747
|
+
if (entry.relation.kind === "unresolvable") {
|
|
3748
|
+
return `${head} does not resolve to a commit in this repository at all, so it is evidence about an object nobody can produce and ${tail}`;
|
|
3749
|
+
}
|
|
3750
|
+
if (entry.relation.kind === "shipped-change") {
|
|
3751
|
+
const shipped = entry.relation.shipped;
|
|
3752
|
+
const named = shipped.slice(0, NAMED_SHIPPED_PATHS).join(", ");
|
|
3753
|
+
const more = shipped.length > NAMED_SHIPPED_PATHS
|
|
3754
|
+
? ` and ${String(shipped.length - NAMED_SHIPPED_PATHS)} more`
|
|
3755
|
+
: "";
|
|
3756
|
+
return (`${head} is an ancestor of the commit under audit ${auditedHead}, but ${String(shipped.length)} path(s) ` +
|
|
3757
|
+
`outside ${PAPERWORK_ROOT}/ differ between them (${named}${more}), so shipped work no verdict reviewed ` +
|
|
3758
|
+
`is riding in on a review of something else and ${tail}`);
|
|
3759
|
+
}
|
|
3760
|
+
if (entry.relation.kind === "descendant") {
|
|
3761
|
+
return `${head} is a DESCENDANT of the commit under audit ${auditedHead}, so the reviewers read a tree this commit does not contain and ${tail}`;
|
|
3762
|
+
}
|
|
3763
|
+
if (entry.relation.kind === "undetermined") {
|
|
3764
|
+
return `${head} could not be placed relative to the commit under audit ${auditedHead}, so whether it reviews this work is unknown and ${tail}: ${entry.relation.reason}`;
|
|
3765
|
+
}
|
|
3766
|
+
return `${head} is a commit in this repository and is neither the commit under audit ${auditedHead} nor an ancestor of it, so it is a review of other work and ${tail}`;
|
|
3767
|
+
});
|
|
3768
|
+
}
|
|
3769
|
+
/** One operator-facing line per verdict the audit ADMITTED, naming its route. */
|
|
3770
|
+
export function describeAdmittedVerdicts(admitted, auditedHead) {
|
|
3771
|
+
return [...admitted]
|
|
3772
|
+
.sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0))
|
|
3773
|
+
.map((entry) => entry.relation.kind === "evidence-only-ancestor"
|
|
3774
|
+
? `${entry.path} declares head ${entry.declared}, an ancestor of the commit under audit ${auditedHead} whose ` +
|
|
3775
|
+
`${String(entry.relation.changed.length)} differing path(s) are all under ${PAPERWORK_ROOT}/, so the shipped ` +
|
|
3776
|
+
`content it reviewed is the shipped content of this commit`
|
|
3777
|
+
: `${entry.path} declares head ${entry.declared}, which IS the commit under audit`);
|
|
3778
|
+
}
|
|
3779
|
+
/**
|
|
3780
|
+
* Read the declared delivery mode's merge authority out of a context directory.
|
|
3781
|
+
*
|
|
3782
|
+
* LIFTED OUT OF `dual-review-decorrelation` BY M4-P10, AND THE LIFT IS THE
|
|
3783
|
+
* POINT RATHER THAN A TIDY-UP. M4-P10 adds a SECOND check that applies exactly
|
|
3784
|
+
* where DR-0012's delegated grant applies (`verdict-pair-approves`, condition
|
|
3785
|
+
* 2). Copying the regime reading into it would have produced two readers of one
|
|
3786
|
+
* fact, which is the shape this repository's fix-round contract calls fixing the
|
|
3787
|
+
* INSTANCE rather than the MECHANISM: a later correction to one reader would
|
|
3788
|
+
* leave the other fail-open, and the three sites already repaired inside this
|
|
3789
|
+
* block (recorded in the comments below) are the evidence that such corrections
|
|
3790
|
+
* happen.
|
|
3791
|
+
*
|
|
3792
|
+
* Every message below is the one `dual-review-decorrelation` shipped, byte for
|
|
3793
|
+
* byte, except that the two REPORT lines now name the CALLING check. That is
|
|
3794
|
+
* deliberate: a reader who sees `REPORT verdict-pair-approves ... declares no
|
|
3795
|
+
* delivery mode` must be able to tell which guard declined to run.
|
|
3796
|
+
*/
|
|
3797
|
+
function establishDelegatedRegime(checkId, contextDirectory, phase, source) {
|
|
3798
|
+
const charterPresent = contextDocumentPresentAt(contextDirectory, CHARTER_DOCUMENT, source);
|
|
3799
|
+
if (!charterPresent) {
|
|
3800
|
+
/* THE SENTENCE NAMES THE SOURCE, AND THE CLAIM ABOUT THE MERGE GATE NAMES
|
|
3801
|
+
THE READER BOTH SIDES SHARE (FIX ROUND 2, DV-001). Fix round 1 moved
|
|
3802
|
+
this PRESENCE probe to the commit and left the merge gate's refusal on
|
|
3803
|
+
disk, so a `charter.yaml` written into a working tree and committed
|
|
3804
|
+
nowhere passed the gate's refusal, reached this arm, and was reported as
|
|
3805
|
+
"no charter.yaml" while the file sat in the directory the same line
|
|
3806
|
+
names. The gate now refuses through `missingRegimeDocument` below,
|
|
3807
|
+
which is this same probe, so the second half of this sentence is a
|
|
3808
|
+
property of one shared function rather than a claim about another
|
|
3809
|
+
program that has to be maintained by hand. */
|
|
3810
|
+
return {
|
|
3811
|
+
kind: "report",
|
|
3812
|
+
lines: [
|
|
3813
|
+
`REPORT ${checkId} ${contextDirectory} declares no delivery mode ` +
|
|
3814
|
+
`(no ${CHARTER_DOCUMENT} ${describeContextDocumentSource(source)}), so the verdicts for phase ` +
|
|
3815
|
+
`${phase} were NOT evaluated against a merge-authority regime; the merge gate ` +
|
|
3816
|
+
`scripts/check-dual-review.mjs refuses such a directory outright, through the same presence ` +
|
|
3817
|
+
`reader and therefore against the same source`,
|
|
3818
|
+
],
|
|
3819
|
+
};
|
|
3820
|
+
}
|
|
3821
|
+
const charter = readContextDocumentAt(contextDirectory, CHARTER_DOCUMENT, source);
|
|
3822
|
+
if (!charter.ok) {
|
|
3823
|
+
return {
|
|
3824
|
+
kind: "violation",
|
|
3825
|
+
pointer: "#/produced-by",
|
|
3826
|
+
message: `the charter is present and could not be read, so the declared mode's merge-authority is unknown and decorrelation could not be evaluated: ${charter.reason}`,
|
|
3827
|
+
};
|
|
3828
|
+
}
|
|
3829
|
+
/* SITE TWO OF THE SAME MECHANISM. `asRecord(charter.value)?.["delivery-mode"]`
|
|
3830
|
+
used to flow into `String(modeId)` and into an `===` against every mode's
|
|
3831
|
+
id, so a charter declaring NO delivery mode reddened with the sentence
|
|
3832
|
+
"declares delivery mode undefined, which ... does not define". The verdict
|
|
3833
|
+
was right by luck and the sentence was false: the charter declares no mode
|
|
3834
|
+
rather than one called "undefined". Establishing it first gives absence its
|
|
3835
|
+
own sentence, and gives the `===` below a non-empty string, which is also
|
|
3836
|
+
what stops an id-less mode row (`eachMode` defaults a missing id to "")
|
|
3837
|
+
from matching a charter whose delivery-mode is the empty string. */
|
|
3838
|
+
const modeReading = establishField(asRecord(charter.value), "delivery-mode");
|
|
3839
|
+
if (modeReading.kind !== "established") {
|
|
3840
|
+
return {
|
|
3841
|
+
kind: "violation",
|
|
3842
|
+
pointer: "#/produced-by",
|
|
3843
|
+
message: `${charter.path} ${unestablishedReason(modeReading, "delivery-mode")}, so no mode's merge-authority can be looked up and whether the delegated grant applies to phase ${phase} could not be established`,
|
|
3844
|
+
};
|
|
3845
|
+
}
|
|
3846
|
+
const modeId = modeReading.value;
|
|
3847
|
+
const modesDocument = readContextDocumentAt(contextDirectory, MODES_DOCUMENT, source);
|
|
3848
|
+
if (!modesDocument.ok) {
|
|
3849
|
+
return {
|
|
3850
|
+
kind: "violation",
|
|
3851
|
+
pointer: "#/produced-by",
|
|
3852
|
+
message: `${charter.path} declares delivery mode ${String(modeId)} and ${MODES_DOCUMENT} could not be read, so that mode's merge-authority is unknown and decorrelation could not be evaluated: ${modesDocument.reason}`,
|
|
3853
|
+
};
|
|
3854
|
+
}
|
|
3855
|
+
/* BOTH SIDES CANONICAL, and the direction here is worth stating because it
|
|
3856
|
+
is the one place in this function where collapsing makes a lookup SUCCEED
|
|
3857
|
+
more often rather than fail. `eachMode` builds `row.id` with its own
|
|
3858
|
+
`String(... ?? "")` and is shared with six other consumers, so it is left
|
|
3859
|
+
alone and its output is canonicalised at THIS use site. Finding the mode
|
|
3860
|
+
a charter actually names is the correct reading; the security-relevant
|
|
3861
|
+
comparison is the `merge-authority` one below, and THAT one is fail-closed
|
|
3862
|
+
under collapsing, because more values matching the delegated constant
|
|
3863
|
+
means the decorrelation requirement applies more often, never less. */
|
|
3864
|
+
const mode = eachMode(modesDocument.value).find((row) => {
|
|
3865
|
+
const reading = canonicalScalar(row.id);
|
|
3866
|
+
return reading.ok && reading.value === modeId;
|
|
3867
|
+
});
|
|
3868
|
+
if (mode === undefined) {
|
|
3869
|
+
return {
|
|
3870
|
+
kind: "violation",
|
|
3871
|
+
pointer: "#/produced-by",
|
|
3872
|
+
message: `${charter.path} declares delivery mode ${String(modeId)}, which ${modesDocument.path} does not define, so its merge-authority is unknown`,
|
|
3873
|
+
};
|
|
3874
|
+
}
|
|
3875
|
+
/* SITE THREE, AND IT IS THE WORST OF THE FOUR BECAUSE IT DISABLES THE WHOLE
|
|
3876
|
+
CHECK RATHER THAN ONE DIMENSION. `String(mode.mode["merge-authority"] ?? "")`
|
|
3877
|
+
made a mode that declares NO merge-authority indistinguishable from one
|
|
3878
|
+
declaring some other authority, and the not-a-delegated-grant arm below is
|
|
3879
|
+
a REPORT rather than a violation. Measured on the shipped script before
|
|
3880
|
+
this repair (probe P1 in delivery/work-history/m3-p9.md): a pair sharing
|
|
3881
|
+
one model family, under a mode with its `merge-authority` line deleted,
|
|
3882
|
+
exited 0 GREEN printing "mode full declares merge-authority , which is not
|
|
3883
|
+
a delegated grant". That sentence is false and the exit code authorises
|
|
3884
|
+
the merge the check exists to refuse. The reviewer did not find this one;
|
|
3885
|
+
the derivation did. */
|
|
3886
|
+
const authorityReading = establishField(mode.mode, "merge-authority");
|
|
3887
|
+
if (authorityReading.kind !== "established") {
|
|
3888
|
+
return {
|
|
3889
|
+
kind: "violation",
|
|
3890
|
+
pointer: "#/produced-by",
|
|
3891
|
+
message: `${modesDocument.path} ${unestablishedReason(authorityReading, "merge-authority")} for mode ${modeId}, so whether the delegated grant applies to phase ${phase} could not be established, and a merge check that cannot determine the regime must not report that no decorrelation is required`,
|
|
3892
|
+
};
|
|
3893
|
+
}
|
|
3894
|
+
const authority = authorityReading.value;
|
|
3895
|
+
if (authority !== DELEGATED_MERGE_AUTHORITY) {
|
|
3896
|
+
return {
|
|
3897
|
+
kind: "report",
|
|
3898
|
+
lines: [
|
|
3899
|
+
`REPORT ${checkId} mode ${String(modeId)} declares merge-authority ${authority}, ` +
|
|
3900
|
+
`which is not a delegated grant, so no decorrelation is required of the reviews of phase ${phase}`,
|
|
3901
|
+
],
|
|
3902
|
+
};
|
|
3903
|
+
}
|
|
3904
|
+
return { kind: "delegated" };
|
|
3905
|
+
}
|
|
3906
|
+
/* ------------------------------------------------------------------ */
|
|
3907
|
+
/* review-families: DR-0038's declared single-family exception (M4-P11) */
|
|
3908
|
+
/* ------------------------------------------------------------------ */
|
|
3909
|
+
/** The charter field DR-0038's declaration lives in (M4-D-28). */
|
|
3910
|
+
export const REVIEW_FAMILIES_FIELD = "review-families";
|
|
3911
|
+
/** The document that carries it. */
|
|
3912
|
+
export const CHARTER_DOCUMENT = "charter.yaml";
|
|
3913
|
+
/**
|
|
3914
|
+
* The documents that say WHICH merge-authority regime is in force.
|
|
3915
|
+
*
|
|
3916
|
+
* MOVED HERE FROM `scripts/check-dual-review.mjs` (FIX ROUND 2, DV-001). The
|
|
3917
|
+
* script held its own copy of this list AND its own presence probe, and the
|
|
3918
|
+
* probe read the WORKING TREE while `establishDelegatedRegime` read the
|
|
3919
|
+
* COMMIT. Two probes of one fact against two sources is the mechanism this
|
|
3920
|
+
* phase has now paid for twice: each answered correctly about its own source,
|
|
3921
|
+
* so nothing ever reported a disagreement, and an uncommitted `charter.yaml`
|
|
3922
|
+
* took the gate from error to GREEN on a correlated committed pair.
|
|
3923
|
+
*/
|
|
3924
|
+
export const REGIME_DOCUMENTS = [CHARTER_DOCUMENT, MODES_DOCUMENT];
|
|
3925
|
+
/**
|
|
3926
|
+
* The first regime document that is NOT present at the source a decision over
|
|
3927
|
+
* this context would be made from, or `undefined` when both are.
|
|
3928
|
+
*
|
|
3929
|
+
* WHY THE REFUSAL LIVES AT THE MERGE GATE AND THE REPORT LIVES IN THE CHECK,
|
|
3930
|
+
* unchanged from M3-P9 and restated because this round moved the probe: the
|
|
3931
|
+
* derived check runs on ANY verdict with ANY context, and M3-P7's verdict
|
|
3932
|
+
* contexts carry a plan and a work history and no charter, so a check that
|
|
3933
|
+
* reddened on an absent charter reddened eight of that phase's tests. The
|
|
3934
|
+
* check therefore REPORTS, and `scripts/check-dual-review.mjs`, which is the
|
|
3935
|
+
* command DR-0012's grant runs through, refuses. What changed is that the
|
|
3936
|
+
* refusal and the report are now ONE probe with two callers, so they cannot
|
|
3937
|
+
* answer about different sources.
|
|
3938
|
+
*
|
|
3939
|
+
* THE SOURCE IS A PARAMETER, not resolved here, so a caller that has already
|
|
3940
|
+
* resolved one (the gate script resolves it when it loads the corpus) refuses
|
|
3941
|
+
* against the SAME commit it read the verdicts from rather than a second
|
|
3942
|
+
* `rev-parse` that could land elsewhere.
|
|
3943
|
+
*/
|
|
3944
|
+
export function missingRegimeDocument(contextDirectory, source = resolveCorpusSource(contextDirectory)) {
|
|
3945
|
+
for (const document of REGIME_DOCUMENTS) {
|
|
3946
|
+
if (!contextDocumentPresentAt(contextDirectory, document, source)) {
|
|
3947
|
+
return {
|
|
3948
|
+
document,
|
|
3949
|
+
source,
|
|
3950
|
+
reason: `${join(contextDirectory, document)} does not exist ${describeContextDocumentSource(source)}, so ` +
|
|
3951
|
+
`the declared mode's merge-authority is unknown and no decorrelation verdict can be reached; a ` +
|
|
3952
|
+
`merge check that cannot determine the regime reports error, never green`,
|
|
3953
|
+
};
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
return undefined;
|
|
3957
|
+
}
|
|
3958
|
+
function gitIn(args, cwd) {
|
|
3959
|
+
/* The buffer is raised because a charter is an operator document with no
|
|
3960
|
+
declared size bound, and a truncated read would decode as a DIFFERENT
|
|
3961
|
+
document rather than as a failure. */
|
|
3962
|
+
const run = spawnSync("git", args, { cwd, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
3963
|
+
if (run.error !== undefined) {
|
|
3964
|
+
return { ok: false, reason: `git ${args.join(" ")} could not be run: ${String(run.error)}` };
|
|
3965
|
+
}
|
|
3966
|
+
if (run.status !== 0) {
|
|
3967
|
+
return {
|
|
3968
|
+
ok: false,
|
|
3969
|
+
reason: `git ${args.join(" ")} exited ${String(run.status)}: ${(run.stderr ?? "").replace(/\s+/g, " ").trim()}`,
|
|
3970
|
+
};
|
|
3971
|
+
}
|
|
3972
|
+
return { ok: true, stdout: run.stdout ?? "" };
|
|
3973
|
+
}
|
|
3974
|
+
/**
|
|
3975
|
+
* Is a `review-families` key present in the WORKING TREE's charter?
|
|
3976
|
+
*
|
|
3977
|
+
* Asked for exactly one purpose: to tell "this project makes no declaration"
|
|
3978
|
+
* apart from "this project makes a declaration that is not committed". The
|
|
3979
|
+
* first is absence and changes nothing. The second is an ERROR, because the
|
|
3980
|
+
* whole value of the exception over a silent one is that a reader can check
|
|
3981
|
+
* it, and an uncommitted claim is one nothing can be checked against.
|
|
3982
|
+
*/
|
|
3983
|
+
function treeDeclaresReviewFamilies(contextDirectory) {
|
|
3984
|
+
const tree = readContextDocument(contextDirectory, CHARTER_DOCUMENT);
|
|
3985
|
+
if (!tree.ok) {
|
|
3986
|
+
return false;
|
|
3987
|
+
}
|
|
3988
|
+
const record = asRecord(tree.value);
|
|
3989
|
+
return record !== undefined && REVIEW_FAMILIES_FIELD in record;
|
|
3990
|
+
}
|
|
3991
|
+
/**
|
|
3992
|
+
* Read DR-0038's declaration OUT OF THE GIT OBJECT DATABASE, never out of the
|
|
3993
|
+
* working tree.
|
|
3994
|
+
*
|
|
3995
|
+
* WHY THE COMMITTED BLOB IS THE ONLY ONE THAT COUNTS. This is the anti-widening
|
|
3996
|
+
* rule the scope auditor and `loadDeclaration` (src/gates/release.ts:817) both
|
|
3997
|
+
* already apply, one condition along: a phase must not be able to switch off,
|
|
3998
|
+
* inside its own working tree, the condition that would otherwise have refused
|
|
3999
|
+
* its merge. A declaration read from disk is one an implementer can add,
|
|
4000
|
+
* merge under, and delete, leaving a merged head whose record says the
|
|
4001
|
+
* cross-family requirement was met.
|
|
4002
|
+
*
|
|
4003
|
+
* `HEAD:./charter.yaml` AND NOT `HEAD:charter.yaml`, and the difference is not
|
|
4004
|
+
* cosmetic. A path without the leading `./` is resolved against the repository
|
|
4005
|
+
* ROOT, so a context directory that happens to sit inside a larger repository
|
|
4006
|
+
* (which every fixture staged under a checkout does) would silently read that
|
|
4007
|
+
* repository's charter instead of its own. With `./` git resolves relative to
|
|
4008
|
+
* the directory it was run in, which is the one the caller named.
|
|
4009
|
+
*/
|
|
4010
|
+
export function readReviewFamilies(contextDirectory, ref = "HEAD") {
|
|
4011
|
+
const resolved = gitIn(["rev-parse", `${ref}^{commit}`], contextDirectory);
|
|
4012
|
+
const relativePath = `./${CHARTER_DOCUMENT}`;
|
|
4013
|
+
if (!resolved.ok) {
|
|
4014
|
+
if (treeDeclaresReviewFamilies(contextDirectory)) {
|
|
4015
|
+
return {
|
|
4016
|
+
kind: "error",
|
|
4017
|
+
reason: `${join(contextDirectory, CHARTER_DOCUMENT)} declares ${REVIEW_FAMILIES_FIELD} in the working tree and ` +
|
|
4018
|
+
`${contextDirectory} has no resolvable git ref ${ref}, so the declaration cannot be attributed to a ` +
|
|
4019
|
+
`commit; an exception read from an uncommitted file is error, never permission (${resolved.reason})`,
|
|
4020
|
+
};
|
|
4021
|
+
}
|
|
4022
|
+
return { kind: "absent" };
|
|
4023
|
+
}
|
|
4024
|
+
const refSha = resolved.stdout.trim();
|
|
4025
|
+
const shown = gitIn(["show", `${refSha}:${relativePath}`], contextDirectory);
|
|
4026
|
+
if (!shown.ok) {
|
|
4027
|
+
if (treeDeclaresReviewFamilies(contextDirectory)) {
|
|
4028
|
+
return {
|
|
4029
|
+
kind: "error",
|
|
4030
|
+
reason: `${join(contextDirectory, CHARTER_DOCUMENT)} declares ${REVIEW_FAMILIES_FIELD} in the working tree and ` +
|
|
4031
|
+
`${refSha}:${relativePath} could not be read, so the declaration is not committed and cannot be ` +
|
|
4032
|
+
`attributed; an exception read from an uncommitted file is error, never permission (${shown.reason})`,
|
|
4033
|
+
};
|
|
4034
|
+
}
|
|
4035
|
+
return { kind: "absent" };
|
|
4036
|
+
}
|
|
4037
|
+
const body = shown.stdout;
|
|
4038
|
+
const sha256 = createHash("sha256").update(body).digest("hex");
|
|
4039
|
+
const provenance = {
|
|
4040
|
+
path: CHARTER_DOCUMENT,
|
|
4041
|
+
ref,
|
|
4042
|
+
refSha,
|
|
4043
|
+
sha256,
|
|
4044
|
+
};
|
|
4045
|
+
const decoded = decodeDocument(body, join(contextDirectory, CHARTER_DOCUMENT));
|
|
4046
|
+
if (!decoded.ok) {
|
|
4047
|
+
return {
|
|
4048
|
+
kind: "error",
|
|
4049
|
+
reason: `${refSha}:${relativePath} does not decode, so whether it declares ${REVIEW_FAMILIES_FIELD} could not be established: ${decoded.reason}`,
|
|
4050
|
+
};
|
|
4051
|
+
}
|
|
4052
|
+
const charter = asRecord(decoded.value);
|
|
4053
|
+
if (charter === undefined || !(REVIEW_FAMILIES_FIELD in charter)) {
|
|
4054
|
+
return { kind: "absent" };
|
|
4055
|
+
}
|
|
4056
|
+
const declaration = asRecord(charter[REVIEW_FAMILIES_FIELD]);
|
|
4057
|
+
if (declaration === undefined) {
|
|
4058
|
+
return {
|
|
4059
|
+
kind: "error",
|
|
4060
|
+
reason: `${refSha}:${relativePath} carries ${REVIEW_FAMILIES_FIELD} and it is not a map, so no declared family set can be read from it`,
|
|
4061
|
+
};
|
|
4062
|
+
}
|
|
4063
|
+
const reasonReading = establishField(declaration, "reason");
|
|
4064
|
+
if (reasonReading.kind !== "established") {
|
|
4065
|
+
return {
|
|
4066
|
+
kind: "error",
|
|
4067
|
+
reason: `${refSha}:${relativePath} ${unestablishedReason(reasonReading, `${REVIEW_FAMILIES_FIELD}.reason`)}; ` +
|
|
4068
|
+
`narrowing an owner-reserved merge condition costs a stated reason, so a declaration without one is error`,
|
|
4069
|
+
};
|
|
4070
|
+
}
|
|
4071
|
+
const available = declaration["available"];
|
|
4072
|
+
if (!Array.isArray(available) || available.length === 0) {
|
|
4073
|
+
return {
|
|
4074
|
+
kind: "error",
|
|
4075
|
+
reason: `${refSha}:${relativePath} declares ${REVIEW_FAMILIES_FIELD}.available as ${Array.isArray(available) ? "an empty list" : "not a list"}, so no family set can be read from it`,
|
|
4076
|
+
};
|
|
4077
|
+
}
|
|
4078
|
+
const families = [];
|
|
4079
|
+
const declaredAs = [];
|
|
4080
|
+
for (let index = 0; index < available.length; index += 1) {
|
|
4081
|
+
const entry = available[index];
|
|
4082
|
+
/* Each entry goes through the SAME canonical form a verdict's
|
|
4083
|
+
`produced-by` goes through, because the two are compared to each other
|
|
4084
|
+
below. A declaration canonicalised one way and an observation
|
|
4085
|
+
canonicalised another is the fix-round-2 mechanism with two documents
|
|
4086
|
+
instead of one. */
|
|
4087
|
+
const reading = establishField({ entry }, "entry");
|
|
4088
|
+
if (reading.kind !== "established") {
|
|
4089
|
+
return {
|
|
4090
|
+
kind: "error",
|
|
4091
|
+
reason: `${refSha}:${relativePath} ${unestablishedReason(reading, `${REVIEW_FAMILIES_FIELD}.available[${String(index)}]`)}, so the declared family set cannot be compared with what the verdicts carry`,
|
|
4092
|
+
};
|
|
4093
|
+
}
|
|
4094
|
+
if (families.includes(reading.value)) {
|
|
4095
|
+
/* REFUSED RATHER THAN DEDUPLICATED, and the direction is why. The
|
|
4096
|
+
exception applies when exactly ONE family is declared, so collapsing
|
|
4097
|
+
`[Anthropic, anthropic]` to one entry would turn a document that reads
|
|
4098
|
+
as two families into a single-family declaration. That is the only
|
|
4099
|
+
canonicalisation in this file that would be fail-OPEN, so it is a
|
|
4100
|
+
refusal instead. */
|
|
4101
|
+
return {
|
|
4102
|
+
kind: "error",
|
|
4103
|
+
reason: `${refSha}:${relativePath} lists ${String(entry)} in ${REVIEW_FAMILIES_FIELD}.available more than once once canonicalised, so how many families it declares cannot be established`,
|
|
4104
|
+
};
|
|
4105
|
+
}
|
|
4106
|
+
families.push(reading.value);
|
|
4107
|
+
declaredAs.push(String(entry));
|
|
4108
|
+
}
|
|
4109
|
+
return {
|
|
4110
|
+
kind: "declared",
|
|
4111
|
+
families: [...families].sort(),
|
|
4112
|
+
declaredAs,
|
|
4113
|
+
reason: declaration["reason"],
|
|
4114
|
+
provenance,
|
|
4115
|
+
};
|
|
4116
|
+
}
|
|
4117
|
+
/** One line naming where a declaration came from, for a detail or a report. */
|
|
4118
|
+
export function reviewFamiliesProvenanceLine(provenance) {
|
|
4119
|
+
return (`declaration ${provenance.path} read from ${provenance.ref} ` +
|
|
4120
|
+
`(${provenance.refSha}), blob sha256 ${provenance.sha256}`);
|
|
4121
|
+
}
|
|
4122
|
+
/**
|
|
4123
|
+
* DR-0038's exception, and its two falsifiers, over one committed corpus.
|
|
4124
|
+
*
|
|
4125
|
+
* THE EXCEPTION NARROWS EXACTLY ONE DIMENSION. `produced-by` stops being
|
|
4126
|
+
* required to differ. `framing` and `review-contract` are untouched, because
|
|
4127
|
+
* T-007's whole finding is that model decorrelation and CONTRACT decorrelation
|
|
4128
|
+
* are different properties: a single-family environment still has two framings
|
|
4129
|
+
* and two contracts available to it, so relaxing those would be relaxing
|
|
4130
|
+
* something the environment does not force.
|
|
4131
|
+
*
|
|
4132
|
+
* FALSIFIER 1, CONTRADICTION BY THE CORPUS. If the project's own committed
|
|
4133
|
+
* verdicts carry two or more distinct canonicalised `produced-by` values, the
|
|
4134
|
+
* declaration is contradicted by the project's own record and this is RED. A
|
|
4135
|
+
* project that has demonstrably used two cannot claim one.
|
|
4136
|
+
*
|
|
4137
|
+
* FALSIFIER 2, THE NAME MUST MATCH. A verdict whose `produced-by` canonicalises
|
|
4138
|
+
* to anything other than the declared family is RED. Without this, a
|
|
4139
|
+
* declaration could name a family nothing in the record uses and still buy the
|
|
4140
|
+
* relaxation.
|
|
4141
|
+
*
|
|
4142
|
+
* THE SCOPE IS THE WHOLE COMMITTED CORPUS, NOT THE ONE (phase, head) GROUP,
|
|
4143
|
+
* and that is deliberate. "This project has one family available" is a claim
|
|
4144
|
+
* about the project, so the widest set of its own verdicts is what can refute
|
|
4145
|
+
* it. Scoping the falsifiers to the group under review would let a project
|
|
4146
|
+
* whose history carries three families declare one, provided the two reviews
|
|
4147
|
+
* in front of the check happened to agree.
|
|
4148
|
+
*
|
|
4149
|
+
* WHAT IT DOES NOT CATCH, said here and not only in the plan: a project with a
|
|
4150
|
+
* second family AVAILABLE that has simply never used it. Nothing in a record of
|
|
4151
|
+
* what WAS used reaches what COULD have been used. `readReviewFamilies` answers
|
|
4152
|
+
* that with provenance rather than detection: the claim is attributable to a
|
|
4153
|
+
* commit and a blob, so it is dated and signed even where it is not refutable.
|
|
4154
|
+
*/
|
|
4155
|
+
export function singleFamilyException(contextDirectory, loaded) {
|
|
4156
|
+
/* ONE RESOLUTION, THREADED THROUGH, AND THAT IS THE STRUCTURAL HALF OF THE
|
|
4157
|
+
FIX (M4-P11 fix round 1). The declaration is no longer read at whatever
|
|
4158
|
+
`HEAD` happens to mean when `readReviewFamilies` is called: it is read at
|
|
4159
|
+
the EXACT commit sha the pair corpus was enumerated from, and the
|
|
4160
|
+
falsifiers' own corpus is then read from that same sha. A declaration and
|
|
4161
|
+
the evidence that refutes it come out of ONE TREE by construction, so
|
|
4162
|
+
"they disagree about what exists" is not a state this function can be put
|
|
4163
|
+
into rather than a state it checks for and hopes to catch.
|
|
4164
|
+
|
|
4165
|
+
On the worktree arm there is no commit to pin to, so `HEAD` is passed and
|
|
4166
|
+
`readReviewFamilies` performs the same `rev-parse` that just failed: it
|
|
4167
|
+
returns `absent`, or `error` if the working tree declares anyway. Either
|
|
4168
|
+
way no exception is granted, which is why one source of truth with no git
|
|
4169
|
+
is safe and two sources with git was not. */
|
|
4170
|
+
const declarationRef = loaded.source.kind === "commit" ? loaded.source.refSha : "HEAD";
|
|
4171
|
+
const reading = readReviewFamilies(contextDirectory, declarationRef);
|
|
4172
|
+
if (reading.kind === "error") {
|
|
4173
|
+
return { kind: "error", reason: reading.reason };
|
|
4174
|
+
}
|
|
4175
|
+
if (reading.kind === "absent") {
|
|
4176
|
+
/* AN ABSENT DECLARATION IS NOT PERMISSION. src/gates/release.ts:1037 states
|
|
4177
|
+
the rule for the sibling case; here it means the cross-family requirement
|
|
4178
|
+
applies unchanged, so a same-family pair stays red. */
|
|
4179
|
+
return { kind: "not-declared" };
|
|
4180
|
+
}
|
|
4181
|
+
if (reading.families.length !== 1) {
|
|
4182
|
+
/* A declaration of TWO OR MORE families is a valid declaration and it is
|
|
4183
|
+
not this exception. The environment says it has more than one, so
|
|
4184
|
+
DR-0012 condition 1 is satisfiable honestly and nothing is narrowed. */
|
|
4185
|
+
return { kind: "not-declared" };
|
|
4186
|
+
}
|
|
4187
|
+
/* THE FALSIFIERS' CORPUS IS READ HERE AND NOT BY THE CALLER, for two
|
|
4188
|
+
reasons that both matter. It is a WIDER set than the pair corpus, because
|
|
4189
|
+
the claim being falsified is about the project rather than about the two
|
|
4190
|
+
reviews in front of the check (CR-M4P11-002). And reading it only after a
|
|
4191
|
+
single-family declaration has been established means the wider read is
|
|
4192
|
+
paid for only by the runs that claim the exception, instead of by every
|
|
4193
|
+
verdict validated anywhere. */
|
|
4194
|
+
const paperwork = loaded.source.kind === "commit"
|
|
4195
|
+
? loadPaperworkVerdicts(contextDirectory, loaded.source.ref, loaded.source.refSha)
|
|
4196
|
+
: /* UNREACHABLE BY CONSTRUCTION AND NOT LEFT TO CHANCE: `reading` is
|
|
4197
|
+
`declared` only when `readReviewFamilies` resolved a commit, and it
|
|
4198
|
+
resolves the same ref this corpus failed to resolve. It is written
|
|
4199
|
+
as a refusal rather than an assertion because M2-C-3 says a check
|
|
4200
|
+
that cannot establish its subject reports error, never a verdict. */
|
|
4201
|
+
{
|
|
4202
|
+
ok: false,
|
|
4203
|
+
reason: `${CHARTER_DOCUMENT} declares ${REVIEW_FAMILIES_FIELD} at ${declarationRef} and ${contextDirectory} ` +
|
|
4204
|
+
`has no resolvable commit to read the refuting record from, so the exception could not be evaluated`,
|
|
4205
|
+
};
|
|
4206
|
+
if (!paperwork.ok) {
|
|
4207
|
+
return { kind: "error", reason: paperwork.reason };
|
|
4208
|
+
}
|
|
4209
|
+
const corpus = paperwork.verdicts;
|
|
4210
|
+
const corpusScope = describeVerdictCorpusSource(paperwork.source);
|
|
4211
|
+
const declared = reading.families[0];
|
|
4212
|
+
const provenance = reviewFamiliesProvenanceLine(reading.provenance);
|
|
4213
|
+
const violations = [];
|
|
4214
|
+
const observed = new Map();
|
|
4215
|
+
for (const candidate of corpus) {
|
|
4216
|
+
const value = establishField(candidate.record, "produced-by");
|
|
4217
|
+
if (value.kind !== "established") {
|
|
4218
|
+
violations.push({
|
|
4219
|
+
pointer: "#/produced-by",
|
|
4220
|
+
message: `${candidate.path} ${unestablishedReason(value, "produced-by")}, so it cannot be compared with the single family ${REVIEW_FAMILIES_FIELD} declares, and an exception cannot rest on a verdict that does not say what produced it; ${provenance}`,
|
|
4221
|
+
});
|
|
4222
|
+
continue;
|
|
4223
|
+
}
|
|
4224
|
+
observed.set(value.value, [...(observed.get(value.value) ?? []), candidate.path]);
|
|
4225
|
+
}
|
|
4226
|
+
const distinct = [...observed.keys()].sort();
|
|
4227
|
+
if (distinct.length > 1) {
|
|
4228
|
+
violations.push({
|
|
4229
|
+
pointer: "#/produced-by",
|
|
4230
|
+
message: `${CHARTER_DOCUMENT} declares the single review family ${reading.declaredAs.join(", ")} and the ` +
|
|
4231
|
+
`${String(corpus.length)} verdict document(s) committed under ${PAPERWORK_ROOT}/ carry ` +
|
|
4232
|
+
`${String(distinct.length)} distinct produced-by value(s) (${distinct.join(", ")}), so the declaration ` +
|
|
4233
|
+
`is contradicted by this project's own record and the exception does not apply; ${provenance} ` +
|
|
4234
|
+
corpusScope,
|
|
4235
|
+
});
|
|
4236
|
+
}
|
|
4237
|
+
for (const value of distinct) {
|
|
4238
|
+
if (value === declared) {
|
|
4239
|
+
continue;
|
|
4240
|
+
}
|
|
4241
|
+
violations.push({
|
|
4242
|
+
pointer: "#/produced-by",
|
|
4243
|
+
/* BOTH SPELLINGS OF THE DECLARED NAME, for the reason the `phase` label
|
|
4244
|
+
one screen down already gives: the canonical form is what was
|
|
4245
|
+
COMPARED and the operator's form is what is in the file they are
|
|
4246
|
+
holding. Printing only the canonical form tells someone whose charter
|
|
4247
|
+
says `Family-A` about a family called `family-a`. */
|
|
4248
|
+
message: `${CHARTER_DOCUMENT} declares the single review family ${reading.declaredAs.join(", ")} ` +
|
|
4249
|
+
`(canonically ${declared}) and ` +
|
|
4250
|
+
`${observed.get(value).sort().join(", ")} carr${observed.get(value).length === 1 ? "ies" : "y"} ` +
|
|
4251
|
+
`produced-by ${value}, which is not the declared family, so the exception does not apply to it; ${provenance}`,
|
|
4252
|
+
});
|
|
4253
|
+
}
|
|
4254
|
+
if (violations.length > 0) {
|
|
4255
|
+
return { kind: "refused", violations };
|
|
4256
|
+
}
|
|
4257
|
+
return {
|
|
4258
|
+
kind: "exempt",
|
|
4259
|
+
family: declared,
|
|
4260
|
+
reading,
|
|
4261
|
+
reports: [
|
|
4262
|
+
`REPORT single-family-declared ${CHARTER_DOCUMENT} declares exactly one review family ` +
|
|
4263
|
+
`(${reading.declaredAs.join(", ")}) and all ${String(corpus.length)} verdict document(s) committed ` +
|
|
4264
|
+
`under ${PAPERWORK_ROOT}/ carry it, so produced-by is NOT required to differ; framing and ` +
|
|
4265
|
+
`review-contract still are; reason: ${reading.reason}; ${provenance} ` +
|
|
4266
|
+
corpusScope,
|
|
4267
|
+
],
|
|
4268
|
+
};
|
|
4269
|
+
}
|
|
2902
4270
|
/**
|
|
2903
4271
|
* DR-0012's merge precondition, made into a comparison a command can make
|
|
2904
4272
|
* against the verdict FILES rather than against a session's memory (M3R-004).
|
|
@@ -2934,15 +4302,30 @@ function decorrelationTriple(record) {
|
|
|
2934
4302
|
* criteria contract. So `review-contract` is compared separately and is
|
|
2935
4303
|
* witnessed separately (criterion 7b).
|
|
2936
4304
|
*
|
|
2937
|
-
* WHAT IT DOES NOT REACH, named rather than left to be found
|
|
2938
|
-
*
|
|
2939
|
-
*
|
|
2940
|
-
*
|
|
2941
|
-
*
|
|
2942
|
-
*
|
|
2943
|
-
*
|
|
2944
|
-
*
|
|
2945
|
-
*
|
|
4305
|
+
* WHAT IT DOES NOT REACH, named rather than left to be found, AND BOTH ITEMS
|
|
4306
|
+
* THIS PARAGRAPH USED TO NAME HAVE BEEN CLOSED BY M4-P10. The first was
|
|
4307
|
+
* condition (d) of step 3b, that neither verdict carries an unresolved high or
|
|
4308
|
+
* medium finding; that is now the sibling check `verdict-pair-approves`, and
|
|
4309
|
+
* the schema's own root `if`/`then` was widened from [high, critical] to
|
|
4310
|
+
* [medium, high, critical] at the same time, because it had been one severity
|
|
4311
|
+
* narrower than DR-0012 condition 2 ever since it shipped. The second was that
|
|
4312
|
+
* nothing decided whether two verdicts describe the same HEAD; the schema now
|
|
4313
|
+
* requires `head` and this check groups by `(phase, head)`.
|
|
4314
|
+
*
|
|
4315
|
+
* WHAT IS STILL NOT REACHED, so the paragraph does not read as complete. This
|
|
4316
|
+
* check compares `produced-by` as a canonicalised STRING, never as a model
|
|
4317
|
+
* FAMILY, so two values naming one vendor pass as decorrelated; that was
|
|
4318
|
+
* measured twice against this repository's own reviews and recorded at
|
|
4319
|
+
* delivery/verification/m4-prototype-probes.md:1. THE DEFERRAL CHAIN FOR IT
|
|
4320
|
+
* TERMINATED WITH NO OWNER, which is what CR-VS-003 found and what this
|
|
4321
|
+
* paragraph used to hide: it said "closing it is M4-P11's declared scope", and
|
|
4322
|
+
* M4-P11 declined the mechanism (delivery/work-history/m4-p11.md:142) and
|
|
4323
|
+
* shipped the single-family EXCEPTION instead, which is a different question.
|
|
4324
|
+
* No later phase picked it up. The string comparison therefore STANDS, and what
|
|
4325
|
+
* the DR-0047 sweep changed is that the green line now says so: see
|
|
4326
|
+
* `producedByCaveat`, which also names what would close it. And a
|
|
4327
|
+
* `produced-by` line is written BY the reviewing agent, so it is forgeable; an
|
|
4328
|
+
* observed alternative exists and is M4-D-06's business, not this check's.
|
|
2946
4329
|
*/
|
|
2947
4330
|
export const dualReviewDecorrelation = {
|
|
2948
4331
|
id: "dual-review-decorrelation",
|
|
@@ -3013,135 +4396,80 @@ export const dualReviewDecorrelation = {
|
|
|
3013
4396
|
`error`. That is the path DR-0012's grant runs through, and it must never
|
|
3014
4397
|
report green without knowing the regime. Imposing the same refusal here
|
|
3015
4398
|
imposed it on a path the grant has nothing to do with. */
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
};
|
|
4399
|
+
/* ONE RESOLUTION FOR THE WHOLE DECISION (M4-P11 fix round 1). The regime,
|
|
4400
|
+
the declaration and both corpora are read from THIS value, so no two of
|
|
4401
|
+
them can describe different trees. Resolved before the regime rather
|
|
4402
|
+
than after it because the regime is the first thing that can end the
|
|
4403
|
+
run, and a regime read from an uncommitted charter was measured turning
|
|
4404
|
+
a red correlated pair green at exit 0. */
|
|
4405
|
+
const source = resolveCorpusSource(contextDirectory);
|
|
4406
|
+
const regime = establishDelegatedRegime("dual-review-decorrelation", contextDirectory, phase, source);
|
|
4407
|
+
if (regime.kind === "report") {
|
|
4408
|
+
return { violations: [], reports: regime.lines };
|
|
3026
4409
|
}
|
|
3027
|
-
|
|
3028
|
-
if (!charter.ok) {
|
|
4410
|
+
if (regime.kind === "violation") {
|
|
3029
4411
|
return {
|
|
3030
|
-
violations: [
|
|
3031
|
-
{
|
|
3032
|
-
pointer: "#/produced-by",
|
|
3033
|
-
message: `the charter is present and could not be read, so the declared mode's merge-authority is unknown and decorrelation could not be evaluated: ${charter.reason}`,
|
|
3034
|
-
},
|
|
3035
|
-
],
|
|
4412
|
+
violations: [{ pointer: regime.pointer, message: regime.message }],
|
|
3036
4413
|
reports: [],
|
|
3037
4414
|
};
|
|
3038
4415
|
}
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
id, so a charter declaring NO delivery mode reddened with the sentence
|
|
3042
|
-
"declares delivery mode undefined, which ... does not define". The verdict
|
|
3043
|
-
was right by luck and the sentence was false: the charter declares no mode
|
|
3044
|
-
rather than one called "undefined". Establishing it first gives absence its
|
|
3045
|
-
own sentence, and gives the `===` below a non-empty string, which is also
|
|
3046
|
-
what stops an id-less mode row (`eachMode` defaults a missing id to "")
|
|
3047
|
-
from matching a charter whose delivery-mode is the empty string. */
|
|
3048
|
-
const modeReading = establishField(asRecord(charter.value), "delivery-mode");
|
|
3049
|
-
if (modeReading.kind !== "established") {
|
|
3050
|
-
return {
|
|
3051
|
-
violations: [
|
|
3052
|
-
{
|
|
3053
|
-
pointer: "#/produced-by",
|
|
3054
|
-
message: `${charter.path} ${unestablishedReason(modeReading, "delivery-mode")}, so no mode's merge-authority can be looked up and whether the delegated grant applies to phase ${phase} could not be established`,
|
|
3055
|
-
},
|
|
3056
|
-
],
|
|
3057
|
-
reports: [],
|
|
3058
|
-
};
|
|
3059
|
-
}
|
|
3060
|
-
const modeId = modeReading.value;
|
|
3061
|
-
const modesDocument = readContextDocument(contextDirectory, MODES_DOCUMENT);
|
|
3062
|
-
if (!modesDocument.ok) {
|
|
3063
|
-
return {
|
|
3064
|
-
violations: [
|
|
3065
|
-
{
|
|
3066
|
-
pointer: "#/produced-by",
|
|
3067
|
-
message: `${charter.path} declares delivery mode ${String(modeId)} and ${MODES_DOCUMENT} could not be read, so that mode's merge-authority is unknown and decorrelation could not be evaluated: ${modesDocument.reason}`,
|
|
3068
|
-
},
|
|
3069
|
-
],
|
|
3070
|
-
reports: [],
|
|
3071
|
-
};
|
|
3072
|
-
}
|
|
3073
|
-
/* BOTH SIDES CANONICAL, and the direction here is worth stating because it
|
|
3074
|
-
is the one place in this function where collapsing makes a lookup SUCCEED
|
|
3075
|
-
more often rather than fail. `eachMode` builds `row.id` with its own
|
|
3076
|
-
`String(... ?? "")` and is shared with six other consumers, so it is left
|
|
3077
|
-
alone and its output is canonicalised at THIS use site. Finding the mode
|
|
3078
|
-
a charter actually names is the correct reading; the security-relevant
|
|
3079
|
-
comparison is the `merge-authority` one below, and THAT one is fail-closed
|
|
3080
|
-
under collapsing, because more values matching the delegated constant
|
|
3081
|
-
means the decorrelation requirement applies more often, never less. */
|
|
3082
|
-
const mode = eachMode(modesDocument.value).find((row) => {
|
|
3083
|
-
const reading = canonicalScalar(row.id);
|
|
3084
|
-
return reading.ok && reading.value === modeId;
|
|
3085
|
-
});
|
|
3086
|
-
if (mode === undefined) {
|
|
4416
|
+
const committed = loadCommittedVerdicts(contextDirectory, source);
|
|
4417
|
+
if (!committed.ok) {
|
|
3087
4418
|
return {
|
|
3088
|
-
violations: [
|
|
3089
|
-
{
|
|
3090
|
-
pointer: "#/produced-by",
|
|
3091
|
-
message: `${charter.path} declares delivery mode ${String(modeId)}, which ${modesDocument.path} does not define, so its merge-authority is unknown`,
|
|
3092
|
-
},
|
|
3093
|
-
],
|
|
4419
|
+
violations: [{ pointer: "#/produced-by", message: committed.reason }],
|
|
3094
4420
|
reports: [],
|
|
3095
4421
|
};
|
|
3096
4422
|
}
|
|
3097
|
-
/*
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
the
|
|
3108
|
-
|
|
3109
|
-
|
|
4423
|
+
/* DR-0038's EXCEPTION IS DECIDED HERE, BEFORE THE GROUP IS ASSEMBLED, AND
|
|
4424
|
+
THE POSITION IS LOAD-BEARING (M4-P11). Both falsifiers are claims about
|
|
4425
|
+
the project's WHOLE committed corpus, not about the pair in front of the
|
|
4426
|
+
check, so they are answerable without a head and they are answered first.
|
|
4427
|
+
Putting them after the head resolution would have made a contradicted
|
|
4428
|
+
declaration invisible on exactly the corpus that contradicts it: this
|
|
4429
|
+
repository's own two real review verdicts predate M4-P10's required
|
|
4430
|
+
`head` field, so the head arm returns before any of this would run.
|
|
4431
|
+
|
|
4432
|
+
A CONTRADICTED DECLARATION RETURNS IMMEDIATELY. It is red either way, so
|
|
4433
|
+
nothing is authorised by the early return, and what a reader needs first
|
|
4434
|
+
is that the project's declaration is false rather than a list of
|
|
4435
|
+
downstream consequences of believing it. */
|
|
4436
|
+
const exception = singleFamilyException(contextDirectory, committed);
|
|
4437
|
+
if (exception.kind === "error") {
|
|
4438
|
+
/* M2-C-3. A check that cannot establish whether an exception applies must
|
|
4439
|
+
not decide that it does not and carry on: that would silently impose
|
|
4440
|
+
the strict rule on a project that may have declared honestly, and,
|
|
4441
|
+
worse, would report a normal red that hides an unreadable declaration. */
|
|
3110
4442
|
return {
|
|
3111
|
-
violations: [
|
|
3112
|
-
{
|
|
3113
|
-
pointer: "#/produced-by",
|
|
3114
|
-
message: `${modesDocument.path} ${unestablishedReason(authorityReading, "merge-authority")} for mode ${modeId}, so whether the delegated grant applies to phase ${phase} could not be established, and a merge check that cannot determine the regime must not report that no decorrelation is required`,
|
|
3115
|
-
},
|
|
3116
|
-
],
|
|
4443
|
+
violations: [{ pointer: "#/produced-by", message: exception.reason }],
|
|
3117
4444
|
reports: [],
|
|
3118
4445
|
};
|
|
3119
4446
|
}
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
return {
|
|
3123
|
-
violations: [],
|
|
3124
|
-
reports: [
|
|
3125
|
-
`REPORT dual-review-decorrelation mode ${String(modeId)} declares merge-authority ${authority}, ` +
|
|
3126
|
-
`which is not a delegated grant, so no decorrelation is required of the reviews of phase ${phase}`,
|
|
3127
|
-
],
|
|
3128
|
-
};
|
|
4447
|
+
if (exception.kind === "refused") {
|
|
4448
|
+
return { violations: exception.violations, reports: [] };
|
|
3129
4449
|
}
|
|
3130
|
-
const
|
|
3131
|
-
|
|
4450
|
+
const exemptDimensions = exception.kind === "exempt" ? new Set(["produced-by"]) : new Set();
|
|
4451
|
+
const exceptionReports = exception.kind === "exempt" ? exception.reports : [];
|
|
4452
|
+
/* THE JOIN KEY IS NOW (phase, head), WHICH IS M4-P10's FIRST CHANGE. Until
|
|
4453
|
+
this line the key was `phase` alone and the DIRECTORY was what scoped a
|
|
4454
|
+
set of verdicts to one head, a convention declared in
|
|
4455
|
+
delivery/work-history/m3-p9.md and enforced by nothing. Under that
|
|
4456
|
+
convention two reviews of two DIFFERENT heads sitting in one directory
|
|
4457
|
+
were compared as a pair and the grant read as satisfied, which is
|
|
4458
|
+
DR-0012 condition 1's head clause
|
|
4459
|
+
(delivery/decisions/DR-0012-delegated-merge-authority.md:22) asserted by
|
|
4460
|
+
an operator rather than checked. The instance's OWN head is established
|
|
4461
|
+
first, so a verdict that does not say what it reviewed cannot select a
|
|
4462
|
+
group at all. */
|
|
4463
|
+
const ownHead = headKeyOf(verdict, "this verdict");
|
|
4464
|
+
if (!ownHead.ok) {
|
|
3132
4465
|
return {
|
|
3133
|
-
violations: [{ pointer: "#/
|
|
4466
|
+
violations: [{ pointer: "#/head", message: ownHead.message }],
|
|
3134
4467
|
reports: [],
|
|
3135
4468
|
};
|
|
3136
4469
|
}
|
|
3137
|
-
const
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
rather than one canonical value against one raw one. */
|
|
3141
|
-
(candidate) => {
|
|
3142
|
-
const reading = establishField(candidate.record, "phase");
|
|
3143
|
-
return reading.kind === "established" && reading.value === phaseKey;
|
|
3144
|
-
});
|
|
4470
|
+
const headKey = ownHead.value;
|
|
4471
|
+
const grouped = headGroupFor(committed.verdicts, phaseKey, headKey);
|
|
4472
|
+
const group = grouped.members;
|
|
3145
4473
|
/* MEMBERSHIP FIRST. DR-0012 condition 1 says the two reviews are WRITTEN TO
|
|
3146
4474
|
`delivery/review/` AND COMMITTED, so a verdict that is not among them is
|
|
3147
4475
|
not a review this rule can be satisfied by, however well decorrelated the
|
|
@@ -3153,20 +4481,35 @@ export const dualReviewDecorrelation = {
|
|
|
3153
4481
|
violations: [
|
|
3154
4482
|
{
|
|
3155
4483
|
pointer: "#/phase",
|
|
3156
|
-
message: `this verdict is not among the ${String(group.length)} verdict document(s) committed under ${REVIEW_DIRECTORY} for phase ${phase}, so it is not a review the delegated grant can be satisfied by`,
|
|
4484
|
+
message: `this verdict is not among the ${String(group.length)} verdict document(s) committed under ${REVIEW_DIRECTORY} for phase ${phase} at head ${headKey}, so it is not a review the delegated grant can be satisfied by ${describeVerdictCorpusSource(committed.source)}`,
|
|
3157
4485
|
},
|
|
3158
4486
|
],
|
|
3159
4487
|
reports: [],
|
|
3160
4488
|
};
|
|
3161
4489
|
}
|
|
3162
|
-
|
|
4490
|
+
/* THE SIBLINGS THAT COULD NOT BE LOOKED AT OR COULD NOT BE KEYED ARE
|
|
4491
|
+
CARRIED IN, NOT DROPPED. See `loadCommittedVerdicts` and `headGroupFor`:
|
|
4492
|
+
a candidate whose bytes do not read or decode, and a same-phase verdict
|
|
4493
|
+
with an unusable phase or head, each shrink the group silently
|
|
4494
|
+
otherwise, and a shrinking group is the fail-open shape. */
|
|
4495
|
+
const violations = [...committed.unexaminable, ...grouped.unkeyed];
|
|
3163
4496
|
if (group.length < 2) {
|
|
3164
4497
|
violations.push({
|
|
3165
4498
|
pointer: "#/phase",
|
|
3166
|
-
message: `only ${String(group.length)} verdict document(s) exist under ${REVIEW_DIRECTORY} for phase ${phase}, and a delegated grant requires two independent clean-room reviews of the exact head`,
|
|
4499
|
+
message: `only ${String(group.length)} verdict document(s) exist under ${REVIEW_DIRECTORY} for phase ${phase} at head ${headKey}, and a delegated grant requires two independent clean-room reviews of the exact head ${describeVerdictCorpusSource(committed.source)}`,
|
|
3167
4500
|
});
|
|
3168
4501
|
}
|
|
3169
4502
|
for (const dimension of DECORRELATION_DIMENSIONS) {
|
|
4503
|
+
if (exemptDimensions.has(dimension)) {
|
|
4504
|
+
/* THE ONE NARROWED DIMENSION (DR-0038, M4-P11). `continue` skips the
|
|
4505
|
+
DISTINCTNESS requirement only, and it is reached only after both
|
|
4506
|
+
falsifiers passed, which means every committed verdict has already
|
|
4507
|
+
been read and found to carry the one declared family. So this is not
|
|
4508
|
+
a dimension that stopped being looked at: it is one that was looked
|
|
4509
|
+
at against a different rule. The two dimensions below are untouched,
|
|
4510
|
+
which is what stops the exception relaxing the whole check. */
|
|
4511
|
+
continue;
|
|
4512
|
+
}
|
|
3170
4513
|
/* SITE ONE, THE ONE CR-001 REPORTS. ABSENCE IS ITS OWN VERDICT AND IT IS A
|
|
3171
4514
|
FAIL, and the choice was deliberate rather than inherited.
|
|
3172
4515
|
|
|
@@ -3207,16 +4550,376 @@ export const dualReviewDecorrelation = {
|
|
|
3207
4550
|
});
|
|
3208
4551
|
}
|
|
3209
4552
|
}
|
|
4553
|
+
/* THE REPORT NAMES THE DIMENSIONS ACTUALLY COMPARED, never the constant.
|
|
4554
|
+
Printing the full triple while one of its members was exempt is the
|
|
4555
|
+
sentence DR-0038 exists to stop being written: "distinct on produced-by"
|
|
4556
|
+
about a pair that was not required to be. */
|
|
4557
|
+
const compared = DECORRELATION_DIMENSIONS.filter((dimension) => !exemptDimensions.has(dimension));
|
|
4558
|
+
return {
|
|
4559
|
+
violations,
|
|
4560
|
+
reports: violations.length > 0
|
|
4561
|
+
? /* THE EXCEPTION IS PRINTED EVEN ON A RED, because the owner's whole
|
|
4562
|
+
requirement is that nobody can hide it. A red run whose reader
|
|
4563
|
+
cannot see that produced-by was exempt is one where the exception
|
|
4564
|
+
is invisible exactly when the record is being read most closely. */
|
|
4565
|
+
[...exceptionReports]
|
|
4566
|
+
: [
|
|
4567
|
+
...exceptionReports,
|
|
4568
|
+
`REPORT dual-review-decorrelation ${String(group.length)} verdict(s) for phase ${phase} at head ${headKey} are distinct on ${compared.join(", ")}${producedByCaveat(compared)}`,
|
|
4569
|
+
],
|
|
4570
|
+
};
|
|
4571
|
+
},
|
|
4572
|
+
};
|
|
4573
|
+
/* ------------------------------------------------------------------ */
|
|
4574
|
+
/* verdict-pair-approves (M4-P10 step 5, DR-0012 condition 2) */
|
|
4575
|
+
/* ------------------------------------------------------------------ */
|
|
4576
|
+
/**
|
|
4577
|
+
* The severities DR-0012 condition 2 bars an APPROVE from sitting beside.
|
|
4578
|
+
*
|
|
4579
|
+
* `low` is absent DELIBERATELY and the record says why:
|
|
4580
|
+
* delivery/decisions/DR-0012-delegated-merge-authority.md:23 permits merging
|
|
4581
|
+
* with a low finding provided it is fixed or tracked with a reason. The same
|
|
4582
|
+
* three words are the escalation enum in `schemas/verdict.schema.json`, and the
|
|
4583
|
+
* two must agree; M4-P10 widened both together, because the shipped pair had
|
|
4584
|
+
* the schema stopping at `high` while the decision said `medium`.
|
|
4585
|
+
*/
|
|
4586
|
+
export const BLOCKING_SEVERITIES = ["medium", "high", "critical"];
|
|
4587
|
+
/**
|
|
4588
|
+
* The whole severity vocabulary, canonicalised, and the reason it exists BESIDE
|
|
4589
|
+
* the blocking list rather than being inferred from it.
|
|
4590
|
+
*
|
|
4591
|
+
* WITHOUT IT, AN UNRECOGNISED SEVERITY IS SILENTLY NON-BLOCKING. `includes` over
|
|
4592
|
+
* the blocking three answers "is this one of the three", and a review ranking a
|
|
4593
|
+
* defect `blocker`, `sev1` or `showstopper` gets `false` from that question and
|
|
4594
|
+
* sails through. The verdict schema forbids those words, and nothing on this
|
|
4595
|
+
* gate's path validates the committed siblings, so the schema is not the guard
|
|
4596
|
+
* here. Under a delegated grant an unrecognised severity has not been shown
|
|
4597
|
+
* non-blocking, and unshown must be refused; the four words are the ones
|
|
4598
|
+
* `schemas/verdict.schema.json` and `schemas/finding.schema.json` share.
|
|
4599
|
+
*/
|
|
4600
|
+
const SEVERITY_VOCABULARY = ["low", "medium", "high", "critical"];
|
|
4601
|
+
/**
|
|
4602
|
+
* The closed verdict vocabulary, exactly as `schemas/verdict.schema.json` spells
|
|
4603
|
+
* it, and the one word in it that authorises a merge.
|
|
4604
|
+
*
|
|
4605
|
+
* WHY THE RAW SPELLING IS CHECKED HERE AND CANONICALISATION IS NOT ENOUGH, which
|
|
4606
|
+
* is the opposite of the rule the decorrelation check follows one screen up and
|
|
4607
|
+
* is opposite for a reason that is worth stating rather than looking like an
|
|
4608
|
+
* inconsistency. THAT check REFUSES when two values are the same, so collapsing
|
|
4609
|
+
* more spellings onto one value produces MORE refusals and is fail-CLOSED. THIS
|
|
4610
|
+
* check APPROVES when a value equals one particular word, so collapsing produces
|
|
4611
|
+
* more APPROVALS and is fail-OPEN: a sibling reading `Approve`, which
|
|
4612
|
+
* `schemas/verdict.schema.json` forbids and which nothing on this path
|
|
4613
|
+
* validates, would canonicalise to `approve` and be read as an authorisation.
|
|
4614
|
+
* The direction of the comparison decides the direction of the collapse.
|
|
4615
|
+
*
|
|
4616
|
+
* So the canonical reading is still used to ESTABLISH that a value is there and
|
|
4617
|
+
* is comparable, and the RAW string then has to be one of the two words.
|
|
4618
|
+
*/
|
|
4619
|
+
const VERDICT_VOCABULARY = ["APPROVE", "FIX-ROUND-NEEDED"];
|
|
4620
|
+
const APPROVING_VERDICT = "APPROVE";
|
|
4621
|
+
/**
|
|
4622
|
+
* DR-0012 CONDITION 2, MADE INTO A PREDICATE
|
|
4623
|
+
* (delivery/decisions/DR-0012-delegated-merge-authority.md:23).
|
|
4624
|
+
*
|
|
4625
|
+
* WHAT WAS MISSING, stated as the gap rather than as a feature. Before this
|
|
4626
|
+
* check, `scripts/check-dual-review.mjs` could not see a verdict's VALUE at
|
|
4627
|
+
* all: measured against the whole of that script, `grep -c` returned 0 for
|
|
4628
|
+
* `APPROVE`, 0 for `severity` and 0 for `findings`. So two properly
|
|
4629
|
+
* decorrelated reviews that both REFUSED the merge passed the gate green, and
|
|
4630
|
+
* so did an APPROVE sitting beside a finding the review itself ranked medium.
|
|
4631
|
+
* Condition 1 looked checked and condition 2 was asserted by a human, which is
|
|
4632
|
+
* the worse of the two states because it reads as progress.
|
|
4633
|
+
*
|
|
4634
|
+
* WHY IT IS A SEPARATE CHECK RATHER THAN MORE OF `dual-review-decorrelation`.
|
|
4635
|
+
* They are different predicates over the same set, and section 2.3 rule 3's
|
|
4636
|
+
* Kind B falsification is per-check: DEREGISTERING this one must make a
|
|
4637
|
+
* refusing pair pass, which is only a witness if there is one id to deregister.
|
|
4638
|
+
* Folding condition 2 into the decorrelation check would have made that
|
|
4639
|
+
* witness unavailable and would have made one red indistinguishable from the
|
|
4640
|
+
* other in the gate's output.
|
|
4641
|
+
*
|
|
4642
|
+
* WHY IT EVALUATES THE COMMITTED GROUP AND NEVER THE INSTANCE'S OWN FIELDS,
|
|
4643
|
+
* which is the one place its shape differs from its sibling's. DR-0012
|
|
4644
|
+
* condition 2 is a property of the two reviews WRITTEN TO `delivery/review/`
|
|
4645
|
+
* AND COMMITTED. A document handed to this check that is not among them is not
|
|
4646
|
+
* a review the grant can be satisfied by, and it also cannot break the
|
|
4647
|
+
* predicate: what is asserted is about the committed set, which the stray
|
|
4648
|
+
* document is not a member of. So there is no membership test here, and the
|
|
4649
|
+
* empty case is not a hole: a `(phase, head)` selecting fewer than two
|
|
4650
|
+
* committed verdicts is refused by the pair-size rule below.
|
|
4651
|
+
*
|
|
4652
|
+
* WHAT IT DOES NOT REACH, named rather than left to be found. "Unresolved" is
|
|
4653
|
+
* a state of the review THREAD, and this check reads documents: a finding that
|
|
4654
|
+
* was raised, fixed in a later round and left in the file still reddens here.
|
|
4655
|
+
* That is the fail-closed direction and it is a real cost, paid deliberately,
|
|
4656
|
+
* because the alternative is a resolution field an author sets on their own
|
|
4657
|
+
* finding. Nothing here decides whether a `severity` was ranked honestly
|
|
4658
|
+
* either; a review that calls a critical defect `low` passes, and no keyword
|
|
4659
|
+
* reaches that.
|
|
4660
|
+
*/
|
|
4661
|
+
export const verdictPairApproves = {
|
|
4662
|
+
id: "verdict-pair-approves",
|
|
4663
|
+
type: "verdict",
|
|
4664
|
+
requiresContext: true,
|
|
4665
|
+
run(instance, contextDirectory) {
|
|
4666
|
+
if (contextDirectory === undefined) {
|
|
4667
|
+
/* Unreachable through `runChecks`, which SKIPS first. Fail closed rather
|
|
4668
|
+
than trusting a caller that reaches the check directly. */
|
|
4669
|
+
return {
|
|
4670
|
+
violations: [{ pointer: "#/verdict", message: "no context directory was supplied" }],
|
|
4671
|
+
reports: [],
|
|
4672
|
+
};
|
|
4673
|
+
}
|
|
4674
|
+
const verdict = asRecord(instance);
|
|
4675
|
+
const phaseReading = establishField(verdict, "phase");
|
|
4676
|
+
if (phaseReading.kind !== "established") {
|
|
4677
|
+
return {
|
|
4678
|
+
violations: [
|
|
4679
|
+
{
|
|
4680
|
+
pointer: "#/phase",
|
|
4681
|
+
message: `the verdict ${unestablishedReason(phaseReading, "phase")}, so the other reviews of the same work cannot be selected`,
|
|
4682
|
+
},
|
|
4683
|
+
],
|
|
4684
|
+
reports: [],
|
|
4685
|
+
};
|
|
4686
|
+
}
|
|
4687
|
+
const phaseKey = phaseReading.value;
|
|
4688
|
+
const phase = verdict?.["phase"];
|
|
4689
|
+
const source = resolveCorpusSource(contextDirectory);
|
|
4690
|
+
const regime = establishDelegatedRegime("verdict-pair-approves", contextDirectory, phase, source);
|
|
4691
|
+
if (regime.kind === "report") {
|
|
4692
|
+
return { violations: [], reports: regime.lines };
|
|
4693
|
+
}
|
|
4694
|
+
if (regime.kind === "violation") {
|
|
4695
|
+
return {
|
|
4696
|
+
violations: [{ pointer: regime.pointer, message: regime.message }],
|
|
4697
|
+
reports: [],
|
|
4698
|
+
};
|
|
4699
|
+
}
|
|
4700
|
+
const ownHead = headKeyOf(verdict, "this verdict");
|
|
4701
|
+
if (!ownHead.ok) {
|
|
4702
|
+
return {
|
|
4703
|
+
violations: [{ pointer: "#/head", message: ownHead.message }],
|
|
4704
|
+
reports: [],
|
|
4705
|
+
};
|
|
4706
|
+
}
|
|
4707
|
+
const headKey = ownHead.value;
|
|
4708
|
+
const committed = loadCommittedVerdicts(contextDirectory, source);
|
|
4709
|
+
if (!committed.ok) {
|
|
4710
|
+
return {
|
|
4711
|
+
violations: [{ pointer: "#/verdict", message: committed.reason }],
|
|
4712
|
+
reports: [],
|
|
4713
|
+
};
|
|
4714
|
+
}
|
|
4715
|
+
const grouped = headGroupFor(committed.verdicts, phaseKey, headKey);
|
|
4716
|
+
const group = grouped.members;
|
|
4717
|
+
/* SAME TWO SOURCES AS THE SIBLING CHECK, AND THE REASON IS SHARPER HERE.
|
|
4718
|
+
This predicate says the pair APPROVES, so every document that could not
|
|
4719
|
+
be examined is a document that could have been the refusal. */
|
|
4720
|
+
const violations = [...committed.unexaminable, ...grouped.unkeyed];
|
|
4721
|
+
if (group.length < 2) {
|
|
4722
|
+
violations.push({
|
|
4723
|
+
pointer: "#/verdict",
|
|
4724
|
+
message: `only ${String(group.length)} verdict document(s) exist under ${REVIEW_DIRECTORY} for phase ${phase} at head ${headKey}, and DR-0012 condition 2 is a property of the PAIR, so it cannot be satisfied by fewer than two ${describeVerdictCorpusSource(committed.source)}`,
|
|
4725
|
+
});
|
|
4726
|
+
}
|
|
4727
|
+
for (const candidate of group) {
|
|
4728
|
+
const reading = establishField(candidate.record, "verdict");
|
|
4729
|
+
if (reading.kind !== "established") {
|
|
4730
|
+
violations.push({
|
|
4731
|
+
pointer: "#/verdict",
|
|
4732
|
+
message: `${candidate.path} ${unestablishedReason(reading, "verdict")}, so whether this review approves the merge could not be established, and a merge check that cannot read a verdict must not report the pair clean`,
|
|
4733
|
+
});
|
|
4734
|
+
}
|
|
4735
|
+
else {
|
|
4736
|
+
const raw = candidate.record["verdict"];
|
|
4737
|
+
if (!VERDICT_VOCABULARY.includes(raw)) {
|
|
4738
|
+
violations.push({
|
|
4739
|
+
pointer: "#/verdict",
|
|
4740
|
+
message: `${candidate.path} declares verdict ${raw}, which is not one of the two words the closed vocabulary admits (${VERDICT_VOCABULARY.join(", ")}), so it cannot be read as an authorisation however it is spelled`,
|
|
4741
|
+
});
|
|
4742
|
+
}
|
|
4743
|
+
else if (raw !== APPROVING_VERDICT) {
|
|
4744
|
+
violations.push({
|
|
4745
|
+
pointer: "#/verdict",
|
|
4746
|
+
message: `${candidate.path} reads ${raw} for phase ${phase} at head ${headKey}, so the pair does not approve this head and the delegated grant's condition 2 is not met`,
|
|
4747
|
+
});
|
|
4748
|
+
}
|
|
4749
|
+
}
|
|
4750
|
+
violations.push(...blockingFindings(candidate, phase, headKey));
|
|
4751
|
+
}
|
|
3210
4752
|
return {
|
|
3211
4753
|
violations,
|
|
3212
4754
|
reports: violations.length > 0
|
|
3213
4755
|
? []
|
|
3214
4756
|
: [
|
|
3215
|
-
`REPORT
|
|
4757
|
+
`REPORT verdict-pair-approves ${String(group.length)} verdict(s) for phase ${phase} at head ${headKey} read APPROVE and carry no finding at ${BLOCKING_SEVERITIES.join(", ")}`,
|
|
3216
4758
|
],
|
|
3217
4759
|
};
|
|
3218
4760
|
},
|
|
3219
4761
|
};
|
|
4762
|
+
/**
|
|
4763
|
+
* Every finding in one verdict that DR-0012 condition 2 bars a merge over.
|
|
4764
|
+
*
|
|
4765
|
+
* SEPARATE FROM THE CHECK BODY because the shapes it has to refuse are the
|
|
4766
|
+
* interesting part and they are easy to lose in a loop. `findings` that is not
|
|
4767
|
+
* a list, an entry that is not a map, and a `severity` that cannot be
|
|
4768
|
+
* established are all REFUSALS rather than skips, for the reason the whole of
|
|
4769
|
+
* this section follows: a value that has not been established is not a value
|
|
4770
|
+
* that has been shown safe, and under a grant unshown must be refused.
|
|
4771
|
+
*/
|
|
4772
|
+
function blockingFindings(candidate, phase, headKey) {
|
|
4773
|
+
const raw = candidate.record["findings"];
|
|
4774
|
+
if (raw === undefined) {
|
|
4775
|
+
return [
|
|
4776
|
+
{
|
|
4777
|
+
pointer: "#/findings",
|
|
4778
|
+
message: `${candidate.path} declares no findings, so whether it carries a blocking one could not be established for phase ${phase} at head ${headKey}`,
|
|
4779
|
+
},
|
|
4780
|
+
];
|
|
4781
|
+
}
|
|
4782
|
+
if (!Array.isArray(raw)) {
|
|
4783
|
+
return [
|
|
4784
|
+
{
|
|
4785
|
+
pointer: "#/findings",
|
|
4786
|
+
message: `${candidate.path} declares findings as ${raw === null ? "null" : typeof raw === "object" ? "a map" : `a ${typeof raw}`}, which is not a list of findings, so whether it carries a blocking one could not be established`,
|
|
4787
|
+
},
|
|
4788
|
+
];
|
|
4789
|
+
}
|
|
4790
|
+
const out = [];
|
|
4791
|
+
for (let index = 0; index < raw.length; index += 1) {
|
|
4792
|
+
const finding = asRecord(raw[index]);
|
|
4793
|
+
if (finding === undefined) {
|
|
4794
|
+
out.push({
|
|
4795
|
+
pointer: `#/findings/${String(index)}`,
|
|
4796
|
+
message: `${candidate.path} finding ${String(index)} is not a map, so its severity could not be established`,
|
|
4797
|
+
});
|
|
4798
|
+
continue;
|
|
4799
|
+
}
|
|
4800
|
+
const severity = establishField(finding, "severity");
|
|
4801
|
+
if (severity.kind !== "established") {
|
|
4802
|
+
out.push({
|
|
4803
|
+
pointer: `#/findings/${String(index)}/severity`,
|
|
4804
|
+
message: `${candidate.path} finding ${String(index)} ${unestablishedReason(severity, "severity")}, so whether it blocks the merge could not be established`,
|
|
4805
|
+
});
|
|
4806
|
+
continue;
|
|
4807
|
+
}
|
|
4808
|
+
const id = establishField(finding, "id");
|
|
4809
|
+
const named = id.kind === "established" ? finding["id"] : `at index ${String(index)}`;
|
|
4810
|
+
if (!SEVERITY_VOCABULARY.includes(severity.value)) {
|
|
4811
|
+
out.push({
|
|
4812
|
+
pointer: `#/findings/${String(index)}/severity`,
|
|
4813
|
+
message: `${candidate.path} ranks finding ${named} ${severity.value}, which is not one of the four severities the kernel's vocabulary admits (${SEVERITY_VOCABULARY.join(", ")}), so whether it blocks the merge could not be established`,
|
|
4814
|
+
});
|
|
4815
|
+
continue;
|
|
4816
|
+
}
|
|
4817
|
+
if (BLOCKING_SEVERITIES.includes(severity.value)) {
|
|
4818
|
+
out.push({
|
|
4819
|
+
pointer: `#/findings/${String(index)}/severity`,
|
|
4820
|
+
message: `${candidate.path} carries finding ${named} at severity ${severity.value} for phase ${phase} at head ${headKey}, and a delegated grant is not satisfied while a review carries an unresolved finding at ${BLOCKING_SEVERITIES.join(", ")}`,
|
|
4821
|
+
});
|
|
4822
|
+
}
|
|
4823
|
+
}
|
|
4824
|
+
return out;
|
|
4825
|
+
}
|
|
4826
|
+
/* ------------------------------------------------------------------ */
|
|
4827
|
+
/* model-resolution-subject-echo (M4-P7 criteria 1, 2 and 7) */
|
|
4828
|
+
/* ------------------------------------------------------------------ */
|
|
4829
|
+
/**
|
|
4830
|
+
* THE SUBJECT ECHO AGREES WITH THE RESOLUTION IT SITS BESIDE, THE RECORD WAS
|
|
4831
|
+
* WRITTEN AFTER THE TURN ENDED, AND AN OVERRIDE WAS PERMITTED BEFORE IT WAS
|
|
4832
|
+
* APPLIED.
|
|
4833
|
+
*
|
|
4834
|
+
* All three compare SIBLING FIELDS of one document, so all three are Kind B
|
|
4835
|
+
* and none of them is reachable from a keyword (schemas/README.md's Kind A and
|
|
4836
|
+
* Kind B section, DR-0013 clause 8). The schema next door can require that
|
|
4837
|
+
* `subject`, `turnEnd` and `resolution` are all PRESENT, which is what makes a
|
|
4838
|
+
* launch-time record unrepresentable, and it stops exactly there: it cannot
|
|
4839
|
+
* say that two present values agree.
|
|
4840
|
+
*
|
|
4841
|
+
* WHY THE ECHO MATTERS AT ALL, since a record that echoes itself sounds
|
|
4842
|
+
* circular. It is not the record checking itself against itself. `subject` is
|
|
4843
|
+
* a VERBATIM copy of the launch request the adapter was handed and
|
|
4844
|
+
* `resolution` is what the adapter's resolver actually consumed, and the
|
|
4845
|
+
* hazard is that those two diverge silently: a resolver that read the wrong
|
|
4846
|
+
* role's row produces a perfectly well-formed record whose family token is
|
|
4847
|
+
* then attributed to a task it was never about. That is the misattribution
|
|
4848
|
+
* guard src/gates/schemas/release-record.schema.json:26 exists for, one seam
|
|
4849
|
+
* along, and the kernel-side half of it is in src/model-resolution.ts where
|
|
4850
|
+
* the request is compared against a copy the kernel itself holds.
|
|
4851
|
+
*
|
|
4852
|
+
* THE OVERRIDE DIRECTION IS THE ONE MOST LIKELY TO BE GOT WRONG. M4-P7
|
|
4853
|
+
* criterion 7 wants BOTH directions: a role whose `charter-override` is
|
|
4854
|
+
* `allowed` takes the charter's tier, and a role whose permission is anything
|
|
4855
|
+
* else does not. The second direction is the one a resolver written from the
|
|
4856
|
+
* happy path silently drops, because nothing about it looks like a failure.
|
|
4857
|
+
*/
|
|
4858
|
+
export const modelResolutionSubjectEcho = {
|
|
4859
|
+
id: "model-resolution-subject-echo",
|
|
4860
|
+
type: "model-resolution",
|
|
4861
|
+
requiresContext: false,
|
|
4862
|
+
run(instance) {
|
|
4863
|
+
const document = asRecord(instance);
|
|
4864
|
+
if (document === undefined) {
|
|
4865
|
+
return EMPTY;
|
|
4866
|
+
}
|
|
4867
|
+
const subject = asRecord(document["subject"]);
|
|
4868
|
+
const resolution = asRecord(document["resolution"]);
|
|
4869
|
+
if (subject === undefined || resolution === undefined) {
|
|
4870
|
+
/* The schema requires both and runs first in `cmdValidate`, so this arm
|
|
4871
|
+
is not reached through the command. It is kept fail-open rather than
|
|
4872
|
+
inventing a second diagnostic for a missing field the schema already
|
|
4873
|
+
names, which would print the same defect twice under two wordings. */
|
|
4874
|
+
return EMPTY;
|
|
4875
|
+
}
|
|
4876
|
+
const violations = [];
|
|
4877
|
+
const echoedRole = subject["role"];
|
|
4878
|
+
const resolvedRole = resolution["role"];
|
|
4879
|
+
if (echoedRole !== resolvedRole) {
|
|
4880
|
+
violations.push({
|
|
4881
|
+
pointer: "#/resolution/role",
|
|
4882
|
+
message: `the resolution is for role ${String(resolvedRole)} and the subject echo says the launch request named role ${String(echoedRole)}, so this record resolves a different subject than it claims`,
|
|
4883
|
+
});
|
|
4884
|
+
}
|
|
4885
|
+
const overrideApplied = resolution["overrideApplied"];
|
|
4886
|
+
const resolvedTier = resolution["tier"];
|
|
4887
|
+
if (overrideApplied === false && resolvedTier !== subject["requestedTier"]) {
|
|
4888
|
+
violations.push({
|
|
4889
|
+
pointer: "#/resolution/tier",
|
|
4890
|
+
message: `no charter override was applied and the resolved tier ${String(resolvedTier)} is not the requested tier ${String(subject["requestedTier"])}, so the tier changed with nothing recorded as having changed it`,
|
|
4891
|
+
});
|
|
4892
|
+
}
|
|
4893
|
+
if (overrideApplied === true) {
|
|
4894
|
+
if (resolvedTier !== resolution["charterTier"]) {
|
|
4895
|
+
violations.push({
|
|
4896
|
+
pointer: "#/resolution/tier",
|
|
4897
|
+
message: `a charter override was applied and the resolved tier ${String(resolvedTier)} is not the charter tier ${String(resolution["charterTier"])}, so the record cites a charter it did not follow`,
|
|
4898
|
+
});
|
|
4899
|
+
}
|
|
4900
|
+
const permission = asRecord(resolution["observation"])?.["configPermission"];
|
|
4901
|
+
if (permission !== "allowed") {
|
|
4902
|
+
violations.push({
|
|
4903
|
+
pointer: "#/resolution/observation/configPermission",
|
|
4904
|
+
message: `a charter override was applied while the role's charter-override permission was observed to be ${String(permission)}, so the override was taken where the role forbids it`,
|
|
4905
|
+
});
|
|
4906
|
+
}
|
|
4907
|
+
}
|
|
4908
|
+
const writtenAt = document["writtenAt"];
|
|
4909
|
+
const endedAt = asRecord(document["turnEnd"])?.["endedAt"];
|
|
4910
|
+
if (typeof writtenAt === "string" && typeof endedAt === "string") {
|
|
4911
|
+
const written = Date.parse(writtenAt);
|
|
4912
|
+
const ended = Date.parse(endedAt);
|
|
4913
|
+
if (Number.isFinite(written) && Number.isFinite(ended) && written < ended) {
|
|
4914
|
+
violations.push({
|
|
4915
|
+
pointer: "#/writtenAt",
|
|
4916
|
+
message: `the record says it was written at ${writtenAt}, before the turn ended at ${endedAt}, so it cannot carry what the turn resolved and is a restatement of the request`,
|
|
4917
|
+
});
|
|
4918
|
+
}
|
|
4919
|
+
}
|
|
4920
|
+
return { violations, reports: [] };
|
|
4921
|
+
},
|
|
4922
|
+
};
|
|
3220
4923
|
/* ------------------------------------------------------------------ */
|
|
3221
4924
|
/* The registry */
|
|
3222
4925
|
/* ------------------------------------------------------------------ */
|
|
@@ -3258,6 +4961,14 @@ const registry = [
|
|
|
3258
4961
|
id, and `registeredChecks` returns a copy, so this array's position carries
|
|
3259
4962
|
no meaning any check reads. */
|
|
3260
4963
|
dualReviewDecorrelation,
|
|
4964
|
+
/* M4-P10 step 5. Appended rather than inserted, for the reason recorded on
|
|
4965
|
+
the M3-P7 block above: `checksFor` filters by declared type and sorts by
|
|
4966
|
+
id, so this array's position carries no meaning any check reads. */
|
|
4967
|
+
verdictPairApproves,
|
|
4968
|
+
/* M4-P7. Appended rather than inserted, for the reason recorded on the
|
|
4969
|
+
M3-P7 block above: `checksFor` filters by declared type and sorts by id,
|
|
4970
|
+
so this array's position carries no meaning any check reads. */
|
|
4971
|
+
modelResolutionSubjectEcho,
|
|
3261
4972
|
];
|
|
3262
4973
|
/** Register a check. Later phases append their own (section 2.3's table). */
|
|
3263
4974
|
export function registerCheck(check) {
|