@granular-software/sdk 0.4.64 → 0.4.65

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.
@@ -6534,6 +6534,11 @@ var Session = class {
6534
6534
  }
6535
6535
  }
6536
6536
  buildDirectedInvocationEffectContext(params, feedbackContext) {
6537
+ const commitUnavailable = async () => {
6538
+ throw new Error(
6539
+ "This directed browser tool invocation has no product commit transport"
6540
+ );
6541
+ };
6537
6542
  return {
6538
6543
  effectClientId: this.clientId,
6539
6544
  sandboxId: params.sandboxId || "",
@@ -6546,6 +6551,10 @@ var Session = class {
6546
6551
  userId: "",
6547
6552
  subjectId: ""
6548
6553
  },
6554
+ commit: {
6555
+ effect: commitUnavailable,
6556
+ transition: commitUnavailable
6557
+ },
6549
6558
  ...feedbackContext ? {
6550
6559
  feedback: feedbackContext.feedback,
6551
6560
  transientFeedback: feedbackContext.transientFeedback
@@ -12317,6 +12326,12 @@ var StateTransitionExpectedOutcomeSchema = external_exports.union([
12317
12326
  summary: external_exports.string().optional()
12318
12327
  }).strict()
12319
12328
  ]);
12329
+ var StateTransitionOutcomeSchema = external_exports.object({
12330
+ label: external_exports.string().min(1).optional(),
12331
+ to: external_exports.string().min(1),
12332
+ primary: external_exports.boolean().optional(),
12333
+ disposition: external_exports.enum(["continue", "error"])
12334
+ }).strict();
12320
12335
  var StateMachineTransitionSchema = external_exports.object({
12321
12336
  name: external_exports.string().min(1),
12322
12337
  from: external_exports.string().min(1),
@@ -12328,7 +12343,8 @@ var StateMachineTransitionSchema = external_exports.object({
12328
12343
  requirements: StateTransitionRequirementsSchema.optional(),
12329
12344
  permission: StateTransitionPermissionSchema.optional(),
12330
12345
  risk: external_exports.enum(["low", "medium", "high"]).optional(),
12331
- expectedOutcome: StateTransitionExpectedOutcomeSchema.optional()
12346
+ expectedOutcome: StateTransitionExpectedOutcomeSchema.optional(),
12347
+ outcomes: external_exports.record(external_exports.string().min(1), StateTransitionOutcomeSchema).optional()
12332
12348
  }).strict();
12333
12349
  external_exports.object({
12334
12350
  name: external_exports.string().min(1),
@@ -13009,6 +13025,238 @@ var effectBehaviorsMetamodelPackage = defineMetamodelPackage({
13009
13025
  }
13010
13026
  });
13011
13027
 
13028
+ // src/commit.ts
13029
+ var MAX_PROJECTION_CHANGES = 1e3;
13030
+ var MAX_SAFE_SUMMARY_KEYS = 32;
13031
+ var MAX_SAFE_TEXT_LENGTH = 2e3;
13032
+ var definedTransitionMetadata = /* @__PURE__ */ new WeakMap();
13033
+ function getDefinedTransitionMetadata(transition) {
13034
+ return definedTransitionMetadata.get(transition);
13035
+ }
13036
+ function requireNonEmptyString(value, path2) {
13037
+ if (typeof value !== "string" || value.trim().length === 0) {
13038
+ throw new Error(`${path2} must be a non-empty string`);
13039
+ }
13040
+ return value.trim();
13041
+ }
13042
+ function validateObjectReference(value, path2) {
13043
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
13044
+ throw new Error(`${path2} must be an object reference`);
13045
+ }
13046
+ const reference = value;
13047
+ requireNonEmptyString(reference.className, `${path2}.className`);
13048
+ requireNonEmptyString(reference.id, `${path2}.id`);
13049
+ if (reference.path !== void 0) {
13050
+ requireNonEmptyString(reference.path, `${path2}.path`);
13051
+ }
13052
+ }
13053
+ function validateScalarRecord(value, path2) {
13054
+ if (!value || typeof value !== "object" || Array.isArray(value) || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) {
13055
+ throw new Error(`${path2} must be an object of scalar values`);
13056
+ }
13057
+ for (const [key, item] of Object.entries(value)) {
13058
+ if (item !== null && typeof item !== "string" && typeof item !== "number" && typeof item !== "boolean") {
13059
+ throw new Error(
13060
+ `${path2}.${key} must be a string, finite number, boolean, or null`
13061
+ );
13062
+ }
13063
+ if (typeof item === "number" && !Number.isFinite(item)) {
13064
+ throw new Error(`${path2}.${key} must be finite`);
13065
+ }
13066
+ }
13067
+ }
13068
+ function validateProjectedRecord(value, path2) {
13069
+ validateObjectReference(value, path2);
13070
+ const record = value;
13071
+ if (record.label !== void 0) {
13072
+ if (typeof record.label !== "string") {
13073
+ throw new Error(`${path2}.label must be a string`);
13074
+ }
13075
+ if (record.label.length > MAX_SAFE_TEXT_LENGTH) {
13076
+ throw new Error(
13077
+ `${path2}.label exceeds ${MAX_SAFE_TEXT_LENGTH} characters`
13078
+ );
13079
+ }
13080
+ }
13081
+ validateScalarRecord(record.fields, `${path2}.fields`);
13082
+ }
13083
+ function validateBoundedJson(value, path2, depth = 0) {
13084
+ if (depth > 12) throw new Error(`${path2} is nested too deeply`);
13085
+ if (value === null || typeof value === "boolean") return;
13086
+ if (typeof value === "number") {
13087
+ if (!Number.isFinite(value)) throw new Error(`${path2} must be finite`);
13088
+ return;
13089
+ }
13090
+ if (typeof value === "string") {
13091
+ if (value.length > MAX_SAFE_TEXT_LENGTH) {
13092
+ throw new Error(`${path2} exceeds ${MAX_SAFE_TEXT_LENGTH} characters`);
13093
+ }
13094
+ return;
13095
+ }
13096
+ if (Array.isArray(value)) {
13097
+ if (value.length > MAX_PROJECTION_CHANGES) {
13098
+ throw new Error(`${path2} contains too many values`);
13099
+ }
13100
+ value.forEach(
13101
+ (item, index) => validateBoundedJson(item, `${path2}[${index}]`, depth + 1)
13102
+ );
13103
+ return;
13104
+ }
13105
+ if (!value || typeof value !== "object") {
13106
+ throw new Error(`${path2} contains an unsupported value`);
13107
+ }
13108
+ const entries = Object.entries(value);
13109
+ if (entries.length > 256) throw new Error(`${path2} contains too many keys`);
13110
+ for (const [key, item] of entries) {
13111
+ if (/token|secret|password|authorization|cookie/i.test(key)) {
13112
+ throw new Error(`${path2}.${key} is not allowed in a commit projection`);
13113
+ }
13114
+ validateBoundedJson(item, `${path2}.${key}`, depth + 1);
13115
+ }
13116
+ }
13117
+ function validateProjectionResult(declaration, projection) {
13118
+ if (!projection || typeof projection !== "object") {
13119
+ throw new Error("Projection mapper must return an object");
13120
+ }
13121
+ requireNonEmptyString(
13122
+ projection.source?.reference,
13123
+ "projection.source.reference"
13124
+ );
13125
+ if (projection.source.version !== void 0) {
13126
+ requireNonEmptyString(
13127
+ projection.source.version,
13128
+ "projection.source.version"
13129
+ );
13130
+ }
13131
+ if (!Array.isArray(projection.changes)) {
13132
+ throw new Error("projection.changes must be an array");
13133
+ }
13134
+ if (projection.changes.length > MAX_PROJECTION_CHANGES) {
13135
+ throw new Error(
13136
+ `projection.changes exceeds the ${MAX_PROJECTION_CHANGES} change limit`
13137
+ );
13138
+ }
13139
+ if (projection.primaryTarget) {
13140
+ validateObjectReference(
13141
+ projection.primaryTarget,
13142
+ "projection.primaryTarget"
13143
+ );
13144
+ }
13145
+ if (projection.safeSummary) {
13146
+ const entries = Object.entries(projection.safeSummary);
13147
+ if (entries.length > MAX_SAFE_SUMMARY_KEYS) {
13148
+ throw new Error(
13149
+ `projection.safeSummary exceeds the ${MAX_SAFE_SUMMARY_KEYS} key limit`
13150
+ );
13151
+ }
13152
+ validateBoundedJson(projection.safeSummary, "projection.safeSummary");
13153
+ validateScalarRecord(projection.safeSummary, "projection.safeSummary");
13154
+ }
13155
+ projection.changes.forEach((change, index) => {
13156
+ const changePath = `projection.changes[${index}]`;
13157
+ validateBoundedJson(change, changePath);
13158
+ if (!change || typeof change !== "object" || Array.isArray(change)) {
13159
+ throw new Error(`${changePath} must be an object`);
13160
+ }
13161
+ const rawChange = change;
13162
+ const kind = requireNonEmptyString(rawChange.kind, `${changePath}.kind`);
13163
+ if (kind === "object") {
13164
+ const operation = requireNonEmptyString(
13165
+ rawChange.operation,
13166
+ `${changePath}.operation`
13167
+ );
13168
+ if (operation !== "created" && operation !== "updated" && operation !== "deleted") {
13169
+ throw new Error(
13170
+ `${changePath}.operation must be created, updated, or deleted`
13171
+ );
13172
+ }
13173
+ if (operation === "deleted") {
13174
+ validateObjectReference(rawChange.target, `${changePath}.target`);
13175
+ } else {
13176
+ validateProjectedRecord(rawChange.record, `${changePath}.record`);
13177
+ }
13178
+ } else if (kind === "relationship") {
13179
+ const operation = requireNonEmptyString(
13180
+ rawChange.operation,
13181
+ `${changePath}.operation`
13182
+ );
13183
+ if (operation !== "connected" && operation !== "disconnected") {
13184
+ throw new Error(
13185
+ `${changePath}.operation must be connected or disconnected`
13186
+ );
13187
+ }
13188
+ requireNonEmptyString(
13189
+ rawChange.relationship,
13190
+ `${changePath}.relationship`
13191
+ );
13192
+ validateObjectReference(rawChange.from, `${changePath}.from`);
13193
+ validateObjectReference(rawChange.to, `${changePath}.to`);
13194
+ } else if (kind === "state_observation") {
13195
+ if (rawChange.operation !== void 0) {
13196
+ throw new Error(
13197
+ `${changePath}.operation is not valid for an observation`
13198
+ );
13199
+ }
13200
+ validateObjectReference(rawChange.target, `${changePath}.target`);
13201
+ requireNonEmptyString(rawChange.machine, `${changePath}.machine`);
13202
+ requireNonEmptyString(rawChange.state, `${changePath}.state`);
13203
+ } else {
13204
+ throw new Error(`${changePath}.kind is unsupported: ${kind}`);
13205
+ }
13206
+ });
13207
+ const objectOperations = /* @__PURE__ */ new Map();
13208
+ for (const change of projection.changes) {
13209
+ if (change.kind !== "object") continue;
13210
+ const reference = change.operation === "deleted" ? change.target : change.record;
13211
+ const key = `${reference.className}\0${reference.id}`;
13212
+ const operations = objectOperations.get(key) || {
13213
+ deleted: false,
13214
+ upserted: false
13215
+ };
13216
+ if (change.operation === "deleted") operations.deleted = true;
13217
+ else operations.upserted = true;
13218
+ if (operations.deleted && operations.upserted) {
13219
+ throw new Error(
13220
+ `projection.changes cannot both delete and upsert ${reference.className}/${reference.id}; return only its canonical final state`
13221
+ );
13222
+ }
13223
+ objectOperations.set(key, operations);
13224
+ }
13225
+ const outcome = projection.outcome;
13226
+ if (declaration.kind === "transition") {
13227
+ if (!outcome) {
13228
+ throw new Error("A transition projection must return an outcome");
13229
+ }
13230
+ requireNonEmptyString(outcome.key, "projection.outcome.key");
13231
+ if (outcome.error) {
13232
+ validateBoundedJson(outcome.error, "projection.outcome.error");
13233
+ }
13234
+ } else if (projection.outcome !== void 0) {
13235
+ throw new Error("An effect projection cannot declare a transition outcome");
13236
+ }
13237
+ }
13238
+ function canonicalizeCommitValue(value) {
13239
+ const normalize = (current) => {
13240
+ if (current === null || typeof current === "boolean" || typeof current === "string") {
13241
+ return typeof current === "string" ? current.normalize("NFC") : current;
13242
+ }
13243
+ if (typeof current === "number") {
13244
+ if (!Number.isFinite(current)) {
13245
+ throw new Error("Cannot canonicalize a non-finite number");
13246
+ }
13247
+ return Object.is(current, -0) ? 0 : current;
13248
+ }
13249
+ if (Array.isArray(current)) return current.map(normalize);
13250
+ if (current && typeof current === "object") {
13251
+ return Object.fromEntries(
13252
+ Object.entries(current).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key.normalize("NFC"), normalize(item)])
13253
+ );
13254
+ }
13255
+ throw new Error(`Cannot canonicalize ${typeof current}`);
13256
+ };
13257
+ return JSON.stringify(normalize(value));
13258
+ }
13259
+
13012
13260
  // src/effect-runtime.ts
13013
13261
  function computeEffectKey(effect) {
13014
13262
  const attachedClass = effect.className?.trim();
@@ -13073,6 +13321,45 @@ function resolveInvocationMode(context) {
13073
13321
  }
13074
13322
  return "execute";
13075
13323
  }
13324
+ async function sha256Hex(value) {
13325
+ const digest = await globalThis.crypto.subtle.digest(
13326
+ "SHA-256",
13327
+ new TextEncoder().encode(value)
13328
+ );
13329
+ return Array.from(
13330
+ new Uint8Array(digest),
13331
+ (byte) => byte.toString(16).padStart(2, "0")
13332
+ ).join("");
13333
+ }
13334
+ async function resolveInvocationIdempotencyKey(request) {
13335
+ const supplied = request.context?.idempotencyKey?.trim();
13336
+ if (supplied) return supplied;
13337
+ const invocationId = request.context?.invocationId?.trim();
13338
+ if (!invocationId) {
13339
+ throw new Error(
13340
+ `Committed effect ${request.effectKey} requires an invocation id`
13341
+ );
13342
+ }
13343
+ const digest = await sha256Hex(
13344
+ canonicalizeCommitValue({
13345
+ sandboxId: request.context?.sandboxId || "",
13346
+ environmentId: request.context?.environmentId || "",
13347
+ effectKey: request.effectKey,
13348
+ invocationId,
13349
+ input: request.input
13350
+ })
13351
+ );
13352
+ return `gci_${digest}`;
13353
+ }
13354
+ function createUnavailableCommitContext(message) {
13355
+ const unavailable = async () => {
13356
+ throw new Error(message);
13357
+ };
13358
+ return {
13359
+ effect: unavailable,
13360
+ transition: unavailable
13361
+ };
13362
+ }
13076
13363
  function resolveReverseEffect(effectMap, currentEffect, request, behaviors) {
13077
13364
  const explicitReverseHandler = request.context?.invocation?.reverseHandler?.trim();
13078
13365
  const configuredReverseHandler = behaviors.reverse?.handler?.trim();
@@ -13144,22 +13431,32 @@ function resolveHandlerForMode(effectMap, effect, request) {
13144
13431
  throw new Error(`Dry run is not supported for ${request.effectKey}`);
13145
13432
  }
13146
13433
  if (mode === "reverse") {
13434
+ const sourceEffectKey = request.context?.invocation?.sourceEffectKey?.trim();
13435
+ if (sourceEffectKey && sourceEffectKey !== request.effectKey) {
13436
+ return { effect, mode, handler: effect.handler };
13437
+ }
13438
+ const namedReverseHandler = request.context?.invocation?.reverseHandler?.trim() || behaviors.reverse?.handler?.trim();
13439
+ if (namedReverseHandler) {
13440
+ const reverseEffect = resolveReverseEffect(
13441
+ effectMap,
13442
+ effect,
13443
+ request,
13444
+ behaviors
13445
+ );
13446
+ if (reverseEffect) {
13447
+ return {
13448
+ effect: reverseEffect,
13449
+ mode,
13450
+ handler: reverseEffect.handler
13451
+ };
13452
+ }
13453
+ throw new Error(
13454
+ `Reverse effect ${namedReverseHandler} is not registered for ${request.effectKey}`
13455
+ );
13456
+ }
13147
13457
  if (effect.reverseHandler) {
13148
13458
  return { effect, mode, handler: effect.reverseHandler };
13149
13459
  }
13150
- const reverseEffect = resolveReverseEffect(
13151
- effectMap,
13152
- effect,
13153
- request,
13154
- behaviors
13155
- );
13156
- if (reverseEffect) {
13157
- return {
13158
- effect: reverseEffect,
13159
- mode,
13160
- handler: reverseEffect.reverseHandler || reverseEffect.handler
13161
- };
13162
- }
13163
13460
  throw new Error(
13164
13461
  `Reverse execution is not supported for ${request.effectKey}`
13165
13462
  );
@@ -13260,18 +13557,142 @@ async function invokeRegisteredEffect(effectMap, request, options = {}) {
13260
13557
  throw new Error(`Effect handler not found: ${request.effectKey}`);
13261
13558
  }
13262
13559
  const resolved = resolveHandlerForMode(effectMap, effect, request);
13560
+ const isPreResolvedNamedReverse = resolved.mode === "reverse" && Boolean(request.context?.invocation?.sourceEffectKey?.trim()) && request.context?.invocation?.sourceEffectKey?.trim() !== request.effectKey;
13561
+ const resolvedEffectKey = computeEffectKey(resolved.effect);
13562
+ if (resolved.mode === "reverse" && resolved.effect !== effect && (effect.commit || resolved.effect.commit)) {
13563
+ throw new Error(
13564
+ `Committed named reverse effect ${resolvedEffectKey} must be reserved and dispatched using its own effect key; customer code was not invoked`
13565
+ );
13566
+ }
13567
+ const declaration = resolved.effect.commit;
13568
+ const commitTransport = options.commit;
13569
+ const commitRequired = Boolean(declaration) && (resolved.mode === "execute" || resolved.mode === "reverse");
13570
+ if (commitRequired && !commitTransport) {
13571
+ throw new Error(
13572
+ `Committed effect ${request.effectKey} has no durable commit transport; customer code was not invoked`
13573
+ );
13574
+ }
13575
+ if (commitRequired && declaration?.kind === "transition" && !request.context?.invocation?.transition) {
13576
+ throw new Error(
13577
+ `Transition effect ${request.effectKey} has no resolved transition context; customer code was not invoked`
13578
+ );
13579
+ }
13580
+ const idempotencyKey = commitRequired ? await resolveInvocationIdempotencyKey(request) : request.context?.idempotencyKey;
13581
+ let commitStarted = false;
13582
+ let commitPromise = null;
13583
+ const beginCommit = (requestedKind, productResult) => {
13584
+ if (!declaration || !commitRequired || !commitTransport) {
13585
+ return Promise.reject(
13586
+ new Error(
13587
+ `Effect ${request.effectKey} does not declare an active ${requestedKind} commit`
13588
+ )
13589
+ );
13590
+ }
13591
+ if (declaration.kind !== requestedKind) {
13592
+ return Promise.reject(
13593
+ new Error(
13594
+ `Effect ${request.effectKey} declares ${declaration.kind} commit, not ${requestedKind}`
13595
+ )
13596
+ );
13597
+ }
13598
+ if (commitStarted) {
13599
+ return Promise.reject(
13600
+ new Error(
13601
+ `Effect invocation ${request.context?.invocationId || request.effectKey} already emitted its commit`
13602
+ )
13603
+ );
13604
+ }
13605
+ commitStarted = true;
13606
+ commitPromise = (async () => {
13607
+ let projection;
13608
+ try {
13609
+ projection = declaration.project(productResult);
13610
+ validateProjectionResult(declaration, projection);
13611
+ } catch (error) {
13612
+ const message = error instanceof Error ? error.message : String(error);
13613
+ if (commitTransport.mappingFailed) {
13614
+ await commitTransport.mappingFailed({
13615
+ effectKey: request.effectKey,
13616
+ effectName: request.effectName,
13617
+ invocationId: request.context?.invocationId || "",
13618
+ idempotencyKey: idempotencyKey || "",
13619
+ environmentId: request.context?.environmentId || "",
13620
+ message
13621
+ });
13622
+ }
13623
+ throw new Error(
13624
+ `Product mutation may have succeeded, but its commit projection is invalid: ${message}`
13625
+ );
13626
+ }
13627
+ if (requestedKind === "transition") {
13628
+ const transition = request.context?.invocation?.transition;
13629
+ const outcome = projection.outcome;
13630
+ if (!transition || !outcome?.key) {
13631
+ throw new Error(
13632
+ `Transition effect ${request.effectKey} did not resolve an authored outcome`
13633
+ );
13634
+ }
13635
+ if (!Object.prototype.hasOwnProperty.call(
13636
+ transition.outcomes,
13637
+ outcome.key
13638
+ )) {
13639
+ throw new Error(
13640
+ `Transition ${transition.machine}.${transition.transition} does not declare outcome ${outcome.key}`
13641
+ );
13642
+ }
13643
+ }
13644
+ const invocationId = request.context?.invocationId || "";
13645
+ const environmentId = request.context?.environmentId || "";
13646
+ const sandboxId = request.context?.sandboxId || "";
13647
+ if (!invocationId || !environmentId || !sandboxId || !idempotencyKey) {
13648
+ throw new Error(
13649
+ `Committed effect ${request.effectKey} is missing its trusted invocation scope`
13650
+ );
13651
+ }
13652
+ const commitRequest = {
13653
+ kind: requestedKind,
13654
+ effectKey: request.effectKey,
13655
+ effectName: request.effectName,
13656
+ operationLabel: resolved.effect.label || resolved.effect.name,
13657
+ invocationId,
13658
+ idempotencyKey,
13659
+ sandboxId,
13660
+ environmentId,
13661
+ ...request.context?.sessionId ? { sessionId: request.context.sessionId } : {},
13662
+ ...request.context?.jobId ? { jobId: request.context.jobId } : {},
13663
+ ...request.context?.buildId ? { buildId: request.context.buildId } : {},
13664
+ ...request.context?.invocation?.artifactId ? { artifactId: request.context.invocation.artifactId } : {},
13665
+ projection,
13666
+ ...requestedKind === "transition" && request.context?.invocation?.transition ? { transition: request.context.invocation.transition } : {}
13667
+ };
13668
+ const receipt = await commitTransport.persist(commitRequest);
13669
+ await options.commitAcknowledged?.(receipt);
13670
+ return receipt;
13671
+ })();
13672
+ return commitPromise;
13673
+ };
13674
+ const commitContext = commitRequired ? {
13675
+ effect: (productResult) => beginCommit("effect", productResult),
13676
+ transition: (productResult) => beginCommit("transition", productResult)
13677
+ } : createUnavailableCommitContext(
13678
+ `Effect ${request.effectKey} is not executing a declared product commit`
13679
+ );
13263
13680
  const context = {
13264
13681
  ...request.context || {},
13682
+ ...idempotencyKey ? { idempotencyKey } : {},
13683
+ commit: commitContext,
13265
13684
  behaviors: normalizeEffectBehaviors(
13266
13685
  request.context?.behaviors || effect.metamodels || void 0
13267
13686
  ),
13268
13687
  invocation: {
13269
13688
  mode: resolved.mode,
13270
- sourceEffectKey: request.effectKey,
13271
- sourceEffectName: request.effectName,
13689
+ ...commitRequired && declaration ? { commitKind: declaration.kind } : {},
13690
+ sourceEffectKey: isPreResolvedNamedReverse ? request.context?.invocation?.sourceEffectKey || request.effectKey : request.effectKey,
13691
+ sourceEffectName: isPreResolvedNamedReverse ? request.context?.invocation?.sourceEffectName || request.effectName : request.effectName,
13272
13692
  ...request.context?.invocation?.reverseHandler ? { reverseHandler: request.context.invocation.reverseHandler } : {},
13273
13693
  ...request.context?.invocation?.artifactId ? { artifactId: request.context.invocation.artifactId } : {},
13274
- ...request.context?.invocation?.artifactOptions ? { artifactOptions: request.context.invocation.artifactOptions } : {}
13694
+ ...request.context?.invocation?.artifactOptions ? { artifactOptions: request.context.invocation.artifactOptions } : {},
13695
+ ...request.context?.invocation?.transition ? { transition: request.context.invocation.transition } : {}
13275
13696
  }
13276
13697
  };
13277
13698
  const feedbackContext = options.feedback ? createInvocationFeedbackContext(options.feedback) : null;
@@ -13295,6 +13716,16 @@ async function invokeRegisteredEffect(effectMap, request, options = {}) {
13295
13716
  handlerFailed = true;
13296
13717
  handlerError = error;
13297
13718
  }
13719
+ let commitError;
13720
+ let commitFailed = false;
13721
+ if (commitPromise) {
13722
+ try {
13723
+ await commitPromise;
13724
+ } catch (error) {
13725
+ commitFailed = true;
13726
+ commitError = error;
13727
+ }
13728
+ }
13298
13729
  let feedbackError;
13299
13730
  let feedbackFailed = false;
13300
13731
  if (feedbackContext) {
@@ -13308,9 +13739,17 @@ async function invokeRegisteredEffect(effectMap, request, options = {}) {
13308
13739
  if (handlerFailed) {
13309
13740
  throw handlerError;
13310
13741
  }
13742
+ if (commitFailed) {
13743
+ throw commitError;
13744
+ }
13311
13745
  if (feedbackFailed) {
13312
13746
  throw feedbackError;
13313
13747
  }
13748
+ if (commitRequired && !commitStarted) {
13749
+ throw new Error(
13750
+ `Mutating effect ${request.effectKey} returned without acknowledging its product mutation`
13751
+ );
13752
+ }
13314
13753
  return handlerResult;
13315
13754
  }
13316
13755
 
@@ -13379,12 +13818,7 @@ function toRecordSearchResult(className, node) {
13379
13818
  (field) => normalizeGraphPathSegment(field.name) === "real_id" && typeof field.value === "string" && field.value.trim()
13380
13819
  );
13381
13820
  const id = typeof realIdField?.value === "string" ? realIdField.value.trim() : graphPathId;
13382
- const rawLabel = typeof node.label === "string" && node.label.trim() ? node.label : "";
13383
- if (fields.length === 0 && rawLabel && isPlaceholderRecordLabel(rawLabel, graphPathId, path2)) {
13384
- return null;
13385
- }
13386
- const fallbackLabel = displayLabelFromFields(fields);
13387
- const label = rawLabel && !isPlaceholderRecordLabel(rawLabel, id, path2) ? rawLabel : fallbackLabel || rawLabel || id;
13821
+ const label = typeof node.label === "string" && node.label.trim() ? node.label.trim() : path2 || id;
13388
13822
  return {
13389
13823
  path: path2,
13390
13824
  className,
@@ -13394,30 +13828,6 @@ function toRecordSearchResult(className, node) {
13394
13828
  fields
13395
13829
  };
13396
13830
  }
13397
- function isPlaceholderRecordLabel(label, id, path2) {
13398
- const normalizedLabel = normalizeGraphPathSegment(label);
13399
- return normalizedLabel === normalizeGraphPathSegment(id) || normalizedLabel === normalizeGraphPathSegment(path2);
13400
- }
13401
- function displayLabelFromFields(fields) {
13402
- const preferredFieldNames = [
13403
- "name",
13404
- "title",
13405
- "label",
13406
- "display_name",
13407
- "file_name",
13408
- "number",
13409
- "code"
13410
- ];
13411
- for (const preferred of preferredFieldNames) {
13412
- const match = fields.find(
13413
- (field) => normalizeGraphPathSegment(field.name) === preferred && typeof field.value === "string" && field.value.trim()
13414
- );
13415
- if (typeof match?.value === "string") {
13416
- return match.value.trim();
13417
- }
13418
- }
13419
- return null;
13420
- }
13421
13831
  function normalizeRecordSearchText(value) {
13422
13832
  return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, " ").replace(/\s+/g, " ").trim();
13423
13833
  }
@@ -14380,7 +14790,8 @@ function normalizeStateMachines(values) {
14380
14790
  requirements: parseJsonRecord(transition?.requirements) || parseJsonRecord(transition?.requirements_json),
14381
14791
  permission: parseJsonValue(transition?.permission) ?? parseJsonValue(transition?.permission_json),
14382
14792
  risk: transition?.risk === "low" || transition?.risk === "medium" || transition?.risk === "high" ? transition.risk : null,
14383
- expectedOutcome: parseJsonValue(transition?.expectedOutcome) ?? parseJsonValue(transition?.expected_outcome_json)
14793
+ expectedOutcome: parseJsonValue(transition?.expectedOutcome) ?? parseJsonValue(transition?.expected_outcome_json),
14794
+ outcomes: parseJsonRecord(transition?.outcomes) || parseJsonRecord(transition?.outcomes_json)
14384
14795
  })).filter(
14385
14796
  (transition) => transition.name.length > 0 && transition.from.length > 0 && transition.to.length > 0
14386
14797
  );
@@ -14473,6 +14884,11 @@ function transitionMetadataGraphqlArgs(transition) {
14473
14884
  `expected_outcome_json: ${JSON.stringify(JSON.stringify(transition.expectedOutcome))}`
14474
14885
  );
14475
14886
  }
