@mnstry/atelier 0.2.0-alpha.5 → 0.2.0-alpha.8

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 (383) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/README.md +86 -25
  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/docs/assurance-controls.md +12 -2
  31. package/docs/atelier-runtime.md +13 -2
  32. package/docs/atelier-sync.md +172 -0
  33. package/docs/blocks/claims.md +28 -18
  34. package/docs/blocks/will-not-do.md +12 -3
  35. package/docs/coauthor-session.md +57 -0
  36. package/docs/connected-composition.md +75 -0
  37. package/docs/decisions.md +167 -0
  38. package/docs/exact-upgrades.md +159 -0
  39. package/docs/extension-pack-lifecycle.md +33 -0
  40. package/docs/guided-upgrades.md +99 -0
  41. package/docs/install.md +65 -9
  42. package/docs/intake-and-guides.md +65 -0
  43. package/docs/integration-and-review-plan.md +409 -0
  44. package/docs/integration-contract-decisions.md +22 -0
  45. package/docs/local-review.md +84 -0
  46. package/docs/local-services.md +227 -0
  47. package/docs/local-state.md +107 -0
  48. package/docs/obsidian-contract.md +687 -0
  49. package/docs/obsidian.md +387 -0
  50. package/docs/portable-presentation.md +328 -0
  51. package/docs/presentation-consumer-boundaries.md +75 -0
  52. package/docs/presentation-review-disposition.md +47 -0
  53. package/docs/project-command-inventory.md +38 -0
  54. package/docs/project-options.md +53 -0
  55. package/docs/release-engineering.md +99 -2
  56. package/docs/review-portability.md +55 -0
  57. package/docs/skill-steward.md +156 -0
  58. package/docs/upgrade.md +70 -2
  59. package/docs/vault-service.md +292 -0
  60. package/fixtures/atelier-coauthor/invalid/authority.v1.json +5 -0
  61. package/fixtures/atelier-coauthor/valid/config.v1.json +12 -0
  62. package/fixtures/atelier-guide/invalid/authority.v1.json +11 -0
  63. package/fixtures/atelier-guide/valid/capability.v1.json +10 -0
  64. package/fixtures/atelier-guide/valid/consent.v1.json +11 -0
  65. package/fixtures/atelier-guide/valid/engagement.v1.json +7 -0
  66. package/fixtures/atelier-guide/valid/offer.v1.json +10 -0
  67. package/fixtures/atelier-intake/invalid/authority.v1.json +9 -0
  68. package/fixtures/atelier-intake/valid/attempt.v1.json +8 -0
  69. package/fixtures/atelier-intake/valid/completion.v1.json +9 -0
  70. package/fixtures/atelier-intake/valid/source.v1.json +7 -0
  71. package/fixtures/atelier-repository-observation/invalid/complete-with-blocker.v1.json +18 -0
  72. package/fixtures/atelier-repository-observation/valid/complete-local.v1.json +48 -0
  73. package/fixtures/atelier-review/invalid/authenticated-identity.v1.json +22 -0
  74. package/fixtures/atelier-review/valid/bound.v1.json +229 -0
  75. package/fixtures/atelier-review/valid/bundle.v1.json +267 -0
  76. package/fixtures/atelier-review/valid/contribution.v1.json +22 -0
  77. package/fixtures/atelier-review/valid/lifecycle.v1.json +14 -0
  78. package/fixtures/atelier-skill-steward/audit/invalid/source-mutation.v1.json +26 -0
  79. package/fixtures/atelier-skill-steward/audit/valid/clean.v1.json +26 -0
  80. package/fixtures/atelier-skill-steward/candidates/invalid/workflow-key.v1.json +37 -0
  81. package/fixtures/atelier-skill-steward/candidates/valid/create.v1.json +37 -0
  82. package/fixtures/atelier-skill-steward/lock/invalid/bad-digest.v1.json +18 -0
  83. package/fixtures/atelier-skill-steward/lock/valid/installed.v1.json +23 -0
  84. package/fixtures/atelier-skill-steward/plan/invalid/delete-action.v1.json +33 -0
  85. package/fixtures/atelier-skill-steward/plan/valid/add.v1.json +33 -0
  86. package/fixtures/atelier-upgrade-transaction/adoption-policy/invalid/example.json +12 -0
  87. package/fixtures/atelier-upgrade-transaction/adoption-policy/valid/example.json +11 -0
  88. package/fixtures/atelier-upgrade-transaction/migration/invalid/example.json +20 -0
  89. package/fixtures/atelier-upgrade-transaction/migration/valid/example.json +19 -0
  90. package/fixtures/atelier-upgrade-transaction/upgrade-plan/invalid/example.json +58 -0
  91. package/fixtures/atelier-upgrade-transaction/upgrade-plan/valid/example.json +57 -0
  92. package/fixtures/atelier-upgrade-transaction/upgrade-receipt/invalid/example.json +15 -0
  93. package/fixtures/atelier-upgrade-transaction/upgrade-receipt/valid/example.json +14 -0
  94. package/fixtures/atelier-vault-publication/invalid/publication.json +5 -0
  95. package/fixtures/atelier-vault-publication/valid/publication.json +11 -0
  96. package/fixtures/decisions/request/invalid/unknown-authority.v1.json +61 -0
  97. package/fixtures/decisions/request/valid/triage.v1.json +60 -0
  98. package/fixtures/decisions/result/invalid/execution-authority.v1.json +50 -0
  99. package/fixtures/decisions/result/valid/abstained.v1.json +23 -0
  100. package/fixtures/decisions/result/valid/assessed.v1.json +50 -0
  101. package/fixtures/obsidian/acceptance/ap05-extra-notes.json +14 -0
  102. package/fixtures/obsidian/acceptance/receipts/G07.valid.v1.json +63 -0
  103. package/fixtures/obsidian/acceptance/receipts/G13.valid.v1.json +69 -0
  104. package/fixtures/obsidian/acceptance/receipts/G14.valid.v1.json +62 -0
  105. package/fixtures/obsidian/acceptance/receipts/G15.valid.v1.json +56 -0
  106. package/fixtures/obsidian/acceptance/receipts/G16.valid.v1.json +73 -0
  107. package/fixtures/obsidian/acceptance/receipts/G17.valid.v1.json +75 -0
  108. package/fixtures/obsidian/acceptance/receipts/G18.valid.v1.json +64 -0
  109. package/fixtures/obsidian/acceptance/service-sentinels.json +10 -0
  110. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/duplicate-evidence-name.v1.json +39 -0
  111. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/evidence-by-absolute-path.v1.json +34 -0
  112. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/missing-candidate-tree.v1.json +33 -0
  113. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/no-evidence.v1.json +28 -0
  114. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-outcome.v1.json +34 -0
  115. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-top-level-field.v1.json +35 -0
  116. package/fixtures/obsidian/contracts/acceptance-receipt/valid/node-gate.v1.json +34 -0
  117. package/fixtures/obsidian/contracts/apply-policy/invalid/automatic-without-edit-class.v1.json +27 -0
  118. package/fixtures/obsidian/contracts/apply-policy/invalid/conflict-overwrite.v1.json +29 -0
  119. package/fixtures/obsidian/contracts/apply-policy/invalid/missing-policy-digest.v1.json +28 -0
  120. package/fixtures/obsidian/contracts/apply-policy/invalid/unbounded-batch.v1.json +29 -0
  121. package/fixtures/obsidian/contracts/apply-policy/invalid/unimplemented-edit-class.v1.json +30 -0
  122. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-mode.v1.json +29 -0
  123. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-selector-operator.v1.json +22 -0
  124. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-top-level-field.v1.json +30 -0
  125. package/fixtures/obsidian/contracts/apply-policy/valid/automatic-scoped.v1.json +29 -0
  126. package/fixtures/obsidian/contracts/apply-policy/valid/manual-default.v1.json +20 -0
  127. package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-path-in-ext.v1.json +30 -0
  128. package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-repository-root.v1.json +27 -0
  129. package/fixtures/obsidian/contracts/corpus-profile/invalid/duplicate-repository-id.v1.json +27 -0
  130. package/fixtures/obsidian/contracts/corpus-profile/invalid/overlapping-managed-roots.v1.json +27 -0
  131. package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-enrollment.v1.json +27 -0
  132. package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-top-level-field.v1.json +28 -0
  133. package/fixtures/obsidian/contracts/corpus-profile/valid/three-repositories.v1.json +27 -0
  134. package/fixtures/obsidian/contracts/edit-operation/invalid/absolute-recovery-ref.v1.json +21 -0
  135. package/fixtures/obsidian/contracts/edit-operation/invalid/inline-observed-bytes.v1.json +22 -0
  136. package/fixtures/obsidian/contracts/edit-operation/invalid/missing-base-digest.v1.json +20 -0
  137. package/fixtures/obsidian/contracts/edit-operation/invalid/short-idempotency-key.v1.json +21 -0
  138. package/fixtures/obsidian/contracts/edit-operation/invalid/unknown-kind.v1.json +21 -0
  139. package/fixtures/obsidian/contracts/edit-operation/valid/pending-body-replacement.v1.json +21 -0
  140. package/fixtures/obsidian/contracts/ext-settings/invalid/absolute-path-prefix.v1.json +28 -0
  141. package/fixtures/obsidian/contracts/ext-settings/invalid/apply-policy-in-portable-settings.v1.json +30 -0
  142. package/fixtures/obsidian/contracts/ext-settings/invalid/duplicate-scope-id.v1.json +27 -0
  143. package/fixtures/obsidian/contracts/ext-settings/invalid/machine-local-vault-path.v1.json +28 -0
  144. package/fixtures/obsidian/contracts/ext-settings/invalid/service-port-in-portable-settings.v1.json +28 -0
  145. package/fixtures/obsidian/contracts/ext-settings/invalid/unknown-default-scope.v1.json +27 -0
  146. package/fixtures/obsidian/contracts/ext-settings/valid/disabled.v1.json +5 -0
  147. package/fixtures/obsidian/contracts/ext-settings/valid/enabled-with-scopes.v1.json +27 -0
  148. package/fixtures/obsidian/contracts/generation-manifest/invalid/absolute-note-path.v1.json +114 -0
  149. package/fixtures/obsidian/contracts/generation-manifest/invalid/complete-with-unwritten-notes.v1.json +114 -0
  150. package/fixtures/obsidian/contracts/generation-manifest/invalid/derived-link-claimed-declared.v1.json +114 -0
  151. package/fixtures/obsidian/contracts/generation-manifest/invalid/duplicate-note-identity.v1.json +114 -0
  152. package/fixtures/obsidian/contracts/generation-manifest/invalid/in-scope-endpoint-missing.v1.json +114 -0
  153. package/fixtures/obsidian/contracts/generation-manifest/invalid/title-only-note-path.v1.json +114 -0
  154. package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-note-field.v1.json +115 -0
  155. package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-relation-type.v1.json +114 -0
  156. package/fixtures/obsidian/contracts/generation-manifest/invalid/withheld-endpoint.v1.json +114 -0
  157. package/fixtures/obsidian/contracts/generation-manifest/valid/two-notes.v1.json +114 -0
  158. package/fixtures/obsidian/contracts/oracles/scope-cases.json +412 -0
  159. package/fixtures/obsidian/contracts/proposal-receipt/invalid/accepted-without-proposal-id.v1.json +12 -0
  160. package/fixtures/obsidian/contracts/proposal-receipt/invalid/missing-adapter-operation-id.v1.json +11 -0
  161. package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-backpressure-outcome.v1.json +12 -0
  162. package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-top-level-field.v1.json +13 -0
  163. package/fixtures/obsidian/contracts/proposal-receipt/valid/accepted-new.v1.json +12 -0
  164. package/fixtures/obsidian/contracts/proposal-receipt/valid/deferred.v1.json +12 -0
  165. package/fixtures/obsidian/contracts/publication-journal/invalid/absolute-recovery-ref.v1.json +47 -0
  166. package/fixtures/obsidian/contracts/publication-journal/invalid/duplicate-sequence.v1.json +47 -0
  167. package/fixtures/obsidian/contracts/publication-journal/invalid/empty-protocol-id.v1.json +47 -0
  168. package/fixtures/obsidian/contracts/publication-journal/invalid/missing-protocol-id.v1.json +46 -0
  169. package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-entry-field.v1.json +48 -0
  170. package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-state.v1.json +47 -0
  171. package/fixtures/obsidian/contracts/publication-journal/valid/committed.v1.json +47 -0
  172. package/fixtures/obsidian/contracts/publication-journal/valid/updating-after-restart.v1.json +22 -0
  173. package/fixtures/obsidian/contracts/scope/invalid/absolute-path-prefix.v1.json +15 -0
  174. package/fixtures/obsidian/contracts/scope/invalid/duplicate-ids.v1.json +18 -0
  175. package/fixtures/obsidian/contracts/scope/invalid/expansion-without-node-budget.v1.json +40 -0
  176. package/fixtures/obsidian/contracts/scope/invalid/full-mode-with-subset.v1.json +8 -0
  177. package/fixtures/obsidian/contracts/scope/invalid/unknown-mode.v1.json +41 -0
  178. package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-key.v1.json +15 -0
  179. package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-operator.v1.json +14 -0
  180. package/fixtures/obsidian/contracts/scope/invalid/unknown-top-level-field.v1.json +42 -0
  181. package/fixtures/obsidian/contracts/scope/valid/explicit-empty.v1.json +8 -0
  182. package/fixtures/obsidian/contracts/scope/valid/focus.v1.json +11 -0
  183. package/fixtures/obsidian/contracts/scope/valid/full.v1.json +8 -0
  184. package/fixtures/obsidian/contracts/scope/valid/scoped-set-expression.v1.json +41 -0
  185. package/fixtures/obsidian/contracts/service-state/invalid/hostname-instead-of-literal-loopback.v1.json +23 -0
  186. package/fixtures/obsidian/contracts/service-state/invalid/missing-consent.v1.json +18 -0
  187. package/fixtures/obsidian/contracts/service-state/invalid/privileged-port.v1.json +23 -0
  188. package/fixtures/obsidian/contracts/service-state/invalid/unknown-top-level-field.v1.json +24 -0
  189. package/fixtures/obsidian/contracts/service-state/invalid/wildcard-bind.v1.json +23 -0
  190. package/fixtures/obsidian/contracts/service-state/valid/healthy.v1.json +23 -0
  191. package/fixtures/obsidian/contracts/source-snapshot/invalid/absolute-file-path.v1.json +48 -0
  192. package/fixtures/obsidian/contracts/source-snapshot/invalid/duplicate-repository-id.v1.json +48 -0
  193. package/fixtures/obsidian/contracts/source-snapshot/invalid/mixed-read.v1.json +48 -0
  194. package/fixtures/obsidian/contracts/source-snapshot/invalid/parent-traversal-path.v1.json +48 -0
  195. package/fixtures/obsidian/contracts/source-snapshot/invalid/unknown-file-field.v1.json +49 -0
  196. package/fixtures/obsidian/contracts/source-snapshot/valid/two-repositories.v1.json +48 -0
  197. package/fixtures/obsidian/edits/workspace.json +65 -0
  198. package/fixtures/obsidian/materialization/expected-full.json +99 -0
  199. package/fixtures/obsidian/materialization/expected-scoped.json +39 -0
  200. package/fixtures/obsidian/materialization/workspace.json +54 -0
  201. package/fixtures/obsidian/proposals/expected-shapes.json +11 -0
  202. package/fixtures/ui/presentation/pane.v1.json +7 -0
  203. package/fixtures/ui/presentation/reference.v1.json +293 -0
  204. package/package.json +61 -6
  205. package/skills/claude/atelier-guided-coauthor/SKILL.md +49 -0
  206. package/skills/claude/atelier-guided-upgrade/SKILL.md +97 -0
  207. package/skills/claude/atelier-open/SKILL.md +5 -0
  208. package/skills/claude/atelier-skill-steward/SKILL.md +62 -0
  209. package/skills/claude/mnstry-readiness/SKILL.md +5 -0
  210. package/skills/codex/atelier-guided-coauthor/SKILL.md +49 -0
  211. package/skills/codex/atelier-guided-upgrade/SKILL.md +97 -0
  212. package/skills/codex/atelier-open/SKILL.md +5 -0
  213. package/skills/codex/atelier-skill-steward/SKILL.md +62 -0
  214. package/skills/codex/mnstry-readiness/SKILL.md +5 -0
  215. package/src/access/preview-enforcer.mjs +84 -0
  216. package/src/boundary/content-rules.mjs +9 -4
  217. package/src/boundary/policy.mjs +74 -42
  218. package/src/cli/project-options.mjs +58 -0
  219. package/src/cli/run.mjs +61 -3
  220. package/src/coauthor/session.mjs +161 -0
  221. package/src/coauthor/store.mjs +173 -0
  222. package/src/collaboration/event-ledger.mjs +16 -33
  223. package/src/collaboration/inspection-bundle.mjs +223 -0
  224. package/src/collaboration/review-contracts.mjs +25 -0
  225. package/src/collaboration/review-store.mjs +305 -0
  226. package/src/commands/coauthor.mjs +26 -0
  227. package/src/commands/config.mjs +6 -1
  228. package/src/commands/extension-pack.mjs +15 -8
  229. package/src/commands/init.mjs +44 -5
  230. package/src/commands/obsidian.mjs +368 -0
  231. package/src/commands/review.mjs +147 -0
  232. package/src/commands/setup.mjs +51 -14
  233. package/src/commands/skills.mjs +199 -0
  234. package/src/commands/sync.mjs +107 -0
  235. package/src/composition/registry.mjs +27 -0
  236. package/src/composition/wire.mjs +122 -0
  237. package/src/contracts/corpus.mjs +35 -0
  238. package/src/decisions/contracts.d.mts +96 -0
  239. package/src/decisions/contracts.mjs +321 -0
  240. package/src/disclosure/content-scan.mjs +13 -0
  241. package/src/export/atelier-export-contract.mjs +16 -1
  242. package/src/extension-packs/lifecycle.mjs +154 -0
  243. package/src/graph/graph.mjs +64 -2
  244. package/src/graph/knowledge-graph.mjs +548 -24
  245. package/src/guides/contracts.mjs +54 -0
  246. package/src/index.mjs +29 -0
  247. package/src/intake/store.mjs +119 -0
  248. package/src/preview/channel.mjs +89 -0
  249. package/src/project/config.mjs +50 -49
  250. package/src/project/durable-state.mjs +161 -0
  251. package/src/project/file-class.mjs +166 -0
  252. package/src/project/private-state.mjs +11 -1
  253. package/src/projection/obsidian/contracts.mjs +571 -0
  254. package/src/projection/obsidian/edits/align.mjs +169 -0
  255. package/src/projection/obsidian/edits/apply.mjs +889 -0
  256. package/src/projection/obsidian/edits/arbitrate.mjs +414 -0
  257. package/src/projection/obsidian/edits/contribution.mjs +108 -0
  258. package/src/projection/obsidian/edits/index.mjs +19 -0
  259. package/src/projection/obsidian/edits/object-identity.mjs +61 -0
  260. package/src/projection/obsidian/edits/object-store.mjs +517 -0
  261. package/src/projection/obsidian/edits/observe.mjs +123 -0
  262. package/src/projection/obsidian/edits/policy.mjs +123 -0
  263. package/src/projection/obsidian/edits/regions.mjs +648 -0
  264. package/src/projection/obsidian/materialize/byte-lens.mjs +98 -0
  265. package/src/projection/obsidian/materialize/index.mjs +7 -0
  266. package/src/projection/obsidian/materialize/path-registry.mjs +105 -0
  267. package/src/projection/obsidian/materialize/prepare-view.mjs +746 -0
  268. package/src/projection/obsidian/materialize/settings.mjs +57 -0
  269. package/src/projection/obsidian/materialize/stage.mjs +27 -0
  270. package/src/projection/obsidian/proposals/adapter.mjs +572 -0
  271. package/src/projection/obsidian/proposals/backpressure.mjs +113 -0
  272. package/src/projection/obsidian/proposals/contribution.mjs +55 -0
  273. package/src/projection/obsidian/proposals/index.mjs +23 -0
  274. package/src/projection/obsidian/proposals/observation.mjs +216 -0
  275. package/src/projection/obsidian/proposals/queue.mjs +277 -0
  276. package/src/projection/obsidian/proposals/router.mjs +168 -0
  277. package/src/projection/obsidian/publication/bridge-script.mjs +266 -0
  278. package/src/projection/obsidian/publication/exchange.mjs +117 -0
  279. package/src/projection/obsidian/publication/index.mjs +5 -0
  280. package/src/projection/obsidian/publication/publisher.mjs +588 -0
  281. package/src/projection/obsidian/publication/test-seam.mjs +16 -0
  282. package/src/projection/obsidian/publication/transport.mjs +139 -0
  283. package/src/projection/obsidian/recovery/index.mjs +6 -0
  284. package/src/projection/obsidian/recovery/journal.mjs +134 -0
  285. package/src/projection/obsidian/recovery/late-writer.mjs +46 -0
  286. package/src/projection/obsidian/recovery/restart.mjs +316 -0
  287. package/src/projection/obsidian/recovery/store.mjs +234 -0
  288. package/src/projection/obsidian/selection-ui/conflict-view.mjs +91 -0
  289. package/src/projection/obsidian/selection-ui/contribution.mjs +141 -0
  290. package/src/projection/obsidian/selection-ui/focus.mjs +82 -0
  291. package/src/projection/obsidian/selection-ui/index.mjs +12 -0
  292. package/src/projection/obsidian/selection-ui/policy-setup.mjs +107 -0
  293. package/src/projection/obsidian/selection-ui/receipt.mjs +173 -0
  294. package/src/projection/obsidian/selection-ui/selection-state.mjs +122 -0
  295. package/src/projection/obsidian/selection-ui/selection.mjs +109 -0
  296. package/src/projection/project.mjs +28 -19
  297. package/src/readiness-protocols/evidence.mjs +282 -0
  298. package/src/readiness-protocols/runtime.mjs +4 -3
  299. package/src/readiness-protocols/source-read.mjs +39 -0
  300. package/src/runtime/git-adapter.mjs +189 -0
  301. package/src/runtime/local-state.mjs +439 -0
  302. package/src/runtime/obsidian/app-capability.mjs +110 -0
  303. package/src/runtime/obsidian/app-production-seams.mjs +96 -0
  304. package/src/runtime/obsidian/contributions/proposal-adapter.mjs +4 -0
  305. package/src/runtime/obsidian/contributions/selection-ui.mjs +4 -0
  306. package/src/runtime/obsidian/contributions/source-apply.mjs +4 -0
  307. package/src/runtime/obsidian/contributions.mjs +27 -0
  308. package/src/runtime/obsidian/documents.mjs +37 -0
  309. package/src/runtime/obsidian/enablement.mjs +53 -0
  310. package/src/runtime/obsidian/engine.mjs +532 -0
  311. package/src/runtime/obsidian/errors.mjs +14 -0
  312. package/src/runtime/obsidian/extension-points.mjs +103 -0
  313. package/src/runtime/obsidian/index.mjs +34 -0
  314. package/src/runtime/obsidian/lifecycle.mjs +258 -0
  315. package/src/runtime/obsidian/machine-settings.mjs +260 -0
  316. package/src/runtime/obsidian/observation.mjs +158 -0
  317. package/src/runtime/obsidian/opening.mjs +243 -0
  318. package/src/runtime/obsidian/pending-edits.mjs +141 -0
  319. package/src/runtime/obsidian/pipeline.mjs +122 -0
  320. package/src/runtime/obsidian/private-lock.mjs +156 -0
  321. package/src/runtime/obsidian/process-identity.mjs +58 -0
  322. package/src/runtime/obsidian/service-client.mjs +66 -0
  323. package/src/runtime/obsidian/service-main.mjs +89 -0
  324. package/src/runtime/obsidian/service-record.mjs +169 -0
  325. package/src/runtime/obsidian/service-server.mjs +121 -0
  326. package/src/runtime/obsidian/service.mjs +216 -0
  327. package/src/runtime/obsidian/startup-adapters.mjs +87 -0
  328. package/src/runtime/obsidian/state-store.mjs +180 -0
  329. package/src/runtime/obsidian/sync-notice.mjs +33 -0
  330. package/src/runtime/obsidian/tick-loop.mjs +84 -0
  331. package/src/runtime/obsidian/watchers.mjs +36 -0
  332. package/src/runtime/repository-observation.mjs +491 -0
  333. package/src/runtime/supervisor.mjs +803 -0
  334. package/src/server/local-sidecar.mjs +44 -0
  335. package/src/server/server.mjs +1 -0
  336. package/src/skills/steward.mjs +780 -0
  337. package/src/ui/presentation/browser.mjs +278 -0
  338. package/src/ui/presentation/contract.mjs +116 -0
  339. package/src/ui/presentation/index.mjs +7 -0
  340. package/src/ui/presentation/native.mjs +153 -0
  341. package/src/ui/presentation/proof.mjs +57 -0
  342. package/src/ui/presentation/schema-check.mjs +30 -0
  343. package/src/ui/presentation/schema.generated.mjs +1371 -0
  344. package/src/ui/presentation/state.mjs +43 -0
  345. package/src/ui/presentation/styles.mjs +67 -0
  346. package/src/ui/presentation/tokens.mjs +74 -0
  347. package/src/ui/presentation/web.mjs +85 -0
  348. package/src/ui/review-page.mjs +32 -0
  349. package/src/upgrade/explanation.mjs +33 -0
  350. package/src/upgrade/provenance.mjs +338 -0
  351. package/src/upgrade/transaction-files.mjs +103 -0
  352. package/src/upgrade/transaction.mjs +475 -0
  353. package/src/upgrade/upgrade.mjs +63 -28
  354. package/src/vault/hosts.mjs +21 -0
  355. package/src/vault/identity.mjs +19 -0
  356. package/src/vault/index.mjs +7 -0
  357. package/src/vault/interface.mjs +11 -0
  358. package/src/vault/metadata.mjs +31 -0
  359. package/src/vault/privacy.mjs +148 -0
  360. package/src/vault/probe.mjs +89 -0
  361. package/src/vault/service.mjs +131 -0
  362. package/src/vault/source.mjs +45 -0
  363. package/src/vault/storage.mjs +23 -0
  364. package/templates/distribution-workspace/.claude/launch.json +6 -2
  365. package/templates/distribution-workspace/gitignore +1 -0
  366. package/templates/distribution-workspace/project/README.md +2 -2
  367. package/templates/external-project-workspace/README.md +45 -0
  368. package/templates/external-project-workspace/answers.example.json +4 -0
  369. package/templates/external-project-workspace/atelier.project.json +40 -0
  370. package/templates/external-project-workspace/boundary-policy.v1.json +60 -0
  371. package/templates/external-project-workspace/gitignore +8 -0
  372. package/templates/external-project-workspace/pack-lifecycle.json +14 -0
  373. package/templates/external-project-workspace/packs/protocols/contract-gate.v1.json +109 -0
  374. package/templates/external-project-workspace/packs/sample-pack.v1.json +48 -0
  375. package/templates/external-project-workspace/repo-access.v1.json +9 -0
  376. package/templates/external-project-workspace/source/README.md +11 -0
  377. package/templates/launch.json +6 -2
  378. package/templates/private-domain-workspace/.claude/launch.json +6 -2
  379. package/templates/private-domain-workspace/domain/README.md +2 -2
  380. package/templates/private-domain-workspace/gitignore +1 -0
  381. package/templates/shared-project-workspace/.claude/launch.json +6 -2
  382. package/templates/shared-project-workspace/gitignore +1 -0
  383. package/templates/shared-project-workspace/project/README.md +2 -2
