wowbagger 0.1.0-alpha.7 → 0.1.0-alpha.8
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 +193 -1
- package/README.md +194 -16
- package/adapters/claude-code/wowbagger-adapter.json +1 -1
- package/adapters/codex/wowbagger-adapter.json +1 -1
- package/adapters/opencode/wowbagger-adapter.json +1 -1
- package/docs/adapter-contract.md +66 -6
- package/docs/host-contract.md +287 -0
- package/docs/mutation-contract.md +558 -17
- package/docs/work-claim-contract.md +56 -24
- package/package.json +14 -1
- package/schemas/bare-ready-result.json +27 -0
- package/schemas/bare-validation-result.json +23 -0
- package/schemas/common.json +470 -0
- package/schemas/core-capabilities-response.json +346 -0
- package/schemas/core-envelope.json +141 -0
- package/schemas/core-inspect-response.json +37 -0
- package/schemas/core-inspect-workbench-response.json +340 -0
- package/schemas/core-list-error-response.json +242 -0
- package/schemas/core-list-query.json +98 -0
- package/schemas/core-list-response.json +140 -0
- package/schemas/core-report-response.json +379 -0
- package/schemas/core-transition-error-response.json +852 -0
- package/schemas/core-transition-request.json +47 -0
- package/schemas/core-transition-response.json +49 -0
- package/schemas/index.json +108 -0
- package/schemas/ledger-mutation-refusal.json +288 -0
- package/schemas/report-config-v1.json +140 -0
- package/schemas/report-config-v2.json +242 -0
- package/skills/wowbagger/SKILL.md +151 -38
- package/src/adapter/core-probe.js +87 -7
- package/src/claim-capabilities.js +4 -5
- package/src/claim-coordinator.js +15 -2
- package/src/claim-prospective.js +151 -0
- package/src/claim-publication.js +49 -9
- package/src/claim-sync.js +66 -0
- package/src/cli.js +671 -49
- package/src/extension-provision.js +33 -0
- package/src/git-autocommit.js +36 -6
- package/src/git-reconciliation.js +53 -9
- package/src/launch.js +52 -0
- package/src/lifecycle.js +153 -0
- package/src/limits.js +39 -0
- package/src/list.js +365 -0
- package/src/mutation.js +145 -118
- package/src/projection.js +39 -0
- package/src/report-graph.js +122 -21
- package/src/report-html.js +219 -51
- package/src/report-view.js +189 -0
- package/src/report.js +145 -15
- package/src/validate.js +4 -1
- package/src/workbench.js +117 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Local mutation contract
|
|
2
2
|
|
|
3
|
-
Status: versions 1
|
|
4
|
-
local-filesystem runtime currently emits version
|
|
3
|
+
Status: versions 1 through 5 are defined; the pre-alpha standalone
|
|
4
|
+
local-filesystem runtime currently emits version 5.
|
|
5
5
|
|
|
6
6
|
This document defines the machine contract implemented by the local-filesystem
|
|
7
7
|
mutation backend. It supplements [SPEC.md](../SPEC.md) and
|
|
@@ -10,7 +10,10 @@ mutation backend. It supplements [SPEC.md](../SPEC.md) and
|
|
|
10
10
|
|
|
11
11
|
The executable supports `validate`, `ready`, and the commands below. Clients
|
|
12
12
|
must still call `capabilities` and honor its advertised limits before assuming a
|
|
13
|
-
backend can provide a particular write guarantee.
|
|
13
|
+
backend can provide a particular write guarantee. A non-agent consumer that
|
|
14
|
+
launches this executable directly reads
|
|
15
|
+
[docs/host-contract.md](host-contract.md) for the package seam, the process
|
|
16
|
+
tuple, and the packaged JSON Schemas.
|
|
14
17
|
|
|
15
18
|
## Contract versions
|
|
16
19
|
|
|
@@ -159,10 +162,9 @@ and stops. That is the intended outcome, not a regression.
|
|
|
159
162
|
|
|
160
163
|
### Version 4
|
|
161
164
|
|
|
162
|
-
Version 4
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
complete difference against published version 3 (`0.1.0-alpha.6`):
|
|
165
|
+
Version 4 retained every version 3 request, response, state, exit, locking,
|
|
166
|
+
CAS, publication, and recovery rule except for these explicit deltas, which are
|
|
167
|
+
the complete difference against published version 3 (`0.1.0-alpha.6`):
|
|
166
168
|
|
|
167
169
|
- every core command envelope carries `contract_version: 4`;
|
|
168
170
|
- **the bounded item source.** `MAX_ITEM_SOURCE_BYTES` is 8,388,608 and bounds
|
|
@@ -211,6 +213,58 @@ members exactly must negotiate version 3 before reading a date refusal, and a
|
|
|
211
213
|
consumer that must not have its accepted input narrowed underneath it must
|
|
212
214
|
negotiate version 4 before writing an item.
|
|
213
215
|
|
|
216
|
+
### Version 5
|
|
217
|
+
|
|
218
|
+
Version 5 is the version this document defines and the runtime emits. It
|
|
219
|
+
retains every version 4 request, response, state, exit, locking, CAS,
|
|
220
|
+
publication, and recovery rule except for these explicit deltas, which are the
|
|
221
|
+
complete difference against published version 4 (`0.1.0-alpha.7`):
|
|
222
|
+
|
|
223
|
+
- every core command envelope carries `contract_version: 5`;
|
|
224
|
+
- **the bounded list operation.** `list` is a new read-only core command
|
|
225
|
+
(section 5.1). It enumerates a validated ledger as bounded item summaries
|
|
226
|
+
under closed filters, one selected sort field, and cursor pagination. It adds
|
|
227
|
+
the refusals `list-snapshot-changed` (exit 4) and `list-response-too-large`
|
|
228
|
+
(exit 2);
|
|
229
|
+
- **the advertised list bounds.** `result.operations.list` and four new
|
|
230
|
+
`result.limits` members carry the list contract's exact numbers (section 4).
|
|
231
|
+
A version 4 consumer that validated `result.operations` or `result.limits` by
|
|
232
|
+
exact members refuses the new members, which is the same fail-closed outcome
|
|
233
|
+
the version field already produces;
|
|
234
|
+
- **the bounded workbench projection.** `inspect` accepts the opt-in flag pair
|
|
235
|
+
`--workbench --as-of YYYY-MM-DD` and answers with a bounded per-item lifecycle
|
|
236
|
+
affordance projection under `result.workbench` (section 5.2). It adds the
|
|
237
|
+
refusal `workbench-response-too-large` (exit 2) and the advertisement
|
|
238
|
+
`result.operations.inspect.workbench` with three new `result.limits` members
|
|
239
|
+
(section 4). An `inspect` invocation without `--workbench` keeps its exact
|
|
240
|
+
result members and its exact bytes; and
|
|
241
|
+
- **named custom report views.** `report` accepts an optional `--view <name>`
|
|
242
|
+
against report configuration `report_version: 2`, adds `result.view` to a
|
|
243
|
+
named success, adds the refusal `report-view-not-found` (exit 2), and is
|
|
244
|
+
advertised at `result.operations.report` (section 4). An invocation without
|
|
245
|
+
`--view` keeps its exact result members and its exact bytes under either
|
|
246
|
+
configuration version.
|
|
247
|
+
|
|
248
|
+
Nothing else moves. `validate` and `ready` carry no version member and their
|
|
249
|
+
bytes are unchanged. `create`, `transition`, `patch`, `mint-id`, and
|
|
250
|
+
`capabilities` change only in the `contract_version` number, and an `inspect`
|
|
251
|
+
invocation without `--workbench` changes only in that number too: no root
|
|
252
|
+
member, result member, issue member, error code, or exit meaning of an existing
|
|
253
|
+
invocation changes.
|
|
254
|
+
|
|
255
|
+
A version 4 consumer fails closed against a version 5 core the same way: it
|
|
256
|
+
reads `contract_version: 5` from `capabilities --json`, does not recognize it,
|
|
257
|
+
and stops.
|
|
258
|
+
|
|
259
|
+
The bootstrap wire, adapter approval, instruction, handoff, and fixture-format
|
|
260
|
+
versions are separate version domains and remain version 1. The adapter
|
|
261
|
+
contract remains version 2 and the work-claim API remains version 2. The list
|
|
262
|
+
query and the workbench projection each have their own version domain,
|
|
263
|
+
`query_version` and `projection_version`, both `1`: a consumer negotiates them
|
|
264
|
+
through `result.operations.list.query_version` and
|
|
265
|
+
`result.operations.inspect.workbench.projection_version`, not through the core
|
|
266
|
+
contract version.
|
|
267
|
+
|
|
214
268
|
## 1. Scope
|
|
215
269
|
|
|
216
270
|
The contract keeps four concerns separate:
|
|
@@ -263,6 +317,8 @@ The local commands are:
|
|
|
263
317
|
~~~text
|
|
264
318
|
wowbagger capabilities --json
|
|
265
319
|
wowbagger inspect --ledger <dir> --id <id> --json
|
|
320
|
+
wowbagger inspect --ledger <dir> --id <id> --workbench --as-of YYYY-MM-DD --json
|
|
321
|
+
wowbagger list --ledger <dir> --input <json-file|-> --json
|
|
266
322
|
wowbagger create --ledger <dir> --input <json-file|-> --json [--auto-commit]
|
|
267
323
|
wowbagger transition --ledger <dir> --input <json-file|-> --json [--auto-commit]
|
|
268
324
|
wowbagger patch --ledger <dir> --input <json-file|-> --json [--auto-commit]
|
|
@@ -312,7 +368,7 @@ answer in two domains.
|
|
|
312
368
|
|
|
313
369
|
| Domain | Root `namespace` | `contract_version` | Version to negotiate |
|
|
314
370
|
|---|---|---|---|
|
|
315
|
-
| core | absent |
|
|
371
|
+
| core | absent | 5 | top-level `contract_version` of `capabilities --json` |
|
|
316
372
|
| work-claim | `work-claim` | 1, the legacy envelope marker | `result.operations.work_claim.api_version` of `claim capabilities --json` |
|
|
317
373
|
| ledger-publication | `ledger-publication` | 1, the legacy envelope marker | the same work-claim `api_version` |
|
|
318
374
|
| ledger-mutation | `ledger-mutation` | 1, the legacy envelope marker | the same work-claim `api_version` |
|
|
@@ -339,14 +395,20 @@ legacy claim-envelope marker, and a consumer must never compare it with the core
|
|
|
339
395
|
| `ready` | bare result `{as_of, valid, ready}` | bare result `{valid, errors}` on an invalid ledger, exit 1 |
|
|
340
396
|
| `capabilities` | core | core |
|
|
341
397
|
| `inspect` | core | core |
|
|
398
|
+
| `list` | core | core |
|
|
342
399
|
| `mint-id` | core | core |
|
|
343
400
|
| `report` | core | core |
|
|
344
401
|
| `create` | core | core, or ledger-mutation when the claim fence refuses |
|
|
345
402
|
| `transition` | core | core, or ledger-mutation when the claim fence refuses |
|
|
403
|
+
| `parent-migrate` | core | core, or ledger-mutation when the claim fence refuses |
|
|
404
|
+
| `snooze` | core | core, or ledger-mutation when the claim fence refuses |
|
|
346
405
|
| `patch` | core | core, or ledger-mutation when the claim fence refuses |
|
|
406
|
+
| `extensions-provision` | core | core |
|
|
347
407
|
| `provision` | work-claim | work-claim |
|
|
348
408
|
| `claim capabilities/read/acquire/renew/release` | work-claim | work-claim |
|
|
349
409
|
| `claim-verify` | work-claim | work-claim |
|
|
410
|
+
| `claim-merge-verify` | work-claim | work-claim |
|
|
411
|
+
| `claim-sync` | work-claim | work-claim |
|
|
350
412
|
| `claim-adopt` | work-claim | work-claim |
|
|
351
413
|
| `mutation-finalize` | work-claim | work-claim |
|
|
352
414
|
| `claim verify` | ledger-publication, `command: "read"` | ledger-publication |
|
|
@@ -359,6 +421,14 @@ A core response has exactly `ok`, `command`, `contract_version`, and one of
|
|
|
359
421
|
claim-domain response has exactly `ok`, `namespace`, `command`,
|
|
360
422
|
`contract_version`, `state`, and one of `result` or `error`; a `claim
|
|
361
423
|
capabilities` response omits `state`, and a claimed-publication response adds
|
|
424
|
+
|
|
425
|
+
Successful mutation responses may carry `result.changed_paths`. It is the
|
|
426
|
+
complete deterministic ledger-relative set whose bytes this invocation changed
|
|
427
|
+
in the working tree. It does not mean Git committed those paths. `create`
|
|
428
|
+
returns only its item path; `transition`, `patch`, and `publish-claimed` return
|
|
429
|
+
their item path plus the tracked reconciliation log when that log changed.
|
|
430
|
+
`--auto-commit` additionally returns `commit_paths` and `git_commit`; its
|
|
431
|
+
`changed_paths` equals the committed set.
|
|
362
432
|
`operation_id` once schema validation has accepted it. No expected envelope has
|
|
363
433
|
undocumented root members.
|
|
364
434
|
|
|
@@ -468,9 +538,10 @@ presence is reported separately as bounded recovery_artifacts.
|
|
|
468
538
|
| Exit | Condition | Error codes |
|
|
469
539
|
|---:|---|---|
|
|
470
540
|
| 0 | Successful command; a mutation is state committed. | none |
|
|
471
|
-
|
|
|
472
|
-
|
|
|
473
|
-
|
|
|
541
|
+
| 1 | A bare-result command found the ledger invalid, or `report` failed before or during publication. `report` answers an invalid ledger here rather than at exit 3. | bare result `{valid, errors}`, ledger-invalid (`report` only), report-read-failed, report-write-failed |
|
|
542
|
+
| 2 | Argument, request, lookup, or candidate/lifecycle/layout-precondition failure. | invalid-request, item-not-found, transition-precondition-failed, patch-precondition-failed, candidate-invalid, item-source-too-large, items-directory-unavailable, list-response-too-large, report-config-invalid, report-view-not-found |
|
|
543
|
+
| 3 | The complete configured ledger is invalid, for every command except `report`. | ledger-invalid |
|
|
544
|
+
| 4 | Cooperative comparison, lock, identity, or default-path conflict. | revision-conflict, lock-held, id-collision, path-collision, auto-commit-preflight-failed, mutation-finalize-refused, list-snapshot-changed |
|
|
474
545
|
| 5 | The backend lacks the required capability or write scope. | atomic-scope-required, capability-unavailable |
|
|
475
546
|
| 6 | An unexpected operating or post-publication recovery condition. | operation-failed, post-commit-recovery-required, write-outcome-unknown, git-commit-failed, git-commit-outcome-unknown, post-commit-reconciliation-failed |
|
|
476
547
|
|
|
@@ -596,21 +667,95 @@ members above. This is the one input to `capabilities`, so the response is
|
|
|
596
667
|
deterministic for a given working directory but not fixed across working
|
|
597
668
|
directories.
|
|
598
669
|
|
|
599
|
-
### Contract version
|
|
670
|
+
### Contract version 5 capability delta
|
|
600
671
|
|
|
601
672
|
The preceding JSON and three-member Git-dependent coupling remain the exact
|
|
602
|
-
version 1 definition. Versions 2, 3, and
|
|
603
|
-
paths; all omitted paths retain their version 1 values:
|
|
673
|
+
version 1 definition. Versions 2, 3, 4, and 5 change only the following
|
|
674
|
+
capability paths; all omitted paths retain their version 1 values:
|
|
604
675
|
|
|
605
|
-
| Path | Version
|
|
676
|
+
| Path | Version 5 value |
|
|
606
677
|
|---|---|
|
|
607
|
-
| `contract_version` | `
|
|
678
|
+
| `contract_version` | `5` |
|
|
608
679
|
| `result.backend.coordination_scope` | `"same-working-copy-cooperative-writers"` |
|
|
680
|
+
| `result.operations.inspect.workbench` | `{"supported":true,"projection_version":1}` |
|
|
681
|
+
| `result.operations.list` | `{"supported":true,"write_scope":"none","cas_scope":"none","query_version":1}` |
|
|
609
682
|
| `result.operations.patch` | `{"supported":true,"write_scope":"single-item","cas_scope":"exact-byte-sha256"}` |
|
|
683
|
+
| `result.operations.report` | `{"supported":true,"write_scope":"derived-output","config_versions":[1,2],"named_views":true}` |
|
|
610
684
|
| `result.operations.work_claim.api_version` | `2` |
|
|
611
685
|
| `result.limits.max_item_source_bytes` | `8388608` |
|
|
686
|
+
| `result.limits.default_list_page_size` | `50` |
|
|
687
|
+
| `result.limits.max_list_page_size` | `200` |
|
|
688
|
+
| `result.limits.max_list_title_characters` | `120` |
|
|
689
|
+
| `result.limits.max_list_response_bytes` | `131072` |
|
|
690
|
+
| `result.limits.max_workbench_title_characters` | `120` |
|
|
691
|
+
| `result.limits.max_workbench_collection_entries` | `50` |
|
|
692
|
+
| `result.limits.max_workbench_response_bytes` | `65536` |
|
|
612
693
|
| `result.limits.cross_worktree_coordination` | `false` |
|
|
613
694
|
|
|
695
|
+
`result.operations` advertises in the fixed order `inspect`, `list`, `create`,
|
|
696
|
+
`transition`, `patch`, `report`, `work_claim`. `result.limits` advertises
|
|
697
|
+
`max_item_source_bytes` first, then the four list bounds in the order above,
|
|
698
|
+
then the three workbench bounds, then the version 1 booleans. `list` is
|
|
699
|
+
read-only, so its `write_scope` and `cas_scope` are `none`, exactly like
|
|
700
|
+
`inspect`. `inspect` keeps its version 1 members and gains the nested
|
|
701
|
+
`workbench` member, which is how a consumer learns the opt-in affordance
|
|
702
|
+
projection of section 5.2 exists and which projection shape it will receive.
|
|
703
|
+
|
|
704
|
+
The four list bounds are exact numbers, not advice:
|
|
705
|
+
|
|
706
|
+
- `default_list_page_size` is the page size a query that omits `page_size` receives;
|
|
707
|
+
- `max_list_page_size` is the largest `page_size` a query may request;
|
|
708
|
+
- `max_list_title_characters` is the exact number of Unicode code points a row's
|
|
709
|
+
projected title may carry before it is truncated and flagged; and
|
|
710
|
+
- `max_list_response_bytes` bounds the complete `list` response, envelope and
|
|
711
|
+
trailing LF included.
|
|
712
|
+
|
|
713
|
+
A full page of maximum-width rows can exceed `max_list_response_bytes`. When it
|
|
714
|
+
does, the command refuses the page whole with `list-response-too-large` and the
|
|
715
|
+
caller lowers `page_size`; neither bound silently rewrites the other, and a
|
|
716
|
+
`list` response is never a partial page. A consumer with its own transport
|
|
717
|
+
budget uses the lower of that budget and `max_list_response_bytes`.
|
|
718
|
+
|
|
719
|
+
The three workbench bounds are exact numbers on the same terms:
|
|
720
|
+
|
|
721
|
+
- `max_workbench_title_characters` is the exact number of Unicode code points
|
|
722
|
+
the projected item title may carry before it is truncated and flagged;
|
|
723
|
+
- `max_workbench_collection_entries` is the largest number of entries any one
|
|
724
|
+
variable-size workbench collection carries — the relation lists, an option's
|
|
725
|
+
precondition issues, an option's blockers, and the related IDs inside one
|
|
726
|
+
issue. A longer collection carries its first entries, its observed `total`,
|
|
727
|
+
and `truncated: true`; and
|
|
728
|
+
- `max_workbench_response_bytes` bounds the complete workbench response,
|
|
729
|
+
envelope and trailing LF included.
|
|
730
|
+
|
|
731
|
+
The entry bound keeps every projection this core can build well inside the
|
|
732
|
+
response bound, so `max_workbench_response_bytes` is a promise rather than a
|
|
733
|
+
knob: a workbench request carries no size to lower. A projection that would
|
|
734
|
+
exceed it is refused whole with `workbench-response-too-large` at exit 2 rather
|
|
735
|
+
than written short.
|
|
736
|
+
|
|
737
|
+
`result.operations.report` is how a consumer learns what the read-only report
|
|
738
|
+
command accepts, so it never has to probe by generating an artifact.
|
|
739
|
+
`write_scope: "derived-output"` states that the command writes one configured
|
|
740
|
+
output path and no ledger item. `config_versions` lists the accepted
|
|
741
|
+
`<ledger>/.wowbagger/report.json` versions: version 1 is unchanged, and
|
|
742
|
+
`report_version: 2` adds a `views` object whose named members — for example
|
|
743
|
+
`security-blockers` — each project the validated ledger through grouped filters
|
|
744
|
+
with **OR within one filter group; AND across groups**. `named_views: true` says
|
|
745
|
+
the core accepts a selection by name:
|
|
746
|
+
|
|
747
|
+
~~~text
|
|
748
|
+
wowbagger report --ledger <dir> --view <name> --as-of YYYY-MM-DD --json
|
|
749
|
+
~~~
|
|
750
|
+
|
|
751
|
+
A named success adds `result.view` and nothing else; a base report's result is
|
|
752
|
+
unchanged. A selection that names no configured view, or any `--view` against a
|
|
753
|
+
version 1 configuration, is refused with `report-view-not-found` at exit 2,
|
|
754
|
+
`details.view` naming the requested name, and every existing output left as it
|
|
755
|
+
was. The report request, the configuration, and the generated artifact are
|
|
756
|
+
defined in [the README](../README.md#named-custom-report-views); this contract
|
|
757
|
+
defines the advertisement.
|
|
758
|
+
|
|
614
759
|
`result.limits.max_item_source_bytes` is the first member of `result.limits`,
|
|
615
760
|
before `multi_item_atomicity`. It is the exact number of bytes the complete
|
|
616
761
|
serialized item source may occupy, and it applies to successor bytes accepted
|
|
@@ -706,7 +851,7 @@ core contains only fields defined by supported item schema versions:
|
|
|
706
851
|
- schema_version, id, title, kind, status, created, updated;
|
|
707
852
|
- provenance.source and provenance.recorded_at;
|
|
708
853
|
- depends_on and related;
|
|
709
|
-
- optional parent, snoozed_until, completed, killed, archived;
|
|
854
|
+
- optional parent, snoozed_until, completed, killed, archived, deferred;
|
|
710
855
|
- optional number, the core-assigned item identity on schema version 2, and
|
|
711
856
|
optional priority, the caller-supplied schema priority; and
|
|
712
857
|
- decisions with only their defined action, date, summary, rationale, and
|
|
@@ -755,6 +900,332 @@ An operator repairing an invalid ledger reads the bytes and revision of the
|
|
|
755
900
|
items around the fault with `inspect`, and reads the fault itself from
|
|
756
901
|
`validation_errors`, whose `expected_path` and `remediation` name the repair.
|
|
757
902
|
|
|
903
|
+
### 5.1 The bounded list query
|
|
904
|
+
|
|
905
|
+
`list` enumerates a validated ledger. It is read-only, takes no lock, and
|
|
906
|
+
returns no item source and no body: a caller selects a row, then reads that one
|
|
907
|
+
item with `inspect`. `--ledger` is required; `list` performs no ledger discovery
|
|
908
|
+
and binds no implicit path.
|
|
909
|
+
|
|
910
|
+
`list` loads and validates the complete ledger before it projects anything. An
|
|
911
|
+
invalid ledger returns `ledger-invalid`, exit 3, with `details.validation_errors`
|
|
912
|
+
and no rows: there is no partial list.
|
|
913
|
+
|
|
914
|
+
#### The query
|
|
915
|
+
|
|
916
|
+
The request is UTF-8 JSON with one top-level object and exactly these members:
|
|
917
|
+
|
|
918
|
+
| Member | Required | Value |
|
|
919
|
+
|---|---|---|
|
|
920
|
+
| `query_version` | yes | `1` |
|
|
921
|
+
| `as_of` | yes | ISO calendar date; readiness is projected as of this date |
|
|
922
|
+
| `sort` | yes | object with exactly `field` and `direction` |
|
|
923
|
+
| `filters` | no | object with a subset of the closed filter members below |
|
|
924
|
+
| `page_size` | no | integer from 1 to `max_list_page_size`; default `default_list_page_size` |
|
|
925
|
+
| `cursor` | no | an opaque cursor from a previous `list` response |
|
|
926
|
+
|
|
927
|
+
`sort.field` is one of `created`, `id`, `number`, `priority`, `status`, `title`,
|
|
928
|
+
`updated`. `sort.direction` is `ascending` or `descending`.
|
|
929
|
+
|
|
930
|
+
`filters` members are:
|
|
931
|
+
|
|
932
|
+
| Filter | Value | Meaning |
|
|
933
|
+
|---|---|---|
|
|
934
|
+
| `status` | non-empty array of distinct statuses | row status is in the set |
|
|
935
|
+
| `kind` | non-empty array of distinct kinds | row kind is in the set |
|
|
936
|
+
| `ready` | boolean | row readiness as of `as_of` equals the value |
|
|
937
|
+
| `number` | non-empty array of distinct positive integers | row number is in the set |
|
|
938
|
+
| `title_contains` | non-empty string | the stored title contains it |
|
|
939
|
+
|
|
940
|
+
Filters are a conjunction: every named filter must accept the item. A value
|
|
941
|
+
filter is a set, so an empty array or a repeated entry is `invalid-request`
|
|
942
|
+
rather than a silently widened or deduplicated query. `title_contains` is a
|
|
943
|
+
case-sensitive substring test against the whole stored title, not against the
|
|
944
|
+
bounded excerpt a row carries; there is no locale collation, case folding, or
|
|
945
|
+
Unicode normalization.
|
|
946
|
+
|
|
947
|
+
Unknown, missing, and mistyped members are `invalid-request`, exit 2, with the
|
|
948
|
+
aggregated issues of section 3. A JSON number is an integer only when its
|
|
949
|
+
literal is canonical, so `page_size: 1.0` is `invalid-value`.
|
|
950
|
+
|
|
951
|
+
#### Order
|
|
952
|
+
|
|
953
|
+
The selected field decides the primary order. An item that does not carry the
|
|
954
|
+
selected member sorts after every item that does in `ascending` order;
|
|
955
|
+
`descending` is the exact reverse of the ascending primary comparison. Every
|
|
956
|
+
order then breaks ties on ascending immutable ID, in both directions, so the
|
|
957
|
+
total order is stable across invocations and a full traversal never depends on
|
|
958
|
+
file order.
|
|
959
|
+
|
|
960
|
+
#### The response
|
|
961
|
+
|
|
962
|
+
~~~json
|
|
963
|
+
{
|
|
964
|
+
"ok": true,
|
|
965
|
+
"command": "list",
|
|
966
|
+
"contract_version": 5,
|
|
967
|
+
"result": {
|
|
968
|
+
"query_version": 1,
|
|
969
|
+
"as_of": "2026-08-21",
|
|
970
|
+
"snapshot": { "revision": "sha256:…", "item_count": 5 },
|
|
971
|
+
"page": {
|
|
972
|
+
"size": 2,
|
|
973
|
+
"offset": 0,
|
|
974
|
+
"returned": 2,
|
|
975
|
+
"matched": 5,
|
|
976
|
+
"has_more": true,
|
|
977
|
+
"next_cursor": "…"
|
|
978
|
+
},
|
|
979
|
+
"items": []
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
~~~
|
|
983
|
+
|
|
984
|
+
`snapshot.revision` is the ledger-snapshot witness: a digest over every item's
|
|
985
|
+
ledger-relative path and exact item revision, so any item added, removed,
|
|
986
|
+
renamed, or byte-modified changes it. `snapshot.item_count` counts the whole
|
|
987
|
+
validated ledger, not the filtered page.
|
|
988
|
+
|
|
989
|
+
`page.matched` counts every row the filters accept in that snapshot.
|
|
990
|
+
`page.next_cursor` is a string when `has_more` is `true` and `null` otherwise.
|
|
991
|
+
|
|
992
|
+
Each row has exactly:
|
|
993
|
+
|
|
994
|
+
| Member | Presence | Value |
|
|
995
|
+
|---|---|---|
|
|
996
|
+
| `id` | always | immutable item ID; the only identity |
|
|
997
|
+
| `number` | when the item carries one | the short handle, never identity |
|
|
998
|
+
| `title` | always | the title, truncated to `max_list_title_characters` code points |
|
|
999
|
+
| `title_truncated` | always | `true` when the projection dropped code points |
|
|
1000
|
+
| `kind` | always | `task` or `epic` |
|
|
1001
|
+
| `status` | always | the item status |
|
|
1002
|
+
| `priority` | when the item carries one | the supplied priority, never recomputed |
|
|
1003
|
+
| `created` | always | ISO calendar date |
|
|
1004
|
+
| `updated` | always | ISO calendar date |
|
|
1005
|
+
| `revision` | always | the exact item revision, as `inspect` reports it |
|
|
1006
|
+
| `ready` | always | readiness as of `as_of` |
|
|
1007
|
+
|
|
1008
|
+
#### Cursor pagination
|
|
1009
|
+
|
|
1010
|
+
A cursor is opaque and carries no ledger content. It binds three things: the
|
|
1011
|
+
digest of the query it was issued for, the snapshot revision it was issued
|
|
1012
|
+
against, and the offset to resume at. A caller passes it back unchanged with the
|
|
1013
|
+
same query.
|
|
1014
|
+
|
|
1015
|
+
A string that is not a cursor this core issued is `invalid-request`, exit 2, at
|
|
1016
|
+
`/cursor`. It is never treated as a restart from offset zero.
|
|
1017
|
+
|
|
1018
|
+
When either binding no longer holds, the cursor is refused with
|
|
1019
|
+
`list-snapshot-changed`, exit 4:
|
|
1020
|
+
|
|
1021
|
+
~~~json
|
|
1022
|
+
{
|
|
1023
|
+
"ok": false,
|
|
1024
|
+
"command": "list",
|
|
1025
|
+
"contract_version": 5,
|
|
1026
|
+
"error": {
|
|
1027
|
+
"code": "list-snapshot-changed",
|
|
1028
|
+
"message": "The ledger snapshot the cursor was issued against is no longer current.",
|
|
1029
|
+
"details": {
|
|
1030
|
+
"mismatch": "snapshot",
|
|
1031
|
+
"cursor_snapshot_revision": "sha256:…",
|
|
1032
|
+
"current_snapshot_revision": "sha256:…"
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
~~~
|
|
1037
|
+
|
|
1038
|
+
`details.mismatch` is `snapshot` when the ledger changed and `query` when the
|
|
1039
|
+
same cursor was replayed under a different `as_of`, `filters`, or `sort`. Both
|
|
1040
|
+
carry one remedy: restart pagination with no cursor. A caller MUST NOT combine
|
|
1041
|
+
pages from different snapshot revisions; across one unchanged traversal every
|
|
1042
|
+
matching item appears exactly once.
|
|
1043
|
+
|
|
1044
|
+
The response is a live projection on every invocation. A consumer may cache it
|
|
1045
|
+
as transient interface state only; it never becomes a second ledger store.
|
|
1046
|
+
|
|
1047
|
+
#### Exit and error summary
|
|
1048
|
+
|
|
1049
|
+
| Exit | Code | Condition |
|
|
1050
|
+
|---:|---|---|
|
|
1051
|
+
| 0 | none | the page was projected and fits the response bound |
|
|
1052
|
+
| 2 | `invalid-request` | arguments, JSON, query schema, or cursor form |
|
|
1053
|
+
| 2 | `list-response-too-large` | the exact rows exceed `max_list_response_bytes` |
|
|
1054
|
+
| 3 | `ledger-invalid` | the complete configured ledger is invalid |
|
|
1055
|
+
| 4 | `list-snapshot-changed` | the cursor's snapshot or query binding moved |
|
|
1056
|
+
|
|
1057
|
+
### 5.2 The bounded workbench projection
|
|
1058
|
+
|
|
1059
|
+
A workbench shows a person which native lifecycle transitions an item can take
|
|
1060
|
+
before it asks for consent. `inspect` answers that read opt-in:
|
|
1061
|
+
|
|
1062
|
+
~~~text
|
|
1063
|
+
wowbagger inspect --ledger <dir> (--id <id> | --number <n>) --workbench --as-of YYYY-MM-DD --json
|
|
1064
|
+
~~~
|
|
1065
|
+
|
|
1066
|
+
`--workbench` requires `--as-of`, and `--as-of` is accepted only with
|
|
1067
|
+
`--workbench`: the projection reports readiness for an explicit date, so the
|
|
1068
|
+
date is part of the request rather than a default this core invents. An
|
|
1069
|
+
unpaired flag is an `invalid-request` issue at `/arguments`
|
|
1070
|
+
(`missing-argument` for the missing `--as-of`, `conflicting-argument` for the
|
|
1071
|
+
unpaired `--as-of`), and a date that is not an ISO calendar date is
|
|
1072
|
+
`invalid-value`. An invocation without `--workbench` is byte-identical to the
|
|
1073
|
+
lossless read of section 5: the two reads share one item resolution, and only
|
|
1074
|
+
the flagged one answers with a projection.
|
|
1075
|
+
|
|
1076
|
+
The read changes no ledger item, lock, claim journal, reconciliation log, or Git
|
|
1077
|
+
state. It takes no lock: a lock would make a read look like a lease.
|
|
1078
|
+
|
|
1079
|
+
`inspect --workbench` loads and validates the complete ledger and answers from
|
|
1080
|
+
that one snapshot. An invalid ledger returns `ledger-invalid`, exit 3, with
|
|
1081
|
+
`details.validation_errors` and nothing else. The lossless read attaches the
|
|
1082
|
+
resolved item to that refusal so an operator can read the bytes to repair; the
|
|
1083
|
+
workbench read cannot, because an affordance derived from a ledger this core has
|
|
1084
|
+
not judged would be a claim it cannot support. An unresolved selector is
|
|
1085
|
+
`item-not-found`, exit 2, with exactly the selector the caller used, as in
|
|
1086
|
+
section 5.
|
|
1087
|
+
|
|
1088
|
+
#### The response
|
|
1089
|
+
|
|
1090
|
+
~~~json
|
|
1091
|
+
{
|
|
1092
|
+
"ok": true,
|
|
1093
|
+
"command": "inspect",
|
|
1094
|
+
"contract_version": 5,
|
|
1095
|
+
"result": {
|
|
1096
|
+
"workbench": {
|
|
1097
|
+
"projection_version": 1,
|
|
1098
|
+
"as_of": "2026-08-22",
|
|
1099
|
+
"snapshot": { "revision": "sha256:<hex>", "item_count": 5 },
|
|
1100
|
+
"observation": {
|
|
1101
|
+
"authority": "observed-snapshot",
|
|
1102
|
+
"rechecked_by": [
|
|
1103
|
+
"revision", "lock", "claim-fence", "reconciliation", "candidate-validation"
|
|
1104
|
+
]
|
|
1105
|
+
},
|
|
1106
|
+
"item": {
|
|
1107
|
+
"id": "wb_...",
|
|
1108
|
+
"number": 10,
|
|
1109
|
+
"title": "Epic awaiting disposition",
|
|
1110
|
+
"title_truncated": false,
|
|
1111
|
+
"kind": "epic",
|
|
1112
|
+
"status": "backlog",
|
|
1113
|
+
"priority": 1,
|
|
1114
|
+
"parent": "wb_...",
|
|
1115
|
+
"created": "2026-08-02",
|
|
1116
|
+
"updated": "2026-08-05",
|
|
1117
|
+
"revision": "sha256:<hex>",
|
|
1118
|
+
"ready": false,
|
|
1119
|
+
"depends_on": { "entries": ["wb_..."], "total": 1, "truncated": false },
|
|
1120
|
+
"related": { "entries": [], "total": 0, "truncated": false }
|
|
1121
|
+
},
|
|
1122
|
+
"transition_options": [
|
|
1123
|
+
{
|
|
1124
|
+
"to_status": "done",
|
|
1125
|
+
"action": "complete",
|
|
1126
|
+
"decision_required": true,
|
|
1127
|
+
"minimum_date": "2026-08-05",
|
|
1128
|
+
"enabled": false,
|
|
1129
|
+
"precondition_issues": {
|
|
1130
|
+
"entries": [
|
|
1131
|
+
{
|
|
1132
|
+
"code": "live-dependencies",
|
|
1133
|
+
"field": "depends_on",
|
|
1134
|
+
"message": "Completion requires every depends_on target to be done.",
|
|
1135
|
+
"related_ids": { "entries": ["wb_..."], "total": 1, "truncated": false }
|
|
1136
|
+
}
|
|
1137
|
+
],
|
|
1138
|
+
"total": 1,
|
|
1139
|
+
"truncated": false
|
|
1140
|
+
},
|
|
1141
|
+
"blockers": { "entries": [], "total": 0, "truncated": false }
|
|
1142
|
+
}
|
|
1143
|
+
]
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
~~~
|
|
1148
|
+
|
|
1149
|
+
`result` carries exactly one member, `workbench`, and its members are in the
|
|
1150
|
+
order above. The projection is negotiated by `projection_version`, its own
|
|
1151
|
+
version domain, exactly as `list` is negotiated by `query_version`.
|
|
1152
|
+
|
|
1153
|
+
`snapshot` is the same witness `list` returns: it covers every item's
|
|
1154
|
+
ledger-relative path and exact revision, so any item added, removed, renamed, or
|
|
1155
|
+
byte-modified changes it. It is in the response because an option's blockers and
|
|
1156
|
+
precondition issues are functions of the whole ledger, not of the one item, so
|
|
1157
|
+
the item revision alone would not name what was observed.
|
|
1158
|
+
|
|
1159
|
+
`item` is a bounded summary: `number`, `priority`, and `parent` are present only
|
|
1160
|
+
when the item carries them, `title` is projected to
|
|
1161
|
+
`max_workbench_title_characters` with `title_truncated` saying whether it was
|
|
1162
|
+
cut, `depends_on` and `related` are bounded collections, and `ready` is the
|
|
1163
|
+
section 5.1 readiness state for `as_of`. The projection carries no item source
|
|
1164
|
+
and no body: those are the lossless read of section 5, and this response stays
|
|
1165
|
+
inside an advertised bound.
|
|
1166
|
+
|
|
1167
|
+
`revision` is the exact revision of the item's own bytes, and it is what a caller
|
|
1168
|
+
sends as `transition`'s `expected_revision`.
|
|
1169
|
+
|
|
1170
|
+
#### Transition options
|
|
1171
|
+
|
|
1172
|
+
`transition_options` holds one option for every lifecycle target the section 8
|
|
1173
|
+
edge table allows out of this item's kind and status, in ascending status order.
|
|
1174
|
+
A target the table does not allow is absent, so `invalid-edge` never appears in a
|
|
1175
|
+
projected issue. A terminal item has no allowed target and its
|
|
1176
|
+
`transition_options` is `[]`.
|
|
1177
|
+
|
|
1178
|
+
| Member | Meaning |
|
|
1179
|
+
|---|---|
|
|
1180
|
+
| `to_status` | the target status a `transition` request would name |
|
|
1181
|
+
| `action` | the generated decision action, or `null` for the two no-decision edges |
|
|
1182
|
+
| `decision_required` | whether the request must carry `decision.summary` and `decision.rationale` |
|
|
1183
|
+
| `minimum_date` | the earliest legal transition date, `max(created, updated)` |
|
|
1184
|
+
| `enabled` | whether the observed ledger refuses nothing for this target at `minimum_date` |
|
|
1185
|
+
| `precondition_issues` | the observed section 8 precondition issues, bounded |
|
|
1186
|
+
| `blockers` | the observed section 8 multi-item blockers, bounded |
|
|
1187
|
+
|
|
1188
|
+
An option is evaluated at `minimum_date`, so neither date precondition can be
|
|
1189
|
+
what disables it: a caller reads `minimum_date` and sends that date or a later
|
|
1190
|
+
one. Issues and blockers use exactly the `transition` vocabulary — the same
|
|
1191
|
+
`code`, `field`, `message`, `item_id`, and ordering — so a consumer renders one
|
|
1192
|
+
set of refusal reasons. The one reshaped member is an issue's `related_ids`,
|
|
1193
|
+
which is a bounded collection here because that list grows with the ledger.
|
|
1194
|
+
|
|
1195
|
+
`enabled` is `true` exactly when both bounded collections observed nothing:
|
|
1196
|
+
`precondition_issues.total` and `blockers.total` are `0`. A disabled option still
|
|
1197
|
+
names why, so a workbench can show a target and the reason it cannot be taken
|
|
1198
|
+
rather than hiding it.
|
|
1199
|
+
|
|
1200
|
+
#### What the projection does not promise
|
|
1201
|
+
|
|
1202
|
+
`observation` states the semantics in the response rather than leaving them to a
|
|
1203
|
+
reader of this contract. `authority: "observed-snapshot"` says the projection is
|
|
1204
|
+
one observation of the named snapshot: it is not a lease, not a reservation, and
|
|
1205
|
+
not a promise that the option remains executable after the returned revision.
|
|
1206
|
+
`rechecked_by` names what a later `transition` rechecks under lock — the exact
|
|
1207
|
+
revision, the cooperative lock, the claim fence, reconciliation, and complete
|
|
1208
|
+
candidate validation — in the refusal precedence of section 8. A transition
|
|
1209
|
+
dispatched against a returned option can still be refused by any of them, and
|
|
1210
|
+
that refusal is the authority.
|
|
1211
|
+
|
|
1212
|
+
The affordance projection and `transition` share one implementation-level
|
|
1213
|
+
lifecycle definition (`src/lifecycle.js`): the edge table, the generated actions,
|
|
1214
|
+
the precondition issues, and the blockers have exactly one home, so an
|
|
1215
|
+
advertised option cannot drift from what the mutation does. A consumer therefore
|
|
1216
|
+
never reimplements the edge table, the date derivation, the decision
|
|
1217
|
+
requirement, or the blocker rules.
|
|
1218
|
+
|
|
1219
|
+
#### Exit and error summary
|
|
1220
|
+
|
|
1221
|
+
| Exit | Code | Condition |
|
|
1222
|
+
|---:|---|---|
|
|
1223
|
+
| 0 | none | the projection was answered and fits the response bound |
|
|
1224
|
+
| 2 | `invalid-request` | arguments, including the `--workbench`/`--as-of` pairing |
|
|
1225
|
+
| 2 | `item-not-found` | the selector resolved no item in a valid ledger |
|
|
1226
|
+
| 2 | `workbench-response-too-large` | the exact projection exceeds `max_workbench_response_bytes` |
|
|
1227
|
+
| 3 | `ledger-invalid` | the complete configured ledger is invalid |
|
|
1228
|
+
|
|
758
1229
|
## 6. Cooperative lock and snapshot protocol
|
|
759
1230
|
|
|
760
1231
|
Per-ID locks live at:
|
|
@@ -1812,6 +2283,9 @@ patch.
|
|
|
1812
2283
|
| patch-precondition-failed | id, issues |
|
|
1813
2284
|
| candidate-invalid | id, validation_errors |
|
|
1814
2285
|
| item-source-too-large | id, size_bytes, limit_bytes |
|
|
2286
|
+
| list-response-too-large | max_list_response_bytes, response_bytes, page_size |
|
|
2287
|
+
| list-snapshot-changed | mismatch, cursor_snapshot_revision, current_snapshot_revision |
|
|
2288
|
+
| workbench-response-too-large | id, max_workbench_response_bytes, response_bytes |
|
|
1815
2289
|
| revision-conflict | id, expected_revision, actual_revision |
|
|
1816
2290
|
| lock-held | id, lock_path, owner, owner_diagnostic |
|
|
1817
2291
|
| id-collision | id, path, actual_revision |
|
|
@@ -1923,6 +2397,58 @@ A cleanup failure reports the remaining bounded artifacts. Locks are never
|
|
|
1923
2397
|
auto-broken by age. Clients must inspect after committed-recovery or unknown
|
|
1924
2398
|
outcomes and must not retry blindly.
|
|
1925
2399
|
|
|
2400
|
+
### Response loss and once-only dispatch
|
|
2401
|
+
|
|
2402
|
+
A caller can lose the response to a mutation it already dispatched: the
|
|
2403
|
+
transport between the caller and the owning host can drop after the core
|
|
2404
|
+
started, and a dropped transport observes nothing about the ledger. This
|
|
2405
|
+
section is normative for every explicit user-triggered `create`, `transition`,
|
|
2406
|
+
and `patch`.
|
|
2407
|
+
|
|
2408
|
+
Only a complete observed process result establishes an outcome. A result is
|
|
2409
|
+
complete when the process is observed to have exited, both captured streams are
|
|
2410
|
+
complete and within their bounds, and standard output is one valid envelope for
|
|
2411
|
+
the dispatched command. Anything else establishes nothing about the ledger.
|
|
2412
|
+
|
|
2413
|
+
| Observation | Established outcome | Required client action |
|
|
2414
|
+
|---|---|---|
|
|
2415
|
+
| Complete envelope, exit 0, state committed | The mutation applied exactly as returned. | Continue. |
|
|
2416
|
+
| Complete envelope, state unchanged, any nonzero exit | The mutation did not create, remove, rename, or byte-modify an item. | Read the refusal; a revision conflict re-inspects. |
|
|
2417
|
+
| Complete envelope, exit 6 post-commit-recovery-required, state committed | The item is published; a verify or cleanup step still needs recovery. | Inspect the item and clear the reported recovery artifacts. Never repeat the mutation. |
|
|
2418
|
+
| Complete envelope, exit 6 write-outcome-unknown, state unknown | Publication was attempted and the visible bytes are indeterminate. | Validate, inspect, and compare the caller-known revision. Never repeat the mutation. |
|
|
2419
|
+
| Signal, timeout, orphan or containment doubt, or an incomplete or over-limit stream | Nothing. The mutation may or may not have applied. | Treat as unresolved. |
|
|
2420
|
+
| No envelope, a partial envelope, or no response at all because the transport was lost | Nothing. | Treat as unresolved. |
|
|
2421
|
+
|
|
2422
|
+
No row reports success that was not observed, and no row licenses a repeat of
|
|
2423
|
+
the mutation.
|
|
2424
|
+
|
|
2425
|
+
An unresolved outcome is answered by one sequence, never by a retry:
|
|
2426
|
+
|
|
2427
|
+
1. Dispatch once, never replay, invalidate the inspected revision, reconnect,
|
|
2428
|
+
then re-read the ledger.
|
|
2429
|
+
2. Re-read means `validate`, then `inspect` the caller-known ID. For `create`
|
|
2430
|
+
the caller already knows the ID it minted; for `transition` and `patch` the
|
|
2431
|
+
caller already knows the expected revision it sent.
|
|
2432
|
+
3. Compare the observed current state with the caller's own pre-dispatch
|
|
2433
|
+
observation, and report the comparison as current state. A later item state
|
|
2434
|
+
describes only current ledger state; it never proves that the lost dispatch
|
|
2435
|
+
caused it.
|
|
2436
|
+
4. Dispatch a fresh mutation only after a person reviews that comparison, and
|
|
2437
|
+
only as a new explicit decision built on the current revision.
|
|
2438
|
+
|
|
2439
|
+
Exit 4 is not response loss. A `revision-conflict` is a complete observed
|
|
2440
|
+
refusal with state unchanged: the item changed after it was inspected, and the
|
|
2441
|
+
core proves the write did not run. It invalidates the inspected revision and
|
|
2442
|
+
requires re-inspection; it never becomes an unresolved outcome and never
|
|
2443
|
+
triggers a retry of the same request bytes.
|
|
2444
|
+
|
|
2445
|
+
This contract carries no operation identity. There is no operation ID, no
|
|
2446
|
+
durable outcome store, and no replay endpoint, because the caller never
|
|
2447
|
+
replays: once-only dispatch plus honest unresolved outcomes need no
|
|
2448
|
+
correlation. Introducing replay-safe correlation requires a new contract
|
|
2449
|
+
decision that defines request binding, retention, and collision behavior before
|
|
2450
|
+
implementation.
|
|
2451
|
+
|
|
1926
2452
|
## 11. Normative design vectors
|
|
1927
2453
|
|
|
1928
2454
|
The synthetic vectors under
|
|
@@ -1944,6 +2470,21 @@ frontmatter, the body append with its exclusivity refusal, and the declared
|
|
|
1944
2470
|
extension-member correction with its untouched anchored extension nodes and
|
|
1945
2471
|
its undeclared-member refusal.
|
|
1946
2472
|
|
|
2473
|
+
The `list` cases cover the seven categories section 5.1 is accepted against: an
|
|
2474
|
+
empty ledger's witnessed empty page (`list-empty`), a mixed-status filtered
|
|
2475
|
+
first page with its continuation cursor (`list-page`), a cursor whose bound
|
|
2476
|
+
snapshot moved (`list-stale-cursor`), an invalid ledger refused with no rows
|
|
2477
|
+
beside the error (`list-invalid-ledger`), and a maximum page whose exact rows
|
|
2478
|
+
exceed the advertised response bound (`list-response-too-large`).
|
|
2479
|
+
|
|
2480
|
+
The `workbench` cases cover section 5.2 the same way: a task's four allowed
|
|
2481
|
+
edges with their generated actions and derived minimum date
|
|
2482
|
+
(`workbench-task-options`), an epic carrying both observed precondition issues
|
|
2483
|
+
and both observed disposition blockers (`workbench-epic-blocked`), a terminal
|
|
2484
|
+
item with no target at all (`workbench-terminal`), and an invalid ledger refused
|
|
2485
|
+
with no projection beside the error (`workbench-invalid-ledger`). Every one
|
|
2486
|
+
declares identical before and after ledger digests: the projection is a read.
|
|
2487
|
+
|
|
1947
2488
|
The runtime executes every vector as a black-box CLI test, including exact
|
|
1948
2489
|
response bytes and the complete before/after ledger snapshot.
|
|
1949
2490
|
|