14887
+ if (transition.outcomes) {
14888
+ args.push(
14889
+ `outcomes_json: ${JSON.stringify(JSON.stringify(transition.outcomes))}`
14890
+ );
14891
+ }
14476
14892
  return args.length > 0 ? `, ${args.join(", ")}` : "";
14477
14893
  }
14478
14894
  function buildStateMachineModelMutations(modelPath, machines) {
@@ -14751,7 +15167,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14751
15167
  name: String!
14752
15168
  state_machine: StateMachine!
14753
15169
  add_state(name: String!, is_final: Boolean, label: String, description: String): StateMachineMutation!
14754
- add_transition(name: String!, from: String!, to: String!, label: String, description: String, action_json: String, assignee_json: String, requirements_json: String, permission_json: String, risk: String, expected_outcome_json: String): StateMachineMutation!
15170
+ add_transition(name: String!, from: String!, to: String!, label: String, description: String, action_json: String, assignee_json: String, requirements_json: String, permission_json: String, risk: String, expected_outcome_json: String, outcomes_json: String): StateMachineMutation!
14755
15171
  activate_transition(name: String!): StateMachineMutation!
14756
15172
  }
14757
15173
 
@@ -14768,6 +15184,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14768
15184
  type StateMachineSnapshotMutation {
14769
15185
  snapshot: StateMachineSnapshot!
14770
15186
  activate_transition(name: String!): StateMachineSnapshotMutation!
15187
+ commit_transition(name: String!, outcome: String!, to: String!, commit_id: String!, source_version: String): StateMachineSnapshotMutation!
14771
15188
  observe_state(state: String!, force: Boolean, source: String): StateMachineSnapshotMutation!
14772
15189
  }
