@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,369 @@
1
+ // Renderer dependencies are injected so Atelier carries no graph-drawing package:
2
+ // deps = { Sigma, Graph, EdgeArrowProgram, FA2Layout? } (sigma 3, graphology,
3
+ // sigma/rendering's EdgeArrowProgram, graphology-layout-forceatlas2/worker).
4
+ //
5
+ // The constellation: every readable record, alive. Sigma (WebGL) draws nodes and
6
+ // edges; two canvases around it draw zone glows (behind) and labels (in front).
7
+ // Labels are placed by priority with collision checks, faded by zoom, and drawn
8
+ // with a halo instead of a box. Hovering lights a neighbourhood; selecting keeps it.
9
+ // Selecting never moves the camera. "Show on map" and "Open" are explicit.
10
+ import { zoneGeometry, LAYOUT_WEIGHTS } from '../geometry.mjs'
11
+ import { placeLabels, labelThreshold, labelBudget } from '../labels.mjs'
12
+
13
+ const reduced = () => matchMedia('(prefers-reduced-motion: reduce)').matches
14
+ const hexToRgba = (hex, a) => { const n = parseInt(hex.slice(1, 7), 16); return `rgba(${n >> 16},${(n >> 8) & 255},${n & 255},${a})` }
15
+ const mix = (a, b, t) => { const p = parseInt(a.slice(1, 7), 16), q = parseInt(b.slice(1, 7), 16); const c = s => Math.round(((p >> s) & 255) * (1 - t) + ((q >> s) & 255) * t); return '#' + [16, 8, 0].map(s => c(s).toString(16).padStart(2, '0')).join('') }
16
+
17
+ export function createConstellation({ container, model, palette, font = 'system-ui, sans-serif', deps, reserveSelector = '[data-graph-reserve]', onSelect = () => {}, onHover = () => {}, onOpen = () => {} }) {
18
+ const { Sigma, Graph, EdgeArrowProgram, FA2Layout = null } = deps ?? {}
19
+ if (typeof Sigma !== 'function' || typeof Graph !== 'function' || typeof EdgeArrowProgram !== 'function') throw new TypeError('Inject sigma 3 (Sigma, EdgeArrowProgram) and graphology (Graph)')
20
+ if (model.nodes.some(n => !Number.isFinite(n.x) || !Number.isFinite(n.y))) throw new TypeError('Lay the model out first (layoutWorkspace or stored positions)')
21
+ const zones = new Map(model.zones.map(z => [z.id, z]))
22
+ const zoneColor = id => palette.zones[(zones.get(id)?.index ?? 0) % palette.zones.length]
23
+ const graph = new Graph({ type: 'directed', multi: true })
24
+ const byId = new Map(model.nodes.map(n => [n.id, n]))
25
+ // Label importance is the record's connection rank (0..1), so it adapts to any degree spread.
26
+ const ranked = [...model.nodes].sort((a, b) => b.degree - a.degree || (a.title < b.title ? -1 : 1))
27
+ const rank = new Map(ranked.map((n, i) => [n.id, 1 - i / Math.max(1, ranked.length - 1)]))
28
+ const widthCache = new Map()
29
+ for (const n of model.nodes) {
30
+ graph.addNode(n.id, { x: n.x, y: n.y, finalX: n.x, finalY: n.y, size: Math.min(10, 1.3 + 0.95 * Math.log2(1 + n.degree)), color: zoneColor(n.zone), label: n.title, zone: n.zone })
31
+ }
32
+ for (const e of model.edges) graph.addEdgeWithKey(e.id, e.source, e.target, { structural: e.kind === 'relation', kind: e.kind, type: 'line' })
33
+
34
+ const zoneCanvas = document.createElement('canvas')
35
+ const labelCanvas = document.createElement('canvas')
36
+ zoneCanvas.className = 'layer-zones'
37
+ labelCanvas.className = 'layer-labels'
38
+ zoneCanvas.setAttribute('aria-hidden', 'true')
39
+ labelCanvas.setAttribute('aria-hidden', 'true')
40
+ const sigmaHost = document.createElement('div')
41
+ sigmaHost.className = 'layer-sigma'
42
+ container.replaceChildren(zoneCanvas, sigmaHost, labelCanvas)
43
+
44
+ // Node sizes are in screen pixels; a small viewport needs proportionally smaller dots.
45
+ const sizeScale = () => Math.max(0.45, Math.min(1, Math.min(container.clientWidth, container.clientHeight) / 820))
46
+ const state = { hovered: null, selected: null, lens: null, dimAll: false, neighbours: new Set(), lit: null, zonesOn: true, linksOn: true, labelBoost: 1, focusZone: null }
47
+ let geometry = model.zoneGeometry ?? zoneGeometry(model)
48
+ let placedLabels = []
49
+ let frameTimes = []
50
+ const overlayTimes = []
51
+
52
+ function neighbourhood(id) {
53
+ const set = new Set([id])
54
+ if (id && graph.hasNode(id)) graph.forEachNeighbor(id, n => set.add(n))
55
+ return set
56
+ }
57
+ function activeSet() {
58
+ if (state.hovered) return neighbourhood(state.hovered)
59
+ if (state.selected) return neighbourhood(state.selected)
60
+ return null
61
+ }
62
+
63
+ let renderer = null
64
+ renderer = new Sigma(graph, sigmaHost, {
65
+ renderLabels: false,
66
+ renderEdgeLabels: false,
67
+ defaultEdgeType: 'line',
68
+ edgeProgramClasses: { arrow: EdgeArrowProgram },
69
+ zIndex: true,
70
+ minCameraRatio: 0.035,
71
+ maxCameraRatio: 1.6,
72
+ stagePadding: 40,
73
+ hideEdgesOnMove: false,
74
+ defaultDrawNodeHover: () => {},
75
+ nodeReducer(node, data) {
76
+ const set = activeSet()
77
+ const lit = state.lit
78
+ const res = { ...data, size: data.size * sizeScale() }
79
+ if (set) {
80
+ if (!set.has(node)) { res.color = mix(data.color, palette.dim, 0.82); res.zIndex = 0 }
81
+ else { res.zIndex = 2; if (node === state.hovered || node === state.selected) { res.size = data.size * 1.35; res.color = data.color } }
82
+ } else if (lit) {
83
+ if (!lit.has(node)) { res.color = mix(data.color, palette.dim, 0.86); res.zIndex = 0 } else res.zIndex = 2
84
+ } else if (state.focusZone && data.zone !== state.focusZone) {
85
+ res.color = mix(data.color, palette.dim, 0.7)
86
+ }
87
+ return res
88
+ },
89
+ edgeReducer(edge, data) {
90
+ const res = { ...data }
91
+ const [s, t] = graph.extremities(edge)
92
+ const set = activeSet()
93
+ if (set) {
94
+ const focus = state.hovered ?? state.selected
95
+ if (s !== focus && t !== focus) { res.hidden = true; return res }
96
+ res.color = data.structural ? palette.edgeStructural : mix(palette.edge, palette.canvas, 0.15)
97
+ res.size = data.structural ? 1.8 : 1
98
+ if (data.structural) res.type = 'arrow'
99
+ res.zIndex = 2
100
+ return res
101
+ }
102
+ if (state.lit) {
103
+ if (!(state.lit.has(s) && state.lit.has(t))) { res.hidden = true; return res }
104
+ res.color = mix(palette.edgeStructural, palette.canvas, 0.35); res.size = 1; return res
105
+ }
106
+ // Close in, link-only edges become long strands across the view; keep declared ones.
107
+ if (data.kind === 'link' && (!state.linksOn || state.near)) { res.hidden = true; return res }
108
+ // Solid colours pre-mixed with the canvas: no alpha accumulation into white.
109
+ res.color = data.structural ? mix(palette.edgeStructural, palette.canvas, 0.45) : data.kind === 'containment' ? mix(palette.edge, palette.canvas, 0.86) : mix(palette.edge, palette.canvas, 0.8)
110
+ res.size = data.structural ? 1.1 : 0.45
111
+ return res
112
+ },
113
+ })
114
+ const camera = renderer.getCamera()
115
+ state.near = false
116
+ camera.on('updated', s => { const near = s.ratio < 0.3; if (near !== state.near) { state.near = near; renderer.refresh({ skipIndexation: true }) } })
117
+
118
+ // ── Layers ──
119
+ function sizeCanvas(canvas) {
120
+ const dpr = Math.min(2, window.devicePixelRatio || 1)
121
+ const w = container.clientWidth, h = container.clientHeight
122
+ if (canvas.width !== Math.round(w * dpr) || canvas.height !== Math.round(h * dpr)) {
123
+ canvas.width = Math.round(w * dpr); canvas.height = Math.round(h * dpr)
124
+ canvas.style.width = w + 'px'; canvas.style.height = h + 'px'
125
+ }
126
+ const ctx = canvas.getContext('2d')
127
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
128
+ ctx.clearRect(0, 0, w, h)
129
+ return { ctx, w, h }
130
+ }
131
+ function unitPixels() {
132
+ const a = renderer.graphToViewport({ x: 0, y: 0 }), b = renderer.graphToViewport({ x: 100, y: 0 })
133
+ return Math.hypot(b.x - a.x, b.y - a.y) / 100
134
+ }
135
+ let glow = null
136
+ function buildGlow() {
137
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity
138
+ for (const z of geometry) for (const b of z.blobs) { const r = 34 + 11 * Math.sqrt(b.weight); minX = Math.min(minX, b.x - r); maxX = Math.max(maxX, b.x + r); minY = Math.min(minY, b.y - r); maxY = Math.max(maxY, b.y + r) }
139
+ const size = 1400, scale = size / Math.max(maxX - minX, maxY - minY, 1)
140
+ const canvas = document.createElement('canvas')
141
+ canvas.width = Number.isFinite(minX) ? Math.ceil((maxX - minX) * scale) : 0; canvas.height = Number.isFinite(minY) ? Math.ceil((maxY - minY) * scale) : 0
142
+ const ctx = canvas.getContext('2d')
143
+ ctx.globalCompositeOperation = palette.mode === 'dark' ? 'lighter' : 'multiply'
144
+ for (const zone of geometry) {
145
+ const color = zoneColor(zone.id)
146
+ for (const blob of zone.blobs) {
147
+ const x = (blob.x - minX) * scale, y = (blob.y - minY) * scale, r = (34 + 11 * Math.sqrt(blob.weight)) * scale
148
+ const g = ctx.createRadialGradient(x, y, 0, x, y, r)
149
+ const a = (palette.mode === 'dark' ? 0.085 : 0.06) * Math.min(2.2, Math.sqrt(blob.weight))
150
+ g.addColorStop(0, hexToRgba(color, a)); g.addColorStop(1, hexToRgba(color, 0))
151
+ ctx.fillStyle = g; ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill()
152
+ }
153
+ }
154
+ glow = { canvas, minX, minY, maxX, maxY }
155
+ }
156
+ function drawZones() {
157
+ const { ctx } = sizeCanvas(zoneCanvas)
158
+ if (!state.zonesOn) return
159
+ if (!glow) buildGlow()
160
+ // An empty or single-point readable set has no area to glow.
161
+ if (!glow.canvas.width || !glow.canvas.height || !Number.isFinite(glow.minX)) return
162
+ const a = renderer.graphToViewport({ x: glow.minX, y: glow.minY }), b = renderer.graphToViewport({ x: glow.maxX, y: glow.maxY })
163
+ ctx.globalAlpha = (activeSet() || state.lit || state.focusZone) ? 0.45 : 1
164
+ // The viewport flips the graph's y axis; mirror the glow image to match.
165
+ const left = Math.min(a.x, b.x), width = Math.abs(b.x - a.x), height = Math.abs(b.y - a.y)
166
+ if (a.y > b.y) { ctx.save(); ctx.translate(left, a.y); ctx.scale(1, -1); ctx.drawImage(glow.canvas, 0, 0, width, height); ctx.restore() }
167
+ else ctx.drawImage(glow.canvas, left, a.y, width, height)
168
+ ctx.globalAlpha = 1
169
+ }
170
+ let zoneLabelRects = []
171
+ function reservedRects() {
172
+ const host = container.getBoundingClientRect()
173
+ return [...document.querySelectorAll(reserveSelector)].filter(el => !el.hidden).map(el => { const r = el.getBoundingClientRect(); return { left: r.left - host.left, top: r.top - host.top, right: r.right - host.left, bottom: r.bottom - host.top } }).filter(r => r.right > r.left)
174
+ }
175
+ function drawZoneNames(ctx, chrome) {
176
+ const ratio = camera.ratio
177
+ const dim = !!(activeSet() || state.lit)
178
+ // Zone names: strongest when zoomed out, gone when close.
179
+ const alpha = Math.max(0, Math.min(1, (ratio - 0.32) / 0.4)) * (dim ? 0.35 : 1)
180
+ zoneLabelRects = []
181
+ if (alpha <= 0.02 || !state.zonesOn) return
182
+ ctx.textAlign = 'center'
183
+ ctx.textBaseline = 'middle'
184
+ for (const zone of [...geometry].sort((a, b) => b.count - a.count)) {
185
+ const p = renderer.graphToViewport(zone.anchor)
186
+ const size = Math.round(Math.min(19, 11 + Math.sqrt(zone.count) / 3))
187
+ ctx.font = `600 ${size}px ${font}`
188
+ ctx.letterSpacing = '0.14em'
189
+ const text = zone.label.toUpperCase()
190
+ const width = ctx.measureText(text).width
191
+ const vw = container.clientWidth, vh = container.clientHeight
192
+ p.x = Math.max(width / 2 + 10, Math.min(vw - width / 2 - 10, p.x))
193
+ if (p.y < 70 || p.y > vh - 20) continue
194
+ const rect = { left: p.x - width / 2 - 6, right: p.x + width / 2 + 6, top: p.y - size, bottom: p.y + size }
195
+ if (zoneLabelRects.some(r => overlaps(r, rect, 4))) continue
196
+ if (chrome.some(r => overlaps(r, rect, 4))) continue
197
+ zoneLabelRects.push({ ...rect, zone: zone.id })
198
+ ctx.letterSpacing = '0.14em'
199
+ ctx.lineWidth = 5
200
+ ctx.strokeStyle = hexToRgba(palette.labelHalo, 0.75 * alpha)
201
+ ctx.strokeText(text, p.x, p.y)
202
+ ctx.fillStyle = hexToRgba(zoneColor(zone.id), Math.min(1, 0.95 * alpha))
203
+ ctx.fillText(text, p.x, p.y)
204
+ ctx.letterSpacing = '0px'
205
+ }
206
+ }
207
+ const overlaps = (a, b, gap = 0) => a.left < b.right + gap && a.right + gap > b.left && a.top < b.bottom + gap && a.bottom + gap > b.top
208
+
209
+ function drawLabels() {
210
+ const { ctx, w, h } = sizeCanvas(labelCanvas)
211
+ const ratio = camera.ratio
212
+ // Chrome claims its space first: no zone name or label under the host's controls.
213
+ const chrome = reservedRects()
214
+ drawZoneNames(ctx, chrome)
215
+ ctx.letterSpacing = '0px'
216
+ ctx.textAlign = 'left'
217
+ const set = activeSet()
218
+ const focus = state.hovered ?? state.selected
219
+ const candidates = []
220
+ // Importance first; zoom decides how deep into the list we go.
221
+ // Bounded work per frame: the neighbourhood when one is active, otherwise the
222
+ // highest-ranked records the current zoom could show.
223
+ const pool = set ? [...set] : state.lit ? [...state.lit] : ranked.slice(0, Math.min(ranked.length, Math.round(ranked.length * Math.min(1, 1.15 - 0.8 * ratio) + 60))).map(n => n.id)
224
+ if (state.selected && !pool.includes(state.selected)) pool.push(state.selected)
225
+ for (const id of pool) {
226
+ const node = byId.get(id)
227
+ const display = renderer.getNodeDisplayData(id)
228
+ if (!display || display.hidden) continue
229
+ const p = renderer.framedGraphToViewport(display)
230
+ if (p.x < -50 || p.y < -20 || p.x > w + 50 || p.y > h + 20) continue
231
+ let priority = rank.get(id)
232
+ let threshold = labelThreshold(ratio, state.labelBoost)
233
+ if (set) { priority += id === focus ? 10 : 2; threshold = 0 }
234
+ else if (state.lit) { priority += 1.5; threshold = Math.min(threshold, 0.25) }
235
+ if (id === state.selected) { priority += 20; threshold = 0 }
236
+ if (priority < threshold) continue
237
+ candidates.push({ id, node, p, priority, r: renderer.scaleSize(display.size) })
238
+ }
239
+ const style = c => { const emphasis = c.id === focus || c.id === state.selected; return { emphasis, size: emphasis ? 14 : c.priority > 0.75 || set ? 12.5 : 11.5, weight: emphasis ? 650 : c.priority > 0.6 ? 560 : 450 } }
240
+ const prepared = candidates.map(c => { const st = style(c); let text = c.node.title; if (text.length > 42 && !st.emphasis) text = text.slice(0, 40).trimEnd() + '\u2026'; return { id: c.id, x: c.p.x, y: c.p.y, r: c.r, priority: c.priority, text, size: st.size, emphasis: st.emphasis, weight: st.weight } })
241
+ const weightOf = new Map(prepared.map(c => [c.id, c]))
242
+ const measure = (text, size, emphasis) => {
243
+ const c = prepared.find(x => x.text === text && x.size === size) ?? { weight: emphasis ? 650 : 450 }
244
+ ctx.font = `${c.weight} ${size}px ${font}`
245
+ const key = ctx.font + '|' + text
246
+ let width = widthCache.get(key)
247
+ if (width === undefined) { width = ctx.measureText(text).width; widthCache.set(key, width) }
248
+ return width
249
+ }
250
+ const budget = set ? 60 : labelBudget(w, h, ratio, state.labelBoost)
251
+ placedLabels = placeLabels({ candidates: prepared, measure, width: w, height: h, reserved: [...zoneLabelRects, ...chrome], budget })
252
+ ctx.lineJoin = 'round'
253
+ ctx.textBaseline = 'middle'
254
+ for (const label of placedLabels) {
255
+ const c = weightOf.get(label.id)
256
+ ctx.font = `${c.weight} ${label.size}px ${font}`
257
+ ctx.lineWidth = 3
258
+ ctx.strokeStyle = hexToRgba(palette.labelHalo, 0.85)
259
+ ctx.strokeText(label.text, label.x, label.y)
260
+ ctx.fillStyle = c.emphasis ? palette.label : hexToRgba(palette.label, set && !c.emphasis ? 0.92 : Math.min(1, 0.62 + c.priority * 0.5))
261
+ ctx.fillText(label.text, label.x, label.y)
262
+ }
263
+ // A selection ring, drawn here so it stays crisp at any zoom.
264
+ if (state.selected && graph.hasNode(state.selected)) {
265
+ const d = renderer.getNodeDisplayData(state.selected)
266
+ if (d) {
267
+ const p = renderer.framedGraphToViewport(d), r = renderer.scaleSize(d.size) + 5
268
+ ctx.lineWidth = 2; ctx.strokeStyle = palette.selection
269
+ ctx.beginPath(); ctx.arc(p.x, p.y, r, 0, Math.PI * 2); ctx.stroke()
270
+ }
271
+ }
272
+ }
273
+
274
+ let last = performance.now()
275
+ renderer.on('afterRender', () => {
276
+ const now = performance.now()
277
+ frameTimes.push(now - last); last = now
278
+ if (frameTimes.length > 120) frameTimes.shift()
279
+ const t0 = performance.now()
280
+ drawZones()
281
+ const t1 = performance.now()
282
+ drawLabels()
283
+ overlayTimes.push({ zones: t1 - t0, labels: performance.now() - t1 })
284
+ if (overlayTimes.length > 120) overlayTimes.shift()
285
+ })
286
+
287
+ // ── Interaction ──
288
+ let dragged = null, dragMoved = false
289
+ renderer.on('enterNode', ({ node }) => { state.hovered = node; container.style.cursor = 'pointer'; onHover(node); renderer.refresh({ skipIndexation: true }) })
290
+ renderer.on('leaveNode', () => { state.hovered = null; container.style.cursor = ''; onHover(null); renderer.refresh({ skipIndexation: true }) })
291
+ renderer.on('clickNode', ({ node }) => { if (!dragMoved) onSelect(node) })
292
+ renderer.on('doubleClickNode', ({ node, event }) => { event.preventSigmaDefault(); onOpen(node) })
293
+ renderer.on('clickStage', () => { if (!dragMoved) onSelect(null) })
294
+ renderer.on('downNode', ({ node }) => { dragged = node; dragMoved = false; graph.setNodeAttribute(node, 'fixed', true) })
295
+ renderer.getMouseCaptor().on('mousemovebody', event => {
296
+ if (!dragged) return
297
+ dragMoved = true
298
+ const p = renderer.viewportToGraph(event)
299
+ graph.mergeNodeAttributes(dragged, { x: p.x, y: p.y })
300
+ event.preventSigmaDefault(); event.original.preventDefault(); event.original.stopPropagation()
301
+ })
302
+ renderer.getMouseCaptor().on('mouseup', () => {
303
+ if (dragged) { if (!physics) graph.removeNodeAttribute(dragged, 'fixed'); if (dragMoved) refreshGeometry() }
304
+ dragged = null
305
+ setTimeout(() => { dragMoved = false }, 0)
306
+ })
307
+
308
+ // ── Physics (play) ──
309
+ let physics = null, geometryTimer = null
310
+ function refreshGeometry() {
311
+ const moved = { ...model, nodes: model.nodes.map(n => ({ ...n, x: graph.getNodeAttribute(n.id, 'x'), y: graph.getNodeAttribute(n.id, 'y') })) }
312
+ geometry = zoneGeometry(moved)
313
+ glow = null
314
+ }
315
+ function setPhysics(on) {
316
+ if (on && !physics && !reduced() && FA2Layout) {
317
+ graph.forEachEdge((e, a) => graph.setEdgeAttribute(e, 'weight', LAYOUT_WEIGHTS[a.kind] ?? 0.22))
318
+ physics = new FA2Layout(graph, { settings: { linLogMode: true, outboundAttractionDistribution: true, gravity: 0.12, scalingRatio: 14, barnesHutOptimize: graph.order > 600, slowDown: 6 }, getEdgeWeight: 'weight' })
319
+ physics.start()
320
+ geometryTimer = setInterval(refreshGeometry, 400)
321
+ } else if (!on && physics) {
322
+ physics.kill(); physics = null; clearInterval(geometryTimer); refreshGeometry()
323
+ }
324
+ return !!physics
325
+ }
326
+
327
+ // ── Intro: the camera settles onto the whole while zones and labels fade in.
328
+ // Camera-only, so no graph reindexing; skipped entirely under reduced motion.
329
+ function intro() {
330
+ if (reduced()) return Promise.resolve()
331
+ camera.setState({ x: 0.5, y: 0.5, ratio: 1.55, angle: 0 })
332
+ container.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 700, easing: 'ease-out' })
333
+ return camera.animate({ ratio: 1 }, { duration: 1100, easing: 'cubicOut' })
334
+ }
335
+
336
+ const api = {
337
+ renderer, graph, camera,
338
+ intro,
339
+ get hovered() { return state.hovered },
340
+ select(id) { state.selected = id; renderer.refresh({ skipIndexation: true }) },
341
+ setLens(ids) { state.lit = ids ? new Set(ids) : null; renderer.refresh({ skipIndexation: true }) },
342
+ setFocusZone(zone) { state.focusZone = zone; renderer.refresh({ skipIndexation: true }) },
343
+ setOption(key, value) { state[key] = value; renderer.refresh({ skipIndexation: true }) },
344
+ setPhysics,
345
+ get physics() { return !!physics },
346
+ showNode(id, { ratio = 0.22, duration = 600 } = {}) {
347
+ const d = renderer.getNodeDisplayData(id)
348
+ if (!d) return Promise.resolve()
349
+ return camera.animate({ x: d.x, y: d.y, ratio: Math.min(camera.ratio, ratio) }, { duration: reduced() ? 0 : duration })
350
+ },
351
+ dive(id) {
352
+ const d = renderer.getNodeDisplayData(id)
353
+ if (!d) return Promise.resolve(null)
354
+ const before = camera.getState()
355
+ return camera.animate({ x: d.x, y: d.y, ratio: 0.08 }, { duration: reduced() ? 0 : 520 }).then(() => ({ before }))
356
+ },
357
+ restore(cameraState) { return camera.animate(cameraState, { duration: reduced() ? 0 : 480 }) },
358
+ fit() { return camera.animate({ x: 0.5, y: 0.5, ratio: 1, angle: 0 }, { duration: reduced() ? 0 : 450 }) },
359
+ zoom(factor) { return camera.animate({ ratio: Math.max(0.035, Math.min(1.6, camera.ratio / factor)) }, { duration: reduced() ? 0 : 220 }) },
360
+ nodeViewport(id) { const d = renderer.getNodeDisplayData(id); return d ? renderer.framedGraphToViewport(d) : null },
361
+ labels: () => placedLabels.map(l => ({ ...l })),
362
+ zoneLabels: () => zoneLabelRects.map(r => ({ ...r })),
363
+ frameStats() { const s = [...frameTimes].sort((a, b) => a - b); const med = k => { const v = overlayTimes.map(o => o[k]).sort((a, b) => a - b); return v[Math.floor(v.length / 2)] ?? null }; return { samples: s.length, medianMs: s[Math.floor(s.length / 2)] ?? null, p90Ms: s[Math.floor(s.length * 0.9)] ?? null, overlayZonesMs: med('zones'), overlayLabelsMs: med('labels') } },
364
+ setPalette(next) { Object.assign(palette, next); glow = null; graph.updateEachNodeAttributes((id, a) => ({ ...a, color: zoneColor(a.zone) }), { attributes: ['color'] }); renderer.refresh() },
365
+ refresh() { renderer.refresh({ skipIndexation: true }) },
366
+ kill() { setPhysics(false); renderer.kill(); container.replaceChildren() },
367
+ }
368
+ return api
369
+ }
@@ -0,0 +1,23 @@
1
+ // Tiny DOM helpers. Text is always assigned as text; no HTML strings.
2
+ export const $ = id => document.getElementById(id)
3
+ export function h(tag, props = {}, ...children) {
4
+ const node = document.createElement(tag)
5
+ for (const [key, value] of Object.entries(props ?? {})) {
6
+ if (value === undefined || value === null || value === false) continue
7
+ if (key === 'class') node.className = value
8
+ else if (key === 'text') node.textContent = value
9
+ else if (key === 'style') node.setAttribute('style', value)
10
+ else if (key.startsWith('on') && typeof value === 'function') node.addEventListener(key.slice(2).toLowerCase(), value)
11
+ else if (key === 'dataset') Object.assign(node.dataset, value)
12
+ else if (key === 'checked' || key === 'disabled' || key === 'hidden' || key === 'open' || key === 'selected') node[key] = !!value
13
+ else if (value === true) node.setAttribute(key, '')
14
+ else node.setAttribute(key, String(value))
15
+ }
16
+ for (const child of children.flat(Infinity)) {
17
+ if (child === null || child === undefined || child === false) continue
18
+ node.append(child instanceof Node ? child : document.createTextNode(String(child)))
19
+ }
20
+ if (tag === 'textarea' && children.length) node.value = children.flat().filter(c => typeof c === 'string').join('')
21
+ return node
22
+ }
23
+ export const clear = node => { while (node.firstChild) node.firstChild.remove() }
@@ -0,0 +1,6 @@
1
+ // Browser rooms for the graph workspace. Renderer packages are injected by the host.
2
+ export { mountGraphWorkspace, applyTheme } from './workspace.mjs'
3
+ export { createConstellation } from './constellation.mjs'
4
+ export { createWorkbench } from './workbench.mjs'
5
+ export { renderReview } from './review.mjs'
6
+ export { GRAPH_WORKSPACE_CSS, injectGraphWorkspaceStyles } from './styles.mjs'
@@ -0,0 +1,55 @@
1
+ // The review: before and proposed, evidence state, who decides. Nothing else.
2
+ // Local and simulated: it shows the intent a host would receive and sends nothing.
3
+ import { h, clear } from './dom.mjs'
4
+ import { readiness } from '../authoring.mjs'
5
+
6
+ const STATUS = {
7
+ 'nothing-drafted': ['Nothing drafted yet', 'Shape a concept or explain a relation first.'],
8
+ 'needs-attention': ['Needs attention', 'Some steps are incomplete; the step rail shows which.'],
9
+ 'ready-for-human-review': ['Ready for a person to review', 'Complete and grounded in records you can read. Not a judgment that it is true; a person decides that.'],
10
+ }
11
+
12
+ export function renderReview(root, { model, store, onBack, onOpen, backLabel = '← Back to the workbench' }) {
13
+ const plan = model.plan
14
+ const visible = new Set(model.nodes.map(n => n.id))
15
+ const byId = new Map(model.nodes.map(n => [n.id, n]))
16
+ const r = readiness(plan, store, visible)
17
+ const [label, explain] = STATUS[r.status]
18
+ clear(root)
19
+ root.hidden = false
20
+ const panel = h('section', { class: 'review', 'aria-labelledby': 'review-title' })
21
+ panel.append(h('header', { class: 'review-head' }, h('button', { class: 'back', type: 'button', id: 'review-back', onclick: onBack }, backLabel),
22
+ h('h2', { id: 'review-title', tabindex: '-1', text: 'Your proposed change' }),
23
+ h('p', { class: 'review-status', id: 'review-status', dataset: { status: r.status } }, h('b', { text: label }), ' ', explain)))
24
+ for (const item of r.items) {
25
+ const card = h('article', { class: 'change' })
26
+ if (item.kind === 'concept') {
27
+ const c = plan.concepts.find(x => x.id === item.id), d = store.conceptDraft(item.id)
28
+ card.append(h('h3', {}, h('button', { class: 'link', type: 'button', onclick: () => onOpen({ kind: 'concept', id: c.id }) }, c.title)))
29
+ card.append(pair('Meaning', c.definition, d.definition), pair('Same one?', c.identityRule, d.identityRule))
30
+ for (const [k, v] of [['Example', d.example], ['Must stay separate', d.nearMiss], ['Would prove it wrong', d.disconfirming]]) if (v) card.append(h('p', { class: 'line' }, h('span', { class: 'label', text: k + ' ' }), v))
31
+ card.append(h('p', { class: 'label', text: `Evidence · ${d.evidence.length}` }), h('ul', { class: 'cited' }, d.evidence.map(b => h('li', { text: visible.has(b.id) ? byId.get(b.id).title : 'A record outside what you can read' }))))
32
+ } else {
33
+ const rel = plan.relations.find(x => x.id === item.id), d = store.relationDraft(item.id)
34
+ const t = id => plan.concepts.find(c => c.id === id)?.title ?? id
35
+ card.append(h('h3', { text: `${t(rel.from)} → ${t(rel.to)}` }), pair('Meaning', rel.meaning, d.meaning))
36
+ for (const [k, v] of [['Forward', d.forward], ['Reverse', d.reverse], ['Would prove it wrong', d.disconfirming]]) if (v) card.append(h('p', { class: 'line' }, h('span', { class: 'label', text: k + ' ' }), v))
37
+ card.append(h('p', { class: 'label', text: `Evidence · ${d.evidence.length}` }), h('ul', { class: 'cited' }, d.evidence.map(b => h('li', { text: visible.has(b.id) ? byId.get(b.id).title : 'A record outside what you can read' }))))
38
+ card.append(h('p', { class: 'step-help', text: 'One directed relation. The reverse reading is explanation, not a second relation.' }))
39
+ }
40
+ if (item.guide.errors) card.append(h('p', { class: 'guide-error', text: `${item.guide.errors} step(s) still need attention.` }))
41
+ if (item.guide.warnings) card.append(h('p', { class: 'guide-warn', text: `${item.guide.warnings} suggestion(s) remain.` }))
42
+ panel.append(card)
43
+ }
44
+ panel.append(h('div', { class: 'decide' },
45
+ h('p', {}, h('b', { text: 'A person decides. ' }), `Nothing has been saved to the source, applied to the plan or sent anywhere. Drafts stay where this workspace keeps them (${({ tab: 'this tab', browser: 'this browser', host: 'the workspace host' })[store.persistence] ?? 'this tab'}).`),
46
+ h('details', {}, h('summary', { text: 'The intent a host would receive (not sent)' }), h('pre', { text: JSON.stringify({ type: 'proposeModelChange', draftRef: `local-browser-draft:${model.systemId}`, items: r.items.map(i => `${i.kind}:${i.id}`) }, null, 2) }))))
47
+ root.replaceChildren(panel)
48
+ root.querySelector('#review-title').focus()
49
+ return r
50
+ }
51
+
52
+ function pair(label, before, after) {
53
+ if ((before ?? '') === (after ?? '')) return h('div', { class: 'pair same' }, h('p', { class: 'label', text: label }), h('p', { text: before || '—' }), h('small', { text: 'Unchanged' }))
54
+ return h('div', { class: 'pair' }, h('p', { class: 'label', text: label }), h('div', { class: 'pair-grid' }, h('div', {}, h('small', { text: 'Now' }), h('p', { text: before || '—' })), h('div', {}, h('small', { text: 'Proposed' }), h('blockquote', { text: after || '—' }))))
55
+ }
@@ -0,0 +1,11 @@
1
+ // Scoped styles for the default graph workspace shell (CSS nesting under .atelier-gw).
2
+ // Colours, type and motion come from the theme's custom properties.
3
+ export const GRAPH_WORKSPACE_CSS = ".atelier-gw{container:atelier-gw / inline-size;position:relative;background:var(--canvas);color:var(--ink);font-family:var(--font-sans);font-size:14px;line-height:1.45;-webkit-font-smoothing:antialiased;overflow:hidden;\n/* V6: minimal chrome around a full-bleed constellation; quiet, focused rooms. */\n*{box-sizing:border-box}\n[hidden]{display:none!important}\n\nbutton,input,select,textarea{font:inherit;color:inherit}\nbutton{cursor:pointer;background:none;border:0}\nbutton:disabled{opacity:.45;cursor:default}\n:focus-visible{outline:2px solid var(--focus);outline-offset:2px}\nh2,h3,p{margin:0}\n.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}\n.skip{position:absolute;z-index:100;left:12px;top:10px;transform:translateY(-200%);background:var(--accent);color:var(--accent-ink);padding:8px 12px;border-radius:var(--radius)}\n.skip:focus{transform:none}\n.eyebrow{display:flex;align-items:center;gap:7px;font-size:11.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);font-weight:600;margin-bottom:6px}\n.label{font-size:11.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-3);font-weight:600;margin:14px 0 6px}\n.dot{display:inline-block;width:9px;height:9px;border-radius:50%;flex:none}\n.dot-concept{display:inline-block;width:9px;height:9px;transform:rotate(45deg);background:var(--accent);flex:none;margin-right:2px}\n.primary{background:var(--accent);color:var(--accent-ink);font-weight:600;padding:9px 16px;border-radius:999px;min-height:38px}\n.secondary{border:1px solid var(--line-strong);padding:7px 14px;border-radius:999px;min-height:36px}\n.secondary.small{min-height:30px;padding:3px 11px;font-size:12.5px}\n.quiet{color:var(--ink-2);padding:8px 12px;border-radius:999px;min-height:36px}\n.quiet:hover,.secondary:hover{background:var(--surface-2)}\n.link{color:var(--accent);text-decoration:underline;text-underline-offset:3px;padding:2px 0}\n\n.stage{position:relative;width:100%;height:100%;overflow:hidden}\n.constellation{position:absolute;inset:0}\n.constellation>*{position:absolute;inset:0}\n.layer-zones{z-index:0}\n.layer-sigma{z-index:1}\n.layer-labels{z-index:2;pointer-events:none}\n&.text-mode .constellation,&.text-mode .zoom{visibility:hidden}\n\n/* chrome */\n.top{position:absolute;z-index:10;top:14px;left:14px;right:14px;display:grid;grid-template-columns:1fr minmax(240px,560px) 1fr;gap:10px;align-items:start;pointer-events:none}\n.top>*{pointer-events:auto}\n.top-right{display:flex;gap:8px;justify-self:end}\n.pill{display:inline-flex;align-items:center;gap:8px;background:var(--surface-glass);border:1px solid var(--line);border-radius:999px;padding:8px 14px;min-height:38px;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);font-weight:500}\n.pill.icon{width:40px;justify-content:center;padding:0;font-size:16px}\n.pill[aria-pressed=true]{background:var(--accent);color:var(--accent-ink);border-color:transparent}\n.emblem{color:var(--accent);font-size:16px}\n.caret{color:var(--ink-3);font-size:11px}\n.menu-wrap{position:relative}\n.menu{position:absolute;top:46px;left:0;z-index:30;min-width:300px;max-width:360px;background:var(--surface);border:1px solid var(--line);border-radius:var(--radius-lg);box-shadow:var(--shadow);padding:6px}\n.top-right .menu{left:auto;right:0}\n.menu-item{display:flex;flex-direction:column;align-items:flex-start;width:100%;text-align:left;padding:9px 12px;border-radius:var(--radius)}\n.menu-item small{color:var(--ink-3);font-size:12px}\n.menu-item:hover,.menu-item[aria-checked=true]{background:var(--surface-2)}\n.menu-item[aria-checked=true] span::after{content:' ✓';color:var(--accent)}\n.menu-label{font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);padding:10px 12px 4px}\n.menu-note{font-size:12px;color:var(--ink-3);padding:4px 12px}\n.display .row{display:flex;align-items:center;gap:10px;padding:8px 12px;font-size:13.5px}\n.display .row.col{flex-direction:column;align-items:stretch;gap:4px}\n.display input[type=checkbox]{width:16px;height:16px;accent-color:var(--accent)}\n.display input[type=range]{accent-color:var(--accent)}\n.command-wrap{position:relative;justify-self:center;width:100%}\n.command-wrap input{width:100%;background:var(--surface-glass);border:1px solid var(--line);border-radius:999px;padding:9px 52px 9px 18px;min-height:40px;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px)}\n.command-wrap input::placeholder{color:var(--ink-3)}\n.command-wrap kbd{position:absolute;right:14px;top:11px;font-size:11px;color:var(--ink-3);border:1px solid var(--line);border-radius:6px;padding:0 6px;font-family:inherit}\n.command-results{list-style:none;margin:6px 0 0;padding:6px;position:absolute;left:0;right:0;background:var(--surface);border:1px solid var(--line);border-radius:var(--radius-lg);box-shadow:var(--shadow);max-height:60vh;overflow:auto}\n.cmd{display:flex;flex-direction:column;align-items:flex-start;width:100%;text-align:left;padding:8px 12px;border-radius:var(--radius)}\n.cmd:hover,.cmd:focus-visible,[aria-selected=true]>.cmd{background:var(--surface-2)}\n.cmd-hint{font-size:12px;color:var(--ink-3)}\n.corner{position:absolute;z-index:5;left:18px;bottom:16px;display:flex;flex-direction:column;gap:2px;font-size:12px;color:var(--ink-3);max-width:min(520px,60vw);pointer-events:none}\n.corner strong{color:var(--ink-2);font-weight:600;font-size:13px}\n.provenance{color:var(--warn)}\n&[data-private=false] .provenance{color:var(--ink-3)}\n.zoom{position:absolute;z-index:5;right:16px;bottom:16px;display:flex;flex-direction:column;background:var(--surface-glass);border:1px solid var(--line);border-radius:12px;overflow:hidden;backdrop-filter:blur(14px)}\n.zoom button{width:38px;height:36px;font-size:17px;color:var(--ink-2)}\n.zoom button+button{border-top:1px solid var(--line)}\n.zoom button:hover{background:var(--surface-2)}\n.toast{position:absolute;z-index:40;left:50%;top:70px;transform:translateX(-50%);background:var(--surface);border:1px solid var(--line);padding:8px 14px;border-radius:999px;box-shadow:var(--shadow);font-size:13px}\n\n/* selection card */\n.card{position:absolute;z-index:8;left:50%;bottom:18px;transform:translateX(-50%);width:min(560px,calc(100vw - 140px));background:var(--surface-glass);border:1px solid var(--line);border-radius:var(--radius-lg);box-shadow:var(--shadow);padding:16px 18px 14px;backdrop-filter:blur(18px);-webkit-backdrop-filter:blur(18px);animation:rise var(--motion) var(--ease)}\n@keyframes rise{from{opacity:0;transform:translate(-50%,10px)}to{opacity:1;transform:translate(-50%,0)}}\n.card h2{font-family:var(--font-display);font-size:19px;font-weight:600;letter-spacing:-.01em;padding-right:28px}\n.card-summary{color:var(--ink-2);margin-top:4px}\n.card-meta{color:var(--ink-3);font-size:12.5px;margin-top:6px}\n.card-actions{display:flex;gap:8px;margin-top:12px;align-items:center}\n.card-close{position:absolute;right:10px;top:8px;width:32px;height:32px;border-radius:50%;color:var(--ink-3);font-size:18px}\n.card-close:hover{background:var(--surface-2)}\n.card-neighbours{margin-top:10px;font-size:13px}\n.card-neighbours summary{cursor:pointer;color:var(--ink-2)}\n.card-neighbours ul{list-style:none;margin:6px 0 0;padding:0;max-height:150px;overflow:auto;columns:2;column-gap:12px}\n.link-row{display:flex;gap:8px;text-align:left;width:100%;padding:4px 6px;border-radius:8px;color:var(--ink-2)}\n.link-row:hover{background:var(--surface-2);color:var(--ink)}\n.dir{color:var(--accent);width:14px;flex:none}\n.question-card{position:absolute;z-index:8;top:66px;left:14px;width:min(360px,calc(100vw - 28px));background:var(--surface-glass);border:1px solid var(--line);border-radius:var(--radius-lg);box-shadow:var(--shadow);padding:14px 16px;backdrop-filter:blur(18px)}\n.question-card select{width:100%;background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:8px 10px;margin-bottom:8px}\n.q-lit{font-weight:600}\n.q-note{font-size:12.5px;color:var(--ink-3);margin:6px 0 10px}\n&.diving .top,&.diving .corner,&.diving .zoom,&.diving .card,&.diving .question-card{opacity:0;transition:opacity 200ms}\n\n/* text view */\n.text-view{position:absolute;inset:0;z-index:3;overflow:auto;padding:76px max(20px,calc(50vw - 520px)) 40px;background:var(--canvas);display:none}\n&.text-mode .text-view{display:block}\n.tv-title{font-family:var(--font-display);font-size:22px;margin-bottom:10px}\n.text-view input[type=search]{width:100%;max-width:420px;background:var(--surface);border:1px solid var(--line);border-radius:999px;padding:8px 14px;margin-bottom:10px}\n.text-view h3{font-size:13px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-3);margin:14px 0 6px}\n.tv-zone{margin:6px 0;border-top:1px solid var(--line);padding-top:6px}\n.tv-zone summary{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:600;padding:6px 0}\n.tv-list{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:2px 12px}\n.tv-item{display:flex;flex-wrap:wrap;align-items:center;gap:2px 8px;width:100%;text-align:left;padding:6px 8px;border-radius:8px}\n.tv-item small{flex-basis:100%;color:var(--ink-3);font-size:12px}\n.tv-item:hover,.tv-item[aria-pressed=true]{background:var(--surface-2)}\n\n/* workbench */\n.workbench{position:absolute;inset:0;z-index:20;display:grid;grid-template-rows:auto minmax(0,1fr);background:radial-gradient(ellipse at 35% 50%,var(--surface-2),var(--canvas) 70%);animation:fade var(--motion) var(--ease)}\n@keyframes fade{from{opacity:0}to{opacity:1}}\n.wb-head{display:flex;align-items:center;gap:14px;padding:12px 18px;border-bottom:1px solid var(--line);min-height:56px}\n.back{color:var(--ink-2);padding:6px 10px;border-radius:999px}\n.back:hover{background:var(--surface-2)}\n.trail{display:flex;align-items:center;gap:6px;flex:1;min-width:0;overflow:hidden;font-size:13px;color:var(--ink-3)}\n.crumb{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:220px;color:var(--ink-2)}\nbutton.crumb{text-decoration:underline;text-underline-offset:3px}\n.crumb.current{color:var(--ink);font-weight:600}\n.save-status{font-size:12.5px;color:var(--ink-3);display:flex;align-items:center;gap:6px;white-space:nowrap}\n.save-status::before{content:'';width:7px;height:7px;border-radius:50%;background:var(--ink-3)}\n.save-status[data-state=saved]::before,.save-status[data-state=restored]::before{background:var(--ok)}\n.save-status[data-state=saving]::before{background:var(--warn)}\n.wb-body{display:grid;grid-template-columns:minmax(0,1fr) 440px;grid-template-rows:minmax(0,1fr);min-height:0}\n.wb-graph{position:relative;overflow:hidden}\n.wb-edges{position:absolute;inset:0;width:100%;height:100%;overflow:visible}\n.wb-cards{position:absolute;inset:0}\n.wb-edge line{stroke:var(--line-strong);stroke-width:1.4}\n.wb-edge path{fill:var(--line-strong)}\n.wb-edge.is-declared line{stroke:var(--ink-3);stroke-width:1.6}\n.wb-edge.is-declared path{fill:var(--ink-3)}\n.wb-edge.is-link line{stroke-dasharray:2 5}\n.wb-edge.is-challenge line{stroke:var(--danger);stroke-dasharray:7 4}\n.wb-edge.is-challenge path{fill:var(--danger)}\n.wb-edge.is-declares line{stroke:var(--accent);stroke-dasharray:4 4;opacity:.8}\n.wb-edge.is-declares path{fill:var(--accent)}\n.wb-card{position:absolute;left:0;top:0;width:224px;display:flex;flex-direction:column;align-items:flex-start;gap:3px;text-align:left;background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:11px 13px;box-shadow:0 4px 18px rgba(0,0,0,.18);transition:border-color var(--motion)}\n.wb-card:hover{border-color:var(--line-strong)}\n.wb-center{width:280px;border:1.5px solid var(--accent);padding:14px 16px;cursor:default}\n.wb-kind{display:flex;align-items:center;gap:7px;font-size:11.5px;color:var(--ink-3);letter-spacing:.04em}\n.wb-title{font-weight:600;font-size:14.5px;line-height:1.3}\n.wb-center .wb-title{font-family:var(--font-display);font-size:18px}\n.wb-summary{font-size:12.5px;color:var(--ink-2);display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}\n.wb-meta{font-size:11.5px;color:var(--ink-3);display:flex;gap:6px;align-items:center;flex-wrap:wrap}\n.tag{font-weight:600;font-size:10.5px;padding:0 6px;border-radius:999px;border:1px solid var(--line-strong)}\n.tag-cited{border-color:var(--ok);color:var(--ok)}\n.tag-proposed{border-color:var(--warn);color:var(--warn);border-style:dashed}\n.wb-pill{position:absolute;left:0;top:0;z-index:2;display:inline-flex;align-items:center;gap:4px;font-size:11.5px;padding:2px 9px;border-radius:999px;background:var(--canvas);border:1px solid var(--line);color:var(--ink-2);white-space:nowrap}\n.wb-pill-arrow{display:inline-block;color:var(--ink)}\n.wb-more{position:absolute;left:16px;bottom:12px;font-size:12px;color:var(--ink-3)}\n.wb-graph.is-list{overflow:auto}\n.wb-graph.is-list .wb-cards{position:relative;min-height:100%}\n.wb-sheet{background:var(--surface);border-left:1px solid var(--line);overflow:auto;padding:22px 26px 30px}\n.wb-sheet h2{font-family:var(--font-display);font-size:24px;font-weight:600;letter-spacing:-.015em;line-height:1.2;margin-bottom:4px}\n.wb-sheet h2:focus{outline:none}\n.sheet-count{font-size:12.5px;color:var(--ink-3)}\n.step-rail{list-style:none;margin:16px 0 18px;padding:0;display:flex;flex-wrap:wrap;gap:6px}\n.step{display:inline-flex;align-items:center;gap:6px;padding:5px 11px 5px 6px;border-radius:999px;border:1px solid var(--line);font-size:12.5px;color:var(--ink-2)}\n.step[aria-current=step]{border-color:var(--accent);color:var(--ink);background:var(--surface-2)}\n.step-n{width:20px;height:20px;border-radius:50%;display:grid;place-items:center;font-size:11px;background:var(--surface-2);color:var(--ink-2)}\n.step-answered .step-n{background:var(--ok);color:var(--canvas)}\n.step-deferred .step-n{background:transparent;border:1px dashed var(--ink-3)}\n.step-attention .step-n{background:var(--warn);color:var(--canvas)}\n.step-body h3{font-family:var(--font-display);font-size:20px;font-weight:600;line-height:1.3}\n.step-help{color:var(--ink-3);font-size:13px;margin:4px 0 10px}\n.step-body textarea{width:100%;min-height:150px;resize:vertical;background:var(--canvas);border:1px solid var(--line-strong);border-radius:12px;padding:12px 14px;font-size:16px;line-height:1.55}\n.plan-wording{margin-top:8px;font-size:13px;color:var(--ink-2)}\n.plan-wording summary{cursor:pointer;color:var(--ink-3)}\n.guide{list-style:none;margin:8px 0 0;padding:0;font-size:13px}\n.guide li{padding-left:16px;position:relative;margin-top:3px}\n.guide li::before{position:absolute;left:0;font-weight:700}\n.guide-error{color:var(--danger)}.guide-error::before{content:'!'}\n.guide-warn{color:var(--warn)}.guide-warn::before{content:'•'}\n.guide-info{color:var(--ink-3)}.guide-info::before{content:'i'}\n.defer{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--ink-2);margin-top:12px}\n.defer input{width:16px;height:16px;accent-color:var(--accent)}\n.sheet-actions{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-top:20px;padding-top:14px;border-top:1px solid var(--line)}\n.sheet-actions.left{justify-content:flex-start;border-top:0;padding-top:0}\n.sheet-actions select{background:var(--canvas);border:1px solid var(--line);border-radius:999px;padding:7px 10px;max-width:180px}\n.sheet-note{font-size:12px;color:var(--ink-3);margin-top:12px}\n.lede{color:var(--ink-2);font-size:15px;margin:6px 0 12px}\n.facts{display:grid;grid-template-columns:96px 1fr;gap:6px 12px;font-size:13px;margin:0 0 16px}\n.facts dt{color:var(--ink-3)}.facts dd{margin:0;word-break:break-word}\n.sheet-h{font-size:13px;margin:12px 0 6px}\n.link-list,.cited,.suggest{list-style:none;margin:0;padding:0}\n.cited li,.suggest li{display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid var(--line);font-size:13.5px}\n.suggest-title,.cited li span{flex:1;min-width:0}\n.find{width:100%;background:var(--canvas);border:1px solid var(--line);border-radius:999px;padding:7px 12px;margin-top:12px}\n\n/* review */\n.review-room{position:absolute;inset:0;z-index:30;background:var(--canvas);overflow:auto;animation:fade var(--motion) var(--ease)}\n.review{max-width:860px;margin:0 auto;padding:28px 24px 60px}\n.review-head h2{font-family:var(--font-display);font-size:30px;font-weight:600;letter-spacing:-.02em;margin:12px 0 6px}\n.review-head h2:focus{outline:none}\n.review-status{color:var(--ink-2)}\n.review-status b{color:var(--ink)}\n.review-status[data-status=ready-for-human-review] b{color:var(--ok)}\n.review-status[data-status=needs-attention] b{color:var(--warn)}\n.change{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius-lg);padding:18px 20px;margin-top:16px}\n.change h3{font-size:18px;margin-bottom:8px}\n.pair{margin-top:10px}.pair-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}\n.pair small{color:var(--ink-3)}\n.pair blockquote{margin:2px 0 0;padding-left:12px;border-left:3px solid var(--accent)}\n.pair.same small{display:block}\n.line{margin-top:6px}\n.decide{margin-top:22px;padding:16px 20px;border-radius:var(--radius-lg);border:1px solid var(--line-strong)}\n.decide pre{font-size:12px;white-space:pre-wrap;color:var(--ink-2)}\n.decide summary{cursor:pointer;color:var(--ink-3);margin-top:8px}\n\n@container atelier-gw (max-width:860px){\n .wb-body{grid-template-columns:1fr;grid-template-rows:minmax(300px,48dvh) auto;overflow:auto}\n .wb-sheet{border-left:0;border-top:1px solid var(--line)}\n .workbench{overflow:auto}\n}\n@container atelier-gw (max-width:720px){\n .top{grid-template-columns:auto 1fr;top:10px;left:10px;right:10px}\n .command-wrap{grid-column:1/-1;grid-row:2}\n .top-right{grid-column:2;grid-row:1}\n .menu{min-width:min(320px,calc(100vw - 20px))}\n .card{width:calc(100vw - 20px);bottom:10px;padding:14px}\n .card-neighbours ul{columns:1}\n .corner{bottom:auto;top:112px;left:12px;max-width:calc(100vw - 24px)}\n .corner span:not(.provenance){display:none}\n .zoom{bottom:auto;top:104px;right:10px}\n .question-card{top:150px}\n .text-view{padding:120px 14px 30px}\n .wb-head{flex-wrap:wrap;gap:8px}\n .save-status{flex-basis:100%}\n .wb-sheet{padding:18px 16px 30px}\n .pair-grid{grid-template-columns:1fr}\n}\n@media (prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}}\n.step-plan .step-n{background:transparent;border:1px solid var(--accent);color:var(--accent)}\n.top>.menu-wrap{display:flex;gap:8px;align-items:flex-start}\n.primary-action{border-color:var(--accent)}\n\n}\n"
4
+
5
+ export function injectGraphWorkspaceStyles(doc = document) {
6
+ if (doc.getElementById('atelier-gw-styles')) return
7
+ const style = doc.createElement('style')
8
+ style.id = 'atelier-gw-styles'
9
+ style.textContent = GRAPH_WORKSPACE_CSS
10
+ doc.head.append(style)
11
+ }