@tiphys/kernel 0.0.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 (207) hide show
  1. package/AGENTS.md +663 -0
  2. package/LICENSE +202 -0
  3. package/assurance-modes.yaml +299 -0
  4. package/checklists/clean-room.yaml +325 -0
  5. package/checklists/env-failure-diagnosis.yaml +68 -0
  6. package/checklists/flake-playbook.yaml +68 -0
  7. package/checklists/hazard-review.yaml +144 -0
  8. package/checklists/plan-review.yaml +103 -0
  9. package/dist/bin/tiphys.d.ts +2 -0
  10. package/dist/bin/tiphys.js +92 -0
  11. package/dist/src/adapters/load.d.ts +202 -0
  12. package/dist/src/adapters/load.js +440 -0
  13. package/dist/src/brief.d.ts +32 -0
  14. package/dist/src/brief.js +66 -0
  15. package/dist/src/checklists.d.ts +169 -0
  16. package/dist/src/checklists.js +310 -0
  17. package/dist/src/checks.d.ts +1539 -0
  18. package/dist/src/checks.js +5025 -0
  19. package/dist/src/cli.d.ts +9 -0
  20. package/dist/src/cli.js +68 -0
  21. package/dist/src/commands/brief.d.ts +92 -0
  22. package/dist/src/commands/brief.js +343 -0
  23. package/dist/src/commands/checklist.d.ts +42 -0
  24. package/dist/src/commands/checklist.js +168 -0
  25. package/dist/src/commands/cutover.d.ts +35 -0
  26. package/dist/src/commands/cutover.js +448 -0
  27. package/dist/src/commands/doctor.d.ts +264 -0
  28. package/dist/src/commands/doctor.js +1546 -0
  29. package/dist/src/commands/gates.d.ts +9 -0
  30. package/dist/src/commands/gates.js +360 -0
  31. package/dist/src/commands/init.d.ts +30 -0
  32. package/dist/src/commands/init.js +177 -0
  33. package/dist/src/commands/lock.d.ts +34 -0
  34. package/dist/src/commands/lock.js +340 -0
  35. package/dist/src/commands/mode.d.ts +40 -0
  36. package/dist/src/commands/mode.js +134 -0
  37. package/dist/src/commands/next.d.ts +130 -0
  38. package/dist/src/commands/next.js +597 -0
  39. package/dist/src/commands/plan.d.ts +20 -0
  40. package/dist/src/commands/plan.js +105 -0
  41. package/dist/src/commands/pool.d.ts +1 -0
  42. package/dist/src/commands/pool.js +139 -0
  43. package/dist/src/commands/resume.d.ts +1 -0
  44. package/dist/src/commands/resume.js +88 -0
  45. package/dist/src/commands/spawn.d.ts +1 -0
  46. package/dist/src/commands/spawn.js +195 -0
  47. package/dist/src/commands/status.d.ts +21 -0
  48. package/dist/src/commands/status.js +122 -0
  49. package/dist/src/commands/sync.d.ts +47 -0
  50. package/dist/src/commands/sync.js +341 -0
  51. package/dist/src/commands/teardown.d.ts +1 -0
  52. package/dist/src/commands/teardown.js +87 -0
  53. package/dist/src/commands/tuition.d.ts +21 -0
  54. package/dist/src/commands/tuition.js +218 -0
  55. package/dist/src/commands/validate.d.ts +78 -0
  56. package/dist/src/commands/validate.js +430 -0
  57. package/dist/src/commands/watch.d.ts +1 -0
  58. package/dist/src/commands/watch.js +172 -0
  59. package/dist/src/cutover.d.ts +584 -0
  60. package/dist/src/cutover.js +1444 -0
  61. package/dist/src/exclusion.d.ts +389 -0
  62. package/dist/src/exclusion.js +843 -0
  63. package/dist/src/exec/env.d.ts +278 -0
  64. package/dist/src/exec/env.js +334 -0
  65. package/dist/src/fleet.d.ts +223 -0
  66. package/dist/src/fleet.js +298 -0
  67. package/dist/src/gates/adapters/http-json.d.ts +5 -0
  68. package/dist/src/gates/adapters/http-json.js +283 -0
  69. package/dist/src/gates/adapters/migrations-command.d.ts +1 -0
  70. package/dist/src/gates/adapters/migrations-command.js +373 -0
  71. package/dist/src/gates/citations.d.ts +408 -0
  72. package/dist/src/gates/citations.js +1169 -0
  73. package/dist/src/gates/coverage.d.ts +375 -0
  74. package/dist/src/gates/coverage.js +836 -0
  75. package/dist/src/gates/credentials.d.ts +233 -0
  76. package/dist/src/gates/credentials.js +752 -0
  77. package/dist/src/gates/deploy.d.ts +1 -0
  78. package/dist/src/gates/deploy.js +33 -0
  79. package/dist/src/gates/gate-classes.d.ts +56 -0
  80. package/dist/src/gates/gate-classes.js +633 -0
  81. package/dist/src/gates/manifest.d.ts +99 -0
  82. package/dist/src/gates/manifest.js +208 -0
  83. package/dist/src/gates/merge-preconditions.d.ts +319 -0
  84. package/dist/src/gates/merge-preconditions.js +932 -0
  85. package/dist/src/gates/migrations.d.ts +1 -0
  86. package/dist/src/gates/migrations.js +36 -0
  87. package/dist/src/gates/pin.d.ts +114 -0
  88. package/dist/src/gates/pin.js +154 -0
  89. package/dist/src/gates/red-witness.d.ts +22 -0
  90. package/dist/src/gates/red-witness.js +482 -0
  91. package/dist/src/gates/release.d.ts +283 -0
  92. package/dist/src/gates/release.js +820 -0
  93. package/dist/src/gates/result.d.ts +116 -0
  94. package/dist/src/gates/result.js +91 -0
  95. package/dist/src/gates/run.d.ts +614 -0
  96. package/dist/src/gates/run.js +1614 -0
  97. package/dist/src/gates/schemas/citation-config.schema.json +59 -0
  98. package/dist/src/gates/schemas/coverage-config.schema.json +77 -0
  99. package/dist/src/gates/schemas/gate-manifest.schema.json +125 -0
  100. package/dist/src/gates/schemas/gate-result.schema.json +160 -0
  101. package/dist/src/gates/schemas/phase-declaration.schema.json +87 -0
  102. package/dist/src/gates/schemas/release-record.schema.json +119 -0
  103. package/dist/src/gates/schemas/verifier-config.schema.json +101 -0
  104. package/dist/src/gates/schemas/witness-spec.schema.json +110 -0
  105. package/dist/src/gates/scope.d.ts +131 -0
  106. package/dist/src/gates/scope.js +1018 -0
  107. package/dist/src/gates/suite.d.ts +217 -0
  108. package/dist/src/gates/suite.js +968 -0
  109. package/dist/src/gates/validate.d.ts +121 -0
  110. package/dist/src/gates/validate.js +414 -0
  111. package/dist/src/hooks.d.ts +84 -0
  112. package/dist/src/hooks.js +125 -0
  113. package/dist/src/index.d.ts +31 -0
  114. package/dist/src/index.js +30 -0
  115. package/dist/src/liveness.d.ts +321 -0
  116. package/dist/src/liveness.js +396 -0
  117. package/dist/src/lock.d.ts +256 -0
  118. package/dist/src/lock.js +792 -0
  119. package/dist/src/model-resolution.d.ts +159 -0
  120. package/dist/src/model-resolution.js +307 -0
  121. package/dist/src/modes.d.ts +149 -0
  122. package/dist/src/modes.js +258 -0
  123. package/dist/src/path-identity.d.ts +34 -0
  124. package/dist/src/path-identity.js +48 -0
  125. package/dist/src/plan.d.ts +73 -0
  126. package/dist/src/plan.js +153 -0
  127. package/dist/src/pool.d.ts +326 -0
  128. package/dist/src/pool.js +988 -0
  129. package/dist/src/roles.d.ts +461 -0
  130. package/dist/src/roles.js +776 -0
  131. package/dist/src/spawn.d.ts +482 -0
  132. package/dist/src/spawn.js +1003 -0
  133. package/dist/src/status.d.ts +116 -0
  134. package/dist/src/status.js +148 -0
  135. package/dist/src/task.d.ts +504 -0
  136. package/dist/src/task.js +307 -0
  137. package/dist/src/teardown.d.ts +39 -0
  138. package/dist/src/teardown.js +422 -0
  139. package/dist/src/tuition.d.ts +159 -0
  140. package/dist/src/tuition.js +311 -0
  141. package/dist/src/validate.d.ts +263 -0
  142. package/dist/src/validate.js +742 -0
  143. package/dist/src/version.d.ts +3 -0
  144. package/dist/src/version.js +38 -0
  145. package/dist/src/watcher.d.ts +275 -0
  146. package/dist/src/watcher.js +849 -0
  147. package/dist/src/witness/run.d.ts +299 -0
  148. package/dist/src/witness/run.js +1373 -0
  149. package/dist/src/witness/spec.d.ts +270 -0
  150. package/dist/src/witness/spec.js +475 -0
  151. package/dist/tsconfig.src.tsbuildinfo +1 -0
  152. package/gate-registry.yaml +526 -0
  153. package/gates.manifest.json +257 -0
  154. package/package.json +72 -3
  155. package/role-model-config.yaml +88 -0
  156. package/roles/README.md +128 -0
  157. package/roles/_shared-dispatch-contract.md +87 -0
  158. package/roles/adversarial-plan-reviewer.md +80 -0
  159. package/roles/clean-room-reviewer.md +140 -0
  160. package/roles/implementer.md +463 -0
  161. package/roles/investigator.md +138 -0
  162. package/roles/plan-writer.md +95 -0
  163. package/schemas/README.md +82 -0
  164. package/schemas/assurance-modes.schema.json +264 -0
  165. package/schemas/charter.schema.json +185 -0
  166. package/schemas/checklist.schema.json +114 -0
  167. package/schemas/cutover-state.schema.json +64 -0
  168. package/schemas/decision-record.schema.json +88 -0
  169. package/schemas/executor-record.schema.json +36 -0
  170. package/schemas/final-report.schema.json +90 -0
  171. package/schemas/finding.schema.json +106 -0
  172. package/schemas/gate-registry.schema.json +260 -0
  173. package/schemas/mechanism-index.schema.json +94 -0
  174. package/schemas/model-resolution.schema.json +362 -0
  175. package/schemas/plan.schema.json +300 -0
  176. package/schemas/report.schema.json +579 -0
  177. package/schemas/role-brief.schema.json +105 -0
  178. package/schemas/role-model-config.schema.json +90 -0
  179. package/schemas/status-line.schema.json +40 -0
  180. package/schemas/tuition.schema.json +191 -0
  181. package/schemas/verdict.schema.json +295 -0
  182. package/schemas/work-history.schema.json +183 -0
  183. package/schemas/write-bypass.schema.json +69 -0
  184. package/templates/charter.example.yaml +54 -0
  185. package/templates/decision-record.example.yaml +27 -0
  186. package/templates/final-report.example.yaml +80 -0
  187. package/templates/plan.example.yaml +87 -0
  188. package/templates/report.example.yaml +236 -0
  189. package/templates/warnings.md +74 -0
  190. package/templates/work-history.example.yaml +185 -0
  191. package/tuition/README.md +76 -0
  192. package/tuition/T-001.yaml +48 -0
  193. package/tuition/T-002.yaml +51 -0
  194. package/tuition/T-003.yaml +100 -0
  195. package/tuition/T-004.yaml +52 -0
  196. package/tuition/T-005.yaml +72 -0
  197. package/tuition/T-006.yaml +81 -0
  198. package/tuition/T-007.yaml +56 -0
  199. package/tuition/T-008.yaml +111 -0
  200. package/tuition/T-009.yaml +50 -0
  201. package/tuition/T-015.yaml +36 -0
  202. package/tuition/T-016.yaml +36 -0
  203. package/tuition/T-017.yaml +46 -0
  204. package/tuition/T-018.yaml +84 -0
  205. package/tuition/T-021.yaml +40 -0
  206. package/tuition/T-022.yaml +36 -0
  207. package/tuition/mechanism-index.yaml +256 -0
