wowbagger 0.1.0-alpha.9 → 0.5.0
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 +509 -0
- package/README.md +272 -136
- package/docs/adapter-contract.md +1 -1
- package/docs/host-contract.md +7 -1
- package/docs/mutation-contract.md +354 -82
- package/docs/work-claim-contract.md +466 -88
- package/package.json +2 -2
- package/schemas/core-capabilities-response.json +1 -1
- package/schemas/core-envelope.json +4 -3
- package/schemas/index.json +18 -0
- package/schemas/ledger-repair-proposal.json +170 -0
- package/schemas/ledger-repair-request.json +61 -0
- package/schemas/ledger-repair-response.json +90 -0
- package/schemas/report-config-v1.json +4 -0
- package/schemas/report-config-v2.json +5 -0
- package/skills/wowbagger/SKILL.md +242 -59
- package/src/adapter/core-probe.js +3 -4
- package/src/adapter/process-outcome.js +8 -1
- package/src/claim-capabilities.js +3 -3
- package/src/claim-coordinator.js +61 -12
- package/src/claim-journal.js +212 -7
- package/src/claim-prospective.js +1 -28
- package/src/claim-publication.js +412 -78
- package/src/claim-request.js +9 -0
- package/src/claim-store.js +9 -4
- package/src/cli.js +302 -56
- package/src/extensions.js +1 -0
- package/src/git-autocommit.js +106 -43
- package/src/git-reconciliation.js +74 -19
- package/src/git-worktrees.js +73 -0
- package/src/instrumentation.js +1 -0
- package/src/launch.js +2 -2
- package/src/ledger-repair.js +1170 -0
- package/src/mutation.js +73 -15
- package/src/reconciliation-classifier.js +117 -0
- package/src/report-evidence.js +158 -41
- package/src/report-graph.js +201 -73
- package/src/report-html.js +358 -155
- package/src/report-impact.js +106 -0
- package/src/report-selection.js +97 -0
- package/src/report-sequencing.js +4 -4
- package/src/report-svg.js +74 -20
- package/src/report-view.js +14 -1
- package/src/report.js +109 -23
- package/src/version-drift.js +98 -0
- package/src/worktree-identity.js +165 -0
package/src/claim-request.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// requestSchemaError encodes for the reference model — production code must not
|
|
5
5
|
// depend on test code, so the rules are duplicated here rather than imported.
|
|
6
6
|
import { pointer } from './request.js';
|
|
7
|
+
import { validateLedgerRepairRequest } from './ledger-repair.js';
|
|
7
8
|
|
|
8
9
|
const NAMESPACE_ID = /^wbns_[a-f0-9]{32}$/;
|
|
9
10
|
const ITEM_ID = /^wb_[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
@@ -24,7 +25,15 @@ const REQUIRED_MEMBERS = {
|
|
|
24
25
|
// Returns an array of {path, code, message} issues (empty when the request is valid).
|
|
25
26
|
// The request must already be JSON-parsed and deep-normalized (plain objects/arrays,
|
|
26
27
|
// JsonNumber unwrapped) — this module only checks shape, not JSON syntax.
|
|
28
|
+
//
|
|
29
|
+
// `number-repair` is a ledger-repair request, not a work-claim one. It is
|
|
30
|
+
// answered here because the claim journal validates every entry it replays
|
|
31
|
+
// through this seam, so a repair request the journal accepts must be the exact
|
|
32
|
+
// request the command accepts. Its members are the repair domain's own.
|
|
27
33
|
export function validateClaimRequest(operation, request) {
|
|
34
|
+
if (operation === 'number-repair') {
|
|
35
|
+
return validateLedgerRepairRequest(request);
|
|
36
|
+
}
|
|
28
37
|
const required = REQUIRED_MEMBERS[operation];
|
|
29
38
|
if (!isPlainObject(request)) {
|
|
30
39
|
return [problem([], 'invalid-type', `The ${operation} request must be a JSON object.`)];
|
package/src/claim-store.js
CHANGED
|
@@ -127,7 +127,12 @@ export function namespaceLockHeld(hold, storePath) {
|
|
|
127
127
|
&& hold.storePath === storePath;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
|
|
130
|
+
// `withClaimLock` is the one file-lock primitive; the counter name says which
|
|
131
|
+
// lock a caller is taking, so a profile can hold each one to its own number
|
|
132
|
+
// instead of watching a single total drift.
|
|
133
|
+
export async function withClaimLock(storePath, fn, {
|
|
134
|
+
counter = 'namespace_lock_acquisitions',
|
|
135
|
+
} = {}) {
|
|
131
136
|
const directory = path.dirname(storePath);
|
|
132
137
|
await mkdir(directory, { recursive: true });
|
|
133
138
|
const lockPath = `${storePath}.lock`;
|
|
@@ -143,7 +148,7 @@ export async function withClaimLock(storePath, fn) {
|
|
|
143
148
|
}
|
|
144
149
|
|
|
145
150
|
try {
|
|
146
|
-
await acquireClaimLock(candidatePath, lockPath, recoveryPath);
|
|
151
|
+
await acquireClaimLock(candidatePath, lockPath, recoveryPath, counter);
|
|
147
152
|
const hold = { [NAMESPACE_LOCK_HOLD]: true, storePath, released: false };
|
|
148
153
|
try {
|
|
149
154
|
return await fn(hold);
|
|
@@ -158,7 +163,7 @@ export async function withClaimLock(storePath, fn) {
|
|
|
158
163
|
}
|
|
159
164
|
}
|
|
160
165
|
|
|
161
|
-
async function acquireClaimLock(candidatePath, lockPath, recoveryPath) {
|
|
166
|
+
async function acquireClaimLock(candidatePath, lockPath, recoveryPath, counter) {
|
|
162
167
|
for (;;) {
|
|
163
168
|
const recoveryOwner = await readLockOwner(recoveryPath);
|
|
164
169
|
if (recoveryOwner) {
|
|
@@ -168,7 +173,7 @@ async function acquireClaimLock(candidatePath, lockPath, recoveryPath) {
|
|
|
168
173
|
}
|
|
169
174
|
try {
|
|
170
175
|
await link(candidatePath, lockPath);
|
|
171
|
-
recordCount(
|
|
176
|
+
recordCount(counter);
|
|
172
177
|
return;
|
|
173
178
|
} catch (error) {
|
|
174
179
|
if (error?.code !== 'EEXIST') throw error;
|
package/src/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { mkdir, open, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { access, link, mkdir, open, unlink, writeFile } from 'node:fs/promises';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
5
6
|
|
|
@@ -7,13 +8,16 @@ import { resolveClaimBackend, resolveWorkClaimCapability } from './claim-capabil
|
|
|
7
8
|
import {
|
|
8
9
|
appendClaimEntry,
|
|
9
10
|
claimJournalPath,
|
|
11
|
+
isClaimJournalCapacityError,
|
|
10
12
|
claimReconcileLogPath,
|
|
13
|
+
parseReconcileLog,
|
|
11
14
|
replayClaimJournal,
|
|
12
15
|
writeReconcileLog,
|
|
13
16
|
} from './claim-journal.js';
|
|
14
17
|
import { claimAcquire, claimRead, claimRelease, claimRenew } from './claim-operations.js';
|
|
15
18
|
import {
|
|
16
19
|
adoptItemRevision,
|
|
20
|
+
hydrateClaimJournalFromHead,
|
|
17
21
|
publishClaimed,
|
|
18
22
|
reconcileClaimJournal,
|
|
19
23
|
readPublicationOutcome,
|
|
@@ -23,7 +27,7 @@ import {
|
|
|
23
27
|
} from './claim-publication.js';
|
|
24
28
|
import { loadExtensionDeclaration } from './extensions.js';
|
|
25
29
|
import { validateClaimRequest } from './claim-request.js';
|
|
26
|
-
import { checkProspectiveMerge
|
|
30
|
+
import { checkProspectiveMerge } from './claim-prospective.js';
|
|
27
31
|
import { proposeExtensionDeclaration } from './extension-provision.js';
|
|
28
32
|
import { selectCommittedAdoptions } from './claim-sync.js';
|
|
29
33
|
import {
|
|
@@ -36,6 +40,14 @@ import {
|
|
|
36
40
|
import { readGitTreeFile, readGitTreeLedger } from './git-reconciliation.js';
|
|
37
41
|
import { finalizeFromRecoveryToken, withAutoCommit } from './git-autocommit.js';
|
|
38
42
|
import { loadLedger } from './ledger.js';
|
|
43
|
+
import {
|
|
44
|
+
finalizeNumberRepairCommit,
|
|
45
|
+
isNumberRepairRecoveryToken,
|
|
46
|
+
ledgerRepairInvalidRequest,
|
|
47
|
+
numberRepair,
|
|
48
|
+
numberRepairProposal,
|
|
49
|
+
} from './ledger-repair.js';
|
|
50
|
+
import { inspectVersionDrift } from './version-drift.js';
|
|
39
51
|
import {
|
|
40
52
|
assertReportOutputOutsideLedger,
|
|
41
53
|
buildReportModel,
|
|
@@ -80,10 +92,20 @@ import { normalizeJsonValue, parseJsonRequest, sortIssues } from './request.js';
|
|
|
80
92
|
import { selectReady } from './ready.js';
|
|
81
93
|
import { isCalendarDate, validateLedger } from './validate.js';
|
|
82
94
|
import { inspectWorkbench } from './workbench.js';
|
|
95
|
+
import { readWorktreeIdentity } from './worktree-identity.js';
|
|
83
96
|
|
|
84
97
|
const CLAIM_OPERATIONS = { read: claimRead, acquire: claimAcquire, renew: claimRenew, release: claimRelease };
|
|
85
98
|
const MUTATION_CONTRACT_VERSION = 5;
|
|
86
|
-
const
|
|
99
|
+
const ITEM_ID = /^wb_[0-7][0-9A-HJKMNP-TV-Z]{25}$/;
|
|
100
|
+
const AUTO_COMMIT_COMMANDS = new Set([
|
|
101
|
+
'create',
|
|
102
|
+
'transition',
|
|
103
|
+
'parent-migrate',
|
|
104
|
+
'snooze',
|
|
105
|
+
'patch',
|
|
106
|
+
'publish-claimed',
|
|
107
|
+
'number-repair',
|
|
108
|
+
]);
|
|
87
109
|
// The report failures this command states. A code outside the set is an
|
|
88
110
|
// unexpected condition, never a classification the command can pass through.
|
|
89
111
|
const REPORT_FAILURE_CODES = new Set([
|
|
@@ -110,7 +132,7 @@ const COMMAND_SUMMARIES = {
|
|
|
110
132
|
list: 'List a validated ledger as bounded, paginated item summaries.',
|
|
111
133
|
create: 'Create one ledger item through atomic, no-clobber publication.',
|
|
112
134
|
transition: "Transition one item's lifecycle, guarded by lock and compare-and-swap.",
|
|
113
|
-
'parent-migrate': 'Move one
|
|
135
|
+
'parent-migrate': 'Move one item to or from an epic with CAS fencing.',
|
|
114
136
|
snooze: 'Set or clear an item snooze date with CAS fencing.',
|
|
115
137
|
patch: "Patch an item's priority and relation lists, guarded the same way.",
|
|
116
138
|
'extensions-provision': 'Declare explicitly selected existing extension members.',
|
|
@@ -120,6 +142,11 @@ const COMMAND_SUMMARIES = {
|
|
|
120
142
|
'claim-sync': 'Import committed adoption evidence into the local claim journal.',
|
|
121
143
|
'claim-adopt': 'Rule a committed out-of-protocol item revision legitimate.',
|
|
122
144
|
'mutation-finalize': 'Complete the Git commit an auto-commit mutation could not establish.',
|
|
145
|
+
'version-drift': 'Check installed skill and running core version compatibility.',
|
|
146
|
+
'number-repair': 'Apply a proposed duplicate-number repair to a ledger the mutation gate refuses.',
|
|
147
|
+
provision: 'Provision a Git-backed work-claim namespace.',
|
|
148
|
+
claim: 'Read and coordinate work claims for one ledger.',
|
|
149
|
+
'claim-verify': 'Reconcile the durable claim journal with ledger bytes and Git.',
|
|
123
150
|
};
|
|
124
151
|
|
|
125
152
|
const KNOWN_COMMANDS = new Set([
|
|
@@ -144,6 +171,8 @@ const KNOWN_COMMANDS = new Set([
|
|
|
144
171
|
'claim-sync',
|
|
145
172
|
'claim-adopt',
|
|
146
173
|
'mutation-finalize',
|
|
174
|
+
'version-drift',
|
|
175
|
+
'number-repair',
|
|
147
176
|
]);
|
|
148
177
|
|
|
149
178
|
const CLAIM_SUBCOMMAND_SUMMARIES = {
|
|
@@ -178,6 +207,22 @@ export async function runCli(argumentsList, { scenario } = {}) {
|
|
|
178
207
|
return;
|
|
179
208
|
}
|
|
180
209
|
|
|
210
|
+
if (command === 'version-drift') {
|
|
211
|
+
const parsedOptions = parseContractOptions(command, argumentsList.slice(1));
|
|
212
|
+
if (parsedOptions.issues.length > 0) {
|
|
213
|
+
writeInvalidRequest(command, parsedOptions.issues);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
writeClaimEnvelope(await inspectVersionDrift({
|
|
217
|
+
skillPath: parsedOptions.options.skill
|
|
218
|
+
?? fileURLToPath(new URL('../skills/wowbagger/SKILL.md', import.meta.url)),
|
|
219
|
+
packagePath: fileURLToPath(new URL('../package.json', import.meta.url)),
|
|
220
|
+
runningDistribution: DISTRIBUTION_VERSION,
|
|
221
|
+
runningContractVersion: 5,
|
|
222
|
+
}));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
181
226
|
if (command === 'capabilities') {
|
|
182
227
|
const parsedOptions = parseContractOptions(command, argumentsList.slice(1));
|
|
183
228
|
if (parsedOptions.issues.length > 0) {
|
|
@@ -380,8 +425,25 @@ export async function runCli(argumentsList, { scenario } = {}) {
|
|
|
380
425
|
writeInvalidRequest(command, parsedRequest.issues);
|
|
381
426
|
return;
|
|
382
427
|
}
|
|
428
|
+
const ledger = await loadLedger(parsedOptions.options.ledger);
|
|
429
|
+
const validation = validateLedger(ledger);
|
|
430
|
+
if (!validation.valid) {
|
|
431
|
+
process.stdout.write(`${JSON.stringify({
|
|
432
|
+
ok: false,
|
|
433
|
+
command,
|
|
434
|
+
contract_version: MUTATION_CONTRACT_VERSION,
|
|
435
|
+
state: 'unchanged',
|
|
436
|
+
error: {
|
|
437
|
+
code: 'ledger-invalid',
|
|
438
|
+
message: 'The configured ledger is invalid.',
|
|
439
|
+
details: { validation_errors: validation.errors },
|
|
440
|
+
},
|
|
441
|
+
})}\n`);
|
|
442
|
+
process.exitCode = 3;
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
383
445
|
const proposal = proposeExtensionDeclaration({
|
|
384
|
-
ledger
|
|
446
|
+
ledger,
|
|
385
447
|
members: parsedRequest.value?.members,
|
|
386
448
|
});
|
|
387
449
|
if (!proposal.ok) {
|
|
@@ -415,34 +477,72 @@ export async function runCli(argumentsList, { scenario } = {}) {
|
|
|
415
477
|
})}\n`);
|
|
416
478
|
return;
|
|
417
479
|
}
|
|
480
|
+
const sameDeclaration = (declaration) => declaration
|
|
481
|
+
&& Object.keys(declaration.members).length === Object.keys(proposal.declaration.members).length
|
|
482
|
+
&& Object.entries(proposal.declaration.members)
|
|
483
|
+
.every(([name, type]) => declaration.members[name] === type);
|
|
484
|
+
const refuseConflict = () => {
|
|
485
|
+
process.stdout.write(`${JSON.stringify({
|
|
486
|
+
ok: false,
|
|
487
|
+
command,
|
|
488
|
+
contract_version: MUTATION_CONTRACT_VERSION,
|
|
489
|
+
state: 'unchanged',
|
|
490
|
+
error: {
|
|
491
|
+
code: 'extension-declaration-conflict',
|
|
492
|
+
message: 'The ledger already carries a different extension declaration.',
|
|
493
|
+
details: { output: '.wowbagger/extensions.json' },
|
|
494
|
+
},
|
|
495
|
+
})}\n`);
|
|
496
|
+
process.exitCode = 4;
|
|
497
|
+
};
|
|
418
498
|
const existing = await loadExtensionDeclaration(parsedOptions.options.ledger);
|
|
419
499
|
if (existing.declared) {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
if (!same) {
|
|
423
|
-
process.stdout.write(`${JSON.stringify({
|
|
424
|
-
ok: false,
|
|
425
|
-
command,
|
|
426
|
-
contract_version: MUTATION_CONTRACT_VERSION,
|
|
427
|
-
state: 'unchanged',
|
|
428
|
-
error: {
|
|
429
|
-
code: 'extension-declaration-conflict',
|
|
430
|
-
message: 'The ledger already carries a different extension declaration.',
|
|
431
|
-
details: { output: '.wowbagger/extensions.json' },
|
|
432
|
-
},
|
|
433
|
-
})}\n`);
|
|
434
|
-
process.exitCode = 4;
|
|
500
|
+
if (!sameDeclaration(existing.declaration)) {
|
|
501
|
+
refuseConflict();
|
|
435
502
|
return;
|
|
436
503
|
}
|
|
437
504
|
} else {
|
|
438
505
|
await mkdir(path.dirname(output), { recursive: true });
|
|
439
|
-
|
|
506
|
+
if (scenario === 'extension-provision-concurrent-same') {
|
|
507
|
+
await writeFile(
|
|
508
|
+
output,
|
|
509
|
+
'{\n "extensions_version": 1,\n "members": {"tags":"string-list"}\n}\n',
|
|
510
|
+
{ flag: 'wx' },
|
|
511
|
+
);
|
|
512
|
+
} else if (scenario === 'extension-provision-concurrent-different') {
|
|
513
|
+
await writeFile(
|
|
514
|
+
output,
|
|
515
|
+
'{"extensions_version":1,"members":{"tier":"string"}}\n',
|
|
516
|
+
{ flag: 'wx' },
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
const temporary = path.join(path.dirname(output), `.extensions-${randomUUID()}.tmp`);
|
|
520
|
+
const handle = await open(temporary, 'wx');
|
|
440
521
|
try {
|
|
441
522
|
await handle.writeFile(proposal.source, 'utf8');
|
|
442
523
|
await handle.sync();
|
|
443
524
|
} finally {
|
|
444
525
|
await handle.close();
|
|
445
526
|
}
|
|
527
|
+
try {
|
|
528
|
+
await extensionProvisionCheckpoint(scenario, parsedOptions.options.ledger);
|
|
529
|
+
try {
|
|
530
|
+
await link(temporary, output);
|
|
531
|
+
} catch (error) {
|
|
532
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
533
|
+
const winner = await loadExtensionDeclaration(parsedOptions.options.ledger);
|
|
534
|
+
if (!winner.declared || !sameDeclaration(winner.declaration)) {
|
|
535
|
+
refuseConflict();
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
} finally {
|
|
540
|
+
try {
|
|
541
|
+
await unlink(temporary);
|
|
542
|
+
} catch (error) {
|
|
543
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
446
546
|
}
|
|
447
547
|
process.stdout.write(`${JSON.stringify({
|
|
448
548
|
ok: true,
|
|
@@ -608,6 +708,12 @@ export async function runCli(argumentsList, { scenario } = {}) {
|
|
|
608
708
|
writeClaimInvalidRequest(command, parsedOptions.issues);
|
|
609
709
|
return;
|
|
610
710
|
}
|
|
711
|
+
if (parsedOptions.options.id !== undefined && !ITEM_ID.test(parsedOptions.options.id)) {
|
|
712
|
+
writeClaimInvalidRequest(command, [
|
|
713
|
+
issue('/arguments', 'invalid-value', 'Argument --id must be a canonical Wowbagger item ID.'),
|
|
714
|
+
]);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
611
717
|
const ledgerDirectory = parsedOptions.options.ledger;
|
|
612
718
|
const gitCommonDir = await resolveVerifiedGitCommonDir(ledgerDirectory);
|
|
613
719
|
const namespace = gitCommonDir ? await readNamespace(ledgerDirectory) : null;
|
|
@@ -621,6 +727,7 @@ export async function runCli(argumentsList, { scenario } = {}) {
|
|
|
621
727
|
ledgerDirectory,
|
|
622
728
|
gitCommonDir,
|
|
623
729
|
namespace,
|
|
730
|
+
targetItemId: parsedOptions.options.id ?? null,
|
|
624
731
|
}));
|
|
625
732
|
return;
|
|
626
733
|
}
|
|
@@ -815,6 +922,13 @@ export async function runCli(argumentsList, { scenario } = {}) {
|
|
|
815
922
|
writeClaimInvalidRequest(command, parsedOptions.issues);
|
|
816
923
|
return;
|
|
817
924
|
}
|
|
925
|
+
if (isNumberRepairRecoveryToken(parsedOptions.options.recoveryToken)) {
|
|
926
|
+
writeClaimEnvelope(await finalizeNumberRepairCommit({
|
|
927
|
+
ledgerDirectory: parsedOptions.options.ledger,
|
|
928
|
+
token: parsedOptions.options.recoveryToken,
|
|
929
|
+
}));
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
818
932
|
writeClaimEnvelope(await finalizeFromRecoveryToken({
|
|
819
933
|
ledgerDirectory: parsedOptions.options.ledger,
|
|
820
934
|
token: parsedOptions.options.recoveryToken,
|
|
@@ -822,6 +936,47 @@ export async function runCli(argumentsList, { scenario } = {}) {
|
|
|
822
936
|
return;
|
|
823
937
|
}
|
|
824
938
|
|
|
939
|
+
// The proposal is request-free and read-only: the ledger is its whole input.
|
|
940
|
+
if (command === 'number-repair-proposal') {
|
|
941
|
+
const parsedOptions = parseContractOptions(command, argumentsList.slice(1));
|
|
942
|
+
if (parsedOptions.issues.length > 0) {
|
|
943
|
+
writeClaimEnvelope(ledgerRepairInvalidRequest(command, parsedOptions.issues));
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
writeClaimEnvelope(await numberRepairProposal(parsedOptions.options.ledger));
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (command === 'number-repair') {
|
|
951
|
+
const parsedOptions = parseContractOptions(command, argumentsList.slice(1));
|
|
952
|
+
if (parsedOptions.issues.length > 0) {
|
|
953
|
+
writeClaimEnvelope(ledgerRepairInvalidRequest(command, parsedOptions.issues));
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
let bytes;
|
|
957
|
+
try {
|
|
958
|
+
bytes = await requestSource(parsedOptions.options.input);
|
|
959
|
+
} catch {
|
|
960
|
+
writeClaimEnvelope(ledgerRepairInvalidRequest(command, [
|
|
961
|
+
issue('/input', 'invalid-value', 'Request input could not be read.'),
|
|
962
|
+
]));
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
const parsedRequest = parseJsonRequest(bytes);
|
|
966
|
+
if (parsedRequest.issues.length > 0) {
|
|
967
|
+
writeClaimEnvelope(ledgerRepairInvalidRequest(command, parsedRequest.issues));
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
// normalizeJsonValue rebuilds the tree into plain objects with every
|
|
971
|
+
// JsonNumber unwrapped, so the exact-member check cannot be slipped past
|
|
972
|
+
// with a `__proto__` member and the number witnesses compare as numbers.
|
|
973
|
+
writeClaimEnvelope(await numberRepair(normalizeJsonValue(parsedRequest.value), {
|
|
974
|
+
ledgerDirectory: parsedOptions.options.ledger,
|
|
975
|
+
autoCommit: parsedOptions.options.autoCommit === true,
|
|
976
|
+
}));
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
|
|
825
980
|
if (command === 'claim-adopt') {
|
|
826
981
|
const parsedOptions = parseContractOptions(command, argumentsList.slice(1));
|
|
827
982
|
if (parsedOptions.issues.length > 0) {
|
|
@@ -1302,29 +1457,34 @@ function parseContractOptions(command, argumentsList) {
|
|
|
1302
1457
|
const options = {};
|
|
1303
1458
|
const issues = [];
|
|
1304
1459
|
const seen = new Set();
|
|
1305
|
-
const valueFlags = command === '
|
|
1306
|
-
? new Map([['--
|
|
1307
|
-
: command === '
|
|
1308
|
-
? new Map([['--ledger', 'ledger'], ['--
|
|
1309
|
-
: command === '
|
|
1310
|
-
|
|
1311
|
-
|| command === '
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
? new Map([['--ledger', 'ledger']])
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1460
|
+
const valueFlags = command === 'version-drift'
|
|
1461
|
+
? new Map([['--skill', 'skill']])
|
|
1462
|
+
: command === 'inspect'
|
|
1463
|
+
? new Map([['--ledger', 'ledger'], ['--id', 'id'], ['--number', 'number'], ['--as-of', 'asOf']])
|
|
1464
|
+
: command === 'report'
|
|
1465
|
+
? new Map([['--ledger', 'ledger'], ['--as-of', 'asOf'], ['--out', 'out'], ['--view', 'view']])
|
|
1466
|
+
: command === 'create' || command === 'transition' || command === 'patch' || command === 'list'
|
|
1467
|
+
|| command === 'parent-migrate'
|
|
1468
|
+
|| command === 'snooze'
|
|
1469
|
+
|| command === 'extensions-provision'
|
|
1470
|
+
|| command === 'publish-claimed' || command === 'publication-read'
|
|
1471
|
+
|| command === 'claim-read' || command === 'claim-acquire' || command === 'claim-renew'
|
|
1472
|
+
|| command === 'claim-release' || command === 'claim-adopt'
|
|
1473
|
+
|| command === 'number-repair'
|
|
1474
|
+
? new Map([['--ledger', 'ledger'], ['--input', 'input']])
|
|
1475
|
+
: command === 'mutation-finalize'
|
|
1476
|
+
? new Map([['--ledger', 'ledger'], ['--recovery-token', 'recoveryToken']])
|
|
1477
|
+
: command === 'claim-merge-verify'
|
|
1478
|
+
? new Map([['--ledger', 'ledger'], ['--base', 'base'], ['--head', 'head']])
|
|
1479
|
+
: command === 'claim-verify'
|
|
1480
|
+
? new Map([['--ledger', 'ledger'], ['--id', 'id']])
|
|
1481
|
+
: command === 'provision' || command === 'claim-capabilities'
|
|
1482
|
+
|| command === 'claim-sync' || command === 'number-repair-proposal'
|
|
1483
|
+
? new Map([['--ledger', 'ledger']])
|
|
1484
|
+
: command === 'mint-id'
|
|
1485
|
+
? new Map([['--date', 'date']])
|
|
1486
|
+
: new Map();
|
|
1487
|
+
// Bare flags carry no value. `--auto-commit` is accepted only on the
|
|
1328
1488
|
// commands whose Git finalization it folds in; `--workbench` only on the one
|
|
1329
1489
|
const bareFlags = AUTO_COMMIT_COMMANDS.has(command)
|
|
1330
1490
|
? new Map([['--auto-commit', 'autoCommit']])
|
|
@@ -1333,13 +1493,17 @@ function parseContractOptions(command, argumentsList) {
|
|
|
1333
1493
|
: command === 'inspect'
|
|
1334
1494
|
? new Map([['--workbench', 'workbench']])
|
|
1335
1495
|
: new Map();
|
|
1336
|
-
const optionalFlags = command === '
|
|
1337
|
-
? new Set(['--
|
|
1338
|
-
: command === '
|
|
1339
|
-
? new Set(['--
|
|
1340
|
-
: command === '
|
|
1341
|
-
? new Set(['--
|
|
1342
|
-
:
|
|
1496
|
+
const optionalFlags = command === 'version-drift'
|
|
1497
|
+
? new Set(['--skill'])
|
|
1498
|
+
: command === 'mint-id'
|
|
1499
|
+
? new Set(['--date'])
|
|
1500
|
+
: command === 'report'
|
|
1501
|
+
? new Set(['--out', '--view'])
|
|
1502
|
+
: command === 'inspect'
|
|
1503
|
+
? new Set(['--id', '--number', '--as-of'])
|
|
1504
|
+
: command === 'claim-verify'
|
|
1505
|
+
? new Set(['--id'])
|
|
1506
|
+
: new Set();
|
|
1343
1507
|
for (let index = 0; index < argumentsList.length; index += 1) {
|
|
1344
1508
|
const argument = argumentsList[index];
|
|
1345
1509
|
if (argument === '--json') {
|
|
@@ -1372,6 +1536,7 @@ function parseContractOptions(command, argumentsList) {
|
|
|
1372
1536
|
continue;
|
|
1373
1537
|
}
|
|
1374
1538
|
seen.add(argument);
|
|
1539
|
+
|
|
1375
1540
|
const value = argumentsList[index + 1];
|
|
1376
1541
|
if (!value || value.startsWith('--')) {
|
|
1377
1542
|
issues.push(argumentIssue(index + 1, 'missing-argument', `Argument ${argument} requires a value.`));
|
|
@@ -1410,6 +1575,23 @@ function parseContractOptions(command, argumentsList) {
|
|
|
1410
1575
|
return { options, issues: sortIssues(issues) };
|
|
1411
1576
|
}
|
|
1412
1577
|
|
|
1578
|
+
async function extensionProvisionCheckpoint(scenario, ledgerDirectory) {
|
|
1579
|
+
if (scenario !== 'extension-provision-pause-before-link') return;
|
|
1580
|
+
const reached = path.join(ledgerDirectory, '.wowbagger-test-extension-provision-reached');
|
|
1581
|
+
const allowed = path.join(ledgerDirectory, '.wowbagger-test-extension-provision-continue');
|
|
1582
|
+
await writeFile(reached, 'reached\n');
|
|
1583
|
+
const deadline = Date.now() + 2_000;
|
|
1584
|
+
while (Date.now() < deadline) {
|
|
1585
|
+
try {
|
|
1586
|
+
await access(allowed);
|
|
1587
|
+
return;
|
|
1588
|
+
} catch (error) {
|
|
1589
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
1590
|
+
}
|
|
1591
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1592
|
+
}
|
|
1593
|
+
throw new Error('Timed out waiting to continue extension declaration publication.');
|
|
1594
|
+
}
|
|
1413
1595
|
// `--auto-commit` is the only bare flag beyond `--json`, and it changes what
|
|
1414
1596
|
// happens after the mutation, never the mutation itself.
|
|
1415
1597
|
function autoCommitted(command, options, run, scenario, operationId = null, targetItemId = null) {
|
|
@@ -1451,6 +1633,7 @@ function writeInvalidRequest(command, issues) {
|
|
|
1451
1633
|
ok: false,
|
|
1452
1634
|
command,
|
|
1453
1635
|
contract_version: MUTATION_CONTRACT_VERSION,
|
|
1636
|
+
...(command === 'parent-migrate' || command === 'snooze' ? { state: 'unchanged' } : {}),
|
|
1454
1637
|
error: outcome.error,
|
|
1455
1638
|
})}\n`);
|
|
1456
1639
|
process.exitCode = outcome.exit;
|
|
@@ -1623,18 +1806,42 @@ async function runClaimCommand(claimCommand, argumentsList) {
|
|
|
1623
1806
|
const operation = CLAIM_OPERATIONS[claimCommand];
|
|
1624
1807
|
if (claimCommand === 'read') {
|
|
1625
1808
|
try {
|
|
1626
|
-
|
|
1809
|
+
// The lock-free read never writes: it projects committed evidence in
|
|
1810
|
+
// memory so an unhydrated worktree still answers with the real claim.
|
|
1811
|
+
const replayed = await hydrateClaimJournalFromHead({
|
|
1812
|
+
ledgerDirectory: parsedOptions.options.ledger,
|
|
1813
|
+
gitCommonDir,
|
|
1814
|
+
namespace,
|
|
1815
|
+
replayed: await replayClaimJournal(journalPath, namespace),
|
|
1816
|
+
persist: false,
|
|
1817
|
+
});
|
|
1627
1818
|
writeClaimEnvelope(operation(replayed.state, request, new Date().toISOString()).envelope);
|
|
1628
|
-
} catch {
|
|
1629
|
-
writeClaimEnvelope(claimStoreUnavailable(
|
|
1819
|
+
} catch (error) {
|
|
1820
|
+
writeClaimEnvelope(claimStoreUnavailable(
|
|
1821
|
+
claimCommand,
|
|
1822
|
+
isClaimJournalCapacityError(error) ? 'journal-capacity-exceeded' : 'claim-store-unreadable',
|
|
1823
|
+
));
|
|
1630
1824
|
}
|
|
1631
1825
|
return;
|
|
1632
1826
|
}
|
|
1633
1827
|
try {
|
|
1634
1828
|
const envelope = await withClaimLock(storePath, async () => {
|
|
1829
|
+
// A claim lifecycle command classifies item reconciliation and refuses
|
|
1830
|
+
// on it, so it must reason from the same writer evidence every other
|
|
1831
|
+
// classifying surface uses; otherwise one command grants a claim on
|
|
1832
|
+
// exactly the state another command refuses to write. It writes no item
|
|
1833
|
+
// byte and no writer-attributed entry, so it reads the identity it
|
|
1834
|
+
// already answers to rather than creating one. Journal and identity are
|
|
1835
|
+
// both store state this command only reads, so neither reading failure
|
|
1836
|
+
// is anything but an unreadable store.
|
|
1635
1837
|
let replayed;
|
|
1838
|
+
let currentWorktreeId;
|
|
1636
1839
|
try {
|
|
1637
1840
|
replayed = await replayClaimJournal(journalPath, namespace);
|
|
1841
|
+
currentWorktreeId = await readWorktreeIdentity({
|
|
1842
|
+
ledgerDirectory: parsedOptions.options.ledger,
|
|
1843
|
+
gitCommonDir,
|
|
1844
|
+
});
|
|
1638
1845
|
} catch (error) {
|
|
1639
1846
|
throw taggedFailure('CLAIM_STORE_UNREADABLE', error);
|
|
1640
1847
|
}
|
|
@@ -1648,6 +1855,8 @@ async function runClaimCommand(claimCommand, argumentsList) {
|
|
|
1648
1855
|
replayed,
|
|
1649
1856
|
physicalNow,
|
|
1650
1857
|
targetItemId: request.item_id,
|
|
1858
|
+
currentWorktreeId,
|
|
1859
|
+
writeLogOnUnsafe: false,
|
|
1651
1860
|
});
|
|
1652
1861
|
} catch (error) {
|
|
1653
1862
|
throw taggedFailure(
|
|
@@ -1657,7 +1866,15 @@ async function runClaimCommand(claimCommand, argumentsList) {
|
|
|
1657
1866
|
error,
|
|
1658
1867
|
);
|
|
1659
1868
|
}
|
|
1660
|
-
|
|
1869
|
+
// Release relinquishes authority; it never extends or grants any. A
|
|
1870
|
+
// barrier that refuses it strands the lease in the worktree least able
|
|
1871
|
+
// to clear the barrier, because no other worktree can take the item over
|
|
1872
|
+
// while the claim is held. Acquire and renew do extend authority against
|
|
1873
|
+
// bytes nobody has ruled legitimate, so they keep refusing. Only this
|
|
1874
|
+
// classification is bypassed: an unresolvable identity, an unreadable
|
|
1875
|
+
// journal, and a clock floor that will not persist all throw before this
|
|
1876
|
+
// point, and the tuple compare-and-swap below still rules on the request.
|
|
1877
|
+
if (reconciled.unsafe && claimCommand !== 'release') {
|
|
1661
1878
|
return claimStoreUnavailable(claimCommand, 'publication-reconciliation-required', {
|
|
1662
1879
|
findings: reconciled.findings,
|
|
1663
1880
|
});
|
|
@@ -1697,6 +1914,10 @@ async function runClaimCommand(claimCommand, argumentsList) {
|
|
|
1697
1914
|
writeClaimEnvelope(claimStoreUnavailable(claimCommand, 'claim-store-locked'));
|
|
1698
1915
|
return;
|
|
1699
1916
|
}
|
|
1917
|
+
if (isClaimJournalCapacityError(error)) {
|
|
1918
|
+
writeClaimEnvelope(claimStoreUnavailable(claimCommand, 'journal-capacity-exceeded'));
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1700
1921
|
if (error?.code === 'CLAIM_STORE_UNREADABLE') {
|
|
1701
1922
|
writeClaimEnvelope(claimStoreUnavailable(claimCommand, 'claim-store-unreadable'));
|
|
1702
1923
|
return;
|
|
@@ -1827,7 +2048,7 @@ function usage(command) {
|
|
|
1827
2048
|
return 'Usage: wowbagger claim-adopt --ledger <dir> --input <request.json> --json';
|
|
1828
2049
|
}
|
|
1829
2050
|
if (command === 'claim-verify') {
|
|
1830
|
-
return 'Usage: wowbagger claim-verify --ledger <dir> --json';
|
|
2051
|
+
return 'Usage: wowbagger claim-verify --ledger <dir> [--id <wb_...>] --json';
|
|
1831
2052
|
}
|
|
1832
2053
|
if (command === 'snooze') {
|
|
1833
2054
|
return 'Usage: wowbagger snooze --ledger <dir> --input <request.json> --json [--auto-commit]';
|
|
@@ -1843,6 +2064,17 @@ function usage(command) {
|
|
|
1843
2064
|
return 'Usage: wowbagger mutation-finalize --ledger <dir> --recovery-token <token> --json';
|
|
1844
2065
|
}
|
|
1845
2066
|
|
|
2067
|
+
if (command === 'version-drift') {
|
|
2068
|
+
return 'Usage: wowbagger version-drift [--skill <path>] --json';
|
|
2069
|
+
}
|
|
2070
|
+
if (command === 'number-repair-proposal') {
|
|
2071
|
+
return 'Usage: wowbagger number-repair-proposal --ledger <dir> --json';
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
if (command === 'number-repair') {
|
|
2075
|
+
return 'Usage: wowbagger number-repair --ledger <dir> --input <json-file|-> --json [--auto-commit]';
|
|
2076
|
+
}
|
|
2077
|
+
|
|
1846
2078
|
if (command === 'claim') {
|
|
1847
2079
|
return 'Usage: wowbagger claim <read|acquire|renew|release|capabilities> [options]';
|
|
1848
2080
|
}
|
|
@@ -1977,6 +2209,20 @@ function commandHelp(command) {
|
|
|
1977
2209
|
].join('\n');
|
|
1978
2210
|
}
|
|
1979
2211
|
|
|
2212
|
+
if (command === 'list') {
|
|
2213
|
+
return [
|
|
2214
|
+
header,
|
|
2215
|
+
'',
|
|
2216
|
+
`${usage(command)}`,
|
|
2217
|
+
'',
|
|
2218
|
+
'Request JSON members: query_version, as_of, filters, sort, page_size, and cursor.',
|
|
2219
|
+
'sort is {field, direction}; direction is ascending or descending.',
|
|
2220
|
+
'Use the returned page.next_cursor as cursor to resume a stable snapshot.',
|
|
2221
|
+
'A changed snapshot returns list-snapshot-changed; restart without cursor.',
|
|
2222
|
+
'',
|
|
2223
|
+
].join('\n');
|
|
2224
|
+
}
|
|
2225
|
+
|
|
1980
2226
|
return [
|
|
1981
2227
|
header,
|
|
1982
2228
|
'',
|