@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,390 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { join, relative, resolve } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { refuseOpenForWrite, singleLine } from "../task.js";
5
+ import { loadManifest } from "./manifest.js";
6
+ import { exitCodeForStatus, makeGateResult, renderGateResult } from "./result.js";
7
+ import { listWitnessSpecFiles, loadWitnessSpec, memberTouchedFiles, } from "../witness/spec.js";
8
+ import { SPAWN_GREP, computePhaseDiff, evaluateWitness, gitIn, makeScratchRoot, readTestFilesAtHead, removeScratchRoot, resolveRepoRoot, shellSpawnsAndParses, } from "../witness/run.js";
9
+ /**
10
+ * THE RED-WITNESS GATE (kernel plan M2, M2-P2 steps 6 and 7).
11
+ *
12
+ * Registered in gates.manifest.json as `red-witness`, applicability
13
+ * `required`, precondition `diff-touches` on `src/` and `bin/`, unitLabel
14
+ * `witnesses evaluated`. Invoked by the runner as
15
+ *
16
+ * node src/gates/red-witness.ts --result <path> --evidence <dir>
17
+ * --base <ref> --head <ref>
18
+ *
19
+ * plus `--baseline <ref>` when invoked directly: merge-time re-verification
20
+ * is a PARAMETER, not an enforcement (M2-D-08); it defaults to `--base`.
21
+ *
22
+ * WHAT THE GATE DECIDES.
23
+ * - Every witness spec changed in the phase diff (the phase's OWN
24
+ * witnesses) is evaluated by the harness.
25
+ * - Every STORED witness any of whose dangerous-state members touches a
26
+ * changed file is re-evaluated; one now green against any of its own
27
+ * members is red with reason "witness no longer guards its behavior"
28
+ * naming the witness and the measured rate (M2R-002, the N-401 shape).
29
+ * - A changed source file under src/ or bin/ with no witness spec
30
+ * covering it is RED, never not-applicable (step 7).
31
+ * - `--base` absent is `error` (M2-C-3).
32
+ *
33
+ * DEPTH REQUIREMENT, documented rather than assumed (STATE.md CR-902
34
+ * carry-forward): the gate needs `--base` and `--head` resolvable with
35
+ * history (the diff is base...head, a merge-base diff) and an UNSHALLOW
36
+ * repository, because the harness scratch-clones it and git refuses to
37
+ * clone from a shallow source. Both are satisfied by `fetch-depth: 0` on
38
+ * the CI checkout, which is owned by the workflow's owner, not this phase.
39
+ * A shallow repository is `error` naming the requirement. On pull_request
40
+ * events the checkout HEAD is a synthetic merge commit, so the audited
41
+ * head is always taken from `--head`, never from the checkout.
42
+ */
43
+ const USAGE = "usage: node src/gates/red-witness.ts --result <path> --evidence <dir> " +
44
+ "--base <ref> [--head <ref>] [--baseline <ref>] [--phase <id>]";
45
+ function parseArgs(argv) {
46
+ const options = {};
47
+ const known = new Map([
48
+ ["--result", "result"],
49
+ ["--evidence", "evidence"],
50
+ ["--base", "base"],
51
+ ["--head", "head"],
52
+ ["--baseline", "baseline"],
53
+ ["--phase", "phase"],
54
+ ]);
55
+ for (let index = 0; index < argv.length; index += 2) {
56
+ const flag = argv[index];
57
+ const field = known.get(flag);
58
+ if (field === undefined) {
59
+ return { usageError: `unknown option ${flag}` };
60
+ }
61
+ const value = argv[index + 1];
62
+ if (value === undefined || value.startsWith("--")) {
63
+ return { usageError: `${flag} requires a value` };
64
+ }
65
+ options[field] = value;
66
+ }
67
+ return { options };
68
+ }
69
+ function now() {
70
+ return new Date().toISOString();
71
+ }
72
+ function errorOutcome(startedAt, detail) {
73
+ const result = makeGateResult({
74
+ gate: "red-witness",
75
+ status: "error",
76
+ units: 0,
77
+ unitLabel: "witnesses evaluated",
78
+ startedAt,
79
+ endedAt: now(),
80
+ detail,
81
+ });
82
+ return {
83
+ result,
84
+ exitCode: exitCodeForStatus(result.status),
85
+ evaluations: [],
86
+ reEvaluationMs: 0,
87
+ };
88
+ }
89
+ /** True when the repo-relative path is a phase-audited source path. */
90
+ function isAuditedSource(path) {
91
+ return path.startsWith("src/") || path.startsWith("bin/");
92
+ }
93
+ /**
94
+ * Run the red-witness gate against a repository. Exported so tests can
95
+ * drive it without a subprocess; the CLI below is a thin shell over it.
96
+ */
97
+ export function runRedWitnessGate(run) {
98
+ const startedAt = now();
99
+ const rootProbe = resolveRepoRoot(run.repoRoot);
100
+ if (rootProbe.root === undefined) {
101
+ return errorOutcome(startedAt, rootProbe.reason);
102
+ }
103
+ const repoRoot = rootProbe.root;
104
+ const shallow = gitIn(repoRoot, ["rev-parse", "--is-shallow-repository"]);
105
+ if (!shallow.ok) {
106
+ return errorOutcome(startedAt, shallow.reason);
107
+ }
108
+ if (shallow.stdout.trim() === "true") {
109
+ return errorOutcome(startedAt, "this repository is a shallow clone; the red-witness gate requires " +
110
+ "full history (fetch-depth: 0) because the phase diff is a " +
111
+ "merge-base diff and the harness scratch-clones the repository, " +
112
+ "which git refuses from a shallow source");
113
+ }
114
+ const diffOutcome = computePhaseDiff(repoRoot, run.base, run.head ?? "HEAD");
115
+ if (!diffOutcome.ok) {
116
+ return errorOutcome(startedAt, diffOutcome.reason);
117
+ }
118
+ const diff = diffOutcome.diff;
119
+ const manifestPath = join(repoRoot, "gates.manifest.json");
120
+ const manifest = loadManifest(manifestPath);
121
+ if (!manifest.ok) {
122
+ return errorOutcome(startedAt, `the destructiveCommands list could not be read (rule (e) needs it): ` +
123
+ `${manifest.reason}${manifest.diagnostics.length > 0 ? `: ${manifest.diagnostics.join("; ")}` : ""}`);
124
+ }
125
+ const behaviorsShown = gitIn(repoRoot, [
126
+ "show",
127
+ `${diff.headSha}:test/behaviors.json`,
128
+ ]);
129
+ if (!behaviorsShown.ok) {
130
+ return errorOutcome(startedAt, `test/behaviors.json could not be read at the audited head: ${behaviorsShown.reason}`);
131
+ }
132
+ let behaviors;
133
+ try {
134
+ behaviors = new Set(Object.keys(JSON.parse(behaviorsShown.stdout)));
135
+ }
136
+ catch (error) {
137
+ return errorOutcome(startedAt, `test/behaviors.json does not parse at the audited head: ${singleLine(String(error))}`);
138
+ }
139
+ const testFilesOutcome = readTestFilesAtHead(repoRoot, diff.headSha);
140
+ if (!testFilesOutcome.ok) {
141
+ return errorOutcome(startedAt, testFilesOutcome.reason);
142
+ }
143
+ // Rule (f)'s derivation: the spawn grep over the changed files' head
144
+ // contents (deleted files have no head content and cannot be touched by a
145
+ // member either). Shell scripts (*.sh) are screened by the shell
146
+ // spawn-and-parse derivation instead of the JS token grep (CR-H2), so a
147
+ // bin/ shell script that classifies another program's output (the V-2
148
+ // shape) is not invisible to the capture obligation.
149
+ const spawningChangedFiles = [];
150
+ for (const [path, file] of diff.files) {
151
+ if (file.status === "D") {
152
+ continue;
153
+ }
154
+ const shown = gitIn(repoRoot, ["show", `${diff.headSha}:${path}`]);
155
+ if (!shown.ok) {
156
+ continue;
157
+ }
158
+ const spawns = path.endsWith(".sh")
159
+ ? shellSpawnsAndParses(shown.stdout)
160
+ : SPAWN_GREP.test(shown.stdout);
161
+ if (spawns) {
162
+ spawningChangedFiles.push(path);
163
+ }
164
+ }
165
+ spawningChangedFiles.sort();
166
+ const witnessDir = join(repoRoot, "witness");
167
+ const listing = listWitnessSpecFiles(witnessDir);
168
+ if (!listing.ok) {
169
+ return errorOutcome(startedAt, listing.reason);
170
+ }
171
+ const reasons = [];
172
+ const specs = [];
173
+ const seenIds = new Map();
174
+ for (const path of listing.paths) {
175
+ const loaded = loadWitnessSpec(path);
176
+ const repoRelative = relative(repoRoot, path).split("\\").join("/");
177
+ if (!loaded.ok) {
178
+ reasons.push(`${loaded.reason}${loaded.diagnostics.length > 0 ? `: ${loaded.diagnostics.join("; ")}` : ""}`);
179
+ continue;
180
+ }
181
+ const previous = seenIds.get(loaded.spec.id);
182
+ if (previous !== undefined) {
183
+ reasons.push(`witness id ${loaded.spec.id} is declared by both ${previous} and ${repoRelative}`);
184
+ continue;
185
+ }
186
+ seenIds.set(loaded.spec.id, repoRelative);
187
+ specs.push({ spec: loaded.spec, path, repoRelative });
188
+ }
189
+ const readPatchAtHead = (patchPath) => {
190
+ const shown = gitIn(repoRoot, ["show", `${diff.headSha}:${patchPath}`]);
191
+ return shown.ok ? shown.stdout : undefined;
192
+ };
193
+ const own = specs.filter((entry) => diff.files.has(entry.repoRelative));
194
+ const stored = specs.filter((entry) => !diff.files.has(entry.repoRelative));
195
+ const triggeredStored = stored.filter((entry) => entry.spec.dangerousStates.some((member) => memberTouchedFiles(member, readPatchAtHead).some((file) => diff.files.has(file))));
196
+ // Coverage (step 7): source changed with no witness spec covering it is
197
+ // red, never not-applicable. Coverage semantics are decision D-P2-2 in
198
+ // the work history.
199
+ const covered = new Set();
200
+ for (const entry of specs) {
201
+ for (const member of entry.spec.dangerousStates) {
202
+ for (const file of memberTouchedFiles(member, readPatchAtHead)) {
203
+ covered.add(file);
204
+ }
205
+ }
206
+ }
207
+ const uncovered = [];
208
+ for (const [path, file] of diff.files) {
209
+ if (file.status === "D" || !isAuditedSource(path)) {
210
+ continue;
211
+ }
212
+ if (!covered.has(path)) {
213
+ uncovered.push(path);
214
+ }
215
+ }
216
+ uncovered.sort();
217
+ if (uncovered.length > 0) {
218
+ reasons.push(`source changed with no witness spec covering it: ${uncovered.join(", ")}`);
219
+ }
220
+ const evaluations = [];
221
+ let reEvaluationMs = 0;
222
+ const scratchRoot = makeScratchRoot();
223
+ try {
224
+ const baseInputs = {
225
+ repoRoot,
226
+ headSha: diff.headSha,
227
+ baselineRef: run.baseline ?? run.base,
228
+ diff,
229
+ destructiveCommands: manifest.manifest.destructiveCommands,
230
+ behaviors,
231
+ testFiles: testFilesOutcome.files,
232
+ spawningChangedFiles,
233
+ scratchRoot,
234
+ };
235
+ for (const entry of own) {
236
+ const inputs = { ...baseInputs, phaseOwn: true };
237
+ if (run.hooks !== undefined) {
238
+ inputs.hooks = run.hooks;
239
+ }
240
+ const evaluation = evaluateWitness(entry.spec, entry.repoRelative, inputs);
241
+ evaluations.push(evaluation);
242
+ if (evaluation.status !== "green") {
243
+ reasons.push(`witness ${evaluation.witness}: ${evaluation.status}: ${evaluation.reasons.join("; ")}`);
244
+ }
245
+ }
246
+ const reEvaluationStart = Date.now();
247
+ for (const entry of triggeredStored) {
248
+ const inputs = { ...baseInputs, phaseOwn: false };
249
+ if (run.hooks !== undefined) {
250
+ inputs.hooks = run.hooks;
251
+ }
252
+ const evaluation = evaluateWitness(entry.spec, entry.repoRelative, inputs);
253
+ evaluations.push(evaluation);
254
+ if (evaluation.status === "red") {
255
+ const rates = evaluation.members
256
+ .filter((member) => member.rate !== undefined)
257
+ .map((member) => `member ${String(member.index)} red ` +
258
+ `${String(member.rate?.red)}/${String(member.rate?.total)}`)
259
+ .join(", ");
260
+ reasons.push(`witness ${evaluation.witness} no longer guards its behavior ` +
261
+ `(${rates === "" ? evaluation.reasons.join("; ") : rates})`);
262
+ }
263
+ else if (evaluation.status === "error") {
264
+ reasons.push(`witness ${evaluation.witness}: error: ${evaluation.reasons.join("; ")}`);
265
+ }
266
+ }
267
+ reEvaluationMs = Date.now() - reEvaluationStart;
268
+ }
269
+ finally {
270
+ removeScratchRoot(scratchRoot);
271
+ }
272
+ let status = "green";
273
+ if (evaluations.some((evaluation) => evaluation.status === "error")) {
274
+ status = "error";
275
+ }
276
+ else if (reasons.length > 0) {
277
+ status = "red";
278
+ }
279
+ const evidence = [];
280
+ if (run.evidenceDir !== undefined) {
281
+ const recordsPath = join(run.evidenceDir, "witness-records.json");
282
+ const refusal = refuseOpenForWrite(recordsPath);
283
+ if (refusal === undefined) {
284
+ try {
285
+ mkdirSync(run.evidenceDir, { recursive: true });
286
+ writeFileSync(recordsPath, `${JSON.stringify({
287
+ base: diff.baseSha,
288
+ head: diff.headSha,
289
+ spawningChangedFiles,
290
+ uncoveredSources: uncovered,
291
+ reEvaluationMs,
292
+ evaluations,
293
+ }, null, 2)}\n`);
294
+ evidence.push("witness-records.json");
295
+ }
296
+ catch {
297
+ // The record still carries the verdict; evidence is best-effort.
298
+ }
299
+ }
300
+ }
301
+ const detail = `${String(evaluations.length)} witness(es) evaluated ` +
302
+ `(${String(own.length)} own, ${String(triggeredStored.length)} stored ` +
303
+ `re-evaluated in ${String(reEvaluationMs)}ms); ` +
304
+ (reasons.length === 0
305
+ ? "every witness red against every declared dangerous state and green at head"
306
+ : reasons.join("; "));
307
+ const result = makeGateResult({
308
+ gate: "red-witness",
309
+ status,
310
+ units: evaluations.length,
311
+ unitLabel: "witnesses evaluated",
312
+ startedAt,
313
+ endedAt: now(),
314
+ detail,
315
+ evidence,
316
+ });
317
+ return {
318
+ result,
319
+ exitCode: exitCodeForStatus(result.status),
320
+ evaluations,
321
+ reEvaluationMs,
322
+ };
323
+ }
324
+ function main(argv) {
325
+ const parsed = parseArgs(argv);
326
+ if (parsed.options === undefined) {
327
+ process.stderr.write(`tiphys red-witness: ${parsed.usageError}\n${USAGE}\n`);
328
+ return 64;
329
+ }
330
+ const options = parsed.options;
331
+ if (options.result === undefined) {
332
+ process.stderr.write(`tiphys red-witness: --result is required\n${USAGE}\n`);
333
+ return 64;
334
+ }
335
+ let outcome;
336
+ if (options.base === undefined) {
337
+ // M2-C-3 (M2R-003): a required invocation parameter absent is error,
338
+ // never not-applicable and never a guess.
339
+ outcome = errorOutcome(now(), "--base was not supplied; the phase diff cannot be computed (M2-C-3)");
340
+ }
341
+ else {
342
+ const run = { repoRoot: process.cwd(), base: options.base };
343
+ if (options.head !== undefined) {
344
+ run.head = options.head;
345
+ }
346
+ if (options.baseline !== undefined) {
347
+ run.baseline = options.baseline;
348
+ }
349
+ if (options.evidence !== undefined) {
350
+ run.evidenceDir = options.evidence;
351
+ }
352
+ try {
353
+ outcome = runRedWitnessGate(run);
354
+ }
355
+ catch (error) {
356
+ // No throw may escape as exit 1: that is the RED code (the runner's
357
+ // own crash-discipline rule, applied to this gate).
358
+ outcome = errorOutcome(now(), `the red-witness gate failed: ${singleLine(error.message ?? String(error))}`);
359
+ }
360
+ }
361
+ const refusal = refuseOpenForWrite(options.result);
362
+ if (refusal !== undefined) {
363
+ process.stderr.write(`tiphys red-witness: ${refusal}\n`);
364
+ return 21;
365
+ }
366
+ try {
367
+ writeFileSync(options.result, renderGateResult(outcome.result));
368
+ }
369
+ catch (error) {
370
+ process.stderr.write(`tiphys red-witness: the result record could not be written: ${singleLine(String(error))}\n`);
371
+ return 21;
372
+ }
373
+ process.stdout.write(`red-witness: ${outcome.result.status} (${outcome.result.detail})\n`);
374
+ return outcome.exitCode;
375
+ }
376
+ const invokedDirectly = (() => {
377
+ const entry = process.argv[1];
378
+ if (entry === undefined) {
379
+ return false;
380
+ }
381
+ try {
382
+ return import.meta.url === pathToFileURL(resolve(entry)).href;
383
+ }
384
+ catch {
385
+ return false;
386
+ }
387
+ })();
388
+ if (invokedDirectly) {
389
+ process.exit(main(process.argv.slice(2)));
390
+ }
@@ -0,0 +1,283 @@
1
+ import type { GateStatus } from "./result.ts";
2
+ import type { SchemaDocument } from "./validate.ts";
3
+ /**
4
+ * THE RELEASE-VERIFICATION CONTRACT (kernel plan M2, M2-P7 step 3; DR-0014;
5
+ * delivery/verification/release-verification-interface.md).
6
+ *
7
+ * ONE post-merge verification contract with two registry entries (`deploy`
8
+ * and `migrations`). The kernel owns the SUBJECT, the CLOCK, the LOOP and
9
+ * the OUTCOME MAPPING; an adapter owns the platform mapping and nothing
10
+ * else. An adapter is an executable named by committed configuration,
11
+ * spawned as a subprocess with a request file path in argv, performing ONE
12
+ * bounded observation per invocation and writing its response to a
13
+ * kernel-supplied record path. It returns an OUTCOME, never a status path:
14
+ * JSON-pointer extraction exists only inside the http-json adapter's own
15
+ * configuration, and the kernel never learns what a statusPath is.
16
+ *
17
+ * WHY THE BOUNDARY IS A PROCESS (M2-D-07, investigation section 5): it
18
+ * works for any language, and it makes the per-attempt timeout ENFORCEABLE
19
+ * rather than promised. The kernel terminates an attempt that overruns
20
+ * (`spawnSync`'s `timeout` and `killSignal` options) and records that
21
+ * attempt as `error`, so an adapter that hangs cannot hang the kernel.
22
+ *
23
+ * C-2 EXEMPTION, STATED WHERE THE TERMINATION HAPPENS AND NOWHERE ELSE.
24
+ * C-2 forbids pid, process liveness, signals and /proc FOR IDENTITY OR
25
+ * EXCLUSION. The per-attempt termination above is neither: it is a timeout
26
+ * bound on a child this kernel itself spawned and still holds the handle
27
+ * of, it probes nothing, identifies nothing and excludes nothing, and no
28
+ * pid is ever read, recorded or compared. That is the one place any kill
29
+ * may appear in this module or its adapters, and it appears as a spawn
30
+ * option, not as a process.kill call.
31
+ *
32
+ * NO FAILURE VOCABULARY SHIPS (plan step 2, T-003 lesson 4). No
33
+ * non-success platform state was ever captured, so none is named anywhere
34
+ * in this module or its adapters. The safe rule instead: one satisfying
35
+ * value per configured adapter; every other observed value is recorded
36
+ * verbatim and treated as `pending` until the deadline, at which point it
37
+ * becomes red naming the last observed value. Unknown never becomes green.
38
+ *
39
+ * NEVER auto-background (C-3): the loop is a foreground await, every spawn
40
+ * is spawnSync, nothing is detached and nothing is unref'd.
41
+ */
42
+ /** The one declaration path (design decision D-p7-1 in the work history). */
43
+ export declare const DECLARATION_PATH = "release-verification.json";
44
+ /** The contract versions this kernel accepts (fail-closed rule 6). */
45
+ export declare const ACCEPTED_CONTRACT_VERSIONS: readonly string[];
46
+ /**
47
+ * The subject: what the kernel knows for certain the moment a merge
48
+ * happens. Platform vocabulary (deployment id, workflow run id, migration
49
+ * set) is the adapter's, and the kernel never learns it.
50
+ */
51
+ export interface ReleaseSubject {
52
+ repository: string;
53
+ integrationRef: string;
54
+ mergedSha: string;
55
+ mergedAt: string;
56
+ phaseId: string;
57
+ }
58
+ export declare const SUBJECT_FIELDS: readonly (keyof ReleaseSubject)[];
59
+ /**
60
+ * The outcome vocabulary, six values. There is deliberately no `unknown`,
61
+ * no `skipped` and no `warn`: a soft state is where a false green hides
62
+ * (M2-C-3).
63
+ */
64
+ export type VerificationOutcome = "satisfied" | "failed" | "pending" | "absent" | "not-applicable" | "error";
65
+ export declare const VERIFICATION_OUTCOMES: readonly VerificationOutcome[];
66
+ /**
67
+ * THE TOTAL MAPPING to GateResult status, implemented in exactly one place
68
+ * (plan step 3). `satisfied` is green with `units` 1 per verification
69
+ * satisfied (or the adapter's own examined count, D-p7-3). `pending` and
70
+ * `absent` are NEVER terminal: they are loop states, and the two entries
71
+ * below are reachable only through the deadline conversion in
72
+ * `runVerification`, which turns both into red with TEXTUALLY DISTINCT
73
+ * reasons: `deadline reached, last observed <value>` for pending and
74
+ * `deadline reached, no release object for subject` for absent. An adapter
75
+ * cannot make pending mean pass.
76
+ */
77
+ export declare const OUTCOME_TO_STATUS: Readonly<Record<VerificationOutcome, GateStatus>>;
78
+ export declare const DEADLINE_REASON_ABSENT = "deadline reached, no release object for subject";
79
+ export declare function deadlineReasonPending(lastObserved: string): string;
80
+ /** The kernel's clock. No defaults anywhere: the numbers are the project's. */
81
+ export interface VerificationClock {
82
+ intervalMs: number;
83
+ deadlineMs: number;
84
+ attemptTimeoutMs: number;
85
+ maxAttempts?: number;
86
+ }
87
+ /** What the adapter wrote, after the seven rules accepted it. */
88
+ export interface AdapterResponse {
89
+ contractVersion: string;
90
+ adapter: string;
91
+ subject: ReleaseSubject;
92
+ outcome: VerificationOutcome;
93
+ resolved?: {
94
+ kind: string;
95
+ id: string;
96
+ createdAt?: unknown;
97
+ } & Record<string, unknown>;
98
+ observedAt: string;
99
+ observation?: {
100
+ raw?: unknown;
101
+ detail: string;
102
+ };
103
+ reason?: string;
104
+ units?: number;
105
+ precondition?: {
106
+ id: string;
107
+ evidence?: string[];
108
+ };
109
+ transport?: {
110
+ httpStatus?: number;
111
+ };
112
+ }
113
+ /** One attempt's record: number, instant, outcome, identity, transport. */
114
+ export interface AttemptRecord {
115
+ attempt: number;
116
+ at: string;
117
+ outcome: VerificationOutcome | "invalid";
118
+ detail: string;
119
+ resolved?: unknown;
120
+ transport: {
121
+ exitCode: number | null;
122
+ signal: string | null;
123
+ terminatedByTimeout: boolean;
124
+ httpStatus?: number;
125
+ };
126
+ /**
127
+ * The older-than-the-merge observation (investigation guarantee 9,
128
+ * section 8 item 8): recorded, deliberately NOT promoted to an error
129
+ * until one real counter-example settles whether a platform reuses or
130
+ * backdates a record on redeploy.
131
+ */
132
+ releaseObjectOlderThanMerge?: boolean;
133
+ }
134
+ export type VerificationVerdict = {
135
+ kind: "satisfied";
136
+ units: number;
137
+ detail: string;
138
+ resolved: unknown;
139
+ } | {
140
+ kind: "failed";
141
+ reason: string;
142
+ } | {
143
+ kind: "not-applicable";
144
+ declared: boolean;
145
+ reason: string;
146
+ preconditionId: string;
147
+ evidence: string[];
148
+ } | {
149
+ kind: "error";
150
+ reason: string;
151
+ } | {
152
+ kind: "deadline";
153
+ lastOutcome: "pending" | "absent";
154
+ reason: string;
155
+ };
156
+ export interface VerificationRun {
157
+ verdict: VerificationVerdict;
158
+ attempts: AttemptRecord[];
159
+ /** Paths relative to evidenceDir, in creation order. */
160
+ evidence: string[];
161
+ }
162
+ export declare function responseSchema(): SchemaDocument;
163
+ export declare function declarationSchema(): SchemaDocument;
164
+ /**
165
+ * THE SEVEN FAIL-CLOSED RULES on an adapter response (plan step 3), each
166
+ * corresponding to a way a verifier can be fooled. Rule 1 (exit 0 with no
167
+ * response written) is enforced by the caller, which owns the file's
168
+ * existence; rules 2 to 7 are here. The subject echo (rule 3) is checked
169
+ * field by field BEFORE the outcome is read, so an adapter that verified
170
+ * something else cannot have its outcome looked at for this subject.
171
+ */
172
+ export type ResponseValidation = {
173
+ ok: true;
174
+ response: AdapterResponse;
175
+ } | {
176
+ ok: false;
177
+ rule: number;
178
+ reason: string;
179
+ };
180
+ export declare function validateAdapterResponse(body: string, subject: ReleaseSubject): ResponseValidation;
181
+ /**
182
+ * The encoded forms of a credential value the kernel can derive from the
183
+ * value alone (CR-P7H-3). Redacting only the verbatim bytes let a trivially
184
+ * reversible copy through: an adapter emitting the token as a standalone
185
+ * base64 blob (an HTTP Basic `Authorization: Basic <base64(token)>` of the
186
+ * token) leaked a recoverable secret into the stderr evidence. Each form here
187
+ * is a DETERMINISTIC, enumerable transform of the same value, so the set is
188
+ * bounded and cheap.
189
+ *
190
+ * WHAT THIS DOES NOT COVER, stated so a green is auditable rather than
191
+ * silently partial (never soften a work history): forms that fold in bytes
192
+ * the kernel does not hold, e.g. base64 of `"user:" + value` for a full Basic
193
+ * credential PAIR (the username is the project's, not the kernel's), or a
194
+ * value re-encoded by a transport the kernel never sees (gzip, hex, a second
195
+ * base64 round). Those are residue the reference adapters do not produce; a
196
+ * third-party adapter that composes a credential with unknown surrounding
197
+ * bytes owns that redaction, and the guarantee scoped here is the value and
198
+ * its own single-step base64 and percent encodings.
199
+ */
200
+ export declare function secretForms(value: string): string[];
201
+ /**
202
+ * Replace every resolved credential VALUE, and its enumerable encoded forms
203
+ * (see secretForms), with a named placeholder, everywhere in the text.
204
+ */
205
+ export declare function redactSecrets(text: string, secrets: readonly {
206
+ name: string;
207
+ value: string;
208
+ }[]): string;
209
+ export interface RunVerificationOptions {
210
+ verification: string;
211
+ subject: ReleaseSubject;
212
+ adapter: string[];
213
+ config: unknown;
214
+ clock: VerificationClock;
215
+ /** Absolute directory the attempt evidence is written into. */
216
+ evidenceDir: string;
217
+ /** Resolved credential values, for redaction only. Never written. */
218
+ secrets?: readonly {
219
+ name: string;
220
+ value: string;
221
+ }[];
222
+ }
223
+ /**
224
+ * THE KERNEL-OWNED LOOP. One bounded adapter observation per attempt, one
225
+ * record per attempt, foreground polling (C-3), verdict at the deadline.
226
+ * `pending` and `absent` loop; everything else is terminal, including every
227
+ * fail-closed violation (design decision D-p7-5: a broken adapter cannot
228
+ * reach a verdict, and M2-C-3 makes that `error` now, not red later).
229
+ */
230
+ export declare function runVerification(options: RunVerificationOptions): Promise<VerificationRun>;
231
+ export interface DeclaredVerification {
232
+ mode: "none" | "adapter";
233
+ reason?: string;
234
+ adapter?: string[];
235
+ config?: unknown;
236
+ clock?: VerificationClock;
237
+ credentials?: string[];
238
+ }
239
+ export interface Declaration {
240
+ version: number;
241
+ repository: string;
242
+ integrationRef: string;
243
+ verifications: {
244
+ deploy?: DeclaredVerification;
245
+ migrations?: DeclaredVerification;
246
+ };
247
+ }
248
+ export type DeclarationLoad = {
249
+ ok: true;
250
+ declaration: Declaration;
251
+ /** sha256 hex of the blob bytes read from the governing ref. */
252
+ sha256: string;
253
+ /** The resolved commit sha of the governing ref. */
254
+ ref: string;
255
+ refSha: string;
256
+ } | {
257
+ ok: false;
258
+ reason: string;
259
+ };
260
+ /**
261
+ * Read the declaration from a COMMITTED ref, never from the working tree
262
+ * (design decision D-p7-2; the anti-widening rule of plan step 7, same as
263
+ * the scope auditor's: a phase must not be able to switch off, inside its
264
+ * own branch, the check that would have caught it). The record carries the
265
+ * declaration blob's sha256 so a reviewer can see which text authorized
266
+ * whatever happened.
267
+ */
268
+ export declare function loadDeclaration(ref: string, cwd: string): DeclarationLoad;
269
+ /**
270
+ * THE GATE ENTRY, shared by the two thin entry points. The manifest keeps
271
+ * two static entries exactly as section 1.4 declares them; this function is
272
+ * what each names. Returns the process exit code.
273
+ *
274
+ * The governing ref is `--base` when supplied (the anti-widening read),
275
+ * else HEAD: always a COMMITTED state, so the subject's mergedSha and
276
+ * mergedAt are the resolved commit's, and an uncommitted edit can neither
277
+ * enable, disable nor reconfigure a verification. R-032's blocking half:
278
+ * the verdict record this gate writes is keyed to that sha in its detail
279
+ * line, and the consumption contract is that THE NEXT DISPATCH REQUIRES A
280
+ * GREEN VERDICT RECORD FOR THE MERGED SHA; the enforcement is wired at M4
281
+ * with the pilot (M2-D-11), and M2 adds no dispatch block to spawn.
282
+ */
283
+ export declare function runReleaseGate(name: "deploy" | "migrations", args: string[]): Promise<number>;