@mnstry/atelier 0.2.0-alpha.1 → 0.2.0-alpha.11

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 (414) hide show
  1. package/CHANGELOG.md +413 -0
  2. package/README.md +402 -205
  3. package/SECURITY.md +6 -1
  4. package/contracts/atelier-adoption-policy.v1.schema.json +49 -0
  5. package/contracts/atelier-coauthor.v1.schema.json +163 -0
  6. package/contracts/atelier-decision-request.v1.schema.json +476 -0
  7. package/contracts/atelier-decision-result.v1.schema.json +500 -0
  8. package/contracts/atelier-guide.v1.schema.json +227 -0
  9. package/contracts/atelier-intake.v1.schema.json +151 -0
  10. package/contracts/atelier-migration.v2.schema.json +61 -0
  11. package/contracts/atelier-obsidian-acceptance-receipt.v1.schema.json +211 -0
  12. package/contracts/atelier-obsidian-apply-policy.v1.schema.json +298 -0
  13. package/contracts/atelier-obsidian-corpus-profile.v1.schema.json +98 -0
  14. package/contracts/atelier-obsidian-edit-operation.v1.schema.json +138 -0
  15. package/contracts/atelier-obsidian-ext-settings.v1.schema.json +287 -0
  16. package/contracts/atelier-obsidian-generation-manifest.v1.schema.json +353 -0
  17. package/contracts/atelier-obsidian-proposal-receipt.v1.schema.json +110 -0
  18. package/contracts/atelier-obsidian-publication-journal.v1.schema.json +167 -0
  19. package/contracts/atelier-obsidian-scope.v1.schema.json +264 -0
  20. package/contracts/atelier-obsidian-service-state.v1.schema.json +149 -0
  21. package/contracts/atelier-obsidian-source-snapshot.v1.schema.json +179 -0
  22. package/contracts/atelier-pane-presentation.v1.schema.json +76 -0
  23. package/contracts/atelier-presentation.v1.schema.json +1293 -0
  24. package/contracts/atelier-repository-observation.v1.schema.json +163 -0
  25. package/contracts/atelier-review.v1.schema.json +1601 -0
  26. package/contracts/atelier-skill-steward.v1.schema.json +692 -0
  27. package/contracts/atelier-upgrade-plan.v2.schema.json +398 -0
  28. package/contracts/atelier-upgrade-receipt.v1.schema.json +99 -0
  29. package/contracts/atelier-vault-publication.v1.schema.json +58 -0
  30. package/contracts/public-api-baseline.json +57 -0
  31. package/docs/assurance-controls.md +49 -0
  32. package/docs/atelier-runtime.md +28 -2
  33. package/docs/atelier-sync.md +172 -0
  34. package/docs/blocks/audience-visibility.md +9 -0
  35. package/docs/blocks/claims.md +61 -0
  36. package/docs/blocks/conformance-admission.md +15 -0
  37. package/docs/blocks/will-not-do.md +18 -0
  38. package/docs/coauthor-session.md +57 -0
  39. package/docs/connected-composition.md +75 -0
  40. package/docs/continuity.md +3 -4
  41. package/docs/decisions.md +167 -0
  42. package/docs/design.md +167 -0
  43. package/docs/exact-upgrades.md +159 -0
  44. package/docs/extension-pack-lifecycle.md +33 -0
  45. package/docs/guided-upgrades.md +99 -0
  46. package/docs/install.md +99 -16
  47. package/docs/intake-and-guides.md +65 -0
  48. package/docs/integration-and-review-plan.md +409 -0
  49. package/docs/integration-contract-decisions.md +22 -0
  50. package/docs/knowledge-graph.md +8 -4
  51. package/docs/local-review.md +84 -0
  52. package/docs/local-services.md +328 -0
  53. package/docs/local-state.md +107 -0
  54. package/docs/obsidian-contract.md +698 -0
  55. package/docs/obsidian.md +484 -0
  56. package/docs/portable-presentation.md +328 -0
  57. package/docs/presentation-consumer-boundaries.md +75 -0
  58. package/docs/presentation-review-disposition.md +47 -0
  59. package/docs/project-command-inventory.md +38 -0
  60. package/docs/project-options.md +53 -0
  61. package/docs/release-engineering.md +174 -12
  62. package/docs/repo-boundary-guard.md +12 -2
  63. package/docs/review-portability.md +55 -0
  64. package/docs/skill-steward.md +156 -0
  65. package/docs/upgrade.md +155 -5
  66. package/docs/vault-service.md +292 -0
  67. package/fixtures/atelier-coauthor/invalid/authority.v1.json +5 -0
  68. package/fixtures/atelier-coauthor/valid/config.v1.json +12 -0
  69. package/fixtures/atelier-guide/invalid/authority.v1.json +11 -0
  70. package/fixtures/atelier-guide/valid/capability.v1.json +10 -0
  71. package/fixtures/atelier-guide/valid/consent.v1.json +11 -0
  72. package/fixtures/atelier-guide/valid/engagement.v1.json +7 -0
  73. package/fixtures/atelier-guide/valid/offer.v1.json +10 -0
  74. package/fixtures/atelier-intake/invalid/authority.v1.json +9 -0
  75. package/fixtures/atelier-intake/valid/attempt.v1.json +8 -0
  76. package/fixtures/atelier-intake/valid/completion.v1.json +9 -0
  77. package/fixtures/atelier-intake/valid/source.v1.json +7 -0
  78. package/fixtures/atelier-repository-observation/invalid/complete-with-blocker.v1.json +18 -0
  79. package/fixtures/atelier-repository-observation/valid/complete-local.v1.json +48 -0
  80. package/fixtures/atelier-review/invalid/authenticated-identity.v1.json +22 -0
  81. package/fixtures/atelier-review/valid/bound.v1.json +229 -0
  82. package/fixtures/atelier-review/valid/bundle.v1.json +267 -0
  83. package/fixtures/atelier-review/valid/contribution.v1.json +22 -0
  84. package/fixtures/atelier-review/valid/lifecycle.v1.json +14 -0
  85. package/fixtures/atelier-skill-steward/audit/invalid/source-mutation.v1.json +26 -0
  86. package/fixtures/atelier-skill-steward/audit/valid/clean.v1.json +26 -0
  87. package/fixtures/atelier-skill-steward/candidates/invalid/workflow-key.v1.json +37 -0
  88. package/fixtures/atelier-skill-steward/candidates/valid/create.v1.json +37 -0
  89. package/fixtures/atelier-skill-steward/lock/invalid/bad-digest.v1.json +18 -0
  90. package/fixtures/atelier-skill-steward/lock/valid/installed.v1.json +23 -0
  91. package/fixtures/atelier-skill-steward/plan/invalid/delete-action.v1.json +33 -0
  92. package/fixtures/atelier-skill-steward/plan/valid/add.v1.json +33 -0
  93. package/fixtures/atelier-upgrade-transaction/adoption-policy/invalid/example.json +12 -0
  94. package/fixtures/atelier-upgrade-transaction/adoption-policy/valid/example.json +11 -0
  95. package/fixtures/atelier-upgrade-transaction/migration/invalid/example.json +20 -0
  96. package/fixtures/atelier-upgrade-transaction/migration/valid/example.json +19 -0
  97. package/fixtures/atelier-upgrade-transaction/upgrade-plan/invalid/example.json +58 -0
  98. package/fixtures/atelier-upgrade-transaction/upgrade-plan/valid/example.json +57 -0
  99. package/fixtures/atelier-upgrade-transaction/upgrade-receipt/invalid/example.json +15 -0
  100. package/fixtures/atelier-upgrade-transaction/upgrade-receipt/valid/example.json +14 -0
  101. package/fixtures/atelier-vault-publication/invalid/publication.json +5 -0
  102. package/fixtures/atelier-vault-publication/valid/publication.json +11 -0
  103. package/fixtures/decisions/request/invalid/unknown-authority.v1.json +61 -0
  104. package/fixtures/decisions/request/valid/triage.v1.json +60 -0
  105. package/fixtures/decisions/result/invalid/execution-authority.v1.json +50 -0
  106. package/fixtures/decisions/result/valid/abstained.v1.json +23 -0
  107. package/fixtures/decisions/result/valid/assessed.v1.json +50 -0
  108. package/fixtures/obsidian/acceptance/ap05-extra-notes.json +14 -0
  109. package/fixtures/obsidian/acceptance/receipts/G07.valid.v1.json +63 -0
  110. package/fixtures/obsidian/acceptance/receipts/G13.valid.v1.json +69 -0
  111. package/fixtures/obsidian/acceptance/receipts/G14.valid.v1.json +62 -0
  112. package/fixtures/obsidian/acceptance/receipts/G15.valid.v1.json +56 -0
  113. package/fixtures/obsidian/acceptance/receipts/G16.valid.v1.json +73 -0
  114. package/fixtures/obsidian/acceptance/receipts/G17.valid.v1.json +75 -0
  115. package/fixtures/obsidian/acceptance/receipts/G18.valid.v1.json +64 -0
  116. package/fixtures/obsidian/acceptance/service-sentinels.json +10 -0
  117. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/duplicate-evidence-name.v1.json +39 -0
  118. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/evidence-by-absolute-path.v1.json +34 -0
  119. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/missing-candidate-tree.v1.json +33 -0
  120. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/no-evidence.v1.json +28 -0
  121. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-outcome.v1.json +34 -0
  122. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-top-level-field.v1.json +35 -0
  123. package/fixtures/obsidian/contracts/acceptance-receipt/valid/node-gate.v1.json +34 -0
  124. package/fixtures/obsidian/contracts/apply-policy/invalid/automatic-without-edit-class.v1.json +27 -0
  125. package/fixtures/obsidian/contracts/apply-policy/invalid/conflict-overwrite.v1.json +29 -0
  126. package/fixtures/obsidian/contracts/apply-policy/invalid/missing-policy-digest.v1.json +28 -0
  127. package/fixtures/obsidian/contracts/apply-policy/invalid/unbounded-batch.v1.json +29 -0
  128. package/fixtures/obsidian/contracts/apply-policy/invalid/unimplemented-edit-class.v1.json +30 -0
  129. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-mode.v1.json +29 -0
  130. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-selector-operator.v1.json +22 -0
  131. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-top-level-field.v1.json +30 -0
  132. package/fixtures/obsidian/contracts/apply-policy/valid/automatic-scoped.v1.json +29 -0
  133. package/fixtures/obsidian/contracts/apply-policy/valid/manual-default.v1.json +20 -0
  134. package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-path-in-ext.v1.json +30 -0
  135. package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-repository-root.v1.json +27 -0
  136. package/fixtures/obsidian/contracts/corpus-profile/invalid/duplicate-repository-id.v1.json +27 -0
  137. package/fixtures/obsidian/contracts/corpus-profile/invalid/overlapping-managed-roots.v1.json +27 -0
  138. package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-enrollment.v1.json +27 -0
  139. package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-top-level-field.v1.json +28 -0
  140. package/fixtures/obsidian/contracts/corpus-profile/valid/three-repositories.v1.json +27 -0
  141. package/fixtures/obsidian/contracts/edit-operation/invalid/absolute-recovery-ref.v1.json +21 -0
  142. package/fixtures/obsidian/contracts/edit-operation/invalid/inline-observed-bytes.v1.json +22 -0
  143. package/fixtures/obsidian/contracts/edit-operation/invalid/missing-base-digest.v1.json +20 -0
  144. package/fixtures/obsidian/contracts/edit-operation/invalid/short-idempotency-key.v1.json +21 -0
  145. package/fixtures/obsidian/contracts/edit-operation/invalid/unknown-kind.v1.json +21 -0
  146. package/fixtures/obsidian/contracts/edit-operation/valid/pending-body-replacement.v1.json +21 -0
  147. package/fixtures/obsidian/contracts/ext-settings/invalid/absolute-path-prefix.v1.json +28 -0
  148. package/fixtures/obsidian/contracts/ext-settings/invalid/apply-policy-in-portable-settings.v1.json +30 -0
  149. package/fixtures/obsidian/contracts/ext-settings/invalid/duplicate-scope-id.v1.json +27 -0
  150. package/fixtures/obsidian/contracts/ext-settings/invalid/machine-local-vault-path.v1.json +28 -0
  151. package/fixtures/obsidian/contracts/ext-settings/invalid/service-port-in-portable-settings.v1.json +28 -0
  152. package/fixtures/obsidian/contracts/ext-settings/invalid/unknown-default-scope.v1.json +27 -0
  153. package/fixtures/obsidian/contracts/ext-settings/valid/disabled.v1.json +5 -0
  154. package/fixtures/obsidian/contracts/ext-settings/valid/enabled-with-scopes.v1.json +27 -0
  155. package/fixtures/obsidian/contracts/generation-manifest/invalid/absolute-note-path.v1.json +114 -0
  156. package/fixtures/obsidian/contracts/generation-manifest/invalid/complete-with-unwritten-notes.v1.json +114 -0
  157. package/fixtures/obsidian/contracts/generation-manifest/invalid/derived-link-claimed-declared.v1.json +114 -0
  158. package/fixtures/obsidian/contracts/generation-manifest/invalid/duplicate-note-identity.v1.json +114 -0
  159. package/fixtures/obsidian/contracts/generation-manifest/invalid/in-scope-endpoint-missing.v1.json +114 -0
  160. package/fixtures/obsidian/contracts/generation-manifest/invalid/title-only-note-path.v1.json +114 -0
  161. package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-note-field.v1.json +115 -0
  162. package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-relation-type.v1.json +114 -0
  163. package/fixtures/obsidian/contracts/generation-manifest/invalid/withheld-endpoint.v1.json +114 -0
  164. package/fixtures/obsidian/contracts/generation-manifest/valid/two-notes.v1.json +114 -0
  165. package/fixtures/obsidian/contracts/oracles/scope-cases.json +412 -0
  166. package/fixtures/obsidian/contracts/proposal-receipt/invalid/accepted-without-proposal-id.v1.json +12 -0
  167. package/fixtures/obsidian/contracts/proposal-receipt/invalid/missing-adapter-operation-id.v1.json +11 -0
  168. package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-backpressure-outcome.v1.json +12 -0
  169. package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-top-level-field.v1.json +13 -0
  170. package/fixtures/obsidian/contracts/proposal-receipt/valid/accepted-new.v1.json +12 -0
  171. package/fixtures/obsidian/contracts/proposal-receipt/valid/deferred.v1.json +12 -0
  172. package/fixtures/obsidian/contracts/publication-journal/invalid/absolute-recovery-ref.v1.json +47 -0
  173. package/fixtures/obsidian/contracts/publication-journal/invalid/duplicate-sequence.v1.json +47 -0
  174. package/fixtures/obsidian/contracts/publication-journal/invalid/empty-protocol-id.v1.json +47 -0
  175. package/fixtures/obsidian/contracts/publication-journal/invalid/missing-protocol-id.v1.json +46 -0
  176. package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-entry-field.v1.json +48 -0
  177. package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-state.v1.json +47 -0
  178. package/fixtures/obsidian/contracts/publication-journal/valid/committed.v1.json +47 -0
  179. package/fixtures/obsidian/contracts/publication-journal/valid/updating-after-restart.v1.json +22 -0
  180. package/fixtures/obsidian/contracts/scope/invalid/absolute-path-prefix.v1.json +15 -0
  181. package/fixtures/obsidian/contracts/scope/invalid/duplicate-ids.v1.json +18 -0
  182. package/fixtures/obsidian/contracts/scope/invalid/expansion-without-node-budget.v1.json +40 -0
  183. package/fixtures/obsidian/contracts/scope/invalid/full-mode-with-subset.v1.json +8 -0
  184. package/fixtures/obsidian/contracts/scope/invalid/unknown-mode.v1.json +41 -0
  185. package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-key.v1.json +15 -0
  186. package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-operator.v1.json +14 -0
  187. package/fixtures/obsidian/contracts/scope/invalid/unknown-top-level-field.v1.json +42 -0
  188. package/fixtures/obsidian/contracts/scope/valid/explicit-empty.v1.json +8 -0
  189. package/fixtures/obsidian/contracts/scope/valid/focus.v1.json +11 -0
  190. package/fixtures/obsidian/contracts/scope/valid/full.v1.json +8 -0
  191. package/fixtures/obsidian/contracts/scope/valid/scoped-set-expression.v1.json +41 -0
  192. package/fixtures/obsidian/contracts/service-state/invalid/hostname-instead-of-literal-loopback.v1.json +23 -0
  193. package/fixtures/obsidian/contracts/service-state/invalid/missing-consent.v1.json +18 -0
  194. package/fixtures/obsidian/contracts/service-state/invalid/privileged-port.v1.json +23 -0
  195. package/fixtures/obsidian/contracts/service-state/invalid/unknown-top-level-field.v1.json +24 -0
  196. package/fixtures/obsidian/contracts/service-state/invalid/wildcard-bind.v1.json +23 -0
  197. package/fixtures/obsidian/contracts/service-state/valid/healthy.v1.json +23 -0
  198. package/fixtures/obsidian/contracts/source-snapshot/invalid/absolute-file-path.v1.json +48 -0
  199. package/fixtures/obsidian/contracts/source-snapshot/invalid/duplicate-repository-id.v1.json +48 -0
  200. package/fixtures/obsidian/contracts/source-snapshot/invalid/mixed-read.v1.json +48 -0
  201. package/fixtures/obsidian/contracts/source-snapshot/invalid/parent-traversal-path.v1.json +48 -0
  202. package/fixtures/obsidian/contracts/source-snapshot/invalid/unknown-file-field.v1.json +49 -0
  203. package/fixtures/obsidian/contracts/source-snapshot/valid/two-repositories.v1.json +48 -0
  204. package/fixtures/obsidian/edits/workspace.json +65 -0
  205. package/fixtures/obsidian/materialization/expected-full.json +99 -0
  206. package/fixtures/obsidian/materialization/expected-scoped.json +39 -0
  207. package/fixtures/obsidian/materialization/workspace.json +54 -0
  208. package/fixtures/obsidian/proposals/expected-shapes.json +11 -0
  209. package/fixtures/projects/sample-workspace/content/source.html.kg.json +4 -1
  210. package/fixtures/projects/source-formats-workspace/content/data.json.kg.json +4 -1
  211. package/fixtures/projects/source-formats-workspace/content/logo.png.kg.json +4 -1
  212. package/fixtures/projects/source-formats-workspace/content/metrics.csv.kg.json +4 -1
  213. package/fixtures/projects/source-formats-workspace/content/pipeline.yaml.kg.json +4 -1
  214. package/fixtures/ui/presentation/pane.v1.json +7 -0
  215. package/fixtures/ui/presentation/reference.v1.json +293 -0
  216. package/package.json +86 -10
  217. package/skills/claude/atelier-guided-coauthor/SKILL.md +49 -0
  218. package/skills/claude/atelier-guided-upgrade/SKILL.md +97 -0
  219. package/skills/claude/atelier-local-service/SKILL.md +47 -0
  220. package/skills/claude/atelier-open/SKILL.md +11 -5
  221. package/skills/claude/atelier-public-boundary/SKILL.md +31 -0
  222. package/skills/claude/atelier-skill-steward/SKILL.md +62 -0
  223. package/skills/claude/mnstry-readiness/SKILL.md +5 -0
  224. package/skills/codex/atelier-guided-coauthor/SKILL.md +49 -0
  225. package/skills/codex/atelier-guided-upgrade/SKILL.md +97 -0
  226. package/skills/codex/atelier-local-service/SKILL.md +47 -0
  227. package/skills/codex/atelier-open/SKILL.md +11 -5
  228. package/skills/codex/atelier-public-boundary/SKILL.md +31 -0
  229. package/skills/codex/atelier-skill-steward/SKILL.md +62 -0
  230. package/skills/codex/mnstry-readiness/SKILL.md +5 -0
  231. package/src/access/preview-enforcer.mjs +84 -0
  232. package/src/boundary/content-rules.mjs +283 -20
  233. package/src/boundary/policy.mjs +213 -91
  234. package/src/cli/execute-command.mjs +36 -0
  235. package/src/cli/project-options.mjs +58 -0
  236. package/src/cli/run.mjs +78 -10
  237. package/src/coauthor/session.mjs +161 -0
  238. package/src/coauthor/store.mjs +173 -0
  239. package/src/collaboration/event-ledger.mjs +348 -0
  240. package/src/collaboration/index.mjs +17 -0
  241. package/src/collaboration/inspection-bundle.mjs +223 -0
  242. package/src/collaboration/proposals.mjs +265 -65
  243. package/src/collaboration/review-contracts.mjs +25 -0
  244. package/src/collaboration/review-store.mjs +305 -0
  245. package/src/commands/attestation.mjs +20 -6
  246. package/src/commands/coauthor.mjs +26 -0
  247. package/src/commands/config.mjs +6 -1
  248. package/src/commands/disclosure.mjs +133 -0
  249. package/src/commands/distribution.mjs +2 -1
  250. package/src/commands/extension-pack.mjs +17 -9
  251. package/src/commands/init.mjs +46 -6
  252. package/src/commands/obsidian.mjs +371 -0
  253. package/src/commands/review.mjs +147 -0
  254. package/src/commands/server.mjs +1 -4
  255. package/src/commands/setup.mjs +51 -14
  256. package/src/commands/skills.mjs +199 -0
  257. package/src/commands/sync.mjs +107 -0
  258. package/src/composition/registry.mjs +27 -0
  259. package/src/composition/wire.mjs +122 -0
  260. package/src/contracts/corpus.mjs +35 -0
  261. package/src/decisions/contracts.d.mts +96 -0
  262. package/src/decisions/contracts.mjs +321 -0
  263. package/src/disclosure/content-scan.mjs +206 -0
  264. package/src/egress/check.mjs +7 -38
  265. package/src/egress/forbidden-egress.mjs +32 -18
  266. package/src/export/atelier-export-contract.mjs +16 -1
  267. package/src/extension-packs/lifecycle.mjs +154 -0
  268. package/src/graph/graph.mjs +176 -316
  269. package/src/graph/knowledge-graph.mjs +643 -43
  270. package/src/guides/contracts.mjs +54 -0
  271. package/src/harness/context-client.mjs +9 -1
  272. package/src/index.mjs +41 -0
  273. package/src/intake/store.mjs +119 -0
  274. package/src/preview/channel.mjs +89 -0
  275. package/src/project/config.mjs +112 -52
  276. package/src/project/durable-state.mjs +161 -0
  277. package/src/project/file-class.mjs +180 -0
  278. package/src/project/package-root.mjs +10 -0
  279. package/src/project/path-match.mjs +38 -15
  280. package/src/project/private-state.mjs +120 -0
  281. package/src/projection/obsidian/contracts.mjs +571 -0
  282. package/src/projection/obsidian/edits/align.mjs +169 -0
  283. package/src/projection/obsidian/edits/apply.mjs +889 -0
  284. package/src/projection/obsidian/edits/arbitrate.mjs +414 -0
  285. package/src/projection/obsidian/edits/contribution.mjs +108 -0
  286. package/src/projection/obsidian/edits/index.mjs +19 -0
  287. package/src/projection/obsidian/edits/object-identity.mjs +61 -0
  288. package/src/projection/obsidian/edits/object-store.mjs +517 -0
  289. package/src/projection/obsidian/edits/observe.mjs +123 -0
  290. package/src/projection/obsidian/edits/policy.mjs +123 -0
  291. package/src/projection/obsidian/edits/regions.mjs +648 -0
  292. package/src/projection/obsidian/materialize/byte-lens.mjs +98 -0
  293. package/src/projection/obsidian/materialize/index.mjs +7 -0
  294. package/src/projection/obsidian/materialize/path-registry.mjs +105 -0
  295. package/src/projection/obsidian/materialize/prepare-view.mjs +746 -0
  296. package/src/projection/obsidian/materialize/settings.mjs +57 -0
  297. package/src/projection/obsidian/materialize/stage.mjs +27 -0
  298. package/src/projection/obsidian/proposals/adapter.mjs +572 -0
  299. package/src/projection/obsidian/proposals/backpressure.mjs +113 -0
  300. package/src/projection/obsidian/proposals/contribution.mjs +55 -0
  301. package/src/projection/obsidian/proposals/index.mjs +23 -0
  302. package/src/projection/obsidian/proposals/observation.mjs +216 -0
  303. package/src/projection/obsidian/proposals/queue.mjs +277 -0
  304. package/src/projection/obsidian/proposals/router.mjs +168 -0
  305. package/src/projection/obsidian/publication/bridge-script.mjs +266 -0
  306. package/src/projection/obsidian/publication/exchange.mjs +117 -0
  307. package/src/projection/obsidian/publication/index.mjs +5 -0
  308. package/src/projection/obsidian/publication/publisher.mjs +588 -0
  309. package/src/projection/obsidian/publication/test-seam.mjs +16 -0
  310. package/src/projection/obsidian/publication/transport.mjs +212 -0
  311. package/src/projection/obsidian/recovery/index.mjs +6 -0
  312. package/src/projection/obsidian/recovery/journal.mjs +134 -0
  313. package/src/projection/obsidian/recovery/late-writer.mjs +46 -0
  314. package/src/projection/obsidian/recovery/restart.mjs +316 -0
  315. package/src/projection/obsidian/recovery/store.mjs +234 -0
  316. package/src/projection/obsidian/selection-ui/conflict-view.mjs +91 -0
  317. package/src/projection/obsidian/selection-ui/contribution.mjs +141 -0
  318. package/src/projection/obsidian/selection-ui/focus.mjs +82 -0
  319. package/src/projection/obsidian/selection-ui/index.mjs +12 -0
  320. package/src/projection/obsidian/selection-ui/policy-setup.mjs +107 -0
  321. package/src/projection/obsidian/selection-ui/receipt.mjs +173 -0
  322. package/src/projection/obsidian/selection-ui/selection-state.mjs +122 -0
  323. package/src/projection/obsidian/selection-ui/selection.mjs +109 -0
  324. package/src/projection/project.mjs +28 -19
  325. package/src/readiness-protocols/evidence.mjs +282 -0
  326. package/src/readiness-protocols/runtime.mjs +4 -3
  327. package/src/readiness-protocols/source-read.mjs +39 -0
  328. package/src/runtime/git-adapter.mjs +189 -0
  329. package/src/runtime/local-state.mjs +439 -0
  330. package/src/runtime/obsidian/app-capability.mjs +138 -0
  331. package/src/runtime/obsidian/app-production-seams.mjs +107 -0
  332. package/src/runtime/obsidian/contributions/proposal-adapter.mjs +4 -0
  333. package/src/runtime/obsidian/contributions/selection-ui.mjs +4 -0
  334. package/src/runtime/obsidian/contributions/source-apply.mjs +4 -0
  335. package/src/runtime/obsidian/contributions.mjs +27 -0
  336. package/src/runtime/obsidian/documents.mjs +37 -0
  337. package/src/runtime/obsidian/enablement.mjs +53 -0
  338. package/src/runtime/obsidian/engine.mjs +532 -0
  339. package/src/runtime/obsidian/errors.mjs +14 -0
  340. package/src/runtime/obsidian/extension-points.mjs +103 -0
  341. package/src/runtime/obsidian/index.mjs +34 -0
  342. package/src/runtime/obsidian/lifecycle.mjs +258 -0
  343. package/src/runtime/obsidian/machine-settings.mjs +260 -0
  344. package/src/runtime/obsidian/observation.mjs +158 -0
  345. package/src/runtime/obsidian/opening.mjs +280 -0
  346. package/src/runtime/obsidian/pending-edits.mjs +141 -0
  347. package/src/runtime/obsidian/pipeline.mjs +122 -0
  348. package/src/runtime/obsidian/private-lock.mjs +156 -0
  349. package/src/runtime/obsidian/process-identity.mjs +58 -0
  350. package/src/runtime/obsidian/service-client.mjs +66 -0
  351. package/src/runtime/obsidian/service-entry-path.mjs +9 -0
  352. package/src/runtime/obsidian/service-main.mjs +93 -0
  353. package/src/runtime/obsidian/service-record.mjs +169 -0
  354. package/src/runtime/obsidian/service-server.mjs +121 -0
  355. package/src/runtime/obsidian/service.mjs +216 -0
  356. package/src/runtime/obsidian/startup-adapters.mjs +87 -0
  357. package/src/runtime/obsidian/state-store.mjs +180 -0
  358. package/src/runtime/obsidian/sync-notice.mjs +33 -0
  359. package/src/runtime/obsidian/tick-loop.mjs +84 -0
  360. package/src/runtime/obsidian/watchers.mjs +36 -0
  361. package/src/runtime/repository-observation.mjs +491 -0
  362. package/src/runtime/supervisor.mjs +803 -0
  363. package/src/server/local-sidecar.mjs +125 -59
  364. package/src/server/security.mjs +89 -4
  365. package/src/server/server.mjs +4 -2
  366. package/src/skills/steward.mjs +780 -0
  367. package/src/support/feedback-report.mjs +4 -3
  368. package/src/ui/presentation/browser.mjs +278 -0
  369. package/src/ui/presentation/contract.mjs +116 -0
  370. package/src/ui/presentation/index.mjs +7 -0
  371. package/src/ui/presentation/native.mjs +153 -0
  372. package/src/ui/presentation/proof.mjs +57 -0
  373. package/src/ui/presentation/schema-check.mjs +30 -0
  374. package/src/ui/presentation/schema.generated.mjs +1371 -0
  375. package/src/ui/presentation/state.mjs +43 -0
  376. package/src/ui/presentation/styles.mjs +67 -0
  377. package/src/ui/presentation/tokens.mjs +74 -0
  378. package/src/ui/presentation/web.mjs +85 -0
  379. package/src/ui/review-page.mjs +32 -0
  380. package/src/upgrade/explanation.mjs +33 -0
  381. package/src/upgrade/provenance.mjs +338 -0
  382. package/src/upgrade/transaction-files.mjs +103 -0
  383. package/src/upgrade/transaction.mjs +475 -0
  384. package/src/upgrade/upgrade.mjs +65 -29
  385. package/src/vault/hosts.mjs +21 -0
  386. package/src/vault/identity.mjs +19 -0
  387. package/src/vault/index.mjs +7 -0
  388. package/src/vault/interface.mjs +11 -0
  389. package/src/vault/metadata.mjs +31 -0
  390. package/src/vault/privacy.mjs +148 -0
  391. package/src/vault/probe.mjs +89 -0
  392. package/src/vault/service.mjs +131 -0
  393. package/src/vault/source.mjs +45 -0
  394. package/src/vault/storage.mjs +23 -0
  395. package/templates/distribution-workspace/.claude/launch.json +6 -2
  396. package/templates/distribution-workspace/gitignore +1 -0
  397. package/templates/distribution-workspace/project/README.md +2 -2
  398. package/templates/external-project-workspace/README.md +45 -0
  399. package/templates/external-project-workspace/answers.example.json +4 -0
  400. package/templates/external-project-workspace/atelier.project.json +40 -0
  401. package/templates/external-project-workspace/boundary-policy.v1.json +60 -0
  402. package/templates/external-project-workspace/gitignore +8 -0
  403. package/templates/external-project-workspace/pack-lifecycle.json +14 -0
  404. package/templates/external-project-workspace/packs/protocols/contract-gate.v1.json +109 -0
  405. package/templates/external-project-workspace/packs/sample-pack.v1.json +48 -0
  406. package/templates/external-project-workspace/repo-access.v1.json +9 -0
  407. package/templates/external-project-workspace/source/README.md +11 -0
  408. package/templates/launch.json +6 -2
  409. package/templates/private-domain-workspace/.claude/launch.json +6 -2
  410. package/templates/private-domain-workspace/domain/README.md +2 -2
  411. package/templates/private-domain-workspace/gitignore +1 -0
  412. package/templates/shared-project-workspace/.claude/launch.json +6 -2
  413. package/templates/shared-project-workspace/gitignore +1 -0
  414. package/templates/shared-project-workspace/project/README.md +2 -2
