@mnstry/atelier 0.2.0-alpha.11 → 0.2.0-alpha.12

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 (81) hide show
  1. package/CHANGELOG.md +344 -0
  2. package/README.md +10 -6
  3. package/contracts/atelier-obsidian-generation-manifest.v2.schema.json +372 -0
  4. package/contracts/compat-baseline.json +1 -1
  5. package/docs/assurance-controls.md +3 -2
  6. package/docs/install.md +3 -3
  7. package/docs/local-services.md +62 -11
  8. package/docs/obsidian-contract.md +669 -35
  9. package/docs/obsidian-plugin.md +585 -0
  10. package/docs/obsidian.md +233 -50
  11. package/docs/release-engineering.md +7 -1
  12. package/docs/upgrade.md +23 -3
  13. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/case-only-collision.v2.json +160 -0
  14. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/flat-note-path.v2.json +160 -0
  15. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/hidden-folder.v2.json +160 -0
  16. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/identity-in-the-body.v2.json +160 -0
  17. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/link-breaking-name.v2.json +160 -0
  18. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/missing-identity-region.v2.json +156 -0
  19. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/missing-layout-version.v2.json +159 -0
  20. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/note-named-like-a-folder.v2.json +160 -0
  21. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/unknown-region-kind.v2.json +160 -0
  22. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/v1-contract-version.v2.json +161 -0
  23. package/fixtures/obsidian/contracts/generation-manifest-v2/valid/three-notes.v2.json +160 -0
  24. package/fixtures/obsidian/edits/workspace.json +1 -1
  25. package/fixtures/obsidian/materialization/expected-full.json +21 -21
  26. package/fixtures/obsidian/materialization/expected-full.layout-1.json +99 -0
  27. package/fixtures/obsidian/materialization/expected-scoped.json +7 -7
  28. package/fixtures/obsidian/materialization/expected-scoped.layout-1.json +39 -0
  29. package/package.json +7 -3
  30. package/plugins/obsidian/main.js +444 -0
  31. package/plugins/obsidian/manifest.json +9 -0
  32. package/plugins/obsidian/styles.css +22 -0
  33. package/src/commands/obsidian.mjs +89 -10
  34. package/src/contracts/corpus.mjs +6 -0
  35. package/src/egress/forbidden-egress.mjs +2 -0
  36. package/src/project/private-state.mjs +10 -1
  37. package/src/projection/obsidian/contracts.mjs +224 -18
  38. package/src/projection/obsidian/edits/apply.mjs +12 -7
  39. package/src/projection/obsidian/edits/regions.mjs +137 -42
  40. package/src/projection/obsidian/materialize/index.mjs +6 -3
  41. package/src/projection/obsidian/materialize/path-registry.mjs +443 -57
  42. package/src/projection/obsidian/materialize/prepare-view.mjs +382 -131
  43. package/src/projection/obsidian/materialize/redaction.mjs +327 -0
  44. package/src/projection/obsidian/materialize/settings.mjs +113 -34
  45. package/src/projection/obsidian/plugin-bridge/bundle.mjs +50 -0
  46. package/src/projection/obsidian/plugin-bridge/channel.mjs +174 -0
  47. package/src/projection/obsidian/plugin-bridge/index.mjs +10 -0
  48. package/src/projection/obsidian/proposals/observation.mjs +10 -5
  49. package/src/projection/obsidian/publication/bridge-script.mjs +11 -5
  50. package/src/projection/obsidian/publication/index.mjs +2 -1
  51. package/src/projection/obsidian/publication/publisher.mjs +173 -26
  52. package/src/projection/obsidian/publication/transport.mjs +63 -8
  53. package/src/projection/obsidian/publication/vault-list.mjs +199 -0
  54. package/src/projection/obsidian/recovery/restart.mjs +3 -1
  55. package/src/projection/obsidian/recovery/store.mjs +11 -4
  56. package/src/projection/obsidian/selection-ui/focus.mjs +14 -7
  57. package/src/projection/obsidian/selection-ui/selection-state.mjs +2 -2
  58. package/src/projection/obsidian/selection-ui/selection.mjs +8 -3
  59. package/src/runtime/obsidian/app-capability.mjs +104 -12
  60. package/src/runtime/obsidian/app-production-seams.mjs +126 -39
  61. package/src/runtime/obsidian/app-registration.mjs +166 -0
  62. package/src/runtime/obsidian/engine.mjs +127 -11
  63. package/src/runtime/obsidian/extension-points.mjs +1 -1
  64. package/src/runtime/obsidian/index.mjs +5 -1
  65. package/src/runtime/obsidian/launch-plan.mjs +88 -0
  66. package/src/runtime/obsidian/lifecycle.mjs +105 -14
  67. package/src/runtime/obsidian/opening.mjs +227 -55
  68. package/src/runtime/obsidian/pending-edits.mjs +10 -0
  69. package/src/runtime/obsidian/plugin-channel.mjs +281 -0
  70. package/src/runtime/obsidian/plugin-choice.mjs +170 -0
  71. package/src/runtime/obsidian/plugin-drift.mjs +76 -0
  72. package/src/runtime/obsidian/plugin-presence.mjs +48 -0
  73. package/src/runtime/obsidian/service-main.mjs +18 -5
  74. package/src/runtime/obsidian/service-record.mjs +36 -0
  75. package/src/runtime/obsidian/service-server.mjs +50 -12
  76. package/src/runtime/obsidian/service.mjs +100 -6
  77. package/src/runtime/obsidian/state-store.mjs +13 -1
  78. package/src/server/local-sidecar.mjs +24 -5
  79. package/src/server/server.mjs +18 -5
  80. package/src/upgrade/upgrade.mjs +1 -1
  81. package/templates/external-project-workspace/pack-lifecycle.json +1 -1
