@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
@@ -0,0 +1,372 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://mnstry.ai/schemas/atelier/atelier-obsidian-generation-manifest.v2.schema.json",
4
+ "title": "Obsidian generation manifest",
5
+ "$comment": "contract revision 2.0.0; portable shape; vault layout 2: readable paths mirroring each repository, the identity of every note in a generated region of its own, authored and generated byte regions, link inversion map, attachment hashes, completeness and freshness. Withheld endpoints never appear.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": [
9
+ "schema",
10
+ "layoutVersion",
11
+ "generationId",
12
+ "scopeId",
13
+ "snapshotId",
14
+ "notes",
15
+ "links",
16
+ "attachments",
17
+ "completeness",
18
+ "freshness"
19
+ ],
20
+ "properties": {
21
+ "schema": {
22
+ "const": "atelier-obsidian-generation-manifest/v2"
23
+ },
24
+ "layoutVersion": {
25
+ "const": 2
26
+ },
27
+ "generationId": {
28
+ "$ref": "#/$defs/identifier"
29
+ },
30
+ "scopeId": {
31
+ "$ref": "#/$defs/identifier"
32
+ },
33
+ "snapshotId": {
34
+ "$ref": "#/$defs/identifier"
35
+ },
36
+ "notes": {
37
+ "type": "array",
38
+ "maxItems": 1000000,
39
+ "items": {
40
+ "type": "object",
41
+ "additionalProperties": false,
42
+ "required": [
43
+ "repoId",
44
+ "nodeId",
45
+ "path",
46
+ "title",
47
+ "noteDigest",
48
+ "regions"
49
+ ],
50
+ "properties": {
51
+ "repoId": {
52
+ "$ref": "#/$defs/identifier"
53
+ },
54
+ "nodeId": {
55
+ "$ref": "#/$defs/identifier"
56
+ },
57
+ "path": {
58
+ "$ref": "#/$defs/notePath"
59
+ },
60
+ "title": {
61
+ "type": "string",
62
+ "minLength": 1,
63
+ "maxLength": 512
64
+ },
65
+ "noteDigest": {
66
+ "$ref": "#/$defs/digest"
67
+ },
68
+ "regions": {
69
+ "type": "object",
70
+ "additionalProperties": false,
71
+ "required": [
72
+ "identity",
73
+ "body",
74
+ "generated"
75
+ ],
76
+ "properties": {
77
+ "frontmatter": {
78
+ "$ref": "#/$defs/byteRange"
79
+ },
80
+ "identity": {
81
+ "$ref": "#/$defs/byteRange"
82
+ },
83
+ "body": {
84
+ "$ref": "#/$defs/byteRange"
85
+ },
86
+ "generated": {
87
+ "type": "array",
88
+ "maxItems": 64,
89
+ "items": {
90
+ "type": "object",
91
+ "additionalProperties": false,
92
+ "required": [
93
+ "kind",
94
+ "range"
95
+ ],
96
+ "properties": {
97
+ "kind": {
98
+ "enum": [
99
+ "relations",
100
+ "outside-selection",
101
+ "representation",
102
+ "identity"
103
+ ]
104
+ },
105
+ "range": {
106
+ "$ref": "#/$defs/byteRange"
107
+ },
108
+ "ext": {
109
+ "type": "object"
110
+ }
111
+ }
112
+ }
113
+ },
114
+ "ext": {
115
+ "type": "object"
116
+ }
117
+ }
118
+ },
119
+ "ext": {
120
+ "type": "object"
121
+ }
122
+ }
123
+ }
124
+ },
125
+ "links": {
126
+ "type": "array",
127
+ "maxItems": 5000000,
128
+ "items": {
129
+ "type": "object",
130
+ "additionalProperties": false,
131
+ "required": [
132
+ "edgeId",
133
+ "sourceNodeId",
134
+ "targetNodeId",
135
+ "type",
136
+ "origin",
137
+ "targetState"
138
+ ],
139
+ "properties": {
140
+ "edgeId": {
141
+ "$ref": "#/$defs/identifier"
142
+ },
143
+ "sourceNodeId": {
144
+ "$ref": "#/$defs/identifier"
145
+ },
146
+ "targetNodeId": {
147
+ "$ref": "#/$defs/identifier"
148
+ },
149
+ "type": {
150
+ "$ref": "#/$defs/relationType"
151
+ },
152
+ "origin": {
153
+ "enum": [
154
+ "declared",
155
+ "markdown"
156
+ ]
157
+ },
158
+ "targetState": {
159
+ "enum": [
160
+ "in-scope",
161
+ "outside-selection"
162
+ ]
163
+ },
164
+ "inversions": {
165
+ "type": "array",
166
+ "maxItems": 4096,
167
+ "items": {
168
+ "type": "object",
169
+ "additionalProperties": false,
170
+ "required": [
171
+ "source",
172
+ "note"
173
+ ],
174
+ "properties": {
175
+ "source": {
176
+ "$ref": "#/$defs/byteRange"
177
+ },
178
+ "note": {
179
+ "$ref": "#/$defs/byteRange"
180
+ },
181
+ "ext": {
182
+ "type": "object"
183
+ }
184
+ }
185
+ }
186
+ },
187
+ "ext": {
188
+ "type": "object"
189
+ }
190
+ },
191
+ "if": {
192
+ "required": [
193
+ "type"
194
+ ],
195
+ "properties": {
196
+ "type": {
197
+ "const": "links_to"
198
+ }
199
+ }
200
+ },
201
+ "then": {
202
+ "properties": {
203
+ "origin": {
204
+ "const": "markdown"
205
+ }
206
+ }
207
+ },
208
+ "else": {
209
+ "properties": {
210
+ "origin": {
211
+ "const": "declared"
212
+ }
213
+ }
214
+ }
215
+ }
216
+ },
217
+ "attachments": {
218
+ "type": "array",
219
+ "maxItems": 1000000,
220
+ "items": {
221
+ "type": "object",
222
+ "additionalProperties": false,
223
+ "required": [
224
+ "path",
225
+ "digest",
226
+ "byteLength"
227
+ ],
228
+ "properties": {
229
+ "path": {
230
+ "$ref": "#/$defs/vaultPath"
231
+ },
232
+ "digest": {
233
+ "$ref": "#/$defs/digest"
234
+ },
235
+ "byteLength": {
236
+ "type": "integer",
237
+ "minimum": 0
238
+ },
239
+ "ext": {
240
+ "type": "object"
241
+ }
242
+ }
243
+ }
244
+ },
245
+ "completeness": {
246
+ "type": "object",
247
+ "additionalProperties": false,
248
+ "required": [
249
+ "status",
250
+ "expectedNotes",
251
+ "writtenNotes"
252
+ ],
253
+ "properties": {
254
+ "status": {
255
+ "enum": [
256
+ "complete",
257
+ "partial"
258
+ ]
259
+ },
260
+ "expectedNotes": {
261
+ "type": "integer",
262
+ "minimum": 0
263
+ },
264
+ "writtenNotes": {
265
+ "type": "integer",
266
+ "minimum": 0
267
+ },
268
+ "ext": {
269
+ "type": "object"
270
+ }
271
+ }
272
+ },
273
+ "freshness": {
274
+ "type": "object",
275
+ "additionalProperties": false,
276
+ "required": [
277
+ "status",
278
+ "checkedAt"
279
+ ],
280
+ "properties": {
281
+ "status": {
282
+ "enum": [
283
+ "current",
284
+ "stale",
285
+ "unknown"
286
+ ]
287
+ },
288
+ "checkedAt": {
289
+ "$ref": "#/$defs/timestamp"
290
+ },
291
+ "ext": {
292
+ "type": "object"
293
+ }
294
+ }
295
+ },
296
+ "ext": {
297
+ "type": "object"
298
+ },
299
+ "contractVersion": {
300
+ "type": "string",
301
+ "pattern": "^2\\.[0-9]+\\.[0-9]+$"
302
+ }
303
+ },
304
+ "$defs": {
305
+ "identifier": {
306
+ "type": "string",
307
+ "minLength": 1,
308
+ "maxLength": 128,
309
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$"
310
+ },
311
+ "digest": {
312
+ "type": "string",
313
+ "pattern": "^sha256:[0-9a-f]{64}$"
314
+ },
315
+ "timestamp": {
316
+ "type": "string",
317
+ "format": "date-time"
318
+ },
319
+ "relativePath": {
320
+ "type": "string",
321
+ "minLength": 1,
322
+ "maxLength": 1024,
323
+ "pattern": "^(?![/~])(?![A-Za-z]:)(?!.*(?:^|/)\\.\\.(?:/|$))[^\\\\\\u0000]+$"
324
+ },
325
+ "byteRange": {
326
+ "type": "object",
327
+ "additionalProperties": false,
328
+ "required": [
329
+ "start",
330
+ "end"
331
+ ],
332
+ "properties": {
333
+ "start": {
334
+ "type": "integer",
335
+ "minimum": 0
336
+ },
337
+ "end": {
338
+ "type": "integer",
339
+ "minimum": 0
340
+ },
341
+ "ext": {
342
+ "type": "object"
343
+ }
344
+ }
345
+ },
346
+ "relationType": {
347
+ "enum": [
348
+ "related",
349
+ "supports",
350
+ "supersedes",
351
+ "implements",
352
+ "depends_on",
353
+ "evidences",
354
+ "contradicts",
355
+ "belongs_to",
356
+ "links_to"
357
+ ]
358
+ },
359
+ "vaultPath": {
360
+ "type": "string",
361
+ "minLength": 1,
362
+ "maxLength": 1024,
363
+ "pattern": "^(?:(?![ .])[^/\\\\\\u0000-\\u001f\\u007f-\\u009f\\u2028\\u2029:*?\"<>|#^\\[\\]]+(?<![ .])/)+(?![ .])[^/\\\\\\u0000-\\u001f\\u007f-\\u009f\\u2028\\u2029:*?\"<>|#^\\[\\]]+(?<![ .])$"
364
+ },
365
+ "notePath": {
366
+ "type": "string",
367
+ "minLength": 1,
368
+ "maxLength": 1024,
369
+ "pattern": "^(?:(?![ .])[^/\\\\\\u0000-\\u001f\\u007f-\\u009f\\u2028\\u2029:*?\"<>|#^\\[\\]]+(?<![ .])/)+(?![ .])[^/\\\\\\u0000-\\u001f\\u007f-\\u009f\\u2028\\u2029:*?\"<>|#^\\[\\]]+(?<![ .])\\.md$"
370
+ }
371
+ }
372
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema": "atelier-compat-baseline@v1",
3
- "baselineTag": "v0.2.0-alpha.0",
3
+ "baselineTag": "v0.2.0-alpha.11",
4
4
  "preEpochTags": [
5
5
  "v0.1.0-alpha.0",
6
6
  "v0.1.0-alpha.1",
@@ -15,13 +15,14 @@ commit and packed-tarball digest.
15
15
  | Resident Sync state cannot escape or grow without bound | Contained private directories, no-follow/identity-checked leaves, a 4,096-entry observation ceiling, bounded state projection, nonce-bound locks with aged abandoned-claim recovery, lock-directory identity validation and an ambiguous live-PID maximum age, consumed plans under age/file/byte ceilings, and corruption-recovering hash-chained traces with per-event/resident ceilings | `sync:test` observation/state ceilings, redirected-state/lock, plan lifecycle, concurrent enrollment/recovery, corruption, and trace ceilings | Machine-local state is diagnostic convenience; Git and readable repository files remain authoritative |
16
16
  | Local review serves only generated publication output | Loopback-only bind, required `atelier.manifest.json`, realpath containment, enrolled safe types, POSIX no-follow plus cross-platform leaf type/identity validation for local state, and host/fetch-site/origin/method/nonce checks | `server:security:test`; sidecar case in `assurance:mutation-smoke` | The sidecar is a local review tool, not user authentication or runtime authorization |
17
17
  | Collaboration cannot become a hidden apply path | Proposal authority is capability-derived; records are typed; POSIX ledger and snapshot leaves are no-follow, while every platform rejects redirected leaves and validates opened file identity when available; ledger reads are bounded, one-pass for lists, and corrupt tails fail closed | `collaboration:test` and ledger ceiling/performance tests | Collaboration remains copy-only proposal metadata; snapshots are rebuildable projections and Git review owns source changes |
18
- | The Obsidian maintenance service is reachable from this machine only, and only by its owner | Bind and client are restricted to the literal `127.0.0.1` or `::1`; `Host` must be the exact bound authority; cross-site `Origin` and `Sec-Fetch-Site` are refused; four fixed operations with exact paths and methods; status, tick and stop need a per-runtime random bearer kept only in the mode-0600 record; payloads are bounded and name their runtime; health carries no path or title | `test/obsidian-maintenance.test.mjs` listener, disclosure and unauthorised-request cases, each with a deliberately broken listener that must fail | A local process running as the same user can read the record; the bearer is ownership proof between local processes, not user authentication |
18
+ | The Obsidian maintenance service is reachable from this machine only, and only by its owner | Bind and client are restricted to the literal `127.0.0.1` or `::1`; `Host` must be the exact bound authority; cross-site `Origin` and `Sec-Fetch-Site` are refused; four fixed operations and five fixed plugin commands with exact paths and methods; status, tick and stop need a per-runtime random bearer kept only in the mode-0600 record; payloads are bounded and name their runtime; health carries no path or title | `test/obsidian-maintenance.test.mjs` listener, disclosure and unauthorised-request cases, each with a deliberately broken listener that must fail | A local process running as the same user can read the record; the bearer is ownership proof between local processes, not user authentication |
19
+ | Atelier's Obsidian plugin reaches only its own vault's maintenance service, writes nothing and can only report | The plugin calls a literal loopback address through Node's `http` only. Its random per-vault key is kept owner-only in private state and in a mode-0600 data file inside a vault root that must be private (`0700`; Atelier's own vault roots are made so, a linked or any other root never receives the key) and never crosses the wire: the service proves it holds the key over its own exact address before the plugin sends anything that names the vault, the plugin proves the same key, and every later request and answer is sealed with a per-session key and a counter that only goes up; a challenge is answered once and only while fresh, handshakes are used once, lapse, and are bounded per view. A session grants presence and read-only status of that one view, never status, tick or stop of the service; payloads are 1 KiB JSON objects of exact fields; the plugin's files are Atelier-owned settings whose digests are pinned in the generation manifest, whose previous bytes are displaced to recovery when replaced, and which never hold a view back on a path a person has to repair | `test/obsidian-plugin.test.mjs`: the shipped plugin run in a stand-in app whose writes throw, with every request, its exact body and every required module recorded; parity of constants and of every proof and MAC with the channel contract; a program squatting the service's address that learns no key, view or path and, replaying all it received, opens no session and sets no version; a relayed answer that does not verify; the refusal table with deliberately broken channels that must fail; publication, privacy and recovery cases; an egress-scan variant that must fail | A process running as the same user, including another plugin in the same app, can read the data file, hold sessions for that view and claim an app version for it; a claimed version never bypasses the app's own answer for the vault or the in-app capability check. The plugin's app version stands in only where the command-line tool gives none and the process table does not show the app stopped; if the tool then still coordinates while a second app without the plugin holds the vault, that version may be the other app's. The plugin is optional: restricted mode leaves the command-line path unchanged |
19
20
  | A maintenance service is stopped or reported as running only when it is proven to be this workspace's own | Record and health must agree on service name, workspace, runtime identifier, PID, executable digest and loopback address; anything else on the port is `occupied`; a reused PID is never signalled; a malformed or foreign record refuses; `start` stops only the child it created, by handle | Unowned and silent listener, wrong-identity, PID-reuse, stale-record, malformed-record, idempotent and concurrent start, and launcher-survival cases, with mutation controls | Status probes have a deadline: a service busy in a long synchronous tick can be reported `occupied` until it answers |
