@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,557 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { spawnSync } from 'node:child_process'
|
|
4
|
+
|
|
5
|
+
export const PROJECT_CONFIG_ARG_PREFIX = '--project-config='
|
|
6
|
+
export const PROJECT_CONFIG_ENV = 'MNSTRY_ATELIER_PROJECT_CONFIG'
|
|
7
|
+
export const PROJECT_CONFIG_SCHEMA = 'mnstry.atelier-project-config@v1'
|
|
8
|
+
export const LOCAL_OVERLAY_SCHEMA = 'mnstry.atelier-local-overlay@v1'
|
|
9
|
+
export const LOCAL_OVERLAY_ENV = 'MNSTRY_ATELIER_LOCAL_CONFIG'
|
|
10
|
+
export const LOCAL_STATE_DIR = '.atelier-local'
|
|
11
|
+
export const LOCAL_OVERLAY_FILES = ['atelier.local.json', 'atelier.workspace.local.json']
|
|
12
|
+
|
|
13
|
+
// A workspace accumulates git folders that are not Atelier repos at all: vendored
|
|
14
|
+
// checkouts, app-builder exports, scratch clones. Declaring one `external`
|
|
15
|
+
// acknowledges its identity without implying a read boundary, so validation stops
|
|
16
|
+
// demanding metadata for content the workspace does not own. It is deliberately not
|
|
17
|
+
// a way to silence checks on a repo you do manage.
|
|
18
|
+
export const EXTERNAL_REPO_KIND = 'external'
|
|
19
|
+
|
|
20
|
+
export const isExternalRepo = (repo) => firstString(repo?.kind) === EXTERNAL_REPO_KIND
|
|
21
|
+
|
|
22
|
+
// Kept here rather than imported from repo-identity.mjs so config validation stays
|
|
23
|
+
// free of that module's git/provider dependencies.
|
|
24
|
+
export const SUPPORTED_IDENTITY_PROVIDERS = ['github']
|
|
25
|
+
|
|
26
|
+
export const asObject = (value) => (value && typeof value === 'object' && !Array.isArray(value) ? value : {})
|
|
27
|
+
|
|
28
|
+
export function parseArgs(argv = process.argv.slice(2)) {
|
|
29
|
+
const out = { _: [] }
|
|
30
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
31
|
+
const arg = argv[index]
|
|
32
|
+
if (!arg.startsWith('--')) {
|
|
33
|
+
out._.push(arg)
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
const [key, ...rest] = arg.slice(2).split('=')
|
|
37
|
+
if (rest.length) {
|
|
38
|
+
out[key] = rest.join('=')
|
|
39
|
+
} else if (argv[index + 1] && !argv[index + 1].startsWith('--')) {
|
|
40
|
+
out[key] = argv[index + 1]
|
|
41
|
+
index += 1
|
|
42
|
+
} else {
|
|
43
|
+
out[key] = true
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return out
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function firstString(...values) {
|
|
50
|
+
for (const value of values) {
|
|
51
|
+
if (typeof value === 'string' && value.trim()) return value.trim()
|
|
52
|
+
}
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function resolvePathValue(value, baseDir) {
|
|
57
|
+
if (typeof value !== 'string' || !value.trim()) return null
|
|
58
|
+
return path.isAbsolute(value) ? path.resolve(value) : path.resolve(baseDir, value)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function readJson(file) {
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
64
|
+
} catch (error) {
|
|
65
|
+
throw new Error(`invalid JSON at ${file}: ${error.message}`)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function writeJson(file, value) {
|
|
70
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
71
|
+
const text = `${JSON.stringify(value, null, 2)}\n`
|
|
72
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`
|
|
73
|
+
fs.writeFileSync(tmp, text)
|
|
74
|
+
fs.renameSync(tmp, file)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function projectConfigArg(argv = process.argv.slice(2), prefix = PROJECT_CONFIG_ARG_PREFIX) {
|
|
78
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
79
|
+
const arg = argv[index]
|
|
80
|
+
if (typeof arg !== 'string') continue
|
|
81
|
+
if (arg.startsWith(prefix)) return arg.slice(prefix.length).trim() || null
|
|
82
|
+
if ((arg === '--project-config' || arg === '--project') && argv[index + 1]) return String(argv[index + 1]).trim() || null
|
|
83
|
+
if (arg.startsWith('--project=')) return arg.slice('--project='.length).trim() || null
|
|
84
|
+
}
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function stripProjectConfigArgs(argv = [], prefix = PROJECT_CONFIG_ARG_PREFIX) {
|
|
89
|
+
return argv.filter((arg) => typeof arg !== 'string' || !arg.startsWith(prefix))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function readProjectConfig(configPath) {
|
|
93
|
+
const doc = readJson(configPath)
|
|
94
|
+
if (!doc || typeof doc !== 'object' || Array.isArray(doc)) {
|
|
95
|
+
throw new Error(`atelier project config must be a JSON object: ${configPath}`)
|
|
96
|
+
}
|
|
97
|
+
return doc
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function mergeLocalOverlay(target, source) {
|
|
101
|
+
if (!source || typeof source !== 'object' || Array.isArray(source)) return target
|
|
102
|
+
target.schema = firstString(source.schema, target.schema) || LOCAL_OVERLAY_SCHEMA
|
|
103
|
+
target.actor = { ...asObject(target.actor), ...asObject(source.actor) }
|
|
104
|
+
target.harness = { ...asObject(target.harness), ...asObject(source.harness) }
|
|
105
|
+
target.preferences = { ...asObject(target.preferences), ...asObject(source.preferences) }
|
|
106
|
+
target.repoPaths = { ...asObject(target.repoPaths), ...asObject(source.repoPaths) }
|
|
107
|
+
target.repos = { ...asObject(target.repos), ...asObject(source.repos) }
|
|
108
|
+
return target
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function localOverlayCandidatePaths({ configDir, env = process.env, cwd = process.cwd() } = {}) {
|
|
112
|
+
const envPath = firstString(env[LOCAL_OVERLAY_ENV])
|
|
113
|
+
return [
|
|
114
|
+
path.join(configDir, LOCAL_STATE_DIR, 'workspace.json'),
|
|
115
|
+
path.join(configDir, LOCAL_STATE_DIR, 'atelier.local.json'),
|
|
116
|
+
...LOCAL_OVERLAY_FILES.map((name) => path.join(configDir, name)),
|
|
117
|
+
envPath ? resolvePathValue(envPath, cwd) : null,
|
|
118
|
+
].filter(Boolean)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function readLocalOverlay({ configDir, env = process.env, cwd = process.cwd() } = {}) {
|
|
122
|
+
const overlay = { schema: LOCAL_OVERLAY_SCHEMA, repos: {}, repoPaths: {}, actor: {}, harness: {}, preferences: {} }
|
|
123
|
+
const paths = []
|
|
124
|
+
for (const file of localOverlayCandidatePaths({ configDir, env, cwd })) {
|
|
125
|
+
if (!file || !fs.existsSync(file)) continue
|
|
126
|
+
mergeLocalOverlay(overlay, readJson(file))
|
|
127
|
+
paths.push(file)
|
|
128
|
+
}
|
|
129
|
+
return { overlay, paths }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function parseRepoPathOverrides(argv = process.argv.slice(2), cwd = process.cwd()) {
|
|
133
|
+
const out = new Map()
|
|
134
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
135
|
+
const arg = argv[index]
|
|
136
|
+
let value = null
|
|
137
|
+
if (arg === '--repo-path' && argv[index + 1]) {
|
|
138
|
+
value = argv[index + 1]
|
|
139
|
+
index += 1
|
|
140
|
+
} else if (typeof arg === 'string' && arg.startsWith('--repo-path=')) {
|
|
141
|
+
value = arg.slice('--repo-path='.length)
|
|
142
|
+
}
|
|
143
|
+
if (!value || !value.includes('=')) continue
|
|
144
|
+
const [name, ...rest] = value.split('=')
|
|
145
|
+
const repoPath = rest.join('=')
|
|
146
|
+
if (firstString(name) && firstString(repoPath)) out.set(name.trim(), resolvePathValue(repoPath, cwd))
|
|
147
|
+
}
|
|
148
|
+
return out
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function overlayRepoPath(overlay, repoName) {
|
|
152
|
+
if (!repoName) return null
|
|
153
|
+
const direct = firstString(overlay.overlay?.repoPaths?.[repoName], overlay.repoPaths?.[repoName])
|
|
154
|
+
const repo = asObject(overlay.overlay?.repos?.[repoName] ?? overlay.repos?.[repoName])
|
|
155
|
+
return firstString(direct, repo.path, repo.localPath)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function resolveRepoPath({ repo, repoName, configDir, workspaceRoot, overlay, cliRepoPaths }) {
|
|
159
|
+
if (repoName && cliRepoPaths.has(repoName)) return cliRepoPaths.get(repoName)
|
|
160
|
+
const overlayPath = overlayRepoPath(overlay, repoName)
|
|
161
|
+
const fromOverlay = resolvePathValue(overlayPath, configDir)
|
|
162
|
+
if (fromOverlay) return fromOverlay
|
|
163
|
+
const fromConfig = resolvePathValue(repo.path, configDir) || resolvePathValue(repo.path, workspaceRoot)
|
|
164
|
+
if (fromConfig) return fromConfig
|
|
165
|
+
return discoverSiblingRepoPath({ repo, repoName, configDir, workspaceRoot })
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function repoPathSource({ repo, repoName, overlay, cliRepoPaths, workspaceRoot, configDir }) {
|
|
169
|
+
if (repoName && cliRepoPaths.has(repoName)) return 'cli'
|
|
170
|
+
if (overlayRepoPath(overlay, repoName)) return 'local-overlay'
|
|
171
|
+
if (firstString(repo.path)) return 'tracked-config'
|
|
172
|
+
if (discoverSiblingRepoPath({ repo, repoName, configDir, workspaceRoot })) return 'sibling-discovery'
|
|
173
|
+
return null
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function discoverSiblingRepoPath({ repo, repoName, configDir, workspaceRoot }) {
|
|
177
|
+
if (!repoName) return null
|
|
178
|
+
const candidates = [
|
|
179
|
+
path.join(workspaceRoot, repoName),
|
|
180
|
+
path.join(configDir, repoName),
|
|
181
|
+
path.join(path.dirname(configDir), repoName),
|
|
182
|
+
]
|
|
183
|
+
for (const candidate of candidates) {
|
|
184
|
+
if (!fs.existsSync(candidate)) continue
|
|
185
|
+
if (repo.remote && !gitRemoteMatches(candidate, repo.remote)) continue
|
|
186
|
+
return path.resolve(candidate)
|
|
187
|
+
}
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function gitRemoteMatches(repoPath, expected) {
|
|
192
|
+
const result = spawnSync('git', ['-C', repoPath, 'remote', 'get-url', 'origin'], { encoding: 'utf8' })
|
|
193
|
+
if (result.status !== 0) return false
|
|
194
|
+
const actual = normalizeRemote(result.stdout.trim())
|
|
195
|
+
return actual === normalizeRemote(expected)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function normalizeRemote(value) {
|
|
199
|
+
return String(value || '')
|
|
200
|
+
.trim()
|
|
201
|
+
.replace(/^git@github\.com:/, 'https://github.com/')
|
|
202
|
+
.replace(/\.git$/, '')
|
|
203
|
+
.toLowerCase()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function gitRemoteUrl(repoPath) {
|
|
207
|
+
if (!repoPath || !fs.existsSync(repoPath)) return null
|
|
208
|
+
const result = spawnSync('git', ['-C', repoPath, 'remote', 'get-url', 'origin'], { encoding: 'utf8' })
|
|
209
|
+
return result.status === 0 ? firstString(result.stdout) : null
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function remoteHost(url) {
|
|
213
|
+
const text = String(url ?? '').trim()
|
|
214
|
+
if (!text) return null
|
|
215
|
+
const scp = text.match(/^[^/@]+@([^:/]+):/)
|
|
216
|
+
if (scp) return scp[1].toLowerCase()
|
|
217
|
+
const parsed = text.match(/^[a-z][a-z0-9+.-]*:\/\/(?:[^@/]+@)?([^/:]+)/i)
|
|
218
|
+
return parsed ? parsed[1].toLowerCase() : null
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function fallbackRoot(env, fallback) {
|
|
222
|
+
const config = typeof fallback === 'string' ? { value: fallback } : asObject(fallback)
|
|
223
|
+
return firstString(config.value, config.env ? env[config.env] : null)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function resolveProjectConfig({
|
|
227
|
+
argv = process.argv.slice(2),
|
|
228
|
+
env = process.env,
|
|
229
|
+
cwd = process.cwd(),
|
|
230
|
+
configArgPrefix = PROJECT_CONFIG_ARG_PREFIX,
|
|
231
|
+
configEnv = PROJECT_CONFIG_ENV,
|
|
232
|
+
defaults = {},
|
|
233
|
+
} = {}) {
|
|
234
|
+
const argPath = projectConfigArg(argv, configArgPrefix)
|
|
235
|
+
const envPath = firstString(env[configEnv])
|
|
236
|
+
const source = argPath ? 'cli' : envPath ? 'env' : 'default'
|
|
237
|
+
const defaultRepoOpsRoot = fallbackRoot(env, defaults.repoOpsRoot)
|
|
238
|
+
const defaultWorkspaceRoot = fallbackRoot(env, defaults.workspaceRoot)
|
|
239
|
+
const explicitPath = argPath || envPath
|
|
240
|
+
const requestedPath = explicitPath || (defaultRepoOpsRoot || defaultWorkspaceRoot ? null : 'atelier.project.json')
|
|
241
|
+
|
|
242
|
+
let config = {}
|
|
243
|
+
let configPath = null
|
|
244
|
+
let configDir = cwd
|
|
245
|
+
let missingConfigPath = null
|
|
246
|
+
|
|
247
|
+
if (requestedPath) {
|
|
248
|
+
configPath = resolvePathValue(requestedPath, cwd)
|
|
249
|
+
if (configPath && fs.existsSync(configPath)) {
|
|
250
|
+
config = readProjectConfig(configPath)
|
|
251
|
+
configDir = path.dirname(configPath)
|
|
252
|
+
} else if (defaultRepoOpsRoot || defaultWorkspaceRoot) {
|
|
253
|
+
missingConfigPath = configPath
|
|
254
|
+
configPath = null
|
|
255
|
+
} else {
|
|
256
|
+
throw new Error(`atelier project config not found: ${configPath || requestedPath}`)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const roots = asObject(config.roots)
|
|
261
|
+
const graph = asObject(config.graph)
|
|
262
|
+
const projection = asObject(config.projection)
|
|
263
|
+
const alignment = asObject(config.alignment)
|
|
264
|
+
const runtime = asObject(config.runtime)
|
|
265
|
+
const boundaries = asObject(config.boundaries)
|
|
266
|
+
|
|
267
|
+
const workspaceRoot =
|
|
268
|
+
resolvePathValue(firstString(roots.workspace, roots.workspaceRoot), configDir) ||
|
|
269
|
+
resolvePathValue(defaultWorkspaceRoot, cwd) ||
|
|
270
|
+
configDir
|
|
271
|
+
const repoOpsRoot =
|
|
272
|
+
resolvePathValue(firstString(roots.repoOps, roots.repoOpsRoot), configDir) ||
|
|
273
|
+
resolvePathValue(defaultRepoOpsRoot, cwd) ||
|
|
274
|
+
workspaceRoot
|
|
275
|
+
const repoAccessPath =
|
|
276
|
+
resolvePathValue(firstString(graph.repoAccessPath, graph.repoAccessConfig), repoOpsRoot) ||
|
|
277
|
+
resolvePathValue(firstString(defaults.repoAccessPath), repoOpsRoot) ||
|
|
278
|
+
path.join(configDir, 'repo-access.v1.json')
|
|
279
|
+
const graphPath =
|
|
280
|
+
resolvePathValue(firstString(graph.outputPath, graph.workspaceGraphPath, graph.workspaceGraph), repoOpsRoot) ||
|
|
281
|
+
resolvePathValue(firstString(defaults.workspaceGraphPath), repoOpsRoot) ||
|
|
282
|
+
path.join(configDir, 'atelier-output', 'knowledge.graph.json')
|
|
283
|
+
const outputRoot =
|
|
284
|
+
resolvePathValue(firstString(projection.outputRoot, projection.root), configDir) ||
|
|
285
|
+
resolvePathValue(firstString(defaults.outputRoot), repoOpsRoot) ||
|
|
286
|
+
path.join(configDir, 'atelier-output')
|
|
287
|
+
const readinessPath =
|
|
288
|
+
resolvePathValue(firstString(projection.readinessPath, projection.readiness), configDir) ||
|
|
289
|
+
resolvePathValue(firstString(defaults.readinessPath), outputRoot) ||
|
|
290
|
+
path.join(outputRoot, 'atelier-readiness.json')
|
|
291
|
+
const boundaryPolicyPath =
|
|
292
|
+
resolvePathValue(firstString(boundaries.policyPath), configDir) ||
|
|
293
|
+
resolvePathValue(firstString(defaults.boundaryPolicyPath), configDir) ||
|
|
294
|
+
path.join(configDir, 'boundary-policy.v1.json')
|
|
295
|
+
const governanceLedgerPath =
|
|
296
|
+
resolvePathValue(firstString(boundaries.governanceLedgerPath), configDir) ||
|
|
297
|
+
resolvePathValue(firstString(defaults.governanceLedgerPath), configDir) ||
|
|
298
|
+
null
|
|
299
|
+
const appRepoName = firstString(alignment.appRepo, config.appRepoName, defaults.appRepoName)
|
|
300
|
+
const appRootValue = firstString(alignment.appRoot, config.appRoot)
|
|
301
|
+
const appRoot = resolvePathValue(appRootValue, workspaceRoot) || (appRepoName ? path.join(workspaceRoot, appRepoName) : null)
|
|
302
|
+
const alignmentRootValue = firstString(alignment.root, alignment.path, config.alignmentRoot)
|
|
303
|
+
const alignmentRoot =
|
|
304
|
+
resolvePathValue(alignmentRootValue, appRoot || workspaceRoot) ||
|
|
305
|
+
(appRoot && defaults.alignmentRoot ? path.join(appRoot, defaults.alignmentRoot) : null)
|
|
306
|
+
|
|
307
|
+
const localOverlay = readLocalOverlay({ configDir, env, cwd })
|
|
308
|
+
const cliRepoPaths = parseRepoPathOverrides(argv, cwd)
|
|
309
|
+
const resolved = {
|
|
310
|
+
schema: firstString(config.schema) || PROJECT_CONFIG_SCHEMA,
|
|
311
|
+
source,
|
|
312
|
+
config,
|
|
313
|
+
configPath,
|
|
314
|
+
configDir,
|
|
315
|
+
requestedPath,
|
|
316
|
+
missingConfigPath,
|
|
317
|
+
repoOpsRoot,
|
|
318
|
+
workspaceRoot,
|
|
319
|
+
repoAccessPath,
|
|
320
|
+
graphPath,
|
|
321
|
+
workspaceGraphPath: graphPath,
|
|
322
|
+
outputRoot,
|
|
323
|
+
readinessPath,
|
|
324
|
+
boundaryPolicyPath,
|
|
325
|
+
governanceLedgerPath,
|
|
326
|
+
strictNewRepos: boundaries.strictNewRepos === true || defaults.strictNewRepos === true,
|
|
327
|
+
runtimeRoot: resolvePathValue(firstString(runtime.root, runtime.mnstryRuntimeRoot), configDir),
|
|
328
|
+
appRepoName,
|
|
329
|
+
appRoot,
|
|
330
|
+
alignmentRoot,
|
|
331
|
+
localOverlay,
|
|
332
|
+
repos: (Array.isArray(config.repos) ? config.repos : []).map((repo) => {
|
|
333
|
+
const repoName = firstString(repo.name) || (repo.path ? path.basename(repo.path) : null)
|
|
334
|
+
const repoPath = resolveRepoPath({ repo, repoName, configDir, workspaceRoot, overlay: localOverlay.overlay, cliRepoPaths })
|
|
335
|
+
const external = isExternalRepo(repo)
|
|
336
|
+
return {
|
|
337
|
+
...repo,
|
|
338
|
+
name: repoName || (repoPath ? path.basename(repoPath) : null),
|
|
339
|
+
path: repoPath,
|
|
340
|
+
external,
|
|
341
|
+
readBoundary: external ? null : firstString(repo.readBoundary) || 'team',
|
|
342
|
+
pathSource: repoPath ? repoPathSource({ repo, repoName, overlay: localOverlay.overlay, cliRepoPaths, workspaceRoot, configDir }) : null,
|
|
343
|
+
}
|
|
344
|
+
}),
|
|
345
|
+
}
|
|
346
|
+
resolved.localState = ensureLocalState(resolved, { write: true })
|
|
347
|
+
return resolved
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function commandProject({ argv = process.argv.slice(2), env = process.env, cwd = process.cwd() } = {}) {
|
|
351
|
+
const project = resolveProjectConfig({ argv, env, cwd })
|
|
352
|
+
// Fail closed at CLI entry, but only when a real config file was loaded; the
|
|
353
|
+
// defaults/no-file path resolves with an empty config that would spuriously
|
|
354
|
+
// fail document validation.
|
|
355
|
+
if (project.configPath != null) {
|
|
356
|
+
const configErrors = validateProjectConfigDoc(project.config)
|
|
357
|
+
if (configErrors.length) {
|
|
358
|
+
throw new Error(`invalid atelier project config at ${project.configPath}:\n${configErrors.join('\n')}`)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (project.schema !== PROJECT_CONFIG_SCHEMA) {
|
|
362
|
+
throw new Error(`project config schema must be ${PROJECT_CONFIG_SCHEMA}`)
|
|
363
|
+
}
|
|
364
|
+
if (!project.repos.length) {
|
|
365
|
+
throw new Error('project config must declare at least one repo')
|
|
366
|
+
}
|
|
367
|
+
if (!project.repos.some((repo) => !repo.external)) {
|
|
368
|
+
throw new Error(`project config must declare at least one managed repo; every entry is kind "${EXTERNAL_REPO_KIND}"`)
|
|
369
|
+
}
|
|
370
|
+
return project
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function localStateRoot(projectOrDir) {
|
|
374
|
+
const configDir = typeof projectOrDir === 'string' ? projectOrDir : projectOrDir?.configDir
|
|
375
|
+
return path.join(configDir || process.cwd(), LOCAL_STATE_DIR)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function gitRootFor(dir) {
|
|
379
|
+
const result = spawnSync('git', ['-C', dir, 'rev-parse', '--show-toplevel'], { encoding: 'utf8' })
|
|
380
|
+
return result.status === 0 ? result.stdout.trim() : null
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function isIgnoredByGit(gitRoot, rel) {
|
|
384
|
+
const result = spawnSync('git', ['-C', gitRoot, 'check-ignore', '-q', rel], { encoding: 'utf8' })
|
|
385
|
+
return result.status === 0
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function ensureLocalState(project, { write = false } = {}) {
|
|
389
|
+
const root = localStateRoot(project)
|
|
390
|
+
const gitRoot = gitRootFor(project.configDir)
|
|
391
|
+
const rel = gitRoot ? path.relative(gitRoot, root).split(path.sep).join('/') : LOCAL_STATE_DIR
|
|
392
|
+
const ignored = gitRoot ? isIgnoredByGit(gitRoot, `${rel}/`) || isIgnoredByGit(gitRoot, rel) : true
|
|
393
|
+
const report = {
|
|
394
|
+
root,
|
|
395
|
+
ignored,
|
|
396
|
+
created: false,
|
|
397
|
+
overlayPaths: localOverlayCandidatePaths({ configDir: project.configDir }).filter((file) => fs.existsSync(file)),
|
|
398
|
+
warnings: [],
|
|
399
|
+
}
|
|
400
|
+
if (!ignored) {
|
|
401
|
+
report.warnings.push(`${LOCAL_STATE_DIR}/ is not ignored; add it to .gitignore before Atelier writes local machine state`)
|
|
402
|
+
return report
|
|
403
|
+
}
|
|
404
|
+
if (write && !fs.existsSync(root)) {
|
|
405
|
+
fs.mkdirSync(root, { recursive: true })
|
|
406
|
+
report.created = true
|
|
407
|
+
}
|
|
408
|
+
return report
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function loadRepoAccess(project) {
|
|
412
|
+
if (!fs.existsSync(project.repoAccessPath)) {
|
|
413
|
+
return { schema: 'mnstry.atelier-repo-access@v1', defaultReadBoundary: 'team', repos: {} }
|
|
414
|
+
}
|
|
415
|
+
const doc = readJson(project.repoAccessPath)
|
|
416
|
+
return {
|
|
417
|
+
defaultReadBoundary: firstString(doc.defaultReadBoundary) || 'team',
|
|
418
|
+
repos: asObject(doc.repos),
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function unknownKeyErrors(value, label, allowed) {
|
|
423
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return []
|
|
424
|
+
return Object.keys(value)
|
|
425
|
+
.filter((key) => !allowed.has(key))
|
|
426
|
+
.map((key) => `${label} must not include additional property ${key}`)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function stringPathErrors(value, label) {
|
|
430
|
+
if (value == null) return []
|
|
431
|
+
return typeof value === 'string' && value.trim() ? [] : [`${label} must be a non-empty string path`]
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// The contract reserves `ext` as an extension container whose members are
|
|
435
|
+
// namespaced objects; contents beyond that are deliberately unvalidated at v1
|
|
436
|
+
// and carry no authority over first-class fields. Structured consumers of an
|
|
437
|
+
// ext member (for example the extension-pack loader) fail closed on their own.
|
|
438
|
+
function extErrors(value, label) {
|
|
439
|
+
if (value == null) return []
|
|
440
|
+
if (typeof value !== 'object' || Array.isArray(value)) return [`${label} must be an object`]
|
|
441
|
+
return Object.entries(value)
|
|
442
|
+
.filter(([, member]) => !member || typeof member !== 'object' || Array.isArray(member))
|
|
443
|
+
.map(([key]) => `${label}.${key} must be an object; ext members are namespaced objects`)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export function validateProjectConfigDoc(doc, { neutralTemplate = false } = {}) {
|
|
447
|
+
const errors = []
|
|
448
|
+
if (!doc || typeof doc !== 'object' || Array.isArray(doc)) return ['project config must be a JSON object']
|
|
449
|
+
errors.push(...unknownKeyErrors(doc, '/', new Set(['schema', 'contractVersion', 'name', 'roots', 'graph', 'projection', 'alignment', 'runtime', 'boundaries', 'setup', 'repos', 'ext'])))
|
|
450
|
+
if (doc.schema !== PROJECT_CONFIG_SCHEMA) errors.push(`schema must be ${PROJECT_CONFIG_SCHEMA}`)
|
|
451
|
+
if (doc.contractVersion != null && (typeof doc.contractVersion !== 'string' || !/^1\.\d+\.\d+$/.test(doc.contractVersion))) {
|
|
452
|
+
errors.push('contractVersion must be a string matching 1.<minor>.<patch>')
|
|
453
|
+
}
|
|
454
|
+
errors.push(...extErrors(doc.ext, 'ext'))
|
|
455
|
+
|
|
456
|
+
const roots = asObject(doc.roots)
|
|
457
|
+
const graph = asObject(doc.graph)
|
|
458
|
+
const projection = asObject(doc.projection)
|
|
459
|
+
const alignment = asObject(doc.alignment)
|
|
460
|
+
const runtime = asObject(doc.runtime)
|
|
461
|
+
const boundaries = asObject(doc.boundaries)
|
|
462
|
+
const setup = asObject(doc.setup)
|
|
463
|
+
if (doc.roots != null && roots !== doc.roots) errors.push('roots must be an object')
|
|
464
|
+
if (doc.graph != null && graph !== doc.graph) errors.push('graph must be an object')
|
|
465
|
+
if (doc.projection != null && projection !== doc.projection) errors.push('projection must be an object')
|
|
466
|
+
if (doc.alignment != null && alignment !== doc.alignment) errors.push('alignment must be an object')
|
|
467
|
+
if (doc.runtime != null && runtime !== doc.runtime) errors.push('runtime must be an object')
|
|
468
|
+
if (doc.boundaries != null && boundaries !== doc.boundaries) errors.push('boundaries must be an object')
|
|
469
|
+
if (doc.setup != null && setup !== doc.setup) errors.push('setup must be an object')
|
|
470
|
+
const hasRepoArray = Array.isArray(doc.repos) && doc.repos.length > 0
|
|
471
|
+
const hasAlignmentScaffold = Boolean(firstString(alignment.appRepo) && firstString(alignment.root, alignment.path))
|
|
472
|
+
if (!hasRepoArray && !hasAlignmentScaffold) errors.push('repos must be a non-empty array or alignment must define appRepo and root')
|
|
473
|
+
|
|
474
|
+
errors.push(...unknownKeyErrors(roots, 'roots', new Set(['workspace', 'workspaceRoot', 'repoOps', 'repoOpsRoot', 'ext'])))
|
|
475
|
+
errors.push(...unknownKeyErrors(graph, 'graph', new Set(['repoAccessPath', 'repoAccessConfig', 'outputPath', 'workspaceGraphPath', 'workspaceGraph', 'externalRelationPrefixes', 'externalRelationIds', 'ext'])))
|
|
476
|
+
errors.push(...unknownKeyErrors(projection, 'projection', new Set(['outputRoot', 'root', 'readinessPath', 'readiness', 'ext'])))
|
|
477
|
+
errors.push(...unknownKeyErrors(alignment, 'alignment', new Set(['appRepo', 'appRoot', 'root', 'path', 'ext'])))
|
|
478
|
+
errors.push(...unknownKeyErrors(runtime, 'runtime', new Set(['root', 'mnstryRuntimeRoot', 'ext'])))
|
|
479
|
+
errors.push(...unknownKeyErrors(boundaries, 'boundaries', new Set(['policyPath', 'governanceLedgerPath', 'strictNewRepos', 'ext'])))
|
|
480
|
+
errors.push(...unknownKeyErrors(setup, 'setup', new Set(['profile', 'include', 'exclude', 'ext'])))
|
|
481
|
+
errors.push(...extErrors(roots.ext, 'roots.ext'))
|
|
482
|
+
errors.push(...extErrors(graph.ext, 'graph.ext'))
|
|
483
|
+
errors.push(...extErrors(projection.ext, 'projection.ext'))
|
|
484
|
+
errors.push(...extErrors(alignment.ext, 'alignment.ext'))
|
|
485
|
+
errors.push(...extErrors(runtime.ext, 'runtime.ext'))
|
|
486
|
+
errors.push(...extErrors(boundaries.ext, 'boundaries.ext'))
|
|
487
|
+
errors.push(...extErrors(setup.ext, 'setup.ext'))
|
|
488
|
+
errors.push(...stringPathErrors(roots.workspace, 'roots.workspace'))
|
|
489
|
+
errors.push(...stringPathErrors(graph.outputPath, 'graph.outputPath'))
|
|
490
|
+
errors.push(...stringPathErrors(graph.repoAccessPath, 'graph.repoAccessPath'))
|
|
491
|
+
errors.push(...stringPathErrors(graph.workspaceGraphPath, 'graph.workspaceGraphPath'))
|
|
492
|
+
errors.push(...stringPathErrors(projection.outputRoot, 'projection.outputRoot'))
|
|
493
|
+
errors.push(...stringPathErrors(projection.readinessPath, 'projection.readinessPath'))
|
|
494
|
+
errors.push(...stringPathErrors(alignment.root, 'alignment.root'))
|
|
495
|
+
errors.push(...stringPathErrors(alignment.path, 'alignment.path'))
|
|
496
|
+
errors.push(...stringPathErrors(alignment.appRoot, 'alignment.appRoot'))
|
|
497
|
+
errors.push(...stringPathErrors(boundaries.policyPath, 'boundaries.policyPath'))
|
|
498
|
+
errors.push(...stringPathErrors(boundaries.governanceLedgerPath, 'boundaries.governanceLedgerPath'))
|
|
499
|
+
if (boundaries.strictNewRepos != null && typeof boundaries.strictNewRepos !== 'boolean') {
|
|
500
|
+
errors.push('boundaries.strictNewRepos must be a boolean')
|
|
501
|
+
}
|
|
502
|
+
if (setup.profile != null && !['single-repo', 'private-domain', 'shared-project', 'multi-repo', 'monorepo', 'control-workspace'].includes(setup.profile)) {
|
|
503
|
+
errors.push('setup.profile is invalid')
|
|
504
|
+
}
|
|
505
|
+
if (alignment.appRepo != null && !firstString(alignment.appRepo)) errors.push('alignment.appRepo must be a non-empty string')
|
|
506
|
+
|
|
507
|
+
for (const [index, repo] of (Array.isArray(doc.repos) ? doc.repos : []).entries()) {
|
|
508
|
+
if (!repo || typeof repo !== 'object' || Array.isArray(repo)) {
|
|
509
|
+
errors.push(`repos[${index}] must be an object`)
|
|
510
|
+
continue
|
|
511
|
+
}
|
|
512
|
+
errors.push(...unknownKeyErrors(repo, `repos[${index}]`, new Set(['name', 'path', 'readBoundary', 'role', 'kind', 'remote', 'identity', 'aliases', 'required', 'ext'])))
|
|
513
|
+
errors.push(...extErrors(repo.ext, `repos[${index}].ext`))
|
|
514
|
+
if (!firstString(repo.name)) errors.push(`repos[${index}].name is required`)
|
|
515
|
+
if (repo.identity != null) {
|
|
516
|
+
const identity = asObject(repo.identity)
|
|
517
|
+
errors.push(...unknownKeyErrors(identity, `repos[${index}].identity`, new Set(['provider', 'id', 'ext'])))
|
|
518
|
+
errors.push(...extErrors(identity.ext, `repos[${index}].identity.ext`))
|
|
519
|
+
if (!SUPPORTED_IDENTITY_PROVIDERS.includes(identity.provider)) {
|
|
520
|
+
errors.push(`repos[${index}].identity.provider must be one of ${SUPPORTED_IDENTITY_PROVIDERS.join(', ')}`)
|
|
521
|
+
}
|
|
522
|
+
if (!firstString(identity.id)) errors.push(`repos[${index}].identity.id is required and must be the provider's stable id, not a name`)
|
|
523
|
+
}
|
|
524
|
+
if (repo.aliases != null) {
|
|
525
|
+
if (!Array.isArray(repo.aliases)) errors.push(`repos[${index}].aliases must be a list of former names`)
|
|
526
|
+
else if (repo.aliases.some((alias) => !firstString(alias))) errors.push(`repos[${index}].aliases must contain non-empty names`)
|
|
527
|
+
else if (new Set(repo.aliases.map((alias) => alias.toLowerCase())).size !== repo.aliases.length) {
|
|
528
|
+
errors.push(`repos[${index}].aliases must not repeat a name`)
|
|
529
|
+
} else if (repo.aliases.some((alias) => alias.toLowerCase() === String(repo.name ?? '').toLowerCase())) {
|
|
530
|
+
errors.push(`repos[${index}].aliases must not repeat the current name`)
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (repo.path != null && !firstString(repo.path)) errors.push(`repos[${index}].path must be a non-empty string when present`)
|
|
534
|
+
if (repo.readBoundary && !['private', 'team', 'operator', 'staff', 'public', 'sensitive'].includes(repo.readBoundary)) {
|
|
535
|
+
errors.push(`repos[${index}].readBoundary is invalid`)
|
|
536
|
+
}
|
|
537
|
+
if (isExternalRepo(repo) && repo.readBoundary != null) {
|
|
538
|
+
errors.push(`repos[${index}] is kind "${EXTERNAL_REPO_KIND}" and must not declare readBoundary; an external repo is unmanaged and implies no boundary`)
|
|
539
|
+
}
|
|
540
|
+
if (repo.required != null && typeof repo.required !== 'boolean') errors.push(`repos[${index}].required must be a boolean`)
|
|
541
|
+
if (repo.path && path.isAbsolute(repo.path)) errors.push(`repos[${index}].path must be relative; put machine-local absolute paths in ignored local overlay`)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (neutralTemplate) {
|
|
545
|
+
const text = JSON.stringify(doc)
|
|
546
|
+
const adapterSpecificNamePattern = new RegExp(['h', 'e', 'a', 'r', 't', 'h'].join(''), 'i')
|
|
547
|
+
if (adapterSpecificNamePattern.test(text)) errors.push('example project config must not require adapter-specific names')
|
|
548
|
+
if (!firstString(roots.workspace, roots.workspaceRoot)) errors.push('example project config must define roots.workspace')
|
|
549
|
+
if (!firstString(roots.repoOps, roots.repoOpsRoot)) errors.push('example project config must define roots.repoOps')
|
|
550
|
+
if (!firstString(graph.outputPath, graph.workspaceGraphPath, graph.workspaceGraph)) errors.push('example project config must define a graph output path')
|
|
551
|
+
if (!firstString(graph.repoAccessPath, graph.repoAccessConfig)) errors.push('example project config must define repo access path')
|
|
552
|
+
if (!firstString(projection.outputRoot, projection.root) && !hasAlignmentScaffold) {
|
|
553
|
+
errors.push('example project config must define projection outputRoot or alignment root')
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return errors
|
|
557
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { basenameOf, matchesPathPattern, normalizeRelPath } from './path-match.mjs'
|
|
2
|
+
|
|
3
|
+
export const FILE_CLASS_SCHEMA = 'mnstry.atelier-file-classes@v1'
|
|
4
|
+
|
|
5
|
+
export const SOURCE = 'source'
|
|
6
|
+
export const GENERATED_PROJECTION = 'generated-projection'
|
|
7
|
+
export const DISTRIBUTED_RUNTIME_COPY = 'distributed-runtime-copy'
|
|
8
|
+
|
|
9
|
+
export const FILE_CLASSES = Object.freeze([SOURCE, GENERATED_PROJECTION, DISTRIBUTED_RUNTIME_COPY])
|
|
10
|
+
|
|
11
|
+
// What each class means for automated handling. Sync loops, merge policies, upgrade
|
|
12
|
+
// tooling, and CI guards all read this instead of each keeping a list of filenames
|
|
13
|
+
// that drifts out of step with the others.
|
|
14
|
+
export const FILE_CLASS_HANDLING = Object.freeze({
|
|
15
|
+
[SOURCE]: Object.freeze({ rederivable: false, discardable: false, conflictsNeedHuman: true }),
|
|
16
|
+
[GENERATED_PROJECTION]: Object.freeze({ rederivable: true, discardable: true, conflictsNeedHuman: false }),
|
|
17
|
+
[DISTRIBUTED_RUNTIME_COPY]: Object.freeze({ rederivable: true, discardable: true, conflictsNeedHuman: false }),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
// The kit's own declaration — the single place these paths are classified. A
|
|
21
|
+
// distributed-runtime-copy is canonical in exactly one repo role and rederivable
|
|
22
|
+
// everywhere else; that is why the class carries `canonicalRepoRole` rather than
|
|
23
|
+
// being a bare list of names. Folding runtime copies into a plain "generated" list
|
|
24
|
+
// makes a sync loop discard canonical source in the repo that owns it.
|
|
25
|
+
export const KIT_FILE_CLASSES = Object.freeze([
|
|
26
|
+
{ pattern: '**/atelier.manifest.json', class: GENERATED_PROJECTION },
|
|
27
|
+
{ pattern: '**/atelier-ledger.html', class: GENERATED_PROJECTION },
|
|
28
|
+
{ pattern: '**/atelier-shell.js', class: GENERATED_PROJECTION },
|
|
29
|
+
{ pattern: '**/knowledge.graph.json', class: GENERATED_PROJECTION },
|
|
30
|
+
{ pattern: 'atelier-output/**', class: GENERATED_PROJECTION },
|
|
31
|
+
{ pattern: 'atelier-readers/**', class: GENERATED_PROJECTION },
|
|
32
|
+
{ pattern: 'atelier.lock.json', class: GENERATED_PROJECTION },
|
|
33
|
+
{ pattern: 'atelier-runtime.lock.json', class: GENERATED_PROJECTION },
|
|
34
|
+
].map(Object.freeze))
|
|
35
|
+
|
|
36
|
+
const isObject = (value) => value && typeof value === 'object' && !Array.isArray(value)
|
|
37
|
+
|
|
38
|
+
export function validateFileClasses(entries, { label = 'fileClasses' } = {}) {
|
|
39
|
+
const errors = []
|
|
40
|
+
if (!Array.isArray(entries)) return [`${label} must be an array`]
|
|
41
|
+
const seen = new Set()
|
|
42
|
+
for (const [index, entry] of entries.entries()) {
|
|
43
|
+
const at = `${label}[${index}]`
|
|
44
|
+
if (!isObject(entry)) {
|
|
45
|
+
errors.push(`${at} must be an object`)
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
for (const key of Object.keys(entry)) {
|
|
49
|
+
if (!['pattern', 'class', 'canonicalRepoRole', 'note'].includes(key)) errors.push(`${at} must not include additional property ${key}`)
|
|
50
|
+
}
|
|
51
|
+
const pattern = typeof entry.pattern === 'string' ? entry.pattern.trim() : ''
|
|
52
|
+
if (!pattern) errors.push(`${at}.pattern must be a non-empty string`)
|
|
53
|
+
else if (seen.has(pattern)) errors.push(`${at}.pattern duplicates an earlier entry: ${pattern}`)
|
|
54
|
+
else seen.add(pattern)
|
|
55
|
+
if (!FILE_CLASSES.includes(entry.class)) errors.push(`${at}.class must be one of ${FILE_CLASSES.join(', ')}`)
|
|
56
|
+
if (entry.class === DISTRIBUTED_RUNTIME_COPY && !(typeof entry.canonicalRepoRole === 'string' && entry.canonicalRepoRole.trim())) {
|
|
57
|
+
errors.push(`${at}.canonicalRepoRole is required for ${DISTRIBUTED_RUNTIME_COPY}; a copy must declare where it is canonical`)
|
|
58
|
+
}
|
|
59
|
+
if (entry.class !== DISTRIBUTED_RUNTIME_COPY && entry.canonicalRepoRole != null) {
|
|
60
|
+
errors.push(`${at}.canonicalRepoRole is only meaningful for ${DISTRIBUTED_RUNTIME_COPY}`)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return errors
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolve the class of a path for a given repo role.
|
|
68
|
+
*
|
|
69
|
+
* Later entries win, as in .gitignore, so an adopter can narrow a kit default by
|
|
70
|
+
* appending a more specific pattern.
|
|
71
|
+
*
|
|
72
|
+
* A distributed-runtime-copy resolves to `source` in the repo role that owns it and
|
|
73
|
+
* stays a copy everywhere else. That role-dependence is the whole point: the same
|
|
74
|
+
* path is canonical in one repo and freely rederivable in its consumers.
|
|
75
|
+
*/
|
|
76
|
+
export function classifyPath(filePath, { repoRole = null, fileClasses = KIT_FILE_CLASSES } = {}) {
|
|
77
|
+
const rel = normalizeRelPath(filePath)
|
|
78
|
+
let matched = null
|
|
79
|
+
for (const entry of fileClasses) {
|
|
80
|
+
if (matchesPathPattern(entry.pattern, rel)) matched = entry
|
|
81
|
+
}
|
|
82
|
+
if (!matched) {
|
|
83
|
+
return { path: rel, class: SOURCE, pattern: null, canonicalRepoRole: null, canonicalHere: true, declared: false, handling: FILE_CLASS_HANDLING[SOURCE] }
|
|
84
|
+
}
|
|
85
|
+
const canonicalHere = matched.class === DISTRIBUTED_RUNTIME_COPY ? repoRole === matched.canonicalRepoRole : matched.class === SOURCE
|
|
86
|
+
const resolved = matched.class === DISTRIBUTED_RUNTIME_COPY && canonicalHere ? SOURCE : matched.class
|
|
87
|
+
return {
|
|
88
|
+
path: rel,
|
|
89
|
+
class: resolved,
|
|
90
|
+
declaredClass: matched.class,
|
|
91
|
+
pattern: matched.pattern,
|
|
92
|
+
canonicalRepoRole: matched.canonicalRepoRole ?? null,
|
|
93
|
+
canonicalHere,
|
|
94
|
+
declared: true,
|
|
95
|
+
handling: FILE_CLASS_HANDLING[resolved],
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function createPathClassifier({ repoRole = null, fileClasses = KIT_FILE_CLASSES } = {}) {
|
|
100
|
+
return (filePath) => classifyPath(filePath, { repoRole, fileClasses })
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Basenames of projections a walker can skip outright, derived from the declaration
|
|
104
|
+
// rather than restated next to it.
|
|
105
|
+
export function generatedProjectionBasenames(fileClasses = KIT_FILE_CLASSES) {
|
|
106
|
+
const names = new Set()
|
|
107
|
+
for (const entry of fileClasses) {
|
|
108
|
+
if (entry.class !== GENERATED_PROJECTION) continue
|
|
109
|
+
const base = basenameOf(entry.pattern)
|
|
110
|
+
if (base && !base.includes('*')) names.add(base)
|
|
111
|
+
}
|
|
112
|
+
return names
|
|
113
|
+
}
|