wowbagger 0.1.0-alpha.13 → 0.1.0-alpha.17

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/src/cli.js CHANGED
@@ -38,6 +38,14 @@ import {
38
38
  import { readGitTreeFile, readGitTreeLedger } from './git-reconciliation.js';
39
39
  import { finalizeFromRecoveryToken, withAutoCommit } from './git-autocommit.js';
40
40
  import { loadLedger } from './ledger.js';
41
+ import {
42
+ finalizeNumberRepairCommit,
43
+ isNumberRepairRecoveryToken,
44
+ ledgerRepairInvalidRequest,
45
+ numberRepair,
46
+ numberRepairProposal,
47
+ } from './ledger-repair.js';
48
+ import { inspectVersionDrift } from './version-drift.js';
41
49
  import {
42
50
  assertReportOutputOutsideLedger,
43
51
  buildReportModel,
@@ -93,6 +101,7 @@ const AUTO_COMMIT_COMMANDS = new Set([
93
101
  'snooze',
94
102
  'patch',
95
103
  'publish-claimed',
104
+ 'number-repair',
96
105
  ]);
97
106
  // The report failures this command states. A code outside the set is an
98
107
  // unexpected condition, never a classification the command can pass through.
@@ -130,6 +139,8 @@ const COMMAND_SUMMARIES = {
130
139
  'claim-sync': 'Import committed adoption evidence into the local claim journal.',
131
140
  'claim-adopt': 'Rule a committed out-of-protocol item revision legitimate.',
132
141
  'mutation-finalize': 'Complete the Git commit an auto-commit mutation could not establish.',
142
+ 'version-drift': 'Check installed skill and running core version compatibility.',
143
+ 'number-repair': 'Apply a proposed duplicate-number repair to a ledger the mutation gate refuses.',
133
144
  provision: 'Provision a Git-backed work-claim namespace.',
134
145
  claim: 'Read and coordinate work claims for one ledger.',
135
146
  'claim-verify': 'Reconcile the durable claim journal with ledger bytes and Git.',
@@ -157,6 +168,8 @@ const KNOWN_COMMANDS = new Set([
157
168
  'claim-sync',
158
169
  'claim-adopt',
159
170
  'mutation-finalize',
171
+ 'version-drift',
172
+ 'number-repair',
160
173
  ]);
161
174
 
162
175
  const CLAIM_SUBCOMMAND_SUMMARIES = {
@@ -191,6 +204,22 @@ export async function runCli(argumentsList, { scenario } = {}) {
191
204
  return;
192
205
  }
193
206
 
207
+ if (command === 'version-drift') {
208
+ const parsedOptions = parseContractOptions(command, argumentsList.slice(1));
209
+ if (parsedOptions.issues.length > 0) {
210
+ writeInvalidRequest(command, parsedOptions.issues);
211
+ return;
212
+ }
213
+ writeClaimEnvelope(await inspectVersionDrift({
214
+ skillPath: parsedOptions.options.skill
215
+ ?? fileURLToPath(new URL('../skills/wowbagger/SKILL.md', import.meta.url)),
216
+ packagePath: fileURLToPath(new URL('../package.json', import.meta.url)),
217
+ runningDistribution: DISTRIBUTION_VERSION,
218
+ runningContractVersion: 5,
219
+ }));
220
+ return;
221
+ }
222
+
194
223
  if (command === 'capabilities') {
195
224
  const parsedOptions = parseContractOptions(command, argumentsList.slice(1));
196
225
  if (parsedOptions.issues.length > 0) {
@@ -828,6 +857,13 @@ export async function runCli(argumentsList, { scenario } = {}) {
828
857
  writeClaimInvalidRequest(command, parsedOptions.issues);
829
858
  return;
830
859
  }
860
+ if (isNumberRepairRecoveryToken(parsedOptions.options.recoveryToken)) {
861
+ writeClaimEnvelope(await finalizeNumberRepairCommit({
862
+ ledgerDirectory: parsedOptions.options.ledger,
863
+ token: parsedOptions.options.recoveryToken,
864
+ }));
865
+ return;
866
+ }
831
867
  writeClaimEnvelope(await finalizeFromRecoveryToken({
832
868
  ledgerDirectory: parsedOptions.options.ledger,
833
869
  token: parsedOptions.options.recoveryToken,
@@ -835,6 +871,47 @@ export async function runCli(argumentsList, { scenario } = {}) {
835
871
  return;
836
872
  }
837
873
 
874
+ // The proposal is request-free and read-only: the ledger is its whole input.
875
+ if (command === 'number-repair-proposal') {
876
+ const parsedOptions = parseContractOptions(command, argumentsList.slice(1));
877
+ if (parsedOptions.issues.length > 0) {
878
+ writeClaimEnvelope(ledgerRepairInvalidRequest(command, parsedOptions.issues));
879
+ return;
880
+ }
881
+ writeClaimEnvelope(await numberRepairProposal(parsedOptions.options.ledger));
882
+ return;
883
+ }
884
+
885
+ if (command === 'number-repair') {
886
+ const parsedOptions = parseContractOptions(command, argumentsList.slice(1));
887
+ if (parsedOptions.issues.length > 0) {
888
+ writeClaimEnvelope(ledgerRepairInvalidRequest(command, parsedOptions.issues));
889
+ return;
890
+ }
891
+ let bytes;
892
+ try {
893
+ bytes = await requestSource(parsedOptions.options.input);
894
+ } catch {
895
+ writeClaimEnvelope(ledgerRepairInvalidRequest(command, [
896
+ issue('/input', 'invalid-value', 'Request input could not be read.'),
897
+ ]));
898
+ return;
899
+ }
900
+ const parsedRequest = parseJsonRequest(bytes);
901
+ if (parsedRequest.issues.length > 0) {
902
+ writeClaimEnvelope(ledgerRepairInvalidRequest(command, parsedRequest.issues));
903
+ return;
904
+ }
905
+ // normalizeJsonValue rebuilds the tree into plain objects with every
906
+ // JsonNumber unwrapped, so the exact-member check cannot be slipped past
907
+ // with a `__proto__` member and the number witnesses compare as numbers.
908
+ writeClaimEnvelope(await numberRepair(normalizeJsonValue(parsedRequest.value), {
909
+ ledgerDirectory: parsedOptions.options.ledger,
910
+ autoCommit: parsedOptions.options.autoCommit === true,
911
+ }));
912
+ return;
913
+ }
914
+
838
915
  if (command === 'claim-adopt') {
839
916
  const parsedOptions = parseContractOptions(command, argumentsList.slice(1));
840
917
  if (parsedOptions.issues.length > 0) {
@@ -1315,28 +1392,31 @@ function parseContractOptions(command, argumentsList) {
1315
1392
  const options = {};
1316
1393
  const issues = [];
1317
1394
  const seen = new Set();
1318
- const valueFlags = command === 'inspect'
1319
- ? new Map([['--ledger', 'ledger'], ['--id', 'id'], ['--number', 'number'], ['--as-of', 'asOf']])
1320
- : command === 'report'
1321
- ? new Map([['--ledger', 'ledger'], ['--as-of', 'asOf'], ['--out', 'out'], ['--view', 'view']])
1322
- : command === 'create' || command === 'transition' || command === 'patch' || command === 'list'
1323
- || command === 'parent-migrate'
1324
- || command === 'snooze'
1325
- || command === 'extensions-provision'
1326
- || command === 'publish-claimed' || command === 'publication-read'
1327
- || command === 'claim-read' || command === 'claim-acquire' || command === 'claim-renew'
1328
- || command === 'claim-release' || command === 'claim-adopt'
1329
- ? new Map([['--ledger', 'ledger'], ['--input', 'input']])
1330
- : command === 'mutation-finalize'
1331
- ? new Map([['--ledger', 'ledger'], ['--recovery-token', 'recoveryToken']])
1332
- : command === 'claim-merge-verify'
1333
- ? new Map([['--ledger', 'ledger'], ['--base', 'base'], ['--head', 'head']])
1334
- : command === 'provision' || command === 'claim-capabilities' || command === 'claim-verify'
1335
- || command === 'claim-sync'
1336
- ? new Map([['--ledger', 'ledger']])
1337
- : command === 'mint-id'
1338
- ? new Map([['--date', 'date']])
1339
- : new Map();
1395
+ const valueFlags = command === 'version-drift'
1396
+ ? new Map([['--skill', 'skill']])
1397
+ : command === 'inspect'
1398
+ ? new Map([['--ledger', 'ledger'], ['--id', 'id'], ['--number', 'number'], ['--as-of', 'asOf']])
1399
+ : command === 'report'
1400
+ ? new Map([['--ledger', 'ledger'], ['--as-of', 'asOf'], ['--out', 'out'], ['--view', 'view']])
1401
+ : command === 'create' || command === 'transition' || command === 'patch' || command === 'list'
1402
+ || command === 'parent-migrate'
1403
+ || command === 'snooze'
1404
+ || command === 'extensions-provision'
1405
+ || command === 'publish-claimed' || command === 'publication-read'
1406
+ || command === 'claim-read' || command === 'claim-acquire' || command === 'claim-renew'
1407
+ || command === 'claim-release' || command === 'claim-adopt'
1408
+ || command === 'number-repair'
1409
+ ? new Map([['--ledger', 'ledger'], ['--input', 'input']])
1410
+ : command === 'mutation-finalize'
1411
+ ? new Map([['--ledger', 'ledger'], ['--recovery-token', 'recoveryToken']])
1412
+ : command === 'claim-merge-verify'
1413
+ ? new Map([['--ledger', 'ledger'], ['--base', 'base'], ['--head', 'head']])
1414
+ : command === 'provision' || command === 'claim-capabilities' || command === 'claim-verify'
1415
+ || command === 'claim-sync' || command === 'number-repair-proposal'
1416
+ ? new Map([['--ledger', 'ledger']])
1417
+ : command === 'mint-id'
1418
+ ? new Map([['--date', 'date']])
1419
+ : new Map();
1340
1420
  // Bare flags carry no value. `--auto-commit` is accepted only on the
1341
1421
  // commands whose Git finalization it folds in; `--workbench` only on the one
1342
1422
  const bareFlags = AUTO_COMMIT_COMMANDS.has(command)
@@ -1346,13 +1426,15 @@ function parseContractOptions(command, argumentsList) {
1346
1426
  : command === 'inspect'
1347
1427
  ? new Map([['--workbench', 'workbench']])
1348
1428
  : new Map();
1349
- const optionalFlags = command === 'mint-id'
1350
- ? new Set(['--date'])
1351
- : command === 'report'
1352
- ? new Set(['--out', '--view'])
1353
- : command === 'inspect'
1354
- ? new Set(['--id', '--number', '--as-of'])
1355
- : new Set();
1429
+ const optionalFlags = command === 'version-drift'
1430
+ ? new Set(['--skill'])
1431
+ : command === 'mint-id'
1432
+ ? new Set(['--date'])
1433
+ : command === 'report'
1434
+ ? new Set(['--out', '--view'])
1435
+ : command === 'inspect'
1436
+ ? new Set(['--id', '--number', '--as-of'])
1437
+ : new Set();
1356
1438
  for (let index = 0; index < argumentsList.length; index += 1) {
1357
1439
  const argument = argumentsList[index];
1358
1440
  if (argument === '--json') {
@@ -1654,20 +1736,18 @@ async function runClaimCommand(claimCommand, argumentsList) {
1654
1736
  }
1655
1737
  try {
1656
1738
  const envelope = await withClaimLock(storePath, async () => {
1657
- let replayed;
1658
- try {
1659
- replayed = await replayClaimJournal(journalPath, namespace);
1660
- } catch (error) {
1661
- throw taggedFailure('CLAIM_STORE_UNREADABLE', error);
1662
- }
1663
1739
  // A claim lifecycle command classifies item reconciliation and refuses
1664
1740
  // on it, so it must reason from the same writer evidence every other
1665
1741
  // classifying surface uses; otherwise one command grants a claim on
1666
1742
  // exactly the state another command refuses to write. It writes no item
1667
1743
  // byte and no writer-attributed entry, so it reads the identity it
1668
- // already answers to rather than creating one.
1744
+ // already answers to rather than creating one. Journal and identity are
1745
+ // both store state this command only reads, so neither reading failure
1746
+ // is anything but an unreadable store.
1747
+ let replayed;
1669
1748
  let currentWorktreeId;
1670
1749
  try {
1750
+ replayed = await replayClaimJournal(journalPath, namespace);
1671
1751
  currentWorktreeId = await readWorktreeIdentity({
1672
1752
  ledgerDirectory: parsedOptions.options.ledger,
1673
1753
  gitCommonDir,
@@ -1890,6 +1970,17 @@ function usage(command) {
1890
1970
  return 'Usage: wowbagger mutation-finalize --ledger <dir> --recovery-token <token> --json';
1891
1971
  }
1892
1972
 
1973
+ if (command === 'version-drift') {
1974
+ return 'Usage: wowbagger version-drift [--skill <path>] --json';
1975
+ }
1976
+ if (command === 'number-repair-proposal') {
1977
+ return 'Usage: wowbagger number-repair-proposal --ledger <dir> --json';
1978
+ }
1979
+
1980
+ if (command === 'number-repair') {
1981
+ return 'Usage: wowbagger number-repair --ledger <dir> --input <json-file|-> --json [--auto-commit]';
1982
+ }
1983
+
1893
1984
  if (command === 'claim') {
1894
1985
  return 'Usage: wowbagger claim <read|acquire|renew|release|capabilities> [options]';
1895
1986
  }
@@ -119,17 +119,25 @@ async function finalize({
119
119
  }) {
120
120
  const { root, prefix } = placement;
121
121
  const logPath = ledgerRelative(ledgerDirectory, claimReconcileLogPath(path.resolve(ledgerDirectory), namespace));
122
- const journalOwned = command !== 'create';
122
+ // Every mutation, create included, appends its own journal entries and
123
+ // rebuilds the derived reconciliation log, so every commit set carries that
124
+ // log alongside the item.
125
+ //
126
+ // Log dirt that predates the invocation is a separate question. A mutation on
127
+ // an existing item rebuilds residue its own prior claim operations left in
128
+ // this checkout; a create allocates a fresh identity and has no standing to
129
+ // absorb bytes that were already there, so it refuses instead.
130
+ const absorbsPriorLogDirt = command !== 'create';
123
131
 
124
132
  // Preflight, before any Wowbagger write. Any staged path and every dirty
125
- // ledger path except the journal-owning command's derived log refuse: a broad
126
- // commit would otherwise absorb foreign work.
133
+ // ledger path except an absorbable derived log refuse: a broad commit would
134
+ // otherwise take in foreign work.
127
135
  const before = await inspectWorktree(root, prefix);
128
136
  if (before.staged.length > 0) {
129
137
  return shape.preflightFailed('staged-paths-present', { staged_paths: bounded(before.staged) });
130
138
  }
131
139
  const foreignDirtyBefore = before.dirtyLedger.filter((entry) => (
132
- !(journalOwned && entry === logPath)
140
+ !(absorbsPriorLogDirt && entry === logPath)
133
141
  ));
134
142
  if (foreignDirtyBefore.length > 0) {
135
143
  return shape.preflightFailed('ledger-not-clean', { dirty_paths: bounded(foreignDirtyBefore) });
@@ -165,13 +173,13 @@ async function finalize({
165
173
  });
166
174
  }
167
175
 
168
- // Reconciliation rewrites the tracked log. Only a command that will commit
169
- // that log may proceed with it dirty; create commits the item alone.
176
+ // Reconciliation rewrites the tracked log. Every command here will commit
177
+ // that log, so its own rewrite is never stray; anything else is.
170
178
  const afterVerify = await inspectWorktree(root, prefix);
171
179
  if (afterVerify.staged.length > 0) {
172
180
  return shape.preflightFailed('staged-paths-present', { staged_paths: bounded(afterVerify.staged) });
173
181
  }
174
- const strayBefore = afterVerify.dirtyLedger.filter((entry) => !(journalOwned && entry === logPath));
182
+ const strayBefore = afterVerify.dirtyLedger.filter((entry) => entry !== logPath);
175
183
  if (strayBefore.length > 0) {
176
184
  return shape.preflightFailed('ledger-not-clean', { dirty_paths: bounded(strayBefore) });
177
185
  }
@@ -210,10 +218,10 @@ async function finalize({
210
218
  const itemChanged = await itemDiffersFromHead(root, prefix, published.path, published.revision);
211
219
  const commitSet = [
212
220
  ...(itemChanged ? [published.path] : []),
213
- ...(journalOwned ? [logPath] : []),
221
+ logPath,
214
222
  ].sort(compareText);
215
223
  const subject = commitSubject(command, published.number, published.id);
216
- const witness = journalOwned ? shape.terminalWitness(outcome, published) : null;
224
+ const witness = shape.terminalWitness(outcome, published);
217
225
  const context = {
218
226
  command,
219
227
  head,
@@ -232,7 +240,6 @@ async function finalize({
232
240
  context,
233
241
  commitSet,
234
242
  digests,
235
- journalOwned,
236
243
  ledgerDirectory,
237
244
  logPath,
238
245
  ownArtifacts: shape.recoveryArtifactPaths(outcome),
@@ -287,7 +294,7 @@ async function finalize({
287
294
  gitCommonDir,
288
295
  namespace,
289
296
  targetItemId: published.id,
290
- writeLogWhenEmpty: journalOwned,
297
+ writeLogWhenEmpty: true,
291
298
  });
292
299
  const reconciliation = reconciliationFailureReason(reconciled, commit.commit, context.operationId, published.id);
293
300
  if (reconciliation) {
@@ -305,7 +312,7 @@ async function finalize({
305
312
  // Every check that must hold between publication and staging. Returning a
306
313
  // reason here means the item is published and no Git write has happened yet.
307
314
  async function prepareCommitSet({
308
- context, commitSet, digests, journalOwned, ledgerDirectory, logPath, ownArtifacts, prefix, root, witness,
315
+ context, commitSet, digests, ledgerDirectory, logPath, ownArtifacts, prefix, root, witness,
309
316
  }) {
310
317
  let itemBytes;
311
318
  try {
@@ -330,7 +337,6 @@ async function prepareCommitSet({
330
337
  ));
331
338
  if (stray.length > 0) return { stage: 'prepare-commit-set', reason: 'tree-changed' };
332
339
 
333
- if (!journalOwned) return null;
334
340
  let logBytes;
335
341
  try {
336
342
  logBytes = await readFile(path.join(path.resolve(ledgerDirectory), logPath));
@@ -852,7 +858,6 @@ export async function finalizeFromRecoveryToken({ ledgerDirectory, token }) {
852
858
 
853
859
  async function runFinalize({ gitCommonDir, ledgerDirectory, namespace, payload, placement }) {
854
860
  const { root, prefix } = placement;
855
- const journalOwned = payload.command !== 'create';
856
861
  const logPath = ledgerRelative(ledgerDirectory, claimReconcileLogPath(path.resolve(ledgerDirectory), namespace));
857
862
  const witness = payload.terminal_witness ?? null;
858
863
 
@@ -877,14 +882,15 @@ async function runFinalize({ gitCommonDir, ledgerDirectory, namespace, payload,
877
882
  const itemChanged = await itemDiffersFromHead(root, prefix, derived.path, payload.published_revision);
878
883
  commitSet = [
879
884
  ...(itemChanged ? [derived.path] : []),
880
- ...(journalOwned ? [logPath] : []),
885
+ logPath,
881
886
  ].sort(compareText);
882
887
  } else {
883
888
  commitSet = [...tokenPaths].sort(compareText);
884
889
  }
885
- const allowedCommitSets = journalOwned
886
- ? [[logPath], [derived.path, logPath]]
887
- : [[derived.path]];
890
+ // A token is only ever issued by the auto-commit path above, where every
891
+ // command owns its reconciliation log, so recovery accepts the same two
892
+ // shapes for every command.
893
+ const allowedCommitSets = [[logPath], [derived.path, logPath]];
888
894
  const tokenMatchesMutation = allowedCommitSets.some((allowed) => sameSet(tokenPaths, allowed));
889
895
  if (payload.item_path !== derived.path
890
896
  || !tokenMatchesMutation
@@ -910,7 +916,7 @@ async function runFinalize({ gitCommonDir, ledgerDirectory, namespace, payload,
910
916
  }
911
917
 
912
918
  const refusal = await finalizePreconditions({
913
- commitSet, derived, digests, gitPaths, journalOwned, ledgerDirectory, logPath, payload, prefix, root, witness,
919
+ commitSet, derived, digests, gitPaths, ledgerDirectory, logPath, payload, prefix, root, witness,
914
920
  });
915
921
  if (refusal) return finalizeRefused(refusal.reason, refusal.details);
916
922
 
@@ -952,7 +958,7 @@ async function runFinalize({ gitCommonDir, ledgerDirectory, namespace, payload,
952
958
  }
953
959
 
954
960
  async function finalizePreconditions({
955
- commitSet, derived, digests, gitPaths, journalOwned, ledgerDirectory, logPath, payload, prefix, root, witness,
961
+ commitSet, derived, digests, gitPaths, ledgerDirectory, logPath, payload, prefix, root, witness,
956
962
  }) {
957
963
  let itemBytes;
958
964
  try {
@@ -973,7 +979,6 @@ async function finalizePreconditions({
973
979
  }
974
980
  const stray = observed.dirtyLedger.filter((entry) => !commitSet.includes(entry));
975
981
  if (stray.length > 0) return { reason: 'ledger-not-clean', details: { dirty_paths: bounded(stray) } };
976
- if (!journalOwned) return null;
977
982
  let logBytes;
978
983
  try {
979
984
  logBytes = await readFile(path.join(path.resolve(ledgerDirectory), logPath));
package/src/launch.js CHANGED
@@ -17,8 +17,8 @@ import { fileURLToPath } from 'node:url';
17
17
 
18
18
  // The engine floor the package declares. A host that resolves its own Node
19
19
  // executable rather than reusing `process.execPath` must check it, because a
20
- // Node 18 executable will fail somewhere inside the core rather than at launch.
21
- export const MINIMUM_NODE_MAJOR = 20;
20
+ // Node 22 executable will fail somewhere inside the core rather than at launch.
21
+ export const MINIMUM_NODE_MAJOR = 24;
22
22
 
23
23
  // The absolute path of the core script. It is resolved from this module's own
24
24
  // URL, so it is correct for a global install, a local install, a workspace