@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,258 @@
1
+ /**
2
+ * READING THE SHIPPED ASSURANCE MODE DEFINITIONS (kernel plan M3, M3-P3
3
+ * step 5).
4
+ *
5
+ * `assurance-modes.yaml` ships at the package root, beside `gate-registry.yaml`
6
+ * and the `schemas/` directory. This module locates it, decodes it, and
7
+ * answers one question: what does a declared mode require. Nothing here
8
+ * RESOLVES a mode into behaviour and nothing here enforces one. M3 never
9
+ * executes `direct-pr` or `local-only`, and building an enforcement engine for
10
+ * a mode this milestone never enters is the M1-P3 failure the plan is trying
11
+ * not to repeat.
12
+ *
13
+ * NO VALIDATION HAPPENS HERE, deliberately. `tiphys validate --type
14
+ * assurance-modes` is the command that says whether the document is
15
+ * well-formed, and duplicating its rules in a reader would produce a second
16
+ * opinion to keep in sync. This module reads what is there and reports what it
17
+ * cannot find.
18
+ */
19
+ import { readdirSync } from "node:fs";
20
+ import { fileURLToPath } from "node:url";
21
+ import { dirname, join } from "node:path";
22
+ import { decodeDocument, readOperatorPath } from "./validate.js";
23
+ /** The shipped document's basename, at the package root. */
24
+ export const MODES_FILENAME = "assurance-modes.yaml";
25
+ /**
26
+ * Locate the package root by walking UP from this module and testing for the
27
+ * shipped document.
28
+ *
29
+ * The depth differs between the two layouts this code runs in: from source it
30
+ * is `src/` and the root is one level up, and from the built entry it is
31
+ * `dist/src/` and the root is two levels up. Counting `..` would be right in
32
+ * exactly one of them, which is the layout-dependent break
33
+ * `schemasDirectory()` already documents. Walking up and TESTING is right in
34
+ * both, and in a relocated copy as well.
35
+ */
36
+ export function packageRoot() {
37
+ let directory = dirname(fileURLToPath(import.meta.url));
38
+ for (let depth = 0; depth < 8; depth += 1) {
39
+ try {
40
+ if (readdirSync(directory).includes(MODES_FILENAME)) {
41
+ return directory;
42
+ }
43
+ }
44
+ catch {
45
+ /* not readable here; keep walking */
46
+ }
47
+ const parent = dirname(directory);
48
+ if (parent === directory) {
49
+ break;
50
+ }
51
+ directory = parent;
52
+ }
53
+ throw new Error(`the shipped ${MODES_FILENAME} was not found above this module; the installation is incomplete`);
54
+ }
55
+ function asRecord(value) {
56
+ return typeof value === "object" && value !== null && !Array.isArray(value)
57
+ ? value
58
+ : undefined;
59
+ }
60
+ function strings(record, key) {
61
+ const value = record[key];
62
+ return Array.isArray(value)
63
+ ? value.filter((entry) => typeof entry === "string")
64
+ : [];
65
+ }
66
+ function optionalStrings(record, key) {
67
+ return record[key] === undefined ? undefined : strings(record, key);
68
+ }
69
+ /** Read and decode the shipped document, or say why it could not be read. */
70
+ export function readModes(path = join(packageRoot(), MODES_FILENAME)) {
71
+ const read = readOperatorPath(path);
72
+ if (!read.ok) {
73
+ return { ok: false, reason: read.reason };
74
+ }
75
+ const decoded = decodeDocument(read.body, path);
76
+ if (!decoded.ok) {
77
+ return { ok: false, reason: decoded.reason };
78
+ }
79
+ const document = asRecord(decoded.value);
80
+ if (document === undefined) {
81
+ return { ok: false, reason: `${path} does not decode to a mapping` };
82
+ }
83
+ const raw = document["modes"];
84
+ if (!Array.isArray(raw)) {
85
+ return { ok: false, reason: `${path} declares no modes list` };
86
+ }
87
+ const modes = [];
88
+ for (const entry of raw) {
89
+ const record = asRecord(entry);
90
+ if (record === undefined) {
91
+ continue;
92
+ }
93
+ modes.push({
94
+ id: String(record["id"] ?? ""),
95
+ declaredBy: String(record["declared-by"] ?? ""),
96
+ pipeline: strings(record, "pipeline"),
97
+ skips: strings(record, "skips"),
98
+ gateSets: strings(record, "gate-sets"),
99
+ mergeAuthority: String(record["merge-authority"] ?? ""),
100
+ grantedBy: typeof record["granted-by"] === "string" ? record["granted-by"] : undefined,
101
+ conditions: optionalStrings(record, "conditions"),
102
+ reviewContracts: optionalStrings(record, "review-contracts"),
103
+ escalationBounds: asRecord(record["escalation-bounds"]),
104
+ });
105
+ }
106
+ return { ok: true, path, raw: decoded.value, modes };
107
+ }
108
+ /**
109
+ * The id blueprint section 8 gives the un-downgraded process BY NAME: "The
110
+ * current proven process is the definition of `full`."
111
+ *
112
+ * SIBLING LITERAL, named so neither site can drift unnoticed:
113
+ * `REFERENCE_MODE_ID` at src/checks.ts:275 is the same string, used as the
114
+ * reference pipeline every other mode's omissions are measured against. The two
115
+ * are pinned from opposite directions by registered tests: the checks-side
116
+ * literal by the assertion that deleting `full` produces `no mode declares id
117
+ * full` (test/assurance-modes.test.ts:314), and this one by the assertion that
118
+ * the mode `mode show` annotates as un-downgraded is the mode named `full`.
119
+ */
120
+ const UNDOWNGRADED_MODE_ID = "full";
121
+ /**
122
+ * The execution status of one mode, DERIVED rather than looked up in a list of
123
+ * ids (CR-004 item 2, DR-0020).
124
+ *
125
+ * Two facts are available and both are checkable by the reader: whether this is
126
+ * the kernel's own document, and whether this mode IS the one blueprint section
127
+ * 8 names as the un-downgraded process, "The current proven process is the
128
+ * definition of `full`. Downgrades are declared, never improvised."
129
+ *
130
+ * WHY THE NAME AND NOT THE SKIP COUNT (CR-002, round 9). This function used
131
+ * `mode.skips.length === 0` as its proxy for "this is the un-downgraded mode".
132
+ * The proxy held only because `full` happened to be the only mode with an empty
133
+ * list, nothing enforced that, and `skips[]` is shipped DATA. Two measured
134
+ * consequences, both at exit 0 with every registry gate green: giving `full` one
135
+ * bogus `skips[]` entry made this function say that no phase of the tiphys
136
+ * project had ever been delivered under `full`, which is false about the mode
137
+ * this project has delivered every phase under; and giving `direct-pr` an empty
138
+ * `skips[]` made a mode nobody has ever entered claim to be the one the project
139
+ * follows, with `merge-authority: owner` printed beneath it, which is not the
140
+ * regime in force (DR-0015). Blueprint section 8 defines `full` BY NAME, so the
141
+ * name is the primary fact and the skip count is a consequence of it.
142
+ *
143
+ * THE COUNT IS STILL REPORTED, as a fact about the mode rather than as the
144
+ * ground of the claim. That distinction is the whole finding: a number may be
145
+ * shown without being believed.
146
+ *
147
+ * WHAT MAKES THE `full` SENTENCE TRUE IS DATA, AND IT IS GUARDED IN TWO PLACES
148
+ * THAT COVER DIFFERENT DOCUMENTS. Keying off the name moves the burden: the
149
+ * claim is only as good as the `full` in the document being served genuinely
150
+ * being un-downgraded.
151
+ *
152
+ * The VALIDATOR carries it for EVERY document (round 10, CRB9-02).
153
+ * `mode-no-undeclared-downgrade` rejects a `skips[]` entry that the same mode's
154
+ * pipeline runs, and also one that the reference does not run; on the reference
155
+ * those two exhaust the cases, so a `full` declaring any skipped stage at all is
156
+ * refused and this sentence is never printed over it. Until that second part
157
+ * existed, a `full` whose stage had MOVED from `pipeline` into `skips` was an
158
+ * HONEST downgrade that validated at exit 0, and `tiphys mode show --mode full`
159
+ * printed this sentence about fifteen lines above a `skips: deploy-verify` row.
160
+ *
161
+ * A REGISTERED TEST carries it for THIS repository's own document, asserting the
162
+ * shipped `full` declares no skipped stage. It is kept rather than superseded:
163
+ * a test guards the document, the check guards every other document, and the
164
+ * failure this pair exists to prevent is a property asserted where it is stated
165
+ * and not enforced where it is consumed.
166
+ *
167
+ * WHAT THIS DELIBERATELY DOES NOT SAY. It does not say that tiphys runs
168
+ * anything: nothing runs on tiphys before M4. The un-downgraded mode of the
169
+ * kernel's own document is the process the tiphys PROJECT follows for its own
170
+ * delivery; the downgraded ones have never been entered at all. And for a
171
+ * document that is not the kernel's own, the answer is that tiphys does not
172
+ * know, because it does not.
173
+ */
174
+ export function executionStatus(mode, context) {
175
+ if (!context.shippedDocument) {
176
+ return ("not determinable here. This is not the kernel's own assurance-modes.yaml, " +
177
+ "so nothing tiphys ships records whether any phase has been delivered under " +
178
+ "this mode (DR-0020).");
179
+ }
180
+ if (mode.id === UNDOWNGRADED_MODE_ID) {
181
+ return (`this mode is ${UNDOWNGRADED_MODE_ID}, which blueprint section 8 defines by name as the ` +
182
+ "un-downgraded process, and it is the one the tiphys project follows for its own delivery.");
183
+ }
184
+ return (`DECLARED AND VALIDATED, NEVER EXERCISED. This mode is not ${UNDOWNGRADED_MODE_ID}, which ` +
185
+ "blueprint section 8 defines by name as the un-downgraded process, so no phase of the " +
186
+ `tiphys project has ever been delivered under it. It declares ${String(mode.skips.length)} ` +
187
+ "skipped stage(s). Its pipeline and its gate selection are checked by validation only " +
188
+ "(DR-0020).");
189
+ }
190
+ /**
191
+ * The standing limits of this release, printed on every invocation.
192
+ *
193
+ * IT SAYS ONLY WHAT THE SHIPPED SCHEMAS DO. The vocabularies really are closed
194
+ * enums, so "a document naming any other id is rejected" is the enum's own
195
+ * behaviour and not a claim about intent. The M4 sentence is attributed to
196
+ * DR-0020 rather than stated as a property of the code.
197
+ */
198
+ export const RELEASE_LIMITS = "limits: the mode, stage and role vocabularies in the shipped schemas are this " +
199
+ "repository's own closed enums, so a document naming any other id is rejected and a " +
200
+ "consuming project cannot extend them at v0.1.0; whether to open them is an M4 question " +
201
+ "(DR-0020). This command SHOWS a declared mode: nothing in this release resolves a " +
202
+ "project into a mode, enforces one, or runs one.";
203
+ /**
204
+ * Render one mode for a human or for a brief.
205
+ *
206
+ * THE SHAPE IS PART OF THE CONTRACT, because criterion 2 asserts over it: a
207
+ * section is a line ending in a colon at column zero, and its items are the
208
+ * lines indented by exactly two spaces beneath it. That makes "prints exactly
209
+ * the twelve stage ids in order" something a test can extract rather than
210
+ * something a reader has to eyeball.
211
+ */
212
+ export function renderMode(mode, context) {
213
+ const lines = [`mode: ${mode.id}`];
214
+ /* SECOND LINE, not a footnote. CR-004 measured that `mode show` printed a
215
+ never-exercised mode with exactly the confidence of the exercised one, and
216
+ that the only disclosure lived in `delivery/`, which the package excludes. */
217
+ lines.push(`execution-status: ${executionStatus(mode, context)}`);
218
+ lines.push(`merge-authority: ${mode.mergeAuthority}`);
219
+ if (mode.grantedBy !== undefined) {
220
+ lines.push(`granted-by: ${mode.grantedBy}`);
221
+ }
222
+ const section = (name, items) => {
223
+ if (items === undefined) {
224
+ return;
225
+ }
226
+ lines.push(`${name}:`);
227
+ if (items.length === 0) {
228
+ lines.push(" (none)");
229
+ return;
230
+ }
231
+ for (const item of items) {
232
+ lines.push(` ${item}`);
233
+ }
234
+ };
235
+ section("pipeline", mode.pipeline);
236
+ section("skips", mode.skips);
237
+ section("gate-sets", mode.gateSets);
238
+ section("review-contracts", mode.reviewContracts);
239
+ if (mode.escalationBounds !== undefined) {
240
+ /* CR-004 item 3. The bounds are DATA an orchestrator brief cites. Nothing
241
+ in this release counts a fix round or detects a recurrence, so a bare
242
+ `escalation-bounds:` header invites the reader to assume an enforcement
243
+ engine that does not exist. */
244
+ lines.push("escalation-bounds (data an orchestrator brief cites; nothing in this release counts fix rounds, detects recurrence, or enforces these):");
245
+ for (const key of Object.keys(mode.escalationBounds).sort()) {
246
+ lines.push(` ${key}: ${String(mode.escalationBounds[key])}`);
247
+ }
248
+ }
249
+ if (mode.conditions !== undefined) {
250
+ lines.push("conditions:");
251
+ for (const condition of mode.conditions) {
252
+ lines.push(` ${condition.replace(/\s+/g, " ").trim()}`);
253
+ }
254
+ }
255
+ lines.push(`declared-by: ${mode.declaredBy.replace(/\s+/g, " ").trim()}`);
256
+ lines.push(RELEASE_LIMITS);
257
+ return lines;
258
+ }
@@ -0,0 +1,2 @@
1
+ /** Compare two existing paths by their canonical filesystem identity. */
2
+ export declare function pathsIdentifySameObject(left: string, right: string): boolean;
@@ -0,0 +1,10 @@
1
+ import { realpathSync } from "node:fs";
2
+ /** Compare two existing paths by their canonical filesystem identity. */
3
+ export function pathsIdentifySameObject(left, right) {
4
+ try {
5
+ return realpathSync(left) === realpathSync(right);
6
+ }
7
+ catch {
8
+ return false;
9
+ }
10
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * THE PHASE-DECLARATION PROJECTION (kernel plan M3, M3-P1 step 2; D-M3-18).
3
+ *
4
+ * The M2-P4 scope auditor reads one document per phase from
5
+ * `delivery/plan/phase-declarations/<phase-id-lowercased>.json`, out of the
6
+ * MERGE BASE of the audited branch. Until now that document was a SECOND
7
+ * HAND-AUTHORED SOURCE beside the plan, and two hand-authored sources of the
8
+ * same fact drift. This module makes it a GENERATED VIEW of the plan, so the
9
+ * plan is the one source and the auditor's input is derived from it.
10
+ *
11
+ * WHAT THE PROJECTION EMITS, taken from the DELIVERED
12
+ * `src/gates/schemas/phase-declaration.schema.json` rather than from a
13
+ * description of it. The delivered schema sets `additionalProperties: false`,
14
+ * so an extra property is a REJECTION and not an ignored field, and the
15
+ * emitted key set is exactly these five, camelCase:
16
+ *
17
+ * id, branch, filesToTouch, declaredExtras, citations
18
+ *
19
+ * TWO VOCABULARIES MEET HERE AND NOWHERE ELSE. The plan spells the same
20
+ * fields `files-to-touch` and `extras`, because the plan is a document
21
+ * authored for people. This projector is the single translation point, and
22
+ * that is D-M3-18's whole value.
23
+ *
24
+ * THE GLOSS IS STRIPPED HERE OR NOWHERE (M2R-016). The auditor matches a
25
+ * declared path as a literal string or as a directory prefix and interprets
26
+ * nothing, so a plan entry written as
27
+ *
28
+ * `src/cli.ts` (edit only if step 4 requires it)
29
+ *
30
+ * must project to `src/cli.ts`. A projector that passed the glossed form
31
+ * through would emit a declaration under which every real change is
32
+ * undeclared, and the auditor would be right to reject it.
33
+ */
34
+ /** The five properties the delivered phase-declaration schema requires. */
35
+ export interface PhaseDeclaration {
36
+ id: string;
37
+ branch: string;
38
+ filesToTouch: string[];
39
+ declaredExtras: string[];
40
+ citations: string[];
41
+ }
42
+ export declare function stripGloss(entry: string): string;
43
+ /**
44
+ * Is this a literal path the scope auditor can match?
45
+ *
46
+ * The auditor compares strings exactly or as a directory prefix and
47
+ * interprets nothing, so an entry still carrying whitespace after the gloss
48
+ * strip is prose. The projector REFUSES rather than emitting it: an emitted
49
+ * prose entry is a declaration nobody can satisfy, and the failure would
50
+ * surface as an unrelated scope-gate red on some later branch.
51
+ */
52
+ export declare function isLiteralPath(entry: string): boolean;
53
+ export interface ProjectionResult {
54
+ ok: true;
55
+ declaration: PhaseDeclaration;
56
+ /** `m3-p1.json`, the filename the auditor looks for. */
57
+ filename: string;
58
+ }
59
+ export interface ProjectionFailure {
60
+ ok: false;
61
+ reason: string;
62
+ }
63
+ /**
64
+ * Project one phase of a decoded plan into its declaration.
65
+ *
66
+ * The filename is the phase id LOWERCASED, because CI derives `--phase` from
67
+ * the branch with a lowercase regex and the auditor looks the declaration up
68
+ * by that name. An uppercase filename is a declaration the auditor never
69
+ * finds, which reads exactly like a phase with no declaration at all.
70
+ */
71
+ export declare function projectPhase(plan: unknown, phaseId: string): ProjectionResult | ProjectionFailure;
72
+ /** The declaration as the auditor reads it: JSON, one trailing newline. */
73
+ export declare function renderDeclaration(declaration: PhaseDeclaration): string;
@@ -0,0 +1,153 @@
1
+ /**
2
+ * THE PHASE-DECLARATION PROJECTION (kernel plan M3, M3-P1 step 2; D-M3-18).
3
+ *
4
+ * The M2-P4 scope auditor reads one document per phase from
5
+ * `delivery/plan/phase-declarations/<phase-id-lowercased>.json`, out of the
6
+ * MERGE BASE of the audited branch. Until now that document was a SECOND
7
+ * HAND-AUTHORED SOURCE beside the plan, and two hand-authored sources of the
8
+ * same fact drift. This module makes it a GENERATED VIEW of the plan, so the
9
+ * plan is the one source and the auditor's input is derived from it.
10
+ *
11
+ * WHAT THE PROJECTION EMITS, taken from the DELIVERED
12
+ * `src/gates/schemas/phase-declaration.schema.json` rather than from a
13
+ * description of it. The delivered schema sets `additionalProperties: false`,
14
+ * so an extra property is a REJECTION and not an ignored field, and the
15
+ * emitted key set is exactly these five, camelCase:
16
+ *
17
+ * id, branch, filesToTouch, declaredExtras, citations
18
+ *
19
+ * TWO VOCABULARIES MEET HERE AND NOWHERE ELSE. The plan spells the same
20
+ * fields `files-to-touch` and `extras`, because the plan is a document
21
+ * authored for people. This projector is the single translation point, and
22
+ * that is D-M3-18's whole value.
23
+ *
24
+ * THE GLOSS IS STRIPPED HERE OR NOWHERE (M2R-016). The auditor matches a
25
+ * declared path as a literal string or as a directory prefix and interprets
26
+ * nothing, so a plan entry written as
27
+ *
28
+ * `src/cli.ts` (edit only if step 4 requires it)
29
+ *
30
+ * must project to `src/cli.ts`. A projector that passed the glossed form
31
+ * through would emit a declaration under which every real change is
32
+ * undeclared, and the auditor would be right to reject it.
33
+ */
34
+ /**
35
+ * Reduce one plan `files-to-touch` entry to the bare path.
36
+ *
37
+ * THIS FUNCTION FEEDS THE GATE THAT EXISTS TO PREVENT SCOPE WIDENING, so an
38
+ * over-eager strip here is worse than no strip at all.
39
+ *
40
+ * MEASURED DEFECT, fix round 1 (B-001, high). The first version truncated at
41
+ * the FIRST `(` and produced:
42
+ *
43
+ * "src/app/(marketing)/page.tsx" -> "src/app/"
44
+ * "src/(lib)/util.ts" -> "src/"
45
+ *
46
+ * `src/gates/scope.ts` treats a trailing slash as a DIRECTORY PREFIX GRANT, so
47
+ * each of those turned one declared file into an entire tree. Parenthesised
48
+ * path segments are ordinary in real projects (Next.js route groups are the
49
+ * obvious case), so this is not a corner.
50
+ *
51
+ * THE RULE NOW, and each clause is load-bearing:
52
+ *
53
+ * 1. A gloss is stripped only when it is TRAILING and the whole of it is
54
+ * parenthesised: `<path> (anything)` with the closing paren at the end.
55
+ * 2. It is stripped only when WHITESPACE separates it from the path. A path
56
+ * whose own last segment is parenthesised, `src/app/(marketing)`, has no
57
+ * such whitespace and is left alone.
58
+ * 3. What precedes the gloss must be ONE non-whitespace token, which is what
59
+ * a literal path is. That is the "plausible path" test, and it is why an
60
+ * interior `(` can never trigger a strip: the interior case never matches
61
+ * the trailing form at all.
62
+ *
63
+ * Anything the rule does not recognise is returned UNCHANGED and rejected
64
+ * loudly by `projectPhase`, because a prose entry that reaches the auditor as
65
+ * a literal string fails visibly, while a silently truncated one grants a
66
+ * tree.
67
+ */
68
+ const TRAILING_GLOSS = /^(\S+)\s+\([\s\S]*\)$/;
69
+ export function stripGloss(entry) {
70
+ let text = entry.trim();
71
+ const trailing = TRAILING_GLOSS.exec(text);
72
+ if (trailing !== null) {
73
+ text = trailing[1].trim();
74
+ }
75
+ /* Backticks are markdown, never part of a path. Stripped AFTER the gloss,
76
+ because the plan writes `` `path` (gloss) `` and the backticks close
77
+ before the gloss opens. */
78
+ text = text.replace(/^`+/, "").replace(/`+$/, "").trim();
79
+ /* A trailing comma survives a list written inline. */
80
+ text = text.replace(/,+$/, "").trim();
81
+ return text;
82
+ }
83
+ /**
84
+ * Is this a literal path the scope auditor can match?
85
+ *
86
+ * The auditor compares strings exactly or as a directory prefix and
87
+ * interprets nothing, so an entry still carrying whitespace after the gloss
88
+ * strip is prose. The projector REFUSES rather than emitting it: an emitted
89
+ * prose entry is a declaration nobody can satisfy, and the failure would
90
+ * surface as an unrelated scope-gate red on some later branch.
91
+ */
92
+ export function isLiteralPath(entry) {
93
+ return entry !== "" && !/\s/.test(entry);
94
+ }
95
+ function asRecord(value) {
96
+ return typeof value === "object" && value !== null && !Array.isArray(value)
97
+ ? value
98
+ : undefined;
99
+ }
100
+ function stringArray(value) {
101
+ return Array.isArray(value)
102
+ ? value.filter((item) => typeof item === "string")
103
+ : [];
104
+ }
105
+ /**
106
+ * Project one phase of a decoded plan into its declaration.
107
+ *
108
+ * The filename is the phase id LOWERCASED, because CI derives `--phase` from
109
+ * the branch with a lowercase regex and the auditor looks the declaration up
110
+ * by that name. An uppercase filename is a declaration the auditor never
111
+ * finds, which reads exactly like a phase with no declaration at all.
112
+ */
113
+ export function projectPhase(plan, phaseId) {
114
+ const document = asRecord(plan);
115
+ if (document === undefined) {
116
+ return { ok: false, reason: "the plan document is not a mapping" };
117
+ }
118
+ const phases = Array.isArray(document["phases"]) ? document["phases"] : [];
119
+ const phase = asRecord(phases.find((candidate) => asRecord(candidate)?.["id"] === phaseId));
120
+ if (phase === undefined) {
121
+ return { ok: false, reason: `the plan contains no phase ${phaseId}` };
122
+ }
123
+ const filesToTouch = stringArray(phase["files-to-touch"])
124
+ .map(stripGloss)
125
+ .filter((path) => path !== "");
126
+ const declaredExtras = stringArray(phase["extras"])
127
+ .map(stripGloss)
128
+ .filter((path) => path !== "");
129
+ const prose = [...filesToTouch, ...declaredExtras].filter((path) => !isLiteralPath(path));
130
+ if (prose.length > 0) {
131
+ return {
132
+ ok: false,
133
+ reason: `phase ${phaseId} declares an entry that is not a literal path and ` +
134
+ `cannot be projected: ${prose.map((p) => JSON.stringify(p)).join(", ")}`,
135
+ };
136
+ }
137
+ const declaration = {
138
+ id: String(phase["id"]),
139
+ branch: String(phase["branch"]),
140
+ filesToTouch,
141
+ declaredExtras,
142
+ citations: stringArray(phase["citations"]),
143
+ };
144
+ return {
145
+ ok: true,
146
+ declaration,
147
+ filename: `${phaseId.toLowerCase()}.json`,
148
+ };
149
+ }
150
+ /** The declaration as the auditor reads it: JSON, one trailing newline. */
151
+ export function renderDeclaration(declaration) {
152
+ return `${JSON.stringify(declaration, undefined, 2)}\n`;
153
+ }
@@ -0,0 +1,130 @@
1
+ import type { Fleet } from "./fleet.ts";
2
+ /**
3
+ * Worktree pool over a project clone (kernel plan v1, M1-P3 step 3).
4
+ * BUILD from the contract (plan decision D-1, FM-026): a clean disposable
5
+ * worktree per task at <fleet>/worktrees/<task-id>, parallel-safe through
6
+ * unique paths, O_EXCL record creation, and git worktree add's own
7
+ * locking. That safety is claimed only at the width M1 actually uses:
8
+ * criterion 15's two concurrent creates, which are witnessed. This
9
+ * phase's own verification measured failures above roughly six-way
10
+ * concurrency on both the fetch and the worktree add, and hardening
11
+ * for that width is deferred to M5 (see the deferral list in
12
+ * delivery/work-history/m1-p3.md). Do not read this as a guarantee at
13
+ * arbitrary concurrency. Substrate-neutral: pure filesystem and git
14
+ * (DR-0007).
15
+ *
16
+ * Base resolution is the five binding steps of EXT-F-03: resolve the
17
+ * project's configured remote and its default branch, fetch that branch,
18
+ * record the fetched base SHA in the pool record (and the CLI emits it on
19
+ * stdout; M1-P4 spawn copies it into tasks/<id>/meta.json as baseSha),
20
+ * create the task branch and worktree directly from that exact SHA, and
21
+ * on fetch failure fail rather than silently use a stale local branch,
22
+ * unless --offline was explicitly passed, in which case the last fetched
23
+ * remote-tracking SHA is used and offline: true is recorded. The clone's
24
+ * local branches are never consulted: a stale local branch is never the
25
+ * base, whether behind or ahead of the remote.
26
+ *
27
+ * The pool record lives BESIDE the worktree (worktrees/<task-id>.pool.json,
28
+ * plain JSON per D-3), never inside it, so the record can never dirty the
29
+ * destroy-time cleanliness check (FM-059: no exemption list, ever).
30
+ *
31
+ * Destroy refuses a dirty worktree (uncommitted changes or untracked
32
+ * files) unless --discard, which is reserved for the teardown scout path
33
+ * (PR-010). A transient git index.lock during destroy is retried; the
34
+ * lock file is removed only under a fail-safe staleness proof (provably
35
+ * no holder via lsof plus mtime age beyond a threshold; any uncertainty
36
+ * means leave it and fail loudly), per FM-036 and FM-051.
37
+ */
38
+ /** Task branch created by the pool at the fetched base SHA. */
39
+ export declare function taskBranchName(taskId: string): string;
40
+ /** Task ids are single safe path segments. */
41
+ export declare const TASK_ID_PATTERN: RegExp;
42
+ export interface PoolRecord {
43
+ taskId: string;
44
+ project: string;
45
+ remote: string;
46
+ branch: string;
47
+ baseSha: string;
48
+ branchName: string;
49
+ offline: boolean;
50
+ createdAt: string;
51
+ }
52
+ export type PoolResult<T> = {
53
+ ok: true;
54
+ value: T;
55
+ } | {
56
+ ok: false;
57
+ reason: string;
58
+ };
59
+ /** Exported for the contention-classification test. */
60
+ export declare function isTransientGitLockError(stderr: string): boolean;
61
+ export declare function recordPath(fleet: Fleet, taskId: string): string;
62
+ export declare function worktreePath(fleet: Fleet, taskId: string): string;
63
+ export declare function readPoolRecord(fleet: Fleet, taskId: string): PoolRecord | undefined;
64
+ export interface CreateOptions {
65
+ taskId: string;
66
+ project: string;
67
+ offline: boolean;
68
+ }
69
+ /**
70
+ * pool create (EXT-F-03 five steps; see module doc). Returns the pool
71
+ * record on success.
72
+ *
73
+ * On failure NOTHING IS REMOVED. The pool record, the worktree
74
+ * directory and the task branch may each survive, depending on how far
75
+ * the attempt got, and the reason line names exactly which of them did
76
+ * and the command that clears them. The automatic rollback this
77
+ * docstring used to promise was deleted deliberately: it served a
78
+ * concurrent-create path M1 never enters (parallelism is off until M5)
79
+ * and produced four consecutive rounds of defects, including deleting
80
+ * state it had not validated. Failing loudly and leaving state is the
81
+ * chosen contract, not an oversight.
82
+ */
83
+ export declare function poolCreate(fleet: Fleet, options: CreateOptions): Promise<PoolResult<PoolRecord>>;
84
+ export interface PoolListEntry {
85
+ taskId: string;
86
+ headSha: string;
87
+ }
88
+ /** One entry per pool record, with the worktree's current HEAD SHA. */
89
+ export declare function poolList(fleet: Fleet): PoolListEntry[];
90
+ export interface LsofProbe {
91
+ available: boolean;
92
+ exitCode: number | null;
93
+ stdout: string;
94
+ }
95
+ /** Age a lock file must reach before a staleness proof is even considered. */
96
+ export declare const STALE_LOCK_AGE_MS = 300000;
97
+ /**
98
+ * Fail-safe staleness proof for a git lock file (FM-036, FM-051): true
99
+ * only when the lock exists, its mtime age exceeds the threshold, and
100
+ * lsof is available and shows provably no holder (exit 1, empty stdout).
101
+ * Any uncertainty (lsof missing, erroring, or listing holders) is false:
102
+ * the lock is left in place and the operation fails loudly.
103
+ */
104
+ export declare function provablyStaleLock(lockFile: string, opts?: {
105
+ nowMs?: number;
106
+ ageThresholdMs?: number;
107
+ runLsof?: (path: string) => LsofProbe;
108
+ }): boolean;
109
+ export interface DestroyOptions {
110
+ taskId: string;
111
+ /** Override the dirty-worktree refusal (plan step 3, PR-010). */
112
+ discard: boolean;
113
+ /**
114
+ * Authorize deleting a task branch that carries commits beyond its
115
+ * recorded base (V-1). Deliberately distinct from discard, whose
116
+ * plan-defined meaning is the dirty-tree override only: conflating
117
+ * them would make the scout path silently destroy committed work.
118
+ * M1-P4 teardown passes this from its ship path after its landedness
119
+ * judgement, which makes the dependency on teardown explicit rather
120
+ * than assumed.
121
+ */
122
+ deleteBranchForce: boolean;
123
+ }
124
+ /** What a completed destroy removed, for the operator's record. */
125
+ export interface DestroyOutcome {
126
+ deletedBranch?: string;
127
+ /** The branch tip at deletion time: the recovery handle (V-1). */
128
+ deletedSha?: string;
129
+ }
130
+ export declare function poolDestroy(fleet: Fleet, options: DestroyOptions): Promise<PoolResult<DestroyOutcome>>;