@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/CHANGELOG.md CHANGED
@@ -1,5 +1,390 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0-alpha.12
4
+
5
+ ### Added
6
+
7
+ - Atelier's own Obsidian plugin (phase 1: presence and status). Every vault
8
+ the maintenance service publishes now carries it under
9
+ `.obsidian/plugins/atelier-projection/` with its entry in
10
+ `.obsidian/community-plugins.json`; the person never installs anything, and
11
+ Obsidian asks once per vault whether to trust the vault's plugins. Once
12
+ trusted, a status bar item says whether the view is current, updating, held
13
+ for an edit, stale or out of reach of the service, and "Atelier: show
14
+ status" says why. The plugin writes nothing, runs nothing it is sent and
15
+ reaches only the workspace's maintenance service on its literal loopback
16
+ address. Without it (restricted mode, an app older than 1.13.7, before the
17
+ prompt is answered, or turned off in the vault) everything works as before.
18
+ See `docs/obsidian-plugin.md`.
19
+ - A person who turns Atelier's plugin off in a vault, or uninstalls it there,
20
+ is followed. Once the app has the plugin's entry in
21
+ `community-plugins.json` (it was in place before an app opened the vault,
22
+ or the plugin ran there), a list without it is recorded as the person's
23
+ decision (private state, `state/plugin/choices/`): the entry is not added
24
+ back, a deleted plugin folder is not made again, and the plugin files still
25
+ there are kept current. An entry published while an app held the vault is
26
+ only offered until then, since that app may write back the list it read
27
+ before, and a list without it is no decision of the person's. `status`, `open` and the new
28
+ `atelier obsidian plugin show` report `turned-off-in-this-vault`. Turning the
29
+ plugin on again in Obsidian's settings is followed the same way;
30
+ `atelier obsidian plugin on --scope ID` brings the entry and the files back
31
+ (the way back after an uninstall): at once, on a tick that names the view,
32
+ while the maintenance service runs (with `--adapter=obsidian-cli`, one of
33
+ an earlier release is replaced by the installed one first, as `open` does),
34
+ and otherwise at the view's next publication.
35
+ - The maintenance service answers five plugin commands (`/plugin/challenge`,
36
+ `/plugin/hello`, `/plugin/lease`, `/plugin/release`, `/plugin/status`,
37
+ protocol `atelier-obsidian-plugin-channel/v2`) and grants presence and
38
+ read-only status of one view to a plugin that proves it holds that vault's
39
+ key. The key is random per vault, kept owner-only in private state and in
40
+ that vault's plugin data file, and never crosses the wire: the service
41
+ proves it holds the key over its own exact address before the plugin sends
42
+ anything that names the vault, and every later request and answer is sealed
43
+ with a key for that session and a counter that only goes up. A program that
44
+ takes the service's port while the service is down learns nothing it can
45
+ use: an answer the session's key does not seal ends the session in the
46
+ plugin, so such a program receives one command of a session at most; a
47
+ challenge is answered once and only within thirty seconds of the time it
48
+ names, and at most four handshakes wait per view. `status` reports which
49
+ views a plugin holds open, and so do `atelier obsidian status` and `open`.
50
+ - While one launch of the plugin holds a view open and the command-line
51
+ tool gives no version (no vault open yet, or no answer in time), the app
52
+ version the plugin reports counts as checked (reason `plugin-reported`), in
53
+ the service's adapter factory and in `open`, unless the process table shows
54
+ no app running (a lease outlives a crashed app by a few seconds, and then
55
+ vouches for no version). A version the tool does give
56
+ decides, since the tool may reach another app holding the same vault, and
57
+ whether the app and its tool are installed is still the probe's answer.
58
+
59
+ ### Changed
60
+
61
+ - Node.js 24 is supported alongside Node.js 22. The engines range is now
62
+ `>=22.18.0 <23 || >=24.13.1 <25`, so an install on Node 24 no longer warns.
63
+ Graph and projection files come out byte-identical on either major: CI runs
64
+ the complete suite on 24.13.1 and on the newest 24, and a new
65
+ `cross-node-bytes` job builds one workspace on 22 and on 24 and compares
66
+ every file written. The byte-determinism test names the supported majors
67
+ and checks them against `package.json`. The floor is 24.13.1, not the
68
+ first 24 LTS, because earlier 24 releases have an `fs.rmSync` defect
69
+ (nodejs/node#61020): removing a symbolic link to a directory throws
70
+ `EISDIR`, and a broken symbolic link is silently left in place. The
71
+ complete suite fails on 24.11.0 for that reason.
72
+ - **Breaking:** Obsidian generation manifests of the new vault layout are
73
+ `atelier-obsidian-generation-manifest/v2`, a new contract major that records
74
+ `layoutVersion: 2` and each note's identity region, and a vault an earlier
75
+ release published is laid out again once (below). The v1 contract is
76
+ unchanged and still validates every generation an earlier release wrote; a
77
+ reader of generation manifests must accept both majors.
78
+ - Obsidian views are laid out in vault layout 2, so a file name reads as the
79
+ note's title and the vault reads as the repositories. Folders mirror each
80
+ repository under a folder named after it, so a view's folders show the
81
+ repository identity and source directory chain of its notes, as its notes'
82
+ `atelier-repo` and `atelier-source` properties do; this is acceptable for
83
+ every vault, scoped ones included. A note's file name is its title (the
84
+ front-matter `title`, else its first H1, else the file stem as written),
85
+ with a trailing source extension dropped and made safe for macOS, Linux,
86
+ Windows and Obsidian links. Two notes of a view that would share a name in
87
+ one folder, compared case- and normalization-insensitively, are told apart
88
+ by the source file stem and then by a short stable id; a name that collides
89
+ with nothing carries no hash. A wrapped file keeps its own name beside its
90
+ note (`<file name>.md`), and an embedded file is copied to its mirrored
91
+ path. Links name their target by its full vault path. A repository folder
92
+ that would be `notes` or `attachments` spelled another way is told apart by
93
+ a short id, since an upgraded vault keeps those two folders of the earlier
94
+ layout.
95
+ - Each view allocates its paths among its own notes, seeded from what its
96
+ prior generation published: a path does not change on retitle or when
97
+ other files come and go, a note that leaves the view releases its path
98
+ there (a renamed source takes its name back), a note of another view never
99
+ causes a qualifier, and a lost path registry costs no view its paths. One
100
+ identity may have different paths in different views. The registry keeps
101
+ each view's allocation in a section of its own, and drops the sections of
102
+ views the engine no longer maintains. `@mnstry/atelier/obsidian/materialize`
103
+ adds `allocateViewPaths`; `allocateWorkspacePaths` is deprecated: it still
104
+ allocates the earlier layout's paths for a whole workspace, exactly as
105
+ before, and Atelier no longer calls it.
106
+ - A source that does not fit is laid out anyway and reported, never a reason
107
+ to refuse the workspace: a folder chain too long for the path budget keeps a
108
+ readable prefix and a short stable id, or, when not even that fits, the note
109
+ sits directly in its repository's folder; a source folder that meets a file
110
+ of the same name is qualified with an id. Only a name that does not fit even
111
+ in its repository's folder refuses the view. What is worth a look (these, an
112
+ author's own identity keys shadowing the generated ones, generated text that
113
+ names a note outside the view without refusing it) is recorded in the
114
+ manifest and the view's freshness entry, and shown by
115
+ `atelier obsidian status`, naming the note.
116
+ - Every note names its identity in three generated front-matter properties,
117
+ `atelier-id`, `atelier-repo` and `atelier-source`, or, when its own front
118
+ matter could not take them unchanged in meaning, in a generated block at
119
+ its end. An edit to them is never applied to a source.
120
+ - A vault published by an earlier release is laid out again once. Every
121
+ earlier path is retired through the publisher's remove units: moved to the
122
+ recovery area, or kept where it is when somebody edited it. The app's
123
+ bookmarks, open tabs and graph positions of the earlier paths are lost once.
124
+ A view that holds a note for an open edit keeps its earlier layout until the
125
+ edit is applied or withdrawn, and such an edit still applies; an edit that
126
+ closes on a tick (automatic mode applies on the tick it observes) keeps it
127
+ for that tick, so the file the person edited becomes the published note and
128
+ is retired like any other. See "Vault layout" in `docs/obsidian-contract.md`.
129
+ - The redaction guard is re-based on the readable layout. Atelier never
130
+ generates a reference to a note outside a view: every path the emitter
131
+ writes must be one allocated to the view, and every identity block names
132
+ its own note. Generated prose repeats author text (the titles, summaries
133
+ and tags of in-view notes), which is carried as authored; the deny-list over
134
+ it, read as a reader sees it with the emitter's escapes removed and in NFC,
135
+ follows the audience, as defence in depth. For a note the audience may not
136
+ see it refuses an unambiguous identifier (an identity qualified by any
137
+ repository, a repository-qualified path, a repository-relative path with a
138
+ folder, a vault path) and reports a bare-word identity or a file name at a
139
+ repository's root (`README.md`); for a note the audience may see but the
140
+ view does not select it only reports. A view may newly refuse with
141
+ `redaction-failure`; the refusal names the rule and the in-view note, never
142
+ the value. Both rules run over every note, cached ones included. The deny
143
+ matcher is one automaton: building it takes time and memory linear in the
144
+ values it holds, on every preparation, and it reads each text once, however
145
+ many there are.
146
+ - A focus query names each note by an anchored regular-expression path term
147
+ (`path:/^…$/`, query version `obsidian-graph-search-paths/v2`), so it matches
148
+ exactly the selected notes; a focus persisted with the earlier version is
149
+ still read.
150
+ - `atelier obsidian open` makes the first open of a view automatic: Obsidian
151
+ no longer has to be quit, and no vault folder has to be opened by hand. It
152
+ makes the app know the view's vault as one of its vaults before it opens
153
+ it. With Obsidian running and
154
+ answering its command line, the vault is added through the app itself
155
+ (`vault-open` through `obsidian-cli eval`, a constant script with the folder
156
+ as a base64 JSON payload), verified in the app's own vault list by real
157
+ path, and opened; `open` waits until the app answers for exactly that vault
158
+ and then asks for the view again, so the first publication runs through the
159
+ app with every editor check. Quitting Obsidian is no longer needed. With
160
+ Obsidian not running, the view is published on the path with no app, and
161
+ the vault is added to the app's `obsidian.json` before the app is started
162
+ on it: only while the process table shows, positively, that no Obsidian
163
+ runs (read again immediately before the rename), only a file Obsidian
164
+ created, owned by this user and not a link, atomically (a temporary file in
165
+ the same directory, fsynced, renamed), keeping every other key and entry,
166
+ and with a timestamped backup beside it; a write that fails at any step
167
+ leaves the file as it was and nothing beside it; an Obsidian that never ran
168
+ on the account (no such file) is asked to be started once. With Obsidian running
169
+ and no vault open, a vault the app already lists is opened by path; one it
170
+ does not list is answered as `app-version-unsupported` / `no-vault-open`
171
+ (open any vault, or quit Obsidian, then open again) and nothing is
172
+ written. Every failure is typed and names its next step. See "What `open`
173
+ does in each state of Obsidian" in docs/obsidian.md and "Obsidian's vault
174
+ list" in docs/obsidian-contract.md.
175
+ - A tick requested over the service's listener may name one view
176
+ (`POST /tick` with `scopeId`; `requestServiceTick({ scopeId })`), which the
177
+ engine then prepares and publishes once more, whatever its state
178
+ (`engine.requestPreparation(scopeId)`, a one-shot request consumed at the
179
+ next tick), with the app's qualification asked again. `open` names its view
180
+ on both ticks it requests. A view whose last publication did not settle is
181
+ also tried again without a request: as soon as the app looks different (it
182
+ quit or started, or opened or closed a vault in its list), and otherwise
183
+ after a delay that starts at 30 seconds (`DEFAULT_PUBLICATION_RETRY_MS`) and
184
+ doubles, up to the full reconciliation interval. Before, it waited for a
185
+ change or the five-minute reconciliation. What the app looks like is read
186
+ from the process table and its vault list alone (`appStateSignature({
187
+ processes, settings })`), so nothing runs in Obsidian to find out; the app
188
+ is asked for its version only when a view is about to be published, and
189
+ the service's adapter factory asks it without blocking
190
+ (`createQualifiedAdapterFactory` answers a promise for an app probe with
191
+ `inspect()`). `engine.requestPreparation` takes a request only for a view
192
+ the project declared at the last tick (at most 64 before the first), and
193
+ answers whether it did. The service listener answers a POST member it does
194
+ not know, or a view on a stop, with 400 `request-member-unknown` instead of
195
+ 409, and checks a view against the scope contract's own identifier.
196
+ - Command-line calls to the app reach only the vault they are about. A call
197
+ about a vault names it first (`vault=<id>`), so it reaches that vault's
198
+ window whichever window has focus; only when that id would name another
199
+ vault first does it run in the vault's folder, and not at all when a vault
200
+ the app lists before it at a folder above it (a home folder, say) would take
201
+ a call run there. The vault root, the settings entry, the vault check and
202
+ the publication bridge's check that the app holds this vault use the path as
203
+ the file system stores it, so on macOS a data root given in another letter
204
+ case neither hides a vault listed above it nor routes a call there, and a
205
+ store written before, or an app that holds the vault under another spelling
206
+ or through a link, keeps working: publication through the app, and `open`'s
207
+ check that the app answers for the vault. Publication calls do so only while the app's list
208
+ shows the view's vault open; every other call runs in a directory that is
209
+ no vault. Maintenance never reopens a vault window that was closed and never
210
+ reaches another vault, and the directory a command or service was started
211
+ in no longer picks, or opens, a vault. `open` never adds a view's vault
212
+ inside a folder Obsidian already lists as a vault, which would show the
213
+ view's notes too: it answers `launch-failed` / `vault-inside-another-vault`.
214
+ A listed folder is compared as written first; its real path is read only
215
+ when its last component is the vault root's, so a vault on a mount that
216
+ does not answer is never waited on. The maintenance service is started in
217
+ the root directory.
218
+ - A view's vault that Obsidian has open in more than one window, one per
219
+ entry of its vault list that names the folder (under another letter case,
220
+ or through a link), is not published: a publication would coordinate with
221
+ one window only. The view reports `publisher-conflict` /
222
+ `vault-open-in-several-windows` (removing the extra entries from Obsidian's
223
+ vault list clears it; closing a window may not, since Obsidian keeps the last
224
+ window it closed marked open), and `open` answers the same, names the entries (`open in Obsidian
225
+ as: …`, `duplicates` in JSON) and launches nothing. While one entry of the
226
+ folder has a window, calls and `open` reach only that entry, so a closed
227
+ entry of the same folder is never opened beside it.
228
+ - Obsidian's settings file is not written larger than 4 MiB
229
+ (`obsidian-settings-too-large`), nor through a second name (a hard link,
230
+ `obsidian-settings-unsafe`). Of Atelier's backups beside it, the first (the
231
+ list as it was before Atelier wrote it) and the latest are kept. A write
232
+ that cannot be read back is `registration-not-read-back`, no longer
233
+ `app-started-during-registration`. A Flatpak or snap build of Obsidian on
234
+ Linux, which never reads that file, is recognised (the build whose vault
235
+ list was written last; its installation only when no build wrote one), and
236
+ the file is neither read nor written for it (`obsidian-sandboxed`); the
237
+ vault is added through the running app. An addition the running app did not answer is looked up in
238
+ its list, and is `addition-not-answered` when it is not there, no longer
239
+ `app-did-not-list-its-vaults`. Adding a vault through the app also puts its
240
+ folder in the operating system's recent documents (Recent Items on macOS),
241
+ as Obsidian's own "open folder as vault" does.
242
+ - A maintenance service still running an earlier release after an upgrade
243
+ is replaced by `open`. Every service records the release it runs, the
244
+ package version and a digest of every runtime module it ships (`src/`,
245
+ `contracts/`), in its record's `executable.ext.release`
246
+ (`releaseIdentity()`). A service of the workspace that proves itself ours
247
+ but runs an earlier version than the installed one, the same version with
248
+ another entry module or other modules, records no release, or refuses a
249
+ tick that names a view (as 0.2.0-alpha.11 and earlier do), is stopped
250
+ through its own listener and the installed release is started under the
251
+ consent already recorded; `open` shows `service: restarted (outdated)`
252
+ (`service.restarted` in JSON). A service of a later version (versions
253
+ ordered as semantic versions, 0.2.0-alpha.11 < 0.2.0-alpha.12 < 0.2.0), or
254
+ of one that cannot be ordered, is never replaced by an earlier release, so
255
+ two installations used on one workspace do not replace each other's service
256
+ on every open: `open` answers `service-unavailable` /
257
+ `service-other-release`, whose next step is `atelier obsidian service stop`,
258
+ then open again. The same version string on both sides is compared by
259
+ content even when it cannot be ordered (a fork's `dev`), so that next step
260
+ cannot loop. A tick refused because a concurrent command replaced the
261
+ runtime just before is asked of the runtime that took its place, and
262
+ nothing is restarted. A busy service is not stopped, and nothing that does
263
+ not prove itself ours is touched. `requestServiceTick` takes the start
264
+ options of the installed entry as `service` for this (`runtimeRelease`,
265
+ `releaseStanding`).
266
+ - The `status` next step for `publisher-conflict` / `editor-uncoordinated`
267
+ names `atelier obsidian open` (which adds the vault to Obsidian and
268
+ publishes through it) or quitting Obsidian; after `open` itself tried, it
269
+ names quitting Obsidian. `launch-failed` no longer asks for a vault folder
270
+ to be opened by hand.
271
+ - The settings unit owns two more things in a vault: the `atelier-projection`
272
+ entry of `community-plugins.json` (appended while the person wants the
273
+ plugin there; every other entry kept in order; written only over the exact
274
+ bytes the decision was made on, so a change the person makes meanwhile
275
+ stops that unit as `settings-changed` and the view is tried again) and the
276
+ plugin's four files, whose digests are pinned in the generation manifest
277
+ under `ext["mnstry.atelier.obsidian"].settings`.
278
+ Whatever occupies a plugin path is displaced to recovery, never lost; a
279
+ plugin path a person has to repair (a link, a folder where a file goes, a
280
+ file nobody may read, a folder nobody may write) never holds a view back; a
281
+ generation whose plugin files the person repaired, changed or removed is
282
+ published again as it is at the maintenance service's next tick; and
283
+ another writer racing a plugin file makes the view try again.
284
+ `isUserOwnedSettingsPath` answers `false` for these paths.
285
+ - Vault roots that Atelier places under its data root are created private
286
+ (`0700`), and an existing one is made private before the plugin's key is
287
+ written into it. A vault root Atelier did not place never receives the key
288
+ unless it is already private, and a vault path under the data root that is
289
+ a link to another folder never receives it, and its mode is never changed.
290
+ - A settings file nobody may read (`core-plugins.json`, say) is reported as
291
+ `path-unsafe` and left for the person instead of failing the publication.
292
+ - `plugins/` is part of the egress scan, and the release audit requires the
293
+ plugin's three files in the package.
294
+
295
+ ### Fixed
296
+
297
+ - A publication no longer stops with an untyped `state leaf changed while
298
+ opening` when another program replaces a note by rename at the instant the
299
+ publisher opens it (an editor or sync tool saving the note). The note is
300
+ read again, since every rename leaves a complete file; a note that keeps
301
+ being replaced is reported as `disk-changed` for that note only, and the
302
+ other notes are published. No bytes were ever lost: the publication threw
303
+ before writing.
304
+ - `open` no longer makes Obsidian forget which of your vaults to reopen. A
305
+ quit Obsidian started with an `obsidian://` link opens only that vault and
306
+ drops the reopen flag of every other one; `open` now starts it plainly on
307
+ macOS (it reopens every vault it had open, and the view's vault, which `open`
308
+ added flagged to reopen) and hands it the vault's link through its command
309
+ line once the app itself answers there, never through the operating system.
310
+ On Linux a quit Obsidian is still started with the link (a known limit).
311
+ - `open` names the vault by its id (`obsidian://open?vault=<id>`) where the id
312
+ reaches it first, instead of its path, which Obsidian matches against its
313
+ vault list by string prefix.
314
+ - Obsidian with its command line turned off (the default of a new
315
+ installation) answers every command with "Command line interface is not
316
+ enabled"; that answer was read as an unreadable version. It is now
317
+ `app-cli-unavailable` / `cli-turned-off`, whose next step names the setting
318
+ (Settings > General > Advanced > Command line interface).
319
+ - A view whose prepared generation was already the committed one (on the
320
+ first tick of a service, or prepared again on request) was marked `stale`
321
+ when the app could not be qualified, for example while it ran with no vault
322
+ open: the editor adapter was built before the publisher found nothing to
323
+ publish. That adapter is now built only when the publisher needs one.
324
+ - After a refused publication, the tick `open` asked for did not try the view
325
+ again (the engine attempted only views with changes), so `open` reported the
326
+ old conflict until the five-minute reconciliation.
327
+ - `open` launched `obsidian://open?path=` for a vault the app did not know,
328
+ and Obsidian showed "Vault not found. Unable to find a vault for the URL";
329
+ `open` then answered `launch-failed` / `app-did-not-answer-for-this-vault`.
330
+ - The production check that the app answers for a vault parsed the app's
331
+ answer twice and so never saw an answer; it now reads it once (twice only
332
+ for a quoted string), compares real paths, and asks from inside the vault
333
+ folder.
334
+ - An app whose vault window is still loading answers a command with `Error:
335
+ Command "version" not found`; that was read as an unreadable version and
336
+ ended `open` as `app-version-unsupported`. It is now read as not up yet.
337
+ - `atelier dev` on a port that was already taken (8137 by default) printed
338
+ only `[internal-error] command failed without a safe diagnostic`, and the
339
+ message that says what to do appeared only with `ATELIER_DEBUG=1`. It is now
340
+ `port-in-use`, naming the port, with `--port=<free port>` as the next step.
341
+ A port the operating system refuses (EACCES, usually one below 1024) is
342
+ `port-permission-denied` with the same remedy, and a `--port` or `PORT`
343
+ value that is not a port from 0 to 65535 is `port-invalid`, naming which
344
+ one, instead of Node's `ERR_SOCKET_BAD_PORT`. Other failures without a
345
+ typed code are still redacted.
346
+
347
+ ## 0.2.0-alpha.11
348
+
349
+ ### Fixed
350
+
351
+ - `status` now gives the same reason-specific next step as `open` for a
352
+ publication that stopped because an Obsidian that may hold the vault could
353
+ not be coordinated with (`publisher-conflict`, reason
354
+ `editor-uncoordinated`). The text no longer names one cause, since the
355
+ reason has several: another vault open, a command line that did not answer,
356
+ an unknown process table (on Linux, any app on a system Electron), or an app
357
+ started during an app-closed publication. The advice depends on whether the
358
+ view was ever published. Before a first publication: quit Obsidian (on
359
+ Linux, also any app that runs on a system Electron) so the view is
360
+ published, then `atelier obsidian open` starts Obsidian on it. After one:
361
+ the same, or open the view in the app as it is with `atelier obsidian open
362
+ --allow-stale`. A concurrent publisher keeps its advice.
363
+ - The service's app qualification checks no version for an app that is not
364
+ running, and that answer is remembered for up to ten seconds. An editor
365
+ adapter built on it could coordinate with an Obsidian started inside that
366
+ window, whose version nobody had checked, and publish through it in-app.
367
+ The adapter now receives the qualification and does not coordinate, or ask
368
+ the app anything, unless a version was checked. An answer that checked no
369
+ version (the app was not running, or none was found) is no longer reused,
370
+ so the next publication after the app starts reads its version and
371
+ coordinates. A publication that stops this way is tried again on the next
372
+ change or at the next full reconciliation, every five minutes.
373
+ - On macOS the app is found only at `/Applications/Obsidian.app`. An
374
+ Obsidian installed elsewhere is `app-missing`: nothing is published through
375
+ it, and publication waits while it runs. This is now documented.
376
+ - On Linux an app started through a differently named link or launcher was
377
+ read as absent, because the process name is the name it was started
378
+ through. A process whose name is not recognised is now identified by the
379
+ executable `/proc/<pid>/exe` resolves to. A read failure other than a
380
+ process that exited or whose executable this user may not read, or a table
381
+ in which nothing resolves, is `unknown`.
382
+ - The `ps` call of the process probe had no timeout, so a hung `ps` blocked
383
+ the service. It is killed after five seconds and the reading is `unknown`.
384
+ - Documented that from 0.2.0-alpha.10 the app-free publication path runs in
385
+ production, with its re-probe window of up to two seconds plus one note
386
+ while Obsidian is not running (see "Known limits" in `docs/obsidian.md`).
387
+
3
388
  ## 0.2.0-alpha.10
4
389
 
5
390
  ### Fixed
package/README.md CHANGED
@@ -54,7 +54,7 @@ governed projections
54
54
  You can see the complete loop in a disposable sample workspace:
55
55
 
56
56
  ```bash
57
- npm install --save-dev @mnstry/atelier@0.2.0-alpha.10
57
+ npm install --save-dev @mnstry/atelier@0.2.0-alpha.12
58
58
  npx mnstry-atelier init --fixture=sample-workspace --target ./sample
59
59
  npx mnstry-atelier graph --project ./sample/atelier.project.json
60
60
  npx mnstry-atelier project --project ./sample/atelier.project.json
@@ -374,11 +374,11 @@ to MNSTRY, and a document MNSTRY declines can still be fully conformant.
374
374
 
375
375
  ## Install and go deeper
376
376
 
377
- Node.js `>=22.18.0 <23` is required. Pin the prerelease while the package
378
- remains in alpha:
377
+ Node.js 22 (22.18.0 or later) or Node.js 24 (24.13.1 or later) is required.
378
+ Pin the prerelease while the package remains in alpha:
379
379
 
380
380
  ```bash
381
- npm install --save-dev @mnstry/atelier@0.2.0-alpha.10
381
+ npm install --save-dev @mnstry/atelier@0.2.0-alpha.12
382
382
  ```
383
383
 
384
384
  Then choose the path that matches what you are building:
@@ -397,7 +397,7 @@ Then choose the path that matches what you are building:
397
397
 
398
398
  ## Status and command reference
399
399
 
400
- Current package: `@mnstry/atelier@0.2.0-alpha.10`.
400
+ Current package: `@mnstry/atelier@0.2.0-alpha.12`.
401
401
 
402
402
  The alpha package is usable and contract-tested, but its library API may still
403
403
  change before a stable release. Pin the exact version in production toolchains.
@@ -452,7 +452,11 @@ edit made in the editor before anything is republished. A body edit is applied
452
452
  to its source manually or under a scoped policy the user installs; anything
453
453
  structural stays a copy-only proposal. Atelier never commits to Git for it.
454
454
  It is disabled unless a project enables it, and it is proven on one platform
455
- and one app version. Read [what it does and its known limits](docs/obsidian.md),
455
+ and one app version. Every vault it publishes carries
456
+ [Atelier's own Obsidian plugin](docs/obsidian-plugin.md), which Obsidian asks
457
+ you to trust once per vault; it shows whether the view is current, writes
458
+ nothing, and talks only to the local maintenance service, and the projection
459
+ works the same without it. Read [what it does and its known limits](docs/obsidian.md),
456
460
  the [contract](docs/obsidian-contract.md) and the
457
461
  [maintenance service](docs/local-services.md) before relying on it. The
458
462
  package subpaths are `@mnstry/atelier/obsidian` and