@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
@@ -755,6 +755,620 @@ export declare function unresolvableCitationTree(contextDirectory: string, path:
755
755
  export declare const DECORRELATION_DIMENSIONS: readonly string[];
756
756
  /** The merge-authority value that makes decorrelation a precondition of merge. */
757
757
  export declare const DELEGATED_MERGE_AUTHORITY = "delegated-under-conditions";
758
+ /**
759
+ * SAY WHAT `produced-by` ACTUALLY COMPARED, ON THE GREEN LINE (CR-VS-003).
760
+ *
761
+ * The comparison is `canonicalScalar`: NFKC, whitespace collapse, lowercase,
762
+ * then `!==`. Reproduced at the swept head with one variable changed: two
763
+ * `produced-by` strings naming two different MODELS of one vendor, in the
764
+ * vendor-plus-model-plus-organisation form this project's own reviews use, are
765
+ * certified "distinct on produced-by", green, exit 0. Two models of ONE family
766
+ * pass as decorrelated, which is T-001's own property failing inside the
767
+ * kernel's decorrelation check. The two strings are quoted verbatim in the
768
+ * sweep evidence rather than here, because no vendor model name may appear in
769
+ * the kernel's shipped surface (test/schemas.test.ts:800).
770
+ *
771
+ * WHY THIS IS A SENTENCE AND NOT A FAMILY VOCABULARY, and the reason is a
772
+ * settled one rather than an omission. M4-P10 deferred the family comparison to
773
+ * M4-P11 (delivery/work-history/m4-p10.md:654); M4-P11 DECLINED the mechanism in
774
+ * its own words, "a closed enum of family names was rejected: no such vocabulary
775
+ * can be kept current" (delivery/work-history/m4-p11.md:142). That reason holds
776
+ * and shipping the enum that phase rejected would be reopening it by the back
777
+ * door. What was never done is the OTHER half of the reviewer's own proposal:
778
+ * stop the green line reading as a cross-family assertion. A bundle-level green
779
+ * saying "distinct on produced-by" is read as "two families reviewed this", and
780
+ * nothing here establishes that. So the line now says what it measured.
781
+ *
782
+ * WHAT WOULD CLOSE IT, named rather than left open: a `produced-by-family`
783
+ * field in `schemas/verdict.schema.json`, required, compared instead of the free
784
+ * string, with the vocabulary OPEN (any two distinct values decorrelate) so no
785
+ * list has to be kept current. That is a schema change and a decision record,
786
+ * both outside this fix round's declared files.
787
+ */
788
+ export declare function producedByCaveat(compared: readonly string[]): string;
789
+ export interface LoadedVerdict {
790
+ path: string;
791
+ record: Record<string, unknown>;
792
+ }
793
+ /**
794
+ * What reading a candidate document's own `kind` produced, in the THREE
795
+ * outcomes that fix round 2 exists to keep apart.
796
+ *
797
+ * THE MECHANISM FIX ROUND 2 CLOSES: `establishField` already separates ABSENT
798
+ * from UNUSABLE from UNCANONICAL, and both selection sites consumed it with a
799
+ * single `!== "established"`, which folds those outcomes back into one silent
800
+ * skip. So "this document declares no type" and "this document declares a type
801
+ * nobody could read" printed as the same fact, and that fact is the determinate
802
+ * negative "not a verdict". Measured at the round-1 head: a third review
803
+ * reading `verdict: FIX-ROUND-NEEDED` whose `kind:` was a one-element YAML list
804
+ * was dropped and `scripts/check-dual-review.mjs` reported that the pair
805
+ * approves. Eight deformations of one refusing document reached that same
806
+ * green, and the table is in delivery/work-history/m4-p10.md's section 13.
807
+ *
808
+ * WHERE THE LINE IS DRAWN, and it is drawn at the PRESENCE OF THE KEY rather
809
+ * than at the validity of its value:
810
+ *
811
+ * `verdict` the key is there and canonicalises to the word. A member.
812
+ * `other` the document ANSWERED and the answer is not `verdict`. That
813
+ * is a mapping carrying no `kind` key at all, a document that
814
+ * is not a mapping (a list, a scalar, an empty file), and a
815
+ * `kind` that reads as some other word. Each is a determinate
816
+ * negative: nothing here claims to be a typed document, or it
817
+ * claims to be a different one.
818
+ * `unreadable` the key IS there and its reading failed: a list, a map, a
819
+ * number, a boolean, null, an empty or whitespace-only string,
820
+ * or a string carrying a character outside printable ASCII.
821
+ * Writing a `kind` key is the claim to be a typed document, so
822
+ * a failed reading of it is a failed claim, not an absent one.
823
+ *
824
+ * WHY NOT REFUSE EVERY DOCUMENT THAT IS NOT A VERDICT. Because a project is
825
+ * entitled to keep other YAML beside its reviews, and a check that errored on
826
+ * it would be unusable. The property is not that every file is a verdict; it is
827
+ * that a file which LOOKS LIKE a verdict and could not be read as one is NAMED.
828
+ *
829
+ * EXPORTED, and that is the other half of the fix. `scripts/check-dual-review.mjs`
830
+ * had its OWN selection rule, a raw `value["kind"] !== "verdict"`, which is a
831
+ * second reader of one fact: it dropped `kind: Verdict` and `kind: " verdict "`
832
+ * that this file's canonicalisation admits. Two readers of one fact is the shape
833
+ * this repository's fix-round contract calls fixing the INSTANCE rather than the
834
+ * MECHANISM, and the sibling `establishDelegatedRegime` one screen down was
835
+ * lifted for exactly that reason. There is now one reader.
836
+ */
837
+ export type VerdictKindReading = {
838
+ kind: "verdict";
839
+ record: Record<string, unknown>;
840
+ } | {
841
+ kind: "other";
842
+ } | {
843
+ kind: "unreadable";
844
+ found: string;
845
+ };
846
+ export declare function readVerdictKind(value: unknown): VerdictKindReading;
847
+ /**
848
+ * WHERE A CORPUS WAS READ FROM, carried WITH the corpus and never assumed.
849
+ *
850
+ * THE MECHANISM THIS TYPE EXISTS TO CLOSE (M4-P11 fix round 1, CR-M4P11-001
851
+ * and CR-M4P11-002). One decision was assembled out of TWO SOURCES OF TRUTH:
852
+ * the declaration was read from the git object database by
853
+ * `readReviewFamilies`, and the corpus that is supposed to refute it was read
854
+ * from the WORKING TREE. Two sources means the two halves can be made to
855
+ * disagree, and the actor the check exists to guard against controls the half
856
+ * that is not committed.
857
+ *
858
+ * Measured by the reviewer at ONE commit, three arms, only the working tree
859
+ * changing: all verdicts present gave red exit 1; an UNCOMMITTED `rm` of the
860
+ * contradicting verdict gave not-applicable exit 20 with the exception
861
+ * GRANTED; copying the file back gave red again. `git ls-tree` listed the file
862
+ * throughout and `git show` still read `produced-by: family-b` from it. The
863
+ * declaration was tamper-EVIDENT and the evidence that refutes it was not.
864
+ *
865
+ * So the source is decided ONCE per load and travels with the result, and
866
+ * every sentence a check prints about a corpus ends with the source it was
867
+ * actually read from. A reader can tell a claim about a commit from a claim
868
+ * about somebody's working tree, which is SC-011 applied to the CORPUS rather
869
+ * than to a single check's verdict.
870
+ */
871
+ export type VerdictCorpusSource = {
872
+ kind: "commit";
873
+ ref: string;
874
+ refSha: string;
875
+ scope: string;
876
+ } | {
877
+ kind: "worktree";
878
+ reason: string;
879
+ scope: string;
880
+ };
881
+ /**
882
+ * The corpus, plus the source it was read from. Never one without the other.
883
+ *
884
+ * `unexaminable` IS M4-P10 FIX ROUND 2's CHANNEL, CARRIED ACROSS M4-P11's
885
+ * RESTRUCTURE RATHER THAN DROPPED. M4-P10 added it to the single filesystem
886
+ * loop this phase replaced with a commit arm and a worktree arm, and every
887
+ * caller seeds its violation list from it. A candidate that passed the
888
+ * extension filter and could not be read, did not decode, or declares a `kind`
889
+ * whose reading failed is NOT the determinate answer "not a verdict": it is
890
+ * "nobody knows whether this is a verdict", and dropping it SHRINKS the set the
891
+ * merge predicates reason over, which is the fail-open direction for a
892
+ * predicate that approves when the set is clean.
893
+ */
894
+ export interface LoadedVerdictCorpus {
895
+ verdicts: LoadedVerdict[];
896
+ unexaminable: Diagnostic[];
897
+ source: VerdictCorpusSource;
898
+ }
899
+ /**
900
+ * How to name the set a check just looked at, in the check's own output.
901
+ *
902
+ * SC-011 one scope out: "every verdict this project has committed" and "every
903
+ * file that happens to be sitting in one directory right now" are different
904
+ * claims and must not print the same sentence. This renders a TRAILING
905
+ * parenthetical rather than a clause in the middle of one, so a sentence that
906
+ * already names its subject keeps its shape and gains a provenance tail.
907
+ */
908
+ export declare function describeVerdictCorpusSource(source: VerdictCorpusSource): string;
909
+ /**
910
+ * How to name the source ONE context document was looked for in.
911
+ *
912
+ * FIX ROUND 2, DV-001. The regime report line used to say "no charter.yaml"
913
+ * about a directory with a `charter.yaml` sitting in it, because the probe had
914
+ * moved to the commit and the sentence had not. A record that names a document
915
+ * and not the SOURCE it was looked for in is unfalsifiable by the person
916
+ * reading it, which is the same SC-011 property `describeVerdictCorpusSource`
917
+ * exists for one scope out.
918
+ */
919
+ export declare function describeContextDocumentSource(source: VerdictCorpusSource): string;
920
+ /**
921
+ * The verdict documents a PAIR decision is made over: `delivery/review/`.
922
+ *
923
+ * TWO ARMS, AND WHICH ONE RAN IS REPORTED RATHER THAN INFERRED.
924
+ *
925
+ * THE COMMIT ARM is taken whenever `<context>` resolves `ref`, and it reads
926
+ * the directory's entries out of the git object database. The filesystem is
927
+ * not consulted at all, so an uncommitted addition, deletion or edit cannot
928
+ * change what this returns. That is the anti-widening rule
929
+ * `readReviewFamilies` and `loadDeclaration` (src/gates/release.ts:817)
930
+ * already apply to a DECLARATION, now applied to the evidence beside it. It
931
+ * closes both directions of the same hole: an uncommitted DELETION can no
932
+ * longer remove a verdict that contradicts a declaration, and an uncommitted
933
+ * ADDITION can no longer manufacture the pair DR-0012 condition 2 requires.
934
+ *
935
+ * THE WORKTREE ARM is taken only when there is no resolvable ref, which is the
936
+ * pre-existing behaviour for a context that is not a git repository at all,
937
+ * and it SAYS SO in every sentence it produces. No exception can be granted on
938
+ * this arm, because `readReviewFamilies` resolves the same ref and returns
939
+ * absent or error when it cannot: with no git there is one source of truth and
940
+ * nothing to disagree.
941
+ *
942
+ * THE SCOPE STAYS `delivery/review/` HERE, and widening it was measured wrong.
943
+ * `headGroupFor` turns this set into the reviews of one `(phase, head)`, and
944
+ * five of the seven verdict documents in this repository's tree are fixtures
945
+ * for this check's own tests. See `PAPERWORK_ROOT` above; the widest set is
946
+ * what the FALSIFIERS use, and it is a different question.
947
+ *
948
+ * A file that does not carry `kind: verdict` is SKIPPED rather than reported,
949
+ * because that directory also holds this project's prose reviews and a check
950
+ * that reddened on a markdown file would be unusable. What is NOT skipped is
951
+ * the directory being unreadable, which the caller turns into a violation:
952
+ * "nothing to compare" and "could not look" are different facts.
953
+ *
954
+ * AND A CANDIDATE THAT COULD NOT BE LOOKED AT IS THE SECOND HALF OF THAT SAME
955
+ * SENTENCE, WHICH THE FIRST ROUND WROTE AND APPLIED AT ONE SITE ONLY. A
956
+ * `.yaml`, `.yml` or `.json` file here has passed the only filter that
957
+ * separates a candidate verdict from a prose review, so bytes that cannot be
958
+ * READ and bytes that do not DECODE are not "this is not a verdict", they are
959
+ * "nobody knows whether this is a verdict". Dropping such a file SHRINKS the
960
+ * set the merge predicates reason over, which is the fail-open direction for a
961
+ * predicate that approves when the set is clean: measured at the reviewed head,
962
+ * a third review reading FIX-ROUND-NEEDED with one malformed line left
963
+ * `verdict-pair-approves` printing that the pair approves. So they are returned
964
+ * as diagnostics and every caller seeds its violation list with them, exactly
965
+ * as `headGroupFor` already does for a sibling with no usable head.
966
+ */
967
+ export declare function loadCommittedVerdicts(contextDirectory: string, source?: VerdictCorpusSource): ({
968
+ ok: true;
969
+ } & LoadedVerdictCorpus) | {
970
+ ok: false;
971
+ reason: string;
972
+ };
973
+ /**
974
+ * Resolve, ONCE, the source every document of one decision is read from.
975
+ *
976
+ * EXPORTED because the two merge-precondition checks each resolve it at the
977
+ * top of their own run and hand the SAME value to the regime reader, the
978
+ * declaration reader and both corpus loaders. One resolution is what makes
979
+ * "the halves disagree" unrepresentable rather than merely unlikely.
980
+ */
981
+ export declare function resolveCorpusSource(contextDirectory: string, ref?: string): VerdictCorpusSource;
982
+ /**
983
+ * THE MECHANISM THIS SECTION CLOSES, STATED ONE LEVEL UP FROM THE FINDING:
984
+ * a gate whose verdict is about EVIDENCE THE CALLER SUPPLIED, with no check
985
+ * that the evidence is about the thing being audited.
986
+ *
987
+ * `headGroupFor` above groups by the head THE VERDICT DOCUMENTS THEMSELVES
988
+ * DECLARE. Before this section nothing compared that value to the commit the
989
+ * gate was running against, and nothing established that it named a commit at
990
+ * all. Both arms were reproduced end to end through the shipped script:
991
+ *
992
+ * ARM A two decorrelated APPROVE verdicts naming
993
+ * deadbeefdeadbeefdeadbeefdeadbeefdeadbeef, which
994
+ * `git cat-file -t` reports as not an object, in a repository whose
995
+ * HEAD is a real commit -> GREEN, 2 units, exit 0
996
+ * ARM B the same verdicts plus a further commit of work no verdict
997
+ * mentions -> GREEN, exit 0
998
+ *
999
+ * So ONE committed approving pair turned this gate green for that phase at
1000
+ * every later head, indefinitely, on evidence about one old commit. That is
1001
+ * T-009's own rule ("a gate result is evidence only for the configuration it
1002
+ * ran under") failing inside the gate that carries DR-0012's merge grant.
1003
+ *
1004
+ * THE ASYMMETRY THAT MAKES THE FIX SOUND, and it is the whole of the design.
1005
+ * The AUDITED head comes from the CALLER: the gate runner passes `--head` from
1006
+ * the pull-request event (the registry entry now declares `parameters: [head]`,
1007
+ * the same mechanism `scope` has always used), and with no `--head` it is the
1008
+ * commit the context's own `HEAD` resolves to, which is what the checkout put
1009
+ * there. The DECLARED head comes from a document an agent under review wrote.
1010
+ * One of those two is a fact about the run and the other is a claim in the
1011
+ * evidence, and the claim must be measured against the fact rather than
1012
+ * replacing it.
1013
+ */
1014
+ /** Resolve one ref to a full commit sha inside a context directory. */
1015
+ export declare function resolveCommitIn(contextDirectory: string, ref: string): {
1016
+ ok: true;
1017
+ sha: string;
1018
+ } | {
1019
+ ok: false;
1020
+ reason: string;
1021
+ };
1022
+ /**
1023
+ * The commit a merge gate's verdict is ABOUT.
1024
+ *
1025
+ * THREE OUTCOMES AND NOT TWO, for the same reason `RegimeOutcome` has three:
1026
+ * "there is no commit to anchor to" and "the anchor could not be established"
1027
+ * are different facts. `unanchored` is reached only on the WORKTREE arm with
1028
+ * no `--head`, which is a context that is not a git repository at all, and
1029
+ * every sentence built from it says so rather than implying an anchor that was
1030
+ * never taken.
1031
+ */
1032
+ export type AuditedHead = {
1033
+ kind: "anchored";
1034
+ head: string;
1035
+ how: string;
1036
+ } | {
1037
+ kind: "unanchored";
1038
+ reason: string;
1039
+ } | {
1040
+ kind: "error";
1041
+ reason: string;
1042
+ };
1043
+ export declare function resolveAuditedHead(contextDirectory: string, requested: string | undefined, source: VerdictCorpusSource): AuditedHead;
1044
+ /**
1045
+ * THE MECHANISM THIS SECTION CLOSES, and it is the anchor above written one
1046
+ * relation too narrow: AN ANCHOR EXPRESSED AS EQUALITY WHERE THE RELATION THAT
1047
+ * CAN ACTUALLY HOLD IS ANCESTRY PLUS A CONSTRAINT ON WHAT CHANGED IN BETWEEN.
1048
+ *
1049
+ * `partitionByAuditedHead` compared the declared head to the audited one with
1050
+ * `===`, and no real flow can satisfy that. A reviewer reads commit X and
1051
+ * writes a verdict naming X; COMMITTING that verdict produces X+1; CI audits
1052
+ * X+1, or a merge commit above it. The declared head is therefore ALWAYS a
1053
+ * strict ancestor of the audited one, so under equality every real run reported
1054
+ * not-applicable. Measured on the branch that introduced the anchor, at
1055
+ * 5867a918cda809f7c5d4bc366fc7940458c140c0: the verdicts declared that commit,
1056
+ * the gate audited its DIRECT CHILD 0ddd06a73d49c1910449d01303bc9c2579f5f492,
1057
+ * and the record read not-applicable, exit 21, "is a review of other work and
1058
+ * is not evidence about this head".
1059
+ *
1060
+ * That is the same cannot-do-its-job shape one status along from the defect the
1061
+ * anchor fixed: "green forever once fed" became "never green", and a gate that
1062
+ * cannot go green is as uninformative as one that cannot go red (T-008's own
1063
+ * rule, applied to the other pole).
1064
+ *
1065
+ * WHAT MAKES THE RELAXATION SAFE, AND IT IS THE WHOLE DESIGN. Ancestry ALONE
1066
+ * would restore the original defect wearing a different hat: an approving pair
1067
+ * lands, and every later descendant carries it, including descendants full of
1068
+ * unreviewed source. So ancestry is admitted only when the TREES agree
1069
+ * everywhere except the project's own paperwork root. A verdict is evidence
1070
+ * about the SHIPPED CONTENT it read, and if that content is byte-identical in
1071
+ * the audited commit then the audited commit is the thing the reviewer
1072
+ * approved, whatever paperwork was committed on top of it.
1073
+ *
1074
+ * IT IS A CLAIM ABOUT THE TWO TREES, NOT ABOUT EACH INTERVENING COMMIT, and
1075
+ * the difference is stated rather than left to be discovered. `git diff
1076
+ * --name-only <declared>..<audited>` compares the endpoints, so a commit that
1077
+ * adds `src/x.ts` and a later one that removes it leave no entry and are
1078
+ * admitted, where a per-commit enumeration would refuse them. That case is
1079
+ * admitted DELIBERATELY: the audited tree's shipped content is then exactly
1080
+ * what the reviewers read, which is the property the gate is protecting. A
1081
+ * per-commit walk would refuse an ordinary revert-before-merge and buy nothing,
1082
+ * because there is no shipped byte in the audited tree that no verdict covers.
1083
+ *
1084
+ * `--no-renames` IS LOAD-BEARING. With rename detection on (git's default for
1085
+ * `git diff` since 2.9) a rename from `src/a.ts` to `delivery/b.md` prints the
1086
+ * DESTINATION ONLY, so the deletion of a source file would be invisible and the
1087
+ * gap would read as paperwork. Disabling it prints both sides.
1088
+ */
1089
+ /** How a verdict's declared head stands to the commit under audit. */
1090
+ export type HeadRelation =
1091
+ /** The verdict names the audited commit itself. */
1092
+ {
1093
+ kind: "same";
1094
+ }
1095
+ /** A strict ancestor whose gap to the audited commit is paperwork only. */
1096
+ | {
1097
+ kind: "evidence-only-ancestor";
1098
+ changed: string[];
1099
+ }
1100
+ /** A strict ancestor, but shipped content changed in between. */
1101
+ | {
1102
+ kind: "shipped-change";
1103
+ shipped: string[];
1104
+ }
1105
+ /** A real commit here that the audited commit is an ancestor OF. */
1106
+ | {
1107
+ kind: "descendant";
1108
+ }
1109
+ /** A real commit here on neither side of the audited one. */
1110
+ | {
1111
+ kind: "unrelated";
1112
+ }
1113
+ /** Forty hex digits naming no commit in this repository. */
1114
+ | {
1115
+ kind: "unresolvable";
1116
+ reason: string;
1117
+ }
1118
+ /** git could not answer, so the relation is not known. Never admitted. */
1119
+ | {
1120
+ kind: "undetermined";
1121
+ reason: string;
1122
+ };
1123
+ /**
1124
+ * Place one declared head against the commit under audit.
1125
+ *
1126
+ * EQUAL PASSES, unchanged, and it is checked first so a context git cannot be
1127
+ * questioned about still answers the one relation that needs no git at all.
1128
+ *
1129
+ * A DESCENDANT IS REFUSED, and it has its own sentence rather than being folded
1130
+ * into "not an ancestor". A verdict naming a commit BELOW the audited one is a
1131
+ * review of work the audited commit does not contain, which is the fail-open
1132
+ * direction stated backwards: the reviewers saw more than is being merged, and
1133
+ * nothing here establishes that what they approved about the extra work says
1134
+ * anything about the tree without it.
1135
+ */
1136
+ export declare function relateDeclaredHead(contextDirectory: string, declared: string, auditedHead: string): HeadRelation;
1137
+ /** A verdict admitted to the audited corpus, and the relation that admitted it. */
1138
+ export interface AdmittedVerdict {
1139
+ path: string;
1140
+ declared: string;
1141
+ relation: HeadRelation;
1142
+ }
1143
+ /** One verdict that is not about the audited commit, and why it is not. */
1144
+ export interface OffHeadVerdict {
1145
+ path: string;
1146
+ declared: string;
1147
+ /** Which route refused it. Printed, never summarised to a boolean. */
1148
+ relation: HeadRelation;
1149
+ }
1150
+ /** The corpus split by the commit under audit. */
1151
+ export interface HeadPartition {
1152
+ onHead: LoadedVerdict[];
1153
+ /**
1154
+ * The same verdicts, with the relation that admitted each one.
1155
+ *
1156
+ * CARRIED SEPARATELY RATHER THAN ATTACHED TO `onHead`, because `onHead` is
1157
+ * the set the checks are RUN OVER and its element type is what every other
1158
+ * caller of the loader consumes. This is the DISCLOSURE half: a green reached
1159
+ * through ancestry and a green reached through equality are different facts,
1160
+ * and a gate that printed one sentence for both would be the unfalsifiable
1161
+ * record `describeVerdictCorpusSource` exists to stop, one relation along.
1162
+ */
1163
+ admitted: AdmittedVerdict[];
1164
+ offHead: OffHeadVerdict[];
1165
+ /**
1166
+ * Verdicts whose own `head` could not be established at all, as DOCUMENTS
1167
+ * and as the sentences that name them.
1168
+ *
1169
+ * BOTH SHAPES, because the two callers need different halves and deriving one
1170
+ * from the other by matching on a message prefix is the string-parsing shape
1171
+ * this file refuses everywhere else. A caller that keeps them in the corpus
1172
+ * needs the documents; a caller that reports them needs the sentences.
1173
+ */
1174
+ unkeyed: Diagnostic[];
1175
+ unkeyedVerdicts: LoadedVerdict[];
1176
+ }
1177
+ /**
1178
+ * Split a loaded corpus into the verdicts that are about the audited commit
1179
+ * and the ones that are not.
1180
+ *
1181
+ * FIVE REFUSAL ROUTES, AND THEY ARE STRUCTURALLY DIFFERENT RATHER THAN ONE
1182
+ * SHAPE FIVE TIMES, which is what makes them members of a class instead of
1183
+ * instances of a finding:
1184
+ *
1185
+ * RESOLUTION the declared head is not a commit in this repository. The
1186
+ * document is evidence about an object nobody can produce.
1187
+ * SHIPPED GAP the declared head IS an ancestor, and shipped content changed
1188
+ * between it and the audited commit. Unreviewed work is riding
1189
+ * in on a review of something else.
1190
+ * DESCENDANT the declared head is BELOW the audited commit. The reviewers
1191
+ * read a tree the audited commit does not contain.
1192
+ * UNRELATED a real commit on neither side. A review of another line.
1193
+ * UNDETERMINED git could not place it. Never admitted, because a relation
1194
+ * nobody established must not read as the one that passes.
1195
+ *
1196
+ * ADMISSION IS TWO ROUTES AND THEY ARE ALSO PRINTED: the declared head IS the
1197
+ * audited commit, or it is an ancestor whose whole gap is paperwork. See
1198
+ * `relateDeclaredHead` for why the second is safe and for what it deliberately
1199
+ * does not refuse.
1200
+ *
1201
+ * Every refusal leaves the verdict out of the audited group and IS PRINTED with
1202
+ * the route it took, because a document silently dropped from a merge
1203
+ * corpus is the fail-open direction this file has already been bitten by at
1204
+ * `loadCommittedVerdicts`, at the `phase` canonicalisation and at
1205
+ * `headGroupFor`. A reader is owed the fact that the corpus holds two
1206
+ * approving reviews of something else.
1207
+ */
1208
+ export declare function partitionByAuditedHead(contextDirectory: string, verdicts: readonly LoadedVerdict[], auditedHead: string): HeadPartition;
1209
+ /** One operator-facing line per verdict the audit excluded, naming its route. */
1210
+ export declare function describeOffHeadVerdicts(offHead: readonly OffHeadVerdict[], auditedHead: string): string[];
1211
+ /** One operator-facing line per verdict the audit ADMITTED, naming its route. */
1212
+ export declare function describeAdmittedVerdicts(admitted: readonly AdmittedVerdict[], auditedHead: string): string[];
1213
+ /** The charter field DR-0038's declaration lives in (M4-D-28). */
1214
+ export declare const REVIEW_FAMILIES_FIELD = "review-families";
1215
+ /** The document that carries it. */
1216
+ export declare const CHARTER_DOCUMENT = "charter.yaml";
1217
+ /**
1218
+ * The documents that say WHICH merge-authority regime is in force.
1219
+ *
1220
+ * MOVED HERE FROM `scripts/check-dual-review.mjs` (FIX ROUND 2, DV-001). The
1221
+ * script held its own copy of this list AND its own presence probe, and the
1222
+ * probe read the WORKING TREE while `establishDelegatedRegime` read the
1223
+ * COMMIT. Two probes of one fact against two sources is the mechanism this
1224
+ * phase has now paid for twice: each answered correctly about its own source,
1225
+ * so nothing ever reported a disagreement, and an uncommitted `charter.yaml`
1226
+ * took the gate from error to GREEN on a correlated committed pair.
1227
+ */
1228
+ export declare const REGIME_DOCUMENTS: string[];
1229
+ /**
1230
+ * The first regime document that is NOT present at the source a decision over
1231
+ * this context would be made from, or `undefined` when both are.
1232
+ *
1233
+ * WHY THE REFUSAL LIVES AT THE MERGE GATE AND THE REPORT LIVES IN THE CHECK,
1234
+ * unchanged from M3-P9 and restated because this round moved the probe: the
1235
+ * derived check runs on ANY verdict with ANY context, and M3-P7's verdict
1236
+ * contexts carry a plan and a work history and no charter, so a check that
1237
+ * reddened on an absent charter reddened eight of that phase's tests. The
1238
+ * check therefore REPORTS, and `scripts/check-dual-review.mjs`, which is the
1239
+ * command DR-0012's grant runs through, refuses. What changed is that the
1240
+ * refusal and the report are now ONE probe with two callers, so they cannot
1241
+ * answer about different sources.
1242
+ *
1243
+ * THE SOURCE IS A PARAMETER, not resolved here, so a caller that has already
1244
+ * resolved one (the gate script resolves it when it loads the corpus) refuses
1245
+ * against the SAME commit it read the verdicts from rather than a second
1246
+ * `rev-parse` that could land elsewhere.
1247
+ */
1248
+ export declare function missingRegimeDocument(contextDirectory: string, source?: VerdictCorpusSource): {
1249
+ document: string;
1250
+ source: VerdictCorpusSource;
1251
+ reason: string;
1252
+ } | undefined;
1253
+ /**
1254
+ * Where a declaration was read from, so a claim nobody can refute is at least
1255
+ * ATTRIBUTABLE AND DATED.
1256
+ *
1257
+ * This is the honest half of DR-0038's third constraint. Two falsifiers below
1258
+ * catch a project whose own record contradicts the declaration. NEITHER of
1259
+ * them catches a project that HAS a second family available and has simply
1260
+ * never used it, and nothing inside the record can: the record holds what was
1261
+ * used, not what was reachable. So the countermeasure for that residue is
1262
+ * provenance rather than detection, on the src/gates/release.ts:1028 pattern,
1263
+ * and the gap is stated here rather than left to be found.
1264
+ */
1265
+ export interface ReviewFamiliesProvenance {
1266
+ /** The path inside the commit, as `git show` was asked for it. */
1267
+ path: string;
1268
+ /** The ref the declaration was read from, as the caller spelled it. */
1269
+ ref: string;
1270
+ /** That ref resolved to a commit sha. */
1271
+ refSha: string;
1272
+ /** sha256 of the exact blob bytes the declaration was decoded from. */
1273
+ sha256: string;
1274
+ }
1275
+ /**
1276
+ * What reading `review-families` produced.
1277
+ *
1278
+ * THREE OUTCOMES AND NOT TWO, for the reason `RegimeOutcome` gives one screen
1279
+ * up: "no declaration" and "a declaration that could not be established" are
1280
+ * different facts. The first leaves DR-0012 condition 1 applying unchanged,
1281
+ * which is a REPORT-nothing. The second is an ERROR, because a check that
1282
+ * cannot establish whether an exception applies must never decide that it does
1283
+ * not apply and carry on (M2-C-3).
1284
+ */
1285
+ export type ReviewFamiliesReading = {
1286
+ kind: "absent";
1287
+ } | {
1288
+ kind: "error";
1289
+ reason: string;
1290
+ } | {
1291
+ kind: "declared";
1292
+ /** Canonicalised, deduplicated by construction, sorted. Compared. */
1293
+ families: string[];
1294
+ /** The operator's own spelling, in document order. Printed. */
1295
+ declaredAs: string[];
1296
+ reason: string;
1297
+ provenance: ReviewFamiliesProvenance;
1298
+ };
1299
+ /**
1300
+ * Read DR-0038's declaration OUT OF THE GIT OBJECT DATABASE, never out of the
1301
+ * working tree.
1302
+ *
1303
+ * WHY THE COMMITTED BLOB IS THE ONLY ONE THAT COUNTS. This is the anti-widening
1304
+ * rule the scope auditor and `loadDeclaration` (src/gates/release.ts:817) both
1305
+ * already apply, one condition along: a phase must not be able to switch off,
1306
+ * inside its own working tree, the condition that would otherwise have refused
1307
+ * its merge. A declaration read from disk is one an implementer can add,
1308
+ * merge under, and delete, leaving a merged head whose record says the
1309
+ * cross-family requirement was met.
1310
+ *
1311
+ * `HEAD:./charter.yaml` AND NOT `HEAD:charter.yaml`, and the difference is not
1312
+ * cosmetic. A path without the leading `./` is resolved against the repository
1313
+ * ROOT, so a context directory that happens to sit inside a larger repository
1314
+ * (which every fixture staged under a checkout does) would silently read that
1315
+ * repository's charter instead of its own. With `./` git resolves relative to
1316
+ * the directory it was run in, which is the one the caller named.
1317
+ */
1318
+ export declare function readReviewFamilies(contextDirectory: string, ref?: string): ReviewFamiliesReading;
1319
+ /** One line naming where a declaration came from, for a detail or a report. */
1320
+ export declare function reviewFamiliesProvenanceLine(provenance: ReviewFamiliesProvenance): string;
1321
+ /** What the single-family arm concluded about one committed corpus. */
1322
+ export type SingleFamilyOutcome = {
1323
+ kind: "not-declared";
1324
+ } | {
1325
+ kind: "error";
1326
+ reason: string;
1327
+ } | {
1328
+ kind: "refused";
1329
+ violations: Diagnostic[];
1330
+ } | {
1331
+ kind: "exempt";
1332
+ family: string;
1333
+ reading: Extract<ReviewFamiliesReading, {
1334
+ kind: "declared";
1335
+ }>;
1336
+ reports: string[];
1337
+ };
1338
+ /**
1339
+ * DR-0038's exception, and its two falsifiers, over one committed corpus.
1340
+ *
1341
+ * THE EXCEPTION NARROWS EXACTLY ONE DIMENSION. `produced-by` stops being
1342
+ * required to differ. `framing` and `review-contract` are untouched, because
1343
+ * T-007's whole finding is that model decorrelation and CONTRACT decorrelation
1344
+ * are different properties: a single-family environment still has two framings
1345
+ * and two contracts available to it, so relaxing those would be relaxing
1346
+ * something the environment does not force.
1347
+ *
1348
+ * FALSIFIER 1, CONTRADICTION BY THE CORPUS. If the project's own committed
1349
+ * verdicts carry two or more distinct canonicalised `produced-by` values, the
1350
+ * declaration is contradicted by the project's own record and this is RED. A
1351
+ * project that has demonstrably used two cannot claim one.
1352
+ *
1353
+ * FALSIFIER 2, THE NAME MUST MATCH. A verdict whose `produced-by` canonicalises
1354
+ * to anything other than the declared family is RED. Without this, a
1355
+ * declaration could name a family nothing in the record uses and still buy the
1356
+ * relaxation.
1357
+ *
1358
+ * THE SCOPE IS THE WHOLE COMMITTED CORPUS, NOT THE ONE (phase, head) GROUP,
1359
+ * and that is deliberate. "This project has one family available" is a claim
1360
+ * about the project, so the widest set of its own verdicts is what can refute
1361
+ * it. Scoping the falsifiers to the group under review would let a project
1362
+ * whose history carries three families declare one, provided the two reviews
1363
+ * in front of the check happened to agree.
1364
+ *
1365
+ * WHAT IT DOES NOT CATCH, said here and not only in the plan: a project with a
1366
+ * second family AVAILABLE that has simply never used it. Nothing in a record of
1367
+ * what WAS used reaches what COULD have been used. `readReviewFamilies` answers
1368
+ * that with provenance rather than detection: the claim is attributable to a
1369
+ * commit and a blob, so it is dated and signed even where it is not refutable.
1370
+ */
1371
+ export declare function singleFamilyException(contextDirectory: string, loaded: LoadedVerdictCorpus): SingleFamilyOutcome;
758
1372
  /**
759
1373
  * DR-0012's merge precondition, made into a comparison a command can make
760
1374
  * against the verdict FILES rather than against a session's memory (M3R-004).
@@ -790,17 +1404,114 @@ export declare const DELEGATED_MERGE_AUTHORITY = "delegated-under-conditions";
790
1404
  * criteria contract. So `review-contract` is compared separately and is
791
1405
  * witnessed separately (criterion 7b).
792
1406
  *
793
- * WHAT IT DOES NOT REACH, named rather than left to be found. Condition (d) of
794
- * step 3b, that neither verdict carries an unresolved high or medium finding,
795
- * is NOT checked here: the verdict schema's own root `if`/`then` already
796
- * forbids APPROVE beside a high or critical finding, and "unresolved" is a
797
- * state of the review thread rather than of the document. Nothing here decides
798
- * whether the two verdicts describe the same HEAD either: the verdict schema
799
- * carries no head field, so `phase` is the join key and the DIRECTORY is what
800
- * scopes a set of verdicts to one head. Both are stated in
801
- * delivery/work-history/m3-p9.md as declared readings rather than absorbed.
1407
+ * WHAT IT DOES NOT REACH, named rather than left to be found, AND BOTH ITEMS
1408
+ * THIS PARAGRAPH USED TO NAME HAVE BEEN CLOSED BY M4-P10. The first was
1409
+ * condition (d) of step 3b, that neither verdict carries an unresolved high or
1410
+ * medium finding; that is now the sibling check `verdict-pair-approves`, and
1411
+ * the schema's own root `if`/`then` was widened from [high, critical] to
1412
+ * [medium, high, critical] at the same time, because it had been one severity
1413
+ * narrower than DR-0012 condition 2 ever since it shipped. The second was that
1414
+ * nothing decided whether two verdicts describe the same HEAD; the schema now
1415
+ * requires `head` and this check groups by `(phase, head)`.
1416
+ *
1417
+ * WHAT IS STILL NOT REACHED, so the paragraph does not read as complete. This
1418
+ * check compares `produced-by` as a canonicalised STRING, never as a model
1419
+ * FAMILY, so two values naming one vendor pass as decorrelated; that was
1420
+ * measured twice against this repository's own reviews and recorded at
1421
+ * delivery/verification/m4-prototype-probes.md:1. THE DEFERRAL CHAIN FOR IT
1422
+ * TERMINATED WITH NO OWNER, which is what CR-VS-003 found and what this
1423
+ * paragraph used to hide: it said "closing it is M4-P11's declared scope", and
1424
+ * M4-P11 declined the mechanism (delivery/work-history/m4-p11.md:142) and
1425
+ * shipped the single-family EXCEPTION instead, which is a different question.
1426
+ * No later phase picked it up. The string comparison therefore STANDS, and what
1427
+ * the DR-0047 sweep changed is that the green line now says so: see
1428
+ * `producedByCaveat`, which also names what would close it. And a
1429
+ * `produced-by` line is written BY the reviewing agent, so it is forgeable; an
1430
+ * observed alternative exists and is M4-D-06's business, not this check's.
802
1431
  */
803
1432
  export declare const dualReviewDecorrelation: DerivedCheck;
1433
+ /**
1434
+ * The severities DR-0012 condition 2 bars an APPROVE from sitting beside.
1435
+ *
1436
+ * `low` is absent DELIBERATELY and the record says why:
1437
+ * delivery/decisions/DR-0012-delegated-merge-authority.md:23 permits merging
1438
+ * with a low finding provided it is fixed or tracked with a reason. The same
1439
+ * three words are the escalation enum in `schemas/verdict.schema.json`, and the
1440
+ * two must agree; M4-P10 widened both together, because the shipped pair had
1441
+ * the schema stopping at `high` while the decision said `medium`.
1442
+ */
1443
+ export declare const BLOCKING_SEVERITIES: readonly string[];
1444
+ /**
1445
+ * DR-0012 CONDITION 2, MADE INTO A PREDICATE
1446
+ * (delivery/decisions/DR-0012-delegated-merge-authority.md:23).
1447
+ *
1448
+ * WHAT WAS MISSING, stated as the gap rather than as a feature. Before this
1449
+ * check, `scripts/check-dual-review.mjs` could not see a verdict's VALUE at
1450
+ * all: measured against the whole of that script, `grep -c` returned 0 for
1451
+ * `APPROVE`, 0 for `severity` and 0 for `findings`. So two properly
1452
+ * decorrelated reviews that both REFUSED the merge passed the gate green, and
1453
+ * so did an APPROVE sitting beside a finding the review itself ranked medium.
1454
+ * Condition 1 looked checked and condition 2 was asserted by a human, which is
1455
+ * the worse of the two states because it reads as progress.
1456
+ *
1457
+ * WHY IT IS A SEPARATE CHECK RATHER THAN MORE OF `dual-review-decorrelation`.
1458
+ * They are different predicates over the same set, and section 2.3 rule 3's
1459
+ * Kind B falsification is per-check: DEREGISTERING this one must make a
1460
+ * refusing pair pass, which is only a witness if there is one id to deregister.
1461
+ * Folding condition 2 into the decorrelation check would have made that
1462
+ * witness unavailable and would have made one red indistinguishable from the
1463
+ * other in the gate's output.
1464
+ *
1465
+ * WHY IT EVALUATES THE COMMITTED GROUP AND NEVER THE INSTANCE'S OWN FIELDS,
1466
+ * which is the one place its shape differs from its sibling's. DR-0012
1467
+ * condition 2 is a property of the two reviews WRITTEN TO `delivery/review/`
1468
+ * AND COMMITTED. A document handed to this check that is not among them is not
1469
+ * a review the grant can be satisfied by, and it also cannot break the
1470
+ * predicate: what is asserted is about the committed set, which the stray
1471
+ * document is not a member of. So there is no membership test here, and the
1472
+ * empty case is not a hole: a `(phase, head)` selecting fewer than two
1473
+ * committed verdicts is refused by the pair-size rule below.
1474
+ *
1475
+ * WHAT IT DOES NOT REACH, named rather than left to be found. "Unresolved" is
1476
+ * a state of the review THREAD, and this check reads documents: a finding that
1477
+ * was raised, fixed in a later round and left in the file still reddens here.
1478
+ * That is the fail-closed direction and it is a real cost, paid deliberately,
1479
+ * because the alternative is a resolution field an author sets on their own
1480
+ * finding. Nothing here decides whether a `severity` was ranked honestly
1481
+ * either; a review that calls a critical defect `low` passes, and no keyword
1482
+ * reaches that.
1483
+ */
1484
+ export declare const verdictPairApproves: DerivedCheck;
1485
+ /**
1486
+ * THE SUBJECT ECHO AGREES WITH THE RESOLUTION IT SITS BESIDE, THE RECORD WAS
1487
+ * WRITTEN AFTER THE TURN ENDED, AND AN OVERRIDE WAS PERMITTED BEFORE IT WAS
1488
+ * APPLIED.
1489
+ *
1490
+ * All three compare SIBLING FIELDS of one document, so all three are Kind B
1491
+ * and none of them is reachable from a keyword (schemas/README.md's Kind A and
1492
+ * Kind B section, DR-0013 clause 8). The schema next door can require that
1493
+ * `subject`, `turnEnd` and `resolution` are all PRESENT, which is what makes a
1494
+ * launch-time record unrepresentable, and it stops exactly there: it cannot
1495
+ * say that two present values agree.
1496
+ *
1497
+ * WHY THE ECHO MATTERS AT ALL, since a record that echoes itself sounds
1498
+ * circular. It is not the record checking itself against itself. `subject` is
1499
+ * a VERBATIM copy of the launch request the adapter was handed and
1500
+ * `resolution` is what the adapter's resolver actually consumed, and the
1501
+ * hazard is that those two diverge silently: a resolver that read the wrong
1502
+ * role's row produces a perfectly well-formed record whose family token is
1503
+ * then attributed to a task it was never about. That is the misattribution
1504
+ * guard src/gates/schemas/release-record.schema.json:26 exists for, one seam
1505
+ * along, and the kernel-side half of it is in src/model-resolution.ts where
1506
+ * the request is compared against a copy the kernel itself holds.
1507
+ *
1508
+ * THE OVERRIDE DIRECTION IS THE ONE MOST LIKELY TO BE GOT WRONG. M4-P7
1509
+ * criterion 7 wants BOTH directions: a role whose `charter-override` is
1510
+ * `allowed` takes the charter's tier, and a role whose permission is anything
1511
+ * else does not. The second direction is the one a resolver written from the
1512
+ * happy path silently drops, because nothing about it looks like a failure.
1513
+ */
1514
+ export declare const modelResolutionSubjectEcho: DerivedCheck;
804
1515
  /** Register a check. Later phases append their own (section 2.3's table). */
805
1516
  export declare function registerCheck(check: DerivedCheck): void;
806
1517
  /** Remove a check by id. Returns whether one was removed. */