20
21
  | Maintenance never sweeps recovery or staging, and one workspace has one ticking engine | The service deletes only its generated record; the engine and the publisher own every other write; a hard kill is settled by the publisher's journaled restart recovery; each tick holds a private per-workspace lock taken from a holder only with proof of abandonment | Directory-listing oracles across start, ticks, stop, hard kill and stale record; hard kill between exchange and manifest commit; lock exclusion and takeover cases; each with a mutation control | A live lock holder that recorded no health address needs a person; a tick that outlasts the shutdown grace period ends with the process and is settled on the next start |
21
22
  | Source application policy is re-read before every automatic apply | The engine authorizes from disk immediately before each dispatch; manual mode never reaches the apply operation; the service keeps this on every tick; the apply operation reads the installed policy again at its decision and immediately before the write, recomputes its digest and evaluates its selector over the current canonical graph; `policy install` verifies that digest and names the expected one; whether this machine may see the object is asked before anything else, so an absent and a withheld object get one answer | Revocation, manual-mode and apply-unavailable cases in `test/obsidian-maintenance.test.mjs`; policy, refusal-table and late-revocation cases in `test/obsidian-edits.test.mjs`, each with a deliberately broken rule that must fail | A policy is private configuration of one machine; whoever can write that private state can install one |
22
23
  | Applying an edit to a source file cannot lose another program's bytes | One operation for a person and for a policy: exact identity, path and digest checks at commit; the write is an atomic exchange, never a check followed by a rename; displaced bytes that are not the base are exchanged back and retained; the old source stays as a backup that is looked at again for late writers; write-ahead record and intent before the exchange; restart recovery decides from digests only, and so does an exchange that reports a failure; generated regions never reach a source: the tail is intact, or removed with the end of the authored text proven, and anything else refuses; every git directory is refused, nested, respelled or relocated; the hold on an edited note lifts only on a read of the note at that moment; no git command that writes | `test/obsidian-edits.test.mjs`: end to end through the real engine, a refusal table with a whole-project byte and `git status` oracle, a real second process writing in three styles and four timings with both outcomes asserted, a crash at every durable step, a disclosure walk, a generated tail cut at every byte offset from both sides and a fuzz over the whole note; every oracle has a mutation control | macOS arm64 on APFS only; refuses where no atomic exchange exists; the source becomes a new file (permission bits kept, hard links refused); a late writer's bytes are retained and surfaced, never merged; a hard link made after the path check is not detected; a fragment of generated text shorter than eight bytes left inside an edited body cannot be told from typing |
