@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.
Files changed (94) hide show
  1. package/AGENTS.md +56 -4
  2. package/assurance-modes.yaml +23 -2
  3. package/dist/bin/tiphys.js +86 -8
  4. package/dist/src/adapters/load.d.ts +202 -0
  5. package/dist/src/adapters/load.js +440 -0
  6. package/dist/src/brief.js +27 -20
  7. package/dist/src/checks.d.ts +720 -9
  8. package/dist/src/checks.js +1874 -163
  9. package/dist/src/cli.js +11 -0
  10. package/dist/src/commands/brief.js +27 -4
  11. package/dist/src/commands/cutover.d.ts +35 -0
  12. package/dist/src/commands/cutover.js +448 -0
  13. package/dist/src/commands/doctor.d.ts +229 -0
  14. package/dist/src/commands/doctor.js +968 -27
  15. package/dist/src/commands/init.d.ts +3 -3
  16. package/dist/src/commands/init.js +57 -8
  17. package/dist/src/commands/lock.d.ts +33 -0
  18. package/dist/src/commands/lock.js +117 -6
  19. package/dist/src/commands/next.d.ts +130 -0
  20. package/dist/src/commands/next.js +597 -0
  21. package/dist/src/commands/pool.js +12 -1
  22. package/dist/src/commands/resume.d.ts +1 -0
  23. package/dist/src/commands/resume.js +88 -0
  24. package/dist/src/commands/spawn.js +51 -2
  25. package/dist/src/commands/status.d.ts +6 -4
  26. package/dist/src/commands/status.js +6 -4
  27. package/dist/src/commands/sync.d.ts +47 -0
  28. package/dist/src/commands/sync.js +341 -0
  29. package/dist/src/commands/teardown.js +10 -2
  30. package/dist/src/commands/validate.js +70 -0
  31. package/dist/src/cutover.d.ts +584 -0
  32. package/dist/src/cutover.js +1444 -0
  33. package/dist/src/exclusion.d.ts +389 -0
  34. package/dist/src/exclusion.js +843 -0
  35. package/dist/src/exec/env.d.ts +152 -2
  36. package/dist/src/exec/env.js +146 -2
  37. package/dist/src/fleet.d.ts +172 -0
  38. package/dist/src/fleet.js +219 -1
  39. package/dist/src/gates/citations.js +7 -1
  40. package/dist/src/gates/coverage.d.ts +113 -22
  41. package/dist/src/gates/coverage.js +166 -31
  42. package/dist/src/gates/credentials.d.ts +159 -0
  43. package/dist/src/gates/credentials.js +221 -2
  44. package/dist/src/gates/gate-classes.d.ts +56 -0
  45. package/dist/src/gates/gate-classes.js +633 -0
  46. package/dist/src/gates/merge-preconditions.d.ts +319 -0
  47. package/dist/src/gates/merge-preconditions.js +932 -0
  48. package/dist/src/gates/red-witness.js +105 -13
  49. package/dist/src/gates/run.d.ts +49 -1
  50. package/dist/src/gates/run.js +83 -5
  51. package/dist/src/gates/schemas/phase-declaration.schema.json +45 -0
  52. package/dist/src/gates/suite.js +48 -7
  53. package/dist/src/hooks.d.ts +55 -3
  54. package/dist/src/hooks.js +69 -6
  55. package/dist/src/index.d.ts +31 -0
  56. package/dist/src/index.js +30 -0
  57. package/dist/src/lock.d.ts +82 -4
  58. package/dist/src/lock.js +314 -22
  59. package/dist/src/model-resolution.d.ts +159 -0
  60. package/dist/src/model-resolution.js +307 -0
  61. package/dist/src/path-identity.d.ts +32 -0
  62. package/dist/src/path-identity.js +38 -0
  63. package/dist/src/pool.d.ts +197 -1
  64. package/dist/src/pool.js +289 -22
  65. package/dist/src/roles.d.ts +31 -0
  66. package/dist/src/roles.js +42 -0
  67. package/dist/src/spawn.d.ts +307 -2
  68. package/dist/src/spawn.js +690 -19
  69. package/dist/src/status.d.ts +27 -2
  70. package/dist/src/status.js +34 -5
  71. package/dist/src/task.d.ts +295 -55
  72. package/dist/src/task.js +125 -123
  73. package/dist/src/teardown.d.ts +7 -0
  74. package/dist/src/teardown.js +120 -12
  75. package/dist/src/validate.d.ts +44 -11
  76. package/dist/src/validate.js +44 -34
  77. package/dist/src/watcher.js +1 -11
  78. package/dist/src/witness/run.d.ts +32 -7
  79. package/dist/src/witness/run.js +76 -30
  80. package/dist/src/witness/spec.d.ts +168 -0
  81. package/dist/src/witness/spec.js +240 -18
  82. package/dist/tsconfig.src.tsbuildinfo +1 -1
  83. package/gate-registry.yaml +136 -0
  84. package/gates.manifest.json +63 -1
  85. package/package.json +18 -3
  86. package/roles/implementer.md +3 -0
  87. package/schemas/README.md +1 -0
  88. package/schemas/assurance-modes.schema.json +1 -1
  89. package/schemas/charter.schema.json +19 -0
  90. package/schemas/cutover-state.schema.json +64 -0
  91. package/schemas/executor-record.schema.json +36 -0
  92. package/schemas/model-resolution.schema.json +362 -0
  93. package/schemas/verdict.schema.json +9 -3
  94. package/schemas/write-bypass.schema.json +69 -0
