@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,324 @@
1
+ import { canonicalize } from '../attestation/jcs.mjs'
2
+ import { compareGraphSnapshots } from './compare.mjs'
3
+
4
+ const clone = value => value === null ? null : JSON.parse(canonicalize(value))
5
+ const validBinding = (binding, now) => binding && typeof binding.key === 'string' && binding.key.length > 0 &&
6
+ Number.isSafeInteger(binding.expiresAtMs) && binding.expiresAtMs > now
7
+ const qualifiedKey = ref => ref && typeof ref.repoId === 'string' && ref.repoId &&
8
+ typeof ref.localId === 'string' && ref.localId ? canonicalize([ref.repoId, ref.localId]) : null
9
+
10
+ // One foreground host owns this controller. Binding and revalidate are trusted
11
+ // host ports, never browser-supplied grants. Builds compute in isolation; only
12
+ // the synchronous commit below may replace the published projection.
13
+ export function createGraphRefresh({
14
+ build, validate, revalidate, stage = () => ({ commit() {}, discard() {} }),
15
+ clear = () => {}, onCleanup = () => {}, debounceMs = 40, historyLimit = 3, now = Date.now,
16
+ } = {}) {
17
+ for (const port of [build, validate, revalidate, stage, clear, onCleanup]) {
18
+ if (typeof port !== 'function') throw new TypeError('Graph refresh requires host ports')
19
+ }
20
+ if (!Number.isSafeInteger(debounceMs) || debounceMs < 0 || debounceMs > 60_000 ||
21
+ !Number.isSafeInteger(historyLimit) || historyLimit < 0 || historyLimit > 20) {
22
+ throw new TypeError('Invalid graph refresh bounds')
23
+ }
24
+ let running = false
25
+ let generation = 0
26
+ let binding = null
27
+ let current = null
28
+ let history = []
29
+ let selectedRef = null
30
+ let phase = 'stopped'
31
+ let failure = null
32
+ let pending = false
33
+ let timer = null
34
+ let expiryTimer = null
35
+ let job = null
36
+ let active = null
37
+ const listeners = new Set()
38
+ // Host resource custody, separate from reader state. At most one unresolved
39
+ // transaction exists: a failure blocks further builds/staging until resolved.
40
+ const cleanupTasks = new Map()
41
+ let cleanupSequence = 0
42
+
43
+ function cleanupOutcome(entry, state) {
44
+ const outcome = Object.freeze({ cleanupId: entry.cleanupId, state, code: state === 'failed' ? 'stage-cleanup-failed' : null })
45
+ try { onCleanup(outcome)?.catch?.(() => {}) } catch { /* retained status remains available to the host */ }
46
+ }
47
+
48
+ function discard(entry) {
49
+ if (entry.inFlight) return entry.inFlight
50
+ entry.state = 'cleaning'
51
+ // Defer invocation so even a synchronous rejection settles after custody
52
+ // and the shared in-flight promise have been installed.
53
+ entry.inFlight = Promise.resolve().then(() => entry.transaction.discard()).then(() => {
54
+ cleanupTasks.delete(entry.cleanupId)
55
+ cleanupOutcome(entry, 'resolved')
56
+ return true
57
+ }, () => {
58
+ entry.state = 'failed'
59
+ cleanupOutcome(entry, 'failed')
60
+ return false
61
+ }).finally(() => { entry.inFlight = null })
62
+ return entry.inFlight
63
+ }
64
+
65
+ function status() {
66
+ expire()
67
+ return { phase, generation, stale: current !== null && current.generation !== generation, failure }
68
+ }
69
+
70
+ function emit(type) {
71
+ const event = { type, phase, generation, stale: current !== null && current.generation !== generation, failure }
72
+ // Events contain no private refs, diagnostics or former projection digests.
73
+ for (const listener of [...listeners]) {
74
+ try { listener(Object.freeze({ ...event })) } catch { /* one consumer cannot block another's invalidation */ }
75
+ }
76
+ }
77
+
78
+ function purge(nextPhase) {
79
+ generation += 1
80
+ active?.abort()
81
+ clearTimeout(timer)
82
+ clearTimeout(expiryTimer)
83
+ timer = expiryTimer = null
84
+ pending = false
85
+ binding = null
86
+ current = selectedRef = null
87
+ history = []
88
+ phase = nextPhase
89
+ failure = null
90
+ try {
91
+ const result = clear()
92
+ if (result && typeof result.then === 'function') {
93
+ result.catch?.(() => {})
94
+ throw new Error('cache clear must be synchronous')
95
+ }
96
+ } catch { failure = 'cache-clear-failed' }
97
+ emit('clear')
98
+ }
99
+
100
+ function expire() {
101
+ if (binding && !validBinding(binding, now())) purge(running ? 'denied' : 'stopped')
102
+ }
103
+
104
+ function armExpiry() {
105
+ clearTimeout(expiryTimer)
106
+ if (!binding) return
107
+ // Re-arm long grants; node timers use a signed 32-bit delay.
108
+ expiryTimer = setTimeout(() => { expire(); if (binding) armExpiry() }, Math.min(2_147_483_647, Math.max(1, binding.expiresAtMs - now())))
109
+ expiryTimer.unref?.()
110
+ }
111
+
112
+ function eligible(expectedGeneration, expectedBinding) {
113
+ expire()
114
+ return running && generation === expectedGeneration && binding === expectedBinding && validBinding(binding, now())
115
+ }
116
+
117
+ async function permitted(expectedBinding, projection) {
118
+ try { return (await revalidate(expectedBinding, clone(projection))) === true } catch { return false }
119
+ }
120
+
121
+ async function runBuild() {
122
+ const expectedGeneration = generation
123
+ const expectedBinding = binding
124
+ const controller = new AbortController()
125
+ active = controller
126
+ let transaction = null
127
+ let committed = false
128
+ try {
129
+ if (!eligible(expectedGeneration, expectedBinding)) return
130
+ if (cleanupTasks.size) {
131
+ phase = 'failed'
132
+ failure = 'stage-cleanup-pending'
133
+ emit('state')
134
+ return
135
+ }
136
+ phase = 'building'
137
+ failure = null
138
+ emit('state')
139
+ if (!(await permitted(expectedBinding, null))) {
140
+ if (eligible(expectedGeneration, expectedBinding)) purge('denied')
141
+ return
142
+ }
143
+ if (!eligible(expectedGeneration, expectedBinding)) return
144
+ const built = await build({ generation: expectedGeneration, binding: expectedBinding, signal: controller.signal })
145
+ if (!eligible(expectedGeneration, expectedBinding)) return
146
+ const projection = clone(built)
147
+ if ((await validate(projection)) !== true) throw new Error('unsupported')
148
+ if (!eligible(expectedGeneration, expectedBinding)) return
149
+ if (!(await permitted(expectedBinding, projection))) {
150
+ if (eligible(expectedGeneration, expectedBinding)) purge('denied')
151
+ return
152
+ }
153
+ if (!eligible(expectedGeneration, expectedBinding)) return
154
+ transaction = await stage({ projection: clone(projection), generation: expectedGeneration, signal: controller.signal })
155
+ if (!transaction || typeof transaction.commit !== 'function' || typeof transaction.discard !== 'function') {
156
+ throw new Error('stage-failed')
157
+ }
158
+ if (!eligible(expectedGeneration, expectedBinding)) return
159
+ if (!(await permitted(expectedBinding, projection))) {
160
+ if (eligible(expectedGeneration, expectedBinding)) purge('denied')
161
+ return
162
+ }
163
+ if (!eligible(expectedGeneration, expectedBinding)) return
164
+ // No await between the final fence, host commit, and pointer replacement.
165
+ // Commit is a synchronous atomic host operation; asynchronous writes are
166
+ // unsupported because they cannot be fenced by this controller.
167
+ const result = transaction.commit()
168
+ if (result && typeof result.then === 'function') {
169
+ result.catch?.(() => {})
170
+ throw new Error('commit-must-be-synchronous')
171
+ }
172
+ if (!eligible(expectedGeneration, expectedBinding)) return
173
+ committed = true
174
+ if (current && current.projection.projectionDigest !== projection.projectionDigest) {
175
+ history = [current, ...history.filter(entry => entry.projection.projectionDigest !== current.projection.projectionDigest)].slice(0, historyLimit)
176
+ }
177
+ current = { projection, generation: expectedGeneration }
178
+ if (selectedRef && !projection.nodes.some(node => qualifiedKey(node.ref) === qualifiedKey(selectedRef))) selectedRef = null
179
+ phase = 'current'
180
+ failure = null
181
+ emit('publish')
182
+ } catch (error) {
183
+ if (eligible(expectedGeneration, expectedBinding)) {
184
+ phase = error?.message === 'unsupported' ? 'unsupported' : 'failed'
185
+ // Arbitrary build errors may contain paths or private source content.
186
+ failure = phase === 'unsupported' ? 'projection-unsupported' : 'build-or-save-failed'
187
+ emit('state')
188
+ }
189
+ } finally {
190
+ if (!committed && transaction && typeof transaction.discard === 'function') {
191
+ const entry = { cleanupId: `cleanup-${++cleanupSequence}`, transaction, state: 'cleaning', inFlight: null, retryInFlight: null }
192
+ cleanupTasks.set(entry.cleanupId, entry)
193
+ if (!(await discard(entry)) && eligible(expectedGeneration, expectedBinding)) {
194
+ phase = 'failed'; failure = 'stage-cleanup-failed'; emit('state')
195
+ }
196
+ }
197
+ if (active === controller) active = null
198
+ }
199
+ }
200
+
201
+ function launch() {
202
+ clearTimeout(timer)
203
+ timer = null
204
+ expire()
205
+ if (!running || !binding || job || !pending) return
206
+ pending = false
207
+ // One build/stage at a time: an abort-ignoring builder cannot create an
208
+ // unbounded queue of writers. Its return is still fenced by generation.
209
+ job = runBuild().finally(() => {
210
+ job = null
211
+ if (pending && running && binding) schedule()
212
+ })
213
+ }
214
+
215
+ function schedule() {
216
+ if (!timer) timer = setTimeout(launch, debounceMs)
217
+ }
218
+
219
+ function invalidate() {
220
+ expire()
221
+ if (!running || !binding) return
222
+ generation += 1
223
+ active?.abort()
224
+ pending = true
225
+ failure = null
226
+ phase = current ? 'stale' : 'pending'
227
+ emit('invalidate')
228
+ // A fixed coalescing window avoids starvation under continuous edits.
229
+ schedule()
230
+ }
231
+
232
+ async function read(digest = null) {
233
+ expire()
234
+ const expectedBinding = binding
235
+ if (!running || !expectedBinding) return { ...status(), projection: null, selectedRef: null, historical: false }
236
+ const entry = digest === null ? current : [current, ...history].find(item => item?.projection.projectionDigest === digest)
237
+ if (!entry) return { ...status(), projection: null, selectedRef: null, historical: false }
238
+ const allowed = await permitted(expectedBinding, entry.projection)
239
+ expire()
240
+ if (binding !== expectedBinding || !running) return { ...status(), projection: null, selectedRef: null, historical: false }
241
+ if (!allowed) {
242
+ purge('denied')
243
+ return { ...status(), projection: null, selectedRef: null, historical: false }
244
+ }
245
+ // A source-only edit may happen while access is checked: label that view
246
+ // stale. Access changes and stop purge the captured entry altogether.
247
+ return { ...status(), stale: entry.generation !== generation, projection: clone(entry.projection), selectedRef: entry === current ? clone(selectedRef) : null, historical: entry !== current }
248
+ }
249
+
250
+ return {
251
+ start() {
252
+ if (running) return
253
+ running = true
254
+ phase = binding ? 'pending' : 'denied'
255
+ invalidate()
256
+ },
257
+ stop() {
258
+ running = false
259
+ purge('stopped')
260
+ },
261
+ // Call only with a trusted host binding whose key binds principal, exact
262
+ // repository set, grant revision/expiry, policy and filter. Every call
263
+ // clears history, selection, and caches before accepting a replacement.
264
+ setAccess(nextBinding) {
265
+ purge(running ? 'denied' : 'stopped')
266
+ if (failure || !validBinding(nextBinding, now())) return
267
+ binding = Object.freeze({ ...nextBinding })
268
+ armExpiry()
269
+ if (running) invalidate()
270
+ },
271
+ invalidateAccess() { purge(running ? 'denied' : 'stopped') },
272
+ invalidate,
273
+ read,
274
+ status,
275
+ // Trusted host-only resource APIs. Never serialize these into a reader
276
+ // response; they carry no projection, binding, path, or raw exception.
277
+ cleanupStatus() { return [...cleanupTasks.values()].map(entry => ({ cleanupId: entry.cleanupId, state: entry.state })) },
278
+ async retryCleanup(cleanupId) {
279
+ const entry = cleanupTasks.get(cleanupId)
280
+ if (!entry) return false
281
+ // Share the complete host retry, including its failure result and the
282
+ // single rebuild request. A failed shared attempt needs a new host call.
283
+ if (!entry.retryInFlight) {
284
+ entry.retryInFlight = (async () => {
285
+ const resolved = await discard(entry)
286
+ if (resolved && running && binding && cleanupTasks.size === 0) invalidate()
287
+ return resolved
288
+ })().finally(() => { entry.retryInFlight = null })
289
+ }
290
+ return entry.retryInFlight
291
+ },
292
+ async select(ref, digest) {
293
+ const result = await read()
294
+ if (!result.projection || result.generation !== generation || result.stale || digest !== current?.projection.projectionDigest ||
295
+ digest !== result.projection.projectionDigest ||
296
+ !qualifiedKey(ref) || !result.projection.nodes.some(node => qualifiedKey(node.ref) === qualifiedKey(ref))) return false
297
+ selectedRef = clone(ref)
298
+ return true
299
+ },
300
+ async compare(beforeDigest, afterDigest) {
301
+ const expectedBinding = binding
302
+ const expectedGeneration = generation
303
+ const before = await read(beforeDigest)
304
+ const after = await read(afterDigest)
305
+ if (!expectedBinding || binding !== expectedBinding || expectedGeneration !== generation || !before.projection || !after.projection) return null
306
+ return compareGraphSnapshots(before.projection, after.projection)
307
+ },
308
+ subscribe(listener) {
309
+ if (typeof listener !== 'function') throw new TypeError('Refresh listener must be a function')
310
+ listeners.add(listener)
311
+ return () => listeners.delete(listener)
312
+ },
313
+ async flush() {
314
+ clearTimeout(timer)
315
+ timer = null
316
+ while (job || (running && pending)) {
317
+ if (!job) launch()
318
+ if (job) await job
319
+ else break
320
+ }
321
+ return status()
322
+ },
323
+ }
324
+ }
@@ -0,0 +1,3 @@
1
+ export { createGraphRefresh } from './controller.mjs'
2
+ export { createLocalGraphRefresh } from './local.mjs'
3
+ export { compareGraphSnapshots } from './compare.mjs'
@@ -0,0 +1,78 @@
1
+ import { createFsWatcherFactory } from '../runtime/obsidian/watchers.mjs'
2
+ import { createGraphRefresh } from './controller.mjs'
3
+
4
+ // Attach to the existing sidecar's start/close, never to a second listener or
5
+ // daemon. Watch events are hints. Periodic rebuilds reconcile dropped events;
6
+ // the canonical builder/file cache remains the source of incremental reuse.
7
+ export function createLocalGraphRefresh({ roots, watcherFactory, reconcileMs = 30_000, ...ports } = {}) {
8
+ if (!Array.isArray(roots) || roots.length === 0 || roots.some(root => !root ||
9
+ typeof root.id !== 'string' || !root.id || typeof root.path !== 'string' || !root.path) ||
10
+ new Set(roots.map(root => root.id)).size !== roots.length ||
11
+ !Number.isSafeInteger(reconcileMs) || reconcileMs < 10 || reconcileMs > 2_147_483_647) {
12
+ throw new TypeError('Invalid local graph refresh roots or interval')
13
+ }
14
+ const controller = createGraphRefresh(ports)
15
+ let authorizedRoots = roots.map(root => ({ ...root }))
16
+ let watcher = null
17
+ let interval = null
18
+ let watchState = 'stopped'
19
+ let started = false
20
+ let resourceEpoch = 0
21
+
22
+ function stopResources() {
23
+ resourceEpoch += 1
24
+ clearInterval(interval)
25
+ interval = null
26
+ try { watcher?.close() } catch { /* stopped state still purges delivery */ }
27
+ watcher = null
28
+ watchState = 'stopped'
29
+ }
30
+
31
+ function startResources() {
32
+ if (!started || watcher || interval || ['denied', 'stopped'].includes(controller.status().phase)) return
33
+ watchState = 'watching'
34
+ const epoch = resourceEpoch
35
+ const onUnavailable = () => { if (started && epoch === resourceEpoch) watchState = 'unavailable' }
36
+ const watch = watcherFactory ?? createFsWatcherFactory({ onUnavailable })
37
+ try {
38
+ watcher = watch({ roots: authorizedRoots, onEvent() { if (started && epoch === resourceEpoch) controller.invalidate() }, onUnavailable })
39
+ if (!watcher || typeof watcher.close !== 'function') throw new Error('watcher unavailable')
40
+ } catch { watchState = 'unavailable' }
41
+ interval = setInterval(() => {
42
+ // Reconciliation must not repeatedly cancel a healthy long build.
43
+ if (!['building', 'pending', 'stale'].includes(controller.status().phase)) controller.invalidate()
44
+ }, reconcileMs)
45
+ interval.unref?.()
46
+ }
47
+
48
+ controller.subscribe(event => { if (event.type === 'clear') stopResources() })
49
+ return {
50
+ ...controller,
51
+ start() {
52
+ if (started) return
53
+ started = true
54
+ controller.start()
55
+ startResources()
56
+ },
57
+ stop() {
58
+ started = false
59
+ stopResources()
60
+ controller.stop()
61
+ },
62
+ setAccess(nextBinding, nextRoots = roots) {
63
+ // Roots are supplied by the trusted host for this exact decision. They
64
+ // do not establish permission. Closing old watchers prevents an event
65
+ // from a formerly permitted root invalidating a different reader view.
66
+ controller.invalidateAccess()
67
+ if (!Array.isArray(nextRoots) || !nextRoots.length || nextRoots.some(root => !root ||
68
+ typeof root.id !== 'string' || !root.id || typeof root.path !== 'string' || !root.path) ||
69
+ new Set(nextRoots.map(root => root.id)).size !== nextRoots.length) {
70
+ throw new TypeError('Invalid authorized refresh roots')
71
+ }
72
+ authorizedRoots = nextRoots.map(root => ({ ...root }))
73
+ controller.setAccess(nextBinding)
74
+ startResources()
75
+ },
76
+ status() { return { ...controller.status(), watchState } },
77
+ }
78
+ }
@@ -0,0 +1,64 @@
1
+ import { referenceKey } from './semantic-picking.mjs'
2
+
3
+ // Inject the exact locally supplied Babylon symbols and engine. No browser globals,
4
+ // engine construction, camera attachControl, subscriptions or scheduler live here.
5
+ export function createBabylonPortFactory(B, engine, { onSceneCreated = () => {} } = {}) {
6
+ return (profile, bind) => {
7
+ const scene = new B.Scene(engine)
8
+ try {
9
+ scene.useRightHandedSystem = true
10
+ scene.clearColor = new B.Color4(0.098, 0.102, 0.098, 1)
11
+ const camera = new B.ArcRotateCamera('view-camera', Math.PI / 2, 1.41, 10, B.Vector3.Zero(), scene)
12
+ camera.lowerRadiusLimit = 4
13
+ camera.upperRadiusLimit = 55
14
+ const light = new B.HemisphericLight('view-light', new B.Vector3(0, 1, 0), scene)
15
+ light.intensity = 1.5
16
+ const nodes = new Map()
17
+ const pickable = new Set()
18
+ for (const [index, node] of profile.nodes.entries()) {
19
+ // The mesh name is intentionally unrelated to domain identity.
20
+ const mesh = B.MeshBuilder.CreateSphere(`node-glyph-${index}`, { diameter: 0.5, segments: 12 }, scene)
21
+ mesh.position.copyFromFloats(...node.position)
22
+ const material = new B.StandardMaterial(`glyph-material-${index}`, scene)
23
+ material.diffuseColor = new B.Color3(0.65, 0.70, 0.72)
24
+ material.specularColor = new B.Color3(0.2, 0.2, 0.15)
25
+ mesh.material = material
26
+ nodes.set(referenceKey(node.ref), mesh)
27
+ pickable.add(mesh)
28
+ bind(mesh, node)
29
+ }
30
+ profile.edges.forEach((edge, index) => {
31
+ const line = B.MeshBuilder.CreateLines(`relation-${index}`, {
32
+ points: [nodes.get(referenceKey(edge.from)).position, nodes.get(referenceKey(edge.to)).position],
33
+ }, scene)
34
+ line.isPickable = false
35
+ line.color = new B.Color3(0.45, 0.48, 0.44)
36
+ })
37
+ onSceneCreated({ scene, camera }) // Local host/qualifier seam; never a semantic event.
38
+ return {
39
+ render() { scene.render() },
40
+ pick(x, y) {
41
+ const hit = scene.pick(x, y, mesh => pickable.has(mesh))
42
+ return hit?.hit ? hit.pickedMesh : null
43
+ },
44
+ focus(position) { camera.setTarget(B.Vector3.FromArray(position)) },
45
+ select(ref) {
46
+ const key = ref ? referenceKey(ref) : null
47
+ for (const [nodeKey, mesh] of nodes) {
48
+ const selected = key === nodeKey
49
+ mesh.scaling.setAll(selected ? 1.3 : 1)
50
+ mesh.material.emissiveColor = selected ? new B.Color3(0.44, 0.35, 0.17) : B.Color3.Black()
51
+ }
52
+ },
53
+ dispose() {
54
+ nodes.clear()
55
+ pickable.clear()
56
+ scene.dispose()
57
+ },
58
+ }
59
+ } catch (error) {
60
+ scene.dispose()
61
+ throw error
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,2 @@
1
+ export { createGraphNavigator, layoutGraph } from './navigator.mjs'
2
+ export { mountGraphCanvas } from './web-host.mjs'