14773
15190
 
@@ -14814,6 +15231,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14814
15231
  permission_json: String
14815
15232
  risk: String
14816
15233
  expected_outcome_json: String
15234
+ outcomes_json: String
14817
15235
  }
14818
15236
 
14819
15237
  type StateMachinePath {
@@ -14824,6 +15242,10 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14824
15242
  type StateMachineTransitionEvent {
14825
15243
  sequence: Int!
14826
15244
  occurred_at: Float!
15245
+ commit_id: String
15246
+ outcome: String
15247
+ source_version: String
15248
+ projected_from_mismatch: String
14827
15249
  transition: StateMachineTransition!
14828
15250
  from: StateMachineState!
14829
15251
  to: StateMachineState!
@@ -14889,7 +15311,8 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14889
15311
  requirements_json,
14890
15312
  permission_json,
14891
15313
  risk,
14892
- expected_outcome_json
15314
+ expected_outcome_json,
15315
+ outcomes_json
14893
15316
  }) => {
14894
15317
  await run(
14895
15318
  value.target.add_state_machine_transition(
@@ -14905,7 +15328,8 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14905
15328
  requirementsJson: requirements_json,
14906
15329
  permissionJson: permission_json,
14907
15330
  risk,
14908
- expectedOutcomeJson: expected_outcome_json
15331
+ expectedOutcomeJson: expected_outcome_json,
15332
+ outcomesJson: outcomes_json
14909
15333
  }
14910
15334
  )
14911
15335
  );
