@mnstry/atelier 0.2.0-alpha.13 → 0.2.0-alpha.15
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 +283 -0
- package/README.md +102 -52
- package/SECURITY.md +1 -1
- package/TRADEMARKS.md +2 -2
- package/contracts/atelier-extension-manifest.v1.schema.json +412 -0
- package/contracts/atelier-graph-view.v1.schema.json +1085 -0
- package/contracts/public-api-baseline.json +298 -19
- package/docs/README.md +90 -0
- package/docs/blocks/capability-stewardship.md +1 -1
- package/docs/blocks/claims.md +2 -2
- package/docs/blocks/conformance-admission.md +1 -1
- package/docs/blocks/learning-harnesses.md +1 -1
- package/docs/{atelier-sync.md → guides/atelier-sync.md} +20 -14
- package/docs/{capability-stewardship.md → guides/capability-stewardship.md} +2 -4
- package/docs/guides/co-authoring.md +113 -0
- package/docs/{distributions.md → guides/distributions.md} +13 -8
- package/docs/guides/extensions.md +189 -0
- package/docs/guides/graph-refresh.md +152 -0
- package/docs/guides/graph-workspace.md +64 -0
- package/docs/{ingestion.md → guides/ingestion.md} +16 -15
- package/docs/{inquiry-harnesses.md → guides/inquiry-harnesses.md} +2 -3
- package/docs/{knowledge-workspace.md → guides/knowledge-workspace.md} +2 -2
- package/docs/{learning-harnesses.md → guides/learning-harnesses.md} +2 -3
- package/docs/{learning.md → guides/learning.md} +12 -5
- package/docs/{obsidian.md → guides/obsidian.md} +370 -23
- package/docs/guides/ontology-guide.md +111 -0
- package/docs/{personal-workspace.md → guides/personal-workspace.md} +17 -5
- package/docs/{research-starter.md → guides/research-starter.md} +1 -1
- package/docs/{tenant-readiness.md → guides/tenant-readiness.md} +39 -0
- package/docs/{upgrade.md → guides/upgrade.md} +197 -11
- package/docs/knowledge-health/assessment/capabilities.md +6 -15
- package/docs/knowledge-health/facilitator.md +15 -4
- package/docs/knowledge-health/{devday/participant-response.md → participant-response.md} +4 -2
- package/docs/knowledge-health/quickstart.md +97 -75
- package/docs/knowledge-health/readme.md +13 -5
- package/docs/maintainers/describing-the-atelier.md +173 -0
- package/docs/{release-engineering.md → maintainers/release-engineering.md} +5 -4
- package/docs/{announcements.md → reference/announcements.md} +1 -1
- package/docs/{architecture.md → reference/architecture.md} +31 -5
- package/docs/{atelier-runtime.md → reference/atelier-runtime.md} +1 -1
- package/docs/{connected-composition.md → reference/connected-composition.md} +7 -10
- package/docs/{coordination.md → reference/coordination.md} +1 -1
- package/docs/{decisions.md → reference/decisions.md} +3 -3
- package/docs/{design.md → reference/design.md} +1 -1
- package/docs/reference/edge-policy.md +151 -0
- package/docs/reference/graph-view-structured.md +119 -0
- package/docs/reference/graph-view.md +139 -0
- package/docs/reference/graph-workspace.md +37 -0
- package/docs/{intake-and-guides.md → reference/intake-and-guides.md} +3 -4
- package/docs/{knowledge-graph.md → reference/knowledge-graph.md} +120 -6
- package/docs/{local-services.md → reference/local-services.md} +43 -4
- package/docs/{obsidian-contract.md → reference/obsidian-contract.md} +29 -4
- package/docs/{obsidian-plugin.md → reference/obsidian-plugin.md} +218 -68
- package/docs/{practical-judgment.md → reference/phronesis.md} +12 -3
- package/docs/reference/portability.md +92 -0
- package/docs/{portable-presentation.md → reference/portable-presentation.md} +2 -2
- package/docs/{presentation-consumer-boundaries.md → reference/presentation-consumer-boundaries.md} +5 -0
- package/docs/{project-options.md → reference/project-options.md} +17 -0
- package/docs/{repo-boundary-guard.md → reference/repo-boundary-guard.md} +50 -4
- package/docs/{review-portability.md → reference/review-portability.md} +4 -0
- package/docs/{semantic-operation.md → reference/semantic-operation.md} +4 -3
- package/docs/{template-profile.md → reference/template-profile.md} +1 -1
- package/docs/{trackables.md → reference/trackables.md} +2 -2
- package/docs/start/independent-start.md +89 -0
- package/docs/{install.md → start/install.md} +9 -9
- package/docs/{knowledge-setup.md → start/knowledge-setup.md} +5 -5
- package/docs/{research-starter-quickstart.md → start/research-starter-quickstart.md} +6 -6
- package/fixtures/atelier-architecture/catalog/invalid/document.json +2 -2
- package/fixtures/atelier-architecture/catalog/valid/document.json +2 -2
- package/fixtures/graph-view/synthetic-authorized-input.v1.json +437 -0
- package/fixtures/graph-view/synthetic-view.v1.json +1 -0
- package/fixtures/knowledge-health/workshop/README.md +1 -1
- package/fixtures/templates/local-library.v1.json +3 -1
- package/package.json +22 -8
- package/plugins/obsidian/main.js +57 -8
- package/plugins/obsidian/manifest.json +1 -1
- package/skills/claude/atelier-guided-upgrade/SKILL.md +1 -1
- package/skills/claude/atelier-knowledge-setup/SKILL.md +2 -2
- package/skills/claude/atelier-local-service/SKILL.md +1 -1
- package/skills/claude/atelier-practical-judgment/SKILL.md +7 -2
- package/skills/claude/atelier-skill-steward/SKILL.md +1 -1
- package/skills/claude/mnstry-readiness/SKILL.md +2 -2
- package/skills/codex/atelier-guided-upgrade/SKILL.md +1 -1
- package/skills/codex/atelier-knowledge-setup/SKILL.md +2 -2
- package/skills/codex/atelier-local-service/SKILL.md +1 -1
- package/skills/codex/atelier-practical-judgment/SKILL.md +7 -2
- package/skills/codex/atelier-skill-steward/SKILL.md +1 -1
- package/skills/codex/mnstry-readiness/SKILL.md +2 -2
- package/src/architecture/catalog.mjs +2 -2
- package/src/attestation/sign.mjs +1 -1
- package/src/boundary/content-rules.mjs +534 -68
- package/src/boundary/policy.mjs +187 -60
- package/src/cli/run.mjs +26 -9
- package/src/commands/distribution.mjs +1 -1
- package/src/commands/extension.mjs +27 -0
- package/src/commands/ingest.mjs +1 -1
- package/src/commands/learn.mjs +1 -1
- package/src/commands/obsidian.mjs +49 -21
- package/src/commands/review.mjs +3 -0
- package/src/edge-policy/contract.mjs +69 -0
- package/src/edge-policy/policy.mjs +142 -0
- package/src/edge-policy/source-citation-adapter.mjs +43 -0
- package/src/extension-packs/loader.mjs +4 -3
- package/src/extensions/admission.mjs +83 -0
- package/src/extensions/client.d.mts +23 -0
- package/src/extensions/client.mjs +99 -0
- package/src/extensions/contracts.d.mts +29 -0
- package/src/extensions/contracts.mjs +163 -0
- package/src/extensions/convex.d.mts +24 -0
- package/src/extensions/convex.mjs +189 -0
- package/src/extensions/delegation.mjs +54 -0
- package/src/extensions/drizzle.d.mts +11 -0
- package/src/extensions/drizzle.mjs +33 -0
- package/src/extensions/postgres.d.mts +7 -0
- package/src/extensions/postgres.mjs +66 -0
- package/src/extensions/prisma.d.mts +13 -0
- package/src/extensions/prisma.mjs +63 -0
- package/src/extensions/project.mjs +432 -0
- package/src/extensions/scaffold-convex.mjs +130 -0
- package/src/extensions/scaffold-sql.mjs +144 -0
- package/src/extensions/server.d.mts +20 -0
- package/src/extensions/server.mjs +102 -0
- package/src/extensions/sql-scope.mjs +22 -0
- package/src/graph/graph.mjs +13 -5
- package/src/graph/knowledge-graph.mjs +20 -2
- package/src/graph/repo-contribution.mjs +405 -0
- package/src/graph/repo-contribution.types.mjs +32 -0
- package/src/graph-access/edge-decision-adapter.mjs +41 -0
- package/src/graph-access/edge-record-adapter.mjs +73 -0
- package/src/graph-access/index.mjs +253 -0
- package/src/graph-access/multi-contribution-record-adapter.mjs +101 -0
- package/src/graph-access/multi-record-projection-adapter.mjs +74 -0
- package/src/graph-access/source-adapter.mjs +81 -0
- package/src/graph-refresh/compare.mjs +110 -0
- package/src/graph-refresh/controller.mjs +324 -0
- package/src/graph-refresh/index.mjs +3 -0
- package/src/graph-refresh/local.mjs +78 -0
- package/src/graph-view/babylon/babylon-scene-port.mjs +64 -0
- package/src/graph-view/babylon/index.mjs +2 -0
- package/src/graph-view/babylon/navigator.mjs +312 -0
- package/src/graph-view/babylon/provenance.mjs +36 -0
- package/src/graph-view/babylon/scene-controller.mjs +131 -0
- package/src/graph-view/babylon/semantic-picking.mjs +35 -0
- package/src/graph-view/babylon/web-host.mjs +188 -0
- package/src/graph-view/cli.mjs +3 -0
- package/src/graph-view/command.mjs +34 -0
- package/src/graph-view/contracts.mjs +79 -0
- package/src/graph-view/errors.mjs +5 -0
- package/src/graph-view/index.mjs +4 -0
- package/src/graph-view/intents.mjs +30 -0
- package/src/graph-view/query.mjs +213 -0
- package/src/graph-view/refs.mjs +2 -0
- package/src/graph-view/structured/browser.mjs +262 -0
- package/src/graph-view/structured/styles.mjs +8 -0
- package/src/graph-workspace/authoring.mjs +185 -0
- package/src/graph-workspace/browser/constellation.mjs +369 -0
- package/src/graph-workspace/browser/dom.mjs +23 -0
- package/src/graph-workspace/browser/index.mjs +6 -0
- package/src/graph-workspace/browser/review.mjs +55 -0
- package/src/graph-workspace/browser/styles.mjs +11 -0
- package/src/graph-workspace/browser/workbench.mjs +351 -0
- package/src/graph-workspace/browser/workspace.mjs +431 -0
- package/src/graph-workspace/disclosure.mjs +39 -0
- package/src/graph-workspace/geometry.mjs +78 -0
- package/src/graph-workspace/index.mjs +11 -0
- package/src/graph-workspace/labels.mjs +50 -0
- package/src/graph-workspace/model.mjs +109 -0
- package/src/graph-workspace/recipes.mjs +88 -0
- package/src/graph-workspace/scale.mjs +95 -0
- package/src/graph-workspace/theme.mjs +156 -0
- package/src/ingestion/optional/graphify/graphify_supplier.py +5 -0
- package/src/ingestion/optional/graphify/native-closure-pins.json +275 -0
- package/src/ingestion/optional/graphify/native-pins.json +306 -0
- package/src/knowledge/ontology-guide.mjs +152 -0
- package/src/knowledge-health/proof/installed-check.mjs +2 -2
- package/src/knowledge-health/rule-identity.mjs +14 -9
- package/src/knowledge-health/workshop.mjs +22 -2
- package/src/personal-workspace/index.mjs +1 -1
- package/src/personal-workspace/selection.mjs +34 -3
- package/src/project/config.mjs +1 -1
- package/src/project/path-match.mjs +2 -1
- package/src/project/providers/github.mjs +24 -0
- package/src/project/repo-identity.mjs +3 -22
- package/src/projection/obsidian/contracts.mjs +1 -1
- package/src/projection/obsidian/materialize/path-registry.mjs +1 -1
- package/src/projection/obsidian/materialize/prepare-view.mjs +88 -6
- package/src/projection/obsidian/materialize/redaction.mjs +1 -1
- package/src/projection/obsidian/personal-workspace.mjs +84 -5
- package/src/projection/obsidian/plugin-bridge/channel.mjs +1 -1
- package/src/projection/obsidian/proposals/adapter.mjs +46 -23
- package/src/projection/obsidian/proposals/observation.mjs +40 -2
- package/src/projection/obsidian/publication/publisher.mjs +96 -8
- package/src/projection/project.mjs +21 -16
- package/src/readiness/readiness.mjs +20 -11
- package/src/readiness-protocols/bundled-pack.mjs +14 -766
- package/src/readiness-protocols/bundled-selection.mjs +32 -0
- package/src/readiness-protocols/evidence.mjs +2 -1
- package/src/readiness-protocols/packs/mnstry.mjs +766 -0
- package/src/readiness-protocols/runtime.mjs +43 -6
- package/src/runtime/obsidian/engine.mjs +387 -76
- package/src/runtime/obsidian/lifecycle.mjs +20 -15
- package/src/runtime/obsidian/login-item.mjs +6 -3
- package/src/runtime/obsidian/machine-settings.mjs +52 -8
- package/src/runtime/obsidian/observation.mjs +36 -4
- package/src/runtime/obsidian/opening.mjs +4 -3
- package/src/runtime/obsidian/personal-selection.mjs +187 -0
- package/src/runtime/obsidian/pipeline.mjs +115 -6
- package/src/runtime/obsidian/private-lock.mjs +50 -6
- package/src/runtime/obsidian/selection-continuity.mjs +124 -0
- package/src/runtime/obsidian/service-main.mjs +68 -16
- package/src/runtime/obsidian/service.mjs +80 -6
- package/src/runtime/obsidian/unheld-evidence.mjs +1 -1
- package/src/runtime/obsidian/view-permission.mjs +335 -0
- package/src/ui/control-system.mjs +201 -0
- package/src/ui/html-primitives.mjs +3 -0
- package/src/upgrade/closure-diagnostics.mjs +135 -0
- package/src/upgrade/closure.mjs +649 -0
- package/src/upgrade/upgrade.mjs +42 -11
- package/templates/distribution-workspace/README.md +2 -2
- package/templates/external-project-workspace/atelier.project.json +3 -0
- package/templates/external-project-workspace/pack-lifecycle.json +3 -1
- package/templates/knowledge-workspace/README.md +2 -2
- package/docs/integration-and-review-plan.md +0 -409
- package/docs/integration-contract-decisions.md +0 -44
- package/docs/knowledge-health/devday/facilitator.md +0 -16
- package/docs/knowledge-health/devday/public-default.md +0 -107
- package/docs/knowledge-health/devday/quickstart.md +0 -18
- package/docs/knowledge-health/devday/support.md +0 -22
- package/docs/knowledge-health/exercise.md +0 -17
- package/docs/knowledge-health/support.md +0 -34
- package/docs/presentation-review-disposition.md +0 -47
- package/docs/progressive-ingestion-acceptance.md +0 -89
- package/docs/progressive-ingestion.md +0 -235
- /package/docs/{exact-upgrades.md → guides/exact-upgrades.md} +0 -0
- /package/docs/{guided-upgrades.md → guides/guided-upgrades.md} +0 -0
- /package/docs/{local-review.md → guides/local-review.md} +0 -0
- /package/docs/{skill-steward.md → guides/skill-steward.md} +0 -0
- /package/docs/{template-upgrades.md → guides/template-upgrades.md} +0 -0
- /package/docs/{assurance-controls.md → maintainers/assurance-controls.md} +0 -0
- /package/docs/{attestation.md → reference/attestation.md} +0 -0
- /package/docs/{coauthor-session.md → reference/coauthor-session.md} +0 -0
- /package/docs/{continuity.md → reference/continuity.md} +0 -0
- /package/docs/{contract-stability.md → reference/contract-stability.md} +0 -0
- /package/docs/{decision-practices.md → reference/decision-practices.md} +0 -0
- /package/docs/{evidence-navigation.md → reference/evidence-navigation.md} +0 -0
- /package/docs/{extension-pack-lifecycle.md → reference/extension-pack-lifecycle.md} +0 -0
- /package/docs/{interaction-and-reflection.md → reference/interaction-and-reflection.md} +0 -0
- /package/docs/{local-state.md → reference/local-state.md} +0 -0
- /package/docs/{ontology.md → reference/ontology.md} +0 -0
- /package/docs/{optional-ingestion.md → reference/optional-ingestion.md} +0 -0
- /package/docs/{project-command-inventory.md → reference/project-command-inventory.md} +0 -0
- /package/docs/{semantic-ingestion.md → reference/semantic-ingestion.md} +0 -0
- /package/docs/{vault-service.md → reference/vault-service.md} +0 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { createSceneController } from './scene-controller.mjs'
|
|
2
|
+
import { assertGraphViewIntent } from '../intents.mjs'
|
|
3
|
+
import { refKey as suppliedRefKey } from '../refs.mjs'
|
|
4
|
+
|
|
5
|
+
const LIMIT = 48
|
|
6
|
+
const EDGE_LIMIT = 96
|
|
7
|
+
export const refKey = ref => {
|
|
8
|
+
if (!ref || Object.keys(ref).sort().join(',') !== 'localId,repoId' ||
|
|
9
|
+
['repoId', 'localId'].some(field => typeof ref[field] !== 'string' || !ref[field].trim() || ref[field].length > 512)) {
|
|
10
|
+
throw new TypeError('Qualified repository and local reference required')
|
|
11
|
+
}
|
|
12
|
+
return suppliedRefKey(ref)
|
|
13
|
+
}
|
|
14
|
+
const copyRef = ref => Object.freeze({ repoId: ref.repoId, localId: ref.localId })
|
|
15
|
+
const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0
|
|
16
|
+
const clone = value => JSON.parse(JSON.stringify(value))
|
|
17
|
+
|
|
18
|
+
// Coordinates are a repeatable presentation of the received view, never relations.
|
|
19
|
+
// Compute once over the complete bounded input, then retain across lens changes.
|
|
20
|
+
export function layoutGraph(nodes) {
|
|
21
|
+
const ordered = [...nodes].sort((a, b) => compare(refKey(a.ref), refKey(b.ref)))
|
|
22
|
+
const width = Math.max(1, Math.ceil(Math.sqrt(ordered.length)))
|
|
23
|
+
return new Map(ordered.map((node, index) => [refKey(node.ref), [
|
|
24
|
+
(index % width - (width - 1) / 2) * 1.8,
|
|
25
|
+
0,
|
|
26
|
+
(Math.floor(index / width) - (Math.ceil(ordered.length / width) - 1) / 2) * 1.8,
|
|
27
|
+
]]))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function receive(view) {
|
|
31
|
+
if (view?.schema !== 'atelier.graph-view@v1' || !/^sha256:[a-f0-9]{64}$/.test(view.projectionDigest ?? '')) {
|
|
32
|
+
throw new TypeError('Versioned graph-view projection and digest required')
|
|
33
|
+
}
|
|
34
|
+
if (!Array.isArray(view.nodes) || !Array.isArray(view.edges) || view.nodes.length > 500 || view.edges.length > 2000) {
|
|
35
|
+
throw new RangeError('Bounded graph-view envelope exceeded')
|
|
36
|
+
}
|
|
37
|
+
if (new TextEncoder().encode(JSON.stringify(view)).byteLength > 2 * 1024 * 1024) throw new RangeError('Bounded graph-view byte envelope exceeded')
|
|
38
|
+
if (!Array.isArray(view.evidence) || view.evidence.length > 4000 || !Array.isArray(view.questions) || view.questions.length > 100 ||
|
|
39
|
+
!Array.isArray(view.capabilities?.queries)) throw new TypeError('Bounded evidence, questions and query capabilities required')
|
|
40
|
+
// Retain only presentation inputs and opaque intent targets. Source bodies,
|
|
41
|
+
// host claims and arbitrary extension fields have no renderer storage path.
|
|
42
|
+
const received = {
|
|
43
|
+
schema: view.schema, projectionDigest: view.projectionDigest, completeness: view.completeness,
|
|
44
|
+
nodes: view.nodes.map(node => ({ ref: copyRef(node.ref), title: node.title, displayEligible: node.displayEligible,
|
|
45
|
+
sourceRef: { revision: node.sourceRef?.revision } })),
|
|
46
|
+
edges: view.edges.map(edge => ({ edgeId: edge.edgeId, source: copyRef(edge.source), target: copyRef(edge.target),
|
|
47
|
+
predicate: edge.predicate, acceptanceState: edge.acceptanceState })),
|
|
48
|
+
evidence: view.evidence.map(item => ({ evidenceId: item.evidenceId })),
|
|
49
|
+
questions: view.questions.map(item => ({ questionId: item.questionId })),
|
|
50
|
+
capabilities: { queries: [...view.capabilities.queries] },
|
|
51
|
+
page: view.page ? { limit: view.page.limit, offset: view.page.offset, nextCursor: view.page.nextCursor, truncated: view.page.truncated,
|
|
52
|
+
returned: Object.fromEntries(['nodes','edges','evidence','questions','diagnostics'].map(key => [key, view.page.returned?.[key]])),
|
|
53
|
+
omitted: Object.fromEntries(['nodes','edges','evidence','questions','diagnostics'].map(key => [key, view.page.omitted?.[key]])) } : null,
|
|
54
|
+
}
|
|
55
|
+
const nodes = new Map()
|
|
56
|
+
for (const node of received.nodes) {
|
|
57
|
+
const key = refKey(node.ref)
|
|
58
|
+
if (nodes.has(key) || typeof node.title !== 'string' || !node.title.trim() || node.title.length > 4096 || node.displayEligible !== true) throw new TypeError('Unique eligible titled nodes required')
|
|
59
|
+
if (typeof node.sourceRef?.revision !== 'string' || !node.sourceRef.revision) throw new TypeError('Source revision required')
|
|
60
|
+
nodes.set(key, node)
|
|
61
|
+
}
|
|
62
|
+
const edgeIds = new Set()
|
|
63
|
+
for (const edge of received.edges) {
|
|
64
|
+
if (typeof edge.edgeId !== 'string' || !edge.edgeId || edgeIds.has(edge.edgeId) ||
|
|
65
|
+
!nodes.has(refKey(edge.source)) || !nodes.has(refKey(edge.target)) ||
|
|
66
|
+
typeof edge.predicate !== 'string' || !edge.predicate || edge.acceptanceState !== 'active') throw new TypeError('Unique active edges with received endpoints required')
|
|
67
|
+
edgeIds.add(edge.edgeId)
|
|
68
|
+
}
|
|
69
|
+
return { view: received, nodes, positions: layoutGraph(received.nodes) }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Reuses the retained DOM-free scene controller. This layer only maps the
|
|
73
|
+
// producer's graph-view refs into its transient versioned presentation bindings.
|
|
74
|
+
export function createGraphNavigator({ createPort, onIntent = () => {} }) {
|
|
75
|
+
let current = null
|
|
76
|
+
let selected = null
|
|
77
|
+
let selectedEdgeId = null
|
|
78
|
+
let questionId = null
|
|
79
|
+
let focused = null
|
|
80
|
+
let lens = 'all'
|
|
81
|
+
let paused = false
|
|
82
|
+
let disposed = false
|
|
83
|
+
let shown = []
|
|
84
|
+
let sceneEdgeCount = 0
|
|
85
|
+
let sceneScope = null
|
|
86
|
+
let matchingCount = 0
|
|
87
|
+
let eligibleEdgeCount = 0
|
|
88
|
+
let sceneController
|
|
89
|
+
let sceneReady = false
|
|
90
|
+
let activePort = null
|
|
91
|
+
let preparedPort = null
|
|
92
|
+
let silent = 0
|
|
93
|
+
let publishing = false
|
|
94
|
+
let publicationSelection = null
|
|
95
|
+
|
|
96
|
+
function live() {
|
|
97
|
+
if (disposed || !current) throw new Error('Graph navigator unavailable')
|
|
98
|
+
return current
|
|
99
|
+
}
|
|
100
|
+
function nodeFor(ref) { return live().nodes.get(refKey(ref)) }
|
|
101
|
+
function binding(node) {
|
|
102
|
+
return { domain: 'atelier', kind: node.ref.repoId, id: node.ref.localId, revision: node.sourceRef.revision }
|
|
103
|
+
}
|
|
104
|
+
function emit(type, fields) {
|
|
105
|
+
const intent = assertGraphViewIntent(live().view, { type, projectionDigest: live().view.projectionDigest, ...fields })
|
|
106
|
+
// Notification failures cannot undo an already applied presentation change.
|
|
107
|
+
if (!silent) try { onIntent(intent) } catch { /* The consuming host owns delivery recovery. */ }
|
|
108
|
+
return intent
|
|
109
|
+
}
|
|
110
|
+
function candidates(input, nextLens, focus) {
|
|
111
|
+
const endpoints = new Set(input.view.edges.filter(edge => nextLens === 'all' ||
|
|
112
|
+
(nextLens === 'contradictions' && edge.predicate === 'contradicts') ||
|
|
113
|
+
(nextLens === 'support' && ['supports', 'evidences'].includes(edge.predicate)))
|
|
114
|
+
.flatMap(edge => [refKey(edge.source), refKey(edge.target)]))
|
|
115
|
+
let result = [...input.nodes.values()].filter(node => nextLens === 'all' || endpoints.has(refKey(node.ref)) ||
|
|
116
|
+
(focus && refKey(node.ref) === refKey(focus)))
|
|
117
|
+
if (focus) {
|
|
118
|
+
const key = refKey(focus)
|
|
119
|
+
const neighbors = new Set(input.view.edges.filter(edge => refKey(edge.source) === key || refKey(edge.target) === key)
|
|
120
|
+
.flatMap(edge => [refKey(edge.source), refKey(edge.target)]))
|
|
121
|
+
result = result.filter(node => refKey(node.ref) === key || neighbors.has(refKey(node.ref)))
|
|
122
|
+
}
|
|
123
|
+
result.sort((a, b) => compare(refKey(a.ref), refKey(b.ref)))
|
|
124
|
+
if (focus) result.sort((a, b) => Number(refKey(b.ref) === refKey(focus)) - Number(refKey(a.ref) === refKey(focus)))
|
|
125
|
+
return result
|
|
126
|
+
}
|
|
127
|
+
function profile(input, nextLens, focus) {
|
|
128
|
+
const matching = candidates(input, nextLens, focus)
|
|
129
|
+
const visible = matching.slice(0, LIMIT)
|
|
130
|
+
const visibleKeys = new Set(visible.map(node => refKey(node.ref)))
|
|
131
|
+
const edges = input.view.edges.filter(edge => visibleKeys.has(refKey(edge.source)) && visibleKeys.has(refKey(edge.target)) &&
|
|
132
|
+
(nextLens === 'all' || (nextLens === 'contradictions' && edge.predicate === 'contradicts') ||
|
|
133
|
+
(nextLens === 'support' && ['supports', 'evidences'].includes(edge.predicate))))
|
|
134
|
+
.sort((a, b) => compare(a.edgeId, b.edgeId))
|
|
135
|
+
const result = {
|
|
136
|
+
profileId: 'atelier.graph-view', profileRevision: input.view.projectionDigest,
|
|
137
|
+
nodes: visible.map(node => ({ ref: binding(node), label: node.title, position: input.positions.get(refKey(node.ref)),
|
|
138
|
+
sourceRevision: node.sourceRef.revision, evidenceIds: [] })),
|
|
139
|
+
edges: edges.slice(0, EDGE_LIMIT).map(edge => ({ from: binding(input.nodes.get(refKey(edge.source))),
|
|
140
|
+
to: binding(input.nodes.get(refKey(edge.target))), label: edge.predicate, edgeId: edge.edgeId })),
|
|
141
|
+
}
|
|
142
|
+
return { result, visible: visible.map(node => copyRef(node.ref)), matchingCount: matching.length, eligibleEdgeCount: edges.length }
|
|
143
|
+
}
|
|
144
|
+
function publishScene(input, nextLens, focus) {
|
|
145
|
+
const prepared = profile(input, nextLens, focus)
|
|
146
|
+
if (!prepared.result.nodes.length) {
|
|
147
|
+
sceneController.dispose()
|
|
148
|
+
sceneController = makeController()
|
|
149
|
+
sceneReady = false
|
|
150
|
+
activePort = null
|
|
151
|
+
} else {
|
|
152
|
+
const retained = current?.view.projectionDigest === input.view.projectionDigest && selected &&
|
|
153
|
+
prepared.visible.some(ref => refKey(ref) === refKey(selected)) ? input.nodes.get(refKey(selected)) : null
|
|
154
|
+
publicationSelection = retained ? binding(retained) : null
|
|
155
|
+
publishing = true
|
|
156
|
+
try {
|
|
157
|
+
if (!sceneReady) sceneController.mount(prepared.result)
|
|
158
|
+
else sceneController.rebuild(prepared.result)
|
|
159
|
+
} finally { publishing = false; publicationSelection = null }
|
|
160
|
+
sceneReady = true
|
|
161
|
+
activePort = preparedPort
|
|
162
|
+
if (paused) sceneController.pause()
|
|
163
|
+
}
|
|
164
|
+
shown = prepared.visible
|
|
165
|
+
matchingCount = prepared.matchingCount
|
|
166
|
+
eligibleEdgeCount = prepared.eligibleEdgeCount
|
|
167
|
+
sceneEdgeCount = prepared.result.edges.length
|
|
168
|
+
sceneScope = focus ? { kind: 'one-hop', ref: copyRef(focus) } : { kind: 'lens', lens: nextLens }
|
|
169
|
+
return { status: sceneReady ? 'ready' : 'empty' }
|
|
170
|
+
}
|
|
171
|
+
function makeController() { return createSceneController({ createPort(profile, bind) {
|
|
172
|
+
const port = createPort(profile, bind)
|
|
173
|
+
for (const method of ['render', 'pick', 'focus', 'select', 'dispose']) {
|
|
174
|
+
if (typeof port?.[method] !== 'function') { port?.dispose?.(); throw new TypeError('Complete scene port required') }
|
|
175
|
+
}
|
|
176
|
+
// A fresh mount does not ask the retained controller to restore selection.
|
|
177
|
+
// Initialize the new port before publishing it, including after an empty lens.
|
|
178
|
+
try { port.select(publicationSelection) } catch (error) { port.dispose(); throw error }
|
|
179
|
+
// Reconcile the retained controller's rebuild highlight against the current
|
|
180
|
+
// consumer selection; an old node cannot undo a clear or edge selection.
|
|
181
|
+
const boundary = { render: () => port.render(), pick: (x, y) => port.pick(x, y),
|
|
182
|
+
focus: position => port.focus(position), dispose: () => port.dispose(),
|
|
183
|
+
select(ref) { port.select(publishing ? publicationSelection : ref) } }
|
|
184
|
+
preparedPort = boundary
|
|
185
|
+
return boundary
|
|
186
|
+
}, onSemanticEvent(event) {
|
|
187
|
+
if (event.type !== 'select') return
|
|
188
|
+
const ref = { repoId: event.target.kind, localId: event.target.id }
|
|
189
|
+
if (!shown.some(item => refKey(item) === refKey(ref))) return
|
|
190
|
+
selected = copyRef(ref)
|
|
191
|
+
selectedEdgeId = null
|
|
192
|
+
emit('select', { ref: selected })
|
|
193
|
+
} }) }
|
|
194
|
+
sceneController = makeController()
|
|
195
|
+
const api = Object.freeze({
|
|
196
|
+
mount(view) {
|
|
197
|
+
if (disposed || current) throw new Error('Mount requires a fresh navigator')
|
|
198
|
+
const next = receive(view)
|
|
199
|
+
const result = publishScene(next, lens, null)
|
|
200
|
+
current = next
|
|
201
|
+
return result
|
|
202
|
+
},
|
|
203
|
+
update(view) {
|
|
204
|
+
live()
|
|
205
|
+
const next = receive(view)
|
|
206
|
+
const result = publishScene(next, lens, focused && next.nodes.has(refKey(focused)) ? focused : null)
|
|
207
|
+
const changed = current.view.projectionDigest !== next.view.projectionDigest
|
|
208
|
+
current = next
|
|
209
|
+
if (changed || (selected && !next.nodes.has(refKey(selected)))) { selected = null; activePort?.select(null) }
|
|
210
|
+
if (changed || (selectedEdgeId && !next.view.edges.some(edge => edge.edgeId === selectedEdgeId))) selectedEdgeId = null
|
|
211
|
+
if (questionId && !next.view.questions?.some(question => question.questionId === questionId)) questionId = null
|
|
212
|
+
if (focused && !next.nodes.has(refKey(focused))) focused = null
|
|
213
|
+
return result
|
|
214
|
+
},
|
|
215
|
+
select(ref, { projectionDigest = live().view.projectionDigest, focus = false } = {}) {
|
|
216
|
+
live()
|
|
217
|
+
if (paused) return { status: 'paused' }
|
|
218
|
+
if (projectionDigest !== current.view.projectionDigest) return { status: 'stale' }
|
|
219
|
+
const node = nodeFor(ref)
|
|
220
|
+
if (!node) return { status: 'unavailable' }
|
|
221
|
+
if (!shown.some(item => refKey(item) === refKey(ref))) {
|
|
222
|
+
const result = publishScene(current, lens, ref)
|
|
223
|
+
if (result.status !== 'ready') return result
|
|
224
|
+
}
|
|
225
|
+
const result = sceneController.applyCommand({ type: 'select', target: binding(node) })
|
|
226
|
+
if (focus && result.status === 'applied') {
|
|
227
|
+
sceneController.applyCommand({ type: 'focus', target: binding(node) })
|
|
228
|
+
focused = copyRef(ref)
|
|
229
|
+
}
|
|
230
|
+
return { status: result.status }
|
|
231
|
+
},
|
|
232
|
+
focus(ref) {
|
|
233
|
+
if (paused) return { status: 'paused' }
|
|
234
|
+
const node = nodeFor(ref)
|
|
235
|
+
if (!node) return { status: 'unavailable' }
|
|
236
|
+
if (!shown.some(item => refKey(item) === refKey(ref))) {
|
|
237
|
+
const result = publishScene(current, lens, ref)
|
|
238
|
+
if (result.status !== 'ready') return result
|
|
239
|
+
}
|
|
240
|
+
const result = sceneController.applyCommand({ type: 'focus', target: binding(node) })
|
|
241
|
+
if (result.status === 'applied') focused = copyRef(ref)
|
|
242
|
+
return { status: result.status }
|
|
243
|
+
},
|
|
244
|
+
setLens(nextLens) {
|
|
245
|
+
live()
|
|
246
|
+
if (paused) return { status: 'paused' }
|
|
247
|
+
if (!['all', 'support', 'contradictions'].includes(nextLens)) return { status: 'unsupported' }
|
|
248
|
+
const result = publishScene(current, nextLens, null)
|
|
249
|
+
lens = nextLens; focused = null
|
|
250
|
+
return result
|
|
251
|
+
},
|
|
252
|
+
expand(ref, direction = 'both') {
|
|
253
|
+
if (paused) return { status: 'paused' }
|
|
254
|
+
if (!nodeFor(ref) || !['in', 'out', 'both'].includes(direction)) return { status: 'unavailable' }
|
|
255
|
+
return { status: 'requested', intent: emit('expand', { ref: copyRef(ref), direction }) }
|
|
256
|
+
},
|
|
257
|
+
selectEdge(edgeId, { projectionDigest = live().view.projectionDigest } = {}) {
|
|
258
|
+
live()
|
|
259
|
+
if (paused) return { status: 'paused' }
|
|
260
|
+
if (projectionDigest !== current.view.projectionDigest) return { status: 'stale' }
|
|
261
|
+
if (!current.view.edges.some(edge => edge.edgeId === edgeId)) return { status: 'unavailable' }
|
|
262
|
+
activePort?.select(null)
|
|
263
|
+
selectedEdgeId = edgeId
|
|
264
|
+
selected = null
|
|
265
|
+
return { status: 'requested', intent: emit('select', { edgeId }) }
|
|
266
|
+
},
|
|
267
|
+
openEvidence(evidenceId) {
|
|
268
|
+
if (paused) return { status: 'paused' }
|
|
269
|
+
if (!live().view.evidence?.some(item => item.evidenceId === evidenceId)) return { status: 'unavailable' }
|
|
270
|
+
return { status: 'requested', intent: emit('openEvidence', { evidenceId }) }
|
|
271
|
+
},
|
|
272
|
+
setQuestion(nextId) {
|
|
273
|
+
if (paused) return { status: 'paused' }
|
|
274
|
+
if (!live().view.questions?.some(item => item.questionId === nextId)) return { status: 'unavailable' }
|
|
275
|
+
questionId = nextId
|
|
276
|
+
return { status: 'requested', intent: emit('setQuestion', { questionId }) }
|
|
277
|
+
},
|
|
278
|
+
receiveSelection(selection) {
|
|
279
|
+
live()
|
|
280
|
+
if (paused) return { status: 'paused' }
|
|
281
|
+
if (selection === null) { activePort?.select(null); selected = selectedEdgeId = null; return { status: 'applied' } }
|
|
282
|
+
if (selection?.projectionDigest !== current.view.projectionDigest) return { status: 'stale' }
|
|
283
|
+
const fields = Object.keys(selection).sort().join(',')
|
|
284
|
+
if (fields !== 'projectionDigest,ref' && fields !== 'edgeId,projectionDigest') throw new TypeError('Selection readback requires one exact target')
|
|
285
|
+
assertGraphViewIntent(current.view, { ...selection, type: 'select' })
|
|
286
|
+
silent++
|
|
287
|
+
try {
|
|
288
|
+
return selection.ref ? api.select(selection.ref, { projectionDigest: selection.projectionDigest }) :
|
|
289
|
+
api.selectEdge(selection.edgeId, { projectionDigest: selection.projectionDigest })
|
|
290
|
+
} finally { silent-- }
|
|
291
|
+
},
|
|
292
|
+
pickAt(x, y) { live(); return sceneReady && sceneController.pickAt(x, y) ? { status: 'selected' } : null },
|
|
293
|
+
frame() { return sceneController.frame() },
|
|
294
|
+
pause() { live(); if (sceneReady) sceneController.pause(); paused = true },
|
|
295
|
+
resume() { live(); if (sceneReady) sceneController.resume(); paused = false },
|
|
296
|
+
snapshot() {
|
|
297
|
+
live()
|
|
298
|
+
return clone({ projectionDigest: current.view.projectionDigest, selected, selectedEdgeId, questionId, focused, lens, paused, shown,
|
|
299
|
+
sceneNodes: shown.length, sceneEdges: sceneEdgeCount, omittedNodes: matchingCount - shown.length,
|
|
300
|
+
omittedEdges: eligibleEdgeCount - sceneEdgeCount, sceneScope,
|
|
301
|
+
unrenderedProjectionNodes: current.view.nodes.length - shown.length,
|
|
302
|
+
unrenderedProjectionEdges: current.view.edges.length - sceneEdgeCount,
|
|
303
|
+
page: current.view.page ?? null, completeness: current.view.completeness ?? 'unqualified-fixture' })
|
|
304
|
+
},
|
|
305
|
+
dispose() {
|
|
306
|
+
if (disposed) return
|
|
307
|
+
disposed = true
|
|
308
|
+
try { sceneController.dispose() } finally { current = activePort = preparedPort = null; selected = focused = selectedEdgeId = questionId = null; shown = [] }
|
|
309
|
+
},
|
|
310
|
+
})
|
|
311
|
+
return api
|
|
312
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Source lineage only; no release, permission or acceptance authority.
|
|
2
|
+
export const sourceProvenance = Object.freeze({
|
|
3
|
+
"description": "Local additive portable extraction; distribution and host integration remain separate receiving gates.",
|
|
4
|
+
"controllerBaseline": "42ae72a5b161389ca0652eae3294ae643d26b1d6",
|
|
5
|
+
"privateConsumerBaseline": "562f1e3fa880492b3e5c941c011adfd7c21b0370",
|
|
6
|
+
"correctedConsumerSource": "e4fe5117d5545271b68cb7113e5cd0e10b31cf09",
|
|
7
|
+
"graphViewProducer": "c5d787330692b664d37f7c5bca7e03f6b37f3d41",
|
|
8
|
+
"portableChanges": [
|
|
9
|
+
"Consumer imports point to the received graph-view helpers and sibling retained modules.",
|
|
10
|
+
"Index provides internal opt-in entry; no package root export or mandatory Babylon dependency.",
|
|
11
|
+
"Tests and invented fixture moved into dedicated consumer paths."
|
|
12
|
+
],
|
|
13
|
+
"authorityLimit": "This record describes source lineage only; it is not a review verdict, grant, canonical admission, release or host acceptance.",
|
|
14
|
+
"retainedModules": [
|
|
15
|
+
{
|
|
16
|
+
"module": "scene-controller.mjs",
|
|
17
|
+
"sha256": "721f151beb9912235524ceba658c3654fde1da4cda0af2624f8d35a5518963d2",
|
|
18
|
+
"changes": [],
|
|
19
|
+
"extractedSha256": "721f151beb9912235524ceba658c3654fde1da4cda0af2624f8d35a5518963d2"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"module": "semantic-picking.mjs",
|
|
23
|
+
"sha256": "3577fa576b1074051c4c2c2aa94ccccb4dd0fb4b18293d70751497ced4ba8e6b",
|
|
24
|
+
"changes": [],
|
|
25
|
+
"extractedSha256": "3577fa576b1074051c4c2c2aa94ccccb4dd0fb4b18293d70751497ced4ba8e6b"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"module": "babylon-scene-port.mjs",
|
|
29
|
+
"sha256": "335cb8681ed584f51f6e1a8fa97cb36450a32ce197afaa58b8d05372b4537e07",
|
|
30
|
+
"changes": [
|
|
31
|
+
"Adapter-local generated mesh and material labels normalized to neutral generic names; identity and geometry behavior unchanged."
|
|
32
|
+
],
|
|
33
|
+
"extractedSha256": "683f703b09c35ac0935a4ffa7bb00cf819eacb2351c99821215bfd27a222d0ee"
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
})
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { referenceKey, semanticBinding, createSemanticPickingMap } from './semantic-picking.mjs'
|
|
2
|
+
|
|
3
|
+
// DOM-free contribution boundary. Hosts own engine, render-loop, input and overlays.
|
|
4
|
+
// Scene ports own only their scene, camera, meshes and materials.
|
|
5
|
+
export function createSceneController({ createPort, onSemanticEvent = () => {} }) {
|
|
6
|
+
let current = null
|
|
7
|
+
let disposed = false
|
|
8
|
+
let paused = false
|
|
9
|
+
|
|
10
|
+
function live() {
|
|
11
|
+
if (disposed) throw new Error('Controller disposed')
|
|
12
|
+
if (!current) throw new Error('Scene not mounted')
|
|
13
|
+
return current
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function prepare(profile) {
|
|
17
|
+
// Freeze producer bytes before allocating any graphics resources.
|
|
18
|
+
const copy = structuredClone(profile)
|
|
19
|
+
if (!copy.profileId || !copy.profileRevision || !Array.isArray(copy.nodes) || !Array.isArray(copy.edges)) {
|
|
20
|
+
throw new TypeError('Versioned graph projection required')
|
|
21
|
+
}
|
|
22
|
+
if (!copy.nodes.length || copy.nodes.length > 48 || copy.edges.length > 96) throw new RangeError('Small graph envelope exceeded')
|
|
23
|
+
const nodes = new Map()
|
|
24
|
+
for (const node of copy.nodes) {
|
|
25
|
+
const key = referenceKey(node.ref)
|
|
26
|
+
semanticBinding(node)
|
|
27
|
+
if (nodes.has(key)) throw new Error('Duplicate semantic reference')
|
|
28
|
+
if (typeof node.label !== 'string' || !node.label.trim() || !Array.isArray(node.position) ||
|
|
29
|
+
node.position.length !== 3 || node.position.some(value => !Number.isFinite(value))) {
|
|
30
|
+
throw new TypeError('Label and finite scene position required')
|
|
31
|
+
}
|
|
32
|
+
nodes.set(key, node)
|
|
33
|
+
}
|
|
34
|
+
for (const edge of copy.edges) {
|
|
35
|
+
if (!nodes.has(referenceKey(edge.from)) || !nodes.has(referenceKey(edge.to)) ||
|
|
36
|
+
typeof edge.label !== 'string' || !edge.label.trim()) throw new Error('Unresolved relation')
|
|
37
|
+
}
|
|
38
|
+
const picking = createSemanticPickingMap()
|
|
39
|
+
let port
|
|
40
|
+
try {
|
|
41
|
+
port = createPort(copy, (mesh, node) => {
|
|
42
|
+
const supplied = nodes.get(referenceKey(node.ref))
|
|
43
|
+
if (!supplied) throw new Error('Port attempted to bind an unresolved reference')
|
|
44
|
+
picking.bind(mesh, supplied)
|
|
45
|
+
})
|
|
46
|
+
for (const method of ['render', 'pick', 'focus', 'select', 'dispose']) {
|
|
47
|
+
if (typeof port?.[method] !== 'function') throw new TypeError('Incomplete scene port')
|
|
48
|
+
}
|
|
49
|
+
return { profile: copy, nodes, picking, port, selected: [] }
|
|
50
|
+
} catch (error) {
|
|
51
|
+
port?.dispose?.()
|
|
52
|
+
picking.clear()
|
|
53
|
+
throw error
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function release(scene) {
|
|
58
|
+
scene.picking.clear()
|
|
59
|
+
scene.port.dispose()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function emit(scene, type, binding) {
|
|
63
|
+
// No mesh/engine/DOM handles escape this event.
|
|
64
|
+
const event = Object.freeze({ type, profileId: scene.profile.profileId,
|
|
65
|
+
profileRevision: scene.profile.profileRevision, ...binding })
|
|
66
|
+
onSemanticEvent(event)
|
|
67
|
+
return event
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return Object.freeze({
|
|
71
|
+
mount(profile) {
|
|
72
|
+
if (disposed) throw new Error('Controller disposed')
|
|
73
|
+
if (current) throw new Error('Use rebuild for a replaced scene')
|
|
74
|
+
current = prepare(profile)
|
|
75
|
+
},
|
|
76
|
+
applyCommand(command) {
|
|
77
|
+
const scene = live()
|
|
78
|
+
if (paused) return { status: 'paused' }
|
|
79
|
+
if (!['select', 'inspect', 'focus'].includes(command?.type)) return { status: 'unsupported', type: command?.type }
|
|
80
|
+
const node = scene.nodes.get(referenceKey(command.target))
|
|
81
|
+
if (!node) return { status: 'unavailable-reference' }
|
|
82
|
+
const binding = semanticBinding(node)
|
|
83
|
+
if (command.type === 'select') {
|
|
84
|
+
scene.port.select(binding.target)
|
|
85
|
+
scene.selected = [binding.target]
|
|
86
|
+
}
|
|
87
|
+
if (command.type === 'focus') scene.port.focus(node.position)
|
|
88
|
+
return { status: 'applied', event: emit(scene, command.type, binding) }
|
|
89
|
+
},
|
|
90
|
+
pickAt(x, y) {
|
|
91
|
+
const scene = live()
|
|
92
|
+
if (paused || !Number.isFinite(x) || !Number.isFinite(y)) return null
|
|
93
|
+
const binding = scene.picking.resolve(scene.port.pick(x, y))
|
|
94
|
+
if (!binding) return null
|
|
95
|
+
scene.port.select(binding.target)
|
|
96
|
+
scene.selected = [binding.target]
|
|
97
|
+
return emit(scene, 'select', binding)
|
|
98
|
+
},
|
|
99
|
+
frame() {
|
|
100
|
+
if (disposed || paused || !current) return false
|
|
101
|
+
current.port.render()
|
|
102
|
+
return true
|
|
103
|
+
},
|
|
104
|
+
pause() { live(); paused = true },
|
|
105
|
+
resume() { live(); paused = false },
|
|
106
|
+
rebuild(profile) {
|
|
107
|
+
const old = live()
|
|
108
|
+
const next = prepare(profile)
|
|
109
|
+
// Preserve only exact references still provided by the next producer revision.
|
|
110
|
+
next.selected = old.selected.filter(ref => next.nodes.has(referenceKey(ref)))
|
|
111
|
+
try { next.port.select(next.selected[0] ?? null) }
|
|
112
|
+
catch (error) { release(next); throw error }
|
|
113
|
+
current = next
|
|
114
|
+
release(old)
|
|
115
|
+
},
|
|
116
|
+
structuredProjection() {
|
|
117
|
+
const scene = live()
|
|
118
|
+
return { profileId: scene.profile.profileId, profileRevision: scene.profile.profileRevision,
|
|
119
|
+
selected: structuredClone(scene.selected),
|
|
120
|
+
nodes: [...scene.nodes.values()].map(node => ({ label: node.label, ...semanticBinding(node) })),
|
|
121
|
+
relations: scene.profile.edges.map(edge => structuredClone(edge)),
|
|
122
|
+
interactionLimits: 'Structured projection; no spatial rendering or activity completion claim' }
|
|
123
|
+
},
|
|
124
|
+
dispose() {
|
|
125
|
+
if (disposed) return
|
|
126
|
+
disposed = true
|
|
127
|
+
if (current) release(current)
|
|
128
|
+
current = null
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Contribution only. CR-1 owns the adopted reference and event contracts.
|
|
2
|
+
export function referenceKey(ref) {
|
|
3
|
+
const fields = ['domain', 'kind', 'id', 'revision']
|
|
4
|
+
if (!ref || fields.some(field => typeof ref[field] !== 'string' || !ref[field].trim())) {
|
|
5
|
+
throw new TypeError('A complete versioned semantic reference is required')
|
|
6
|
+
}
|
|
7
|
+
return JSON.stringify(fields.map(field => ref[field]))
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function semanticBinding(node) {
|
|
11
|
+
referenceKey(node.ref)
|
|
12
|
+
if (typeof node.sourceRevision !== 'string' || !node.sourceRevision.trim() ||
|
|
13
|
+
!Array.isArray(node.evidenceIds) || node.evidenceIds.some(id => typeof id !== 'string' || !id.trim())) {
|
|
14
|
+
throw new TypeError('Source revision and declared evidence identities are required')
|
|
15
|
+
}
|
|
16
|
+
return Object.freeze({
|
|
17
|
+
target: Object.freeze(Object.fromEntries(['domain', 'kind', 'id', 'revision'].map(key => [key, node.ref[key]]))),
|
|
18
|
+
sourceRevision: node.sourceRevision,
|
|
19
|
+
evidenceIds: Object.freeze([...node.evidenceIds]),
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Mesh objects have adapter-local custody. Mesh names and metadata never establish identity.
|
|
24
|
+
export function createSemanticPickingMap() {
|
|
25
|
+
let bindings = new WeakMap()
|
|
26
|
+
return Object.freeze({
|
|
27
|
+
bind(mesh, node) {
|
|
28
|
+
if (!mesh || typeof mesh !== 'object') throw new TypeError('Mesh object required')
|
|
29
|
+
if (bindings.has(mesh)) throw new Error('Mesh already bound')
|
|
30
|
+
bindings.set(mesh, semanticBinding(node))
|
|
31
|
+
},
|
|
32
|
+
resolve(mesh) { return mesh && typeof mesh === 'object' ? bindings.get(mesh) ?? null : null },
|
|
33
|
+
clear() { bindings = new WeakMap() },
|
|
34
|
+
})
|
|
35
|
+
}
|