@@ -0,0 +1,99 @@
1
+ import type { SchemaDocument } from "./validate.ts";
2
+ /**
3
+ * THE GATE MANIFEST (kernel plan M2, M2-P1 step 6).
4
+ *
5
+ * One manifest per repository. It declares which gates exist, how each is
6
+ * invoked, what one unit of its work is, whether an unmet precondition
7
+ * fails the run, and which commands can destroy work.
8
+ *
9
+ * SCHEMA RESOLUTION FROM SOURCE AND FROM dist/ (step 5). The schema
10
+ * documents sit beside this module at `./schemas/*.json` and are resolved
11
+ * through `import.meta.url`, so the same code finds
12
+ * `src/gates/schemas/...` when run from source and
13
+ * `dist/src/gates/schemas/...` when run from the compiled entry. `tsc` does
14
+ * not copy JSON, which was MEASURED rather than assumed (the measurement is
15
+ * in the work history), so the build script copies the directory into
16
+ * `dist/` and criterion 10 witnesses both documents in `npm pack` output.
17
+ * They are deliberately NOT imported as modules: `resolveJsonModule` is off
18
+ * in both tsconfigs, and turning it on would inline the schema into the
19
+ * bundle and make the shipped document and the enforced document two
20
+ * different things.
21
+ *
22
+ * M2-C-6: the manifest path comes from the caller, so it is read through the
23
+ * DELIVERED `readRegularFileIfPresent`. A named pipe at the manifest path
24
+ * reports `error` naming the path and the type, and never blocks.
25
+ */
26
+ export type PreconditionKind = "file-exists" | "file-absent" | "branch-matches" | "diff-touches" | "command-exit-zero";
27
+ export declare const PRECONDITION_KINDS: readonly PreconditionKind[];
28
+ export type RunParameter = "base" | "head" | "phase";
29
+ export interface PreconditionSpec {
30
+ id: string;
31
+ kind: PreconditionKind;
32
+ path?: string;
33
+ pattern?: string;
34
+ paths?: string[];
35
+ command?: string[];
36
+ }
37
+ export interface GateEntry {
38
+ id: string;
39
+ command: string[];
40
+ unitLabel: string;
41
+ applicability: "required" | "conditional";
42
+ parameters?: RunParameter[];
43
+ precondition?: PreconditionSpec;
44
+ modes?: string[];
45
+ }
46
+ export interface GateManifest {
47
+ version: number;
48
+ gates: GateEntry[];
49
+ destructiveCommands: string[];
50
+ }
51
+ export type ManifestLoad = {
52
+ ok: true;
53
+ manifest: GateManifest;
54
+ sha256: string;
55
+ body: string;
56
+ } | {
57
+ ok: false;
58
+ reason: string;
59
+ diagnostics: string[];
60
+ };
61
+ export declare function manifestSchema(): SchemaDocument;
62
+ export declare function resultSchema(): SchemaDocument;
63
+ /**
64
+ * Absolute paths of every schema document `manifest-self-check` validates.
65
+ *
66
+ * ENUMERATED, NOT ENUMERABLE-BY-HAND (fix round, M2-P1). This milestone's
67
+ * plan (kernel-plan-v1.md) adds four more schema documents to this same
68
+ * directory across M2-P2, M2-P4, M2-P5, M2-P6 and M2-P7. A fixed two-entry
69
+ * list here was the mechanism, not a fluke: it named the two documents that
70
+ * existed at the time this function was written and would keep returning
71
+ * exactly those two forever, so manifest-self-check would silently stop
72
+ * validating every document a later phase ships, while test/gates.test.ts's
73
+ * independent readdir-based parity check reddens correctly the moment a
74
+ * phase adds one (CR-812's test, `test/gates.test.ts` near line 2378). The
75
+ * fix is to make the SAME enumeration the source of truth here: read the
76
+ * directory beside this module, keep entries matching `*.schema.json`, and
77
+ * sort them by filename so the order is deterministic across platforms and
78
+ * across runs (readdir order is not guaranteed by POSIX). Every document a
79
+ * phase drops into `src/gates/schemas/` is then validated automatically,
80
+ * and the parity property the test checks becomes structural rather than
81
+ * a thing two call sites have to be kept in sync by hand.
82
+ */
83
+ export declare function schemaDocumentPaths(): string[];
84
+ /**
85
+ * Validate an already-parsed manifest document. Returns the diagnostics in
86
+ * the contract's deterministic order (by pointer, then message).
87
+ */
88
+ export declare function validateManifestDocument(document: unknown): string[];
89
+ /** Validate an already-parsed gate result record. */
90
+ export declare function validateResultDocument(document: unknown): string[];
91
+ /**
92
+ * Load and validate a manifest from a path the caller supplied.
93
+ *
94
+ * Fail closed at every step: not a regular file, absent, unparseable, or
95
+ * schema-invalid all produce `ok: false` with a reason. None of them
96
+ * produces a manifest with fewer gates in it, which is the failure this
97
+ * whole milestone exists to make impossible.
98
+ */
99
+ export declare function loadManifest(path: string): ManifestLoad;
@@ -0,0 +1,208 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readdirSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import { readRegularFileIfPresent } from "../task.js";
5
+ import { DIAGNOSTIC_MESSAGES, formatDiagnostics, loadSchema, validate, } from "./validate.js";
6
+ export const PRECONDITION_KINDS = [
7
+ "file-exists",
8
+ "file-absent",
9
+ "branch-matches",
10
+ "diff-touches",
11
+ "command-exit-zero",
12
+ ];
13
+ const schemaDirectory = new URL("./schemas/", import.meta.url);
14
+ function readSchemaDocument(name) {
15
+ const path = fileURLToPath(new URL(name, schemaDirectory));
16
+ const read = readRegularFileIfPresent(path);
17
+ if (read.kind !== "read") {
18
+ throw new Error(read.kind === "absent"
19
+ ? `schema document ${path} is missing from this installation`
20
+ : read.reason);
21
+ }
22
+ const parsed = JSON.parse(read.body);
23
+ const loaded = loadSchema(parsed, name);
24
+ if (!loaded.ok) {
25
+ throw new Error(loaded.reason);
26
+ }
27
+ return loaded.schema;
28
+ }
29
+ /** The two schema documents this milestone ships, loaded once. */
30
+ let cachedManifestSchema;
31
+ let cachedResultSchema;
32
+ export function manifestSchema() {
33
+ if (cachedManifestSchema === undefined) {
34
+ cachedManifestSchema = readSchemaDocument("gate-manifest.schema.json");
35
+ }
36
+ return cachedManifestSchema;
37
+ }
38
+ export function resultSchema() {
39
+ if (cachedResultSchema === undefined) {
40
+ cachedResultSchema = readSchemaDocument("gate-result.schema.json");
41
+ }
42
+ return cachedResultSchema;
43
+ }
44
+ /**
45
+ * Absolute paths of every schema document `manifest-self-check` validates.
46
+ *
47
+ * ENUMERATED, NOT ENUMERABLE-BY-HAND (fix round, M2-P1). This milestone's
48
+ * plan (kernel-plan-v1.md) adds four more schema documents to this same
49
+ * directory across M2-P2, M2-P4, M2-P5, M2-P6 and M2-P7. A fixed two-entry
50
+ * list here was the mechanism, not a fluke: it named the two documents that
51
+ * existed at the time this function was written and would keep returning
52
+ * exactly those two forever, so manifest-self-check would silently stop
53
+ * validating every document a later phase ships, while test/gates.test.ts's
54
+ * independent readdir-based parity check reddens correctly the moment a
55
+ * phase adds one (CR-812's test, `test/gates.test.ts` near line 2378). The
56
+ * fix is to make the SAME enumeration the source of truth here: read the
57
+ * directory beside this module, keep entries matching `*.schema.json`, and
58
+ * sort them by filename so the order is deterministic across platforms and
59
+ * across runs (readdir order is not guaranteed by POSIX). Every document a
60
+ * phase drops into `src/gates/schemas/` is then validated automatically,
61
+ * and the parity property the test checks becomes structural rather than
62
+ * a thing two call sites have to be kept in sync by hand.
63
+ */
64
+ export function schemaDocumentPaths() {
65
+ const directory = fileURLToPath(schemaDirectory);
66
+ return readdirSync(directory)
67
+ .filter((name) => name.endsWith(".schema.json"))
68
+ .sort()
69
+ .map((name) => fileURLToPath(new URL(name, schemaDirectory)));
70
+ }
71
+ /**
72
+ * The kind-specific required fields, checked here rather than in the schema.
73
+ *
74
+ * WHY NOT IN THE SCHEMA. Expressing "kind file-exists requires path" needs
75
+ * conditional composition (`if`/`then`, or `oneOf`), and both are outside
76
+ * M2-D-04's closed keyword set, which DR-0013 decided against growing. So
77
+ * the check lives in code and emits the SAME `INVALID <pointer> <message>`
78
+ * diagnostics, which keeps one diagnostic contract rather than two. When
79
+ * M3-P1 swaps in Ajv the constraint can move into the schema without any
80
+ * consumer noticing, because the output line is unchanged.
81
+ */
82
+ const REQUIRED_FIELD_FOR_KIND = {
83
+ "file-exists": "path",
84
+ "file-absent": "path",
85
+ "branch-matches": "pattern",
86
+ "diff-touches": "paths",
87
+ "command-exit-zero": "command",
88
+ };
89
+ function preconditionFieldDiagnostics(manifest) {
90
+ const found = [];
91
+ const gates = manifest.gates;
92
+ if (!Array.isArray(gates)) {
93
+ return found;
94
+ }
95
+ for (let index = 0; index < gates.length; index += 1) {
96
+ const gate = gates[index];
97
+ const precondition = gate?.precondition;
98
+ if (precondition === undefined || precondition === null) {
99
+ continue;
100
+ }
101
+ const kind = precondition["kind"];
102
+ if (typeof kind !== "string" || !PRECONDITION_KINDS.includes(kind)) {
103
+ continue;
104
+ }
105
+ const field = REQUIRED_FIELD_FOR_KIND[kind];
106
+ if (precondition[field] === undefined) {
107
+ found.push({
108
+ pointer: `#/gates/${index}/precondition/${field}`,
109
+ message: DIAGNOSTIC_MESSAGES.required(field),
110
+ });
111
+ }
112
+ }
113
+ return found;
114
+ }
115
+ /** Every duplicate gate id, as diagnostics. A registry keyed by name cannot hold two. */
116
+ function duplicateIdDiagnostics(manifest) {
117
+ const found = [];
118
+ const gates = manifest.gates;
119
+ if (!Array.isArray(gates)) {
120
+ return found;
121
+ }
122
+ const seen = new Set();
123
+ for (let index = 0; index < gates.length; index += 1) {
124
+ const id = gates[index]?.id;
125
+ if (typeof id !== "string") {
126
+ continue;
127
+ }
128
+ if (seen.has(id)) {
129
+ found.push({
130
+ pointer: `#/gates/${index}/id`,
131
+ message: DIAGNOSTIC_MESSAGES.duplicateId(JSON.stringify(id)),
132
+ });
133
+ }
134
+ seen.add(id);
135
+ }
136
+ return found;
137
+ }
138
+ /**
139
+ * Validate an already-parsed manifest document. Returns the diagnostics in
140
+ * the contract's deterministic order (by pointer, then message).
141
+ */
142
+ export function validateManifestDocument(document) {
143
+ const diagnostics = [
144
+ ...validate(manifestSchema(), document),
145
+ ...preconditionFieldDiagnostics(document),
146
+ ...duplicateIdDiagnostics(document),
147
+ ];
148
+ diagnostics.sort((a, b) => {
149
+ if (a.pointer !== b.pointer) {
150
+ return a.pointer < b.pointer ? -1 : 1;
151
+ }
152
+ if (a.message === b.message) {
153
+ return 0;
154
+ }
155
+ return a.message < b.message ? -1 : 1;
156
+ });
157
+ return formatDiagnostics(diagnostics);
158
+ }
159
+ /** Validate an already-parsed gate result record. */
160
+ export function validateResultDocument(document) {
161
+ return formatDiagnostics(validate(resultSchema(), document));
162
+ }
163
+ /**
164
+ * Load and validate a manifest from a path the caller supplied.
165
+ *
166
+ * Fail closed at every step: not a regular file, absent, unparseable, or
167
+ * schema-invalid all produce `ok: false` with a reason. None of them
168
+ * produces a manifest with fewer gates in it, which is the failure this
169
+ * whole milestone exists to make impossible.
170
+ */
171
+ export function loadManifest(path) {
172
+ const read = readRegularFileIfPresent(path);
173
+ if (read.kind === "absent") {
174
+ return {
175
+ ok: false,
176
+ reason: `manifest ${path} does not exist`,
177
+ diagnostics: [],
178
+ };
179
+ }
180
+ if (read.kind === "refused") {
181
+ return { ok: false, reason: read.reason, diagnostics: [] };
182
+ }
183
+ let parsed;
184
+ try {
185
+ parsed = JSON.parse(read.body);
186
+ }
187
+ catch (error) {
188
+ return {
189
+ ok: false,
190
+ reason: `manifest ${path} does not parse as JSON: ${error.message}`,
191
+ diagnostics: [],
192
+ };
193
+ }
194
+ const diagnostics = validateManifestDocument(parsed);
195
+ if (diagnostics.length > 0) {
196
+ return {
197
+ ok: false,
198
+ reason: `manifest ${path} is not a valid gate manifest`,
199
+ diagnostics,
200
+ };
201
+ }
202
+ return {
203
+ ok: true,
204
+ manifest: parsed,
205
+ sha256: createHash("sha256").update(read.body).digest("hex"),
206
+ body: read.body,
207
+ };
208
+ }
@@ -0,0 +1,319 @@
1
+ import type { GateStatus } from "./result.ts";
2
+ /**
3
+ * THE MERGE PRECONDITION READER (kernel plan M4, M4-P12; DR-0012, DR-0036,
4
+ * T-009, R-064, R-065a).
5
+ *
6
+ * WHAT THIS IS NOT. It is not a merge command. M4-D-09 puts the merge
7
+ * capability in the plugin at cutover and DR-0036 keeps merge authority with
8
+ * the current process for the whole of M4. What is missing today is not the
9
+ * ability to merge; it is any ARTIFACT saying the six conditions of
10
+ * delivery/decisions/DR-0012-delegated-merge-authority.md:22 to :27 held at the
11
+ * head that was merged. So this gate produces one ROW PER CONDITION, each
12
+ * carrying the head sha it was evaluated against, and the orchestrator reads it
13
+ * before merging, by hand.
14
+ *
15
+ * THE HAZARD CLASS, in the plan's words: A MERGE PRECONDITION CHECK THAT IS
16
+ * GREEN BECAUSE IT COULD NOT LOOK. Every arm below is written against one of
17
+ * its members, and each member is named where it is refused rather than in a
18
+ * list nobody rereads:
19
+ *
20
+ * a 401 piped into a `|| true` -> `probeApi` runs FIRST and an
21
+ * unreachable or refusing API is
22
+ * `error` with units 0. CLAUDE.md
23
+ * standing warning 6 records this shape
24
+ * costing a whole watcher.
25
+ * an API failure called N/A -> `not-applicable` is reachable from
26
+ * exactly one place here, the
27
+ * NO-VERDICT-AT-THIS-HEAD arm, and it
28
+ * carries an evaluated precondition
29
+ * record (SC-011).
30
+ * CI-green read off the BRANCH -> condition 4 compares
31
+ * `check_run.head_sha` against the head
32
+ * under evaluation and reddens when they
33
+ * differ, which is T-009 one scope down.
34
+ * scope satisfied by a record EXISTING -> condition 5 reads the record's
35
+ * STATUS word.
36
+ * arbitration satisfied by a file EXISTING -> condition 6 requires the
37
+ * document to name BOTH verdicts and the
38
+ * SAME head.
39
+ * a ruleset read as a default -> an empty body from the ruleset API is
40
+ * `error`, never an assumed shape.
41
+ *
42
+ * WHY CONDITIONS 1 AND 2 ARE COMPOSED AND NOT REIMPLEMENTED (plan step 6).
43
+ * M4-P10 shipped `dual-review-decorrelation` (DR-0012 condition 1) and
44
+ * `verdict-pair-approves` (condition 2) as derived checks in `src/checks.ts`,
45
+ * and `scripts/check-dual-review.mjs` is the runner around them. This gate is a
46
+ * SECOND CALLER of the same exported primitives rather than a second copy of
47
+ * the rules: it resolves the corpus with `resolveCorpusSource`, refuses a
48
+ * context whose delivery regime is undeterminable with `missingRegimeDocument`,
49
+ * reads the DR-0038 declaration with `readReviewFamilies`, and then runs the
50
+ * two checks BY ID out of `registeredChecks()`.
51
+ *
52
+ * BY ID, AND A MISSING REGISTRATION IS `error`. `scripts/check-dual-review.mjs`
53
+ * prints `0 registered check(s) named X` beside its own verdict, which is the
54
+ * Kind B witness that deregistering the check is visible. On the MERGE path a
55
+ * condition with no check behind it is a condition nobody evaluated, so here it
56
+ * is `error` and never a green with the comparison quietly skipped.
57
+ *
58
+ * WHY THE ROW STATUS AND THE GATE STATUS ARE NOT THE SAME WORD, and this is the
59
+ * one place the plan and the criteria say different things, so the reconciliation
60
+ * is written down rather than left to a reader. Plan step 4
61
+ * (delivery/plan/kernel-plan-m4.md:1911) says an ABSENT scope record is `error`;
62
+ * criterion 4 (delivery/plan/kernel-plan-m4.md:1938) says condition 5 is RED when
63
+ * no record exists. Both hold at once because they speak about different objects:
64
+ * the CONDITION is unsatisfied either way and its row reads `red` with a reason
65
+ * naming which arm it was, while the GATE could not reach a verdict about an
66
+ * instrument that is not there, so the gate's own status is `error`. Everything a
67
+ * reader needs is printed: the row, its reason, and the gate word.
68
+ */
69
+ declare const GATE_ID = "merge-preconditions";
70
+ declare const UNIT_LABEL = "merge preconditions evaluated";
71
+ /**
72
+ * The id of the one precondition this gate can report unmet.
73
+ *
74
+ * SC-011: `not-applicable` ASSERTS that a precondition was evaluated. The
75
+ * precondition here is "a merge is being proposed at this head", evidenced by
76
+ * at least one committed verdict document naming it. A head with no such
77
+ * document is not a merge waiting on six conditions; it is a branch nobody has
78
+ * reviewed yet, and reporting red for that would make the gate unusable on
79
+ * every push while making it say something false.
80
+ */
81
+ declare const PRECONDITION_ID = "merge-preconditions-verdict-names-this-head";
82
+ interface Flags {
83
+ result?: string;
84
+ evidence?: string;
85
+ head?: string;
86
+ phase?: string;
87
+ context?: string;
88
+ repo?: string;
89
+ "api-base"?: string;
90
+ "scope-record"?: string;
91
+ arbitrations?: string;
92
+ }
93
+ /**
94
+ * ROW STATUS IS THE SAME FOUR-WORD VOCABULARY MINUS `not-applicable`.
95
+ * A row cannot be not-applicable: the gate as a whole is, or every condition
96
+ * was evaluated. Keeping the words identical to `GateStatus` is deliberate, so
97
+ * a reader does not have to learn a second vocabulary for the thing the gate
98
+ * exists to print.
99
+ */
100
+ export type RowStatus = "green" | "red" | "error";
101
+ export interface ConditionRow {
102
+ /** `condition-1` .. `condition-6`, or `branch-protection`. */
103
+ id: string;
104
+ /** The DR-0012 clause, or the requirement id, this row is about. */
105
+ clause: string;
106
+ status: RowStatus;
107
+ /** The head sha this row was evaluated against. Criterion 8 wants it here. */
108
+ head: string;
109
+ /** One sentence, printed on EVERY arm including green. */
110
+ sentence: string;
111
+ }
112
+ export declare function renderRow(row: ConditionRow): string;
113
+ /**
114
+ * The gate word for a set of rows.
115
+ *
116
+ * `error` DOMINATES `red`, which is M2-C-3's direction: a run that could not
117
+ * look at one condition has not reached a verdict about the merge, and a red
118
+ * would be a verdict. A red that is also accompanied by an error still reports
119
+ * error, and both rows print either way, so nothing is hidden by the ordering.
120
+ */
121
+ export declare function gateStatusForRows(rows: readonly ConditionRow[]): GateStatus;
122
+ export type ApiResponse = {
123
+ ok: true;
124
+ status: number;
125
+ body: string;
126
+ } | {
127
+ ok: false;
128
+ reason: string;
129
+ };
130
+ /**
131
+ * ONE REQUEST, AND THE FAILURE ARM IS WRITTEN FIRST.
132
+ *
133
+ * CLAUDE.md standing warning 6 records the exact defect this shape exists
134
+ * against: a watcher whose failure arm was a `.catch(() => {})` emitted nothing
135
+ * and was indistinguishable from a run still in progress. There is no catch
136
+ * here that returns a value the caller can mistake for data: a transport
137
+ * failure becomes `{ok: false, reason}` and every caller turns that into
138
+ * `error`.
139
+ *
140
+ * NO CREDENTIAL IS READ, AND THAT IS A RULE RATHER THAN AN OVERSIGHT.
141
+ * test/m2-exit-test.test.ts:425 asserts, by grepping every file under
142
+ * `src/gates/`, that no production gate reads a LITERAL-NAMED environment
143
+ * variable, because such a read is an ambient switch that changes a gate's
144
+ * reported status with nothing in the record to say so. An earlier draft of
145
+ * this module read `GH_TOKEN` and that test caught it, which is the guard
146
+ * working. Measured 2026-09-17 from this container, with NO Authorization
147
+ * header at all: `GET /repos/{slug}`, `GET /repos/{slug}/rulesets` and
148
+ * `GET /repos/{slug}/commits/{sha}/check-runs` each answered HTTP 200, because
149
+ * the agent proxy substitutes credentials on the way out and the value in
150
+ * `GH_TOKEN` is irrelevant (CLAUDE.md standing warning 6's invalid-token
151
+ * control measures the same thing). WHAT THIS COSTS, recorded rather than left
152
+ * to be found: in a deployment where the API genuinely requires a credential,
153
+ * every request here answers 401 or 404 and the gate reports `error`. That is
154
+ * the fail-closed direction and never a silent pass, and supplying a token
155
+ * would have to be a DECLARED FLAG in the registry command rather than an
156
+ * ambient environment read.
157
+ */
158
+ export declare function requestJson(url: string): Promise<ApiResponse>;
159
+ export type JsonReading = {
160
+ ok: true;
161
+ value: unknown;
162
+ } | {
163
+ ok: false;
164
+ reason: string;
165
+ };
166
+ /**
167
+ * PARSE A RESPONSE BODY, AND AN EMPTY ONE IS A FAILURE RATHER THAN A DEFAULT.
168
+ *
169
+ * Criterion 6 is the whole reason this is a named function: the ruleset API
170
+ * answering with a zero-length body must produce `error`, because a default
171
+ * here is the silent pass this phase exists against. `JSON.parse("")` throws,
172
+ * so the empty case would reach the same place anyway; it is separated out so
173
+ * the REASON a reader is given names the emptiness rather than a parser
174
+ * message that says nothing about what happened.
175
+ */
176
+ export declare function readJsonBody(url: string, response: ApiResponse): JsonReading;
177
+ /**
178
+ * `owner/name` out of a git remote URL, or undefined when the URL is not one.
179
+ *
180
+ * DERIVED RATHER THAN WRITTEN INTO THE REGISTRY, and the reason is DR-0029: the
181
+ * kernel is just another project under the scheme, so a registry entry naming
182
+ * ONE repository would be the kernel's registry claiming to be everybody's. The
183
+ * remote is the fact this gate is actually about.
184
+ */
185
+ export declare function slugFromRemote(url: string): string | undefined;
186
+ interface CheckRun {
187
+ name?: unknown;
188
+ status?: unknown;
189
+ conclusion?: unknown;
190
+ head_sha?: unknown;
191
+ }
192
+ /**
193
+ * Condition 4, and the comparison that makes it worth having.
194
+ *
195
+ * T-009 ONE SCOPE DOWN. "CI is green" is never a complete sentence: the
196
+ * complete one names the event and the HEAD SHA. A check that asked the API for
197
+ * the newest run on the BRANCH would report green off a run for an earlier
198
+ * head, which is exactly the state this repository spent four hours and
199
+ * twenty-one minutes in. So every check run the API returns is compared on
200
+ * `head_sha` and one that names a different commit is reported as the different
201
+ * commit it is, never counted.
202
+ */
203
+ export declare function judgeCheckRuns(head: string, runs: readonly CheckRun[], context: string): {
204
+ ok: boolean;
205
+ sentence: string;
206
+ };
207
+ /**
208
+ * Condition 5, and the distinction the plan's hazard list names: the condition
209
+ * is satisfied by the record's STATUS, never by the record's EXISTENCE.
210
+ *
211
+ * The absent arm and the red arm are DIFFERENT REASONS on purpose (criterion
212
+ * 4). `instrument` is what the caller turns into the gate's own word: an absent
213
+ * record means the gate could not look, which is `error` at gate level while
214
+ * the condition itself is unsatisfied.
215
+ */
216
+ export declare function judgeScopeRecord(path: string, read: {
217
+ kind: "read";
218
+ body: string;
219
+ } | {
220
+ kind: "absent";
221
+ } | {
222
+ kind: "refused";
223
+ reason: string;
224
+ }): {
225
+ ok: boolean;
226
+ sentence: string;
227
+ instrument: "present" | "missing";
228
+ };
229
+ /**
230
+ * Condition 6, and EXISTENCE IS NOT THE TEST.
231
+ *
232
+ * Criterion 5 asks for two structurally different members of that class, and
233
+ * they are structurally different because they fail on different halves of the
234
+ * same document: one that names only ONE of the two verdicts has read half the
235
+ * evidence, and one that names a DIFFERENT HEAD has read the right number of
236
+ * documents about the wrong commit. A check that tested for the file's presence
237
+ * passes both.
238
+ *
239
+ * VERDICTS ARE IDENTIFIED BY FILE NAME because `schemas/verdict.schema.json`
240
+ * gives a verdict no id of its own; its required keys are kind, phase, head,
241
+ * verdict, produced-by, framing, review-contract, findings, criteria and
242
+ * deviations-judged. The existing arbitration documents in `delivery/review/`
243
+ * already cite their reviews by path, so the convention that exists is read
244
+ * rather than a field invented.
245
+ *
246
+ * THE FILE NAME AND NOT THE FULL PATH, and that is a measured correction rather
247
+ * than a preference. `loadCommittedVerdicts` returns ABSOLUTE paths when it
248
+ * falls back to the worktree and repository-relative ones when it reads a
249
+ * commit, so comparing whole paths made the same document resolve or not
250
+ * resolve depending on which corpus source happened to be chosen. The file name
251
+ * is the part that is stable across both, and it is the part an arbitration
252
+ * document's `- reviews:` line contains either way.
253
+ */
254
+ export declare function judgeArbitration(path: string, head: string, verdictPaths: readonly string[], read: {
255
+ kind: "read";
256
+ body: string;
257
+ } | {
258
+ kind: "absent";
259
+ } | {
260
+ kind: "refused";
261
+ reason: string;
262
+ }): {
263
+ ok: boolean;
264
+ sentence: string;
265
+ };
266
+ interface RulesetRule {
267
+ type?: unknown;
268
+ parameters?: Record<string, unknown>;
269
+ }
270
+ export interface RulesetReading {
271
+ id: string;
272
+ name: string;
273
+ enforcement: string;
274
+ rules: RulesetRule[];
275
+ }
276
+ /**
277
+ * The ruleset encoding, and the TWO members of criterion 7.
278
+ *
279
+ * `enforcement: disabled` is PRESENT-BUT-TOOTHLESS and a
280
+ * `required_status_checks` rule that does not name `gates` is
281
+ * PRESENT-BUT-WRONG. They are different failures and they are reported with
282
+ * different sentences, because a reader told only "the ruleset is wrong" has to
283
+ * go and find out which.
284
+ *
285
+ * R-065a IS DATA, NOT A VERDICT. Squash-only is an OWNER action the owner has
286
+ * deferred; the plan (step 7) says report its state and do not judge it, so
287
+ * `allowed_merge_methods` is printed and never turns this row red.
288
+ */
289
+ export declare function judgeRulesets(rulesets: readonly RulesetReading[]): {
290
+ ok: boolean;
291
+ sentence: string;
292
+ };
293
+ export interface VerdictForHead {
294
+ path: string;
295
+ record: Record<string, unknown>;
296
+ }
297
+ /**
298
+ * Condition 3, and what it DOES and DOES NOT establish.
299
+ *
300
+ * DR-0012 condition 3 (delivery/decisions/DR-0012-delegated-merge-authority.md:24)
301
+ * says both reviewers were given the phase's acceptance criteria as their
302
+ * contract and both WALKED OR EXECUTED them. What is reachable from the verdict
303
+ * documents alone is that each one declares a `review-contract` and carries a
304
+ * non-empty `criteria[]` whose entries each record `met`. Whether that walk
305
+ * COVERS every acceptance criterion the plan declares is a comparison against a
306
+ * different document and it is the shipped Kind B check
307
+ * `verdict-criteria-complete`, which resolves a `plan.yaml` out of its context.
308
+ * This repository has no such document, so running it here would make this row
309
+ * permanently error about the instrument rather than about the merge. The row's
310
+ * sentence therefore says which half it established, and the other half is
311
+ * recorded as residue in delivery/work-history/m4-p12.md rather than implied.
312
+ */
313
+ export declare function judgeCriteriaWalked(verdicts: readonly VerdictForHead[]): {
314
+ ok: boolean;
315
+ sentence: string;
316
+ };
317
+ export declare function runGate(flags: Flags): Promise<number>;
318
+ export declare function main(argv: string[]): Promise<number>;
319
+ export { GATE_ID, PRECONDITION_ID, UNIT_LABEL };