@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
|
@@ -1,3 +1,59 @@
|
|
|
1
|
+
import { performance } from 'node:perf_hooks'
|
|
2
|
+
|
|
3
|
+
// What the caller of cooperative work stopped it with: its `signal`, or its
|
|
4
|
+
// `guard` (the engine's check that the settings which selected a view still
|
|
5
|
+
// hold). Such an error is marked, so whatever catches it on the way out can
|
|
6
|
+
// tell it from a failure of the work itself and hand it on under its own code.
|
|
7
|
+
const SCHEDULING_STOP = Symbol('atelier.cooperative-stop')
|
|
8
|
+
const markedStop = (error) => {
|
|
9
|
+
if (error !== null && typeof error === 'object') { try { Object.defineProperty(error, SCHEDULING_STOP, { value: true }) } catch { /* a frozen error stays unmarked */ } }
|
|
10
|
+
return error
|
|
11
|
+
}
|
|
12
|
+
export const isSchedulingStop = (error) => error !== null && typeof error === 'object' && error[SCHEDULING_STOP] === true
|
|
13
|
+
|
|
14
|
+
// Internal scheduling shared by preparation and publication. The budget is a
|
|
15
|
+
// yield target, not a deadline: one checked unit and native fsync stay atomic.
|
|
16
|
+
export function createCooperativeBudget({ budgetMs = 8, maxUnits = 32, signal, guard = null, onBurst = null } = {}) {
|
|
17
|
+
if (!(Number.isFinite(budgetMs) && budgetMs > 0 && Number.isInteger(maxUnits) && maxUnits > 0)) throw new TypeError('finite positive scheduling budget required')
|
|
18
|
+
let started = performance.now(), units = 0, maxUnitMs = 0, last = started
|
|
19
|
+
const checkSignal = () => {
|
|
20
|
+
if (signal?.aborted) { const error = new Error('cooperative work cancelled at a completed boundary'); error.code = 'ABORT_ERR'; throw markedStop(error) }
|
|
21
|
+
}
|
|
22
|
+
const check = () => {
|
|
23
|
+
checkSignal()
|
|
24
|
+
if (guard) { try { guard() } catch (error) { throw markedStop(error) } }
|
|
25
|
+
}
|
|
26
|
+
const checkpoint = async (phase, force = false) => {
|
|
27
|
+
const now = performance.now()
|
|
28
|
+
maxUnitMs = Math.max(maxUnitMs, now - last); units += 1
|
|
29
|
+
checkSignal()
|
|
30
|
+
if (force || units >= maxUnits || now - started >= budgetMs) {
|
|
31
|
+
check()
|
|
32
|
+
onBurst?.({ phase, elapsedMs: now - started, units, maxUnitMs })
|
|
33
|
+
await new Promise(resolve => setImmediate(resolve))
|
|
34
|
+
check()
|
|
35
|
+
started = performance.now(); units = 0; maxUnitMs = 0
|
|
36
|
+
}
|
|
37
|
+
last = performance.now()
|
|
38
|
+
}
|
|
39
|
+
const finish = phase => { const now = performance.now(); maxUnitMs = Math.max(maxUnitMs, now - last); onBurst?.({ phase, elapsedMs: now - started, units: units + 1, maxUnitMs }) }
|
|
40
|
+
return { checkpoint, check, finish }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function runCooperatively(iterator, options) {
|
|
44
|
+
const budget = createCooperativeBudget(options)
|
|
45
|
+
budget.check()
|
|
46
|
+
try {
|
|
47
|
+
for (;;) {
|
|
48
|
+
const step = iterator.next()
|
|
49
|
+
if (step.done) { budget.finish('prepare-finalize'); return step.value }
|
|
50
|
+
if (step.value === 'final-cache-commit') budget.check()
|
|
51
|
+
else await budget.checkpoint(step.value)
|
|
52
|
+
}
|
|
53
|
+
} catch (error) { iterator.return?.(); throw error }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
1
57
|
import { createHash } from 'node:crypto'
|
|
2
58
|
import { unclosedFenceAtEnd } from '../../../graph/knowledge-graph.mjs'
|
|
3
59
|
import {
|
|
@@ -19,7 +75,7 @@ import { isUserOwnedSettingsPath, prepareSettings } from './settings.mjs'
|
|
|
19
75
|
//
|
|
20
76
|
// A view is laid out in vault layout 2: folders mirror the repository, the
|
|
21
77
|
// file name is the note's title and each note names its identity in generated
|
|
22
|
-
// front-matter properties (see "Vault layout" in docs/obsidian-contract.md).
|
|
78
|
+
// front-matter properties (see "Vault layout" in docs/reference/obsidian-contract.md).
|
|
23
79
|
// Layout 1, the earlier release's, is kept for a view whose prior generation
|
|
24
80
|
// is in layout 1 and holds a note with an open edit, and for recovering a
|
|
25
81
|
// layout 1 generation as it was published.
|
|
@@ -638,14 +694,14 @@ function layoutOf({ layout, priorManifest, layoutHeld }) {
|
|
|
638
694
|
}
|
|
639
695
|
|
|
640
696
|
export function prepareView(options = {}) {
|
|
641
|
-
return
|
|
697
|
+
return drainPreparation(prepareSteps(REDACTION_RULES, options))
|
|
642
698
|
}
|
|
643
699
|
|
|
644
700
|
// Test seam only: the mutation controls of the redaction guard substitute a
|
|
645
701
|
// rule through this. Runtime code uses prepareView.
|
|
646
702
|
export function createViewPreparationForOracleTests(rules = REDACTION_RULES) {
|
|
647
703
|
const guard = { ...REDACTION_RULES, ...rules }
|
|
648
|
-
return (options = {}) =>
|
|
704
|
+
return (options = {}) => drainPreparation(prepareSteps(guard, options))
|
|
649
705
|
}
|
|
650
706
|
|
|
651
707
|
// `heldNotePaths` are the vault files held for an open edit: they stay in the
|
|
@@ -653,7 +709,7 @@ export function createViewPreparationForOracleTests(rules = REDACTION_RULES) {
|
|
|
653
709
|
// notes that hold a layout 1 view in layout 1 (open edits and edits closed on
|
|
654
710
|
// this tick); without it, `heldNotePaths` do. `viewScopeIds`, when given, are
|
|
655
711
|
// the views still maintained: the registry drops the sections of any other.
|
|
656
|
-
function
|
|
712
|
+
function* prepareSteps(guard, { snapshot, profile, scope, persistentPathRegistry = null, priorManifest = null, existingSettings = null, plugin = null, clock, generationId, vaultRootBytes, maxFullPathBytes, cache = null, heldNotePaths = null, layoutHeldNotePaths = null, viewScopeIds = null, layout: requestedLayout } = {}, recheckSources = false) {
|
|
657
713
|
if (cache !== null && !isPreparationCache(cache)) refuse('invalid-preparation-cache', 'the preparation cache must come from createPreparationCache')
|
|
658
714
|
assertObsidianContract('corpus-profile', profile)
|
|
659
715
|
assertObsidianContract('scope', scope)
|
|
@@ -670,6 +726,7 @@ function prepareWithRules(guard, { snapshot, profile, scope, persistentPathRegis
|
|
|
670
726
|
if (viewScopeIds !== null && !(Array.isArray(viewScopeIds) && viewScopeIds.every((id) => typeof id === 'string'))) refuse('invalid-view-scopes', 'viewScopeIds must be an array of scope identities')
|
|
671
727
|
const layout = layoutOf({ layout: requestedLayout, priorManifest, layoutHeld: layoutHeldNotePaths ?? heldNotePaths ?? [] })
|
|
672
728
|
const checkedAt = timestampFrom(clock)
|
|
729
|
+
yield 'prepare-validation'
|
|
673
730
|
const canonical = canonicalSnapshotOf(snapshot.graph)
|
|
674
731
|
|
|
675
732
|
// Selection and visibility come from the contract, never from this module.
|
|
@@ -686,6 +743,7 @@ function prepareWithRules(guard, { snapshot, profile, scope, persistentPathRegis
|
|
|
686
743
|
// Every view's allocation of the registry, this one's included; a registry of another shape or workspace refuses.
|
|
687
744
|
const views = viewsOfRegistry(persistentPathRegistry, profile.workspaceId)
|
|
688
745
|
|
|
746
|
+
yield 'selection-and-universe'
|
|
689
747
|
const edgeById = new Map(canonical.edges.map((edge) => [edge.id, edge]))
|
|
690
748
|
const vaultEdges = selection.vaultEdges.map((id) => edgeById.get(id)).filter((edge) => vault.has(edge.source) && vault.has(edge.target))
|
|
691
749
|
const outsideEdges = selection.outsideSelectionEdges.map((id) => edgeById.get(id)).filter((edge) => vault.has(edge.source) !== vault.has(edge.target))
|
|
@@ -714,6 +772,7 @@ function prepareWithRules(guard, { snapshot, profile, scope, persistentPathRegis
|
|
|
714
772
|
// taken. A view prepared in layout 1 holds layout 1 files, none of which is a
|
|
715
773
|
// layout 2 path. The registry keeps the result as this view's section,
|
|
716
774
|
// whichever layout the view is prepared in.
|
|
775
|
+
yield 'link-and-asset-index'
|
|
717
776
|
const readable = allocateViewPaths({
|
|
718
777
|
published: publishedOf(priorManifest),
|
|
719
778
|
nodes: [...vault].map((id) => nodeById.get(id)),
|
|
@@ -747,6 +806,7 @@ function prepareWithRules(guard, { snapshot, profile, scope, persistentPathRegis
|
|
|
747
806
|
return { key: { edgeKey: occurrence.target }, markdown: target.markdown, wikilink: target.wikilink, alias: true }
|
|
748
807
|
}
|
|
749
808
|
|
|
809
|
+
yield 'path-allocation'
|
|
750
810
|
const read = sourceReader(snapshot)
|
|
751
811
|
const orderedNodes = [...vault].map((id) => nodeById.get(id)).sort((left, right) => compare(left.repo, right.repo) || compare(left.id, right.id))
|
|
752
812
|
// Edges indexed by endpoint once, so a note's rows cost its degree and not
|
|
@@ -778,6 +838,7 @@ function prepareWithRules(guard, { snapshot, profile, scope, persistentPathRegis
|
|
|
778
838
|
const titles = titleRenderings()
|
|
779
839
|
|
|
780
840
|
for (const node of orderedNodes) {
|
|
841
|
+
yield 'emit-note'
|
|
781
842
|
const notePathValue = pathOf(node)
|
|
782
843
|
const attachmentPathValue = node.extension === 'md' ? null : attachmentOf(node)
|
|
783
844
|
const pinned = read.pinned(node.repo, node.path)
|
|
@@ -814,12 +875,14 @@ function prepareWithRules(guard, { snapshot, profile, scope, persistentPathRegis
|
|
|
814
875
|
// One copy per asset, however many notes embed it, read through the pinned
|
|
815
876
|
// snapshot like every other source.
|
|
816
877
|
for (const asset of embeddedAssets.values()) {
|
|
878
|
+
yield 'emit-asset'
|
|
817
879
|
const attachmentPath = assetPaths.get(asset.id)
|
|
818
880
|
const { bytes, rawDigest } = read(asset.repo, asset.path)
|
|
819
881
|
attachments.push({ path: attachmentPath, digest: rawDigest, byteLength: bytes.length, ext: { [EXT_KEY]: { kind: 'embedded-asset', repoId: asset.repo, assetPath: asset.path } } })
|
|
820
882
|
files.push({ path: attachmentPath, kind: 'attachment', bytes, digest: rawDigest })
|
|
821
883
|
}
|
|
822
884
|
|
|
885
|
+
yield 'emit-complete'
|
|
823
886
|
const links = vaultEdges
|
|
824
887
|
.map((edge) => {
|
|
825
888
|
const inversions = edge.type === DERIVED_RELATION_TYPE ? inversionsByEdge.get(`${edge.source}\u0000${edge.target}`) ?? [] : []
|
|
@@ -835,6 +898,7 @@ function prepareWithRules(guard, { snapshot, profile, scope, persistentPathRegis
|
|
|
835
898
|
})
|
|
836
899
|
.sort((left, right) => compare(left.edgeId, right.edgeId))
|
|
837
900
|
|
|
901
|
+
yield 'manifest-links'
|
|
838
902
|
const settings = prepareSettings({ existing: existingSettings, plugin })
|
|
839
903
|
files.push(...settings.files)
|
|
840
904
|
const occupied = new Set()
|
|
@@ -847,6 +911,7 @@ function prepareWithRules(guard, { snapshot, profile, scope, persistentPathRegis
|
|
|
847
911
|
files.sort((left, right) => compare(left.path, right.path))
|
|
848
912
|
attachments.sort((left, right) => compare(left.path, right.path))
|
|
849
913
|
|
|
914
|
+
yield 'settings-and-path-guard'
|
|
850
915
|
const contentDigest = createHash('sha256')
|
|
851
916
|
for (const file of files) contentDigest.update(`${file.path}\u0000${file.digest}\n`)
|
|
852
917
|
const manifest = {
|
|
@@ -877,6 +942,7 @@ function prepareWithRules(guard, { snapshot, profile, scope, persistentPathRegis
|
|
|
877
942
|
// the audience may see but this view does not select, every identifier is
|
|
878
943
|
// reported. An identifier this view's own notes share names nothing outside
|
|
879
944
|
// it.
|
|
945
|
+
yield 'manifest-validation'
|
|
880
946
|
const viewAttachments = new Set(attachments.map((item) => item.path))
|
|
881
947
|
const visible = new Set(universe.nodes)
|
|
882
948
|
const censusRepositories = new Set([...canonical.nodes, ...censusAssets].filter(usable).map((item) => item.repo))
|
|
@@ -927,6 +993,7 @@ function prepareWithRules(guard, { snapshot, profile, scope, persistentPathRegis
|
|
|
927
993
|
const allocatedInView = new Map(orderedNodes.map((node) => [node.id, pathOf(node)]))
|
|
928
994
|
const wrapperAttachments = new Set(orderedNodes.filter((node) => node.extension !== 'md').map((node) => (legacy ? `attachments/${legacyBasename(pathOf(node))}.${/^[a-z0-9]{1,16}$/.test(String(node.extension).toLowerCase()) ? String(node.extension).toLowerCase() : 'bin'}` : attachmentOf(node))))
|
|
929
995
|
const allowedAttachments = new Set([...wrapperAttachments, ...assetPaths.values()])
|
|
996
|
+
yield 'redaction-index'
|
|
930
997
|
const guardReported = assertViewRedaction({
|
|
931
998
|
manifest,
|
|
932
999
|
files,
|
|
@@ -954,6 +1021,7 @@ function prepareWithRules(guard, { snapshot, profile, scope, persistentPathRegis
|
|
|
954
1021
|
})
|
|
955
1022
|
}
|
|
956
1023
|
|
|
1024
|
+
yield 'redaction-validation'
|
|
957
1025
|
const prior = new Map((priorManifest?.notes ?? []).map((note) => [note.path, note.noteDigest]))
|
|
958
1026
|
const changes = { added: [], changed: [], unchanged: [], removed: [] }
|
|
959
1027
|
for (const note of notes) changes[!prior.has(note.path) ? 'added' : prior.get(note.path) === note.noteDigest ? 'unchanged' : 'changed'].push(note.path)
|
|
@@ -962,8 +1030,16 @@ function prepareWithRules(guard, { snapshot, profile, scope, persistentPathRegis
|
|
|
962
1030
|
// under the publication protocol.
|
|
963
1031
|
changes.removed = [...prior.keys()].filter((notePathValue) => !present.has(notePathValue)).sort(compare)
|
|
964
1032
|
|
|
965
|
-
//
|
|
966
|
-
//
|
|
1033
|
+
// A yield lets a source change after it was emitted (or reused). Check the
|
|
1034
|
+
// selected sources against the same pin again, before exposing the result
|
|
1035
|
+
// or replacing the derived cache. This does not create a new snapshot.
|
|
1036
|
+
if (recheckSources) {
|
|
1037
|
+
for (const node of orderedNodes) { yield 'recheck-source'; read(node.repo, node.path) }
|
|
1038
|
+
for (const asset of embeddedAssets.values()) { yield 'recheck-asset'; read(asset.repo, asset.path) }
|
|
1039
|
+
}
|
|
1040
|
+
// The last cancellation boundary precedes the cache commit. There is no
|
|
1041
|
+
// yield between that commit and returning its complete prepared result.
|
|
1042
|
+
yield 'final-cache-commit'
|
|
967
1043
|
if (nextCache) cache.notes = nextCache
|
|
968
1044
|
if (noteDiagnostics.length > 0) manifest.ext[EXT_KEY].diagnostics = noteDiagnostics
|
|
969
1045
|
|
|
@@ -995,3 +1071,9 @@ export function withEligibility(graph, isEligible, isAssetEligible = null) {
|
|
|
995
1071
|
: {}),
|
|
996
1072
|
}
|
|
997
1073
|
}
|
|
1074
|
+
|
|
1075
|
+
function drainPreparation(iterator) { for (;;) { const step = iterator.next(); if (step.done) return step.value } }
|
|
1076
|
+
// A wait lets a source change after it was emitted or reused, so the selected sources are read against their pins once
|
|
1077
|
+
// more before the result is returned. A caller that checks them itself, as the engine does by stat hint, turns that
|
|
1078
|
+
// second reading off with `scheduling.recheckSources: false`.
|
|
1079
|
+
export async function prepareViewCooperatively(options = {}) { return runCooperatively(prepareSteps(REDACTION_RULES, options, options.scheduling?.recheckSources !== false), options.scheduling) }
|
|
@@ -5,7 +5,7 @@ import { refuse } from './byte-lens.mjs'
|
|
|
5
5
|
// judges the assembled result, every note included, whether the note was
|
|
6
6
|
// emitted on this call or reused from the preparation cache, and refuses the
|
|
7
7
|
// view with `redaction-failure`, naming the in-view note (or file) and the
|
|
8
|
-
// rule, never the value. See "The redaction guard" in docs/obsidian-contract.md.
|
|
8
|
+
// rule, never the value. See "The redaction guard" in docs/reference/obsidian-contract.md.
|
|
9
9
|
//
|
|
10
10
|
// allow-list every path the emitter wrote (note and attachment paths,
|
|
11
11
|
// rewritten link and embed targets, link targets in generated
|
|
@@ -5,8 +5,9 @@ import { Worker, isMainThread, parentPort, workerData } from 'node:worker_thread
|
|
|
5
5
|
import { PersonalWorkspaceRefusal, composePersonalWorkspace } from '@mnstry/atelier/personal-workspace'
|
|
6
6
|
import { ensureLocalState, resolveProjectConfig, validateProjectConfigDoc } from '@mnstry/atelier/project'
|
|
7
7
|
import { buildCanonicalGraph } from '../../graph/graph.mjs'
|
|
8
|
-
import { refuse } from '../../runtime/obsidian/errors.mjs'
|
|
8
|
+
import { ObsidianMaintenanceRefusal, refuse } from '../../runtime/obsidian/errors.mjs'
|
|
9
9
|
import { OBSIDIAN_EXT_KEY } from './contracts.mjs'
|
|
10
|
+
import { readPersonalSelectionSnapshot } from '../../personal-workspace/selection.mjs'
|
|
10
11
|
|
|
11
12
|
// The one personal-workspace route of the Obsidian projection.
|
|
12
13
|
//
|
|
@@ -60,6 +61,9 @@ const BINDING = Symbol('atelier.obsidian.personal-workspace-binding')
|
|
|
60
61
|
const ROUTES = new WeakMap()
|
|
61
62
|
// The last validity key the composition confirmed for each binding: { inputs, facts }.
|
|
62
63
|
const CONFIRMED = new WeakMap()
|
|
64
|
+
// The selection record a binding was loaded under, when its loader followed the person's confirmed selection instead of
|
|
65
|
+
// naming a generation itself (runtime/obsidian/personal-selection.mjs): { sequence, head, observe, recheck }.
|
|
66
|
+
const SELECTED = new WeakMap()
|
|
63
67
|
export const EVERYTHING_SCOPE_ID = 'everything'
|
|
64
68
|
const EXT_SCHEMA = 'atelier-obsidian-ext-settings/v1'
|
|
65
69
|
// The settings member of a bound project names its generation here. It survives any copy of the project (a structured
|
|
@@ -389,6 +393,12 @@ export async function validatePersonalWorkspace(project) {
|
|
|
389
393
|
if (binding === null) return null
|
|
390
394
|
const route = ROUTES.get(binding)
|
|
391
395
|
if (route === undefined) return { ok: false, code: 'personal-binding-unrecognized' }
|
|
396
|
+
// A project pinned to a confirmed selection holds only while the history still ends, intact, with that selection's
|
|
397
|
+
// record: what observation of the record cannot see (a record before it broken, a confirmation that landed before
|
|
398
|
+
// the engine first looked) is seen here.
|
|
399
|
+
if (SELECTED.get(binding)?.recheck) {
|
|
400
|
+
try { assertPersonalSelection(project) } catch (error) { if (!(error instanceof ObsidianMaintenanceRefusal)) throw error; CONFIRMED.delete(binding); return { ok: false, code: error.code } }
|
|
401
|
+
}
|
|
392
402
|
let composed
|
|
393
403
|
if (route.offThread) composed = await composeInWorker(binding, route.worker)
|
|
394
404
|
else {
|
|
@@ -400,12 +410,79 @@ export async function validatePersonalWorkspace(project) {
|
|
|
400
410
|
return { ok: true, changed: previous?.inputs !== composed.key.inputs || previous?.facts !== composed.key.facts }
|
|
401
411
|
}
|
|
402
412
|
|
|
413
|
+
const selectionRecord = ({ personalHome }, sequence) => path.join(personalHome, 'selections', `${String(sequence).padStart(6, '0')}.json`)
|
|
414
|
+
|
|
415
|
+
// The most recently accepted head observed in this process. Durable continuity is checked by the loader's note
|
|
416
|
+
// callback before this index changes. The index only tells an engine when another reader requires it to reload.
|
|
417
|
+
const READ_UP_TO = new Map()
|
|
418
|
+
|
|
419
|
+
export function notePersonalSelectionHead(personalHome, head, { refusesShortened = true, note = null } = {}) {
|
|
420
|
+
if (note !== null) note(head)
|
|
421
|
+
else {
|
|
422
|
+
const before = READ_UP_TO.get(personalHome)?.sequence ?? 0
|
|
423
|
+
if (refusesShortened && head.sequence < before) refuse('selection-history-shortened', 'the selection history of the personal workspace ends at an earlier record than this process has read; nothing is used from it', { source: 'personal-workspace' })
|
|
424
|
+
}
|
|
425
|
+
if (refusesShortened) READ_UP_TO.set(personalHome, { sequence: head.sequence, head: head.head })
|
|
426
|
+
return head
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Pins a bound project to the selection record it was loaded under, and answers the project. From then on the engine
|
|
430
|
+
// observes that record and the place of the next one as configuration (personalWorkspaceInputs): a confirmation
|
|
431
|
+
// appended to the history, or a history cut short, is a change. And every time the composition is asked again
|
|
432
|
+
// (validatePersonalWorkspace) the history is read again and must still end with that record. `observe: false`,
|
|
433
|
+
// `recheck: false` and `refusesShortened: false` are test seams only, the mutation controls of each.
|
|
434
|
+
export function pinPersonalSelection(project, { sequence, head }, { observe = true, recheck = true, refusesShortened = true, note = null } = {}) {
|
|
435
|
+
SELECTED.set(personalWorkspaceBindingOf(project), Object.freeze({ sequence, head, observe, recheck, refusesShortened, note }))
|
|
436
|
+
return project
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// The selection a project is pinned to, as { personalHome, generationId, sequence, head }; null for any other project.
|
|
440
|
+
export function personalSelectionOf(project) {
|
|
441
|
+
const binding = personalWorkspaceBindingOf(project)
|
|
442
|
+
const pinned = binding === null ? undefined : SELECTED.get(binding)
|
|
443
|
+
return pinned === undefined ? null : { personalHome: binding.personalHome, generationId: binding.generationId, sequence: pinned.sequence, head: pinned.head }
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Refuses `personal-selection-changed` unless the history still ends with the record this project is pinned to; a
|
|
447
|
+
// history that cannot be read refuses with the module's own code. What is read is noted first
|
|
448
|
+
// (notePersonalSelectionHead): the same durable continuity rule applies to this read, before its head is compared
|
|
449
|
+
// with the pin. Nothing for a project that is not pinned.
|
|
450
|
+
export function assertPersonalSelection(project) {
|
|
451
|
+
const pinned = personalSelectionOf(project)
|
|
452
|
+
if (pinned === null) return
|
|
453
|
+
let now
|
|
454
|
+
try { now = readPersonalSelectionSnapshot({ personalHome: pinned.personalHome }) } catch (error) { asMaintenanceRefusal(error, 'the selection history of the personal workspace could not be read; nothing is used from it') }
|
|
455
|
+
notePersonalSelectionHead(pinned.personalHome, now, SELECTED.get(personalWorkspaceBindingOf(project)))
|
|
456
|
+
if (now.sequence !== pinned.sequence || now.head !== pinned.head) refuse('personal-selection-changed', 'the confirmed selection of the personal workspace is no longer the one this project was loaded under; nothing is used from it', { source: 'personal-workspace' })
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Whether another reader in this process accepted a head different from the pin: a later confirmation or a newly
|
|
460
|
+
// confirmed shorter history. In memory, with no read. False for a project that is not pinned.
|
|
461
|
+
export function personalSelectionBehind(project) {
|
|
462
|
+
const pinned = personalSelectionOf(project)
|
|
463
|
+
const latest = pinned === null ? null : READ_UP_TO.get(pinned.personalHome)
|
|
464
|
+
return latest !== undefined && latest !== null && (latest.sequence !== pinned.sequence || latest.head !== pinned.head)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Reads where the history of a pinned project's home ends now, for the note alone: what the read refuses is not this
|
|
468
|
+
// caller's to raise. For the engine's guard, which sees a selection record's file change in the middle of a tick and
|
|
469
|
+
// stops the view as `mixed-read` without reading the history: the record that stopped it is read while it is there, so
|
|
470
|
+
// it counts when it is removed before the next tick. Nothing for a project that is not pinned.
|
|
471
|
+
export function notePersonalSelectionNow(project) {
|
|
472
|
+
try { assertPersonalSelection(project) } catch (error) { if (!(error instanceof ObsidianMaintenanceRefusal)) throw error }
|
|
473
|
+
}
|
|
474
|
+
|
|
403
475
|
// The authored and generation files whose change means the binding must be asked again: the manifest, the overlay and
|
|
404
|
-
// the bound generation's record
|
|
476
|
+
// the bound generation's record; and, for a project pinned to a selection, that selection's record and the place of the
|
|
477
|
+
// next one. Observed as configuration by the engine. Empty for any other project.
|
|
405
478
|
export function personalWorkspaceInputs(project) {
|
|
406
479
|
const binding = personalWorkspaceBindingOf(project)
|
|
407
480
|
if (binding === null) return []
|
|
408
|
-
|
|
481
|
+
const pinned = SELECTED.get(binding)
|
|
482
|
+
return [
|
|
483
|
+
path.join(binding.personalHome, 'atelier.personal.json'), path.join(binding.personalHome, 'atelier.overlay.json'), path.join(generationRoot(binding), 'generation.json'),
|
|
484
|
+
...(pinned?.observe ? [selectionRecord(binding, pinned.sequence), selectionRecord(binding, pinned.sequence + 1)] : []),
|
|
485
|
+
]
|
|
409
486
|
}
|
|
410
487
|
|
|
411
488
|
// The repository of a bound project's private notes, or null. A vault edit to one of them is never applied into a
|
|
@@ -415,10 +492,12 @@ export function personalOverlayRepoOf(project) {
|
|
|
415
492
|
}
|
|
416
493
|
|
|
417
494
|
// Settings that a personal binding takes from the overlay or does not have yet. A saved view is authored in the
|
|
418
|
-
// overlay, so `view add` would write a module-owned generation file
|
|
419
|
-
//
|
|
495
|
+
// overlay, so `view add` would write a module-owned generation file. The machine records an owner-only audience or
|
|
496
|
+
// an explicit clear. Custom audiences refuse; vault location remains unavailable for this binding.
|
|
420
497
|
export function refuseOnPersonalBinding(project, operation) {
|
|
421
498
|
if (personalWorkspaceBindingOf(project) === null) return
|
|
499
|
+
if (operation === 'audience set me' || operation === 'audience clear') return
|
|
500
|
+
if (operation.startsWith('audience ')) refuse('personal-binding-owner-only', 'a personal workspace vault is owner only; use `audience set me` or `audience clear`', { operation })
|
|
422
501
|
if (operation === 'view add') refuse('views-from-personal-overlay', 'the views of a personal workspace come from its overlay; nothing was written', { operation })
|
|
423
502
|
refuse('personal-binding-decision-unavailable', `${operation} is not available for a personal workspace yet; nothing was written`, { operation })
|
|
424
503
|
}
|
|
@@ -38,7 +38,7 @@ import { createHmac } from 'node:crypto'
|
|
|
38
38
|
// and the exact bytes of the answer. A handshake is used once and lapses after
|
|
39
39
|
// five seconds, a session after fifteen minutes. Nothing here takes a path to
|
|
40
40
|
// act on, a command or code, and every command maps to state the service
|
|
41
|
-
// already keeps (see docs/obsidian-plugin.md).
|
|
41
|
+
// already keeps (see docs/reference/obsidian-plugin.md).
|
|
42
42
|
//
|
|
43
43
|
// The plugin cannot import this module: it runs inside the app, from the
|
|
44
44
|
// vault. It carries the same constants and the same computations, and the test
|
|
@@ -15,6 +15,7 @@ import { isIdentifier } from '../edits/object-identity.mjs'
|
|
|
15
15
|
import { openObjectStore } from '../edits/object-store.mjs'
|
|
16
16
|
import { decideApply } from '../edits/policy.mjs'
|
|
17
17
|
import { applyEditLens } from '../edits/regions.mjs'
|
|
18
|
+
import { createCooperativeBudget } from '../materialize/prepare-view.mjs'
|
|
18
19
|
import { PERSONAL_VALIDATION_PENDING, personalOverlayRepoOf } from '../personal-workspace.mjs'
|
|
19
20
|
import { PublicationRefusal } from '../recovery/store.mjs'
|
|
20
21
|
import {
|
|
@@ -188,6 +189,9 @@ export function createProposalAdapterForOracleTests(primitives = PROPOSAL_ADAPTE
|
|
|
188
189
|
// Set when an observation found the personal workspace's graph pending: the edits it could not look at are offered
|
|
189
190
|
// again on the next observation, not parked until a full reconciliation.
|
|
190
191
|
let observePendingAgain = false
|
|
192
|
+
// The observation under way, if any. An observation waits between two edits, and what it reads of this adapter
|
|
193
|
+
// (the flag above, the workspace it opens) is read when it begins: so the next one begins when this one has ended.
|
|
194
|
+
let observing = Promise.resolve()
|
|
191
195
|
|
|
192
196
|
// -- the workspace of one call -------------------------------------------
|
|
193
197
|
|
|
@@ -431,10 +435,13 @@ export function createProposalAdapterForOracleTests(primitives = PROPOSAL_ADAPTE
|
|
|
431
435
|
|
|
432
436
|
// One repository: its lock, then each of its operations in turn. What goes wrong with one repository, or with one
|
|
433
437
|
// operation, is reported by its code and stops nothing else.
|
|
434
|
-
|
|
438
|
+
// `budget`, when a tick hands one over, gives the event loop a turn between two operations: each operation is
|
|
439
|
+
// settled without interruption, and the lock of the repository is held from the first to the last.
|
|
440
|
+
async function settleRepository(workspace, repoId, works, report, budget = null) {
|
|
435
441
|
try {
|
|
436
|
-
const held = await withRepositoryLock(workspace, repoId, () => {
|
|
437
|
-
for (const work of works) {
|
|
442
|
+
const held = await withRepositoryLock(workspace, repoId, async () => {
|
|
443
|
+
for (const [index, work] of works.entries()) {
|
|
444
|
+
if (budget !== null && index > 0) await budget.checkpoint('settle-operation', true)
|
|
438
445
|
try { report.outcomes.push(settleOne(workspace, work.item, work)) } catch (error) {
|
|
439
446
|
if (error?.crashSeam === true || !isTyped(error)) throw error
|
|
440
447
|
report.outcomes.push({ status: 'failed', code: error.code, editId: work.item.editId, repoId, nodeId: work.item.nodeId, adapterOperationId: work.item.adapterOperationId, state: null, proposalId: null })
|
|
@@ -448,6 +455,26 @@ export function createProposalAdapterForOracleTests(primitives = PROPOSAL_ADAPTE
|
|
|
448
455
|
}
|
|
449
456
|
}
|
|
450
457
|
|
|
458
|
+
// One observation, from opening the workspace to the flag it leaves for the next.
|
|
459
|
+
async function observeNow(context, { retryRefused = false } = {}) {
|
|
460
|
+
const workspace = open(context)
|
|
461
|
+
const edits = Array.isArray(context.edits) ? context.edits : []
|
|
462
|
+
// A private note of a personal workspace lives in a generation the personal-workspace module owns: its edit is
|
|
463
|
+
// not observed, recorded or proposed, and stays held in the vault. Answered by name, with nothing written.
|
|
464
|
+
const overlayRepoId = personalOverlayRepoOf(workspace.project)
|
|
465
|
+
const isPrivate = (edit) => overlayRepoId !== null && edit?.identity?.repoId === overlayRepoId
|
|
466
|
+
const privateNotes = edits.filter((edit) => isPrivate(edit) && edit.closedAt === null)
|
|
467
|
+
.map((edit) => ({ editId: edit.editId, repoId: edit.identity.repoId, nodeId: edit.identity.nodeId, status: 'refused', code: 'personal-overlay-not-proposed' }))
|
|
468
|
+
const shared = edits.filter((edit) => !isPrivate(edit))
|
|
469
|
+
// The graph is built only when an edit is looked at, as for any project. When the personal workspace's graph is
|
|
470
|
+
// pending, the edits it reached are not reported or recorded, and are offered again on the next observation.
|
|
471
|
+
const retry = retryRefused || observePendingAgain
|
|
472
|
+
const looked = await observation.run.cooperatively(workspace, { edits: shared, retryRefused: retry })
|
|
473
|
+
const pending = looked.filter((item) => item.code === PERSONAL_VALIDATION_PENDING)
|
|
474
|
+
observePendingAgain = pending.length > 0
|
|
475
|
+
return { adapterId: PROPOSAL_ADAPTER_ID, observed: [...privateNotes, ...looked.filter((item) => item.code !== PERSONAL_VALIDATION_PENDING)], ...(pending.length > 0 ? { deferred: PERSONAL_VALIDATION_PENDING } : {}) }
|
|
476
|
+
}
|
|
477
|
+
|
|
451
478
|
return {
|
|
452
479
|
id: PROPOSAL_ADAPTER_ID,
|
|
453
480
|
|
|
@@ -455,23 +482,10 @@ export function createProposalAdapterForOracleTests(primitives = PROPOSAL_ADAPTE
|
|
|
455
482
|
// observed from its preserved bytes against the source as it is now and recorded as what it is, a bounded number
|
|
456
483
|
// per tick and never twice. `retryRefused` offers once more the edits whose observation refused before anything
|
|
457
484
|
// could be recorded. Writes no source and no vault. Returns codes and identifiers only.
|
|
458
|
-
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
// not observed, recorded or proposed, and stays held in the vault. Answered by name, with nothing written.
|
|
463
|
-
const overlayRepoId = personalOverlayRepoOf(workspace.project)
|
|
464
|
-
const isPrivate = (edit) => overlayRepoId !== null && edit?.identity?.repoId === overlayRepoId
|
|
465
|
-
const privateNotes = edits.filter((edit) => isPrivate(edit) && edit.closedAt === null)
|
|
466
|
-
.map((edit) => ({ editId: edit.editId, repoId: edit.identity.repoId, nodeId: edit.identity.nodeId, status: 'refused', code: 'personal-overlay-not-proposed' }))
|
|
467
|
-
const shared = edits.filter((edit) => !isPrivate(edit))
|
|
468
|
-
// The graph is built only when an edit is looked at, as for any project. When the personal workspace's graph is
|
|
469
|
-
// pending, the edits it reached are not reported or recorded, and are offered again on the next observation.
|
|
470
|
-
const retry = retryRefused || observePendingAgain
|
|
471
|
-
const looked = observation.run(workspace, { edits: shared, retryRefused: retry })
|
|
472
|
-
const pending = looked.filter((item) => item.code === PERSONAL_VALIDATION_PENDING)
|
|
473
|
-
observePendingAgain = pending.length > 0
|
|
474
|
-
return { adapterId: PROPOSAL_ADAPTER_ID, observed: [...privateNotes, ...looked.filter((item) => item.code !== PERSONAL_VALIDATION_PENDING)], ...(pending.length > 0 ? { deferred: PERSONAL_VALIDATION_PENDING } : {}) }
|
|
485
|
+
observe(context, options = {}) {
|
|
486
|
+
const run = observing.then(() => observeNow(context, options))
|
|
487
|
+
observing = run.catch(() => {})
|
|
488
|
+
return run
|
|
475
489
|
},
|
|
476
490
|
|
|
477
491
|
// The tick. `context` is { project, workspaceRoot, workspaceId, repositoryRoots, edits }: the pending edit
|
|
@@ -481,6 +495,9 @@ export function createProposalAdapterForOracleTests(primitives = PROPOSAL_ADAPTE
|
|
|
481
495
|
const nowMs = Date.parse(isoTime(clock))
|
|
482
496
|
const report = { adapterId: PROPOSAL_ADAPTER_ID, examined: 0, outcomes: [], repositories: [] }
|
|
483
497
|
const edits = Array.isArray(context.edits) ? context.edits : []
|
|
498
|
+
// The tick goes on answering while it routes: a turn of the event loop when describing has taken its share
|
|
499
|
+
// of time, and one before each repository and between two of its operations.
|
|
500
|
+
const budget = createCooperativeBudget()
|
|
484
501
|
const { operations: heads, unreadable } = workspace.queue().heads()
|
|
485
502
|
for (const item of unreadable) report.repositories.push({ repoId: null, code: item.code })
|
|
486
503
|
const byEdit = new Map(edits.map((edit) => [edit.editId, edit]))
|
|
@@ -497,7 +514,8 @@ export function createProposalAdapterForOracleTests(primitives = PROPOSAL_ADAPTE
|
|
|
497
514
|
const editState = edit === null || edit.closedAt === null ? 'open' : ['withdrawn', 'superseded'].includes(edit.state) ? edit.state : 'closed'
|
|
498
515
|
let item = itemOfHead(head, edit)
|
|
499
516
|
if (edit !== null && head.state !== 'submitted') {
|
|
500
|
-
try { item = { ...item, ...(describe(workspace, edit) ?? {}) } } catch (error) { if (!isTyped(error)) throw error; report.outcomes.push({ status: 'failed', code: error.code, editId: head.editId, repoId: head.repoId, nodeId: head.nodeId, adapterOperationId: head.adapterOperationId, state: head.state, proposalId: null }); continue }
|
|
517
|
+
try { item = { ...item, ...(describe(workspace, edit) ?? {}) } } catch (error) { if (!isTyped(error)) throw error; report.outcomes.push({ status: 'failed', code: error.code, editId: head.editId, repoId: head.repoId, nodeId: head.nodeId, adapterOperationId: head.adapterOperationId, state: head.state, proposalId: null }); await budget.checkpoint('describe-operation'); continue }
|
|
518
|
+
await budget.checkpoint('describe-operation')
|
|
501
519
|
}
|
|
502
520
|
add(head.repoId, { item, editState })
|
|
503
521
|
}
|
|
@@ -510,11 +528,16 @@ export function createProposalAdapterForOracleTests(primitives = PROPOSAL_ADAPTE
|
|
|
510
528
|
for (const edit of looked) {
|
|
511
529
|
report.examined += 1
|
|
512
530
|
let item
|
|
513
|
-
try { item = describe(workspace, edit) } catch (error) { if (!isTyped(error)) throw error; report.outcomes.push({ status: 'failed', code: error.code, editId: edit.editId, repoId: edit.identity.repoId, nodeId: edit.identity.nodeId, adapterOperationId: null, state: null, proposalId: null }); continue }
|
|
531
|
+
try { item = describe(workspace, edit) } catch (error) { if (!isTyped(error)) throw error; report.outcomes.push({ status: 'failed', code: error.code, editId: edit.editId, repoId: edit.identity.repoId, nodeId: edit.identity.nodeId, adapterOperationId: null, state: null, proposalId: null }); await budget.checkpoint('describe-edit'); continue }
|
|
514
532
|
if (item !== null) add(item.repoId, { item, editState: 'open' })
|
|
533
|
+
await budget.checkpoint('describe-edit')
|
|
515
534
|
}
|
|
516
535
|
|
|
517
|
-
for (const [repoId, list] of [...works].sort(([left], [right]) => compareText(left, right)))
|
|
536
|
+
for (const [repoId, list] of [...works].sort(([left], [right]) => compareText(left, right))) {
|
|
537
|
+
await budget.checkpoint('settle-repository', true)
|
|
538
|
+
await settleRepository(workspace, repoId, list, report, budget)
|
|
539
|
+
}
|
|
540
|
+
budget.finish('propose-finalize')
|
|
518
541
|
return report
|
|
519
542
|
},
|
|
520
543
|
|
|
@@ -10,6 +10,7 @@ import { SOURCE_APPLY_PRIMITIVES, SourceApplyRefusal, locateSource } from '../ed
|
|
|
10
10
|
import { EditArbitrationRefusal } from '../edits/arbitrate.mjs'
|
|
11
11
|
import { createEditObserverForOracleTests, editIdempotencyKey, observeEdit } from '../edits/observe.mjs'
|
|
12
12
|
import { decideApply } from '../edits/policy.mjs'
|
|
13
|
+
import { createCooperativeBudget } from '../materialize/prepare-view.mjs'
|
|
13
14
|
import { PublicationRefusal, readFileBytes, sha256Digest } from '../recovery/store.mjs'
|
|
14
15
|
import { PROPOSAL_BACKPRESSURE } from './backpressure.mjs'
|
|
15
16
|
|
|
@@ -38,6 +39,14 @@ import { PROPOSAL_BACKPRESSURE } from './backpressure.mjs'
|
|
|
38
39
|
// reconciliation cadence, never on every tick. A bounded number of edits is
|
|
39
40
|
// looked at per tick, in the order they were observed, starting where the last
|
|
40
41
|
// tick stopped.
|
|
42
|
+
//
|
|
43
|
+
// One edit is one unit: it is read, judged and recorded without interruption.
|
|
44
|
+
// Between two edits the cooperative form (`cooperatively`, which the adapter
|
|
45
|
+
// uses on a tick) gives the event loop a turn, so the process that runs the
|
|
46
|
+
// tick goes on answering while a burst of edits is observed. A second
|
|
47
|
+
// cooperative observation waits for the first to finish; what is looked at,
|
|
48
|
+
// and in what order, is the same in both forms. The uninterrupted form
|
|
49
|
+
// cannot wait, so it refuses to run beside a cooperative one.
|
|
41
50
|
|
|
42
51
|
const EXT = OBSIDIAN_EXT_KEY
|
|
43
52
|
const GONE = new Set(['ENOENT', 'ENOTDIR', 'ELOOP'])
|
|
@@ -203,7 +212,8 @@ export function createTickObservation(options = {}) {
|
|
|
203
212
|
// `workspace` is the opened workspace of the adapter; `edits` the pending edit records, open and closed;
|
|
204
213
|
// `retryRefused` offers the edits whose observation refused earlier once more. Returns one entry per edit looked
|
|
205
214
|
// at: { editId, repoId, nodeId, status: 'observed' | 'refused', code, kind?, operationState?, idempotencyKey? }.
|
|
206
|
-
|
|
215
|
+
// Yields between two edits, never within one, and returns the entries when it is done.
|
|
216
|
+
function* observeSteps(workspace, { edits, retryRefused = false }) {
|
|
207
217
|
if (retryRefused) unobservable.clear()
|
|
208
218
|
const now = isoTime(workspace.clock)
|
|
209
219
|
const candidates = edits.filter((edit) => edit.closedAt === null && OPEN_EDIT_STATES.includes(edit.state) && !recorded.has(edit.editId) && !unobservable.has(edit.editId))
|
|
@@ -212,7 +222,8 @@ export function createTickObservation(options = {}) {
|
|
|
212
222
|
const looked = [...candidates.slice(start), ...candidates.slice(0, start)].slice(0, bounds.maxObservedPerTick)
|
|
213
223
|
cursor = candidates.length > bounds.maxObservedPerTick ? start + looked.length : 0
|
|
214
224
|
const observed = []
|
|
215
|
-
for (const edit of looked) {
|
|
225
|
+
for (const [index, edit] of looked.entries()) {
|
|
226
|
+
if (index > 0) yield 'observe-edit'
|
|
216
227
|
const entry = { editId: edit.editId, repoId: edit.identity.repoId, nodeId: edit.identity.nodeId }
|
|
217
228
|
try {
|
|
218
229
|
const outcome = observeOne(workspace, edit, now)
|
|
@@ -226,4 +237,31 @@ export function createTickObservation(options = {}) {
|
|
|
226
237
|
}
|
|
227
238
|
return observed
|
|
228
239
|
}
|
|
240
|
+
|
|
241
|
+
// Cooperative observations asked for and not yet finished, and the last of them.
|
|
242
|
+
let underWay = 0
|
|
243
|
+
let last = Promise.resolve()
|
|
244
|
+
function observePendingEdits(workspace, input) {
|
|
245
|
+
if (underWay > 0) throw Object.assign(new Error('a cooperative observation of this instance is under way; the uninterrupted form cannot run beside it'), { code: 'observation-under-way' })
|
|
246
|
+
const steps = observeSteps(workspace, input)
|
|
247
|
+
for (;;) { const step = steps.next(); if (step.done) return step.value }
|
|
248
|
+
}
|
|
249
|
+
// The same observation with a turn of the event loop between two edits. One at a time: an observation asked for
|
|
250
|
+
// while another is under way starts when that one has finished, so no edit is looked at by both. One that failed
|
|
251
|
+
// holds back none that follow.
|
|
252
|
+
observePendingEdits.cooperatively = (workspace, input) => {
|
|
253
|
+
underWay += 1
|
|
254
|
+
const run = last.then(async () => {
|
|
255
|
+
const budget = createCooperativeBudget()
|
|
256
|
+
const steps = observeSteps(workspace, input)
|
|
257
|
+
for (;;) {
|
|
258
|
+
const step = steps.next()
|
|
259
|
+
if (step.done) { budget.finish('observe-finalize'); return step.value }
|
|
260
|
+
await budget.checkpoint(step.value, true)
|
|
261
|
+
}
|
|
262
|
+
}).finally(() => { underWay -= 1 })
|
|
263
|
+
last = run.catch(() => {})
|
|
264
|
+
return run
|
|
265
|
+
}
|
|
266
|
+
return observePendingEdits
|
|
229
267
|
}
|