@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.
- package/CHANGELOG.md +383 -0
- package/LICENSE +202 -0
- package/NOTICE +5 -0
- package/README.md +262 -0
- package/SECURITY.md +93 -0
- package/TRADEMARKS.md +88 -0
- package/announcements/2026-08-06-announcements-channel.v1.json +12 -0
- package/announcements/keys/mnstry-announcements.public.v1.json +9 -0
- package/bin/atelier.mjs +2 -0
- package/bin/mnstry-atelier.mjs +3 -0
- package/contracts/analysis-adapter.v1.schema.json +248 -0
- package/contracts/atelier-action-intent.v1.schema.json +143 -0
- package/contracts/atelier-analysis-adapter.v1.schema.json +360 -0
- package/contracts/atelier-attestation.v1.schema.json +239 -0
- package/contracts/atelier-boundary-policy.v1.schema.json +339 -0
- package/contracts/atelier-claim.v1.schema.json +97 -0
- package/contracts/atelier-export.v1.schema.json +1015 -0
- package/contracts/atelier-extension-pack.v1.schema.json +290 -0
- package/contracts/atelier-kit-manifest.v1.schema.json +273 -0
- package/contracts/atelier-lock.v1.schema.json +364 -0
- package/contracts/atelier-migration.v1.schema.json +162 -0
- package/contracts/atelier-project-config.v1.schema.json +352 -0
- package/contracts/atelier-readiness-protocol.v1.schema.json +517 -0
- package/contracts/atelier-readiness-run.v1.schema.json +229 -0
- package/contracts/atelier-readiness.v1.schema.json +457 -0
- package/contracts/compat-baseline.json +10 -0
- package/contracts/git-promote-event.v1.schema.json +145 -0
- package/contracts/knowledge-source-sidecar.v1.schema.json +157 -0
- package/contracts/mnstry-atelier-semantic-profile.v1.json +254 -0
- package/docs/announcements.md +101 -0
- package/docs/atelier-runtime.md +34 -0
- package/docs/attestation.md +201 -0
- package/docs/continuity.md +55 -0
- package/docs/contract-stability.md +111 -0
- package/docs/distributions.md +220 -0
- package/docs/install.md +140 -0
- package/docs/knowledge-graph.md +140 -0
- package/docs/ontology.md +206 -0
- package/docs/release-engineering.md +151 -0
- package/docs/repo-boundary-guard.md +199 -0
- package/docs/tenant-readiness.md +64 -0
- package/docs/upgrade.md +124 -0
- package/fixtures/analysis/invalid/native-graph.v1.json +4 -0
- package/fixtures/analysis/invalid/promoted-claim.v1.json +17 -0
- package/fixtures/analysis/invalid/runtime-import.v1.json +20 -0
- package/fixtures/analysis/valid/output.claims.v1.json +17 -0
- package/fixtures/analysis-adapter/invalid/manifest-hidden-provider.v1.json +24 -0
- package/fixtures/analysis-adapter/invalid/output-frontmatter-mutation.v1.json +21 -0
- package/fixtures/analysis-adapter/invalid/output-native-graph.v1.json +14 -0
- package/fixtures/analysis-adapter/manifest.disabled.v1.json +22 -0
- package/fixtures/analysis-adapter/manifest.enabled-local.v1.json +23 -0
- package/fixtures/analysis-adapter/output.claims.v1.json +21 -0
- package/fixtures/atelier-action-intent/invalid/runtime-mutation-authority.v1.json +16 -0
- package/fixtures/atelier-action-intent/invalid/unknown-top-level-field.v1.json +17 -0
- package/fixtures/atelier-action-intent/invalid/writes-beyond-clipboard.v1.json +16 -0
- package/fixtures/atelier-action-intent/valid/copy-agent-prompt.v1.json +16 -0
- package/fixtures/atelier-action-intent/valid/copy-repo-path.v1.json +22 -0
- package/fixtures/atelier-analysis-adapter/invalid/hidden-model-provider.v1.json +24 -0
- package/fixtures/atelier-analysis-adapter/invalid/inert-with-execute-command.v1.json +24 -0
- package/fixtures/atelier-analysis-adapter/invalid/proposals-with-runtime-import.v1.json +30 -0
- package/fixtures/atelier-analysis-adapter/valid/manifest-inert.v1.json +24 -0
- package/fixtures/atelier-analysis-adapter/valid/proposals-claim-only.v1.json +27 -0
- package/fixtures/atelier-attestation/invalid/bad-digest.v1.json +23 -0
- package/fixtures/atelier-attestation/invalid/conformance-scope.v1.json +23 -0
- package/fixtures/atelier-attestation/invalid/missing-signature.v1.json +22 -0
- package/fixtures/atelier-attestation/invalid/unknown-root-field.v1.json +24 -0
- package/fixtures/atelier-attestation/keys/roundtrip-issuer.public.v1.json +9 -0
- package/fixtures/atelier-attestation/valid/rejected-with-reasons.v1.json +49 -0
- package/fixtures/atelier-attestation/valid/signed-admitted.v1.json +35 -0
- package/fixtures/atelier-attestation/valid/signed-roundtrip.v1.json +29 -0
- package/fixtures/atelier-attestation/valid/unsigned-advisory.v1.json +23 -0
- package/fixtures/atelier-claim/invalid/unknown-provider.v1.json +15 -0
- package/fixtures/atelier-claim/invalid/unknown-top-level-field.v1.json +16 -0
- package/fixtures/atelier-claim/invalid/wrong-schema-const.v1.json +15 -0
- package/fixtures/atelier-claim/valid/adapter-style-proposed-relation.v1.json +19 -0
- package/fixtures/atelier-claim/valid/codex-proposed-relation.v1.json +16 -0
- package/fixtures/atelier-claim/valid/manual-proposed-relation.v1.json +16 -0
- package/fixtures/atelier-claim/valid/readiness-proposed-relation.v1.json +18 -0
- package/fixtures/atelier-export/invalid/dirty-unforced-empty-taint.v1.json +19 -0
- package/fixtures/atelier-export/invalid/object-local-audience-visibility.v1.json +11 -0
- package/fixtures/atelier-export/invalid/open-object-class-owner.v1.json +23 -0
- package/fixtures/atelier-export/invalid/pricing-legacy-amount.v1.json +23 -0
- package/fixtures/atelier-export/invalid/public-disguised-internal-source-ref.v1.json +11 -0
- package/fixtures/atelier-export/invalid/public-nested-private-source-ref.v1.json +17 -0
- package/fixtures/atelier-export/invalid/public-resolved-private-source-ref.v1.json +13 -0
- package/fixtures/atelier-export/invalid/public-resolved-sensitive-source-ref.v1.json +13 -0
- package/fixtures/atelier-export/invalid/public-source-ref-missing-provenance.v1.json +20 -0
- package/fixtures/atelier-export/invalid/public-source-team-kg.json +12 -0
- package/fixtures/atelier-export/invalid/source-node-legacy-visibility.v1.json +21 -0
- package/fixtures/atelier-export/invalid/source-node-unknown-field.v1.json +18 -0
- package/fixtures/atelier-export/invalid/unresolved-public-source-ref.v1.json +11 -0
- package/fixtures/atelier-export/sample-studio-offer.v1.json +397 -0
- package/fixtures/atelier-extension-pack/invalid/bundled-slug-collision.v1.json +22 -0
- package/fixtures/atelier-extension-pack/invalid/escapes-root.v1.json +22 -0
- package/fixtures/atelier-extension-pack/invalid/posture-tampered.v1.json +22 -0
- package/fixtures/atelier-extension-pack/invalid/protocols/autonomous-authority.v1.json +118 -0
- package/fixtures/atelier-extension-pack/invalid/protocols/offer-map.v1.json +109 -0
- package/fixtures/atelier-extension-pack/invalid/redefines-runtime-term.v1.json +19 -0
- package/fixtures/atelier-extension-pack/invalid/wrong-namespace.v1.json +19 -0
- package/fixtures/atelier-extension-pack/valid/protocols/contract-gate.v1.json +109 -0
- package/fixtures/atelier-extension-pack/valid/sample-pack.v1.json +48 -0
- package/fixtures/atelier-kit-manifest/invalid/runtime-copy-without-canonical-role.v1.json +53 -0
- package/fixtures/atelier-kit-manifest/invalid/telemetry-send-path.v1.json +21 -0
- package/fixtures/atelier-kit-manifest/valid/mnstry-atelier.valid.v1.json +76 -0
- package/fixtures/atelier-lock/invalid/absolute-contract-path.v1.json +40 -0
- package/fixtures/atelier-lock/invalid/runtime-mutation-enabled.v1.json +41 -0
- package/fixtures/atelier-lock/invalid/unknown-top-level-field.v1.json +42 -0
- package/fixtures/atelier-lock/valid/extension-pack.v1.json +55 -0
- package/fixtures/atelier-lock/valid/minimal.v1.json +44 -0
- package/fixtures/atelier-migration/invalid/live-apply-mode.v1.json +21 -0
- package/fixtures/atelier-migration/invalid/missing-review-evidence.v1.json +20 -0
- package/fixtures/atelier-migration/invalid/runtime-mutation-enabled.v1.json +21 -0
- package/fixtures/atelier-migration/valid/minimal.v1.json +32 -0
- package/fixtures/atelier-project-config/invalid/absolute-repo-path.v1.json +11 -0
- package/fixtures/atelier-project-config/invalid/ext-not-object.v1.json +12 -0
- package/fixtures/atelier-project-config/invalid/external-kind-read-boundary.v1.json +12 -0
- package/fixtures/atelier-project-config/invalid/missing-repos-and-alignment.v1.json +7 -0
- package/fixtures/atelier-project-config/invalid/unknown-top-level-field.v1.json +14 -0
- package/fixtures/atelier-project-config/valid/alignment-scaffold.v1.json +15 -0
- package/fixtures/atelier-project-config/valid/ext-extension-packs.v1.json +36 -0
- package/fixtures/atelier-project-config/valid/repo-workspace.v1.json +23 -0
- package/fixtures/atelier-project-config/validator-only/ext-member-not-object.v1.json +14 -0
- package/fixtures/atelier-readiness/invalid/dry-run-only-false.v1.json +87 -0
- package/fixtures/atelier-readiness/invalid/support-bundle-send-path.v1.json +87 -0
- package/fixtures/atelier-readiness/invalid/unknown-top-level-field.v1.json +88 -0
- package/fixtures/atelier-readiness/valid/ready-workspace.v1.json +84 -0
- package/fixtures/boundary-policy/invalid/content-rule-exception-blanket-path.v1.json +67 -0
- package/fixtures/boundary-policy/invalid/invalid-actor-private-repo-mapping.v1.json +20 -0
- package/fixtures/boundary-policy/invalid/legacy-warning-missing-warning.v1.json +20 -0
- package/fixtures/boundary-policy/invalid/missing-repo-coverage.v1.json +19 -0
- package/fixtures/boundary-policy/invalid/unknown-key.v1.json +12 -0
- package/fixtures/boundary-policy/valid/content-rule-exception.v1.json +67 -0
- package/fixtures/boundary-policy/valid/legacy-warning.v1.json +33 -0
- package/fixtures/boundary-policy/valid/strict-private-shared.v1.json +40 -0
- package/fixtures/git-promote/invalid/provision-named-revocable.v1.json +29 -0
- package/fixtures/git-promote/private-to-team.v1.json +29 -0
- package/fixtures/knowledge-source-sidecar/invalid/legacy-visibility-field.v1.json +17 -0
- package/fixtures/knowledge-source-sidecar/invalid/missing-audience.v1.json +15 -0
- package/fixtures/knowledge-source-sidecar/invalid/unknown-relation-kind.v1.json +18 -0
- package/fixtures/knowledge-source-sidecar/valid/html-source.v1.json +18 -0
- package/fixtures/knowledge-source-sidecar/valid/pdf-evidence.v1.json +18 -0
- package/fixtures/projects/neutral-project/atelier.project.json +15 -0
- package/fixtures/projects/neutral-project/ops/config/repo-access.v1.json +12 -0
- package/fixtures/projects/neutral-project/project-knowledge.graph.json +69 -0
- package/fixtures/projects/sample-workspace/atelier.project.json +23 -0
- package/fixtures/projects/sample-workspace/boundary-policy.v1.json +59 -0
- package/fixtures/projects/sample-workspace/content/README.md +16 -0
- package/fixtures/projects/sample-workspace/content/source.html +5 -0
- package/fixtures/projects/sample-workspace/content/source.html.kg.json +15 -0
- package/fixtures/projects/sample-workspace/repo-access.v1.json +9 -0
- package/fixtures/projects/source-formats-workspace/atelier.project.json +23 -0
- package/fixtures/projects/source-formats-workspace/content/README.md +17 -0
- package/fixtures/projects/source-formats-workspace/content/data.json +7 -0
- package/fixtures/projects/source-formats-workspace/content/data.json.kg.json +15 -0
- package/fixtures/projects/source-formats-workspace/content/logo.png.kg.json +15 -0
- package/fixtures/projects/source-formats-workspace/content/metrics.csv +3 -0
- package/fixtures/projects/source-formats-workspace/content/metrics.csv.kg.json +15 -0
- package/fixtures/projects/source-formats-workspace/content/pipeline.yaml +4 -0
- package/fixtures/projects/source-formats-workspace/content/pipeline.yaml.kg.json +15 -0
- package/fixtures/projects/source-formats-workspace/repo-access.v1.json +9 -0
- package/fixtures/readiness-protocols/extension-pack/invalid/protocol-missing-path.v1.json +20 -0
- package/fixtures/readiness-protocols/extension-pack/valid/with-protocol.v1.json +24 -0
- package/fixtures/readiness-protocols/protocol/invalid/missing-check-target.v1.json +103 -0
- package/fixtures/readiness-protocols/protocol/invalid/runtime-mutation-enabled.v1.json +107 -0
- package/fixtures/readiness-protocols/protocol/valid/minimal.v1.json +100 -0
- package/fixtures/readiness-protocols/run/invalid/missing-evidence.v1.json +37 -0
- package/fixtures/readiness-protocols/run/invalid/pass-with-blocker.v1.json +22 -0
- package/fixtures/readiness-protocols/run/invalid/runtime-mutation-enabled.v1.json +22 -0
- package/fixtures/readiness-protocols/run/valid/pass.v1.json +44 -0
- package/fixtures/support/invalid/leaky-support.v1.json +13 -0
- package/fixtures/support/valid/minimal-safe.v1.json +20 -0
- package/package.json +98 -0
- package/skills/claude/atelier-open/SKILL.md +68 -0
- package/skills/claude/mnstry-readiness/SKILL.md +41 -0
- package/skills/codex/atelier-open/SKILL.md +68 -0
- package/skills/codex/mnstry-readiness/SKILL.md +41 -0
- package/src/analysis/adapter.mjs +20 -0
- package/src/analysis/analysis-claim-contract.mjs +335 -0
- package/src/attestation/jcs.mjs +64 -0
- package/src/attestation/sign.mjs +305 -0
- package/src/boundary/content-rules.mjs +262 -0
- package/src/boundary/policy.mjs +806 -0
- package/src/check-atelier-export-contract.mjs +116 -0
- package/src/cli/run.mjs +283 -0
- package/src/collaboration/proposals.mjs +262 -0
- package/src/commands/analysis.mjs +8 -0
- package/src/commands/announcements.mjs +308 -0
- package/src/commands/attestation.mjs +198 -0
- package/src/commands/boundary.mjs +25 -0
- package/src/commands/config.mjs +4 -0
- package/src/commands/context.mjs +3 -0
- package/src/commands/distribution.mjs +224 -0
- package/src/commands/egress.mjs +11 -0
- package/src/commands/extension-pack.mjs +209 -0
- package/src/commands/feedback.mjs +3 -0
- package/src/commands/graph.mjs +3 -0
- package/src/commands/init.mjs +130 -0
- package/src/commands/lock.mjs +3 -0
- package/src/commands/project.mjs +3 -0
- package/src/commands/promote.mjs +4 -0
- package/src/commands/readiness.mjs +3 -0
- package/src/commands/server.mjs +6 -0
- package/src/commands/setup.mjs +250 -0
- package/src/commands/support.mjs +3 -0
- package/src/commands/upgrade.mjs +3 -0
- package/src/contracts/corpus.mjs +171 -0
- package/src/egress/check.mjs +45 -0
- package/src/egress/forbidden-egress.mjs +323 -0
- package/src/export/atelier-export-contract.mjs +722 -0
- package/src/extension-packs/loader.mjs +491 -0
- package/src/graph/graph.mjs +370 -0
- package/src/graph/knowledge-graph.mjs +887 -0
- package/src/harness/context-client.mjs +144 -0
- package/src/harness/context.mjs +27 -0
- package/src/index.mjs +88 -0
- package/src/project/config.mjs +557 -0
- package/src/project/file-class.mjs +113 -0
- package/src/project/git-ignore.mjs +31 -0
- package/src/project/path-match.mjs +30 -0
- package/src/project/repo-identity.mjs +176 -0
- package/src/projection/alignment-projection.mjs +169 -0
- package/src/projection/policy.mjs +90 -0
- package/src/projection/project.mjs +152 -0
- package/src/readiness/readiness.mjs +312 -0
- package/src/readiness-protocols/bundled-pack.mjs +766 -0
- package/src/readiness-protocols/runtime.mjs +419 -0
- package/src/server/local-sidecar.mjs +640 -0
- package/src/server/security.mjs +229 -0
- package/src/server/server.mjs +42 -0
- package/src/support/feedback-report.mjs +414 -0
- package/src/support/support-bundle.mjs +226 -0
- package/src/ui/html-primitives.mjs +104 -0
- package/src/upgrade/upgrade.mjs +701 -0
- package/src/validate-atelier-export-dry-run.mjs +75 -0
- package/templates/atelier.project.example.json +23 -0
- package/templates/distribution-workspace/.claude/launch.json +12 -0
- package/templates/distribution-workspace/README.md +86 -0
- package/templates/distribution-workspace/atelier.project.json +36 -0
- package/templates/distribution-workspace/boundary-policy.v1.json +61 -0
- package/templates/distribution-workspace/gitignore +29 -0
- package/templates/distribution-workspace/governance/repo-boundary-ledger.md +5 -0
- package/templates/distribution-workspace/project/README.md +19 -0
- package/templates/distribution-workspace/repo-access.v1.json +9 -0
- package/templates/launch.json +12 -0
- package/templates/private-domain-workspace/.claude/launch.json +12 -0
- package/templates/private-domain-workspace/README.md +50 -0
- package/templates/private-domain-workspace/atelier.project.json +28 -0
- package/templates/private-domain-workspace/boundary-policy.v1.json +54 -0
- package/templates/private-domain-workspace/domain/README.md +19 -0
- package/templates/private-domain-workspace/gitignore +29 -0
- package/templates/private-domain-workspace/governance/repo-boundary-ledger.md +4 -0
- package/templates/private-domain-workspace/repo-access.v1.json +9 -0
- package/templates/shared-project-workspace/.claude/launch.json +12 -0
- package/templates/shared-project-workspace/README.md +51 -0
- package/templates/shared-project-workspace/atelier.project.json +28 -0
- package/templates/shared-project-workspace/boundary-policy.v1.json +61 -0
- package/templates/shared-project-workspace/gitignore +29 -0
- package/templates/shared-project-workspace/governance/repo-boundary-ledger.md +5 -0
- package/templates/shared-project-workspace/project/README.md +19 -0
- 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
|
+
}
|