@@ -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
- * Every verdict document committed under `<context>/delivery/review/`.
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
- * A file that is not a regular file, does not decode, or does not carry
2654
- * `kind: verdict` is SKIPPED rather than reported, because that directory also
2655
- * holds this project's prose reviews and a check that reddened on a markdown
2656
- * file would be unusable. What is NOT skipped is the directory being
2657
- * unreadable, which the caller turns into a violation: "nothing to compare" and
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 loadCommittedVerdicts(contextDirectory) {
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
- names = readdirSync(directory);
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 verdicts = [];
3188
+ const documents = [];
3189
+ const unreadable = [];
2689
3190
  for (const name of names.sort()) {
2690
- if (!/\.(ya?ml|json)$/i.test(name)) {
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
- const decoded = decodeDocument(read.body, path);
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
- return { ok: true, verdicts };
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. Condition (d) of
2938
- * step 3b, that neither verdict carries an unresolved high or medium finding,
2939
- * is NOT checked here: the verdict schema's own root `if`/`then` already
2940
- * forbids APPROVE beside a high or critical finding, and "unresolved" is a
2941
- * state of the review thread rather than of the document. Nothing here decides
2942
- * whether the two verdicts describe the same HEAD either: the verdict schema
2943
- * carries no head field, so `phase` is the join key and the DIRECTORY is what
2944
- * scopes a set of verdicts to one head. Both are stated in
2945
- * delivery/work-history/m3-p9.md as declared readings rather than absorbed.
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
- const charterPresent = classifyEntry(join(contextDirectory, "charter.yaml")).kind !== "absent";
3017
- if (!charterPresent) {
3018
- return {
3019
- violations: [],
3020
- reports: [
3021
- `REPORT dual-review-decorrelation ${contextDirectory} declares no delivery mode ` +
3022
- `(no charter.yaml), so the verdicts for phase ${phase} were NOT evaluated against a ` +
3023
- `merge-authority regime; scripts/check-dual-review.mjs refuses such a directory outright`,
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
- const charter = readContextDocument(contextDirectory, "charter.yaml");
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
- /* SITE TWO OF THE SAME MECHANISM. `asRecord(charter.value)?.["delivery-mode"]`
3040
- used to flow into `String(modeId)` and into an `===` against every mode's
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
- /* SITE THREE, AND IT IS THE WORST OF THE FOUR BECAUSE IT DISABLES THE WHOLE
3098
- CHECK RATHER THAN ONE DIMENSION. `String(mode.mode["merge-authority"] ?? "")`
3099
- made a mode that declares NO merge-authority indistinguishable from one
3100
- declaring some other authority, and the not-a-delegated-grant arm below is
3101
- a REPORT rather than a violation. Measured on the shipped script before
3102
- this repair (probe P1 in delivery/work-history/m3-p9.md): a pair sharing
3103
- one model family, under a mode with its `merge-authority` line deleted,
3104
- exited 0 GREEN printing "mode full declares merge-authority , which is not
3105
- a delegated grant". That sentence is false and the exit code authorises
3106
- the merge the check exists to refuse. The reviewer did not find this one;
3107
- the derivation did. */
3108
- const authorityReading = establishField(mode.mode, "merge-authority");
3109
- if (authorityReading.kind !== "established") {
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
- const authority = authorityReading.value;
3121
- if (authority !== DELEGATED_MERGE_AUTHORITY) {
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 committed = loadCommittedVerdicts(contextDirectory);
3131
- if (!committed.ok) {
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: "#/produced-by", message: committed.reason }],
4466
+ violations: [{ pointer: "#/head", message: ownHead.message }],
3134
4467
  reports: [],
3135
4468
  };
3136
4469
  }
3137
- const group = committed.verdicts.filter(
3138
- /* BOTH SIDES CANONICAL. `phase` above is already canonical; the sibling's
3139
- is read through the same function so the two are compared in one form
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
- const violations = [];
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 dual-review-decorrelation ${String(group.length)} verdict(s) for phase ${phase} are distinct on ${DECORRELATION_DIMENSIONS.join(", ")}`,
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) {