nawabari 0.6.1 → 0.7.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/dist/cli.js +434 -9
- package/dist/cli.js.map +1 -1
- package/dist/domain/errors.d.ts +1 -1
- package/dist/domain/errors.js.map +1 -1
- package/dist/domain/session-backend.d.ts +3 -1
- package/dist/domain/session-backend.js +48 -1
- package/dist/domain/session-backend.js.map +1 -1
- package/dist/domain/session.d.ts +51 -1
- package/dist/domain/session.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/operation-authorization.d.ts +98 -3
- package/dist/operation-authorization.js +48 -21
- package/dist/operation-authorization.js.map +1 -1
- package/dist/resource-claims.d.ts +17 -0
- package/dist/resource-claims.js +35 -0
- package/dist/resource-claims.js.map +1 -1
- package/dist/session-registry.d.ts +80 -2
- package/dist/session-registry.js +292 -38
- package/dist/session-registry.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { runDoctor } from "./domain/doctor.js";
|
|
|
3
3
|
import { DomainError, EXIT_CODES, failure } from "./domain/errors.js";
|
|
4
4
|
import { MAX_SESSION_LIST_LIMIT, } from "./domain/session.js";
|
|
5
5
|
import { EVIDENCE_MAX_DIFF_BYTES, EVIDENCE_MAX_DIFF_HUNKS, EVIDENCE_MAX_DIFF_PATHS } from "./repository-evidence.js";
|
|
6
|
+
import { isResourceClaimMode } from "./resource-claims.js";
|
|
6
7
|
import { createLocalSessionBackend } from "./domain/session-backend.js";
|
|
7
8
|
import { defaultCliIO, renderFailure, renderSuccess } from "./presentation.js";
|
|
8
9
|
import { MACHINE_CONTRACT_ID, MACHINE_CONTRACT_SCHEMA_VERSION, machineContract } from "./contract.js";
|
|
@@ -116,16 +117,76 @@ const HELP_COMMANDS = [
|
|
|
116
117
|
value: "<read|write|exclusive-write>",
|
|
117
118
|
required: true,
|
|
118
119
|
}),
|
|
120
|
+
option("--if-generation", "Expected claim-set generation for CAS; mutually exclusive with --force", {
|
|
121
|
+
value: "<non-negative-integer>",
|
|
122
|
+
}),
|
|
123
|
+
option("--force", "Explicitly permit unconditional complete replacement; mutually exclusive with --if-generation"),
|
|
119
124
|
option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
|
|
120
125
|
option("--repository", "Expected repository identity", { value: "<id>" }),
|
|
121
126
|
],
|
|
122
127
|
notes: [
|
|
123
|
-
"The desired claim set
|
|
124
|
-
"
|
|
128
|
+
"The desired claim set is a full replacement performed atomically in one updateClaims() transaction; " +
|
|
129
|
+
"use exactly one concurrency intent: --if-generation <non-negative-integer> for claim-set generation CAS, " +
|
|
130
|
+
"or explicit --force for unconditional replacement. On any invalid, conflicting, or stale claim the prior set is left unchanged.",
|
|
125
131
|
"Each --resource must be immediately followed by its own --mode; pairing is positional adjacency, not flag order.",
|
|
126
132
|
"Target grammar: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
|
|
127
133
|
],
|
|
128
134
|
},
|
|
135
|
+
{
|
|
136
|
+
name: "session mutate",
|
|
137
|
+
summary: "Atomically apply exact-resource claim additions, changes, and releases",
|
|
138
|
+
usage: `${CLI_NAME} session mutate [<session>|--session <id>] [--repository <id>] ` +
|
|
139
|
+
`(--upsert-resource <path-or-glob> --mode <read|write|exclusive-write> | --release-resource <path-or-glob>)+ ` +
|
|
140
|
+
`(--if-generation <non-negative-safe-int> | --force)`,
|
|
141
|
+
options: [
|
|
142
|
+
option("--upsert-resource", "Exact repository-relative resource to add or change; each occurrence must be immediately followed by --mode", { value: "<path-or-glob>" }),
|
|
143
|
+
option("--mode", "Mode for the immediately preceding --upsert-resource", {
|
|
144
|
+
value: "<read|write|exclusive-write>",
|
|
145
|
+
}),
|
|
146
|
+
option("--release-resource", "Exact repository-relative resource to release; repeatable", {
|
|
147
|
+
value: "<path-or-glob>",
|
|
148
|
+
}),
|
|
149
|
+
option("--if-generation", "Expected claim-set generation for CAS; mutually exclusive with --force", {
|
|
150
|
+
value: "<non-negative-safe-int>",
|
|
151
|
+
}),
|
|
152
|
+
option("--force", "Explicitly permit unconditional atomic mutation; mutually exclusive with --if-generation"),
|
|
153
|
+
option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
|
|
154
|
+
option("--repository", "Expected repository identity", { value: "<id>" }),
|
|
155
|
+
],
|
|
156
|
+
notes: [
|
|
157
|
+
"Apply one or more ordered upsert/release deltas in one backend transaction; exactly one concurrency intent is required.",
|
|
158
|
+
"Every --upsert-resource must be immediately followed by its own --mode; --release-resource takes exactly one value.",
|
|
159
|
+
"Target grammar: optional first positional <session> is an alias for --session <id>; do not supply both.",
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: "session transition",
|
|
164
|
+
summary: "Atomically transition one exact resource claim mode",
|
|
165
|
+
usage: `${CLI_NAME} session transition [<session>|--session <id>] [--repository <id>] ` +
|
|
166
|
+
`--resource <path-or-glob> --mode <read|write|exclusive-write> ` +
|
|
167
|
+
`(--if-generation <non-negative-safe-int> | --force)`,
|
|
168
|
+
options: [
|
|
169
|
+
option("--resource", "Exact repository-relative resource to acquire or change", {
|
|
170
|
+
value: "<path-or-glob>",
|
|
171
|
+
required: true,
|
|
172
|
+
}),
|
|
173
|
+
option("--mode", "Target mode for the immediately preceding --resource", {
|
|
174
|
+
value: "<read|write|exclusive-write>",
|
|
175
|
+
required: true,
|
|
176
|
+
}),
|
|
177
|
+
option("--if-generation", "Expected claim-set generation for CAS; mutually exclusive with --force", {
|
|
178
|
+
value: "<non-negative-safe-int>",
|
|
179
|
+
}),
|
|
180
|
+
option("--force", "Explicitly permit unconditional atomic mutation; mutually exclusive with --if-generation"),
|
|
181
|
+
option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
|
|
182
|
+
option("--repository", "Expected repository identity", { value: "<id>" }),
|
|
183
|
+
],
|
|
184
|
+
notes: [
|
|
185
|
+
"Exactly one resource/mode pair is projected as one atomic upsert delta; same mode is an idempotent no-op.",
|
|
186
|
+
"Use exactly one concurrency intent: --if-generation <non-negative-safe-int> or explicit --force.",
|
|
187
|
+
"The target grammar accepts an optional first positional <session> or --session <id>, but not both.",
|
|
188
|
+
],
|
|
189
|
+
},
|
|
129
190
|
{
|
|
130
191
|
name: "session claims",
|
|
131
192
|
summary: "List canonical resource claims",
|
|
@@ -169,16 +230,76 @@ const HELP_COMMANDS = [
|
|
|
169
230
|
value: "<read|write|exclusive-write>",
|
|
170
231
|
required: true,
|
|
171
232
|
}),
|
|
233
|
+
option("--if-generation", "Expected claim-set generation for CAS; mutually exclusive with --force", {
|
|
234
|
+
value: "<non-negative-integer>",
|
|
235
|
+
}),
|
|
236
|
+
option("--force", "Explicitly permit unconditional complete replacement; mutually exclusive with --if-generation"),
|
|
172
237
|
option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
|
|
173
238
|
option("--repository", "Expected repository identity", { value: "<id>" }),
|
|
174
239
|
],
|
|
175
240
|
notes: [
|
|
176
|
-
"The desired claim set
|
|
177
|
-
"
|
|
241
|
+
"The desired claim set is a full replacement performed atomically in one updateClaims() transaction; " +
|
|
242
|
+
"use exactly one concurrency intent: --if-generation <non-negative-integer> for claim-set generation CAS, " +
|
|
243
|
+
"or explicit --force for unconditional replacement. On any invalid, conflicting, or stale claim the prior set is left unchanged.",
|
|
178
244
|
"Each --resource must be immediately followed by its own --mode; pairing is positional adjacency, not flag order.",
|
|
179
245
|
"Target grammar matches session update: optional first positional <session-id> is an alias for --session <id>; do not supply both.",
|
|
180
246
|
],
|
|
181
247
|
},
|
|
248
|
+
{
|
|
249
|
+
name: "resource mutate",
|
|
250
|
+
summary: "Atomically apply exact-resource claim additions, changes, and releases (alias)",
|
|
251
|
+
usage: `${CLI_NAME} resource mutate [<session>|--session <id>] [--repository <id>] ` +
|
|
252
|
+
`(--upsert-resource <path-or-glob> --mode <read|write|exclusive-write> | --release-resource <path-or-glob>)+ ` +
|
|
253
|
+
`(--if-generation <non-negative-safe-int> | --force)`,
|
|
254
|
+
options: [
|
|
255
|
+
option("--upsert-resource", "Exact repository-relative resource to add or change; each occurrence must be immediately followed by --mode", { value: "<path-or-glob>" }),
|
|
256
|
+
option("--mode", "Mode for the immediately preceding --upsert-resource", {
|
|
257
|
+
value: "<read|write|exclusive-write>",
|
|
258
|
+
}),
|
|
259
|
+
option("--release-resource", "Exact repository-relative resource to release; repeatable", {
|
|
260
|
+
value: "<path-or-glob>",
|
|
261
|
+
}),
|
|
262
|
+
option("--if-generation", "Expected claim-set generation for CAS; mutually exclusive with --force", {
|
|
263
|
+
value: "<non-negative-safe-int>",
|
|
264
|
+
}),
|
|
265
|
+
option("--force", "Explicitly permit unconditional atomic mutation; mutually exclusive with --if-generation"),
|
|
266
|
+
option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
|
|
267
|
+
option("--repository", "Expected repository identity", { value: "<id>" }),
|
|
268
|
+
],
|
|
269
|
+
notes: [
|
|
270
|
+
"Apply one or more ordered upsert/release deltas in one backend transaction; exactly one concurrency intent is required.",
|
|
271
|
+
"Every --upsert-resource must be immediately followed by its own --mode; --release-resource takes exactly one value.",
|
|
272
|
+
"Target grammar matches session mutate: optional first positional <session> is an alias for --session <id>; do not supply both.",
|
|
273
|
+
],
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
name: "resource transition",
|
|
277
|
+
summary: "Atomically transition one exact resource claim mode (alias)",
|
|
278
|
+
usage: `${CLI_NAME} resource transition [<session>|--session <id>] [--repository <id>] ` +
|
|
279
|
+
`--resource <path-or-glob> --mode <read|write|exclusive-write> ` +
|
|
280
|
+
`(--if-generation <non-negative-safe-int> | --force)`,
|
|
281
|
+
options: [
|
|
282
|
+
option("--resource", "Exact repository-relative resource to acquire or change", {
|
|
283
|
+
value: "<path-or-glob>",
|
|
284
|
+
required: true,
|
|
285
|
+
}),
|
|
286
|
+
option("--mode", "Target mode for the immediately preceding --resource", {
|
|
287
|
+
value: "<read|write|exclusive-write>",
|
|
288
|
+
required: true,
|
|
289
|
+
}),
|
|
290
|
+
option("--if-generation", "Expected claim-set generation for CAS; mutually exclusive with --force", {
|
|
291
|
+
value: "<non-negative-safe-int>",
|
|
292
|
+
}),
|
|
293
|
+
option("--force", "Explicitly permit unconditional atomic mutation; mutually exclusive with --if-generation"),
|
|
294
|
+
option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
|
|
295
|
+
option("--repository", "Expected repository identity", { value: "<id>" }),
|
|
296
|
+
],
|
|
297
|
+
notes: [
|
|
298
|
+
"Exactly one resource/mode pair is projected as one atomic upsert delta; same mode is an idempotent no-op.",
|
|
299
|
+
"Use exactly one concurrency intent: --if-generation <non-negative-safe-int> or explicit --force.",
|
|
300
|
+
"The target grammar accepts an optional first positional <session> or --session <id>, but not both.",
|
|
301
|
+
],
|
|
302
|
+
},
|
|
182
303
|
{
|
|
183
304
|
name: "resource list",
|
|
184
305
|
summary: "List canonical resource claims (alias)",
|
|
@@ -398,7 +519,18 @@ function helpPayload(spec) {
|
|
|
398
519
|
session_targeting: {
|
|
399
520
|
canonical: "--session <id>",
|
|
400
521
|
positional_alias: "<session-id> as the first argument after a session-scoped subcommand",
|
|
401
|
-
commands: [
|
|
522
|
+
commands: [
|
|
523
|
+
"show",
|
|
524
|
+
"inspect",
|
|
525
|
+
"claim",
|
|
526
|
+
"claims",
|
|
527
|
+
"release",
|
|
528
|
+
"update",
|
|
529
|
+
"mutate",
|
|
530
|
+
"transition",
|
|
531
|
+
"close",
|
|
532
|
+
"discard",
|
|
533
|
+
],
|
|
402
534
|
ambiguity: "supplying both positional and --session is rejected",
|
|
403
535
|
discard_requires_explicit_target: true,
|
|
404
536
|
},
|
|
@@ -668,6 +800,213 @@ function parseTargetedOptions(arguments_, allowed, required = false) {
|
|
|
668
800
|
}
|
|
669
801
|
return parsed;
|
|
670
802
|
}
|
|
803
|
+
const CLAIM_CONCURRENCY_OPTION_NAMES = new Set(["--if-generation", "--force"]);
|
|
804
|
+
function isClaimConcurrencyOption(name) {
|
|
805
|
+
return CLAIM_CONCURRENCY_OPTION_NAMES.has(name);
|
|
806
|
+
}
|
|
807
|
+
/** Shared #176 CAS/force option vocabulary and value consumption. */
|
|
808
|
+
function consumeClaimConcurrencyOption(name, inlineValue, nextValue, state) {
|
|
809
|
+
if (name === "--force") {
|
|
810
|
+
if (inlineValue !== null) {
|
|
811
|
+
return failure(usageError("INVALID_ARGUMENT", "--force does not accept a value.", { option: name }));
|
|
812
|
+
}
|
|
813
|
+
if (state.force) {
|
|
814
|
+
return failure(usageError("INVALID_ARGUMENT", "--force may be supplied only once.", { option: name }));
|
|
815
|
+
}
|
|
816
|
+
return { ok: true, value: { state: { ...state, force: true }, consumed_next_value: false } };
|
|
817
|
+
}
|
|
818
|
+
const value = inlineValue ?? nextValue;
|
|
819
|
+
const negativeGeneration = /^-\d+$/u.test(value ?? "");
|
|
820
|
+
if (value === undefined || value === "" || (inlineValue === null && value.startsWith("-") && !negativeGeneration)) {
|
|
821
|
+
return failure(usageError("MISSING_ARGUMENT", `${name} requires a value.`, { option: name }));
|
|
822
|
+
}
|
|
823
|
+
if (state.expected_claim_set_generation !== null) {
|
|
824
|
+
return failure(usageError("INVALID_ARGUMENT", "--if-generation may be supplied only once.", { option: name }));
|
|
825
|
+
}
|
|
826
|
+
if (!/^\d+$/u.test(value)) {
|
|
827
|
+
return failure(usageError("INVALID_ARGUMENT", "--if-generation requires a non-negative safe integer.", {
|
|
828
|
+
option: name,
|
|
829
|
+
value,
|
|
830
|
+
}));
|
|
831
|
+
}
|
|
832
|
+
const parsedGeneration = Number(value);
|
|
833
|
+
if (!Number.isSafeInteger(parsedGeneration)) {
|
|
834
|
+
return failure(usageError("INVALID_ARGUMENT", "--if-generation requires a non-negative safe integer.", {
|
|
835
|
+
option: name,
|
|
836
|
+
value,
|
|
837
|
+
}));
|
|
838
|
+
}
|
|
839
|
+
return {
|
|
840
|
+
ok: true,
|
|
841
|
+
value: {
|
|
842
|
+
state: { ...state, expected_claim_set_generation: parsedGeneration },
|
|
843
|
+
consumed_next_value: inlineValue === null,
|
|
844
|
+
},
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
function finalizeClaimConcurrency(state, requireConcurrencyIntent) {
|
|
848
|
+
if (!requireConcurrencyIntent && (state.force || state.expected_claim_set_generation !== null)) {
|
|
849
|
+
return failure(usageError("INVALID_ARGUMENT", "session claim does not accept --if-generation or --force.", {
|
|
850
|
+
options: ["--if-generation", "--force"],
|
|
851
|
+
}));
|
|
852
|
+
}
|
|
853
|
+
if (requireConcurrencyIntent && state.force && state.expected_claim_set_generation !== null) {
|
|
854
|
+
return failure(usageError("INVALID_ARGUMENT", "Specify exactly one of --if-generation or --force, not both.", {
|
|
855
|
+
options: ["--if-generation", "--force"],
|
|
856
|
+
}));
|
|
857
|
+
}
|
|
858
|
+
if (requireConcurrencyIntent && !state.force && state.expected_claim_set_generation === null) {
|
|
859
|
+
return failure(usageError("MISSING_ARGUMENT", "Exactly one of --if-generation or --force is required.", {
|
|
860
|
+
options: ["--if-generation", "--force"],
|
|
861
|
+
}));
|
|
862
|
+
}
|
|
863
|
+
return { ok: true, value: state };
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Parse the public atomic delta grammar without interpreting resource identity.
|
|
867
|
+
* Resource canonicalization, duplicate/conflict authority, and all mutation
|
|
868
|
+
* semantics remain in the domain/backend primitive.
|
|
869
|
+
*/
|
|
870
|
+
function parseClaimDeltaMutation(arguments_) {
|
|
871
|
+
const positional = splitPositionalSessionTarget(arguments_);
|
|
872
|
+
if (!positional.ok)
|
|
873
|
+
return positional;
|
|
874
|
+
let sessionId = positional.value.sessionId;
|
|
875
|
+
let repository = null;
|
|
876
|
+
const deltas = [];
|
|
877
|
+
let pendingUpsertResource = null;
|
|
878
|
+
let concurrencyState = {
|
|
879
|
+
expected_claim_set_generation: null,
|
|
880
|
+
force: false,
|
|
881
|
+
};
|
|
882
|
+
const recognized = new Set([
|
|
883
|
+
"--session",
|
|
884
|
+
"--repository",
|
|
885
|
+
"--upsert-resource",
|
|
886
|
+
"--mode",
|
|
887
|
+
"--release-resource",
|
|
888
|
+
...CLAIM_CONCURRENCY_OPTION_NAMES,
|
|
889
|
+
]);
|
|
890
|
+
const rejectPending = (name) => failure(usageError("INVALID_ARGUMENT", `${name} cannot appear between --upsert-resource and its --mode; only --mode is permitted immediately after --upsert-resource.`, { option: name }));
|
|
891
|
+
const targetArguments = positional.value.arguments;
|
|
892
|
+
for (let index = 0; index < targetArguments.length; index += 1) {
|
|
893
|
+
const { name, inlineValue } = optionParts(targetArguments[index]);
|
|
894
|
+
if (!recognized.has(name)) {
|
|
895
|
+
if (pendingUpsertResource !== null)
|
|
896
|
+
return rejectPending(name);
|
|
897
|
+
return failure(usageError("INVALID_ARGUMENT", `Unknown option: ${name}.`, { option: name }));
|
|
898
|
+
}
|
|
899
|
+
if (pendingUpsertResource !== null && name !== "--mode")
|
|
900
|
+
return rejectPending(name);
|
|
901
|
+
if (isClaimConcurrencyOption(name)) {
|
|
902
|
+
const consumed = consumeClaimConcurrencyOption(name, inlineValue, targetArguments[index + 1], concurrencyState);
|
|
903
|
+
if (!consumed.ok)
|
|
904
|
+
return consumed;
|
|
905
|
+
concurrencyState = consumed.value.state;
|
|
906
|
+
if (consumed.value.consumed_next_value)
|
|
907
|
+
index += 1;
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
const value = inlineValue ?? targetArguments[index + 1];
|
|
911
|
+
if (value === undefined || value === "" || (inlineValue === null && value.startsWith("-"))) {
|
|
912
|
+
return failure(usageError("MISSING_ARGUMENT", `${name} requires a value.`, { option: name }));
|
|
913
|
+
}
|
|
914
|
+
if (inlineValue === null)
|
|
915
|
+
index += 1;
|
|
916
|
+
if (name === "--session") {
|
|
917
|
+
if (sessionId !== null) {
|
|
918
|
+
return failure(usageError("INVALID_ARGUMENT", "Specify a session target either positionally or with --session, not both.", {
|
|
919
|
+
option: "--session",
|
|
920
|
+
}));
|
|
921
|
+
}
|
|
922
|
+
sessionId = value;
|
|
923
|
+
}
|
|
924
|
+
else if (name === "--repository") {
|
|
925
|
+
repository = value;
|
|
926
|
+
}
|
|
927
|
+
else if (name === "--upsert-resource") {
|
|
928
|
+
pendingUpsertResource = value;
|
|
929
|
+
}
|
|
930
|
+
else if (name === "--release-resource") {
|
|
931
|
+
deltas.push({ kind: "release", resource: value });
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
if (pendingUpsertResource === null) {
|
|
935
|
+
return failure(usageError("INVALID_ARGUMENT", "--mode must be immediately preceded by its own --upsert-resource.", {
|
|
936
|
+
option: "--mode",
|
|
937
|
+
}));
|
|
938
|
+
}
|
|
939
|
+
if (!isResourceClaimMode(value)) {
|
|
940
|
+
return failure(usageError("INVALID_ARGUMENT", "--mode requires read, write, or exclusive-write.", {
|
|
941
|
+
option: "--mode",
|
|
942
|
+
value,
|
|
943
|
+
}));
|
|
944
|
+
}
|
|
945
|
+
deltas.push({
|
|
946
|
+
kind: "upsert",
|
|
947
|
+
resource: pendingUpsertResource,
|
|
948
|
+
mode: value,
|
|
949
|
+
});
|
|
950
|
+
pendingUpsertResource = null;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
if (pendingUpsertResource !== null) {
|
|
954
|
+
return failure(usageError("MISSING_ARGUMENT", "--upsert-resource must be immediately followed by --mode.", {
|
|
955
|
+
option: "--mode",
|
|
956
|
+
}));
|
|
957
|
+
}
|
|
958
|
+
if (deltas.length === 0) {
|
|
959
|
+
return failure(usageError("MISSING_ARGUMENT", "At least one --upsert-resource or --release-resource is required.", {
|
|
960
|
+
options: ["--upsert-resource", "--release-resource"],
|
|
961
|
+
}));
|
|
962
|
+
}
|
|
963
|
+
const concurrency = finalizeClaimConcurrency(concurrencyState, true);
|
|
964
|
+
if (!concurrency.ok)
|
|
965
|
+
return concurrency;
|
|
966
|
+
return {
|
|
967
|
+
ok: true,
|
|
968
|
+
value: {
|
|
969
|
+
session_id: sessionId,
|
|
970
|
+
repository,
|
|
971
|
+
deltas,
|
|
972
|
+
expected_claim_set_generation: concurrency.value.expected_claim_set_generation,
|
|
973
|
+
force: concurrency.value.force,
|
|
974
|
+
},
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Parse the one-resource transition convenience grammar. The underlying
|
|
979
|
+
* mutation remains the canonical multi-delta primitive; this parser only
|
|
980
|
+
* constrains its public projection to exactly one upsert pair.
|
|
981
|
+
*/
|
|
982
|
+
function parseClaimTransition(arguments_) {
|
|
983
|
+
const parsed = parseClaimReplacementPairs(arguments_);
|
|
984
|
+
if (!parsed.ok)
|
|
985
|
+
return parsed;
|
|
986
|
+
if (parsed.value.pairs.length !== 1) {
|
|
987
|
+
return failure(usageError("INVALID_ARGUMENT", "transition accepts exactly one --resource/--mode pair.", {
|
|
988
|
+
pair_count: parsed.value.pairs.length,
|
|
989
|
+
}));
|
|
990
|
+
}
|
|
991
|
+
const pair = parsed.value.pairs[0];
|
|
992
|
+
if (pair === undefined || !isResourceClaimMode(pair.mode)) {
|
|
993
|
+
return failure(usageError("INVALID_ARGUMENT", "--mode requires read, write, or exclusive-write.", {
|
|
994
|
+
option: "--mode",
|
|
995
|
+
value: pair?.mode ?? null,
|
|
996
|
+
}));
|
|
997
|
+
}
|
|
998
|
+
return {
|
|
999
|
+
ok: true,
|
|
1000
|
+
value: {
|
|
1001
|
+
session_id: parsed.value.session_id,
|
|
1002
|
+
repository: parsed.value.repository,
|
|
1003
|
+
resource: pair.resource,
|
|
1004
|
+
mode: pair.mode,
|
|
1005
|
+
expected_claim_set_generation: parsed.value.expected_claim_set_generation,
|
|
1006
|
+
force: parsed.value.force,
|
|
1007
|
+
},
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
671
1010
|
/**
|
|
672
1011
|
* `update` accepts a complete desired claim set as repeated
|
|
673
1012
|
* `--resource <path> --mode <mode>` pairs. Pairing is by strict local
|
|
@@ -676,7 +1015,7 @@ function parseTargetedOptions(arguments_, allowed, required = false) {
|
|
|
676
1015
|
* parallel-array position, so argv order can never associate a resource
|
|
677
1016
|
* with the wrong mode.
|
|
678
1017
|
*/
|
|
679
|
-
function parseClaimReplacementPairs(arguments_) {
|
|
1018
|
+
function parseClaimReplacementPairs(arguments_, requireConcurrencyIntent = true) {
|
|
680
1019
|
const positional = splitPositionalSessionTarget(arguments_);
|
|
681
1020
|
if (!positional.ok)
|
|
682
1021
|
return positional;
|
|
@@ -684,16 +1023,34 @@ function parseClaimReplacementPairs(arguments_) {
|
|
|
684
1023
|
let repository = null;
|
|
685
1024
|
const pairs = [];
|
|
686
1025
|
let pendingResource = null;
|
|
1026
|
+
let concurrencyState = {
|
|
1027
|
+
expected_claim_set_generation: null,
|
|
1028
|
+
force: false,
|
|
1029
|
+
};
|
|
687
1030
|
sessionId = positional.value.sessionId;
|
|
688
1031
|
const targetArguments = positional.value.arguments;
|
|
689
1032
|
for (let index = 0; index < targetArguments.length; index += 1) {
|
|
690
1033
|
const { name, inlineValue } = optionParts(targetArguments[index]);
|
|
691
|
-
|
|
1034
|
+
const isConcurrencyOption = isClaimConcurrencyOption(name);
|
|
1035
|
+
if (name !== "--session" &&
|
|
1036
|
+
name !== "--repository" &&
|
|
1037
|
+
name !== "--resource" &&
|
|
1038
|
+
name !== "--mode" &&
|
|
1039
|
+
!(requireConcurrencyIntent && isConcurrencyOption)) {
|
|
692
1040
|
return failure(usageError("INVALID_ARGUMENT", `Unknown option: ${name}.`, { option: name }));
|
|
693
1041
|
}
|
|
694
1042
|
if (pendingResource !== null && name !== "--mode") {
|
|
695
1043
|
return failure(usageError("INVALID_ARGUMENT", `${name} cannot appear between --resource and its --mode; only --mode is permitted immediately after --resource.`, { option: name }));
|
|
696
1044
|
}
|
|
1045
|
+
if (requireConcurrencyIntent && isConcurrencyOption) {
|
|
1046
|
+
const consumed = consumeClaimConcurrencyOption(name, inlineValue, targetArguments[index + 1], concurrencyState);
|
|
1047
|
+
if (!consumed.ok)
|
|
1048
|
+
return consumed;
|
|
1049
|
+
concurrencyState = consumed.value.state;
|
|
1050
|
+
if (consumed.value.consumed_next_value)
|
|
1051
|
+
index += 1;
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
697
1054
|
const value = inlineValue ?? targetArguments[index + 1];
|
|
698
1055
|
if (value === undefined || value === "" || (inlineValue === null && value.startsWith("-"))) {
|
|
699
1056
|
return failure(usageError("MISSING_ARGUMENT", `${name} requires a value.`, { option: name }));
|
|
@@ -726,7 +1083,19 @@ function parseClaimReplacementPairs(arguments_) {
|
|
|
726
1083
|
if (pairs.length === 0) {
|
|
727
1084
|
return failure(usageError("MISSING_ARGUMENT", "--resource requires a value.", { option: "--resource" }));
|
|
728
1085
|
}
|
|
729
|
-
|
|
1086
|
+
const concurrency = finalizeClaimConcurrency(concurrencyState, requireConcurrencyIntent);
|
|
1087
|
+
if (!concurrency.ok)
|
|
1088
|
+
return concurrency;
|
|
1089
|
+
return {
|
|
1090
|
+
ok: true,
|
|
1091
|
+
value: {
|
|
1092
|
+
session_id: sessionId,
|
|
1093
|
+
repository,
|
|
1094
|
+
pairs,
|
|
1095
|
+
expected_claim_set_generation: concurrency.value.expected_claim_set_generation,
|
|
1096
|
+
force: concurrency.value.force,
|
|
1097
|
+
},
|
|
1098
|
+
};
|
|
730
1099
|
}
|
|
731
1100
|
/**
|
|
732
1101
|
* `claim` accepts exactly one `--resource`/`--mode` pair. Reuses the same
|
|
@@ -735,7 +1104,7 @@ function parseClaimReplacementPairs(arguments_) {
|
|
|
735
1104
|
* first pair (last-wins).
|
|
736
1105
|
*/
|
|
737
1106
|
function parseSingleClaimPair(arguments_) {
|
|
738
|
-
const parsed = parseClaimReplacementPairs(arguments_);
|
|
1107
|
+
const parsed = parseClaimReplacementPairs(arguments_, false);
|
|
739
1108
|
if (!parsed.ok)
|
|
740
1109
|
return parsed;
|
|
741
1110
|
if (parsed.value.pairs.length > 1) {
|
|
@@ -806,6 +1175,40 @@ function sessionContext(cwd) {
|
|
|
806
1175
|
async function resolveSelectedSession(backend, context, sessionId) {
|
|
807
1176
|
return sessionId === null ? backend.resolveCurrentSession(context) : backend.getSession(context, sessionId);
|
|
808
1177
|
}
|
|
1178
|
+
async function executeClaimDeltaMutation(arguments_, backend, context, operation) {
|
|
1179
|
+
const parsed = parseClaimDeltaMutation(arguments_);
|
|
1180
|
+
if (!parsed.ok)
|
|
1181
|
+
return parsed;
|
|
1182
|
+
if (backend.applyClaimDeltas === undefined)
|
|
1183
|
+
return claimCapabilityUnavailable(operation);
|
|
1184
|
+
const concurrency = parsed.value.force
|
|
1185
|
+
? { force: true }
|
|
1186
|
+
: { expected_claim_set_generation: parsed.value.expected_claim_set_generation };
|
|
1187
|
+
const result = await backend.applyClaimDeltas(context, {
|
|
1188
|
+
session_id: parsed.value.session_id,
|
|
1189
|
+
repository: parsed.value.repository,
|
|
1190
|
+
deltas: parsed.value.deltas,
|
|
1191
|
+
...concurrency,
|
|
1192
|
+
});
|
|
1193
|
+
return result.ok ? { ok: true, value: result.value } : result;
|
|
1194
|
+
}
|
|
1195
|
+
async function executeClaimTransition(arguments_, backend, context, operation) {
|
|
1196
|
+
const parsed = parseClaimTransition(arguments_);
|
|
1197
|
+
if (!parsed.ok)
|
|
1198
|
+
return parsed;
|
|
1199
|
+
if (backend.applyClaimDeltas === undefined)
|
|
1200
|
+
return claimCapabilityUnavailable(operation);
|
|
1201
|
+
const concurrency = parsed.value.force
|
|
1202
|
+
? { force: true }
|
|
1203
|
+
: { expected_claim_set_generation: parsed.value.expected_claim_set_generation };
|
|
1204
|
+
const result = await backend.applyClaimDeltas(context, {
|
|
1205
|
+
session_id: parsed.value.session_id,
|
|
1206
|
+
repository: parsed.value.repository,
|
|
1207
|
+
deltas: [{ kind: "upsert", resource: parsed.value.resource, mode: parsed.value.mode }],
|
|
1208
|
+
...concurrency,
|
|
1209
|
+
});
|
|
1210
|
+
return result.ok ? { ok: true, value: result.value } : result;
|
|
1211
|
+
}
|
|
809
1212
|
async function executeCommand(commandArguments, dependencies) {
|
|
810
1213
|
const [command, subcommand, ...rest] = commandArguments;
|
|
811
1214
|
const context = sessionContext(dependencies.cwd);
|
|
@@ -832,6 +1235,9 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
832
1235
|
return parsed;
|
|
833
1236
|
if (dependencies.backend.updateClaims === undefined)
|
|
834
1237
|
return claimCapabilityUnavailable(subcommand);
|
|
1238
|
+
const concurrency = parsed.value.force
|
|
1239
|
+
? { force: true }
|
|
1240
|
+
: { expected_claim_set_generation: parsed.value.expected_claim_set_generation };
|
|
835
1241
|
const result = await dependencies.backend.updateClaims(context, {
|
|
836
1242
|
session_id: parsed.value.session_id,
|
|
837
1243
|
repository: parsed.value.repository,
|
|
@@ -839,9 +1245,16 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
839
1245
|
resource: pair.resource,
|
|
840
1246
|
mode: pair.mode,
|
|
841
1247
|
})),
|
|
1248
|
+
...concurrency,
|
|
842
1249
|
});
|
|
843
1250
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
844
1251
|
}
|
|
1252
|
+
if (subcommand === "mutate") {
|
|
1253
|
+
return executeClaimDeltaMutation(rest, dependencies.backend, context, "session mutate");
|
|
1254
|
+
}
|
|
1255
|
+
if (subcommand === "transition") {
|
|
1256
|
+
return executeClaimTransition(rest, dependencies.backend, context, "session transition");
|
|
1257
|
+
}
|
|
845
1258
|
if (subcommand === "claims") {
|
|
846
1259
|
const parsed = parseTargetedOptions(rest, new Set(["--session"]));
|
|
847
1260
|
if (!parsed.ok)
|
|
@@ -860,6 +1273,7 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
860
1273
|
const result = await dependencies.backend.releaseClaims(context, {
|
|
861
1274
|
session_id: parsed.value.session_id,
|
|
862
1275
|
claim_ids: parsed.value.claim_id === null ? null : [parsed.value.claim_id],
|
|
1276
|
+
force: true,
|
|
863
1277
|
});
|
|
864
1278
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
865
1279
|
}
|
|
@@ -995,6 +1409,9 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
995
1409
|
return parsed;
|
|
996
1410
|
if (dependencies.backend.updateClaims === undefined)
|
|
997
1411
|
return claimCapabilityUnavailable(resourceSubcommand);
|
|
1412
|
+
const concurrency = parsed.value.force
|
|
1413
|
+
? { force: true }
|
|
1414
|
+
: { expected_claim_set_generation: parsed.value.expected_claim_set_generation };
|
|
998
1415
|
const result = await dependencies.backend.updateClaims(context, {
|
|
999
1416
|
session_id: parsed.value.session_id,
|
|
1000
1417
|
repository: parsed.value.repository,
|
|
@@ -1002,9 +1419,16 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
1002
1419
|
resource: pair.resource,
|
|
1003
1420
|
mode: pair.mode,
|
|
1004
1421
|
})),
|
|
1422
|
+
...concurrency,
|
|
1005
1423
|
});
|
|
1006
1424
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
1007
1425
|
}
|
|
1426
|
+
if (resourceSubcommand === "mutate") {
|
|
1427
|
+
return executeClaimDeltaMutation(rest, dependencies.backend, context, "resource mutate");
|
|
1428
|
+
}
|
|
1429
|
+
if (resourceSubcommand === "transition") {
|
|
1430
|
+
return executeClaimTransition(rest, dependencies.backend, context, "resource transition");
|
|
1431
|
+
}
|
|
1008
1432
|
if (resourceSubcommand === "list" || resourceSubcommand === "claims") {
|
|
1009
1433
|
const parsed = parseTargetedOptions(rest, new Set(["--session"]));
|
|
1010
1434
|
if (!parsed.ok)
|
|
@@ -1023,6 +1447,7 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
1023
1447
|
const result = await dependencies.backend.releaseClaims(context, {
|
|
1024
1448
|
session_id: parsed.value.session_id,
|
|
1025
1449
|
claim_ids: parsed.value.claim_id === null ? null : [parsed.value.claim_id],
|
|
1450
|
+
force: true,
|
|
1026
1451
|
});
|
|
1027
1452
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
1028
1453
|
}
|