@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,31 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
const GIT_IGNORE_TIMEOUT_MS = 8000
|
|
4
|
+
|
|
5
|
+
// One batched call per root. Per-file `git check-ignore` is correct but too slow
|
|
6
|
+
// once a workspace has thousands of files, which is how adopters end up skipping
|
|
7
|
+
// the check entirely and shipping a machine-dependent census.
|
|
8
|
+
export function gitIgnoredPaths(root) {
|
|
9
|
+
const ignored = new Set()
|
|
10
|
+
if (!root) return ignored
|
|
11
|
+
const result = spawnSync('git', ['-C', root, 'ls-files', '--others', '--ignored', '--exclude-standard', '--directory', '-z'], {
|
|
12
|
+
encoding: 'utf8',
|
|
13
|
+
timeout: GIT_IGNORE_TIMEOUT_MS,
|
|
14
|
+
})
|
|
15
|
+
if (result.status !== 0 || !result.stdout) return ignored
|
|
16
|
+
for (const entry of result.stdout.split('\0')) {
|
|
17
|
+
if (entry) ignored.add(entry.replace(/\/+$/, ''))
|
|
18
|
+
}
|
|
19
|
+
return ignored
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const includeEverything = () => false
|
|
23
|
+
|
|
24
|
+
// `--directory` collapses a fully ignored directory to a single entry, so callers
|
|
25
|
+
// must test each directory before descending into it; a file deep inside an
|
|
26
|
+
// ignored directory is not itself listed.
|
|
27
|
+
export function gitIgnoreFilter(root) {
|
|
28
|
+
const ignored = gitIgnoredPaths(root)
|
|
29
|
+
if (!ignored.size) return includeEverything
|
|
30
|
+
return (rel) => ignored.has(String(rel ?? '').replace(/\/+$/, ''))
|
|
31
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
export const normalizeRelPath = (value) =>
|
|
4
|
+
String(value ?? '')
|
|
5
|
+
.replaceAll('\\', '/')
|
|
6
|
+
.replace(/^\.\/+/, '')
|
|
7
|
+
|
|
8
|
+
const escapeRe = (value) => String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
9
|
+
|
|
10
|
+
// Shared by the boundary policy and the file-class resolver. Both used to carry
|
|
11
|
+
// their own copy; two glob dialects in one kit is exactly the drift this file exists
|
|
12
|
+
// to prevent.
|
|
13
|
+
export function matchesPathPattern(pattern, rel) {
|
|
14
|
+
const normalizedPattern = normalizeRelPath(pattern)
|
|
15
|
+
const normalizedRel = normalizeRelPath(rel)
|
|
16
|
+
if (normalizedPattern === normalizedRel) return true
|
|
17
|
+
if (normalizedPattern.endsWith('/**')) {
|
|
18
|
+
return normalizedRel === normalizedPattern.slice(0, -3) || normalizedRel.startsWith(normalizedPattern.slice(0, -2))
|
|
19
|
+
}
|
|
20
|
+
if (normalizedPattern.startsWith('**/')) {
|
|
21
|
+
const tail = normalizedPattern.slice(3)
|
|
22
|
+
return normalizedRel === tail || normalizedRel.endsWith(`/${tail}`) || matchesPathPattern(tail, normalizedRel)
|
|
23
|
+
}
|
|
24
|
+
if (normalizedPattern.includes('*')) {
|
|
25
|
+
return new RegExp(`^${normalizedPattern.split('*').map(escapeRe).join('.*')}$`).test(normalizedRel)
|
|
26
|
+
}
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const basenameOf = (pattern) => path.posix.basename(normalizeRelPath(pattern))
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import { SUPPORTED_IDENTITY_PROVIDERS, gitRemoteUrl, remoteHost } from './config.mjs'
|
|
3
|
+
|
|
4
|
+
export const SUPPORTED_PROVIDERS = Object.freeze([...SUPPORTED_IDENTITY_PROVIDERS])
|
|
5
|
+
|
|
6
|
+
const PROVIDER_HOSTS = Object.freeze({ 'github.com': 'github' })
|
|
7
|
+
|
|
8
|
+
const lower = (value) => String(value ?? '').trim().toLowerCase()
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse owner/name out of a remote URL. This is the *recorded* name, which after a
|
|
12
|
+
* rename is the stale one — hosting providers redirect the old URL indefinitely, so
|
|
13
|
+
* a clone keeps fetching happily under a name that no longer exists.
|
|
14
|
+
*/
|
|
15
|
+
export function parseRemoteRef(url) {
|
|
16
|
+
const host = remoteHost(url)
|
|
17
|
+
if (!host) return null
|
|
18
|
+
const text = String(url).trim().replace(/\.git$/, '')
|
|
19
|
+
const tail = text.match(/^[^/@]+@[^:/]+:(.+)$/) ?? text.match(/^[a-z][a-z0-9+.-]*:\/\/[^/]+\/(.+)$/i)
|
|
20
|
+
if (!tail) return null
|
|
21
|
+
const parts = tail[1].split('/').filter(Boolean)
|
|
22
|
+
if (parts.length < 2) return null
|
|
23
|
+
return { host, provider: PROVIDER_HOSTS[host] ?? null, owner: lower(parts[0]), name: lower(parts.slice(1).join('/')) }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Ask the provider to resolve the recorded ref. GitHub follows the rename redirect
|
|
27
|
+
// and answers with the stable numeric id plus the current full name, which is the
|
|
28
|
+
// only signal that survives a rename.
|
|
29
|
+
export function githubLookup({ owner, name }) {
|
|
30
|
+
try {
|
|
31
|
+
const out = execFileSync('gh', ['api', `repos/${owner}/${name}`, '--jq', '{id: .id, fullName: .full_name}'], {
|
|
32
|
+
encoding: 'utf8',
|
|
33
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
34
|
+
})
|
|
35
|
+
const parsed = JSON.parse(out)
|
|
36
|
+
if (!parsed?.id) return null
|
|
37
|
+
return { id: String(parsed.id), fullName: lower(parsed.fullName), currentName: lower(String(parsed.fullName).split('/').pop()) }
|
|
38
|
+
} catch {
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const defaultLookups = { github: githubLookup }
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolve a clone's stable identity.
|
|
47
|
+
*
|
|
48
|
+
* Never keys on the root commit: repos created from a shared template have
|
|
49
|
+
* identical root commits, so that heuristic reports false duplicates, and it
|
|
50
|
+
* cannot see a rename at all. Never keys on the folder name either. When the
|
|
51
|
+
* provider is unreachable it falls back to the recorded identity and then to
|
|
52
|
+
* declared aliases, and says so in `source` rather than guessing.
|
|
53
|
+
*/
|
|
54
|
+
export function resolveRepoIdentity(cloneDir, { repo = {}, lookups = defaultLookups, remoteUrl = null } = {}) {
|
|
55
|
+
const url = remoteUrl ?? gitRemoteUrl(cloneDir)
|
|
56
|
+
const ref = parseRemoteRef(url)
|
|
57
|
+
const declared = repo.identity ?? null
|
|
58
|
+
const aliases = (Array.isArray(repo.aliases) ? repo.aliases : []).map(lower)
|
|
59
|
+
const base = {
|
|
60
|
+
path: cloneDir ?? null,
|
|
61
|
+
remoteUrl: url,
|
|
62
|
+
provider: ref?.provider ?? declared?.provider ?? null,
|
|
63
|
+
owner: ref?.owner ?? null,
|
|
64
|
+
remoteName: ref?.name ?? null,
|
|
65
|
+
declaredId: declared?.id ? String(declared.id) : null,
|
|
66
|
+
aliases,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const lookup = base.provider ? lookups[base.provider] : null
|
|
70
|
+
if (ref && lookup) {
|
|
71
|
+
const found = lookup(ref)
|
|
72
|
+
if (found) {
|
|
73
|
+
return {
|
|
74
|
+
...base,
|
|
75
|
+
ok: true,
|
|
76
|
+
id: found.id,
|
|
77
|
+
currentName: found.currentName,
|
|
78
|
+
fullName: found.fullName,
|
|
79
|
+
renamed: Boolean(ref.name && found.currentName && ref.name !== found.currentName),
|
|
80
|
+
source: 'provider',
|
|
81
|
+
reachable: true,
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (base.declaredId) {
|
|
87
|
+
return { ...base, ok: true, id: base.declaredId, currentName: lower(repo.name) || base.remoteName, renamed: false, source: 'recorded-identity', reachable: false }
|
|
88
|
+
}
|
|
89
|
+
if (base.remoteName && aliases.includes(base.remoteName)) {
|
|
90
|
+
return { ...base, ok: true, id: null, currentName: lower(repo.name), renamed: true, source: 'alias', reachable: false }
|
|
91
|
+
}
|
|
92
|
+
if (base.remoteName) {
|
|
93
|
+
return { ...base, ok: true, id: null, currentName: base.remoteName, renamed: false, source: 'remote-url', reachable: false }
|
|
94
|
+
}
|
|
95
|
+
return { ...base, ok: false, id: null, currentName: null, renamed: false, source: 'unresolved', reachable: false }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function identityKey(identity) {
|
|
99
|
+
if (identity?.id) return `${identity.provider ?? 'unknown'}:${identity.id}`
|
|
100
|
+
if (identity?.owner && identity?.currentName) return `${identity.provider ?? identity.remoteUrl ?? 'unknown'}:${identity.owner}/${identity.currentName}`
|
|
101
|
+
return null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Report identity problems a workspace cannot see on its own: stale folder names
|
|
106
|
+
* after a rename, two clones that are really the same repo, and config entries
|
|
107
|
+
* pointing at a repo the provider does not know.
|
|
108
|
+
*/
|
|
109
|
+
export function auditRepoIdentities(project, { lookups = defaultLookups } = {}) {
|
|
110
|
+
const findings = []
|
|
111
|
+
const byKey = new Map()
|
|
112
|
+
|
|
113
|
+
for (const repo of project.repos ?? []) {
|
|
114
|
+
if (repo.external || !repo.path) continue
|
|
115
|
+
const identity = resolveRepoIdentity(repo.path, { repo, lookups })
|
|
116
|
+
|
|
117
|
+
if (!identity.ok) {
|
|
118
|
+
findings.push({
|
|
119
|
+
severity: 'warning',
|
|
120
|
+
code: 'repo-identity-unresolved',
|
|
121
|
+
repo: repo.name,
|
|
122
|
+
message: `${repo.name}: no origin remote, so the workspace cannot tell this clone apart from any other`,
|
|
123
|
+
})
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
if (identity.source === 'provider' && identity.renamed) {
|
|
127
|
+
findings.push({
|
|
128
|
+
severity: 'warning',
|
|
129
|
+
code: 'repo-renamed-upstream',
|
|
130
|
+
repo: repo.name,
|
|
131
|
+
message: `${repo.name}: remote still says "${identity.remoteName}" but the provider now calls it "${identity.currentName}" — a rename is a metadata update, not a new identity`,
|
|
132
|
+
details: { from: identity.remoteName, to: identity.currentName, id: identity.id },
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
if (identity.currentName && repo.name && lower(repo.name) !== identity.currentName) {
|
|
136
|
+
const viaAlias = identity.aliases.includes(lower(repo.name)) || identity.aliases.includes(identity.remoteName ?? '')
|
|
137
|
+
findings.push({
|
|
138
|
+
severity: viaAlias ? 'warning' : 'error',
|
|
139
|
+
code: viaAlias ? 'repo-name-alias-deprecated' : 'repo-folder-name-stale',
|
|
140
|
+
repo: repo.name,
|
|
141
|
+
message: viaAlias
|
|
142
|
+
? `${repo.name}: resolved through a recorded alias; update the config name to "${identity.currentName}"`
|
|
143
|
+
: `${repo.name}: config name does not match the canonical name "${identity.currentName}"; record it under aliases if this rename is intentional`,
|
|
144
|
+
details: { configured: repo.name, canonical: identity.currentName },
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
if (identity.id && !identity.declaredId) {
|
|
148
|
+
findings.push({
|
|
149
|
+
severity: 'warning',
|
|
150
|
+
code: 'repo-identity-undeclared',
|
|
151
|
+
repo: repo.name,
|
|
152
|
+
message: `${repo.name}: record identity { provider: "${identity.provider}", id: "${identity.id}" } so this repo survives a rename while the provider is unreachable`,
|
|
153
|
+
details: { provider: identity.provider, id: identity.id },
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const key = identityKey(identity)
|
|
158
|
+
if (!key) continue
|
|
159
|
+
const first = byKey.get(key)
|
|
160
|
+
if (first) {
|
|
161
|
+
// A retired clone left behind by a rename. It fetches fine through the
|
|
162
|
+
// provider redirect, which is exactly why it goes unnoticed.
|
|
163
|
+
findings.push({
|
|
164
|
+
severity: 'error',
|
|
165
|
+
code: 'repo-identity-duplicate',
|
|
166
|
+
repo: repo.name,
|
|
167
|
+
message: `${repo.name} and ${first.repo} resolve to one repository (${key}); park the retired clone instead of syncing both`,
|
|
168
|
+
details: { key, clones: [first.path, repo.path] },
|
|
169
|
+
})
|
|
170
|
+
continue
|
|
171
|
+
}
|
|
172
|
+
byKey.set(key, { repo: repo.name, path: repo.path })
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { ok: !findings.some((item) => item.severity === 'error'), findings }
|
|
176
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { canProjectNode, projectGraph } from './policy.mjs'
|
|
3
|
+
|
|
4
|
+
export const ALIGNMENT_PROJECTION_SCHEMA = 'mnstry.alignment-projection@v1'
|
|
5
|
+
export const PROJECTION_POLICY_VERSION = 'projection-policy@v1'
|
|
6
|
+
|
|
7
|
+
// Root graph names are workspace-configured via sduiMap.rootGraphs. The kit
|
|
8
|
+
// ships no defaults, so name-based alignment classification stays inactive
|
|
9
|
+
// until a workspace supplies its own graph names.
|
|
10
|
+
export const DEFAULT_ROOT_GRAPHS = []
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_PROOF_GATES = [
|
|
13
|
+
'tenant-purity',
|
|
14
|
+
'token-check',
|
|
15
|
+
'sdui-check',
|
|
16
|
+
'action-check',
|
|
17
|
+
'media-check',
|
|
18
|
+
'consent-check',
|
|
19
|
+
'provider-check',
|
|
20
|
+
'browser-proof',
|
|
21
|
+
'storybook-proof',
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
function posixRelative(root, file) {
|
|
25
|
+
if (!root || !file) return file ?? null
|
|
26
|
+
return path.relative(root, file).split(path.sep).join('/')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function defaultSourceCommitResolver() {
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function projectionRecord(node, { target = 'team', sourceCommitResolver = defaultSourceCommitResolver } = {}) {
|
|
34
|
+
const verdict = canProjectNode(node, target)
|
|
35
|
+
return {
|
|
36
|
+
target,
|
|
37
|
+
sourceKgId: node.id,
|
|
38
|
+
allowed: verdict.ok,
|
|
39
|
+
reasons: verdict.reasons,
|
|
40
|
+
policyVersion: PROJECTION_POLICY_VERSION,
|
|
41
|
+
sourceCommit: sourceCommitResolver(node),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function stableProjectionForCheck(value) {
|
|
46
|
+
return {
|
|
47
|
+
...value,
|
|
48
|
+
generatedAt: null,
|
|
49
|
+
projectionRecords: (value?.projectionRecords ?? []).map((record) => ({ ...record, sourceCommit: null })),
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function hasAlignmentPath(node, alignmentRepo, alignmentRoot) {
|
|
54
|
+
if (!alignmentRepo || !alignmentRoot) return false
|
|
55
|
+
return node.repo === alignmentRepo && String(node.path ?? '').startsWith(alignmentRoot.replace(/\/?$/, '/'))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isAlignmentNode(node, { alignmentRepo, alignmentRoot, alignmentTags, rootGraphs }) {
|
|
59
|
+
if (hasAlignmentPath(node, alignmentRepo, alignmentRoot)) return true
|
|
60
|
+
const tags = new Set(node.tags ?? [])
|
|
61
|
+
if (alignmentTags.some((tag) => tags.has(tag))) return true
|
|
62
|
+
const haystack = [node.title, node.summary, node.path].join(' ').toLowerCase()
|
|
63
|
+
return rootGraphs.some((name) => haystack.includes(name.toLowerCase()))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function buildAlignmentProjection({
|
|
67
|
+
graph,
|
|
68
|
+
target = 'team',
|
|
69
|
+
workspaceRoot = null,
|
|
70
|
+
graphPath = null,
|
|
71
|
+
sduiMap = {},
|
|
72
|
+
conformance = {},
|
|
73
|
+
tenantTheme = {},
|
|
74
|
+
tokenReport = '',
|
|
75
|
+
alignmentRepo = null,
|
|
76
|
+
alignmentRoot = null,
|
|
77
|
+
alignmentTags = ['mnstry-alignment', 'sdui'],
|
|
78
|
+
rootGraphs = sduiMap.rootGraphs ?? DEFAULT_ROOT_GRAPHS,
|
|
79
|
+
proofGates = DEFAULT_PROOF_GATES,
|
|
80
|
+
previousProjection = null,
|
|
81
|
+
now = () => new Date(),
|
|
82
|
+
sourceCommitResolver = defaultSourceCommitResolver,
|
|
83
|
+
source = {},
|
|
84
|
+
} = {}) {
|
|
85
|
+
const gatedGraph = projectGraph(graph ?? { nodes: [], edges: [] }, { target })
|
|
86
|
+
const alignmentNodes = (gatedGraph.nodes ?? []).filter((node) =>
|
|
87
|
+
isAlignmentNode(node, { alignmentRepo, alignmentRoot, alignmentTags, rootGraphs })
|
|
88
|
+
)
|
|
89
|
+
const alignmentIds = new Set(alignmentNodes.map((node) => node.id))
|
|
90
|
+
const alignmentEdges = (gatedGraph.edges ?? []).filter((edge) => alignmentIds.has(edge.source) || alignmentIds.has(edge.target))
|
|
91
|
+
const audienceCounts = {}
|
|
92
|
+
const statusCounts = {}
|
|
93
|
+
|
|
94
|
+
for (const node of alignmentNodes) {
|
|
95
|
+
audienceCounts[node.audience] = (audienceCounts[node.audience] ?? 0) + 1
|
|
96
|
+
statusCounts[node.status] = (statusCounts[node.status] ?? 0) + 1
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const primitiveMap = sduiMap.primitiveMap ?? []
|
|
100
|
+
const gaps = []
|
|
101
|
+
for (const node of alignmentNodes) {
|
|
102
|
+
if (!node.surfaced && String(node.path ?? '').endsWith('.md')) {
|
|
103
|
+
gaps.push({ severity: 'info', type: 'unsurfaced-alignment-doc', node: node.id, path: `${node.repo}/${node.path}` })
|
|
104
|
+
}
|
|
105
|
+
if (hasAlignmentPath(node, alignmentRepo, alignmentRoot) && !Object.values(node.relations ?? {}).some((targets) => Array.isArray(targets) && targets.length)) {
|
|
106
|
+
gaps.push({ severity: 'info', type: 'missing-declared-relations', node: node.id, path: `${node.repo}/${node.path}` })
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const diagnostic of graph?.diagnostics ?? []) {
|
|
111
|
+
gaps.push({
|
|
112
|
+
severity: diagnostic.severity ?? 'warning',
|
|
113
|
+
type: diagnostic.type ?? 'graph-diagnostic',
|
|
114
|
+
node: diagnostic.node ?? null,
|
|
115
|
+
path: diagnostic.path ? `${diagnostic.repo}/${diagnostic.path}` : null,
|
|
116
|
+
message: diagnostic.message ?? '',
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const projectionRecords = (graph?.nodes ?? []).map((node) => projectionRecord(node, { target, sourceCommitResolver }))
|
|
121
|
+
const projection = {
|
|
122
|
+
schema: ALIGNMENT_PROJECTION_SCHEMA,
|
|
123
|
+
generatedAt: previousProjection?.generatedAt ?? now().toISOString(),
|
|
124
|
+
source: {
|
|
125
|
+
graph: source.graph ?? posixRelative(workspaceRoot, graphPath),
|
|
126
|
+
sduiMap: source.sduiMap ?? null,
|
|
127
|
+
conformanceContract: source.conformanceContract ?? null,
|
|
128
|
+
},
|
|
129
|
+
summary: {
|
|
130
|
+
graphNodes: graph?.nodeCount ?? graph?.nodes?.length ?? 0,
|
|
131
|
+
graphEdges: graph?.edgeCount ?? graph?.edges?.length ?? 0,
|
|
132
|
+
alignmentNodes: alignmentNodes.length,
|
|
133
|
+
alignmentEdges: alignmentEdges.length,
|
|
134
|
+
rootGraphs: rootGraphs.length,
|
|
135
|
+
sduiMappings: primitiveMap.length,
|
|
136
|
+
proofGates: proofGates.length,
|
|
137
|
+
audienceCounts,
|
|
138
|
+
statusCounts,
|
|
139
|
+
gaps: gaps.length,
|
|
140
|
+
blockedByProjection: gatedGraph.blocked.length,
|
|
141
|
+
diagnostics: (graph?.diagnostics ?? []).length,
|
|
142
|
+
},
|
|
143
|
+
projectionGate: {
|
|
144
|
+
target: gatedGraph.target,
|
|
145
|
+
blocked: gatedGraph.blocked,
|
|
146
|
+
},
|
|
147
|
+
projectionRecords,
|
|
148
|
+
rootGraphs,
|
|
149
|
+
proofGates,
|
|
150
|
+
contracts: {
|
|
151
|
+
conformanceVersion: conformance.version ?? conformance.contractVersion ?? null,
|
|
152
|
+
tenantThemeName: tenantTheme.name ?? tenantTheme.project ?? null,
|
|
153
|
+
tokenReportPresent: Boolean(String(tokenReport).trim()),
|
|
154
|
+
acceptedSduiPrimitiveCount: sduiMap.acceptedSduiPrimitives?.length ?? 0,
|
|
155
|
+
},
|
|
156
|
+
nodes: alignmentNodes,
|
|
157
|
+
edges: alignmentEdges,
|
|
158
|
+
primitiveMap,
|
|
159
|
+
gaps,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const comparableProjection = stableProjectionForCheck(projection)
|
|
163
|
+
const comparablePrevious = previousProjection ? stableProjectionForCheck(previousProjection) : null
|
|
164
|
+
if (JSON.stringify(comparableProjection) !== JSON.stringify(comparablePrevious)) {
|
|
165
|
+
projection.generatedAt = now().toISOString()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return projection
|
|
169
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export const LOCAL_AUDIENCES = ['public', 'team', 'operator', 'staff', 'private', 'sensitive']
|
|
2
|
+
export const RUNTIME_VISIBILITIES = ['private', 'shared', 'platform', 'public']
|
|
3
|
+
export const RUNTIME_SPACE_DATA_BOUNDARIES = [
|
|
4
|
+
'public_shareable',
|
|
5
|
+
'member_shareable',
|
|
6
|
+
'client_visible',
|
|
7
|
+
'practitioner_private',
|
|
8
|
+
'specially_protected',
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
export const VALID_AUDIENCES = new Set(LOCAL_AUDIENCES)
|
|
12
|
+
export const VALID_RUNTIME_VISIBILITIES = new Set(RUNTIME_VISIBILITIES)
|
|
13
|
+
export const VALID_RUNTIME_SPACE_DATA_BOUNDARIES = new Set(RUNTIME_SPACE_DATA_BOUNDARIES)
|
|
14
|
+
|
|
15
|
+
export const PROJECTION_TARGETS = {
|
|
16
|
+
public: new Set(['public']),
|
|
17
|
+
team: new Set(['public', 'team']),
|
|
18
|
+
operator: new Set(['public', 'team', 'operator', 'staff']),
|
|
19
|
+
local: new Set(LOCAL_AUDIENCES),
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function normalizeAudience(value) {
|
|
23
|
+
return typeof value === 'string' && VALID_AUDIENCES.has(value.trim()) ? value.trim() : null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function projectionTarget(value = 'local') {
|
|
27
|
+
return Object.hasOwn(PROJECTION_TARGETS, value) ? value : 'local'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function canProjectNode(node, target = 'local') {
|
|
31
|
+
const normalizedTarget = projectionTarget(target)
|
|
32
|
+
const audience = normalizeAudience(node?.audience)
|
|
33
|
+
const reasons = []
|
|
34
|
+
if (!audience) reasons.push('missing-or-invalid-audience')
|
|
35
|
+
if (audience && !PROJECTION_TARGETS[normalizedTarget].has(audience)) {
|
|
36
|
+
reasons.push(`audience-${audience}-blocked-for-${normalizedTarget}`)
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
ok: reasons.length === 0,
|
|
40
|
+
audience,
|
|
41
|
+
target: normalizedTarget,
|
|
42
|
+
reasons,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function projectGraph(graph, { target = 'local' } = {}) {
|
|
47
|
+
const nodes = Array.isArray(graph?.nodes) ? graph.nodes : []
|
|
48
|
+
const edges = Array.isArray(graph?.edges) ? graph.edges : []
|
|
49
|
+
const allowedNodes = []
|
|
50
|
+
const blocked = []
|
|
51
|
+
const byId = new Map()
|
|
52
|
+
|
|
53
|
+
for (const node of nodes) {
|
|
54
|
+
const verdict = canProjectNode(node, target)
|
|
55
|
+
if (verdict.ok) {
|
|
56
|
+
allowedNodes.push(node)
|
|
57
|
+
byId.set(node.id, node)
|
|
58
|
+
} else {
|
|
59
|
+
blocked.push({ id: node?.id ?? null, repo: node?.repo ?? null, path: node?.path ?? null, reasons: verdict.reasons })
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const allowedEdges = []
|
|
64
|
+
for (const edge of edges) {
|
|
65
|
+
if (byId.has(edge.source) && byId.has(edge.target)) allowedEdges.push(edge)
|
|
66
|
+
else blocked.push({ edge, reasons: ['edge-endpoint-blocked'] })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
target: projectionTarget(target),
|
|
71
|
+
nodes: allowedNodes,
|
|
72
|
+
edges: allowedEdges,
|
|
73
|
+
blocked,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function publicSourceVerdict(nodes, kgIds) {
|
|
78
|
+
const byId = new Map((nodes ?? []).map((node) => [node.id, node]))
|
|
79
|
+
const failures = []
|
|
80
|
+
for (const kgId of kgIds) {
|
|
81
|
+
const node = byId.get(kgId)
|
|
82
|
+
if (!node) {
|
|
83
|
+
failures.push({ kgId, reason: 'source-node-not-found' })
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
const verdict = canProjectNode(node, 'public')
|
|
87
|
+
if (!verdict.ok) failures.push({ kgId, reason: verdict.reasons.join(','), audience: node.audience ?? null })
|
|
88
|
+
}
|
|
89
|
+
return { ok: failures.length === 0, failures }
|
|
90
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { commandProject, readJson, writeJson } from '../project/config.mjs'
|
|
4
|
+
import { summarizeReadinessJourney } from '../readiness-protocols/runtime.mjs'
|
|
5
|
+
|
|
6
|
+
const esc = (value) => String(value ?? '').replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"')
|
|
7
|
+
|
|
8
|
+
// CSS hex colors come in exactly four widths: #rgb, #rgba, #rrggbb, #rrggbbaa.
|
|
9
|
+
// A bare {3,8} range also admits 5- and 7-digit strings that no browser parses.
|
|
10
|
+
const HEX_COLOR = /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/
|
|
11
|
+
const THEME_TOKENS = [
|
|
12
|
+
['background', '--atelier-bg'],
|
|
13
|
+
['surface', '--atelier-surface'],
|
|
14
|
+
['text', '--atelier-text'],
|
|
15
|
+
['accent', '--atelier-accent'],
|
|
16
|
+
['eyebrow', '--atelier-eyebrow'],
|
|
17
|
+
]
|
|
18
|
+
const KNOWN_THEME_TOKENS = new Set(THEME_TOKENS.map(([token]) => token))
|
|
19
|
+
|
|
20
|
+
function resolveDistributionBranding(project) {
|
|
21
|
+
const distribution = project.config?.ext?.['mnstry.atelier']?.distribution ?? {}
|
|
22
|
+
const theme = distribution.theme ?? {}
|
|
23
|
+
// Theme values are interpolated into a <style> block, so the hex-only rule
|
|
24
|
+
// is the CSS-injection guard; a deterministic build must not silently vary.
|
|
25
|
+
// Unknown keys throw for the same reason: a typo that silently drops an
|
|
26
|
+
// override would make two builds of the same config disagree by accident.
|
|
27
|
+
for (const token of Object.keys(theme)) {
|
|
28
|
+
if (!KNOWN_THEME_TOKENS.has(token)) {
|
|
29
|
+
throw new Error(`unknown distribution theme key (theme.${token})`)
|
|
30
|
+
}
|
|
31
|
+
if (typeof theme[token] !== 'string' || !HEX_COLOR.test(theme[token])) {
|
|
32
|
+
throw new Error(`distribution theme values must be hex colors (theme.${token})`)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const overrides = THEME_TOKENS
|
|
36
|
+
.filter(([token]) => typeof theme[token] === 'string')
|
|
37
|
+
.map(([token, property]) => `${property}:${theme[token]}`)
|
|
38
|
+
return {
|
|
39
|
+
name: distribution.name || 'MNSTRY Atelier',
|
|
40
|
+
eyebrow: distribution.eyebrow || 'local projection',
|
|
41
|
+
themeCss: overrides.length ? `:root{${overrides.join(';')}}\n` : '',
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function buildProjectProjection(project) {
|
|
46
|
+
const graph = readJson(project.graphPath)
|
|
47
|
+
const branding = resolveDistributionBranding(project)
|
|
48
|
+
const readinessJourney = summarizeReadinessJourney(project)
|
|
49
|
+
const output = path.join(project.outputRoot, 'index.html')
|
|
50
|
+
const dimensionCards = readinessJourney.dimensions.map((item) => `<article class="readiness-card" data-protocol="${esc(item.protocolId)}">
|
|
51
|
+
<p class="eyebrow">${esc(item.label)}</p>
|
|
52
|
+
<h3>${esc(item.title)}</h3>
|
|
53
|
+
<p><strong>${esc(item.status)}</strong> · ${esc(item.score)} / 100</p>
|
|
54
|
+
<p>${esc(item.blockers[0] || 'review-ready')}</p>
|
|
55
|
+
<button type="button" data-copy="${esc(item.agentPrompt)}">Copy protocol prompt</button>
|
|
56
|
+
</article>`).join('\n')
|
|
57
|
+
const cards = graph.nodes.map((node) => `<article class="card" data-node="${esc(node.id)}">
|
|
58
|
+
<p class="eyebrow">${esc(node.repo)} · ${esc(node.type)} · ${esc(node.audience)}</p>
|
|
59
|
+
<h2>${esc(node.title || node.id)}</h2>
|
|
60
|
+
<p>${esc(node.summary || node.path)}</p>
|
|
61
|
+
</article>`).join('\n')
|
|
62
|
+
const html = `<!doctype html>
|
|
63
|
+
<html lang="en">
|
|
64
|
+
<head>
|
|
65
|
+
<meta charset="utf-8">
|
|
66
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
67
|
+
<meta name="mnstry:atelier" content="project-projection">
|
|
68
|
+
<title>${esc(project.config.name || branding.name)}</title>
|
|
69
|
+
<style>
|
|
70
|
+
:root{--atelier-bg:#151312;--atelier-surface:#201a18;--atelier-text:#f4eee5;--atelier-accent:#e3b56e;--atelier-eyebrow:#c39a5b}
|
|
71
|
+
body{margin:0;font:16px/1.5 ui-sans-serif,system-ui;background:var(--atelier-bg);color:var(--atelier-text)}
|
|
72
|
+
main{max-width:1120px;margin:0 auto;padding:32px 20px}
|
|
73
|
+
h1{font-size:clamp(2rem,4vw,4rem);margin:.2em 0}
|
|
74
|
+
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:16px}
|
|
75
|
+
.card{border:1px solid #3a302b;border-radius:8px;padding:16px;background:var(--atelier-surface)}
|
|
76
|
+
.readiness{margin:24px 0 32px;padding:20px;border:1px solid #463b34;border-radius:8px;background:#1c1715}
|
|
77
|
+
.readiness-head{display:flex;gap:16px;align-items:flex-start;justify-content:space-between;flex-wrap:wrap}
|
|
78
|
+
.readiness-score{font-size:2rem;font-weight:700;color:var(--atelier-accent)}
|
|
79
|
+
.readiness-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px;margin-top:16px}
|
|
80
|
+
.readiness-card{border:1px solid #342b26;border-radius:8px;padding:14px;background:#171312}
|
|
81
|
+
.readiness-card h3{font-size:1.05rem;margin:.2rem 0}
|
|
82
|
+
.readiness-card button{min-height:44px;border:1px solid #755f43;border-radius:6px;background:#241d18;color:var(--atelier-text);padding:8px 10px;cursor:pointer}
|
|
83
|
+
.eyebrow{color:var(--atelier-eyebrow);text-transform:uppercase;letter-spacing:.08em;font-size:.8rem}
|
|
84
|
+
a{color:var(--atelier-accent)}
|
|
85
|
+
${branding.themeCss}</style>
|
|
86
|
+
</head>
|
|
87
|
+
<body><main>
|
|
88
|
+
<p class="eyebrow">${esc(branding.name)} · ${esc(branding.eyebrow)}</p>
|
|
89
|
+
<h1>${esc(project.config.name || 'Project Workspace')}</h1>
|
|
90
|
+
<p>${graph.counts.nodes} graph nodes · ${graph.counts.edges} graph edges · ${graph.counts.diagnostics} diagnostics</p>
|
|
91
|
+
<section class="readiness" aria-labelledby="tenant-readiness-title">
|
|
92
|
+
<div class="readiness-head">
|
|
93
|
+
<div>
|
|
94
|
+
<p class="eyebrow">MNSTRY Tenant Readiness</p>
|
|
95
|
+
<h2 id="tenant-readiness-title">Readiness Journey</h2>
|
|
96
|
+
<p>${esc(readinessJourney.dimensions.length)} dimensions · next protocol: ${esc(readinessJourney.nextProtocol || 'none')}</p>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="readiness-score">${esc(readinessJourney.score)}%</div>
|
|
99
|
+
</div>
|
|
100
|
+
<div class="readiness-grid">${dimensionCards}</div>
|
|
101
|
+
</section>
|
|
102
|
+
<section class="grid">${cards}</section>
|
|
103
|
+
<script>
|
|
104
|
+
document.addEventListener('click', async (event) => {
|
|
105
|
+
const button = event.target.closest('button[data-copy]')
|
|
106
|
+
if (!button) return
|
|
107
|
+
try {
|
|
108
|
+
await navigator.clipboard.writeText(button.getAttribute('data-copy') || '')
|
|
109
|
+
button.textContent = 'Copied'
|
|
110
|
+
} catch {
|
|
111
|
+
button.textContent = 'Copy failed'
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
</script>
|
|
115
|
+
</main></body></html>
|
|
116
|
+
`
|
|
117
|
+
return { output, html, graph, readinessJourney }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function runProjectCommand(argv = process.argv.slice(2)) {
|
|
121
|
+
const check = argv.includes('--check')
|
|
122
|
+
const project = commandProject({ argv })
|
|
123
|
+
const projection = buildProjectProjection(project)
|
|
124
|
+
if (check) {
|
|
125
|
+
const current = fs.existsSync(projection.output) ? fs.readFileSync(projection.output, 'utf8') : null
|
|
126
|
+
if (current !== projection.html) {
|
|
127
|
+
console.error(`project projection is stale: ${projection.output}`)
|
|
128
|
+
process.exit(1)
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
fs.mkdirSync(path.dirname(projection.output), { recursive: true })
|
|
132
|
+
fs.writeFileSync(projection.output, projection.html)
|
|
133
|
+
writeJson(path.join(project.outputRoot, 'atelier.manifest.json'), {
|
|
134
|
+
schema: 'mnstry.atelier-manifest@v1',
|
|
135
|
+
generatedAt: 'deterministic',
|
|
136
|
+
graphPath: project.graphPath,
|
|
137
|
+
entry: 'index.html',
|
|
138
|
+
tenantReadiness: {
|
|
139
|
+
score: projection.readinessJourney.score,
|
|
140
|
+
ready: projection.readinessJourney.ready,
|
|
141
|
+
dimensions: projection.readinessJourney.dimensions.map((item) => ({
|
|
142
|
+
key: item.key,
|
|
143
|
+
protocolId: item.protocolId,
|
|
144
|
+
status: item.status,
|
|
145
|
+
score: item.score,
|
|
146
|
+
})),
|
|
147
|
+
},
|
|
148
|
+
nodes: projection.graph.nodes.map((node) => ({ id: node.id, title: node.title, audience: node.audience, path: node.path })),
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
console.log(`project projection: ${projection.graph.counts.nodes} nodes -> ${projection.output}`)
|
|
152
|
+
}
|