@@ -14926,6 +15350,19 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14926
15350
  );
14927
15351
  return value;
14928
15352
  },
15353
+ commit_transition: async (value, { name, outcome, to, commit_id, source_version }) => {
15354
+ await run(
15355
+ value.target.commit_state_machine_transition(
15356
+ value.name,
15357
+ name,
15358
+ outcome,
15359
+ to,
15360
+ commit_id,
15361
+ source_version
15362
+ )
15363
+ );
15364
+ return value;
15365
+ },
14929
15366
  observe_state: async (value, { state, force, source }) => {
14930
15367
  await run(
14931
15368
  value.target.observe_state_machine_state(
@@ -14955,7 +15392,8 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14955
15392
  requirements_json: (value) => value.requirements_json || null,
14956
15393
  permission_json: (value) => value.permission_json || null,
14957
15394
  risk: (value) => value.risk || null,
14958
- expected_outcome_json: (value) => value.expected_outcome_json || null
15395
+ expected_outcome_json: (value) => value.expected_outcome_json || null,
15396
+ outcomes_json: (value) => value.outcomes_json || null
14959
15397
  },
14960
15398
  StateMachinePath: {
14961
15399
  states: (value) => value.states,
@@ -14964,6 +15402,10 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14964
15402
  StateMachineTransitionEvent: {
14965
15403
  sequence: (value) => value.sequence,
14966
15404
  occurred_at: (value) => value.occurred_at,
15405
+ commit_id: (value) => value.commit_id || null,
15406
+ outcome: (value) => value.outcome || null,
15407
+ source_version: (value) => value.source_version || null,
15408
+ projected_from_mismatch: (value) => value.projected_from_mismatch || null,
14967
15409
  transition: (value) => value.transition,
14968
15410
  from: (value) => value.from,
14969
15411
  to: (value) => value.to
@@ -15037,6 +15479,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
15037
15479
  permission_json
15038
15480
  risk
15039
15481
  expected_outcome_json
15482
+ outcomes_json
15040
15483
  }
15041
15484
  }`
15042
15485
  ]
@@ -15666,6 +16109,133 @@ var Environment = class _Environment {
15666
16109
  getAwaitingCount: async () => this.getAwaitingRecordCount()
15667
16110
  };
15668
16111
  }
16112
+ /**
16113
+ * Acknowledge a declared product mutation that already happened outside a
16114
+ * Granular-run effect (for example, in a webhook consumer). These methods
16115
+ * run the declaration's pure projection mapper; they never call its handler.
16116
+ */
16117
+ get commit() {
16118
+ return {
16119
+ effect: async (effect, productResult, options = {}) => this.persistExternalEffect(effect, productResult, options),
16120
+ transition: async (transition, productResult, options = {}) => this.persistExternalTransition(transition, productResult, options),
16121
+ get: async (commitId) => this.controlPlaneRequest(
16122
+ `/control/environments/${this.environmentId}/commits/${encodeURIComponent(
16123
+ commitId
16124
+ )}`
16125
+ ),
16126
+ retry: async (commitId) => this.controlPlaneRequest(
16127
+ `/control/environments/${this.environmentId}/commits/${encodeURIComponent(
16128
+ commitId
16129
+ )}/retry`,
16130
+ { method: "POST" }
16131
+ )
16132
+ };
16133
+ }
16134
+ /** Inspect or retry the agent synchronization of a product snapshot. */
16135
+ get observation() {
16136
+ return {
16137
+ get: async (observationId) => this.controlPlaneRequest(
16138
+ `/control/environments/${this.environmentId}/observations/${encodeURIComponent(
16139
+ observationId
16140
+ )}`
16141
+ ),
16142
+ retry: async (observationId) => this.controlPlaneRequest(
16143
+ `/control/environments/${this.environmentId}/observations/${encodeURIComponent(
16144
+ observationId
16145
+ )}/retry`,
16146
+ { method: "POST" }
16147
+ )
16148
+ };
16149
+ }
16150
+ async persistExternalEffect(effect, productResult, options) {
16151
+ const projection = effect.commit.project(productResult);
16152
+ validateProjectionResult(effect.commit, projection);
16153
+ this.requireExternalIdentity(projection.source.version, options);
16154
+ return this.controlPlaneRequest(
16155
+ `/control/environments/${this.environmentId}/external-commits`,
16156
+ {
16157
+ method: "POST",
16158
+ body: JSON.stringify({
16159
+ kind: "effect",
16160
+ effectKey: computeEffectKey2(effect),
16161
+ ...options.sourceEventId?.trim() ? { sourceEventId: options.sourceEventId.trim() } : {},
16162
+ projection
16163
+ })
16164
+ }
16165
+ );
16166
+ }
16167
+ async persistExternalTransition(transition, productResult, options) {
16168
+ const metadata = getDefinedTransitionMetadata(transition);
16169
+ if (!metadata) {
16170
+ throw new Error(
16171
+ "environment.commit.transition requires a transition returned by defineStateMachine"
16172
+ );
16173
+ }
16174
+ const projection = transition.effect.commit.project(productResult);
16175
+ validateProjectionResult(transition.effect.commit, projection);
16176
+ this.requireExternalIdentity(projection.source.version, options);
16177
+ if (!Object.prototype.hasOwnProperty.call(
16178
+ transition.outcomes,
16179
+ projection.outcome.key
16180
+ )) {
16181
+ throw new Error(
16182
+ `Transition ${metadata.machine}.${metadata.transition} does not declare outcome ${projection.outcome.key}`
16183
+ );
16184
+ }
16185
+ if (!projection.primaryTarget) {
16186
+ throw new Error(
16187
+ "An external transition projection requires primaryTarget to identify the transitioned product record"
16188
+ );
16189
+ }
16190
+ return this.controlPlaneRequest(
16191
+ `/control/environments/${this.environmentId}/external-commits`,
16192
+ {
16193
+ method: "POST",
16194
+ body: JSON.stringify({
16195
+ kind: "transition",
16196
+ effectKey: computeEffectKey2(transition.effect),
16197
+ ...options.sourceEventId?.trim() ? { sourceEventId: options.sourceEventId.trim() } : {},
16198
+ projection,
16199
+ transition: {
16200
+ className: projection.primaryTarget.className,
16201
+ objectId: projection.primaryTarget.id,
16202
+ ...projection.primaryTarget.path ? { objectPath: projection.primaryTarget.path } : {},
16203
+ machine: metadata.machine,
16204
+ transition: metadata.transition,
16205
+ from: transition.from
16206
+ }
16207
+ })
16208
+ }
16209
+ );
16210
+ }
16211
+ requireExternalIdentity(sourceVersion, options) {
16212
+ if (!options.sourceEventId?.trim() && !sourceVersion?.trim()) {
16213
+ throw new Error(
16214
+ "An external commit requires sourceEventId or a source version from its projection mapper"
16215
+ );
16216
+ }
16217
+ }
16218
+ /**
16219
+ * Synchronize a versioned product snapshot without claiming an effect or
16220
+ * lifecycle transition. This records no transition history.
16221
+ */
16222
+ async observe(mapper, productResult) {
16223
+ const declaration = { kind: "effect"};
16224
+ const projection = mapper(productResult);
16225
+ validateProjectionResult(declaration, projection);
16226
+ if (!projection.source.version?.trim()) {
16227
+ throw new Error(
16228
+ "environment.observe requires a monotonic source version or serialized adapter sequence"
16229
+ );
16230
+ }
16231
+ return this.controlPlaneRequest(
16232
+ `/control/environments/${this.environmentId}/observations`,
16233
+ {
16234
+ method: "POST",
16235
+ body: JSON.stringify({ projection })
16236
+ }
16237
+ );
16238
+ }
15669
16239
  /**
15670
16240
  * Mirror product-owned workflow state into Granular without making Granular
15671
16241
  * own the customer application's state machine.
@@ -17072,6 +17642,14 @@ var EnvironmentSession = class extends Session {
17072
17642
  }
17073
17643
  };
17074
17644
  }
17645
+ get mutations() {
17646
+ return {
17647
+ list: (options = {}) => this.sessionDataRequest(
17648
+ "/mutations",
17649
+ options
17650
+ )
17651
+ };
17652
+ }
17075
17653
  get artifacts() {
17076
17654
  return {
17077
17655
  list: (options = {}) => {
@@ -18571,6 +19149,7 @@ var Granular = class _Granular {
18571
19149
  const serialized = {
18572
19150
  effectKey: computeEffectKey2(effect),
18573
19151
  name: effect.name,
19152
+ ...effect.label ? { label: effect.label } : {},
18574
19153
  description: effect.description,
18575
19154
  inputSchema: effect.inputSchema,
18576
19155
  stability: effect.stability || "stable",
@@ -18594,6 +19173,9 @@ var Granular = class _Granular {
18594
19173
  if (effect.metamodels !== void 0) {
18595
19174
  serialized.metamodels = effect.metamodels;
18596
19175
  }
19176
+ if (effect.commit !== void 0) {
19177
+ serialized.commit = { kind: effect.commit.kind };
19178
+ }
18597
19179
  return serialized;
18598
19180
  }
18599
19181
  async publishSandboxEffectCatalog(host) {
@@ -18814,16 +19396,60 @@ var Granular = class _Granular {
18814
19396
  };
18815
19397
  wsClient.registerRpcHandler("effect.invoke", async (params) => {
18816
19398
  const request = params;
18817
- return invokeRegisteredEffect(
19399
+ let commitReceipt;
19400
+ const result = await invokeRegisteredEffect(
18818
19401
  this.getSandboxEffectMap(sandboxId),
18819
19402
  request,
18820
19403
  {
19404
+ commitAcknowledged: (receipt) => {
19405
+ commitReceipt = receipt;
19406
+ },
19407
+ commit: {
19408
+ persist: (commitRequest) => this.request(
19409
+ `/control/environments/${encodeURIComponent(
19410
+ commitRequest.environmentId
19411
+ )}/commits`,
19412
+ {
19413
+ method: "POST",
19414
+ body: JSON.stringify({
19415
+ kind: commitRequest.kind,
19416
+ invocationId: commitRequest.invocationId,
19417
+ idempotencyKey: commitRequest.idempotencyKey,
19418
+ effectKey: commitRequest.effectKey,
19419
+ projection: commitRequest.projection
19420
+ })
19421
+ }
19422
+ ),
19423
+ mappingFailed: (failure) => this.request(
19424
+ `/control/environments/${encodeURIComponent(
19425
+ failure.environmentId
19426
+ )}/commit-invocations/${encodeURIComponent(
19427
+ failure.invocationId
19428
+ )}`,
19429
+ {
19430
+ method: "PATCH",
19431
+ body: JSON.stringify({
19432
+ status: "mapping_failed",
19433
+ error: {
19434
+ code: "commit_projection_mapping_failed",
19435
+ message: failure.message,
19436
+ retryable: false
19437
+ }
19438
+ })
19439
+ }
19440
+ )
19441
+ },
18821
19442
  feedback: {
18822
19443
  invocationId: request.callId,
18823
19444
  publish: (method, publishParams) => wsClient.call(method, publishParams)
18824
19445
  }
18825
19446
  }
18826
19447
  );
19448
+ return {
19449
+ __granularEffectInvocationResult: true,
19450
+ result,
19451
+ ...commitReceipt ? { commit: commitReceipt } : {}
19452
+ };
18827
19453
  });
18828
19454
  wsClient.on("open", () => {
18829
19455
  void this.synchronizeEffectHost(host).catch((error) => {