@@ -1,3 +1,5 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
1
3
  import { basenameOf, matchesPathPattern, normalizeRelPath } from './path-match.mjs'
2
4
 
3
5
  export const FILE_CLASS_SCHEMA = 'mnstry.atelier-file-classes@v1'
@@ -6,8 +8,13 @@ export const SOURCE = 'source'
6
8
  export const GENERATED_PROJECTION = 'generated-projection'
7
9
  export const DISTRIBUTED_RUNTIME_COPY = 'distributed-runtime-copy'
8
10
 
11
+ // Declarable classes for tracked files. The kit manifest schema closes this set.
9
12
  export const FILE_CLASSES = Object.freeze([SOURCE, GENERATED_PROJECTION, DISTRIBUTED_RUNTIME_COPY])
10
13
 
14
+ // A machine-local file that git never tracks. It is not declarable: nothing
15
+ // tracked can be ignored-local, so it stays out of FILE_CLASSES.
16
+ export const IGNORED_LOCAL = 'ignored-local'
17
+
11
18
  // What each class means for automated handling. Sync loops, merge policies, upgrade
12
19
  // tooling, and CI guards all read this instead of each keeping a list of filenames
13
20
  // that drifts out of step with the others.
@@ -15,8 +22,17 @@ export const FILE_CLASS_HANDLING = Object.freeze({
15
22
  [SOURCE]: Object.freeze({ rederivable: false, discardable: false, conflictsNeedHuman: true }),
16
23
  [GENERATED_PROJECTION]: Object.freeze({ rederivable: true, discardable: true, conflictsNeedHuman: false }),
17
24
  [DISTRIBUTED_RUNTIME_COPY]: Object.freeze({ rederivable: true, discardable: true, conflictsNeedHuman: false }),
25
+ // Holds this machine's only reference to external editable state. Never
26
+ // swept, never synced, never a merge conflict.
27
+ [IGNORED_LOCAL]: Object.freeze({ rederivable: false, discardable: false, conflictsNeedHuman: false }),
18
28
  })