23
24
  | A structural edit made in a vault becomes one copy-only proposal and never a source change | The proposal adapter routes by whole identity to the existing store of the enrolled repository or refuses typed and writes nothing; it records the operation in private immutable state first, looks in the persisted store for its operation identity before creating, under a private per-repository lock taken over only with proof; it preflights the ledger limits, defers `ledger-full`, refuses `proposal-too-large` and `ledger-corrupt`, bounds batches and retries, and never compacts; a proposal holds codes, offsets and digests and no note text; acceptance is shown and never acted on | `test/obsidian-proposals.test.mjs`: routing table with a whole-project byte and `git status` oracle, identity survival against the real store, a crash at every step, two real processes, generated ledgers at 256 KiB, 16 MiB and 10,000 events and an invalid line, a no-busy-loop count of store reads, a disclosure walk; engine cases in `test/obsidian-maintenance.test.mjs`; every oracle has a mutation control | In manual mode an operation exists only after apply has looked at the edit; a refused or exhausted operation waits for a person; the store directory lives in the repository and must be git-ignored; two workspaces that enrol one repository create one proposal each |
24
- | Opening a view reports `current` only for the exact, verified, present generation and a qualified app | `open` needs a tick of the proven service after the request, a persisted `current`, an independent read-back of the trusted pointer and every note, and an app at or above the minimum version that answers for that vault; every other outcome is typed, distinct and exits non-zero; the app and the operating system are reached only through injected seams, and the production ones only behind `--adapter=obsidian-cli` | `test/obsidian-open.test.mjs`: outcome table, read-back, app-floor, busy and read-only cases, each with a deliberately broken rule that must fail; a spawn guard and a production-seam import check | The production probe and launcher are not exercised against a real app here; `indexing` relies on an undocumented readiness field until acceptance qualifies it |
25
+ | Opening a view reports `current` only for the exact, verified, present generation and a qualified app | `open` needs a tick of the proven service after the request, a persisted `current`, an independent read-back of the trusted pointer and every note, and an app at or above the minimum version that lists the vault and answers for it; the vault is added to the app's list through the app while it runs, and in its settings file only while the process table shows no Obsidian, atomically, with a backup, keeping every other key, and leaving nothing behind when a step fails; never inside a folder the app lists as a vault; a call about the vault is routed, from the app's list, to that vault only (its folder or its id) or not made; every other outcome is typed, distinct and exits non-zero; the app and the operating system are reached only through injected seams, and the production ones only behind `--adapter=obsidian-cli` | `test/obsidian-open.test.mjs`: outcome table, read-back, app-floor, busy, read-only, every app state, settings-file refusal and retry cases, each with a deliberately broken rule that must fail; a failure injected at every file-system call of the settings write; call routing against a model of the app's own routing; a spawn guard (a private HOME, on Linux a private XDG_RUNTIME_DIR, and no such child on Windows) and a production-seam import check; the opt-in `test/obsidian-first-open-real-app.test.mjs` against an isolated app | The operating system's URL opener is not exercised by any test (the real-app suite hands URLs to its isolated app directly); `indexing` relies on an undocumented readiness field; the real-app suite has run on macOS only |
25
26
  | Operating-system startup is never installed implicitly | `buildStartupAdapter` is pure text generation; Windows refuses typed; the service refuses `--startup` without a consent that covers startup | Builder purity oracle (no filesystem, process or home-directory access; no machine path beyond its inputs) and startup-consent refusal | Generated units have not been qualified against real launchd or systemd hosts |
