@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,701 @@
|
|
|
1
|
+
import { execFileSync, spawnSync } from 'node:child_process'
|
|
2
|
+
import crypto from 'node:crypto'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { buildGraph } from '../graph/graph.mjs'
|
|
6
|
+
import { buildProjectProjection } from '../projection/project.mjs'
|
|
7
|
+
import { buildReadiness } from '../readiness/readiness.mjs'
|
|
8
|
+
import { validateJsonSchema } from '../export/atelier-export-contract.mjs'
|
|
9
|
+
import {
|
|
10
|
+
commandProject,
|
|
11
|
+
firstString,
|
|
12
|
+
parseArgs,
|
|
13
|
+
readJson,
|
|
14
|
+
resolvePathValue,
|
|
15
|
+
writeJson,
|
|
16
|
+
} from '../project/config.mjs'
|
|
17
|
+
import {
|
|
18
|
+
BOUNDARY_POLICY_SCHEMA,
|
|
19
|
+
checkBoundaryPolicy,
|
|
20
|
+
installBoundaryHooks,
|
|
21
|
+
loadBoundaryPolicy,
|
|
22
|
+
} from '../boundary/policy.mjs'
|
|
23
|
+
import { bundledMnstryReadinessPackV1 } from '../readiness-protocols/bundled-pack.mjs'
|
|
24
|
+
import { loadExtensionPacks } from '../extension-packs/loader.mjs'
|
|
25
|
+
|
|
26
|
+
export const ATELIER_LOCK_SCHEMA = 'mnstry.atelier-lock@v1'
|
|
27
|
+
export const ATELIER_MIGRATION_SCHEMA = 'mnstry.atelier-migration@v1'
|
|
28
|
+
export const LOCK_FILE = 'atelier.lock.json'
|
|
29
|
+
export const MIGRATION_CLASSES = new Set([
|
|
30
|
+
'generated_refresh',
|
|
31
|
+
'config_schema',
|
|
32
|
+
'hook_update',
|
|
33
|
+
'template_scaffold',
|
|
34
|
+
'extension_pack',
|
|
35
|
+
'semantic_review',
|
|
36
|
+
'breaking',
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
const packageRoot = path.resolve(new URL('../..', import.meta.url).pathname)
|
|
40
|
+
const packageJson = readJson(path.join(packageRoot, 'package.json'))
|
|
41
|
+
const lockSchema = readJson(path.join(packageRoot, 'contracts', 'atelier-lock.v1.schema.json'))
|
|
42
|
+
const REVIEW_MARKER_RE = /(Atelier-Boundary-Review|boundary-review)\s*:\s*(approved|reviewed)/i
|
|
43
|
+
const SEMANTIC_FIELD_RE = /(^|["'\s.-])(kg[.]audience|audience|handling|sensitivity|data_boundary)(["'\s:.-]|$)/i
|
|
44
|
+
const FORBIDDEN_AUTHORITY_FLAGS = [
|
|
45
|
+
'telemetry',
|
|
46
|
+
'egress',
|
|
47
|
+
'sendPath',
|
|
48
|
+
'runtimeMutation',
|
|
49
|
+
'runtimeImport',
|
|
50
|
+
'runtimeApply',
|
|
51
|
+
'analysisExecution',
|
|
52
|
+
'hiddenProvider',
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
const normalize = (value) => String(value ?? '').replaceAll('\\', '/').replace(/^\.\/+/, '')
|
|
56
|
+
const asArray = (value) => (Array.isArray(value) ? value : [])
|
|
57
|
+
const isObject = (value) => value && typeof value === 'object' && !Array.isArray(value)
|
|
58
|
+
|
|
59
|
+
function sha256(value) {
|
|
60
|
+
return crypto.createHash('sha256').update(typeof value === 'string' ? value : JSON.stringify(value)).digest('hex')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function git(root, args, { allowFail = true } = {}) {
|
|
64
|
+
const result = spawnSync('git', args, { cwd: root, encoding: 'utf8' })
|
|
65
|
+
if (!allowFail && result.status !== 0) throw new Error(result.stderr.trim() || `git ${args.join(' ')} failed`)
|
|
66
|
+
return result.status === 0 ? result.stdout.trim() : null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function packageGitRoot() {
|
|
70
|
+
const root = git(packageRoot, ['rev-parse', '--show-toplevel'])
|
|
71
|
+
return root && path.resolve(root) === path.resolve(packageRoot) ? root : null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function packageGitSha() {
|
|
75
|
+
return packageGitRoot() ? git(packageRoot, ['rev-parse', 'HEAD']) || null : null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function packageSource() {
|
|
79
|
+
const gitSha = packageGitSha()
|
|
80
|
+
const repository = packageGitRoot()
|
|
81
|
+
? git(packageRoot, ['config', '--get', 'remote.origin.url']) || packageJson.repository?.url || null
|
|
82
|
+
: packageJson.repository?.url || null
|
|
83
|
+
if (repository && gitSha) return { type: 'git', repository, gitSha }
|
|
84
|
+
if (repository) return { type: 'private_github', repository, gitSha: null }
|
|
85
|
+
return {
|
|
86
|
+
type: 'local_path',
|
|
87
|
+
path: '.',
|
|
88
|
+
gitSha,
|
|
89
|
+
repository,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function lockPathForProject(project) {
|
|
94
|
+
return path.join(project.configDir, LOCK_FILE)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function readOptionalJson(file) {
|
|
98
|
+
if (!fs.existsSync(file)) return null
|
|
99
|
+
return readJson(file)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function safeRead(file) {
|
|
103
|
+
try {
|
|
104
|
+
return fs.readFileSync(file, 'utf8')
|
|
105
|
+
} catch {
|
|
106
|
+
return ''
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function loadPolicyForLock(project) {
|
|
111
|
+
const loaded = loadBoundaryPolicy(project)
|
|
112
|
+
if (!loaded.ok) return { policy: null, policyPath: loaded.policyPath, digest: null, errors: loaded.errors }
|
|
113
|
+
return {
|
|
114
|
+
policy: loaded.policy,
|
|
115
|
+
policyPath: loaded.policyPath,
|
|
116
|
+
digest: `sha256:${sha256(`${JSON.stringify(loaded.policy, null, 2)}\n`)}`,
|
|
117
|
+
errors: [],
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function buildAtelierLock({ project, templateId = 'existing-workspace', appliedMigrations = [], packs = [] } = {}) {
|
|
122
|
+
const policy = loadPolicyForLock(project)
|
|
123
|
+
return {
|
|
124
|
+
schema: ATELIER_LOCK_SCHEMA,
|
|
125
|
+
generatedAt: new Date().toISOString(),
|
|
126
|
+
package: {
|
|
127
|
+
name: packageJson.name,
|
|
128
|
+
version: packageJson.version,
|
|
129
|
+
source: packageSource(),
|
|
130
|
+
},
|
|
131
|
+
template: {
|
|
132
|
+
id: templateId,
|
|
133
|
+
version: packageJson.version,
|
|
134
|
+
},
|
|
135
|
+
runtime: {
|
|
136
|
+
node: {
|
|
137
|
+
range: packageJson.engines?.node || '>=22.18.0 <23',
|
|
138
|
+
},
|
|
139
|
+
telemetry: 'none',
|
|
140
|
+
egress: 'local-only',
|
|
141
|
+
mutation: 'not-implemented',
|
|
142
|
+
runtimeImport: false,
|
|
143
|
+
analysisExecution: false,
|
|
144
|
+
},
|
|
145
|
+
contracts: {
|
|
146
|
+
projectConfig: 'mnstry.atelier-project-config@v1',
|
|
147
|
+
knowledgeGraph: 'mnstry.atelier-knowledge-graph@v1',
|
|
148
|
+
boundaryPolicy: BOUNDARY_POLICY_SCHEMA,
|
|
149
|
+
exportContract: 'atelier-export@v1',
|
|
150
|
+
},
|
|
151
|
+
// Digest asymmetry, on purpose: the bundled pack is an in-memory object,
|
|
152
|
+
// so its digest covers JSON.stringify of that object; loaded file packs
|
|
153
|
+
// carry raw-byte digests computed by the loader (computePackDigest) over
|
|
154
|
+
// the manifest bytes plus every referenced protocol file's bytes, so any
|
|
155
|
+
// on-disk edit to the manifest or a protocol is visible byte-for-byte.
|
|
156
|
+
extensionPacks: [
|
|
157
|
+
{
|
|
158
|
+
id: bundledMnstryReadinessPackV1.id,
|
|
159
|
+
version: bundledMnstryReadinessPackV1.version,
|
|
160
|
+
digest: `sha256:${sha256(bundledMnstryReadinessPackV1)}`,
|
|
161
|
+
},
|
|
162
|
+
...packs.map((pack) => ({ id: pack.id, version: pack.version, digest: pack.digest })),
|
|
163
|
+
],
|
|
164
|
+
boundaryPolicy: {
|
|
165
|
+
path: policy.policyPath ? path.relative(project.configDir, policy.policyPath) : null,
|
|
166
|
+
digest: policy.digest,
|
|
167
|
+
snapshot: policy.policy,
|
|
168
|
+
},
|
|
169
|
+
lastSuccessfulUpgrade: null,
|
|
170
|
+
appliedMigrations,
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function writeAtelierLock({ project, templateId = 'existing-workspace' } = {}) {
|
|
175
|
+
const lockPath = lockPathForProject(project)
|
|
176
|
+
// Packs load in throwing mode on purpose: a broken extension pack cannot be
|
|
177
|
+
// locked. Lock verification is disabled for this one load so re-pinning is
|
|
178
|
+
// not blocked by the very drift it records; the previous lock is never
|
|
179
|
+
// moved or set aside, so it exists at every observable point, and writeJson
|
|
180
|
+
// replaces it atomically (temp file + renameSync). A load failure throws
|
|
181
|
+
// before any write and leaves the previous lock untouched.
|
|
182
|
+
const { packs } = loadExtensionPacks(project, { verifyLock: false })
|
|
183
|
+
const lock = buildAtelierLock({ project, templateId, packs })
|
|
184
|
+
writeJson(lockPath, lock)
|
|
185
|
+
return lock
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function loadAtelierLock(project) {
|
|
189
|
+
const lockPath = lockPathForProject(project)
|
|
190
|
+
return { lockPath, lock: readOptionalJson(lockPath) }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function validateMigrationRecord(migration) {
|
|
194
|
+
const errors = []
|
|
195
|
+
if (!isObject(migration)) return ['migration must be an object']
|
|
196
|
+
if (migration.schema !== ATELIER_MIGRATION_SCHEMA) errors.push(`schema must be ${ATELIER_MIGRATION_SCHEMA}`)
|
|
197
|
+
if (!firstString(migration.id)) errors.push('id is required')
|
|
198
|
+
if (!MIGRATION_CLASSES.has(migration.class)) errors.push('class is invalid')
|
|
199
|
+
if (!firstString(migration.title)) errors.push('title is required')
|
|
200
|
+
if (!firstString(migration.description)) errors.push('description is required')
|
|
201
|
+
if (!Array.isArray(migration.files)) errors.push('files must be a list')
|
|
202
|
+
if (!Array.isArray(migration.requiredPostChecks)) errors.push('requiredPostChecks must be a list')
|
|
203
|
+
for (const flag of FORBIDDEN_AUTHORITY_FLAGS) {
|
|
204
|
+
if (migration.authority?.[flag] === true) errors.push(`migration authority.${flag} must not be true`)
|
|
205
|
+
}
|
|
206
|
+
if (migration.class === 'breaking' && migration.explicitConfirmationRequired !== true) {
|
|
207
|
+
errors.push('breaking migrations require explicitConfirmationRequired true')
|
|
208
|
+
}
|
|
209
|
+
return errors
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export const BASE_MIGRATIONS = [
|
|
213
|
+
{
|
|
214
|
+
schema: ATELIER_MIGRATION_SCHEMA,
|
|
215
|
+
id: 'atelier-lock-create@0.1.0-alpha.0',
|
|
216
|
+
from: '*',
|
|
217
|
+
to: packageJson.version,
|
|
218
|
+
class: 'config_schema',
|
|
219
|
+
title: 'Create Atelier lockfile',
|
|
220
|
+
description: 'Record the installed package, template, contract versions, and boundary policy digest.',
|
|
221
|
+
files: [LOCK_FILE],
|
|
222
|
+
safety: 'safe',
|
|
223
|
+
reviewMarkerRequired: false,
|
|
224
|
+
explicitConfirmationRequired: false,
|
|
225
|
+
apply: 'writeLock',
|
|
226
|
+
requiredPostChecks: ['lock check'],
|
|
227
|
+
authority: safeAuthority(),
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
schema: ATELIER_MIGRATION_SCHEMA,
|
|
231
|
+
id: 'generated-refresh@0.1.0-alpha.0',
|
|
232
|
+
from: '*',
|
|
233
|
+
to: packageJson.version,
|
|
234
|
+
class: 'generated_refresh',
|
|
235
|
+
title: 'Refresh generated Atelier read models',
|
|
236
|
+
description: 'Regenerate graph, projection, manifest, and readiness artifacts from source files.',
|
|
237
|
+
files: ['knowledge graph output', 'project projection output', 'readiness output'],
|
|
238
|
+
safety: 'safe-generated',
|
|
239
|
+
reviewMarkerRequired: false,
|
|
240
|
+
explicitConfirmationRequired: false,
|
|
241
|
+
apply: 'refreshGenerated',
|
|
242
|
+
requiredPostChecks: ['graph --check', 'project --check', 'readiness --check'],
|
|
243
|
+
authority: safeAuthority(),
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
schema: ATELIER_MIGRATION_SCHEMA,
|
|
247
|
+
id: 'boundary-hook-update@0.1.0-alpha.0',
|
|
248
|
+
from: '*',
|
|
249
|
+
to: packageJson.version,
|
|
250
|
+
class: 'hook_update',
|
|
251
|
+
title: 'Install or refresh boundary hooks',
|
|
252
|
+
description: 'Install staged boundary guards without overwriting unrelated user hooks.',
|
|
253
|
+
files: ['.git/hooks/pre-commit', '.git/hooks/pre-push'],
|
|
254
|
+
safety: 'guarded-local',
|
|
255
|
+
reviewMarkerRequired: false,
|
|
256
|
+
explicitConfirmationRequired: false,
|
|
257
|
+
apply: 'installBoundaryHooks',
|
|
258
|
+
requiredPostChecks: ['boundary check --staged'],
|
|
259
|
+
authority: safeAuthority(),
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
schema: ATELIER_MIGRATION_SCHEMA,
|
|
263
|
+
id: 'template-scaffold@0.1.0-alpha.0',
|
|
264
|
+
from: '*',
|
|
265
|
+
to: packageJson.version,
|
|
266
|
+
class: 'template_scaffold',
|
|
267
|
+
title: 'Add missing upgrade documentation scaffold',
|
|
268
|
+
description: 'Add upgrade guidance only when the target file is absent.',
|
|
269
|
+
files: ['docs/upgrade.md'],
|
|
270
|
+
safety: 'additive',
|
|
271
|
+
reviewMarkerRequired: false,
|
|
272
|
+
explicitConfirmationRequired: false,
|
|
273
|
+
apply: 'templateScaffold',
|
|
274
|
+
requiredPostChecks: ['lock check'],
|
|
275
|
+
authority: safeAuthority(),
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
schema: ATELIER_MIGRATION_SCHEMA,
|
|
279
|
+
id: 'extension-pack-sync@0.2.0-alpha.0',
|
|
280
|
+
from: '*',
|
|
281
|
+
to: packageJson.version,
|
|
282
|
+
class: 'extension_pack',
|
|
283
|
+
title: 'Sync extension pack lock entries',
|
|
284
|
+
description: 'Re-verify declared extension packs and record their digests in the lock.',
|
|
285
|
+
files: [LOCK_FILE],
|
|
286
|
+
safety: 'safe',
|
|
287
|
+
reviewMarkerRequired: false,
|
|
288
|
+
explicitConfirmationRequired: false,
|
|
289
|
+
apply: 'syncExtensionPacks',
|
|
290
|
+
requiredPostChecks: ['lock check'],
|
|
291
|
+
authority: safeAuthority(),
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
schema: ATELIER_MIGRATION_SCHEMA,
|
|
295
|
+
id: 'boundary-policy-schema-pin@0.1.0-alpha.0',
|
|
296
|
+
from: '*',
|
|
297
|
+
to: packageJson.version,
|
|
298
|
+
class: 'config_schema',
|
|
299
|
+
title: 'Pin boundary policy schema',
|
|
300
|
+
description: 'Verify boundary policies remain on the current MNSTRY Atelier boundary schema.',
|
|
301
|
+
files: ['boundary-policy.v1.json'],
|
|
302
|
+
safety: 'check-only',
|
|
303
|
+
reviewMarkerRequired: false,
|
|
304
|
+
explicitConfirmationRequired: false,
|
|
305
|
+
apply: 'validateBoundarySchema',
|
|
306
|
+
requiredPostChecks: ['boundary check'],
|
|
307
|
+
authority: safeAuthority(),
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
schema: ATELIER_MIGRATION_SCHEMA,
|
|
311
|
+
id: 'semantic-review-guard@0.1.0-alpha.0',
|
|
312
|
+
from: '*',
|
|
313
|
+
to: packageJson.version,
|
|
314
|
+
class: 'semantic_review',
|
|
315
|
+
title: 'Verify semantic review markers',
|
|
316
|
+
description: 'Fail closed when staged audience or sensitivity changes lack an approved review marker.',
|
|
317
|
+
files: ['*.md', '*.kg.json'],
|
|
318
|
+
safety: 'semantic-fail-closed',
|
|
319
|
+
reviewMarkerRequired: true,
|
|
320
|
+
explicitConfirmationRequired: false,
|
|
321
|
+
apply: 'semanticReviewCheck',
|
|
322
|
+
requiredPostChecks: ['boundary check --staged'],
|
|
323
|
+
authority: safeAuthority(),
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
schema: ATELIER_MIGRATION_SCHEMA,
|
|
327
|
+
id: 'breaking-placeholder@0.1.0-alpha.0',
|
|
328
|
+
from: '<0.1.0-alpha.0',
|
|
329
|
+
to: packageJson.version,
|
|
330
|
+
class: 'breaking',
|
|
331
|
+
title: 'Breaking migration placeholder',
|
|
332
|
+
description: 'Reserved fixture for explicit-confirmation behavior; not selected for current locks.',
|
|
333
|
+
files: [],
|
|
334
|
+
safety: 'breaking',
|
|
335
|
+
reviewMarkerRequired: false,
|
|
336
|
+
explicitConfirmationRequired: true,
|
|
337
|
+
apply: 'none',
|
|
338
|
+
requiredPostChecks: [],
|
|
339
|
+
authority: safeAuthority(),
|
|
340
|
+
active: false,
|
|
341
|
+
},
|
|
342
|
+
]
|
|
343
|
+
|
|
344
|
+
function safeAuthority() {
|
|
345
|
+
return {
|
|
346
|
+
telemetry: false,
|
|
347
|
+
egress: false,
|
|
348
|
+
sendPath: false,
|
|
349
|
+
runtimeMutation: false,
|
|
350
|
+
runtimeImport: false,
|
|
351
|
+
runtimeApply: false,
|
|
352
|
+
analysisExecution: false,
|
|
353
|
+
hiddenProvider: false,
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function validateMigrationRegistry(migrations = BASE_MIGRATIONS) {
|
|
358
|
+
return migrations.flatMap((migration) => validateMigrationRecord(migration).map((message) => `${migration.id ?? '(unknown migration)'}: ${message}`))
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function currentState(project) {
|
|
362
|
+
const { lock, lockPath } = loadAtelierLock(project)
|
|
363
|
+
const loadedPolicy = loadBoundaryPolicy(project)
|
|
364
|
+
const boundaryReport = loadedPolicy.ok ? checkBoundaryPolicy({ project, policy: loadedPolicy.policy, staged: true }) : null
|
|
365
|
+
const gitStatus = projectGitStatus(project)
|
|
366
|
+
return { lock, lockPath, loadedPolicy, boundaryReport, gitStatus }
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function selectedMigrations({ lock, migrations = BASE_MIGRATIONS } = {}) {
|
|
370
|
+
const selected = []
|
|
371
|
+
if (!lock) {
|
|
372
|
+
selected.push(migrations.find((migration) => migration.id.startsWith('atelier-lock-create@')))
|
|
373
|
+
selected.push(migrations.find((migration) => migration.id.startsWith('generated-refresh@')))
|
|
374
|
+
return selected.filter(Boolean)
|
|
375
|
+
}
|
|
376
|
+
if (lock.package?.version !== packageJson.version || lock.package?.name !== packageJson.name) {
|
|
377
|
+
selected.push(...migrations.filter((migration) => migration.active !== false && ['generated_refresh', 'hook_update', 'template_scaffold', 'config_schema', 'extension_pack', 'breaking'].includes(migration.class)))
|
|
378
|
+
}
|
|
379
|
+
return selected.filter(Boolean)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function weakeningFindings({ lock, currentPolicy }) {
|
|
383
|
+
const findings = []
|
|
384
|
+
const previous = lock?.boundaryPolicy?.snapshot
|
|
385
|
+
if (!previous || !currentPolicy) return findings
|
|
386
|
+
if (previous.mode === 'strict' && currentPolicy.mode !== 'strict') findings.push('boundary policy mode changed from strict to non-strict')
|
|
387
|
+
for (const [repoName, previousRepo] of Object.entries(previous.repos ?? {})) {
|
|
388
|
+
const currentRepo = currentPolicy.repos?.[repoName]
|
|
389
|
+
if (!currentRepo) {
|
|
390
|
+
findings.push(`boundary policy removed repo ${repoName}`)
|
|
391
|
+
continue
|
|
392
|
+
}
|
|
393
|
+
for (const audience of asArray(previousRepo.forbiddenAudiences)) {
|
|
394
|
+
if (!asArray(currentRepo.forbiddenAudiences).includes(audience)) {
|
|
395
|
+
findings.push(`boundary policy removed forbidden audience ${audience} from ${repoName}`)
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (previousRepo.kind === 'shared' && currentRepo.kind !== 'shared') {
|
|
399
|
+
findings.push(`boundary policy changed ${repoName} from shared to ${currentRepo.kind}`)
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return findings
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function stagedSemanticFindings(project) {
|
|
406
|
+
const findings = []
|
|
407
|
+
for (const repo of project.repos ?? []) {
|
|
408
|
+
if (!isGitRepo(repo.path)) continue
|
|
409
|
+
const result = spawnSync('git', ['-C', repo.path, 'diff', '--cached', '--unified=0', '--', '*.md', '*.kg.json'], { encoding: 'utf8' })
|
|
410
|
+
if (result.status !== 0 || !result.stdout.trim()) continue
|
|
411
|
+
const diff = result.stdout
|
|
412
|
+
const changedSemantic = diff
|
|
413
|
+
.split('\n')
|
|
414
|
+
.some((line) => /^[+-](?![+-])/.test(line) && SEMANTIC_FIELD_RE.test(line))
|
|
415
|
+
if (changedSemantic && !REVIEW_MARKER_RE.test(diff)) findings.push(`${repo.name}: staged semantic field change needs Atelier-Boundary-Review marker`)
|
|
416
|
+
}
|
|
417
|
+
return findings
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export function planUpgrade({ project, migrations = BASE_MIGRATIONS, allowDirtyGenerated = false, confirmBreaking = [] } = {}) {
|
|
421
|
+
const state = currentState(project)
|
|
422
|
+
const registryErrors = validateMigrationRegistry(migrations)
|
|
423
|
+
const planned = selectedMigrations({ lock: state.lock, migrations })
|
|
424
|
+
const blockers = []
|
|
425
|
+
const warnings = []
|
|
426
|
+
const requiredConfirmations = []
|
|
427
|
+
|
|
428
|
+
blockers.push(...registryErrors)
|
|
429
|
+
if (!state.loadedPolicy.ok) blockers.push(...state.loadedPolicy.errors)
|
|
430
|
+
if (state.boundaryReport && !state.boundaryReport.ok) blockers.push(...state.boundaryReport.errors.map((item) => item.message))
|
|
431
|
+
blockers.push(...weakeningFindings({ lock: state.lock, currentPolicy: state.loadedPolicy.policy }))
|
|
432
|
+
blockers.push(...stagedSemanticFindings(project))
|
|
433
|
+
|
|
434
|
+
const dirtyAuthored = state.gitStatus.flatMap((repo) => repo.dirty.filter((item) => !item.generated).map((item) => `${repo.name}/${item.path}`))
|
|
435
|
+
const dirtyGenerated = state.gitStatus.flatMap((repo) => repo.dirty.filter((item) => item.generated).map((item) => `${repo.name}/${item.path}`))
|
|
436
|
+
if (dirtyAuthored.length) blockers.push(`dirty authored files block upgrade apply: ${dirtyAuthored.join(', ')}`)
|
|
437
|
+
if (dirtyGenerated.length && !allowDirtyGenerated) warnings.push(`generated files are dirty; apply requires --allow-dirty-generated: ${dirtyGenerated.join(', ')}`)
|
|
438
|
+
|
|
439
|
+
for (const migration of planned) {
|
|
440
|
+
if (migration.class === 'breaking' && !confirmBreaking.includes(migration.id)) {
|
|
441
|
+
requiredConfirmations.push(migration.id)
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
ok: blockers.length === 0,
|
|
447
|
+
schema: 'mnstry.atelier-upgrade-plan@v1',
|
|
448
|
+
generatedAt: new Date().toISOString(),
|
|
449
|
+
package: {
|
|
450
|
+
name: packageJson.name,
|
|
451
|
+
version: packageJson.version,
|
|
452
|
+
gitSha: packageGitSha(),
|
|
453
|
+
},
|
|
454
|
+
lock: {
|
|
455
|
+
path: state.lockPath,
|
|
456
|
+
present: Boolean(state.lock),
|
|
457
|
+
version: state.lock?.package?.version ?? null,
|
|
458
|
+
packageName: state.lock?.package?.name ?? null,
|
|
459
|
+
},
|
|
460
|
+
migrations: planned,
|
|
461
|
+
blockers,
|
|
462
|
+
warnings,
|
|
463
|
+
requiredConfirmations,
|
|
464
|
+
dirty: state.gitStatus,
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function isGitRepo(repoPath) {
|
|
469
|
+
return Boolean(repoPath && fs.existsSync(path.join(repoPath, '.git')))
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function projectGitStatus(project) {
|
|
473
|
+
return (project.repos ?? []).map((repo) => {
|
|
474
|
+
if (!isGitRepo(repo.path)) return { name: repo.name, path: repo.path, git: false, dirty: [] }
|
|
475
|
+
const result = spawnSync('git', ['-C', repo.path, 'status', '--porcelain'], { encoding: 'utf8' })
|
|
476
|
+
const dirty = result.status === 0
|
|
477
|
+
? result.stdout
|
|
478
|
+
.split('\n')
|
|
479
|
+
.map((line) => line.trimEnd())
|
|
480
|
+
.filter(Boolean)
|
|
481
|
+
.map((line) => {
|
|
482
|
+
const rel = normalize(line.slice(3).replace(/^"|"$/g, ''))
|
|
483
|
+
return { path: rel, status: line.slice(0, 2), generated: isGeneratedPath(project, repo, rel) }
|
|
484
|
+
})
|
|
485
|
+
: []
|
|
486
|
+
return { name: repo.name, path: repo.path, git: true, dirty }
|
|
487
|
+
})
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function isGeneratedPath(project, repo, rel) {
|
|
491
|
+
const generatedRoots = [
|
|
492
|
+
'atelier-output',
|
|
493
|
+
'.mnstry-local',
|
|
494
|
+
relativeInside(repo.path, project.outputRoot),
|
|
495
|
+
relativeInside(repo.path, path.dirname(project.graphPath)),
|
|
496
|
+
relativeInside(repo.path, path.dirname(project.readinessPath)),
|
|
497
|
+
].filter(Boolean).map(normalize)
|
|
498
|
+
const generatedFiles = [
|
|
499
|
+
relativeInside(repo.path, project.graphPath),
|
|
500
|
+
relativeInside(repo.path, project.readinessPath),
|
|
501
|
+
'knowledge.graph.json',
|
|
502
|
+
'atelier-readiness.json',
|
|
503
|
+
'atelier.manifest.json',
|
|
504
|
+
].filter(Boolean).map(normalize)
|
|
505
|
+
return generatedFiles.includes(rel) || generatedRoots.some((root) => root && (rel === root || rel.startsWith(`${root}/`)))
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function relativeInside(root, target) {
|
|
509
|
+
if (!root || !target) return null
|
|
510
|
+
const rel = path.relative(root, target)
|
|
511
|
+
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return null
|
|
512
|
+
return rel
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function runGeneratedRefresh(project) {
|
|
516
|
+
const graph = buildGraph(project)
|
|
517
|
+
writeJson(project.graphPath, graph)
|
|
518
|
+
const projection = buildProjectProjection(project)
|
|
519
|
+
fs.mkdirSync(path.dirname(projection.output), { recursive: true })
|
|
520
|
+
fs.writeFileSync(projection.output, projection.html)
|
|
521
|
+
writeJson(path.join(project.outputRoot, 'atelier.manifest.json'), {
|
|
522
|
+
schema: 'mnstry.atelier-manifest@v1',
|
|
523
|
+
generatedAt: 'deterministic',
|
|
524
|
+
graphPath: project.graphPath,
|
|
525
|
+
entry: 'index.html',
|
|
526
|
+
nodes: projection.graph.nodes.map((node) => ({ id: node.id, title: node.title, audience: node.audience, path: node.path })),
|
|
527
|
+
})
|
|
528
|
+
writeJson(project.readinessPath, buildReadiness({ project, graph }))
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function runTemplateScaffold(project) {
|
|
532
|
+
const docsDir = path.join(project.configDir, 'docs')
|
|
533
|
+
const target = path.join(docsDir, 'upgrade.md')
|
|
534
|
+
if (fs.existsSync(target)) return
|
|
535
|
+
fs.mkdirSync(docsDir, { recursive: true })
|
|
536
|
+
fs.writeFileSync(target, `# Atelier Upgrade\n\nRun \`atelier upgrade --dry-run --project ./atelier.project.json\` before applying upstream Atelier changes.\n`)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export function applyMigration(project, migration) {
|
|
540
|
+
if (migration.apply === 'writeLock') {
|
|
541
|
+
writeAtelierLock({ project })
|
|
542
|
+
} else if (migration.apply === 'refreshGenerated') {
|
|
543
|
+
runGeneratedRefresh(project)
|
|
544
|
+
} else if (migration.apply === 'installBoundaryHooks') {
|
|
545
|
+
installBoundaryHooks({ project, force: false })
|
|
546
|
+
} else if (migration.apply === 'templateScaffold') {
|
|
547
|
+
runTemplateScaffold(project)
|
|
548
|
+
} else if (migration.apply === 'validateBoundarySchema') {
|
|
549
|
+
const loaded = loadBoundaryPolicy(project)
|
|
550
|
+
if (!loaded.ok) throw new Error(loaded.errors.join('\n'))
|
|
551
|
+
} else if (migration.apply === 'syncExtensionPacks') {
|
|
552
|
+
// Re-verify declared packs and re-pin their digests. writeAtelierLock
|
|
553
|
+
// loads packs in throwing mode, so a broken pack fails the migration
|
|
554
|
+
// closed instead of being recorded; drifted digests are re-pinned for the
|
|
555
|
+
// 'lock check' post-check.
|
|
556
|
+
writeAtelierLock({ project })
|
|
557
|
+
} else if (migration.apply === 'semanticReviewCheck') {
|
|
558
|
+
const findings = stagedSemanticFindings(project)
|
|
559
|
+
if (findings.length) throw new Error(findings.join('\n'))
|
|
560
|
+
} else if (migration.apply === 'none') {
|
|
561
|
+
// explicit no-op reserved for inactive placeholder migrations
|
|
562
|
+
} else {
|
|
563
|
+
throw new Error(`migration ${migration.id} has unrecognized apply action "${migration.apply}"`)
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function checkoutBranch(project, branch) {
|
|
568
|
+
const repos = (project.repos ?? []).filter((repo) => isGitRepo(repo.path))
|
|
569
|
+
for (const repo of repos) {
|
|
570
|
+
const current = git(repo.path, ['branch', '--show-current']) || ''
|
|
571
|
+
if (current === branch) continue
|
|
572
|
+
const exists = git(repo.path, ['rev-parse', '--verify', branch])
|
|
573
|
+
if (exists) git(repo.path, ['switch', branch], { allowFail: false })
|
|
574
|
+
else git(repo.path, ['switch', '-c', branch], { allowFail: false })
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function commitRepos(project, message) {
|
|
579
|
+
for (const repo of project.repos ?? []) {
|
|
580
|
+
if (!isGitRepo(repo.path)) continue
|
|
581
|
+
git(repo.path, ['add', '-A'], { allowFail: false })
|
|
582
|
+
const diff = git(repo.path, ['diff', '--cached', '--name-only'])
|
|
583
|
+
if (!diff) continue
|
|
584
|
+
git(repo.path, ['commit', '--no-verify', '-m', message], { allowFail: false })
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export function applyUpgrade({ project, branch, allowDirtyGenerated = false, confirmBreaking = [] } = {}) {
|
|
589
|
+
const plan = planUpgrade({ project, allowDirtyGenerated, confirmBreaking })
|
|
590
|
+
const dirtyGeneratedOnly = plan.dirty.every((repo) => repo.dirty.every((item) => item.generated))
|
|
591
|
+
if (!plan.ok) throw new Error(plan.blockers.join('\n'))
|
|
592
|
+
if (!dirtyGeneratedOnly && plan.dirty.some((repo) => repo.dirty.length)) throw new Error('upgrade apply requires clean repos')
|
|
593
|
+
if (plan.requiredConfirmations.length) throw new Error(`breaking migrations require confirmation: ${plan.requiredConfirmations.join(', ')}`)
|
|
594
|
+
if (branch) checkoutBranch(project, branch)
|
|
595
|
+
for (const migration of plan.migrations) applyMigration(project, migration)
|
|
596
|
+
if (!plan.migrations.some((migration) => migration.apply === 'writeLock')) writeAtelierLock({ project })
|
|
597
|
+
const nextLock = loadAtelierLock(project).lock || writeAtelierLock({ project })
|
|
598
|
+
nextLock.lastSuccessfulUpgrade = new Date().toISOString()
|
|
599
|
+
nextLock.appliedMigrations = [
|
|
600
|
+
...asArray(nextLock.appliedMigrations),
|
|
601
|
+
...plan.migrations.map((migration) => ({ id: migration.id, hash: sha256(migration), appliedAt: nextLock.lastSuccessfulUpgrade })),
|
|
602
|
+
]
|
|
603
|
+
writeJson(lockPathForProject(project), nextLock)
|
|
604
|
+
runGeneratedRefresh(project)
|
|
605
|
+
commitRepos(project, `Apply MNSTRY Atelier upgrade ${packageJson.version}`)
|
|
606
|
+
return { ok: true, plan, lock: nextLock }
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export function checkAtelierLock(project) {
|
|
610
|
+
const { lock, lockPath } = loadAtelierLock(project)
|
|
611
|
+
const errors = []
|
|
612
|
+
if (!lock) errors.push(`lockfile not found: ${lockPath}`)
|
|
613
|
+
if (lock && lock.schema !== ATELIER_LOCK_SCHEMA) errors.push(`lock schema must be ${ATELIER_LOCK_SCHEMA}`)
|
|
614
|
+
if (lock) errors.push(...validateJsonSchema(lockSchema, lock).map((error) => `lock schema ${error}`))
|
|
615
|
+
if (lock && lock.package?.name !== packageJson.name) errors.push(`lock package name ${lock.package?.name} does not match ${packageJson.name}`)
|
|
616
|
+
if (lock && lock.package?.version !== packageJson.version) errors.push(`lock package version ${lock.package?.version} does not match ${packageJson.version}`)
|
|
617
|
+
if (lock && lock.contracts?.boundaryPolicy !== BOUNDARY_POLICY_SCHEMA) errors.push(`lock boundary policy contract must be ${BOUNDARY_POLICY_SCHEMA}`)
|
|
618
|
+
const loadedPolicy = loadPolicyForLock(project)
|
|
619
|
+
if (lock?.boundaryPolicy?.digest && loadedPolicy.digest && lock.boundaryPolicy.digest !== loadedPolicy.digest) {
|
|
620
|
+
errors.push('boundary policy digest has changed; run upgrade --dry-run before applying')
|
|
621
|
+
}
|
|
622
|
+
if (lock) {
|
|
623
|
+
// Pack drift, both directions. The loader treats a declared pack missing
|
|
624
|
+
// from the lock as a warning; lock check turns it into an error.
|
|
625
|
+
const packReport = loadExtensionPacks(project, { report: true })
|
|
626
|
+
for (const item of packReport.errors) {
|
|
627
|
+
errors.push(item.packId ? `extension pack ${item.packId}: ${item.message}` : `extension packs: ${item.message}`)
|
|
628
|
+
}
|
|
629
|
+
const lockPacks = asArray(lock.extensionPacks)
|
|
630
|
+
for (const pack of packReport.packs) {
|
|
631
|
+
const entry = lockPacks.find((item) => item?.id === pack.id)
|
|
632
|
+
if (!entry) {
|
|
633
|
+
errors.push(`extension pack ${pack.id} is not recorded in the lock; run upgrade --dry-run before applying`)
|
|
634
|
+
} else if (entry.version !== pack.version || entry.digest !== pack.digest) {
|
|
635
|
+
errors.push(`extension pack ${pack.id} has drifted from the lock; run upgrade --dry-run before applying`)
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
// Declared covers loaded packs, disabled (skipped) packs, and packs whose
|
|
639
|
+
// load failed — those failures are already reported above.
|
|
640
|
+
const declaredIds = new Set([
|
|
641
|
+
...packReport.packs.map((pack) => pack.id),
|
|
642
|
+
...packReport.skipped.map((item) => item.packId),
|
|
643
|
+
...packReport.errors.map((item) => item.packId).filter(Boolean),
|
|
644
|
+
])
|
|
645
|
+
for (const entry of lockPacks) {
|
|
646
|
+
if (!isObject(entry) || entry.id === bundledMnstryReadinessPackV1.id) continue
|
|
647
|
+
if (!declaredIds.has(entry.id)) {
|
|
648
|
+
errors.push(`lock records extension pack ${entry.id} that is no longer declared; run upgrade --dry-run before applying`)
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return { ok: errors.length === 0, lockPath, errors, lock }
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export function runLockCommand(argv = process.argv.slice(2)) {
|
|
656
|
+
const args = parseArgs(argv)
|
|
657
|
+
const subcommand = args._[0] || 'check'
|
|
658
|
+
const project = commandProject({ argv })
|
|
659
|
+
if (subcommand === 'write') {
|
|
660
|
+
const lock = writeAtelierLock({ project, templateId: firstString(args.template, args['template-id'], project.config?.template?.id) || 'existing-workspace' })
|
|
661
|
+
console.log(JSON.stringify({ ok: true, path: lockPathForProject(project), lock }, null, 2))
|
|
662
|
+
process.exit(0)
|
|
663
|
+
}
|
|
664
|
+
if (subcommand === 'check') {
|
|
665
|
+
const report = checkAtelierLock(project)
|
|
666
|
+
console.log(JSON.stringify(report, null, 2))
|
|
667
|
+
process.exit(report.ok ? 0 : 1)
|
|
668
|
+
}
|
|
669
|
+
console.error(`Unknown lock command: ${subcommand}`)
|
|
670
|
+
process.exit(1)
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
export function runUpgradeCommand(argv = process.argv.slice(2)) {
|
|
674
|
+
const args = parseArgs(argv)
|
|
675
|
+
const project = commandProject({ argv })
|
|
676
|
+
const confirmBreaking = asArray(args['confirm-breaking']).concat(firstString(args['confirm-breaking']) ? [String(args['confirm-breaking'])] : [])
|
|
677
|
+
const allowDirtyGenerated = Boolean(args['allow-dirty-generated'])
|
|
678
|
+
if (args.check) {
|
|
679
|
+
const report = checkAtelierLock(project)
|
|
680
|
+
console.log(JSON.stringify(report, null, 2))
|
|
681
|
+
process.exit(report.ok ? 0 : 1)
|
|
682
|
+
}
|
|
683
|
+
if (args.apply) {
|
|
684
|
+
try {
|
|
685
|
+
const report = applyUpgrade({
|
|
686
|
+
project,
|
|
687
|
+
branch: firstString(args.branch) || `codex/atelier-upgrade-${new Date().toISOString().slice(0, 10).replaceAll('-', '')}`,
|
|
688
|
+
allowDirtyGenerated,
|
|
689
|
+
confirmBreaking,
|
|
690
|
+
})
|
|
691
|
+
console.log(JSON.stringify(report, null, 2))
|
|
692
|
+
process.exit(0)
|
|
693
|
+
} catch (error) {
|
|
694
|
+
console.error(error.message)
|
|
695
|
+
process.exit(1)
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
const plan = planUpgrade({ project, allowDirtyGenerated, confirmBreaking })
|
|
699
|
+
console.log(JSON.stringify(plan, null, 2))
|
|
700
|
+
process.exit(plan.blockers.length ? 1 : 0)
|
|
701
|
+
}
|