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.
- package/CHANGELOG.md +33 -0
- package/README.md +20 -9
- package/docs/mutation-contract.md +1036 -0
- package/docs/work-claim-contract.md +599 -0
- package/package.json +5 -2
- package/scripts/migrate-schema-2.js +7 -0
- package/skills/wowbagger/SKILL.md +27 -13
- package/src/claim-journal.js +2 -2
- package/src/claim-publication.js +29 -3
- package/src/cli.js +46 -5
- package/src/git-reconciliation.js +10 -6
- package/src/ledger.js +1 -0
- package/src/mutation.js +1 -1
- package/src/schema-migration.js +1 -1
|
@@ -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
|
|
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.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
123
|
+
8. Commit the item change, or merge the worker commit into the coordinating
|
|
110
124
|
branch.
|
|
111
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
package/src/claim-journal.js
CHANGED
|
@@ -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(
|
|
25
|
-
return path.join(
|
|
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) {
|
package/src/claim-publication.js
CHANGED
|
@@ -477,7 +477,7 @@ export async function reconcileClaimJournal({
|
|
|
477
477
|
}
|
|
478
478
|
|
|
479
479
|
await writeReconcileLog(
|
|
480
|
-
claimReconcileLogPath(path.
|
|
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
|
-
|
|
564
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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(
|
|
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
|
|
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
|
|
27
|
-
|
|
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) =>
|
|
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 ??
|
|
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,
|
package/src/schema-migration.js
CHANGED
|
@@ -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
|
|
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)) {
|