@mnstry/atelier 0.2.0-alpha.10 → 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 +385 -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 +681 -39
  9. package/docs/obsidian-plugin.md +585 -0
  10. package/docs/obsidian.md +292 -48
  11. package/docs/release-engineering.md +7 -1
  12. package/docs/upgrade.md +32 -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 +124 -17
  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 +115 -14
  60. package/src/runtime/obsidian/app-production-seams.mjs +126 -37
  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 +240 -51
  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 +19 -4
  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,15 +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.
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).
307
352
 
308
353
  `open` and `status` answer with a freshness state, not a promise. `current`
309
354
  means the vault is the present generation, verified by read-back, and the app
@@ -315,7 +360,9 @@ and the view is not republished over it. The remaining outcomes (`indexing`,
315
360
  `publisher-conflict`, `app-missing`, `app-version-unsupported`,
316
361
  `app-cli-unavailable`, `launch-failed`, `service-unavailable`, `busy`,
317
362
  `disabled`) each name what to do next, and exit code 3 says the answer is not
318
- 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.
319
366
 
320
367
  Edits made in the vault are preserved before anything is republished. A body
321
368
  replacement is held as a pending edit; under manual mode it reaches its source
@@ -338,17 +385,120 @@ absolute directory. Private state inside an enrolled repository is refused.
338
385
  Publication is proven on macOS arm64 only and is refused on Windows; see
