@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,351 @@
1
+ // The workbench: a sparse graph of rich cards beside a focused authoring sheet.
2
+ // One subject at a time (record, concept or plan relation). The sheet follows the
3
+ // focused authoring register: quiet header, step rail with counts, one step in view,
4
+ // one primary action, deferral, and an honest "saved in this browser" status.
5
+ import { h, clear } from './dom.mjs'
6
+ import { conceptSteps, relationSteps, guideConcept, guideRelation, stepState } from '../authoring.mjs'
7
+
8
+ const VERB = { depends_on: 'depends on', supports: 'supports', contradicts: 'challenges', evidences: 'is evidence about', related: 'links to', supersedes: 'replaces', belongs_to: 'belongs to', implements: 'puts into practice', declares: 'declares' }
9
+ const verb = p => Object.hasOwn(VERB, p) ? VERB[p] : p.replaceAll('_', ' ')
10
+ const reduced = () => matchMedia('(prefers-reduced-motion: reduce)').matches
11
+
12
+ export function createWorkbench({ root, model, palette, store, vocab, onBack, onReview, onShowOnMap }) {
13
+ const byId = new Map(model.nodes.map(n => [n.id, n]))
14
+ const zoneIndex = new Map(model.zones.map(z => [z.id, z.index]))
15
+ const zoneColor = id => palette.zones[(zoneIndex.get(id) ?? 0) % palette.zones.length]
16
+ const plan = model.plan ?? { concepts: [], relations: [], questions: [] }
17
+ const conceptById = new Map(plan.concepts.map(c => [c.id, c]))
18
+ const visible = new Set(model.nodes.map(n => n.id))
19
+ let subject = null
20
+ let trail = []
21
+ let step = 0
22
+ let lastLayout = null
23
+ let unsubscribe = null
24
+ const adjacency = new Map()
25
+ for (const e of model.edges) {
26
+ for (const [k, other, dir] of [[e.source, e.target, 'out'], [e.target, e.source, 'in']]) {
27
+ if (!adjacency.has(k)) adjacency.set(k, [])
28
+ adjacency.get(k).push({ edge: e, other, dir })
29
+ }
30
+ }
31
+
32
+ const head = h('header', { class: 'wb-head' })
33
+ const graphHost = h('div', { class: 'wb-graph', role: 'group', 'aria-label': 'Subject and its closest connections' })
34
+ const edgeSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
35
+ edgeSvg.setAttribute('class', 'wb-edges'); edgeSvg.setAttribute('aria-hidden', 'true')
36
+ const cardLayer = h('div', { class: 'wb-cards' })
37
+ graphHost.append(edgeSvg, cardLayer)
38
+ const sheet = h('aside', { class: 'wb-sheet', 'aria-labelledby': 'wb-title' })
39
+ root.replaceChildren(head, h('div', { class: 'wb-body' }, graphHost, sheet))
40
+ new ResizeObserver(() => { if (subject && !root.hidden) layoutGraph(false) }).observe(graphHost)
41
+
42
+ // ── Subject neighbourhoods ──
43
+ function neighboursOf(s) {
44
+ if (s.kind === 'record') {
45
+ const list = (adjacency.get(s.id) ?? []).map(a => ({ kind: 'record', id: a.other, predicate: a.edge.type, dir: a.dir, structural: (a.edge.kind === 'relation') }))
46
+ const unique = []
47
+ const seen = new Set()
48
+ list.sort((a, b) => Number(b.structural) - Number(a.structural) || (byId.get(b.id).degree - byId.get(a.id).degree))
49
+ for (const n of list) if (!seen.has(n.id)) { seen.add(n.id); unique.push(n) }
50
+ const node = byId.get(s.id)
51
+ const concepts = node.concepts.filter(id => conceptById.has(id)).map(id => ({ kind: 'concept', id, predicate: 'declares', dir: 'out', structural: true }))
52
+ return { shown: [...concepts, ...unique].slice(0, 10), total: unique.length + concepts.length }
53
+ }
54
+ if (s.kind === 'concept') {
55
+ const rel = plan.relations.filter(r => r.from === s.id || r.to === s.id).map(r => ({ kind: 'concept', id: r.from === s.id ? r.to : r.from, predicate: r.predicate, dir: r.from === s.id ? 'out' : 'in', structural: true, relation: r.id }))
56
+ const declared = (conceptById.get(s.id)?.declaredBy ?? []).filter(id => byId.has(id)).map(id => ({ kind: 'record', id, predicate: 'declares', dir: 'in', structural: true }))
57
+ return { shown: [...rel, ...declared].slice(0, 10), total: rel.length + declared.length }
58
+ }
59
+ const r = plan.relations.find(x => x.id === s.id)
60
+ return { shown: r ? [{ kind: 'concept', id: r.to, predicate: r.predicate, dir: 'out', structural: true, relation: r.id }] : [], total: 1, relation: r }
61
+ }
62
+
63
+ function card(item, role) {
64
+ const isConcept = item.kind === 'concept'
65
+ const n = isConcept ? null : byId.get(item.id)
66
+ const c = isConcept ? conceptById.get(item.id) : null
67
+ const cited = !isConcept && Object.values(store.data.concepts).some(d => d.evidence.some(b => b.id === item.id))
68
+ const draft = isConcept ? store.conceptDraft(item.id) : undefined
69
+ const proposed = isConcept && draft && (draft.definition !== c.definition || draft.identityRule !== c.identityRule)
70
+ const el = h('button', { class: `wb-card wb-${role} ${isConcept ? 'is-concept' : 'is-record'}`, type: 'button', dataset: { kind: item.kind, id: item.id },
71
+ 'aria-label': isConcept ? `${c.title}, concept` : `${n.title}, ${vocab.record} in ${n.zoneLabel}` },
72
+ h('span', { class: 'wb-kind' }, h('i', { class: isConcept ? 'dot-concept' : 'dot', style: isConcept ? '' : `background:${zoneColor(n.zone)}` }), isConcept ? 'Concept' : n.zoneLabel),
73
+ h('span', { class: 'wb-title', text: isConcept ? c.title : n.title }),
74
+ h('span', { class: 'wb-summary', text: isConcept ? (draft?.definition || c.definition || '') : n.summary }),
75
+ h('span', { class: 'wb-meta' }, isConcept ? `${c.declaredBy.length} declaring ${c.declaredBy.length === 1 ? vocab.record : vocab.records}` : `${n.type}${n.status ? ' · ' + n.status : ''} · ${n.degree} links`,
76
+ cited ? h('b', { class: 'tag tag-cited', text: 'Cited' }) : null, proposed ? h('b', { class: 'tag tag-proposed', text: 'Proposed' }) : null))
77
+ if (role === 'neighbour') el.addEventListener('click', () => walk({ kind: item.kind, id: item.id }, el))
78
+ return el
79
+ }
80
+
81
+ function layoutGraph(animate) {
82
+ const w = graphHost.clientWidth, hgt = graphHost.clientHeight
83
+ const { shown, total, relation } = neighboursOf(subject)
84
+ // A relayout (resize, citation) rebuilds the cards; keep keyboard focus where it was.
85
+ const active = cardLayer.contains(document.activeElement) ? document.activeElement : null
86
+ const refocus = active && { kind: active.dataset.kind, id: active.dataset.id, role: active.classList.contains('wb-center') ? 'center' : 'neighbour' }
87
+ clear(cardLayer)
88
+ while (edgeSvg.firstChild) edgeSvg.firstChild.remove()
89
+ const center = card({ kind: subject.kind === 'relation' ? 'concept' : subject.kind, id: subject.kind === 'relation' ? relation.from : subject.id }, 'center')
90
+ cardLayer.append(center)
91
+ const cards = shown.map(item => ({ item, el: card(item, 'neighbour') }))
92
+ for (const c of cards) cardLayer.append(c.el)
93
+ const narrow = w < 760
94
+ graphHost.classList.toggle('is-list', narrow)
95
+ const positions = []
96
+ const cw = center.offsetWidth, ch = center.offsetHeight
97
+ const cx = narrow ? w / 2 : w / 2, cy = narrow ? ch / 2 + 12 : hgt / 2
98
+ place(center, cx - cw / 2, cy - ch / 2)
99
+ const rects = new Map([['center', { x: cx - cw / 2, y: cy - ch / 2, w: cw, h: ch }]])
100
+ if (narrow) {
101
+ let y = cy + ch / 2 + 22
102
+ for (const c of cards) { const cw2 = Math.min(w - 24, 340); c.el.style.width = cw2 + 'px'; place(c.el, (w - cw2) / 2, y); rects.set(c.item.id, { x: (w - cw2) / 2, y, w: cw2, h: c.el.offsetHeight }); y += c.el.offsetHeight + 10 }
103
+ graphHost.style.minHeight = y + 'px'
104
+ } else {
105
+ graphHost.style.minHeight = ''
106
+ // Incoming connections on the left, outgoing on the right: the page reads as a flow.
107
+ const columns = { in: cards.filter(c => c.item.dir === 'in'), out: cards.filter(c => c.item.dir !== 'in') }
108
+ const gap = 12, room = hgt - 64
109
+ for (const [side, list] of Object.entries(columns)) {
110
+ let used = 0
111
+ const fits = []
112
+ for (const c of list) { const eh = c.el.offsetHeight; if (used + eh > room) { c.el.remove(); continue } fits.push(c); used += eh + gap }
113
+ columns[side] = fits
114
+ let y = Math.max(18, cy - (used - gap) / 2)
115
+ for (const c of fits) {
116
+ const ew = c.el.offsetWidth, eh = c.el.offsetHeight
117
+ const lane = Math.max(84, Math.min(150, (w - cw - 2 * ew - 28) / 2))
118
+ const x = side === 'in' ? Math.max(14, cx - cw / 2 - lane - ew) : Math.min(w - ew - 14, cx + cw / 2 + lane)
119
+ place(c.el, x, y)
120
+ rects.set(c.item.id, { x, y, w: ew, h: eh })
121
+ y += eh + gap
122
+ }
123
+ }
124
+ cards.splice(0, cards.length, ...columns.in, ...columns.out)
125
+ const a = rects.get('center')
126
+ for (const c of cards) {
127
+ const b = rects.get(c.item.id)
128
+ const from = c.item.dir === 'out' ? a : b, to = c.item.dir === 'out' ? b : a
129
+ drawEdge(from, to, c.item, c.item.relation)
130
+ }
131
+ }
132
+ if (total > rects.size - 1) cardLayer.append(h('p', { class: 'wb-more', text: `${total - (rects.size - 1)} more connections are listed in the sheet and in Text view.` }))
133
+ lastLayout = { cards: [...rects.entries()].map(([id, r]) => ({ id, ...r })), shown: rects.size - 1, total, narrow }
134
+ if (refocus) [...cardLayer.querySelectorAll(`.wb-${refocus.role}`)].find(x => x.dataset.kind === refocus.kind && x.dataset.id === refocus.id)?.focus({ preventScroll: true })
135
+ if (animate && subject.from && !reduced()) {
136
+ const r = rects.get('center')
137
+ const dx = subject.from.x - (r.x + r.w / 2) - graphHost.getBoundingClientRect().left, dy = subject.from.y - (r.y + r.h / 2) - graphHost.getBoundingClientRect().top
138
+ center.animate([{ transform: `translate(${dx}px, ${dy}px) scale(0.35)`, opacity: 0.2 }, { transform: 'none', opacity: 1 }], { duration: 420, easing: 'cubic-bezier(0.2,0,0,1)' })
139
+ for (const c of cards) c.el.animate([{ opacity: 0, transform: 'scale(0.92)' }, { opacity: 1, transform: 'none' }], { duration: 360, delay: 140, easing: 'ease-out', fill: 'backwards' })
140
+ }
141
+ }
142
+ function place(el, x, y) { el.style.transform = `translate(${Math.round(x)}px, ${Math.round(y)}px)` }
143
+
144
+ function drawEdge(a, b, item, relationId) {
145
+ const ac = { x: a.x + a.w / 2, y: a.y + a.h / 2 }, bc = { x: b.x + b.w / 2, y: b.y + b.h / 2 }
146
+ const clip = (r, from, to) => {
147
+ const dx = to.x - from.x, dy = to.y - from.y
148
+ const tx = dx ? (r.w / 2 + 6) / Math.abs(dx) : Infinity, ty = dy ? (r.h / 2 + 6) / Math.abs(dy) : Infinity
149
+ const t = Math.min(tx, ty)
150
+ return { x: from.x + dx * t, y: from.y + dy * t }
151
+ }
152
+ const start = clip(a, ac, bc), end = clip(b, bc, ac)
153
+ const ns = 'http://www.w3.org/2000/svg'
154
+ const g = document.createElementNS(ns, 'g')
155
+ g.setAttribute('class', `wb-edge ${item.structural ? 'is-declared' : 'is-link'} ${item.predicate === 'contradicts' ? 'is-challenge' : ''} ${item.predicate === 'declares' ? 'is-declares' : ''}`)
156
+ const line = document.createElementNS(ns, 'line')
157
+ for (const [k, v] of Object.entries({ x1: start.x, y1: start.y, x2: end.x, y2: end.y })) line.setAttribute(k, v.toFixed(1))
158
+ const angle = Math.atan2(end.y - start.y, end.x - start.x)
159
+ const head = document.createElementNS(ns, 'path')
160
+ const s = 8
161
+ head.setAttribute('d', `M${end.x},${end.y} L${end.x - Math.cos(angle - 0.42) * s},${end.y - Math.sin(angle - 0.42) * s} L${end.x - Math.cos(angle + 0.42) * s},${end.y - Math.sin(angle + 0.42) * s}Z`)
162
+ g.append(line, head)
163
+ edgeSvg.append(g)
164
+ if (item.structural) {
165
+ const mid = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 }
166
+ const pill = h('button', { class: 'wb-pill', type: 'button', style: `transform:translate(${Math.round(mid.x)}px, ${Math.round(mid.y)}px) translate(-50%,-50%)` }, verb(item.predicate), h('span', { 'aria-hidden': 'true', class: 'wb-pill-arrow', style: `transform:rotate(${angle}rad)`, text: '→' }))
167
+ pill.setAttribute('aria-label', `${verb(item.predicate)}, direction from ${item.dir === 'out' ? 'this subject' : 'the other card'}`)
168
+ if (relationId) pill.addEventListener('click', () => walk({ kind: 'relation', id: relationId }))
169
+ else pill.tabIndex = -1
170
+ cardLayer.append(pill)
171
+ }
172
+ }
173
+
174
+ // ── Sheet ──
175
+ function renderSheet() {
176
+ clear(sheet)
177
+ if (subject.kind === 'record') return recordSheet()
178
+ const isConcept = subject.kind === 'concept'
179
+ const item = isConcept ? conceptById.get(subject.id) : plan.relations.find(r => r.id === subject.id)
180
+ const titleOf = id => conceptById.get(id)?.title ?? id
181
+ const steps = isConcept ? conceptSteps(item) : relationSteps(item, titleOf)
182
+ const d = isConcept ? store.concept(item) : store.relation(item)
183
+ const guide = isConcept ? guideConcept(item, d, visible) : guideRelation(d, visible)
184
+ const originals = isConcept ? { definition: item.definition, identityRule: item.identityRule } : { meaning: item.meaning }
185
+ const states = steps.map(s => stepState(d[s.id], guide.fields[s.id], store.deferred(subject.kind, subject.id, s.id), originals[s.id]))
186
+ const countText = list => `${list.filter(x => x === 'answered').length} of ${steps.length} drafted · ${list.filter(x => x === 'plan').length} use the plan wording · ${list.filter(x => x === 'deferred').length} deferred`
187
+ step = Math.min(step, steps.length - 1)
188
+ sheet.append(h('p', { class: 'eyebrow', text: isConcept ? `${vocab.concept} · in your plan` : 'Relation · in your plan' }),
189
+ h('h2', { id: 'wb-title', tabindex: '-1', text: isConcept ? item.title : `${titleOf(item.from)} ${verb(item.predicate)} ${titleOf(item.to)}` }),
190
+ h('p', { class: 'sheet-count', id: 'step-count', text: countText(states) }))
191
+ const rail = h('ol', { class: 'step-rail', 'aria-label': 'Steps' })
192
+ steps.forEach((s, i) => rail.append(h('li', {}, h('button', { class: `step step-${states[i]}`, type: 'button', 'aria-current': i === step ? 'step' : null, dataset: { step: s.id },
193
+ onclick: () => { step = i; renderSheet(); sheet.querySelector('.step-body textarea, .step-body input, .step-body button')?.focus() } },
194
+ h('span', { class: 'step-n', text: states[i] === 'answered' ? '✓' : states[i] === 'plan' ? '•' : String(i + 1) }), h('span', { text: s.label })))))
195
+ sheet.append(rail)
196
+ const s = steps[step]
197
+ const body = h('section', { class: 'step-body', 'aria-labelledby': 'step-prompt' })
198
+ body.append(h('h3', { id: 'step-prompt', text: s.prompt }), h('p', { class: 'step-help', text: s.help }))
199
+ const original = isConcept ? { definition: item.definition, identityRule: item.identityRule }[s.id] : s.id === 'meaning' ? item.meaning : undefined
200
+ if (s.kind === 'evidence') body.append(evidenceStep(item, d))
201
+ else {
202
+ const ta = h('textarea', { id: 'step-input', rows: 5, 'aria-describedby': 'step-guide', oninput: e => { d[s.id] = e.target.value; store.changed(); updateGuide() } }, d[s.id])
203
+ body.append(ta)
204
+ if (original !== undefined && original !== d[s.id]) body.append(h('details', { class: 'plan-wording' }, h('summary', { text: 'Current plan wording' }), h('p', { text: original })))
205
+ }
206
+ const guideList = h('ul', { class: 'guide', id: 'step-guide', 'aria-live': 'polite' })
207
+ const updateGuide = () => {
208
+ const g = isConcept ? guideConcept(item, d, visible) : guideRelation(d, visible)
209
+ clear(guideList)
210
+ for (const m of g.fields[s.id] ?? []) guideList.append(h('li', { class: 'guide-' + m.tone, text: m.text }))
211
+ const now = steps.map(x => stepState(d[x.id], g.fields[x.id], store.deferred(subject.kind, subject.id, x.id), originals[x.id]))
212
+ sheet.querySelector('#step-count').textContent = countText(now)
213
+ const button = rail.querySelectorAll('.step')[step]
214
+ const st = now[step]
215
+ button.className = `step step-${st}`
216
+ button.querySelector('.step-n').textContent = st === 'answered' ? '✓' : st === 'plan' ? '•' : String(step + 1)
217
+ layoutCardsText()
218
+ }
219
+ body.append(guideList)
220
+ body.append(h('label', { class: 'defer' }, h('input', { type: 'checkbox', checked: store.deferred(subject.kind, subject.id, s.id), onchange: e => { store.defer(subject.kind, subject.id, s.id, e.target.checked); renderSheet() } }), ' Defer this step'))
221
+ sheet.append(body)
222
+ updateGuide()
223
+ const last = step === steps.length - 1
224
+ sheet.append(h('div', { class: 'sheet-actions' },
225
+ h('button', { class: 'quiet', type: 'button', disabled: step === 0, onclick: () => { step--; renderSheet(); focusStep() } }, 'Back'),
226
+ last ? h('button', { class: 'primary', type: 'button', id: 'review-proposal', onclick: () => onReview() }, 'Review the proposal')
227
+ : h('button', { class: 'primary', type: 'button', id: 'next-step', onclick: () => { step++; renderSheet(); focusStep() } }, 'Next')))
228
+ sheet.append(h('p', { class: 'sheet-note', text: 'Guidance here is a simulation; Atelier’s ontology guide runs on the host. Completeness is not a verdict on truth.' }))
229
+ }
230
+ function focusStep() { sheet.querySelector('#step-input, .step-body .cite')?.focus() }
231
+ function layoutCardsText() {
232
+ const c = cardLayer.querySelector('.wb-center.is-concept .wb-summary')
233
+ if (c && subject.kind === 'concept') c.textContent = store.conceptDraft(subject.id)?.definition ?? ''
234
+ }
235
+
236
+ function evidenceStep(item, d) {
237
+ const isConcept = !Object.hasOwn(item, 'predicate')
238
+ const wrap = h('div', { class: 'evidence' })
239
+ const cited = h('ul', { class: 'cited' })
240
+ for (const b of d.evidence) cited.append(h('li', {}, h('span', { text: visible.has(b.id) ? byId.get(b.id).title : 'A record outside what you can read' }),
241
+ h('button', { class: 'link', type: 'button', onclick: () => { d.evidence = d.evidence.filter(x => x.id !== b.id); store.changed(); renderSheet() } }, 'Remove')))
242
+ wrap.append(h('p', { class: 'label', text: d.evidence.length ? 'Cited' : 'Nothing cited yet' }), cited)
243
+ const cite = id => { if (!byId.has(id)) return; if (!d.evidence.some(b => b.id === id)) { d.evidence.push({ id, title: byId.get(id).title, citedAt: new Date().toISOString() }); store.changed(); renderSheet(); layoutGraph(false) } }
244
+ const suggest = (title, ids, note) => {
245
+ const list = ids.filter(id => byId.has(id) && !d.evidence.some(b => b.id === id)).slice(0, 6)
246
+ if (!list.length) return
247
+ wrap.append(h('p', { class: 'label', text: title }), note ? h('p', { class: 'step-help', text: note }) : null)
248
+ const ul = h('ul', { class: 'suggest' })
249
+ for (const id of list) ul.append(h('li', {}, h('span', { class: 'dot', style: `background:${zoneColor(byId.get(id).zone)}` }), h('span', { class: 'suggest-title', text: byId.get(id).title }), h('button', { class: 'cite secondary small', type: 'button', onclick: () => cite(id) }, 'Cite')))
250
+ wrap.append(ul)
251
+ }
252
+ const Records = vocab.records[0].toUpperCase() + vocab.records.slice(1)
253
+ if (isConcept) {
254
+ suggest(`${Records} that declare this concept`, item.declaredBy, 'The record itself carries this concept’s tag.')
255
+ const term = item.title.toLowerCase()
256
+ const matches = model.nodes.filter(n => !item.declaredBy.includes(n.id) && (n.title + ' ' + n.summary).toLowerCase().includes(term)).map(n => n.id)
257
+ suggest('Text matches', matches, 'These mention the word. That alone does not make them members.')
258
+ } else {
259
+ const ends = [item.from, item.to].map(id => new Set(conceptById.get(id)?.declaredBy ?? []))
260
+ suggest(`${Records} that declare both concepts`, [...ends[0]].filter(id => ends[1].has(id)), 'Each carries both concept tags.')
261
+ suggest(`${Records} that declare either concept`, [...new Set([...ends[0], ...ends[1]])].filter(id => !(ends[0].has(id) && ends[1].has(id))), 'One end only. That alone does not show the relation.')
262
+ }
263
+ const find = h('input', { type: 'search', class: 'find', placeholder: `Find any ${vocab.record}…`, 'aria-label': `Find a ${vocab.record} to cite` })
264
+ const results = h('ul', { class: 'suggest' })
265
+ find.addEventListener('input', () => {
266
+ clear(results)
267
+ const q = find.value.trim().toLowerCase()
268
+ if (q.length < 2) return
269
+ for (const n of model.nodes.filter(n => n.title.toLowerCase().includes(q)).slice(0, 6)) results.append(h('li', {}, h('span', { class: 'dot', style: `background:${zoneColor(n.zone)}` }), h('span', { class: 'suggest-title', text: n.title }), h('button', { class: 'cite secondary small', type: 'button', onclick: () => cite(n.id) }, 'Cite')))
270
+ })
271
+ wrap.append(find, results)
272
+ return wrap
273
+ }
274
+
275
+ function recordSheet() {
276
+ const n = byId.get(subject.id)
277
+ const links = adjacency.get(n.id) ?? []
278
+ sheet.append(h('p', { class: 'eyebrow' }, h('i', { class: 'dot', style: `background:${zoneColor(n.zone)}` }), n.zoneLabel),
279
+ h('h2', { id: 'wb-title', tabindex: '-1', text: n.title }),
280
+ h('p', { class: 'lede', text: n.summary }))
281
+ const facts = h('dl', { class: 'facts' })
282
+ for (const [k, v] of [['Kind', n.type], ['Status', n.status], ['Location', n.path], ['Links', `${n.degree} (${n.relationDegree} declared relations)`], ['Declares', n.concepts.length ? n.concepts.map(id => conceptById.get(id)?.title ?? id).join(', ') : 'No concept tags']]) if (v) facts.append(h('dt', { text: k }), h('dd', { text: v }))
283
+ sheet.append(facts)
284
+ if (plan.concepts.length) {
285
+ const select = h('select', { 'aria-label': 'Concept draft to cite into' }, plan.concepts.map(c => h('option', { value: c.id, text: c.title })))
286
+ sheet.append(h('div', { class: 'sheet-actions left' }, select, h('button', { class: 'secondary', type: 'button', id: 'cite-record', onclick: () => {
287
+ const c = conceptById.get(select.value), d = store.concept(c)
288
+ if (!d.evidence.some(b => b.id === n.id)) { d.evidence.push({ id: n.id, title: n.title, citedAt: new Date().toISOString() }); store.changed() }
289
+ walk({ kind: 'concept', id: c.id }); step = conceptSteps(c).findIndex(s => s.kind === 'evidence'); renderSheet()
290
+ } }, 'Cite in this concept’s draft')))
291
+ }
292
+ const list = h('ul', { class: 'link-list' })
293
+ for (const a of links.sort((x, y) => Number((y.edge.kind === 'relation')) - Number((x.edge.kind === 'relation'))).slice(0, 40)) {
294
+ const other = byId.get(a.other)
295
+ list.append(h('li', {}, h('button', { class: 'link-row', type: 'button', onclick: () => walk({ kind: 'record', id: other.id }) },
296
+ h('span', { class: 'dir', 'aria-hidden': 'true', text: a.dir === 'out' ? '→' : '←' }),
297
+ h('span', { text: a.dir === 'out' ? `${verb(a.edge.type)} ${other.title}` : `${other.title} ${verb(a.edge.type)} this` }))))
298
+ }
299
+ sheet.append(h('h3', { class: 'sheet-h', text: `Connections · ${links.length}` }), list)
300
+ if (links.length > 40) sheet.append(h('p', { class: 'step-help', text: `${links.length - 40} more in Text view.` }))
301
+ sheet.append(h('div', { class: 'sheet-actions left' }, h('button', { class: 'quiet', type: 'button', onclick: () => onShowOnMap(n.id) }, 'Show on the constellation')))
302
+ }
303
+
304
+ function renderHead() {
305
+ clear(head)
306
+ head.append(h('button', { class: 'back', type: 'button', id: 'wb-back', onclick: onBack }, '← Constellation'))
307
+ const crumbs = h('nav', { class: 'trail', 'aria-label': 'Walked so far' })
308
+ trail.forEach((t, i) => {
309
+ if (i) crumbs.append(h('span', { class: 'sep', 'aria-hidden': 'true', text: '›' }))
310
+ const label = t.kind === 'record' ? byId.get(t.id).title : t.kind === 'concept' ? conceptById.get(t.id).title : 'Relation'
311
+ crumbs.append(i === trail.length - 1 ? h('span', { class: 'crumb current', 'aria-current': 'page', text: label }) : h('button', { class: 'crumb', type: 'button', onclick: () => { trail = trail.slice(0, i + 1); subject = { ...t }; step = 0; render(false) } }, label))
312
+ })
313
+ const status = h('span', { class: 'save-status', id: 'save-status', role: 'status' })
314
+ const where = { tab: 'in this tab only', browser: 'in this browser only', host: 'to the workspace host' }[store.persistence] ?? 'in this tab only'
315
+ const paint = st => { status.dataset.state = st; status.textContent = { saving: `Saving ${where}\u2026`, saved: `Saved ${where}`, restored: `Draft restored, saved ${where}`, empty: 'Nothing drafted yet', unavailable: 'Storage unavailable \u2014 the draft lives in this tab' }[st] ?? st }
316
+ paint(store.status)
317
+ unsubscribe?.()
318
+ unsubscribe = store.onStatus(paint)
319
+ head.append(crumbs, status)
320
+ }
321
+
322
+ function walk(next, fromEl) {
323
+ const r = fromEl?.getBoundingClientRect()
324
+ subject = { ...next, from: r ? { x: r.left + r.width / 2, y: r.top + r.height / 2 } : null }
325
+ trail = [...trail.filter(t => !(t.kind === next.kind && t.id === next.id)), { kind: next.kind, id: next.id }].slice(-6)
326
+ step = 0
327
+ render(true)
328
+ }
329
+
330
+ function render(animate) {
331
+ renderHead()
332
+ layoutGraph(animate)
333
+ renderSheet()
334
+ }
335
+
336
+ return {
337
+ open(next, { from = null } = {}) {
338
+ root.hidden = false
339
+ subject = { ...next, from }
340
+ trail = [{ kind: next.kind, id: next.id }]
341
+ step = 0
342
+ render(true)
343
+ root.querySelector('#wb-title')?.focus()
344
+ },
345
+ close() { root.hidden = true },
346
+ get subject() { return subject && { kind: subject.kind, id: subject.id } },
347
+ get layout() { return lastLayout },
348
+ goToStep(id) { const steps = subject.kind === 'concept' ? conceptSteps(conceptById.get(subject.id)) : relationSteps(plan.relations.find(r => r.id === subject.id), x => x); step = Math.max(0, steps.findIndex(s => s.id === id)); renderSheet() },
349
+ refresh() { if (subject) render(false) },
350
+ }
351
+ }