@mnstry/atelier 0.2.0-alpha.13 → 0.2.0-alpha.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/CHANGELOG.md +283 -0
  2. package/README.md +102 -52
  3. package/SECURITY.md +1 -1
  4. package/TRADEMARKS.md +2 -2
  5. package/contracts/atelier-extension-manifest.v1.schema.json +412 -0
  6. package/contracts/atelier-graph-view.v1.schema.json +1085 -0
  7. package/contracts/public-api-baseline.json +298 -19
  8. package/docs/README.md +90 -0
  9. package/docs/blocks/capability-stewardship.md +1 -1
  10. package/docs/blocks/claims.md +2 -2
  11. package/docs/blocks/conformance-admission.md +1 -1
  12. package/docs/blocks/learning-harnesses.md +1 -1
  13. package/docs/{atelier-sync.md → guides/atelier-sync.md} +20 -14
  14. package/docs/{capability-stewardship.md → guides/capability-stewardship.md} +2 -4
  15. package/docs/guides/co-authoring.md +113 -0
  16. package/docs/{distributions.md → guides/distributions.md} +13 -8
  17. package/docs/guides/extensions.md +189 -0
  18. package/docs/guides/graph-refresh.md +152 -0
  19. package/docs/guides/graph-workspace.md +64 -0
  20. package/docs/{ingestion.md → guides/ingestion.md} +16 -15
  21. package/docs/{inquiry-harnesses.md → guides/inquiry-harnesses.md} +2 -3
  22. package/docs/{knowledge-workspace.md → guides/knowledge-workspace.md} +2 -2
  23. package/docs/{learning-harnesses.md → guides/learning-harnesses.md} +2 -3
  24. package/docs/{learning.md → guides/learning.md} +12 -5
  25. package/docs/{obsidian.md → guides/obsidian.md} +370 -23
  26. package/docs/guides/ontology-guide.md +111 -0
  27. package/docs/{personal-workspace.md → guides/personal-workspace.md} +17 -5
  28. package/docs/{research-starter.md → guides/research-starter.md} +1 -1
  29. package/docs/{tenant-readiness.md → guides/tenant-readiness.md} +39 -0
  30. package/docs/{upgrade.md → guides/upgrade.md} +197 -11
  31. package/docs/knowledge-health/assessment/capabilities.md +6 -15
  32. package/docs/knowledge-health/facilitator.md +15 -4
  33. package/docs/knowledge-health/{devday/participant-response.md → participant-response.md} +4 -2
  34. package/docs/knowledge-health/quickstart.md +97 -75
  35. package/docs/knowledge-health/readme.md +13 -5
  36. package/docs/maintainers/describing-the-atelier.md +173 -0
  37. package/docs/{release-engineering.md → maintainers/release-engineering.md} +5 -4
  38. package/docs/{announcements.md → reference/announcements.md} +1 -1
  39. package/docs/{architecture.md → reference/architecture.md} +31 -5
  40. package/docs/{atelier-runtime.md → reference/atelier-runtime.md} +1 -1
  41. package/docs/{connected-composition.md → reference/connected-composition.md} +7 -10
  42. package/docs/{coordination.md → reference/coordination.md} +1 -1
  43. package/docs/{decisions.md → reference/decisions.md} +3 -3
  44. package/docs/{design.md → reference/design.md} +1 -1
  45. package/docs/reference/edge-policy.md +151 -0
  46. package/docs/reference/graph-view-structured.md +119 -0
  47. package/docs/reference/graph-view.md +139 -0
  48. package/docs/reference/graph-workspace.md +37 -0
  49. package/docs/{intake-and-guides.md → reference/intake-and-guides.md} +3 -4
  50. package/docs/{knowledge-graph.md → reference/knowledge-graph.md} +120 -6
  51. package/docs/{local-services.md → reference/local-services.md} +43 -4
  52. package/docs/{obsidian-contract.md → reference/obsidian-contract.md} +29 -4
  53. package/docs/{obsidian-plugin.md → reference/obsidian-plugin.md} +218 -68
  54. package/docs/{practical-judgment.md → reference/phronesis.md} +12 -3
  55. package/docs/reference/portability.md +92 -0
  56. package/docs/{portable-presentation.md → reference/portable-presentation.md} +2 -2
  57. package/docs/{presentation-consumer-boundaries.md → reference/presentation-consumer-boundaries.md} +5 -0
  58. package/docs/{project-options.md → reference/project-options.md} +17 -0
  59. package/docs/{repo-boundary-guard.md → reference/repo-boundary-guard.md} +50 -4
  60. package/docs/{review-portability.md → reference/review-portability.md} +4 -0
  61. package/docs/{semantic-operation.md → reference/semantic-operation.md} +4 -3
  62. package/docs/{template-profile.md → reference/template-profile.md} +1 -1
  63. package/docs/{trackables.md → reference/trackables.md} +2 -2
  64. package/docs/start/independent-start.md +89 -0
  65. package/docs/{install.md → start/install.md} +9 -9
  66. package/docs/{knowledge-setup.md → start/knowledge-setup.md} +5 -5
  67. package/docs/{research-starter-quickstart.md → start/research-starter-quickstart.md} +6 -6
  68. package/fixtures/atelier-architecture/catalog/invalid/document.json +2 -2
  69. package/fixtures/atelier-architecture/catalog/valid/document.json +2 -2
  70. package/fixtures/graph-view/synthetic-authorized-input.v1.json +437 -0
  71. package/fixtures/graph-view/synthetic-view.v1.json +1 -0
  72. package/fixtures/knowledge-health/workshop/README.md +1 -1
  73. package/fixtures/templates/local-library.v1.json +3 -1
  74. package/package.json +22 -8
  75. package/plugins/obsidian/main.js +57 -8
  76. package/plugins/obsidian/manifest.json +1 -1
  77. package/skills/claude/atelier-guided-upgrade/SKILL.md +1 -1
  78. package/skills/claude/atelier-knowledge-setup/SKILL.md +2 -2
  79. package/skills/claude/atelier-local-service/SKILL.md +1 -1
  80. package/skills/claude/atelier-practical-judgment/SKILL.md +7 -2
  81. package/skills/claude/atelier-skill-steward/SKILL.md +1 -1
  82. package/skills/claude/mnstry-readiness/SKILL.md +2 -2
  83. package/skills/codex/atelier-guided-upgrade/SKILL.md +1 -1
  84. package/skills/codex/atelier-knowledge-setup/SKILL.md +2 -2
  85. package/skills/codex/atelier-local-service/SKILL.md +1 -1
  86. package/skills/codex/atelier-practical-judgment/SKILL.md +7 -2
  87. package/skills/codex/atelier-skill-steward/SKILL.md +1 -1
  88. package/skills/codex/mnstry-readiness/SKILL.md +2 -2
  89. package/src/architecture/catalog.mjs +2 -2
  90. package/src/attestation/sign.mjs +1 -1
  91. package/src/boundary/content-rules.mjs +534 -68
  92. package/src/boundary/policy.mjs +187 -60
  93. package/src/cli/run.mjs +26 -9
  94. package/src/commands/distribution.mjs +1 -1
  95. package/src/commands/extension.mjs +27 -0
  96. package/src/commands/ingest.mjs +1 -1
  97. package/src/commands/learn.mjs +1 -1
  98. package/src/commands/obsidian.mjs +49 -21
  99. package/src/commands/review.mjs +3 -0
  100. package/src/edge-policy/contract.mjs +69 -0
  101. package/src/edge-policy/policy.mjs +142 -0
  102. package/src/edge-policy/source-citation-adapter.mjs +43 -0
  103. package/src/extension-packs/loader.mjs +4 -3
  104. package/src/extensions/admission.mjs +83 -0
  105. package/src/extensions/client.d.mts +23 -0
  106. package/src/extensions/client.mjs +99 -0
  107. package/src/extensions/contracts.d.mts +29 -0
  108. package/src/extensions/contracts.mjs +163 -0
  109. package/src/extensions/convex.d.mts +24 -0
  110. package/src/extensions/convex.mjs +189 -0
  111. package/src/extensions/delegation.mjs +54 -0
  112. package/src/extensions/drizzle.d.mts +11 -0
  113. package/src/extensions/drizzle.mjs +33 -0
  114. package/src/extensions/postgres.d.mts +7 -0
  115. package/src/extensions/postgres.mjs +66 -0
  116. package/src/extensions/prisma.d.mts +13 -0
  117. package/src/extensions/prisma.mjs +63 -0
  118. package/src/extensions/project.mjs +432 -0
  119. package/src/extensions/scaffold-convex.mjs +130 -0
  120. package/src/extensions/scaffold-sql.mjs +144 -0
  121. package/src/extensions/server.d.mts +20 -0
  122. package/src/extensions/server.mjs +102 -0
  123. package/src/extensions/sql-scope.mjs +22 -0
  124. package/src/graph/graph.mjs +13 -5
  125. package/src/graph/knowledge-graph.mjs +20 -2
  126. package/src/graph/repo-contribution.mjs +405 -0
  127. package/src/graph/repo-contribution.types.mjs +32 -0
  128. package/src/graph-access/edge-decision-adapter.mjs +41 -0
  129. package/src/graph-access/edge-record-adapter.mjs +73 -0
  130. package/src/graph-access/index.mjs +253 -0
  131. package/src/graph-access/multi-contribution-record-adapter.mjs +101 -0
  132. package/src/graph-access/multi-record-projection-adapter.mjs +74 -0
  133. package/src/graph-access/source-adapter.mjs +81 -0
  134. package/src/graph-refresh/compare.mjs +110 -0
  135. package/src/graph-refresh/controller.mjs +324 -0
  136. package/src/graph-refresh/index.mjs +3 -0
  137. package/src/graph-refresh/local.mjs +78 -0
  138. package/src/graph-view/babylon/babylon-scene-port.mjs +64 -0
  139. package/src/graph-view/babylon/index.mjs +2 -0
  140. package/src/graph-view/babylon/navigator.mjs +312 -0
  141. package/src/graph-view/babylon/provenance.mjs +36 -0
  142. package/src/graph-view/babylon/scene-controller.mjs +131 -0
  143. package/src/graph-view/babylon/semantic-picking.mjs +35 -0
  144. package/src/graph-view/babylon/web-host.mjs +188 -0
  145. package/src/graph-view/cli.mjs +3 -0
  146. package/src/graph-view/command.mjs +34 -0
  147. package/src/graph-view/contracts.mjs +79 -0
  148. package/src/graph-view/errors.mjs +5 -0
  149. package/src/graph-view/index.mjs +4 -0
  150. package/src/graph-view/intents.mjs +30 -0
  151. package/src/graph-view/query.mjs +213 -0
  152. package/src/graph-view/refs.mjs +2 -0
  153. package/src/graph-view/structured/browser.mjs +262 -0
  154. package/src/graph-view/structured/styles.mjs +8 -0
  155. package/src/graph-workspace/authoring.mjs +185 -0
  156. package/src/graph-workspace/browser/constellation.mjs +369 -0
  157. package/src/graph-workspace/browser/dom.mjs +23 -0
  158. package/src/graph-workspace/browser/index.mjs +6 -0
  159. package/src/graph-workspace/browser/review.mjs +55 -0
  160. package/src/graph-workspace/browser/styles.mjs +11 -0
  161. package/src/graph-workspace/browser/workbench.mjs +351 -0
  162. package/src/graph-workspace/browser/workspace.mjs +431 -0
  163. package/src/graph-workspace/disclosure.mjs +39 -0
  164. package/src/graph-workspace/geometry.mjs +78 -0
  165. package/src/graph-workspace/index.mjs +11 -0
  166. package/src/graph-workspace/labels.mjs +50 -0
  167. package/src/graph-workspace/model.mjs +109 -0
  168. package/src/graph-workspace/recipes.mjs +88 -0
  169. package/src/graph-workspace/scale.mjs +95 -0
  170. package/src/graph-workspace/theme.mjs +156 -0
  171. package/src/ingestion/optional/graphify/graphify_supplier.py +5 -0
  172. package/src/ingestion/optional/graphify/native-closure-pins.json +275 -0
  173. package/src/ingestion/optional/graphify/native-pins.json +306 -0
  174. package/src/knowledge/ontology-guide.mjs +152 -0
  175. package/src/knowledge-health/proof/installed-check.mjs +2 -2
  176. package/src/knowledge-health/rule-identity.mjs +14 -9
  177. package/src/knowledge-health/workshop.mjs +22 -2
  178. package/src/personal-workspace/index.mjs +1 -1
  179. package/src/personal-workspace/selection.mjs +34 -3
  180. package/src/project/config.mjs +1 -1
  181. package/src/project/path-match.mjs +2 -1
  182. package/src/project/providers/github.mjs +24 -0
  183. package/src/project/repo-identity.mjs +3 -22
  184. package/src/projection/obsidian/contracts.mjs +1 -1
  185. package/src/projection/obsidian/materialize/path-registry.mjs +1 -1
  186. package/src/projection/obsidian/materialize/prepare-view.mjs +88 -6
  187. package/src/projection/obsidian/materialize/redaction.mjs +1 -1
  188. package/src/projection/obsidian/personal-workspace.mjs +84 -5
  189. package/src/projection/obsidian/plugin-bridge/channel.mjs +1 -1
  190. package/src/projection/obsidian/proposals/adapter.mjs +46 -23
  191. package/src/projection/obsidian/proposals/observation.mjs +40 -2
  192. package/src/projection/obsidian/publication/publisher.mjs +96 -8
  193. package/src/projection/project.mjs +21 -16
  194. package/src/readiness/readiness.mjs +20 -11
  195. package/src/readiness-protocols/bundled-pack.mjs +14 -766
  196. package/src/readiness-protocols/bundled-selection.mjs +32 -0
  197. package/src/readiness-protocols/evidence.mjs +2 -1
  198. package/src/readiness-protocols/packs/mnstry.mjs +766 -0
  199. package/src/readiness-protocols/runtime.mjs +43 -6
  200. package/src/runtime/obsidian/engine.mjs +387 -76
  201. package/src/runtime/obsidian/lifecycle.mjs +20 -15
  202. package/src/runtime/obsidian/login-item.mjs +6 -3
  203. package/src/runtime/obsidian/machine-settings.mjs +52 -8
  204. package/src/runtime/obsidian/observation.mjs +36 -4
  205. package/src/runtime/obsidian/opening.mjs +4 -3
  206. package/src/runtime/obsidian/personal-selection.mjs +187 -0
  207. package/src/runtime/obsidian/pipeline.mjs +115 -6
  208. package/src/runtime/obsidian/private-lock.mjs +50 -6
  209. package/src/runtime/obsidian/selection-continuity.mjs +124 -0
  210. package/src/runtime/obsidian/service-main.mjs +68 -16
  211. package/src/runtime/obsidian/service.mjs +80 -6
  212. package/src/runtime/obsidian/unheld-evidence.mjs +1 -1
  213. package/src/runtime/obsidian/view-permission.mjs +335 -0
  214. package/src/ui/control-system.mjs +201 -0
  215. package/src/ui/html-primitives.mjs +3 -0
  216. package/src/upgrade/closure-diagnostics.mjs +135 -0
  217. package/src/upgrade/closure.mjs +649 -0
  218. package/src/upgrade/upgrade.mjs +42 -11
  219. package/templates/distribution-workspace/README.md +2 -2
  220. package/templates/external-project-workspace/atelier.project.json +3 -0
  221. package/templates/external-project-workspace/pack-lifecycle.json +3 -1
  222. package/templates/knowledge-workspace/README.md +2 -2
  223. package/docs/integration-and-review-plan.md +0 -409
  224. package/docs/integration-contract-decisions.md +0 -44
  225. package/docs/knowledge-health/devday/facilitator.md +0 -16
  226. package/docs/knowledge-health/devday/public-default.md +0 -107
  227. package/docs/knowledge-health/devday/quickstart.md +0 -18
  228. package/docs/knowledge-health/devday/support.md +0 -22
  229. package/docs/knowledge-health/exercise.md +0 -17
  230. package/docs/knowledge-health/support.md +0 -34
  231. package/docs/presentation-review-disposition.md +0 -47
  232. package/docs/progressive-ingestion-acceptance.md +0 -89
  233. package/docs/progressive-ingestion.md +0 -235
  234. /package/docs/{exact-upgrades.md → guides/exact-upgrades.md} +0 -0
  235. /package/docs/{guided-upgrades.md → guides/guided-upgrades.md} +0 -0
  236. /package/docs/{local-review.md → guides/local-review.md} +0 -0
  237. /package/docs/{skill-steward.md → guides/skill-steward.md} +0 -0
  238. /package/docs/{template-upgrades.md → guides/template-upgrades.md} +0 -0
  239. /package/docs/{assurance-controls.md → maintainers/assurance-controls.md} +0 -0
  240. /package/docs/{attestation.md → reference/attestation.md} +0 -0
  241. /package/docs/{coauthor-session.md → reference/coauthor-session.md} +0 -0
  242. /package/docs/{continuity.md → reference/continuity.md} +0 -0
  243. /package/docs/{contract-stability.md → reference/contract-stability.md} +0 -0
  244. /package/docs/{decision-practices.md → reference/decision-practices.md} +0 -0
  245. /package/docs/{evidence-navigation.md → reference/evidence-navigation.md} +0 -0
  246. /package/docs/{extension-pack-lifecycle.md → reference/extension-pack-lifecycle.md} +0 -0
  247. /package/docs/{interaction-and-reflection.md → reference/interaction-and-reflection.md} +0 -0
  248. /package/docs/{local-state.md → reference/local-state.md} +0 -0
  249. /package/docs/{ontology.md → reference/ontology.md} +0 -0
  250. /package/docs/{optional-ingestion.md → reference/optional-ingestion.md} +0 -0
  251. /package/docs/{project-command-inventory.md → reference/project-command-inventory.md} +0 -0
  252. /package/docs/{semantic-ingestion.md → reference/semantic-ingestion.md} +0 -0
  253. /package/docs/{vault-service.md → reference/vault-service.md} +0 -0
