@tiphys/kernel 0.0.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/AGENTS.md +663 -0
  2. package/LICENSE +202 -0
  3. package/assurance-modes.yaml +299 -0
  4. package/checklists/clean-room.yaml +325 -0
  5. package/checklists/env-failure-diagnosis.yaml +68 -0
  6. package/checklists/flake-playbook.yaml +68 -0
  7. package/checklists/hazard-review.yaml +144 -0
  8. package/checklists/plan-review.yaml +103 -0
  9. package/dist/bin/tiphys.d.ts +2 -0
  10. package/dist/bin/tiphys.js +92 -0
  11. package/dist/src/adapters/load.d.ts +202 -0
  12. package/dist/src/adapters/load.js +440 -0
  13. package/dist/src/brief.d.ts +32 -0
  14. package/dist/src/brief.js +66 -0
  15. package/dist/src/checklists.d.ts +169 -0
  16. package/dist/src/checklists.js +310 -0
  17. package/dist/src/checks.d.ts +1539 -0
  18. package/dist/src/checks.js +5025 -0
  19. package/dist/src/cli.d.ts +9 -0
  20. package/dist/src/cli.js +68 -0
  21. package/dist/src/commands/brief.d.ts +92 -0
  22. package/dist/src/commands/brief.js +343 -0
  23. package/dist/src/commands/checklist.d.ts +42 -0
  24. package/dist/src/commands/checklist.js +168 -0
  25. package/dist/src/commands/cutover.d.ts +35 -0
  26. package/dist/src/commands/cutover.js +448 -0
  27. package/dist/src/commands/doctor.d.ts +264 -0
  28. package/dist/src/commands/doctor.js +1546 -0
  29. package/dist/src/commands/gates.d.ts +9 -0
  30. package/dist/src/commands/gates.js +360 -0
  31. package/dist/src/commands/init.d.ts +30 -0
  32. package/dist/src/commands/init.js +177 -0
  33. package/dist/src/commands/lock.d.ts +34 -0
  34. package/dist/src/commands/lock.js +340 -0
  35. package/dist/src/commands/mode.d.ts +40 -0
  36. package/dist/src/commands/mode.js +134 -0
  37. package/dist/src/commands/next.d.ts +130 -0
  38. package/dist/src/commands/next.js +597 -0
  39. package/dist/src/commands/plan.d.ts +20 -0
  40. package/dist/src/commands/plan.js +105 -0
  41. package/dist/src/commands/pool.d.ts +1 -0
  42. package/dist/src/commands/pool.js +139 -0
  43. package/dist/src/commands/resume.d.ts +1 -0
  44. package/dist/src/commands/resume.js +88 -0
  45. package/dist/src/commands/spawn.d.ts +1 -0
  46. package/dist/src/commands/spawn.js +195 -0
  47. package/dist/src/commands/status.d.ts +21 -0
  48. package/dist/src/commands/status.js +122 -0
  49. package/dist/src/commands/sync.d.ts +47 -0
  50. package/dist/src/commands/sync.js +341 -0
  51. package/dist/src/commands/teardown.d.ts +1 -0
  52. package/dist/src/commands/teardown.js +87 -0
  53. package/dist/src/commands/tuition.d.ts +21 -0
  54. package/dist/src/commands/tuition.js +218 -0
  55. package/dist/src/commands/validate.d.ts +78 -0
  56. package/dist/src/commands/validate.js +430 -0
  57. package/dist/src/commands/watch.d.ts +1 -0
  58. package/dist/src/commands/watch.js +172 -0
  59. package/dist/src/cutover.d.ts +584 -0
  60. package/dist/src/cutover.js +1444 -0
  61. package/dist/src/exclusion.d.ts +389 -0
  62. package/dist/src/exclusion.js +843 -0
  63. package/dist/src/exec/env.d.ts +278 -0
  64. package/dist/src/exec/env.js +334 -0
  65. package/dist/src/fleet.d.ts +223 -0
  66. package/dist/src/fleet.js +298 -0
  67. package/dist/src/gates/adapters/http-json.d.ts +5 -0
  68. package/dist/src/gates/adapters/http-json.js +283 -0
  69. package/dist/src/gates/adapters/migrations-command.d.ts +1 -0
  70. package/dist/src/gates/adapters/migrations-command.js +373 -0
  71. package/dist/src/gates/citations.d.ts +408 -0
  72. package/dist/src/gates/citations.js +1169 -0
  73. package/dist/src/gates/coverage.d.ts +375 -0
  74. package/dist/src/gates/coverage.js +836 -0
  75. package/dist/src/gates/credentials.d.ts +233 -0
  76. package/dist/src/gates/credentials.js +752 -0
  77. package/dist/src/gates/deploy.d.ts +1 -0
  78. package/dist/src/gates/deploy.js +33 -0
  79. package/dist/src/gates/gate-classes.d.ts +56 -0
  80. package/dist/src/gates/gate-classes.js +633 -0
  81. package/dist/src/gates/manifest.d.ts +99 -0
  82. package/dist/src/gates/manifest.js +208 -0
  83. package/dist/src/gates/merge-preconditions.d.ts +319 -0
  84. package/dist/src/gates/merge-preconditions.js +932 -0
  85. package/dist/src/gates/migrations.d.ts +1 -0
  86. package/dist/src/gates/migrations.js +36 -0
  87. package/dist/src/gates/pin.d.ts +114 -0
  88. package/dist/src/gates/pin.js +154 -0
  89. package/dist/src/gates/red-witness.d.ts +22 -0
  90. package/dist/src/gates/red-witness.js +482 -0
  91. package/dist/src/gates/release.d.ts +283 -0
  92. package/dist/src/gates/release.js +820 -0
  93. package/dist/src/gates/result.d.ts +116 -0
  94. package/dist/src/gates/result.js +91 -0
  95. package/dist/src/gates/run.d.ts +614 -0
  96. package/dist/src/gates/run.js +1614 -0
  97. package/dist/src/gates/schemas/citation-config.schema.json +59 -0
  98. package/dist/src/gates/schemas/coverage-config.schema.json +77 -0
  99. package/dist/src/gates/schemas/gate-manifest.schema.json +125 -0
  100. package/dist/src/gates/schemas/gate-result.schema.json +160 -0
  101. package/dist/src/gates/schemas/phase-declaration.schema.json +87 -0
  102. package/dist/src/gates/schemas/release-record.schema.json +119 -0
  103. package/dist/src/gates/schemas/verifier-config.schema.json +101 -0
  104. package/dist/src/gates/schemas/witness-spec.schema.json +110 -0
  105. package/dist/src/gates/scope.d.ts +131 -0
  106. package/dist/src/gates/scope.js +1018 -0
  107. package/dist/src/gates/suite.d.ts +217 -0
  108. package/dist/src/gates/suite.js +968 -0
  109. package/dist/src/gates/validate.d.ts +121 -0
  110. package/dist/src/gates/validate.js +414 -0
  111. package/dist/src/hooks.d.ts +84 -0
  112. package/dist/src/hooks.js +125 -0
  113. package/dist/src/index.d.ts +31 -0
  114. package/dist/src/index.js +30 -0
  115. package/dist/src/liveness.d.ts +321 -0
  116. package/dist/src/liveness.js +396 -0
  117. package/dist/src/lock.d.ts +256 -0
  118. package/dist/src/lock.js +792 -0
  119. package/dist/src/model-resolution.d.ts +159 -0
  120. package/dist/src/model-resolution.js +307 -0
  121. package/dist/src/modes.d.ts +149 -0
  122. package/dist/src/modes.js +258 -0
  123. package/dist/src/path-identity.d.ts +34 -0
  124. package/dist/src/path-identity.js +48 -0
  125. package/dist/src/plan.d.ts +73 -0
  126. package/dist/src/plan.js +153 -0
  127. package/dist/src/pool.d.ts +326 -0
  128. package/dist/src/pool.js +988 -0
  129. package/dist/src/roles.d.ts +461 -0
  130. package/dist/src/roles.js +776 -0
  131. package/dist/src/spawn.d.ts +482 -0
  132. package/dist/src/spawn.js +1003 -0
  133. package/dist/src/status.d.ts +116 -0
  134. package/dist/src/status.js +148 -0
  135. package/dist/src/task.d.ts +504 -0
  136. package/dist/src/task.js +307 -0
  137. package/dist/src/teardown.d.ts +39 -0
  138. package/dist/src/teardown.js +422 -0
  139. package/dist/src/tuition.d.ts +159 -0
  140. package/dist/src/tuition.js +311 -0
  141. package/dist/src/validate.d.ts +263 -0
  142. package/dist/src/validate.js +742 -0
  143. package/dist/src/version.d.ts +3 -0
  144. package/dist/src/version.js +38 -0
  145. package/dist/src/watcher.d.ts +275 -0
  146. package/dist/src/watcher.js +849 -0
  147. package/dist/src/witness/run.d.ts +299 -0
  148. package/dist/src/witness/run.js +1373 -0
  149. package/dist/src/witness/spec.d.ts +270 -0
  150. package/dist/src/witness/spec.js +475 -0
  151. package/dist/tsconfig.src.tsbuildinfo +1 -0
  152. package/gate-registry.yaml +526 -0
  153. package/gates.manifest.json +257 -0
  154. package/package.json +72 -3
  155. package/role-model-config.yaml +88 -0
  156. package/roles/README.md +128 -0
  157. package/roles/_shared-dispatch-contract.md +87 -0
  158. package/roles/adversarial-plan-reviewer.md +80 -0
  159. package/roles/clean-room-reviewer.md +140 -0
  160. package/roles/implementer.md +463 -0
  161. package/roles/investigator.md +138 -0
  162. package/roles/plan-writer.md +95 -0
  163. package/schemas/README.md +82 -0
  164. package/schemas/assurance-modes.schema.json +264 -0
  165. package/schemas/charter.schema.json +185 -0
  166. package/schemas/checklist.schema.json +114 -0
  167. package/schemas/cutover-state.schema.json +64 -0
  168. package/schemas/decision-record.schema.json +88 -0
  169. package/schemas/executor-record.schema.json +36 -0
  170. package/schemas/final-report.schema.json +90 -0
  171. package/schemas/finding.schema.json +106 -0
  172. package/schemas/gate-registry.schema.json +260 -0
  173. package/schemas/mechanism-index.schema.json +94 -0
  174. package/schemas/model-resolution.schema.json +362 -0
  175. package/schemas/plan.schema.json +300 -0
  176. package/schemas/report.schema.json +579 -0
  177. package/schemas/role-brief.schema.json +105 -0
  178. package/schemas/role-model-config.schema.json +90 -0
  179. package/schemas/status-line.schema.json +40 -0
  180. package/schemas/tuition.schema.json +191 -0
  181. package/schemas/verdict.schema.json +295 -0
  182. package/schemas/work-history.schema.json +183 -0
  183. package/schemas/write-bypass.schema.json +69 -0
  184. package/templates/charter.example.yaml +54 -0
  185. package/templates/decision-record.example.yaml +27 -0
  186. package/templates/final-report.example.yaml +80 -0
  187. package/templates/plan.example.yaml +87 -0
  188. package/templates/report.example.yaml +236 -0
  189. package/templates/warnings.md +74 -0
  190. package/templates/work-history.example.yaml +185 -0
  191. package/tuition/README.md +76 -0
  192. package/tuition/T-001.yaml +48 -0
  193. package/tuition/T-002.yaml +51 -0
  194. package/tuition/T-003.yaml +100 -0
  195. package/tuition/T-004.yaml +52 -0
  196. package/tuition/T-005.yaml +72 -0
  197. package/tuition/T-006.yaml +81 -0
  198. package/tuition/T-007.yaml +56 -0
  199. package/tuition/T-008.yaml +111 -0
  200. package/tuition/T-009.yaml +50 -0
  201. package/tuition/T-015.yaml +36 -0
  202. package/tuition/T-016.yaml +36 -0
  203. package/tuition/T-017.yaml +46 -0
  204. package/tuition/T-018.yaml +84 -0
  205. package/tuition/T-021.yaml +40 -0
  206. package/tuition/T-022.yaml +36 -0
  207. package/tuition/mechanism-index.yaml +256 -0
