@mnstry/atelier 0.2.0-alpha.14 → 0.2.0-alpha.16
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 +54 -0
- package/README.md +80 -47
- 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/claims.md +2 -2
- package/docs/blocks/conformance-admission.md +1 -1
- package/docs/{atelier-sync.md → guides/atelier-sync.md} +20 -14
- 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 +76 -0
- package/docs/{ingestion.md → guides/ingestion.md} +13 -12
- package/docs/{knowledge-workspace.md → guides/knowledge-workspace.md} +2 -2
- package/docs/{learning.md → guides/learning.md} +12 -5
- package/docs/{obsidian.md → guides/obsidian.md} +134 -47
- package/docs/guides/ontology-guide.md +111 -0
- package/docs/{personal-workspace.md → guides/personal-workspace.md} +7 -4
- 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} +40 -9
- 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 -5
- 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} +1 -1
- package/docs/{obsidian-contract.md → reference/obsidian-contract.md} +6 -4
- package/docs/{obsidian-plugin.md → reference/obsidian-plugin.md} +5 -66
- 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} +27 -4
- package/docs/{review-portability.md → reference/review-portability.md} +4 -0
- 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} +6 -6
- 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 +21 -8
- 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 +5 -4
- package/src/boundary/policy.mjs +60 -12
- package/src/cli/run.mjs +24 -8
- 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 +28 -16
- 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/knowledge-graph.mjs +1 -1
- 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/dom.mjs +23 -0
- package/src/graph-workspace/browser/index.mjs +6 -0
- package/src/graph-workspace/browser/map.mjs +374 -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 +433 -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 +100 -0
- package/src/graph-workspace/scale.mjs +98 -0
- package/src/graph-workspace/theme.mjs +156 -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 +7 -7
- package/src/knowledge-health/workshop.mjs +22 -2
- package/src/personal-workspace/selection.mjs +17 -2
- package/src/project/config.mjs +1 -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 +1 -1
- package/src/projection/obsidian/materialize/redaction.mjs +1 -1
- package/src/projection/obsidian/personal-workspace.mjs +24 -28
- package/src/projection/obsidian/plugin-bridge/channel.mjs +1 -1
- package/src/projection/obsidian/publication/publisher.mjs +25 -4
- package/src/projection/project.mjs +18 -14
- 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 +99 -26
- package/src/runtime/obsidian/lifecycle.mjs +3 -3
- package/src/runtime/obsidian/login-item.mjs +2 -2
- package/src/runtime/obsidian/machine-settings.mjs +52 -8
- package/src/runtime/obsidian/opening.mjs +2 -2
- package/src/runtime/obsidian/personal-selection.mjs +88 -26
- package/src/runtime/obsidian/pipeline.mjs +75 -7
- package/src/runtime/obsidian/private-lock.mjs +11 -2
- package/src/runtime/obsidian/selection-continuity.mjs +124 -0
- package/src/runtime/obsidian/service-main.mjs +15 -9
- package/src/runtime/obsidian/service.mjs +14 -0
- package/src/runtime/obsidian/unheld-evidence.mjs +1 -1
- package/src/runtime/obsidian/view-permission.mjs +2 -1
- package/src/upgrade/upgrade.mjs +23 -4
- 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/{capability-stewardship.md → guides/capability-stewardship.md} +0 -0
- /package/docs/{exact-upgrades.md → guides/exact-upgrades.md} +0 -0
- /package/docs/{guided-upgrades.md → guides/guided-upgrades.md} +0 -0
- /package/docs/{inquiry-harnesses.md → guides/inquiry-harnesses.md} +0 -0
- /package/docs/{learning-harnesses.md → guides/learning-harnesses.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/{semantic-operation.md → reference/semantic-operation.md} +0 -0
- /package/docs/{vault-service.md → reference/vault-service.md} +0 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import crypto from 'node:crypto'
|
|
2
2
|
import fs from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
|
+
import formats from 'ajv-formats'
|
|
5
|
+
import { canonicalize } from '../attestation/jcs.mjs'
|
|
4
6
|
import { createProposalStore } from '../collaboration/proposals.mjs'
|
|
5
7
|
import { readJson, writeJson } from '../project/config.mjs'
|
|
6
8
|
import { bundledReadinessPack, bundledReadinessProtocols, protocolById } from './bundled-pack.mjs'
|
|
@@ -126,7 +128,40 @@ export function claimsForProtocolRun({ protocol, answers, projectName, runId, cr
|
|
|
126
128
|
})
|
|
127
129
|
}
|
|
128
130
|
|
|
131
|
+
// createdAt is the existing run contract's date-time, rather than a filename or filesystem timestamp. Use the same
|
|
132
|
+
// full-format validator as the schema and refuse values the host cannot place on a timeline (including leap seconds).
|
|
133
|
+
const validCreatedAt = formats.get('date-time').validate
|
|
134
|
+
function runInstant(createdAt) {
|
|
135
|
+
if (typeof createdAt !== 'string' || !validCreatedAt(createdAt)) throw new Error('invalid-readiness-run-created-at')
|
|
136
|
+
// Compare the whole second separately so fractions finer than a millisecond do not collapse into a false tie.
|
|
137
|
+
const fraction = /\.(\d+)/.exec(createdAt)?.[1] ?? ''
|
|
138
|
+
const wholeSecond = Date.parse(createdAt.replace(/\.\d+/, ''))
|
|
139
|
+
if (!Number.isFinite(wholeSecond)) throw new Error('invalid-readiness-run-created-at')
|
|
140
|
+
return { wholeSecond, fraction }
|
|
141
|
+
}
|
|
142
|
+
const compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0
|
|
143
|
+
function orderedProtocolRuns(runs) {
|
|
144
|
+
const seen = new Map()
|
|
145
|
+
return runs.map((run) => {
|
|
146
|
+
const instant = runInstant(run?.createdAt)
|
|
147
|
+
if (typeof run?.runId !== 'string' || !/^readiness-run-[a-f0-9]{24}$/.test(run.runId)) throw new Error('invalid-readiness-run-id')
|
|
148
|
+
const content = canonicalize(run)
|
|
149
|
+
if (seen.has(run.runId) && seen.get(run.runId) !== content) throw new Error('conflicting-readiness-run-id')
|
|
150
|
+
seen.set(run.runId, content)
|
|
151
|
+
return { run, ...instant }
|
|
152
|
+
}).sort((left, right) => {
|
|
153
|
+
const precision = Math.max(left.fraction.length, right.fraction.length)
|
|
154
|
+
return left.wholeSecond - right.wholeSecond
|
|
155
|
+
|| compareText(left.fraction.padEnd(precision, '0'), right.fraction.padEnd(precision, '0'))
|
|
156
|
+
|| compareText(left.run.runId, right.run.runId)
|
|
157
|
+
}).map(({ run }) => run)
|
|
158
|
+
}
|
|
159
|
+
function latestRunsByProtocol(runs) {
|
|
160
|
+
return new Map(orderedProtocolRuns(runs).map((run) => [run.protocolId, run]))
|
|
161
|
+
}
|
|
162
|
+
|
|
129
163
|
export function buildReadinessRun({ project, protocol, answers = {}, createdAt = nowIso() }) {
|
|
164
|
+
runInstant(createdAt)
|
|
130
165
|
const normalizedAnswers = normalizeAnswers(protocol, answers)
|
|
131
166
|
const evaluation = evaluateProtocolAnswers(protocol, normalizedAnswers)
|
|
132
167
|
const seed = JSON.stringify({ project: project.config.name || path.basename(project.workspaceRoot), protocol: protocol.id, answers: normalizedAnswers, createdAt })
|
|
@@ -155,6 +190,7 @@ export function buildReadinessRun({ project, protocol, answers = {}, createdAt =
|
|
|
155
190
|
}
|
|
156
191
|
|
|
157
192
|
export function writeReadinessRun(project, run) {
|
|
193
|
+
runInstant(run?.createdAt)
|
|
158
194
|
const dir = readinessRunsDir(project)
|
|
159
195
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
160
196
|
const file = path.join(dir, `${run.runId}.json`)
|
|
@@ -188,7 +224,7 @@ export function createReadinessProposal(project, run) {
|
|
|
188
224
|
export function listProtocolRuns(project) {
|
|
189
225
|
const dir = readinessRunsDir(project)
|
|
190
226
|
if (!fs.existsSync(dir)) return []
|
|
191
|
-
|
|
227
|
+
const runs = fs.readdirSync(dir)
|
|
192
228
|
.filter((name) => name.endsWith('.json'))
|
|
193
229
|
.sort()
|
|
194
230
|
.map((name) => {
|
|
@@ -199,6 +235,7 @@ export function listProtocolRuns(project) {
|
|
|
199
235
|
}
|
|
200
236
|
})
|
|
201
237
|
.filter(Boolean)
|
|
238
|
+
return orderedProtocolRuns(runs)
|
|
202
239
|
}
|
|
203
240
|
|
|
204
241
|
// Shared per-dimension facts derived from a stored run. Used by both the
|
|
@@ -222,8 +259,7 @@ function packProtocolRecords(registry) {
|
|
|
222
259
|
}
|
|
223
260
|
|
|
224
261
|
export function summarizeReadinessJourney(project, { runs = listProtocolRuns(project), registry = null } = {}) {
|
|
225
|
-
const runsByProtocol =
|
|
226
|
-
for (const run of runs) runsByProtocol.set(run.protocolId, run)
|
|
262
|
+
const runsByProtocol = latestRunsByProtocol(runs)
|
|
227
263
|
const dimensions = DIMENSIONS.map(([key, label, protocolId]) => {
|
|
228
264
|
const protocol = protocolById(protocolId)
|
|
229
265
|
const facts = dimensionRunFacts(runsByProtocol.get(protocolId), protocolId)
|
|
@@ -281,8 +317,9 @@ function answersFor(run, key) {
|
|
|
281
317
|
// the runtime and CLI tests instead; if it ever gets a schema, register it so
|
|
282
318
|
// compat checking takes over.
|
|
283
319
|
export function buildTenantPacket(project, { runs = listProtocolRuns(project), registry = null } = {}) {
|
|
284
|
-
const
|
|
285
|
-
const
|
|
320
|
+
const orderedRuns = orderedProtocolRuns(runs)
|
|
321
|
+
const byId = new Map(orderedRuns.map((run) => [run.protocolId, run]))
|
|
322
|
+
const journey = summarizeReadinessJourney(project, { runs: orderedRuns, registry })
|
|
286
323
|
// Generic pack contribution: built entirely from run fields, no per-field
|
|
287
324
|
// plucking (the literal byId.get(...) reads below stay bundled-only). Only
|
|
288
325
|
// required-gate pack protocols can block export; advisory ones just report.
|
|
@@ -363,7 +400,7 @@ export function buildTenantPacket(project, { runs = listProtocolRuns(project), r
|
|
|
363
400
|
...packExportBlockers,
|
|
364
401
|
],
|
|
365
402
|
openQuestions: answersFor(byId.get('mnstry.readiness:tenant-packet'), 'openQuestions') ?? [],
|
|
366
|
-
proposedClaims:
|
|
403
|
+
proposedClaims: orderedRuns.flatMap((run) => run.claims ?? []).map((claim) => claim.claimId),
|
|
367
404
|
ext: {
|
|
368
405
|
'mnstry.atelier': {
|
|
369
406
|
extensionPacks: packContribution,
|
|
@@ -12,11 +12,12 @@ import { readObsidianEnablement } from './enablement.mjs'
|
|
|
12
12
|
import { ObsidianMaintenanceRefusal, refuse } from './errors.mjs'
|
|
13
13
|
import { createMaintenanceExtensions } from './extension-points.mjs'
|
|
14
14
|
import {
|
|
15
|
-
assertOutsideRepositories, authorizeAutomaticApply,
|
|
16
|
-
readMachineSettings, resolveDataRoot, workspaceStateRoot,
|
|
15
|
+
assertOutsideRepositories, authorizeAutomaticApply, ensureWorkspaceIdentity, initializeMachineSettings, protectedRoots, readLocalPointer,
|
|
16
|
+
readMachineSettings, resolveDataRoot, workspaceStateRoot,
|
|
17
17
|
} from './machine-settings.mjs'
|
|
18
18
|
import { configKey, hasIndexedHint, listConfigFiles, listSourceFiles, listVaultNotes, readFileFacts, reconcile, reconcileCooperatively, sha256Digest, sourceKey, vaultKey } from './observation.mjs'
|
|
19
19
|
import { dispatchAutomaticApply, heldPaths, layoutHeldPaths, observeVaultEdits, preserveInRecoveryStore, trustedNoteBases } from './pending-edits.mjs'
|
|
20
|
+
import { personalExclusionRootsNow } from './personal-selection.mjs'
|
|
20
21
|
import { createProductionSeams, eligibilityFor } from './pipeline.mjs'
|
|
21
22
|
import { PLUGIN_FILES_WAIT_FOR_APP } from './plugin-presence.mjs'
|
|
22
23
|
import { ENGINE_LOCK_DIRECTORY, acquirePrivateGenerationLock, createAbandonmentProof } from './private-lock.mjs'
|
|
@@ -107,7 +108,7 @@ export const ENGINE_PRIMITIVES = Object.freeze({
|
|
|
107
108
|
observedAssetsOf: (graph) => (graph.assets ?? []).filter((asset) => asset.eligible === true).map(({ repo, path: assetPath }) => ({ repo, path: assetPath })),
|
|
108
109
|
// A change the guard sees in the middle of a tick is read, where it may be a confirmed selection, while it is there.
|
|
109
110
|
noteSelectionAtGuard: notePersonalSelectionNow,
|
|
110
|
-
// A loaded project pinned to a selection
|
|
111
|
+
// A loaded project pinned to a selection different from the most recently accepted head in this process
|
|
111
112
|
// is loaded again at the next tick, whatever the files say: another reader in the process may have read a
|
|
112
113
|
// confirmation that is gone again before this engine looked (personalSelectionBehind).
|
|
113
114
|
reloadsBehindSelection: personalSelectionBehind,
|
|
@@ -167,6 +168,11 @@ export const ENGINE_PRIMITIVES = Object.freeze({
|
|
|
167
168
|
})
|
|
168
169
|
|
|
169
170
|
const isTypedRefusal = (error) => error instanceof ObsidianMaintenanceRefusal || error instanceof AtelierDiagnosticError || error instanceof ObsidianContractRefusal || error instanceof PublicationRefusal
|
|
171
|
+
// The private-state location proof refused (assertLocation's own code): the state root itself is no longer proven
|
|
172
|
+
// outside every root declared now. That is never one view's refusal: it stops the whole tick, so no later view goes
|
|
173
|
+
// on writing under roots it can no longer trust. A view's own vault in a bad place keeps its own codes and stops
|
|
174
|
+
// only that view.
|
|
175
|
+
const isLocationRefusal = (error) => isTypedRefusal(error) && error.code === 'managed-root-unproven'
|
|
170
176
|
// The code a view is refused with when its own vault cannot be placed: a typed refusal's, or `vault-location-unusable`
|
|
171
177
|
// for a file-system error there (a file in the way, no permission, a read-only volume). Null for anything else, which
|
|
172
178
|
// is a fault of the engine and stops the tick.
|
|
@@ -287,7 +293,7 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
287
293
|
let firstTick = true
|
|
288
294
|
let running = false
|
|
289
295
|
let watching = { signature: null, handle: null }
|
|
290
|
-
let known = null // { stateStore, maintenanceMode } once a workspace has been resolved
|
|
296
|
+
let known = null // { stateStore, maintenanceMode, repositoryRoots } once a workspace has been resolved
|
|
291
297
|
let heldLock = null
|
|
292
298
|
let releaseOwed = false
|
|
293
299
|
// Views somebody asked to have prepared and published once more, at the next tick.
|
|
@@ -310,7 +316,7 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
310
316
|
try { return (await readUnheldEvidence({ vaultRoot: store.vaultRoot }))?.unlisted === true } catch { return false }
|
|
311
317
|
}
|
|
312
318
|
|
|
313
|
-
async function takeLock(workspaceRoot, workspaceId) {
|
|
319
|
+
async function takeLock(workspaceRoot, workspaceId, validateLocation = undefined) {
|
|
314
320
|
// A lock an earlier tick could not give back is still this process's, and its ticket on disk unreleased: a tick
|
|
315
321
|
// that tried to take it anew would find its own ticket and answer `busy` until the process ends. It is given back
|
|
316
322
|
// first. A release that fails again fails this tick, and is owed to the next.
|
|
@@ -322,6 +328,7 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
322
328
|
if (heldLock) return { acquired: true }
|
|
323
329
|
const lock = await rules.acquireEngineLock({
|
|
324
330
|
workspaceRoot, directory: path.join(workspaceRoot, ENGINE_LOCK_DIRECTORY), workspaceId, purpose: 'maintenance-engine', service: lockOwner, clock, proveAbandoned,
|
|
331
|
+
...(validateLocation === undefined ? {} : { validateLocation }),
|
|
325
332
|
})
|
|
326
333
|
if (lock.acquired) heldLock = lock
|
|
327
334
|
return lock
|
|
@@ -369,7 +376,7 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
369
376
|
// A vault root that is gone (a folder under the data root the person removed, say) is looked for again too, so the
|
|
370
377
|
// store made in its place says why rather than failing on the folder that is not there.
|
|
371
378
|
if (cached?.signature === signature && fs.lstatSync(cached.store.vaultRoot, { throwIfNoEntry: false }) !== undefined) {
|
|
372
|
-
try { cached.store.checkAllocatedVault?.(); return cached.store } catch (error) { if (!isTypedRefusal(error)) throw error }
|
|
379
|
+
try { cached.store.checkAllocatedVault?.(); return cached.store } catch (error) { if (!isTypedRefusal(error) || isLocationRefusal(error)) throw error }
|
|
373
380
|
}
|
|
374
381
|
stores.delete(scope.scopeId)
|
|
375
382
|
const store = seams.createRecoveryStore({ workspaceRoot, workspaceId, scopeId: scope.scopeId, repositoryRoots })
|
|
@@ -392,22 +399,67 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
392
399
|
const blankEntry = (scopeId, now) => ({ scopeId, state: 'stale', reason: 'not-yet-published', generationId: null, preparedGenerationId: null, verified: false, heldNotes: [], changeClasses: [], retainedEdits: 0, checkedAt: now })
|
|
393
400
|
const demote = (entry, state, reason, now) => ({ ...entry, state, reason, verified: false, checkedAt: now })
|
|
394
401
|
|
|
395
|
-
//
|
|
396
|
-
|
|
402
|
+
// The private-state location guard's own refusals: under one, the state root itself is in question.
|
|
403
|
+
const MANAGED_ROOT_REFUSALS = new Set(['managed-root-inside-repository', 'managed-root-not-absolute', 'managed-root-realpath-failed',
|
|
404
|
+
'managed-root-realpath-overlap', 'managed-root-symlink-alias', 'repository-inside-managed-root', 'managed-root-unproven'])
|
|
405
|
+
// The roots private state must stay outside, as declared now: each project's own, and a personal home's manifest
|
|
406
|
+
// read again rather than as it was when the project loaded. Throws when they cannot be read.
|
|
407
|
+
const currentExclusionRoots = (declared) => declared.flatMap((value) => {
|
|
408
|
+
const binding = personalWorkspaceBindingOf(value)
|
|
409
|
+
return [...protectedRoots(value), ...(binding === null ? [] : personalExclusionRootsNow(binding.personalHome))]
|
|
410
|
+
})
|
|
411
|
+
// Whether a state root is proven outside every root declared now. A locator that refuses, or roots that cannot be
|
|
412
|
+
// read, prove nothing.
|
|
413
|
+
const locationProvenNow = (root, loaded) => {
|
|
414
|
+
try {
|
|
415
|
+
const declared = [...(locateProject === null ? [] : [locateProject()]), ...(loaded === null ? [] : [loaded])]
|
|
416
|
+
if (declared.length === 0) return false
|
|
417
|
+
assertOutsideRepositories({ managedRoot: root, repositoryRoots: currentExclusionRoots(declared) })
|
|
418
|
+
return true
|
|
419
|
+
} catch { return false }
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Persists "refused" or "disabled" over whatever is known, and only when a workspace already has state. Nothing is
|
|
423
|
+
// written for a location refusal, or unless the state root is proven outside every root declared now: a manifest
|
|
424
|
+
// that newly declares a root holding this state is never written into, cached store or not. When no project can be
|
|
425
|
+
// read now, a cached store is held to the roots it was last proven outside; without either, nothing is written.
|
|
426
|
+
async function persistOutcome({ enablement, state, reason, now, scopeIds = [], locationRefused = false }) {
|
|
427
|
+
if (locationRefused) return
|
|
428
|
+
// The roots declared now. A configured locator that cannot answer leaves them unknown: earlier roots never stand
|
|
429
|
+
// in. Without a locator or a readable project, a cached store is held to the roots it was last proven outside.
|
|
430
|
+
const rootsNow = () => {
|
|
431
|
+
let located = null
|
|
432
|
+
if (locateProject !== null) located = locateProject()
|
|
433
|
+
const declared = [located, project].filter((value) => value !== null && value !== undefined)
|
|
434
|
+
if (declared.length > 0) return currentExclusionRoots(declared)
|
|
435
|
+
if (locateProject === null && Array.isArray(known?.repositoryRoots)) return known.repositoryRoots
|
|
436
|
+
return null
|
|
437
|
+
}
|
|
438
|
+
let repositoryRoots
|
|
439
|
+
try { repositoryRoots = rootsNow() } catch { return }
|
|
440
|
+
if (repositoryRoots === null) return
|
|
441
|
+
const outside = (root) => { try { assertOutsideRepositories({ managedRoot: root, repositoryRoots }); return true } catch { return false } }
|
|
442
|
+
const outsideNow = (root) => { try { const roots = rootsNow(); if (roots === null) return false; assertOutsideRepositories({ managedRoot: root, repositoryRoots: roots }); return true } catch { return false } }
|
|
397
443
|
if (!known) {
|
|
398
|
-
let
|
|
399
|
-
|
|
444
|
+
let source = project
|
|
445
|
+
if (locateProject !== null) { try { source = locateProject() } catch { return } }
|
|
446
|
+
if (source === null || source === undefined) return
|
|
400
447
|
let pointer = null
|
|
401
|
-
try { pointer =
|
|
448
|
+
try { pointer = readLocalPointer(source) } catch { pointer = null }
|
|
402
449
|
if (!pointer) return
|
|
403
450
|
let root
|
|
404
|
-
try { root = workspaceStateRoot(resolveDataRoot({ dataRoot, pointer, project:
|
|
451
|
+
try { root = workspaceStateRoot(resolveDataRoot({ dataRoot, pointer, project: source, env, platform }), pointer.workspaceId) } catch { return }
|
|
452
|
+
if (!outside(root)) return
|
|
405
453
|
const stateStore = createMaintenanceStateStore({ workspaceRoot: root, workspaceId: pointer.workspaceId })
|
|
406
454
|
if (!stateStore.exists()) return
|
|
407
|
-
known = { stateStore: createMaintenanceStateStore({ workspaceRoot: fs.realpathSync(root), workspaceId: pointer.workspaceId }), maintenanceMode: 'manual' }
|
|
455
|
+
known = { stateStore: createMaintenanceStateStore({ workspaceRoot: fs.realpathSync(root), workspaceId: pointer.workspaceId }), maintenanceMode: 'manual', repositoryRoots }
|
|
408
456
|
}
|
|
457
|
+
if (!outside(known.stateStore.workspaceRoot)) return
|
|
458
|
+
// Proven again inside the acquisition and once it returns, against the roots declared then.
|
|
459
|
+
const stillOutside = () => { if (!outsideNow(known.stateStore.workspaceRoot)) refuse('managed-root-unproven', 'the private state can no longer be proved outside every root declared now; nothing is written') }
|
|
409
460
|
// Another engine is ticking this workspace: its state is not ours to write.
|
|
410
|
-
if (!(await takeLock(known.stateStore.workspaceRoot, known.stateStore.workspaceId)).acquired) return
|
|
461
|
+
try { if (!(await takeLock(known.stateStore.workspaceRoot, known.stateStore.workspaceId, stillOutside)).acquired) return } catch (error) { if (isTypedRefusal(error) && error.code === 'managed-root-unproven') return; throw error }
|
|
462
|
+
if (!outsideNow(known.stateStore.workspaceRoot)) return
|
|
411
463
|
const previous = readPreviousFreshness(known.stateStore)
|
|
412
464
|
const entries = new Map()
|
|
413
465
|
for (const entry of previous?.scopes ?? []) entries.set(entry.scopeId, demote(entry, state, reason, now))
|
|
@@ -466,20 +518,28 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
466
518
|
}
|
|
467
519
|
|
|
468
520
|
// 2. Identity and machine settings, outside every repository.
|
|
469
|
-
const pointer = ensureWorkspaceIdentity({ project, ...(randomBytes ? { randomBytes } : {}) })
|
|
470
|
-
const { workspaceId } = pointer
|
|
471
|
-
const requestedRoot = workspaceStateRoot(resolveDataRoot({ dataRoot, pointer, project, env, platform }), workspaceId)
|
|
472
521
|
const repositoryRoots = protectedRoots(project)
|
|
473
|
-
|
|
522
|
+
// The project this tick loaded. `project` itself may be cleared later in the tick to force a reload.
|
|
523
|
+
const loadedProject = project
|
|
524
|
+
// Each phase that writes after a cooperative wait proves the location again against the roots declared now; the
|
|
525
|
+
// refusal is a location refusal, so refusal bookkeeping writes nothing either. A held engine lock is still released.
|
|
526
|
+
const assertLocation = () => {
|
|
527
|
+
if (!locationProvenNow(workspaceRoot, loadedProject)) refuse('managed-root-unproven', 'the private state can no longer be proved outside every root declared now; this tick writes nothing more')
|
|
528
|
+
}
|
|
529
|
+
const stateRootOf = (named) => workspaceStateRoot(resolveDataRoot({ dataRoot, pointer: named, project, env, platform }), named.workspaceId)
|
|
530
|
+
const pointer = ensureWorkspaceIdentity({ project, ...(randomBytes ? { randomBytes } : {}),
|
|
531
|
+
validate: (named) => assertOutsideRepositories({ managedRoot: stateRootOf(named), repositoryRoots }) })
|
|
532
|
+
const { workspaceId } = pointer
|
|
533
|
+
const requestedRoot = stateRootOf(pointer)
|
|
474
534
|
fs.mkdirSync(requestedRoot, { recursive: true, mode: 0o700 })
|
|
475
535
|
const workspaceRoot = fs.realpathSync(requestedRoot)
|
|
476
|
-
const lock = await takeLock(workspaceRoot, workspaceId)
|
|
536
|
+
const lock = await takeLock(workspaceRoot, workspaceId, assertLocation)
|
|
477
537
|
if (!lock.acquired) return { state: 'busy', reason: 'engine-lock-held', lock: { reason: lock.reason, holder: lock.holder ?? null }, changes: [], scopes: [], pendingEdits: [], dispatched: [], lateWriters: [] }
|
|
478
|
-
|
|
479
|
-
|
|
538
|
+
assertLocation()
|
|
539
|
+
const machine = initializeMachineSettings({ workspaceRoot, workspaceId, repositoryRoots, updatedAt: now, personal: personalWorkspaceBindingOf(project) !== null })
|
|
480
540
|
const eligibility = fixedEligibility ?? eligibilityFor({ machine, project })
|
|
481
541
|
const stateStore = createMaintenanceStateStore({ workspaceRoot, workspaceId })
|
|
482
|
-
known = { stateStore, maintenanceMode: machine.maintenanceMode }
|
|
542
|
+
known = { stateStore, maintenanceMode: machine.maintenanceMode, repositoryRoots }
|
|
483
543
|
// A view refused for a file-system error (a vault folder that could not be allocated or placed, a configuration
|
|
484
544
|
// file that could not be read again, a source removed while it was read, candidates that could not be staged) is
|
|
485
545
|
// settled under the refusal's code alone, and such a tick ends without a failure. The error's code (`EACCES`, `EIO`,
|
|
@@ -496,6 +556,7 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
496
556
|
// A view whose own vault cannot be placed: a typed refusal's code and detail, or `vault-location-unusable` for a
|
|
497
557
|
// file-system error there, whose code is its cause.
|
|
498
558
|
const refuseViewAlone = (scopeId, error) => {
|
|
559
|
+
if (isLocationRefusal(error)) throw error
|
|
499
560
|
const code = viewRefusalCode(error)
|
|
500
561
|
if (code === null) throw error
|
|
501
562
|
allocationRefusals.set(scopeId, code)
|
|
@@ -528,6 +589,7 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
528
589
|
const record = readVaultAllocation({ workspaceRoot, workspaceId, scopeId: scope.scopeId })
|
|
529
590
|
if (record === null ? !hasCommittedGeneration({ workspaceRoot, scopeId: scope.scopeId }) : allocatedFolderState(record) === 'missing') {
|
|
530
591
|
const known = await appList()
|
|
592
|
+
assertLocation()
|
|
531
593
|
if (!known.ok) refuse('app-vault-list-unreadable', 'Obsidian\'s vault list could not be read, so no folder is allocated for this view yet; it is tried again at the next tick', { cause: known.code })
|
|
532
594
|
vaults = known.vaults
|
|
533
595
|
}
|
|
@@ -575,6 +637,7 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
575
637
|
looked.push({ scope, store, manifest, bases, vault })
|
|
576
638
|
}
|
|
577
639
|
} finally { reportPhase('vault-observation', vaultStartedMs) }
|
|
640
|
+
assertLocation()
|
|
578
641
|
const pending = stateStore.readPendingEdits()
|
|
579
642
|
let edits = pending.edits
|
|
580
643
|
for (const { scope, store, manifest, bases, vault } of looked) {
|
|
@@ -599,6 +662,7 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
599
662
|
let observed = null
|
|
600
663
|
if (proposalAdapter !== null && rules.observeEdits(proposalAdapter)) {
|
|
601
664
|
try { observed = await proposalAdapter.observe(adapterContext(), { retryRefused: full }) } catch { observed = { adapterId: proposalAdapter.id, failed: 'proposal-adapter-threw' } }
|
|
665
|
+
assertLocation()
|
|
602
666
|
}
|
|
603
667
|
|
|
604
668
|
// 4. Automatic apply, through the registered operation only.
|
|
@@ -608,6 +672,7 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
608
672
|
edits, applyOperation: extensions.applyOperation(), now, nowMs, retryIntervalMs,
|
|
609
673
|
authorize: () => rules.authorize({ workspaceRoot, workspaceId }),
|
|
610
674
|
})
|
|
675
|
+
assertLocation()
|
|
611
676
|
edits = outcome.edits
|
|
612
677
|
dispatched = outcome.dispatched
|
|
613
678
|
stateStore.writePendingEdits({ ...pending, edits })
|
|
@@ -619,6 +684,7 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
619
684
|
let proposals = null
|
|
620
685
|
if (proposalAdapter !== null) {
|
|
621
686
|
try { proposals = await proposalAdapter.propose(adapterContext()) } catch { proposals = { adapterId: proposalAdapter.id, failed: 'proposal-adapter-threw' } }
|
|
687
|
+
assertLocation()
|
|
622
688
|
}
|
|
623
689
|
|
|
624
690
|
// 5. Late writers. The publisher looks twice per publication; a holder can write later still.
|
|
@@ -652,6 +718,7 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
652
718
|
const sourcesStartedMs = phaseTime()
|
|
653
719
|
let sources
|
|
654
720
|
try { sources = await reconcileCooperatively({ index, files: listSourceFiles(project, { assets: observedAssets }), prefix: SOURCE_PREFIX, full, hinted, lstat }, createCooperativeBudget()) } finally { reportPhase('source-observation', sourcesStartedMs) }
|
|
721
|
+
assertLocation()
|
|
655
722
|
for (const change of sources.changes) changes.push({ changeClass: change.changeClass })
|
|
656
723
|
const { scopes: _declared, ...settingsWithoutScopes } = enablement.settings
|
|
657
724
|
// What the configuration says apart from this integration's own member: a change to the member alone is an
|
|
@@ -712,7 +779,9 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
712
779
|
entries.set(scopeId, demote(previous?.scopes.find((entry) => entry.scopeId === scopeId && entry.state !== 'disabled') ?? blankEntry(scopeId, now), 'stale', code, now))
|
|
713
780
|
}
|
|
714
781
|
for (const entry of previous?.scopes ?? []) if (!entries.has(entry.scopeId)) entries.set(entry.scopeId, demote(entry, 'disabled', 'scope-not-configured', now))
|
|
715
|
-
|
|
782
|
+
// Freshness is written only while the state root is still proven outside the roots declared now: a manifest that
|
|
783
|
+
// changed during this tick to enclose it gets no write, refusal or not.
|
|
784
|
+
const persist = () => { if (locationProvenNow(workspaceRoot, loadedProject)) stateStore.writeFreshness(freshnessDocument({ workspaceId, enablement: 'enabled', maintenanceMode: machine.maintenanceMode, now, entries })) }
|
|
716
785
|
persist()
|
|
717
786
|
|
|
718
787
|
// 8. Rebuild and publish.
|
|
@@ -729,6 +798,8 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
729
798
|
let selectionNoted = false
|
|
730
799
|
const noteSelectionOnce = () => { if (!selectionNoted) { selectionNoted = true; rules.noteSelectionAtGuard(project) } }
|
|
731
800
|
const assertBindings = () => {
|
|
801
|
+
// Every cooperative wait of preparation and publication resumes here before it writes again.
|
|
802
|
+
assertLocation()
|
|
732
803
|
let changed
|
|
733
804
|
try {
|
|
734
805
|
changed = bindings.some(file => (readFileFacts(file.absolute)?.digest ?? null) !== file.digest)
|
|
@@ -835,7 +906,7 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
835
906
|
profile: seams.profileFor({ project, workspaceId, audienceAllow: machine.audienceAllow }),
|
|
836
907
|
}
|
|
837
908
|
} catch (error) {
|
|
838
|
-
if (!isTypedRefusal(error)) throw error
|
|
909
|
+
if (!isTypedRefusal(error) || isLocationRefusal(error)) throw error
|
|
839
910
|
for (const scopeId of attempt.keys()) { entries.set(scopeId, demote(entries.get(scopeId), 'stale', error.code, now)); noteRefusalCause(scopeId, error) }
|
|
840
911
|
if (REREAD_CODES.has(error.code)) forceFull = true
|
|
841
912
|
// The personal workspace refused the bound generation: the project is loaded again at the next tick, so a
|
|
@@ -845,7 +916,7 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
845
916
|
for (const { scope, store } of built ? scopes.filter((item) => attempt.has(item.scope.scopeId)) : []) {
|
|
846
917
|
const { scopeId } = scope
|
|
847
918
|
const entry = entries.get(scopeId)
|
|
848
|
-
// What this attempt found worth naming: notes and rules (see "Notes that were laid out anyway" in docs/obsidian-contract.md).
|
|
919
|
+
// What this attempt found worth naming: notes and rules (see "Notes that were laid out anyway" in docs/reference/obsidian-contract.md).
|
|
849
920
|
let diagnostics = []
|
|
850
921
|
const settle = (state, reason, extra = {}) => {
|
|
851
922
|
const { diagnostics: _earlier, ...rest } = { ...entry, ...extra, state, reason, verified: extra.verified === true, checkedAt: now }
|
|
@@ -951,6 +1022,8 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
951
1022
|
}
|
|
952
1023
|
} catch (error) {
|
|
953
1024
|
if (!isTypedRefusal(error)) { settle('stale', 'publisher-error'); persist(); throw error }
|
|
1025
|
+
// Nothing more is recorded for any view: the tick stops and refuses as a whole.
|
|
1026
|
+
if (isLocationRefusal(error)) throw error
|
|
954
1027
|
// A redaction refusal names the note and the rule it concerns, never the value.
|
|
955
1028
|
const where = error.code === 'redaction-failure' ? error.detail ?? {} : {}
|
|
956
1029
|
diagnostics = typeof where.rule === 'string' ? [{ code: error.code, rule: where.rule, ...(typeof where.notePath === 'string' ? { notePath: where.notePath } : {}), ...(typeof where.filePath === 'string' ? { filePath: where.filePath } : {}) }] : []
|
|
@@ -1010,7 +1083,7 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
1010
1083
|
throw error
|
|
1011
1084
|
}
|
|
1012
1085
|
// Fail closed: nothing is published under a refusal, and every known view says why.
|
|
1013
|
-
try { await persistOutcome({ enablement: 'refused', state: 'stale', reason: error.code, now: isoTime(clock) }) } catch { /* the refusal itself is still reported */ }
|
|
1086
|
+
try { await persistOutcome({ enablement: 'refused', state: 'stale', reason: error.code, now: isoTime(clock), locationRefused: MANAGED_ROOT_REFUSALS.has(error.code) }) } catch { /* the refusal itself is still reported */ }
|
|
1014
1087
|
project = null
|
|
1015
1088
|
semantic = null
|
|
1016
1089
|
firstTick = true
|
|
@@ -16,10 +16,10 @@ import {
|
|
|
16
16
|
serviceNameFor, servicePaths, writeServiceSettings,
|
|
17
17
|
} from './service-record.mjs'
|
|
18
18
|
import { personalHomeOf } from './personal-selection.mjs'
|
|
19
|
-
import { resolveServiceWorkspace } from './service.mjs'
|
|
19
|
+
import { resolveServiceWorkspace, serviceLocationCheck } from './service.mjs'
|
|
20
20
|
|
|
21
21
|
// start / status / stop of the maintenance service of one workspace, as
|
|
22
|
-
// docs/local-services.md describes them.
|
|
22
|
+
// docs/reference/local-services.md describes them.
|
|
23
23
|
//
|
|
24
24
|
// Ownership is proven, never assumed. A running service is ours only when the
|
|
25
25
|
// owner-only record and the health answer agree on service name, workspace,
|
|
@@ -67,7 +67,7 @@ export const LIFECYCLE_PRIMITIVES = Object.freeze({
|
|
|
67
67
|
function context({ loadProject, locateProject = null, dataRoot, env, platform, create = false, randomBytes }) {
|
|
68
68
|
if (typeof loadProject !== 'function') throw new TypeError('the service lifecycle needs loadProject')
|
|
69
69
|
const project = locateProject === null ? loadProject() : locateProject()
|
|
70
|
-
if (create) ensureWorkspaceIdentity({ project, ...(randomBytes ? { randomBytes } : {}) })
|
|
70
|
+
if (create) ensureWorkspaceIdentity({ project, ...(randomBytes ? { randomBytes } : {}), validate: serviceLocationCheck({ project, dataRoot, env, platform }) })
|
|
71
71
|
const workspace = resolveServiceWorkspace({ project, dataRoot, env, platform, create })
|
|
72
72
|
return { project, workspace }
|
|
73
73
|
}
|
|
@@ -10,7 +10,7 @@ import { ensureWorkspaceIdentity, readLocalPointer, resolveDataRoot } from './ma
|
|
|
10
10
|
import { SERVICE_MANAGER_KINDS } from './service-managers.mjs'
|
|
11
11
|
import { readLastStartup, readServiceSettings, serviceNameFor, servicePaths, writeServiceSettings } from './service-record.mjs'
|
|
12
12
|
import { personalHomeOf } from './personal-selection.mjs'
|
|
13
|
-
import { resolveServiceWorkspace } from './service.mjs'
|
|
13
|
+
import { resolveServiceWorkspace, serviceLocationCheck } from './service.mjs'
|
|
14
14
|
import { STARTUP_PLATFORMS, buildStartupAdapter, startupSearchPath } from './startup-adapters.mjs'
|
|
15
15
|
|
|
16
16
|
// A login item: the operating system starts this workspace's maintenance
|
|
@@ -198,7 +198,7 @@ export async function installLoginItem(options = {}) {
|
|
|
198
198
|
const { loadProject, dataRoot, env = process.env, platform = process.platform, manager, consent, ownEntry, entryArgs = [], nodePath = realNodePath(), pathValue = env.PATH, clock = () => new Date(), temporary = temporaryRoots(), beforeInstall = null } = options
|
|
199
199
|
checkManager(manager, platform)
|
|
200
200
|
const project = loadProject()
|
|
201
|
-
ensureWorkspaceIdentity({ project, ...(dataRoot === undefined ? {} : { dataRoot }) })
|
|
201
|
+
ensureWorkspaceIdentity({ project, ...(dataRoot === undefined ? {} : { dataRoot }), validate: serviceLocationCheck({ project, dataRoot, env, platform }) })
|
|
202
202
|
const workspace = resolveServiceWorkspace({ project, dataRoot, env, platform, create: true })
|
|
203
203
|
const { entryPath, source } = resolveLoginItemEntry({ project, ownEntry, temporary })
|
|
204
204
|
const recorded = readLoginItemRecord(workspace)
|
|
@@ -3,6 +3,7 @@ import fs from 'node:fs'
|
|
|
3
3
|
import os from 'node:os'
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import { LOCAL_STATE_DIR, localStateRoot } from '../../project/config.mjs'
|
|
6
|
+
import { withPrivateLock } from '../../project/durable-state.mjs'
|
|
6
7
|
import { checkManagedRoots } from '../../project/file-class.mjs'
|
|
7
8
|
import { atomicReplacePrivateText, ensureContainedPrivateDirectory, readRegularTextNoFollow } from '../../project/private-state.mjs'
|
|
8
9
|
import { OBSIDIAN_EXT_KEY, ObsidianContractRefusal, assertObsidianContract } from '../../projection/obsidian/contracts.mjs'
|
|
@@ -146,12 +147,20 @@ export function writeLocalPointer(project, pointer) {
|
|
|
146
147
|
}
|
|
147
148
|
|
|
148
149
|
// Reads the persisted workspace identity, or issues a random one and persists
|
|
149
|
-
// it. `randomBytes` is injectable so tests are deterministic.
|
|
150
|
-
|
|
150
|
+
// it. `randomBytes` is injectable so tests are deterministic. `validate`, when
|
|
151
|
+
// given, checks the location an identity names: a new identity is checked
|
|
152
|
+
// before its pointer is written, so a refused location leaves no pointer, and
|
|
153
|
+
// the identity actually persisted is checked again before it is returned.
|
|
154
|
+
export function ensureWorkspaceIdentity({ project, dataRoot, randomBytes = cryptoRandomBytes, validate = null } = {}) {
|
|
151
155
|
const existing = readLocalPointer(project)
|
|
152
|
-
if (existing) return existing
|
|
153
|
-
const pointer = { schema: LOCAL_POINTER_SCHEMA, workspaceId: `ws-${randomBytes(12).toString('hex')}`, ...(dataRoot === undefined ? {} : { dataRoot: path.resolve(dataRoot) }) }
|
|
154
|
-
|
|
156
|
+
if (existing) { validate?.(existing); return existing }
|
|
157
|
+
const pointer = validatePointer({ schema: LOCAL_POINTER_SCHEMA, workspaceId: `ws-${randomBytes(12).toString('hex')}`, ...(dataRoot === undefined ? {} : { dataRoot: path.resolve(dataRoot) }) })
|
|
158
|
+
validate?.(pointer)
|
|
159
|
+
writeLocalPointer(project, pointer)
|
|
160
|
+
const persisted = readLocalPointer(project)
|
|
161
|
+
if (persisted === null) refuse('invalid-local-pointer', 'the Obsidian pointer was not kept')
|
|
162
|
+
validate?.(persisted)
|
|
163
|
+
return persisted
|
|
155
164
|
}
|
|
156
165
|
|
|
157
166
|
// ---------------------------------------------------------------------------
|
|
@@ -260,6 +269,18 @@ export function defaultMachineSettings({ workspaceId, updatedAt }) {
|
|
|
260
269
|
}
|
|
261
270
|
}
|
|
262
271
|
|
|
272
|
+
// The already adopted owner-only audience for a personal binding, recorded in the same machine setting an ordinary
|
|
273
|
+
// workspace uses. Existing explicit decisions keep their exact list and unclassified choice; a clear stays empty.
|
|
274
|
+
export function ownerOnlyPersonalSettings(settings, { decidedAt }) {
|
|
275
|
+
const decision = settings.decisions.audience
|
|
276
|
+
if (decision === null && settings.audienceAllow.length === 0) {
|
|
277
|
+
return withDecision({ ...settings, audienceAllow: [...ONLY_YOU_AUDIENCES], updatedAt: decidedAt }, 'audience',
|
|
278
|
+
{ choice: 'only-you', unclassified: 'shown' }, { decidedAt, via: 'defaults' })
|
|
279
|
+
}
|
|
280
|
+
if (decision?.choice === 'only-you' || (decision?.choice === 'custom' && settings.audienceAllow.length === 0)) return settings
|
|
281
|
+
refuse('personal-binding-owner-only', 'a personal workspace vault is owner only; select `audience set me` or clear its audience before publishing')
|
|
282
|
+
}
|
|
283
|
+
|
|
263
284
|
// The settings with one decision recorded, validated; nothing is written. `decision` holds the decision's own members
|
|
264
285
|
// (`{ choice, unclassified }`, `{ parent }`, `{ choice }`); the stamp says when, by whom (an identifier, or null when not
|
|
265
286
|
// known) and how.
|
|
@@ -278,15 +299,38 @@ export function readMachineSettings({ workspaceRoot, workspaceId }) {
|
|
|
278
299
|
return document === null ? null : normalizeMachineSettings(document, workspaceId)
|
|
279
300
|
}
|
|
280
301
|
|
|
281
|
-
|
|
282
|
-
export function writeMachineSettings({ workspaceRoot, workspaceId, settings, repositoryRoots }) {
|
|
283
|
-
const document = normalizeMachineSettings(settings, workspaceId)
|
|
302
|
+
function withMachineSettingsWriteLock({ workspaceRoot, repositoryRoots }, operation) {
|
|
284
303
|
assertOutsideRepositories({ managedRoot: workspaceRoot, repositoryRoots })
|
|
285
304
|
fs.mkdirSync(workspaceRoot, { recursive: true, mode: 0o700 })
|
|
305
|
+
return withPrivateLock(path.join(settingsDirectory(workspaceRoot), 'machine-settings.lock'), operation)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function writeMachineSettingsUnlocked({ workspaceRoot, document }) {
|
|
286
309
|
atomicReplacePrivateText(path.join(settingsDirectory(workspaceRoot), 'machine.json'), canonicalJson(document))
|
|
287
310
|
return document
|
|
288
311
|
}
|
|
289
312
|
|
|
313
|
+
// Writes v2, whatever version it is handed, and returns what it wrote. Commands and initialization share this lock.
|
|
314
|
+
export function writeMachineSettings({ workspaceRoot, workspaceId, settings, repositoryRoots }) {
|
|
315
|
+
const document = normalizeMachineSettings(settings, workspaceId)
|
|
316
|
+
return withMachineSettingsWriteLock({ workspaceRoot, repositoryRoots }, () => writeMachineSettingsUnlocked({ workspaceRoot, document }))
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Read-only when already decided. If initialization is needed, reread under the write lock so a committed clear wins.
|
|
320
|
+
export function initializeMachineSettings({ workspaceRoot, workspaceId, repositoryRoots, updatedAt, personal = false }) {
|
|
321
|
+
const initialized = (current) => {
|
|
322
|
+
const initial = current ?? defaultMachineSettings({ workspaceId, updatedAt })
|
|
323
|
+
return personal ? ownerOnlyPersonalSettings(initial, { decidedAt: updatedAt }) : initial
|
|
324
|
+
}
|
|
325
|
+
const existing = readMachineSettings({ workspaceRoot, workspaceId })
|
|
326
|
+
if (initialized(existing) === existing) return existing
|
|
327
|
+
return withMachineSettingsWriteLock({ workspaceRoot, repositoryRoots }, () => {
|
|
328
|
+
const current = readMachineSettings({ workspaceRoot, workspaceId })
|
|
329
|
+
const document = normalizeMachineSettings(initialized(current), workspaceId)
|
|
330
|
+
return document === current ? current : writeMachineSettingsUnlocked({ workspaceRoot, document })
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
|
|
290
334
|
// ---------------------------------------------------------------------------
|
|
291
335
|
// Installed apply policy
|
|
292
336
|
// ---------------------------------------------------------------------------
|
|
@@ -134,7 +134,7 @@ export const REASON_NEXT = Object.freeze({
|
|
|
134
134
|
// publisher or editor is involved.
|
|
135
135
|
'plugin-file-changed': 'a file of Atelier\'s plugin in this vault (under .obsidian/plugins) changed while the view was published, by another program or by you, so the publication did not finish; it is retried automatically, and if this keeps happening, stop what keeps writing there (a sync tool, say)',
|
|
136
136
|
'settings-changed': 'a settings file Atelier writes in this vault (the plugin list or a plugin setting under .obsidian) changed while the view was published, by Obsidian, another program or you, and was not written over; it is retried automatically, reading the file as it is then',
|
|
137
|
-
'selection-history-shortened': 'the selection history
|
|
137
|
+
'selection-history-shortened': 'the selection history no longer follows the machine-private continuity evidence, so nothing is published. Put the removed records back, or explicitly confirm the generation again through the personal workspace selection operation. One fresh confirmation accepts the shorter history if the generation is still eligible. Restarting the service preserves this refusal.',
|
|
138
138
|
'vault-inside-another-vault': 'Obsidian lists another vault at a folder that contains this view\'s vault; open never adds a vault inside another one, and never sends a call that could reach that vault instead: remove that vault from Obsidian\'s vault list, or keep Atelier\'s data root outside that folder, then open again',
|
|
139
139
|
})
|
|
140
140
|
|
|
@@ -253,7 +253,7 @@ export function scopeReport({ workspace, scopeId, repositoryRoots, serviceState,
|
|
|
253
253
|
freshness: entry === null ? null : { state: entry.state, reason: entry.reason, verified: entry.verified, generationId: entry.generationId, preparedGenerationId: entry.preparedGenerationId, heldNoteCount: entry.heldNotes.length, retainedEdits: entry.retainedEdits, checkedAt: entry.checkedAt },
|
|
254
254
|
readBack: { readable: verification.readable, reason: verification.reason, generationId: verification.generationId, intact: verification.intact, noteCount: verification.noteCount, differing: verification.differing, missing: verification.missing },
|
|
255
255
|
pendingEdits: pendingSummary(stateStore, scopeId, applyAvailable),
|
|
256
|
-
// Notes of the view worth naming, and the rule concerned; see "Notes that were laid out anyway" in docs/obsidian-contract.md.
|
|
256
|
+
// Notes of the view worth naming, and the rule concerned; see "Notes that were laid out anyway" in docs/reference/obsidian-contract.md.
|
|
257
257
|
...(entry?.diagnostics ? { diagnostics: entry.diagnostics } : {}),
|
|
258
258
|
vaultRoot: verification.vaultRoot ?? null,
|
|
259
259
|
}
|