wowbagger 0.1.0-alpha.1 → 0.1.0-alpha.2

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.
@@ -18,8 +18,8 @@ so a version mismatch is detectable rather than silent.
18
18
  wowbagger capabilities --json
19
19
  ```
20
20
 
21
- Read `contract_version` from the result. **This skill requires
22
- `contract_version: 2`.**
21
+ Read the top-level `contract_version` from this core response. **This skill
22
+ requires core `contract_version: 2`.**
23
23
 
24
24
  - Command not found → the core is not installed. Tell the user, point them at
25
25
  <https://github.com/lstutzman/wowbagger>, and stop. Do not fall back to
@@ -67,6 +67,9 @@ wowbagger transition --ledger <dir> --input request.json --json
67
67
 
68
68
  - `create` publishes only a caller-supplied canonical ID, atomically and
69
69
  no-clobber. It will not invent an ID for you.
70
+ - `create` starts an empty ledger on schema version 2 and returns the selected
71
+ version at `result.item.core.schema_version`. A non-empty schema-version-1
72
+ ledger stays on version 1 until its complete ledger is migrated.
70
73
  - `transition` changes **one** item. If the change would require touching a
71
74
  dependent or a child, it refuses. That refusal is correct — make it a
72
75
  reviewable multi-file Git change instead of forcing it.
@@ -77,6 +80,7 @@ wowbagger transition --ledger <dir> --input request.json --json
77
80
  ## Work claims are merge-coordinated
78
81
 
79
82
  ```sh
83
+ wowbagger claim capabilities --ledger <dir> --json
80
84
  wowbagger provision --ledger <dir> --json
81
85
  wowbagger claim capabilities --ledger <dir> --json
82
86
  wowbagger claim read|acquire|renew|release --ledger <dir> --input request.json --json
@@ -94,25 +98,35 @@ It is not exclusive coordination. Direct filesystem writes, hostile processes,
94
98
  other clones, and alternate tools can bypass the protocol. Never present a
95
99
  claim as a lock or build a dispatch loop that requires exclusive ownership.
96
100
 
101
+ An item stays in `backlog` while claimed work runs. The active claim is the work-in-flight signal.
102
+ Do not use legacy `transition` to set `in-progress` after acquiring a claim; it
103
+ correctly refuses with `active-claim-write-refused`.
104
+
97
105
  Use the claimed write path as one complete loop:
98
106
 
99
- 1. Run `provision` once for the ledger. Keep its `ledger_namespace`.
100
- 2. Run `claim capabilities --ledger <dir> --json`. Stop if the namespace is
101
- absent or the mode is not `merge-coordinated`.
102
- 3. Read the current claim record. Acquire with its observed state in
107
+ 1. Before provisioning, run `claim capabilities --ledger <dir> --json`. Require
108
+ `result.operations.work_claim.supported: true`; `false` means the ledger is
109
+ not in an accessible Git checkout. Stop before mutation.
110
+ 2. Run `provision` once for the ledger. Keep its `ledger_namespace`.
111
+ 3. Run `claim capabilities --ledger <dir> --json` again. Require
112
+ `result.operations.work_claim.api_version: 1`. Do not compare the claim
113
+ response's top-level `contract_version` with the core version; it is the
114
+ legacy claim-envelope marker. Stop if the namespace is absent or the mode is
115
+ not `merge-coordinated`.
116
+ 4. Read the current claim record. Acquire with its observed state in
103
117
  `expected`; keep the returned `owner_id`, `epoch`, and expiry as the fence.
104
- 4. Renew before the lease expires if the work continues.
105
- 5. Inspect the item again. Build the complete desired item bytes.
106
- 6. Call `publish-claimed` with a unique operation ID, the exact inspected
118
+ 5. Renew before the lease expires if the work continues.
119
+ 6. Inspect the item again. Build the complete desired item bytes.
120
+ 7. Call `publish-claimed` with a unique operation ID, the exact inspected
107
121
  revision, the candidate bytes and digest, and the active claim fence. Never
108
122
  retry with only the operation ID; retry the complete request.
109
- 7. Commit the item change, or merge the worker commit into the coordinating
123
+ 8. Commit the item change, or merge the worker commit into the coordinating
110
124
  branch.
