@mnstry/atelier 0.2.0-alpha.1

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 (260) hide show
  1. package/CHANGELOG.md +383 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +5 -0
  4. package/README.md +262 -0
  5. package/SECURITY.md +93 -0
  6. package/TRADEMARKS.md +88 -0
  7. package/announcements/2026-08-06-announcements-channel.v1.json +12 -0
  8. package/announcements/keys/mnstry-announcements.public.v1.json +9 -0
  9. package/bin/atelier.mjs +2 -0
  10. package/bin/mnstry-atelier.mjs +3 -0
  11. package/contracts/analysis-adapter.v1.schema.json +248 -0
  12. package/contracts/atelier-action-intent.v1.schema.json +143 -0
  13. package/contracts/atelier-analysis-adapter.v1.schema.json +360 -0
  14. package/contracts/atelier-attestation.v1.schema.json +239 -0
  15. package/contracts/atelier-boundary-policy.v1.schema.json +339 -0
  16. package/contracts/atelier-claim.v1.schema.json +97 -0
  17. package/contracts/atelier-export.v1.schema.json +1015 -0
  18. package/contracts/atelier-extension-pack.v1.schema.json +290 -0
  19. package/contracts/atelier-kit-manifest.v1.schema.json +273 -0
  20. package/contracts/atelier-lock.v1.schema.json +364 -0
  21. package/contracts/atelier-migration.v1.schema.json +162 -0
  22. package/contracts/atelier-project-config.v1.schema.json +352 -0
  23. package/contracts/atelier-readiness-protocol.v1.schema.json +517 -0
  24. package/contracts/atelier-readiness-run.v1.schema.json +229 -0
  25. package/contracts/atelier-readiness.v1.schema.json +457 -0
  26. package/contracts/compat-baseline.json +10 -0
  27. package/contracts/git-promote-event.v1.schema.json +145 -0
  28. package/contracts/knowledge-source-sidecar.v1.schema.json +157 -0
  29. package/contracts/mnstry-atelier-semantic-profile.v1.json +254 -0
  30. package/docs/announcements.md +101 -0
  31. package/docs/atelier-runtime.md +34 -0
  32. package/docs/attestation.md +201 -0
  33. package/docs/continuity.md +55 -0
  34. package/docs/contract-stability.md +111 -0
  35. package/docs/distributions.md +220 -0
  36. package/docs/install.md +140 -0
  37. package/docs/knowledge-graph.md +140 -0
  38. package/docs/ontology.md +206 -0
  39. package/docs/release-engineering.md +151 -0
  40. package/docs/repo-boundary-guard.md +199 -0
  41. package/docs/tenant-readiness.md +64 -0
  42. package/docs/upgrade.md +124 -0
  43. package/fixtures/analysis/invalid/native-graph.v1.json +4 -0
  44. package/fixtures/analysis/invalid/promoted-claim.v1.json +17 -0
  45. package/fixtures/analysis/invalid/runtime-import.v1.json +20 -0
  46. package/fixtures/analysis/valid/output.claims.v1.json +17 -0
  47. package/fixtures/analysis-adapter/invalid/manifest-hidden-provider.v1.json +24 -0
  48. package/fixtures/analysis-adapter/invalid/output-frontmatter-mutation.v1.json +21 -0
  49. package/fixtures/analysis-adapter/invalid/output-native-graph.v1.json +14 -0
  50. package/fixtures/analysis-adapter/manifest.disabled.v1.json +22 -0
  51. package/fixtures/analysis-adapter/manifest.enabled-local.v1.json +23 -0
  52. package/fixtures/analysis-adapter/output.claims.v1.json +21 -0
  53. package/fixtures/atelier-action-intent/invalid/runtime-mutation-authority.v1.json +16 -0
  54. package/fixtures/atelier-action-intent/invalid/unknown-top-level-field.v1.json +17 -0
  55. package/fixtures/atelier-action-intent/invalid/writes-beyond-clipboard.v1.json +16 -0
  56. package/fixtures/atelier-action-intent/valid/copy-agent-prompt.v1.json +16 -0
  57. package/fixtures/atelier-action-intent/valid/copy-repo-path.v1.json +22 -0
  58. package/fixtures/atelier-analysis-adapter/invalid/hidden-model-provider.v1.json +24 -0
  59. package/fixtures/atelier-analysis-adapter/invalid/inert-with-execute-command.v1.json +24 -0
  60. package/fixtures/atelier-analysis-adapter/invalid/proposals-with-runtime-import.v1.json +30 -0
  61. package/fixtures/atelier-analysis-adapter/valid/manifest-inert.v1.json +24 -0
  62. package/fixtures/atelier-analysis-adapter/valid/proposals-claim-only.v1.json +27 -0
  63. package/fixtures/atelier-attestation/invalid/bad-digest.v1.json +23 -0
  64. package/fixtures/atelier-attestation/invalid/conformance-scope.v1.json +23 -0
  65. package/fixtures/atelier-attestation/invalid/missing-signature.v1.json +22 -0
  66. package/fixtures/atelier-attestation/invalid/unknown-root-field.v1.json +24 -0
  67. package/fixtures/atelier-attestation/keys/roundtrip-issuer.public.v1.json +9 -0
  68. package/fixtures/atelier-attestation/valid/rejected-with-reasons.v1.json +49 -0
  69. package/fixtures/atelier-attestation/valid/signed-admitted.v1.json +35 -0
  70. package/fixtures/atelier-attestation/valid/signed-roundtrip.v1.json +29 -0
  71. package/fixtures/atelier-attestation/valid/unsigned-advisory.v1.json +23 -0
  72. package/fixtures/atelier-claim/invalid/unknown-provider.v1.json +15 -0
  73. package/fixtures/atelier-claim/invalid/unknown-top-level-field.v1.json +16 -0
  74. package/fixtures/atelier-claim/invalid/wrong-schema-const.v1.json +15 -0
  75. package/fixtures/atelier-claim/valid/adapter-style-proposed-relation.v1.json +19 -0
  76. package/fixtures/atelier-claim/valid/codex-proposed-relation.v1.json +16 -0
  77. package/fixtures/atelier-claim/valid/manual-proposed-relation.v1.json +16 -0
  78. package/fixtures/atelier-claim/valid/readiness-proposed-relation.v1.json +18 -0
  79. package/fixtures/atelier-export/invalid/dirty-unforced-empty-taint.v1.json +19 -0
  80. package/fixtures/atelier-export/invalid/object-local-audience-visibility.v1.json +11 -0
  81. package/fixtures/atelier-export/invalid/open-object-class-owner.v1.json +23 -0
  82. package/fixtures/atelier-export/invalid/pricing-legacy-amount.v1.json +23 -0
  83. package/fixtures/atelier-export/invalid/public-disguised-internal-source-ref.v1.json +11 -0
  84. package/fixtures/atelier-export/invalid/public-nested-private-source-ref.v1.json +17 -0
  85. package/fixtures/atelier-export/invalid/public-resolved-private-source-ref.v1.json +13 -0
  86. package/fixtures/atelier-export/invalid/public-resolved-sensitive-source-ref.v1.json +13 -0
  87. package/fixtures/atelier-export/invalid/public-source-ref-missing-provenance.v1.json +20 -0
  88. package/fixtures/atelier-export/invalid/public-source-team-kg.json +12 -0
  89. package/fixtures/atelier-export/invalid/source-node-legacy-visibility.v1.json +21 -0
  90. package/fixtures/atelier-export/invalid/source-node-unknown-field.v1.json +18 -0
  91. package/fixtures/atelier-export/invalid/unresolved-public-source-ref.v1.json +11 -0
  92. package/fixtures/atelier-export/sample-studio-offer.v1.json +397 -0
  93. package/fixtures/atelier-extension-pack/invalid/bundled-slug-collision.v1.json +22 -0
  94. package/fixtures/atelier-extension-pack/invalid/escapes-root.v1.json +22 -0
  95. package/fixtures/atelier-extension-pack/invalid/posture-tampered.v1.json +22 -0
  96. package/fixtures/atelier-extension-pack/invalid/protocols/autonomous-authority.v1.json +118 -0
  97. package/fixtures/atelier-extension-pack/invalid/protocols/offer-map.v1.json +109 -0
  98. package/fixtures/atelier-extension-pack/invalid/redefines-runtime-term.v1.json +19 -0
  99. package/fixtures/atelier-extension-pack/invalid/wrong-namespace.v1.json +19 -0
  100. package/fixtures/atelier-extension-pack/valid/protocols/contract-gate.v1.json +109 -0
  101. package/fixtures/atelier-extension-pack/valid/sample-pack.v1.json +48 -0
  102. package/fixtures/atelier-kit-manifest/invalid/runtime-copy-without-canonical-role.v1.json +53 -0
  103. package/fixtures/atelier-kit-manifest/invalid/telemetry-send-path.v1.json +21 -0
  104. package/fixtures/atelier-kit-manifest/valid/mnstry-atelier.valid.v1.json +76 -0
  105. package/fixtures/atelier-lock/invalid/absolute-contract-path.v1.json +40 -0
  106. package/fixtures/atelier-lock/invalid/runtime-mutation-enabled.v1.json +41 -0
  107. package/fixtures/atelier-lock/invalid/unknown-top-level-field.v1.json +42 -0
  108. package/fixtures/atelier-lock/valid/extension-pack.v1.json +55 -0
  109. package/fixtures/atelier-lock/valid/minimal.v1.json +44 -0
  110. package/fixtures/atelier-migration/invalid/live-apply-mode.v1.json +21 -0
  111. package/fixtures/atelier-migration/invalid/missing-review-evidence.v1.json +20 -0
  112. package/fixtures/atelier-migration/invalid/runtime-mutation-enabled.v1.json +21 -0
  113. package/fixtures/atelier-migration/valid/minimal.v1.json +32 -0
  114. package/fixtures/atelier-project-config/invalid/absolute-repo-path.v1.json +11 -0
  115. package/fixtures/atelier-project-config/invalid/ext-not-object.v1.json +12 -0
  116. package/fixtures/atelier-project-config/invalid/external-kind-read-boundary.v1.json +12 -0
  117. package/fixtures/atelier-project-config/invalid/missing-repos-and-alignment.v1.json +7 -0
  118. package/fixtures/atelier-project-config/invalid/unknown-top-level-field.v1.json +14 -0
  119. package/fixtures/atelier-project-config/valid/alignment-scaffold.v1.json +15 -0
  120. package/fixtures/atelier-project-config/valid/ext-extension-packs.v1.json +36 -0
  121. package/fixtures/atelier-project-config/valid/repo-workspace.v1.json +23 -0
  122. package/fixtures/atelier-project-config/validator-only/ext-member-not-object.v1.json +14 -0
  123. package/fixtures/atelier-readiness/invalid/dry-run-only-false.v1.json +87 -0
  124. package/fixtures/atelier-readiness/invalid/support-bundle-send-path.v1.json +87 -0
  125. package/fixtures/atelier-readiness/invalid/unknown-top-level-field.v1.json +88 -0
  126. package/fixtures/atelier-readiness/valid/ready-workspace.v1.json +84 -0
  127. package/fixtures/boundary-policy/invalid/content-rule-exception-blanket-path.v1.json +67 -0
  128. package/fixtures/boundary-policy/invalid/invalid-actor-private-repo-mapping.v1.json +20 -0
  129. package/fixtures/boundary-policy/invalid/legacy-warning-missing-warning.v1.json +20 -0
  130. package/fixtures/boundary-policy/invalid/missing-repo-coverage.v1.json +19 -0
  131. package/fixtures/boundary-policy/invalid/unknown-key.v1.json +12 -0
  132. package/fixtures/boundary-policy/valid/content-rule-exception.v1.json +67 -0
  133. package/fixtures/boundary-policy/valid/legacy-warning.v1.json +33 -0
  134. package/fixtures/boundary-policy/valid/strict-private-shared.v1.json +40 -0
  135. package/fixtures/git-promote/invalid/provision-named-revocable.v1.json +29 -0
  136. package/fixtures/git-promote/private-to-team.v1.json +29 -0
  137. package/fixtures/knowledge-source-sidecar/invalid/legacy-visibility-field.v1.json +17 -0
  138. package/fixtures/knowledge-source-sidecar/invalid/missing-audience.v1.json +15 -0
  139. package/fixtures/knowledge-source-sidecar/invalid/unknown-relation-kind.v1.json +18 -0
  140. package/fixtures/knowledge-source-sidecar/valid/html-source.v1.json +18 -0
  141. package/fixtures/knowledge-source-sidecar/valid/pdf-evidence.v1.json +18 -0
  142. package/fixtures/projects/neutral-project/atelier.project.json +15 -0
  143. package/fixtures/projects/neutral-project/ops/config/repo-access.v1.json +12 -0
  144. package/fixtures/projects/neutral-project/project-knowledge.graph.json +69 -0
  145. package/fixtures/projects/sample-workspace/atelier.project.json +23 -0
  146. package/fixtures/projects/sample-workspace/boundary-policy.v1.json +59 -0
  147. package/fixtures/projects/sample-workspace/content/README.md +16 -0
  148. package/fixtures/projects/sample-workspace/content/source.html +5 -0
  149. package/fixtures/projects/sample-workspace/content/source.html.kg.json +15 -0
  150. package/fixtures/projects/sample-workspace/repo-access.v1.json +9 -0
  151. package/fixtures/projects/source-formats-workspace/atelier.project.json +23 -0
  152. package/fixtures/projects/source-formats-workspace/content/README.md +17 -0
  153. package/fixtures/projects/source-formats-workspace/content/data.json +7 -0
  154. package/fixtures/projects/source-formats-workspace/content/data.json.kg.json +15 -0
  155. package/fixtures/projects/source-formats-workspace/content/logo.png.kg.json +15 -0
  156. package/fixtures/projects/source-formats-workspace/content/metrics.csv +3 -0
  157. package/fixtures/projects/source-formats-workspace/content/metrics.csv.kg.json +15 -0
  158. package/fixtures/projects/source-formats-workspace/content/pipeline.yaml +4 -0
  159. package/fixtures/projects/source-formats-workspace/content/pipeline.yaml.kg.json +15 -0
  160. package/fixtures/projects/source-formats-workspace/repo-access.v1.json +9 -0
  161. package/fixtures/readiness-protocols/extension-pack/invalid/protocol-missing-path.v1.json +20 -0
  162. package/fixtures/readiness-protocols/extension-pack/valid/with-protocol.v1.json +24 -0
  163. package/fixtures/readiness-protocols/protocol/invalid/missing-check-target.v1.json +103 -0
  164. package/fixtures/readiness-protocols/protocol/invalid/runtime-mutation-enabled.v1.json +107 -0
  165. package/fixtures/readiness-protocols/protocol/valid/minimal.v1.json +100 -0
  166. package/fixtures/readiness-protocols/run/invalid/missing-evidence.v1.json +37 -0
  167. package/fixtures/readiness-protocols/run/invalid/pass-with-blocker.v1.json +22 -0
  168. package/fixtures/readiness-protocols/run/invalid/runtime-mutation-enabled.v1.json +22 -0
  169. package/fixtures/readiness-protocols/run/valid/pass.v1.json +44 -0
  170. package/fixtures/support/invalid/leaky-support.v1.json +13 -0
  171. package/fixtures/support/valid/minimal-safe.v1.json +20 -0
  172. package/package.json +98 -0
  173. package/skills/claude/atelier-open/SKILL.md +68 -0
  174. package/skills/claude/mnstry-readiness/SKILL.md +41 -0
  175. package/skills/codex/atelier-open/SKILL.md +68 -0
  176. package/skills/codex/mnstry-readiness/SKILL.md +41 -0
  177. package/src/analysis/adapter.mjs +20 -0
  178. package/src/analysis/analysis-claim-contract.mjs +335 -0
  179. package/src/attestation/jcs.mjs +64 -0
  180. package/src/attestation/sign.mjs +305 -0
  181. package/src/boundary/content-rules.mjs +262 -0
  182. package/src/boundary/policy.mjs +806 -0
  183. package/src/check-atelier-export-contract.mjs +116 -0
  184. package/src/cli/run.mjs +283 -0
  185. package/src/collaboration/proposals.mjs +262 -0
  186. package/src/commands/analysis.mjs +8 -0
  187. package/src/commands/announcements.mjs +308 -0
  188. package/src/commands/attestation.mjs +198 -0
  189. package/src/commands/boundary.mjs +25 -0
  190. package/src/commands/config.mjs +4 -0
  191. package/src/commands/context.mjs +3 -0
  192. package/src/commands/distribution.mjs +224 -0
  193. package/src/commands/egress.mjs +11 -0
  194. package/src/commands/extension-pack.mjs +209 -0
  195. package/src/commands/feedback.mjs +3 -0
  196. package/src/commands/graph.mjs +3 -0
  197. package/src/commands/init.mjs +130 -0
  198. package/src/commands/lock.mjs +3 -0
  199. package/src/commands/project.mjs +3 -0
  200. package/src/commands/promote.mjs +4 -0
  201. package/src/commands/readiness.mjs +3 -0
  202. package/src/commands/server.mjs +6 -0
  203. package/src/commands/setup.mjs +250 -0
  204. package/src/commands/support.mjs +3 -0
  205. package/src/commands/upgrade.mjs +3 -0
  206. package/src/contracts/corpus.mjs +171 -0
  207. package/src/egress/check.mjs +45 -0
  208. package/src/egress/forbidden-egress.mjs +323 -0
  209. package/src/export/atelier-export-contract.mjs +722 -0
  210. package/src/extension-packs/loader.mjs +491 -0
  211. package/src/graph/graph.mjs +370 -0
  212. package/src/graph/knowledge-graph.mjs +887 -0
  213. package/src/harness/context-client.mjs +144 -0
  214. package/src/harness/context.mjs +27 -0
  215. package/src/index.mjs +88 -0
  216. package/src/project/config.mjs +557 -0
  217. package/src/project/file-class.mjs +113 -0
  218. package/src/project/git-ignore.mjs +31 -0
  219. package/src/project/path-match.mjs +30 -0
  220. package/src/project/repo-identity.mjs +176 -0
  221. package/src/projection/alignment-projection.mjs +169 -0
  222. package/src/projection/policy.mjs +90 -0
  223. package/src/projection/project.mjs +152 -0
  224. package/src/readiness/readiness.mjs +312 -0
  225. package/src/readiness-protocols/bundled-pack.mjs +766 -0
  226. package/src/readiness-protocols/runtime.mjs +419 -0
  227. package/src/server/local-sidecar.mjs +640 -0
  228. package/src/server/security.mjs +229 -0
  229. package/src/server/server.mjs +42 -0
  230. package/src/support/feedback-report.mjs +414 -0
  231. package/src/support/support-bundle.mjs +226 -0
  232. package/src/ui/html-primitives.mjs +104 -0
  233. package/src/upgrade/upgrade.mjs +701 -0
  234. package/src/validate-atelier-export-dry-run.mjs +75 -0
  235. package/templates/atelier.project.example.json +23 -0
  236. package/templates/distribution-workspace/.claude/launch.json +12 -0
  237. package/templates/distribution-workspace/README.md +86 -0
  238. package/templates/distribution-workspace/atelier.project.json +36 -0
  239. package/templates/distribution-workspace/boundary-policy.v1.json +61 -0
  240. package/templates/distribution-workspace/gitignore +29 -0
  241. package/templates/distribution-workspace/governance/repo-boundary-ledger.md +5 -0
  242. package/templates/distribution-workspace/project/README.md +19 -0
  243. package/templates/distribution-workspace/repo-access.v1.json +9 -0
  244. package/templates/launch.json +12 -0
  245. package/templates/private-domain-workspace/.claude/launch.json +12 -0
  246. package/templates/private-domain-workspace/README.md +50 -0
  247. package/templates/private-domain-workspace/atelier.project.json +28 -0
  248. package/templates/private-domain-workspace/boundary-policy.v1.json +54 -0
  249. package/templates/private-domain-workspace/domain/README.md +19 -0
  250. package/templates/private-domain-workspace/gitignore +29 -0
  251. package/templates/private-domain-workspace/governance/repo-boundary-ledger.md +4 -0
  252. package/templates/private-domain-workspace/repo-access.v1.json +9 -0
  253. package/templates/shared-project-workspace/.claude/launch.json +12 -0
  254. package/templates/shared-project-workspace/README.md +51 -0
  255. package/templates/shared-project-workspace/atelier.project.json +28 -0
  256. package/templates/shared-project-workspace/boundary-policy.v1.json +61 -0
  257. package/templates/shared-project-workspace/gitignore +29 -0
  258. package/templates/shared-project-workspace/governance/repo-boundary-ledger.md +5 -0
  259. package/templates/shared-project-workspace/project/README.md +19 -0
  260. package/templates/shared-project-workspace/repo-access.v1.json +9 -0
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+ import {
7
+ validateAtelierExportContract,
8
+ validateJsonSchema,
9
+ validateSchemaShape,
10
+ } from './export/atelier-export-contract.mjs'
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
13
+ const packageRoot = path.resolve(__dirname, '..')
14
+ const schemaPath = path.join(packageRoot, 'contracts/atelier-export.v1.schema.json')
15
+ const fixtureRoot = path.join(packageRoot, 'fixtures/atelier-export')
16
+ const invalidFixtureRoot = path.join(fixtureRoot, 'invalid')
17
+
18
+ function fail(message) {
19
+ console.error(`[atelier-export-contract:check] ${message}`)
20
+ throw new Error(message)
21
+ }
22
+
23
+ function readJson(file) {
24
+ try {
25
+ return JSON.parse(fs.readFileSync(file, 'utf8'))
26
+ } catch (error) {
27
+ fail(`${path.relative(process.cwd(), file)} is not valid JSON: ${error.message}`)
28
+ }
29
+ }
30
+
31
+ function asArray(value) {
32
+ return Array.isArray(value) ? value : []
33
+ }
34
+
35
+ function clone(value) {
36
+ return JSON.parse(JSON.stringify(value))
37
+ }
38
+
39
+ function applyPointerMutation(target, pointer, value) {
40
+ if (!pointer.startsWith('/')) throw new Error(`invalid JSON pointer ${pointer}`)
41
+ const parts = pointer.slice(1).split('/').map((part) => part.replaceAll('~1', '/').replaceAll('~0', '~'))
42
+ let node = target
43
+ for (const part of parts.slice(0, -1)) {
44
+ if (node == null || !(part in node)) throw new Error(`missing JSON pointer parent ${pointer}`)
45
+ node = node[part]
46
+ }
47
+ const leaf = parts.at(-1)
48
+ if (node == null || leaf == null || !(leaf in node)) throw new Error(`missing JSON pointer leaf ${pointer}`)
49
+ node[leaf] = value
50
+ }
51
+
52
+ function validateInvalidFixture(file, { schema }) {
53
+ const spec = readJson(file)
54
+ const basePath = path.resolve(path.dirname(file), spec.base)
55
+ const doc = clone(readJson(basePath))
56
+ for (const mutation of asArray(spec.mutations)) {
57
+ applyPointerMutation(doc, mutation.path, mutation.value)
58
+ }
59
+
60
+ const report = validateAtelierExportContract(doc, { schema, dryRunOnly: true })
61
+ const errors = [
62
+ ...report.errors,
63
+ ...report.semanticProfileViolations.map((violation) => `${violation.path} ${violation.message}`),
64
+ ]
65
+ if (errors.length === 0) {
66
+ return [`negative fixture ${path.basename(file)} unexpectedly passed`]
67
+ }
68
+
69
+ return asArray(spec.expectedErrors)
70
+ .filter((expected) => !errors.some((error) => error.includes(expected)))
71
+ .map((expected) => `negative fixture ${path.basename(file)} did not produce expected error: ${expected}`)
72
+ }
73
+
74
+ export function runAtelierExportContractCheck() {
75
+ const schema = readJson(schemaPath)
76
+ const failures = validateSchemaShape(schema)
77
+ const positiveFixtures = fs.readdirSync(fixtureRoot)
78
+ .filter((name) => name.endsWith('.json'))
79
+ .map((name) => path.join(fixtureRoot, name))
80
+
81
+ for (const file of positiveFixtures) {
82
+ const doc = readJson(file)
83
+ failures.push(...validateJsonSchema(schema, doc).map((error) => `${path.relative(process.cwd(), file)}: schema ${error}`))
84
+ const report = validateAtelierExportContract(doc, { schema, dryRunOnly: true })
85
+ failures.push(...report.errors.map((error) => `${path.relative(process.cwd(), file)}: ${error}`))
86
+ failures.push(
87
+ ...report.semanticProfileViolations.map((violation) => {
88
+ return `${path.relative(process.cwd(), file)}: ${violation.path} ${violation.message}`
89
+ })
90
+ )
91
+ }
92
+
93
+ if (fs.existsSync(invalidFixtureRoot)) {
94
+ for (const name of fs.readdirSync(invalidFixtureRoot).filter((entry) => entry.endsWith('.json')).sort()) {
95
+ failures.push(...validateInvalidFixture(path.join(invalidFixtureRoot, name), { schema }))
96
+ }
97
+ }
98
+
99
+ if (failures.length) {
100
+ for (const failure of failures) console.error(`[atelier-export-contract:check] ${failure}`)
101
+ return 1
102
+ }
103
+
104
+ console.log(`[atelier-export-contract:check] ${positiveFixtures.length} positive fixture(s) and invalid fixtures passed`)
105
+ return 0
106
+ }
107
+
108
+ const currentFile = fileURLToPath(import.meta.url)
109
+ if (process.argv[1] && path.resolve(process.argv[1]) === currentFile) {
110
+ try {
111
+ process.exitCode = runAtelierExportContractCheck()
112
+ } catch (error) {
113
+ console.error(`[atelier-export-contract:check] ${error instanceof Error ? error.message : String(error)}`)
114
+ process.exitCode = 1
115
+ }
116
+ }
@@ -0,0 +1,283 @@
1
+ import { spawnSync } from 'node:child_process'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+
6
+ export const DEFAULT_BRAND = Object.freeze({ command: 'atelier', displayName: 'MNSTRY Atelier', version: null })
7
+
8
+ export const commandMap = new Map([
9
+ ['contract', ['src/check-atelier-export-contract.mjs']],
10
+ ['init', ['src/commands/init.mjs']],
11
+ ['setup', ['src/commands/setup.mjs', 'setup']],
12
+ ['adopt', ['src/commands/setup.mjs', 'adopt']],
13
+ ['export', ['src/validate-atelier-export-dry-run.mjs']],
14
+ ['export:dry-run', ['src/validate-atelier-export-dry-run.mjs']],
15
+ ['dry-run', ['src/validate-atelier-export-dry-run.mjs']],
16
+ ['graph', ['src/commands/graph.mjs']],
17
+ ['project', ['src/commands/project.mjs']],
18
+ ['build', ['src/commands/project.mjs']],
19
+ ['dev', ['src/commands/server.mjs']],
20
+ ['server', ['src/commands/server.mjs']],
21
+ ['readiness', ['src/commands/readiness.mjs']],
22
+ ['generated', ['src/commands/readiness.mjs', '--check']],
23
+ ['generated:check', ['src/commands/readiness.mjs', '--check']],
24
+ ['doctor', ['src/commands/setup.mjs', 'doctor']],
25
+ ['context', ['src/commands/context.mjs']],
26
+ ['resolve', ['src/commands/context.mjs', 'resolve']],
27
+ ['capabilities', ['src/commands/context.mjs', 'capabilities']],
28
+ ['proposal', ['src/commands/context.mjs', 'proposal']],
29
+ ['support', ['src/commands/support.mjs']],
30
+ ['support:bundle', ['src/commands/support.mjs']],
31
+ ['egress', ['src/commands/egress.mjs']],
32
+ ['egress:check', ['src/commands/egress.mjs']],
33
+ ['boundary', ['src/commands/boundary.mjs']],
34
+ ['boundary:check', ['src/commands/boundary.mjs', 'check']],
35
+ ['boundary:doctor', ['src/commands/boundary.mjs', 'doctor']],
36
+ ['boundary:push-check', ['src/commands/boundary.mjs', 'push-check']],
37
+ ['boundary:audit', ['src/commands/boundary.mjs', 'audit']],
38
+ ['boundary:install-hooks', ['src/commands/boundary.mjs', 'install-hooks']],
39
+ ['promote', ['src/commands/promote.mjs']],
40
+ ['upgrade', ['src/commands/upgrade.mjs']],
41
+ ['lock', ['src/commands/lock.mjs']],
42
+ ['lock:check', ['src/commands/lock.mjs', 'check']],
43
+ ['lock:write', ['src/commands/lock.mjs', 'write']],
44
+ ['analysis', ['src/commands/analysis.mjs']],
45
+ ['analyze', ['src/commands/analysis.mjs']],
46
+ ['config', ['src/commands/config.mjs']],
47
+ ['config:check', ['src/commands/config.mjs']],
48
+ ['manifest', ['src/commands/config.mjs']],
49
+ ['extension-pack', ['src/commands/extension-pack.mjs']],
50
+ ['extension-pack:validate', ['src/commands/extension-pack.mjs', 'validate']],
51
+ ['extension-pack:list', ['src/commands/extension-pack.mjs', 'list']],
52
+ ['distribution', ['src/commands/distribution.mjs']],
53
+ ['distribution:check', ['src/commands/distribution.mjs', 'check']],
54
+ ['attestation', ['src/commands/attestation.mjs']],
55
+ ['feedback', ['src/commands/feedback.mjs']],
56
+ ['feedback:check', ['src/commands/feedback.mjs', 'check']],
57
+ ['announcements', ['src/commands/announcements.mjs']],
58
+ ['announcements:list', ['src/commands/announcements.mjs', 'list']],
59
+ ])
60
+
61
+ function isDefaultBrand(brand) {
62
+ return brand.command === DEFAULT_BRAND.command && brand.displayName === DEFAULT_BRAND.displayName
63
+ }
64
+
65
+ function validateBrand(brand) {
66
+ if (!brand || typeof brand !== 'object') throw new Error('brand must be an object')
67
+ if (typeof brand.command !== 'string' || !/^[a-z][a-z0-9-]*$/.test(brand.command)) {
68
+ throw new Error(`brand.command must match /^[a-z][a-z0-9-]*$/: ${String(brand.command)}`)
69
+ }
70
+ if (typeof brand.displayName !== 'string' || brand.displayName.length === 0) {
71
+ throw new Error('brand.displayName must be a non-empty string')
72
+ }
73
+ // Control characters (C0 including ESC, and DEL) in a display name can
74
+ // forge or corrupt terminal output (ANSI injection), so they are rejected.
75
+ // The message deliberately never echoes the offending value.
76
+ if (/[\u0000-\u001f\u007f]/.test(brand.displayName)) {
77
+ throw new Error('brand.displayName must not contain control characters')
78
+ }
79
+ }
80
+
81
+ export function buildHelpText(brand = DEFAULT_BRAND, atelierVersion = null) {
82
+ const header = isDefaultBrand(brand)
83
+ ? `MNSTRY Atelier
84
+
85
+ Usage:
86
+ atelier <command> [args]
87
+ mnstry-atelier <command> [args] # legacy alias
88
+ mnstry atelier <command> [args] # compatibility`
89
+ : `${brand.displayName}
90
+ powered by MNSTRY Atelier ${atelierVersion}
91
+
92
+ Usage:
93
+ ${brand.command} <command> [args]`
94
+ return `${header}
95
+
96
+ Core commands:
97
+ init Create a blank-slate project from a template.
98
+ adopt Add Atelier to an existing repo/workspace.
99
+ setup --yes Repair ignored local machine state.
100
+ graph [--check] Build or check the knowledge graph.
101
+ project [--check] Build or check the workspace projection.
102
+ build [--check] Build or check a realm portal.
103
+ dev Run the local Atelier sidecar.
104
+ readiness [--check] Build or check readiness.
105
+ readiness protocols List bundled MNSTRY readiness protocols.
106
+ readiness journey Show the MNSTRY tenant-readiness journey.
107
+ readiness run PROTOCOL_ID Run a claim-first readiness protocol.
108
+ readiness packet Generate an ignored tenant-readiness draft.
109
+ readiness export --dry-run Preview readiness export blockers.
110
+ generated check Run terminal generated-artifact freshness.
111
+ context flow ... Resolve session-bound harness context.
112
+ export --dry-run FILE Validate atelier-export@v1 dry-run artifact.
113
+ support bundle --dry-run Preview a no-send support bundle.
114
+ feedback --message TEXT Write a local, never-sent feedback report.
115
+ announcements list List and verify MNSTRY announcements.
116
+ egress check Check extracted Atelier paths for forbidden egress.
117
+ boundary check Enforce private/shared repo placement rules.
118
+ boundary audit Report content-rule matches tree-wide without blocking.
119
+ boundary install-hooks Install staged/private-domain Git guards.
120
+ promote Record a git.promote disclosure event.
121
+ upgrade --dry-run Plan a safe package/template upgrade.
122
+ upgrade --apply Apply a branch-based reviewable upgrade.
123
+ lock check|write Verify or create atelier.lock.json.
124
+ config check Validate project config.
125
+ extension-pack validate Validate declared extension packs.
126
+ extension-pack list List declared extension packs.
127
+ distribution check Check a distribution for MNSTRY attribution.
128
+
129
+ Attestation commands:
130
+ attestation hash FILE Print the canonical payload hash of a payload.
131
+ attestation sign FILE Sign an unsigned attestation with a signing key file.
132
+ attestation verify FILE Verify an attestation against a public key file.
133
+ attestation keygen --key-id ID Generate a signing key pair.
134
+
135
+ Every project-aware command accepts --project-config=PATH or
136
+ MNSTRY_ATELIER_PROJECT_CONFIG=PATH. Machine-local repo paths belong in
137
+ .atelier-local/, atelier.local.json, or atelier.workspace.local.json.`
138
+ }
139
+
140
+ export function buildCommandHelpText(command, brand = DEFAULT_BRAND) {
141
+ const c = brand.command
142
+ const help = {
143
+ init: `Usage: ${c} init [--template private-domain|shared-project|sample-workspace|distribution] [--target DIR] [--actor ID]
144
+
145
+ Creates tracked starter files and an Atelier lockfile. It does not install hooks unless asked separately. An unrecognized --template exits 1 and writes nothing; omit --template for the blank scaffold.`,
146
+ adopt: `Usage: ${c} adopt [--profile single-repo|private-domain|shared-project|multi-repo|monorepo|control-workspace] [--target DIR] [--yes]
147
+
148
+ Creates or checks a tracked atelier.project.json for an existing workspace and writes machine-local path bindings only to ignored local overlay files.`,
149
+ setup: `Usage: ${c} setup --yes [--project ./atelier.project.json]
150
+
151
+ Ensures ignored local Atelier state exists, verifies ignore coverage, and records unambiguous local repo path bindings.`,
152
+ doctor: `Usage: ${c} doctor [--project ./atelier.project.json] [--fix] [--dry-run]
153
+
154
+ Reports project config, local overlay, and repo boundary readiness. --fix only repairs ignored local state.`,
155
+ boundary: `Usage: ${c} boundary check|push-check|audit|install-hooks [--project ./atelier.project.json] [--staged]
156
+
157
+ check --staged judges the staged diff. push-check reads pre-push ref updates on stdin and judges only the pushed range. audit scans the whole tree and reports without blocking, so an accepted usage never strands unrelated work.`,
158
+ graph: `Usage: ${c} graph [--check] [--project ./atelier.project.json]
159
+
160
+ Builds or checks the project knowledge graph from tracked sources plus ignored local path bindings.`,
161
+ readiness: `Usage:
162
+ ${c} readiness [--check] [--project ./atelier.project.json]
163
+ ${c} readiness protocols [--json]
164
+ ${c} readiness journey [--project ./atelier.project.json]
165
+ ${c} readiness run mnstry.readiness:offer-map [--answers answers.json] [--project ./atelier.project.json]
166
+ ${c} readiness packet [--project ./atelier.project.json]
167
+ ${c} readiness export --dry-run [--project ./atelier.project.json]
168
+
169
+ Runs the bundled MNSTRY readiness pack claim-first. Protocol state and packet drafts stay in ignored .atelier-local/readiness/ unless explicitly materialized by a reviewed future flow.`,
170
+ 'extension-pack': `Usage:
171
+ ${c} extension-pack validate [--json] [--project ./atelier.project.json]
172
+ ${c} extension-pack list [--json] [--project ./atelier.project.json]
173
+
174
+ Loads every extension pack declared under ext["mnstry.atelier"].extensionPacks in the tracked project config and reports one line per pack. Packs load additively alongside the bundled MNSTRY protocols and can never replace them. list is the default subcommand.`,
175
+ distribution: `Usage: ${c} distribution check [--target DIR] [--pack DIR]
176
+
177
+ Checks a distribution package for the required MNSTRY attribution markers. Blocking: the distribution README.md byte check, and a CLI probe that EXECUTES the target's declared bin with --version (spawned with the current Node, cwd set to the target — only run this against distributions you trust) and requires the attribution in its output; a target that looks like a distribution but declares no probe-able bin, or ships a malformed package.json, is also blocking. The extension-pack manifest attribution key is advisory and reported only. The normative wording lives in TRADEMARKS.md under "Required attribution"; see also docs/attestation.md and docs/distributions.md.`,
178
+ attestation: `Usage:
179
+ ${c} attestation hash <payload.json>
180
+ ${c} attestation sign <attestation.json> [--key FILE] [--out FILE]
181
+ ${c} attestation verify <attestation.json> --public-key FILE [--payload FILE] [--json]
182
+ ${c} attestation keygen --key-id ID [--algorithm ed25519|es256] [--out FILE]
183
+
184
+ Records and checks admission decisions. hash prints the canonical payload hash (RFC 8785 JCS, SHA-256). sign reads the local signing key file. verify reads a public key file and exits 1 when it judges the attestation invalid. keygen writes the signing key file mode 0600, refuses to overwrite, and prints only the public key document.`,
185
+ feedback: `Usage:
186
+ ${c} feedback create --message TEXT | --message-file PATH [--context FILE] [--include-gates]
187
+ ${c} feedback check FILE
188
+
189
+ Assembles a local feedback report under ignored .atelier-local/feedback/ (mode 0600), scanned with the support-bundle banned key and value patterns before writing; any match refuses the write naming pattern label and location only. Files given to --message-file and --context must be valid UTF-8 text of at most 262144 bytes. The scan is a backstop, not clearance: read the whole report before sharing it. The kit has no send path — sharing the file is always the user's own explicit act.`,
190
+ announcements: `Usage: ${c} announcements list [--dir DIR] [--public-key FILE] | verify <file> [--public-key FILE] [--json] | show <file> [--public-key FILE]
191
+
192
+ MNSTRY announcements are a pull-only channel: signed JSON documents under announcements/ in the repository. The trust anchor is always the committed MNSTRY key, or one you pass explicitly with --public-key; --dir changes only where documents are read from and never which key verifies them. Every run names the key and keyId it used. The kit never fetches anything — receiving announcements is the git pull you chose to run, and show refuses to print a body whose signature does not verify.`,
193
+ }
194
+ return help[command] || `Usage: ${c} ${command} [args]\n\nRun ${c} --help for the command list.`
195
+ }
196
+
197
+ export function buildVersionText(brand = DEFAULT_BRAND, atelierVersion = null) {
198
+ if (isDefaultBrand(brand)) return `${atelierVersion}`
199
+ return `${brand.displayName} ${brand.version ?? atelierVersion} — powered by MNSTRY Atelier ${atelierVersion}`
200
+ }
201
+
202
+ function normalizeArgs(argv) {
203
+ const args = [...argv]
204
+ if (args[0] === 'atelier') args.shift()
205
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') return { help: true, args }
206
+ if (args[0] === '--version' || args[0] === '-v' || args[0] === 'version') return { version: true, args }
207
+
208
+ if (args[0] === 'export' && args[1] === '--dry-run') args.splice(0, 2, 'export:dry-run')
209
+ if (args[0] === 'generated' && args[1] === 'check') args.splice(0, 2, 'generated:check')
210
+ if (args[0] === 'egress' && args[1] === 'check') args.splice(0, 2, 'egress:check')
211
+ if (args[0] === 'boundary' && args[1] === 'check') args.splice(0, 2, 'boundary:check')
212
+ if (args[0] === 'boundary' && args[1] === 'doctor') args.splice(0, 2, 'boundary:doctor')
213
+ if (args[0] === 'boundary' && args[1] === 'push-check') args.splice(0, 2, 'boundary:push-check')
214
+ if (args[0] === 'boundary' && args[1] === 'audit') args.splice(0, 2, 'boundary:audit')
215
+ if (args[0] === 'boundary' && args[1] === 'install-hooks') args.splice(0, 2, 'boundary:install-hooks')
216
+ if (args[0] === 'lock' && args[1] === 'check') args.splice(0, 2, 'lock:check')
217
+ if (args[0] === 'lock' && args[1] === 'write') args.splice(0, 2, 'lock:write')
218
+ if (args[0] === 'config' && args[1] === 'check') args.splice(0, 2, 'config:check')
219
+ if (args[0] === 'extension-pack' && args[1] === 'validate') args.splice(0, 2, 'extension-pack:validate')
220
+ if (args[0] === 'extension-pack' && args[1] === 'list') args.splice(0, 2, 'extension-pack:list')
221
+ if (args[0] === 'distribution' && args[1] === 'check') args.splice(0, 2, 'distribution:check')
222
+ if (args[0] === 'support' && args[1] === 'bundle') args.splice(0, 2, 'support:bundle')
223
+ if (args[0] === 'support:bundle' && args[1] === '--dry-run') args.splice(1, 1)
224
+ return { help: false, args }
225
+ }
226
+
227
+ export async function runCli({
228
+ argv,
229
+ env = process.env,
230
+ cwd = process.cwd(),
231
+ brand = DEFAULT_BRAND,
232
+ stdout = console.log,
233
+ stderr = console.error,
234
+ }) {
235
+ validateBrand(brand)
236
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..')
237
+ const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'))
238
+ const atelierVersion = packageJson.version
239
+
240
+ const normalized = normalizeArgs(argv)
241
+ if (normalized.help) {
242
+ stdout(buildHelpText(brand, atelierVersion))
243
+ return 0
244
+ }
245
+ if (normalized.version) {
246
+ stdout(buildVersionText(brand, atelierVersion))
247
+ return 0
248
+ }
249
+
250
+ const [command, ...rest] = normalized.args
251
+ if (rest[0] === '--help' || rest[0] === '-h') {
252
+ stdout(buildCommandHelpText(command, brand))
253
+ return 0
254
+ }
255
+ const target = commandMap.get(command)
256
+ if (!target) {
257
+ stderr(`Unknown ${brand.displayName} command: ${command}`)
258
+ stdout(buildHelpText(brand, atelierVersion))
259
+ return 1
260
+ }
261
+
262
+ const [script, ...prefixArgs] = target
263
+ const scriptPath = path.join(packageRoot, script)
264
+ if (!fs.existsSync(scriptPath)) {
265
+ stderr(`${brand.displayName} command is not available in this package install: ${command}`)
266
+ return 1
267
+ }
268
+
269
+ const result = spawnSync(process.execPath, [scriptPath, ...prefixArgs, ...rest], {
270
+ cwd,
271
+ stdio: 'inherit',
272
+ env: {
273
+ ...env,
274
+ MNSTRY_ATELIER_PACKAGE_ROOT: packageRoot,
275
+ },
276
+ })
277
+
278
+ if (result.error) {
279
+ stderr(result.error.message)
280
+ return 1
281
+ }
282
+ return result.status ?? 0
283
+ }
@@ -0,0 +1,262 @@
1
+ import crypto from 'node:crypto'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+
5
+ export const ATELIER_PROPOSAL_SCHEMA = 'atelier-proposal@v1'
6
+ export const ATELIER_PROPOSALS_SCHEMA = 'atelier-proposals@v1'
7
+ export const PROPOSAL_REVIEW_STATUSES = new Set(['reviewed', 'accepted', 'rejected', 'superseded'])
8
+
9
+ const DIRECT_WRITE_ACTION_RE = /(?:^|[._:-])(apply|write|commit|persist|mutate|db|database)(?:$|[._:-])/i
10
+
11
+ function nowIso() {
12
+ return new Date().toISOString()
13
+ }
14
+
15
+ function cleanIdentity(value, max = 160) {
16
+ return String(value || '').trim().slice(0, max)
17
+ }
18
+
19
+ function stableCompare(left, right) {
20
+ return String(left).localeCompare(String(right), 'en')
21
+ }
22
+
23
+ function safeJsonText(value, max = 50000) {
24
+ if (value == null) return ''
25
+ if (typeof value === 'string') return value.slice(0, max)
26
+ try {
27
+ return JSON.stringify(value, null, 2).slice(0, max)
28
+ } catch {
29
+ return String(value).slice(0, max)
30
+ }
31
+ }
32
+
33
+ function ensurePrivateDir(dir) {
34
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
35
+ try {
36
+ fs.chmodSync(dir, 0o700)
37
+ } catch {
38
+ // Best effort on filesystems that do not support chmod.
39
+ }
40
+ }
41
+
42
+ function secureWriteJson(file, payload) {
43
+ ensurePrivateDir(path.dirname(file))
44
+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`
45
+ fs.writeFileSync(tmp, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 })
46
+ fs.renameSync(tmp, file)
47
+ try {
48
+ fs.chmodSync(file, 0o600)
49
+ } catch {
50
+ // Best effort on filesystems that do not support chmod.
51
+ }
52
+ }
53
+
54
+ function pathContainedBy(root, candidate) {
55
+ const resolvedRoot = path.resolve(root)
56
+ const resolvedCandidate = path.resolve(candidate)
57
+ return resolvedCandidate === resolvedRoot || resolvedCandidate.startsWith(`${resolvedRoot}${path.sep}`)
58
+ }
59
+
60
+ function proposalId(seed = crypto.randomBytes(16).toString('hex')) {
61
+ return `proposal-${crypto.createHash('sha256').update(seed).digest('hex').slice(0, 32)}`
62
+ }
63
+
64
+ export function actionIsCopyOnly(action) {
65
+ const value = String(action || '')
66
+ return Boolean(value) && !DIRECT_WRITE_ACTION_RE.test(value)
67
+ }
68
+
69
+ export function copyOnlyActionSummary(action) {
70
+ return {
71
+ action: String(action || ''),
72
+ copyOnly: actionIsCopyOnly(action),
73
+ directWrite: false,
74
+ applyEndpoint: null,
75
+ }
76
+ }
77
+
78
+ export function canTransitionProposal(from, to) {
79
+ if (from === to) return true
80
+ if (from === 'proposed') return to === 'reviewed' || to === 'rejected' || to === 'superseded'
81
+ if (from === 'reviewed') return to === 'accepted' || to === 'rejected' || to === 'superseded'
82
+ if (from === 'accepted') return to === 'superseded'
83
+ return false
84
+ }
85
+
86
+ export function acceptedProposalCopy(record) {
87
+ const id = record?.proposal?.id || 'unknown-proposal'
88
+ const targetPath = record?.proposal?.path || 'unknown target'
89
+ const action = record?.proposal?.action || 'copy-only proposal'
90
+ const diff = safeJsonText(record?.diff || record?.proposal?.diff || '')
91
+ return {
92
+ proposalId: id,
93
+ targetPath,
94
+ action,
95
+ directWrite: false,
96
+ applyEndpoint: null,
97
+ diff,
98
+ agentInstructions: [
99
+ `Accepted Atelier proposal ${id}.`,
100
+ 'Use normal repo editing in the operator checkout if you choose to apply this handoff.',
101
+ 'There is no browser apply endpoint and this record does not grant direct write authority.',
102
+ `Target: ${targetPath}`,
103
+ `Action: ${action}`,
104
+ ].join('\n'),
105
+ }
106
+ }
107
+
108
+ export function createProposalStore({
109
+ workspaceRoot = process.cwd(),
110
+ proposalsDir = path.join(workspaceRoot, '.atelier-proposals'),
111
+ workspaceId = null,
112
+ } = {}) {
113
+ const workspaceRootReal = fs.realpathSync(workspaceRoot)
114
+ ensurePrivateDir(proposalsDir)
115
+
116
+ function proposalPath(id) {
117
+ const clean = String(id || '').replace(/[^a-z0-9-]/gi, '')
118
+ if (!clean) throw new Error('proposal id is required')
119
+ const file = path.join(proposalsDir, `${clean}.json`)
120
+ const candidateDir = fs.realpathSync(proposalsDir)
121
+ if (!pathContainedBy(workspaceRootReal, candidateDir)) {
122
+ throw new Error('proposal directory escapes workspace')
123
+ }
124
+ return file
125
+ }
126
+
127
+ // Read is a lookup, not an assertion: an unusable id and an unreadable file
128
+ // are both "no such proposal", never a throw. Callers render 404 from null.
129
+ // listProposals() has always guarded its parse; this is the same contract.
130
+ function readProposal(id) {
131
+ let file
132
+ try {
133
+ file = proposalPath(id)
134
+ } catch {
135
+ return null
136
+ }
137
+ if (!fs.existsSync(file)) return null
138
+ try {
139
+ return JSON.parse(fs.readFileSync(file, 'utf8'))
140
+ } catch {
141
+ return null
142
+ }
143
+ }
144
+
145
+ function listProposals() {
146
+ if (!fs.existsSync(proposalsDir)) return []
147
+ return fs.readdirSync(proposalsDir)
148
+ .filter((name) => name.endsWith('.json'))
149
+ .sort(stableCompare)
150
+ .map((name) => {
151
+ try {
152
+ return JSON.parse(fs.readFileSync(path.join(proposalsDir, name), 'utf8'))
153
+ } catch {
154
+ return null
155
+ }
156
+ })
157
+ .filter(Boolean)
158
+ .sort((left, right) => stableCompare(right.proposal?.updatedAt || '', left.proposal?.updatedAt || ''))
159
+ }
160
+
161
+ function createProposal(body = {}) {
162
+ const action = cleanIdentity(body.action || body.proposal?.action || 'copy.repoPath', 120)
163
+ if (!actionIsCopyOnly(action)) {
164
+ return {
165
+ ok: false,
166
+ status: 409,
167
+ error: 'direct-write proposal action refused',
168
+ }
169
+ }
170
+
171
+ const createdAt = nowIso()
172
+ const id = proposalId([
173
+ createdAt,
174
+ body.sessionId,
175
+ body.viewId,
176
+ body.path,
177
+ action,
178
+ safeJsonText(body.proposal || body.diff || ''),
179
+ ].join('\0'))
180
+ const record = {
181
+ schema: ATELIER_PROPOSAL_SCHEMA,
182
+ workspaceId,
183
+ proposal: {
184
+ id,
185
+ status: 'proposed',
186
+ createdAt,
187
+ updatedAt: createdAt,
188
+ sessionId: cleanIdentity(body.sessionId, 120),
189
+ viewId: cleanIdentity(body.viewId, 120),
190
+ path: cleanIdentity(body.path || body.rel, 500),
191
+ action,
192
+ intent: cleanIdentity(body.intent || body.proposal?.intent, 500),
193
+ reason: cleanIdentity(body.proposal?.reason || body.reason, 1000),
194
+ storage: {
195
+ kind: 'local',
196
+ ignored: true,
197
+ },
198
+ authority: copyOnlyActionSummary(action),
199
+ },
200
+ diff: safeJsonText(body.diff || body.proposal?.diff || ''),
201
+ payload: body.proposal && typeof body.proposal === 'object' ? body.proposal : {},
202
+ }
203
+ secureWriteJson(proposalPath(id), record)
204
+ return { ok: true, status: 200, record }
205
+ }
206
+
207
+ function reviewProposal(id, body = {}) {
208
+ const record = readProposal(id)
209
+ if (!record) {
210
+ return { ok: false, status: 404, error: 'proposal not found' }
211
+ }
212
+ if (body.proposalId && body.proposalId !== id) {
213
+ return { ok: false, status: 409, error: 'ambiguous proposal review refused' }
214
+ }
215
+
216
+ const nextStatus = cleanIdentity(body.status, 40)
217
+ if (!PROPOSAL_REVIEW_STATUSES.has(nextStatus)) {
218
+ return { ok: false, status: 400, error: 'unsupported proposal review status' }
219
+ }
220
+ if (body.expectedStatus && body.expectedStatus !== record.proposal.status) {
221
+ return { ok: false, status: 409, error: 'stale proposal review refused: status changed' }
222
+ }
223
+ if (body.expectedUpdatedAt && body.expectedUpdatedAt !== record.proposal.updatedAt) {
224
+ return { ok: false, status: 409, error: 'stale proposal review refused: timestamp changed' }
225
+ }
226
+ if (!canTransitionProposal(record.proposal.status, nextStatus)) {
227
+ return {
228
+ ok: false,
229
+ status: 409,
230
+ error: `invalid proposal review transition ${record.proposal.status} -> ${nextStatus}`,
231
+ }
232
+ }
233
+
234
+ const updatedAt = nowIso()
235
+ record.proposal = {
236
+ ...record.proposal,
237
+ status: nextStatus,
238
+ updatedAt,
239
+ review: {
240
+ reviewer: cleanIdentity(body.reviewer || 'unknown reviewer', 160),
241
+ notes: cleanIdentity(body.notes, 2000),
242
+ reviewedAt: updatedAt,
243
+ },
244
+ }
245
+ if (nextStatus === 'accepted') {
246
+ record.copyable = acceptedProposalCopy(record)
247
+ } else {
248
+ delete record.copyable
249
+ }
250
+ secureWriteJson(proposalPath(id), record)
251
+ return { ok: true, status: 200, record }
252
+ }
253
+
254
+ return {
255
+ proposalsDir,
256
+ proposalPath,
257
+ readProposal,
258
+ listProposals,
259
+ createProposal,
260
+ reviewProposal,
261
+ }
262
+ }
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import { runAnalysisAdapterCommand } from '../analysis/adapter.mjs'
3
+ try {
4
+ runAnalysisAdapterCommand()
5
+ } catch (error) {
6
+ console.error(error.message)
7
+ process.exit(1)
8
+ }