@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,722 @@
1
+ import Ajv2020 from 'ajv/dist/2020.js'
2
+ import addFormats from 'ajv-formats'
3
+
4
+ import { publicSourceVerdict } from '../projection/policy.mjs'
5
+
6
+ export const LOCAL_AUDIENCES = ['public', 'team', 'operator', 'staff', 'private', 'sensitive']
7
+ export const RUNTIME_VISIBILITIES = ['private', 'shared', 'platform', 'public']
8
+
9
+ export const OBJECT_CLASSES = [
10
+ 'offer',
11
+ 'public_surface',
12
+ 'surface',
13
+ 'sdui_block',
14
+ 'commitment_path',
15
+ 'space',
16
+ 'trackable',
17
+ 'consent_boundary',
18
+ 'staff_prep',
19
+ 'provider_egress',
20
+ ]
21
+
22
+ export const RUNTIME_OWNERS = [
23
+ 'identity',
24
+ 'catalog',
25
+ 'commitments',
26
+ 'events',
27
+ 'projection',
28
+ 'consent',
29
+ 'messaging',
30
+ 'providers',
31
+ 'audit',
32
+ ]
33
+
34
+ export const ALLOWED_RUNTIME_TARGETS = [
35
+ 'content.material',
36
+ 'core.artifact',
37
+ 'core.trackable',
38
+ 'scheduling.commitment',
39
+ 'space.space',
40
+ 'space.provision',
41
+ ]
42
+
43
+ const LOCAL_AUDIENCE_SET = new Set(LOCAL_AUDIENCES)
44
+ const RUNTIME_VISIBILITY_SET = new Set(RUNTIME_VISIBILITIES)
45
+ const LOCAL_RUNTIME_VISIBILITY_SET = new Set(
46
+ LOCAL_AUDIENCES.filter((audience) => !RUNTIME_VISIBILITY_SET.has(audience))
47
+ )
48
+ const OBJECT_CLASS_SET = new Set(OBJECT_CLASSES)
49
+ const RUNTIME_OWNER_SET = new Set(RUNTIME_OWNERS)
50
+ const ALLOWED_RUNTIME_TARGET_SET = new Set(ALLOWED_RUNTIME_TARGETS)
51
+
52
+ const OBJECT_COLLECTION_TARGETS = {
53
+ offers: 'content.material',
54
+ surfaces: 'content.material',
55
+ sdui: 'content.material',
56
+ commitmentPaths: 'scheduling.commitment',
57
+ spaces: 'space.space',
58
+ trackables: 'core.trackable',
59
+ consentBoundaries: 'core.artifact',
60
+ staffPrep: 'core.artifact',
61
+ providerEgress: 'core.artifact',
62
+ }
63
+
64
+ const IMPORT_OBJECT_CLASS_TARGETS = {
65
+ offer: 'content.material',
66
+ public_surface: 'content.material',
67
+ surface: 'content.material',
68
+ sdui_block: 'content.material',
69
+ commitment_path: 'scheduling.commitment',
70
+ space: 'space.space',
71
+ trackable: 'core.trackable',
72
+ consent_boundary: 'core.artifact',
73
+ staff_prep: 'core.artifact',
74
+ provider_egress: 'core.artifact',
75
+ }
76
+
77
+ const TARGET_FIELD_NAMES = new Set([
78
+ 'runtimeObject',
79
+ 'runtime_object',
80
+ 'targetObject',
81
+ 'target_object',
82
+ 'targetRuntimeObject',
83
+ 'target_runtime_object',
84
+ 'targetClass',
85
+ 'target_class',
86
+ ])
87
+
88
+ const MUTATION_FLAG_KEYS = new Set([
89
+ 'apply',
90
+ 'write',
91
+ 'databasewrite',
92
+ 'database_write',
93
+ 'dbwrite',
94
+ 'db_write',
95
+ 'runtimemutation',
96
+ 'runtime_mutation',
97
+ 'mutation',
98
+ 'mutate',
99
+ 'persist',
100
+ 'commit',
101
+ 'commitwrite',
102
+ 'commit_write',
103
+ ])
104
+
105
+ const TOP_LEVEL_MUTATION_SECTION_KEYS = new Set([
106
+ 'applyPlan',
107
+ 'databaseWrite',
108
+ 'database_write',
109
+ 'dbWrite',
110
+ 'db_write',
111
+ 'runtimeMutation',
112
+ 'runtime_mutation',
113
+ 'mutationPlan',
114
+ 'mutation_plan',
115
+ 'writePlan',
116
+ 'write_plan',
117
+ ])
118
+
119
+ const SOURCE_REF_FIELD_NAMES = new Set([
120
+ 'sourceRef',
121
+ 'sourceRefs',
122
+ 'sourceNodeId',
123
+ 'sourceNodeIds',
124
+ 'sourceKgId',
125
+ 'sourceKgIds',
126
+ 'evidenceRef',
127
+ 'evidenceRefs',
128
+ 'evidenceKgId',
129
+ 'evidenceKgIds',
130
+ 'kgId',
131
+ 'kgIds',
132
+ ])
133
+
134
+ function asArray(value) {
135
+ return Array.isArray(value) ? value : []
136
+ }
137
+
138
+ function stableCompare(left, right) {
139
+ return String(left).localeCompare(String(right), 'en')
140
+ }
141
+
142
+ function uniqueSorted(values) {
143
+ return [...new Set(values)].sort(stableCompare)
144
+ }
145
+
146
+ function objectId(object, fallback) {
147
+ if (typeof object?.id === 'string' && object.id.length > 0) return object.id
148
+ if (typeof object?.providerClass === 'string' && object.providerClass.length > 0) {
149
+ return `provider-egress:${object.providerClass}`
150
+ }
151
+ return fallback
152
+ }
153
+
154
+ function pointerEscape(value) {
155
+ return String(value).replaceAll('~', '~0').replaceAll('/', '~1')
156
+ }
157
+
158
+ function formatLocation(parts) {
159
+ return `/${parts.map(pointerEscape).join('/')}`
160
+ }
161
+
162
+ function ajvForSchema() {
163
+ const ajv = new Ajv2020({
164
+ allErrors: true,
165
+ strict: false,
166
+ })
167
+ addFormats(ajv)
168
+ return ajv
169
+ }
170
+
171
+ function formatAjvError(error) {
172
+ const location = error.instancePath || '/'
173
+ if (error.keyword === 'additionalProperties') {
174
+ return `${location} must not include additional property ${error.params?.additionalProperty ?? '(unknown)'}`
175
+ }
176
+ if (error.keyword === 'required') {
177
+ return `${location} must include required property ${error.params?.missingProperty ?? '(unknown)'}`
178
+ }
179
+ if (error.keyword === 'enum') {
180
+ return `${location} must be one of ${asArray(error.params?.allowedValues ?? error.schema).join(', ')}`
181
+ }
182
+ if (error.keyword === 'const') {
183
+ return `${location} must be ${JSON.stringify(error.params?.allowedValue ?? error.schema)}`
184
+ }
185
+ if (error.keyword === 'pattern') {
186
+ return `${location} must match pattern ${JSON.stringify(error.params?.pattern ?? error.schema)}`
187
+ }
188
+ if (error.keyword === 'minItems') {
189
+ return `${location} must contain at least ${error.params?.limit ?? error.schema} item(s)`
190
+ }
191
+ return `${location} ${error.message ?? 'failed schema validation'}`
192
+ }
193
+
194
+ export function validateJsonSchema(schema, doc) {
195
+ const validate = ajvForSchema().compile(schema)
196
+ if (validate(doc)) return []
197
+ return asArray(validate.errors).map(formatAjvError)
198
+ }
199
+
200
+ export function validateSchemaShape(schema) {
201
+ const errors = []
202
+ const sourceSha = schema?.properties?.sourceCommit?.properties?.sha
203
+ const pricing = schema?.$defs?.offer?.properties?.pricing
204
+ const band = pricing?.properties?.bands?.items
205
+ const objectClassEnum = schema?.$defs?.objectClass?.enum
206
+ const ownerEnum = schema?.$defs?.runtimeOwnerName?.enum
207
+ const visibilityEnum = schema?.$defs?.visibility?.enum
208
+ const dirtyRule = asArray(schema?.allOf).find((rule) => rule?.if?.properties?.dirtyTree?.const === true)
209
+
210
+ if (sourceSha?.pattern !== '^[0-9a-f]{40}$') {
211
+ errors.push('schema sourceCommit.sha must require a 40-character architecture repo SHA')
212
+ }
213
+ if (pricing?.properties?.currency?.pattern !== '^[A-Z]{3}$') {
214
+ errors.push('schema pricing currency must require ISO-4217 shape')
215
+ }
216
+ if (!band?.required?.includes('amountMinor')) {
217
+ errors.push('schema pricing band must require amountMinor')
218
+ }
219
+ if (band?.properties?.amount) {
220
+ errors.push('schema pricing band must not expose legacy amount')
221
+ }
222
+ if (band?.properties?.amountMinor?.type !== 'integer') {
223
+ errors.push('schema pricing band amountMinor must be integer')
224
+ }
225
+ for (const value of OBJECT_CLASSES) {
226
+ if (!objectClassEnum?.includes(value)) errors.push(`schema objectClass enum missing ${value}`)
227
+ }
228
+ for (const value of asArray(objectClassEnum)) {
229
+ if (!OBJECT_CLASS_SET.has(value)) errors.push(`schema objectClass enum must not include unknown member ${value}`)
230
+ }
231
+ for (const value of RUNTIME_OWNERS) {
232
+ if (!ownerEnum?.includes(value)) errors.push(`schema runtimeOwnerName enum missing ${value}`)
233
+ }
234
+ for (const value of asArray(ownerEnum)) {
235
+ if (!RUNTIME_OWNER_SET.has(value)) errors.push(`schema runtimeOwnerName enum must not include unknown member ${value}`)
236
+ }
237
+ for (const value of RUNTIME_VISIBILITIES) {
238
+ if (!visibilityEnum?.includes(value)) errors.push(`schema runtime visibility enum missing ${value}`)
239
+ }
240
+ for (const value of LOCAL_AUDIENCES.filter((audience) => !RUNTIME_VISIBILITY_SET.has(audience))) {
241
+ if (visibilityEnum?.includes(value)) errors.push(`schema runtime visibility enum must not include local audience ${value}`)
242
+ }
243
+ if (dirtyRule?.then?.properties?.forced?.const !== true || !asArray(dirtyRule?.then?.required).includes('forced')) {
244
+ errors.push('schema must force dirtyTree exports to set forced true')
245
+ }
246
+ if (dirtyRule?.then?.properties?.visibilityGate?.properties?.taint?.minItems !== 1) {
247
+ errors.push('schema must force dirtyTree exports to carry non-empty taint')
248
+ }
249
+ const sourceNode = schema?.$defs?.sourceNode
250
+ if (!sourceNode?.required?.includes('audience')) {
251
+ errors.push('schema sourceNode must require audience')
252
+ }
253
+ if (sourceNode?.required?.includes('visibility') || Object.hasOwn(sourceNode?.properties ?? {}, 'visibility')) {
254
+ errors.push('schema sourceNode must not expose visibility')
255
+ }
256
+ if (!Object.hasOwn(sourceNode?.properties ?? {}, 'audience')) {
257
+ errors.push('schema sourceNode must expose audience')
258
+ }
259
+ return errors
260
+ }
261
+
262
+ function summarizeObjects(doc) {
263
+ const byCollection = {}
264
+ let total = 0
265
+ for (const collection of Object.keys(doc?.objects ?? {}).sort(stableCompare)) {
266
+ const rows = asArray(doc.objects[collection])
267
+ const ids = rows.map((object, index) => objectId(object, `${collection}[${index}]`)).sort(stableCompare)
268
+ byCollection[collection] = { count: rows.length, ids }
269
+ total += rows.length
270
+ }
271
+ return { total, byCollection }
272
+ }
273
+
274
+ function summarizeSourceNodes(sourceNodesById) {
275
+ return [...sourceNodesById.values()]
276
+ .map((sourceNode) => ({
277
+ kgId: sourceNode.kgId,
278
+ audience: sourceNode.audience ?? null,
279
+ publicProjectable: sourceNode?.audience === 'public',
280
+ }))
281
+ .sort((left, right) => stableCompare(left.kgId, right.kgId))
282
+ }
283
+
284
+ function deriveGraphProvenanceMode(doc, graphNodes) {
285
+ if (Array.isArray(graphNodes) && graphNodes.length > 0) return 'verified'
286
+ if (asArray(doc?.provenance?.sourceNodes).length > 0) return 'self-attested'
287
+ return 'absent'
288
+ }
289
+
290
+ function addViolation(violations, pathValue, message, value = undefined) {
291
+ const violation = { path: pathValue, message }
292
+ if (value !== undefined) violation.value = value
293
+ violations.push(violation)
294
+ }
295
+
296
+ function collectSourceNodes(doc, errors) {
297
+ const sourceNodesById = new Map()
298
+ for (const [index, sourceNode] of asArray(doc?.provenance?.sourceNodes).entries()) {
299
+ const location = `/provenance/sourceNodes/${index}`
300
+ if (sourceNode == null || typeof sourceNode !== 'object' || Array.isArray(sourceNode)) {
301
+ errors.push(`${location} must be an object`)
302
+ continue
303
+ }
304
+ if (Object.hasOwn(sourceNode, 'visibility')) {
305
+ errors.push(`${location} must use audience, not visibility`)
306
+ }
307
+ if (typeof sourceNode.kgId !== 'string' || sourceNode.kgId.length === 0) {
308
+ errors.push(`${location}/kgId must be a non-empty string`)
309
+ continue
310
+ }
311
+ if (sourceNodesById.has(sourceNode.kgId)) {
312
+ errors.push(`${location}/kgId duplicates provenance source node ${sourceNode.kgId}`)
313
+ }
314
+ if (!LOCAL_AUDIENCE_SET.has(sourceNode.audience)) {
315
+ errors.push(`${location}/audience must be one of ${LOCAL_AUDIENCES.join(', ')}`)
316
+ }
317
+ sourceNodesById.set(sourceNode.kgId, sourceNode)
318
+ }
319
+ return sourceNodesById
320
+ }
321
+
322
+ function collectExportObjects(doc, errors) {
323
+ const exportObjectsById = new Map()
324
+ for (const [collection, rows] of Object.entries(doc?.objects ?? {})) {
325
+ for (const [index, object] of asArray(rows).entries()) {
326
+ const id = typeof object?.id === 'string' ? object.id : null
327
+ if (!id) continue
328
+ if (exportObjectsById.has(id)) {
329
+ errors.push(`/objects/${collection}/${index}/id duplicates export object ${id}`)
330
+ }
331
+ exportObjectsById.set(id, {
332
+ collection,
333
+ index,
334
+ object,
335
+ path: `/objects/${collection}/${index}`,
336
+ })
337
+ }
338
+ }
339
+ return exportObjectsById
340
+ }
341
+
342
+ function resolveSourceRef(ref, context, stack = []) {
343
+ const { sourceNodesById, exportObjectsById, errors, ownerPath } = context
344
+ if (sourceNodesById.has(ref)) return [sourceNodesById.get(ref)]
345
+
346
+ const exportObject = exportObjectsById.get(ref)
347
+ if (!exportObject) {
348
+ errors.push(`${ownerPath} sourceRef ${ref} does not resolve to provenance.sourceNodes or an export object`)
349
+ return []
350
+ }
351
+
352
+ if (stack.includes(ref)) {
353
+ errors.push(`${ownerPath} sourceRef ${ref} creates a sourceRef cycle`)
354
+ return []
355
+ }
356
+
357
+ const nestedRefs = asArray(exportObject.object?.sourceRefs)
358
+ if (nestedRefs.length === 0) {
359
+ errors.push(`${ownerPath} sourceRef ${ref} resolves to export object ${exportObject.path} without provenance sourceRefs`)
360
+ return []
361
+ }
362
+
363
+ return nestedRefs.flatMap((nestedRef) => {
364
+ if (typeof nestedRef !== 'string' || nestedRef.length === 0) {
365
+ errors.push(`${exportObject.path}/sourceRefs must contain only non-empty strings`)
366
+ return []
367
+ }
368
+ return resolveSourceRef(nestedRef, { ...context, ownerPath: `${exportObject.path}/sourceRefs` }, [...stack, ref])
369
+ })
370
+ }
371
+
372
+ function collectSourceRefFields(value, pathParts = [], refs = []) {
373
+ if (value == null || typeof value !== 'object') return refs
374
+ if (Array.isArray(value)) {
375
+ for (const [index, child] of value.entries()) collectSourceRefFields(child, [...pathParts, index], refs)
376
+ return refs
377
+ }
378
+
379
+ for (const [key, child] of Object.entries(value)) {
380
+ const nextPath = [...pathParts, key]
381
+ if (SOURCE_REF_FIELD_NAMES.has(key)) {
382
+ const values = Array.isArray(child) ? child : [child]
383
+ for (const [index, ref] of values.entries()) {
384
+ const refPath = Array.isArray(child) ? [...nextPath, index] : nextPath
385
+ refs.push({ ref, path: formatLocation(refPath) })
386
+ }
387
+ continue
388
+ }
389
+ collectSourceRefFields(child, nextPath, refs)
390
+ }
391
+ return refs
392
+ }
393
+
394
+ function validateSourceRefs(doc, sourceNodesById, exportObjectsById, errors) {
395
+ const publicResolvedKgIds = new Set()
396
+ for (const [collection, rows] of Object.entries(doc?.objects ?? {})) {
397
+ for (const [index, object] of asArray(rows).entries()) {
398
+ if (object == null || typeof object !== 'object' || Array.isArray(object)) continue
399
+ const ownerPath = `/objects/${collection}/${index}`
400
+ const refs = collectSourceRefFields(object, ['objects', collection, index])
401
+ const seen = new Set()
402
+ const resolvedSourceNodes = []
403
+
404
+ for (const { ref, path } of refs) {
405
+ if (seen.has(`${path}:${ref}`)) continue
406
+ seen.add(`${path}:${ref}`)
407
+ if (typeof ref !== 'string' || ref.length === 0) {
408
+ errors.push(`${path} must be a non-empty source reference string`)
409
+ continue
410
+ }
411
+ resolvedSourceNodes.push(
412
+ ...resolveSourceRef(ref, {
413
+ sourceNodesById,
414
+ exportObjectsById,
415
+ errors,
416
+ ownerPath: path,
417
+ })
418
+ )
419
+ }
420
+
421
+ if (object.visibility === 'public') {
422
+ for (const sourceNode of resolvedSourceNodes) {
423
+ publicResolvedKgIds.add(sourceNode.kgId)
424
+ if (sourceNode.audience !== 'public') {
425
+ errors.push(
426
+ `${ownerPath} is public but source node ${sourceNode.kgId} audience ${sourceNode.audience ?? '(missing)'} is not public-projectable`
427
+ )
428
+ }
429
+ }
430
+ }
431
+ }
432
+ }
433
+ return [...publicResolvedKgIds].sort(stableCompare)
434
+ }
435
+
436
+ function validateGraphSourceRefs(nodes, publicSourceRefs, errors) {
437
+ if (!Array.isArray(nodes) || nodes.length === 0) return
438
+ const verdict = publicSourceVerdict(nodes, publicSourceRefs)
439
+ if (!verdict.ok) {
440
+ for (const failure of verdict.failures) {
441
+ errors.push(`public sourceRef ${failure.kgId} is not public-safe: ${failure.reason}`)
442
+ }
443
+ }
444
+ }
445
+
446
+ function validateGraphProvenance(doc, nodes, errors) {
447
+ if (!Array.isArray(nodes) || nodes.length === 0) return
448
+ const graphById = new Map((nodes ?? []).map((node) => [node.id, node]))
449
+ for (const [index, sourceNode] of asArray(doc?.provenance?.sourceNodes).entries()) {
450
+ const kgId = sourceNode?.kgId
451
+ const graphNode = graphById.get(kgId)
452
+ if (!graphNode) {
453
+ errors.push(`provenance sourceNode ${index} kgId ${kgId ?? '(missing)'} was not found in the source provenance graph`)
454
+ continue
455
+ }
456
+ if (sourceNode?.audience && sourceNode.audience !== graphNode.audience) {
457
+ errors.push(`provenance sourceNode ${index} audience ${sourceNode.audience} does not match graph audience ${graphNode.audience}`)
458
+ }
459
+ }
460
+ }
461
+
462
+ function validateMutationIntent(doc, errors, { dryRunOnly }) {
463
+ if (dryRunOnly && doc?.importPlan?.mode === 'apply') {
464
+ errors.push('/importPlan/mode must be dry-run for atelier-export@v1 dry-run validation')
465
+ }
466
+ if (dryRunOnly && doc?.importPlan?.mode != null && doc.importPlan.mode !== 'dry-run') {
467
+ errors.push('/importPlan/mode must be dry-run')
468
+ }
469
+
470
+ const topLevel = doc && typeof doc === 'object' && !Array.isArray(doc) ? doc : {}
471
+ for (const key of Object.keys(topLevel)) {
472
+ if (!TOP_LEVEL_MUTATION_SECTION_KEYS.has(key)) continue
473
+ errors.push(`/${key} declares mutation/write intent and is not allowed in dry-run validation`)
474
+ }
475
+
476
+ const sections = [
477
+ { path: [], value: doc },
478
+ { path: ['importPlan'], value: doc?.importPlan },
479
+ { path: ['import'], value: doc?.import },
480
+ { path: ['importer'], value: doc?.importer },
481
+ { path: ['runtimeImport'], value: doc?.runtimeImport },
482
+ { path: ['execution'], value: doc?.execution },
483
+ ]
484
+
485
+ for (const section of sections) scanMutationFlags(section.value, section.path, errors)
486
+ }
487
+
488
+ function scanMutationFlags(value, pathParts, errors) {
489
+ if (value == null || typeof value !== 'object') return
490
+ if (Array.isArray(value)) {
491
+ for (const [index, item] of value.entries()) scanMutationFlags(item, [...pathParts, index], errors)
492
+ return
493
+ }
494
+
495
+ for (const [key, child] of Object.entries(value)) {
496
+ const normalizedKey = key.toLowerCase()
497
+ const location = formatLocation([...pathParts, key])
498
+ if (MUTATION_FLAG_KEYS.has(normalizedKey) && child !== false && child !== null && child !== 'dry-run') {
499
+ errors.push(`${location} declares mutation/write intent and is not allowed in dry-run validation`)
500
+ }
501
+ if (key === 'dryRun' && child === false) {
502
+ errors.push(`${location} must not be false for dry-run validation`)
503
+ }
504
+ if (key === 'mode' && typeof child === 'string' && ['apply', 'write', 'mutate'].includes(child)) {
505
+ errors.push(`${location} declares ${child} intent and is not allowed in dry-run validation`)
506
+ }
507
+ scanMutationFlags(child, [...pathParts, key], errors)
508
+ }
509
+ }
510
+
511
+ function runtimeTargetForImportClass(objectClass) {
512
+ if (ALLOWED_RUNTIME_TARGET_SET.has(objectClass)) return objectClass
513
+ return IMPORT_OBJECT_CLASS_TARGETS[objectClass] ?? null
514
+ }
515
+
516
+ function validateImportPlanTargets(doc, runtimeTargets, semanticProfileViolations) {
517
+ for (const [index, owner] of asArray(doc?.importPlan?.runtimeOwners).entries()) {
518
+ const sourceObjectClass = owner?.objectClass
519
+ const targetObject = runtimeTargetForImportClass(sourceObjectClass)
520
+ const ownerPath = `/importPlan/runtimeOwners/${index}/objectClass`
521
+ if (!targetObject) {
522
+ addViolation(
523
+ semanticProfileViolations,
524
+ ownerPath,
525
+ `unsupported target runtime object/class ${sourceObjectClass ?? '(missing)'}`,
526
+ sourceObjectClass ?? null
527
+ )
528
+ continue
529
+ }
530
+ runtimeTargets.push({
531
+ path: `/importPlan/runtimeOwners/${index}`,
532
+ sourceObjectClass,
533
+ targetObject,
534
+ owner: owner?.owner ?? null,
535
+ })
536
+ }
537
+
538
+ for (const [index, operation] of asArray(doc?.importPlan?.operations).entries()) {
539
+ const sourceObjectClass = operation?.objectClass
540
+ const targetObject = runtimeTargetForImportClass(sourceObjectClass)
541
+ const operationPath = `/importPlan/operations/${index}/objectClass`
542
+ if (!targetObject) {
543
+ addViolation(
544
+ semanticProfileViolations,
545
+ operationPath,
546
+ `unsupported target runtime object/class ${sourceObjectClass ?? '(missing)'}`,
547
+ sourceObjectClass ?? null
548
+ )
549
+ continue
550
+ }
551
+ runtimeTargets.push({
552
+ path: `/importPlan/operations/${index}`,
553
+ externalId: operation?.externalId ?? null,
554
+ sourceObjectClass,
555
+ targetObject,
556
+ op: operation?.op ?? null,
557
+ status: operation?.status ?? null,
558
+ })
559
+ }
560
+ }
561
+
562
+ function validateDeclaredRuntimeTargetFields(value, pathParts, semanticProfileViolations) {
563
+ if (value == null || typeof value !== 'object') return
564
+ if (Array.isArray(value)) {
565
+ for (const [index, item] of value.entries()) {
566
+ validateDeclaredRuntimeTargetFields(item, [...pathParts, index], semanticProfileViolations)
567
+ }
568
+ return
569
+ }
570
+
571
+ for (const [key, child] of Object.entries(value)) {
572
+ const location = formatLocation([...pathParts, key])
573
+ if (TARGET_FIELD_NAMES.has(key) && typeof child === 'string' && !ALLOWED_RUNTIME_TARGET_SET.has(child)) {
574
+ addViolation(semanticProfileViolations, location, `unsupported target runtime object/class ${child}`, child)
575
+ }
576
+ validateDeclaredRuntimeTargetFields(child, [...pathParts, key], semanticProfileViolations)
577
+ }
578
+ }
579
+
580
+ function deriveObjectRuntimeTargets(doc, runtimeTargets, semanticProfileViolations, errors) {
581
+ for (const [collection, targetObject] of Object.entries(OBJECT_COLLECTION_TARGETS)) {
582
+ for (const [index, object] of asArray(doc?.objects?.[collection]).entries()) {
583
+ const objectPath = `/objects/${collection}/${index}`
584
+ const id = objectId(object, `${collection}[${index}]`)
585
+ const target = {
586
+ path: objectPath,
587
+ id,
588
+ collection,
589
+ targetObject,
590
+ }
591
+
592
+ if (Object.hasOwn(object ?? {}, 'visibility')) {
593
+ if (!RUNTIME_VISIBILITY_SET.has(object.visibility)) {
594
+ addViolation(
595
+ semanticProfileViolations,
596
+ `${objectPath}/visibility`,
597
+ `runtime visibility must be one of ${RUNTIME_VISIBILITIES.join(', ')}`,
598
+ object.visibility ?? null
599
+ )
600
+ if (LOCAL_RUNTIME_VISIBILITY_SET.has(object.visibility)) {
601
+ errors.push(
602
+ `${objectPath}/visibility uses local audience ${object.visibility}; runtime/export visibility must be one of ${RUNTIME_VISIBILITIES.join(', ')}`
603
+ )
604
+ }
605
+ } else {
606
+ target.visibility = object.visibility
607
+ }
608
+ }
609
+
610
+ runtimeTargets.push(target)
611
+ }
612
+ }
613
+ }
614
+
615
+ function summarizeImportOperations(doc) {
616
+ const counts = { ready: 0, warning: 0, blocked: 0, other: 0 }
617
+ for (const operation of asArray(doc?.importPlan?.operations)) {
618
+ if (Object.hasOwn(counts, operation?.status)) counts[operation.status] += 1
619
+ else counts.other += 1
620
+ }
621
+ let worstOperationStatus = 'ready'
622
+ if (counts.other > 0) worstOperationStatus = 'other'
623
+ if (counts.warning > 0) worstOperationStatus = 'warning'
624
+ if (counts.blocked > 0) worstOperationStatus = 'blocked'
625
+ return {
626
+ counts,
627
+ worstOperationStatus,
628
+ total: asArray(doc?.importPlan?.operations).length,
629
+ }
630
+ }
631
+
632
+ function hasPublicRuntimeVisibility(runtimeTargets) {
633
+ return runtimeTargets.some((target) => target.visibility === 'public')
634
+ }
635
+
636
+ function reportBase({
637
+ accepted,
638
+ importable,
639
+ graphProvenanceMode,
640
+ errors,
641
+ warnings,
642
+ objects,
643
+ sourceNodes,
644
+ runtimeTargets,
645
+ semanticProfileViolations,
646
+ operationSummary,
647
+ }) {
648
+ return {
649
+ accepted,
650
+ importable,
651
+ worstOperationStatus: operationSummary.worstOperationStatus,
652
+ graphProvenanceMode,
653
+ operationSummary,
654
+ errors: uniqueSorted(errors),
655
+ warnings: uniqueSorted(warnings),
656
+ objects,
657
+ sourceNodes,
658
+ runtimeTargets,
659
+ semanticProfileViolations: [...semanticProfileViolations].sort((left, right) => {
660
+ return stableCompare(`${left.path}:${left.message}`, `${right.path}:${right.message}`)
661
+ }),
662
+ }
663
+ }
664
+
665
+ export function validateAtelierExportContract(doc, { schema, graphNodes = [], dryRunOnly = false } = {}) {
666
+ const errors = []
667
+ const warnings = []
668
+ const semanticProfileViolations = []
669
+ const runtimeTargets = []
670
+
671
+ if (schema) errors.push(...validateJsonSchema(schema, doc).map((error) => `schema ${error}`))
672
+
673
+ const sourceNodesById = collectSourceNodes(doc, errors)
674
+ const exportObjectsById = collectExportObjects(doc, errors)
675
+ validateGraphProvenance(doc, graphNodes, errors)
676
+ const publicSourceRefs = validateSourceRefs(doc, sourceNodesById, exportObjectsById, errors)
677
+ validateGraphSourceRefs(graphNodes, publicSourceRefs, errors)
678
+ validateMutationIntent(doc, errors, { dryRunOnly })
679
+ deriveObjectRuntimeTargets(doc, runtimeTargets, semanticProfileViolations, errors)
680
+ validateImportPlanTargets(doc, runtimeTargets, semanticProfileViolations)
681
+ validateDeclaredRuntimeTargetFields(doc, [], semanticProfileViolations)
682
+
683
+ for (const [index, owner] of asArray(doc?.importPlan?.runtimeOwners).entries()) {
684
+ if (!OBJECT_CLASS_SET.has(owner?.objectClass)) {
685
+ errors.push(`runtime owner ${index} invalid runtime owner objectClass ${owner?.objectClass ?? '(missing)'}`)
686
+ }
687
+ if (!RUNTIME_OWNER_SET.has(owner?.owner)) {
688
+ errors.push(`runtime owner ${index} invalid runtime owner ${owner?.owner ?? '(missing)'}`)
689
+ }
690
+ }
691
+
692
+ for (const [index, operation] of asArray(doc?.importPlan?.operations).entries()) {
693
+ if (!OBJECT_CLASS_SET.has(operation?.objectClass)) {
694
+ errors.push(`operation ${index} invalid operation objectClass ${operation?.objectClass ?? '(missing)'}`)
695
+ }
696
+ }
697
+
698
+ runtimeTargets.sort((left, right) => stableCompare(left.path, right.path))
699
+ const operationSummary = summarizeImportOperations(doc)
700
+ const graphProvenanceMode = deriveGraphProvenanceMode(doc, graphNodes)
701
+ const accepted = errors.length === 0 && semanticProfileViolations.length === 0
702
+ const graphReadyForPublicImport =
703
+ graphProvenanceMode === 'verified' || !hasPublicRuntimeVisibility(runtimeTargets)
704
+ const importable =
705
+ accepted &&
706
+ operationSummary.worstOperationStatus === 'ready' &&
707
+ asArray(doc?.blockers).length === 0 &&
708
+ graphReadyForPublicImport
709
+
710
+ return reportBase({
711
+ accepted,
712
+ importable,
713
+ graphProvenanceMode,
714
+ errors,
715
+ warnings,
716
+ objects: summarizeObjects(doc),
717
+ sourceNodes: summarizeSourceNodes(sourceNodesById),
718
+ runtimeTargets,
719
+ semanticProfileViolations,
720
+ operationSummary,
721
+ })
722
+ }