wowbagger 0.1.0-alpha.1

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/LICENSE +201 -0
  3. package/README.md +464 -0
  4. package/adapters/claude-code/entrypoint.js +19 -0
  5. package/adapters/claude-code/wowbagger-adapter.json +25 -0
  6. package/adapters/codex/entrypoint.js +11 -0
  7. package/adapters/codex/wowbagger-adapter.json +25 -0
  8. package/adapters/opencode/entrypoint.js +11 -0
  9. package/adapters/opencode/wowbagger-adapter.json +25 -0
  10. package/bin/wowbagger.js +7 -0
  11. package/package.json +51 -0
  12. package/skills/wowbagger/SKILL.md +136 -0
  13. package/src/adapter/approval.js +135 -0
  14. package/src/adapter/bootstrap.js +43 -0
  15. package/src/adapter/context.js +34 -0
  16. package/src/adapter/core-probe.js +231 -0
  17. package/src/adapter/describe.js +383 -0
  18. package/src/adapter/entrypoint-main.js +335 -0
  19. package/src/adapter/entrypoint-path.js +103 -0
  20. package/src/adapter/handoff.js +124 -0
  21. package/src/adapter/instructions.js +106 -0
  22. package/src/adapter/invoke.js +294 -0
  23. package/src/adapter/limits.js +26 -0
  24. package/src/adapter/manifest.js +93 -0
  25. package/src/adapter/messages.js +15 -0
  26. package/src/adapter/paths.js +88 -0
  27. package/src/adapter/process-outcome.js +1116 -0
  28. package/src/adapter/schema-helpers.js +60 -0
  29. package/src/claim-capabilities.js +54 -0
  30. package/src/claim-coordinator.js +85 -0
  31. package/src/claim-journal.js +236 -0
  32. package/src/claim-operations.js +138 -0
  33. package/src/claim-publication.js +739 -0
  34. package/src/claim-request.js +140 -0
  35. package/src/claim-store.js +198 -0
  36. package/src/cli.js +1130 -0
  37. package/src/dependencies.js +3 -0
  38. package/src/git-reconciliation.js +62 -0
  39. package/src/ledger.js +296 -0
  40. package/src/mint.js +32 -0
  41. package/src/mutation.js +1979 -0
  42. package/src/namespace.js +35 -0
  43. package/src/ready.js +85 -0
  44. package/src/request.js +246 -0
  45. package/src/schema-migration.js +300 -0
  46. package/src/validate.js +1208 -0