@@ -1,3 +1,4 @@
1
+ import { createCooperativeBudget, isSchedulingStop } from '../materialize/prepare-view.mjs'
1
2
  import fs from 'node:fs'
2
3
  import fsp from 'node:fs/promises'
3
4
  import path from 'node:path'
@@ -15,6 +16,10 @@ import { probeExchange } from './exchange.mjs'
15
16
  import { CRASH_INJECTION_TEST_SEAM } from './test-seam.mjs'
16
17
  import { createDirectAdapter } from './transport.mjs'
17
18
 
19
+ // The caller's private-state location refusals: once the state root may lie in a declared repository, no further
20
+ // entry is written there.
21
+ const isLocationRefusal = (error) => /^managed-root-|^repository-inside-managed-root$/.test(String(error?.code ?? ''))
22
+
18
23
  // publishView: conditional publication of one prepared view into an editable
19
24
  // vault. The order is the contract's:
20
25
  //
@@ -317,11 +322,33 @@ export function planUnits({ files, priorManifest, pointer, ledger, pluginDisk =
317
322
  return units
318
323
  }
319
324
 
325
+ // The code of a failure met while candidates were being cleaned up after a staging that did not complete, for the detail
326
+ // of what ended it. Nothing else of the failure is kept: its message may name a path.
327
+ const cleanupCauseOf = (failure) => (failure === null ? {} : { cleanupCause: typeof failure?.code === 'string' && failure.code !== '' ? failure.code.slice(0, 64) : 'unknown' })
328
+ // The stop or refusal itself is handed on, so the caller can still tell it by identity: its `detail` is replaced by a
329
+ // copy that adds `cleanupCause`, and an object shared as its detail is left as it was. A caller that throws the same
330
+ // error object again keeps that field on it.
331
+ function withCleanupCause(error, failure) {
332
+ if (failure !== null) try { error.detail = { ...(error.detail ?? {}), ...cleanupCauseOf(failure) } } catch { /* an error that cannot be extended is handed on as it is */ }
333
+ return error
334
+ }
335
+
320
336
  // The rules the oracle tests are sensitive to. Production always uses these; the tests substitute a deliberately
321
337
  // broken one through publishViewForOracleTests to prove the oracle can fail.
322
338
  export const PUBLICATION_PRIMITIVES = Object.freeze({
323
339
  // The one operation the `direct-unheld` path writes into the vault: a file created where nothing is.
324
340
  unheldWrites: (op) => op === 'create',
341
+ // Whether an error that stopped staging was the caller's stop (its settings check, or a cancellation), handed on
342
+ // under its own code once the candidates are cleaned up, rather than reported as a staging failure.
343
+ callerStoppedStaging: (error) => isSchedulingStop(error),
344
+ // Whether a refusal raised while candidates are being staged (the vault's folder replaced or gone under the run) is
345
+ // handed on only once those candidates are cleaned up, as a stop is. A candidate staged before the journal is
346
+ // written is named by no journal, so restart recovery would never remove one left there.
347
+ refusedStagingCleansUp: () => true,
348
+ // The code the journal's last entry gives an error that ended the publication once the journal was open: a
349
+ // caller's stop under its own code, which is what the caller is told, and anything else as a fault of the publisher.
350
+ // Decided by the tag a stop carries, never by what an error looks like; a stop that carries no code has none to give.
351
+ journalFailureCode: (error) => (isSchedulingStop(error) && typeof error.code === 'string' && error.code !== '' ? error.code.slice(0, 200) : 'publisher-error'),
325
352
  })
326
353
 
327
354
  // Whether the `direct-unheld` path applies, after the adapter found an app it cannot coordinate with (see the head of
@@ -355,6 +382,11 @@ export async function publishViewForOracleTests(options = {}, primitives = PUBLI
355
382
  if (!adapter || typeof adapter.probe !== 'function') throw new TypeError('publishView needs an editor coordination adapter')
356
383
  if (expectedGeneration !== null && typeof expectedGeneration !== 'string') throw new TypeError('expectedGeneration must be a generation identity or null')
357
384
 
385
+ const budget = createCooperativeBudget(options.scheduling)
386
+ budget.check()
387
+ // The caller's guard alone, not the cancellation signal: a cancelled publication still records its failure, and so
388
+ // does one the guard stops for another reason (a changed selection or settings). Only a location refusal suppresses it.
389
+ const locationStillProven = () => { try { options.scheduling?.guard?.(); return true } catch (error) { return !isLocationRefusal(error) } }
358
390
  const releases = []
359
391
  let journal = null
360
392
  try {
@@ -384,9 +416,16 @@ export async function publishViewForOracleTests(options = {}, primitives = PUBLI
384
416
  store.checkAllocatedVault?.()
385
417
  acquire(store.vaultLockPath, () => acquireVaultLock(store), 'into this vault')
386
418
  store.checkAllocatedVault?.()
419
+ await budget.checkpoint('recover', true)
420
+ store.checkAllocatedVault?.()
387
421
  const recovered = recoverPublicationsLocked({ store, clock })
422
+ await budget.checkpoint('recovery-complete', true)
423
+ store.checkAllocatedVault?.()
388
424
  const pointer = store.readCurrent()
389
425
  if (pointer?.generationId === manifest.generationId && !pluginFilesDrifted(preparedView, store)) {
426
+ await options.beforeCommit?.()
427
+ budget.check()
428
+ store.checkAllocatedVault?.()
390
429
  return { state: 'committed', alreadyCommitted: true, generationId: manifest.generationId, journalId: pointer.journalId, notes: [], retainedEdits: pointer.retained ?? [], lateWriters: [], recovered }
391
430
  }
392
431
  if ((pointer?.generationId ?? null) !== expectedGeneration) refuse('generation-mismatch', 'the committed generation is not the one this publication expects', { committed: pointer?.generationId ?? null })
@@ -394,9 +433,11 @@ export async function publishViewForOracleTests(options = {}, primitives = PUBLI
394
433
  // Path selection. An app with this vault open in several windows is refused as such: publishing through one of
395
434
  // them would leave the others uncoordinated.
396
435
  const probe = await adapter.probe({ vaultRoot: store.vaultRoot })
436
+ budget.check()
397
437
  store.checkAllocatedVault?.()
398
438
  const unheld = probe.state !== 'coordinated' && probe.state !== 'absent' && await unheldPathApplies({ probe, pointer, store, unheldEvidence, platform })
399
- // The evidence took time to read: the folder is checked again.
439
+ // The evidence took time to read: the location and the folder are checked again.
440
+ budget.check()
400
441
  if (unheld) store.checkAllocatedVault?.()
401
442
  if (probe.state !== 'coordinated' && probe.state !== 'absent' && !unheld) refuse(probe.code === 'vault-open-in-several-windows' ? probe.code : 'editor-uncoordinated', `an Obsidian process may have this vault open and cannot be coordinated with: ${probe.reason}`)
402
443
  const mode = probe.state === 'coordinated' ? 'in-app' : unheld ? 'direct-unheld' : 'direct'
@@ -426,12 +467,16 @@ export async function publishViewForOracleTests(options = {}, primitives = PUBLI
426
467
  stagingDir = store.stagingDir(journalId)
427
468
  // On the `direct-unheld` path a replacement is never written, so no candidate is staged for one.
428
469
  for (const unit of units.filter((item) => item.op === 'create' || (item.op === 'replace' && (mode !== 'direct-unheld' || rules.unheldWrites(item.op))))) {
470
+ await budget.checkpoint('stage-candidate')
471
+ store.checkAllocatedVault?.()
429
472
  const existing = fs.lstatSync(path.join(store.vaultRoot, unit.path), { throwIfNoEntry: false })
430
473
  unit.preparedPath = store.preparedPath(journalId, unit.unit)
431
474
  stageCandidate(unit.preparedPath, unit.bytes, unit.mode ?? (existing?.isFile() ? existing.mode & 0o777 : 0o644), created)
432
475
  // A replacement is exchanged, and the exchange leaves the displaced bytes at this path: it is in recovery.
433
476
  unit.stagedPath = unit.op === 'replace' ? store.exchangeCandidatePath(journalId, unit.unit) : unit.preparedPath
434
477
  }
478
+ await budget.checkpoint('staging-complete', true)
479
+ store.checkAllocatedVault?.()
435
480
  syncPrivateDirectory(stagingDir)
436
481
  const manifestFile = path.join(store.journalDir(journalId), 'manifest.json')
437
482
  publishPrivateFile(manifestFile, manifestBytes)
@@ -446,28 +491,49 @@ export async function publishViewForOracleTests(options = {}, primitives = PUBLI
446
491
  // The journal now names every exchange candidate by path and digest. Only complete, fsynced files are moved
447
492
  // in, so a file at an exchange path is never a partial candidate.
448
493
  for (const unit of units.filter((item) => item.stagedPath && item.stagedPath !== item.preparedPath)) {
494
+ await budget.checkpoint('move-candidate')
495
+ store.checkAllocatedVault?.()
449
496
  moveToExchangePath(store, journalId, unit, moved)
450
497
  }
451
498
  } catch (error) {
452
- if (error instanceof PublicationRefusal) throw error
499
+ const stopped = rules.callerStoppedStaging(error)
500
+ // A refusal raised between two candidates (the publisher's own check found the vault's folder replaced or gone)
501
+ // is handed on under its own code too, and like a stop only after the cleanup below.
502
+ const refused = error instanceof PublicationRefusal && !stopped
503
+ if (refused && !rules.refusedStagingCleansUp(error)) throw error
453
504
  // No payload has named any of these paths, so nothing was exchanged and they can only hold our own
454
505
  // candidates. In staging they are removed by name. In recovery a file is removed only when its bytes are
455
506
  // the candidate's; anything else stays for restart recovery, which the journal header points at it. Only
456
507
  // empty directories are removed, so a failed staging leaves nothing that looks like an unfinished unit.
508
+ // Each file is cleaned up on its own, so one that cannot be removed (EBUSY, EPERM, a volume gone) leaves the
509
+ // others removed; the first such failure is kept.
510
+ let cleanupFailure = null
511
+ const cleanUp = (operation) => { try { operation() } catch (failure) { cleanupFailure ??= failure } }
457
512
  for (const unit of moved) {
458
- if (readFileDigest(unit.stagedPath) === unit.candidateDigest) fs.rmSync(unit.stagedPath, { force: true })
513
+ cleanUp(() => { if (readFileDigest(unit.stagedPath) === unit.candidateDigest) fs.rmSync(unit.stagedPath, { force: true }) })
459
514
  try { fs.rmdirSync(path.dirname(unit.stagedPath)) } catch { /* not empty: left for restart recovery */ }
460
515
  }
461
- for (const file of created) fs.rmSync(file, { force: true })
516
+ for (const file of created) cleanUp(() => fs.rmSync(file, { force: true }))
462
517
  for (const directory of [stagingDir, store.unitsRoot(journalId)]) if (directory) try { fs.rmdirSync(directory) } catch { /* absent, or not empty */ }
463
- refuse('staging-failed', 'the candidates could not be staged; nothing in the vault was touched', { cause: error.code ?? String(error.message) })
518
+ // The caller stopped the publication between two candidates: nothing failed to stage, and its reason (a
519
+ // setting that changed during the tick, `mixed-read`, or a cancellation) is the one reported. So is a refusal's.
520
+ // A cleanup that failed does not replace either: it is named in their detail as `cleanupCause`, and what it could
521
+ // not remove stays where it is.
522
+ if (stopped || refused) throw withCleanupCause(error, cleanupFailure)
523
+ refuse('staging-failed', 'the candidates could not be staged; nothing in the vault was touched', { cause: error.code ?? String(error.message), ...cleanupCauseOf(cleanupFailure) })
464
524
  }
525
+ await budget.checkpoint('journal-and-moves-complete', true)
526
+ store.checkAllocatedVault?.()
465
527
  crash('after-staging')
466
528
 
467
- const context = { store, journal, journalId, channel, clock, crash, mode, rules }
529
+ // `check` runs the caller's guard: every wait for the app resumes through it before the next recovery, journal or
530
+ // vault write.
531
+ const context = { store, journal, journalId, channel, clock, crash, mode, rules, check: () => budget.check() }
468
532
  const results = []
469
533
  // Kept notes are compared first, several at a time; one that holds exactly its candidate bytes is unchanged.
470
534
  const unchanged = await unchangedKeepUnits(units, store)
535
+ // The comparison read the disk: the location is proven again before any unit's recovery, journal or vault write.
536
+ budget.check()
471
537
  // The direct path has no editor coordination, so it is only right while no
472
538
  // Obsidian runs. Staging takes time and an app may have started since path
473
539
  // selection: the process table is read again immediately before the first
@@ -479,14 +545,19 @@ export async function publishViewForOracleTests(options = {}, primitives = PUBLI
479
545
  // unit refuses, a kept one included, so nothing is committed.
480
546
  let lastProbe = null
481
547
  for (const unit of units) {
548
+ await budget.checkpoint('publish-unit')
549
+ store.checkAllocatedVault?.()
482
550
  if (mode === 'direct' && (lastProbe === null || Date.now() - lastProbe > 2000)) {
483
551
  lastProbe = Date.now()
484
552
  const again = await adapter.probe({ vaultRoot: store.vaultRoot })
553
+ budget.check()
485
554
  if (again.state !== 'absent') context.uncoordinated = again.reason
486
555
  }
487
556
  if (mode === 'direct-unheld' && !context.uncoordinated && (lastProbe === null || Date.now() - lastProbe > 2000)) {
488
557
  lastProbe = Date.now()
489
- if (!(await readsUnlisted(unheldEvidence, store))) context.uncoordinated = 'the vault list of the app no longer shows, positively, that no Obsidian lists this vault'
558
+ const listed = await readsUnlisted(unheldEvidence, store)
559
+ budget.check()
560
+ if (!listed) context.uncoordinated = 'the vault list of the app no longer shows, positively, that no Obsidian lists this vault'
490
561
  }
491
562
  store.checkAllocatedVault?.()
492
563
  if (unit.op === 'keep' && unchanged.has(unit.path)) {
@@ -501,6 +572,8 @@ export async function publishViewForOracleTests(options = {}, primitives = PUBLI
501
572
  }
502
573
  }
503
574
 
575
+ await budget.checkpoint('publication-complete', true)
576
+ store.checkAllocatedVault?.()
504
577
  const blocking = results.filter((result) => result.blocking)
505
578
  const retainedEdits = results.filter((result) => result.retained).map((result) => result.retained)
506
579
  // Notes kept earlier stay surfaced until they leave the vault or come back into a view.
@@ -512,11 +585,22 @@ export async function publishViewForOracleTests(options = {}, primitives = PUBLI
512
585
 
513
586
  // Quiet period, then look at the displaced files again.
514
587
  if (quietPeriodMs > 0 && results.some((result) => result.recoveryRef)) await sleep(quietPeriodMs)
588
+ budget.check()
515
589
  const lateWriters = recheckDisplacedFiles({ store, journalIds: [journalId], clock })
516
590
  for (const finding of lateWriters.filter((item) => item.code === 'late-writer-captured')) {
517
591
  journal.append({ step: 'verify', outcome: 'conflict', state: 'verifying', notePath: finding.notePath, beforeDigest: finding.digestAtMove, afterDigest: finding.observedDigest, recoveryRef: finding.displacedRef,
518
592
  detail: { unit: finding.unit, code: 'late-writer-captured', objectRef: finding.objectRef } })
519
593
  }
594
+ await budget.checkpoint('verify-before-commit', true)
595
+ // The resident engine revalidates the selected source pins here, while
596
+ // publication still owns its locks. Its check may yield; no durability
597
+ // unit or committed pointer is half-written across that wait. It comes
598
+ // before the entry that says the publication settled: restart recovery
599
+ // commits a journal that holds that entry, so a refusal after it would be
600
+ // committed by the next publication of this view.
601
+ await options.beforeCommit?.()
602
+ budget.check()
603
+ store.checkAllocatedVault?.()
520
604
  journal.append({ step: 'verify', outcome: 'ok', state: 'verifying', detail: { settled: true, retained: retainedEdits } })
521
605
  crash('before-manifest-commit')
522
606
  store.checkAllocatedVault?.()
@@ -528,7 +612,9 @@ export async function publishViewForOracleTests(options = {}, primitives = PUBLI
528
612
  return { ...base, state: 'committed', lateWriters }
529
613
  } catch (error) {
530
614
  if (!(error instanceof PublicationRefusal)) {
531
- if (journal) try { journal.append({ step: 'verify', outcome: 'failed', state: 'failed', detail: { code: 'publisher-error', message: String(error.code ?? error.message).slice(0, 200) } }) } catch { /* the journal itself cannot be written */ }
615
+ // A private-state location refusal records nothing more there, a failure entry included, and neither does a
616
+ // failure that leaves the location unproven now (a rejected wait for the app). Locks are still released.
617
+ if (journal && !isLocationRefusal(error) && locationStillProven()) try { journal.append({ step: 'verify', outcome: 'failed', state: 'failed', detail: { code: rules.journalFailureCode(error), message: String(error.code ?? error.message).slice(0, 200) } }) } catch { /* the journal itself cannot be written */ }
532
618
  throw error
533
619
  }
534
620
  return { state: 'refused', refusal: { code: error.code, message: error.message, detail: error.detail }, notes: [], retainedEdits: [], lateWriters: [] }
@@ -705,6 +791,7 @@ async function publishOneUnit(unit, context) {
705
791
  }
706
792
  const baseObject = observe(current)
707
793
  const inspected = await context.channel.inspect({ vaultRoot: store.vaultRoot, path: unit.path })
794
+ context.check?.()
708
795
  if (inspected.status !== 'inspected') {
709
796
  capture('failed', inspected.status, { beforeDigest: currentDigest })
710
797
  if (plan.stagedPath) retire(plan, context)
@@ -723,6 +810,7 @@ async function publishOneUnit(unit, context) {
723
810
  ? { op: 'publish', mode: 'remove', vaultRoot: store.vaultRoot, path: unit.path, operationId, baseSha256: hex(plan.baseDigest), recoveryPath: displaced }
724
811
  : { op: 'publish', mode: 'replace', vaultRoot: store.vaultRoot, path: unit.path, operationId, baseSha256: hex(plan.baseDigest), candidateSha256: hex(plan.candidateDigest), stagedPath: plan.stagedPath, recoveryPath: displaced })
725
812
  context.crash('after-publish')
813
+ context.check?.()
726
814
 
727
815
  if (reply.status === 'published' || reply.status === 'published-external-captured' || reply.status === 'removed' || reply.status === 'removed-external-captured') {
728
816
  syncPrivateDirectory(path.dirname(displaced))
@@ -3,6 +3,8 @@ import { isDeepStrictEqual } from 'node:util'
3
3
  import path from 'node:path'
4
4
  import { commandProject, readJson, writeJson } from '../project/config.mjs'
5
5
  import { summarizeReadinessJourney } from '../readiness-protocols/runtime.mjs'
6
+ import { bundledReadinessEnabled } from '../readiness-protocols/bundled-selection.mjs'
7
+ import { atelierControlStyles } from '../ui/control-system.mjs'
6
8
 
7
9
  const esc = (value) => String(value ?? '').replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;')
8
10
 
@@ -44,16 +46,17 @@ function resolveDistributionBranding(project) {
44
46
  }
45
47
 
46
48
  export function buildProjectProjection(project) {
49
+ const includeBundled = bundledReadinessEnabled(project)
47
50
  const graph = readJson(project.graphPath)
48
51
  const branding = resolveDistributionBranding(project)
49
- const readinessJourney = summarizeReadinessJourney(project)
52
+ const readinessJourney = includeBundled ? summarizeReadinessJourney(project) : null
50
53
  const output = path.join(project.outputRoot, 'index.html')
51
- const dimensionCards = readinessJourney.dimensions.map((item) => `<article class="readiness-card" data-protocol="${esc(item.protocolId)}">
54
+ const dimensionCards = (readinessJourney?.dimensions ?? []).map((item) => `<article class="readiness-card" data-protocol="${esc(item.protocolId)}">
52
55
  <p class="eyebrow">${esc(item.label)}</p>
53
56
  <h3>${esc(item.title)}</h3>
54
57
  <p><strong>${esc(item.status)}</strong> · ${esc(item.score)} / 100 answers complete</p>
55
58
  <p>${esc(item.blockers[0] || 'review-ready')}</p>
56
- <button type="button" data-copy="${esc(item.agentPrompt)}">Copy protocol prompt</button>
59
+ <button type="button" class="system-control" data-variant="secondary" data-size="compact" data-copy="${esc(item.agentPrompt)}">Copy protocol prompt</button>
57
60
  </article>`).join('\n')
58
61
  const cards = graph.nodes.map((node) => `<article class="card" data-node="${esc(node.id)}">
59
62
  <p class="eyebrow">${esc(node.repo)} · ${esc(node.type)} · ${esc(node.audience)}</p>
@@ -80,16 +83,16 @@ h1{font-size:clamp(2rem,4vw,4rem);margin:.2em 0}
80
83
  .readiness-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px;margin-top:16px}
81
84
  .readiness-card{border:1px solid #342b26;border-radius:8px;padding:14px;background:#171312}
82
85
  .readiness-card h3{font-size:1.05rem;margin:.2rem 0}
83
- .readiness-card button{min-height:44px;border:1px solid #755f43;border-radius:6px;background:#241d18;color:var(--atelier-text);padding:8px 10px;cursor:pointer}
84
86
  .eyebrow{color:var(--atelier-eyebrow);text-transform:uppercase;letter-spacing:.08em;font-size:.8rem}
85
87
  a{color:var(--atelier-accent)}
88
+ ${atelierControlStyles}
86
89
  ${branding.themeCss}</style>
87
90
  </head>
88
91
  <body><main>
89
92
  <p class="eyebrow">${esc(branding.name)} · ${esc(branding.eyebrow)}</p>
90
93
  <h1>${esc(project.config.name || 'Project Workspace')}</h1>
91
94
  <p>${graph.counts.nodes} graph nodes · ${graph.counts.edges} graph edges · ${graph.counts.diagnostics} diagnostics</p>
92
- <section class="readiness" aria-labelledby="tenant-readiness-title">
95
+ ${readinessJourney ? `<section class="readiness" aria-labelledby="tenant-readiness-title">
93
96
  <div class="readiness-head">
94
97
  <div>
95
98
  <p class="eyebrow">MNSTRY Tenant Readiness</p>
@@ -100,7 +103,7 @@ ${branding.themeCss}</style>
100
103
  <div class="readiness-score">${esc(readinessJourney.score)}%</div>
101
104
  </div>
102
105
  <div class="readiness-grid">${dimensionCards}</div>
103
- </section>
106
+ </section>` : ''}
104
107
  <section class="grid">${cards}</section>
105
108
  <script>
106
109
  document.addEventListener('click', async (event) => {
@@ -125,16 +128,18 @@ export function buildProjectManifest(project, projection) {
125
128
  generatedAt: 'deterministic',
126
129
  graphPath: path.relative(project.outputRoot, project.graphPath).split(path.sep).join('/'),
127
130
  entry: 'index.html',
128
- tenantReadiness: {
129
- score: projection.readinessJourney.score,
130
- ready: projection.readinessJourney.ready,
131
- dimensions: projection.readinessJourney.dimensions.map((item) => ({
132
- key: item.key,
133
- protocolId: item.protocolId,
134
- status: item.status,
135
- score: item.score,
136
- })),
137
- },
131
+ ...(projection.readinessJourney ? {
132
+ tenantReadiness: {
133
+ score: projection.readinessJourney.score,
134
+ ready: projection.readinessJourney.ready,
135
+ dimensions: projection.readinessJourney.dimensions.map((item) => ({
136
+ key: item.key,
137
+ protocolId: item.protocolId,
138
+ status: item.status,
139
+ score: item.score,
140
+ })),
141
+ },
142
+ } : {}),
138
143
  nodes: projection.graph.nodes.map((node) => ({ id: node.id, title: node.title, audience: node.audience, path: node.path })),
139
144
  }
140
145
  }
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { bundledReadinessProtocols } from '../readiness-protocols/bundled-pack.mjs'
4
+ import { bundledReadinessEnabled, requireBundledReadiness, requireSelectedReadinessProtocol } from '../readiness-protocols/bundled-selection.mjs'
4
5
  import { createProtocolRegistry, loadExtensionPacks } from '../extension-packs/loader.mjs'
5
6
  import {
6
7
  buildReadinessExportDryRun,
@@ -131,6 +132,7 @@ function configRelative(project, file) {
131
132
  }
132
133
 
133
134
  function buildProjectReadiness({ project, graph = null } = {}) {
135
+ const includeBundled = bundledReadinessEnabled(project)
134
136
  const loadedGraph = graph || (fs.existsSync(project.graphPath) ? readJson(project.graphPath) : null)
135
137
  const projectionEntry = path.join(project.outputRoot, 'index.html')
136
138
  const manifest = path.join(project.outputRoot, 'atelier.manifest.json')
@@ -167,7 +169,7 @@ function buildProjectReadiness({ project, graph = null } = {}) {
167
169
  enabled: false,
168
170
  authority: 'claim-only',
169
171
  },
170
- tenantReadiness: summarizeReadinessJourney(project),
172
+ ...(includeBundled ? { tenantReadiness: summarizeReadinessJourney(project) } : {}),
171
173
  }
172
174
  }
173
175
 
@@ -201,10 +203,11 @@ export function stableReadinessForCheck(value) {
201
203
 
202
204
  // Loads declared extension packs (fail closed: any pack error throws before a
203
205
  // protocol becomes resolvable) and composes the explicit registry. Projects
204
- // with no declared packs get a bundled-only registry.
206
+ // without an explicit selection retain the bundled default.
205
207
  function projectRegistry(project) {
208
+ const includeBundled = bundledReadinessEnabled(project)
206
209
  const { packs } = loadExtensionPacks(project)
207
- return createProtocolRegistry({ packs })
210
+ return createProtocolRegistry({ packs, includeBundled })
208
211
  }
209
212
 
210
213
  export function runReadinessCommand(argv = process.argv.slice(2)) {
@@ -219,15 +222,16 @@ export function runReadinessCommand(argv = process.argv.slice(2)) {
219
222
  const envConfig = process.env[PROJECT_CONFIG_ENV]
220
223
  const projectAware = projectConfigArg(argv) !== null
221
224
  || (typeof envConfig === 'string' && envConfig.trim() !== '')
222
- const entries = bundledReadinessProtocols.map((protocol) => ({ protocol, source: null }))
225
+ let includeBundled = true
226
+ let entries = bundledReadinessProtocols.map((protocol) => ({ protocol, source: null }))
223
227
  if (projectAware) {
224
228
  const project = commandProject({ argv })
225
229
  const registry = projectRegistry(project)
226
- for (const pack of registry.packs) {
227
- for (const record of pack.protocols ?? []) {
228
- entries.push({ protocol: record.protocol, source: pack.id })
229
- }
230
- }
230
+ includeBundled = bundledReadinessEnabled(project)
231
+ entries = registry.listProtocols().map((protocol) => ({
232
+ protocol,
233
+ source: registry.packs.find((pack) => pack.protocols?.some((record) => record.id === protocol.id))?.id ?? null,
234
+ }))
231
235
  }
232
236
  if (detail) {
233
237
  console.log(`${JSON.stringify({
@@ -235,7 +239,7 @@ export function runReadinessCommand(argv = process.argv.slice(2)) {
235
239
  protocols: entries.map(({ protocol, source }) => (source ? { ...protocol, source } : protocol)),
236
240
  }, null, 2)}\n`)
237
241
  } else {
238
- console.log('MNSTRY readiness protocols')
242
+ console.log(includeBundled ? 'MNSTRY readiness protocols' : 'Atelier readiness protocols')
239
243
  for (const { protocol, source } of entries) {
240
244
  console.log(`- ${protocol.id}: ${protocol.title}${source ? ` (source: ${source})` : ''}`)
241
245
  }
@@ -245,6 +249,7 @@ export function runReadinessCommand(argv = process.argv.slice(2)) {
245
249
 
246
250
  if (subcommand === 'journey') {
247
251
  const project = commandProject({ argv })
252
+ requireBundledReadiness(project)
248
253
  const journey = summarizeReadinessJourney(project, { registry: projectRegistry(project) })
249
254
  console.log(`${JSON.stringify(journey, null, 2)}\n`)
250
255
  return
@@ -254,12 +259,14 @@ export function runReadinessCommand(argv = process.argv.slice(2)) {
254
259
  const project = commandProject({ argv })
255
260
  const protocolId = parsed._[1]
256
261
  if (!protocolId) throw new Error('readiness run requires a protocol id')
262
+ const registry = projectRegistry(project)
263
+ requireSelectedReadinessProtocol(project, registry, protocolId)
257
264
  const answers = parsed.answers ? readJson(path.resolve(process.cwd(), parsed.answers)) : {}
258
265
  const result = runProtocol(project, protocolId, {
259
266
  answers,
260
267
  write: parsed.write !== false,
261
268
  createProposal: parsed.proposal !== false && parsed['no-proposal'] !== true,
262
- registry: projectRegistry(project),
269
+ registry,
263
270
  })
264
271
  console.log(`${JSON.stringify({
265
272
  ok: result.run.blockers.length === 0,
@@ -279,6 +286,7 @@ export function runReadinessCommand(argv = process.argv.slice(2)) {
279
286
 
280
287
  if (subcommand === 'packet') {
281
288
  const project = commandProject({ argv })
289
+ requireBundledReadiness(project)
282
290
  const packet = buildTenantPacket(project, { registry: projectRegistry(project) })
283
291
  const file = writeTenantPacket(project, packet)
284
292
  console.log(`${JSON.stringify({
@@ -292,6 +300,7 @@ export function runReadinessCommand(argv = process.argv.slice(2)) {
292
300
  if (subcommand === 'export') {
293
301
  if (!argv.includes('--dry-run')) throw new Error('readiness export only supports --dry-run')
294
302
  const project = commandProject({ argv })
303
+ requireBundledReadiness(project)
295
304
  const report = buildReadinessExportDryRun(project, { registry: projectRegistry(project) })
296
305
  console.log(`${JSON.stringify(report, null, 2)}\n`)
297
306
  if (!report.accepted) process.exitCode = 1