wowbagger 0.1.0-alpha.5 → 0.1.0-alpha.7

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 CHANGED
@@ -5,8 +5,515 @@ A change to what a command accepts, refuses, emits, or writes is a behaviour
5
5
  change even when the commit that carried it was labelled refactor, docs, or
6
6
  consolidation. The first tagged release inherits this file.
7
7
 
8
+ ## Unreleased
9
+
10
+ ## 0.1.0-alpha.7 - 2026-08-18
11
+
12
+ ### Changed
13
+
14
+ - **The item source is bounded at every candidate door, and the core contract
15
+ moves to 4.** The published version 3 core accepted a 50-MiB `create` with
16
+ exit `0` and state `committed` in 0.70 s: `create`, `transition`, and `patch`
17
+ had no bound
18
+ anywhere, and `publish-claimed` alone bounded candidates but reported an
19
+ oversized, perfectly canonical candidate as `The candidate source is not
20
+ canonical base64.` One shared `MAX_ITEM_SOURCE_BYTES` of 8,388,608 now bounds
21
+ the complete serialized successor at all four doors, and every one of them
22
+ answers the same named refusal: `item-source-too-large`, exit 2, state
23
+ `unchanged`, details exactly `{id, size_bytes, limit_bytes}` in the core
24
+ domain and `{item_id, size_bytes, limit_bytes}` in the ledger-publication
25
+ domain. The measurement is serialized UTF-8 bytes, so frontmatter, decisions,
26
+ extensions, and body all draw on the same budget; `transition` is bounded
27
+ because its decision block can push a legal stored item past it. Core
28
+ capabilities advertises the value at `result.limits.max_item_source_bytes`.
29
+ This narrows accepted input against a published version, so the core contract
30
+ moves to 4 and version 3 consumers fail closed at negotiation. A ledger
31
+ committed before the bound does not brick: an oversized item still validates,
32
+ still inspects, and a patch that shrinks it under the bound is accepted.
33
+
34
+ - **The work-claim API moves to 2.** The oversized-candidate response replaces
35
+ the error `publish-claimed` version 1 pinned for that input, so
36
+ `result.operations.work_claim.api_version` is now `2` and version 1 consumers
37
+ fail closed. Malformed base64 keeps its version 1 `invalid-request`: without
38
+ canonical base64 there is no item source to measure. The base64-character
39
+ precheck is gone — the 11,534,336-byte serialized-request bound already caps
40
+ what a candidate can decode to, and the precheck was the remaining path that
41
+ answered a genuine size refusal with a false base64 message. That transport
42
+ bound is unchanged and still measures a different object.
43
+ - **A claimed publication no longer takes one cooperative lock per ledger
44
+ item.** `publish-claimed` computed its lock closure from every item in the
45
+ loaded ledger, so publishing one item on a 1,500-item ledger created, wrote,
46
+ fsynced, and unlinked 1,503 lock files. It runs from journal replay through
47
+ its terminal record inside the namespace write lock, and every other
48
+ cooperative writer of a provisioned ledger enters that same lock before it
49
+ writes, so the per-item closure excluded nobody the namespace lock did not
50
+ already exclude. Publication now takes no per-item locks. Newly instrumented
51
+ phase counters measured the cost this removes: on 1,500 items the lock phase
52
+ was 11.2 s of the 12.6 s the publication took, against 0.2 s for reading Git
53
+ HEAD. Everything else is unchanged and proved byte-for-byte identical against
54
+ the previous implementation across all six publication outcome classes —
55
+ success, fence refusal, revision conflict, validation refusal, idempotent
56
+ replay, and indeterminate publication — in envelope, claim journal, and item
57
+ bytes. Every cooperative writer of one ledger must be upgraded together: a
58
+ writer that honors only per-ID locks can race one that honors only the
59
+ namespace lock.
60
+ - **The README installs with `@next` and says why.** The registry mandates a
61
+ `latest` dist-tag, so `latest` mirrors `next`; `@next` stays the documented
62
+ spelling and the explicit prerelease consent.
63
+ - **Cuts happen on the release branch, not in a session worktree.** Merge
64
+ session work first, then cut; the cut command refuses to run anywhere but the
65
+ branch tip. The previous two-phase topology is why the last two release tags
66
+ name merge commits rather than their cut commits.
67
+ `docs/adapter-release-path.md` records the ritual.
68
+
69
+ ### Fixed
70
+
71
+ - **A live publication is no longer reported as a broken lock.** Publication
72
+ lock files recorded `"operation": "publish-claimed"`, but the lock reader
73
+ accepts only `create`, `transition`, and `patch`, so a concurrent writer that
74
+ hit a live publication lock classified it `invalid-shape` — a diagnostic that
75
+ says the lock is corrupt. Publication writes no lock files at all now, so
76
+ there is nothing to misclassify.
77
+
78
+ - **A committed `patch` is forwarded instead of reported as an unknown
79
+ outcome.** The shipped adapter engine still named the pre-widening patchable
80
+ pair `number` and `priority`, so every patch a consumer actually sends — a
81
+ body rewrite, a title correction, a relation-list replacement, a declared
82
+ extension member — read as a non-canonical request, failed result
83
+ correlation, and came back `mutation-outcome-unknown` with recovery guidance
84
+ about a write that had provably committed. `number` is the immutable item
85
+ identity and was never patchable at all. The patchable field set is now what
86
+ mutation contract section 9 names — `title`, `priority`, `depends_on`,
87
+ `related`, `body`, `body_append`, and `extensions` — with the two body write
88
+ modes mutually exclusive and the extensions container judged only on its own
89
+ shape, and correlation follows each member to the surface it is observable
90
+ on. The independent reference model already had all of this, so the drift was
91
+ one-sided and no differential test could see it; the new end-to-end
92
+ core-outcome vectors found it on their first run.
93
+
94
+ - **`publish-claimed` now reconciles the journal unconditionally, like every
95
+ other mutating command.** The work-claim contract has always said an
96
+ uncommitted prior mutation refuses the next `create`, `transition`, `patch`,
97
+ **or `publish-claimed`**. The code only reconciled when it happened to
98
+ observe an unresolved `publish-intent`, and an uncommitted legacy mutation
99
+ leaves none behind. A fixture pinned the gap: with a legacy create and
100
+ transition sitting uncommitted, `claim-verify` returned exit 6 and a legacy
101
+ `create` refused with `publication-reconciliation-required`, while
102
+ `publish-claimed` on a claimed item published straight over the unreconciled
103
+ ledger. It now reconciles before the fence decision on every publication and
104
+ refuses with exit 6 `claim-store-unavailable`,
105
+ `details.reason: "publication-reconciliation-required"`, and
106
+ `details.findings` — the same envelope the legacy fence emits, so one
107
+ `claim-verify` clears every blocked path. **A publication behind an
108
+ unresolvable prior intent now returns that refusal instead of exit 6
109
+ `publication-outcome-unknown`.** The old code named the refused publication's
110
+ own outcome uncertain when it had not run at all; `state: "unchanged"` is the
111
+ honest answer, and the blocking finding still travels in `details.findings`.
112
+ The cost is honest about which read is new. Reconciliation adds no
113
+ complete-ledger read: it produces the snapshot the candidate validation and
114
+ the mutation engine's pre-lock read already share, so a claimed publication
115
+ still reads the working-tree ledger exactly twice. It does add the Git `HEAD`
116
+ read — `rev-parse`, one `ls-tree`, and a batched `cat-file` over every item
117
+ blob at `HEAD` — to every publication that previously had no unresolved
118
+ intent. That is the same read `create`, `transition`, `patch`, and every
119
+ claim lifecycle command already perform, so `publish-claimed` now pays the
120
+ toll its peers pay rather than a new one. Each publication persists one clock
121
+ floor, as it did before; a publication behind a pending intent, which used to
122
+ persist two, now persists one as well.
123
+
124
+ - **A shipped adapter no longer advertises trusted approval it cannot
125
+ exercise.** The three shipped entrypoints declared
126
+ `trusted_approval: {"supported": true, "sources": ["consumer"]}` while
127
+ `runAdapterEntrypoint` passed no approval, clock, nonce store, or core
128
+ executable identity to the invoke engine, so `create`, `transition`, and
129
+ `patch` through every shipped adapter refused `consumer-approval-required`
130
+ and could not succeed on any input. The declaration now reflects the runtime
131
+ of the invocation, the way `optional_features.claims` already reflects the
132
+ core probe: a bare entrypoint run declares no trusted approval and refuses a
133
+ mutation `capability-unavailable` with `missing: ["trusted-approval"]`, the
134
+ refusal section 5.1 already required for an absent declaration, and a host
135
+ that wires an approval source declares it truthfully. The two refusals stay
136
+ distinguishable, because they are different facts — no approval source at all
137
+ versus a source that produced no approval for this invocation — and conformance
138
+ case `07-mutation-approval` now pins both against the runtimes that produce
139
+ them, adding one assertion to the conformance suite.
140
+ - **A committed `create` is forwarded instead of reported as an unknown
141
+ outcome.** Both adapter engines required `schema_version: 1` in a create
142
+ result and re-serialized the expected candidate with the schema 1 default, so
143
+ every create against a real ledger — an empty ledger is schema 2 — failed the
144
+ result correlation, was judged an invalid core envelope, and came back as
145
+ `mutation-outcome-unknown` with recovery guidance about a write that had
146
+ provably committed. The ledger's schema version and the number the core
147
+ assigns under its own lock are the only two members of the answer a caller
148
+ could not have known; both engines now read those from the result and
149
+ re-derive the whole candidate from the request bytes, leaving every other
150
+ member pinned by an exact byte comparison against the source the core
151
+ returned. The defect was unreachable while every shipped mutation refused
152
+ before launch, and surfaced the moment a host runtime carried a real approval
153
+ through to the core.
154
+ - Adapter contract section 5's core-request table and section 5.1's approval
155
+ rule named only `create` and `transition`. Both have accepted `patch` since
156
+ adapter contract version 2; the prose now says so.
157
+
158
+ ### Added
159
+
160
+ - **The conformance suite now measures real core outcomes end to end.** A new
161
+ equivalence case, `16-core-outcome-e2e`, carries nine hand-authored scenarios
162
+ that each run the direct real core in one isolated temporary workspace and,
163
+ separately, spawn the real shipped entrypoint over the bootstrap wire against
164
+ the real core in a second workspace materialized from the same before state:
165
+ `inspect` item-not-found, a committed `create`, a committed `transition`, a
166
+ committed `patch` by body replacement and by declared extension member, the
167
+ six-member date refusal, and all three `ledger-mutation` claim-fence refusal
168
+ classes. Success vectors match the exact core exit, stdout bytes, decoded
169
+ adapter streams with their digests and lengths, and the exact ledger
170
+ post-state; refusal vectors match the exact nonzero exit and unchanged ledger
171
+ bytes, and the fence refusals arrive as `ok: true` adapter transport results
172
+ rather than adapter errors. Before this case, no conformance assertion and no
173
+ bootstrap-wire test carried a mutation through a spawned entrypoint into a
174
+ launched core, which is how two real adapter defects shipped. The suite is
175
+ 210 assertions across 16 cases.
176
+
177
+ Determinism comes from fixed inputs and never from normalizing output: a
178
+ caller-supplied ULID, seeded revisions on isolated temporary ledgers, literal
179
+ dates, and — for the two real claim-fence read-backs — a fixed namespace, a
180
+ hand-authored claim journal, and a seeded future clock floor, so the emitted
181
+ `observed_at` is `max(physical_now, floor)` and therefore the floor. That
182
+ makes the refusal bytes fixed without mocking the core clock, and it expires:
183
+ both runners fail loudly and name `2031-01-15T12:01:00.000Z` once wall time
184
+ reaches it. Goldens are authored from the adapter contract, the work-claim
185
+ contract, and the normative `spec/fixtures/mutations/**` bytes; every byte
186
+ reused from those fixtures carries a `derived_from` pin, so drift on either
187
+ side stops the vector and asks for a reviewed golden change instead of
188
+ regenerating one. Only base64, SHA-256, and byte length are derived, and only
189
+ from an already hand-authored byte string.
190
+
191
+ The conformance host gains a granting approval mode, without which no
192
+ mutation can cross the spawned entrypoint at all. **Adapter contract section
193
+ 10 previously said no conformance fixture could manufacture authority; that
194
+ is no longer true, and the claim is withdrawn rather than quietly narrowed.**
195
+ What replaces it is narrower and checkable: the granting mode is reachable
196
+ only from a fixture's own runtime configuration, which no shipped adapter
197
+ package reads and no wire this contract defines carries; every granting
198
+ scenario runs against a throwaway temporary ledger; the approval is minted
199
+ from the binding the engine resolved and canonicalized by the independent
200
+ reference model. The evidence label is the production adapter engine under a
201
+ conformance host approval provider, not a live consumer approval mechanism.
202
+
203
+ - **A host process can wire consumer approval into a shipped adapter
204
+ entrypoint.** `runAdapterEntrypoint` now accepts an optional `hostRuntime`
205
+ carrying the approval source, the current time, the redeemed-nonce store, and
206
+ the core executable identity the host attests. It is a code-level parameter of
207
+ the embedding process and is deliberately absent from every wire: the
208
+ bootstrap request root schema is exact and has no approval member, so an
209
+ approval a model places on the request is an `invalid-invocation` that never
210
+ reaches the gate, exactly as adapter contract section 5.1 requires. The
211
+ approval may be a finished event or a resolver the adapter calls with the
212
+ exact binding it has just resolved — the argument vector, absolute workspace
213
+ paths, and instruction and handoff digests an approval covers do not exist
214
+ until the adapter has built them, so an interactive consumer prompt cannot
215
+ mint the approval any earlier. A resolver that fails produced no approval and
216
+ the mutation refuses; it never proceeds unapproved. The default is unchanged
217
+ and remains no approval. `test/adapter-host-approval-wire.test.js` carries the
218
+ first approved mutation in this repository to cross a spawned entrypoint into
219
+ a launched core and change a ledger, with its binding digest canonicalized by
220
+ the independent reference model rather than by the engine under test. No
221
+ version moved in any domain: `host.trusted_approval` has been optional since
222
+ version 1, the approval object and binding are untouched, and the mechanism is
223
+ invisible on every wire the contract defines. Adapter contract sections 3.2,
224
+ 3.3, 5.1, 10, and 12 state the seam, the honesty rule, the two-runtime
225
+ evidence, and the version argument.
226
+
227
+ - **The cut is one command, and version drift now fails the cut instead of
228
+ shipping.** `npm run release:cut -- <version> --date YYYY-MM-DD` runs on the
229
+ tip of the release branch, proves every version site is accounted for, plans
230
+ the new bytes in memory, runs the full release gate over them, and leaves one
231
+ `Cut <version>` commit and one annotated `v<version>` tag. It stops there:
232
+ push, `npm publish --tag next`, and the registry check stay separate named
233
+ steps, because no local command can undo any of them. Coverage is proved by
234
+ exact-set equality against a hand-maintained
235
+ `scripts/release-version-sites.json`, not by a global grep — the changelog and
236
+ the dated design records must keep naming old versions, so "grep finds
237
+ nothing" would be the wrong test. A release site added next month is
238
+ unmanifested and refuses the cut. `--dry-run` runs the same planner and the
239
+ same gate against a copy of HEAD and then proves the repository unchanged.
240
+ Reruns converge rather than repair: a cut tag at a clean HEAD reports
241
+ `already cut`, a complete cut commit without its tag resumes at tagging, and a
242
+ tag pointing elsewhere refuses.
243
+ - **The changelog can no longer lose its Unreleased section.** A cut opens a
244
+ fresh empty `## Unreleased` and files the released notes beneath it. The two
245
+ previous cuts renamed the heading instead, which left later changes landing
246
+ under an already published release.
247
+ - **The prerelease channel policy is stated and checkable.**
248
+ `npm run release:channels -- check|repair <version>` encodes it: `latest`
249
+ mirroring `next` at the published version and the first published alpha
250
+ deprecated. The first-choice policy — no `latest` at all, so a bare install
251
+ fails loudly — was refused by the registry itself: npm rejects deleting the
252
+ `latest` tag with E400 (verified live), so the current prerelease replaces
253
+ the dead first alpha as the forced default. `check` is read-only and is the
254
+ post-publish verification step; `repair` is idempotent and never unpublishes.
255
+
256
+ - **`--auto-commit` folds the commit-per-mutation ceremony into the mutation.**
257
+ The invariant is correct and the ceremony around it was the consumer's most
258
+ frequent daily cost: mutate, `git add`, `git commit`, `claim-verify`, repeat,
259
+ ten times for ten items. On a provisioned merge-coordinated ledger the new
260
+ opt-in bare flag on `create`, `transition`, `patch`, and `publish-claimed`
261
+ does that loop inside one invocation. It takes a per-working-tree mutex,
262
+ refuses any staged path anywhere and any dirty path under the ledger, checks
263
+ Git identity, runs an internal pre-mutation `claim-verify`, runs the mutation
264
+ unchanged, then commits **exactly** the changed item plus at most one
265
+ `.wowbagger/reconcile-<namespace>.md` under a fixed subject
266
+ (`wowbagger: transition item #7`; the canonical item ID for a schema-1 item
267
+ with no number). It verifies the resulting commit's parent, subject,
268
+ changed-path set, and every blob, then runs `claim-verify` again before it
269
+ answers. Success adds `git_commit`, `commit_paths`, and `claim_verified` to
270
+ `result`.
271
+
272
+ There is no configuration file setting, environment default, or repository
273
+ default, because a hidden default would make existing mutation automation
274
+ create Git commits unexpectedly. An invocation without the flag is
275
+ byte-identical to before, so the core contract stays 3 and the work-claim API
276
+ stays 1. The flag is direct-CLI only in this release; no adapter advertises or
277
+ constructs it.
278
+
279
+ What it will not do: commit anything on `state: "unchanged"` or
280
+ `state: "unknown"`, including the documented reconcile-log residue a refused
281
+ `publish-claimed` leaves behind; stage a path outside the ledger; broad-add,
282
+ amend, squash, reset, clean, stash, or unstage; pass `--no-verify` or disable
283
+ signing; fabricate an author; customize a commit message; or push, fetch,
284
+ pull, merge, or rebase. Hooks through `core.hooksPath`, `commit.gpgSign`, and
285
+ signing programs are honoured, and a hook that rewrites the subject or the
286
+ tree is reported rather than accepted.
287
+
288
+ - **An honest commit-failed contract, and one idempotent recovery verb.** A
289
+ post-publication Git failure that proves the commit is absent is exit 6
290
+ `git-commit-failed` with `state: "committed"` — the state still describes item
291
+ publication, not Git finalization — carrying the published revision, the exact
292
+ ledger-relative commit set with digests, `failure_stage`, `reason`, and a
293
+ bounded `recovery_token`. `create`, `transition`, and `patch` keep the core
294
+ domain; `publish-claimed` keeps `ledger-publication` and its top-level
295
+ `operation_id`. An **ambiguous** Git outcome is `git-commit-outcome-unknown`,
296
+ never `git-commit-failed`, and a commit that stands while reconciliation then
297
+ refuses is `post-commit-reconciliation-failed`. No failure envelope carries
298
+ hook output, signing output, absolute paths, or environment values.
299
+
300
+ New command: `wowbagger mutation-finalize --ledger <dir> --recovery-token
301
+ <token> --json`, answering in the work-claim domain because it changes Git
302
+ reconciliation state and no item byte. It re-derives every path from the
303
+ ledger and the provisioned namespace — the token is a witness, never authority
304
+ to select a path — re-checks the current bytes and the foreign-change rules,
305
+ creates the exact commit if it is absent, then runs `claim-verify`. When
306
+ `HEAD` already holds that exact commit it verifies and returns it without
307
+ creating a second one, so a lost response and a failed commit recover through
308
+ the same command, and repeating it is safe.
309
+
310
+ A failed attempt leaves its own commit set staged, because the design forbids
311
+ unstaging. Recovery tolerates exactly that residue and refuses anything else
312
+ staged; until it runs, the next `--auto-commit` invocation refuses on
313
+ `staged-paths-present`, which is the intended signal.
314
+
315
+ ## 0.1.0-alpha.6 - 2026-08-17
316
+
317
+ ### Added
318
+
319
+ - **`set.extensions` gives consumer-owned extension members a sanctioned patch
320
+ path.** Two field reports in two days: a consumer's own identifier field
321
+ rides a permitted extension member, and a wrong or missing one had no
322
+ ledger-side repair verb at all. On a provisioned ledger the hand-edit that
323
+ filled the gap is a stale write, so the protocol was forcing the edit it then
324
+ punished. `patch` now accepts an `extensions` container whose members name
325
+ extension members and whose values replace each one whole; `null` removes a
326
+ member. The fixed `set` allowlist is unchanged — `extensions` is one more
327
+ name on it, not an opening for arbitrary keys — so a top-level typo is still
328
+ an `unknown-member` refusal. Which members the container may name comes from
329
+ the committed `<ledger>/.wowbagger/extensions.json`, which declares a member
330
+ name and one value type each (`string`, `integer`, `boolean`, `string-list`).
331
+ **A ledger without that file has no patchable extension member at all**, and
332
+ the refusal names the missing declaration. Five new
333
+ `patch-precondition-failed` issue codes carry the refusals —
334
+ `extension-declaration-missing`, `extension-declaration-invalid`,
335
+ `extension-not-declared`, `extension-value-invalid`, `extension-anchored` —
336
+ in the existing four-member issue shape, with the member at fault named in
337
+ `field`. A member the item writes with a YAML anchor or alias is refused
338
+ rather than replaced, because replacing it would change every node bound to
339
+ the anchor; every member the request does not name keeps its exact
340
+ `extensionNodeIdentity` guarantee. The declaration authorizes a write and
341
+ never describes the ledger: `validate` does not read it, so an item whose
342
+ extension member disagrees with it stays valid and stays repairable. Nested
343
+ extension values still have no patch path and stay a reviewable hand-edit.
344
+ Core contract version stays 3 — the patch request schema widens and no
345
+ response envelope member is added, removed, or renamed — but version 3 is
346
+ published, so `contract_version` cannot answer whether a core carries this:
347
+ probe by sending an extension patch and reading the refusal, or pin the
348
+ distribution version. Documented in mutation contract section 9 and pinned by
349
+ `spec/fixtures/mutations/patch-extensions/`.
350
+
351
+ - **`claim-adopt` gives `unauthorized-revision` a non-destructive remedy.** A
352
+ consumer's staging checkout was blocked exit 6 on three items whose bodies
353
+ were hand-edited in a design session and merged. The refusal was correct, but
354
+ the only documented remedy — restore the authorized revision, then
355
+ `claim-verify` — discards reviewed, merged work. `claim-adopt` records that an
356
+ operator ruled the committed bytes legitimate and moves the coordinator's
357
+ authorized revision to them. It writes no item byte, so `updated` and the body
358
+ survive exactly. It is a standalone verb in the work-claim domain, a sibling
359
+ of `claim-verify`, and it is per item and per revision explicit: the request
360
+ names the item, the revision it believes is authorized, the revision being
361
+ adopted, and who is ruling. There is no adopt-all. It refuses
362
+ `adoption-witness-mismatch` on a stale witness (including a replay of a
363
+ successful adoption), `claim-held` while an unexpired claim holds the item,
364
+ `adoption-revision-uncommitted` unless the adopted revision is at Git `HEAD`
365
+ and in the caller's own working tree, and `adoption-ledger-invalid` when the
366
+ complete ledger would not validate. Success appends one `revision-adoption`
367
+ journal entry naming who, when, and both revisions, so the audit trail records
368
+ the ruling instead of losing it. Adoption is not a fence hole: the next
369
+ out-of-protocol edit is `unauthorized-revision` again, measured against the
370
+ adopted revision. Additive at contract version 1 — one new command, one new
371
+ journal entry type, three new error codes, no existing shape changed.
372
+ Documented in work-claim contract section 3.3 and pinned by
373
+ `spec/fixtures/work-claims/revision-adoption/`.
374
+
375
+ ### Changed
376
+
377
+ - **The report's epic-enablement factor now counts done or killed children
378
+ only.** It counted every child carrying a terminal date, which folded
379
+ archived and deferred children into the numerator: an epic with one done,
380
+ one archived, one deferred, and one backlog child reported enablement 0.75
381
+ while the mutation contract's terminal ratio for the same epic was 0.25. Two
382
+ numbers wore one name. A terminal date is not a terminal disposition —
383
+ archived restores and deferred undefers, both documented edges — so a parked
384
+ child is work postponed, not work retired, and counting it reported progress
385
+ that one transition takes back. The factor now reads the same done-or-killed
386
+ set as the contract and the epic complete rollup: one definition, three
387
+ surfaces. This is display-only and recomputed at render time; no ledger byte,
388
+ no wire shape, and no `ready` ordering changes. What does change is the
389
+ report: an epic with parked children reports a lower percentage, and its open
390
+ children rank lower on the epic-enablement step of `work next`.
391
+ - **Every `unauthorized-revision` remediation string now names both remedies.**
392
+ It was one sentence naming only the restore path, which reads as an
393
+ instruction to throw the edit away; the field report above did exactly that.
394
+ It is now two sentences, and each says what happens to the edit: `Restore the
395
+ authorized revision at <path>, then run claim-verify; that discards the edit.
396
+ Or adopt the committed revision of <path> with claim-adopt, then run
397
+ claim-verify; that keeps the edit.` The finding's `code`, `reason`,
398
+ `observed_surface`, `expected_path`, and revisions are unchanged; only the
399
+ human-readable `remediation` prose changed. `revision-regression` keeps its
400
+ restore-only string on purpose: it only fires while an active claim holds the
401
+ item, which is a state adoption refuses.
402
+ - **`patch` corrects an item title.** `set.title` takes a non-empty schema
403
+ string and replaces the title whole, under the same per-ID lock, exact-byte
404
+ compare-and-swap, candidate complete-ledger validation, and atomic
405
+ publication as every other patch; an item with an active claim is refused,
406
+ and `updated` moves to `request.date`. The scalar node is rewritten in place,
407
+ so the quoting style, the comments, the anchors, and every extension node
408
+ survive byte for byte. This closes a protocol contradiction reported twice
409
+ from the field: correcting a title used to require an out-of-protocol edit,
410
+ and on a provisioned ledger that edit is a stale write, so the next mutation
411
+ refused exit 6 `unauthorized-revision` and every later mutation stayed
412
+ blocked. `null` follows the frontmatter removal convention onto
413
+ `candidate-invalid`, because title is required; `""` is refused one step
414
+ earlier, at the request.
415
+ - **The mutation contract states the frontmatter ownership boundary.** Section
416
+ 9 gains a `Frontmatter ownership` table: one row per member, sorted into
417
+ core-owned (`schema_version`, `id`, `number`, `status`, `created`, `updated`,
418
+ the terminal dates, `decisions`), consumer-editable through `patch` (`title`,
419
+ `priority`, `depends_on`, `related`, `body`), and create-once (`kind`,
420
+ `provenance`, `parent`, `snoozed_until`). The boundary was previously
421
+ discoverable only by sending a patch and reading the refusal. A docs test
422
+ pins every row and the skill teaches the same three classes.
423
+
424
+ ### Decided
425
+
426
+ - **`kind` stays unpatchable, and the contract now says why.** A task-to-epic
427
+ flip changes which parent and children rules the item is validated under and
428
+ which lifecycle edges it may take. It needs its own verb with its own
429
+ preconditions, not a wider patch set.
430
+ - **Extension members stay out of `patch`, and the contract records the
431
+ reasons.** Two field reports asked for a sanctioned path for consumer-owned
432
+ identifier fields riding permitted extension members. The widening was
433
+ assessed against title's machinery and is not the same machinery: the
434
+ fail-closed `set` rule has no room for an arbitrary key, candidate validation
435
+ constrains no extension value, nested and anchored values do not survive a
436
+ whole-value replace the way a scalar does, and the oracle has no observable
437
+ surface to correlate an extension patch against. Section 9 names what a real
438
+ path would need — a `set.extensions` container, a declared per-ledger
439
+ extension schema, a stated rule for anchored and nested values, and an
440
+ oracle-visible surface — so the deferral is a design boundary rather than a
441
+ silence. Their status is stated in the ownership table either way.
442
+ - **`patch` gains `set.body_append`.** It takes a JSON string written after the
443
+ item's current body, under the same string rules `set.body` takes: the empty
444
+ string is valid, the bytes are the UTF-8 encoding of the string exactly, and
445
+ `null` is refused at `/set/body_append` because appending nothing is the empty
446
+ string. It is the same byte splice after the closing delimiter, so no
447
+ frontmatter byte moves, `updated` becomes request.date, and every existing
448
+ body byte survives — the request never names them. `body` and `body_append`
449
+ are mutually exclusive in one request: naming both is an `invalid-request`
450
+ issue at `/set`, exit 2, unchanged. This covers the annotation shape a mirror
451
+ consumer needs without making it carry a merge. The core contract stays
452
+ version 3 — it widens the patch request schema and moves no response envelope
453
+ member — but version 3 is already published without it, so a consumer
454
+ **cannot** probe for append support by reading `contract_version`. Send an
455
+ append and read the refusal instead: a core without it answers `unknown-member`
456
+ at `/set/body_append`, exit 2, unchanged.
457
+
458
+ ### Documentation
459
+
460
+ - **The allowed-edges table carries the defer and undefer edges.** `task` and
461
+ `epic` `backlog` to `deferred` and `deferred` to `backlog` have shipped in
462
+ `src/mutation.js` since deferral existed, both requiring a decision, and the
463
+ ownership table already documented `deferred` as a core-owned field that
464
+ `transition` writes on a defer. Section 8's edge table listed neither row, so
465
+ the one place a consumer looks up what it may drive under-reported the
466
+ lifecycle by two edges. Both rows are added with the evidence the code
467
+ generates — `set deferred; append defer decision` and `clear deferred; append
468
+ undefer decision` — and a docs guard pins the kind, the date, and the
469
+ decision on each. No emitted byte changes and the core contract stays version
470
+ 3: this documents shipped edges, it does not add them.
471
+
472
+ - **The epic derivation section cites one shared definition instead of a
473
+ divergence.** It recorded the report's epic-enablement factor as a different,
474
+ wider number than the terminal ratio. The report factor was narrowed to match
475
+ (see Changed above), so the paragraph is replaced: the contract, the epic
476
+ complete rollup, and the report all count done or killed direct children over
477
+ all direct children, and the section now says outright that a terminal date is
478
+ not the test. The docs guard is re-pointed at the new truth rather than
479
+ relaxed.
480
+
481
+ - **The contract states that `set.body` replaces and never merges.** A consumer
482
+ mirroring an external source regenerated an item body from its upstream card
483
+ and destroyed a ledger-only annotation; every check passed, because
484
+ `expected_revision` is a byte-level lost-update guard with no semantic safety.
485
+ Mutation contract section 9 and the skill's body bullet now say it plainly:
486
+ the replacement is total, and a mirroring consumer MUST read-modify-write from
487
+ the current item body and MUST never regenerate from the source alone. Docs
488
+ guards pin both sentences.
489
+
490
+ - **The contract documents the selector an `inspect` `item-not-found` refusal
491
+ echoes.** `inspect --number <n>` on a number no item carries emits
492
+ `details: {"number": <n>}`, and it has done so since `--number` shipped in
493
+ 0.1.0-alpha.5. Mutation contract section 5 claimed these details contain only
494
+ `id`, so the published prose and the published wire disagreed. Section 5 now
495
+ states the rule the runtime follows — the details carry exactly the selector
496
+ the request used, `id` for `--id` and `number` for `--number` — and
497
+ `spec/fixtures/mutations/inspect-number-not-found/` pins it. No emitted byte
498
+ changes and the core contract stays version 3: this documents a shipped
499
+ shape, it does not introduce one. The adapter surfaces still require id-only
500
+ details, deliberately: the adapter's `inspect` request accepts no `number`
501
+ member and always invokes `--id`, so it can never see the number variant.
502
+
8
503
  ## 0.1.0-alpha.5 - 2026-08-16
9
504
 
505
+ ### Breaking
506
+
507
+ - **`number` is no longer caller-settable on schema version 2 ledgers.**
508
+ `create` refuses a request supplying `item.number` and assigns the next
509
+ number itself (`max + 1` under the number-index lock); `patch` refuses
510
+ `set.number` because the number is the immutable item identity. A consumer
511
+ mirroring a legacy backlog cannot carry its legacy numbers into wowbagger
512
+ handles — keep legacy identifiers in a permitted extension member or in the
513
+ item body instead. (Shipped as part of the number-as-identity work; this
514
+ notice was added after the 0.1.0-alpha.5 tarball was cut, so the packaged
515
+ changelog carries it only inside the contract version 3 delta note.)
516
+
10
517
  ### Added
11
518
 
12
519
  - **The report draws the whole ledger as a 3D dependency graph.** It sits below