@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,223 @@
1
+ /**
2
+ * Fleet-home layout (kernel plan v1, M1-P2 step 1; blueprint section 3 with
3
+ * the SC-002/SC-003 resolutions). The fleet home is a small git repository:
4
+ * durable content (charter/, decisions/, tasks/, backlog.md, package.json,
5
+ * .gitignore) is tracked; state/, worktrees/, and projects/ are ephemeral
6
+ * and gitignored (plan decision D-4, PR-004).
7
+ */
8
+ export declare const FLEET_DIRS: readonly ["charter", "decisions", "state", "tasks", "worktrees", "projects"];
9
+ /** Files every fleet home carries at its root. */
10
+ export declare const FLEET_FILES: readonly ["backlog.md", "package.json", ".gitignore"];
11
+ /**
12
+ * Exactly these entries are gitignored: clones under projects/ are
13
+ * recoverable from their remotes, worktrees/ are disposable, state/ holds
14
+ * beacons and locks (SC-002, plan decision D-4, PR-004). Nothing else.
15
+ */
16
+ export declare const FLEET_IGNORED: readonly ["state/", "worktrees/", "projects/"];
17
+ /**
18
+ * Well-known state file names. The lease lock is built by M1-P3 and the
19
+ * watcher beacon by M1-P5 (convention FM-043); doctor reads both as files
20
+ * only, never probing a process (plan constraint C-2). Task currency, when
21
+ * a later phase needs it, comes exclusively from tasks/<id>/meta.json and
22
+ * the turn-end file, never from a log tail (plan constraint C-1).
23
+ */
24
+ export declare const LOCK_FILE: string;
25
+ export declare const BEACON_FILE: string;
26
+ /** Typed accessors over a validated fleet home. */
27
+ export interface Fleet {
28
+ root: string;
29
+ charterDir: string;
30
+ decisionsDir: string;
31
+ stateDir: string;
32
+ tasksDir: string;
33
+ worktreesDir: string;
34
+ projectsDir: string;
35
+ backlogPath: string;
36
+ packageJsonPath: string;
37
+ gitignorePath: string;
38
+ lockPath: string;
39
+ beaconPath: string;
40
+ }
41
+ /**
42
+ * Return the layout entries missing from dir, in declaration order.
43
+ * Directories are reported with a trailing slash. An empty result means
44
+ * the layout is complete.
45
+ */
46
+ export declare function missingLayoutEntries(dir: string): string[];
47
+ /**
48
+ * Validate the layout at dir and return typed accessors. Throws an Error
49
+ * naming every missing entry when the layout is incomplete.
50
+ */
51
+ export declare function loadFleet(dir: string): Fleet;
52
+ /**
53
+ * A layout entry's type, established BEFORE anything is done with the path.
54
+ * `classifyEntry` in src/task.ts answers "may this be opened as a regular
55
+ * file" and therefore calls a directory irregular, which is the wrong answer
56
+ * for a layout entry: here a directory is the wanted shape. Same discipline,
57
+ * different question, so it is a separate function rather than a flag on that
58
+ * one (plan constraint C-2 is unaffected; nothing here probes a process).
59
+ */
60
+ export type LayoutEntryClass =
61
+ /** Nothing at the path. */
62
+ {
63
+ kind: "absent";
64
+ }
65
+ /** A directory, or a symlink resolving to one. */
66
+ | {
67
+ kind: "directory";
68
+ }
69
+ /** Present and not a directory, or a symlink resolving to nothing. */
70
+ | {
71
+ kind: "other";
72
+ reason: string;
73
+ }
74
+ /** Neither lstat nor stat could answer the question. */
75
+ | {
76
+ kind: "unexaminable";
77
+ reason: string;
78
+ };
79
+ /**
80
+ * Classify a layout path without opening it. lstat first, so a symlink is
81
+ * seen as a symlink; then stat, so a symlink to a directory is a directory
82
+ * and a dangling one is reported as such rather than as absent.
83
+ */
84
+ export declare function classifyLayoutEntry(path: string): LayoutEntryClass;
85
+ /**
86
+ * The EPHEMERAL directories: exactly the gitignored set, with the trailing
87
+ * slash that `.gitignore` needs stripped off. DERIVED from FLEET_IGNORED
88
+ * rather than listed again, because a second list is a second thing to keep
89
+ * in step and the first divergence would be silent: `tiphys resume` would
90
+ * rebuild one set while `.gitignore` ignored another.
91
+ */
92
+ export declare const EPHEMERAL_DIRS: readonly string[];
93
+ /**
94
+ * The DURABLE directories: every fleet directory that is not ephemeral.
95
+ * A clone of a fleet home carries these and not the ephemeral ones, which
96
+ * is the fact `tiphys resume` exists to act on.
97
+ */
98
+ export declare const DURABLE_DIRS: readonly string[];
99
+ /**
100
+ * The durable layout entries missing from dir, in declaration order:
101
+ * directories first with a trailing slash, then the root files. An empty
102
+ * result means the directory carries everything a clone of a fleet home
103
+ * carries, which is the precondition `tiphys resume` requires and never
104
+ * fabricates.
105
+ */
106
+ export declare function missingDurableEntries(dir: string): string[];
107
+ /**
108
+ * THE GUARDED OPEN, AT THE BOTTOM OF THE IMPORT GRAPH.
109
+ *
110
+ * `classifyEntry` in src/task.ts:118 asks exactly this question and its own
111
+ * docblock says where it belongs: "a general filesystem rule and not a task
112
+ * rule, and a dedicated module would be its right home. This module is the
113
+ * lowest one in the import graph that the fix round authorized to touch."
114
+ * This module is LOWER. src/task.ts imports src/lock.ts, src/lock.ts imports
115
+ * src/exclusion.ts, and this file imports nothing from the project at all, so
116
+ * the two modules that hold the kernel's lease and register reads CANNOT
117
+ * reach src/task.ts without making the first import cycle in `src/` (measured
118
+ * at this head: the graph is a strict DAG). They reach these instead.
119
+ *
120
+ * THE DUPLICATION IS REAL AND IT IS NAMED RATHER THAN HIDDEN. Two
121
+ * implementations of one question can drift, and the end state is src/task.ts
122
+ * re-exporting these. That edit is not in this round's declared file set, so
123
+ * it is escalated rather than made, and until it happens the vocabulary, the
124
+ * branch order and the refusal TEXT below are kept identical to src/task.ts's
125
+ * on purpose: doctor already prints "is a named pipe, not a regular file, so
126
+ * it was not opened" for a lease, and a second sentence for the same state
127
+ * would make two true reports read as two different conditions.
128
+ *
129
+ * C-2 is unaffected: lstat and stat are questions about a directory entry,
130
+ * never about a running program.
131
+ */
132
+ export type PathEntryClass =
133
+ /** Nothing at the path. */
134
+ {
135
+ kind: "absent";
136
+ }
137
+ /** A link is there and resolves to nothing: it exists, and it is empty of evidence. */
138
+ | {
139
+ kind: "dangling";
140
+ }
141
+ /** Safe to open. */
142
+ | {
143
+ kind: "regular";
144
+ }
145
+ /** Present, and opening it is not safe: never opened, always named. */
146
+ | {
147
+ kind: "irregular";
148
+ reason: string;
149
+ }
150
+ /** Neither lstat nor stat could answer the question. */
151
+ | {
152
+ kind: "unexaminable";
153
+ reason: string;
154
+ };
155
+ /**
156
+ * lstat first, so a symlink is seen as a symlink; then stat, so a symlink to
157
+ * a regular file is regular and a dangling one is reported as such rather
158
+ * than as absent. The path is never opened.
159
+ */
160
+ export declare function classifyPathEntry(path: string): PathEntryClass;
161
+ /** What a guarded read of a possibly-absent path produced. */
162
+ export type RegularPathRead = {
163
+ kind: "read";
164
+ body: string;
165
+ } | {
166
+ kind: "absent";
167
+ }
168
+ /** Present and not readable, with a reason naming the path. */
169
+ | {
170
+ kind: "refused";
171
+ reason: string;
172
+ };
173
+ /** THE ONE READ of a path that might not be there and might not be a file. */
174
+ export declare function readRegularPathIfPresent(path: string): RegularPathRead;
175
+ /**
176
+ * Refuse an open-for-WRITE of a path that is not a regular file. The hazard
177
+ * is symmetric: open(2) for writing on a FIFO with no reader blocks exactly
178
+ * as reading one with no writer does, so a staged write is as dangerous as a
179
+ * read. Returns the reason, or undefined when the path may be opened (absent
180
+ * included: creating it is the point).
181
+ */
182
+ export declare function refuseOpenPathForWrite(path: string): string | undefined;
183
+ /**
184
+ * THE KERNEL'S OWN WRITE-THEN-RENAME SCRATCH SUFFIXES.
185
+ *
186
+ * `FLEET_IGNORED` above is a DENYLIST of three directory prefixes, and
187
+ * `tiphys sync` derives "durable" as "not covered by it". That derivation is
188
+ * right for everything the ignore rules were written to cover and blind to a
189
+ * class they were never asked about: a transient artifact the KERNEL ITSELF
190
+ * creates beside a TRACKED target, which git therefore reports as an
191
+ * ordinary new durable path.
192
+ *
193
+ * The enumeration behind this list is every path the kernel writes under a
194
+ * fleet home, classified against `FLEET_IGNORED`. The three that land
195
+ * DURABLE without being fleet content are:
196
+ *
197
+ * status/current.json.tmp src/status.ts:142, a fixed name inside the
198
+ * tracked status/ directory.
199
+ * .cutover.<random>.tmp src/cutover.ts:278, a dot-prefixed random
200
+ * name at the fleet ROOT.
201
+ * tiphys-environment.json src/exclusion.ts:303, durable ON PURPOSE
202
+ * (M4-P21 criterion 3) and therefore NOT in
203
+ * this list.
204
+ *
205
+ * Two members, two directories, two naming shapes, which is why the rule is
206
+ * a SUFFIX and not a filename: a rule naming `current.json.tmp` would close
207
+ * the first and leave the second open.
208
+ *
209
+ * `FLEET_IGNORED` IS DELIBERATELY UNCHANGED. It drives `EPHEMERAL_DIRS`,
210
+ * `DURABLE_DIRS` and the `.gitignore` that `tiphys init` writes, so a glob
211
+ * added there would become a directory name `tiphys resume` tried to rebuild.
212
+ * Nothing stops being synced because of this constant; `tiphys sync` gains a
213
+ * refusal, and only for paths matching a suffix below.
214
+ */
215
+ export declare const FLEET_SCRATCH_SUFFIXES: readonly string[];
216
+ /**
217
+ * True when a fleet-relative path is a kernel scratch artifact by its name
218
+ * alone. Name-only on purpose: `tiphys sync` asks this about a path git
219
+ * REPORTED, which may already have been renamed away by the time the
220
+ * question is asked, so a stat here would answer about a different world
221
+ * than the one being committed.
222
+ */
223
+ export declare function isFleetScratchPath(relativePath: string): string | undefined;
@@ -0,0 +1,298 @@
1
+ import { existsSync, lstatSync, readFileSync, statSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ /**
4
+ * Fleet-home layout (kernel plan v1, M1-P2 step 1; blueprint section 3 with
5
+ * the SC-002/SC-003 resolutions). The fleet home is a small git repository:
6
+ * durable content (charter/, decisions/, tasks/, backlog.md, package.json,
7
+ * .gitignore) is tracked; state/, worktrees/, and projects/ are ephemeral
8
+ * and gitignored (plan decision D-4, PR-004).
9
+ */
10
+ export const FLEET_DIRS = [
11
+ "charter",
12
+ "decisions",
13
+ "state",
14
+ "tasks",
15
+ "worktrees",
16
+ "projects",
17
+ ];
18
+ /** Files every fleet home carries at its root. */
19
+ export const FLEET_FILES = ["backlog.md", "package.json", ".gitignore"];
20
+ /**
21
+ * Exactly these entries are gitignored: clones under projects/ are
22
+ * recoverable from their remotes, worktrees/ are disposable, state/ holds
23
+ * beacons and locks (SC-002, plan decision D-4, PR-004). Nothing else.
24
+ */
25
+ export const FLEET_IGNORED = ["state/", "worktrees/", "projects/"];
26
+ /**
27
+ * Well-known state file names. The lease lock is built by M1-P3 and the
28
+ * watcher beacon by M1-P5 (convention FM-043); doctor reads both as files
29
+ * only, never probing a process (plan constraint C-2). Task currency, when
30
+ * a later phase needs it, comes exclusively from tasks/<id>/meta.json and
31
+ * the turn-end file, never from a log tail (plan constraint C-1).
32
+ */
33
+ export const LOCK_FILE = join("state", "orchestrator.lock");
34
+ export const BEACON_FILE = join("state", "watcher.beacon");
35
+ /**
36
+ * Return the layout entries missing from dir, in declaration order.
37
+ * Directories are reported with a trailing slash. An empty result means
38
+ * the layout is complete.
39
+ */
40
+ export function missingLayoutEntries(dir) {
41
+ const missing = [];
42
+ for (const name of FLEET_DIRS) {
43
+ const p = join(dir, name);
44
+ if (!existsSync(p) || !statSync(p).isDirectory()) {
45
+ missing.push(`${name}/`);
46
+ }
47
+ }
48
+ for (const name of FLEET_FILES) {
49
+ const p = join(dir, name);
50
+ if (!existsSync(p) || !statSync(p).isFile()) {
51
+ missing.push(name);
52
+ }
53
+ }
54
+ return missing;
55
+ }
56
+ /**
57
+ * Validate the layout at dir and return typed accessors. Throws an Error
58
+ * naming every missing entry when the layout is incomplete.
59
+ */
60
+ export function loadFleet(dir) {
61
+ const root = resolve(dir);
62
+ const missing = missingLayoutEntries(root);
63
+ if (missing.length > 0) {
64
+ throw new Error(`not a fleet home: ${root} is missing ${missing.join(", ")}`);
65
+ }
66
+ return {
67
+ root,
68
+ charterDir: join(root, "charter"),
69
+ decisionsDir: join(root, "decisions"),
70
+ stateDir: join(root, "state"),
71
+ tasksDir: join(root, "tasks"),
72
+ worktreesDir: join(root, "worktrees"),
73
+ projectsDir: join(root, "projects"),
74
+ backlogPath: join(root, "backlog.md"),
75
+ packageJsonPath: join(root, "package.json"),
76
+ gitignorePath: join(root, ".gitignore"),
77
+ lockPath: join(root, LOCK_FILE),
78
+ beaconPath: join(root, BEACON_FILE),
79
+ };
80
+ }
81
+ /**
82
+ * Classify a layout path without opening it. lstat first, so a symlink is
83
+ * seen as a symlink; then stat, so a symlink to a directory is a directory
84
+ * and a dangling one is reported as such rather than as absent.
85
+ */
86
+ export function classifyLayoutEntry(path) {
87
+ try {
88
+ lstatSync(path);
89
+ }
90
+ catch (error) {
91
+ if (error.code === "ENOENT") {
92
+ return { kind: "absent" };
93
+ }
94
+ return {
95
+ kind: "unexaminable",
96
+ reason: `${path} could not be examined: ${String(error)}`,
97
+ };
98
+ }
99
+ let stats;
100
+ try {
101
+ stats = statSync(path);
102
+ }
103
+ catch (error) {
104
+ if (error.code === "ENOENT") {
105
+ return { kind: "other", reason: `${path} is a symlink to nothing` };
106
+ }
107
+ return {
108
+ kind: "unexaminable",
109
+ reason: `${path} could not be examined: ${String(error)}`,
110
+ };
111
+ }
112
+ if (stats.isDirectory()) {
113
+ return { kind: "directory" };
114
+ }
115
+ return { kind: "other", reason: `${path} exists and is not a directory` };
116
+ }
117
+ /**
118
+ * The EPHEMERAL directories: exactly the gitignored set, with the trailing
119
+ * slash that `.gitignore` needs stripped off. DERIVED from FLEET_IGNORED
120
+ * rather than listed again, because a second list is a second thing to keep
121
+ * in step and the first divergence would be silent: `tiphys resume` would
122
+ * rebuild one set while `.gitignore` ignored another.
123
+ */
124
+ export const EPHEMERAL_DIRS = FLEET_IGNORED.map((entry) => entry.endsWith("/") ? entry.slice(0, -1) : entry);
125
+ /**
126
+ * The DURABLE directories: every fleet directory that is not ephemeral.
127
+ * A clone of a fleet home carries these and not the ephemeral ones, which
128
+ * is the fact `tiphys resume` exists to act on.
129
+ */
130
+ export const DURABLE_DIRS = FLEET_DIRS.filter((name) => !EPHEMERAL_DIRS.includes(name));
131
+ /**
132
+ * The durable layout entries missing from dir, in declaration order:
133
+ * directories first with a trailing slash, then the root files. An empty
134
+ * result means the directory carries everything a clone of a fleet home
135
+ * carries, which is the precondition `tiphys resume` requires and never
136
+ * fabricates.
137
+ */
138
+ export function missingDurableEntries(dir) {
139
+ const missing = [];
140
+ for (const name of DURABLE_DIRS) {
141
+ if (classifyLayoutEntry(join(dir, name)).kind !== "directory") {
142
+ missing.push(`${name}/`);
143
+ }
144
+ }
145
+ for (const name of FLEET_FILES) {
146
+ const p = join(dir, name);
147
+ if (!existsSync(p) || !statSync(p).isFile()) {
148
+ missing.push(name);
149
+ }
150
+ }
151
+ return missing;
152
+ }
153
+ function describePathType(stats) {
154
+ if (stats.isDirectory()) {
155
+ return "a directory";
156
+ }
157
+ if (stats.isFIFO()) {
158
+ return "a named pipe";
159
+ }
160
+ if (stats.isSocket()) {
161
+ return "a socket";
162
+ }
163
+ if (stats.isCharacterDevice()) {
164
+ return "a character device";
165
+ }
166
+ if (stats.isBlockDevice()) {
167
+ return "a block device";
168
+ }
169
+ return "an entry of an unrecognized type";
170
+ }
171
+ /**
172
+ * lstat first, so a symlink is seen as a symlink; then stat, so a symlink to
173
+ * a regular file is regular and a dangling one is reported as such rather
174
+ * than as absent. The path is never opened.
175
+ */
176
+ export function classifyPathEntry(path) {
177
+ try {
178
+ lstatSync(path);
179
+ }
180
+ catch (error) {
181
+ if (error.code === "ENOENT") {
182
+ return { kind: "absent" };
183
+ }
184
+ return {
185
+ kind: "unexaminable",
186
+ reason: `${path} could not be examined: ${String(error)}`,
187
+ };
188
+ }
189
+ let stats;
190
+ try {
191
+ stats = statSync(path);
192
+ }
193
+ catch (error) {
194
+ if (error.code === "ENOENT") {
195
+ return { kind: "dangling" };
196
+ }
197
+ return {
198
+ kind: "unexaminable",
199
+ reason: `${path} could not be examined: ${String(error)}`,
200
+ };
201
+ }
202
+ if (stats.isFile()) {
203
+ return { kind: "regular" };
204
+ }
205
+ return {
206
+ kind: "irregular",
207
+ reason: `${path} is ${describePathType(stats)}, not a regular file, so it was not opened`,
208
+ };
209
+ }
210
+ /** THE ONE READ of a path that might not be there and might not be a file. */
211
+ export function readRegularPathIfPresent(path) {
212
+ const entry = classifyPathEntry(path);
213
+ if (entry.kind === "absent" || entry.kind === "dangling") {
214
+ return { kind: "absent" };
215
+ }
216
+ if (entry.kind === "irregular" || entry.kind === "unexaminable") {
217
+ return { kind: "refused", reason: entry.reason };
218
+ }
219
+ let body;
220
+ try {
221
+ body = readFileSync(path, "utf8");
222
+ }
223
+ catch (error) {
224
+ if (error.code === "ENOENT") {
225
+ // Removed between the probe and the read.
226
+ return { kind: "absent" };
227
+ }
228
+ return {
229
+ kind: "refused",
230
+ reason: `${path} could not be read: ${String(error)}`,
231
+ };
232
+ }
233
+ return { kind: "read", body };
234
+ }
235
+ /**
236
+ * Refuse an open-for-WRITE of a path that is not a regular file. The hazard
237
+ * is symmetric: open(2) for writing on a FIFO with no reader blocks exactly
238
+ * as reading one with no writer does, so a staged write is as dangerous as a
239
+ * read. Returns the reason, or undefined when the path may be opened (absent
240
+ * included: creating it is the point).
241
+ */
242
+ export function refuseOpenPathForWrite(path) {
243
+ const entry = classifyPathEntry(path);
244
+ if (entry.kind === "irregular" || entry.kind === "unexaminable") {
245
+ return entry.reason;
246
+ }
247
+ return undefined;
248
+ }
249
+ /* ------------------------------------------------------------------ */
250
+ /* The fleet home's SYNC CLASSES */
251
+ /* ------------------------------------------------------------------ */
252
+ /**
253
+ * THE KERNEL'S OWN WRITE-THEN-RENAME SCRATCH SUFFIXES.
254
+ *
255
+ * `FLEET_IGNORED` above is a DENYLIST of three directory prefixes, and
256
+ * `tiphys sync` derives "durable" as "not covered by it". That derivation is
257
+ * right for everything the ignore rules were written to cover and blind to a
258
+ * class they were never asked about: a transient artifact the KERNEL ITSELF
259
+ * creates beside a TRACKED target, which git therefore reports as an
260
+ * ordinary new durable path.
261
+ *
262
+ * The enumeration behind this list is every path the kernel writes under a
263
+ * fleet home, classified against `FLEET_IGNORED`. The three that land
264
+ * DURABLE without being fleet content are:
265
+ *
266
+ * status/current.json.tmp src/status.ts:142, a fixed name inside the
267
+ * tracked status/ directory.
268
+ * .cutover.<random>.tmp src/cutover.ts:278, a dot-prefixed random
269
+ * name at the fleet ROOT.
270
+ * tiphys-environment.json src/exclusion.ts:303, durable ON PURPOSE
271
+ * (M4-P21 criterion 3) and therefore NOT in
272
+ * this list.
273
+ *
274
+ * Two members, two directories, two naming shapes, which is why the rule is
275
+ * a SUFFIX and not a filename: a rule naming `current.json.tmp` would close
276
+ * the first and leave the second open.
277
+ *
278
+ * `FLEET_IGNORED` IS DELIBERATELY UNCHANGED. It drives `EPHEMERAL_DIRS`,
279
+ * `DURABLE_DIRS` and the `.gitignore` that `tiphys init` writes, so a glob
280
+ * added there would become a directory name `tiphys resume` tried to rebuild.
281
+ * Nothing stops being synced because of this constant; `tiphys sync` gains a
282
+ * refusal, and only for paths matching a suffix below.
283
+ */
284
+ export const FLEET_SCRATCH_SUFFIXES = [
285
+ ".tmp",
286
+ ".stage",
287
+ ".mutex",
288
+ ];
289
+ /**
290
+ * True when a fleet-relative path is a kernel scratch artifact by its name
291
+ * alone. Name-only on purpose: `tiphys sync` asks this about a path git
292
+ * REPORTED, which may already have been renamed away by the time the
293
+ * question is asked, so a stat here would answer about a different world
294
+ * than the one being committed.
295
+ */
296
+ export function isFleetScratchPath(relativePath) {
297
+ return FLEET_SCRATCH_SUFFIXES.find((suffix) => relativePath.endsWith(suffix));
298
+ }
@@ -0,0 +1,5 @@
1
+ /** RFC 6901 JSON pointer resolution. Absent is distinguished from null. */
2
+ export declare function resolvePointer(document: unknown, pointer: string): {
3
+ found: boolean;
4
+ value?: unknown;
5
+ };