@latysh-development/monolith 0.0.0-stage → 0.1.0-alpha.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 (291) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +132 -2
  3. package/dist/brief/cli.d.ts +30 -0
  4. package/dist/brief/cli.d.ts.map +1 -0
  5. package/dist/brief/cli.js +85 -0
  6. package/dist/brief/collect.d.ts +17 -0
  7. package/dist/brief/collect.d.ts.map +1 -0
  8. package/dist/brief/collect.js +302 -0
  9. package/dist/brief/config.d.ts +40 -0
  10. package/dist/brief/config.d.ts.map +1 -0
  11. package/dist/brief/config.js +85 -0
  12. package/dist/brief/constants.d.ts +10 -0
  13. package/dist/brief/constants.d.ts.map +1 -0
  14. package/dist/brief/constants.js +9 -0
  15. package/dist/brief/docs.d.ts +53 -0
  16. package/dist/brief/docs.d.ts.map +1 -0
  17. package/dist/brief/docs.js +229 -0
  18. package/dist/brief/freshness.d.ts +104 -0
  19. package/dist/brief/freshness.d.ts.map +1 -0
  20. package/dist/brief/freshness.js +263 -0
  21. package/dist/brief/git-signals.d.ts +17 -0
  22. package/dist/brief/git-signals.d.ts.map +1 -0
  23. package/dist/brief/git-signals.js +101 -0
  24. package/dist/brief/index.d.ts +11 -0
  25. package/dist/brief/index.d.ts.map +1 -0
  26. package/dist/brief/index.js +5 -0
  27. package/dist/brief/reconcile.d.ts +75 -0
  28. package/dist/brief/reconcile.d.ts.map +1 -0
  29. package/dist/brief/reconcile.js +259 -0
  30. package/dist/brief/render.d.ts +60 -0
  31. package/dist/brief/render.d.ts.map +1 -0
  32. package/dist/brief/render.js +407 -0
  33. package/dist/brief/sanitize.d.ts +38 -0
  34. package/dist/brief/sanitize.d.ts.map +1 -0
  35. package/dist/brief/sanitize.js +48 -0
  36. package/dist/brief/selectors.d.ts +36 -0
  37. package/dist/brief/selectors.d.ts.map +1 -0
  38. package/dist/brief/selectors.js +75 -0
  39. package/dist/brief/story.d.ts +18 -0
  40. package/dist/brief/story.d.ts.map +1 -0
  41. package/dist/brief/story.js +59 -0
  42. package/dist/brief/types.d.ts +110 -0
  43. package/dist/brief/types.d.ts.map +1 -0
  44. package/dist/brief/types.js +11 -0
  45. package/dist/cli/index.d.ts +3 -0
  46. package/dist/cli/index.d.ts.map +1 -0
  47. package/dist/cli/index.js +166 -0
  48. package/dist/cli/init-args.d.ts +16 -0
  49. package/dist/cli/init-args.d.ts.map +1 -0
  50. package/dist/cli/init-args.js +24 -0
  51. package/dist/cli/ui-args.d.ts +23 -0
  52. package/dist/cli/ui-args.d.ts.map +1 -0
  53. package/dist/cli/ui-args.js +66 -0
  54. package/dist/cli/ui.d.ts +40 -0
  55. package/dist/cli/ui.d.ts.map +1 -0
  56. package/dist/cli/ui.js +180 -0
  57. package/dist/coordinatorium/checkpoint.d.ts +44 -0
  58. package/dist/coordinatorium/checkpoint.d.ts.map +1 -0
  59. package/dist/coordinatorium/checkpoint.js +124 -0
  60. package/dist/coordinatorium/cli.d.ts +39 -0
  61. package/dist/coordinatorium/cli.d.ts.map +1 -0
  62. package/dist/coordinatorium/cli.js +203 -0
  63. package/dist/coordinatorium/errors.d.ts +112 -0
  64. package/dist/coordinatorium/errors.d.ts.map +1 -0
  65. package/dist/coordinatorium/errors.js +145 -0
  66. package/dist/coordinatorium/index.d.ts +11 -0
  67. package/dist/coordinatorium/index.d.ts.map +1 -0
  68. package/dist/coordinatorium/index.js +5 -0
  69. package/dist/coordinatorium/lock.d.ts +187 -0
  70. package/dist/coordinatorium/lock.d.ts.map +1 -0
  71. package/dist/coordinatorium/lock.js +659 -0
  72. package/dist/coordinatorium/mission.d.ts +32 -0
  73. package/dist/coordinatorium/mission.d.ts.map +1 -0
  74. package/dist/coordinatorium/mission.js +87 -0
  75. package/dist/coordinatorium/state-keeper.d.ts +115 -0
  76. package/dist/coordinatorium/state-keeper.d.ts.map +1 -0
  77. package/dist/coordinatorium/state-keeper.js +396 -0
  78. package/dist/coordinatorium/types.d.ts +69 -0
  79. package/dist/coordinatorium/types.d.ts.map +1 -0
  80. package/dist/coordinatorium/types.js +27 -0
  81. package/dist/durable/assess.d.ts +105 -0
  82. package/dist/durable/assess.d.ts.map +1 -0
  83. package/dist/durable/assess.js +239 -0
  84. package/dist/durable/canonical.d.ts +25 -0
  85. package/dist/durable/canonical.d.ts.map +1 -0
  86. package/dist/durable/canonical.js +78 -0
  87. package/dist/durable/cli.d.ts +37 -0
  88. package/dist/durable/cli.d.ts.map +1 -0
  89. package/dist/durable/cli.js +141 -0
  90. package/dist/durable/errors.d.ts +188 -0
  91. package/dist/durable/errors.d.ts.map +1 -0
  92. package/dist/durable/errors.js +317 -0
  93. package/dist/durable/fs-store.d.ts +233 -0
  94. package/dist/durable/fs-store.d.ts.map +1 -0
  95. package/dist/durable/fs-store.js +1393 -0
  96. package/dist/durable/index.d.ts +20 -0
  97. package/dist/durable/index.d.ts.map +1 -0
  98. package/dist/durable/index.js +13 -0
  99. package/dist/durable/log.d.ts +33 -0
  100. package/dist/durable/log.d.ts.map +1 -0
  101. package/dist/durable/log.js +157 -0
  102. package/dist/durable/memory-store.d.ts +51 -0
  103. package/dist/durable/memory-store.d.ts.map +1 -0
  104. package/dist/durable/memory-store.js +302 -0
  105. package/dist/durable/projection.d.ts +35 -0
  106. package/dist/durable/projection.d.ts.map +1 -0
  107. package/dist/durable/projection.js +328 -0
  108. package/dist/durable/prune-check.d.ts +17 -0
  109. package/dist/durable/prune-check.d.ts.map +1 -0
  110. package/dist/durable/prune-check.js +45 -0
  111. package/dist/durable/register.d.ts +100 -0
  112. package/dist/durable/register.d.ts.map +1 -0
  113. package/dist/durable/register.js +532 -0
  114. package/dist/durable/retention.d.ts +49 -0
  115. package/dist/durable/retention.d.ts.map +1 -0
  116. package/dist/durable/retention.js +59 -0
  117. package/dist/durable/runtime.d.ts +497 -0
  118. package/dist/durable/runtime.d.ts.map +1 -0
  119. package/dist/durable/runtime.js +1271 -0
  120. package/dist/durable/seams.d.ts +40 -0
  121. package/dist/durable/seams.d.ts.map +1 -0
  122. package/dist/durable/seams.js +21 -0
  123. package/dist/durable/testing.d.ts +11 -0
  124. package/dist/durable/testing.d.ts.map +1 -0
  125. package/dist/durable/testing.js +25 -0
  126. package/dist/durable/types.d.ts +442 -0
  127. package/dist/durable/types.d.ts.map +1 -0
  128. package/dist/durable/types.js +75 -0
  129. package/dist/graph/builder.d.ts +11 -0
  130. package/dist/graph/builder.d.ts.map +1 -0
  131. package/dist/graph/builder.js +153 -0
  132. package/dist/graph/identity.d.ts +37 -0
  133. package/dist/graph/identity.d.ts.map +1 -0
  134. package/dist/graph/identity.js +46 -0
  135. package/dist/graph/index.d.ts +7 -0
  136. package/dist/graph/index.d.ts.map +1 -0
  137. package/dist/graph/index.js +3 -0
  138. package/dist/graph/resolver.d.ts +41 -0
  139. package/dist/graph/resolver.d.ts.map +1 -0
  140. package/dist/graph/resolver.js +36 -0
  141. package/dist/graph/types.d.ts +42 -0
  142. package/dist/graph/types.d.ts.map +1 -0
  143. package/dist/graph/types.js +1 -0
  144. package/dist/http/index.d.ts +5 -0
  145. package/dist/http/index.d.ts.map +1 -0
  146. package/dist/http/index.js +2 -0
  147. package/dist/http/server.d.ts +76 -0
  148. package/dist/http/server.d.ts.map +1 -0
  149. package/dist/http/server.js +195 -0
  150. package/dist/http/ui-assets.d.ts +22 -0
  151. package/dist/http/ui-assets.d.ts.map +1 -0
  152. package/dist/http/ui-assets.js +100 -0
  153. package/dist/index.d.ts +31 -0
  154. package/dist/index.d.ts.map +1 -0
  155. package/dist/index.js +26 -0
  156. package/dist/init/index.d.ts +12 -0
  157. package/dist/init/index.d.ts.map +1 -0
  158. package/dist/init/index.js +90 -0
  159. package/dist/init/prompts.d.ts +3 -0
  160. package/dist/init/prompts.d.ts.map +1 -0
  161. package/dist/init/prompts.js +47 -0
  162. package/dist/init/scaffold.d.ts +25 -0
  163. package/dist/init/scaffold.d.ts.map +1 -0
  164. package/dist/init/scaffold.js +204 -0
  165. package/dist/init/types.d.ts +14 -0
  166. package/dist/init/types.d.ts.map +1 -0
  167. package/dist/init/types.js +1 -0
  168. package/dist/readmodel/dashboard.d.ts +40 -0
  169. package/dist/readmodel/dashboard.d.ts.map +1 -0
  170. package/dist/readmodel/dashboard.js +236 -0
  171. package/dist/readmodel/index.d.ts +3 -0
  172. package/dist/readmodel/index.d.ts.map +1 -0
  173. package/dist/readmodel/index.js +1 -0
  174. package/dist/readmodel/types.d.ts +104 -0
  175. package/dist/readmodel/types.d.ts.map +1 -0
  176. package/dist/readmodel/types.js +15 -0
  177. package/dist/root/index.d.ts +179 -0
  178. package/dist/root/index.d.ts.map +1 -0
  179. package/dist/root/index.js +418 -0
  180. package/dist/root/safe-write.d.ts +268 -0
  181. package/dist/root/safe-write.d.ts.map +1 -0
  182. package/dist/root/safe-write.js +966 -0
  183. package/dist/root/write-artifacts.d.ts +20 -0
  184. package/dist/root/write-artifacts.d.ts.map +1 -0
  185. package/dist/root/write-artifacts.js +42 -0
  186. package/dist/scriptorium/compile.d.ts +4 -0
  187. package/dist/scriptorium/compile.d.ts.map +1 -0
  188. package/dist/scriptorium/compile.js +185 -0
  189. package/dist/scriptorium/frontmatter.d.ts +13 -0
  190. package/dist/scriptorium/frontmatter.d.ts.map +1 -0
  191. package/dist/scriptorium/frontmatter.js +90 -0
  192. package/dist/scriptorium/index.d.ts +3 -0
  193. package/dist/scriptorium/index.d.ts.map +1 -0
  194. package/dist/scriptorium/index.js +1 -0
  195. package/dist/scriptorium/markdown.d.ts +31 -0
  196. package/dist/scriptorium/markdown.d.ts.map +1 -0
  197. package/dist/scriptorium/markdown.js +88 -0
  198. package/dist/scriptorium/types.d.ts +78 -0
  199. package/dist/scriptorium/types.d.ts.map +1 -0
  200. package/dist/scriptorium/types.js +1 -0
  201. package/dist/types/enforcement.d.ts +11 -0
  202. package/dist/types/enforcement.d.ts.map +1 -0
  203. package/dist/types/enforcement.js +1 -0
  204. package/dist/types/integrity.d.ts +33 -0
  205. package/dist/types/integrity.d.ts.map +1 -0
  206. package/dist/types/integrity.js +1 -0
  207. package/dist/types/project-definition.d.ts +30 -0
  208. package/dist/types/project-definition.d.ts.map +1 -0
  209. package/dist/types/project-definition.js +1 -0
  210. package/dist/ui/assets/cosmic-glass-bg-Dwx47zPu.webp +0 -0
  211. package/dist/ui/assets/index-C4NhMaq7.js +28 -0
  212. package/dist/ui/assets/index-DMZ7ZvZY.css +1 -0
  213. package/dist/ui/index.html +13 -0
  214. package/dist/validate/index.d.ts +6 -0
  215. package/dist/validate/index.d.ts.map +1 -0
  216. package/dist/validate/index.js +214 -0
  217. package/dist/version.d.ts +2 -0
  218. package/dist/version.d.ts.map +1 -0
  219. package/dist/version.js +1 -0
  220. package/package.json +67 -4
  221. package/templates/.gitkeep +0 -0
  222. package/templates/default-instance/instruction/.cursorrules +53 -0
  223. package/templates/default-instance/instruction/.windsurfrules +53 -0
  224. package/templates/default-instance/instruction/CLAUDE.md +53 -0
  225. package/templates/default-instance/monolith/context/boot/HANDOFF.md +49 -0
  226. package/templates/default-instance/monolith/context/boot/WORK_SESSION.md +30 -0
  227. package/templates/default-instance/monolith/context/execution/current-state.md +30 -0
  228. package/templates/default-instance/monolith/context/maps/dependency-map.md +38 -0
  229. package/templates/default-instance/monolith/context/maps/project-map.md +85 -0
  230. package/templates/default-instance/monolith/context/maps/roadmap-index.md +41 -0
  231. package/templates/default-instance/monolith/context/maps/routing-map.md +147 -0
  232. package/templates/default-instance/monolith/context/maps/semantic-map.md +73 -0
  233. package/templates/default-instance/monolith/context/models/model-registry.md +37 -0
  234. package/templates/default-instance/monolith/context/models/privacy-classification.md +35 -0
  235. package/templates/default-instance/monolith/context/templates/plan-template.md +56 -0
  236. package/templates/default-instance/monolith/governance/README.md +29 -0
  237. package/templates/default-instance/monolith/governance/RULE_SCHEMA.md +58 -0
  238. package/templates/default-instance/monolith/learning/INDEX.md +48 -0
  239. package/templates/default-instance/monolith/learning/injection-map.md +49 -0
  240. package/templates/default-instance/monolith/learning/taxonomy.md +30 -0
  241. package/templates/default-instance/monolith/memory/errors/README.md +26 -0
  242. package/templates/default-instance/monolith/memory/errors/SCHEMA.md +47 -0
  243. package/templates/default-instance/monolith/memory/errors/TAXONOMY.md +27 -0
  244. package/templates/default-instance/monolith/memory/long-term/architecture-principles.md +31 -0
  245. package/templates/default-instance/monolith/memory/long-term/decisions.md +36 -0
  246. package/templates/default-instance/monolith/memory/long-term/execution-history.md +20 -0
  247. package/templates/default-instance/monolith/memory/long-term/tech-debt.md +25 -0
  248. package/templates/default-instance/monolith/memory/working/active.md +41 -0
  249. package/templates/default-instance/monolith/memory/working/governance-result.md +0 -0
  250. package/templates/default-instance/monolith/memory/working/integrity-result.md +0 -0
  251. package/templates/default-instance/monolith/memory/working/story-index.md +28 -0
  252. package/templates/default-instance/monolith/protocols/boot/boot-sequence.md +59 -0
  253. package/templates/default-instance/monolith/protocols/cognition/cognitive-modes.md +90 -0
  254. package/templates/default-instance/monolith/protocols/cognition/dry-run.md +56 -0
  255. package/templates/default-instance/monolith/protocols/cognition/execution-trace-format.md +52 -0
  256. package/templates/default-instance/monolith/protocols/cognition/semantic-planning.md +51 -0
  257. package/templates/default-instance/monolith/protocols/execution/hybrid-execution.md +37 -0
  258. package/templates/default-instance/monolith/protocols/execution/local-llm-governance.md +42 -0
  259. package/templates/default-instance/monolith/protocols/governance/command-policy.md +51 -0
  260. package/templates/default-instance/monolith/protocols/governance/core-invariants.md +40 -0
  261. package/templates/default-instance/monolith/protocols/governance/memory-governance.md +54 -0
  262. package/templates/default-instance/monolith/protocols/governance/metadata-schema.md +62 -0
  263. package/templates/default-instance/monolith/protocols/governance/skills.md +53 -0
  264. package/templates/default-instance/monolith/protocols/governance/work-session.md +46 -0
  265. package/templates/default-instance/monolith/skills/INDEX.md +28 -0
  266. package/templates/default-instance/monolith/skills/current-status/SKILL.md +47 -0
  267. package/templates/default-instance/monolith/skills/finalize-story/SKILL.md +86 -0
  268. package/templates/default-instance/monolith/skills/global-status/SKILL.md +56 -0
  269. package/templates/default-instance/monolith/skills/self-improvement/SKILL.md +77 -0
  270. package/templates/default-instance/monolith/state.json +11 -0
  271. package/templates/default-instance/monolith/tests/INDEX.md +57 -0
  272. package/templates/default-instance/monolith/tests/scenarios/architecture-review.md +32 -0
  273. package/templates/default-instance/monolith/tests/scenarios/debug-failure.md +32 -0
  274. package/templates/default-instance/monolith/tests/scenarios/dry-run-activation.md +33 -0
  275. package/templates/default-instance/monolith/tests/scenarios/execution-trace-verification.md +31 -0
  276. package/templates/default-instance/monolith/tests/scenarios/finalize-story.md +35 -0
  277. package/templates/default-instance/monolith/tests/scenarios/implement-feature.md +36 -0
  278. package/templates/default-instance/monolith/tests/scenarios/local-llm-delegation.md +31 -0
  279. package/templates/default-instance/monolith/tests/scenarios/planning-request.md +32 -0
  280. package/templates/default-instance/monolith/tests/scenarios/sensitive-context-task.md +30 -0
  281. package/templates/default-instance/monolith/tests/scenarios/validator-verification.md +33 -0
  282. package/templates/default-instance/monolith/tests/validation/expected-behavior.md +70 -0
  283. package/templates/default-instance/monolith/tests/validators/INDEX.md +17 -0
  284. package/templates/default-instance/monolith/tests/validators/delegation-semantics.md +22 -0
  285. package/templates/default-instance/monolith/tests/validators/mode-consistency.md +25 -0
  286. package/templates/default-instance/monolith/tests/validators/privacy-routing.md +23 -0
  287. package/templates/default-instance/monolith/tests/validators/protocol-references.md +27 -0
  288. package/templates/default-instance/monolith/tests/validators/routing-integrity.md +30 -0
  289. package/templates/default-instance/projectroot/AGENTS.md +107 -0
  290. package/templates/default-instance/projectroot/monolith.config.json +32 -0
  291. package/templates/default-instance/projectroot/project.definition.mjs.template +41 -0
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Single source of truth for where a MONOLITH instance lives (SEC-001A).
3
+ *
4
+ * Policy (ADR-009):
5
+ * - `monolithRoot` is a RELATIVE path strictly INSIDE the project root.
6
+ * - Absent config file, or absent `monolithRoot` key → documented default
7
+ * `.monolith`. A config file that EXISTS but cannot be used (unreadable,
8
+ * invalid JSON, not an object) or a `monolithRoot` that is present but not
9
+ * a non-empty string → configuration error (never silently defaulted).
10
+ * - Absolute paths, any `..` segment, and a root that normalizes to the
11
+ * project root itself are rejected. (`..` is rejected even when it would
12
+ * lexically stay inside: the kernel resolves `a/..` through `a`, so a
13
+ * symlinked `a` makes lexical and real resolution diverge.)
14
+ * - Every existing component of the path must resolve (realpath) inside
15
+ * the real project root; dangling symlinks and symlink loops fail closed.
16
+ * - Violations throw RootContainmentError — callers never fall back.
17
+ *
18
+ * This module only reads (lstat/realpath/open+read). It never creates
19
+ * anything. Check-then-use (TOCTOU) races are NOT closed here; see ADR-009,
20
+ * ADR-011 and ADR-014 (the unified safe read contract implemented below).
21
+ *
22
+ * It also hosts the bounded project-file helpers (`checkProjectFile`,
23
+ * `readProjectFile`, SEC-002) used for root-level files such as
24
+ * `project.definition.mjs`, `monolith.config.json` and `AGENTS.md`, which
25
+ * live next to — not below — `monolithRoot`.
26
+ */
27
+ export declare const DEFAULT_MONOLITH_ROOT = ".monolith";
28
+ export declare const CONFIG_FILE_NAME = "monolith.config.json";
29
+ export type RootErrorCode = 'ROOT_INVALID_TYPE' | 'ROOT_EMPTY' | 'ROOT_ABSOLUTE' | 'ROOT_TRAVERSAL' | 'ROOT_IS_PROJECT_ROOT' | 'ROOT_SYMLINK_ESCAPE' | 'ROOT_UNRESOLVABLE' | 'ROOT_CONFIG_INVALID';
30
+ export declare class RootContainmentError extends Error {
31
+ readonly code: RootErrorCode;
32
+ constructor(code: RootErrorCode, message: string);
33
+ }
34
+ export type ConfigRead = {
35
+ status: 'absent';
36
+ } | {
37
+ status: 'ok';
38
+ cfg: Record<string, unknown>;
39
+ } | {
40
+ status: 'unreadable' | 'invalid-json' | 'not-object';
41
+ detail: string;
42
+ };
43
+ /** Read `monolith.config.json` without interpreting `monolithRoot`. */
44
+ export declare function readConfigFile(targetDir: string): ConfigRead;
45
+ /**
46
+ * Lexical validation. Returns the normalized root: `/`-joined segments with
47
+ * empty and `.` segments removed. Throws RootContainmentError otherwise.
48
+ */
49
+ export declare function normalizeMonolithRoot(value: unknown): string;
50
+ export interface ResolvedMonolithRoot {
51
+ /** Absolute, lexical project root (`resolve(targetDir)`). */
52
+ projectRoot: string;
53
+ /** Real (symlink-free) project root. */
54
+ projectRootReal: string;
55
+ /** Validated relative root, `/`-separated, never empty. */
56
+ monolithRoot: string;
57
+ /** `projectRoot` + `monolithRoot`. Not realpath'd, so display paths stay stable. */
58
+ rootAbs: string;
59
+ source: 'config' | 'default';
60
+ }
61
+ /** Validate a root value against a project directory (no config read). */
62
+ export declare function resolveMonolithRootValue(targetDir: string, value: unknown): ResolvedMonolithRoot;
63
+ /**
64
+ * Resolve the instance root for `targetDir`. Pass an already-read config
65
+ * (`readConfigFile`) to avoid reading twice. Only an ABSENT config file yields
66
+ * the default root; an existing but unusable one throws ROOT_CONFIG_INVALID
67
+ * (fail closed) so a broken config can never redirect writes to `.monolith`.
68
+ */
69
+ export declare function resolveMonolithRoot(targetDir: string, config?: ConfigRead): ResolvedMonolithRoot;
70
+ /**
71
+ * Verify a fixed file below the instance root (state.json, active.md,
72
+ * state.json.lock, ...) against the real project root, so a symlinked file
73
+ * or a symlinked intermediate directory cannot redirect I/O outside.
74
+ * Returns the lexical absolute path. `relFromRoot` must be trusted/fixed.
75
+ */
76
+ export declare function assertContainedFile(root: ResolvedMonolithRoot, relFromRoot: string): string;
77
+ /** Non-throwing variant for read paths that report diagnostics. */
78
+ export declare function checkContainedFile(root: ResolvedMonolithRoot, relFromRoot: string): RootContainmentError | null;
79
+ /** Upper bound for any file this module inspects or reads (1 MiB). */
80
+ export declare const MAX_PROJECT_FILE_BYTES: number;
81
+ export type ProjectFileErrorCode = 'FILE_MISSING' | 'FILE_OUTSIDE_PROJECT' | 'FILE_UNRESOLVABLE' | 'FILE_NOT_REGULAR' | 'FILE_EMPTY' | 'FILE_TOO_LARGE' | 'FILE_CHANGED' | 'FILE_UNREADABLE' | 'FILE_SYMLINK' | 'FILE_HARDLINKED' | 'READ_BOUNDARY_INVALID' | 'READ_UNSUPPORTED_PLATFORM';
82
+ /**
83
+ * Codes that mean "the file system layout is not what a MONOLITH-managed file
84
+ * must look like" — as opposed to missing, empty, oversized or a transient
85
+ * replacement. A caller may want to surface these differently from an I/O fault.
86
+ */
87
+ export declare const READ_SECURITY_CODES: readonly ProjectFileErrorCode[];
88
+ export declare function isReadSecurityViolation(code: ProjectFileErrorCode): boolean;
89
+ export interface ProjectFileOptions {
90
+ /** Size cap in bytes. Defaults to MAX_PROJECT_FILE_BYTES. */
91
+ maxBytes?: number;
92
+ /** Accept a zero-byte file. Defaults to false. */
93
+ allowEmpty?: boolean;
94
+ }
95
+ /**
96
+ * Options of the unified reader. `boundary` is mandatory and explicit: an
97
+ * absolute directory whose ANCESTORS (and the directory itself) are trusted —
98
+ * the project root the user pointed at, or, for a caller that says so, the
99
+ * directory that holds the file. Every component below it is checked.
100
+ */
101
+ export interface BoundedReadOptions extends ProjectFileOptions {
102
+ boundary: string;
103
+ /**
104
+ * @internal Test seam: the `open(2)` flags the platform provides. Omitted in
105
+ * production (taken from `fs.constants`). A missing flag makes the read fail
106
+ * closed with READ_UNSUPPORTED_PLATFORM.
107
+ */
108
+ platformFlags?: {
109
+ noFollow?: number;
110
+ nonBlock?: number;
111
+ };
112
+ /** @internal Test seam: runs between the `lstat` walk and the `open`, so a test can swap the file deterministically. */
113
+ afterInspect?: () => void;
114
+ }
115
+ export type ProjectFileCheck = {
116
+ ok: true;
117
+ path: string;
118
+ size: number;
119
+ } | {
120
+ ok: false;
121
+ code: ProjectFileErrorCode;
122
+ detail: string;
123
+ };
124
+ export type ProjectFileRead = {
125
+ ok: true;
126
+ path: string;
127
+ size: number;
128
+ content: string;
129
+ } | {
130
+ ok: false;
131
+ code: ProjectFileErrorCode;
132
+ detail: string;
133
+ };
134
+ /**
135
+ * The unified safe read (SEC-001B.1-R). One policy for every MONOLITH-managed
136
+ * file; there is no permissive variant. Order matters:
137
+ * 1. platform capabilities (`O_NOFOLLOW`, `O_NONBLOCK`) — absent → fail closed;
138
+ * 2. explicit boundary: `abs` and `boundary` must be absolute (never the cwd),
139
+ * the boundary must be a directory, `abs` must lie strictly below it;
140
+ * 3. `lstat` of EVERY component below the boundary: none may be a symlink
141
+ * (inside the project or not, dangling or not); the last must be a regular
142
+ * file with a single link — decided BEFORE `open`, so a FIFO/device/socket
143
+ * is never opened;
144
+ * 4. `open` with `O_NOFOLLOW|O_NONBLOCK|O_NOCTTY`;
145
+ * 5. `fstat` on the descriptor: still a regular file, same `dev/ino` as step 3,
146
+ * still one link, size within bounds — all before any content is read;
147
+ * 6. optional read of at most `maxBytes + 1` bytes (the cap applies to bytes
148
+ * actually read); the descriptor is always closed.
149
+ * Steps 3 and 4 are separate syscalls: this does NOT defend against a local
150
+ * actor swapping a directory or file between them (dynamic TOCTOU, SEC-001B.3).
151
+ * The identity check detects a swap of the FILE only. `label` is for diagnostics.
152
+ * A file replaced between `lstat` and `open` is FILE_CHANGED (transient,
153
+ * retryable), not a fault.
154
+ */
155
+ export declare function readBoundedFile(abs: string, label: string, opts: BoundedReadOptions, wantContent: boolean): ProjectFileRead | ProjectFileCheck;
156
+ /**
157
+ * `readBoundedFile`, retried across the two states MONOLITH's own writers pass
158
+ * through (neither is a fault): the file is atomically replaced under us
159
+ * (FILE_CHANGED, `rename`), or it has a second link for an instant
160
+ * (FILE_HARDLINKED: a lock is published by `link()`ing its staging file and
161
+ * the staging name is removed right after, SEC-001B.2). After the retries the
162
+ * result is reported as it is — a link that stays is refused, never accepted.
163
+ * Same policy, same options.
164
+ */
165
+ export declare function readBoundedFileStable(abs: string, label: string, opts: BoundedReadOptions, wantContent: boolean): ProjectFileRead | ProjectFileCheck;
166
+ /**
167
+ * Static check of a file below the PROJECT root (not `monolithRoot`) under the
168
+ * unified read policy: exists, no symlink in any component below the root,
169
+ * regular file with one link, non-empty (unless `allowEmpty`) and within
170
+ * `maxBytes`. The content is never read.
171
+ */
172
+ export declare function checkProjectFile(projectRoot: string, relPath: string, opts?: ProjectFileOptions): ProjectFileCheck;
173
+ /** `checkProjectFile` plus a bounded UTF-8 read of the content. */
174
+ export declare function readProjectFile(projectRoot: string, relPath: string, opts?: ProjectFileOptions): ProjectFileRead;
175
+ export { SafeWriteError, WriteBoundary, ParentRef, projectBoundary, trustedDirectoryBoundary, openParent, boundaryPathOf, probeTarget, createExclusive, disposeOwned, publishLink, createFile, replaceFile, detectPlatformCaps, reportWriteWarning, resetWriteWarningState, } from './safe-write.js';
176
+ export { findWriteArtifacts, describeWriteArtifacts } from './write-artifacts.js';
177
+ export type { WriteArtifact, WriteArtifactKind } from './write-artifacts.js';
178
+ export type { PublicationState, WriteErrorCode, WriteWarning, WriteWarningCode, WriteScopeMode, WriteOptions, WriteOutcome, DurabilityRequest, DurabilityReached, TargetProbe, OwnedToken, OwnedFile, DisposeResult, PlatformCaps, TempFileInfo, LinkPublishResult, FsOps, } from './safe-write.js';
179
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/root/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,418 @@
1
+ /**
2
+ * Single source of truth for where a MONOLITH instance lives (SEC-001A).
3
+ *
4
+ * Policy (ADR-009):
5
+ * - `monolithRoot` is a RELATIVE path strictly INSIDE the project root.
6
+ * - Absent config file, or absent `monolithRoot` key → documented default
7
+ * `.monolith`. A config file that EXISTS but cannot be used (unreadable,
8
+ * invalid JSON, not an object) or a `monolithRoot` that is present but not
9
+ * a non-empty string → configuration error (never silently defaulted).
10
+ * - Absolute paths, any `..` segment, and a root that normalizes to the
11
+ * project root itself are rejected. (`..` is rejected even when it would
12
+ * lexically stay inside: the kernel resolves `a/..` through `a`, so a
13
+ * symlinked `a` makes lexical and real resolution diverge.)
14
+ * - Every existing component of the path must resolve (realpath) inside
15
+ * the real project root; dangling symlinks and symlink loops fail closed.
16
+ * - Violations throw RootContainmentError — callers never fall back.
17
+ *
18
+ * This module only reads (lstat/realpath/open+read). It never creates
19
+ * anything. Check-then-use (TOCTOU) races are NOT closed here; see ADR-009,
20
+ * ADR-011 and ADR-014 (the unified safe read contract implemented below).
21
+ *
22
+ * It also hosts the bounded project-file helpers (`checkProjectFile`,
23
+ * `readProjectFile`, SEC-002) used for root-level files such as
24
+ * `project.definition.mjs`, `monolith.config.json` and `AGENTS.md`, which
25
+ * live next to — not below — `monolithRoot`.
26
+ */
27
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, realpathSync, statSync } from 'fs';
28
+ import { isAbsolute, join, relative, resolve, sep, win32 } from 'path';
29
+ export const DEFAULT_MONOLITH_ROOT = '.monolith';
30
+ export const CONFIG_FILE_NAME = 'monolith.config.json';
31
+ export class RootContainmentError extends Error {
32
+ code;
33
+ constructor(code, message) {
34
+ super(`${code}: ${message}`);
35
+ this.name = 'RootContainmentError';
36
+ this.code = code;
37
+ }
38
+ }
39
+ function errMsg(err) {
40
+ return err instanceof Error ? err.message : String(err);
41
+ }
42
+ /** Read `monolith.config.json` without interpreting `monolithRoot`. */
43
+ export function readConfigFile(targetDir) {
44
+ // Bounded, containment-checked read (SEC-002): a FIFO/device/oversized config
45
+ // or a symlink leaving the project is "unreadable" (fail closed), never
46
+ // blocking and never read past MAX_PROJECT_FILE_BYTES. An absent file, and
47
+ // only an absent file, is "absent".
48
+ const read = readProjectFile(targetDir, CONFIG_FILE_NAME, { allowEmpty: true });
49
+ if (!read.ok) {
50
+ if (read.code === 'FILE_MISSING')
51
+ return { status: 'absent' };
52
+ return { status: 'unreadable', detail: `${read.code}: ${read.detail}` };
53
+ }
54
+ let parsed;
55
+ try {
56
+ parsed = JSON.parse(read.content);
57
+ }
58
+ catch (err) {
59
+ return { status: 'invalid-json', detail: errMsg(err) };
60
+ }
61
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
62
+ return { status: 'not-object', detail: 'does not contain a JSON object' };
63
+ }
64
+ return { status: 'ok', cfg: parsed };
65
+ }
66
+ /**
67
+ * Lexical validation. Returns the normalized root: `/`-joined segments with
68
+ * empty and `.` segments removed. Throws RootContainmentError otherwise.
69
+ */
70
+ export function normalizeMonolithRoot(value) {
71
+ if (typeof value !== 'string') {
72
+ throw new RootContainmentError('ROOT_INVALID_TYPE', `monolithRoot must be a string, got ${value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value}.`);
73
+ }
74
+ if (value.length === 0) {
75
+ throw new RootContainmentError('ROOT_EMPTY', 'monolithRoot must not be an empty string.');
76
+ }
77
+ if (value.includes('\0')) {
78
+ throw new RootContainmentError('ROOT_INVALID_TYPE', 'monolithRoot must not contain NUL bytes.');
79
+ }
80
+ // Both flavors are rejected on every platform: a config must mean the same thing everywhere.
81
+ if (isAbsolute(value) || win32.isAbsolute(value) || /^[A-Za-z]:/.test(value)) {
82
+ throw new RootContainmentError('ROOT_ABSOLUTE', `monolithRoot must be relative to the project root, got "${value}".`);
83
+ }
84
+ const segments = value.split(/[\\/]+/).filter((s) => s !== '' && s !== '.');
85
+ if (segments.includes('..')) {
86
+ throw new RootContainmentError('ROOT_TRAVERSAL', `monolithRoot must not contain ".." segments, got "${value}".`);
87
+ }
88
+ if (segments.length === 0) {
89
+ throw new RootContainmentError('ROOT_IS_PROJECT_ROOT', `monolithRoot "${value}" resolves to the project root itself; use a subdirectory such as "${DEFAULT_MONOLITH_ROOT}".`);
90
+ }
91
+ return segments.join('/');
92
+ }
93
+ /** Path-component-aware containment (never a string prefix test). */
94
+ function isInside(baseAbs, candidateAbs) {
95
+ const rel = relative(baseAbs, candidateAbs);
96
+ return rel === '' || (rel.split(/[\\/]/)[0] !== '..' && !isAbsolute(rel));
97
+ }
98
+ function realProjectRoot(projectRoot) {
99
+ try {
100
+ return realpathSync(projectRoot);
101
+ }
102
+ catch (err) {
103
+ throw new RootContainmentError('ROOT_UNRESOLVABLE', `project root "${projectRoot}" cannot be resolved: ${errMsg(err)}`);
104
+ }
105
+ }
106
+ /**
107
+ * Walk `segments` below the real project root. Each component that exists
108
+ * is realpath'd and must stay inside; the first missing component ends the
109
+ * walk (the nearest existing ancestor has already been verified). A symlink
110
+ * whose target is missing, or a loop, is refused.
111
+ */
112
+ function assertSegmentsContained(projectRootReal, segments, label) {
113
+ let current = projectRootReal;
114
+ for (const seg of segments) {
115
+ const next = join(current, seg);
116
+ let isLink;
117
+ try {
118
+ isLink = lstatSync(next).isSymbolicLink();
119
+ }
120
+ catch (err) {
121
+ if (err.code === 'ENOENT')
122
+ return;
123
+ throw new RootContainmentError('ROOT_UNRESOLVABLE', `"${label}" cannot be inspected at "${seg}": ${errMsg(err)}`);
124
+ }
125
+ let real;
126
+ try {
127
+ real = realpathSync(next);
128
+ }
129
+ catch (err) {
130
+ const why = err.code === 'ENOENT' ? 'dangling symlink' : errMsg(err);
131
+ throw new RootContainmentError('ROOT_UNRESOLVABLE', `"${label}" cannot be resolved at "${seg}" (${why}).`);
132
+ }
133
+ if (!isInside(projectRootReal, real)) {
134
+ throw new RootContainmentError('ROOT_SYMLINK_ESCAPE', `"${label}" leaves the project root at "${seg}"${isLink ? ' (symlink)' : ''}.`);
135
+ }
136
+ current = real;
137
+ }
138
+ }
139
+ /** Validate a root value against a project directory (no config read). */
140
+ export function resolveMonolithRootValue(targetDir, value) {
141
+ const monolithRoot = normalizeMonolithRoot(value);
142
+ const projectRoot = resolve(targetDir);
143
+ const projectRootReal = realProjectRoot(projectRoot);
144
+ assertSegmentsContained(projectRootReal, monolithRoot.split('/'), monolithRoot);
145
+ return {
146
+ projectRoot,
147
+ projectRootReal,
148
+ monolithRoot,
149
+ rootAbs: join(projectRoot, monolithRoot),
150
+ source: 'default',
151
+ };
152
+ }
153
+ /**
154
+ * Resolve the instance root for `targetDir`. Pass an already-read config
155
+ * (`readConfigFile`) to avoid reading twice. Only an ABSENT config file yields
156
+ * the default root; an existing but unusable one throws ROOT_CONFIG_INVALID
157
+ * (fail closed) so a broken config can never redirect writes to `.monolith`.
158
+ */
159
+ export function resolveMonolithRoot(targetDir, config = readConfigFile(targetDir)) {
160
+ if (config.status !== 'absent' && config.status !== 'ok') {
161
+ throw new RootContainmentError('ROOT_CONFIG_INVALID', `${CONFIG_FILE_NAME} exists but is unusable (${config.status}: ${config.detail}); refusing to fall back to "${DEFAULT_MONOLITH_ROOT}".`);
162
+ }
163
+ if (config.status === 'ok' && Object.prototype.hasOwnProperty.call(config.cfg, 'monolithRoot')) {
164
+ return { ...resolveMonolithRootValue(targetDir, config.cfg.monolithRoot), source: 'config' };
165
+ }
166
+ return resolveMonolithRootValue(targetDir, DEFAULT_MONOLITH_ROOT);
167
+ }
168
+ /**
169
+ * Verify a fixed file below the instance root (state.json, active.md,
170
+ * state.json.lock, ...) against the real project root, so a symlinked file
171
+ * or a symlinked intermediate directory cannot redirect I/O outside.
172
+ * Returns the lexical absolute path. `relFromRoot` must be trusted/fixed.
173
+ */
174
+ export function assertContainedFile(root, relFromRoot) {
175
+ const segments = [...root.monolithRoot.split('/'), ...relFromRoot.split('/')];
176
+ assertSegmentsContained(root.projectRootReal, segments, `${root.monolithRoot}/${relFromRoot}`);
177
+ return join(root.rootAbs, relFromRoot);
178
+ }
179
+ /** Non-throwing variant for read paths that report diagnostics. */
180
+ export function checkContainedFile(root, relFromRoot) {
181
+ try {
182
+ assertContainedFile(root, relFromRoot);
183
+ return null;
184
+ }
185
+ catch (err) {
186
+ if (err instanceof RootContainmentError)
187
+ return err;
188
+ throw err;
189
+ }
190
+ }
191
+ // ---------------------------------------------------------------------------
192
+ // Unified safe read contract (SEC-002, ADR-011; SEC-001B.1-R, ADR-014)
193
+ // ---------------------------------------------------------------------------
194
+ /** Upper bound for any file this module inspects or reads (1 MiB). */
195
+ export const MAX_PROJECT_FILE_BYTES = 1024 * 1024;
196
+ const READ_CHUNK_BYTES = 64 * 1024;
197
+ /**
198
+ * Codes that mean "the file system layout is not what a MONOLITH-managed file
199
+ * must look like" — as opposed to missing, empty, oversized or a transient
200
+ * replacement. A caller may want to surface these differently from an I/O fault.
201
+ */
202
+ export const READ_SECURITY_CODES = [
203
+ 'FILE_OUTSIDE_PROJECT',
204
+ 'FILE_SYMLINK',
205
+ 'FILE_HARDLINKED',
206
+ 'FILE_NOT_REGULAR',
207
+ 'READ_BOUNDARY_INVALID',
208
+ 'READ_UNSUPPORTED_PLATFORM',
209
+ ];
210
+ export function isReadSecurityViolation(code) {
211
+ return READ_SECURITY_CODES.includes(code);
212
+ }
213
+ function fail(code, detail) {
214
+ return { ok: false, code, detail };
215
+ }
216
+ function errCode(err) {
217
+ return err?.code;
218
+ }
219
+ /** Segments of `abs` below `base`, or `null` when it is not strictly inside. */
220
+ function segmentsBelow(base, abs) {
221
+ const rel = relative(base, abs);
222
+ if (rel === '' || rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel))
223
+ return null;
224
+ return rel.split(sep).filter((s) => s.length > 0);
225
+ }
226
+ /**
227
+ * The unified safe read (SEC-001B.1-R). One policy for every MONOLITH-managed
228
+ * file; there is no permissive variant. Order matters:
229
+ * 1. platform capabilities (`O_NOFOLLOW`, `O_NONBLOCK`) — absent → fail closed;
230
+ * 2. explicit boundary: `abs` and `boundary` must be absolute (never the cwd),
231
+ * the boundary must be a directory, `abs` must lie strictly below it;
232
+ * 3. `lstat` of EVERY component below the boundary: none may be a symlink
233
+ * (inside the project or not, dangling or not); the last must be a regular
234
+ * file with a single link — decided BEFORE `open`, so a FIFO/device/socket
235
+ * is never opened;
236
+ * 4. `open` with `O_NOFOLLOW|O_NONBLOCK|O_NOCTTY`;
237
+ * 5. `fstat` on the descriptor: still a regular file, same `dev/ino` as step 3,
238
+ * still one link, size within bounds — all before any content is read;
239
+ * 6. optional read of at most `maxBytes + 1` bytes (the cap applies to bytes
240
+ * actually read); the descriptor is always closed.
241
+ * Steps 3 and 4 are separate syscalls: this does NOT defend against a local
242
+ * actor swapping a directory or file between them (dynamic TOCTOU, SEC-001B.3).
243
+ * The identity check detects a swap of the FILE only. `label` is for diagnostics.
244
+ * A file replaced between `lstat` and `open` is FILE_CHANGED (transient,
245
+ * retryable), not a fault.
246
+ */
247
+ export function readBoundedFile(abs, label, opts, wantContent) {
248
+ const noFollow = opts.platformFlags ? opts.platformFlags.noFollow : constants.O_NOFOLLOW;
249
+ const nonBlock = opts.platformFlags ? opts.platformFlags.nonBlock : constants.O_NONBLOCK;
250
+ if (typeof noFollow !== 'number' || noFollow === 0 || typeof nonBlock !== 'number' || nonBlock === 0) {
251
+ return fail('READ_UNSUPPORTED_PLATFORM', `${label}: this platform lacks O_NOFOLLOW/O_NONBLOCK, so a managed file cannot be read safely; refusing.`);
252
+ }
253
+ const boundary = opts.boundary;
254
+ if (typeof boundary !== 'string' || boundary.length === 0 || !isAbsolute(boundary)) {
255
+ return fail('READ_BOUNDARY_INVALID', `${label}: a read requires an explicit absolute boundary directory (never the cwd).`);
256
+ }
257
+ if (typeof abs !== 'string' || abs.length === 0 || !isAbsolute(abs)) {
258
+ return fail('READ_BOUNDARY_INVALID', `${label}: the path must be absolute (never resolved against the cwd).`);
259
+ }
260
+ const boundaryLexical = resolve(boundary);
261
+ let boundaryReal;
262
+ try {
263
+ boundaryReal = realpathSync(boundaryLexical);
264
+ if (!statSync(boundaryReal).isDirectory())
265
+ throw new Error('not a directory');
266
+ }
267
+ catch (err) {
268
+ return fail('READ_BOUNDARY_INVALID', `${label}: boundary "${boundaryLexical}" is not a usable directory: ${errMsg(err)}`);
269
+ }
270
+ const target = resolve(abs);
271
+ let base = boundaryLexical;
272
+ let segs = segmentsBelow(boundaryLexical, target);
273
+ if (segs === null) {
274
+ base = boundaryReal;
275
+ segs = segmentsBelow(boundaryReal, target);
276
+ }
277
+ if (segs === null) {
278
+ return fail('FILE_OUTSIDE_PROJECT', `${label} is not strictly inside the read boundary "${boundaryLexical}".`);
279
+ }
280
+ let before;
281
+ let current = base;
282
+ for (let i = 0; i < segs.length; i++) {
283
+ current = join(current, segs[i]);
284
+ const last = i === segs.length - 1;
285
+ let st;
286
+ try {
287
+ st = lstatSync(current, { bigint: true });
288
+ }
289
+ catch (err) {
290
+ if (errCode(err) === 'ENOENT')
291
+ return fail('FILE_MISSING', `${label} does not exist.`);
292
+ return fail('FILE_UNRESOLVABLE', `${label} cannot be inspected at "${segs[i]}": ${errMsg(err)}`);
293
+ }
294
+ if (st.isSymbolicLink()) {
295
+ return fail('FILE_SYMLINK', `${label} passes through a symbolic link at "${segs[i]}"; managed files must not.`);
296
+ }
297
+ if (!last) {
298
+ if (!st.isDirectory())
299
+ return fail('FILE_UNRESOLVABLE', `${label} cannot be resolved: "${segs[i]}" is not a directory.`);
300
+ continue;
301
+ }
302
+ if (!st.isFile())
303
+ return fail('FILE_NOT_REGULAR', `${label} is not a regular file.`);
304
+ if (st.nlink > 1n)
305
+ return fail('FILE_HARDLINKED', `${label} has ${st.nlink} hard links; managed files must have exactly one.`);
306
+ before = st;
307
+ }
308
+ if (before === undefined)
309
+ return fail('READ_BOUNDARY_INVALID', `${label} is the boundary itself, not a file below it.`);
310
+ const maxBytes = opts.maxBytes ?? MAX_PROJECT_FILE_BYTES;
311
+ opts.afterInspect?.();
312
+ let fd;
313
+ try {
314
+ fd = openSync(current, constants.O_RDONLY | noFollow | nonBlock | constants.O_NOCTTY);
315
+ }
316
+ catch (err) {
317
+ if (errCode(err) === 'ENOENT')
318
+ return fail('FILE_MISSING', `${label} does not exist.`);
319
+ // O_NOFOLLOW on a symlink: the path became a link after the lstat.
320
+ if (errCode(err) === 'ELOOP')
321
+ return fail('FILE_SYMLINK', `${label} became a symbolic link while being opened.`);
322
+ return fail('FILE_UNREADABLE', `${label} cannot be opened: ${errMsg(err)}`);
323
+ }
324
+ try {
325
+ const st = fstatSync(fd, { bigint: true });
326
+ if (!st.isFile())
327
+ return fail('FILE_NOT_REGULAR', `${label} is not a regular file.`);
328
+ if (st.dev !== before.dev || st.ino !== before.ino) {
329
+ return fail('FILE_CHANGED', `${label} changed while it was being inspected.`);
330
+ }
331
+ if (st.nlink > 1n)
332
+ return fail('FILE_HARDLINKED', `${label} has ${st.nlink} hard links; managed files must have exactly one.`);
333
+ if (st.size === 0n && !opts.allowEmpty)
334
+ return fail('FILE_EMPTY', `${label} is empty.`);
335
+ if (st.size > BigInt(maxBytes))
336
+ return fail('FILE_TOO_LARGE', `${label} is ${st.size} bytes; the limit is ${maxBytes}.`);
337
+ if (!wantContent)
338
+ return { ok: true, path: abs, size: Number(st.size) };
339
+ const chunks = [];
340
+ let total = 0;
341
+ for (;;) {
342
+ const buf = Buffer.allocUnsafe(Math.min(READ_CHUNK_BYTES, maxBytes + 1 - total));
343
+ const n = readSync(fd, buf, 0, buf.length, null);
344
+ if (n === 0)
345
+ break;
346
+ total += n;
347
+ if (total > maxBytes)
348
+ return fail('FILE_TOO_LARGE', `${label} grew beyond the limit of ${maxBytes} bytes while being read.`);
349
+ chunks.push(buf.subarray(0, n));
350
+ }
351
+ return { ok: true, path: abs, size: total, content: Buffer.concat(chunks).toString('utf8') };
352
+ }
353
+ catch (err) {
354
+ return fail('FILE_UNREADABLE', `${label} cannot be read: ${errMsg(err)}`);
355
+ }
356
+ finally {
357
+ closeSync(fd);
358
+ }
359
+ }
360
+ const CHANGED_RETRIES = 3;
361
+ const LINKED_RETRIES = 5;
362
+ const LINKED_RETRY_DELAY_MS = 2;
363
+ function sleepMs(ms) {
364
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
365
+ }
366
+ /**
367
+ * `readBoundedFile`, retried across the two states MONOLITH's own writers pass
368
+ * through (neither is a fault): the file is atomically replaced under us
369
+ * (FILE_CHANGED, `rename`), or it has a second link for an instant
370
+ * (FILE_HARDLINKED: a lock is published by `link()`ing its staging file and
371
+ * the staging name is removed right after, SEC-001B.2). After the retries the
372
+ * result is reported as it is — a link that stays is refused, never accepted.
373
+ * Same policy, same options.
374
+ */
375
+ export function readBoundedFileStable(abs, label, opts, wantContent) {
376
+ let r = readBoundedFile(abs, label, opts, wantContent);
377
+ let changed = 1;
378
+ let linked = 1;
379
+ while (!r.ok) {
380
+ if (r.code === 'FILE_CHANGED' && changed < CHANGED_RETRIES) {
381
+ changed++;
382
+ }
383
+ else if (r.code === 'FILE_HARDLINKED' && linked < LINKED_RETRIES) {
384
+ linked++;
385
+ sleepMs(LINKED_RETRY_DELAY_MS);
386
+ }
387
+ else {
388
+ break;
389
+ }
390
+ r = readBoundedFile(abs, label, opts, wantContent);
391
+ }
392
+ return r;
393
+ }
394
+ /** `projectRoot` is the explicit boundary; `relPath` is `/`-separated and must be trusted/fixed. */
395
+ function openProjectFile(projectRoot, relPath, opts, wantContent) {
396
+ if (typeof projectRoot !== 'string' || projectRoot.length === 0 || !isAbsolute(projectRoot)) {
397
+ return fail('READ_BOUNDARY_INVALID', `${relPath}: the project root must be an explicit absolute path (never the cwd).`);
398
+ }
399
+ return readBoundedFile(join(projectRoot, relPath), relPath, { ...opts, boundary: projectRoot }, wantContent);
400
+ }
401
+ /**
402
+ * Static check of a file below the PROJECT root (not `monolithRoot`) under the
403
+ * unified read policy: exists, no symlink in any component below the root,
404
+ * regular file with one link, non-empty (unless `allowEmpty`) and within
405
+ * `maxBytes`. The content is never read.
406
+ */
407
+ export function checkProjectFile(projectRoot, relPath, opts = {}) {
408
+ return openProjectFile(projectRoot, relPath, opts, false);
409
+ }
410
+ /** `checkProjectFile` plus a bounded UTF-8 read of the content. */
411
+ export function readProjectFile(projectRoot, relPath, opts = {}) {
412
+ return openProjectFile(projectRoot, relPath, opts, true);
413
+ }
414
+ // ---------------------------------------------------------------------------
415
+ // Safe writes (SEC-001B.2, ADR-012)
416
+ // ---------------------------------------------------------------------------
417
+ export { SafeWriteError, WriteBoundary, ParentRef, projectBoundary, trustedDirectoryBoundary, openParent, boundaryPathOf, probeTarget, createExclusive, disposeOwned, publishLink, createFile, replaceFile, detectPlatformCaps, reportWriteWarning, resetWriteWarningState, } from './safe-write.js';
418
+ export { findWriteArtifacts, describeWriteArtifacts } from './write-artifacts.js';