@tiphys/kernel 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/AGENTS.md +611 -0
  2. package/LICENSE +202 -0
  3. package/assurance-modes.yaml +278 -0
  4. package/checklists/clean-room.yaml +325 -0
  5. package/checklists/env-failure-diagnosis.yaml +68 -0
  6. package/checklists/flake-playbook.yaml +68 -0
  7. package/checklists/hazard-review.yaml +144 -0
  8. package/checklists/plan-review.yaml +103 -0
  9. package/dist/bin/tiphys.d.ts +2 -0
  10. package/dist/bin/tiphys.js +14 -0
  11. package/dist/src/brief.d.ts +32 -0
  12. package/dist/src/brief.js +59 -0
  13. package/dist/src/checklists.d.ts +169 -0
  14. package/dist/src/checklists.js +310 -0
  15. package/dist/src/checks.d.ts +828 -0
  16. package/dist/src/checks.js +3314 -0
  17. package/dist/src/cli.d.ts +9 -0
  18. package/dist/src/cli.js +57 -0
  19. package/dist/src/commands/brief.d.ts +92 -0
  20. package/dist/src/commands/brief.js +320 -0
  21. package/dist/src/commands/checklist.d.ts +42 -0
  22. package/dist/src/commands/checklist.js +168 -0
  23. package/dist/src/commands/doctor.d.ts +35 -0
  24. package/dist/src/commands/doctor.js +605 -0
  25. package/dist/src/commands/gates.d.ts +9 -0
  26. package/dist/src/commands/gates.js +360 -0
  27. package/dist/src/commands/init.d.ts +30 -0
  28. package/dist/src/commands/init.js +128 -0
  29. package/dist/src/commands/lock.d.ts +1 -0
  30. package/dist/src/commands/lock.js +229 -0
  31. package/dist/src/commands/mode.d.ts +40 -0
  32. package/dist/src/commands/mode.js +134 -0
  33. package/dist/src/commands/plan.d.ts +20 -0
  34. package/dist/src/commands/plan.js +105 -0
  35. package/dist/src/commands/pool.d.ts +1 -0
  36. package/dist/src/commands/pool.js +128 -0
  37. package/dist/src/commands/spawn.d.ts +1 -0
  38. package/dist/src/commands/spawn.js +146 -0
  39. package/dist/src/commands/status.d.ts +19 -0
  40. package/dist/src/commands/status.js +120 -0
  41. package/dist/src/commands/teardown.d.ts +1 -0
  42. package/dist/src/commands/teardown.js +79 -0
  43. package/dist/src/commands/tuition.d.ts +21 -0
  44. package/dist/src/commands/tuition.js +218 -0
  45. package/dist/src/commands/validate.d.ts +78 -0
  46. package/dist/src/commands/validate.js +360 -0
  47. package/dist/src/commands/watch.d.ts +1 -0
  48. package/dist/src/commands/watch.js +172 -0
  49. package/dist/src/exec/env.d.ts +128 -0
  50. package/dist/src/exec/env.js +190 -0
  51. package/dist/src/fleet.d.ts +51 -0
  52. package/dist/src/fleet.js +80 -0
  53. package/dist/src/gates/adapters/http-json.d.ts +5 -0
  54. package/dist/src/gates/adapters/http-json.js +283 -0
  55. package/dist/src/gates/adapters/migrations-command.d.ts +1 -0
  56. package/dist/src/gates/adapters/migrations-command.js +373 -0
  57. package/dist/src/gates/citations.d.ts +408 -0
  58. package/dist/src/gates/citations.js +1163 -0
  59. package/dist/src/gates/coverage.d.ts +284 -0
  60. package/dist/src/gates/coverage.js +701 -0
  61. package/dist/src/gates/credentials.d.ts +74 -0
  62. package/dist/src/gates/credentials.js +533 -0
  63. package/dist/src/gates/deploy.d.ts +1 -0
  64. package/dist/src/gates/deploy.js +33 -0
  65. package/dist/src/gates/manifest.d.ts +99 -0
  66. package/dist/src/gates/manifest.js +208 -0
  67. package/dist/src/gates/migrations.d.ts +1 -0
  68. package/dist/src/gates/migrations.js +36 -0
  69. package/dist/src/gates/pin.d.ts +114 -0
  70. package/dist/src/gates/pin.js +154 -0
  71. package/dist/src/gates/red-witness.d.ts +22 -0
  72. package/dist/src/gates/red-witness.js +390 -0
  73. package/dist/src/gates/release.d.ts +283 -0
  74. package/dist/src/gates/release.js +820 -0
  75. package/dist/src/gates/result.d.ts +116 -0
  76. package/dist/src/gates/result.js +91 -0
  77. package/dist/src/gates/run.d.ts +566 -0
  78. package/dist/src/gates/run.js +1536 -0
  79. package/dist/src/gates/schemas/citation-config.schema.json +59 -0
  80. package/dist/src/gates/schemas/coverage-config.schema.json +77 -0
  81. package/dist/src/gates/schemas/gate-manifest.schema.json +125 -0
  82. package/dist/src/gates/schemas/gate-result.schema.json +160 -0
  83. package/dist/src/gates/schemas/phase-declaration.schema.json +42 -0
  84. package/dist/src/gates/schemas/release-record.schema.json +119 -0
  85. package/dist/src/gates/schemas/verifier-config.schema.json +101 -0
  86. package/dist/src/gates/schemas/witness-spec.schema.json +110 -0
  87. package/dist/src/gates/scope.d.ts +131 -0
  88. package/dist/src/gates/scope.js +1018 -0
  89. package/dist/src/gates/suite.d.ts +217 -0
  90. package/dist/src/gates/suite.js +927 -0
  91. package/dist/src/gates/validate.d.ts +121 -0
  92. package/dist/src/gates/validate.js +414 -0
  93. package/dist/src/hooks.d.ts +32 -0
  94. package/dist/src/hooks.js +62 -0
  95. package/dist/src/liveness.d.ts +321 -0
  96. package/dist/src/liveness.js +396 -0
  97. package/dist/src/lock.d.ts +178 -0
  98. package/dist/src/lock.js +500 -0
  99. package/dist/src/modes.d.ts +149 -0
  100. package/dist/src/modes.js +258 -0
  101. package/dist/src/path-identity.d.ts +2 -0
  102. package/dist/src/path-identity.js +10 -0
  103. package/dist/src/plan.d.ts +73 -0
  104. package/dist/src/plan.js +153 -0
  105. package/dist/src/pool.d.ts +130 -0
  106. package/dist/src/pool.js +721 -0
  107. package/dist/src/roles.d.ts +430 -0
  108. package/dist/src/roles.js +734 -0
  109. package/dist/src/spawn.d.ts +177 -0
  110. package/dist/src/spawn.js +332 -0
  111. package/dist/src/status.d.ts +91 -0
  112. package/dist/src/status.js +119 -0
  113. package/dist/src/task.d.ts +264 -0
  114. package/dist/src/task.js +305 -0
  115. package/dist/src/teardown.d.ts +32 -0
  116. package/dist/src/teardown.js +314 -0
  117. package/dist/src/tuition.d.ts +159 -0
  118. package/dist/src/tuition.js +311 -0
  119. package/dist/src/validate.d.ts +230 -0
  120. package/dist/src/validate.js +732 -0
  121. package/dist/src/version.d.ts +3 -0
  122. package/dist/src/version.js +38 -0
  123. package/dist/src/watcher.d.ts +275 -0
  124. package/dist/src/watcher.js +859 -0
  125. package/dist/src/witness/run.d.ts +274 -0
  126. package/dist/src/witness/run.js +1327 -0
  127. package/dist/src/witness/spec.d.ts +102 -0
  128. package/dist/src/witness/spec.js +253 -0
  129. package/dist/tsconfig.src.tsbuildinfo +1 -0
  130. package/gate-registry.yaml +390 -0
  131. package/gates.manifest.json +195 -0
  132. package/package.json +57 -3
  133. package/role-model-config.yaml +88 -0
  134. package/roles/README.md +128 -0
  135. package/roles/_shared-dispatch-contract.md +87 -0
  136. package/roles/adversarial-plan-reviewer.md +80 -0
  137. package/roles/clean-room-reviewer.md +140 -0
  138. package/roles/implementer.md +460 -0
  139. package/roles/investigator.md +138 -0
  140. package/roles/plan-writer.md +95 -0
  141. package/schemas/README.md +81 -0
  142. package/schemas/assurance-modes.schema.json +264 -0
  143. package/schemas/charter.schema.json +166 -0
  144. package/schemas/checklist.schema.json +114 -0
  145. package/schemas/decision-record.schema.json +88 -0
  146. package/schemas/final-report.schema.json +90 -0
  147. package/schemas/finding.schema.json +106 -0
  148. package/schemas/gate-registry.schema.json +260 -0
  149. package/schemas/mechanism-index.schema.json +94 -0
  150. package/schemas/plan.schema.json +300 -0
  151. package/schemas/report.schema.json +579 -0
  152. package/schemas/role-brief.schema.json +105 -0
  153. package/schemas/role-model-config.schema.json +90 -0
  154. package/schemas/status-line.schema.json +40 -0
  155. package/schemas/tuition.schema.json +191 -0
  156. package/schemas/verdict.schema.json +289 -0
  157. package/schemas/work-history.schema.json +183 -0
  158. package/templates/charter.example.yaml +54 -0
  159. package/templates/decision-record.example.yaml +27 -0
  160. package/templates/final-report.example.yaml +80 -0
  161. package/templates/plan.example.yaml +87 -0
  162. package/templates/report.example.yaml +236 -0
  163. package/templates/warnings.md +74 -0
  164. package/templates/work-history.example.yaml +185 -0
  165. package/tuition/README.md +76 -0
  166. package/tuition/T-001.yaml +48 -0
  167. package/tuition/T-002.yaml +51 -0
  168. package/tuition/T-003.yaml +100 -0
  169. package/tuition/T-004.yaml +52 -0
  170. package/tuition/T-005.yaml +72 -0
  171. package/tuition/T-006.yaml +81 -0
  172. package/tuition/T-007.yaml +56 -0
  173. package/tuition/T-008.yaml +111 -0
  174. package/tuition/T-009.yaml +50 -0
  175. package/tuition/T-015.yaml +36 -0
  176. package/tuition/T-016.yaml +36 -0
  177. package/tuition/T-017.yaml +46 -0
  178. package/tuition/T-018.yaml +84 -0
  179. package/tuition/T-021.yaml +40 -0
  180. package/tuition/T-022.yaml +36 -0
  181. package/tuition/mechanism-index.yaml +256 -0
