@mnstry/atelier 0.2.0-alpha.13 → 0.2.0-alpha.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +283 -0
- package/README.md +102 -52
- package/SECURITY.md +1 -1
- package/TRADEMARKS.md +2 -2
- package/contracts/atelier-extension-manifest.v1.schema.json +412 -0
- package/contracts/atelier-graph-view.v1.schema.json +1085 -0
- package/contracts/public-api-baseline.json +298 -19
- package/docs/README.md +90 -0
- package/docs/blocks/capability-stewardship.md +1 -1
- package/docs/blocks/claims.md +2 -2
- package/docs/blocks/conformance-admission.md +1 -1
- package/docs/blocks/learning-harnesses.md +1 -1
- package/docs/{atelier-sync.md → guides/atelier-sync.md} +20 -14
- package/docs/{capability-stewardship.md → guides/capability-stewardship.md} +2 -4
- package/docs/guides/co-authoring.md +113 -0
- package/docs/{distributions.md → guides/distributions.md} +13 -8
- package/docs/guides/extensions.md +189 -0
- package/docs/guides/graph-refresh.md +152 -0
- package/docs/guides/graph-workspace.md +64 -0
- package/docs/{ingestion.md → guides/ingestion.md} +16 -15
- package/docs/{inquiry-harnesses.md → guides/inquiry-harnesses.md} +2 -3
- package/docs/{knowledge-workspace.md → guides/knowledge-workspace.md} +2 -2
- package/docs/{learning-harnesses.md → guides/learning-harnesses.md} +2 -3
- package/docs/{learning.md → guides/learning.md} +12 -5
- package/docs/{obsidian.md → guides/obsidian.md} +370 -23
- package/docs/guides/ontology-guide.md +111 -0
- package/docs/{personal-workspace.md → guides/personal-workspace.md} +17 -5
- package/docs/{research-starter.md → guides/research-starter.md} +1 -1
- package/docs/{tenant-readiness.md → guides/tenant-readiness.md} +39 -0
- package/docs/{upgrade.md → guides/upgrade.md} +197 -11
- package/docs/knowledge-health/assessment/capabilities.md +6 -15
- package/docs/knowledge-health/facilitator.md +15 -4
- package/docs/knowledge-health/{devday/participant-response.md → participant-response.md} +4 -2
- package/docs/knowledge-health/quickstart.md +97 -75
- package/docs/knowledge-health/readme.md +13 -5
- package/docs/maintainers/describing-the-atelier.md +173 -0
- package/docs/{release-engineering.md → maintainers/release-engineering.md} +5 -4
- package/docs/{announcements.md → reference/announcements.md} +1 -1
- package/docs/{architecture.md → reference/architecture.md} +31 -5
- package/docs/{atelier-runtime.md → reference/atelier-runtime.md} +1 -1
- package/docs/{connected-composition.md → reference/connected-composition.md} +7 -10
- package/docs/{coordination.md → reference/coordination.md} +1 -1
- package/docs/{decisions.md → reference/decisions.md} +3 -3
- package/docs/{design.md → reference/design.md} +1 -1
- package/docs/reference/edge-policy.md +151 -0
- package/docs/reference/graph-view-structured.md +119 -0
- package/docs/reference/graph-view.md +139 -0
- package/docs/reference/graph-workspace.md +37 -0
- package/docs/{intake-and-guides.md → reference/intake-and-guides.md} +3 -4
- package/docs/{knowledge-graph.md → reference/knowledge-graph.md} +120 -6
- package/docs/{local-services.md → reference/local-services.md} +43 -4
- package/docs/{obsidian-contract.md → reference/obsidian-contract.md} +29 -4
- package/docs/{obsidian-plugin.md → reference/obsidian-plugin.md} +218 -68
- package/docs/{practical-judgment.md → reference/phronesis.md} +12 -3
- package/docs/reference/portability.md +92 -0
- package/docs/{portable-presentation.md → reference/portable-presentation.md} +2 -2
- package/docs/{presentation-consumer-boundaries.md → reference/presentation-consumer-boundaries.md} +5 -0
- package/docs/{project-options.md → reference/project-options.md} +17 -0
- package/docs/{repo-boundary-guard.md → reference/repo-boundary-guard.md} +50 -4
- package/docs/{review-portability.md → reference/review-portability.md} +4 -0
- package/docs/{semantic-operation.md → reference/semantic-operation.md} +4 -3
- package/docs/{template-profile.md → reference/template-profile.md} +1 -1
- package/docs/{trackables.md → reference/trackables.md} +2 -2
- package/docs/start/independent-start.md +89 -0
- package/docs/{install.md → start/install.md} +9 -9
- package/docs/{knowledge-setup.md → start/knowledge-setup.md} +5 -5
- package/docs/{research-starter-quickstart.md → start/research-starter-quickstart.md} +6 -6
- package/fixtures/atelier-architecture/catalog/invalid/document.json +2 -2
- package/fixtures/atelier-architecture/catalog/valid/document.json +2 -2
- package/fixtures/graph-view/synthetic-authorized-input.v1.json +437 -0
- package/fixtures/graph-view/synthetic-view.v1.json +1 -0
- package/fixtures/knowledge-health/workshop/README.md +1 -1
- package/fixtures/templates/local-library.v1.json +3 -1
- package/package.json +22 -8
- package/plugins/obsidian/main.js +57 -8
- package/plugins/obsidian/manifest.json +1 -1
- package/skills/claude/atelier-guided-upgrade/SKILL.md +1 -1
- package/skills/claude/atelier-knowledge-setup/SKILL.md +2 -2
- package/skills/claude/atelier-local-service/SKILL.md +1 -1
- package/skills/claude/atelier-practical-judgment/SKILL.md +7 -2
- package/skills/claude/atelier-skill-steward/SKILL.md +1 -1
- package/skills/claude/mnstry-readiness/SKILL.md +2 -2
- package/skills/codex/atelier-guided-upgrade/SKILL.md +1 -1
- package/skills/codex/atelier-knowledge-setup/SKILL.md +2 -2
- package/skills/codex/atelier-local-service/SKILL.md +1 -1
- package/skills/codex/atelier-practical-judgment/SKILL.md +7 -2
- package/skills/codex/atelier-skill-steward/SKILL.md +1 -1
- package/skills/codex/mnstry-readiness/SKILL.md +2 -2
- package/src/architecture/catalog.mjs +2 -2
- package/src/attestation/sign.mjs +1 -1
- package/src/boundary/content-rules.mjs +534 -68
- package/src/boundary/policy.mjs +187 -60
- package/src/cli/run.mjs +26 -9
- package/src/commands/distribution.mjs +1 -1
- package/src/commands/extension.mjs +27 -0
- package/src/commands/ingest.mjs +1 -1
- package/src/commands/learn.mjs +1 -1
- package/src/commands/obsidian.mjs +49 -21
- package/src/commands/review.mjs +3 -0
- package/src/edge-policy/contract.mjs +69 -0
- package/src/edge-policy/policy.mjs +142 -0
- package/src/edge-policy/source-citation-adapter.mjs +43 -0
- package/src/extension-packs/loader.mjs +4 -3
- package/src/extensions/admission.mjs +83 -0
- package/src/extensions/client.d.mts +23 -0
- package/src/extensions/client.mjs +99 -0
- package/src/extensions/contracts.d.mts +29 -0
- package/src/extensions/contracts.mjs +163 -0
- package/src/extensions/convex.d.mts +24 -0
- package/src/extensions/convex.mjs +189 -0
- package/src/extensions/delegation.mjs +54 -0
- package/src/extensions/drizzle.d.mts +11 -0
- package/src/extensions/drizzle.mjs +33 -0
- package/src/extensions/postgres.d.mts +7 -0
- package/src/extensions/postgres.mjs +66 -0
- package/src/extensions/prisma.d.mts +13 -0
- package/src/extensions/prisma.mjs +63 -0
- package/src/extensions/project.mjs +432 -0
- package/src/extensions/scaffold-convex.mjs +130 -0
- package/src/extensions/scaffold-sql.mjs +144 -0
- package/src/extensions/server.d.mts +20 -0
- package/src/extensions/server.mjs +102 -0
- package/src/extensions/sql-scope.mjs +22 -0
- package/src/graph/graph.mjs +13 -5
- package/src/graph/knowledge-graph.mjs +20 -2
- package/src/graph/repo-contribution.mjs +405 -0
- package/src/graph/repo-contribution.types.mjs +32 -0
- package/src/graph-access/edge-decision-adapter.mjs +41 -0
- package/src/graph-access/edge-record-adapter.mjs +73 -0
- package/src/graph-access/index.mjs +253 -0
- package/src/graph-access/multi-contribution-record-adapter.mjs +101 -0
- package/src/graph-access/multi-record-projection-adapter.mjs +74 -0
- package/src/graph-access/source-adapter.mjs +81 -0
- package/src/graph-refresh/compare.mjs +110 -0
- package/src/graph-refresh/controller.mjs +324 -0
- package/src/graph-refresh/index.mjs +3 -0
- package/src/graph-refresh/local.mjs +78 -0
- package/src/graph-view/babylon/babylon-scene-port.mjs +64 -0
- package/src/graph-view/babylon/index.mjs +2 -0
- package/src/graph-view/babylon/navigator.mjs +312 -0
- package/src/graph-view/babylon/provenance.mjs +36 -0
- package/src/graph-view/babylon/scene-controller.mjs +131 -0
- package/src/graph-view/babylon/semantic-picking.mjs +35 -0
- package/src/graph-view/babylon/web-host.mjs +188 -0
- package/src/graph-view/cli.mjs +3 -0
- package/src/graph-view/command.mjs +34 -0
- package/src/graph-view/contracts.mjs +79 -0
- package/src/graph-view/errors.mjs +5 -0
- package/src/graph-view/index.mjs +4 -0
- package/src/graph-view/intents.mjs +30 -0
- package/src/graph-view/query.mjs +213 -0
- package/src/graph-view/refs.mjs +2 -0
- package/src/graph-view/structured/browser.mjs +262 -0
- package/src/graph-view/structured/styles.mjs +8 -0
- package/src/graph-workspace/authoring.mjs +185 -0
- package/src/graph-workspace/browser/constellation.mjs +369 -0
- package/src/graph-workspace/browser/dom.mjs +23 -0
- package/src/graph-workspace/browser/index.mjs +6 -0
- package/src/graph-workspace/browser/review.mjs +55 -0
- package/src/graph-workspace/browser/styles.mjs +11 -0
- package/src/graph-workspace/browser/workbench.mjs +351 -0
- package/src/graph-workspace/browser/workspace.mjs +431 -0
- package/src/graph-workspace/disclosure.mjs +39 -0
- package/src/graph-workspace/geometry.mjs +78 -0
- package/src/graph-workspace/index.mjs +11 -0
- package/src/graph-workspace/labels.mjs +50 -0
- package/src/graph-workspace/model.mjs +109 -0
- package/src/graph-workspace/recipes.mjs +88 -0
- package/src/graph-workspace/scale.mjs +95 -0
- package/src/graph-workspace/theme.mjs +156 -0
- package/src/ingestion/optional/graphify/graphify_supplier.py +5 -0
- package/src/ingestion/optional/graphify/native-closure-pins.json +275 -0
- package/src/ingestion/optional/graphify/native-pins.json +306 -0
- package/src/knowledge/ontology-guide.mjs +152 -0
- package/src/knowledge-health/proof/installed-check.mjs +2 -2
- package/src/knowledge-health/rule-identity.mjs +14 -9
- package/src/knowledge-health/workshop.mjs +22 -2
- package/src/personal-workspace/index.mjs +1 -1
- package/src/personal-workspace/selection.mjs +34 -3
- package/src/project/config.mjs +1 -1
- package/src/project/path-match.mjs +2 -1
- package/src/project/providers/github.mjs +24 -0
- package/src/project/repo-identity.mjs +3 -22
- package/src/projection/obsidian/contracts.mjs +1 -1
- package/src/projection/obsidian/materialize/path-registry.mjs +1 -1
- package/src/projection/obsidian/materialize/prepare-view.mjs +88 -6
- package/src/projection/obsidian/materialize/redaction.mjs +1 -1
- package/src/projection/obsidian/personal-workspace.mjs +84 -5
- package/src/projection/obsidian/plugin-bridge/channel.mjs +1 -1
- package/src/projection/obsidian/proposals/adapter.mjs +46 -23
- package/src/projection/obsidian/proposals/observation.mjs +40 -2
- package/src/projection/obsidian/publication/publisher.mjs +96 -8
- package/src/projection/project.mjs +21 -16
- package/src/readiness/readiness.mjs +20 -11
- package/src/readiness-protocols/bundled-pack.mjs +14 -766
- package/src/readiness-protocols/bundled-selection.mjs +32 -0
- package/src/readiness-protocols/evidence.mjs +2 -1
- package/src/readiness-protocols/packs/mnstry.mjs +766 -0
- package/src/readiness-protocols/runtime.mjs +43 -6
- package/src/runtime/obsidian/engine.mjs +387 -76
- package/src/runtime/obsidian/lifecycle.mjs +20 -15
- package/src/runtime/obsidian/login-item.mjs +6 -3
- package/src/runtime/obsidian/machine-settings.mjs +52 -8
- package/src/runtime/obsidian/observation.mjs +36 -4
- package/src/runtime/obsidian/opening.mjs +4 -3
- package/src/runtime/obsidian/personal-selection.mjs +187 -0
- package/src/runtime/obsidian/pipeline.mjs +115 -6
- package/src/runtime/obsidian/private-lock.mjs +50 -6
- package/src/runtime/obsidian/selection-continuity.mjs +124 -0
- package/src/runtime/obsidian/service-main.mjs +68 -16
- package/src/runtime/obsidian/service.mjs +80 -6
- package/src/runtime/obsidian/unheld-evidence.mjs +1 -1
- package/src/runtime/obsidian/view-permission.mjs +335 -0
- package/src/ui/control-system.mjs +201 -0
- package/src/ui/html-primitives.mjs +3 -0
- package/src/upgrade/closure-diagnostics.mjs +135 -0
- package/src/upgrade/closure.mjs +649 -0
- package/src/upgrade/upgrade.mjs +42 -11
- package/templates/distribution-workspace/README.md +2 -2
- package/templates/external-project-workspace/atelier.project.json +3 -0
- package/templates/external-project-workspace/pack-lifecycle.json +3 -1
- package/templates/knowledge-workspace/README.md +2 -2
- package/docs/integration-and-review-plan.md +0 -409
- package/docs/integration-contract-decisions.md +0 -44
- package/docs/knowledge-health/devday/facilitator.md +0 -16
- package/docs/knowledge-health/devday/public-default.md +0 -107
- package/docs/knowledge-health/devday/quickstart.md +0 -18
- package/docs/knowledge-health/devday/support.md +0 -22
- package/docs/knowledge-health/exercise.md +0 -17
- package/docs/knowledge-health/support.md +0 -34
- package/docs/presentation-review-disposition.md +0 -47
- package/docs/progressive-ingestion-acceptance.md +0 -89
- package/docs/progressive-ingestion.md +0 -235
- /package/docs/{exact-upgrades.md → guides/exact-upgrades.md} +0 -0
- /package/docs/{guided-upgrades.md → guides/guided-upgrades.md} +0 -0
- /package/docs/{local-review.md → guides/local-review.md} +0 -0
- /package/docs/{skill-steward.md → guides/skill-steward.md} +0 -0
- /package/docs/{template-upgrades.md → guides/template-upgrades.md} +0 -0
- /package/docs/{assurance-controls.md → maintainers/assurance-controls.md} +0 -0
- /package/docs/{attestation.md → reference/attestation.md} +0 -0
- /package/docs/{coauthor-session.md → reference/coauthor-session.md} +0 -0
- /package/docs/{continuity.md → reference/continuity.md} +0 -0
- /package/docs/{contract-stability.md → reference/contract-stability.md} +0 -0
- /package/docs/{decision-practices.md → reference/decision-practices.md} +0 -0
- /package/docs/{evidence-navigation.md → reference/evidence-navigation.md} +0 -0
- /package/docs/{extension-pack-lifecycle.md → reference/extension-pack-lifecycle.md} +0 -0
- /package/docs/{interaction-and-reflection.md → reference/interaction-and-reflection.md} +0 -0
- /package/docs/{local-state.md → reference/local-state.md} +0 -0
- /package/docs/{ontology.md → reference/ontology.md} +0 -0
- /package/docs/{optional-ingestion.md → reference/optional-ingestion.md} +0 -0
- /package/docs/{project-command-inventory.md → reference/project-command-inventory.md} +0 -0
- /package/docs/{semantic-ingestion.md → reference/semantic-ingestion.md} +0 -0
- /package/docs/{vault-service.md → reference/vault-service.md} +0 -0
|
@@ -0,0 +1,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,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
|
+
}
|