@@ -0,0 +1,482 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { join, relative, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { pathsIdentifySameObject } from "../path-identity.js";
5
+ import { refuseOpenForWrite, singleLine } from "../task.js";
6
+ import { loadManifest } from "./manifest.js";
7
+ import { exitCodeForStatus, makeGateResult, renderGateResult } from "./result.js";
8
+ import { listWitnessSpecFiles, loadWitnessSpec, memberTouchedFiles, parseWitnessSpec, phaseOwnedMemberIndices, } from "../witness/spec.js";
9
+ import { SPAWN_GREP, computePhaseDiff, evaluateWitness, gitIn, makeScratchRoot, readTestFilesAtHead, removeScratchRoot, resolveRepoRoot, shellSpawnsAndParses, } from "../witness/run.js";
10
+ /**
11
+ * THE RED-WITNESS GATE (kernel plan M2, M2-P2 steps 6 and 7).
12
+ *
13
+ * Registered in gates.manifest.json as `red-witness`, applicability
14
+ * `required`, precondition `diff-touches` on `src/` and `bin/`, unitLabel
15
+ * `witnesses evaluated`. Invoked by the runner as
16
+ *
17
+ * node src/gates/red-witness.ts --result <path> --evidence <dir>
18
+ * --base <ref> --head <ref>
19
+ *
20
+ * plus `--baseline <ref>` when invoked directly: merge-time re-verification
21
+ * is a PARAMETER, not an enforcement (M2-D-08); it defaults to `--base`.
22
+ *
23
+ * WHAT THE GATE DECIDES.
24
+ * - Every witness spec changed in the phase diff (the phase's OWN
25
+ * witnesses) is evaluated by the harness.
26
+ * - Every STORED witness any of whose dangerous-state members touches a
27
+ * changed file is re-evaluated; one now green against any of its own
28
+ * members is red with reason "witness no longer guards its behavior"
29
+ * naming the witness and the measured rate (M2R-002, the N-401 shape).
30
+ * - A changed source file under src/ or bin/ with no witness spec
31
+ * covering it is RED, never not-applicable (step 7).
32
+ * - `--base` absent is `error` (M2-C-3).
33
+ *
34
+ * DEPTH REQUIREMENT, documented rather than assumed (STATE.md CR-902
35
+ * carry-forward): the gate needs `--base` and `--head` resolvable with
36
+ * history (the diff is base...head, a merge-base diff) and an UNSHALLOW
37
+ * repository, because the harness scratch-clones it and git refuses to
38
+ * clone from a shallow source. Both are satisfied by `fetch-depth: 0` on
39
+ * the CI checkout, which is owned by the workflow's owner, not this phase.
40
+ * A shallow repository is `error` naming the requirement. On pull_request
41
+ * events the checkout HEAD is a synthetic merge commit, so the audited
42
+ * head is always taken from `--head`, never from the checkout.
43
+ */
44
+ const USAGE = "usage: node src/gates/red-witness.ts --result <path> --evidence <dir> " +
45
+ "--base <ref> [--head <ref>] [--baseline <ref>] [--phase <id>]";
46
+ function parseArgs(argv) {
47
+ const options = {};
48
+ const known = new Map([
49
+ ["--result", "result"],
50
+ ["--evidence", "evidence"],
51
+ ["--base", "base"],
52
+ ["--head", "head"],
53
+ ["--baseline", "baseline"],
54
+ ["--phase", "phase"],
55
+ ]);
56
+ for (let index = 0; index < argv.length; index += 2) {
57
+ const flag = argv[index];
58
+ const field = known.get(flag);
59
+ if (field === undefined) {
60
+ return { usageError: `unknown option ${flag}` };
61
+ }
62
+ const value = argv[index + 1];
63
+ if (value === undefined || value.startsWith("--")) {
64
+ return { usageError: `${flag} requires a value` };
65
+ }
66
+ options[field] = value;
67
+ }
68
+ return { options };
69
+ }
70
+ function now() {
71
+ return new Date().toISOString();
72
+ }
73
+ function errorOutcome(startedAt, detail) {
74
+ const result = makeGateResult({
75
+ gate: "red-witness",
76
+ status: "error",
77
+ units: 0,
78
+ unitLabel: "witnesses evaluated",
79
+ startedAt,
80
+ endedAt: now(),
81
+ detail,
82
+ });
83
+ return {
84
+ result,
85
+ exitCode: exitCodeForStatus(result.status),
86
+ evaluations: [],
87
+ reEvaluationMs: 0,
88
+ };
89
+ }
90
+ /**
91
+ * True when the repo-relative path is a phase-audited source path.
92
+ *
93
+ * WHY `plugin/src/` AND NOT `plugin/`. This list is the COVERAGE OBLIGATION:
94
+ * a changed path here must be touched by some witness's dangerous state or the
95
+ * gate reports it uncovered. `src/` and `bin/` are source trees, and the plugin
96
+ * package's source tree is `plugin/src/`. Adding bare `plugin/` would pull in
97
+ * `plugin/package.json` and `plugin/tsconfig.json`, which no witness mutates
98
+ * and which would therefore redden every plugin phase for its own packaging.
99
+ *
100
+ * WHY IT IS NOT THE SAME LIST AS THE GATE'S PRECONDITION, which reads
101
+ * `src/`, `bin/`, `plugin/`. The precondition decides whether the gate RUNS.
102
+ * This decides what it REQUIRES. T-038 widened the first and left the second,
103
+ * so from that fix until this one a diff touching only `plugin/` ran the gate
104
+ * and took no obligation from it: a plugin phase shipping ZERO witnesses was
105
+ * green. Found by M4-P29 while reading this file for a different reason.
106
+ */
107
+ function isAuditedSource(path) {
108
+ return (path.startsWith("src/") ||
109
+ path.startsWith("bin/") ||
110
+ path.startsWith("plugin/src/"));
111
+ }
112
+ /**
113
+ * Run the red-witness gate against a repository. Exported so tests can
114
+ * drive it without a subprocess; the CLI below is a thin shell over it.
115
+ */
116
+ export function runRedWitnessGate(run) {
117
+ const startedAt = now();
118
+ const rootProbe = resolveRepoRoot(run.repoRoot);
119
+ if (rootProbe.root === undefined) {
120
+ return errorOutcome(startedAt, rootProbe.reason);
121
+ }
122
+ const repoRoot = rootProbe.root;
123
+ const shallow = gitIn(repoRoot, ["rev-parse", "--is-shallow-repository"]);
124
+ if (!shallow.ok) {
125
+ return errorOutcome(startedAt, shallow.reason);
126
+ }
127
+ if (shallow.stdout.trim() === "true") {
128
+ return errorOutcome(startedAt, "this repository is a shallow clone; the red-witness gate requires " +
129
+ "full history (fetch-depth: 0) because the phase diff is a " +
130
+ "merge-base diff and the harness scratch-clones the repository, " +
131
+ "which git refuses from a shallow source");
132
+ }
133
+ const diffOutcome = computePhaseDiff(repoRoot, run.base, run.head ?? "HEAD");
134
+ if (!diffOutcome.ok) {
135
+ return errorOutcome(startedAt, diffOutcome.reason);
136
+ }
137
+ const diff = diffOutcome.diff;
138
+ const manifestPath = join(repoRoot, "gates.manifest.json");
139
+ const manifest = loadManifest(manifestPath);
140
+ if (!manifest.ok) {
141
+ return errorOutcome(startedAt, `the destructiveCommands list could not be read (rule (e) needs it): ` +
142
+ `${manifest.reason}${manifest.diagnostics.length > 0 ? `: ${manifest.diagnostics.join("; ")}` : ""}`);
143
+ }
144
+ const behaviorsShown = gitIn(repoRoot, [
145
+ "show",
146
+ `${diff.headSha}:test/behaviors.json`,
147
+ ]);
148
+ if (!behaviorsShown.ok) {
149
+ return errorOutcome(startedAt, `test/behaviors.json could not be read at the audited head: ${behaviorsShown.reason}`);
150
+ }
151
+ let behaviors;
152
+ try {
153
+ behaviors = new Set(Object.keys(JSON.parse(behaviorsShown.stdout)));
154
+ }
155
+ catch (error) {
156
+ return errorOutcome(startedAt, `test/behaviors.json does not parse at the audited head: ${singleLine(String(error))}`);
157
+ }
158
+ const testFilesOutcome = readTestFilesAtHead(repoRoot, diff.headSha);
159
+ if (!testFilesOutcome.ok) {
160
+ return errorOutcome(startedAt, testFilesOutcome.reason);
161
+ }
162
+ // Rule (f)'s derivation: the spawn grep over the changed files' head
163
+ // contents (deleted files have no head content and cannot be touched by a
164
+ // member either). Shell scripts (*.sh) are screened by the shell
165
+ // spawn-and-parse derivation instead of the JS token grep (CR-H2), so a
166
+ // bin/ shell script that classifies another program's output (the V-2
167
+ // shape) is not invisible to the capture obligation.
168
+ const spawningChangedFiles = [];
169
+ for (const [path, file] of diff.files) {
170
+ if (file.status === "D") {
171
+ continue;
172
+ }
173
+ const shown = gitIn(repoRoot, ["show", `${diff.headSha}:${path}`]);
174
+ if (!shown.ok) {
175
+ continue;
176
+ }
177
+ const spawns = path.endsWith(".sh")
178
+ ? shellSpawnsAndParses(shown.stdout)
179
+ : SPAWN_GREP.test(shown.stdout);
180
+ if (spawns) {
181
+ spawningChangedFiles.push(path);
182
+ }
183
+ }
184
+ spawningChangedFiles.sort();
185
+ const witnessDir = join(repoRoot, "witness");
186
+ const listing = listWitnessSpecFiles(witnessDir);
187
+ if (!listing.ok) {
188
+ return errorOutcome(startedAt, listing.reason);
189
+ }
190
+ const reasons = [];
191
+ const specs = [];
192
+ const seenIds = new Map();
193
+ for (const path of listing.paths) {
194
+ const loaded = loadWitnessSpec(path);
195
+ const repoRelative = relative(repoRoot, path).split("\\").join("/");
196
+ if (!loaded.ok) {
197
+ reasons.push(`${loaded.reason}${loaded.diagnostics.length > 0 ? `: ${loaded.diagnostics.join("; ")}` : ""}`);
198
+ continue;
199
+ }
200
+ const previous = seenIds.get(loaded.spec.id);
201
+ if (previous !== undefined) {
202
+ reasons.push(`witness id ${loaded.spec.id} is declared by both ${previous} and ${repoRelative}`);
203
+ continue;
204
+ }
205
+ seenIds.set(loaded.spec.id, repoRelative);
206
+ specs.push({ spec: loaded.spec, path, repoRelative });
207
+ }
208
+ const readPatchAtHead = (patchPath) => {
209
+ const shown = gitIn(repoRoot, ["show", `${diff.headSha}:${patchPath}`]);
210
+ return shown.ok ? shown.stdout : undefined;
211
+ };
212
+ const own = specs.filter((entry) => diff.files.has(entry.repoRelative));
213
+ const stored = specs.filter((entry) => !diff.files.has(entry.repoRelative));
214
+ /**
215
+ * A patch member's body at the MERGE BASE, the old side of the ownership
216
+ * comparison. `readPatchAtHead` above is the new side. Two readers rather
217
+ * than one because the whole point of reading the body is that the same path
218
+ * can hold different content on the two revisions.
219
+ */
220
+ const readPatchAtMergeBase = (patchPath) => {
221
+ const shown = gitIn(repoRoot, ["show", `${diff.mergeBaseSha}:${patchPath}`]);
222
+ return shown.ok ? shown.stdout : undefined;
223
+ };
224
+ /**
225
+ * The members of an own spec that THIS PHASE AUTHORED, which is rule (d)'s
226
+ * scope. Membership in `own` is file-granular ("some byte of this spec
227
+ * changed") and rule (d)'s obligation is member-granular ("this declared
228
+ * dangerous state must intersect the diff"), so the two are reconciled here
229
+ * rather than by handing the harness a boolean for the whole file.
230
+ *
231
+ * The old side is read at the MERGE BASE, which is the revision the diff
232
+ * itself is taken against. A baseline that is absent, unreadable or invalid
233
+ * yields `undefined`, and `phaseOwnedMemberIndices` then owns every member:
234
+ * an added spec is wholly the phase's, and so is one whose previous version
235
+ * cannot be established.
236
+ *
237
+ * The WHOLE spec goes in, not just its members, because the spec's claim
238
+ * (`behavior` and `tests`) is part of what rule (d) is an obligation on. See
239
+ * `claimRePointed` for which fields are in that set, why the other four are
240
+ * not, and why the comparison is DIRECTIONAL: re-pointing a claim takes the
241
+ * obligation, extending its named tests does not.
242
+ */
243
+ const ownedMembersOf = (entry) => {
244
+ const readers = { head: readPatchAtHead, baseline: readPatchAtMergeBase };
245
+ const shown = gitIn(repoRoot, [
246
+ "show",
247
+ `${diff.mergeBaseSha}:${entry.repoRelative}`,
248
+ ]);
249
+ if (!shown.ok) {
250
+ return phaseOwnedMemberIndices(entry.spec, undefined, readers);
251
+ }
252
+ const baseline = parseWitnessSpec(shown.stdout, `${diff.mergeBaseSha}:${entry.repoRelative}`);
253
+ return phaseOwnedMemberIndices(entry.spec, baseline.ok ? baseline.spec : undefined, readers);
254
+ };
255
+ const triggeredStored = stored.filter((entry) => entry.spec.dangerousStates.some((member) => memberTouchedFiles(member, readPatchAtHead).some((file) => diff.files.has(file))));
256
+ // Coverage (step 7): source changed with no witness spec covering it is
257
+ // red, never not-applicable. Coverage semantics are decision D-P2-2 in
258
+ // the work history.
259
+ const covered = new Set();
260
+ for (const entry of specs) {
261
+ for (const member of entry.spec.dangerousStates) {
262
+ for (const file of memberTouchedFiles(member, readPatchAtHead)) {
263
+ covered.add(file);
264
+ }
265
+ }
266
+ }
267
+ const uncovered = [];
268
+ for (const [path, file] of diff.files) {
269
+ if (file.status === "D" || !isAuditedSource(path)) {
270
+ continue;
271
+ }
272
+ if (!covered.has(path)) {
273
+ uncovered.push(path);
274
+ }
275
+ }
276
+ uncovered.sort();
277
+ if (uncovered.length > 0) {
278
+ reasons.push(`source changed with no witness spec covering it: ${uncovered.join(", ")}`);
279
+ }
280
+ const evaluations = [];
281
+ let reEvaluationMs = 0;
282
+ const scratchRoot = makeScratchRoot();
283
+ try {
284
+ const baseInputs = {
285
+ repoRoot,
286
+ headSha: diff.headSha,
287
+ baselineRef: run.baseline ?? run.base,
288
+ diff,
289
+ destructiveCommands: manifest.manifest.destructiveCommands,
290
+ behaviors,
291
+ testFiles: testFilesOutcome.files,
292
+ spawningChangedFiles,
293
+ scratchRoot,
294
+ };
295
+ for (const entry of own) {
296
+ const inputs = {
297
+ ...baseInputs,
298
+ phaseOwnedMembers: ownedMembersOf(entry),
299
+ };
300
+ if (run.hooks !== undefined) {
301
+ inputs.hooks = run.hooks;
302
+ }
303
+ const evaluation = evaluateWitness(entry.spec, entry.repoRelative, inputs);
304
+ evaluations.push(evaluation);
305
+ if (evaluation.status !== "green") {
306
+ reasons.push(`witness ${evaluation.witness}: ${evaluation.status}: ${evaluation.reasons.join("; ")}`);
307
+ }
308
+ }
309
+ const reEvaluationStart = Date.now();
310
+ for (const entry of triggeredStored) {
311
+ // A stored witness is one the phase diff does not touch at all, so no
312
+ // member of it is the phase's and rule (d) has nothing to apply to.
313
+ const inputs = {
314
+ ...baseInputs,
315
+ phaseOwnedMembers: new Set(),
316
+ };
317
+ if (run.hooks !== undefined) {
318
+ inputs.hooks = run.hooks;
319
+ }
320
+ const evaluation = evaluateWitness(entry.spec, entry.repoRelative, inputs);
321
+ evaluations.push(evaluation);
322
+ if (evaluation.status === "red") {
323
+ const rates = evaluation.members
324
+ .filter((member) => member.rate !== undefined)
325
+ .map((member) => `member ${String(member.index)} red ` +
326
+ `${String(member.rate?.red)}/${String(member.rate?.total)}`)
327
+ .join(", ");
328
+ reasons.push(`witness ${evaluation.witness} no longer guards its behavior ` +
329
+ `(${rates === "" ? evaluation.reasons.join("; ") : rates})`);
330
+ }
331
+ else if (evaluation.status === "error") {
332
+ reasons.push(`witness ${evaluation.witness}: error: ${evaluation.reasons.join("; ")}`);
333
+ }
334
+ }
335
+ reEvaluationMs = Date.now() - reEvaluationStart;
336
+ }
337
+ finally {
338
+ removeScratchRoot(scratchRoot);
339
+ }
340
+ let status = "green";
341
+ if (evaluations.some((evaluation) => evaluation.status === "error")) {
342
+ status = "error";
343
+ }
344
+ else if (reasons.length > 0) {
345
+ status = "red";
346
+ }
347
+ const evidence = [];
348
+ if (run.evidenceDir !== undefined) {
349
+ const recordsPath = join(run.evidenceDir, "witness-records.json");
350
+ const refusal = refuseOpenForWrite(recordsPath);
351
+ if (refusal === undefined) {
352
+ try {
353
+ mkdirSync(run.evidenceDir, { recursive: true });
354
+ writeFileSync(recordsPath, `${JSON.stringify({
355
+ base: diff.baseSha,
356
+ head: diff.headSha,
357
+ spawningChangedFiles,
358
+ uncoveredSources: uncovered,
359
+ reEvaluationMs,
360
+ evaluations,
361
+ }, null, 2)}\n`);
362
+ evidence.push("witness-records.json");
363
+ }
364
+ catch {
365
+ // The record still carries the verdict; evidence is best-effort.
366
+ }
367
+ }
368
+ }
369
+ const detail = `${String(evaluations.length)} witness(es) evaluated ` +
370
+ `(${String(own.length)} own, ${String(triggeredStored.length)} stored ` +
371
+ `re-evaluated in ${String(reEvaluationMs)}ms); ` +
372
+ (reasons.length === 0
373
+ ? "every witness red against every declared dangerous state and green at head"
374
+ : reasons.join("; "));
375
+ const result = makeGateResult({
376
+ gate: "red-witness",
377
+ status,
378
+ units: evaluations.length,
379
+ unitLabel: "witnesses evaluated",
380
+ startedAt,
381
+ endedAt: now(),
382
+ detail,
383
+ evidence,
384
+ });
385
+ return {
386
+ result,
387
+ exitCode: exitCodeForStatus(result.status),
388
+ evaluations,
389
+ reEvaluationMs,
390
+ };
391
+ }
392
+ function main(argv) {
393
+ const parsed = parseArgs(argv);
394
+ if (parsed.options === undefined) {
395
+ process.stderr.write(`tiphys red-witness: ${parsed.usageError}\n${USAGE}\n`);
396
+ return 64;
397
+ }
398
+ const options = parsed.options;
399
+ if (options.result === undefined) {
400
+ process.stderr.write(`tiphys red-witness: --result is required\n${USAGE}\n`);
401
+ return 64;
402
+ }
403
+ let outcome;
404
+ if (options.base === undefined) {
405
+ // M2-C-3 (M2R-003): a required invocation parameter absent is error,
406
+ // never not-applicable and never a guess.
407
+ outcome = errorOutcome(now(), "--base was not supplied; the phase diff cannot be computed (M2-C-3)");
408
+ }
409
+ else {
410
+ const run = { repoRoot: process.cwd(), base: options.base };
411
+ if (options.head !== undefined) {
412
+ run.head = options.head;
413
+ }
414
+ if (options.baseline !== undefined) {
415
+ run.baseline = options.baseline;
416
+ }
417
+ if (options.evidence !== undefined) {
418
+ run.evidenceDir = options.evidence;
419
+ }
420
+ try {
421
+ outcome = runRedWitnessGate(run);
422
+ }
423
+ catch (error) {
424
+ // No throw may escape as exit 1: that is the RED code (the runner's
425
+ // own crash-discipline rule, applied to this gate).
426
+ outcome = errorOutcome(now(), `the red-witness gate failed: ${singleLine(error.message ?? String(error))}`);
427
+ }
428
+ }
429
+ const refusal = refuseOpenForWrite(options.result);
430
+ if (refusal !== undefined) {
431
+ process.stderr.write(`tiphys red-witness: ${refusal}\n`);
432
+ return 21;
433
+ }
434
+ try {
435
+ writeFileSync(options.result, renderGateResult(outcome.result));
436
+ }
437
+ catch (error) {
438
+ process.stderr.write(`tiphys red-witness: the result record could not be written: ${singleLine(String(error))}\n`);
439
+ return 21;
440
+ }
441
+ process.stdout.write(`red-witness: ${outcome.result.status} (${outcome.result.detail})\n`);
442
+ return outcome.exitCode;
443
+ }
444
+ const invokedDirectly = (() => {
445
+ const entry = process.argv[1];
446
+ if (entry === undefined) {
447
+ return false;
448
+ }
449
+ // IDENTITY, NOT STRING EQUALITY (M4-P2 fix round, 2026-09-16). Going
450
+ // through a URL does not change what is compared: `resolve` leaves the
451
+ // caller's spelling intact while `import.meta.url` is canonical, so an
452
+ // invocation through a symlink leaves this gate silently not running.
453
+ return pathsIdentifySameObject(fileURLToPath(import.meta.url), entry);
454
+ })();
455
+ if (invokedDirectly) {
456
+ // `process.exitCode`, NEVER `process.exit(main(...))` (M4-P29). This gate
457
+ // runs as a SUBPROCESS, so fd 1 is a buffered stream the parent owns and a
458
+ // write to one is QUEUED rather than completed. `process.exit` ends the
459
+ // process without draining that queue, so everything past the buffer is
460
+ // DISCARDED, while the exit code survives and the loss is silent.
461
+ //
462
+ // BE EXACT ABOUT WHICH STREAM, because the ceiling differs by a factor of
463
+ // two and the obvious word is the wrong one. `spawnSync`, which is how the
464
+ // registry runner invokes gates (src/gates/run.ts:1528), hands a child
465
+ // SOCKETPAIRS, not pipes; a shell pipeline, a `tee` or a CI log collector
466
+ // hands it a real pipe. Measured at the pre-fix parent commit on this
467
+ // container, same invocation, only the reader changed:
468
+ //
469
+ // reader wrote delivered
470
+ // regular file 176,530 176,530
471
+ // shell pipe 176,530 65,536 (one pipe buffer)
472
+ // spawnSync 434,530 146,176 (the socket send buffer)
473
+ //
474
+ // A regular file never truncates, which is why this survives casual
475
+ // testing. This gate is the measured instance rather than a hypothetical
476
+ // one: its single stdout line carries the whole `detail`, and `detail`
477
+ // grows with the number of uncovered sources and of red witnesses.
478
+ // Assigning `process.exitCode` lets the process end normally, which drains
479
+ // the queue first, and the full capture is
480
+ // witness/captures/m4-p29-gate-cli-stdio.txt.
481
+ process.exitCode = main(process.argv.slice(2));
482
+ }