@tiphys/kernel 0.0.0 → 0.1.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 (181) hide show
  1. package/AGENTS.md +611 -0
  2. package/LICENSE +202 -0
  3. package/assurance-modes.yaml +278 -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 +14 -0
  11. package/dist/src/brief.d.ts +32 -0
  12. package/dist/src/brief.js +59 -0
  13. package/dist/src/checklists.d.ts +169 -0
  14. package/dist/src/checklists.js +310 -0
  15. package/dist/src/checks.d.ts +828 -0
  16. package/dist/src/checks.js +3314 -0
  17. package/dist/src/cli.d.ts +9 -0
  18. package/dist/src/cli.js +57 -0
  19. package/dist/src/commands/brief.d.ts +92 -0
  20. package/dist/src/commands/brief.js +320 -0
  21. package/dist/src/commands/checklist.d.ts +42 -0
  22. package/dist/src/commands/checklist.js +168 -0
  23. package/dist/src/commands/doctor.d.ts +35 -0
  24. package/dist/src/commands/doctor.js +605 -0
  25. package/dist/src/commands/gates.d.ts +9 -0
  26. package/dist/src/commands/gates.js +360 -0
  27. package/dist/src/commands/init.d.ts +30 -0
  28. package/dist/src/commands/init.js +128 -0
  29. package/dist/src/commands/lock.d.ts +1 -0
  30. package/dist/src/commands/lock.js +229 -0
  31. package/dist/src/commands/mode.d.ts +40 -0
  32. package/dist/src/commands/mode.js +134 -0
  33. package/dist/src/commands/plan.d.ts +20 -0
  34. package/dist/src/commands/plan.js +105 -0
  35. package/dist/src/commands/pool.d.ts +1 -0
  36. package/dist/src/commands/pool.js +128 -0
  37. package/dist/src/commands/spawn.d.ts +1 -0
  38. package/dist/src/commands/spawn.js +146 -0
  39. package/dist/src/commands/status.d.ts +19 -0
  40. package/dist/src/commands/status.js +120 -0
  41. package/dist/src/commands/teardown.d.ts +1 -0
  42. package/dist/src/commands/teardown.js +79 -0
  43. package/dist/src/commands/tuition.d.ts +21 -0
  44. package/dist/src/commands/tuition.js +218 -0
  45. package/dist/src/commands/validate.d.ts +78 -0
  46. package/dist/src/commands/validate.js +360 -0
  47. package/dist/src/commands/watch.d.ts +1 -0
  48. package/dist/src/commands/watch.js +172 -0
  49. package/dist/src/exec/env.d.ts +128 -0
  50. package/dist/src/exec/env.js +190 -0
  51. package/dist/src/fleet.d.ts +51 -0
  52. package/dist/src/fleet.js +80 -0
  53. package/dist/src/gates/adapters/http-json.d.ts +5 -0
  54. package/dist/src/gates/adapters/http-json.js +283 -0
  55. package/dist/src/gates/adapters/migrations-command.d.ts +1 -0
  56. package/dist/src/gates/adapters/migrations-command.js +373 -0
  57. package/dist/src/gates/citations.d.ts +408 -0
  58. package/dist/src/gates/citations.js +1163 -0
  59. package/dist/src/gates/coverage.d.ts +284 -0
  60. package/dist/src/gates/coverage.js +701 -0
  61. package/dist/src/gates/credentials.d.ts +74 -0
  62. package/dist/src/gates/credentials.js +533 -0
  63. package/dist/src/gates/deploy.d.ts +1 -0
  64. package/dist/src/gates/deploy.js +33 -0
  65. package/dist/src/gates/manifest.d.ts +99 -0
  66. package/dist/src/gates/manifest.js +208 -0
  67. package/dist/src/gates/migrations.d.ts +1 -0
  68. package/dist/src/gates/migrations.js +36 -0
  69. package/dist/src/gates/pin.d.ts +114 -0
  70. package/dist/src/gates/pin.js +154 -0
  71. package/dist/src/gates/red-witness.d.ts +22 -0
  72. package/dist/src/gates/red-witness.js +390 -0
  73. package/dist/src/gates/release.d.ts +283 -0
  74. package/dist/src/gates/release.js +820 -0
  75. package/dist/src/gates/result.d.ts +116 -0
  76. package/dist/src/gates/result.js +91 -0
  77. package/dist/src/gates/run.d.ts +566 -0
  78. package/dist/src/gates/run.js +1536 -0
  79. package/dist/src/gates/schemas/citation-config.schema.json +59 -0
  80. package/dist/src/gates/schemas/coverage-config.schema.json +77 -0
  81. package/dist/src/gates/schemas/gate-manifest.schema.json +125 -0
  82. package/dist/src/gates/schemas/gate-result.schema.json +160 -0
  83. package/dist/src/gates/schemas/phase-declaration.schema.json +42 -0
  84. package/dist/src/gates/schemas/release-record.schema.json +119 -0
  85. package/dist/src/gates/schemas/verifier-config.schema.json +101 -0
  86. package/dist/src/gates/schemas/witness-spec.schema.json +110 -0
  87. package/dist/src/gates/scope.d.ts +131 -0
  88. package/dist/src/gates/scope.js +1018 -0
  89. package/dist/src/gates/suite.d.ts +217 -0
  90. package/dist/src/gates/suite.js +927 -0
  91. package/dist/src/gates/validate.d.ts +121 -0
  92. package/dist/src/gates/validate.js +414 -0
  93. package/dist/src/hooks.d.ts +32 -0
  94. package/dist/src/hooks.js +62 -0
  95. package/dist/src/liveness.d.ts +321 -0
  96. package/dist/src/liveness.js +396 -0
  97. package/dist/src/lock.d.ts +178 -0
  98. package/dist/src/lock.js +500 -0
  99. package/dist/src/modes.d.ts +149 -0
  100. package/dist/src/modes.js +258 -0
  101. package/dist/src/path-identity.d.ts +2 -0
  102. package/dist/src/path-identity.js +10 -0
  103. package/dist/src/plan.d.ts +73 -0
  104. package/dist/src/plan.js +153 -0
  105. package/dist/src/pool.d.ts +130 -0
  106. package/dist/src/pool.js +721 -0
  107. package/dist/src/roles.d.ts +430 -0
  108. package/dist/src/roles.js +734 -0
  109. package/dist/src/spawn.d.ts +177 -0
  110. package/dist/src/spawn.js +332 -0
  111. package/dist/src/status.d.ts +91 -0
  112. package/dist/src/status.js +119 -0
  113. package/dist/src/task.d.ts +264 -0
  114. package/dist/src/task.js +305 -0
  115. package/dist/src/teardown.d.ts +32 -0
  116. package/dist/src/teardown.js +314 -0
  117. package/dist/src/tuition.d.ts +159 -0
  118. package/dist/src/tuition.js +311 -0
  119. package/dist/src/validate.d.ts +230 -0
  120. package/dist/src/validate.js +732 -0
  121. package/dist/src/version.d.ts +3 -0
  122. package/dist/src/version.js +38 -0
  123. package/dist/src/watcher.d.ts +275 -0
  124. package/dist/src/watcher.js +859 -0
  125. package/dist/src/witness/run.d.ts +274 -0
  126. package/dist/src/witness/run.js +1327 -0
  127. package/dist/src/witness/spec.d.ts +102 -0
  128. package/dist/src/witness/spec.js +253 -0
  129. package/dist/tsconfig.src.tsbuildinfo +1 -0
  130. package/gate-registry.yaml +390 -0
  131. package/gates.manifest.json +195 -0
  132. package/package.json +57 -3
  133. package/role-model-config.yaml +88 -0
  134. package/roles/README.md +128 -0
  135. package/roles/_shared-dispatch-contract.md +87 -0
  136. package/roles/adversarial-plan-reviewer.md +80 -0
  137. package/roles/clean-room-reviewer.md +140 -0
  138. package/roles/implementer.md +460 -0
  139. package/roles/investigator.md +138 -0
  140. package/roles/plan-writer.md +95 -0
  141. package/schemas/README.md +81 -0
  142. package/schemas/assurance-modes.schema.json +264 -0
  143. package/schemas/charter.schema.json +166 -0
  144. package/schemas/checklist.schema.json +114 -0
  145. package/schemas/decision-record.schema.json +88 -0
  146. package/schemas/final-report.schema.json +90 -0
  147. package/schemas/finding.schema.json +106 -0
  148. package/schemas/gate-registry.schema.json +260 -0
  149. package/schemas/mechanism-index.schema.json +94 -0
  150. package/schemas/plan.schema.json +300 -0
  151. package/schemas/report.schema.json +579 -0
  152. package/schemas/role-brief.schema.json +105 -0
  153. package/schemas/role-model-config.schema.json +90 -0
  154. package/schemas/status-line.schema.json +40 -0
  155. package/schemas/tuition.schema.json +191 -0
  156. package/schemas/verdict.schema.json +289 -0
  157. package/schemas/work-history.schema.json +183 -0
  158. package/templates/charter.example.yaml +54 -0
  159. package/templates/decision-record.example.yaml +27 -0
  160. package/templates/final-report.example.yaml +80 -0
  161. package/templates/plan.example.yaml +87 -0
  162. package/templates/report.example.yaml +236 -0
  163. package/templates/warnings.md +74 -0
  164. package/templates/work-history.example.yaml +185 -0
  165. package/tuition/README.md +76 -0
  166. package/tuition/T-001.yaml +48 -0
  167. package/tuition/T-002.yaml +51 -0
  168. package/tuition/T-003.yaml +100 -0
  169. package/tuition/T-004.yaml +52 -0
  170. package/tuition/T-005.yaml +72 -0
  171. package/tuition/T-006.yaml +81 -0
  172. package/tuition/T-007.yaml +56 -0
  173. package/tuition/T-008.yaml +111 -0
  174. package/tuition/T-009.yaml +50 -0
  175. package/tuition/T-015.yaml +36 -0
  176. package/tuition/T-016.yaml +36 -0
  177. package/tuition/T-017.yaml +46 -0
  178. package/tuition/T-018.yaml +84 -0
  179. package/tuition/T-021.yaml +40 -0
  180. package/tuition/T-022.yaml +36 -0
  181. package/tuition/mechanism-index.yaml +256 -0