@@ -0,0 +1,229 @@
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 } 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
+ writeFileSync(`${barrier}.observed`, "");
53
+ const startNs = process.hrtime.bigint();
54
+ const limitNs = BigInt(HOLD_WAIT_LIMIT_MS) * 1000000n;
55
+ let held = false;
56
+ for (;;) {
57
+ if (existsSync(barrier)) {
58
+ held = true;
59
+ break;
60
+ }
61
+ if (process.hrtime.bigint() - startNs >= limitNs) {
62
+ break;
63
+ }
64
+ await sleep(10);
65
+ }
66
+ const waitedMs = Number((process.hrtime.bigint() - startNs) / 1000000n);
67
+ if (!held) {
68
+ throw new Error(`lock test hold point: barrier ${barrier} never appeared within ` +
69
+ `${String(HOLD_WAIT_LIMIT_MS)}ms (waited ${String(waitedMs)}ms on the ` +
70
+ `monotonic clock); this interleave was never staged and the run is ` +
71
+ `not evidence`);
72
+ }
73
+ // Record that the hold really held, and why the wait ended, so the
74
+ // witness can assert the interleave rather than assume it.
75
+ writeFileSync(`${barrier}.released`, `held after ${String(waitedMs)}ms (monotonic), barrier observed\n`);
76
+ return { observed, nowMs };
77
+ }
78
+ function usageError(message) {
79
+ if (message !== undefined) {
80
+ process.stderr.write(`tiphys lock: ${message}\n`);
81
+ }
82
+ process.stderr.write(`${USAGE}\n`);
83
+ return EX_USAGE;
84
+ }
85
+ /**
86
+ * The one failure emitter for acquire, renew, and release (CR-204).
87
+ * When the library classified the failure as a stale mutation claim,
88
+ * the operator is pointed at the claim file rather than at a holder
89
+ * that may not exist; the remedy text lives here, so a CLI that
90
+ * ignored the classification would visibly lose it. Operational
91
+ * failures exit 1 (usage errors alone use EX_USAGE).
92
+ */
93
+ function failure(outcome) {
94
+ // D-2: the claim file is the sole serializer of lock mutations, so
95
+ // deleting one that is actually live lets a second mutation enter the
96
+ // critical section and can produce two live holders. The remedy text
97
+ // therefore states that cost instead of inviting the deletion, and it
98
+ // names the safe order: make sure no tiphys process is running first.
99
+ const remedy = outcome.claimTimeout === true
100
+ ? "; 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"
101
+ : "";
102
+ process.stderr.write(`tiphys lock: ${outcome.reason}${remedy}\n`);
103
+ return 1;
104
+ }
105
+ function parseFlags(rest) {
106
+ const parsed = {
107
+ takeover: false,
108
+ holder: undefined,
109
+ durationSeconds: undefined,
110
+ };
111
+ for (let i = 0; i < rest.length; i += 1) {
112
+ const flag = rest[i];
113
+ if (flag === "--take-over") {
114
+ parsed.takeover = true;
115
+ }
116
+ else if (flag === "--holder" && i + 1 < rest.length) {
117
+ parsed.holder = rest[i + 1];
118
+ i += 1;
119
+ }
120
+ else if (flag === "--duration" && i + 1 < rest.length) {
121
+ const seconds = Number(rest[i + 1]);
122
+ if (!Number.isFinite(seconds) || seconds <= 0) {
123
+ return undefined;
124
+ }
125
+ parsed.durationSeconds = seconds;
126
+ i += 1;
127
+ }
128
+ else {
129
+ return undefined;
130
+ }
131
+ }
132
+ return parsed;
133
+ }
134
+ export async function cmdLock(args) {
135
+ const [subcommand, ...rest] = args;
136
+ if (subcommand === undefined) {
137
+ return usageError();
138
+ }
139
+ const flags = parseFlags(rest);
140
+ if (flags === undefined) {
141
+ return usageError();
142
+ }
143
+ let lockPath;
144
+ try {
145
+ lockPath = loadFleet(process.cwd()).lockPath;
146
+ }
147
+ catch (error) {
148
+ process.stderr.write(`tiphys lock: ${error.message}\n`);
149
+ return 1;
150
+ }
151
+ switch (subcommand) {
152
+ case "acquire": {
153
+ if (flags.holder !== undefined) {
154
+ return usageError("acquire does not take --holder");
155
+ }
156
+ const held = await maybeHoldForTest(lockPath);
157
+ const outcome = await acquireLease(lockPath, {
158
+ takeover: flags.takeover,
159
+ durationSeconds: flags.durationSeconds,
160
+ observed: held?.observed,
161
+ nowMs: held?.nowMs,
162
+ });
163
+ if (!outcome.ok) {
164
+ return failure(outcome);
165
+ }
166
+ const lease = outcome.lease;
167
+ if (lease === null) {
168
+ throw new Error("unreachable: acquire produced no lease");
169
+ }
170
+ process.stdout.write(`acquired ${lease.holderId} expires ${lease.expiresAt}\n`);
171
+ return 0;
172
+ }
173
+ case "renew": {
174
+ if (flags.holder === undefined || flags.takeover) {
175
+ return usageError("renew requires --holder <id>");
176
+ }
177
+ const held = await maybeHoldForTest(lockPath);
178
+ const outcome = await renewLease(lockPath, flags.holder, {
179
+ durationSeconds: flags.durationSeconds,
180
+ observed: held?.observed,
181
+ nowMs: held?.nowMs,
182
+ });
183
+ if (!outcome.ok) {
184
+ return failure(outcome);
185
+ }
186
+ const lease = outcome.lease;
187
+ if (lease === null) {
188
+ throw new Error("unreachable: renew produced no lease");
189
+ }
190
+ process.stdout.write(`renewed ${lease.holderId} expires ${lease.expiresAt}\n`);
191
+ return 0;
192
+ }
193
+ case "release": {
194
+ if (flags.holder === undefined || flags.takeover || flags.durationSeconds !== undefined) {
195
+ return usageError("release requires --holder <id> and no other flags");
196
+ }
197
+ const held = await maybeHoldForTest(lockPath);
198
+ const outcome = await releaseLease(lockPath, flags.holder, {
199
+ observed: held?.observed,
200
+ });
201
+ if (!outcome.ok) {
202
+ return failure(outcome);
203
+ }
204
+ process.stdout.write(`released ${flags.holder}\n`);
205
+ return 0;
206
+ }
207
+ case "status": {
208
+ if (flags.holder !== undefined || flags.takeover || flags.durationSeconds !== undefined) {
209
+ return usageError("status takes no flags");
210
+ }
211
+ const status = leaseStatus(lockPath);
212
+ if (status.state === "free") {
213
+ process.stdout.write("free\n");
214
+ }
215
+ else if (status.state === "corrupt") {
216
+ process.stdout.write(`corrupt ${status.detail}\n`);
217
+ }
218
+ else {
219
+ process.stdout.write(`${status.state} holder ${status.lease.holderId} acquired ` +
220
+ `${status.lease.acquiredAt} expires ${status.lease.expiresAt}\n`);
221
+ }
222
+ // status always exits 0 so a human takeover decision is informed
223
+ // by reading, never blocked by an exit code.
224
+ return 0;
225
+ }
226
+ default:
227
+ return usageError(`unknown lock subcommand "${subcommand}"`);
228
+ }
229
+ }
@@ -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,20 @@
1
+ /**
2
+ * `tiphys plan project --phase-id <id> [--plan <file>] [--out <dir>]`
3
+ * (kernel plan M3, M3-P1 step 2; D-M3-18).
4
+ *
5
+ * Emits the M2-P4 scope auditor's phase declaration from the plan, so the
6
+ * auditor's input becomes a GENERATED VIEW of one source instead of a second
7
+ * hand-authored source that can drift.
8
+ *
9
+ * The output path is `<out>/<phase-id-lowercased>.json`, defaulting to
10
+ * `delivery/plan/phase-declarations`, which is the directory
11
+ * `gates.manifest.json` passes to the auditor as `--declarations`.
12
+ *
13
+ * With no `--out` the document is written; with `--stdout` it is printed and
14
+ * nothing is written, which is what a test or a reviewer wants when checking
15
+ * WHAT would be emitted without touching the tree.
16
+ *
17
+ * Every path this command touches is operator-supplied and is classified
18
+ * before it is opened or written (D-M3-27).
19
+ */
20
+ export declare function cmdPlan(argv: string[]): number;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * `tiphys plan project --phase-id <id> [--plan <file>] [--out <dir>]`
3
+ * (kernel plan M3, M3-P1 step 2; D-M3-18).
4
+ *
5
+ * Emits the M2-P4 scope auditor's phase declaration from the plan, so the
6
+ * auditor's input becomes a GENERATED VIEW of one source instead of a second
7
+ * hand-authored source that can drift.
8
+ *
9
+ * The output path is `<out>/<phase-id-lowercased>.json`, defaulting to
10
+ * `delivery/plan/phase-declarations`, which is the directory
11
+ * `gates.manifest.json` passes to the auditor as `--declarations`.
12
+ *
13
+ * With no `--out` the document is written; with `--stdout` it is printed and
14
+ * nothing is written, which is what a test or a reviewer wants when checking
15
+ * WHAT would be emitted without touching the tree.
16
+ *
17
+ * Every path this command touches is operator-supplied and is classified
18
+ * before it is opened or written (D-M3-27).
19
+ */
20
+ import { mkdirSync, writeFileSync } from "node:fs";
21
+ import { join } from "node:path";
22
+ import { EX_USAGE } from "../cli.js";
23
+ import { projectPhase, renderDeclaration } from "../plan.js";
24
+ import { refuseOpenForWrite } from "../task.js";
25
+ import { classifyContextDirectory, decodeDocument, readOperatorPath, } from "../validate.js";
26
+ const USAGE = "usage: tiphys plan project --phase-id <id> [--plan <file>] " +
27
+ "[--out <dir>] [--stdout]";
28
+ const DEFAULT_PLAN = "templates/plan.example.yaml";
29
+ const DEFAULT_OUT = join("delivery", "plan", "phase-declarations");
30
+ function parseArgs(argv) {
31
+ const args = { plan: DEFAULT_PLAN, out: DEFAULT_OUT, toStdout: false };
32
+ for (let index = 0; index < argv.length; index += 1) {
33
+ const flag = argv[index];
34
+ if (flag === "--stdout") {
35
+ args.toStdout = true;
36
+ continue;
37
+ }
38
+ const value = argv[index + 1];
39
+ if (flag !== "--phase-id" && flag !== "--plan" && flag !== "--out") {
40
+ return { usageError: `unknown option ${flag}` };
41
+ }
42
+ if (value === undefined || value.startsWith("--")) {
43
+ return { usageError: `${flag} requires a value` };
44
+ }
45
+ index += 1;
46
+ if (flag === "--phase-id")
47
+ args.phaseId = value;
48
+ else if (flag === "--plan")
49
+ args.plan = value;
50
+ else
51
+ args.out = value;
52
+ }
53
+ if (args.phaseId === undefined) {
54
+ return { usageError: "--phase-id is required" };
55
+ }
56
+ return { args };
57
+ }
58
+ export function cmdPlan(argv) {
59
+ const [subcommand, ...rest] = argv;
60
+ if (subcommand !== "project") {
61
+ process.stderr.write(`${USAGE}\n`);
62
+ return EX_USAGE;
63
+ }
64
+ const parsed = parseArgs(rest);
65
+ if (parsed.args === undefined) {
66
+ process.stderr.write(`tiphys plan project: ${parsed.usageError ?? "usage error"}\n${USAGE}\n`);
67
+ return EX_USAGE;
68
+ }
69
+ const { phaseId, plan, out, toStdout } = parsed.args;
70
+ const read = readOperatorPath(plan);
71
+ if (!read.ok) {
72
+ process.stderr.write(`tiphys plan project: ${read.reason}\n`);
73
+ return 1;
74
+ }
75
+ const decoded = decodeDocument(read.body, plan);
76
+ if (!decoded.ok) {
77
+ process.stderr.write(`tiphys plan project: ${decoded.reason}\n`);
78
+ return 1;
79
+ }
80
+ const projection = projectPhase(decoded.value, phaseId);
81
+ if (!projection.ok) {
82
+ process.stderr.write(`tiphys plan project: ${projection.reason}\n`);
83
+ return 1;
84
+ }
85
+ const body = renderDeclaration(projection.declaration);
86
+ if (toStdout) {
87
+ process.stdout.write(body);
88
+ return 0;
89
+ }
90
+ mkdirSync(out, { recursive: true });
91
+ const problem = classifyContextDirectory(out);
92
+ if (problem !== undefined) {
93
+ process.stderr.write(`tiphys plan project: ${problem}\n`);
94
+ return 1;
95
+ }
96
+ const target = join(out, projection.filename);
97
+ const refusal = refuseOpenForWrite(target);
98
+ if (refusal !== undefined) {
99
+ process.stderr.write(`tiphys plan project: ${refusal}\n`);
100
+ return 1;
101
+ }
102
+ writeFileSync(target, body, "utf8");
103
+ process.stdout.write(`${target}\n`);
104
+ return 0;
105
+ }
@@ -0,0 +1 @@
1
+ export declare function cmdPool(args: string[]): Promise<number>;
@@ -0,0 +1,128 @@
1
+ import { EX_USAGE } from "../cli.js";
2
+ import { loadFleet } from "../fleet.js";
3
+ import { poolCreate, poolDestroy, poolList } from "../pool.js";
4
+ /**
5
+ * tiphys pool <create --task <id> --project <path> [--offline] | list |
6
+ * destroy --task <id> [--discard]> (kernel plan v1, M1-P3 steps 3-4).
7
+ * Runs in a fleet home (cwd). create emits the fetched base SHA as its
8
+ * only stdout line (EXT-F-03 step 3; M1-P4 spawn captures it).
9
+ */
10
+ const USAGE = "usage: tiphys pool <create --task <id> --project <path> [--offline] | " +
11
+ "list | destroy --task <id> [--discard] [--delete-branch-force]>";
12
+ function usageError(message) {
13
+ if (message !== undefined) {
14
+ process.stderr.write(`tiphys pool: ${message}\n`);
15
+ }
16
+ process.stderr.write(`${USAGE}\n`);
17
+ return EX_USAGE;
18
+ }
19
+ function parseFlags(rest) {
20
+ const parsed = {
21
+ task: undefined,
22
+ project: undefined,
23
+ offline: false,
24
+ discard: false,
25
+ deleteBranchForce: false,
26
+ };
27
+ for (let i = 0; i < rest.length; i += 1) {
28
+ const flag = rest[i];
29
+ if (flag === "--task" && i + 1 < rest.length) {
30
+ parsed.task = rest[i + 1];
31
+ i += 1;
32
+ }
33
+ else if (flag === "--project" && i + 1 < rest.length) {
34
+ parsed.project = rest[i + 1];
35
+ i += 1;
36
+ }
37
+ else if (flag === "--offline") {
38
+ parsed.offline = true;
39
+ }
40
+ else if (flag === "--discard") {
41
+ parsed.discard = true;
42
+ }
43
+ else if (flag === "--delete-branch-force") {
44
+ parsed.deleteBranchForce = true;
45
+ }
46
+ else {
47
+ return undefined;
48
+ }
49
+ }
50
+ return parsed;
51
+ }
52
+ export async function cmdPool(args) {
53
+ const [subcommand, ...rest] = args;
54
+ if (subcommand === undefined) {
55
+ return usageError();
56
+ }
57
+ const flags = parseFlags(rest);
58
+ if (flags === undefined) {
59
+ return usageError();
60
+ }
61
+ let fleet;
62
+ try {
63
+ fleet = loadFleet(process.cwd());
64
+ }
65
+ catch (error) {
66
+ process.stderr.write(`tiphys pool: ${error.message}\n`);
67
+ return 1;
68
+ }
69
+ switch (subcommand) {
70
+ case "create": {
71
+ if (flags.task === undefined ||
72
+ flags.project === undefined ||
73
+ flags.discard ||
74
+ flags.deleteBranchForce) {
75
+ return usageError("create requires --task <id> and --project <path>");
76
+ }
77
+ const result = await poolCreate(fleet, {
78
+ taskId: flags.task,
79
+ project: flags.project,
80
+ offline: flags.offline,
81
+ });
82
+ if (!result.ok) {
83
+ process.stderr.write(`tiphys pool: ${result.reason}\n`);
84
+ return 1;
85
+ }
86
+ process.stdout.write(`${result.value.baseSha}\n`);
87
+ return 0;
88
+ }
89
+ case "list": {
90
+ if (flags.task !== undefined ||
91
+ flags.project !== undefined ||
92
+ flags.offline ||
93
+ flags.discard ||
94
+ flags.deleteBranchForce) {
95
+ return usageError("list takes no flags");
96
+ }
97
+ for (const entry of poolList(fleet)) {
98
+ process.stdout.write(`${entry.taskId} ${entry.headSha}\n`);
99
+ }
100
+ return 0;
101
+ }
102
+ case "destroy": {
103
+ if (flags.task === undefined || flags.project !== undefined || flags.offline) {
104
+ return usageError("destroy requires --task <id>");
105
+ }
106
+ const result = await poolDestroy(fleet, {
107
+ taskId: flags.task,
108
+ discard: flags.discard,
109
+ deleteBranchForce: flags.deleteBranchForce,
110
+ });
111
+ if (!result.ok) {
112
+ process.stderr.write(`tiphys pool: ${result.reason}\n`);
113
+ return 1;
114
+ }
115
+ // Name the deleted branch and its tip on success: that sha is the
116
+ // operator's only recovery handle if the destroy was a mistake
117
+ // (V-1), and it stays valid until the clone is garbage collected.
118
+ const { deletedBranch, deletedSha } = result.value;
119
+ const suffix = deletedBranch === undefined
120
+ ? ""
121
+ : ` (deleted branch ${deletedBranch} was ${deletedSha ?? "unknown"})`;
122
+ process.stdout.write(`destroyed ${flags.task}${suffix}\n`);
123
+ return 0;
124
+ }
125
+ default:
126
+ return usageError(`unknown pool subcommand "${subcommand}"`);
127
+ }
128
+ }
@@ -0,0 +1 @@
1
+ export declare function cmdSpawn(args: string[]): Promise<number>;