@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,491 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import Ajv2020 from 'ajv/dist/2020.js'
|
|
6
|
+
import addFormats from 'ajv-formats'
|
|
7
|
+
import {
|
|
8
|
+
READINESS_PROTOCOL_IDS,
|
|
9
|
+
READINESS_PROTOCOL_SLUGS,
|
|
10
|
+
bundledReadinessProtocols,
|
|
11
|
+
getBundledReadinessProtocol,
|
|
12
|
+
} from '../readiness-protocols/bundled-pack.mjs'
|
|
13
|
+
|
|
14
|
+
// Deterministic extension-pack loader. Packs are declared in the tracked
|
|
15
|
+
// project config under ext["mnstry.atelier"].extensionPacks and load through a
|
|
16
|
+
// fixed pipeline: entry shape, enablement, schema validation, identity,
|
|
17
|
+
// reserved-namespace rules, fixture and protocol path guards, the nine-point
|
|
18
|
+
// safety posture, collision rules against the bundled pack, and lock
|
|
19
|
+
// pin-and-verify. Every check fails closed: in the default (non-report) mode
|
|
20
|
+
// any error throws before a pack becomes visible to the runtime.
|
|
21
|
+
|
|
22
|
+
export const ATELIER_EXT_NAMESPACE = 'mnstry.atelier'
|
|
23
|
+
export const EXTENSION_PACK_SCHEMA = 'mnstry-atelier-extension-pack@v1'
|
|
24
|
+
export const RESERVED_NAMESPACE = 'mnstry'
|
|
25
|
+
// Reserved-namespace rule covers lookalike labels too: 'mnstry' itself plus
|
|
26
|
+
// any label extending it with '.' or '-' ('mnstry-core', 'mnstry-readiness'),
|
|
27
|
+
// aligning with TRADEMARKS.md. Pack namespaces cannot contain '.', but the
|
|
28
|
+
// pattern still guards it so the rule holds if the id grammar ever loosens.
|
|
29
|
+
export const RESERVED_NAMESPACE_PATTERN = /^mnstry([.-]|$)/
|
|
30
|
+
|
|
31
|
+
// Mirrors LOCK_FILE in src/upgrade/upgrade.mjs. Declared locally because the
|
|
32
|
+
// upgrade module imports this loader (writeAtelierLock loads packs before
|
|
33
|
+
// pinning them); importing the constant back would create a module cycle.
|
|
34
|
+
const LOCK_FILE_NAME = 'atelier.lock.json'
|
|
35
|
+
|
|
36
|
+
const PACK_ID_PATTERN = /^[a-z][a-z0-9-]*\.[a-z][a-z0-9-]*$/
|
|
37
|
+
const PACK_VERSION_PATTERN = /^v\d+$/
|
|
38
|
+
|
|
39
|
+
// The nine-point safety posture every loaded protocol must carry, matching the
|
|
40
|
+
// properties the bundled-pack tests pin. The protocol contract keeps
|
|
41
|
+
// safetyPosture as a bare object on purpose (no schema tightening at v1), so
|
|
42
|
+
// points 1-7 are code-enforced here; points 8 and 9 are schema consts and this
|
|
43
|
+
// table backstops them in case the contract ever loosens.
|
|
44
|
+
export const PROTOCOL_POSTURE_POINTS = [
|
|
45
|
+
{ path: 'safetyPosture.runtimeMutation', expected: 'false', holds: (doc) => doc?.safetyPosture?.runtimeMutation === false },
|
|
46
|
+
{ path: 'safetyPosture.externalEgress', expected: 'false', holds: (doc) => doc?.safetyPosture?.externalEgress === false },
|
|
47
|
+
{ path: 'safetyPosture.defaultVisibility', expected: '"private"', holds: (doc) => doc?.safetyPosture?.defaultVisibility === 'private' },
|
|
48
|
+
{ path: 'safetyPosture.authority', expected: '"proposal-only"', holds: (doc) => doc?.safetyPosture?.authority === 'proposal-only' },
|
|
49
|
+
{ path: 'safetyPosture.reviewMode', expected: '"static-inspection"', holds: (doc) => doc?.safetyPosture?.reviewMode === 'static-inspection' },
|
|
50
|
+
{ path: 'safetyPosture.failClosedOnMissingEvidence', expected: 'true', holds: (doc) => doc?.safetyPosture?.failClosedOnMissingEvidence === true },
|
|
51
|
+
{ path: 'safetyPosture.refuses', expected: 'an array including "runtime writes"', holds: (doc) => Array.isArray(doc?.safetyPosture?.refuses) && doc.safetyPosture.refuses.includes('runtime writes') },
|
|
52
|
+
{ path: 'safety.runtimeMutation', expected: 'false', holds: (doc) => doc?.safety?.runtimeMutation === false },
|
|
53
|
+
{ path: 'outputs.runSchema', expected: '"atelier-readiness-run@v1"', holds: (doc) => doc?.outputs?.runSchema === 'atelier-readiness-run@v1' },
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
// Pack member arrays whose ids must live inside the pack's own namespace.
|
|
57
|
+
const MEMBER_ID_FIELDS = [
|
|
58
|
+
['terms', ['id']],
|
|
59
|
+
['protocols', ['id']],
|
|
60
|
+
['lenses', ['id']],
|
|
61
|
+
['readinessRules', ['id']],
|
|
62
|
+
['actionRules', ['id']],
|
|
63
|
+
['exportMappings', ['id', 'sourceTerm']],
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
const PACKAGE_ROOT_URL = new URL('../../', import.meta.url)
|
|
67
|
+
|
|
68
|
+
function isPlainObject(value) {
|
|
69
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function asObject(value) {
|
|
73
|
+
return isPlainObject(value) ? value : {}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readJsonFile(file) {
|
|
77
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let cachedValidators = null
|
|
81
|
+
|
|
82
|
+
// Compiled once and cached at module scope; compiled validators are immutable.
|
|
83
|
+
function compiledValidators() {
|
|
84
|
+
if (!cachedValidators) {
|
|
85
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false })
|
|
86
|
+
addFormats(ajv)
|
|
87
|
+
cachedValidators = {
|
|
88
|
+
pack: ajv.compile(readJsonFile(fileURLToPath(new URL('contracts/atelier-extension-pack.v1.schema.json', PACKAGE_ROOT_URL)))),
|
|
89
|
+
protocol: ajv.compile(readJsonFile(fileURLToPath(new URL('contracts/atelier-readiness-protocol.v1.schema.json', PACKAGE_ROOT_URL)))),
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return cachedValidators
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function ajvMessages(validate) {
|
|
96
|
+
return (validate.errors ?? []).map((error) => `${error.instancePath || '/'} ${error.message}`)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Digest of the exact file bytes, not re-serialized JSON. The bundled entry in
|
|
100
|
+
// buildAtelierLock digests JSON.stringify of an in-memory object instead; file
|
|
101
|
+
// packs always digest raw bytes so on-disk edits are visible byte-for-byte.
|
|
102
|
+
//
|
|
103
|
+
// The digest pins the whole pack content, not just the manifest: it is sha256
|
|
104
|
+
// over a length-prefixed concatenation of parts, where the parts are the pack
|
|
105
|
+
// manifest's raw bytes followed by each referenced protocol file's raw bytes
|
|
106
|
+
// in declaration order. Each part contributes the ASCII decimal byte length,
|
|
107
|
+
// a single '\n', then the bytes themselves. The length prefix makes part
|
|
108
|
+
// boundaries unambiguous, so no concatenation of different files can collide
|
|
109
|
+
// with another split of the same bytes. A locked lock entry therefore fails
|
|
110
|
+
// closed when any referenced protocol file changes, not only the manifest.
|
|
111
|
+
export function computePackDigest(manifestBytes, protocolFilesBytes = []) {
|
|
112
|
+
const hash = crypto.createHash('sha256')
|
|
113
|
+
for (const part of [manifestBytes, ...protocolFilesBytes]) {
|
|
114
|
+
const bytes = Buffer.isBuffer(part) ? part : Buffer.from(part)
|
|
115
|
+
hash.update(`${bytes.length}\n`)
|
|
116
|
+
hash.update(bytes)
|
|
117
|
+
}
|
|
118
|
+
return `sha256:${hash.digest('hex')}`
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function relativePathError(value, { requireJsonSuffix }) {
|
|
122
|
+
if (typeof value !== 'string' || !value.trim()) return 'must be a non-empty relative path'
|
|
123
|
+
if (path.isAbsolute(value)) return 'must not be an absolute path'
|
|
124
|
+
if (value.split(/[\\/]/).some((segment) => segment === '..')) return 'must not contain .. segments'
|
|
125
|
+
if (requireJsonSuffix && !value.endsWith('.json')) return 'must end with .json'
|
|
126
|
+
return null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isUnder(baseDir, resolved) {
|
|
130
|
+
return resolved === baseDir || resolved.startsWith(`${baseDir}${path.sep}`)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Symlink-hardened containment. The lexical isUnder check alone is inert
|
|
134
|
+
// against symlinks: a packs or protocols directory that is a symlink pointing
|
|
135
|
+
// outside the config root resolves lexically inside it. Both the base and the
|
|
136
|
+
// candidate are pushed through realpathSync before comparing, so any declared
|
|
137
|
+
// path whose real resolution leaves the realpathed base is rejected. Call only
|
|
138
|
+
// after the candidate is known to exist (realpathSync throws otherwise).
|
|
139
|
+
function isUnderReal(baseDir, resolved) {
|
|
140
|
+
return isUnder(fs.realpathSync(baseDir), fs.realpathSync(resolved))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function entryShapeErrors(entry, label) {
|
|
144
|
+
if (!isPlainObject(entry)) return [`${label} must be an object`]
|
|
145
|
+
const errors = []
|
|
146
|
+
if (typeof entry.id !== 'string' || !PACK_ID_PATTERN.test(entry.id)) {
|
|
147
|
+
errors.push(`${label}.id must be a namespaced pack id matching ${PACK_ID_PATTERN}`)
|
|
148
|
+
}
|
|
149
|
+
if (typeof entry.version !== 'string' || !PACK_VERSION_PATTERN.test(entry.version)) {
|
|
150
|
+
errors.push(`${label}.version must match ${PACK_VERSION_PATTERN}`)
|
|
151
|
+
}
|
|
152
|
+
if (typeof entry.enabled !== 'boolean') {
|
|
153
|
+
errors.push(`${label}.enabled must be a boolean`)
|
|
154
|
+
}
|
|
155
|
+
const pathError = relativePathError(entry.path, { requireJsonSuffix: true })
|
|
156
|
+
if (pathError) errors.push(`${label}.path ${pathError}`)
|
|
157
|
+
return errors
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function declaredEntries(project) {
|
|
161
|
+
const container = asObject(asObject(asObject(project?.config).ext)[ATELIER_EXT_NAMESPACE])
|
|
162
|
+
const value = container.extensionPacks
|
|
163
|
+
if (value == null) return { entries: [], error: null }
|
|
164
|
+
if (!Array.isArray(value)) {
|
|
165
|
+
return { entries: [], error: `ext["${ATELIER_EXT_NAMESPACE}"].extensionPacks must be an array` }
|
|
166
|
+
}
|
|
167
|
+
return { entries: value, error: null }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function readLockState(configDir) {
|
|
171
|
+
const lockPath = path.join(configDir, LOCK_FILE_NAME)
|
|
172
|
+
if (!fs.existsSync(lockPath)) return { exists: false, entries: [], error: null }
|
|
173
|
+
try {
|
|
174
|
+
const doc = readJsonFile(lockPath)
|
|
175
|
+
return { exists: true, entries: Array.isArray(doc?.extensionPacks) ? doc.extensionPacks : [], error: null }
|
|
176
|
+
} catch {
|
|
177
|
+
// Do not echo parser output: it can quote file content. Name the file only.
|
|
178
|
+
return { exists: true, entries: [], error: `${LOCK_FILE_NAME} is not readable JSON; review it and run atelier lock write` }
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function reservedNamespaceErrors(doc) {
|
|
183
|
+
const errors = []
|
|
184
|
+
const prefix = `${doc.id}:`
|
|
185
|
+
for (const [field, keys] of MEMBER_ID_FIELDS) {
|
|
186
|
+
const items = Array.isArray(doc[field]) ? doc[field] : []
|
|
187
|
+
items.forEach((item, index) => {
|
|
188
|
+
for (const key of keys) {
|
|
189
|
+
const value = item?.[key]
|
|
190
|
+
if (typeof value !== 'string') continue
|
|
191
|
+
if (value.startsWith(`${RESERVED_NAMESPACE}.`)) {
|
|
192
|
+
errors.push(`${field}[${index}].${key} ${value} redefines reserved mnstry runtime term`)
|
|
193
|
+
} else if (!value.startsWith(prefix)) {
|
|
194
|
+
errors.push(`${field}[${index}].${key} ${value} does not belong to pack namespace ${doc.id}`)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
return errors
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function loadProtocolRecords({ doc, packDir, seenProtocolIds, errors, digestParts }) {
|
|
203
|
+
const validators = compiledValidators()
|
|
204
|
+
const records = []
|
|
205
|
+
const refs = Array.isArray(doc.protocols) ? doc.protocols : []
|
|
206
|
+
refs.forEach((ref, index) => {
|
|
207
|
+
const label = `protocols[${index}]`
|
|
208
|
+
const guard = relativePathError(ref.path, { requireJsonSuffix: true })
|
|
209
|
+
if (guard) {
|
|
210
|
+
errors.push(`${label}.path ${guard}`)
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
const resolved = path.resolve(packDir, ref.path)
|
|
214
|
+
if (!isUnder(packDir, resolved)) {
|
|
215
|
+
errors.push(`${label}.path escapes the pack directory`)
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
if (!fs.existsSync(resolved)) {
|
|
219
|
+
errors.push(`${label} protocol file not found: ${ref.path}`)
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
if (!isUnderReal(packDir, resolved)) {
|
|
223
|
+
errors.push(`${label}.path escapes the pack directory`)
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
// Raw bytes are read once and folded into the pack digest (declaration
|
|
227
|
+
// order), so the lock pins protocol content, not just the manifest.
|
|
228
|
+
const protocolBytes = fs.readFileSync(resolved)
|
|
229
|
+
digestParts.push(protocolBytes)
|
|
230
|
+
let protocolDoc
|
|
231
|
+
try {
|
|
232
|
+
protocolDoc = JSON.parse(protocolBytes.toString('utf8'))
|
|
233
|
+
} catch {
|
|
234
|
+
errors.push(`${label} protocol file is not valid JSON: ${ref.path}`)
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
if (!validators.protocol(protocolDoc)) {
|
|
238
|
+
errors.push(...ajvMessages(validators.protocol).map((message) => `${label} protocol file fails atelier-readiness-protocol@v1: ${message}`))
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
if (protocolDoc.id !== ref.id) {
|
|
242
|
+
errors.push(`${label} protocol file id ${protocolDoc.id} does not match the pack entry id ${ref.id}`)
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
let posture = true
|
|
246
|
+
for (const point of PROTOCOL_POSTURE_POINTS) {
|
|
247
|
+
if (!point.holds(protocolDoc)) {
|
|
248
|
+
posture = false
|
|
249
|
+
errors.push(`${label} protocol ${ref.id} violates the required safety posture: ${point.path} must be ${point.expected}`)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const slug = String(ref.id).split(':').at(-1)
|
|
253
|
+
let collision = false
|
|
254
|
+
if (READINESS_PROTOCOL_IDS.includes(ref.id)) {
|
|
255
|
+
collision = true
|
|
256
|
+
errors.push(`${label} protocol id ${ref.id} collides with a bundled protocol id`)
|
|
257
|
+
}
|
|
258
|
+
if (READINESS_PROTOCOL_SLUGS.includes(slug)) {
|
|
259
|
+
collision = true
|
|
260
|
+
errors.push(`${label} protocol slug ${slug} collides with a bundled protocol slug`)
|
|
261
|
+
}
|
|
262
|
+
if (seenProtocolIds.has(ref.id)) {
|
|
263
|
+
collision = true
|
|
264
|
+
errors.push(`${label} duplicate protocol id ${ref.id} across loaded packs`)
|
|
265
|
+
}
|
|
266
|
+
seenProtocolIds.add(ref.id)
|
|
267
|
+
if (!posture || collision) return
|
|
268
|
+
records.push({
|
|
269
|
+
id: ref.id,
|
|
270
|
+
packId: doc.id,
|
|
271
|
+
slug,
|
|
272
|
+
gate: ref.gate,
|
|
273
|
+
title: ref.title ?? protocolDoc.title,
|
|
274
|
+
declaredPath: ref.path,
|
|
275
|
+
path: resolved,
|
|
276
|
+
protocol: protocolDoc,
|
|
277
|
+
})
|
|
278
|
+
})
|
|
279
|
+
return records
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function loadOnePack({ entry, configDir, lock, seenProtocolIds, errors, warnings }) {
|
|
283
|
+
const validators = compiledValidators()
|
|
284
|
+
const packPath = path.resolve(configDir, entry.path)
|
|
285
|
+
if (!isUnder(configDir, packPath)) {
|
|
286
|
+
errors.push(`pack path escapes the project config directory: ${entry.path}`)
|
|
287
|
+
return null
|
|
288
|
+
}
|
|
289
|
+
if (!fs.existsSync(packPath)) {
|
|
290
|
+
errors.push(`pack file not found: ${entry.path}`)
|
|
291
|
+
return null
|
|
292
|
+
}
|
|
293
|
+
if (!isUnderReal(configDir, packPath)) {
|
|
294
|
+
errors.push(`pack path escapes the project config directory: ${entry.path}`)
|
|
295
|
+
return null
|
|
296
|
+
}
|
|
297
|
+
const rawBytes = fs.readFileSync(packPath)
|
|
298
|
+
let doc
|
|
299
|
+
try {
|
|
300
|
+
doc = JSON.parse(rawBytes.toString('utf8'))
|
|
301
|
+
} catch {
|
|
302
|
+
errors.push(`pack file is not valid JSON: ${entry.path}`)
|
|
303
|
+
return null
|
|
304
|
+
}
|
|
305
|
+
if (!validators.pack(doc)) {
|
|
306
|
+
errors.push(...ajvMessages(validators.pack).map((message) => `pack file fails ${EXTENSION_PACK_SCHEMA}: ${message}`))
|
|
307
|
+
return null
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (doc.id !== entry.id) errors.push(`pack id ${doc.id} does not match the declared id ${entry.id}`)
|
|
311
|
+
if (doc.version !== entry.version) errors.push(`pack version ${doc.version} does not match the declared version ${entry.version}`)
|
|
312
|
+
const idPrefix = String(doc.id).split('.')[0]
|
|
313
|
+
if (doc.namespace !== idPrefix) errors.push(`pack namespace ${doc.namespace} must equal the pack id prefix ${idPrefix}`)
|
|
314
|
+
// The namespace-level check must hold even for a memberless pack: with no
|
|
315
|
+
// member ids to inspect, reservedNamespaceErrors never fires, so this line
|
|
316
|
+
// is the only thing standing between a reserved (or lookalike) namespace
|
|
317
|
+
// and a clean load.
|
|
318
|
+
if (RESERVED_NAMESPACE_PATTERN.test(String(doc.namespace))) {
|
|
319
|
+
errors.push(`pack namespace ${doc.namespace} is reserved for the bundled runtime vocabulary`)
|
|
320
|
+
}
|
|
321
|
+
errors.push(...reservedNamespaceErrors(doc))
|
|
322
|
+
if (errors.length) return null
|
|
323
|
+
|
|
324
|
+
const packDir = path.dirname(packPath)
|
|
325
|
+
const fixtures = Array.isArray(doc.fixtures) ? doc.fixtures : []
|
|
326
|
+
fixtures.forEach((fixtureRef, index) => {
|
|
327
|
+
const guard = relativePathError(fixtureRef, { requireJsonSuffix: false })
|
|
328
|
+
if (guard) {
|
|
329
|
+
errors.push(`fixtures[${index}] ${guard}`)
|
|
330
|
+
return
|
|
331
|
+
}
|
|
332
|
+
const resolved = path.resolve(packDir, fixtureRef)
|
|
333
|
+
if (!isUnder(packDir, resolved)) {
|
|
334
|
+
errors.push(`fixtures[${index}] escapes the pack directory`)
|
|
335
|
+
return
|
|
336
|
+
}
|
|
337
|
+
// Fixtures are declarative references; a missing file is not a load error,
|
|
338
|
+
// but an existing one that resolves outside the pack through a symlink is.
|
|
339
|
+
if (!fs.existsSync(resolved)) {
|
|
340
|
+
warnings.push(`fixtures[${index}] reference not found: ${fixtureRef}`)
|
|
341
|
+
} else if (!isUnderReal(packDir, resolved)) {
|
|
342
|
+
errors.push(`fixtures[${index}] escapes the pack directory`)
|
|
343
|
+
}
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
// Digest parts: manifest bytes first, then each referenced protocol file's
|
|
347
|
+
// bytes in declaration order (see computePackDigest for the exact format).
|
|
348
|
+
const digestParts = []
|
|
349
|
+
const protocolRecords = loadProtocolRecords({ doc, packDir, seenProtocolIds, errors, digestParts })
|
|
350
|
+
const digest = computePackDigest(rawBytes, digestParts)
|
|
351
|
+
|
|
352
|
+
const lockEntry = lock.entries.find((item) => item?.id === entry.id) ?? null
|
|
353
|
+
let lockStatus = 'unlocked'
|
|
354
|
+
if (lockEntry) {
|
|
355
|
+
let pinned = true
|
|
356
|
+
if (lockEntry.version !== entry.version) {
|
|
357
|
+
pinned = false
|
|
358
|
+
errors.push(`lock version mismatch for extension pack ${entry.id}: locked ${lockEntry.version}, declared ${entry.version}; review the pack and run atelier lock write`)
|
|
359
|
+
}
|
|
360
|
+
// A lock entry that carries no digest pins nothing; verifying it as
|
|
361
|
+
// 'locked' would be vacuous, so it is a load error with the remedy named.
|
|
362
|
+
if (typeof lockEntry.digest !== 'string' || !lockEntry.digest) {
|
|
363
|
+
pinned = false
|
|
364
|
+
errors.push(`lock entry for extension pack ${entry.id} is missing a string digest; review the pack and run atelier lock write`)
|
|
365
|
+
} else if (lockEntry.digest !== digest) {
|
|
366
|
+
pinned = false
|
|
367
|
+
errors.push(`lock digest mismatch for extension pack ${entry.id}; review the pack and run atelier lock write`)
|
|
368
|
+
}
|
|
369
|
+
if (pinned) lockStatus = 'locked'
|
|
370
|
+
} else if (lock.exists) {
|
|
371
|
+
warnings.push(`extension pack ${entry.id} is not recorded in ${LOCK_FILE_NAME}; run atelier lock write after review`)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (errors.length) return null
|
|
375
|
+
return {
|
|
376
|
+
id: doc.id,
|
|
377
|
+
version: doc.version,
|
|
378
|
+
namespace: doc.namespace,
|
|
379
|
+
declaredPath: entry.path,
|
|
380
|
+
path: packPath,
|
|
381
|
+
digest,
|
|
382
|
+
lock: lockStatus,
|
|
383
|
+
doc,
|
|
384
|
+
// Lenses are validated and stored as write-only metadata; no lens runtime.
|
|
385
|
+
lenses: Array.isArray(doc.lenses) ? doc.lenses : [],
|
|
386
|
+
protocols: protocolRecords,
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function formatLoadFailure(errors) {
|
|
391
|
+
const lines = errors.map((item) => (item.packId ? `${item.packId}: ${item.message}` : item.message))
|
|
392
|
+
return ['extension pack loading failed:', ...lines].join('\n')
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Loads every declared pack in declaration order. With report: false (the
|
|
396
|
+
// default, used by runtime and lock paths) any error throws — fail closed.
|
|
397
|
+
// With report: true (used by extension-pack validate) errors are collected per
|
|
398
|
+
// pack as { packId, message } entries and the caller decides.
|
|
399
|
+
// With verifyLock: false (used only by writeAtelierLock) the existing lock is
|
|
400
|
+
// neither read nor compared: re-pinning must not be blocked by the very drift
|
|
401
|
+
// the new lock records, and the old lock stays on disk untouched. Every pack
|
|
402
|
+
// then reports lock status 'unlocked'; all other checks still fail closed.
|
|
403
|
+
export function loadExtensionPacks(project, { report = false, verifyLock = true } = {}) {
|
|
404
|
+
const errors = []
|
|
405
|
+
const warnings = []
|
|
406
|
+
const skipped = []
|
|
407
|
+
const packs = []
|
|
408
|
+
const protocols = []
|
|
409
|
+
const configDir = project?.configDir ?? process.cwd()
|
|
410
|
+
|
|
411
|
+
const declared = declaredEntries(project)
|
|
412
|
+
if (declared.error) errors.push({ packId: null, message: declared.error })
|
|
413
|
+
|
|
414
|
+
const lock = verifyLock ? readLockState(configDir) : { exists: false, entries: [], error: null }
|
|
415
|
+
if (lock.error) errors.push({ packId: null, message: lock.error })
|
|
416
|
+
|
|
417
|
+
const overlayPacks = asObject(asObject(asObject(asObject(project?.localOverlay).overlay).preferences).extensionPacks)
|
|
418
|
+
const declaredIds = new Set()
|
|
419
|
+
const seenProtocolIds = new Set()
|
|
420
|
+
|
|
421
|
+
declared.entries.forEach((entry, index) => {
|
|
422
|
+
const label = `ext["${ATELIER_EXT_NAMESPACE}"].extensionPacks[${index}]`
|
|
423
|
+
const packId = typeof entry?.id === 'string' && entry.id ? entry.id : null
|
|
424
|
+
const entryErrors = entryShapeErrors(entry, label)
|
|
425
|
+
if (entryErrors.length) {
|
|
426
|
+
for (const message of entryErrors) errors.push({ packId, message })
|
|
427
|
+
return
|
|
428
|
+
}
|
|
429
|
+
if (declaredIds.has(entry.id)) {
|
|
430
|
+
errors.push({ packId: entry.id, message: `duplicate extension pack declaration for ${entry.id}` })
|
|
431
|
+
return
|
|
432
|
+
}
|
|
433
|
+
declaredIds.add(entry.id)
|
|
434
|
+
|
|
435
|
+
// Enablement is disable-only and decided before the file is read: the
|
|
436
|
+
// tracked config can disable, the machine-local overlay can disable, and
|
|
437
|
+
// neither the overlay nor anything else can enable an undeclared pack.
|
|
438
|
+
if (entry.enabled === false) {
|
|
439
|
+
skipped.push({ packId: entry.id, reason: 'disabled-in-config' })
|
|
440
|
+
return
|
|
441
|
+
}
|
|
442
|
+
if (asObject(overlayPacks[entry.id]).enabled === false) {
|
|
443
|
+
skipped.push({ packId: entry.id, reason: 'disabled-by-overlay' })
|
|
444
|
+
return
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const packErrors = []
|
|
448
|
+
const packWarnings = []
|
|
449
|
+
const record = loadOnePack({ entry, configDir, lock, seenProtocolIds, errors: packErrors, warnings: packWarnings })
|
|
450
|
+
for (const message of packErrors) errors.push({ packId: entry.id, message })
|
|
451
|
+
for (const message of packWarnings) warnings.push({ packId: entry.id, message })
|
|
452
|
+
if (!record) return
|
|
453
|
+
packs.push(record)
|
|
454
|
+
protocols.push(...record.protocols)
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
for (const id of Object.keys(overlayPacks)) {
|
|
458
|
+
if (!declaredIds.has(id)) {
|
|
459
|
+
warnings.push({ packId: id, message: `overlay preference references undeclared extension pack ${id}; the overlay can only disable declared packs` })
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const result = { packs, protocols, errors, warnings, skipped }
|
|
464
|
+
if (!report && errors.length) throw new Error(formatLoadFailure(errors))
|
|
465
|
+
return result
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Explicit registry object, no global state. Bundled resolution keeps its
|
|
469
|
+
// existing id | slug | namespaced affordance; pack protocols resolve by full
|
|
470
|
+
// namespaced id only, so a pack can never shadow or squat a bundled slug.
|
|
471
|
+
export function createProtocolRegistry({ packs = [] } = {}) {
|
|
472
|
+
const packProtocols = new Map()
|
|
473
|
+
for (const pack of packs) {
|
|
474
|
+
for (const record of pack.protocols ?? []) packProtocols.set(record.id, record)
|
|
475
|
+
}
|
|
476
|
+
return {
|
|
477
|
+
packs,
|
|
478
|
+
protocolById(id) {
|
|
479
|
+
const bundled = getBundledReadinessProtocol(id)
|
|
480
|
+
if (bundled) return bundled
|
|
481
|
+
const record = packProtocols.get(id)
|
|
482
|
+
return record ? record.protocol : null
|
|
483
|
+
},
|
|
484
|
+
listProtocols() {
|
|
485
|
+
return [
|
|
486
|
+
...bundledReadinessProtocols,
|
|
487
|
+
...[...packProtocols.values()].map((record) => record.protocol),
|
|
488
|
+
]
|
|
489
|
+
},
|
|
490
|
+
}
|
|
491
|
+
}
|