@@ -0,0 +1,566 @@
1
+ import type { GateEntry, GateManifest, RunParameter } from "./manifest.ts";
2
+ import type { GateStatus } from "./result.ts";
3
+ /**
4
+ * THE GATE RUNNER (kernel plan M2, M2-P1 step 7 and step 8).
5
+ *
6
+ * Runs the manifest's gates sequentially in the foreground as subprocesses,
7
+ * captures each one's stdout and stderr into the evidence directory,
8
+ * ingests each one's result record, and writes `summary.json`.
9
+ *
10
+ * C-3, STRUCTURALLY: `spawnSync` only, no `detached`, no `unref`, no flag
11
+ * anywhere that lets a gate outlive this process. C-2, STRUCTURALLY: nothing
12
+ * here reads a pid, probes a process, sends a signal or touches /proc; a
13
+ * gate's outcome is its RECORD plus its exit code, never its liveness.
14
+ *
15
+ * THE GATE SUBPROCESS CONTRACT, stated once because eight phases build
16
+ * against it:
17
+ *
18
+ * invocation <command...> --result <abs path> --evidence <abs dir>
19
+ * plus one --<name> <value> for each parameter the gate
20
+ * DECLARES in the manifest (base, head, phase)
21
+ * cwd the runner's working directory
22
+ * output exactly one GateResult JSON document at --result
23
+ * exit codes 0 green, 1 red, 20 not-applicable, 21 error, 64 usage
24
+ *
25
+ * WHY PARAMETERS ARE DECLARED AND NOT INFERRED. Step 7 requires a gate
26
+ * "whose command requires --phase" to report `error` when `--phase` is
27
+ * absent. The runner can only know that from a declaration or by
28
+ * pattern-matching the gate's command line, and MECHANISMS.md's row
29
+ * "Deciding what another program will do by pattern-matching the text of a
30
+ * file it consumes" records four fix rounds paid for the second option. So
31
+ * the manifest declares it, the schema validates it, and the runner reads
32
+ * it. This adds one field to the plan's manifest field list and is declared
33
+ * as a deviation in the work history.
34
+ *
35
+ * INGEST IS ADVERSARIAL TOWARDS ITS OWN GATES. A gate is another program:
36
+ * it can exit 0 having written nothing, exit nonzero because Node threw,
37
+ * write a record for a different gate, write a record whose status
38
+ * contradicts its exit code, or write a green record having examined
39
+ * nothing. Every one of those is `error`, because `error` is what "I cannot
40
+ * tell you whether the property holds" is called (M2-C-3), and because
41
+ * every one of them otherwise reads as a pass.
42
+ *
43
+ * WHAT "APPLICABLE" MEANS, fixed in round 1 (CR-800). It used to mean "the
44
+ * runner spawned this gate", which is not the same thing and differed from it
45
+ * on the one path that matters: a gate that decides its OWN applicability and
46
+ * exits 20 with a `not-applicable` record was spawned, so it counted as
47
+ * applicable, so the aggregate anti-vacuity rule never fired, so a bundle in
48
+ * which zero gates were green exited 0 with the reason "every applicable gate
49
+ * is green". The two routes to `not-applicable`, runner-evaluated and
50
+ * gate-declared, were guarded differently, and the gate subprocess contract
51
+ * documented above is what tells every gate author the second route exists.
52
+ *
53
+ * So there are now two counts and they answer two different questions:
54
+ *
55
+ * applicable the gate was reached AND did not report `not-applicable`,
56
+ * whichever side decided that. It is the denominator of "how
57
+ * much of this manifest was in play".
58
+ * verdict the gate reached a GREEN OR RED verdict. It is the only
59
+ * count the anti-vacuity rule consults, because it is the only
60
+ * one that means work was actually done.
61
+ *
62
+ * The exit-0 success path is then structurally unable to describe an empty
63
+ * green bucket: it is guarded by `verdict > 0` and asserted again before the
64
+ * summary is written, so an internal inconsistency reports `error` rather
65
+ * than a green nobody measured (SC-011, M2-C-2, M2R-012).
66
+ *
67
+ * ONE RUN OWNS ITS EVIDENCE DIRECTORY (CR-803). Two runners pointed at one
68
+ * `--evidence` directory used to interleave on fixed per-gate paths: the
69
+ * later runner ingested the earlier one's records as its own, and a genuine
70
+ * red was converted to `error` while the surviving bundle was the other run's
71
+ * green. That is the declared hazard "a runner that writes a record for a
72
+ * gate it did not execute", and seven phases run this concurrently.
73
+ *
74
+ * The evidence directory is therefore CLAIMED with an O_EXCL create, the
75
+ * pattern src/lock.ts already carries and which MECHANISMS.md requires a
76
+ * third user to read first. Per that row's rule, a claim that cannot be taken
77
+ * fails LOUDLY and NAMES THE STUCK FILE; there is no steal, no age heuristic
78
+ * and no bounded wait, because an evidence directory is not a contended
79
+ * resource by design and a silent wait would be indistinguishable from an
80
+ * absence of contention. Every run also stamps a `runId` into its summary, so
81
+ * a bundle is attributable, and the summary is replaced atomically through a
82
+ * stage name carrying that runId, which no other run can collide with
83
+ * (MECHANISMS.md, "Atomic file replacement").
84
+ *
85
+ * THE RUNNER OBEYS ITS OWN CRASH DISCIPLINE (CR-801). It used to enforce on
86
+ * its gates the rule that Node's uncaught-exception exit code 1 collides with
87
+ * this phase's own RED code, while itself exiting 1 on an escaping throw,
88
+ * with no summary and, mid-bundle, a gate-authored green record left on disk.
89
+ * `runGates` now folds any escaping throw into `EXIT_GATE_ERROR` AND writes a
90
+ * summary marked aborted, so a consumer can always tell "a gate reported red"
91
+ * from "the runner died before it could report".
92
+ *
93
+ * M2-C-6 IS WIRED AT FOUR PLACES, all of them paths from outside: the
94
+ * manifest path (in `loadManifest`), the evidence directory, every
95
+ * `file-exists` and `file-absent` precondition target, and every gate's
96
+ * record file, which is probed BEFORE the gate is spawned as well as after
97
+ * it returns. Probing before the spawn is the load-bearing half: a gate
98
+ * handed a named pipe to write its record into would block in the kernel
99
+ * forever and the runner would wait on it forever, so the run has to be
100
+ * refused before the child exists.
101
+ */
102
+ export interface RunOptions {
103
+ manifestPath: string;
104
+ evidenceDir: string;
105
+ base?: string;
106
+ head?: string;
107
+ phase?: string;
108
+ only?: string[];
109
+ /**
110
+ * M3-P2 step 4. When true, `manifestPath` names a canonical GATE REGISTRY
111
+ * (`gate-registry.yaml`, `schemas/gate-registry.schema.json`) rather than an
112
+ * M2-P1 gate manifest. The registry is a SUPERSET of the manifest, so it is
113
+ * projected down to one and every line below this point is the M2 runner
114
+ * unchanged: the same `runOneGate`, the same `ingestGateRun`, the same
115
+ * `makeGateResult`. That is deliberate and it is what makes M2-C-2 and
116
+ * M2-C-3 survive the promotion instead of being re-implemented beside it.
117
+ */
118
+ registry?: boolean;
119
+ /**
120
+ * Registry mode only: the assurance mode selecting entries, matched against
121
+ * each entry's `modes[]`. Defaults to `full`.
122
+ */
123
+ mode?: string;
124
+ /** Working directory for gate subprocesses and git. Defaults to cwd. */
125
+ cwd?: string;
126
+ }
127
+ /**
128
+ * A registry entry that declares a gate the runner CANNOT execute, because
129
+ * D-11 settles that it is verified by a clean-room checklist probe rather
130
+ * than by a script (R-043, R-044). It is reported rather than dropped, so
131
+ * that "the report accounts for every gate in the registry" is a property a
132
+ * reader can check from the run's own output instead of taking on trust.
133
+ */
134
+ export interface DeclaredChecklistGate {
135
+ id: string;
136
+ probe: string;
137
+ applicability: "required" | "conditional";
138
+ }
139
+ export interface GateSummaryRow {
140
+ id: string;
141
+ status: GateStatus;
142
+ units: number;
143
+ unitLabel: string;
144
+ vacuous: boolean;
145
+ applicable: boolean;
146
+ detail: string;
147
+ record?: string;
148
+ stdout?: string;
149
+ stderr?: string;
150
+ }
151
+ export interface RunSummary {
152
+ /** Identity of THIS run. A bundle nobody can attribute is not evidence. */
153
+ runId: string;
154
+ manifest: string;
155
+ /** M3-P2: true when `manifest` above named a gate registry, not a manifest. */
156
+ registry?: boolean;
157
+ /** M3-P2: the assurance mode that selected these gates. Registry runs only. */
158
+ mode?: string;
159
+ /**
160
+ * M3-P2: registry entries selected by `mode` that the runner did not
161
+ * execute because they are `verified-by: clean-room-checklist`. Empty for a
162
+ * manifest run. `declared` in `counts` plus this array's length is the
163
+ * number of registry entries the mode selected, which is how a reader
164
+ * checks that the report accounts for every gate.
165
+ */
166
+ declaredByChecklist?: DeclaredChecklistGate[];
167
+ manifestSha256: string;
168
+ startedAt: string;
169
+ endedAt: string;
170
+ parameters: {
171
+ base?: string;
172
+ head?: string;
173
+ phase?: string;
174
+ };
175
+ only: string[];
176
+ manifestGates: number;
177
+ gates: GateSummaryRow[];
178
+ counts: {
179
+ declared: number;
180
+ applicable: number;
181
+ /** green + red: gates that reached a verdict. The anti-vacuity count. */
182
+ verdict: number;
183
+ green: number;
184
+ red: number;
185
+ "not-applicable": number;
186
+ error: number;
187
+ vacuous: number;
188
+ };
189
+ /** Named here as well as in the rows, because the reason line is one line. */
190
+ requiredNotApplicable: string[];
191
+ /** True when a throw escaped the run and this summary is a partial record. */
192
+ aborted: boolean;
193
+ exitCode: number;
194
+ reason: string;
195
+ }
196
+ export interface RunOutcome {
197
+ /**
198
+ * THIS run's identity, on every outcome including a refusal (CR-861). The
199
+ * runId used to live only inside `summary.json`, so a caller had no way to
200
+ * tell whether the summary it was reading was its own: after a refusal the
201
+ * previous run's green summary sat there, `aborted: false`, exit 0, with
202
+ * nothing to mark it stale. Attribution needs the caller to know the id it
203
+ * should expect, and this is where it gets it.
204
+ */
205
+ runId: string;
206
+ exitCode: number;
207
+ summary?: RunSummary;
208
+ /** Set when the run could not start at all (no summary to write). */
209
+ reason?: string;
210
+ }
211
+ export declare const NO_APPLICABLE_GATE = "no applicable gate";
212
+ /** The default assurance mode when `--registry` is given without `--mode`. */
213
+ export declare const DEFAULT_MODE = "full";
214
+ export type RegistryLoad = {
215
+ ok: true;
216
+ manifest: GateManifest;
217
+ sha256: string;
218
+ body: string;
219
+ declaredByChecklist: DeclaredChecklistGate[];
220
+ } | {
221
+ ok: false;
222
+ reason: string;
223
+ diagnostics: string[];
224
+ };
225
+ /**
226
+ * LOAD A CANONICAL GATE REGISTRY AND PROJECT IT ONTO AN M2 GATE MANIFEST
227
+ * (kernel plan M3, M3-P2 steps 2 and 4; R-094).
228
+ *
229
+ * The registry is a SUPERSET of the M2-P1 manifest, so the promotion is a
230
+ * projection and not a rewrite, and this function is the whole of it. Four
231
+ * things happen here and each one is load-bearing:
232
+ *
233
+ * 1. The path is operator-supplied, so it is READ through the delivered
234
+ * `readRegularFileIfPresent` (M2-C-6). A named pipe reports the observed
235
+ * type and never blocks.
236
+ * 2. The document is DECODED (YAML or JSON, `decodeDocument`) and then
237
+ * VALIDATED against `schemas/gate-registry.schema.json` through the
238
+ * M3-P1 Ajv engine. Decoding and validation are separate stages and
239
+ * produce distinguishable diagnostics (DR-0013 YAML clause 3). The
240
+ * registry schema is NOT validated by `src/gates/validate.ts`: it uses
241
+ * `if`/`then`, which is outside M2-D-04's closed keyword set, which is
242
+ * exactly why it lives in the shipped `schemas/` directory.
243
+ * 3. Entries are selected by MODE and by `verified-by`. A
244
+ * `clean-room-checklist` entry has no process to run (D-11: R-043 and
245
+ * R-044 are not computable from a diff), so it is reported as declared
246
+ * rather than silently dropped.
247
+ * 4. The projected manifest is validated AGAIN, against the M2 manifest
248
+ * schema, by `validateManifestDocument`. That second validation is the
249
+ * superset claim turned into a check: if a projection is not a valid M2
250
+ * manifest then the registry is not a superset of the manifest, and the
251
+ * run refuses rather than proceeding on a document the M2 contract does
252
+ * not recognise.
253
+ *
254
+ * Everything after this function is the M2 runner untouched, so M2-C-2 (never
255
+ * green by omission) and M2-C-3 (fail closed) apply to registry runs by
256
+ * CONSTRUCTION rather than by being remembered. A `--registry` path that
257
+ * built `GateResult` literals of its own would be the realistic way those two
258
+ * constraints get dropped by a promotion, and there is no such path.
259
+ */
260
+ export declare function loadRegistry(path: string, mode: string): RegistryLoad;
261
+ /**
262
+ * Which run parameters a gate cannot be evaluated without.
263
+ *
264
+ * Derived from two places and nowhere else: the gate's own `parameters`
265
+ * declaration, and the precondition kind, whose needs are a property of the
266
+ * kind rather than of the gate. `branch-matches` requires `--phase`
267
+ * unconditionally rather than only when the pattern happens to interpolate
268
+ * it, which is stricter than necessary and deliberately so: M2-C-3 says a
269
+ * check that cannot reach a verdict fails closed, and the cost of the strict
270
+ * reading is one flag on an invocation that already carries three.
271
+ */
272
+ export declare function requiredParameters(entry: GateEntry): RunParameter[];
273
+ /**
274
+ * A CRASH IS NOT A SKIP (M3-P11, delivery/plan/m3-p11-phase-spec.md:15).
275
+ *
276
+ * THE DEFECT THIS EXISTS FOR, root-caused by the M3-P9 hazard reviewer. The
277
+ * `command-exit-zero` evaluator below treated a command as "could not run"
278
+ * only when `spawnSync` set `error`, which is the LAUNCHER failing to spawn.
279
+ * A launcher that spawns perfectly well and runs a script that does not
280
+ * exist gets exit 1, which the evaluator read as "the precondition is
281
+ * unmet", so the gate printed `not-applicable`. Measured on this machine,
282
+ * node v26.6.0, four commands, one `spawnSync` each:
283
+ *
284
+ * node missing-script.mjs status 1 error undefined
285
+ * node -e "process.exit(1)" status 1 error undefined
286
+ * ./noexec.sh (mode 644) status null error EACCES
287
+ * ./badinterp.sh (bad shebang) status null error ENOENT
288
+ *
289
+ * Rows one and two are the whole problem: identical to `spawnSync`, opposite
290
+ * in meaning. Row two is a REAL declaration in this repository
291
+ * (`credential-token`'s precondition, gates.manifest.json), so "exit 1 means
292
+ * unmet" cannot simply be withdrawn; and rows three and four already reached
293
+ * `error` before this phase, so they are regression guards rather than the
294
+ * new behaviour.
295
+ *
296
+ * THE DISTINGUISHING EVIDENCE IS THEREFORE NOT THE EXIT CODE (spec step 2).
297
+ * It is whether the paths the command names can be run at all, established
298
+ * BEFORE the spawn, so that "could not run" is a fact about the filesystem
299
+ * rather than an inference from a number that two different situations
300
+ * produce identically.
301
+ *
302
+ * WHICH ARGV ELEMENTS ARE PROBED, stated as a rule rather than left to
303
+ * judgment, because MECHANISMS.md's row about deciding what another program
304
+ * will do by pattern-matching its input is exactly the trap here. An element
305
+ * is a PATH OPERAND when ALL FOUR hold:
306
+ *
307
+ * 1. it is not `command[0]` (the launcher, handled separately);
308
+ * 2. `namesNoPath` does not rule it out (it is not an option, not the code
309
+ * value of one, and not a URL). Fix round 2 hoisted this into its own
310
+ * function so this rule and the wider one below cannot drift apart.
311
+ * 3. it contains `/` (a bare word is not treated as a path HERE, so `.` and
312
+ * `--pin-root src` are left alone; the wider rule below adds the one
313
+ * further test that makes `node check.mjs` visible);
314
+ * 4. it contains no whitespace, and the element before it does not begin
315
+ * with `-` (so an option's VALUE is never probed by THIS rule, whatever
316
+ * the option is).
317
+ *
318
+ * WHAT THIS RULE DOES NOT COVER, so the next reader does not have to
319
+ * rediscover it: an interpreter invoked as `node --flag script.mjs` puts the
320
+ * script after an option and rule 4 skips it; an operand named by an
321
+ * environment variable or produced by a shell is invisible here; and a
322
+ * script that EXISTS but whose own body throws still exits 1 and is still
323
+ * read as unmet, because nothing outside the script can distinguish that
324
+ * from a deliberate refusal. Those residues are recorded in
325
+ * delivery/work-history/m3-p11.md rather than implied.
326
+ *
327
+ * FAIL CLOSED, LOUDLY (M2-C-3). An operand this rule probes and does not
328
+ * find is `error` naming the element and its resolved absolute path, never a
329
+ * quiet `not-applicable`. That is the right direction HERE, and only because
330
+ * rules 2 to 4 keep the set small: this probe runs before the spawn, so a
331
+ * false positive refuses a command that would have exited 0, and no exit code
332
+ * exists yet to tell you it would have. Round 2 hit exactly that with a URL
333
+ * operand.
334
+ *
335
+ * ------------------------------------------------------------------------
336
+ * FIX ROUND 1 (M3-P11), AND THE MECHANISM IT CLOSES.
337
+ *
338
+ * As first written, the paragraphs above established SOME of the
339
+ * preconditions of running and then let the exit code decide. That is one
340
+ * mechanism with two independent halves, and a clean-room hazard reviewer
341
+ * reproduced BOTH of them end to end through the packed CLI as a wrong
342
+ * verdict (`not-applicable` for a command that crashed), which is the exact
343
+ * defect this phase exists to close, surviving inside its own fix:
344
+ *
345
+ * HALF A, the CONDITIONS tested per examined element were incomplete.
346
+ * `classifyEntry` answers "does it exist and is it a regular file". It
347
+ * does not answer "may this process OPEN it". A `chmod 000` script is
348
+ * present and regular, the probe passed it, `node` launched fine (so
349
+ * `spawnSync.error` stayed undefined), the open failed with EACCES, the
350
+ * exit was 1, and 1 meant unmet. Existence and type are two of the
351
+ * conditions for an open; permission is the third and it was untested.
352
+ *
353
+ * HALF B, the SET of elements examined was a proper subset of the
354
+ * path-shaped ones. Rules 2 and 4 above skip an operand that follows an
355
+ * option (`node --flag script.mjs`) and one carrying whitespace, and both
356
+ * were confirmed to produce the same wrong verdict.
357
+ *
358
+ * HALF A IS CLOSED OUTRIGHT, by asking the complete question. A path can be
359
+ * opened for reading exactly when `access(R_OK)` succeeds, which resolves
360
+ * every component's traversal permission and the file's own mode in one
361
+ * call, FOR THE CALLING PROCESS. That last clause is the point: the process
362
+ * that runs this probe is the process that will spawn the command, so the
363
+ * calling UID is the right UID to ask about, and `access` is therefore the
364
+ * correct primitive here even though the pre-existing executable check
365
+ * deliberately reads mode bits instead (that check wants a UID-independent
366
+ * answer; this one wants a UID-dependent one, and both are now applied to
367
+ * the launcher).
368
+ *
369
+ * HALF B IS NOT CLOSED BY WIDENING THIS RULE. This rule hard-refuses BEFORE
370
+ * the spawn, so a false positive here breaks a precondition that would have
371
+ * exited 0, and that is a worse direction to be wrong in than any silent skip.
372
+ * Half B is closed by a SECOND, wider rule (`commandPathCandidates` below)
373
+ * whose result is consulted only when the exit is nonzero, so an exit of 0
374
+ * remains its own proof and no working declaration can be affected.
375
+ *
376
+ * ------------------------------------------------------------------------
377
+ * FIX ROUND 2 (M3-P11) CORRECTED THAT SECOND RULE IN TWO WAYS, and the
378
+ * sentence round 1 wrote here is the one that had to go. Round 1 argued that
379
+ * after a nonzero exit a deliberately OVER-INCLUSIVE scan is the safe
380
+ * direction, because its false positive is only a loud `error` an operator
381
+ * can read. A delta verifier measured that trade and it does not hold:
382
+ * `decideAggregate` checks `counts.error > 0` before anything else, so one
383
+ * false error on one conditional gate fails the WHOLE bundle. The two
384
+ * corrections are documented on `commandPathCandidates` (which elements) and
385
+ * on `attributionGaps` (which moment).
386
+ */
387
+ export interface CommandRunnability {
388
+ /** False when the command could not have run: this is `error`, not unmet. */
389
+ runnable: boolean;
390
+ /** Why not. Empty when runnable. */
391
+ reason: string;
392
+ /** Every element this probe examined, so its scope is data, not a claim. */
393
+ probed: string[];
394
+ }
395
+ /**
396
+ * The path operands of a command, by the four-part rule documented above,
397
+ * MINUS the elements `namesNoPath` rules out. This is the STRICT set: it is
398
+ * probed before the spawn and a failure here is a hard `error`, so it stays
399
+ * conservative and keeps the whitespace and after-an-option guards that the
400
+ * wider rule drops.
401
+ */
402
+ export declare function commandPathOperands(command: string[]): string[];
403
+ /**
404
+ * Every argv element that names a PATH THE COMMAND NEEDED, used only to decide
405
+ * whether a NONZERO exit is attributable to the command's own logic.
406
+ *
407
+ * ------------------------------------------------------------------------
408
+ * FIX ROUND 2 (M3-P11) REWROTE THIS RULE, AND WHY IS THE WHOLE POINT.
409
+ *
410
+ * Fix round 1 defined it as "contains `/`", at any position, launcher
411
+ * included. A delta verifier measured what that costs
412
+ * (delivery/verification/m3-p11-fix-round-1.md, findings 2a and 2b) and the
413
+ * cost is larger than round 1's own note conveyed: `decideAggregate` checks
414
+ * `counts.error > 0` FIRST, so ONE false error on ONE conditional gate fails
415
+ * the ENTIRE bundle. An honest, correctly written precondition could take a
416
+ * consumer's whole delivery down. That is a worse failure than the silent
417
+ * skip this phase set out to abolish, because a silent skip is wrong and
418
+ * quiet while this is wrong and total.
419
+ *
420
+ * THE MECHANISM: "contains a slash" was being used as a proxy for "is a path
421
+ * operand", and it is neither necessary nor sufficient.
422
+ *
423
+ * NOT SUFFICIENT: inline code (`process.exit(existsSync("/marker")?0:1)`),
424
+ * a URL, an `--opt=/value` pair, a date (`2026/08/14`), a regex and plain
425
+ * division all contain `/` and none of them is a path.
426
+ *
427
+ * NOT NECESSARY: `node check.mjs` names a real script with no `/` in it.
428
+ *
429
+ * So the rule now tests four things instead of one. An element at index >= 1
430
+ * is a path this command needed when ALL FOUR hold:
431
+ *
432
+ * 1. it does not itself begin with `-`. An option is not an operand, and
433
+ * `--out=/tmp/x` is an option carrying a value, not a path: probing the
434
+ * whole element was a GUARANTEED false error for every `--opt=/path`
435
+ * form, since no file is ever named `--out=/tmp/x`.
436
+ * 2. the element before it is not in `CODE_VALUED_OPTIONS`. This is the
437
+ * `node -e` case, and it is the one measured in finding 2a.
438
+ * 3. it is not URL-shaped.
439
+ * 4. it either contains `/`, or it carries a `SCRIPT_SUFFIXES` suffix and
440
+ * no whitespace. The second disjunct is new in round 2 and is what
441
+ * closes the bare-operand residue.
442
+ *
443
+ * The launcher (index 0) is deliberately NOT in this set. It is already
444
+ * probed, with the executable conditions on top, by `probeCommandRunnable`
445
+ * before the spawn, so including it here only duplicated that work.
446
+ *
447
+ * WHAT THIS RULE STILL GETS WRONG, stated rather than left to be discovered.
448
+ * A FALSE ERROR remains reachable for an element that is not a path, is not
449
+ * an option's value, and either contains `/` or ends in a script suffix: a
450
+ * bare date operand (`mytool 2026/08/14`), an operand-position regex, and a
451
+ * value passed to an option that takes a non-path value NOT in
452
+ * `CODE_VALUED_OPTIONS` (`awk -v expr=a/b`). A SILENT SKIP remains reachable
453
+ * for an operand with no `/` and no known suffix (`node check`, an
454
+ * extensionless script), for a path named through an environment variable or
455
+ * produced by a shell, and for an `--opt=/path` pair, which rule 1 now
456
+ * declines to probe. Both lists are shorter than round 1's; neither is empty.
457
+ * Full accounting, with the enumeration that produced it, in
458
+ * delivery/work-history/m3-p11.md.
459
+ */
460
+ export declare function commandPathCandidates(command: string[]): string[];
461
+ /**
462
+ * Can this command run at all? See the block comment above for the rule and
463
+ * for what it deliberately does not cover.
464
+ *
465
+ * The launcher is probed only when it names a path (contains `/`); a bare
466
+ * name is a PATH lookup and is left to `spawnSync`'s own `error`, which
467
+ * already reports it, rather than reimplementing PATH resolution here.
468
+ */
469
+ export declare function probeCommandRunnable(command: string[], cwd: string): CommandRunnability;
470
+ /**
471
+ * Every path-shaped argv element this process cannot open, with the reason.
472
+ * Empty means a nonzero exit is ATTRIBUTABLE to the command's own logic;
473
+ * non-empty means it is not, and M2-C-3 says a check that cannot reach a
474
+ * verdict fails closed rather than guessing one.
475
+ *
476
+ * FIX ROUND 2 MOVED THE CALL SITE, and the move is the fix for a second
477
+ * mechanism, independent of which elements are scanned. Round 1 ran this
478
+ * AFTER the spawn, on the nonzero arm only. The question it answers is "did
479
+ * the command have what it needed IN ORDER TO RUN", which is a question about
480
+ * the moment BEFORE the spawn, and answering it from the filesystem AFTER the
481
+ * spawn reads the command's own effects back as evidence about its inputs. A
482
+ * precondition script that legitimately decides "unmet" and deletes itself as
483
+ * its last act (a one-shot or bootstrap script) was therefore reported
484
+ * `error`, deterministically, no timing window needed: measured in
485
+ * delivery/verification/m3-p11-fix-round-1.md as finding 2b.
486
+ *
487
+ * So the scan now runs BEFORE the spawn and its result is CARRIED. The exit
488
+ * code still decides whether the result is consulted: an exit of 0 is its own
489
+ * proof that the command ran, so no declaration that succeeds can be affected
490
+ * by this at all, which is the property round 1 established and round 2 keeps.
491
+ */
492
+ export declare function attributionGaps(command: string[], cwd: string): string[];
493
+ export declare const M2_C_5_DETAIL = "M2-C-5 (a run that cannot name what it executed is not evidence)";
494
+ /**
495
+ * Run the manifest's gates. Returns the aggregate exit code and, when the
496
+ * run got far enough to have one, the summary that was written.
497
+ */
498
+ export interface AggregateCounts {
499
+ declared: number;
500
+ applicable: number;
501
+ verdict: number;
502
+ green: number;
503
+ red: number;
504
+ "not-applicable": number;
505
+ error: number;
506
+ vacuous: number;
507
+ }
508
+ /**
509
+ * THE ONE AGGREGATE DECISION, extracted so it can be EXERCISED rather than
510
+ * read (CR-800's fix round). CR-800 was a reading of these branch conditions
511
+ * that turned out not to hold, and the first test written for the fix
512
+ * asserted on the TEXT of this function, which is the guard-that-asserts-text
513
+ * class MECHANISMS.md records six instances of. A pure function over a counts
514
+ * object can be handed states the runner cannot currently produce, including
515
+ * the internally inconsistent ones the invariants below exist for, so the
516
+ * invariants are witnessed instead of quoted.
517
+ *
518
+ * AGGREGATE PRECEDENCE, fixed here so it is one rule and not a reading. A
519
+ * concrete failure outranks the vacuity check, because "3 gates reported
520
+ * error" tells the operator more than "no applicable gate" and both exit 21
521
+ * anyway. The vacuity check outranks a required not-applicable gate, because
522
+ * a bundle that examined nothing is not a report about any one gate (M2-C-2
523
+ * at the aggregate level, M2R-012).
524
+ */
525
+ export declare function decideAggregate(counts: AggregateCounts, requiredNotApplicable: string[], rows: {
526
+ id: string;
527
+ status: GateStatus;
528
+ }[]): {
529
+ exitCode: number;
530
+ reason: string;
531
+ };
532
+ export declare const RUN_CLAIM_FILE = ".tiphys-gate-run.json";
533
+ /**
534
+ * RELEASE ONLY WHAT THIS RUN HOLDS (CR-860).
535
+ *
536
+ * The mechanism the finding names is: *cleanup that is valid only while the
537
+ * claim is held, performed from a frame that does not know whether the claim
538
+ * is held*. The instance was a release in an inner `finally` followed by a
539
+ * second, unconditional release in an outer `catch`, so a crashed run could
540
+ * unlink a claim that by then belonged to a DIFFERENT run, revoking a live
541
+ * run's exclusion. That is "release a lock you no longer hold", the classic
542
+ * claim-file defect.
543
+ *
544
+ * Two things close it, and both are here because the line deletion alone is
545
+ * the instance fix and this project fixes the mechanism:
546
+ *
547
+ * 1. THIS function reads the claim and unlinks ONLY when the runId is its
548
+ * own, which is what `src/lock.ts` does when it verifies holdership
549
+ * before mutating. A release from a frame that no longer holds the claim
550
+ * is then a no-op rather than a revocation, whatever the call graph does.
551
+ * 2. The call graph is also fixed, in `runGates`: exactly one release, in
552
+ * one `finally`, after every write into the directory. Depending on the
553
+ * guard alone would leave the writes happening outside the claimed
554
+ * region, which is the other half of the same finding.
555
+ *
556
+ * A claim this run does not hold is deliberately LEFT IN PLACE. Deleting
557
+ * another run's claim is the harm; leaving a stranded one costs a human one
558
+ * `rm`, and the refusal text says which file and why.
559
+ */
560
+ export declare function releaseEvidenceDirectory(evidenceDir: string, runId: string): boolean;
561
+ /**
562
+ * The public entry. It exists so that NO throw can escape the runner and be
563
+ * read as this phase's RED exit code by whatever consumes it (CR-801). The
564
+ * runner enforces exactly this rule on its gates; it now obeys it itself.
565
+ */
566
+ export declare function runGates(options: RunOptions): RunOutcome;