26
27
  | Expected operator failures are actionable without leaking internals | Typed project/JSON diagnostics and the CLI execution wrapper | `test/cli-brand.test.mjs` | Unexpected stacks require the operator to opt in with `ATELIER_DEBUG=1` |
27
28
  | Published APIs do not disappear silently | Tag-derived subpath and named-export baseline plus version/tag identity refusal | `public-api:compat`; `test/public-api-compat.test.mjs` | Additive API checks do not decide whether a new version number is appropriate |
package/docs/install.md CHANGED
@@ -34,19 +34,19 @@ The registry is the distribution channel of record, as `docs/continuity.md`
34
34
  commits:
35
35
 
36
36
  ```bash
37
- npm install --save-dev @mnstry/atelier@0.2.0-alpha.10
37
+ npm install --save-dev @mnstry/atelier@0.2.0-alpha.12
38
38
  ```
39
39
 
40
40
  Installing from the matching Git tag resolves to the same reviewed commit:
41
41
 
42
42
  ```bash
43
- npm install --save-dev "git+https://github.com/MNSTRY/atelier.git#v0.2.0-alpha.10"
43
+ npm install --save-dev "git+https://github.com/MNSTRY/atelier.git#v0.2.0-alpha.12"
44
44
  ```
45
45
 
46
46
  Or over SSH:
47
47
 
48
48
  ```bash
49
- npm install --save-dev "git+ssh://git@github.com/MNSTRY/atelier.git#v0.2.0-alpha.10"
49
+ npm install --save-dev "git+ssh://git@github.com/MNSTRY/atelier.git#v0.2.0-alpha.12"
50
50
  ```
51
51
 
52
52
  Keep the `@mnstry/` scope — see the command-form note above for why the
@@ -145,8 +145,9 @@ explicit consent that names its actor.
145
145
  | --- | --- | --- | --- |
146
146
  | Health | `GET /health` | none | service name, workspace identity, runtime identifier, PID, loopback host and port, executable digest, start time, status |
147
147
  | Status | `GET /status` | bearer | the health fields, loop state, last tick, last error code, and per-view freshness with held notes counted, not named |
148
- | Tick now | `POST /tick` | bearer | the state of the tick that ran |
148
+ | Tick now | `POST /tick` | bearer | the state of the tick that ran. The body may also name one view (`scopeId`, a contract identifier), which that tick prepares and publishes once more, asking the app again |
149
149
  | Stop | `POST /stop` | bearer | an acknowledgement naming the runtime and PID, then the service ends |
150
+ | Plugin challenge, hello, lease, release, status | `POST /plugin/challenge`, `/plugin/hello`, `/plugin/lease`, `/plugin/release`, `/plugin/status` | a handshake over one vault's key, which never crosses the wire | Atelier's Obsidian plugin inside that vault: presence and read-only status of that one view ([obsidian-plugin.md](obsidian-plugin.md)) |
150
151
 