@@ -0,0 +1,84 @@
1
+ // Calls one `tick` again and again, never two at a time.
2
+ //
3
+ // The next tick is scheduled only when the previous one has settled, so ticks
4
+ // cannot overlap however long one takes. A tick that throws does not end the
5
+ // loop and does not change the next tick: the error is handed to `onOutcome`,
6
+ // and the delay before the next attempt doubles per consecutive failure up to
7
+ // a ceiling, so a disk that stays full is retried slowly instead of in a tight
8
+ // loop. One success returns the loop to its interval.
9
+ //
10
+ // `tickNow()` asks for a tick that starts after the request: while a tick is
11
+ // in flight the request is answered by one follow-up tick, shared by everyone
12
+ // who asked meanwhile.
13
+
14
+ export const DEFAULT_TICK_INTERVAL_MS = 30 * 1000
15
+ export const DEFAULT_MAX_BACKOFF_MS = 15 * 60 * 1000
16
+
17
+ export const TICK_LOOP_PRIMITIVES = Object.freeze({
18
+ // Whether the loop goes on after a tick threw.
19
+ continuesAfterError: () => true,
20
+ delayAfter: ({ intervalMs, maxBackoffMs, consecutiveFailures }) => (consecutiveFailures === 0 ? intervalMs : Math.min(maxBackoffMs, intervalMs * 2 ** Math.min(consecutiveFailures, 20))),
21
+ })
22
+
23
+ export function createTickLoopForOracleTests({ tick, intervalMs = DEFAULT_TICK_INTERVAL_MS, maxBackoffMs = DEFAULT_MAX_BACKOFF_MS, setTimer = setTimeout, clearTimer = clearTimeout, onOutcome = () => {} }, primitives = TICK_LOOP_PRIMITIVES) {
24
+ if (typeof tick !== 'function') throw new TypeError('the tick loop needs a tick')
25
+ if (!Number.isInteger(intervalMs) || intervalMs < 1 || !Number.isInteger(maxBackoffMs) || maxBackoffMs < intervalMs) throw new TypeError('the tick loop needs a positive interval and a backoff ceiling no smaller than it')
26
+ const rules = { ...TICK_LOOP_PRIMITIVES, ...primitives }
27
+ let timer = null
28
+ let inFlight = null
29
+ let followUp = null // { promise, resolve } while somebody waits for a tick that starts after now
30
+ let started = false
31
+ let stopped = false
32
+ let consecutiveFailures = 0
33
+ let ticks = 0
34
+
35
+ function schedule() {
36
+ if (stopped || !started || timer !== null) return
37
+ timer = setTimer(() => { timer = null; void run() }, rules.delayAfter({ intervalMs, maxBackoffMs, consecutiveFailures }))
38
+ }
39
+
40
+ async function once() {
41
+ let outcome
42
+ try { outcome = { ok: true, report: await tick() } } catch (error) { outcome = { ok: false, error } }
43
+ ticks += 1
44
+ consecutiveFailures = outcome.ok ? 0 : consecutiveFailures + 1
45
+ try { await onOutcome({ ...outcome, consecutiveFailures }) } catch { /* recording an outcome never ends the loop */ }
46
+ if (!outcome.ok && !rules.continuesAfterError()) stopped = true
47
+ return outcome
48
+ }
49
+
50
+ function run() {
51
+ if (stopped) return Promise.resolve({ ok: false, stopped: true })
52
+ if (inFlight) return inFlight
53
+ if (timer !== null) { clearTimer(timer); timer = null }
54
+ inFlight = once().finally(() => {
55
+ inFlight = null
56
+ const waiting = followUp
57
+ followUp = null
58
+ if (waiting) run().then(waiting.resolve)
59
+ else schedule()
60
+ })
61
+ return inFlight
62
+ }
63
+
64
+ return {
65
+ start() { if (!started && !stopped) { started = true; void run() } },
66
+ tickNow() {
67
+ if (stopped) return Promise.resolve({ ok: false, stopped: true })
68
+ if (!inFlight) return run()
69
+ if (!followUp) { let resolve; const promise = new Promise((settle) => { resolve = settle }); followUp = { promise, resolve } }
70
+ return followUp.promise
71
+ },
72
+ // Lets the tick in flight finish; starts no other.
73
+ async stop() {
74
+ stopped = true
75
+ if (timer !== null) { clearTimer(timer); timer = null }
76
+ if (inFlight) await inFlight.catch(() => {})
77
+ },
78
+ state: () => ({ started, stopped, ticking: inFlight !== null, ticks, consecutiveFailures, nextDelayMs: rules.delayAfter({ intervalMs, maxBackoffMs, consecutiveFailures }) }),
79
+ }
80
+ }
81
+
82
+ export function createTickLoop(options) {
83
+ return createTickLoopForOracleTests(options, TICK_LOOP_PRIMITIVES)
84
+ }
@@ -0,0 +1,36 @@
1
+ import fs from 'node:fs'
2
+
3
+ // Filesystem watchers, as hints only.
4
+ //
5
+ // factory({ roots: [{ id, path, recursive }], onEvent }) -> { close() }
6
+ // onEvent({ rootId, relative }) relative is a '/'-separated path under the
7
+ // root, or null for "something under it"
8
+ //
9
+ // A watcher may drop, repeat or delay events and may fail to start at all; the
10
+ // engine never depends on one. It only hashes a named file sooner than the
11
+ // next stat scan would have. Tests inject a factory they drive by hand.
12
+
13
+ export function createNullWatcherFactory() {
14
+ return () => ({ close() {} })
15
+ }
16
+
17
+ // fs.watch, recursive where the platform supports it. It does not keep the
18
+ // process alive, and a root that cannot be watched is reported, not thrown:
19
+ // digest reconciliation still covers it.
20
+ export function createFsWatcherFactory({ watch = fs.watch, onUnavailable = () => {} } = {}) {
21
+ return ({ roots, onEvent }) => {
22
+ const watchers = []
23
+ for (const root of roots) {
24
+ try {
25
+ const watcher = watch(root.path, { recursive: root.recursive !== false, persistent: false }, (_eventType, filename) => {
26
+ onEvent({ rootId: root.id, relative: typeof filename === 'string' && filename !== '' ? filename.split('\\').join('/') : null })
27
+ })
28
+ watcher.on('error', () => onUnavailable({ rootId: root.id }))
29
+ watchers.push(watcher)
30
+ } catch {
31
+ onUnavailable({ rootId: root.id })
32
+ }
33
+ }
34
+ return { close() { for (const watcher of watchers.splice(0)) try { watcher.close() } catch { /* already closed */ } } }
35
+ }
36
+ }
@@ -0,0 +1,491 @@
1
+ import crypto from 'node:crypto'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import { validateJsonSchema } from '../export/atelier-export-contract.mjs'
5
+ import { readRegularTextNoFollow } from '../project/private-state.mjs'
6
+ import {
7
+ classifyRemoteAuthentication,
8
+ gitText,
9
+ inspectGitEngine,
10
+ parseNullConfig,
11
+ runGit,
12
+ sanitizeRemoteUrl,
13
+ } from './git-adapter.mjs'
14
+
15
+ export const ATELIER_REPOSITORY_OBSERVATION_SCHEMA = 'atelier-repository-observation@v1'
16
+ export const ATELIER_REPOSITORY_OBSERVATION_MAX_ENTRIES = 4096
17
+ const OBSERVATION_CONTRACT = JSON.parse(fs.readFileSync(new URL('../../contracts/atelier-repository-observation.v1.schema.json', import.meta.url), 'utf8'))
18
+
19
+ const CLOUD_PATH_PATTERNS = [
20
+ { code: 'provider-managed-onedrive', pattern: /(?:^|[\\/])OneDrive(?:\s*-[^\\/]+)?(?:[\\/]|$)/i },
21
+ { code: 'provider-managed-dropbox', pattern: /(?:^|[\\/])Dropbox(?:[\\/]|$)/i },
22
+ { code: 'provider-managed-google-drive', pattern: /(?:^|[\\/])Google Drive(?:[\\/]|$)/i },
23
+ { code: 'provider-managed-icloud', pattern: /(?:^|[\\/])(?:iCloud Drive|Mobile Documents)(?:[\\/]|$)/i },
24
+ { code: 'provider-managed-cloudstorage', pattern: /(?:^|[\\/])Library[\\/]CloudStorage(?:[\\/]|$)/i },
25
+ ]
26
+
27
+ function normalizeRelative(value) {
28
+ return String(value || '').replaceAll('\\', '/').replace(/^\.\//, '')
29
+ }
30
+
31
+ function sha256(value) {
32
+ return crypto.createHash('sha256').update(value).digest('hex')
33
+ }
34
+
35
+ function firstLine(value) {
36
+ return String(value || '').split(/\r?\n/)[0].trim()
37
+ }
38
+
39
+ function optionalText(gitExecutable, repoRoot, args, { failures = null, label = args[0], allowMissing = false } = {}) {
40
+ const result = runGit(gitExecutable, repoRoot, args, { allowFailure: true })
41
+ if (result.ok) return result.stdout.trim()
42
+ if (!allowMissing && failures) failures.push(blocker('observation-evidence-unavailable', `required Git evidence is unavailable: ${label}`, { probe: label }))
43
+ return null
44
+ }
45
+
46
+ function boolConfig(config, key) {
47
+ const item = config.find((entry) => entry.key.toLowerCase() === key.toLowerCase())
48
+ return /^(?:true|yes|on|1)$/i.test(item?.value || '')
49
+ }
50
+
51
+ function safeConfigKey(key) {
52
+ const segments = String(key || '').split('.')
53
+ if (segments.length < 3) return String(key || '').toLowerCase()
54
+ const section = segments.shift().toLowerCase()
55
+ const variable = segments.pop().toLowerCase()
56
+ return `${section}.[subsection].${variable}`
57
+ }
58
+
59
+ function configValues(config, pattern, { includeValue = false } = {}) {
60
+ return config.filter((entry) => pattern.test(entry.key)).map((entry) => includeValue ? { key: safeConfigKey(entry.key), value: entry.value } : { key: safeConfigKey(entry.key) })
61
+ }
62
+
63
+ export function classifyFilesystemRoot(candidate, { platform = process.platform } = {}) {
64
+ const requested = String(candidate || '')
65
+ if (!requested) return { supported: false, code: 'path-missing', requested, resolved: null }
66
+ if (/^(?:\\\\|\/\/)/.test(requested)) {
67
+ return { supported: false, code: 'network-unc-root', requested, resolved: null }
68
+ }
69
+ if (platform === 'linux' && /^\/mnt\/[a-z](?:\/|$)/i.test(requested)) {
70
+ return { supported: false, code: 'wsl-cross-boundary-root', requested, resolved: null }
71
+ }
72
+ const cloud = CLOUD_PATH_PATTERNS.find(({ pattern }) => pattern.test(requested))
73
+ if (cloud) return { supported: false, code: cloud.code, requested, resolved: null }
74
+
75
+ let resolved
76
+ try {
77
+ resolved = fs.realpathSync.native(requested)
78
+ } catch (error) {
79
+ return { supported: false, code: 'path-unresolvable', requested, resolved: null, detail: error.code || error.message }
80
+ }
81
+ const resolvedCloud = CLOUD_PATH_PATTERNS.find(({ pattern }) => pattern.test(resolved))
82
+ if (resolvedCloud) return { supported: false, code: resolvedCloud.code, requested, resolved }
83
+ if (platform === 'darwin' && /^\/Volumes\//.test(resolved)) {
84
+ return { supported: false, code: 'external-volume-unclassified', requested, resolved }
85
+ }
86
+
87
+ let statfs = null
88
+ try {
89
+ const value = fs.statfsSync(resolved)
90
+ statfs = { type: String(value.type), blockSize: Number(value.bsize), blocks: Number(value.blocks) }
91
+ } catch {
92
+ // Filesystem metadata is useful evidence, but absence is not proof of a
93
+ // remote root. The lexical refusal boundary remains deterministic.
94
+ }
95
+ return { supported: true, code: 'local-filesystem', requested, resolved, statfs }
96
+ }
97
+
98
+ export function resolveRepositoryRoot(candidate, gitExecutable) {
99
+ const filesystem = classifyFilesystemRoot(candidate)
100
+ if (!filesystem.supported) return { ok: false, filesystem, root: filesystem.resolved }
101
+ const rootResult = runGit(gitExecutable, filesystem.resolved, ['rev-parse', '--show-toplevel'], { allowFailure: true })
102
+ if (!rootResult.ok) return { ok: false, filesystem, root: null, error: 'path is not inside a Git worktree' }
103
+ let root
104
+ try {
105
+ root = fs.realpathSync.native(rootResult.stdout.trim())
106
+ } catch {
107
+ root = fs.realpathSync(rootResult.stdout.trim())
108
+ }
109
+ const rootFilesystem = classifyFilesystemRoot(root)
110
+ if (!rootFilesystem.supported) return { ok: false, filesystem: rootFilesystem, root }
111
+ return { ok: true, filesystem: rootFilesystem, root }
112
+ }
113
+
114
+ export function parsePorcelainStatus(text) {
115
+ const records = String(text || '').split('\0')
116
+ const entries = []
117
+ for (let index = 0; index < records.length; index += 1) {
118
+ const record = records[index]
119
+ if (!record) continue
120
+ const code = record.slice(0, 2)
121
+ const item = { code, path: normalizeRelative(record.slice(3)), originalPath: null }
122
+ if (code[0] === 'R' || code[0] === 'C') {
123
+ item.originalPath = normalizeRelative(records[index + 1] || '')
124
+ index += 1
125
+ }
126
+ entries.push(item)
127
+ }
128
+ return entries.sort((left, right) => left.path.localeCompare(right.path, 'en'))
129
+ }
130
+
131
+ function branchState(gitExecutable, root, failures) {
132
+ const branch = optionalText(gitExecutable, root, ['symbolic-ref', '--quiet', '--short', 'HEAD'], { failures, label: 'current branch', allowMissing: true })
133
+ const head = optionalText(gitExecutable, root, ['rev-parse', '--verify', 'HEAD'], { failures, label: 'HEAD identity' })
134
+ const upstream = optionalText(gitExecutable, root, ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'], { failures, label: 'upstream identity', allowMissing: true })
135
+ let ahead = 0
136
+ let behind = 0
137
+ if (upstream) {
138
+ const counts = optionalText(gitExecutable, root, ['rev-list', '--left-right', '--count', `HEAD...${upstream}`], { failures, label: 'upstream divergence counts' })
139
+ const match = counts?.match(/^(\d+)\s+(\d+)$/)
140
+ if (match) {
141
+ ahead = Number(match[1])
142
+ behind = Number(match[2])
143
+ } else if (counts != null) {
144
+ failures.push(blocker('observation-evidence-invalid', 'required Git evidence is malformed: upstream divergence counts', { probe: 'upstream divergence counts' }))
145
+ }
146
+ }
147
+ return { branch, detached: !branch, head, upstream, ahead, behind }
148
+ }
149
+
150
+ function remoteState(gitExecutable, root, failures) {
151
+ const names = (optionalText(gitExecutable, root, ['remote'], { failures, label: 'remote names' }) || '').split(/\r?\n/).filter(Boolean).sort()
152
+ return names.map((name) => {
153
+ const url = optionalText(gitExecutable, root, ['remote', 'get-url', name], { failures, label: `remote URL for ${name}` })
154
+ const pushResult = runGit(gitExecutable, root, ['remote', 'get-url', '--push', '--all', name], { allowFailure: true })
155
+ if (!pushResult.ok) failures.push(blocker('observation-evidence-unavailable', `required Git evidence is unavailable: push destinations for ${name}`, { probe: `push destinations for ${name}` }))
156
+ const pushUrls = pushResult.ok ? pushResult.stdout.split(/\r?\n/).map((value) => value.trim()).filter(Boolean) : []
157
+ const pushUrl = pushUrls.length === 1 ? pushUrls[0] : null
158
+ return {
159
+ name,
160
+ url: sanitizeRemoteUrl(url),
161
+ identityDigest: sha256(sanitizeRemoteUrl(url)),
162
+ authentication: classifyRemoteAuthentication(url),
163
+ pushUrl: pushUrl == null ? null : sanitizeRemoteUrl(pushUrl),
164
+ pushIdentityDigest: pushUrl == null ? null : sha256(sanitizeRemoteUrl(pushUrl)),
165
+ pushAuthentication: pushUrl == null ? 'unknown' : classifyRemoteAuthentication(pushUrl),
166
+ pushTargetCount: pushUrls.length,
167
+ }
168
+ })
169
+ }
170
+
171
+ function submoduleState(gitExecutable, root) {
172
+ if (!fs.existsSync(path.join(root, '.gitmodules'))) {
173
+ const staged = runGit(gitExecutable, root, ['ls-files', '--stage', '-z'], { allowFailure: true })
174
+ const gitlinks = staged.ok
175
+ ? staged.stdout.split('\0').filter(Boolean).flatMap((record) => {
176
+ const match = record.match(/^160000 [0-9a-f]+ \d+\t(.+)$/u)
177
+ return match ? [{ marker: '?', value: match[1] }] : []
178
+ })
179
+ : []
180
+ if (gitlinks.length) {
181
+ return {
182
+ declared: true,
183
+ entries: gitlinks,
184
+ complete: false,
185
+ error: 'gitlink entries exist without .gitmodules',
186
+ }
187
+ }
188
+ return { declared: false, entries: [], complete: staged.ok, error: staged.ok ? null : 'gitlink inventory failed' }
189
+ }
190
+ const result = runGit(gitExecutable, root, ['submodule', 'status', '--recursive'], { allowFailure: true })
191
+ const entries = result.stdout.split(/\r?\n/).filter(Boolean).map((line) => ({
192
+ marker: line[0],
193
+ value: line.slice(1).trim(),
194
+ }))
195
+ const complete = result.ok && entries.every((entry) => entry.marker === ' ')
196
+ return { declared: true, entries, complete, error: result.ok ? null : firstLine(result.stderr) || 'submodule status failed' }
197
+ }
198
+
199
+ function lfsState(gitExecutable, root, config, gitDir, statusEntries, failures) {
200
+ const trackedAttributes = (optionalText(gitExecutable, root, ['ls-files', '--', '**/.gitattributes', '.gitattributes'], { failures, label: 'tracked attributes' }) || '')
201
+ .split(/\r?\n/)
202
+ .filter(Boolean)
203
+ const attributeFiles = trackedAttributes.filter((file) => fs.existsSync(path.join(root, file))).map((file) => ({ label: file, absolute: path.join(root, file) }))
204
+ for (const entry of statusEntries) {
205
+ if (path.basename(entry.path) !== '.gitattributes') continue
206
+ const absolute = path.resolve(root, entry.path)
207
+ const relative = path.relative(root, absolute)
208
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
209
+ failures.push(blocker('observation-evidence-unavailable', 'worktree attributes evidence escapes the repository root'))
210
+ continue
211
+ }
212
+ if (fs.existsSync(absolute)) attributeFiles.push({ label: entry.path, absolute })
213
+ }
214
+ const infoAttributes = gitDir ? path.join(gitDir, 'info', 'attributes') : null
215
+ if (infoAttributes && fs.existsSync(infoAttributes)) attributeFiles.push({ label: '.git/info/attributes', absolute: infoAttributes })
216
+ for (const [variable, label] of [['GIT_ATTR_GLOBAL', 'global Git attributes'], ['GIT_ATTR_SYSTEM', 'system Git attributes']]) {
217
+ const absolute = optionalText(gitExecutable, root, ['var', variable], { failures, label: `${label} location` })
218
+ if (absolute && fs.existsSync(absolute)) attributeFiles.push({ label, absolute })
219
+ }
220
+ const externalAttributes = config.find((entry) => entry.key.toLowerCase() === 'core.attributesfile')
221
+ if (externalAttributes) {
222
+ failures.push(blocker('external-attributes-file-unclassified', 'core.attributesFile can change Git filter semantics and must be removed or classified', { key: externalAttributes.key }))
223
+ }
224
+ const uniqueAttributeFiles = [...new Map(attributeFiles.map((item) => [item.absolute, item])).values()]
225
+ const required = uniqueAttributeFiles.some(({ label, absolute }) => {
226
+ const stat = fs.lstatSync(absolute)
227
+ if (stat.isSymbolicLink() || !stat.isFile()) {
228
+ failures.push(blocker('observation-evidence-unavailable', 'Git attributes evidence is redirected or not a regular file', { path: label }))
229
+ return false
230
+ }
231
+ try {
232
+ return /filter\s*=\s*lfs|filter=lfs/i.test(readRegularTextNoFollow(absolute))
233
+ } catch {
234
+ failures.push(blocker('observation-evidence-unavailable', 'Git attributes evidence changed or could not be read safely', { path: label }))
235
+ return false
236
+ }
237
+ })
238
+ const version = runGit(gitExecutable, root, ['lfs', 'version'], { allowFailure: true })
239
+ const labels = uniqueAttributeFiles.map((item) => item.label)
240
+ if (!required) return { required: false, available: version.ok, complete: !externalAttributes, attributeFiles: labels, error: externalAttributes ? 'external Git attributes are unclassified' : null }
241
+ if (!version.ok) return { required: true, available: false, complete: false, attributeFiles: labels, error: 'Git LFS is required but unavailable' }
242
+ const status = runGit(gitExecutable, root, ['lfs', 'fsck'], { allowFailure: true, timeout: 120_000 })
243
+ return {
244
+ required: true,
245
+ available: true,
246
+ complete: status.ok,
247
+ attributeFiles: labels,
248
+ error: status.ok ? null : firstLine(status.stderr) || 'Git LFS object verification failed',
249
+ }
250
+ }
251
+
252
+ function repositoryFeatures(gitExecutable, root, config, failures) {
253
+ const gitDir = optionalText(gitExecutable, root, ['rev-parse', '--absolute-git-dir'], { failures, label: 'absolute Git directory' })
254
+ const commonDirRaw = optionalText(gitExecutable, root, ['rev-parse', '--git-common-dir'], { failures, label: 'Git common directory' })
255
+ const commonDir = commonDirRaw ? path.resolve(root, commonDirRaw) : null
256
+ const worktrees = (optionalText(gitExecutable, root, ['worktree', 'list', '--porcelain'], { failures, label: 'worktree inventory' }) || '')
257
+ .split(/\r?\n/)
258
+ .filter((line) => line.startsWith('worktree '))
259
+ .map((line) => line.slice('worktree '.length))
260
+ // Classify the filter identity before sanitizing its subsection. Once
261
+ // `filter.lfs.clean` becomes `filter.[subsection].clean`, a standard Git LFS
262
+ // installation is indistinguishable from an unreviewed custom filter. CI
263
+ // runners commonly install these keys globally, so the ordering matters.
264
+ const filters = configValues(config, /^filter\./i)
265
+ const customFilters = config
266
+ .filter((entry) => /^filter\./i.test(entry.key) && !/^filter\.lfs\./i.test(entry.key))
267
+ .map((entry) => ({ key: safeConfigKey(entry.key) }))
268
+ const partialClone = configValues(config, /^(?:extensions\.partialclone|remote\..*\.promisor)$/i)
269
+ const urlRewrites = configValues(config, /^url\..*\.(?:insteadof|pushinsteadof)$/i)
270
+ const sparse = boolConfig(config, 'core.sparsecheckout') || Boolean(gitDir && fs.existsSync(path.join(gitDir, 'info', 'sparse-checkout')))
271
+ const shallowRaw = optionalText(gitExecutable, root, ['rev-parse', '--is-shallow-repository'], { failures, label: 'shallow repository state' })
272
+ if (shallowRaw != null && !/^(?:true|false)$/.test(shallowRaw)) {
273
+ failures.push(blocker('observation-evidence-invalid', 'required Git evidence is malformed: shallow repository state', { probe: 'shallow repository state' }))
274
+ }
275
+ const indexFlagsResult = runGit(gitExecutable, root, ['ls-files', '-v', '-z'], { allowFailure: true })
276
+ if (!indexFlagsResult.ok) failures.push(blocker('observation-evidence-unavailable', 'required Git index visibility evidence is unavailable', { probe: 'index visibility flags' }))
277
+ const indexFlags = indexFlagsResult.ok
278
+ ? indexFlagsResult.stdout.split('\0').filter(Boolean).reduce((counts, record) => {
279
+ const tag = record[0]
280
+ if (tag === 'S' || tag === 's') counts.skipWorktree += 1
281
+ if (/^[a-z]$/.test(tag)) counts.assumeUnchanged += 1
282
+ return counts
283
+ }, { assumeUnchanged: 0, skipWorktree: 0 })
284
+ : { assumeUnchanged: 0, skipWorktree: 0 }
285
+ return {
286
+ gitDir,
287
+ commonDir,
288
+ linkedWorktree: Boolean(gitDir && commonDir && path.resolve(gitDir) !== path.resolve(commonDir)),
289
+ worktrees,
290
+ sparseCheckout: sparse,
291
+ shallowRepository: shallowRaw === 'true',
292
+ indexFlags,
293
+ partialClone,
294
+ urlRewrites,
295
+ filters,
296
+ customFilters,
297
+ hooksPathConfigured: config.some((entry) => entry.key.toLowerCase() === 'core.hookspath'),
298
+ fileModeTrusted: boolConfig(config, 'core.filemode'),
299
+ symlinksTrusted: !config.some((entry) => entry.key.toLowerCase() === 'core.symlinks' && /^(?:false|no|off|0)$/i.test(entry.value || '')),
300
+ signing: {
301
+ required: boolConfig(config, 'commit.gpgsign'),
302
+ keyConfigured: config.some((entry) => entry.key.toLowerCase() === 'user.signingkey'),
303
+ formatConfigured: config.some((entry) => entry.key.toLowerCase() === 'gpg.format'),
304
+ },
305
+ proxy: configValues(config, /^https?\..*proxy$/i),
306
+ }
307
+ }
308
+
309
+ function gitMode(stat, index, { fileModeTrusted, symlinksTrusted }) {
310
+ if (stat.isSymbolicLink()) return '120000'
311
+ if (!stat.isFile()) return null
312
+ const indexMode = index?.length === 1 && index[0].stage === 0 ? index[0].mode : null
313
+ if (!symlinksTrusted && indexMode === '120000') return '120000'
314
+ if (!fileModeTrusted && /^100(?:644|755)$/.test(indexMode || '')) return indexMode
315
+ return (stat.mode & 0o111) === 0 ? '100644' : '100755'
316
+ }
317
+
318
+ function indexEntry(gitExecutable, root, itemPath, failures) {
319
+ const result = runGit(gitExecutable, root, ['--literal-pathspecs', 'ls-files', '--stage', '-z', '--', itemPath], { allowFailure: true })
320
+ if (!result.ok) {
321
+ failures.push(blocker('observation-evidence-unavailable', 'required Git index evidence is unavailable', { path: itemPath }))
322
+ return null
323
+ }
324
+ const records = result.stdout.split('\0').filter(Boolean)
325
+ if (!records.length) return null
326
+ const parsed = records.map((record) => {
327
+ const match = record.match(/^(\d{6}) ([0-9a-f]{40,64}) (\d)\t/)
328
+ if (!match) return null
329
+ return { mode: match[1], blob: match[2], stage: Number(match[3]) }
330
+ })
331
+ if (parsed.some((item) => !item)) {
332
+ failures.push(blocker('observation-evidence-invalid', 'required Git index evidence is malformed', { path: itemPath }))
333
+ return null
334
+ }
335
+ return parsed
336
+ }
337
+
338
+ function worktreeEntry(gitExecutable, root, itemPath, index, features, failures) {
339
+ const absolute = path.join(root, itemPath)
340
+ let stat
341
+ try {
342
+ stat = fs.lstatSync(absolute)
343
+ } catch (error) {
344
+ if (error?.code === 'ENOENT') return null
345
+ failures.push(blocker('observation-evidence-unavailable', 'required worktree evidence is unavailable', { path: itemPath }))
346
+ return null
347
+ }
348
+ const mode = gitMode(stat, index, features)
349
+ if (!mode) {
350
+ failures.push(blocker('observation-evidence-invalid', 'changed worktree path is not a regular file or symlink', { path: itemPath }))
351
+ return null
352
+ }
353
+ const symlinkBytes = stat.isSymbolicLink() ? fs.readlinkSync(absolute, { encoding: 'buffer' }) : null
354
+ const result = symlinkBytes
355
+ ? runGit(gitExecutable, root, ['hash-object', '--stdin'], { allowFailure: true, input: symlinkBytes })
356
+ : runGit(gitExecutable, root, ['--literal-pathspecs', 'hash-object', '--no-filters', '--', itemPath], { allowFailure: true })
357
+ const blob = result.ok ? result.stdout.trim() : null
358
+ if (!blob || !/^[0-9a-f]{40,64}$/.test(blob)) {
359
+ failures.push(blocker('observation-evidence-unavailable', 'required worktree blob evidence is unavailable', { path: itemPath }))
360
+ return null
361
+ }
362
+ const indexResult = symlinkBytes
363
+ ? runGit(gitExecutable, root, ['hash-object', '--stdin'], { allowFailure: true, input: symlinkBytes })
364
+ : runGit(gitExecutable, root, ['--literal-pathspecs', 'hash-object', `--path=${itemPath}`, '--', itemPath], { allowFailure: true })
365
+ const indexBlob = indexResult.ok ? indexResult.stdout.trim() : null
366
+ if (!indexBlob || !/^[0-9a-f]{40,64}$/.test(indexBlob)) {
367
+ failures.push(blocker('observation-evidence-unavailable', 'required filtered blob evidence is unavailable', { path: itemPath }))
368
+ return null
369
+ }
370
+ return { mode, blob, indexBlob }
371
+ }
372
+
373
+ function statusFingerprints(gitExecutable, root, entries, features, failures) {
374
+ return entries.map((entry) => {
375
+ const index = indexEntry(gitExecutable, root, entry.path, failures)
376
+ const worktree = worktreeEntry(gitExecutable, root, entry.path, index, features, failures)
377
+ const originalIndex = entry.originalPath ? indexEntry(gitExecutable, root, entry.originalPath, failures) : null
378
+ return {
379
+ code: entry.code,
380
+ path: entry.path,
381
+ originalPath: entry.originalPath,
382
+ worktree,
383
+ index,
384
+ originalIndex,
385
+ }
386
+ })
387
+ }
388
+
389
+ function blocker(code, message, details = {}) {
390
+ return { code, message, details }
391
+ }
392
+
393
+ function completenessFor({ filesystem, engine, bare, remotes, features, submodules, lfs, acquisitionFailures }) {
394
+ const blockers = [...acquisitionFailures]
395
+ const warnings = []
396
+ if (!filesystem.supported) blockers.push(blocker(filesystem.code, 'repository root is not on a supported local filesystem'))
397
+ if (!engine.supported) blockers.push(blocker('git-version-unsupported', `Git ${engine.version} is older than ${engine.minimum}`))
398
+ if (bare) blockers.push(blocker('bare-repository-unsupported', 'a working tree is required'))
399
+ if (features.sparseCheckout) blockers.push(blocker('sparse-checkout-unsupported', 'sparse workspaces are not complete observations'))
400
+ if (features.shallowRepository) blockers.push(blocker('shallow-repository-unsupported', 'shallow history is not a complete repository observation'))
401
+ if (features.indexFlags.assumeUnchanged || features.indexFlags.skipWorktree) blockers.push(blocker('index-visibility-flags-unsupported', 'assume-unchanged and skip-worktree index flags can hide authored changes from status', features.indexFlags))
402
+ if (features.partialClone.length) blockers.push(blocker('partial-clone-unsupported', 'partial clones may omit required Git objects', { config: features.partialClone }))
403
+ if (features.urlRewrites.length) blockers.push(blocker('url-rewrite-unclassified', 'Git URL rewrite rules make the execution destination ambiguous and must be removed before synchronization', { config: features.urlRewrites.map((entry) => entry.key) }))
404
+ if (features.hooksPathConfigured) blockers.push(blocker('custom-hooks-path-unclassified', 'core.hooksPath changes executable Git behavior and must be removed or classified before synchronization'))
405
+ if (!submodules.complete) blockers.push(blocker('submodules-incomplete', 'all declared submodules must be initialized and clean', { entries: submodules.entries }))
406
+ if (!lfs.complete) blockers.push(blocker('lfs-incomplete', lfs.error || 'Git LFS content is not complete'))
407
+ if (features.customFilters.length) blockers.push(blocker('custom-filter-unclassified', 'custom Git filters must be classified before synchronization', { filters: features.customFilters.map((entry) => entry.key) }))
408
+ for (const remote of remotes) {
409
+ if (remote.authentication === 'unknown') blockers.push(blocker('remote-authentication-unknown', `${remote.name} uses an unsupported remote URL shape`))
410
+ if (remote.pushTargetCount !== 1) blockers.push(blocker('remote-push-destination-ambiguous', `${remote.name} must resolve to exactly one push destination`, { count: remote.pushTargetCount }))
411
+ else if (remote.pushAuthentication === 'unknown') blockers.push(blocker('remote-push-authentication-unknown', `${remote.name} uses an unsupported push URL shape`))
412
+ }
413
+ if (!remotes.length) warnings.push(blocker('remote-missing', 'repository has no remotes; local commits are possible but synchronization is not'))
414
+ if (features.linkedWorktree) warnings.push(blocker('linked-worktree', 'repository is a linked worktree; the common Git directory is shared'))
415
+ if (features.proxy.length) warnings.push(blocker('git-proxy-configured', 'Git proxy configuration is present and must be preserved'))
416
+ return { complete: blockers.length === 0, blockers, warnings }
417
+ }
418
+
419
+ export function observeRepository({ repoRoot, gitExecutable, observedAt = new Date().toISOString() }) {
420
+ const resolved = resolveRepositoryRoot(repoRoot, gitExecutable)
421
+ if (!resolved.ok) {
422
+ return {
423
+ schema: ATELIER_REPOSITORY_OBSERVATION_SCHEMA,
424
+ observedAt,
425
+ complete: false,
426
+ root: resolved.root,
427
+ filesystem: resolved.filesystem,
428
+ blockers: [blocker(resolved.filesystem?.code || 'repository-unresolved', resolved.error || 'repository could not be resolved')],
429
+ warnings: [],
430
+ }
431
+ }
432
+ const root = resolved.root
433
+ const acquisitionFailures = []
434
+ const engine = inspectGitEngine(gitExecutable)
435
+ const bare = gitText(gitExecutable, root, ['rev-parse', '--is-bare-repository']) === 'true'
436
+ const configResult = runGit(gitExecutable, root, ['config', '--null', '--list'], { allowFailure: true })
437
+ const config = configResult.ok ? parseNullConfig(configResult.stdout) : []
438
+ if (!configResult.ok) acquisitionFailures.push(blocker('observation-evidence-unavailable', 'required Git evidence is unavailable: configuration inventory', { probe: 'configuration inventory' }))
439
+ const branch = branchState(gitExecutable, root, acquisitionFailures)
440
+ const statusResult = runGit(gitExecutable, root, ['status', '--porcelain=v1', '-z', '--untracked-files=all'])
441
+ const allEntries = parsePorcelainStatus(statusResult.stdout)
442
+ const entryCeilingExceeded = allEntries.length > ATELIER_REPOSITORY_OBSERVATION_MAX_ENTRIES
443
+ const entries = allEntries.slice(0, ATELIER_REPOSITORY_OBSERVATION_MAX_ENTRIES)
444
+ if (entryCeilingExceeded) {
445
+ acquisitionFailures.push(blocker('observation-change-set-oversized', 'repository change set exceeds the bounded observation entry ceiling', {
446
+ limitEntries: ATELIER_REPOSITORY_OBSERVATION_MAX_ENTRIES,
447
+ observedEntries: allEntries.length,
448
+ }))
449
+ }
450
+ const remotes = remoteState(gitExecutable, root, acquisitionFailures)
451
+ const features = repositoryFeatures(gitExecutable, root, config, acquisitionFailures)
452
+ const submodules = submoduleState(gitExecutable, root)
453
+ const lfs = lfsState(gitExecutable, root, config, features.gitDir, allEntries, acquisitionFailures)
454
+ const conflicts = allEntries.filter((entry) => entry.code.includes('U') || ['AA', 'DD'].includes(entry.code))
455
+ const staged = allEntries.filter((entry) => entry.code[0] && entry.code[0] !== '?' && entry.code[0] !== ' ')
456
+ const unstaged = allEntries.filter((entry) => entry.code[1] && entry.code[1] !== ' ')
457
+ const fingerprints = entryCeilingExceeded ? [] : statusFingerprints(gitExecutable, root, entries, features, acquisitionFailures)
458
+ const completeness = completenessFor({ filesystem: resolved.filesystem, engine, bare, remotes, features, submodules, lfs, acquisitionFailures })
459
+ const statusDigest = sha256(JSON.stringify({ head: branch.head, branch: branch.branch, rawStatusDigest: sha256(statusResult.stdout), fingerprints }))
460
+ return {
461
+ schema: ATELIER_REPOSITORY_OBSERVATION_SCHEMA,
462
+ observedAt,
463
+ complete: completeness.complete,
464
+ root,
465
+ filesystem: resolved.filesystem,
466
+ git: engine,
467
+ bare,
468
+ branch,
469
+ remotes,
470
+ features,
471
+ submodules,
472
+ lfs,
473
+ status: {
474
+ clean: allEntries.length === 0,
475
+ digest: statusDigest,
476
+ entries,
477
+ fingerprints,
478
+ stagedCount: staged.length,
479
+ unstagedCount: unstaged.length,
480
+ conflictCount: conflicts.length,
481
+ observedEntryCount: allEntries.length,
482
+ entriesTruncated: entryCeilingExceeded,
483
+ },
484
+ blockers: completeness.blockers,
485
+ warnings: completeness.warnings,
486
+ }
487
+ }
488
+
489
+ export function validateRepositoryObservation(doc) {
490
+ return validateJsonSchema(OBSERVATION_CONTRACT, doc)
491
+ }