@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.
Files changed (253) hide show
  1. package/CHANGELOG.md +283 -0
  2. package/README.md +102 -52
  3. package/SECURITY.md +1 -1
  4. package/TRADEMARKS.md +2 -2
  5. package/contracts/atelier-extension-manifest.v1.schema.json +412 -0
  6. package/contracts/atelier-graph-view.v1.schema.json +1085 -0
  7. package/contracts/public-api-baseline.json +298 -19
  8. package/docs/README.md +90 -0
  9. package/docs/blocks/capability-stewardship.md +1 -1
  10. package/docs/blocks/claims.md +2 -2
  11. package/docs/blocks/conformance-admission.md +1 -1
  12. package/docs/blocks/learning-harnesses.md +1 -1
  13. package/docs/{atelier-sync.md → guides/atelier-sync.md} +20 -14
  14. package/docs/{capability-stewardship.md → guides/capability-stewardship.md} +2 -4
  15. package/docs/guides/co-authoring.md +113 -0
  16. package/docs/{distributions.md → guides/distributions.md} +13 -8
  17. package/docs/guides/extensions.md +189 -0
  18. package/docs/guides/graph-refresh.md +152 -0
  19. package/docs/guides/graph-workspace.md +64 -0
  20. package/docs/{ingestion.md → guides/ingestion.md} +16 -15
  21. package/docs/{inquiry-harnesses.md → guides/inquiry-harnesses.md} +2 -3
  22. package/docs/{knowledge-workspace.md → guides/knowledge-workspace.md} +2 -2
  23. package/docs/{learning-harnesses.md → guides/learning-harnesses.md} +2 -3
  24. package/docs/{learning.md → guides/learning.md} +12 -5
  25. package/docs/{obsidian.md → guides/obsidian.md} +370 -23
  26. package/docs/guides/ontology-guide.md +111 -0
  27. package/docs/{personal-workspace.md → guides/personal-workspace.md} +17 -5
  28. package/docs/{research-starter.md → guides/research-starter.md} +1 -1
  29. package/docs/{tenant-readiness.md → guides/tenant-readiness.md} +39 -0
  30. package/docs/{upgrade.md → guides/upgrade.md} +197 -11
  31. package/docs/knowledge-health/assessment/capabilities.md +6 -15
  32. package/docs/knowledge-health/facilitator.md +15 -4
  33. package/docs/knowledge-health/{devday/participant-response.md → participant-response.md} +4 -2
  34. package/docs/knowledge-health/quickstart.md +97 -75
  35. package/docs/knowledge-health/readme.md +13 -5
  36. package/docs/maintainers/describing-the-atelier.md +173 -0
  37. package/docs/{release-engineering.md → maintainers/release-engineering.md} +5 -4
  38. package/docs/{announcements.md → reference/announcements.md} +1 -1
  39. package/docs/{architecture.md → reference/architecture.md} +31 -5
  40. package/docs/{atelier-runtime.md → reference/atelier-runtime.md} +1 -1
  41. package/docs/{connected-composition.md → reference/connected-composition.md} +7 -10
  42. package/docs/{coordination.md → reference/coordination.md} +1 -1
  43. package/docs/{decisions.md → reference/decisions.md} +3 -3
  44. package/docs/{design.md → reference/design.md} +1 -1
  45. package/docs/reference/edge-policy.md +151 -0
  46. package/docs/reference/graph-view-structured.md +119 -0
  47. package/docs/reference/graph-view.md +139 -0
  48. package/docs/reference/graph-workspace.md +37 -0
  49. package/docs/{intake-and-guides.md → reference/intake-and-guides.md} +3 -4
  50. package/docs/{knowledge-graph.md → reference/knowledge-graph.md} +120 -6
  51. package/docs/{local-services.md → reference/local-services.md} +43 -4
  52. package/docs/{obsidian-contract.md → reference/obsidian-contract.md} +29 -4
  53. package/docs/{obsidian-plugin.md → reference/obsidian-plugin.md} +218 -68
  54. package/docs/{practical-judgment.md → reference/phronesis.md} +12 -3
  55. package/docs/reference/portability.md +92 -0
  56. package/docs/{portable-presentation.md → reference/portable-presentation.md} +2 -2
  57. package/docs/{presentation-consumer-boundaries.md → reference/presentation-consumer-boundaries.md} +5 -0
  58. package/docs/{project-options.md → reference/project-options.md} +17 -0
  59. package/docs/{repo-boundary-guard.md → reference/repo-boundary-guard.md} +50 -4
  60. package/docs/{review-portability.md → reference/review-portability.md} +4 -0
  61. package/docs/{semantic-operation.md → reference/semantic-operation.md} +4 -3
  62. package/docs/{template-profile.md → reference/template-profile.md} +1 -1
  63. package/docs/{trackables.md → reference/trackables.md} +2 -2
  64. package/docs/start/independent-start.md +89 -0
  65. package/docs/{install.md → start/install.md} +9 -9
  66. package/docs/{knowledge-setup.md → start/knowledge-setup.md} +5 -5
  67. package/docs/{research-starter-quickstart.md → start/research-starter-quickstart.md} +6 -6
  68. package/fixtures/atelier-architecture/catalog/invalid/document.json +2 -2
  69. package/fixtures/atelier-architecture/catalog/valid/document.json +2 -2
  70. package/fixtures/graph-view/synthetic-authorized-input.v1.json +437 -0
  71. package/fixtures/graph-view/synthetic-view.v1.json +1 -0
  72. package/fixtures/knowledge-health/workshop/README.md +1 -1
  73. package/fixtures/templates/local-library.v1.json +3 -1
  74. package/package.json +22 -8
  75. package/plugins/obsidian/main.js +57 -8
  76. package/plugins/obsidian/manifest.json +1 -1
  77. package/skills/claude/atelier-guided-upgrade/SKILL.md +1 -1
  78. package/skills/claude/atelier-knowledge-setup/SKILL.md +2 -2
  79. package/skills/claude/atelier-local-service/SKILL.md +1 -1
  80. package/skills/claude/atelier-practical-judgment/SKILL.md +7 -2
  81. package/skills/claude/atelier-skill-steward/SKILL.md +1 -1
  82. package/skills/claude/mnstry-readiness/SKILL.md +2 -2
  83. package/skills/codex/atelier-guided-upgrade/SKILL.md +1 -1
  84. package/skills/codex/atelier-knowledge-setup/SKILL.md +2 -2
  85. package/skills/codex/atelier-local-service/SKILL.md +1 -1
  86. package/skills/codex/atelier-practical-judgment/SKILL.md +7 -2
  87. package/skills/codex/atelier-skill-steward/SKILL.md +1 -1
  88. package/skills/codex/mnstry-readiness/SKILL.md +2 -2
  89. package/src/architecture/catalog.mjs +2 -2
  90. package/src/attestation/sign.mjs +1 -1
  91. package/src/boundary/content-rules.mjs +534 -68
  92. package/src/boundary/policy.mjs +187 -60
  93. package/src/cli/run.mjs +26 -9
  94. package/src/commands/distribution.mjs +1 -1
  95. package/src/commands/extension.mjs +27 -0
  96. package/src/commands/ingest.mjs +1 -1
  97. package/src/commands/learn.mjs +1 -1
  98. package/src/commands/obsidian.mjs +49 -21
  99. package/src/commands/review.mjs +3 -0
  100. package/src/edge-policy/contract.mjs +69 -0
  101. package/src/edge-policy/policy.mjs +142 -0
  102. package/src/edge-policy/source-citation-adapter.mjs +43 -0
  103. package/src/extension-packs/loader.mjs +4 -3
  104. package/src/extensions/admission.mjs +83 -0
  105. package/src/extensions/client.d.mts +23 -0
  106. package/src/extensions/client.mjs +99 -0
  107. package/src/extensions/contracts.d.mts +29 -0
  108. package/src/extensions/contracts.mjs +163 -0
  109. package/src/extensions/convex.d.mts +24 -0
  110. package/src/extensions/convex.mjs +189 -0
  111. package/src/extensions/delegation.mjs +54 -0
  112. package/src/extensions/drizzle.d.mts +11 -0
  113. package/src/extensions/drizzle.mjs +33 -0
  114. package/src/extensions/postgres.d.mts +7 -0
  115. package/src/extensions/postgres.mjs +66 -0
  116. package/src/extensions/prisma.d.mts +13 -0
  117. package/src/extensions/prisma.mjs +63 -0
  118. package/src/extensions/project.mjs +432 -0
  119. package/src/extensions/scaffold-convex.mjs +130 -0
  120. package/src/extensions/scaffold-sql.mjs +144 -0
  121. package/src/extensions/server.d.mts +20 -0
  122. package/src/extensions/server.mjs +102 -0
  123. package/src/extensions/sql-scope.mjs +22 -0
  124. package/src/graph/graph.mjs +13 -5
  125. package/src/graph/knowledge-graph.mjs +20 -2
  126. package/src/graph/repo-contribution.mjs +405 -0
  127. package/src/graph/repo-contribution.types.mjs +32 -0
  128. package/src/graph-access/edge-decision-adapter.mjs +41 -0
  129. package/src/graph-access/edge-record-adapter.mjs +73 -0
  130. package/src/graph-access/index.mjs +253 -0
  131. package/src/graph-access/multi-contribution-record-adapter.mjs +101 -0
  132. package/src/graph-access/multi-record-projection-adapter.mjs +74 -0
  133. package/src/graph-access/source-adapter.mjs +81 -0
  134. package/src/graph-refresh/compare.mjs +110 -0
  135. package/src/graph-refresh/controller.mjs +324 -0
  136. package/src/graph-refresh/index.mjs +3 -0
  137. package/src/graph-refresh/local.mjs +78 -0
  138. package/src/graph-view/babylon/babylon-scene-port.mjs +64 -0
  139. package/src/graph-view/babylon/index.mjs +2 -0
  140. package/src/graph-view/babylon/navigator.mjs +312 -0
  141. package/src/graph-view/babylon/provenance.mjs +36 -0
  142. package/src/graph-view/babylon/scene-controller.mjs +131 -0
  143. package/src/graph-view/babylon/semantic-picking.mjs +35 -0
  144. package/src/graph-view/babylon/web-host.mjs +188 -0
  145. package/src/graph-view/cli.mjs +3 -0
  146. package/src/graph-view/command.mjs +34 -0
  147. package/src/graph-view/contracts.mjs +79 -0
  148. package/src/graph-view/errors.mjs +5 -0
  149. package/src/graph-view/index.mjs +4 -0
  150. package/src/graph-view/intents.mjs +30 -0
  151. package/src/graph-view/query.mjs +213 -0
  152. package/src/graph-view/refs.mjs +2 -0
  153. package/src/graph-view/structured/browser.mjs +262 -0
  154. package/src/graph-view/structured/styles.mjs +8 -0
  155. package/src/graph-workspace/authoring.mjs +185 -0
  156. package/src/graph-workspace/browser/constellation.mjs +369 -0
  157. package/src/graph-workspace/browser/dom.mjs +23 -0
  158. package/src/graph-workspace/browser/index.mjs +6 -0
  159. package/src/graph-workspace/browser/review.mjs +55 -0
  160. package/src/graph-workspace/browser/styles.mjs +11 -0
  161. package/src/graph-workspace/browser/workbench.mjs +351 -0
  162. package/src/graph-workspace/browser/workspace.mjs +431 -0
  163. package/src/graph-workspace/disclosure.mjs +39 -0
  164. package/src/graph-workspace/geometry.mjs +78 -0
  165. package/src/graph-workspace/index.mjs +11 -0
  166. package/src/graph-workspace/labels.mjs +50 -0
  167. package/src/graph-workspace/model.mjs +109 -0
  168. package/src/graph-workspace/recipes.mjs +88 -0
  169. package/src/graph-workspace/scale.mjs +95 -0
  170. package/src/graph-workspace/theme.mjs +156 -0
  171. package/src/ingestion/optional/graphify/graphify_supplier.py +5 -0
  172. package/src/ingestion/optional/graphify/native-closure-pins.json +275 -0
  173. package/src/ingestion/optional/graphify/native-pins.json +306 -0
  174. package/src/knowledge/ontology-guide.mjs +152 -0
  175. package/src/knowledge-health/proof/installed-check.mjs +2 -2
  176. package/src/knowledge-health/rule-identity.mjs +14 -9
  177. package/src/knowledge-health/workshop.mjs +22 -2
  178. package/src/personal-workspace/index.mjs +1 -1
  179. package/src/personal-workspace/selection.mjs +34 -3
  180. package/src/project/config.mjs +1 -1
  181. package/src/project/path-match.mjs +2 -1
  182. package/src/project/providers/github.mjs +24 -0
  183. package/src/project/repo-identity.mjs +3 -22
  184. package/src/projection/obsidian/contracts.mjs +1 -1
  185. package/src/projection/obsidian/materialize/path-registry.mjs +1 -1
  186. package/src/projection/obsidian/materialize/prepare-view.mjs +88 -6
  187. package/src/projection/obsidian/materialize/redaction.mjs +1 -1
  188. package/src/projection/obsidian/personal-workspace.mjs +84 -5
  189. package/src/projection/obsidian/plugin-bridge/channel.mjs +1 -1
  190. package/src/projection/obsidian/proposals/adapter.mjs +46 -23
  191. package/src/projection/obsidian/proposals/observation.mjs +40 -2
  192. package/src/projection/obsidian/publication/publisher.mjs +96 -8
  193. package/src/projection/project.mjs +21 -16
  194. package/src/readiness/readiness.mjs +20 -11
  195. package/src/readiness-protocols/bundled-pack.mjs +14 -766
  196. package/src/readiness-protocols/bundled-selection.mjs +32 -0
  197. package/src/readiness-protocols/evidence.mjs +2 -1
  198. package/src/readiness-protocols/packs/mnstry.mjs +766 -0
  199. package/src/readiness-protocols/runtime.mjs +43 -6
  200. package/src/runtime/obsidian/engine.mjs +387 -76
  201. package/src/runtime/obsidian/lifecycle.mjs +20 -15
  202. package/src/runtime/obsidian/login-item.mjs +6 -3
  203. package/src/runtime/obsidian/machine-settings.mjs +52 -8
  204. package/src/runtime/obsidian/observation.mjs +36 -4
  205. package/src/runtime/obsidian/opening.mjs +4 -3
  206. package/src/runtime/obsidian/personal-selection.mjs +187 -0
  207. package/src/runtime/obsidian/pipeline.mjs +115 -6
  208. package/src/runtime/obsidian/private-lock.mjs +50 -6
  209. package/src/runtime/obsidian/selection-continuity.mjs +124 -0
  210. package/src/runtime/obsidian/service-main.mjs +68 -16
  211. package/src/runtime/obsidian/service.mjs +80 -6
  212. package/src/runtime/obsidian/unheld-evidence.mjs +1 -1
  213. package/src/runtime/obsidian/view-permission.mjs +335 -0
  214. package/src/ui/control-system.mjs +201 -0
  215. package/src/ui/html-primitives.mjs +3 -0
  216. package/src/upgrade/closure-diagnostics.mjs +135 -0
  217. package/src/upgrade/closure.mjs +649 -0
  218. package/src/upgrade/upgrade.mjs +42 -11
  219. package/templates/distribution-workspace/README.md +2 -2
  220. package/templates/external-project-workspace/atelier.project.json +3 -0
  221. package/templates/external-project-workspace/pack-lifecycle.json +3 -1
  222. package/templates/knowledge-workspace/README.md +2 -2
  223. package/docs/integration-and-review-plan.md +0 -409
  224. package/docs/integration-contract-decisions.md +0 -44
  225. package/docs/knowledge-health/devday/facilitator.md +0 -16
  226. package/docs/knowledge-health/devday/public-default.md +0 -107
  227. package/docs/knowledge-health/devday/quickstart.md +0 -18
  228. package/docs/knowledge-health/devday/support.md +0 -22
  229. package/docs/knowledge-health/exercise.md +0 -17
  230. package/docs/knowledge-health/support.md +0 -34
  231. package/docs/presentation-review-disposition.md +0 -47
  232. package/docs/progressive-ingestion-acceptance.md +0 -89
  233. package/docs/progressive-ingestion.md +0 -235
  234. /package/docs/{exact-upgrades.md → guides/exact-upgrades.md} +0 -0
  235. /package/docs/{guided-upgrades.md → guides/guided-upgrades.md} +0 -0
  236. /package/docs/{local-review.md → guides/local-review.md} +0 -0
  237. /package/docs/{skill-steward.md → guides/skill-steward.md} +0 -0
  238. /package/docs/{template-upgrades.md → guides/template-upgrades.md} +0 -0
  239. /package/docs/{assurance-controls.md → maintainers/assurance-controls.md} +0 -0
  240. /package/docs/{attestation.md → reference/attestation.md} +0 -0
  241. /package/docs/{coauthor-session.md → reference/coauthor-session.md} +0 -0
  242. /package/docs/{continuity.md → reference/continuity.md} +0 -0
  243. /package/docs/{contract-stability.md → reference/contract-stability.md} +0 -0
  244. /package/docs/{decision-practices.md → reference/decision-practices.md} +0 -0
  245. /package/docs/{evidence-navigation.md → reference/evidence-navigation.md} +0 -0
  246. /package/docs/{extension-pack-lifecycle.md → reference/extension-pack-lifecycle.md} +0 -0
  247. /package/docs/{interaction-and-reflection.md → reference/interaction-and-reflection.md} +0 -0
  248. /package/docs/{local-state.md → reference/local-state.md} +0 -0
  249. /package/docs/{ontology.md → reference/ontology.md} +0 -0
  250. /package/docs/{optional-ingestion.md → reference/optional-ingestion.md} +0 -0
  251. /package/docs/{project-command-inventory.md → reference/project-command-inventory.md} +0 -0
  252. /package/docs/{semantic-ingestion.md → reference/semantic-ingestion.md} +0 -0
  253. /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
+ }