package/docs/obsidian.md CHANGED
@@ -72,13 +72,18 @@ A focus keeps the full vault on disk and filters the native Graph view. The
72
72
  query is
73
73
 
74
74
  ```
75
- path:"<note path>" OR path:"<note path>" ...
75
+ path:/^<note path>$/ OR path:/^<note path>$/ ...
76
76
  ```
77
77
 
78
- in the order of the selected set (canonical-id order). Inside the quoted term
79
- a backslash and a double quote are escaped with a backslash; nothing else is
80
- changed, so a path that itself contains search operators (`tag:`, `-`, `OR`,
81
- parentheses) stays text. Paths are NFC-normalized before they are quoted. A
78
+ in the order of the selected set (canonical-id order). Each term is a regular
79
+ expression anchored at both ends, so it matches exactly one note: a plain
80
+ `path:` term matches any path that contains it, and with titles as names one
81
+ note's path can lie inside another's. In the term every regular-expression
82
+ character and the `/` delimiter is escaped with a backslash, and a space and
83
+ a double quote are written as `\x20` and `\x22`, so a path that itself
84
+ contains search operators (`tag:`, `-`, `OR`, parentheses) stays text. Paths
85
+ are NFC-normalized first. A persisted focus of the earlier query version
86
+ (`obsidian-graph-search-paths/v1`, quoted substring terms) is still read. A
82
87
  path that carries a control character, a C1 control, or the U+2028 or U+2029
83
88
  line separator cannot be a search term and refuses with
84
89
  `focus-path-unrepresentable`; so does an absolute path, a backslash path or a
@@ -256,7 +261,7 @@ nothing under `scripts/obsidian/` ships.
256
261
  | --- | --- |
257
262
  | `@mnstry/atelier/obsidian` | `src/runtime/obsidian/index.mjs`: enablement, machine settings, engine, service lifecycle, production seams, app qualification |
258
263
  | `@mnstry/atelier/obsidian/contracts` | `src/projection/obsidian/contracts.mjs`: contract validation, `selectScope`, note paths |