111
- 8. Run `claim-verify` after the commit or merge. It finalizes the Git outcome,
125
+ 9. Run `claim-verify` after the commit or merge. It finalizes the Git outcome,
112
126
  repairs response-loss cases, and reports later revision drift. Run it again
113
127
  before the next fenced operation if the prior outcome was not final.
114
- 9. Release the claim with its current observed state.
115
- 10. Run `validate` and show the resulting diff.
128
+ 10. Release the claim with its current observed state.
129
+ 11. Run `validate` and show the resulting diff.
116
130
 
117
131
  Legacy `create` refuses an ID with claim history. Legacy `transition` refuses an
118
132
  item with an active claim. Do not bypass those refusals. Read
@@ -21,8 +21,8 @@ export function claimJournalPath(commonDir, namespace) {
21
21
  return path.join(commonDir, 'wowbagger', namespace, 'journal.ndjson');
22
22
  }
23
23
 
24
- export function claimReconcileLogPath(repoRoot, namespace) {
25
- return path.join(repoRoot, 'wowbagger', `reconcile-${namespace}.md`);
24
+ export function claimReconcileLogPath(ledgerDirectory, namespace) {
25
+ return path.join(ledgerDirectory, '.wowbagger', `reconcile-${namespace}.md`);
26
26
  }
27
27
 
28
28
  export async function appendClaimEntry(journalPath, entry) {
@@ -477,7 +477,7 @@ export async function reconcileClaimJournal({
477
477
  }
478
478
 
479
479
  await writeReconcileLog(
480
- claimReconcileLogPath(path.dirname(path.resolve(ledgerDirectory)), namespace),
480
+ claimReconcileLogPath(path.resolve(ledgerDirectory), namespace),
481
481
  namespace,
482
482
  entries,
483
483
  );
@@ -495,6 +495,28 @@ export async function reconcileClaimJournal({
495
495
  };
496
496
  }
497
497
 
498
+ function publicationStatuses(entries) {
499
+ const finalizations = new Map(entries
500
+ .filter((entry) => entry.type === 'publish-finalization')
501
+ .map((entry) => [`${entry.operation_id}\0${entry.item_id}`, entry]));
502
+ return entries
503
+ .filter((entry) => (
504
+ entry.type === 'publish-final'
505
+ && entry.outcome?.stdout?.state === 'committed'
506
+ ))
507
+ .map((entry) => {
508
+ const finalization = finalizations.get(`${entry.operation_id}\0${entry.item_id}`);
509
+ return {
510
+ operation_id: entry.operation_id,
511
+ item_id: entry.item_id,
512
+ committed_revision: entry.outcome.stdout.result.committed_revision,
513
+ git_finalized: finalization !== undefined,
514
+ git_commit: finalization?.git_commit ?? null,
515
+ };
516
+ });
517
+ }
518
+
519
+
498
520
  export async function verifyClaimJournal({ ledgerDirectory, gitCommonDir, namespace }) {
499
521
  const storePath = claimStorePath(gitCommonDir, namespace);
500
522
  const journalPath = claimJournalPath(gitCommonDir, namespace);
@@ -519,6 +541,7 @@ export async function verifyClaimJournal({ ledgerDirectory, gitCommonDir, namesp
519
541
  ledger_namespace: namespace,
520
542
  observed_at: reconciled.observedAt,
521
543
  findings: reconciled.findings,
544
+ publications: publicationStatuses(reconciled.entries),
522
545
  },
523
546
  },
524
547
  };
@@ -560,8 +583,11 @@ async function persistTerminal(entries, journalPath, ledgerDirectory, namespace,
560
583
  outcome,
561
584
  });
562
585
  entries.push(terminal);
563
- const repoRoot = path.dirname(path.resolve(ledgerDirectory));
564
- await writeReconcileLog(claimReconcileLogPath(repoRoot, namespace), namespace, entries);
586
+ await writeReconcileLog(
587
+ claimReconcileLogPath(path.resolve(ledgerDirectory), namespace),
588
+ namespace,
589
+ entries,
590
+ );
565
591
  try {
566
592
  await writeClaimState(storePath, state);
567
593
  } catch {
package/src/cli.js CHANGED
@@ -55,7 +55,7 @@ const DISTRIBUTION_VERSION = JSON.parse(
55
55
  const COMMAND_SUMMARIES = {
56
56
  validate: 'Validate a ledger and print the single JSON validation result.',
57
57
  ready: 'Validate a ledger and print the readiness queue for a date.',
58
- capabilities: "Describe the backend's capabilities and versioned contract surface.",
58
+ capabilities: 'Describe the core contract and unbound default claim profile.',
59
59
  inspect: 'Inspect one ledger item as a lossless raw-byte snapshot.',
60
60
  create: 'Create one ledger item through atomic, no-clobber publication.',
61
61
  transition: "Transition one item's lifecycle, guarded by lock and compare-and-swap.",
@@ -63,7 +63,7 @@ const COMMAND_SUMMARIES = {
63
63
  'mint-id': 'Mint a canonical item ID.',
64
64
  provision: 'Provision the work-claim namespace.',
65
65
  claim: 'Work-claim lifecycle operations on the provisioned store.',
66
- 'publish-claimed': 'Publish claim-protected ledger results (unavailable on an advisory backend).',
66
+ 'publish-claimed': 'Publish ledger results when its claim profile enables protected publication.',
67
67
  'claim-verify': 'Reconcile pending and committed claim-protected publications.',
68
68
  };
69
69
 
@@ -83,7 +83,7 @@ const KNOWN_COMMANDS = new Set([
83
83
  ]);
84
84
 
85
85
  const CLAIM_SUBCOMMAND_SUMMARIES = {
86
- capabilities: 'Describe the work-claim backend and its coordination scope.',
86
+ capabilities: "Describe the provisioned ledger's work-claim profile.",
87
87
  read: 'Read the current claims from the provisioned store.',
88
88
  acquire: 'Acquire a cooperative work claim.',
89
89
  renew: 'Renew an existing work claim.',
@@ -844,9 +844,8 @@ async function runClaimCommand(claimCommand, argumentsList) {
844
844
  throw taggedFailure('CLOCK_FLOOR_PERSISTENCE_FAILED', error);
845
845
  }
846
846
  try {
847
- const repoRoot = path.dirname(path.resolve(parsedOptions.options.ledger));
848
847
  await writeReconcileLog(
849
- claimReconcileLogPath(repoRoot, namespace),
848
+ claimReconcileLogPath(path.resolve(parsedOptions.options.ledger), namespace),
850
849
  namespace,
851
850
  [...reconciled.entries, persisted],
852
851
  );
@@ -1056,6 +1055,48 @@ function commandHelp(command) {
1056
1055
  ` ${name.padEnd(12)} ${CLAIM_SUBCOMMAND_SUMMARIES[name]}`
1057
1056
  )),
1058
1057
  '',
1058
+ 'Use claim capabilities --ledger <dir> --json to gate work on one provisioned ledger.',
1059
+ 'The namespace and backend members identify the work-claim capability context.',
1060
+ 'Use operations.work_claim.api_version to negotiate the work-claim API.',
1061
+ 'The top-level claim contract_version is a legacy envelope marker.',
1062
+ '',
1063
+ ].join('\n');
1064
+ }
1065
+
1066
+ if (command === 'capabilities') {
1067
+ return [
1068
+ header,
1069
+ '',
1070
+ `${usage(command)}`,
1071
+ '',
1072
+ 'Use contract_version to negotiate the core contract.',
1073
+ 'Use operations.work_claim.api_version to negotiate the work-claim API.',
1074
+ 'Use claim capabilities --ledger <dir> --json to gate claimed work for one ledger.',
1075
+ '',
1076
+ ].join('\n');
1077
+ }
1078
+
1079
+ if (command === 'provision') {
1080
+ return [
1081
+ header,
1082
+ '',
1083
+ `${usage(command)}`,
1084
+ '',
1085
+ 'Requires a Git checkout; the namespace and claim journal use its shared Git directory.',
1086
+ 'Preflight with claim capabilities --ledger <dir> --json.',
1087
+ 'Require result.operations.work_claim.supported: true before provisioning.',
1088
+ '',
1089
+ ].join('\n');
1090
+ }
1091
+
1092
+ if (command === 'publish-claimed') {
1093
+ return [
1094
+ header,
1095
+ '',
1096
+ `${usage(command)}`,
1097
+ '',
1098
+ 'Requires the ledger-specific claim capability claim_protected_publication: true.',
1099
+ '',
1059
1100
  ].join('\n');
1060
1101
  }
1061
1102
 
@@ -12,7 +12,7 @@ const GIT_ENVIRONMENT = Object.fromEntries(
12
12
  export async function readGitHeadLedger(ledgerDirectory) {
13
13
  const root = (await gitText(ledgerDirectory, ['rev-parse', '--show-toplevel'])).trim();
14
14
  const relativeLedger = path.relative(root, await realpath(ledgerDirectory));
15
- if (relativeLedger === '' || relativeLedger.startsWith(`..${path.sep}`) || path.isAbsolute(relativeLedger)) {
15
+ if (relativeLedger.startsWith(`..${path.sep}`) || path.isAbsolute(relativeLedger)) {
16
16
  throw new Error(`ledger is outside the git worktree: ${relativeLedger}`);
17
17
  }
18
18
  let commit;
@@ -23,12 +23,16 @@ export async function readGitHeadLedger(ledgerDirectory) {
23
23
  throw error;
24
24
  }
25
25
  const gitLedger = toGitPath(relativeLedger);
26
- const listing = await gitBuffer(root, [
27
- 'ls-tree', '-r', '-z', '--name-only', 'HEAD', '--', gitLedger,
28
- ]);
29
- const prefix = `${gitLedger}/`;
26
+ const treeArguments = ['ls-tree', '-r', '-z', '--name-only', 'HEAD'];
27
+ if (gitLedger !== '') treeArguments.push('--', gitLedger);
28
+ const listing = await gitBuffer(root, treeArguments);
29
+ const prefix = gitLedger === '' ? '' : `${gitLedger}/`;
30
30
  const files = listing.toString('utf8').split('\0')
31
- .filter((name) => name.startsWith(prefix) && name.endsWith('.md'));
31
+ .filter((name) => (
32
+ name.startsWith(prefix)
33
+ && !name.slice(prefix.length).startsWith('.wowbagger/')
34
+ && name.endsWith('.md')
35
+ ));
32
36
  const items = new Map();
33
37
  for (const file of files) {
34
38
  const bytes = await gitBuffer(root, ['show', `HEAD:${file}`]);
package/src/ledger.js CHANGED
@@ -140,6 +140,7 @@ async function collectMarkdownFiles(root, directory, fileSystem) {
140
140
  const errors = [];
141
141
 
142
142
  for (const entry of entries.sort((left, right) => compareText(left.name, right.name))) {
143
+ if (directory === root && entry.name === '.wowbagger') continue;
143
144
  const entryPath = path.join(directory, entry.name);
144
145
  let entryType = entry;
145
146
 
package/src/mutation.js CHANGED
@@ -277,7 +277,7 @@ async function createItemUnfenced(ledgerDirectory, request, scenario) {
277
277
  return await finishUncommitted(mutationError('path-collision', 'The default item path is occupied by a different item.', 'unchanged', 4, details));
278
278
  }
279
279
 
280
- const schemaVersion = current.ledger.items[0]?.data.schema_version ?? 1;
280
+ const schemaVersion = current.ledger.items[0]?.data.schema_version ?? 2;
281
281
  const bytes = createCandidateSource(request, schemaVersion);
282
282
  const candidateValidation = validateSerializedCandidate(
283
283
  current.ledger,
@@ -52,7 +52,7 @@ export async function migrateSchema2(ledgerDirectory, { apply = false, onItem =
52
52
  if (inputValidation.valid && ledger.items.length === 0) {
53
53
  throw new SchemaMigrationError(
54
54
  'empty-ledger',
55
- 'An empty ledger has no schema_version stamp to migrate and still defaults to schema version 1. No files were changed.',
55
+ 'An empty ledger has no schema_version stamp to migrate; its first create defaults to schema version 2. No files were changed.',
56
56
  );
57
57
  }
58
58
  if (ledger.items.length > 0 && schemaVersions.size === 1 && schemaVersions.has(2)) {