package/src/cli.js ADDED
@@ -0,0 +1,1130 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { open } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ import { resolveClaimBackend, resolveWorkClaimCapability } from './claim-capabilities.js';
7
+ import {
8
+ appendClaimEntry,
9
+ claimJournalPath,
10
+ claimReconcileLogPath,
11
+ replayClaimJournal,
12
+ writeReconcileLog,
13
+ } from './claim-journal.js';
14
+ import { claimAcquire, claimRead, claimRelease, claimRenew } from './claim-operations.js';
15
+ import {
16
+ publishClaimed,
17
+ reconcileClaimJournal,
18
+ readPublicationOutcome,
19
+ validatePublicationReadRequest,
20
+ validatePublicationRequest,
21
+ verifyClaimJournal,
22
+ } from './claim-publication.js';
23
+ import { validateClaimRequest } from './claim-request.js';
24
+ import {
25
+ claimStorePath,
26
+ resolveGitCommonDir,
27
+ resolveVerifiedGitCommonDir,
28
+ withClaimLock,
29
+ writeClaimState,
30
+ } from './claim-store.js';
31
+ import { loadLedger } from './ledger.js';
32
+ import {
33
+ createItem,
34
+ inspectItem,
35
+ patchItem,
36
+ transitionItem,
37
+ validateCreateRequest,
38
+ validatePatchRequest,
39
+ validateTransitionRequest,
40
+ } from './mutation.js';
41
+ import { mintId } from './mint.js';
42
+ import { provisionNamespace, readNamespace } from './namespace.js';
43
+ import { normalizeJsonValue, parseJsonRequest, sortIssues } from './request.js';
44
+ import { selectReady } from './ready.js';
45
+ import { isCalendarDate, validateLedger } from './validate.js';
46
+
47
+ const CLAIM_OPERATIONS = { read: claimRead, acquire: claimAcquire, renew: claimRenew, release: claimRelease };
48
+ const MUTATION_CONTRACT_VERSION = 2;
49
+
50
+ const MAX_PUBLICATION_REQUEST_BYTES = 11 * 1024 * 1024;
51
+ const DISTRIBUTION_VERSION = JSON.parse(
52
+ readFileSync(fileURLToPath(new URL('../package.json', import.meta.url)), 'utf8'),
53
+ ).version;
54
+
55
+ const COMMAND_SUMMARIES = {
56
+ validate: 'Validate a ledger and print the single JSON validation result.',
57
+ ready: 'Validate a ledger and print the readiness queue for a date.',
58
+ capabilities: "Describe the backend's capabilities and versioned contract surface.",
59
+ inspect: 'Inspect one ledger item as a lossless raw-byte snapshot.',
60
+ create: 'Create one ledger item through atomic, no-clobber publication.',
61
+ transition: "Transition one item's lifecycle, guarded by lock and compare-and-swap.",
62
+ patch: "Patch an item's number and priority fields, guarded the same way.",
63
+ 'mint-id': 'Mint a canonical item ID.',
64
+ provision: 'Provision the work-claim namespace.',
65
+ claim: 'Work-claim lifecycle operations on the provisioned store.',
66
+ 'publish-claimed': 'Publish claim-protected ledger results (unavailable on an advisory backend).',
67
+ 'claim-verify': 'Reconcile pending and committed claim-protected publications.',
68
+ };
69
+
70
+ const KNOWN_COMMANDS = new Set([
71
+ 'validate',
72
+ 'ready',
73
+ 'capabilities',
74
+ 'inspect',
75
+ 'create',
76
+ 'transition',
77
+ 'patch',
78
+ 'mint-id',
79
+ 'provision',
80
+ 'claim',
81
+ 'publish-claimed',
82
+ 'claim-verify',
83
+ ]);
84
+
85
+ const CLAIM_SUBCOMMAND_SUMMARIES = {
86
+ capabilities: 'Describe the work-claim backend and its coordination scope.',
87
+ read: 'Read the current claims from the provisioned store.',
88
+ acquire: 'Acquire a cooperative work claim.',
89
+ renew: 'Renew an existing work claim.',
90
+ release: 'Release an owned work claim.',
91
+ verify: 'Read a durable claimed-publication outcome.',
92
+ };
93
+
94
+ export async function runCli(argumentsList, { scenario } = {}) {
95
+ const command = argumentsList[0];
96
+
97
+ if (command === '--help' || command === '-h') {
98
+ process.stdout.write(globalHelp());
99
+ return;
100
+ }
101
+
102
+ if (command === '--version' || command === '-v') {
103
+ process.stdout.write(`${DISTRIBUTION_VERSION}\n`);
104
+ return;
105
+ }
106
+
107
+ if (command === 'claim' && argumentsList[1] === '--help') {
108
+ process.stdout.write(commandHelp('claim'));
109
+ return;
110
+ }
111
+
112
+ if (KNOWN_COMMANDS.has(command) && argumentsList[1] === '--help') {
113
+ process.stdout.write(commandHelp(command));
114
+ return;
115
+ }
116
+
117
+ if (command === 'capabilities') {
118
+ const parsedOptions = parseContractOptions(command, argumentsList.slice(1));
119
+ if (parsedOptions.issues.length > 0) {
120
+ writeInvalidRequest(command, parsedOptions.issues);
121
+ return;
122
+ }
123
+ process.stdout.write(`${JSON.stringify(await capabilities(parsedOptions.options.ledger))}\n`);
124
+ return;
125
+ }
126
+
127
+ if (command === 'inspect') {
128
+ const parsedOptions = parseContractOptions(command, argumentsList.slice(1));
129
+ if (parsedOptions.issues.length > 0) {
130
+ writeInvalidRequest(command, parsedOptions.issues);
131
+ return;
132
+ }
133
+ const options = parsedOptions.options;
134
+ const result = await inspectItem(options.ledger, options.id);
135
+ if (result.validation) {
136
+ process.stdout.write(`${JSON.stringify({
137
+ ok: false,
138
+ command,
139
+ contract_version: MUTATION_CONTRACT_VERSION,
140
+ error: {
141
+ code: 'ledger-invalid',
142
+ message: 'The configured ledger is invalid.',
143
+ details: { validation_errors: result.validation.errors },
144
+ },
145
+ })}\n`);
146
+ process.exitCode = 3;
147
+ return;
148
+ }
149
+ if (!result.item) {
150
+ process.stdout.write(`${JSON.stringify({
151
+ ok: false,
152
+ command,
153
+ contract_version: MUTATION_CONTRACT_VERSION,
154
+ error: {
155
+ code: 'item-not-found',
156
+ message: 'The requested item was not found.',
157
+ details: { id: options.id },
158
+ },
159
+ })}\n`);
160
+ process.exitCode = 2;
161
+ return;
162
+ }
163
+ process.stdout.write(`${JSON.stringify({
164
+ ok: true,
165
+ command,
166
+ contract_version: MUTATION_CONTRACT_VERSION,
167
+ result: { item: result.item },
168
+ })}\n`);
169
+ return;
170
+ }
171
+
172
+ if (command === 'create') {
173
+ const parsedOptions = parseContractOptions(command, argumentsList.slice(1));
174
+ if (parsedOptions.issues.length > 0) {
175
+ writeInvalidRequest(command, parsedOptions.issues);
176
+ return;
177
+ }
178
+ let bytes;
179
+ try {
180
+ bytes = await requestSource(parsedOptions.options.input);
181
+ } catch {
182
+ writeInvalidRequest(command, [issue('/input', 'invalid-value', 'Request input could not be read.')]);
183
+ return;
184
+ }
185
+ const parsedRequest = parseJsonRequest(bytes);
186
+ const issues = validateCreateRequest(parsedRequest.value, parsedRequest.issues);
187
+ if (issues.length > 0) {
188
+ writeInvalidRequest(command, issues);
189
+ return;
190
+ }
191
+ writeMutation(command, await createItem(parsedOptions.options.ledger, parsedRequest.value, scenario));
192
+ return;
193
+ }
194
+
195
+ if (command === 'transition') {
196
+ const parsedOptions = parseContractOptions(command, argumentsList.slice(1));
197
+ if (parsedOptions.issues.length > 0) {
198
+ writeInvalidRequest(command, parsedOptions.issues);
199
+ return;
200
+ }
201
+ let bytes;
202
+ try {
203
+ bytes = await requestSource(parsedOptions.options.input);
204
+ } catch {
205
+ writeInvalidRequest(command, [issue('/input', 'invalid-value', 'Request input could not be read.')]);
206
+ return;
207
+ }
208
+ const parsedRequest = parseJsonRequest(bytes);
209
+ const issues = validateTransitionRequest(parsedRequest.value, parsedRequest.issues);
210
+ if (issues.length > 0) {
211
+ writeInvalidRequest(command, issues);
212
+ return;
213
+ }
214
+ writeMutation(command, await transitionItem(parsedOptions.options.ledger, parsedRequest.value, scenario));
215
+ return;
216
+ }
217
+
218
+ if (command === 'mint-id') {
219
+ const parsedOptions = parseContractOptions(command, argumentsList.slice(1));
220
+ if (parsedOptions.issues.length > 0) {
221
+ writeInvalidRequest(command, parsedOptions.issues);
222
+ return;
223
+ }
224
+ const date = parsedOptions.options.date;
225
+ if (date !== undefined && !isCalendarDate(date)) {
226
+ writeInvalidRequest(command, [issue('/arguments', 'invalid-value', 'Argument --date must be an ISO calendar date.')]);
227
+ return;
228
+ }
229
+ process.stdout.write(`${JSON.stringify({
230
+ ok: true,
231
+ command,
232
+ contract_version: MUTATION_CONTRACT_VERSION,
233
+ result: { id: mintId(date ?? null) },
234
+ })}\n`);
235
+ return;
236
+ }
237
+
238
+ if (command === 'patch') {
239
+ const parsedOptions = parseContractOptions(command, argumentsList.slice(1));
240
+ if (parsedOptions.issues.length > 0) {
241
+ writeInvalidRequest(command, parsedOptions.issues);
242
+ return;
243
+ }
244
+ let bytes;
245
+ try {
246
+ bytes = await requestSource(parsedOptions.options.input);
247
+ } catch {
248
+ writeInvalidRequest(command, [issue('/input', 'invalid-value', 'Request input could not be read.')]);
249
+ return;
250
+ }
251
+ const parsedRequest = parseJsonRequest(bytes);
252
+ const issues = validatePatchRequest(parsedRequest.value, parsedRequest.issues);
253
+ if (issues.length > 0) {
254
+ writeInvalidRequest(command, issues);
255
+ return;
256
+ }
257
+ writeMutation(command, await patchItem(parsedOptions.options.ledger, parsedRequest.value, scenario));
258
+ return;
259
+ }
260
+
261
+ if (command === 'claim-verify') {
262
+ const parsedOptions = parseContractOptions(command, argumentsList.slice(1));
263
+ if (parsedOptions.issues.length > 0) {
264
+ writeClaimInvalidRequest(command, parsedOptions.issues);
265
+ return;
266
+ }
267
+ const ledgerDirectory = parsedOptions.options.ledger;
268
+ const gitCommonDir = await resolveVerifiedGitCommonDir(ledgerDirectory);
269
+ const namespace = gitCommonDir ? await readNamespace(ledgerDirectory) : null;
270
+ const capability = resolveWorkClaimCapability({ gitCommonDir, namespace });
271
+ if (!capability.claim_protected_publication) {
272
+ writeClaimEnvelope(claimStoreUnavailable(command,
273
+ gitCommonDir ? 'ledger-namespace-unbound' : 'git-directory-not-found'));
274
+ return;
275
+ }
276
+ writeClaimEnvelope(await verifyClaimJournal({
277
+ ledgerDirectory,
278
+ gitCommonDir,
279
+ namespace,
280
+ }));
281
+ return;
282
+ }
283
+
284
+ if (command === 'publish-claimed') {
285
+ const parsedOptions = parseContractOptions(command, argumentsList.slice(1));
286
+ if (parsedOptions.issues.length > 0) {
287
+ writePublicationInvalidRequest(null, 'The request does not match publish-claimed version 1.', {
288
+ issues: parsedOptions.issues,
289
+ });
290
+ return;
291
+ }
292
+ const gitCommonDir = await resolveVerifiedGitCommonDir(parsedOptions.options.ledger);
293
+ const namespace = gitCommonDir ? await readNamespace(parsedOptions.options.ledger) : null;
294
+ const capability = resolveWorkClaimCapability({ gitCommonDir, namespace });
295
+ if (!capability.claim_protected_publication) {
296
+ writeClaimEnvelope({
297
+ exit: 2,
298
+ stdout: {
299
+ ok: false,
300
+ namespace: 'ledger-publication',
301
+ command: 'publish-claimed',
302
+ contract_version: 1,
303
+ state: 'unchanged',
304
+ error: {
305
+ code: 'capability-unavailable',
306
+ message: 'Claim-protected publication is unavailable on an advisory backend.',
307
+ details: { reason: 'advisory-capability' },
308
+ },
309
+ },
310
+ });
311
+ return;
312
+ }
313
+ let bytes;
314
+ try {
315
+ bytes = await requestSource(parsedOptions.options.input, MAX_PUBLICATION_REQUEST_BYTES);
316
+ } catch {
317
+ writePublicationInvalidRequest(null, 'The request does not match publish-claimed version 1.', {
318
+ field: 'input',
319
+ });
320
+ return;
321
+ }
322
+ const parsedRequest = parseJsonRequest(bytes);
323
+ if (parsedRequest.issues.length > 0) {
324
+ writePublicationInvalidRequest(null, 'The request is not unique-key UTF-8 JSON.', {
325
+ issues: parsedRequest.issues,
326
+ });
327
+ return;
328
+ }
329
+ const request = normalizeJsonValue(parsedRequest.value);
330
+ const invalid = validatePublicationRequest(request);
331
+ if (invalid) {
332
+ writeClaimEnvelope(invalid);
333
+ return;
334
+ }
335
+ writeClaimEnvelope(await publishClaimed({
336
+ ledgerDirectory: parsedOptions.options.ledger,
337
+ gitCommonDir,
338
+ namespace,
339
+ request,
340
+ scenario,
341
+ }));
342
+ return;
343
+ }
344
+
345
+ if (command === 'provision') {
346
+ const parsedOptions = parseContractOptions('provision', argumentsList.slice(1));
347
+ if (parsedOptions.issues.length > 0) {
348
+ writeClaimInvalidRequest(command, parsedOptions.issues);
349
+ return;
350
+ }
351
+ const gitCommonDir = await resolveVerifiedGitCommonDir(parsedOptions.options.ledger);
352
+ if (!gitCommonDir) {
353
+ writeClaimEnvelope(claimStoreUnavailable(command, 'git-directory-not-found'));
354
+ return;
355
+ }
356
+ const { namespace } = await provisionNamespace(parsedOptions.options.ledger);
357
+ writeClaimEnvelope({
358
+ exit: 0,
359
+ stdout: {
360
+ ok: true,
361
+ namespace: 'work-claim',
362
+ command,
363
+ contract_version: 1,
364
+ state: 'committed',
365
+ result: { ledger_namespace: namespace },
366
+ },
367
+ });
368
+ return;
369
+ }
370
+
371
+ if (command === 'claim') {
372
+ const subcommand = argumentsList[1];
373
+
374
+ if (subcommand === 'capabilities') {
375
+ const parsedOptions = parseContractOptions('claim-capabilities', argumentsList.slice(2));
376
+ if (parsedOptions.issues.length > 0) {
377
+ writeClaimInvalidRequest(subcommand, parsedOptions.issues);
378
+ return;
379
+ }
380
+ const gitCommonDir = await resolveVerifiedGitCommonDir(parsedOptions.options.ledger);
381
+ const namespace = gitCommonDir ? await readNamespace(parsedOptions.options.ledger) : null;
382
+ process.stdout.write(`${JSON.stringify({
383
+ ok: true,
384
+ namespace: 'work-claim',
385
+ command: subcommand,
386
+ contract_version: 1,
387
+ result: {
388
+ backend: resolveClaimBackend({ gitCommonDir, namespace }),
389
+ operations: { work_claim: resolveWorkClaimCapability({ gitCommonDir, namespace }) },
390
+ },
391
+ })}\n`);
392
+ return;
393
+ }
394
+
395
+ if (subcommand === 'verify') {
396
+ await runPublicationReadCommand(argumentsList.slice(2));
397
+ return;
398
+ }
399
+
400
+ if (Object.hasOwn(CLAIM_OPERATIONS, subcommand)) {
401
+ await runClaimCommand(subcommand, argumentsList.slice(2));
402
+ return;
403
+ }
404
+
405
+ throw new Error(unknownCommandMessage(subcommand));
406
+ }
407
+
408
+ if (command !== 'validate' && command !== 'ready') {
409
+ throw new Error(unknownCommandMessage(command));
410
+ }
411
+
412
+ const options = parseOptions(command, argumentsList.slice(1));
413
+ const ledger = await loadLedger(options.ledger);
414
+ const validation = validateLedger(ledger);
415
+
416
+ if (command === 'validate' || !validation.valid) {
417
+ process.stdout.write(`${JSON.stringify(validation)}\n`);
418
+ if (!validation.valid) {
419
+ process.exitCode = 1;
420
+ }
421
+ return;
422
+ }
423
+
424
+ const readyIds = selectReady(ledger.items, options.asOf);
425
+
426
+ if (!options.json) {
427
+ const byId = new Map(ledger.items.map((item) => [item.data.id, item.data]));
428
+ const lines = readyIds.map((id) => {
429
+ const data = byId.get(id);
430
+ const number = Object.hasOwn(data, 'number') ? `#${data.number}` : '#-';
431
+ const priority = Object.hasOwn(data, 'priority') ? `pri=${data.priority}` : 'pri=-';
432
+ return `${number} ${priority} ${data.title}\n`;
433
+ });
434
+ process.stdout.write(lines.join(''));
435
+ return;
436
+ }
437
+
438
+ const result = {
439
+ as_of: options.asOf,
440
+ valid: true,
441
+ ready: readyIds,
442
+ };
443
+
444
+ process.stdout.write(`${JSON.stringify(result)}\n`);
445
+ }
446
+
447
+ async function capabilities(ledger) {
448
+ const gitCommonDir = await resolveGitCommonDir(ledger ?? process.cwd());
449
+ return {
450
+ ok: true,
451
+ command: 'capabilities',
452
+ contract_version: MUTATION_CONTRACT_VERSION,
453
+ result: {
454
+ backend: {
455
+ name: 'local-filesystem',
456
+ coordination_scope: 'same-working-copy-cooperative-writers',
457
+ },
458
+ operations: {
459
+ inspect: {
460
+ supported: true,
461
+ write_scope: 'none',
462
+ cas_scope: 'none',
463
+ },
464
+ create: {
465
+ supported: true,
466
+ write_scope: 'single-item',
467
+ cas_scope: 'requested-id-lock',
468
+ publication_visibility: 'atomic-no-clobber-or-fail',
469
+ publication_probe: 'per-ledger-operation',
470
+ },
471
+ transition: {
472
+ supported: true,
473
+ write_scope: 'single-item',
474
+ cas_scope: 'exact-byte-sha256',
475
+ },
476
+ patch: {
477
+ supported: true,
478
+ write_scope: 'single-item',
479
+ cas_scope: 'exact-byte-sha256',
480
+ },
481
+ work_claim: resolveWorkClaimCapability({ gitCommonDir }),
482
+ },
483
+ durability: {
484
+ temporary_file_sync: 'required-before-publication',
485
+ directory_sync: 'best-effort-when-supported',
486
+ post_publication_verification: 'exact-bytes-required',
487
+ power_loss_guarantee: 'none',
488
+ },
489
+ limits: {
490
+ multi_item_atomicity: false,
491
+ cross_clone_coordination: false,
492
+ cross_worktree_coordination: false,
493
+ cross_machine_coordination: false,
494
+ noncooperating_writer_protection: false,
495
+ automatic_stale_lock_breaking: false,
496
+ },
497
+ },
498
+ };
499
+ }
500
+
501
+ function parseOptions(command, argumentsList) {
502
+ const options = {};
503
+
504
+ for (let index = 0; index < argumentsList.length; index += 1) {
505
+ const argument = argumentsList[index];
506
+ if (argument === '--ledger') {
507
+ if (options.ledger) {
508
+ throw new Error(usage(command));
509
+ }
510
+ options.ledger = readOptionValue(command, argument, argumentsList, index);
511
+ index += 1;
512
+ } else if (argument === '--as-of' && command === 'ready') {
513
+ if (options.asOf) {
514
+ throw new Error(usage(command));
515
+ }
516
+ options.asOf = readOptionValue(command, argument, argumentsList, index);
517
+ index += 1;
518
+ } else if (argument === '--id' && command === 'inspect') {
519
+ if (options.id) {
520
+ throw new Error(usage(command));
521
+ }
522
+ options.id = readOptionValue(command, argument, argumentsList, index);
523
+ index += 1;
524
+ } else if (argument === '--input' && command === 'create') {
525
+ if (options.input) {
526
+ throw new Error(usage(command));
527
+ }
528
+ options.input = readOptionValue(command, argument, argumentsList, index);
529
+ index += 1;
530
+ } else if (argument === '--json') {
531
+ if (options.json) {
532
+ throw new Error(usage(command));
533
+ }
534
+ options.json = true;
535
+ } else {
536
+ throw new Error(`Unknown argument: ${argument}`);
537
+ }
538
+ }
539
+
540
+ if (!options.ledger || (!options.json && command !== 'ready')
541
+ || (command === 'inspect' && !options.id)
542
+ || (command === 'create' && !options.input)
543
+ || (command === 'ready' && !options.asOf)) {
544
+ throw new Error(usage(command));
545
+ }
546
+
547
+ if (command === 'ready' && !isCalendarDate(options.asOf)) {
548
+ throw new Error('--as-of must be an ISO calendar date.');
549
+ }
550
+
551
+ return options;
552
+ }
553
+
554
+ function parseContractOptions(command, argumentsList) {
555
+ const options = {};
556
+ const issues = [];
557
+ const seen = new Set();
558
+ const valueFlags = command === 'inspect'
559
+ ? new Map([['--ledger', 'ledger'], ['--id', 'id']])
560
+ : command === 'create' || command === 'transition' || command === 'patch'
561
+ || command === 'publish-claimed' || command === 'publication-read'
562
+ || command === 'claim-read' || command === 'claim-acquire' || command === 'claim-renew'
563
+ || command === 'claim-release'
564
+ ? new Map([['--ledger', 'ledger'], ['--input', 'input']])
565
+ : command === 'provision' || command === 'claim-capabilities' || command === 'claim-verify'
566
+ ? new Map([['--ledger', 'ledger']])
567
+ : command === 'mint-id'
568
+ ? new Map([['--date', 'date']])
569
+ : new Map();
570
+ const optionalFlags = command === 'mint-id' ? new Set(['--date']) : new Set();
571
+ for (let index = 0; index < argumentsList.length; index += 1) {
572
+ const argument = argumentsList[index];
573
+ if (argument === '--json') {
574
+ if (seen.has(argument)) {
575
+ issues.push(argumentIssue(index + 1, 'repeated-argument', 'Argument --json must not be repeated.'));
576
+ }
577
+ seen.add(argument);
578
+ options.json = true;
579
+ continue;
580
+ }
581
+ if (!valueFlags.has(argument)) {
582
+ issues.push(argumentIssue(index + 1, 'unknown-argument', `Argument ${argument} is not recognized.`));
583
+ continue;
584
+ }
585
+ const key = valueFlags.get(argument);
586
+ if (seen.has(argument)) {
587
+ issues.push(argumentIssue(index + 1, 'repeated-argument', `Argument ${argument} must not be repeated.`));
588
+ const repeatedValue = argumentsList[index + 1];
589
+ if (repeatedValue && !repeatedValue.startsWith('--')) {
590
+ index += 1;
591
+ }
592
+ continue;
593
+ }
594
+ seen.add(argument);
595
+ const value = argumentsList[index + 1];
596
+ if (!value || value.startsWith('--')) {
597
+ issues.push(argumentIssue(index + 1, 'missing-argument', `Argument ${argument} requires a value.`));
598
+ continue;
599
+ }
600
+ options[key] = value;
601
+ index += 1;
602
+ }
603
+ for (const [flag] of valueFlags) {
604
+ if (!seen.has(flag) && !optionalFlags.has(flag)) {
605
+ issues.push(argumentIssue(-1, 'missing-argument', `Argument ${flag} is required.`));
606
+ }
607
+ }
608
+ if (!seen.has('--json')) {
609
+ issues.push(argumentIssue(-1, 'missing-argument', 'Argument --json is required.'));
610
+ }
611
+ return { options, issues: sortIssues(issues) };
612
+ }
613
+
614
+ function argumentIssue(index, code, message) {
615
+ return {
616
+ path: index < 0 ? '/arguments' : `/arguments/${index}`,
617
+ code,
618
+ message,
619
+ };
620
+ }
621
+
622
+ function writeInvalidRequest(command, issues) {
623
+ const outcome = {
624
+ ok: false,
625
+ exit: 2,
626
+ state: 'unchanged',
627
+ error: {
628
+ code: 'invalid-request',
629
+ message: `The ${command} request is invalid.`,
630
+ details: { issues },
631
+ },
632
+ };
633
+ if (command === 'create' || command === 'transition' || command === 'patch') {
634
+ writeMutation(command, outcome);
635
+ return;
636
+ }
637
+ process.stdout.write(`${JSON.stringify({
638
+ ok: false,
639
+ command,
640
+ contract_version: MUTATION_CONTRACT_VERSION,
641
+ error: outcome.error,
642
+ })}\n`);
643
+ process.exitCode = outcome.exit;
644
+ }
645
+
646
+ function issue(pathValue, code, message) {
647
+ return { path: pathValue, code, message };
648
+ }
649
+
650
+ function writeMutation(command, outcome) {
651
+ if (outcome.stdout) {
652
+ writeClaimEnvelope(outcome);
653
+ return;
654
+ }
655
+ const envelope = outcome.ok
656
+ ? {
657
+ ok: true,
658
+ command,
659
+ contract_version: MUTATION_CONTRACT_VERSION,
660
+ state: outcome.state,
661
+ result: { item: outcome.item },
662
+ }
663
+ : {
664
+ ok: false,
665
+ command,
666
+ contract_version: MUTATION_CONTRACT_VERSION,
667
+ state: outcome.state,
668
+ error: outcome.error,
669
+ };
670
+ process.stdout.write(`${JSON.stringify(envelope)}\n`);
671
+ process.exitCode = outcome.exit;
672
+ }
673
+
674
+
675
+ async function runPublicationReadCommand(argumentsList) {
676
+ const parsedOptions = parseContractOptions('publication-read', argumentsList);
677
+ if (parsedOptions.issues.length > 0) {
678
+ writePublicationInvalidRequest(null, 'The request does not match ledger-publication.read version 1.', {
679
+ issues: parsedOptions.issues,
680
+ });
681
+ return;
682
+ }
683
+ let bytes;
684
+ try {
685
+ bytes = await requestSource(parsedOptions.options.input);
686
+ } catch {
687
+ writePublicationInvalidRequest(null, 'The request does not match ledger-publication.read version 1.', {
688
+ field: 'input',
689
+ });
690
+ return;
691
+ }
692
+ const parsedRequest = parseJsonRequest(bytes);
693
+ const request = normalizeJsonValue(parsedRequest.value);
694
+ const invalid = parsedRequest.issues.length > 0
695
+ ? {
696
+ exit: 2,
697
+ stdout: {
698
+ ok: false,
699
+ namespace: 'ledger-publication',
700
+ command: 'read',
701
+ contract_version: 1,
702
+ state: 'unchanged',
703
+ error: {
704
+ code: 'invalid-request',
705
+ message: 'The request is not unique-key UTF-8 JSON.',
706
+ details: { issues: parsedRequest.issues },
707
+ },
708
+ },
709
+ }
710
+ : validatePublicationReadRequest(request);
711
+ if (invalid) {
712
+ writeClaimEnvelope(invalid);
713
+ return;
714
+ }
715
+ const gitCommonDir = await resolveVerifiedGitCommonDir(parsedOptions.options.ledger);
716
+ const namespace = gitCommonDir ? await readNamespace(parsedOptions.options.ledger) : null;
717
+ if (!gitCommonDir || request.ledger_namespace !== namespace) {
718
+ writeClaimEnvelope({
719
+ exit: 2,
720
+ stdout: {
721
+ ok: false,
722
+ namespace: 'ledger-publication',
723
+ command: 'read',
724
+ contract_version: 1,
725
+ state: 'unchanged',
726
+ operation_id: request.operation_id,
727
+ error: {
728
+ code: 'ledger-namespace-unbound',
729
+ message: 'The ledger namespace is not provisioned for this endpoint.',
730
+ details: {
731
+ ledger_namespace: request.ledger_namespace,
732
+ item_id: request.item_id,
733
+ },
734
+ },
735
+ },
736
+ });
737
+ return;
738
+ }
739
+ writeClaimEnvelope(await readPublicationOutcome({ gitCommonDir, namespace, request }));
740
+ }
741
+
742
+ async function runClaimCommand(claimCommand, argumentsList) {
743
+ const parsedOptions = parseContractOptions(`claim-${claimCommand}`, argumentsList);
744
+ if (parsedOptions.issues.length > 0) {
745
+ writeClaimInvalidRequest(claimCommand, parsedOptions.issues);
746
+ return;
747
+ }
748
+
749
+ let bytes;
750
+ try {
751
+ bytes = await requestSource(parsedOptions.options.input);
752
+ } catch {
753
+ writeClaimInvalidRequest(claimCommand, [issue('/input', 'invalid-value', 'Request input could not be read.')]);
754
+ return;
755
+ }
756
+ const parsedRequest = parseJsonRequest(bytes);
757
+ if (parsedRequest.issues.length > 0) {
758
+ writeClaimInvalidRequest(claimCommand, parsedRequest.issues);
759
+ return;
760
+ }
761
+ // normalizeJsonValue rebuilds the whole tree into plain objects/arrays with
762
+ // every JsonNumber unwrapped. The rebuild is load-bearing here beyond the
763
+ // schema check: claim-operations.js compares CAS witnesses with
764
+ // isDeepStrictEqual, which treats a null-prototype object as unequal to an
765
+ // Object.prototype one even with identical properties, so an un-rebuilt
766
+ // nested object silently fails every takeover comparison.
767
+ const request = normalizeJsonValue(parsedRequest.value);
768
+ const validationIssues = validateClaimRequest(claimCommand, request);
769
+ if (validationIssues.length > 0) {
770
+ writeClaimInvalidRequest(claimCommand, validationIssues);
771
+ return;
772
+ }
773
+
774
+ const gitCommonDir = await resolveVerifiedGitCommonDir(parsedOptions.options.ledger);
775
+ if (!gitCommonDir) {
776
+ writeClaimEnvelope(claimStoreUnavailable(claimCommand, 'git-directory-not-found'));
777
+ return;
778
+ }
779
+
780
+ const namespace = await readNamespace(parsedOptions.options.ledger);
781
+ if (request.ledger_namespace !== namespace) {
782
+ writeClaimEnvelope({
783
+ exit: 2,
784
+ stdout: {
785
+ ok: false,
786
+ namespace: 'work-claim',
787
+ command: claimCommand,
788
+ contract_version: 1,
789
+ state: 'unchanged',
790
+ error: {
791
+ code: 'ledger-namespace-unbound',
792
+ message: 'The ledger namespace is not provisioned for this endpoint.',
793
+ details: { requested_namespace: request.ledger_namespace, provisioned_namespace: namespace },
794
+ },
795
+ },
796
+ });
797
+ return;
798
+ }
799
+
800
+ const storePath = claimStorePath(gitCommonDir, namespace);
801
+ const journalPath = claimJournalPath(gitCommonDir, namespace);
802
+ const operation = CLAIM_OPERATIONS[claimCommand];
803
+ try {
804
+ const envelope = await withClaimLock(storePath, async () => {
805
+ let replayed;
806
+ try {
807
+ replayed = await replayClaimJournal(journalPath, namespace);
808
+ } catch (error) {
809
+ throw taggedFailure('CLAIM_STORE_UNREADABLE', error);
810
+ }
811
+ const physicalNow = new Date().toISOString();
812
+ let reconciled;
813
+ try {
814
+ reconciled = await reconcileClaimJournal({
815
+ ledgerDirectory: parsedOptions.options.ledger,
816
+ gitCommonDir,
817
+ namespace,
818
+ replayed,
819
+ physicalNow,
820
+ });
821
+ } catch (error) {
822
+ throw taggedFailure(
823
+ error?.code === 'CLOCK_FLOOR_PERSISTENCE_FAILED'
824
+ ? 'CLOCK_FLOOR_PERSISTENCE_FAILED'
825
+ : 'CLAIM_RECONCILIATION_FAILED',
826
+ error,
827
+ );
828
+ }
829
+ if (reconciled.unsafe) {
830
+ return claimStoreUnavailable(claimCommand, 'publication-reconciliation-required', {
831
+ findings: reconciled.findings,
832
+ });
833
+ }
834
+ const applied = operation(reconciled.state, request, physicalNow);
835
+ let persisted;
836
+ try {
837
+ persisted = await appendClaimEntry(journalPath, {
838
+ type: 'claim',
839
+ command: claimCommand,
840
+ physical_now: physicalNow,
841
+ request,
842
+ });
843
+ } catch (error) {
844
+ throw taggedFailure('CLOCK_FLOOR_PERSISTENCE_FAILED', error);
845
+ }
846
+ try {
847
+ const repoRoot = path.dirname(path.resolve(parsedOptions.options.ledger));
848
+ await writeReconcileLog(
849
+ claimReconcileLogPath(repoRoot, namespace),
850
+ namespace,
851
+ [...reconciled.entries, persisted],
852
+ );
853
+ } catch {
854
+ // The tracked reconciliation log is derived. The fsync'd journal
855
+ // already committed the claim and the next operation can rebuild it.
856
+ }
857
+ try {
858
+ await writeClaimState(storePath, applied.state);
859
+ } catch {
860
+ // The snapshot is a rebuildable memo. The fsync'd journal is authoritative.
861
+ }
862
+ return applied.envelope;
863
+ });
864
+ writeClaimEnvelope(envelope);
865
+ } catch (error) {
866
+ if (error?.code === 'CLAIM_LOCK_HELD') {
867
+ writeClaimEnvelope(claimStoreUnavailable(claimCommand, 'claim-store-locked'));
868
+ return;
869
+ }
870
+ if (error?.code === 'CLAIM_STORE_UNREADABLE') {
871
+ writeClaimEnvelope(claimStoreUnavailable(claimCommand, 'claim-store-unreadable'));
872
+ return;
873
+ }
874
+ if (error?.code === 'CLAIM_RECONCILIATION_FAILED') {
875
+ writeClaimEnvelope(claimStoreUnavailable(claimCommand, 'publication-reconciliation-required'));
876
+ return;
877
+ }
878
+ if (error?.code === 'CLOCK_FLOOR_PERSISTENCE_FAILED') {
879
+ writeClaimEnvelope({
880
+ exit: 6,
881
+ stdout: {
882
+ ok: false,
883
+ namespace: 'work-claim',
884
+ command: claimCommand,
885
+ contract_version: 1,
886
+ state: 'unchanged',
887
+ error: {
888
+ code: 'clock-floor-persistence-failed',
889
+ message: 'The authoritative clock floor could not be persisted.',
890
+ details: {},
891
+ },
892
+ },
893
+ });
894
+ return;
895
+ }
896
+ throw error;
897
+ }
898
+ }
899
+
900
+ function taggedFailure(code, cause) {
901
+ const failure = new Error(code);
902
+ failure.code = code;
903
+ failure.cause = cause;
904
+ return failure;
905
+ }
906
+
907
+ function claimStoreUnavailable(command, reason, details = {}) {
908
+ return {
909
+ exit: 6,
910
+ stdout: {
911
+ ok: false,
912
+ namespace: 'work-claim',
913
+ command,
914
+ contract_version: 1,
915
+ state: 'unchanged',
916
+ error: {
917
+ code: 'claim-store-unavailable',
918
+ message: 'The durable claim store is unavailable.',
919
+ details: { reason, ...details },
920
+ },
921
+ },
922
+ };
923
+ }
924
+
925
+ function writeClaimEnvelope(envelope) {
926
+ process.stdout.write(`${JSON.stringify(envelope.stdout)}\n`);
927
+ process.exitCode = envelope.exit;
928
+ }
929
+
930
+ function writeClaimInvalidRequest(claimCommand, issues) {
931
+ writeClaimEnvelope({
932
+ exit: 2,
933
+ stdout: {
934
+ ok: false,
935
+ namespace: 'work-claim',
936
+ command: claimCommand,
937
+ contract_version: 1,
938
+ state: 'unchanged',
939
+ error: {
940
+ code: 'invalid-request',
941
+ message: `The ${claimCommand} request is invalid.`,
942
+ details: { issues },
943
+ },
944
+ },
945
+ });
946
+ }
947
+
948
+ function writePublicationInvalidRequest(operationId, message, details) {
949
+ writeClaimEnvelope({
950
+ exit: 2,
951
+ stdout: {
952
+ ok: false,
953
+ namespace: 'ledger-publication',
954
+ command: 'publish-claimed',
955
+ contract_version: 1,
956
+ state: 'unchanged',
957
+ ...(operationId ? { operation_id: operationId } : {}),
958
+ error: { code: 'invalid-request', message, details },
959
+ },
960
+ });
961
+ }
962
+
963
+ function readOptionValue(command, option, argumentsList, index) {
964
+ const value = argumentsList[index + 1];
965
+ if (!value || value.startsWith('--')) {
966
+ throw new Error(usage(command));
967
+ }
968
+ return value;
969
+ }
970
+
971
+ function usage(command) {
972
+ if (command === 'validate') {
973
+ return 'Usage: wowbagger validate --ledger <dir> --json';
974
+ }
975
+
976
+ if (command === 'ready') {
977
+ return 'Usage: wowbagger ready --ledger <dir> --as-of YYYY-MM-DD [--json]';
978
+ }
979
+
980
+ if (command === 'capabilities') {
981
+ return 'Usage: wowbagger capabilities --json';
982
+ }
983
+
984
+ if (command === 'provision') {
985
+ return 'Usage: wowbagger provision --ledger <dir> --json';
986
+ }
987
+
988
+ if (command === 'publish-claimed') {
989
+ return 'Usage: wowbagger publish-claimed';
990
+ }
991
+
992
+ if (command === 'claim-verify') {
993
+ return 'Usage: wowbagger claim-verify --ledger <dir> --json';
994
+ }
995
+
996
+ if (command === 'claim') {
997
+ return 'Usage: wowbagger claim <read|acquire|renew|release|capabilities> [options]';
998
+ }
999
+
1000
+ if (command === 'inspect') {
1001
+ return 'Usage: wowbagger inspect --ledger <dir> --id <id> --json';
1002
+ }
1003
+
1004
+ if (command === 'create') {
1005
+ return 'Usage: wowbagger create --ledger <dir> --input <json-file|-> --json';
1006
+ }
1007
+
1008
+ if (command === 'transition') {
1009
+ return 'Usage: wowbagger transition --ledger <dir> --input <json-file|-> --json';
1010
+ }
1011
+
1012
+ if (command === 'patch') {
1013
+ return 'Usage: wowbagger patch --ledger <dir> --input <json-file|-> --json';
1014
+ }
1015
+
1016
+ if (command === 'mint-id') {
1017
+ return 'Usage: wowbagger mint-id [--date YYYY-MM-DD] --json';
1018
+ }
1019
+
1020
+ return 'Usage: wowbagger ready --ledger <dir> --as-of YYYY-MM-DD [--json]';
1021
+ }
1022
+
1023
+ function globalHelp() {
1024
+ return [
1025
+ 'wowbagger — standalone Markdown ledger validation, readiness selection, and guarded local mutations.',
1026
+ '',
1027
+ 'Usage:',
1028
+ ' wowbagger <command> [options]',
1029
+ ' wowbagger --help',
1030
+ ' wowbagger --version',
1031
+ '',
1032
+ 'Commands:',
1033
+ ...Object.keys(COMMAND_SUMMARIES).map((name) => (
1034
+ ` ${name.padEnd(12)} ${COMMAND_SUMMARIES[name]}`
1035
+ )),
1036
+ '',
1037
+ "Run 'wowbagger <command> --help' for the usage of a specific command.",
1038
+ '',
1039
+ ].join('\n');
1040
+ }
1041
+
1042
+ function commandHelp(command) {
1043
+ const header = COMMAND_SUMMARIES[command]
1044
+ ? `wowbagger ${command} — ${COMMAND_SUMMARIES[command]}`
1045
+ : `wowbagger ${command}`;
1046
+
1047
+ if (command === 'claim') {
1048
+ return [
1049
+ header,
1050
+ '',
1051
+ 'Usage:',
1052
+ ' wowbagger claim <read|acquire|renew|release|capabilities> [options]',
1053
+ '',
1054
+ 'Subcommands:',
1055
+ ...Object.keys(CLAIM_SUBCOMMAND_SUMMARIES).map((name) => (
1056
+ ` ${name.padEnd(12)} ${CLAIM_SUBCOMMAND_SUMMARIES[name]}`
1057
+ )),
1058
+ '',
1059
+ ].join('\n');
1060
+ }
1061
+
1062
+ return [
1063
+ header,
1064
+ '',
1065
+ `${usage(command)}`,
1066
+ '',
1067
+ ].join('\n');
1068
+ }
1069
+
1070
+ function unknownCommandMessage(command) {
1071
+ const suggestion = closestCommand(command);
1072
+ if (suggestion) {
1073
+ return `Unknown command: ${command}\nDid you mean wowbagger ${suggestion}?`;
1074
+ }
1075
+ return `Unknown command: ${command}. Run 'wowbagger --help' for the command inventory.`;
1076
+ }
1077
+
1078
+ function closestCommand(command) {
1079
+ let best = null;
1080
+ let bestDistance = Infinity;
1081
+ for (const name of KNOWN_COMMANDS) {
1082
+ const distance = editDistance(command, name);
1083
+ if (distance < bestDistance) {
1084
+ bestDistance = distance;
1085
+ best = name;
1086
+ }
1087
+ }
1088
+ return bestDistance <= 2 ? best : null;
1089
+ }
1090
+
1091
+ function editDistance(left, right) {
1092
+ const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
1093
+ for (let i = 1; i <= left.length; i += 1) {
1094
+ const current = [i];
1095
+ for (let j = 1; j <= right.length; j += 1) {
1096
+ const cost = left[i - 1] === right[j - 1] ? 0 : 1;
1097
+ current[j] = Math.min(
1098
+ current[j - 1] + 1,
1099
+ previous[j] + 1,
1100
+ previous[j - 1] + cost,
1101
+ );
1102
+ }
1103
+ previous.splice(0, previous.length, ...current);
1104
+ }
1105
+ return previous[right.length];
1106
+ }
1107
+
1108
+ async function requestSource(input, maxBytes = Number.POSITIVE_INFINITY) {
1109
+ if (input === '-') {
1110
+ const chunks = [];
1111
+ let total = 0;
1112
+ for await (const chunk of process.stdin) {
1113
+ total += chunk.length;
1114
+ if (total > maxBytes) throw new Error('request input exceeds its byte limit');
1115
+ chunks.push(chunk);
1116
+ }
1117
+ return Buffer.concat(chunks, total);
1118
+ }
1119
+
1120
+ const handle = await open(input, 'r');
1121
+ try {
1122
+ const info = await handle.stat();
1123
+ if (!info.isFile() || info.size > maxBytes) {
1124
+ throw new Error('request input exceeds its byte limit');
1125
+ }
1126
+ return await handle.readFile();
1127
+ } finally {
1128
+ await handle.close();
1129
+ }
1130
+ }