339
386
  [Known limits](#known-limits).
340
387
 
341
- ### First publication while Obsidian is running
342
-
343
- The publisher writes into a vault only when it can coordinate with every
344
- Obsidian that might have it open. An Obsidian that is running without this
345
- vault open cannot be coordinated with, so the first publication of a view is
346
- refused as `publisher-conflict` with reason `editor-uncoordinated`, and
347
- nothing is written. Quit Obsidian: the service then publishes the view
348
- directly, and `atelier obsidian open --scope ID --adapter=obsidian-cli` starts
349
- Obsidian on it. An Obsidian with no vault open at all answers its command line
350
- with "Vault not found." for every command, its version included; that is
351
- reported as reason `no-vault-open`, and the same remedy applies.
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
468
+ Obsidian that may hold it, or when the process table shows, positively, that
469
+ none runs. Otherwise it stops, and the view reports `publisher-conflict` with
470
+ reason `editor-uncoordinated`. That reason has several causes, and the report
471
+ does not say which: Obsidian runs without this vault open, or with it open but
472
+ its command line did not answer; the process table could not be read, or on
473
+ Linux shows an app that runs on a system Electron, which may be Obsidian; the
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.
352
502
 
353
503
  ## Known limits
354
504
 
@@ -375,7 +525,22 @@ test reported as a pass.
375
525
  is admitted although the protocol depends on the view's undocumented
376
526
  `lastSavedData` field, and the protocol cases have not been re-run on any
377
527
  later release. See the "App capability floor" row of
378
- [obsidian-contract.md](obsidian-contract.md).
528
+ [obsidian-contract.md](obsidian-contract.md). An app that was not running
529
+ when an adapter was qualified had no version to check, and neither had an
530
+ app that was not found. Such an answer is never reused, and the adapter
531
+ built on it does not coordinate with any app it then finds running, and
532
+ asks it nothing. That publication stops as `publisher-conflict` with reason
533
+ `editor-uncoordinated`, and the next publication asks again and reads the
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.
538
+ - Install location (macOS): the app is found only at
539
+ `/Applications/Obsidian.app`, and its command-line tool only inside it. An
540
+ Obsidian installed elsewhere, or a renamed bundle, is `app-missing`.
541
+ Nothing is published through it, publication waits while it runs (the
542
+ process probe still sees it), and `open` answers `app-missing` and cannot
543
+ start it. With it quit, the view is published on the path with no app.
379
544
  - No vault open: while Obsidian runs with no vault open, its command-line tool
380
545
  answers every command, `version` included, with `Vault not found.`. The
381
546
  version floor cannot be checked then, so the app does not qualify:
@@ -383,23 +548,87 @@ test reported as a pass.
383
548
  published through the app. `status` (under `service.app`) and `open` say what
384
549
  to do: open any vault in Obsidian, or quit Obsidian. With the app quit,
385
550
  maintenance publishes on its own path again; with a vault open, it reads
386
- the version again on a later tick. `open` launches nothing while the app
387
- runs with no vault open, and after its own launch it waits, bounded, while
388
- the app is still opening the vault.
389
- - Detecting the app: the process table is read with `ps -A -o comm=`, and the
390
- app is recognised by the executable a process runs, never by its arguments,
391
- so a path argument that contains an `obsidian` directory (the maintenance
392
- service itself, anything under the private data root) is not the app. On
393
- macOS the executable `Obsidian` and the bundle's `Obsidian Helper`
394
- processes count, in any directory. On Linux a process named `obsidian`
395
- counts, which covers the .deb, snap, AppImage and Flatpak builds. A
396
- distribution that runs the app under a system Electron shows only
397
- `electron`: the table is then `unknown`, which is treated as a running app,
398
- so publication through the app-free path waits until no such process runs.
399
- The command-line tool (`obsidian-cli`) is a client of the app and is not
400
- counted. A process renamed by its packager, an app on another machine and an
401
- app that starts after a reading are not seen; see "The path with no app" in
402
- [obsidian-contract.md](obsidian-contract.md).
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.
591
+ - Detecting the app: the process table is read with `ps -A -o comm=`
592
+ (`pid=,comm=` on Linux), and the app is recognised by the executable a
593
+ process runs, never by its arguments, so a path argument that contains an
594
+ `obsidian` directory (the maintenance service itself, anything under the
595
+ private data root) is not the app. A `ps` that has not answered within five
596
+ seconds is killed and the reading is `unknown`, which is treated as a
597
+ running app.
598
+ - macOS: the executable `Obsidian` and the bundle's `Obsidian Helper`
599
+ processes count, in any directory. An app started through a differently
600
+ named link shows that name for its main process; its `Obsidian Helper`
601
+ processes still count.
602
+ - Linux: a process named `obsidian` counts, which covers the .deb, snap,
603
+ AppImage and Flatpak builds. Linux names a process after the path it was
604
+ started through, so for a name that is not recognised the probe reads
605
+ the executable `/proc/<pid>/exe` resolves to: an app started through a
606
+ link or launcher named `obs`, or a child started through
607
+ `/proc/self/exe` whose executable is readable, still counts. A process
608
+ whose executable this user may not read (another user's, or one of this
609
+ user's that hides it from tracing, such as ssh-agent) or that exited
610
+ meanwhile is judged by its name alone. Any other read failure, or a
611
+ table in which no executable resolves at all, is `unknown`. A
612
+ distribution that runs the app under a system Electron shows only
613
+ `electron`: the table is then `unknown`, so publication through the
614
+ app-free path waits until no such process runs.
615
+ - The command-line tool (`obsidian-cli`) is a client of the app and is not
616
+ counted. An app packaged under another executable name, an app on
617
+ another machine and an app that starts after a reading are not seen; see
618
+ "The path with no app" in [obsidian-contract.md](obsidian-contract.md).
619
+ - The app-free path is live: before 0.2.0-alpha.10 the probe always found
620
+ Atelier's own service, so publication never took the path with no app.
621
+ From 0.2.0-alpha.10 it does, whenever the process table shows, positively,
622
+ that no Obsidian runs. It applies only then. The table is read at path
623
+ selection, again immediately before the first note, and again whenever two
624
+ seconds have passed. An app that starts between two readings is not seen
625
+ for at most two seconds plus one note's publication, and in that window a
626
+ note is exchanged with no editor check. Bytes on disk stay protected by the
627
+ on-disk comparison and the exchange. An unsaved buffer in the newly started
628
+ app does not: the app applies its own external-modification merge, which
629
+ can drop overlapping edits. Once a reading shows an app, every remaining
630
+ note refuses. To avoid the window, do not start Obsidian while a view is
631
+ being published with the app closed.
403
632
  - CRLF sources: the editor normalizes line endings when it saves, so a body
404
633
  edit made in the vault to a note whose source uses CRLF is more than a body
405
634
  replacement to the edit lens. It is preserved and becomes a proposal; it is
@@ -408,6 +637,15 @@ test reported as a pass.
408
637
  - An edit the byte lens cannot turn into source bytes (a new or changed link
409
638
  to another note of the vault, an edited front matter) becomes a copy-only
410
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).
411
649
  - Acceptance: a schema-valid receipt closes no gate, the package proof closes
412
650
  no gate, and no adopter acceptance is recorded in this repository.
413
651
 
@@ -421,3 +659,9 @@ The test suite proves each oracle can fail:
421
659
  - `createReceiptValidatorForOracleTests` accepts a rule table with one rule
422
660
  switched off; every negative case that depends on that rule then passes,
423
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,35 @@ 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
+
32
+ ## Upgrading to 0.2.0-alpha.11
33
+
34
+ This release tightens the Obsidian projection's app checks after independent
35
+ review of 0.2.0-alpha.10: Atelier never coordinates with an Obsidian whose version it
36
+ has not checked, the advice for a refused publication is true for every cause
37
+ and appears in `status` as well as `open`, Linux resolves an app started
38
+ through a differently named launcher, and the process check has a timeout.
39
+ No project or machine setting needs to change.
40
+
12
41
  ## Upgrading to 0.2.0-alpha.10
13
42
 
14
43
  This release fixes the first activation of the Obsidian projection. In
@@ -154,7 +183,7 @@ See `docs/contract-stability.md`.
154
183
  ## Before You Upgrade
155
184
 
156
185
  - Start from a clean Git status in the copied workspace.
157
- - 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`.
158
187
  - Keep this package root and copied starter workspaces separate.
159
188
  - Keep upgrade diffs generic: placeholders only, no project-specific private
160
189
  material, transcripts, support bundles, local absolute paths, local agent
@@ -176,7 +205,7 @@ For registry installs, pin the exact version and record the resolved version
176
205
  in the lockfile:
177
206
 
178
207
  ```bash
179
- npm install --save-dev @mnstry/atelier@0.2.0-alpha.10
208
+ npm install --save-dev @mnstry/atelier@0.2.0-alpha.12
180
209
  npx mnstry-atelier lock write --project ./atelier.project.json
181
210
  ```
182
211
 
@@ -184,7 +213,7 @@ For Git installs, pin the release tag rather than a branch, so the lock file
184
213
  records exactly what was reviewed:
185
214
 
186
215
  ```bash
187
- npm install --save-dev "git+https://github.com/MNSTRY/atelier.git#v0.2.0-alpha.10"
216
+ npm install --save-dev "git+https://github.com/MNSTRY/atelier.git#v0.2.0-alpha.12"
188
217
  npx mnstry-atelier lock write --project ./atelier.project.json
189
218
  ```
190
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
+ }