19
29
 
30
+ // The one repo-local file the Obsidian view leaves in a repository or
31
+ // workspace: a pointer to the external data directory and its local settings.
32
+ // It lives under the existing ignored local-state directory.
33
+ export const OBSIDIAN_LOCAL_POINTER = '.atelier-local/obsidian.json'
34
+ export const IGNORED_LOCAL_PATTERNS = Object.freeze([`**/${OBSIDIAN_LOCAL_POINTER}`])
35
+
20
36
  // The kit's own declaration — the single place these paths are classified. A
21
37
  // distributed-runtime-copy is canonical in exactly one repo role and rederivable
22
38
  // everywhere else; that is why the class carries `canonicalRepoRole` rather than
@@ -75,6 +91,12 @@ export function validateFileClasses(entries, { label = 'fileClasses' } = {}) {
75
91
  */
76
92
  export function classifyPath(filePath, { repoRole = null, fileClasses = KIT_FILE_CLASSES } = {}) {
77
93
  const rel = normalizeRelPath(filePath)
94
+ // Not overridable: a declaration cannot turn a local pointer into something
95
+ // a sync loop may discard or commit.
96
+ const localPattern = IGNORED_LOCAL_PATTERNS.find((pattern) => matchesPathPattern(pattern, rel))
97
+ if (localPattern) {
98
+ return { path: rel, class: IGNORED_LOCAL, pattern: localPattern, canonicalRepoRole: null, canonicalHere: true, declared: false, handling: FILE_CLASS_HANDLING[IGNORED_LOCAL] }
99
+ }
78
100
  let matched = null
79
101
  for (const entry of fileClasses) {
80
102
  if (matchesPathPattern(entry.pattern, rel)) matched = entry
@@ -111,3 +133,161 @@ export function generatedProjectionBasenames(fileClasses = KIT_FILE_CLASSES) {
111
133
  }
112
134
  return names
113
135
  }
136
+
137
+ // Directory basenames whose complete subtree is generated. Graph walkers can
138
+ // prune these without restating output-root names beside the declaration.
139
+ export function generatedProjectionDirectoryBasenames(fileClasses = KIT_FILE_CLASSES) {
140
+ const names = new Set()
141
+ for (const entry of fileClasses) {
142
+ if (entry.class !== GENERATED_PROJECTION) continue
143
+ const normalized = normalizeRelPath(entry.pattern)
144
+ if (!normalized.endsWith('/**')) continue
145
+ const parent = normalized.slice(0, -3).split('/').at(-1)
146
+ if (parent && !parent.includes('*')) names.add(parent)
147
+ }
148
+ return names
149
+ }
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // External managed data root: <data>/obsidian/<workspace-id>/
153
+ // ---------------------------------------------------------------------------
154
+
155
+ export const EDITABLE_VAULT = 'editable-vault'
156
+ export const TRUSTED_STATE = 'trusted-state'
157
+ export const RECOVERY_RECORD = 'recovery-record'
158
+ export const DISPOSABLE_STAGING = 'disposable-staging'
159
+ export const UNKNOWN_MANAGED = 'unknown-managed'
160
+
161
+ // Only staging is discardable. A vault holds a person's edits and recovery
162
+ // holds the only other copy of them, so neither is ever generated output, and
163
+ // anything unrecognized is kept.
164
+ export const MANAGED_AREA_HANDLING = Object.freeze({
165
+ [EDITABLE_VAULT]: Object.freeze({ rederivable: false, discardable: false, conflictsNeedHuman: true }),
166
+ [TRUSTED_STATE]: Object.freeze({ rederivable: false, discardable: false, conflictsNeedHuman: true }),
167
+ [RECOVERY_RECORD]: Object.freeze({ rederivable: false, discardable: false, conflictsNeedHuman: true }),
168
+ [DISPOSABLE_STAGING]: Object.freeze({ rederivable: true, discardable: true, conflictsNeedHuman: false }),
169
+ [UNKNOWN_MANAGED]: Object.freeze({ rederivable: false, discardable: false, conflictsNeedHuman: true }),
170
+ })
171
+
172
+ const MANAGED_AREAS = Object.freeze({ vaults: EDITABLE_VAULT, state: TRUSTED_STATE, recovery: RECOVERY_RECORD, staging: DISPOSABLE_STAGING })
173
+
174
+ // Classify a path relative to one workspace's managed data root. Matching is
175
+ // exact and case-sensitive, and a path that climbs, is absolute or names only
176
+ // the area directory itself is never discardable.
177
+ export function classifyManagedPath(relativePath) {
178
+ const rel = normalizeRelPath(relativePath).replace(/\/+$/, '')
179
+ const segments = rel.split('/')
180
+ const contained = rel !== '' && !rel.startsWith('/') && !/^[A-Za-z]:/.test(rel) && !segments.some((part) => part === '..' || part === '.' || part === '')
181
+ const area = contained && segments.length > 1 && Object.hasOwn(MANAGED_AREAS, segments[0]) ? segments[0] : null
182
+ const resolved = area ? MANAGED_AREAS[area] : UNKNOWN_MANAGED
183
+ return { path: rel, area, class: resolved, handling: MANAGED_AREA_HANDLING[resolved] }
184
+ }
185
+
186
+ // The real path of a target that may not exist yet: the deepest existing
187
+ // ancestor is resolved and the missing tail is appended. Only "does not exist"
188
+ // moves up a level. Any other failure (a directory that cannot be searched,
189
+ // a link loop) is thrown: a lexical path standing in for a real one would
190
+ // weaken the alias check exactly where it matters.
191
+ function realLocation(target, realpath) {
192
+ const missing = []
193
+ let current = path.resolve(target)
194
+ for (;;) {
195
+ try {
196
+ return path.join(realpath(current), ...missing.reverse())
197
+ } catch (error) {
198
+ if (error?.code !== 'ENOENT' && error?.code !== 'ENOTDIR') throw error
199
+ const parent = path.dirname(current)
200
+ if (parent === current) return path.resolve(target)
201
+ missing.push(path.basename(current))
202
+ current = parent
203
+ }
204
+ }
205
+ }
206
+
207
+ const containsPath = (outer, inner) => inner === outer || inner.startsWith(outer.endsWith(path.sep) ? outer : `${outer}${path.sep}`)
208
+
209
+ function isSymbolicLink(target, lstat) {
210
+ try {
211
+ return lstat(target).isSymbolicLink()
212
+ } catch {
213
+ return false
214
+ }
215
+ }
216
+
217
+ // True when the path or any of its ancestors is a symbolic link.
218
+ function crossesSymbolicLink(target, lstat) {
219
+ for (let current = path.resolve(target); ; current = path.dirname(current)) {
220
+ if (isSymbolicLink(current, lstat)) return true
221
+ if (path.dirname(current) === current) return false
222
+ }
223
+ }
224
+
225
+ // Refuse a managed root that overlaps an enrolled repository in either
226
+ // direction, under lexical or real paths. `realpath` and `lstat` are the only
227
+ // filesystem reads, and both can be supplied; nothing is created or changed.
228
+ // A real path that cannot be established refuses; it is never replaced by the
229
+ // lexical path.
230
+ export function checkManagedRoots({ managedRoots = [], repositoryRoots = [], realpath = fs.realpathSync.native, lstat = fs.lstatSync } = {}) {
231
+ const refusals = []
232
+ const refuse = (code, managedRoot, repositoryRoot, message) => refusals.push({ code, managedRoot, repositoryRoot, message })
233
+ const locate = (root) => {
234
+ try {
235
+ return { given: root, lexical: path.resolve(root), real: realLocation(root, realpath) }
236
+ } catch (error) {
237
+ return { given: root, lexical: path.resolve(root), real: null, errorCode: error?.code ?? 'unknown' }
238
+ }
239
+ }
240
+ const repositories = repositoryRoots.map(locate)
241
+
242
+ for (const managedRoot of managedRoots) {
243
+ if (typeof managedRoot !== 'string' || !path.isAbsolute(managedRoot)) {
244
+ refuse('managed-root-not-absolute', managedRoot, null, 'a managed root must be an absolute path')
245
+ continue
246
+ }
247
+ const managed = locate(managedRoot)
248
+ if (managed.real === null) {
249
+ refuse('managed-root-realpath-failed', managedRoot, null, `the real path of a managed root cannot be established (${managed.errorCode})`)
250
+ continue
251
+ }
252
+ if (isSymbolicLink(managed.lexical, lstat)) {
253
+ refuse('managed-root-symlink-alias', managedRoot, null, 'a managed root must not be a symbolic link')
254
+ }
255
+ for (const repository of repositories) {
256
+ const inside = (kind) => containsPath(repository[kind], managed[kind])
257
+ const around = (kind) => containsPath(managed[kind], repository[kind])
258
+ if (inside('lexical')) refuse('managed-root-inside-repository', managedRoot, repository.given, 'a managed root must sit outside every enrolled repository')
259
+ else if (around('lexical')) refuse('repository-inside-managed-root', managedRoot, repository.given, 'an enrolled repository must not sit inside a managed root')
260
+ else if (repository.real === null) {
261
+ refuse('managed-root-realpath-failed', managedRoot, repository.given, `the real path of an enrolled repository cannot be established (${repository.errorCode})`)
262
+ } else if (inside('real') || around('real')) {
263
+ // Name the cause that is actually there. Folding letter case and
264
+ // Unicode normalization alone may explain the overlap; otherwise a
265
+ // symbolic link on either path does; otherwise something else presents
266
+ // one location under two names (a mount, for example).
267
+ const folded = (value) => value.normalize('NFC').toLowerCase()
268
+ const byFolding = containsPath(folded(repository.lexical), folded(managed.lexical)) || containsPath(folded(managed.lexical), folded(repository.lexical))
269
+ if (!byFolding && (crossesSymbolicLink(managed.lexical, lstat) || crossesSymbolicLink(repository.lexical, lstat))) {
270
+ refuse('managed-root-symlink-alias', managedRoot, repository.given, 'a symbolic link makes the managed root and an enrolled repository overlap')
271
+ } else {
272
+ refuse('managed-root-realpath-overlap', managedRoot, repository.given,
273
+ 'the managed root and an enrolled repository are written differently but name overlapping locations, and no symbolic link explains it: look for a difference in letter case or Unicode normalization on a filesystem that folds them, or a mount that presents one location under two names')
274
+ }
275
+ }
276
+ }
277
+ }
278
+ return { ok: refusals.length === 0, refusals }
279
+ }
280
+
281
+ // The same guard at enrolment time: a repository that would contain, sit
282
+ // inside or alias an existing managed root cannot be enrolled.
283
+ export function checkRepositoryEnrollment({ repositoryRoot, managedRoots = [], realpath, lstat } = {}) {
284
+ const { refusals } = checkManagedRoots({ managedRoots, repositoryRoots: [repositoryRoot], ...(realpath ? { realpath } : {}), ...(lstat ? { lstat } : {}) })
285
+ const codes = {
286
+ 'managed-root-inside-repository': 'enrollment-contains-managed-root',
287
+ 'repository-inside-managed-root': 'enrollment-inside-managed-root',
288
+ 'managed-root-symlink-alias': 'enrollment-aliases-managed-root',
289
+ 'managed-root-realpath-overlap': 'enrollment-realpath-overlaps-managed-root',
290
+ }
291
+ const blocking = refusals.filter((item) => item.repositoryRoot !== null).map((item) => ({ ...item, code: codes[item.code] ?? item.code }))
292
+ return { ok: blocking.length === 0, refusals: blocking }
293
+ }
@@ -0,0 +1,10 @@
1
+ import path from 'node:path'
2
+ import { fileURLToPath } from 'node:url'
3
+
4
+ export function nativePathFromFileUrl(value) {
5
+ return fileURLToPath(value instanceof URL ? value : new URL(value))
6
+ }
7
+
8
+ export function packageRootFrom(moduleUrl) {
9
+ return path.resolve(nativePathFromFileUrl(new URL('../..', moduleUrl)))
10
+ }
@@ -4,27 +4,50 @@ export const normalizeRelPath = (value) =>
4
4
  String(value ?? '')
5
5
  .replaceAll('\\', '/')
6
6
  .replace(/^\.\/+/, '')
7
+ .replace(/\/{2,}/g, '/')
7
8
 
8
9
  const escapeRe = (value) => String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
9
10
 
10
- // Shared by the boundary policy and the file-class resolver. Both used to carry
11
- // their own copy; two glob dialects in one kit is exactly the drift this file exists
12
- // to prevent.
11
+ function globSource(pattern) {
12
+ let source = ''
13
+ for (let index = 0; index < pattern.length; index += 1) {
14
+ const char = pattern[index]
15
+ if (char !== '*') {
16
+ source += escapeRe(char)
17
+ continue
18
+ }
19
+
20
+ const globstar = pattern[index + 1] === '*'
21
+ if (!globstar) {
22
+ source += '[^/]*'
23
+ continue
24
+ }
25
+
26
+ index += 1
27
+ if (pattern[index + 1] === '/') {
28
+ index += 1
29
+ source += '(?:.*/)?'
30
+ } else if (source.endsWith('/')) {
31
+ source = source.slice(0, -1)
32
+ source += '(?:/.*)?'
33
+ } else {
34
+ source += '.*'
35
+ }
36
+ }
37
+ return source
38
+ }
39
+
40
+ // Shared by the boundary policy and the file-class resolver. The dialect is
41
+ // intentionally portable: case does not depend on the host filesystem, `*`
42
+ // stays inside one segment, and `**` crosses segments. Slashless patterns keep
43
+ // their historical match-base behavior, so `*.md` applies at any depth without
44
+ // making `private/*.md` consume nested directories.
13
45
  export function matchesPathPattern(pattern, rel) {
14
46
  const normalizedPattern = normalizeRelPath(pattern)
15
47
  const normalizedRel = normalizeRelPath(rel)
16
- if (normalizedPattern === normalizedRel) return true
17
- if (normalizedPattern.endsWith('/**')) {
18
- return normalizedRel === normalizedPattern.slice(0, -3) || normalizedRel.startsWith(normalizedPattern.slice(0, -2))
19
- }
20
- if (normalizedPattern.startsWith('**/')) {
21
- const tail = normalizedPattern.slice(3)
22
- return normalizedRel === tail || normalizedRel.endsWith(`/${tail}`) || matchesPathPattern(tail, normalizedRel)
23
- }
24
- if (normalizedPattern.includes('*')) {
25
- return new RegExp(`^${normalizedPattern.split('*').map(escapeRe).join('.*')}$`).test(normalizedRel)
26
- }
27
- return false
48
+ if (!normalizedPattern || !normalizedRel) return false
49
+ const candidate = normalizedPattern.includes('/') ? normalizedRel : path.posix.basename(normalizedRel)
50
+ return new RegExp(`^${globSource(normalizedPattern)}$`, 'i').test(candidate)
28
51
  }
29
52
 
30
53
  export const basenameOf = (pattern) => path.posix.basename(normalizeRelPath(pattern))
@@ -0,0 +1,120 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { randomUUID } from 'node:crypto'
4
+
5
+ function escapes(root, candidate) {
6
+ const relative = path.relative(root, candidate)
7
+ return relative.startsWith('..') || path.isAbsolute(relative)
8
+ }
9
+
10
+ function lstatIfPresent(file) {
11
+ try {
12
+ return fs.lstatSync(file)
13
+ } catch (error) {
14
+ if (error?.code === 'ENOENT') return null
15
+ throw error
16
+ }
17
+ }
18
+
19
+ export function ensureContainedPrivateDirectory({ workspaceRoot, directory, label = 'private state directory' }) {
20
+ const lexicalRoot = path.resolve(workspaceRoot)
21
+ const realRoot = fs.realpathSync(lexicalRoot)
22
+ const requested = path.resolve(directory)
23
+ let relative = path.relative(lexicalRoot, requested)
24
+ if (escapes(lexicalRoot, requested)) relative = path.relative(realRoot, requested)
25
+ if (relative === '' || escapes(realRoot, path.join(realRoot, relative))) {
26
+ if (relative === '') return realRoot
27
+ throw new Error(`${label} escapes workspace`)
28
+ }
29
+
30
+ let current = realRoot
31
+ for (const segment of relative.split(path.sep)) {
32
+ current = path.join(current, segment)
33
+ let stat = lstatIfPresent(current)
34
+ if (!stat) {
35
+ fs.mkdirSync(current, { mode: 0o700 })
36
+ stat = fs.lstatSync(current)
37
+ }
38
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
39
+ throw new Error(`${label} contains a redirected or non-directory component`)
40
+ }
41
+ }
42
+ const resolved = fs.realpathSync(current)
43
+ if (escapes(realRoot, resolved)) throw new Error(`${label} escapes workspace`)
44
+ try {
45
+ fs.chmodSync(resolved, 0o700)
46
+ } catch {
47
+ // Best effort on filesystems that do not support chmod.
48
+ }
49
+ return resolved
50
+ }
51
+
52
+ export function openRegularFileNoFollow(file, flags = fs.constants.O_RDONLY, mode) {
53
+ const before = lstatIfPresent(file)
54
+ if (before && (before.isSymbolicLink() || !before.isFile())) {
55
+ throw new Error('state leaf is not a regular file')
56
+ }
57
+ const descriptor = fs.openSync(file, flags | (fs.constants.O_NOFOLLOW ?? 0), mode)
58
+ try {
59
+ const opened = fs.fstatSync(descriptor)
60
+ if (!opened.isFile()) throw new Error('state leaf is not a regular file')
61
+ // Windows does not expose O_NOFOLLOW. Refuse a pre-existing redirected
62
+ // leaf before open and bind the opened descriptor back to that same file
63
+ // identity where the filesystem supplies stable device/inode values.
64
+ if (before && before.ino !== 0 && (before.dev !== opened.dev || before.ino !== opened.ino)) {
65
+ throw new Error('state leaf changed while opening')
66
+ }
67
+ return descriptor
68
+ } catch (error) {
69
+ fs.closeSync(descriptor)
70
+ throw error
71
+ }
72
+ }
73
+
74
+ export function readRegularTextNoFollow(file) {
75
+ const descriptor = openRegularFileNoFollow(file)
76
+ try {
77
+ return fs.readFileSync(descriptor, 'utf8')
78
+ } finally {
79
+ fs.closeSync(descriptor)
80
+ }
81
+ }
82
+
83
+ export function syncPrivateDirectory(directory) {
84
+ let fd;
85
+ try { fd = fs.openSync(directory, fs.constants.O_RDONLY); fs.fsyncSync(fd); }
86
+ catch (error) {
87
+ if (process.platform !== 'win32' || !['EPERM', 'EACCES', 'EINVAL', 'EISDIR'].includes(error.code)) throw error;
88
+ } finally { if (fd !== undefined) fs.closeSync(fd); }
89
+ }
90
+
91
+ export function atomicReplacePrivateText(file, text, mode = 0o600) {
92
+ const existing = lstatIfPresent(file)
93
+ if (existing && (existing.isSymbolicLink() || !existing.isFile())) {
94
+ throw new Error('state leaf is not a regular file')
95
+ }
96
+ const tmp = `${file}.${randomUUID()}.tmp`
97
+ let descriptor
98
+ try {
99
+ descriptor = openRegularFileNoFollow(
100
+ tmp,
101
+ fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL,
102
+ mode,
103
+ )
104
+ fs.writeFileSync(descriptor, text)
105
+ fs.fsyncSync(descriptor)
106
+ fs.fchmodSync(descriptor, mode)
107
+ fs.closeSync(descriptor)
108
+ descriptor = null
109
+ fs.renameSync(tmp, file)
110
+ syncPrivateDirectory(path.dirname(file))
111
+ } catch (error) {
112
+ if (descriptor != null) fs.closeSync(descriptor)
113
+ try {
114
+ fs.unlinkSync(tmp)
115
+ } catch {
116
+ // The temporary file may not have been created.
117
+ }
118
+ throw error
119
+ }
120
+ }