151
152
  Health carries no path, no note title, no source name and no withheld
152
153
  identity. There is no other operation: no file serving, no command, no
@@ -159,13 +160,29 @@ The listener refuses a request before looking its operation up when:
159
160
  also refuses a name that resolves to loopback;
160
161
  - `Sec-Fetch-Site` is present and is neither `none` nor `same-origin`, or
161
162
  `Origin` is present and is not the listener itself;
162
- - the path is not one of the four, exactly and without a query, or the method
163
+ - the path is not one of the nine, exactly and without a query, or the method
163
164
  is not that path's method.
164
165
 
165
166
  Status, tick and stop additionally refuse without the bearer of the running
166
167
  runtime. A `POST` payload is a JSON object of at most 1 KiB that names the
167
168
  runtime identifier it is meant for, so a request aimed at an earlier runtime on
168
- the same port does nothing.
169
+ the same port does nothing (409, `request-names-another-runtime`). A tick may
170
+ also name one view by its `scopeId`, which must be a contract identifier as
171
+ the scope contract defines it (400, `request-invalid`); any other member, and a
172
+ `scopeId` on a stop, is refused (400, `request-member-unknown`). A refused
173
+ request runs nothing.
174
+
175
+ The plugin commands carry no credential in a header, and refuse one. The key
176
+ of a vault the workspace maintains is random per view and kept owner-only in
177
+ private state and in that vault's plugin data file; it never crosses the
178
+ wire. The service answers a challenge only for a key it holds, with a proof
179
+ over its own exact address, before the plugin sends anything that names the
180
+ vault; the plugin then proves the same key, and every later command and answer
181
+ is sealed with a key for that session, with a counter that only goes up. What
182
+ a session grants is the plugin commands for that one view and nothing else:
183
+ the runtime bearer is not accepted there, and a vault's key is not accepted by
184
+ status, tick or stop. A plugin payload is a JSON object of at most 1 KiB with
185
+ exactly the fields of its command.
169
186
 
