wowbagger 0.1.0-alpha.13 → 0.1.0-alpha.14
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 +200 -0
- package/README.md +8 -8
- package/docs/mutation-contract.md +104 -20
- package/docs/work-claim-contract.md +291 -64
- package/package.json +1 -1
- package/skills/wowbagger/SKILL.md +116 -21
- package/src/claim-coordinator.js +17 -3
- package/src/claim-journal.js +44 -4
- package/src/claim-publication.js +122 -93
- package/src/cli.js +5 -7
- package/src/git-autocommit.js +26 -21
- package/src/mutation.js +15 -2
- package/src/reconciliation-classifier.js +117 -0
|
@@ -14,7 +14,7 @@ publish something.
|
|
|
14
14
|
Install the core separately before using this skill:
|
|
15
15
|
|
|
16
16
|
```sh
|
|
17
|
-
npm install -g wowbagger@0.1.0-alpha.
|
|
17
|
+
npm install -g wowbagger@0.1.0-alpha.14
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
The core requires Node.js 20 or later. This plugin ships only agent
|
|
@@ -34,9 +34,9 @@ wowbagger capabilities --json
|
|
|
34
34
|
|
|
35
35
|
Read the plain distribution version from the first command and the top-level
|
|
36
36
|
`contract_version` from the second. **This skill requires distribution version
|
|
37
|
-
`0.1.0-alpha.
|
|
37
|
+
`0.1.0-alpha.14` and core `contract_version: 5`.**
|
|
38
38
|
|
|
39
|
-
The distribution pin names the published `0.1.0-alpha.
|
|
39
|
+
The distribution pin names the published `0.1.0-alpha.14` release; the cut that
|
|
40
40
|
publishes core `contract_version: 5` moves it. Earlier cores report
|
|
41
41
|
`contract_version: 3` or lower and lack behavior this skill requires, including
|
|
42
42
|
the bounded item source, so the version check refuses them. Do not soften
|
|
@@ -184,6 +184,32 @@ work read as ready.
|
|
|
184
184
|
redaction and no access control. Say that plainly if a user asks for a report
|
|
185
185
|
that hides work from a reader.
|
|
186
186
|
|
|
187
|
+
## Every writer must be on the same core before the first create
|
|
188
|
+
|
|
189
|
+
On a provisioned ledger, `create` now records its allocation in the shared
|
|
190
|
+
claim journal before it publishes anything. That grammar is new, so the upgrade
|
|
191
|
+
is a hard cutover with no automatic migration and no mixed-version grace
|
|
192
|
+
period: **upgrade every writer in one Git coordination domain to the current
|
|
193
|
+
core before the first alpha.14 create.** A worktree left on the old core does
|
|
194
|
+
not write a duplicate — it stops making claim-protected mutations, which is the
|
|
195
|
+
safe outcome, not a usable one.
|
|
196
|
+
|
|
197
|
+
An old core cannot read the new create entry and says so badly. It answers
|
|
198
|
+
exit 6, `error.code` `claim-store-unavailable`, message
|
|
199
|
+
`The durable claim store is unavailable.`, and `error.details.reason`
|
|
200
|
+
`claim-store-unreadable`, leaves state unchanged, and writes no item. Read that
|
|
201
|
+
exact combination as **this repository was written by a newer Wowbagger;
|
|
202
|
+
upgrade this worktree to continue**, and say so to the user: the old binary is
|
|
203
|
+
immutable and can never print better guidance. (Item #185 is open for general
|
|
204
|
+
version-drift detection.)
|
|
205
|
+
|
|
206
|
+
Say what the fix does and does not cover. It closes the reported
|
|
207
|
+
PropertyCompass2 collision: cooperating alpha.14 worktrees of one clone that
|
|
208
|
+
share one Git common directory can no longer commit two items carrying the same
|
|
209
|
+
number. Separate clones, separate machines, alpha.13 writers before the hard
|
|
210
|
+
cutover, and noncooperating writes stay outside that fence and still rely on
|
|
211
|
+
branch integration plus `validate`.
|
|
212
|
+
|
|
187
213
|
## Writing
|
|
188
214
|
|
|
189
215
|
Every write is an explicit, reviewable Git change. Show the user the command
|
|
@@ -434,6 +460,11 @@ authorized predecessor/successor window produces no finding, so another
|
|
|
434
460
|
mutation can run before the first is committed. Do not mistake acceptance for
|
|
435
461
|
durability. Commit each mutation anyway, then run `claim-verify`.
|
|
436
462
|
|
|
463
|
+
`create` never gets that window. A new item has no earlier authorized revision,
|
|
464
|
+
so Git `HEAD` is the only place its authorized bytes can live, and an
|
|
465
|
+
uncommitted create blocks every later mutation — including the next create —
|
|
466
|
+
with `git-finalization-required`.
|
|
467
|
+
|
|
437
468
|
**`claim-verify` is the reconciliation procedure for that refusal.** Do not go
|
|
438
469
|
looking for another verb; there is none. Read `details.findings`, do exactly
|
|
439
470
|
what each finding's `remediation` string says (it names the path), run
|
|
@@ -491,7 +522,10 @@ commit. With `--auto-commit`, `changed_paths` matches `commit_paths`, and
|
|
|
491
522
|
`git_commit` proves the commit.
|
|
492
523
|
|
|
493
524
|
Batch work is where this bites: filing ten items means ten commits, not one
|
|
494
|
-
commit at the end. Tell the user that before starting a batch.
|
|
525
|
+
commit at the end. Tell the user that before starting a batch. There is no
|
|
526
|
+
batch mutation to reach for — the create-then-commit loop is the supported bulk
|
|
527
|
+
pattern, and item #186 is open to design a safe batch create. `--auto-commit`
|
|
528
|
+
on each `create` is the shortest form of that loop.
|
|
495
529
|
|
|
496
530
|
### Or use --auto-commit and let one invocation do it
|
|
497
531
|
|
|
@@ -508,9 +542,11 @@ One flagged invocation refuses if anything is staged anywhere or any foreign
|
|
|
508
542
|
path under the ledger is dirty, reconciles, runs the mutation unchanged,
|
|
509
543
|
commits exactly the changed item plus at most one
|
|
510
544
|
`.wowbagger/reconcile-<namespace>.md` with a fixed subject, verifies that commit,
|
|
511
|
-
and runs `claim-verify` before it answers. A
|
|
512
|
-
|
|
513
|
-
|
|
545
|
+
and runs `claim-verify` before it answers. A successful `create --auto-commit`
|
|
546
|
+
commits exactly two paths: the created item and that reconciliation log. Every
|
|
547
|
+
command rebuilds its own derived reconciliation log during preflight, but
|
|
548
|
+
`create` refuses a log that was already dirty when you invoked it, and every
|
|
549
|
+
other dirty ledger path still refuses.
|
|
514
550
|
|
|
515
551
|
Preflight and post-commit reconciliation block findings for the requested item.
|
|
516
552
|
An unrelated `worktree-synchronization-required` finding remains visible to
|
|
@@ -610,29 +646,85 @@ envelope's `limits.cross_worktree_coordination: false` as permission to write
|
|
|
610
646
|
with hostile or noncooperating tools — it only says the core never synchronizes
|
|
611
647
|
checkouts.
|
|
612
648
|
|
|
613
|
-
A recorded `transition`, `patch`, or claimed publication blocks
|
|
614
|
-
targeting that same item with exit 6 `claim-store-unavailable`,
|
|
615
|
-
`publication-reconciliation-required`. An unrelated item mutation may
|
|
616
|
-
when the only finding is `worktree-synchronization-required`.
|
|
617
|
-
|
|
649
|
+
A recorded `create`, `transition`, `patch`, or claimed publication blocks
|
|
650
|
+
mutations targeting that same item with exit 6 `claim-store-unavailable`,
|
|
651
|
+
reason `publication-reconciliation-required`. An unrelated item mutation may
|
|
652
|
+
proceed when the only finding is `worktree-synchronization-required`.
|
|
653
|
+
|
|
654
|
+
`create` is the one exception to that scoping, because it allocates the next
|
|
655
|
+
number from the items this checkout can see. It also refuses when the journal
|
|
656
|
+
records a committed item this worktree does not hold at all: that item carries
|
|
657
|
+
a number nobody here can read, so the next number allocated here might already
|
|
658
|
+
be taken. A stale revision of an item this worktree does hold is not a blocker
|
|
659
|
+
for `create` — a number is immutable, so the local maximum is still right. The
|
|
660
|
+
refusal is the same exit 6 `claim-store-unavailable` with reason
|
|
661
|
+
`publication-reconciliation-required`, state `unchanged`, and no item file
|
|
662
|
+
written; integrate the missing item, run `claim-verify` until it exits 0, and
|
|
663
|
+
resend the same request, which then takes the next number.
|
|
664
|
+
|
|
665
|
+
That fence stops new collisions; it does not repair old ones. A ledger that
|
|
666
|
+
already carries duplicate numbers is item #182's recovery work: it fails
|
|
667
|
+
`validate` and refuses every mutation until #182 ships. Never hand-edit a
|
|
668
|
+
`number` to clear it — the edit skips the collision and reference checks every
|
|
669
|
+
mutation runs and can leave dangling `depends_on`, `related`, and parent
|
|
670
|
+
references that nothing reports.
|
|
618
671
|
|
|
619
672
|
Read `error.details.findings[0].reason` and act on the named item:
|
|
620
673
|
|
|
621
674
|
- `git-finalization-required` — you wrote the item here and have not committed.
|
|
622
675
|
Commit, then `claim-verify`.
|
|
623
|
-
- `worktree-synchronization-required` — another worktree wrote the item.
|
|
676
|
+
- `worktree-synchronization-required` — another worktree wrote the item.
|
|
677
|
+
`owner_ref` names an **active named worktree** and nothing else: it is always
|
|
678
|
+
the branch of a live worktree that carries the expected revision. If the
|
|
624
679
|
finding names `owner_ref` and `owner_commit`, WAIT for that owner to publish,
|
|
625
|
-
then synchronize this checkout and run `claim-verify`.
|
|
626
|
-
`
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
680
|
+
then synchronize this checkout and run `claim-verify`. `owner_unavailable:
|
|
681
|
+
true` means no such worktree exists, and it covers three cases: the expected
|
|
682
|
+
revision is not reachable at all, a live sibling holds it on a detached
|
|
683
|
+
`HEAD`, or it is reachable only from a tag, a remote-tracking ref, or a
|
|
684
|
+
branch no worktree has checked out. Reachability is not ownership; a ref you
|
|
685
|
+
can see is not a worktree that can publish. Follow the `remediation`, which
|
|
686
|
+
separates those cases. A revision that is **not yet reachable** means WAIT for
|
|
687
|
+
the owning worktree to commit, then synchronize. A revision that is
|
|
688
|
+
**reachable in Git while no active named worktree owner is established** —
|
|
689
|
+
a tag, a remote-tracking ref, an unchecked-out branch, or a detached sibling
|
|
690
|
+
carries it — is not a wait at all: inspect that reachable history, then
|
|
691
|
+
restore the authorized bytes or use explicit `claim-adopt` after review.
|
|
692
|
+
Ownership that **cannot be established from reachable refs** — the item has
|
|
693
|
+
never existed in this checkout — calls for inspecting reachable or dangling
|
|
694
|
+
commits with the same explicit restore or `claim-adopt`. Never merge unrelated
|
|
695
|
+
live work.
|
|
631
696
|
- `unauthorized-revision` — the item changed outside the protocol. Two remedies
|
|
632
697
|
are explicit: **restore** the authorized revision and run `claim-verify` to
|
|
633
698
|
discard the edit, or **adopt** the committed revision and run `claim-verify`
|
|
634
699
|
to keep it. Ask before discarding reviewed work.
|
|
635
700
|
|
|
701
|
+
Two live worktrees answering to one identity, or a worktree roster the
|
|
702
|
+
coordinator could not finish reading, refuse before anything is classified.
|
|
703
|
+
You get exit 6 `claim-store-unavailable`, reason `claim-store-unreadable`, with
|
|
704
|
+
`error.details.identity_diagnostic`: `duplicate-worktree-identity` naming the
|
|
705
|
+
`worktree_id` and `live_worktree_count`, or `worktree-enumeration-failed` with
|
|
706
|
+
no further member. Auto-commit reports the same diagnostic inside
|
|
707
|
+
`auto-commit-preflight-failed` with `retryable: false`. Neither is retryable
|
|
708
|
+
and neither is yours to repair by editing files. Report the diagnostic verbatim,
|
|
709
|
+
including the `worktree_id` and `live_worktree_count`: a duplicate means two
|
|
710
|
+
live worktrees hold the same identity file, usually because a private Git
|
|
711
|
+
directory was copied, and which worktree keeps the UUID is a person's decision.
|
|
712
|
+
An enumeration failure means a registered worktree path could not be read. The
|
|
713
|
+
identity itself is an opaque UUID a worktree writes once into its private Git
|
|
714
|
+
directory. Never create, copy, or edit it.
|
|
715
|
+
|
|
716
|
+
**`claim-verify` is repository-wide, and a clean mutation does not make it
|
|
717
|
+
exit 0.** It names no target, so any blocking finding anywhere in the
|
|
718
|
+
repository keeps it at exit 6 — including a finding on an item belonging to
|
|
719
|
+
work you have nothing to do with. On a repository with live sibling worktrees
|
|
720
|
+
you may commit every one of your own mutations and still never see
|
|
721
|
+
`claim-verify` exit 0. That is current behavior; item #184 is open in triage to
|
|
722
|
+
decide the supported verification surface. When the remaining findings all name
|
|
723
|
+
items you are not working on, say so and stop; do not hand-edit an item, and do
|
|
724
|
+
not run `claim-adopt` on a sibling's item to force exit 0. Adoption moves the
|
|
725
|
+
coordinator's authorized revision and is not a way to silence someone else's
|
|
726
|
+
finding.
|
|
727
|
+
|
|
636
728
|
Adoption is per item and per revision explicit. Name the item and both
|
|
637
729
|
revisions, take them from the finding, and commit the edited bytes first:
|
|
638
730
|
|
|
@@ -692,7 +784,9 @@ Use the claimed write path as one complete loop:
|
|
|
692
784
|
9. Run `claim-verify` after the commit or merge. It finalizes the Git outcome,
|
|
693
785
|
repairs response-loss cases, and reports later revision drift. Require exit
|
|
694
786
|
0 before the next mutating command; exit 6 means findings remain, so act on
|
|
695
|
-
each `remediation` string and run it again.
|
|
787
|
+
each `remediation` string and run it again. If every remaining finding names
|
|
788
|
+
an item you are not working on, that is the repository-wide scope described
|
|
789
|
+
above, not a failure of your work: report it instead of forcing exit 0.
|
|
696
790
|
10. Release the claim with its current observed state.
|
|
697
791
|
11. Run `validate` and show the resulting diff.
|
|
698
792
|
|
|
@@ -717,7 +811,8 @@ response envelopes, refusal precedence, and recovery rules.
|
|
|
717
811
|
`git add <dir> && git commit`.
|
|
718
812
|
8. On a provisioned ledger, run `claim-verify --ledger <dir> --json` and
|
|
719
813
|
require exit 0 before the next `create`, `transition`, `parent-migrate`,
|
|
720
|
-
`snooze`, `patch`, or `publish-claimed`.
|
|
814
|
+
`snooze`, `patch`, or `publish-claimed`. Findings that name only unrelated
|
|
815
|
+
items are repository-wide scope; report them rather than forcing exit 0.
|
|
721
816
|
|
|
722
817
|
Write, commit, `claim-verify`, next write. The unclaimed loop obeys the same
|
|
723
818
|
rule as the claimed one, because both run through the same coordinator. Steps 7
|
package/src/claim-coordinator.js
CHANGED
|
@@ -37,6 +37,12 @@ export async function withLegacyMutationFence(
|
|
|
37
37
|
const capability = resolveWorkClaimCapability({ gitCommonDir, namespace });
|
|
38
38
|
if (!capability.claim_protected_publication) return write();
|
|
39
39
|
|
|
40
|
+
// A create is the one mutation that reads an identity it was not given: it
|
|
41
|
+
// allocates the ledger's next number from the items this checkout holds. Its
|
|
42
|
+
// reconciliation therefore stays target-scoped like every other mutation, and
|
|
43
|
+
// gains one extra barrier below, for the coordinated items this working
|
|
44
|
+
// ledger cannot see at all.
|
|
45
|
+
const create = command === 'create-v1';
|
|
40
46
|
const storePath = claimStorePath(gitCommonDir, namespace);
|
|
41
47
|
const journalPath = claimJournalPath(gitCommonDir, namespace);
|
|
42
48
|
let intent = null;
|
|
@@ -61,9 +67,15 @@ export async function withLegacyMutationFence(
|
|
|
61
67
|
physicalNow: new Date().toISOString(),
|
|
62
68
|
targetItemId: itemId,
|
|
63
69
|
writeLogOnUnsafe: false,
|
|
64
|
-
writeLogWhenEmpty:
|
|
70
|
+
writeLogWhenEmpty: !create,
|
|
65
71
|
});
|
|
66
|
-
|
|
72
|
+
// The extra create barrier promised above rides on the same refusal: a
|
|
73
|
+
// coordinated item this checkout does not hold carries a number nobody
|
|
74
|
+
// here can read, so the next number this create would allocate may be one
|
|
75
|
+
// a sibling worktree already published. A stale revision of an item that
|
|
76
|
+
// is present hides no number: an item's number is immutable, so target
|
|
77
|
+
// scoping above still lets that create through.
|
|
78
|
+
if (reconciled.unsafe || (create && reconciled.missingCoordinatedItems.length > 0)) {
|
|
67
79
|
return claimStoreUnavailable(responseCommand, 'publication-reconciliation-required', {
|
|
68
80
|
findings: reconciled.findings,
|
|
69
81
|
});
|
|
@@ -75,7 +87,7 @@ export async function withLegacyMutationFence(
|
|
|
75
87
|
const projected = [...reconciled.entries];
|
|
76
88
|
const record = reconciled.state.claims.find((entry) => entry.item_id === itemId)
|
|
77
89
|
?? { item_id: itemId, last_epoch: '0', active: null };
|
|
78
|
-
const mustRefuse =
|
|
90
|
+
const mustRefuse = create
|
|
79
91
|
? record.last_epoch !== '0'
|
|
80
92
|
: record.active !== null && observedAt < record.active.expires_at;
|
|
81
93
|
if (mustRefuse) return legacyRefusal(responseCommand, namespace, itemId, observedAt, record);
|
|
@@ -110,6 +122,7 @@ export async function withLegacyMutationFence(
|
|
|
110
122
|
attempt_id: attemptId,
|
|
111
123
|
ledger_namespace: namespace,
|
|
112
124
|
item_id: itemId,
|
|
125
|
+
...(create ? { command } : {}),
|
|
113
126
|
observed_revision: expectedRevision,
|
|
114
127
|
observed_at: observedAt,
|
|
115
128
|
};
|
|
@@ -180,6 +193,7 @@ export async function withLegacyMutationFence(
|
|
|
180
193
|
attempt_id: intent.attempt_id,
|
|
181
194
|
ledger_namespace: namespace,
|
|
182
195
|
item_id: itemId,
|
|
196
|
+
...(create ? { command } : {}),
|
|
183
197
|
observed_revision: intent.expected_revision,
|
|
184
198
|
observed_at: observedAt,
|
|
185
199
|
}));
|
package/src/claim-journal.js
CHANGED
|
@@ -20,6 +20,8 @@ const JOURNAL_ENTRY_TYPES = new Set([
|
|
|
20
20
|
'publish-intent',
|
|
21
21
|
'revision-adoption',
|
|
22
22
|
]);
|
|
23
|
+
// Every command the legacy mutation fence journals.
|
|
24
|
+
const LEGACY_MUTATION_COMMANDS = new Set(['patch-v1', 'transition-v1', 'create-v1']);
|
|
23
25
|
// The reconciliation log is a tracked derived artifact. Entry types that every
|
|
24
26
|
// invocation appends, including refusals and read-only verification, stay out
|
|
25
27
|
// of it so a mutation that changes nothing leaves the working tree unchanged.
|
|
@@ -177,6 +179,7 @@ async function readJournalEntries(journalPath, namespace = null) {
|
|
|
177
179
|
const lines = source.split('\n').filter(Boolean);
|
|
178
180
|
if (lines.length > MAX_JOURNAL_ENTRIES) throw journalCapacityExceeded();
|
|
179
181
|
const entries = lines.map((line) => JSON.parse(line));
|
|
182
|
+
const createAttempts = new Map();
|
|
180
183
|
for (let index = 0; index < entries.length; index += 1) {
|
|
181
184
|
if (!Number.isSafeInteger(entries[index]?.seq) || entries[index].seq !== index + 1) {
|
|
182
185
|
throw journalInvalid('non-contiguous-sequence');
|
|
@@ -187,6 +190,9 @@ async function readJournalEntries(journalPath, namespace = null) {
|
|
|
187
190
|
if (!validJournalEntry(entries[index], namespace)) {
|
|
188
191
|
throw journalInvalid('invalid-entry');
|
|
189
192
|
}
|
|
193
|
+
if (!validCreateResolution(entries[index], createAttempts)) {
|
|
194
|
+
throw journalInvalid('invalid-entry');
|
|
195
|
+
}
|
|
190
196
|
}
|
|
191
197
|
return entries;
|
|
192
198
|
}
|
|
@@ -282,8 +288,10 @@ function validJournalEntry(entry, namespace) {
|
|
|
282
288
|
&& typeof entry.ledger_namespace === 'string'
|
|
283
289
|
&& (namespace === null || entry.ledger_namespace === namespace)
|
|
284
290
|
&& typeof entry.item_id === 'string'
|
|
285
|
-
&&
|
|
286
|
-
&&
|
|
291
|
+
&& LEGACY_MUTATION_COMMANDS.has(entry.command)
|
|
292
|
+
&& (entry.command === 'create-v1'
|
|
293
|
+
? entry.expected_revision === null
|
|
294
|
+
: typeof entry.expected_revision === 'string')
|
|
287
295
|
&& typeof entry.candidate_revision === 'string'
|
|
288
296
|
&& (!Object.hasOwn(entry, 'item_path') || typeof entry.item_path === 'string')
|
|
289
297
|
&& (!Object.hasOwn(entry, 'writer_worktree_id')
|
|
@@ -295,7 +303,7 @@ function validJournalEntry(entry, namespace) {
|
|
|
295
303
|
&& typeof entry.ledger_namespace === 'string'
|
|
296
304
|
&& (namespace === null || entry.ledger_namespace === namespace)
|
|
297
305
|
&& typeof entry.item_id === 'string'
|
|
298
|
-
&&
|
|
306
|
+
&& LEGACY_MUTATION_COMMANDS.has(entry.command)
|
|
299
307
|
&& typeof entry.committed_revision === 'string'
|
|
300
308
|
&& (!Object.hasOwn(entry, 'item_path') || typeof entry.item_path === 'string')
|
|
301
309
|
&& (!Object.hasOwn(entry, 'writer_worktree_id')
|
|
@@ -303,11 +311,19 @@ function validJournalEntry(entry, namespace) {
|
|
|
303
311
|
&& typeof entry.observed_at === 'string';
|
|
304
312
|
}
|
|
305
313
|
if (entry.type === 'legacy-mutation-abort') {
|
|
314
|
+
// A create abort names no predecessor revision, so the absence of one is
|
|
315
|
+
// its evidence. Patch and transition aborts keep their exact legacy shape
|
|
316
|
+
// and never carry a command.
|
|
317
|
+
const validAbortRevision = (
|
|
318
|
+
!Object.hasOwn(entry, 'command') && typeof entry.observed_revision === 'string'
|
|
319
|
+
) || (
|
|
320
|
+
entry.command === 'create-v1' && entry.observed_revision === null
|
|
321
|
+
);
|
|
306
322
|
return typeof entry.attempt_id === 'string'
|
|
307
323
|
&& typeof entry.ledger_namespace === 'string'
|
|
308
324
|
&& (namespace === null || entry.ledger_namespace === namespace)
|
|
309
325
|
&& typeof entry.item_id === 'string'
|
|
310
|
-
&&
|
|
326
|
+
&& validAbortRevision
|
|
311
327
|
&& typeof entry.observed_at === 'string';
|
|
312
328
|
}
|
|
313
329
|
if (entry.type === 'revision-adoption') {
|
|
@@ -351,6 +367,30 @@ function validJournalEntry(entry, namespace) {
|
|
|
351
367
|
&& typeof entry.git_commit === 'string';
|
|
352
368
|
}
|
|
353
369
|
|
|
370
|
+
// A journal-fenced create is only meaningful as a pair: the terminal that ends
|
|
371
|
+
// an attempt must name the intent that opened it, so a standalone create
|
|
372
|
+
// terminal names no authorized attempt and fails closed. Patch and transition
|
|
373
|
+
// entries remain valid standing alone, so only create-v1 attempts are tracked.
|
|
374
|
+
function validCreateResolution(entry, attempts) {
|
|
375
|
+
if (entry.command !== 'create-v1') return true;
|
|
376
|
+
if (entry.type === 'legacy-mutation-intent') {
|
|
377
|
+
attempts.set(entry.attempt_id, entry);
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
if (entry.type !== 'legacy-mutation' && entry.type !== 'legacy-mutation-abort') return true;
|
|
381
|
+
const intent = attempts.get(entry.attempt_id);
|
|
382
|
+
if (intent === undefined || intent.item_id !== entry.item_id) return false;
|
|
383
|
+
// A committed create publishes exactly the revision its intent proposed; an
|
|
384
|
+
// aborted one publishes none.
|
|
385
|
+
if (entry.type === 'legacy-mutation' && intent.candidate_revision !== entry.committed_revision) {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
// One attempt resolves once, so the intent is consumed here and a repeated
|
|
389
|
+
// resolution finds no open attempt to end.
|
|
390
|
+
attempts.delete(entry.attempt_id);
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
|
|
354
394
|
function isRecord(value) {
|
|
355
395
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
356
396
|
}
|