259
- | `@mnstry/atelier/obsidian/materialize` | `prepareView`, settings, path registry |
264
+ | `@mnstry/atelier/obsidian/materialize` | `prepareView`, settings (including the plugin's files and entry), path registry |
260
265
  | `@mnstry/atelier/obsidian/publication` | `publishView`, editor adapters, the exchange |
261
266
  | `@mnstry/atelier/obsidian/recovery` | recovery store, journals, late-writer recheck |
262
267
  | `@mnstry/atelier/obsidian/edits` | edit observation, arbitration, apply policy, source apply |
@@ -272,9 +277,10 @@ What the tarball must and must not carry, and the package proof, are in
272
277
 
273
278
  ## First vault: what to expect
274
279
 
275
- This is the path an adopter walks on one machine, in the order the command
276
- requires it. Every operation and flag below is the shipped `atelier obsidian`
277
- usage text; `atelier obsidian --help` prints it.
280
+ This is the path an adopter walks on one machine. Every operation and flag
281
+ below is the shipped `atelier obsidian` usage text; `atelier obsidian --help`
282
+ prints it. There are three steps, and none of them is done by hand in
283
+ Obsidian.
278
284
 
279
285
  1. Enable the projection in the project configuration. The member
280
286
  `ext["mnstry.atelier.obsidian"]` is an `atelier-obsidian-ext-settings/v1`
@@ -295,19 +301,54 @@ usage text; `atelier obsidian --help` prints it.
295
301
  has to carry; `policy install FILE` installs it and leaves the mode
296
302
  unchanged; `policy revoke` returns the mode to `manual` and stops every
297
303
  later apply.
298
- 3. Start the maintenance service:
299
- `atelier obsidian service start --consent-actor ID --adapter=obsidian-cli`.
300
- Reaching the installed app is never a default, so `--adapter=obsidian-cli`
301
- is required, and the consent actor records who allowed the service to run.
302
- `service status` and `service stop` manage it; `service unit --print
303
- --adapter=obsidian-cli` prints a startup unit and installs nothing.
304
- 4. Open a view: `atelier obsidian open --scope ID --adapter=obsidian-cli`.
305
- The command starts or reconnects maintenance, verifies the vault by reading
306
- it back, and asks the app (version 1.13.7 or later) to open it. Quit
307
- Obsidian before a view's first publication (on Linux, also any app that
308
- runs on a system Electron): while a process that may be Obsidian runs, the
309
- first publication stops, and `open` and `status` say so; see
310
- [First publication while Obsidian is running](#first-publication-while-obsidian-is-running).
304
+ 3. Open a view:
305
+ `atelier obsidian open --scope ID --consent-actor ID --adapter=obsidian-cli`.
306
+ That's it. Reaching the installed app is never a default, so
307
+ `--adapter=obsidian-cli` is required, and the first open of a workspace
308
+ records who allowed the maintenance service to run (`--consent-actor`;
309
+ later opens reconnect to it and need no consent). `open` starts or
310
+ reconnects maintenance and asks it for a tick, makes Obsidian (version
311
+ 1.13.7 or later) know the view's vault as one of its vaults, opens it,
312
+ publishes the view, verifies the vault by reading it back, and waits until
313
+ the app answers for exactly that vault. It does this in whatever state
314
+ Obsidian is in; see
315
+ [What `open` does in each state of Obsidian](#what-open-does-in-each-state-of-obsidian).
316
+
317
+ The maintenance service can also be managed on its own:
318
+ `atelier obsidian service start --consent-actor ID --adapter=obsidian-cli`,
319
+ `service status` and `service stop`; `service unit --print
320
+ --adapter=obsidian-cli` prints a startup unit and installs nothing.
321
+
322
+ After an upgrade of Atelier, a maintenance service started earlier still runs
323
+ the earlier release. `open` replaces it: when the service of this workspace
324
+ proves itself Atelier's own but runs an earlier release than the installed one
325
+ (its record names the release it runs: the package version and a digest of
326
+ its modules; an earlier version, the same version with other modules, or no
327
+ release named), or refuses a tick that names a view as releases up to
328
+ 0.2.0-alpha.11 do,
329
+ `open` stops it as `service stop` would and starts the installed release under
330
+ the consent already recorded, and says `service: restarted (outdated)`. A
331
+ service of a later release is never replaced by an earlier one, so two
332
+ installations used on one workspace (a global and a project-local one, say)
333
+ do not replace each other's service on every open: `open` answers
334
+ `service-unavailable` / `service-other-release`; run `atelier obsidian service
335
+ stop`, then open again, or open with the later release. A service in a long
336
+ tick is not stopped (`open` answers `busy`), and a listener that does not
337
+ prove itself this workspace's service is never touched.
338
+
339
+ The first time Obsidian opens a view's vault it asks "Do you trust the author
340
+ of this vault?", because every vault Atelier publishes carries Atelier's own
341
+ plugin. "Trust author and enable plugins" turns it on: a status bar item then
342
+ says whether the view is current, updating, held for an edit you made, stale,
343
+ or whether the maintenance service does not answer, and "Atelier: show status"
344
+ says why. "Browse vault in Restricted Mode" keeps every community plugin off
345
+ in that vault, and nothing else changes: maintenance, `open` and `status` work
346
+ without the plugin, as they always did. Obsidian keeps the answer per vault in
347
+ its own storage and asks again the next time until the vault is trusted or
348
+ restricted mode is switched on for good in its settings. Turning the plugin
349
+ off in a vault, or uninstalling it there, is respected from then on;
350
+ `atelier obsidian plugin on --scope ID` brings it back. What the plugin does
351
+ and never does is in [obsidian-plugin.md](obsidian-plugin.md).
311
352
 
312
353
  `open` and `status` answer with a freshness state, not a promise. `current`
313
354
  means the vault is the present generation, verified by read-back, and the app
@@ -319,7 +360,9 @@ and the view is not republished over it. The remaining outcomes (`indexing`,
319
360
  `publisher-conflict`, `app-missing`, `app-version-unsupported`,
320
361
  `app-cli-unavailable`, `launch-failed`, `service-unavailable`, `busy`,
321
362
  `disabled`) each name what to do next, and exit code 3 says the answer is not
322
- success.
363
+ success. Both also report Atelier's plugin for each view, under `plugin`:
364
+ present while it holds the vault open in the app, with the app and plugin
365
+ versions, or not present with the reason.
323
366
 
324
367
  Edits made in the vault are preserved before anything is republished. A body
325
368
  replacement is held as a pending edit; under manual mode it reaches its source
@@ -342,34 +385,120 @@ absolute directory. Private state inside an enrolled repository is refused.
342
385
  Publication is proven on macOS arm64 only and is refused on Windows; see
343
386
  [Known limits](#known-limits).
344
387
 
345
- ### First publication while Obsidian is running
346
-
347
- The publisher writes into a vault only when it can coordinate with every
388
+ ### What `open` does in each state of Obsidian
389
+
390
+ Obsidian opens a vault from a link only when the folder is in its own vault
391
+ list, the `vaults` of `obsidian.json` in its user-data directory. `open` puts
392
+ the view's vault there first, and never asks a person to open a folder by
393
+ hand. It then names the vault by its id (`obsidian://open?vault=<id>`), never
394
+ by path where the id reaches it first: a path is matched against the list by
395
+ string prefix, so a link for a note under `/A/strategy-lab` can open the
396
+ listed vault `/A/strategy`. When another vault's folder is named like the id,
397
+ or the id is unusual, the vault's own path is used, which the app matches
398
+ exactly because the folder is listed.
399
+
400
+ - **Obsidian runs and answers its command line (any vault is open).** `open`
401
+ reads the app's vault list through its command line. A vault the app does
402
+ not list is added and opened in a new window through the app itself (it
403
+ writes its own settings), and read back from its list. `open`
404
+ then waits, bounded, until the app answers for exactly this vault, and asks
405
+ maintenance for the view again: the publication runs through the app, which
406
+ now holds the vault, with every editor check of the publication protocol.
407
+ Obsidian does not have to be quit.
408
+ - **Obsidian is not running.** The view is published on the path with no app,
409
+ as before. `open` then adds the vault to the app's settings file (see
410
+ [Obsidian's vault list](obsidian-contract.md#obsidians-vault-list) for
411
+ exactly when and how that one file is written, marked to reopen) and starts
412
+ Obsidian plainly, with no link: an Obsidian started with a link opens only
413
+ that vault and drops the reopen flag of every other one, so your other
414
+ vaults would not come back the next time you start it. Started plainly, it
415
+ reopens every vault it had open, this one included, and once its command
416
+ line answers (the app itself, not the tool's "unable to find Obsidian")
417
+ `open` hands it the vault's link through that tool, never through the
418
+ operating system. On Linux a plain start is not
419
+ qualified yet, and Obsidian is started with the link (the other vaults'
420
+ reopen marks are lost there; a known limit).
421
+ An Obsidian that never ran on this account has no settings file yet, and
422
+ `open` does not create one: start Obsidian once, then open again.
423
+ - **Obsidian runs with no vault open.** Its command line then answers every
424
+ command with "Vault not found.", so nothing can be asked of it, and its
425
+ settings file belongs to the running app, so it is only read. A vault the
426
+ app already lists is opened by path and published through the app as
427
+ above. A vault it does not list yet is answered as outcome
428
+ `app-version-unsupported` with reason `no-vault-open`: open any vault in
429
+ Obsidian, or quit it, and open again. This is the one state of a running
430
+ Obsidian that `open` cannot get through alone.
431
+ - **Obsidian's command line is turned off** (the default of a new
432
+ installation). It then answers every command with "Command line interface
433
+ is not enabled", and only a link reaches it. With Obsidian running, `open`
434
+ adds and launches nothing. With Obsidian quit, `open` adds the vault to its
435
+ settings and starts it, and Obsidian opens the vault. Either way `open` then
436
+ answers `app-cli-unavailable` with reason `cli-turned-off` and says where to
437
+ turn it on: Settings > General > Advanced > Command line interface.
438
+ Publishing into a vault Obsidian holds needs it until Atelier's plugin takes
439
+ over that part.
440
+
441
+ Only `open` adds a vault to Obsidian: the maintenance service never does, so
442
+ it never opens a window nobody asked for. A declared view that was never
443
+ opened is published while Obsidian is quit, and `open --scope ID` adds it.
444
+
445
+ In no state does `open` add a view's vault inside a folder Obsidian already
446
+ lists as a vault (your home folder, say): that vault would show the view's
447
+ notes too, and a command-line call run in the view's folder would reach it.
448
+ `open` answers `launch-failed` with reason `vault-inside-another-vault`:
449
+ remove that vault from Obsidian's vault list, or keep Atelier's data root
450
+ outside that folder. A view's vault that Obsidian lists already, below such a
451
+ vault, is reached by its id instead (see "Which window answers" in
452
+ [Known limits](#known-limits)).
453
+
454
+ Obsidian can list one folder more than once, under another letter case or
455
+ through a link, and then open it in one window per entry. Each of those
456
+ windows holds the view's vault, and a publication coordinates with one window
457
+ only, so none is made: the view reports `publisher-conflict` with reason
458
+ `vault-open-in-several-windows`, and `open` answers the same, names the
459
+ entries (`open in Obsidian as: …`; `duplicates` in JSON) and launches nothing.
460
+ Remove the extra entries from Obsidian's vault list and open again. Closing
461
+ the extra windows is not enough: Obsidian keeps the last window it closed
462
+ marked open, so two entries can stay marked open with one window showing, and
463
+ the view stays refused until the list names the folder once.
464
+ While one entry of the folder has a window, `open` reaches only that one and
465
+ never opens another entry of the same folder beside it.
466
+
467
+ The publisher still writes into a vault only when it can coordinate with every
348
468
  Obsidian that may hold it, or when the process table shows, positively, that
349
469
  none runs. Otherwise it stops, and the view reports `publisher-conflict` with
350
470
  reason `editor-uncoordinated`. That reason has several causes, and the report
351
471
  does not say which: Obsidian runs without this vault open, or with it open but
352
472
  its command line did not answer; the process table could not be read, or on
353
473
  Linux shows an app that runs on a system Electron, which may be Obsidian; the
354
- app's version was not checked when the publication began (it started just
355
- then, or on macOS it is installed outside `/Applications`; see "Known
356
- limits"); or an app started while the view was being published with the app
357
- closed, which stops that publication partway. Before a first publication no
358
- app has the vault open, so only the first way on applies: quit Obsidian (on
359
- Linux, also any app that runs on a system Electron). The service then
360
- publishes the view directly, and
361
- `atelier obsidian open --scope ID --adapter=obsidian-cli` starts Obsidian on
362
- it. `status` and `open` give that as the next step. Once a view
363
- has been published, the last published vault stays in place and there is a
364
- second way on: open it in the running app as it is with
365
- `atelier obsidian open --allow-stale`, which waits, bounded, until the app
366
- answers for it. The publication is retried automatically either way.
367
-
368
- An Obsidian with no vault open at all answers its command line with "Vault not
369
- found." for every command, its version included. That is reported as outcome
370
- `app-version-unsupported` with reason `no-vault-open`, under `service.app` in
371
- `status` and by `open`. Opening any vault in Obsidian resolves it, because the
372
- version can then be read, and so does quitting Obsidian.
474
+ app's version was not checked when the publication began; or an app started
475
+ while the view was being published with the app closed. `status` gives
476
+ `atelier obsidian open` as the next step (it adds the vault to Obsidian and
477
+ publishes through it), or quitting Obsidian (on Linux, also any app that runs
478
+ on a system Electron). When `open` itself already did that and the
479
+ publication still stopped, it says to quit Obsidian and open again.
480
+
481
+ The tick `open` asks for prepares and publishes its view once more, whatever
482
+ state the view is in. A view whose last publication did not settle (refused,
483
+ stale or still updating) is also tried again without waiting for a change: as
484
+ soon as the app looks different (it quit or started, or opened or closed a
485
+ vault in its list), and otherwise after a delay that starts at 30 seconds and
486
+ doubles per attempt, up to the full reconciliation every five minutes. What
487
+ the app looks like is read from the process table and the app's vault list
488
+ alone: maintenance runs nothing in Obsidian to find out, and asks it for its
489
+ version only when a view is about to be published, without blocking.
490
+
491
+ While Atelier's plugin holds the view's vault open in the app and the command
492
+ line gives no version (as it answers while a vault is still loading), the
493
+ version the plugin reports stands in (reason `plugin-reported`); a version the
494
+ command line gives always decides, and while the process table shows no app
495
+ running, a plugin's report counts for nothing. `open` then asks the command
496
+ line nothing: a vault the app's settings file lists is opened by path, and one
497
+ it does not show is answered as `app-cli-unavailable` with reason
498
+ `vault-open-cli-silent` (the app has the vault open; make sure its
499
+ command-line interface is turned on). So is a listed vault the command line
500
+ does not answer for once it is opened, while only the plugin reports the
501
+ version: `open` says so at once instead of waiting for the app.
373
502
 
374
503
  ## Known limits
375
504
 
@@ -402,8 +531,10 @@ test reported as a pass.
402
531
  built on it does not coordinate with any app it then finds running, and
403
532
  asks it nothing. That publication stops as `publisher-conflict` with reason
404
533
  `editor-uncoordinated`, and the next publication asks again and reads the
405
- version. A view that stopped is tried again on its next change, or at the
406
- next full reconciliation, every five minutes.
534
+ version. A view that stopped is tried again on the next tick `open` asks
535
+ for, as soon as the app looks different, and otherwise after a delay that
536
+ starts at 30 seconds and doubles, up to the full reconciliation every five
537
+ minutes. A tick `open` asks for never reuses a remembered app answer.
407
538
  - Install location (macOS): the app is found only at
408
539
  `/Applications/Obsidian.app`, and its command-line tool only inside it. An
409
540
  Obsidian installed elsewhere, or a renamed bundle, is `app-missing`.
@@ -417,9 +548,46 @@ test reported as a pass.
417
548
  published through the app. `status` (under `service.app`) and `open` say what
418
549
  to do: open any vault in Obsidian, or quit Obsidian. With the app quit,
419
550
  maintenance publishes on its own path again; with a vault open, it reads
420
- the version again on a later tick. `open` launches nothing while the app
421
- runs with no vault open, and after its own launch it waits, bounded, while
422
- the app is still opening the vault.
551
+ the version again on a later tick. While the app runs with no vault open,
552
+ `open` has it open a vault only when its list already knows that vault,
553
+ by path; one it does not know cannot be added then (see below). After its
554
+ own launch `open` waits, bounded, while the app is still opening the vault;
555
+ an app whose vault window is still loading answers a command with `Error:
556
+ Command "…" not found`, which is read as not up yet, never as a version.
557
+ - Obsidian's settings file: `open` adds a view's vault to the app's own list,
558
+ `obsidian.json` in its user-data directory: `~/Library/Application
559
+ Support/obsidian` on macOS and `$XDG_CONFIG_HOME/obsidian` (else
560
+ `~/.config/obsidian`) on Linux. It is written only while the process table
561
+ shows, positively, that no Obsidian runs, only when Obsidian created it
562
+ before, and with a backup beside it; see
563
+ [Obsidian's vault list](obsidian-contract.md#obsidians-vault-list). A
564
+ Flatpak or snap build keeps its list inside its sandbox and never reads that
565
+ file. Which build is in use is read from which vault list was written last
566
+ (every build rewrites its own when a vault window opens or closes), and only
567
+ when no build wrote one from its installation; for a Flatpak or snap build
568
+ the file is then neither read nor written: `open` answers
569
+ `obsidian-sandboxed` and says to start Obsidian with any vault open, then
570
+ open again, and adds the vault through the app. On Windows no location is
571
+ known, and `open` adds the vault through the app in the same way.
572
+ - Recent items: adding a vault through the running app is Obsidian's own
573
+ "open folder as vault", which also puts the vault's folder in the operating
574
+ system's recently used documents (Recent Items on macOS).
575
+ - Which window answers: Obsidian answers a command-line call that names a
576
+ vault (`vault=<id>` first) in that vault's window; any other call in the
577
+ window of the first vault in its list whose folder is the tool's working
578
+ directory or contains it, and otherwise in the vault window that had focus
579
+ last. A vault that takes a call is opened when it is closed. Calls about a
580
+ vault therefore name its id; only when that id would name another vault
581
+ first (one whose folder has the id as its name) do they run inside its
582
+ folder instead, and not at all when a vault listed before it at a folder
583
+ above it (your home folder, say) would take them there. The vault root is
584
+ taken in the spelling the file system stores, so a data root given in
585
+ another letter case on macOS changes nothing. Publication calls do this
586
+ only while the app's list shows the view's vault open, and run in a
587
+ directory that is no vault otherwise. So maintenance never reopens a vault
588
+ window you closed while Obsidian keeps running, and never reaches another
589
+ vault: publication then stops as `publisher-conflict` until `atelier
590
+ obsidian open` opens the vault again or Obsidian quits.
423
591
  - Detecting the app: the process table is read with `ps -A -o comm=`
424
592
  (`pid=,comm=` on Linux), and the app is recognised by the executable a
425
593
  process runs, never by its arguments, so a path argument that contains an
@@ -469,6 +637,15 @@ test reported as a pass.
469
637
  - An edit the byte lens cannot turn into source bytes (a new or changed link
470
638
  to another note of the vault, an edited front matter) becomes a copy-only
471
639
  proposal. No operation applies one.
640
+ - Atelier's plugin (phase 1) reports presence and status and decides
641
+ nothing: publication still coordinates with the app through its
642
+ command-line tool, which must be enabled, and the plugin only supplies the
643
+ app version where that tool gives none. A person who turns the plugin off
644
+ in a vault, or uninstalls it there, is followed: its entry is not added
645
+ back and a deleted folder is not made again until the person turns it on
646
+ in Obsidian's settings or runs
647
+ `atelier obsidian plugin on --scope ID`. Plugin files are never removed from
648
+ a vault. See [obsidian-plugin.md](obsidian-plugin.md).
472
649
  - Acceptance: a schema-valid receipt closes no gate, the package proof closes
473
650
  no gate, and no adopter acceptance is recorded in this repository.
474
651
 
@@ -482,3 +659,9 @@ The test suite proves each oracle can fail:
482
659
  - `createReceiptValidatorForOracleTests` accepts a rule table with one rule
483
660
  switched off; every negative case that depends on that rule then passes,
484
661
  and even that closes nothing.
662
+ - `openScopeForOracleTests` with an `OPENING_PRIMITIVES.keptByApp` that
663
+ never holds: an open with Obsidian running without the vault reports the
664
+ publisher conflict it could have cleared, and the first-open oracle fails.
665
+ - `createMaintenanceEngineForOracleTests` with an
666
+ `ENGINE_PRIMITIVES.isRetryDue` that never holds: a refused view waits for
667
+ the full reconciliation, and the retry oracle fails.
@@ -63,7 +63,13 @@ Shipped, and required by the audit:
63
63
  - Every `contracts/atelier-obsidian-<name>.v<n>.schema.json` present in the
64
64
  source tree (lowercase name, versioned). Each must be packed and exported
65
65
  under its own path, and the audit fails if it finds none.
66
- - `docs/obsidian.md` and `docs/obsidian-contract.md`.
66
+ - `docs/obsidian.md`, `docs/obsidian-contract.md` and `docs/obsidian-plugin.md`.
67
+ - Atelier's own Obsidian plugin, which every managed vault receives:
68
+ `plugins/obsidian/manifest.json`, `main.js` and `styles.css`, the only files
69
+ admitted under `plugins/`, with its channel
70
+ (`src/projection/obsidian/plugin-bridge/index.mjs`) and the service's half
71
+ (`src/runtime/obsidian/plugin-channel.mjs`). `main.js` is scanned for egress
72
+ like every packed script.
67
73
  - `fixtures/obsidian/**`: small synthetic text only. Every packed fixture, in
68
74
  any subtree and spelling under `fixtures/`, is at most 262,144 bytes. The
69
75
  acceptance receipts under `fixtures/obsidian/acceptance/receipts/` are
package/docs/upgrade.md CHANGED
@@ -9,6 +9,26 @@ workspace between Atelier package releases. The flow is local-only: it does not
9
9
  provision repositories, contact a Git host, mutate the MNSTRY runtime, or write
10
10
  through a browser view.
11
11
 
12
+ ## Upgrading to 0.2.0-alpha.12
13
+
14
+ This release makes the first open of an Obsidian view automatic and ships
15
+ Atelier's own plugin. `open` adds the view's vault to Obsidian itself, whether
16
+ Obsidian is running or not, and publishes without quitting it; a closed
17
+ Obsidian is started so your other vaults reopen. Vaults are laid out
18
+ readably: folders mirror your repository and file names are note titles,
19
+ with identity kept in a properties block; a vault published by an earlier
20
+ release is laid out again once, keeping held edits. Each managed vault gets
21
+ the Atelier plugin (one trust prompt per vault) with a status bar. A
22
+ maintenance service of an earlier release is replaced by `open` under the
23
+ consent already recorded. No project or machine setting needs to change.
24
+ Machine settings stay v1; generation manifests move to v2 (below).
25
+
26
+ Node.js 24 (24.13.1 or later) is now supported alongside Node.js 22, and
27
+ both write byte-identical graph files, so people on either can share a
28
+ repository. `atelier dev` on a port that is already taken now says
29
+ `port-in-use` and names `--port=<free port>`; before, it printed an internal
30
+ error. A script that read that failure's exit code sees 2 instead of 1.
31
+
12
32
  ## Upgrading to 0.2.0-alpha.11
13
33
 
14
34
  This release tightens the Obsidian projection's app checks after independent
@@ -163,7 +183,7 @@ See `docs/contract-stability.md`.
163
183
  ## Before You Upgrade
164
184
 
165
185
  - Start from a clean Git status in the copied workspace.
166
- - Confirm Node.js matches the package range: `>=22.18.0 <23`.
186
+ - Confirm Node.js matches the package range: `>=22.18.0 <23 || >=24.13.1 <25`.
167
187
  - Keep this package root and copied starter workspaces separate.
168
188
  - Keep upgrade diffs generic: placeholders only, no project-specific private
169
189
  material, transcripts, support bundles, local absolute paths, local agent
@@ -185,7 +205,7 @@ For registry installs, pin the exact version and record the resolved version
185
205
  in the lockfile:
186
206
 
187
207
  ```bash
188
- npm install --save-dev @mnstry/atelier@0.2.0-alpha.11
208
+ npm install --save-dev @mnstry/atelier@0.2.0-alpha.12
189
209
  npx mnstry-atelier lock write --project ./atelier.project.json
190
210
  ```
191
211
 
@@ -193,7 +213,7 @@ For Git installs, pin the release tag rather than a branch, so the lock file
193
213
  records exactly what was reviewed:
194
214
 
195
215
  ```bash
196
- npm install --save-dev "git+https://github.com/MNSTRY/atelier.git#v0.2.0-alpha.11"
216
+ npm install --save-dev "git+https://github.com/MNSTRY/atelier.git#v0.2.0-alpha.12"
197
217
  npx mnstry-atelier lock write --project ./atelier.project.json
198
218
  ```
199
219
 
@@ -0,0 +1,160 @@
1
+ {
2
+ "schema": "atelier-obsidian-generation-manifest/v2",
3
+ "layoutVersion": 2,
4
+ "generationId": "gen-0001",
5
+ "scopeId": "scope-topic",
6
+ "snapshotId": "snap-0001",
7
+ "notes": [
8
+ {
9
+ "repoId": "north",
10
+ "nodeId": "a",
11
+ "title": "Shared concept",
12
+ "path": "north/topics/Shared concept.md",
13
+ "noteDigest": "sha256:5555555555555555555555555555555555555555555555555555555555555555",
14
+ "regions": {
15
+ "frontmatter": {
16
+ "start": 0,
17
+ "end": 120
18
+ },
19
+ "identity": {
20
+ "start": 40,
21
+ "end": 116
22
+ },
23
+ "body": {
24
+ "start": 120,
25
+ "end": 280
26
+ },
27
+ "generated": [
28
+ {
29
+ "kind": "relations",
30
+ "range": {
31
+ "start": 280,
32
+ "end": 340
33
+ }
34
+ }
35
+ ]
36
+ }
37
+ },
38
+ {
39
+ "repoId": "north",
40
+ "nodeId": "d",
41
+ "title": "Shared concept",
42
+ "path": "north/topics/shared CONCEPT.md",
43
+ "noteDigest": "sha256:6666666666666666666666666666666666666666666666666666666666666666",
44
+ "regions": {
45
+ "frontmatter": {
46
+ "start": 0,
47
+ "end": 40
48
+ },
49
+ "body": {
50
+ "start": 40,
51
+ "end": 200
52
+ },
53
+ "generated": [
54
+ {
55
+ "kind": "relations",
56
+ "range": {
57
+ "start": 200,
58
+ "end": 260
59
+ }
60
+ },
61
+ {
62
+ "kind": "identity",
63
+ "range": {
64
+ "start": 260,
65
+ "end": 340
66
+ }
67
+ }
68
+ ],
69
+ "identity": {
70
+ "start": 260,
71
+ "end": 340
72
+ }
73
+ }
74
+ },
75
+ {
76
+ "repoId": "north",
77
+ "nodeId": "e",
78
+ "title": "Diagram",
79
+ "path": "north/figures/diagram.svg.md",
80
+ "noteDigest": "sha256:8888888888888888888888888888888888888888888888888888888888888888",
81
+ "regions": {
82
+ "frontmatter": {
83
+ "start": 0,
84
+ "end": 80
85
+ },
86
+ "identity": {
87
+ "start": 0,
88
+ "end": 80
89
+ },
90
+ "body": {
91
+ "start": 240,
92
+ "end": 240
93
+ },
94
+ "generated": [
95
+ {
96
+ "kind": "representation",
97
+ "range": {
98
+ "start": 80,
99
+ "end": 240
100
+ }
101
+ }
102
+ ]
103
+ }
104
+ }
105
+ ],
106
+ "links": [
107
+ {
108
+ "edgeId": "e4",
109
+ "sourceNodeId": "d",
110
+ "targetNodeId": "a",
111
+ "type": "links_to",
112
+ "origin": "markdown",
113
+ "targetState": "in-scope",
114
+ "inversions": [
115
+ {
116
+ "source": {
117
+ "start": 12,
118
+ "end": 30
119
+ },
120
+ "note": {
121
+ "start": 52,
122
+ "end": 96
123
+ }
124
+ }
125
+ ]
126
+ },
127
+ {
128
+ "edgeId": "e5",
129
+ "sourceNodeId": "a",
130
+ "targetNodeId": "d",
131
+ "type": "supersedes",
132
+ "origin": "declared",
133
+ "targetState": "in-scope"
134
+ },
135
+ {
136
+ "edgeId": "e1",
137
+ "sourceNodeId": "a",
138
+ "targetNodeId": "b",
139
+ "type": "related",
140
+ "origin": "declared",
141
+ "targetState": "outside-selection"
142
+ }
143
+ ],
144
+ "attachments": [
145
+ {
146
+ "path": "north/figures/diagram.svg",
147
+ "digest": "sha256:7777777777777777777777777777777777777777777777777777777777777777",
148
+ "byteLength": 900
149
+ }
150
+ ],
151
+ "completeness": {
152
+ "status": "complete",
153
+ "expectedNotes": 3,
154
+ "writtenNotes": 3
155
+ },
156
+ "freshness": {
157
+ "status": "current",
158
+ "checkedAt": "2026-01-05T10:00:05Z"
159
+ }
160
+ }