170
187
  ### Status values and refusal cases
171
188
 
@@ -178,8 +195,9 @@ the same port does nothing.
178
195
  | `stale-record` | the recorded address is closed and the recorded PID is gone | starts; the new service replaces the record once it listens | refuses; nothing is proven to stop |
179
196
  | `pid-not-ours` | the recorded address is closed and a process has the recorded PID (the number was reused, or a process outlived its listener) | starts; that PID is never signalled | refuses; that PID is never signalled |
180
197
 
181
- `start` is serialized per workspace, spawns the service without a shell, and
182
- detaches it only when asked for a service that survives the launching command.
198
+ `start` is serialized per workspace, spawns the service without a shell, in
199
+ the root directory rather than the one the command runs in, and detaches it
200
+ only when asked for a service that survives the launching command.
183
201
  It waits for health to echo the runtime identifier it generated and the PID of
184
202
  the child it created. If that proof never arrives it stops only that child, by
185
203
  its process handle, and reports the private operational log,
@@ -198,6 +216,39 @@ to end and removes only the generated record. It never looks a process up by
198
216
  port, name or pattern. Ending a proven runtime that ignores the request is
199
217
  opt-in and is proven again immediately before the signal.
200
218
 
219
+ A runtime of an earlier release is replaced by a command that asks for a tick
220
+ with the start options of the installed entry (`requestServiceTick({ service
221
+ })`; `open` does). Every runtime records at its start the release it runs
222
+ (`executable.ext.release` in its record): the package version and a digest of
223
+ every runtime module it ships (`src/` and `contracts/`, by path and content),
224
+ and of the plugin it publishes into every vault (`plugins/`; none where a
225
+ package has no such folder).
226
+ A runtime of an earlier release proves itself `healthy` but records an earlier
227
+ version than this package's, this version with an entry module or modules of
228
+ other content, or no release at all (releases up to the one that began
229
+ recording it); or it refuses a tick that names a view, as releases up to
230
+ 0.2.0-alpha.11 do (their `POST` payload has exactly one member). Versions are
231
+ ordered as semantic versions, a prerelease below its release
232
+ (`0.2.0-alpha.11` < `0.2.0-alpha.12` < `0.2.0`; `releaseStanding` in
233
+ `lifecycle.mjs`). It is stopped as `stop` stops it and the installed entry is
234
+ started, detached, under the consent already recorded; the answer carries
235
+ `restarted: "outdated"`, and `open` shows `service: restarted (outdated)`. A
236
+ runtime that records a later version, or another version that cannot be
237
+ ordered against the installed one, is never
238
+ replaced by this release, nor asked for the tick: the answer is
239
+ `service-other-release` (`open`: `service-unavailable`, with the next step
240
+ `atelier obsidian service stop`, then open again). So two installations used
241
+ on one workspace, a global and a project-local one for example, do not replace
242
+ each other's runtime on every open; within one version, other content is
243
+ replaced. The same version string is compared by content even when it cannot
244
+ be ordered (a fork's `dev`, say), so the runtime `open` just started is always
245
+ this release and "service stop, then open" cannot loop. A listener of now refuses a tick only when it names another runtime,
246
+ which happens when a concurrent command replaced the runtime between the check
247
+ and the request: the record is read again, the runtime that took its place is
248
+ asked, and nothing is restarted. A `busy` runtime is not stopped, and anything
249
+ that is not a proven runtime of this workspace is never stopped. Without the
250
+ start options the runtime is only reported (`service-outdated`).
251
+
201
252
  The service itself refuses to start beside a runtime of the same workspace
202
253
  that proves itself, and ends cleanly when its record no longer names it. Two
203
254
  engines can never tick one workspace together: each tick holds a private
@@ -254,14 +305,14 @@ error, 3 the operation ran and its answer is not success.
254
305
 
255
306
  | Operation | What it does | Writes |
256
307
  | --- | --- | --- |
257
- | `status` | enablement, machine settings, proven service state, per-view freshness, whether an apply operation exists | nothing |
308
+ | `status` | enablement, machine settings, proven service state, per-view freshness with its diagnostics (a code, the rule and the in-view note concerned, never a value; see [the Obsidian contract](obsidian-contract.md#notes-that-were-laid-out-anyway)), whether an apply operation exists | nothing |
258
309
  | `scope list`, `scope show ID` | the declared views | nothing |
259
310
  | `audience show`, `audience set A,B`, `audience clear` | the audiences this machine lets into a view; none by default, which publishes an empty view. A change invalidates every view at the next tick | private machine settings |
260
311
  | `mode show`, `mode set manual\|automatic` | `automatic` refuses without an installed, matching, active automatic policy | private machine settings |
261
312
  | `policy show`, `policy install FILE`, `policy revoke` | `install` validates against the apply-policy contract and stores the policy owner-only beside the machine settings, never in a project, a repository or a note. `revoke` marks the stored policy revoked, which the engine reads before its very next dispatch, and returns the mode to manual | private machine settings |
262
313
  | `service start`, `service status`, `service stop` | `startService`, `serviceStatus`, `stopService`. The first start needs `--consent-actor ID` | what the lifecycle writes |
263
314
  | `service unit --print` | the text `buildStartupAdapter` returns. Installing a unit is not offered | nothing |
264
- | `open [--scope ID]` | starts or reconnects the owned service, asks it for a tick, reads the view back, qualifies the installed app, has the vault opened | what the service writes |
315
+ | `open [--scope ID]` | starts or reconnects the owned service, asks it for a tick, reads the view back, qualifies the installed app, makes the app know the vault (through the app while it runs; in its settings while none runs), has the vault opened, and asks again for a view the app kept from publication | what the service writes; the app's vault list (see [Obsidian's vault list](obsidian-contract.md#obsidians-vault-list)) |
265
316
 
266
317
  Reaching the installed app or the operating system is never a default.
267
318
  `open`, `service start` and `service unit` refuse with
@@ -276,16 +327,16 @@ step. Only `current` is success:
276
327
 
277
328
  | Outcome | Meaning |
278
329
  | --- | --- |
279
- | `current` | the proven service ticked after the request; the view's persisted freshness is `current`; read back independently, the trusted generation is the prepared one and every note has the bytes it was published with; the app meets the minimum version, was asked to open this vault, answers for exactly this vault and has finished reading it |
330
+ | `current` | the proven service ticked after the request; the view's persisted freshness is `current`; read back independently, the trusted generation is the prepared one and every note has the bytes it was published with; the app meets the minimum version, knows this vault as one of its vaults, was asked to open it, answers for exactly this vault and has finished reading it |
280
331
  | `updating` | a publication is under way, a tick outlasted the wait, or a note differs from the trusted generation and has not been looked at yet |
281
332
  | `held-for-your-edit` | an edited note is preserved and held |
282
333
  | `stale-readable` | a last good vault exists and reads back, but is not proven to be the present generation |
283
334
  | `not-prepared` | no generation of this view has been published |
284
335
  | `publisher-conflict` | another publisher or an uncoordinated editor holds the vault |
285
- | `app-missing`, `app-cli-unavailable`, `app-version-unsupported` | no installation; no command-line capability; below the minimum version, or a version that cannot be read (reason `no-vault-open` when the app runs with no vault open and its command line answers nothing else) |
286
- | `launch-failed` | the operating system refused, or the app never answered for this vault |
336
+ | `app-missing`, `app-cli-unavailable`, `app-version-unsupported` | no installation; no command-line capability, or the command line turned off in Obsidian (`cli-turned-off`); below the minimum version, or a version that cannot be read (reason `no-vault-open` when the app runs with no vault open and its command line answers nothing else) |
337
+ | `launch-failed` | the vault could not be added to the app's list (a typed reason names why), the operating system refused, or the app never answered for this vault |
287
338
  | `indexing` | the app answers for this vault and has not finished reading it |
288
- | `service-unavailable` | the service could not be started or is not provably ours (`occupied`, no consent yet, a start that never proved ownership) |
339
+ | `service-unavailable` | the service could not be started or is not provably ours (`occupied`, no consent yet, a start that never proved ownership), or it runs a later release than this command (`service-other-release`) |
289
340
  | `busy` | the service is ours and in a long tick |
290
341
  | `disabled` | the integration is off or not declared |
291
342