@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,340 @@
1
+ import { existsSync, writeFileSync } from "node:fs";
2
+ import { setTimeout as sleep } from "node:timers/promises";
3
+ import { EX_USAGE } from "../cli.js";
4
+ import { loadFleet, refuseOpenPathForWrite } from "../fleet.js";
5
+ import { acquireLease, leaseStatus, observeLease, releaseLease, renewLease, } from "../lock.js";
6
+ /**
7
+ * tiphys lock <acquire [--take-over] [--duration <seconds>] | renew
8
+ * --holder <id> [--duration <seconds>] | release --holder <id> | status>
9
+ * (kernel plan v1, M1-P3 step 2). Runs in a fleet home (cwd). The holder
10
+ * identity for renew and release is the opaque holderId that acquire
11
+ * printed; it outlives any one command invocation (FM-022) and is never
12
+ * derived from the running program (plan constraint C-2).
13
+ */
14
+ const USAGE = "usage: tiphys lock <acquire [--take-over] [--duration <seconds>] | " +
15
+ "renew --holder <id> [--duration <seconds>] | " +
16
+ "release --holder <id> | status>";
17
+ /**
18
+ * Deterministic race-witness hold point (the plan's test determinism
19
+ * rule: scripted interleaves over sleep-based timing). When
20
+ * TIPHYS_LOCK_TEST_HOLD names a barrier path, the mutating subcommands
21
+ * observe the lease and freeze their decision clock first, write
22
+ * <barrier>.observed as a ready marker, then wait for the barrier file
23
+ * to appear before deciding and applying through the one mutation
24
+ * primitive. A test can thereby interleave two real CLI invocations at
25
+ * the exact compare-and-swap point. Inert unless the variable is set.
26
+ *
27
+ * The seam is LOUD about not holding (D-3). It previously had two exits
28
+ * from its wait and only one of them meant "held": on the other the
29
+ * command walked into the mutation carrying a stale observation and
30
+ * told nobody, so a witness could score a compare-and-swap it never
31
+ * staged and still pass. Both exits are now closed with a throw, the
32
+ * bound is monotonic (process.hrtime.bigint, not Date.now, because a
33
+ * realtime bound mixed with monotonic test durations is precisely what
34
+ * made "it failed fast" look like proof the bound was not reached), and
35
+ * the reason the wait ended is written to <barrier>.released so the
36
+ * test can assert the interleave actually happened. This is a test
37
+ * integrity fix; it is NOT a fix for the unattributed U-2 flake, whose
38
+ * trigger remains unexplained.
39
+ */
40
+ const HOLD_WAIT_LIMIT_MS = 30_000;
41
+ async function maybeHoldForTest(lockPath) {
42
+ const barrier = process.env.TIPHYS_LOCK_TEST_HOLD;
43
+ if (barrier === undefined || barrier === "") {
44
+ return undefined;
45
+ }
46
+ if (existsSync(barrier)) {
47
+ throw new Error(`lock test hold point: barrier ${barrier} already existed before the ` +
48
+ `hold; this interleave was never staged and the run is not evidence`);
49
+ }
50
+ const observed = observeLease(lockPath);
51
+ const nowMs = Date.now();
52
+ /* THE MARKER PATHS ARE DERIVED FROM A CALLER-SUPPLIED PATH, so their entry
53
+ type is established before either is opened for writing. A named pipe at
54
+ `<barrier>.observed` blocked this seam forever with zero output, which is
55
+ the same class this round closed in the lease and brief readers; the seam
56
+ is inert unless the variable is set, which changes how it is REACHED and
57
+ not what it does once reached. */
58
+ const observedRefusal = refuseOpenPathForWrite(`${barrier}.observed`);
59
+ if (observedRefusal !== undefined) {
60
+ throw new Error(`lock test hold point: ${observedRefusal}`);
61
+ }
62
+ writeFileSync(`${barrier}.observed`, "");
63
+ const startNs = process.hrtime.bigint();
64
+ const limitNs = BigInt(HOLD_WAIT_LIMIT_MS) * 1000000n;
65
+ let held = false;
66
+ for (;;) {
67
+ if (existsSync(barrier)) {
68
+ held = true;
69
+ break;
70
+ }
71
+ if (process.hrtime.bigint() - startNs >= limitNs) {
72
+ break;
73
+ }
74
+ await sleep(10);
75
+ }
76
+ const waitedMs = Number((process.hrtime.bigint() - startNs) / 1000000n);
77
+ if (!held) {
78
+ throw new Error(`lock test hold point: barrier ${barrier} never appeared within ` +
79
+ `${String(HOLD_WAIT_LIMIT_MS)}ms (waited ${String(waitedMs)}ms on the ` +
80
+ `monotonic clock); this interleave was never staged and the run is ` +
81
+ `not evidence`);
82
+ }
83
+ // Record that the hold really held, and why the wait ended, so the
84
+ // witness can assert the interleave rather than assume it.
85
+ /* Re-established immediately before this second write rather than once
86
+ per call: the two writes are separated by a wait of up to thirty seconds
87
+ and the path is derived from one the CALLER supplied. */
88
+ const releasedRefusal = refuseOpenPathForWrite(`${barrier}.released`);
89
+ if (releasedRefusal !== undefined) {
90
+ throw new Error(`lock test hold point: ${releasedRefusal}`);
91
+ }
92
+ writeFileSync(`${barrier}.released`, `held after ${String(waitedMs)}ms (monotonic), barrier observed\n`);
93
+ return { observed, nowMs };
94
+ }
95
+ /**
96
+ * THE DECISION-CLOCK SEAM (M4-P21 criteria 4 and 5). When
97
+ * TIPHYS_LOCK_TEST_NOW_MS carries a finite number of milliseconds, every
98
+ * mutating subcommand decides against that instant instead of `Date.now()`,
99
+ * by feeding the EXISTING `nowMs` option the lease library already takes
100
+ * (src/lock.ts:568). It exists because the two clock-skew witnesses must
101
+ * move ONE environment's clock ten minutes without touching a system clock,
102
+ * which the plan requires by name, and because criterion 6 is about what the
103
+ * COMMAND prints, so the skew has to reach the command rather than only the
104
+ * library. Inert unless the variable is set, exactly like the hold point
105
+ * above, and a value that does not parse is a loud refusal rather than a
106
+ * silent fall back to the real clock: a seam that quietly ignores its input
107
+ * would make a skew witness green while measuring no skew at all.
108
+ *
109
+ * AND IT IS GATED, BECAUSE IT SHIPS. `judgeByCounter` (src/exclusion.ts:553)
110
+ * computes `unchangedForMs = nowMs - previous.firstSeenMs`, so the module's
111
+ * promised "two readings of ONE clock" become one reading of a clock the
112
+ * CALLER supplies. Measured in the published CLI before this gate existed:
113
+ * the honest challenger was refused with "fencing counter 1 has stood still
114
+ * for 0ms of the 900000ms this environment requires", and the SAME command
115
+ * with this one variable set an hour ahead took a live lease over instantly
116
+ * and still printed `signal=counter`. A captured witness of that run was
117
+ * indistinguishable from an honest one.
118
+ *
119
+ * TWO CHANGES, and they are different in kind. The ALLOWANCE
120
+ * (TIPHYS_ALLOW_TEST_CLOCK=1) makes reaching the seam a declared act rather
121
+ * than a side effect of one environment variable. The LABEL makes the run
122
+ * SAY SO: `injectedClockSuffix` below is appended to every verdict line the
123
+ * shared layer prints, so the output of a measurement can never be quoted as
124
+ * the output of a real judgement. An allowance alone would leave the second
125
+ * hole open, which is why both are here.
126
+ */
127
+ export const TEST_CLOCK_ALLOWANCE = "TIPHYS_ALLOW_TEST_CLOCK";
128
+ function testClockMs() {
129
+ const raw = process.env.TIPHYS_LOCK_TEST_NOW_MS;
130
+ if (raw === undefined || raw === "") {
131
+ return undefined;
132
+ }
133
+ if (process.env[TEST_CLOCK_ALLOWANCE] !== "1") {
134
+ throw new Error(`lock test clock: TIPHYS_LOCK_TEST_NOW_MS is set and ` +
135
+ `${TEST_CLOCK_ALLOWANCE}=1 is not, so this run would have decided ` +
136
+ `cross-environment exclusion against a clock its caller supplied ` +
137
+ `while printing the same verdict an honest run prints; set ` +
138
+ `${TEST_CLOCK_ALLOWANCE}=1 to declare that this run is a measurement, ` +
139
+ `or unset TIPHYS_LOCK_TEST_NOW_MS`);
140
+ }
141
+ const parsed = Number(raw);
142
+ if (!Number.isFinite(parsed)) {
143
+ throw new Error(`lock test clock: TIPHYS_LOCK_TEST_NOW_MS=${raw} does not parse as a ` +
144
+ `number of milliseconds; this run would have measured the real clock ` +
145
+ `and is not evidence`);
146
+ }
147
+ return parsed;
148
+ }
149
+ /**
150
+ * What a verdict line gains when the decision clock was injected. Empty on
151
+ * every honest run, so no shipped output changes for anyone who does not set
152
+ * the variable.
153
+ */
154
+ function injectedClockSuffix() {
155
+ return process.env.TIPHYS_LOCK_TEST_NOW_MS === undefined ||
156
+ process.env.TIPHYS_LOCK_TEST_NOW_MS === ""
157
+ ? ""
158
+ : "(injected-clock)";
159
+ }
160
+ /**
161
+ * Emit the shared exclusion layer's verdict line. One line, and it always
162
+ * names the SIGNAL that reached the verdict (criterion 6): `signal=counter`
163
+ * where the register was reachable and its fencing counter decided, or
164
+ * `signal=clock` where it was not and the command therefore refused rather
165
+ * than deciding cross-environment exclusion on a local clock.
166
+ */
167
+ function reportShared(outcome) {
168
+ if (outcome.shared !== undefined) {
169
+ process.stdout.write(`${labelClock(outcome.shared.line)}\n`);
170
+ }
171
+ }
172
+ /**
173
+ * Mark every `signal=<basis>` token in a verdict line when this run's
174
+ * decision clock was injected. The token is what criterion 6 asks the
175
+ * command to print, so it is the token that has to carry the caveat.
176
+ */
177
+ function labelClock(line) {
178
+ const suffix = injectedClockSuffix();
179
+ return suffix === "" ? line : line.replace(/signal=(counter|clock)/g, `signal=$1${suffix}`);
180
+ }
181
+ function usageError(message) {
182
+ if (message !== undefined) {
183
+ process.stderr.write(`tiphys lock: ${message}\n`);
184
+ }
185
+ process.stderr.write(`${USAGE}\n`);
186
+ return EX_USAGE;
187
+ }
188
+ /**
189
+ * The one failure emitter for acquire, renew, and release (CR-204).
190
+ * When the library classified the failure as a stale mutation claim,
191
+ * the operator is pointed at the claim file rather than at a holder
192
+ * that may not exist; the remedy text lives here, so a CLI that
193
+ * ignored the classification would visibly lose it. Operational
194
+ * failures exit 1 (usage errors alone use EX_USAGE).
195
+ */
196
+ function failure(outcome) {
197
+ // D-2: the claim file is the sole serializer of lock mutations, so
198
+ // deleting one that is actually live lets a second mutation enter the
199
+ // critical section and can produce two live holders. The remedy text
200
+ // therefore states that cost instead of inviting the deletion, and it
201
+ // names the safe order: make sure no tiphys process is running first.
202
+ const remedy = outcome.claimTimeout === true
203
+ ? "; a crashed mutation can leave this file behind, but deleting it while a mutation is genuinely in flight can produce two lock holders, so confirm no tiphys process is running against this fleet before removing it"
204
+ : "";
205
+ process.stderr.write(`tiphys lock: ${labelClock(outcome.reason)}${remedy}\n`);
206
+ return 1;
207
+ }
208
+ function parseFlags(rest) {
209
+ const parsed = {
210
+ takeover: false,
211
+ holder: undefined,
212
+ durationSeconds: undefined,
213
+ };
214
+ for (let i = 0; i < rest.length; i += 1) {
215
+ const flag = rest[i];
216
+ if (flag === "--take-over") {
217
+ parsed.takeover = true;
218
+ }
219
+ else if (flag === "--holder" && i + 1 < rest.length) {
220
+ parsed.holder = rest[i + 1];
221
+ i += 1;
222
+ }
223
+ else if (flag === "--duration" && i + 1 < rest.length) {
224
+ const seconds = Number(rest[i + 1]);
225
+ if (!Number.isFinite(seconds) || seconds <= 0) {
226
+ return undefined;
227
+ }
228
+ parsed.durationSeconds = seconds;
229
+ i += 1;
230
+ }
231
+ else {
232
+ return undefined;
233
+ }
234
+ }
235
+ return parsed;
236
+ }
237
+ export async function cmdLock(args) {
238
+ const [subcommand, ...rest] = args;
239
+ if (subcommand === undefined) {
240
+ return usageError();
241
+ }
242
+ const flags = parseFlags(rest);
243
+ if (flags === undefined) {
244
+ return usageError();
245
+ }
246
+ let lockPath;
247
+ try {
248
+ lockPath = loadFleet(process.cwd()).lockPath;
249
+ }
250
+ catch (error) {
251
+ process.stderr.write(`tiphys lock: ${error.message}\n`);
252
+ return 1;
253
+ }
254
+ switch (subcommand) {
255
+ case "acquire": {
256
+ if (flags.holder !== undefined) {
257
+ return usageError("acquire does not take --holder");
258
+ }
259
+ const held = await maybeHoldForTest(lockPath);
260
+ const outcome = await acquireLease(lockPath, {
261
+ takeover: flags.takeover,
262
+ durationSeconds: flags.durationSeconds,
263
+ observed: held?.observed,
264
+ nowMs: testClockMs() ?? held?.nowMs,
265
+ });
266
+ if (!outcome.ok) {
267
+ return failure(outcome);
268
+ }
269
+ const lease = outcome.lease;
270
+ if (lease === null) {
271
+ throw new Error("unreachable: acquire produced no lease");
272
+ }
273
+ process.stdout.write(`acquired ${lease.holderId} expires ${lease.expiresAt}\n`);
274
+ reportShared(outcome);
275
+ return 0;
276
+ }
277
+ case "renew": {
278
+ if (flags.holder === undefined || flags.takeover) {
279
+ return usageError("renew requires --holder <id>");
280
+ }
281
+ const held = await maybeHoldForTest(lockPath);
282
+ const outcome = await renewLease(lockPath, flags.holder, {
283
+ durationSeconds: flags.durationSeconds,
284
+ observed: held?.observed,
285
+ nowMs: testClockMs() ?? held?.nowMs,
286
+ });
287
+ if (!outcome.ok) {
288
+ return failure(outcome);
289
+ }
290
+ const lease = outcome.lease;
291
+ if (lease === null) {
292
+ throw new Error("unreachable: renew produced no lease");
293
+ }
294
+ process.stdout.write(`renewed ${lease.holderId} expires ${lease.expiresAt}\n`);
295
+ reportShared(outcome);
296
+ return 0;
297
+ }
298
+ case "release": {
299
+ if (flags.holder === undefined || flags.takeover || flags.durationSeconds !== undefined) {
300
+ return usageError("release requires --holder <id> and no other flags");
301
+ }
302
+ const held = await maybeHoldForTest(lockPath);
303
+ const releaseOptions = {
304
+ observed: held?.observed,
305
+ };
306
+ const releaseClock = testClockMs();
307
+ if (releaseClock !== undefined) {
308
+ releaseOptions.nowMs = releaseClock;
309
+ }
310
+ const outcome = await releaseLease(lockPath, flags.holder, releaseOptions);
311
+ if (!outcome.ok) {
312
+ return failure(outcome);
313
+ }
314
+ process.stdout.write(`released ${flags.holder}\n`);
315
+ reportShared(outcome);
316
+ return 0;
317
+ }
318
+ case "status": {
319
+ if (flags.holder !== undefined || flags.takeover || flags.durationSeconds !== undefined) {
320
+ return usageError("status takes no flags");
321
+ }
322
+ const status = leaseStatus(lockPath);
323
+ if (status.state === "free") {
324
+ process.stdout.write("free\n");
325
+ }
326
+ else if (status.state === "corrupt") {
327
+ process.stdout.write(`corrupt ${status.detail}\n`);
328
+ }
329
+ else {
330
+ process.stdout.write(`${status.state} holder ${status.lease.holderId} acquired ` +
331
+ `${status.lease.acquiredAt} expires ${status.lease.expiresAt}\n`);
332
+ }
333
+ // status always exits 0 so a human takeover decision is informed
334
+ // by reading, never blocked by an exit code.
335
+ return 0;
336
+ }
337
+ default:
338
+ return usageError(`unknown lock subcommand "${subcommand}"`);
339
+ }
340
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * `tiphys mode show --mode <id> [--file <path>]` (kernel plan M3, M3-P3
3
+ * step 5; fix round 1 finding B-002).
4
+ *
5
+ * WHY THIS COMMAND EXISTS: a brief or a human has to be able to read what a
6
+ * declared mode requires without parsing YAML by hand. That is the whole
7
+ * scope. It shows; it does not resolve a project into a mode, does not
8
+ * enforce one, and does not run one.
9
+ *
10
+ * IT VALIDATES BEFORE IT SERVES, and that is the point of the fix round.
11
+ * As first shipped this command read the document and printed it, invoking
12
+ * neither the schema nor any registered check, so every guard the phase built
13
+ * was bypassed on the one path built for humans and briefs. The mechanism is
14
+ * A READER THAT DOES NOT VALIDATE WHAT IT READS, and the fix is not "detect
15
+ * one more bad state": it is that this command now runs exactly what
16
+ * `tiphys validate --type assurance-modes --context <dir>` runs, and REFUSES
17
+ * rather than printing when the document fails. A partial answer from an
18
+ * invalid document is worse than an error, because the reader cannot tell.
19
+ *
20
+ * THE CONTEXT IS THE DOCUMENT'S OWN DIRECTORY. The cross-document checks
21
+ * resolve `gate-registry.yaml` and `schemas/charter.schema.json` beside the
22
+ * document, which is where the npm package puts them and where this repository
23
+ * keeps them. That makes the two `requiresContext` checks run here rather than
24
+ * SKIP, which matters: a skip fails the run, and a command that always failed
25
+ * would be a command nobody uses.
26
+ *
27
+ * Exit codes:
28
+ * 0 the document is valid and the mode was found and printed
29
+ * 1 the document could not be read, is invalid, or no mode carries that id
30
+ * 64 usage error (BSD sysexits EX_USAGE)
31
+ *
32
+ * A MODE ID THAT IS NOT DECLARED IS 1, NOT 64, and the difference is not
33
+ * pedantry. 64 means the caller used the command wrongly; 1 means the command
34
+ * worked and the answer is no. `--mode direct-pr` against a document that
35
+ * declares only `full` is a well-formed question with a negative answer, and a
36
+ * caller that cannot tell those apart cannot script this.
37
+ */
38
+ /** Exit code for usage errors, per BSD sysexits EX_USAGE. */
39
+ export declare const EX_USAGE = 64;
40
+ export declare function cmdMode(argv: string[]): number;
@@ -0,0 +1,134 @@
1
+ /**
2
+ * `tiphys mode show --mode <id> [--file <path>]` (kernel plan M3, M3-P3
3
+ * step 5; fix round 1 finding B-002).
4
+ *
5
+ * WHY THIS COMMAND EXISTS: a brief or a human has to be able to read what a
6
+ * declared mode requires without parsing YAML by hand. That is the whole
7
+ * scope. It shows; it does not resolve a project into a mode, does not
8
+ * enforce one, and does not run one.
9
+ *
10
+ * IT VALIDATES BEFORE IT SERVES, and that is the point of the fix round.
11
+ * As first shipped this command read the document and printed it, invoking
12
+ * neither the schema nor any registered check, so every guard the phase built
13
+ * was bypassed on the one path built for humans and briefs. The mechanism is
14
+ * A READER THAT DOES NOT VALIDATE WHAT IT READS, and the fix is not "detect
15
+ * one more bad state": it is that this command now runs exactly what
16
+ * `tiphys validate --type assurance-modes --context <dir>` runs, and REFUSES
17
+ * rather than printing when the document fails. A partial answer from an
18
+ * invalid document is worse than an error, because the reader cannot tell.
19
+ *
20
+ * THE CONTEXT IS THE DOCUMENT'S OWN DIRECTORY. The cross-document checks
21
+ * resolve `gate-registry.yaml` and `schemas/charter.schema.json` beside the
22
+ * document, which is where the npm package puts them and where this repository
23
+ * keeps them. That makes the two `requiresContext` checks run here rather than
24
+ * SKIP, which matters: a skip fails the run, and a command that always failed
25
+ * would be a command nobody uses.
26
+ *
27
+ * Exit codes:
28
+ * 0 the document is valid and the mode was found and printed
29
+ * 1 the document could not be read, is invalid, or no mode carries that id
30
+ * 64 usage error (BSD sysexits EX_USAGE)
31
+ *
32
+ * A MODE ID THAT IS NOT DECLARED IS 1, NOT 64, and the difference is not
33
+ * pedantry. 64 means the caller used the command wrongly; 1 means the command
34
+ * worked and the answer is no. `--mode direct-pr` against a document that
35
+ * declares only `full` is a well-formed question with a negative answer, and a
36
+ * caller that cannot tell those apart cannot script this.
37
+ */
38
+ import { dirname } from "node:path";
39
+ import { runChecks } from "../checks.js";
40
+ import { formatDiagnostics, validateInstance } from "../validate.js";
41
+ import { loadTypeSchema } from "./validate.js";
42
+ import { readModes, renderMode } from "../modes.js";
43
+ /** The artifact type this command reads, and the type its checks are registered for. */
44
+ const MODES_TYPE = "assurance-modes";
45
+ /** Exit code for usage errors, per BSD sysexits EX_USAGE. */
46
+ export const EX_USAGE = 64;
47
+ function usage() {
48
+ return "usage: tiphys mode show --mode <id> [--file <path>]";
49
+ }
50
+ function parseArgs(argv) {
51
+ const options = {};
52
+ for (let index = 0; index < argv.length; index += 1) {
53
+ const argument = argv[index];
54
+ if (argument === "--mode" || argument === "--file") {
55
+ const value = argv[index + 1];
56
+ if (value === undefined || value.startsWith("--")) {
57
+ return { usageError: `${argument} requires a value` };
58
+ }
59
+ if (argument === "--mode") {
60
+ options.mode = value;
61
+ }
62
+ else {
63
+ options.file = value;
64
+ }
65
+ index += 1;
66
+ continue;
67
+ }
68
+ return { usageError: `unknown argument ${argument}` };
69
+ }
70
+ if (options.mode === undefined) {
71
+ return { usageError: "--mode is required" };
72
+ }
73
+ return { options };
74
+ }
75
+ function fail(reason, code) {
76
+ process.stderr.write(`tiphys mode: ${reason}\n`);
77
+ if (code === EX_USAGE) {
78
+ process.stderr.write(`${usage()}\n`);
79
+ }
80
+ return code;
81
+ }
82
+ export function cmdMode(argv) {
83
+ const [subcommand, ...rest] = argv;
84
+ if (subcommand !== "show") {
85
+ return fail(subcommand === undefined
86
+ ? "a subcommand is required"
87
+ : `unknown subcommand ${subcommand}`, EX_USAGE);
88
+ }
89
+ const parsed = parseArgs(rest);
90
+ if (parsed.options === undefined) {
91
+ return fail(parsed.usageError ?? "usage error", EX_USAGE);
92
+ }
93
+ let read;
94
+ try {
95
+ read = parsed.options.file === undefined ? readModes() : readModes(parsed.options.file);
96
+ }
97
+ catch (error) {
98
+ /* `packageRoot` throws when the shipped document is not above this module,
99
+ which is an incomplete installation rather than a caller error. One line,
100
+ never a stack trace, the same policy `tiphys validate` applies to its own
101
+ operator-supplied input. */
102
+ return fail(String(error instanceof Error ? error.message : error), 1);
103
+ }
104
+ if (!read.ok) {
105
+ return fail(read.reason, 1);
106
+ }
107
+ /* VALIDATE, THEN SERVE. Schema first and derived checks second, in the same
108
+ order and through the same functions `tiphys validate` uses, so the two
109
+ commands cannot drift into disagreeing about what a valid document is. */
110
+ const lines = formatDiagnostics(validateInstance(loadTypeSchema(MODES_TYPE), read.raw));
111
+ const checks = runChecks(MODES_TYPE, read.raw, dirname(read.path));
112
+ if (lines.length > 0 || checks.failed) {
113
+ process.stderr.write(`tiphys mode: ${read.path} is not a valid ${MODES_TYPE} document, so it is not served\n`);
114
+ for (const line of [...lines, ...checks.lines]) {
115
+ process.stderr.write(`${line}\n`);
116
+ }
117
+ return 1;
118
+ }
119
+ const wanted = parsed.options.mode;
120
+ const mode = read.modes.find((candidate) => candidate.id === wanted);
121
+ if (mode === undefined) {
122
+ const declared = read.modes.map((candidate) => candidate.id).sort();
123
+ return fail(`${read.path} declares no mode ${wanted}; it declares ${declared.length === 0 ? "none" : declared.join(", ")}`, 1);
124
+ }
125
+ /* WHICH DOCUMENT THIS IS, derived from the invocation rather than from the
126
+ path: no `--file` means `readModes()` walked to the package root and read
127
+ the kernel's own document. CR-004 item 2 turns on that distinction, because
128
+ "no phase has been delivered under this mode" is a claim about the kernel's
129
+ own delivery and is not knowable for a document a consumer supplied. */
130
+ for (const line of renderMode(mode, { shippedDocument: parsed.options.file === undefined })) {
131
+ process.stdout.write(`${line}\n`);
132
+ }
133
+ return 0;
134
+ }
@@ -0,0 +1,130 @@
1
+ import type { Fleet } from "../fleet.ts";
2
+ /**
3
+ * Work remains. DISTINCT FROM 1, which is the command failing, and from 0,
4
+ * which is every in-flight category empty (criterion 1).
5
+ */
6
+ export declare const EXIT_WORK_REMAINS = 3;
7
+ /**
8
+ * WHAT THIS COMMAND CANNOT SEE, as a NAMED LIST (criterion 4).
9
+ *
10
+ * Printed unconditionally. It is not derived from a probe and it does not
11
+ * shorten when something is unreachable, because a list that shrinks when the
12
+ * network is gone tells the reader that fewer things are unknown at exactly
13
+ * the moment more of them are.
14
+ */
15
+ export declare const CANNOT_SEE: readonly string[];
16
+ /** The heading the cannot-see block is printed under. */
17
+ export declare const CANNOT_SEE_HEADING = "cannot see (this command has no network; an absence here is not evidence of absence):";
18
+ /** How a branch turned out to have been delivered, or why that is not settled. */
19
+ export type Delivery = {
20
+ kind: "delivered";
21
+ how: "ancestor" | "squash" | "patch-equivalent";
22
+ } | {
23
+ kind: "open";
24
+ } | {
25
+ kind: "unknown";
26
+ detail: string;
27
+ };
28
+ /**
29
+ * THE DELIVERED-ELSEWHERE PREDICATE (criterion 3).
30
+ *
31
+ * `git branch --merged` reports a squash-merged branch as UNMERGED, because
32
+ * the squash commit's sha is not the branch tip and the branch tip is not an
33
+ * ancestor of the base. This process squash-merges every phase, so under the
34
+ * naive implementation EVERY delivered branch reads as open forever and the
35
+ * stop condition never goes green.
36
+ *
37
+ * THE CITATION THAT STOOD HERE UNTIL ROUND 1 WAS WRONG, AND IT WAS WRONG IN
38
+ * THE SILENT DIRECTION. It read `delivery/STATE.md:54`, which resolves, which
39
+ * is why no gate reddened; the line it resolves to is about two merge blockers
40
+ * and says nothing about `git branch --merged`. Measured:
41
+ * `grep -n 'branch --merged' delivery/STATE.md` returns nothing, at this head
42
+ * and at the merge base, so the sentence is absent rather than moved. The
43
+ * claim itself is measured in this repository and the measurement is the
44
+ * citation now: witness/captures/next-delivered-elsewhere-git.txt records
45
+ * `git branch --merged main` listing neither the squash-merged branch nor the
46
+ * one delivered through another pull request. The same wrong citation is in
47
+ * the plan at delivery/plan/kernel-plan-m4.md:3239 and in this phase's work
48
+ * history; the work history is corrected, and the plan is an owner-approved
49
+ * document this phase does not edit, so it is raised rather than changed.
50
+ *
51
+ * THREE ARMS, AND THE THIRD IS WHY THIS IS NOT JUST A CALL TO `landedness`.
52
+ *
53
+ * 1. ANCESTOR. An ordinary merge or a fast-forward. `landedness` arm one.
54
+ * 2. CONTENT. Merging the branch into the base changes no tree, so the base
55
+ * already carries everything the branch did however it got there. This is
56
+ * MEMBER A, the squash merge, and it is `landedness` arm two.
57
+ * 3. PATCH EQUIVALENCE. Every commit the branch carries has an equivalent
58
+ * patch already upstream, found by patch id rather than by sha. This is
59
+ * MEMBER B: commits that landed inside ANOTHER branch's pull request, so
60
+ * they appear on the base interleaved with commits the branch never had.
61
+ *
62
+ * ARM 3 IS NOT REDUNDANT WITH ARM 2, and the case that separates them is the
63
+ * ordinary one. Once the base moves on and edits the same region again, a
64
+ * three-way merge of the delivered branch CONFLICTS, so arm 2 answers `open`
65
+ * for a branch whose every commit is demonstrably already upstream. Arm 3
66
+ * answers it by patch id, which survives the later edit.
67
+ *
68
+ * ARM 2 IS NOT REDUNDANT WITH ARM 3 EITHER. A squash collapses N commits into
69
+ * one, whose patch id is the id of the COMBINED diff and therefore equals no
70
+ * individual commit's. For any branch of more than one commit, arm 3 sees only
71
+ * unmatched commits and answers `open`.
72
+ *
73
+ * REUSE, NOT A SECOND IMPLEMENTATION (T-005). Arms 1 and 2 are
74
+ * `landedness` at src/teardown.ts:123, the predicate `tiphys teardown` already
75
+ * refuses on. A second copy here would be a second thing to keep true.
76
+ *
77
+ * INCONCLUSIVE IS NOT `open` AND IT IS NOT `delivered`. A git failure returns
78
+ * `unknown`, which the caller counts as work remaining, because the one answer
79
+ * this predicate must never invent is a confident one.
80
+ */
81
+ export declare function deliveredElsewhere(contextDir: string, branchRef: string, baseRef: string): Delivery;
82
+ /** One thing that is in flight, in the category that found it. */
83
+ export interface InFlightItem {
84
+ category: "task" | "worktree" | "branch";
85
+ what: string;
86
+ }
87
+ /**
88
+ * Everything in flight in a fleet home, plus every candidate left unread, plus
89
+ * the disclosures a reader needs to check the judgement rather than trust it.
90
+ *
91
+ * `notes` DOES NOT COUNT TOWARD THE EXIT CODE and that is deliberate. It
92
+ * carries facts about HOW a judgement was made (which base ref each project's
93
+ * branches were compared against, and by which rule that base was chosen), not
94
+ * facts about whether work remains. Putting them in `unknown` would hold the
95
+ * exit at `EXIT_WORK_REMAINS` forever; leaving them out is what let a clone
96
+ * whose `origin/HEAD` points at a feature branch judge every branch against
97
+ * that branch with nothing in the output saying so.
98
+ */
99
+ export interface InFlight {
100
+ items: InFlightItem[];
101
+ unknown: string[];
102
+ notes: string[];
103
+ }
104
+ export declare function baseRefOf(projectDir: string): {
105
+ ok: true;
106
+ ref: string;
107
+ how: string;
108
+ doubt?: string;
109
+ } | {
110
+ ok: false;
111
+ reason: string;
112
+ };
113
+ /**
114
+ * Every in-flight category in a fleet home, in dependency order: the task
115
+ * record, the checkout it owns, and the branch that outlives both.
116
+ */
117
+ export declare function collectInFlight(fleet: Fleet): InFlight;
118
+ /**
119
+ * THE ONE NEXT ACTION (criterion 1). Exactly one, chosen by the first
120
+ * non-empty category in the order above, so the answer is derived from the
121
+ * report rather than picked.
122
+ */
123
+ export declare function nextAction(report: InFlight): string;
124
+ /** The cannot-see block. Unconditional, and it probes nothing to build. */
125
+ export declare function cannotSeeBlock(): string[];
126
+ /** The whole report, as lines, with exactly one `next action:` line. */
127
+ export declare function renderReport(fleet: Fleet, report: InFlight): string[];
128
+ /** Work remains whenever anything is in flight OR anything could not be read. */
129
+ export declare function exitCodeFor(report: InFlight): number;
130
+ export declare function cmdNext(args: string[]): number;