@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.
- package/AGENTS.md +611 -0
- package/LICENSE +202 -0
- package/assurance-modes.yaml +278 -0
- package/checklists/clean-room.yaml +325 -0
- package/checklists/env-failure-diagnosis.yaml +68 -0
- package/checklists/flake-playbook.yaml +68 -0
- package/checklists/hazard-review.yaml +144 -0
- package/checklists/plan-review.yaml +103 -0
- package/dist/bin/tiphys.d.ts +2 -0
- package/dist/bin/tiphys.js +14 -0
- package/dist/src/brief.d.ts +32 -0
- package/dist/src/brief.js +59 -0
- package/dist/src/checklists.d.ts +169 -0
- package/dist/src/checklists.js +310 -0
- package/dist/src/checks.d.ts +828 -0
- package/dist/src/checks.js +3314 -0
- package/dist/src/cli.d.ts +9 -0
- package/dist/src/cli.js +57 -0
- package/dist/src/commands/brief.d.ts +92 -0
- package/dist/src/commands/brief.js +320 -0
- package/dist/src/commands/checklist.d.ts +42 -0
- package/dist/src/commands/checklist.js +168 -0
- package/dist/src/commands/doctor.d.ts +35 -0
- package/dist/src/commands/doctor.js +605 -0
- package/dist/src/commands/gates.d.ts +9 -0
- package/dist/src/commands/gates.js +360 -0
- package/dist/src/commands/init.d.ts +30 -0
- package/dist/src/commands/init.js +128 -0
- package/dist/src/commands/lock.d.ts +1 -0
- package/dist/src/commands/lock.js +229 -0
- package/dist/src/commands/mode.d.ts +40 -0
- package/dist/src/commands/mode.js +134 -0
- package/dist/src/commands/plan.d.ts +20 -0
- package/dist/src/commands/plan.js +105 -0
- package/dist/src/commands/pool.d.ts +1 -0
- package/dist/src/commands/pool.js +128 -0
- package/dist/src/commands/spawn.d.ts +1 -0
- package/dist/src/commands/spawn.js +146 -0
- package/dist/src/commands/status.d.ts +19 -0
- package/dist/src/commands/status.js +120 -0
- package/dist/src/commands/teardown.d.ts +1 -0
- package/dist/src/commands/teardown.js +79 -0
- package/dist/src/commands/tuition.d.ts +21 -0
- package/dist/src/commands/tuition.js +218 -0
- package/dist/src/commands/validate.d.ts +78 -0
- package/dist/src/commands/validate.js +360 -0
- package/dist/src/commands/watch.d.ts +1 -0
- package/dist/src/commands/watch.js +172 -0
- package/dist/src/exec/env.d.ts +128 -0
- package/dist/src/exec/env.js +190 -0
- package/dist/src/fleet.d.ts +51 -0
- package/dist/src/fleet.js +80 -0
- package/dist/src/gates/adapters/http-json.d.ts +5 -0
- package/dist/src/gates/adapters/http-json.js +283 -0
- package/dist/src/gates/adapters/migrations-command.d.ts +1 -0
- package/dist/src/gates/adapters/migrations-command.js +373 -0
- package/dist/src/gates/citations.d.ts +408 -0
- package/dist/src/gates/citations.js +1163 -0
- package/dist/src/gates/coverage.d.ts +284 -0
- package/dist/src/gates/coverage.js +701 -0
- package/dist/src/gates/credentials.d.ts +74 -0
- package/dist/src/gates/credentials.js +533 -0
- package/dist/src/gates/deploy.d.ts +1 -0
- package/dist/src/gates/deploy.js +33 -0
- package/dist/src/gates/manifest.d.ts +99 -0
- package/dist/src/gates/manifest.js +208 -0
- package/dist/src/gates/migrations.d.ts +1 -0
- package/dist/src/gates/migrations.js +36 -0
- package/dist/src/gates/pin.d.ts +114 -0
- package/dist/src/gates/pin.js +154 -0
- package/dist/src/gates/red-witness.d.ts +22 -0
- package/dist/src/gates/red-witness.js +390 -0
- package/dist/src/gates/release.d.ts +283 -0
- package/dist/src/gates/release.js +820 -0
- package/dist/src/gates/result.d.ts +116 -0
- package/dist/src/gates/result.js +91 -0
- package/dist/src/gates/run.d.ts +566 -0
- package/dist/src/gates/run.js +1536 -0
- package/dist/src/gates/schemas/citation-config.schema.json +59 -0
- package/dist/src/gates/schemas/coverage-config.schema.json +77 -0
- package/dist/src/gates/schemas/gate-manifest.schema.json +125 -0
- package/dist/src/gates/schemas/gate-result.schema.json +160 -0
- package/dist/src/gates/schemas/phase-declaration.schema.json +42 -0
- package/dist/src/gates/schemas/release-record.schema.json +119 -0
- package/dist/src/gates/schemas/verifier-config.schema.json +101 -0
- package/dist/src/gates/schemas/witness-spec.schema.json +110 -0
- package/dist/src/gates/scope.d.ts +131 -0
- package/dist/src/gates/scope.js +1018 -0
- package/dist/src/gates/suite.d.ts +217 -0
- package/dist/src/gates/suite.js +927 -0
- package/dist/src/gates/validate.d.ts +121 -0
- package/dist/src/gates/validate.js +414 -0
- package/dist/src/hooks.d.ts +32 -0
- package/dist/src/hooks.js +62 -0
- package/dist/src/liveness.d.ts +321 -0
- package/dist/src/liveness.js +396 -0
- package/dist/src/lock.d.ts +178 -0
- package/dist/src/lock.js +500 -0
- package/dist/src/modes.d.ts +149 -0
- package/dist/src/modes.js +258 -0
- package/dist/src/path-identity.d.ts +2 -0
- package/dist/src/path-identity.js +10 -0
- package/dist/src/plan.d.ts +73 -0
- package/dist/src/plan.js +153 -0
- package/dist/src/pool.d.ts +130 -0
- package/dist/src/pool.js +721 -0
- package/dist/src/roles.d.ts +430 -0
- package/dist/src/roles.js +734 -0
- package/dist/src/spawn.d.ts +177 -0
- package/dist/src/spawn.js +332 -0
- package/dist/src/status.d.ts +91 -0
- package/dist/src/status.js +119 -0
- package/dist/src/task.d.ts +264 -0
- package/dist/src/task.js +305 -0
- package/dist/src/teardown.d.ts +32 -0
- package/dist/src/teardown.js +314 -0
- package/dist/src/tuition.d.ts +159 -0
- package/dist/src/tuition.js +311 -0
- package/dist/src/validate.d.ts +230 -0
- package/dist/src/validate.js +732 -0
- package/dist/src/version.d.ts +3 -0
- package/dist/src/version.js +38 -0
- package/dist/src/watcher.d.ts +275 -0
- package/dist/src/watcher.js +859 -0
- package/dist/src/witness/run.d.ts +274 -0
- package/dist/src/witness/run.js +1327 -0
- package/dist/src/witness/spec.d.ts +102 -0
- package/dist/src/witness/spec.js +253 -0
- package/dist/tsconfig.src.tsbuildinfo +1 -0
- package/gate-registry.yaml +390 -0
- package/gates.manifest.json +195 -0
- package/package.json +57 -3
- package/role-model-config.yaml +88 -0
- package/roles/README.md +128 -0
- package/roles/_shared-dispatch-contract.md +87 -0
- package/roles/adversarial-plan-reviewer.md +80 -0
- package/roles/clean-room-reviewer.md +140 -0
- package/roles/implementer.md +460 -0
- package/roles/investigator.md +138 -0
- package/roles/plan-writer.md +95 -0
- package/schemas/README.md +81 -0
- package/schemas/assurance-modes.schema.json +264 -0
- package/schemas/charter.schema.json +166 -0
- package/schemas/checklist.schema.json +114 -0
- package/schemas/decision-record.schema.json +88 -0
- package/schemas/final-report.schema.json +90 -0
- package/schemas/finding.schema.json +106 -0
- package/schemas/gate-registry.schema.json +260 -0
- package/schemas/mechanism-index.schema.json +94 -0
- package/schemas/plan.schema.json +300 -0
- package/schemas/report.schema.json +579 -0
- package/schemas/role-brief.schema.json +105 -0
- package/schemas/role-model-config.schema.json +90 -0
- package/schemas/status-line.schema.json +40 -0
- package/schemas/tuition.schema.json +191 -0
- package/schemas/verdict.schema.json +289 -0
- package/schemas/work-history.schema.json +183 -0
- package/templates/charter.example.yaml +54 -0
- package/templates/decision-record.example.yaml +27 -0
- package/templates/final-report.example.yaml +80 -0
- package/templates/plan.example.yaml +87 -0
- package/templates/report.example.yaml +236 -0
- package/templates/warnings.md +74 -0
- package/templates/work-history.example.yaml +185 -0
- package/tuition/README.md +76 -0
- package/tuition/T-001.yaml +48 -0
- package/tuition/T-002.yaml +51 -0
- package/tuition/T-003.yaml +100 -0
- package/tuition/T-004.yaml +52 -0
- package/tuition/T-005.yaml +72 -0
- package/tuition/T-006.yaml +81 -0
- package/tuition/T-007.yaml +56 -0
- package/tuition/T-008.yaml +111 -0
- package/tuition/T-009.yaml +50 -0
- package/tuition/T-015.yaml +36 -0
- package/tuition/T-016.yaml +36 -0
- package/tuition/T-017.yaml +46 -0
- package/tuition/T-018.yaml +84 -0
- package/tuition/T-021.yaml +40 -0
- package/tuition/T-022.yaml +36 -0
- 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;
|