wowbagger 0.1.0-alpha.1 → 0.1.0-alpha.2
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.
- package/CHANGELOG.md +33 -0
- package/README.md +20 -9
- package/docs/mutation-contract.md +1036 -0
- package/docs/work-claim-contract.md +599 -0
- package/package.json +5 -2
- package/scripts/migrate-schema-2.js +7 -0
- package/skills/wowbagger/SKILL.md +27 -13
- package/src/claim-journal.js +2 -2
- package/src/claim-publication.js +29 -3
- package/src/cli.js +46 -5
- package/src/git-reconciliation.js +10 -6
- package/src/ledger.js +1 -0
- package/src/mutation.js +1 -1
- package/src/schema-migration.js +1 -1
|
@@ -0,0 +1,1036 @@
|
|
|
1
|
+
# Local mutation contract
|
|
2
|
+
|
|
3
|
+
Status: versions 1 and 2 are defined; the pre-alpha standalone
|
|
4
|
+
local-filesystem runtime currently emits version 2.
|
|
5
|
+
|
|
6
|
+
This document defines the machine contract implemented by the local-filesystem
|
|
7
|
+
mutation backend. It supplements [SPEC.md](../SPEC.md) and
|
|
8
|
+
[ADR 0003](adr/0003-local-mutation-and-cas.md); it does not relax schema version
|
|
9
|
+
1 or 2 lifecycle invariants.
|
|
10
|
+
|
|
11
|
+
The executable supports `validate`, `ready`, and the commands below. Clients
|
|
12
|
+
must still call `capabilities` and honor its advertised limits before assuming a
|
|
13
|
+
backend can provide a particular write guarantee.
|
|
14
|
+
|
|
15
|
+
## Contract versions
|
|
16
|
+
|
|
17
|
+
Version 1 remains the frozen contract described by the version 1 envelopes and
|
|
18
|
+
capability example below. Version 2 retains every version 1 request, response,
|
|
19
|
+
state, exit, locking, CAS, publication, and recovery rule except for these
|
|
20
|
+
explicit deltas:
|
|
21
|
+
|
|
22
|
+
- every core command envelope in this contract carries `contract_version: 2`;
|
|
23
|
+
- one non-empty ledger may use schema version 1 or schema version 2, but never a
|
|
24
|
+
mixture;
|
|
25
|
+
- the capability envelope uses the fixed local mutation scope described in
|
|
26
|
+
section 4 and advertises `patch`; and
|
|
27
|
+
- adapter contract version 2 may invoke `patch` as an approved mutation.
|
|
28
|
+
|
|
29
|
+
The bootstrap wire, work-claim API, adapter approval, instruction, handoff, and
|
|
30
|
+
fixture-format versions are separate version domains and remain version 1.
|
|
31
|
+
|
|
32
|
+
Version negotiation uses distinct existing fields. A core consumer MUST read
|
|
33
|
+
the top-level `contract_version` from `capabilities --json`. A work-claim
|
|
34
|
+
consumer MUST read `result.operations.work_claim.api_version` from
|
|
35
|
+
`claim capabilities --ledger <dir> --json`. It MUST NOT compare a claim
|
|
36
|
+
response's top-level `contract_version` with the core version. That claim member
|
|
37
|
+
remains the version 1 envelope marker for exact version 1 consumers.
|
|
38
|
+
|
|
39
|
+
This rule is the migration path for generic consumers: dispatch by command
|
|
40
|
+
namespace first, then check the version field for that domain. No envelope
|
|
41
|
+
member changes, so existing exact-member consumers remain compatible.
|
|
42
|
+
|
|
43
|
+
## 1. Scope
|
|
44
|
+
|
|
45
|
+
The contract keeps four concerns separate:
|
|
46
|
+
|
|
47
|
+
- capabilities describes guarantees and limitations;
|
|
48
|
+
- inspect reads one item and returns a revision from the same bytes it exposes;
|
|
49
|
+
- create publishes one caller-identified triage item;
|
|
50
|
+
- transition changes one existing item through a guarded lifecycle edge; and
|
|
51
|
+
- patch changes one existing item's caller-supplied fields (section 9).
|
|
52
|
+
|
|
53
|
+
Fenced work claiming is unsupported. Advisory claims may be visible through a
|
|
54
|
+
Git common directory, but they do not protect publication or coordinate a
|
|
55
|
+
mutation. A write lock protects a short mutation attempt; it is not a claim,
|
|
56
|
+
assignment, lease, or reservation. The separate [fenced work-claim
|
|
57
|
+
contract](work-claim-contract.md) defines a future backend protocol; it does
|
|
58
|
+
not add a fencing guarantee to either mutation-contract version.
|
|
59
|
+
|
|
60
|
+
If a future backend advertises safely fenced claims while retaining these
|
|
61
|
+
legacy entry points, it must run them through the same coordinator: transition
|
|
62
|
+
refuses an active claimed `(ledger_namespace, item_id)` and create refuses an
|
|
63
|
+
identity with claim history. Until then, the local runtime's unsupported
|
|
64
|
+
capability is authoritative; callers cannot combine this API with an external
|
|
65
|
+
claim hint and infer fencing.
|
|
66
|
+
|
|
67
|
+
The first backend coordinates only cooperative Wowbagger writers using the same
|
|
68
|
+
ledger directory in one working copy. It does not coordinate clones, worktrees,
|
|
69
|
+
machines, hostile or non-cooperating writers, or Git operations. Its write
|
|
70
|
+
scope is one Markdown item and it has no multi-item atomicity.
|
|
71
|
+
|
|
72
|
+
Schema versions 1 and 2 remain canonical Markdown. One non-empty ledger must
|
|
73
|
+
use one schema version; complete-ledger validation rejects a mixture. Revision
|
|
74
|
+
and lock data are transport state and are not persisted in item frontmatter.
|
|
75
|
+
|
|
76
|
+
Validation and ready selection use the versioned semantics in SPEC.md. Schema
|
|
77
|
+
version 1 ready tasks have an empty depends_on list. Schema version 2 ready
|
|
78
|
+
tasks may retain declared prerequisites, but every target must have status
|
|
79
|
+
done. These schema rules do not change the version 1 request or response
|
|
80
|
+
envelopes in this document.
|
|
81
|
+
|
|
82
|
+
## 2. Commands and transport
|
|
83
|
+
|
|
84
|
+
The local commands are:
|
|
85
|
+
|
|
86
|
+
~~~text
|
|
87
|
+
wowbagger capabilities --json
|
|
88
|
+
wowbagger inspect --ledger <dir> --id <id> --json
|
|
89
|
+
wowbagger create --ledger <dir> --input <json-file|-> --json
|
|
90
|
+
wowbagger transition --ledger <dir> --input <json-file|-> --json
|
|
91
|
+
wowbagger patch --ledger <dir> --input <json-file|-> --json
|
|
92
|
+
wowbagger mint-id [--date YYYY-MM-DD] --json
|
|
93
|
+
~~~
|
|
94
|
+
|
|
95
|
+
A dash for --input means standard input. File and standard-input requests have
|
|
96
|
+
identical semantics. Request bytes must be valid UTF-8 JSON with one top-level
|
|
97
|
+
object and no duplicate member names at any depth. Duplicate members are
|
|
98
|
+
invalid; a parser must not apply last-member-wins behaviour.
|
|
99
|
+
|
|
100
|
+
Unknown, missing, and repeated command arguments are invalid-request. Create,
|
|
101
|
+
transition, and patch use JSON input rather than parallel field flags.
|
|
102
|
+
|
|
103
|
+
### Standard output and standard error
|
|
104
|
+
|
|
105
|
+
For an invocation with --json, the process writes exactly one compact JSON
|
|
106
|
+
object followed by one LF to standard output.
|
|
107
|
+
|
|
108
|
+
Expected request, validation, lookup, conflict, lock, capability, and lifecycle
|
|
109
|
+
failures leave standard error empty. An unexpected operating failure may write
|
|
110
|
+
one UTF-8 diagnostic line of at most 1024 bytes to standard error. That line is
|
|
111
|
+
for a human and must not contain credentials, raw lock contents, or a path
|
|
112
|
+
outside the configured ledger. Automation uses the JSON envelope.
|
|
113
|
+
|
|
114
|
+
A process crash before an envelope is emitted is outside the command protocol.
|
|
115
|
+
A caller must treat the outcome of a mutating command as unknown and follow the
|
|
116
|
+
recovery rules in section 10.
|
|
117
|
+
|
|
118
|
+
### Response envelopes
|
|
119
|
+
|
|
120
|
+
A successful read-only command has exactly:
|
|
121
|
+
|
|
122
|
+
~~~json
|
|
123
|
+
{
|
|
124
|
+
"ok": true,
|
|
125
|
+
"command": "inspect",
|
|
126
|
+
"contract_version": 1,
|
|
127
|
+
"result": {}
|
|
128
|
+
}
|
|
129
|
+
~~~
|
|
130
|
+
|
|
131
|
+
A successful create, transition, or patch adds state:
|
|
132
|
+
|
|
133
|
+
~~~json
|
|
134
|
+
{
|
|
135
|
+
"ok": true,
|
|
136
|
+
"command": "create",
|
|
137
|
+
"contract_version": 1,
|
|
138
|
+
"state": "committed",
|
|
139
|
+
"result": {}
|
|
140
|
+
}
|
|
141
|
+
~~~
|
|
142
|
+
|
|
143
|
+
A read-only error has exactly:
|
|
144
|
+
|
|
145
|
+
~~~json
|
|
146
|
+
{
|
|
147
|
+
"ok": false,
|
|
148
|
+
"command": "inspect",
|
|
149
|
+
"contract_version": 1,
|
|
150
|
+
"error": {
|
|
151
|
+
"code": "item-not-found",
|
|
152
|
+
"message": "The requested item was not found.",
|
|
153
|
+
"details": {}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
~~~
|
|
157
|
+
|
|
158
|
+
Every create, transition, or patch error has a state member:
|
|
159
|
+
|
|
160
|
+
~~~json
|
|
161
|
+
{
|
|
162
|
+
"ok": false,
|
|
163
|
+
"command": "transition",
|
|
164
|
+
"contract_version": 1,
|
|
165
|
+
"state": "unchanged",
|
|
166
|
+
"error": {
|
|
167
|
+
"code": "revision-conflict",
|
|
168
|
+
"message": "The item changed after it was inspected.",
|
|
169
|
+
"details": {}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
~~~
|
|
173
|
+
|
|
174
|
+
No expected envelope has undocumented root members.
|
|
175
|
+
|
|
176
|
+
Mutation state values mean:
|
|
177
|
+
|
|
178
|
+
| State | Meaning |
|
|
179
|
+
|---|---|
|
|
180
|
+
| unchanged | This invocation did not create, remove, rename, or byte-modify a Markdown item. |
|
|
181
|
+
| committed | The intended final path was re-read and contains exactly the expected published bytes. |
|
|
182
|
+
| unknown | Publication was attempted, but the process cannot establish which bytes are visible. |
|
|
183
|
+
|
|
184
|
+
Transient locks and temporary files are not Markdown items. Their possible
|
|
185
|
+
presence is reported separately as bounded recovery_artifacts.
|
|
186
|
+
|
|
187
|
+
### Exit status
|
|
188
|
+
|
|
189
|
+
| Exit | Condition | Error codes |
|
|
190
|
+
|---:|---|---|
|
|
191
|
+
| 0 | Successful command; a mutation is state committed. | none |
|
|
192
|
+
| 2 | Argument, request, lookup, or candidate/lifecycle-precondition failure. | invalid-request, item-not-found, transition-precondition-failed, patch-precondition-failed, candidate-invalid |
|
|
193
|
+
| 3 | The complete configured ledger is invalid. | ledger-invalid |
|
|
194
|
+
| 4 | Cooperative comparison, lock, identity, or default-path conflict. | revision-conflict, lock-held, id-collision, path-collision |
|
|
195
|
+
| 5 | The backend lacks the required capability or write scope. | atomic-scope-required, capability-unavailable |
|
|
196
|
+
| 6 | An unexpected operating or post-publication recovery condition. | operation-failed, post-commit-recovery-required, write-outcome-unknown |
|
|
197
|
+
|
|
198
|
+
Only exit 0 is normal completion. A client must inspect mutation state on every
|
|
199
|
+
nonzero create, transition, or patch result.
|
|
200
|
+
|
|
201
|
+
## 3. Deterministic invalid-request issues
|
|
202
|
+
|
|
203
|
+
invalid-request details are:
|
|
204
|
+
|
|
205
|
+
~~~json
|
|
206
|
+
{
|
|
207
|
+
"issues": [
|
|
208
|
+
{
|
|
209
|
+
"path": "/body",
|
|
210
|
+
"code": "missing-member",
|
|
211
|
+
"message": "Required member body is missing."
|
|
212
|
+
}
|
|
213
|
+
]
|
|
214
|
+
}
|
|
215
|
+
~~~
|
|
216
|
+
|
|
217
|
+
Each issue has exactly path, code, and message:
|
|
218
|
+
|
|
219
|
+
- path is an RFC 6901 JSON Pointer into the decoded request;
|
|
220
|
+
- the empty string identifies the request root;
|
|
221
|
+
- command-line issues use the synthetic root /arguments followed by the
|
|
222
|
+
zero-based argument index when known;
|
|
223
|
+
- code is one of invalid-json, duplicate-key, missing-member, unknown-member,
|
|
224
|
+
invalid-type, invalid-value, missing-argument, repeated-argument, or
|
|
225
|
+
unknown-argument; and
|
|
226
|
+
- message is the stable sentence shown by the normative vector for that issue.
|
|
227
|
+
|
|
228
|
+
A duplicate-key path points to the duplicate occurrence. A syntactically
|
|
229
|
+
unrecoverable JSON document produces one invalid-json issue at the empty path.
|
|
230
|
+
Otherwise, issues are aggregated and sorted by path, then code, then message,
|
|
231
|
+
using ascending Unicode code-point order without locale collation.
|
|
232
|
+
|
|
233
|
+
Unknown members at the request root are rejected. Unknown members inside item
|
|
234
|
+
are schema extensions and are allowed only when they do not change core
|
|
235
|
+
semantics. Controlled core names forbidden by create are invalid-value issues.
|
|
236
|
+
The status refusal additionally teaches the lifecycle rule, with the stable
|
|
237
|
+
message `Item member status is controlled by Wowbagger. Create assigns triage;
|
|
238
|
+
a transition from triage to backlog accepts the item into ready.` A caller
|
|
239
|
+
therefore learns the assigned status from the create result's `core.status`
|
|
240
|
+
and the accepting transition from the refusal, without reading this document
|
|
241
|
+
first.
|
|
242
|
+
|
|
243
|
+
## 4. Capabilities
|
|
244
|
+
|
|
245
|
+
The local backend, run from inside a git working copy, returns:
|
|
246
|
+
|
|
247
|
+
~~~json
|
|
248
|
+
{
|
|
249
|
+
"ok": true,
|
|
250
|
+
"command": "capabilities",
|
|
251
|
+
"contract_version": 1,
|
|
252
|
+
"result": {
|
|
253
|
+
"backend": {
|
|
254
|
+
"name": "local-filesystem",
|
|
255
|
+
"coordination_scope": "shared-git-directory-cooperative-writers"
|
|
256
|
+
},
|
|
257
|
+
"operations": {
|
|
258
|
+
"inspect": {
|
|
259
|
+
"supported": true,
|
|
260
|
+
"write_scope": "none",
|
|
261
|
+
"cas_scope": "none"
|
|
262
|
+
},
|
|
263
|
+
"create": {
|
|
264
|
+
"supported": true,
|
|
265
|
+
"write_scope": "single-item",
|
|
266
|
+
"cas_scope": "requested-id-lock",
|
|
267
|
+
"publication_visibility": "atomic-no-clobber-or-fail",
|
|
268
|
+
"publication_probe": "per-ledger-operation"
|
|
269
|
+
},
|
|
270
|
+
"transition": {
|
|
271
|
+
"supported": true,
|
|
272
|
+
"write_scope": "single-item",
|
|
273
|
+
"cas_scope": "exact-byte-sha256"
|
|
274
|
+
},
|
|
275
|
+
"work_claim": {
|
|
276
|
+
"supported": true,
|
|
277
|
+
"api_version": 1,
|
|
278
|
+
"mode": "advisory",
|
|
279
|
+
"claim_protected_publication": false,
|
|
280
|
+
"fencing_enforced_at": "none",
|
|
281
|
+
"safe_exclusive_dispatch": false
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
"durability": {
|
|
285
|
+
"temporary_file_sync": "required-before-publication",
|
|
286
|
+
"directory_sync": "best-effort-when-supported",
|
|
287
|
+
"post_publication_verification": "exact-bytes-required",
|
|
288
|
+
"power_loss_guarantee": "none"
|
|
289
|
+
},
|
|
290
|
+
"limits": {
|
|
291
|
+
"multi_item_atomicity": false,
|
|
292
|
+
"cross_clone_coordination": false,
|
|
293
|
+
"cross_worktree_coordination": true,
|
|
294
|
+
"cross_machine_coordination": false,
|
|
295
|
+
"noncooperating_writer_protection": false,
|
|
296
|
+
"automatic_stale_lock_breaking": false
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
~~~
|
|
301
|
+
|
|
302
|
+
Outside a git working copy, three members flip together:
|
|
303
|
+
`backend.coordination_scope` becomes `"same-working-copy-cooperative-writers"`,
|
|
304
|
+
`operations.work_claim.supported` becomes `false`, and
|
|
305
|
+
`limits.cross_worktree_coordination` becomes `false`. Every other member of the
|
|
306
|
+
envelope, including the rest of `operations.work_claim`
|
|
307
|
+
(`api_version`, `mode`, `claim_protected_publication`, `fencing_enforced_at`,
|
|
308
|
+
`safe_exclusive_dispatch`), is fixed regardless of git presence: work claims are
|
|
309
|
+
always advisory, never fence a writer, and never advertise safe exclusive
|
|
310
|
+
dispatch.
|
|
311
|
+
|
|
312
|
+
`capabilities` resolves the git common directory by walking upward from the
|
|
313
|
+
`--ledger` directory when given, or from the current working directory
|
|
314
|
+
otherwise (see `resolveGitCommonDir` in `src/claim-store.js`); presence of a
|
|
315
|
+
`.git` directory or file at or above that point is what flips the three
|
|
316
|
+
members above. This is the one input to `capabilities`, so the response is
|
|
317
|
+
deterministic for a given working directory but not fixed across working
|
|
318
|
+
directories.
|
|
319
|
+
|
|
320
|
+
### Contract version 2 capability delta
|
|
321
|
+
|
|
322
|
+
The preceding JSON and three-member Git-dependent coupling remain the exact
|
|
323
|
+
version 1 definition. Version 2 changes only the following capability paths;
|
|
324
|
+
all omitted paths retain their version 1 values:
|
|
325
|
+
|
|
326
|
+
| Path | Version 2 value |
|
|
327
|
+
|---|---|
|
|
328
|
+
| `contract_version` | `2` |
|
|
329
|
+
| `result.backend.coordination_scope` | `"same-working-copy-cooperative-writers"` |
|
|
330
|
+
| `result.operations.patch` | `{"supported":true,"write_scope":"single-item","cas_scope":"exact-byte-sha256"}` |
|
|
331
|
+
| `result.limits.cross_worktree_coordination` | `false` |
|
|
332
|
+
|
|
333
|
+
`result.operations.work_claim.supported` remains independently derived from
|
|
334
|
+
Git-common-directory discovery: it is `true` when claims are visible there and
|
|
335
|
+
`false` otherwise. This core capability envelope reports the unbound default
|
|
336
|
+
claim profile. It does not read a provisioned ledger namespace and does not
|
|
337
|
+
prove that a ledger is provisioned. Use
|
|
338
|
+
`claim capabilities --ledger <dir> --json` to discover whether that ledger is
|
|
339
|
+
unprovisioned and advisory or provisioned and merge-coordinated. Automation
|
|
340
|
+
MUST gate `publish-claimed` on that ledger-specific response. Neither result
|
|
341
|
+
elevates the fixed mutation scope. Version 2 keeps
|
|
342
|
+
`transition.write_scope: "single-item"`,
|
|
343
|
+
`transition.cas_scope: "exact-byte-sha256"`, and
|
|
344
|
+
`limits.multi_item_atomicity: false`. Work-claim visibility across worktrees is
|
|
345
|
+
never cross-worktree mutation coordination.
|
|
346
|
+
|
|
347
|
+
Because capabilities takes no ledger content and does not write, it still
|
|
348
|
+
cannot prove that a particular filesystem supports the required atomic
|
|
349
|
+
no-clobber publication primitive. Create probes or attempts that primitive for
|
|
350
|
+
the configured ledger and returns capability-unavailable unchanged when it is
|
|
351
|
+
unavailable.
|
|
352
|
+
|
|
353
|
+
Directory fsync is capability-reported best effort. Neither a successful file
|
|
354
|
+
sync nor a directory sync is a universal power-loss durability guarantee.
|
|
355
|
+
|
|
356
|
+
## 5. Reads, revisions, and lossless inspection
|
|
357
|
+
|
|
358
|
+
An item revision is:
|
|
359
|
+
|
|
360
|
+
sha256:<64 lowercase hexadecimal characters>
|
|
361
|
+
|
|
362
|
+
The digest covers the complete raw item-file bytes. It is not computed from
|
|
363
|
+
normalized YAML, JSON, line endings, the Markdown body, or a Git object.
|
|
364
|
+
|
|
365
|
+
Inspect loads and validates the complete ledger. For the requested item, one
|
|
366
|
+
validated regular-file handle supplies one raw byte buffer. The implementation
|
|
367
|
+
must parse the item, derive its normalized core view and body, compute the
|
|
368
|
+
revision, and produce source_base64 from that same buffer. It must never combine
|
|
369
|
+
parsed data from one file version with a digest or source from another.
|
|
370
|
+
|
|
371
|
+
The successful item shape is:
|
|
372
|
+
|
|
373
|
+
~~~json
|
|
374
|
+
{
|
|
375
|
+
"id": "wb_...",
|
|
376
|
+
"path": "wb_....md",
|
|
377
|
+
"revision": "sha256:<lowercase hex digest>",
|
|
378
|
+
"source_encoding": "base64",
|
|
379
|
+
"source_media_type": "text/markdown; charset=utf-8",
|
|
380
|
+
"source_base64": "<RFC 4648 base64 without line breaks>",
|
|
381
|
+
"core": {},
|
|
382
|
+
"body": "exact decoded body suffix"
|
|
383
|
+
}
|
|
384
|
+
~~~
|
|
385
|
+
|
|
386
|
+
path is a forward-slash, ledger-relative display path and is not identity.
|
|
387
|
+
Decoding source_base64 recovers every original byte. The decoded bytes must
|
|
388
|
+
hash to revision and must be valid UTF-8.
|
|
389
|
+
|
|
390
|
+
The promotion rule: the item level carries addressing and payload members
|
|
391
|
+
only — id, path, revision, source_encoding, source_media_type, source_base64,
|
|
392
|
+
and body. Every frontmatter field is read from core. id is the single member
|
|
393
|
+
present at both levels, because the item level must identify the resource it
|
|
394
|
+
addresses; no other frontmatter field is promoted, and none will be.
|
|
395
|
+
|
|
396
|
+
core contains only fields defined by supported item schema versions:
|
|
397
|
+
|
|
398
|
+
- schema_version, id, title, kind, status, created, updated;
|
|
399
|
+
- provenance.source and provenance.recorded_at;
|
|
400
|
+
- depends_on and related;
|
|
401
|
+
- optional parent, snoozed_until, completed, killed, archived;
|
|
402
|
+
- optional number and priority, the caller-supplied schema fields;
|
|
403
|
+
and
|
|
404
|
+
- decisions with only their defined action, date, summary, rationale, and
|
|
405
|
+
optional rollup id/status members.
|
|
406
|
+
|
|
407
|
+
Permitted unknown top-level fields, extra provenance members, and unknown
|
|
408
|
+
members or YAML representations inside extension data are omitted from core.
|
|
409
|
+
They remain recoverable from source_base64. This avoids pretending that every
|
|
410
|
+
valid YAML node, tag, anchor, integer, or mapping key has a lossless ordinary
|
|
411
|
+
JSON representation.
|
|
412
|
+
|
|
413
|
+
body is the exact UTF-8-decoded byte suffix after the closing frontmatter
|
|
414
|
+
delimiter. It is not trimmed or normalized. An item with no bytes after the
|
|
415
|
+
delimiter LF has body "". A conventional blank line before Markdown means body
|
|
416
|
+
begins with LF.
|
|
417
|
+
|
|
418
|
+
item-not-found exits 2 and has details containing only id. ledger-invalid exits
|
|
419
|
+
3 and has details.validation_errors equal to the existing deterministic
|
|
420
|
+
SPEC.md validation-error sequence. Neither read-only error has state.
|
|
421
|
+
|
|
422
|
+
## 6. Cooperative lock and snapshot protocol
|
|
423
|
+
|
|
424
|
+
Per-ID locks live at:
|
|
425
|
+
|
|
426
|
+
<ledger>/.wowbagger-locks/<item-id>.lock
|
|
427
|
+
|
|
428
|
+
Create locks its requested new ID and every existing parent or dependency ID.
|
|
429
|
+
Transition locks the target, its referenced parent and dependency items, every
|
|
430
|
+
item whose depends_on contains the target, and every direct child when the
|
|
431
|
+
target is an epic. IDs are unique and acquired in ascending immutable-ID order.
|
|
432
|
+
|
|
433
|
+
Because referring items can be discovered while another cooperative writer is
|
|
434
|
+
finishing, lock acquisition is a closure loop:
|
|
435
|
+
|
|
436
|
+
1. load the complete valid ledger and determine the relevant IDs;
|
|
437
|
+
2. acquire the ordered lock set;
|
|
438
|
+
3. re-read the complete ledger and recompute the relevant set;
|
|
439
|
+
4. if the set expanded, release all locks and retry with the expanded ordered
|
|
440
|
+
set; otherwise continue; and
|
|
441
|
+
5. fail unchanged with operation-failed if a bounded implementation retry limit
|
|
442
|
+
is exhausted.
|
|
443
|
+
|
|
444
|
+
Cooperative create operations that add parent or dependency edges obey the same
|
|
445
|
+
protocol, so holding the target ID lock prevents a new incoming edge from being
|
|
446
|
+
published during terminalization.
|
|
447
|
+
|
|
448
|
+
After the stable lock set is held, the backend re-reads, re-parses, revalidates,
|
|
449
|
+
and re-hashes the target and every relevant referenced or referring item from
|
|
450
|
+
their validated file handles. It then validates the complete current ledger.
|
|
451
|
+
Transition compares expected_revision only after this locked re-read.
|
|
452
|
+
|
|
453
|
+
A transition constructs an in-memory complete ledger with exactly the proposed
|
|
454
|
+
target bytes substituted, then runs complete-ledger validation again before
|
|
455
|
+
publication. If another item would need mutation, the target is not published.
|
|
456
|
+
|
|
457
|
+
### Lock metadata
|
|
458
|
+
|
|
459
|
+
A writer creates the lock file exclusively as valid UTF-8 JSON no larger than
|
|
460
|
+
4096 bytes:
|
|
461
|
+
|
|
462
|
+
~~~json
|
|
463
|
+
{
|
|
464
|
+
"lock_version": 1,
|
|
465
|
+
"item_id": "wb_...",
|
|
466
|
+
"operation": "transition",
|
|
467
|
+
"writer_id": "opaque-random-value",
|
|
468
|
+
"started_at": "2030-01-10T12:34:56.789Z"
|
|
469
|
+
}
|
|
470
|
+
~~~
|
|
471
|
+
|
|
472
|
+
writer_id is an opaque ASCII string of 1 through 128 characters. operation is
|
|
473
|
+
create, transition, or patch. The remaining values must match their schema and
|
|
474
|
+
lock path. Metadata contains no credentials, user name, host name, or command
|
|
475
|
+
arguments.
|
|
476
|
+
|
|
477
|
+
A reader reads at most 4097 bytes. A lock larger than 4096 bytes, invalid UTF-8,
|
|
478
|
+
duplicate-key JSON, invalid JSON, unknown members, or invalid field values is
|
|
479
|
+
still held. lock-held details set owner to null and owner_diagnostic to exactly
|
|
480
|
+
one of too-large, invalid-utf8, duplicate-key, invalid-json, or invalid-shape.
|
|
481
|
+
Valid metadata returns owner and owner_diagnostic null. Raw invalid bytes are
|
|
482
|
+
never returned.
|
|
483
|
+
|
|
484
|
+
Locks are never removed automatically merely because started_at is old. Manual
|
|
485
|
+
recovery follows ADR 0003.
|
|
486
|
+
|
|
487
|
+
## 7. Create
|
|
488
|
+
|
|
489
|
+
### Request
|
|
490
|
+
|
|
491
|
+
Create accepts exactly:
|
|
492
|
+
|
|
493
|
+
~~~json
|
|
494
|
+
{
|
|
495
|
+
"id": "wb_...",
|
|
496
|
+
"item": {
|
|
497
|
+
"title": "Map the fictional route",
|
|
498
|
+
"kind": "task",
|
|
499
|
+
"provenance": {
|
|
500
|
+
"source": "fixture/mutations",
|
|
501
|
+
"recorded_at": "2030-01-10T12:34:56.789Z"
|
|
502
|
+
},
|
|
503
|
+
"depends_on": [],
|
|
504
|
+
"related": []
|
|
505
|
+
},
|
|
506
|
+
"body": "\nA fictional Markdown body.\n"
|
|
507
|
+
}
|
|
508
|
+
~~~
|
|
509
|
+
|
|
510
|
+
| Member | Required | Rules |
|
|
511
|
+
|---|---:|---|
|
|
512
|
+
| id | Yes | Caller-generated canonical Wowbagger ULID. |
|
|
513
|
+
| item | Yes | Frontmatter draft mapping. |
|
|
514
|
+
| item.title | Yes | Non-empty schema string. |
|
|
515
|
+
| item.kind | Yes | task or epic. |
|
|
516
|
+
| item.provenance | Yes | Valid required provenance; extension members are preserved. |
|
|
517
|
+
| item.depends_on | Yes | Valid relation list. |
|
|
518
|
+
| item.related | No | Valid relation list; omitted means empty. |
|
|
519
|
+
| item.parent | No | Valid epic ID. |
|
|
520
|
+
| item.snoozed_until | No | Valid ISO calendar date. |
|
|
521
|
+
| item.number | No | Positive integer; the caller-supplied schema handle. |
|
|
522
|
+
| item.priority | No | Non-negative integer; the caller-supplied schema priority. |
|
|
523
|
+
| item extension members | No | Permitted schema extensions. |
|
|
524
|
+
| body | Yes | JSON string; empty and LF-leading strings are distinct and valid. |
|
|
525
|
+
|
|
526
|
+
If a file named by `--input` cannot be read before a request ID is known,
|
|
527
|
+
create or transition returns `invalid-request` with one `invalid-value` issue at
|
|
528
|
+
`/input`, the stable message `Request input could not be read.`, and mutation
|
|
529
|
+
state `unchanged`.
|
|
530
|
+
|
|
531
|
+
The caller generates id with the timestamp for the intended creation instant
|
|
532
|
+
and at least 80 bits of collision-resistant entropy. Create validates its
|
|
533
|
+
canonical form before acquiring its per-ID lock. id is not accepted inside
|
|
534
|
+
item.
|
|
535
|
+
|
|
536
|
+
No caller writes the base32 encoding themselves: `wowbagger mint-id --json`
|
|
537
|
+
prints a canonical ID for now, `--date` selects another creation date, and
|
|
538
|
+
`src/mint.js` exports `mintId` so an adapter or plugin can mint one without
|
|
539
|
+
shelling out.
|
|
540
|
+
|
|
541
|
+
item must not supply schema_version, id, status, created, updated, completed,
|
|
542
|
+
killed, archived, decisions, or body. For a non-empty valid ledger, create
|
|
543
|
+
inserts the schema_version already used by every existing item. For an empty
|
|
544
|
+
ledger, it inserts schema_version 2. Create returns that selection in
|
|
545
|
+
`result.item.core.schema_version`. It also inserts status triage, created
|
|
546
|
+
and updated equal to the UTC date encoded by id, and related [] when omitted.
|
|
547
|
+
It adds no terminal date or decision.
|
|
548
|
+
|
|
549
|
+
The candidate complete ledger must validate before publication. After the
|
|
550
|
+
requested-ID lock and locked revalidation, create applies this collision
|
|
551
|
+
precedence:
|
|
552
|
+
|
|
553
|
+
1. If the requested ID exists anywhere in the ledger, return id-collision,
|
|
554
|
+
exit 4, and unchanged. details contain id, the existing item's
|
|
555
|
+
ledger-relative path, and actual_revision.
|
|
556
|
+
2. Otherwise, lstat the default path without following symbolic links. If any
|
|
557
|
+
filesystem object occupies it, return path-collision, exit 4, and unchanged.
|
|
558
|
+
details contain id, the ledger-relative default path, and occupant_kind.
|
|
559
|
+
occupant_kind is exactly item or directory. For item it also contains
|
|
560
|
+
occupying_id; for directory occupying_id is absent. The message is exactly
|
|
561
|
+
"The default item path is occupied by a different item." This stable human
|
|
562
|
+
message is shared by both kinds; automation distinguishes them through
|
|
563
|
+
occupant_kind.
|
|
564
|
+
3. Otherwise, continue to candidate validation.
|
|
565
|
+
|
|
566
|
+
Create never chooses a different ID or path for the caller. Collision checks
|
|
567
|
+
do not infer identity from a filename: an item whose frontmatter has another
|
|
568
|
+
ID occupies a path without claiming the requested ID. Complete-ledger
|
|
569
|
+
validation precedes these collision checks. A symbolic link, special file, or
|
|
570
|
+
invalid regular .md occupant therefore produces ledger-invalid rather than
|
|
571
|
+
path-collision. A real directory whose name ends in .md and whose contents
|
|
572
|
+
leave the complete ledger valid is valid input under SPEC.md and produces
|
|
573
|
+
path-collision when it occupies the default path.
|
|
574
|
+
|
|
575
|
+
The default final path is:
|
|
576
|
+
|
|
577
|
+
<ledger>/<id>.md
|
|
578
|
+
|
|
579
|
+
The filename is a portable default, not identity. The request cannot supply an
|
|
580
|
+
arbitrary path: the no-clobber publication protocol and the collision rules
|
|
581
|
+
are defined against the identity-derived default, and an arbitrary path would
|
|
582
|
+
let a caller aim them at anything in the ledger. A repository that prefers a
|
|
583
|
+
naming convention renames the file in Git after create — a reviewable change
|
|
584
|
+
that validation does not care about, because identity resolves from
|
|
585
|
+
frontmatter, never from the filename.
|
|
586
|
+
|
|
587
|
+
### Body
|
|
588
|
+
|
|
589
|
+
The generated source uses UTF-8 and LF for generated frontmatter lines. The
|
|
590
|
+
closing delimiter includes its required LF; body bytes are appended exactly as
|
|
591
|
+
the UTF-8 encoding of request.body.
|
|
592
|
+
|
|
593
|
+
- body "" produces no byte after the closing delimiter LF;
|
|
594
|
+
- body "\nText\n" produces the conventional blank line before Text; and
|
|
595
|
+
- create never invents, trims, or removes a body newline.
|
|
596
|
+
|
|
597
|
+
### Atomic no-clobber publication
|
|
598
|
+
|
|
599
|
+
Create must never reveal an empty or partially written final item. It:
|
|
600
|
+
|
|
601
|
+
1. creates a uniquely named non-.md temporary file in the final directory;
|
|
602
|
+
2. writes the complete intended bytes;
|
|
603
|
+
3. calls fsync or the platform-equivalent sync on the completed open temporary
|
|
604
|
+
file and waits for success;
|
|
605
|
+
4. publishes the complete temporary file to the absent final name with one
|
|
606
|
+
atomic no-clobber primitive, such as same-filesystem hard-link publication;
|
|
607
|
+
5. re-opens the final regular file without following a symbolic link and
|
|
608
|
+
verifies its exact expected bytes;
|
|
609
|
+
6. attempts directory fsync when supported; and
|
|
610
|
+
7. removes the temporary name and locks best effort.
|
|
611
|
+
|
|
612
|
+
A check-then-rename and a rename that can replace a destination are not
|
|
613
|
+
no-clobber publication. Opening the final path and copying bytes into it is
|
|
614
|
+
forbidden. Hard links are not assumed portable: if the configured filesystem
|
|
615
|
+
or platform cannot provide an atomic no-clobber primitive, create returns
|
|
616
|
+
capability-unavailable unchanged and cleans the temporary file best effort.
|
|
617
|
+
|
|
618
|
+
If a failure follows the publication attempt, the backend re-inspects the
|
|
619
|
+
known final path:
|
|
620
|
+
|
|
621
|
+
- exact expected bytes present means state committed; cleanup or sync failure
|
|
622
|
+
returns post-commit-recovery-required;
|
|
623
|
+
- proven absence means state unchanged and operation-failed; and
|
|
624
|
+
- a different or unreadable result means write-outcome-unknown with state
|
|
625
|
+
unknown.
|
|
626
|
+
|
|
627
|
+
Directory sync failure cannot turn verified exact bytes into unchanged. It
|
|
628
|
+
also does not justify a power-loss durability promise.
|
|
629
|
+
|
|
630
|
+
### Recovery by known ID
|
|
631
|
+
|
|
632
|
+
After a crash or unknown create result, automation inspects the caller-known ID.
|
|
633
|
+
If exact intended bytes are present, the create committed. If a different item
|
|
634
|
+
exists, the result is a collision requiring human resolution. Retry is allowed
|
|
635
|
+
only after inspect returns item-not-found and any reported lock or temporary
|
|
636
|
+
artifact has been handled under the audited recovery procedure. The atomic
|
|
637
|
+
no-clobber publication still protects an intervening creator.
|
|
638
|
+
|
|
639
|
+
Successful create returns state committed and the inspect item shape from
|
|
640
|
+
section 5.
|
|
641
|
+
|
|
642
|
+
## 8. Transition
|
|
643
|
+
|
|
644
|
+
### Request
|
|
645
|
+
|
|
646
|
+
Transition accepts exactly:
|
|
647
|
+
|
|
648
|
+
~~~json
|
|
649
|
+
{
|
|
650
|
+
"id": "wb_...",
|
|
651
|
+
"expected_revision": "sha256:<64 lowercase hexadecimal characters>",
|
|
652
|
+
"to_status": "backlog",
|
|
653
|
+
"date": "2030-01-11",
|
|
654
|
+
"decision": {
|
|
655
|
+
"summary": "Accept the fictional item.",
|
|
656
|
+
"rationale": "The fictional scope is ready."
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
~~~
|
|
660
|
+
|
|
661
|
+
| Member | Required | Rules |
|
|
662
|
+
|---|---:|---|
|
|
663
|
+
| id | Yes | Canonical existing item ID. |
|
|
664
|
+
| expected_revision | Yes | Exact lowercase SHA-256 token returned by inspect. |
|
|
665
|
+
| to_status | Yes | Target in the allowed edge table. |
|
|
666
|
+
| date | Yes | ISO calendar date not earlier than existing created or updated. |
|
|
667
|
+
| decision | Conditional | Required exactly when the edge appends evidence. |
|
|
668
|
+
| decision.summary | Conditional | Required non-empty string. |
|
|
669
|
+
| decision.rationale | Conditional | Required non-empty string. |
|
|
670
|
+
|
|
671
|
+
The request cannot supply action, decision date, rollup, body, frontmatter
|
|
672
|
+
patches, or terminal dates. Wowbagger derives them. A decision is rejected for
|
|
673
|
+
an edge that does not append one.
|
|
674
|
+
|
|
675
|
+
### Allowed edges
|
|
676
|
+
|
|
677
|
+
| Kind | From | To | Generated evidence |
|
|
678
|
+
|---|---|---|---|
|
|
679
|
+
| task or epic | triage | backlog | append accept decision |
|
|
680
|
+
| task or epic | triage | killed | set killed; append kill decision |
|
|
681
|
+
| task | backlog | in-progress | none |
|
|
682
|
+
| task | backlog | archived | set archived; append archive decision |
|
|
683
|
+
| task | backlog | killed | set killed; append kill decision |
|
|
684
|
+
| task | in-progress | backlog | none |
|
|
685
|
+
| task | in-progress | done | set completed; append complete decision |
|
|
686
|
+
| task | in-progress | killed | set killed; append kill decision |
|
|
687
|
+
| epic | backlog | done | set completed; append complete decision with generated rollup |
|
|
688
|
+
| epic | backlog | archived | set archived; append archive decision |
|
|
689
|
+
| epic | backlog | killed | set killed; append kill decision |
|
|
690
|
+
| task or epic | archived | backlog | clear archived; append restore decision |
|
|
691
|
+
|
|
692
|
+
Every transition sets updated to request.date. request.date must be greater than
|
|
693
|
+
or equal to both the existing created and existing updated dates. It cannot
|
|
694
|
+
move updated backwards.
|
|
695
|
+
|
|
696
|
+
A non-terminal target clears every terminal date. A terminal target sets only
|
|
697
|
+
its matching date. Existing decisions are retained and the required decision is
|
|
698
|
+
appended. Epic complete rollup is generated from every direct child, each of
|
|
699
|
+
which must already be done or killed, ordered by immutable ID.
|
|
700
|
+
|
|
701
|
+
No other edge is supported. Epics never enter in-progress; done and killed
|
|
702
|
+
items do not reopen; and transition cannot edit identity, title, relations,
|
|
703
|
+
parent, snooze, body, or extension fields. A schema version 2 done transition
|
|
704
|
+
therefore retains the target's depends_on and related lists unchanged.
|
|
705
|
+
|
|
706
|
+
### Lossless preservation
|
|
707
|
+
|
|
708
|
+
Transition preserves body bytes exactly. It preserves every permitted extension
|
|
709
|
+
YAML node, including tags, aliases, mapping structure, scalar precision, and
|
|
710
|
+
extra provenance members, without coercing it through the normalized core JSON
|
|
711
|
+
view. The implementation may reserialize controlled core frontmatter, but must
|
|
712
|
+
retain extension semantics and must not drop extension nodes or comments
|
|
713
|
+
attached to them.
|
|
714
|
+
|
|
715
|
+
The successful response exposes the new complete source_base64 and revision, so
|
|
716
|
+
the caller can verify the exact rewritten bytes.
|
|
717
|
+
|
|
718
|
+
### Revision and candidate validation
|
|
719
|
+
|
|
720
|
+
After the stable lock-set re-read in section 6, a target hash mismatch returns
|
|
721
|
+
revision-conflict, exit 4, and unchanged:
|
|
722
|
+
|
|
723
|
+
~~~json
|
|
724
|
+
{
|
|
725
|
+
"id": "wb_...",
|
|
726
|
+
"expected_revision": "sha256:<request digest>",
|
|
727
|
+
"actual_revision": "sha256:<locked current digest>"
|
|
728
|
+
}
|
|
729
|
+
~~~
|
|
730
|
+
|
|
731
|
+
For a matching revision, the backend builds the one-item proposed ledger and
|
|
732
|
+
validates it completely before publication.
|
|
733
|
+
|
|
734
|
+
Every item whose depends_on contains the target is considered when the target
|
|
735
|
+
would become done, killed, or archived, regardless of the referring item's own
|
|
736
|
+
status. Every direct child is considered for an epic transition. The backend
|
|
737
|
+
does not limit relation checks to ready or non-terminal referring items. For a
|
|
738
|
+
schema version 2 done transition, incoming depends_on references become
|
|
739
|
+
satisfied and remain in place; they do not require another item mutation and
|
|
740
|
+
do not create a multi-item blocker.
|
|
741
|
+
|
|
742
|
+
### Deterministic precondition issues
|
|
743
|
+
|
|
744
|
+
A valid request that cannot produce a valid one-item successor returns
|
|
745
|
+
transition-precondition-failed, exit 2, and unchanged when no multi-item blocker
|
|
746
|
+
exists. details are:
|
|
747
|
+
|
|
748
|
+
~~~json
|
|
749
|
+
{
|
|
750
|
+
"id": "wb_...",
|
|
751
|
+
"issues": [
|
|
752
|
+
{
|
|
753
|
+
"code": "date-before-updated",
|
|
754
|
+
"field": "date",
|
|
755
|
+
"message": "Transition date must not be earlier than the current updated date.",
|
|
756
|
+
"related_ids": []
|
|
757
|
+
}
|
|
758
|
+
]
|
|
759
|
+
}
|
|
760
|
+
~~~
|
|
761
|
+
|
|
762
|
+
Issue codes are date-before-created, date-before-updated, invalid-edge,
|
|
763
|
+
live-dependencies, or nonterminal-children. related_ids are unique immutable IDs
|
|
764
|
+
sorted ascending. Issues sort by code, then field, then their related ID
|
|
765
|
+
sequence. Date checks are all reported: a date earlier than both created and
|
|
766
|
+
updated produces both date issues.
|
|
767
|
+
|
|
768
|
+
For a schema version 1 done transition, any non-empty depends_on list produces
|
|
769
|
+
live-dependencies and related_ids contains the complete list. For a schema
|
|
770
|
+
version 2 done transition, only targets whose status is not done produce
|
|
771
|
+
live-dependencies, and related_ids contains only those unsatisfied prerequisite
|
|
772
|
+
IDs. A successful schema version 2 done transition retains every satisfied ID
|
|
773
|
+
in depends_on and does not append it to related.
|
|
774
|
+
|
|
775
|
+
### Deterministic multi-item refusal
|
|
776
|
+
|
|
777
|
+
If any other item would need mutation, transition returns
|
|
778
|
+
atomic-scope-required, exit 5, and unchanged. It aggregates every blocker:
|
|
779
|
+
|
|
780
|
+
~~~json
|
|
781
|
+
{
|
|
782
|
+
"id": "wb_...",
|
|
783
|
+
"blockers": [
|
|
784
|
+
{
|
|
785
|
+
"code": "child-disposition",
|
|
786
|
+
"item_id": "wb_...",
|
|
787
|
+
"field": "parent"
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
"code": "dependent-disposition",
|
|
791
|
+
"item_id": "wb_...",
|
|
792
|
+
"field": "depends_on"
|
|
793
|
+
}
|
|
794
|
+
],
|
|
795
|
+
"precondition_issues": []
|
|
796
|
+
}
|
|
797
|
+
~~~
|
|
798
|
+
|
|
799
|
+
Blocker codes are:
|
|
800
|
+
|
|
801
|
+
| Code | Condition |
|
|
802
|
+
|---|---|
|
|
803
|
+
| dependent-cleanup | Schema version 1 only: a done target is still present in another item's depends_on, regardless of that item's status. Schema version 2 keeps this reference as satisfied prerequisite history and does not produce this blocker. |
|
|
804
|
+
| dependent-disposition | A killed or archived target is still present in another item's depends_on, regardless of that item's status. |
|
|
805
|
+
| child-disposition | A killed or archived epic has a direct triage, backlog, or in-progress child. |
|
|
806
|
+
|
|
807
|
+
Blockers are unique by code, item_id, and field, then sorted by code, item_id,
|
|
808
|
+
and field using ascending code-point order. An item that is both a child and a
|
|
809
|
+
dependent contributes both blockers. precondition_issues uses the schema above
|
|
810
|
+
and is included even when nonempty, so combined conditions are never hidden by
|
|
811
|
+
an ambiguous precedence rule.
|
|
812
|
+
|
|
813
|
+
If blockers is nonempty, atomic-scope-required is returned after collecting
|
|
814
|
+
both all blockers and all precondition issues. If blockers is empty but
|
|
815
|
+
precondition issues is nonempty, transition-precondition-failed is returned.
|
|
816
|
+
Only an empty set of both proceeds to the candidate validator.
|
|
817
|
+
|
|
818
|
+
### Candidate-validation refusal and precedence
|
|
819
|
+
|
|
820
|
+
The candidate complete-ledger validator is the final authority. When it rejects
|
|
821
|
+
a proposed single-item create or transition for a reason not already
|
|
822
|
+
represented by a more specific collision, multi-item blocker, or transition
|
|
823
|
+
precondition, the command returns candidate-invalid, exit 2, and unchanged:
|
|
824
|
+
|
|
825
|
+
~~~json
|
|
826
|
+
{
|
|
827
|
+
"id": "wb_...",
|
|
828
|
+
"validation_errors": [
|
|
829
|
+
{
|
|
830
|
+
"path": "ledger/wb_....md",
|
|
831
|
+
"field": "parent",
|
|
832
|
+
"code": "nonterminal-child-of-terminal-epic",
|
|
833
|
+
"message": "Triage child wb_... cannot remain under archived epic wb_...; terminalize or reparent the child before the epic transition."
|
|
834
|
+
}
|
|
835
|
+
]
|
|
836
|
+
}
|
|
837
|
+
~~~
|
|
838
|
+
|
|
839
|
+
validation_errors is exactly the deterministic SPEC.md validator sequence for
|
|
840
|
+
the complete proposed ledger, sorted by path, field, code, and message under
|
|
841
|
+
the existing validator rules. The response message is exactly "The proposed
|
|
842
|
+
item would make the ledger invalid."
|
|
843
|
+
|
|
844
|
+
For transition, refusal precedence after locked revalidation is: revision and
|
|
845
|
+
lock conflicts; aggregate all multi-item blockers and ordinary precondition
|
|
846
|
+
issues; atomic-scope-required when blockers exist; otherwise
|
|
847
|
+
transition-precondition-failed when ordinary issues exist; otherwise
|
|
848
|
+
candidate-invalid when the candidate validator reports errors. For create,
|
|
849
|
+
id-collision precedes path-collision as specified in section 7, and both
|
|
850
|
+
precede candidate-invalid. No validator issue already represented by the
|
|
851
|
+
selected more-specific response is duplicated in a second envelope. A proposed
|
|
852
|
+
ledger that remains invalid for any reason is never published.
|
|
853
|
+
|
|
854
|
+
Transition publication uses a fully written and synced same-directory
|
|
855
|
+
temporary file followed by the platform's existing-file atomic replacement
|
|
856
|
+
primitive. It then re-reads exact final bytes. This remains a local filesystem
|
|
857
|
+
operation without universal crash durability or hostile-writer protection.
|
|
858
|
+
|
|
859
|
+
## 9. Patch
|
|
860
|
+
|
|
861
|
+
Patch changes the caller-supplied fields common to schema versions 1 and 2 of
|
|
862
|
+
one existing item — nothing else. It runs under the same per-ID lock, locked re-read,
|
|
863
|
+
exact-byte revision compare-and-swap, candidate complete-ledger validation,
|
|
864
|
+
and atomic same-path publication protocol as transition (section 6), and it
|
|
865
|
+
shares transition's envelopes, exits, and recovery rules except where this
|
|
866
|
+
section says otherwise.
|
|
867
|
+
|
|
868
|
+
### Request
|
|
869
|
+
|
|
870
|
+
Patch accepts exactly:
|
|
871
|
+
|
|
872
|
+
~~~json
|
|
873
|
+
{
|
|
874
|
+
"id": "wb_...",
|
|
875
|
+
"expected_revision": "sha256:...",
|
|
876
|
+
"date": "2030-01-11",
|
|
877
|
+
"set": { "priority": 3, "number": null }
|
|
878
|
+
}
|
|
879
|
+
~~~
|
|
880
|
+
|
|
881
|
+
| Member | Required | Rules |
|
|
882
|
+
|---|---:|---|
|
|
883
|
+
| id | Yes | Canonical existing item ID. |
|
|
884
|
+
| expected_revision | Yes | Exact lowercase SHA-256 token returned by inspect. |
|
|
885
|
+
| date | Yes | ISO calendar date not earlier than existing created or updated. |
|
|
886
|
+
| set | Yes | Mapping naming at least one patchable field. |
|
|
887
|
+
|
|
888
|
+
The patchable field set is exactly `number` and `priority`. A set member
|
|
889
|
+
outside it is an invalid-request issue at its /set pointer — the boundary is
|
|
890
|
+
stated here, not discovered from the implementation. An integer value sets
|
|
891
|
+
the field: number must be a positive integer, priority a non-negative
|
|
892
|
+
integer. null removes the field.
|
|
893
|
+
|
|
894
|
+
Patch sets updated to request.date. A date earlier than the existing created
|
|
895
|
+
or updated date returns patch-precondition-failed, exit 2, and unchanged,
|
|
896
|
+
with date-before-created and date-before-updated issue codes matching
|
|
897
|
+
transition's.
|
|
898
|
+
|
|
899
|
+
Patch appends no decision: the ledger's Git history is the audit trail for a
|
|
900
|
+
consumer-field change. Identity, lifecycle, title, relations, provenance,
|
|
901
|
+
snooze, decisions, body, and extension members cannot change through patch.
|
|
902
|
+
|
|
903
|
+
Patch never mutates another item. A candidate ledger that flags any other
|
|
904
|
+
item — for example a duplicate-number collision with an existing handle —
|
|
905
|
+
returns candidate-invalid, exit 2, and unchanged, and both items keep their
|
|
906
|
+
bytes.
|
|
907
|
+
|
|
908
|
+
### Serialization
|
|
909
|
+
|
|
910
|
+
An updated field is rewritten in place. A newly added number serializes
|
|
911
|
+
directly after id; a newly added priority directly after kind. Body bytes and
|
|
912
|
+
extension nodes are preserved exactly as in transition.
|
|
913
|
+
|
|
914
|
+
### Adapter advertisement
|
|
915
|
+
|
|
916
|
+
The version 1 capabilities envelope and the version 1 adapter core probe do
|
|
917
|
+
not advertise patch. Their operation and command lists are pinned by the
|
|
918
|
+
version 1 adapter contract, so widening them is an adapter contract version
|
|
919
|
+
change. Version 2 makes that change: its capability envelope includes the exact
|
|
920
|
+
`operations.patch` member from section 4 and adapter contract version 2 inserts
|
|
921
|
+
`patch` between `inspect` and `ready` in the fixed core-command order. The
|
|
922
|
+
fail-closed direction is preserved: a version 1 consumer never sees the wider
|
|
923
|
+
surface, and automation that plans from version 1 capabilities does not use
|
|
924
|
+
patch.
|
|
925
|
+
|
|
926
|
+
## 10. Errors, artifacts, and recovery
|
|
927
|
+
|
|
928
|
+
### Stable error details
|
|
929
|
+
|
|
930
|
+
| Code | Required details |
|
|
931
|
+
|---|---|
|
|
932
|
+
| invalid-request | issues |
|
|
933
|
+
| item-not-found | id |
|
|
934
|
+
| ledger-invalid | validation_errors |
|
|
935
|
+
| transition-precondition-failed | id, issues |
|
|
936
|
+
| patch-precondition-failed | id, issues |
|
|
937
|
+
| candidate-invalid | id, validation_errors |
|
|
938
|
+
| revision-conflict | id, expected_revision, actual_revision |
|
|
939
|
+
| lock-held | id, lock_path, owner, owner_diagnostic |
|
|
940
|
+
| id-collision | id, path, actual_revision |
|
|
941
|
+
| path-collision | id, path, occupant_kind; occupying_id iff occupant_kind is item |
|
|
942
|
+
| atomic-scope-required | id, blockers, precondition_issues |
|
|
943
|
+
| capability-unavailable | capability, reason, recovery_artifacts, recovery_artifacts_truncated |
|
|
944
|
+
| operation-failed | id, operation, reason, recovery_artifacts, recovery_artifacts_truncated |
|
|
945
|
+
| post-commit-recovery-required | id, revision, recovery_artifacts, recovery_artifacts_truncated |
|
|
946
|
+
| write-outcome-unknown | id, recovery_artifacts, recovery_artifacts_truncated |
|
|
947
|
+
|
|
948
|
+
ledger-invalid and candidate-invalid validation_errors are exactly the
|
|
949
|
+
deterministic SPEC.md error sequence for the current or proposed ledger,
|
|
950
|
+
respectively. Error messages are stable human summaries; automation branches
|
|
951
|
+
on code, mutation state, and documented details.
|
|
952
|
+
|
|
953
|
+
### Recovery artifact shape
|
|
954
|
+
|
|
955
|
+
recovery_artifacts is an array of at most 16 objects:
|
|
956
|
+
|
|
957
|
+
~~~json
|
|
958
|
+
{
|
|
959
|
+
"path": ".wowbagger-tmp-wb_...",
|
|
960
|
+
"kind": "temporary-file",
|
|
961
|
+
"sha256": "sha256:<lowercase hex digest>",
|
|
962
|
+
"size_bytes": 321
|
|
963
|
+
}
|
|
964
|
+
~~~
|
|
965
|
+
|
|
966
|
+
path is ledger-relative, uses forward slashes, and is at most 1024 Unicode
|
|
967
|
+
scalar values. kind is temporary-file, lock-file, or final-item. sha256 and
|
|
968
|
+
size_bytes are null when the artifact cannot be safely read; otherwise the
|
|
969
|
+
digest covers its exact bytes and size_bytes is a nonnegative integer.
|
|
970
|
+
|
|
971
|
+
Artifacts are unique by path and sorted by path, then kind. If more than 16 are
|
|
972
|
+
observed, the first 16 are returned and recovery_artifacts_truncated is true.
|
|
973
|
+
Otherwise it is false. No artifact content is returned.
|
|
974
|
+
|
|
975
|
+
### Deterministic operation failures and state mapping
|
|
976
|
+
|
|
977
|
+
operation-failed is a mutation-only error. operation is exactly one of:
|
|
978
|
+
|
|
979
|
+
- lock-closure;
|
|
980
|
+
- prepare-temporary;
|
|
981
|
+
- sync-temporary;
|
|
982
|
+
- publish;
|
|
983
|
+
- verify-publication; or
|
|
984
|
+
- cleanup.
|
|
985
|
+
|
|
986
|
+
reason is exactly retry-limit-exhausted, io-error, or verification-failed.
|
|
987
|
+
retry-limit-exhausted is used only with lock-closure. verification-failed is
|
|
988
|
+
used only when verification proves the expected publication is absent for
|
|
989
|
+
create or proves the original bytes remain for transition or patch. All other
|
|
990
|
+
handled filesystem failures use io-error. Platform exception text, errno names,
|
|
991
|
+
numeric OS error codes, and absolute paths are not members of the normative
|
|
992
|
+
JSON envelope and cannot alter operation or reason.
|
|
993
|
+
|
|
994
|
+
Before a publication attempt, lock-closure, prepare-temporary, sync-temporary,
|
|
995
|
+
or cleanup failure returns operation-failed with state unchanged. After a
|
|
996
|
+
publication attempt, the backend must inspect the final path before choosing a
|
|
997
|
+
response:
|
|
998
|
+
|
|
999
|
+
- exact expected final bytes produce state committed; a remaining verify or
|
|
1000
|
+
cleanup problem is post-commit-recovery-required;
|
|
1001
|
+
- proven absence for create or exact original bytes for transition or patch
|
|
1002
|
+
produce operation-failed with state unchanged, operation publish or
|
|
1003
|
+
verify-publication as applicable, and reason io-error or
|
|
1004
|
+
verification-failed as applicable; and
|
|
1005
|
+
- unreadable, different, or otherwise indeterminate final bytes produce
|
|
1006
|
+
write-outcome-unknown with state unknown.
|
|
1007
|
+
|
|
1008
|
+
operation-failed always contains the canonical target id and the bounded
|
|
1009
|
+
recovery artifact fields. Its message is exactly "The mutation operation
|
|
1010
|
+
failed before a commit was established."
|
|
1011
|
+
|
|
1012
|
+
A normal handled unchanged failure removes its own temporary files and locks.
|
|
1013
|
+
A cleanup failure reports the remaining bounded artifacts. Locks are never
|
|
1014
|
+
auto-broken by age. Clients must inspect after committed-recovery or unknown
|
|
1015
|
+
outcomes and must not retry blindly.
|
|
1016
|
+
|
|
1017
|
+
## 11. Normative design vectors
|
|
1018
|
+
|
|
1019
|
+
The synthetic vectors under
|
|
1020
|
+
[spec/fixtures/mutations](../spec/fixtures/mutations/README.md) are the
|
|
1021
|
+
executable compatibility target for this contract. Each case has a manifest with
|
|
1022
|
+
arguments, input transport, expected exit, exact stdout/stderr expectations,
|
|
1023
|
+
and before/after file digests.
|
|
1024
|
+
|
|
1025
|
+
The matrix covers capability honesty; lossless inspect and failure envelopes;
|
|
1026
|
+
caller-known create identity, file/stdin equivalence, strict input, ID and path
|
|
1027
|
+
collision, candidate validation, body boundaries, publication limitation and
|
|
1028
|
+
recovery outcomes; transition revision, locking, date monotonicity, lifecycle
|
|
1029
|
+
edges, the schema version 1 dependent-cleanup blocker and the other two
|
|
1030
|
+
multi-item reasons, terminal referrers, combined blockers,
|
|
1031
|
+
candidate validation, deterministic operation failures, and
|
|
1032
|
+
unchanged/committed/unknown states; and patch field boundaries, CAS,
|
|
1033
|
+
serialization, and preconditions.
|
|
1034
|
+
|
|
1035
|
+
The runtime executes every vector as a black-box CLI test, including exact
|
|
1036
|
+
response bytes and the complete before/after ledger snapshot.
|