@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,475 @@
1
+ import { createHash } from "node:crypto";
2
+ import { fileURLToPath } from "node:url";
3
+ import { join } from "node:path";
4
+ import { readdirSync } from "node:fs";
5
+ import { classifyEntry, readRegularFileIfPresent } from "../task.js";
6
+ import { DIAGNOSTIC_MESSAGES, formatDiagnostics, loadSchema, validate, } from "../gates/validate.js";
7
+ export const WITNESS_CLASSES = [
8
+ "additive",
9
+ "destructive",
10
+ "classification",
11
+ ];
12
+ export const DEFAULT_REPEATS = 5;
13
+ const schemaUrl = new URL("../gates/schemas/witness-spec.schema.json", import.meta.url);
14
+ let cachedSchema;
15
+ /** The witness-spec schema document, loaded once through the closed-set loader. */
16
+ export function witnessSpecSchema() {
17
+ if (cachedSchema === undefined) {
18
+ const path = fileURLToPath(schemaUrl);
19
+ const read = readRegularFileIfPresent(path);
20
+ if (read.kind !== "read") {
21
+ throw new Error(read.kind === "absent"
22
+ ? `schema document ${path} is missing from this installation`
23
+ : read.reason);
24
+ }
25
+ const parsed = JSON.parse(read.body);
26
+ const loaded = loadSchema(parsed, "witness-spec.schema.json");
27
+ if (!loaded.ok) {
28
+ throw new Error(loaded.reason);
29
+ }
30
+ cachedSchema = loaded.schema;
31
+ }
32
+ return cachedSchema;
33
+ }
34
+ /** Absolute path of the schema document, for self-check style consumers. */
35
+ export function witnessSpecSchemaPath() {
36
+ return fileURLToPath(schemaUrl);
37
+ }
38
+ const MEMBER_FIELDS_FOR_KIND = {
39
+ "baseline-ref": ["ref"],
40
+ patch: ["patch"],
41
+ mutation: ["file", "find", "replace"],
42
+ };
43
+ const MEMBER_ALL_FIELDS = ["ref", "patch", "file", "find", "replace"];
44
+ function memberKindDiagnostics(document) {
45
+ const found = [];
46
+ const members = document.dangerousStates;
47
+ if (!Array.isArray(members)) {
48
+ return found;
49
+ }
50
+ for (let index = 0; index < members.length; index += 1) {
51
+ const member = members[index];
52
+ if (member === null || typeof member !== "object") {
53
+ continue;
54
+ }
55
+ const kind = member["kind"];
56
+ if (typeof kind !== "string" || MEMBER_FIELDS_FOR_KIND[kind] === undefined) {
57
+ continue;
58
+ }
59
+ const wanted = MEMBER_FIELDS_FOR_KIND[kind];
60
+ for (const field of wanted) {
61
+ if (member[field] === undefined) {
62
+ found.push({
63
+ pointer: `#/dangerousStates/${String(index)}/${field}`,
64
+ message: DIAGNOSTIC_MESSAGES.required(field),
65
+ });
66
+ }
67
+ }
68
+ for (const field of MEMBER_ALL_FIELDS) {
69
+ if (!wanted.includes(field) && member[field] !== undefined) {
70
+ found.push({
71
+ pointer: `#/dangerousStates/${String(index)}/${field}`,
72
+ message: DIAGNOSTIC_MESSAGES.additionalProperties(field),
73
+ });
74
+ }
75
+ }
76
+ }
77
+ return found;
78
+ }
79
+ /**
80
+ * Validate an already-parsed witness spec. Returns `INVALID <pointer>
81
+ * <message>` lines in the contract's deterministic order.
82
+ */
83
+ export function validateWitnessSpecDocument(document) {
84
+ const diagnostics = [
85
+ ...validate(witnessSpecSchema(), document),
86
+ ...memberKindDiagnostics(document),
87
+ ];
88
+ diagnostics.sort((a, b) => {
89
+ if (a.pointer !== b.pointer) {
90
+ return a.pointer < b.pointer ? -1 : 1;
91
+ }
92
+ if (a.message === b.message) {
93
+ return 0;
94
+ }
95
+ return a.message < b.message ? -1 : 1;
96
+ });
97
+ return formatDiagnostics(diagnostics);
98
+ }
99
+ /**
100
+ * Validate and materialise one spec from its BODY, with `label` naming the
101
+ * source in any reason. Split out of `loadWitnessSpec` so a spec can also be
102
+ * materialised from a git object (`git show <ref>:<path>`) rather than only
103
+ * from a working-tree path: the per-member ownership derivation below needs
104
+ * the merge-base version of a spec, which has no path.
105
+ */
106
+ export function parseWitnessSpec(body, label) {
107
+ let parsed;
108
+ try {
109
+ parsed = JSON.parse(body);
110
+ }
111
+ catch (error) {
112
+ return {
113
+ ok: false,
114
+ reason: `witness spec ${label} does not parse as JSON: ${error.message}`,
115
+ diagnostics: [],
116
+ };
117
+ }
118
+ const diagnostics = validateWitnessSpecDocument(parsed);
119
+ if (diagnostics.length > 0) {
120
+ return {
121
+ ok: false,
122
+ reason: `witness spec ${label} is not a valid witness spec`,
123
+ diagnostics,
124
+ };
125
+ }
126
+ const raw = parsed;
127
+ const spec = {
128
+ id: raw["id"],
129
+ behavior: raw["behavior"],
130
+ tests: [...raw["tests"]],
131
+ class: raw["class"],
132
+ dangerousStates: raw["dangerousStates"].map((member) => ({ ...member })),
133
+ deterministic: raw["deterministic"],
134
+ repeats: raw["repeats"] === undefined ? DEFAULT_REPEATS : raw["repeats"],
135
+ };
136
+ if (raw["consumesExternalOutput"] !== undefined) {
137
+ const consumes = raw["consumesExternalOutput"];
138
+ spec.consumesExternalOutput = {
139
+ program: consumes.program,
140
+ captures: [...consumes.captures],
141
+ provenance: consumes.provenance,
142
+ };
143
+ }
144
+ return {
145
+ ok: true,
146
+ spec,
147
+ sha256: createHash("sha256").update(body).digest("hex"),
148
+ body,
149
+ };
150
+ }
151
+ /** Load and validate one spec from a path the caller supplied (M2-C-6). */
152
+ export function loadWitnessSpec(path) {
153
+ const read = readRegularFileIfPresent(path);
154
+ if (read.kind === "absent") {
155
+ return {
156
+ ok: false,
157
+ reason: `witness spec ${path} does not exist`,
158
+ diagnostics: [],
159
+ };
160
+ }
161
+ if (read.kind === "refused") {
162
+ return { ok: false, reason: read.reason, diagnostics: [] };
163
+ }
164
+ return parseWitnessSpec(read.body, path);
165
+ }
166
+ /**
167
+ * CANONICAL FORM OF ONE DANGEROUS-STATE MEMBER: everything that determines the
168
+ * dangerous state, in a fixed order, JSON-encoded. Two members are the SAME
169
+ * member when their canonical forms are equal. Field order inside the source
170
+ * document is not significant, so reformatting a spec is not authorship.
171
+ *
172
+ * PER KIND, and the three kinds are NOT alike, which is the correction fix
173
+ * round 1 makes:
174
+ *
175
+ * - `mutation` declares `file`, `find` and `replace`, and all three are INLINE
176
+ * in the spec document. The canonical form carries all three, so it is
177
+ * complete: nothing outside the document can change what this member does.
178
+ * - `patch` declares one field, `patch`, and it is a POINTER. The dangerous
179
+ * state is the patch FILE'S BODY, which lives outside the spec document
180
+ * entirely. Keying on the path alone made a member whose body was rewritten
181
+ * top to bottom compare EQUAL to its own previous version, so a phase could
182
+ * turn a dangerous state about file X into one about an unrelated file Y and
183
+ * be judged not to have authored it. The body's sha256 is therefore part of
184
+ * the canonical form. An unreadable body yields `undefined`, which the caller
185
+ * treats as "cannot be established", never as "unchanged".
186
+ * - `baseline-ref` declares `ref`, which is ALSO a pointer: the dangerous state
187
+ * is the whole tree at that ref, and a ref moves. It is deliberately NOT
188
+ * resolved, for two reasons stated rather than assumed. First, rule (d)
189
+ * SKIPS `baseline-ref` members outright (src/witness/run.ts:1295), so a
190
+ * baseline-ref member's ownership has no consequence anywhere: ownership is
191
+ * read in exactly one place and that place skips this kind. Second, resolving
192
+ * the ref would attribute a ref moved by SOMEBODY ELSE to this phase, which
193
+ * is the opposite of what an authorship derivation should say. If rule (d)
194
+ * ever stops skipping this kind, this decision has to be revisited, and the
195
+ * fix would be the patch one: fold the resolved tree, not the ref name.
196
+ */
197
+ export function canonicalMember(member, readPatchBody) {
198
+ if (member.kind === "baseline-ref") {
199
+ return JSON.stringify(["baseline-ref", member.ref]);
200
+ }
201
+ if (member.kind === "mutation") {
202
+ return JSON.stringify(["mutation", member.file, member.find, member.replace]);
203
+ }
204
+ const body = readPatchBody(member.patch);
205
+ if (body === undefined) {
206
+ return undefined;
207
+ }
208
+ return JSON.stringify([
209
+ "patch",
210
+ member.patch,
211
+ createHash("sha256").update(body).digest("hex"),
212
+ ]);
213
+ }
214
+ /**
215
+ * HAS THE SPEC BEEN RE-POINTED, as opposed to strengthened?
216
+ *
217
+ * A witness spec says "these named TESTS guard this BEHAVIOR, and here are the
218
+ * dangerous states they have been shown red against". Rule (d) is an obligation
219
+ * on that sentence, not only on its members: it exists so a phase cannot claim
220
+ * coverage using a dangerous state about code it did not touch. So a phase that
221
+ * rewrites the sentence takes the obligation for every state offered under it.
222
+ *
223
+ * THE TEST IS DIRECTIONAL, NOT EQUALITY, AND THAT DISTINCTION IS THE WHOLE
224
+ * FUNCTION. Fix round 1 compared `[behavior, sortedTests]` for equality, which
225
+ * is wrong in the safe direction and was caught by delta verification: a phase
226
+ * that ADDS one guarding test to an existing spec, changing no member and no
227
+ * behavior, was told its untouched siblings had to intersect the diff. Adding a
228
+ * test is not a re-point. It is strictly strengthening, and that is a MEASURED
229
+ * property of the harness rather than a judgement:
230
+ *
231
+ * - RED arm, src/witness/run.ts:918: `red: exitCode !== 0 && failed.length
232
+ * === tests.length`. A repetition counts as red only when EVERY named test
233
+ * failed, so an added test must ALSO redden against every declared
234
+ * dangerous state or the member stops being red and the spec goes red.
235
+ * - GREEN arm, src/witness/run.ts:1676: `headGreen = exitCode === 0 &&
236
+ * passedNamedTests.length === spec.tests.length`. An added test must ALSO
237
+ * pass at head.
238
+ *
239
+ * Both arms gain an obligation, neither loses one, and both are demonstrated by
240
+ * EXECUTION rather than accepted on trust. So there is nothing for rule (d) to
241
+ * police in an extension: no new dangerous state is declared, and the existing
242
+ * states' burden only grows.
243
+ *
244
+ * WHAT STILL COUNTS AS RE-POINTING, and each is a real relaxation:
245
+ *
246
+ * - `behavior` changed. The sentence is now about something else, and an older
247
+ * phase's dangerous state becomes evidence for a behavior this phase
248
+ * introduced. Any change, in either direction.
249
+ * - A baseline test name DROPPED. That relaxes both arms above: one fewer test
250
+ * must redden against every member and one fewer must pass at head. A phase
251
+ * can drop the test that was doing the work and keep the coverage claim.
252
+ * - A test name SWAPPED, which is a drop plus an addition and is caught by the
253
+ * drop half.
254
+ *
255
+ * So the predicate is `behavior` equality AND `baselineTests` being a SUBSET of
256
+ * `headTests`. Reordering is not a change: the comparison is over sets, which
257
+ * is the same positional indifference the member matching gets from comparing
258
+ * canonical forms rather than indices. Duplicates in `tests` collapse under set
259
+ * semantics, and that is harmless because both arms above count `tests`
260
+ * positionally on the same array, so a duplicate adds an obligation to each
261
+ * side identically.
262
+ *
263
+ * WHY MEMBERS ARE NOT TREATED THE SAME WAY, since adding a member also only
264
+ * ADDS an obligation (every member must independently redden). Because a member
265
+ * IS the thing rule (d) is an obligation on, and a test is not. An added member
266
+ * is a newly declared dangerous state, and checking that a declared dangerous
267
+ * state relates to the phase's own diff is the entire purpose of the rule; make
268
+ * added members exempt and rule (d) is empty. An added test declares no
269
+ * dangerous state at all.
270
+ *
271
+ * WHICH FIELDS ARE CONSIDERED, AND WHY THE OTHERS ARE NOT. The test applied to
272
+ * every field of the closed schema was: does changing THIS FIELD ALONE let a
273
+ * phase assert something new about its own diff while reusing a dangerous state
274
+ * somebody else authored?
275
+ *
276
+ * - `behavior`, `tests`: YES, in the directional sense above. IN.
277
+ * - `class`: NO. It selects which refusal rules apply (rules (a), (e), (g)) and
278
+ * none of them is ownership-gated, so a class change is evaluated in full on
279
+ * every run whether the spec is owned or not. A weakened class is refused by
280
+ * rule (e), which DERIVES the class from the named tests' sources rather than
281
+ * trusting the declaration. OUT, and including it would buy false reds on
282
+ * class fix-ups while closing no attack.
283
+ * - `id`: NO. It is the handle, checked for collisions at
284
+ * src/gates/red-witness.ts:263. Renaming a spec asserts nothing new. OUT.
285
+ * - `deterministic`, `repeats`: NO. They set the red THRESHOLD in the member
286
+ * execution loop (src/witness/run.ts:1517 and src/witness/run.ts:1605), which
287
+ * runs for every member of every evaluated spec regardless of ownership. OUT.
288
+ * - `consumesExternalOutput`: NO. Rules (c) and (f) read it and neither is
289
+ * ownership-gated. OUT.
290
+ */
291
+ export function claimRePointed(headSpec, baselineSpec) {
292
+ if (headSpec.behavior !== baselineSpec.behavior) {
293
+ return true;
294
+ }
295
+ const headTests = new Set(headSpec.tests);
296
+ return baselineSpec.tests.some((name) => !headTests.has(name));
297
+ }
298
+ /**
299
+ * WHICH MEMBERS OF A SPEC THIS PHASE AUTHORED (the ownership scope of rule
300
+ * (d)).
301
+ *
302
+ * Rule (d) requires a declared dangerous state to intersect the phase diff, so
303
+ * that a phase cannot add a witness about unrelated code and claim coverage.
304
+ * That obligation is a claim about AUTHORSHIP, and authorship is per MEMBER.
305
+ * Deriving it from the spec FILE appearing in the diff is one granularity too
306
+ * coarse: it makes every sibling member of an edited file acquire an obligation
307
+ * its author never took on. Measured by the M3 exit test at stage E1.6, where
308
+ * repairing one member's quoted source line reddened two untouched members of
309
+ * the same file.
310
+ *
311
+ * THREE WAYS A PHASE AUTHORS, and the round-1 reviews found that only the first
312
+ * was implemented:
313
+ *
314
+ * 1. A member whose CANONICAL FORM changed, which now includes a patch body.
315
+ * 2. A member with no counterpart at the merge base. Matching is a MULTISET
316
+ * consume rather than a set membership test, so a second copy of an existing
317
+ * member is authored (rule (g) is what refuses that copy, and it must still
318
+ * see it as new).
319
+ * 3. THE CLAIM WAS RE-POINTED. Then every member is authored, because every
320
+ * declared dangerous state is now being offered as evidence for a sentence
321
+ * this phase wrote. There is no narrower attribution available: a claim
322
+ * change cannot be pinned on one member, since the claim is a property of
323
+ * the document. This is the ONLY whole-spec trigger, and keeping it that
324
+ * narrow is the point. An edit to a sibling member, a `repeats` bump, a
325
+ * reformat, a rename, and ADDING A GUARDING TEST all still author nothing,
326
+ * which is what the converse tests hold. `claimRePointed` carries the
327
+ * derivation of why extension is safe and dropping a test is not.
328
+ *
329
+ * `baselineSpec` is `undefined` when the spec did not exist at the merge base,
330
+ * did not parse there, or could not be read there. All three mean the phase is
331
+ * answerable for the whole file, so every member is owned. That is the
332
+ * conservative direction, and it is applied identically to an unreadable patch
333
+ * BODY on either side: the failure mode of this derivation is a member wrongly
334
+ * EXEMPTED, so anything the derivation cannot establish keeps the obligation
335
+ * rather than dropping it.
336
+ */
337
+ export function phaseOwnedMemberIndices(headSpec, baselineSpec, readers) {
338
+ const owned = new Set();
339
+ const ownEveryMember = () => {
340
+ for (let index = 0; index < headSpec.dangerousStates.length; index += 1) {
341
+ owned.add(index);
342
+ }
343
+ return owned;
344
+ };
345
+ if (baselineSpec === undefined) {
346
+ return ownEveryMember();
347
+ }
348
+ if (claimRePointed(headSpec, baselineSpec)) {
349
+ return ownEveryMember();
350
+ }
351
+ const remaining = new Map();
352
+ for (const member of baselineSpec.dangerousStates) {
353
+ const key = canonicalMember(member, readers.baseline);
354
+ if (key === undefined) {
355
+ // A baseline member whose patch body cannot be read is not established,
356
+ // so it matches nothing and cannot exempt a head member.
357
+ continue;
358
+ }
359
+ remaining.set(key, (remaining.get(key) ?? 0) + 1);
360
+ }
361
+ for (let index = 0; index < headSpec.dangerousStates.length; index += 1) {
362
+ const key = canonicalMember(headSpec.dangerousStates[index], readers.head);
363
+ if (key === undefined) {
364
+ // Same rule on the head side: a member whose body cannot be read is
365
+ // owned, never assumed unchanged.
366
+ owned.add(index);
367
+ continue;
368
+ }
369
+ const left = remaining.get(key) ?? 0;
370
+ if (left > 0) {
371
+ remaining.set(key, left - 1);
372
+ continue;
373
+ }
374
+ owned.add(index);
375
+ }
376
+ return owned;
377
+ }
378
+ /**
379
+ * The spec files of a witness directory: the `.json` entries directly inside
380
+ * it, sorted by name. Deliberately NOT recursive: the layout is one document
381
+ * per behavior at `witness/<behavior-id>.json` (M2-D-14), and capture files
382
+ * live in subdirectories that a recursive walk would misread as specs.
383
+ *
384
+ * An absent directory is an empty corpus, not an error: whether that is
385
+ * acceptable is the GATE's coverage decision, not a listing failure.
386
+ */
387
+ export function listWitnessSpecFiles(dir) {
388
+ const entry = classifyEntry(dir);
389
+ if (entry.kind === "absent" || entry.kind === "dangling") {
390
+ return { ok: true, paths: [] };
391
+ }
392
+ if (entry.kind === "unexaminable") {
393
+ return { ok: false, reason: entry.reason };
394
+ }
395
+ let names;
396
+ try {
397
+ names = readdirSync(dir);
398
+ }
399
+ catch (error) {
400
+ if (entry.kind === "regular") {
401
+ return {
402
+ ok: false,
403
+ reason: `${dir} is a regular file, not a witness directory`,
404
+ };
405
+ }
406
+ return {
407
+ ok: false,
408
+ reason: `witness directory ${dir} could not be listed: ${String(error)}`,
409
+ };
410
+ }
411
+ const paths = [];
412
+ for (const name of names.sort()) {
413
+ if (!name.endsWith(".json")) {
414
+ continue;
415
+ }
416
+ const path = join(dir, name);
417
+ const kind = classifyEntry(path);
418
+ if (kind.kind === "regular") {
419
+ paths.push(path);
420
+ continue;
421
+ }
422
+ if (kind.kind === "irregular" || kind.kind === "unexaminable") {
423
+ return { ok: false, reason: kind.reason };
424
+ }
425
+ }
426
+ return { ok: true, paths };
427
+ }
428
+ /**
429
+ * The repository-relative files a member's dangerous state touches.
430
+ * mutation: the named file. patch: the paths in the patch's own headers,
431
+ * parsed from `diff --git a/<x> b/<y>` lines with `+++`/`---` fallbacks.
432
+ * baseline-ref: none; an absent-feature baseline is not a file-level
433
+ * dangerous state (work-history decision D-P2-2).
434
+ */
435
+ export function memberTouchedFiles(member, readPatchBody) {
436
+ if (member.kind === "mutation") {
437
+ return [member.file];
438
+ }
439
+ if (member.kind === "baseline-ref") {
440
+ return [];
441
+ }
442
+ const body = readPatchBody(member.patch);
443
+ if (body === undefined) {
444
+ return [];
445
+ }
446
+ const files = new Set();
447
+ for (const line of body.split("\n")) {
448
+ const header = /^diff --git a\/(.+) b\/(.+)$/.exec(line);
449
+ if (header !== null) {
450
+ files.add(header[1]);
451
+ files.add(header[2]);
452
+ continue;
453
+ }
454
+ const plus = /^\+\+\+ b\/(.+)$/.exec(line);
455
+ if (plus !== null) {
456
+ files.add(plus[1]);
457
+ continue;
458
+ }
459
+ const minus = /^--- a\/(.+)$/.exec(line);
460
+ if (minus !== null) {
461
+ files.add(minus[1]);
462
+ }
463
+ }
464
+ return [...files].sort();
465
+ }
466
+ /** One-line description of a member, used in reasons and records. */
467
+ export function describeMember(member) {
468
+ if (member.kind === "baseline-ref") {
469
+ return `baseline-ref ${member.ref}`;
470
+ }
471
+ if (member.kind === "patch") {
472
+ return `patch ${member.patch}`;
473
+ }
474
+ return `mutation of ${member.file}`;
475
+ }