@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.
@@ -6508,6 +6508,11 @@ var Session = class {
6508
6508
  }
6509
6509
  }
6510
6510
  buildDirectedInvocationEffectContext(params, feedbackContext) {
6511
+ const commitUnavailable = async () => {
6512
+ throw new Error(
6513
+ "This directed browser tool invocation has no product commit transport"
6514
+ );
6515
+ };
6511
6516
  return {
6512
6517
  effectClientId: this.clientId,
6513
6518
  sandboxId: params.sandboxId || "",
@@ -6520,6 +6525,10 @@ var Session = class {
6520
6525
  userId: "",
6521
6526
  subjectId: ""
6522
6527
  },
6528
+ commit: {
6529
+ effect: commitUnavailable,
6530
+ transition: commitUnavailable
6531
+ },
6523
6532
  ...feedbackContext ? {
6524
6533
  feedback: feedbackContext.feedback,
6525
6534
  transientFeedback: feedbackContext.transientFeedback
@@ -12291,6 +12300,12 @@ var StateTransitionExpectedOutcomeSchema = external_exports.union([
12291
12300
  summary: external_exports.string().optional()
12292
12301
  }).strict()
12293
12302
  ]);
12303
+ var StateTransitionOutcomeSchema = external_exports.object({
12304
+ label: external_exports.string().min(1).optional(),
12305
+ to: external_exports.string().min(1),
12306
+ primary: external_exports.boolean().optional(),
12307
+ disposition: external_exports.enum(["continue", "error"])
12308
+ }).strict();
12294
12309
  var StateMachineTransitionSchema = external_exports.object({
12295
12310
  name: external_exports.string().min(1),
12296
12311
  from: external_exports.string().min(1),
@@ -12302,7 +12317,8 @@ var StateMachineTransitionSchema = external_exports.object({
12302
12317
  requirements: StateTransitionRequirementsSchema.optional(),
12303
12318
  permission: StateTransitionPermissionSchema.optional(),
12304
12319
  risk: external_exports.enum(["low", "medium", "high"]).optional(),
12305
- expectedOutcome: StateTransitionExpectedOutcomeSchema.optional()
12320
+ expectedOutcome: StateTransitionExpectedOutcomeSchema.optional(),
12321
+ outcomes: external_exports.record(external_exports.string().min(1), StateTransitionOutcomeSchema).optional()
12306
12322
  }).strict();
12307
12323
  external_exports.object({
12308
12324
  name: external_exports.string().min(1),
@@ -12983,6 +12999,238 @@ var effectBehaviorsMetamodelPackage = defineMetamodelPackage({
12983
12999
  }
12984
13000
  });
12985
13001
 
13002
+ // src/commit.ts
13003
+ var MAX_PROJECTION_CHANGES = 1e3;
13004
+ var MAX_SAFE_SUMMARY_KEYS = 32;
13005
+ var MAX_SAFE_TEXT_LENGTH = 2e3;
13006
+ var definedTransitionMetadata = /* @__PURE__ */ new WeakMap();
13007
+ function getDefinedTransitionMetadata(transition) {
13008
+ return definedTransitionMetadata.get(transition);
13009
+ }
13010
+ function requireNonEmptyString(value, path2) {
13011
+ if (typeof value !== "string" || value.trim().length === 0) {
13012
+ throw new Error(`${path2} must be a non-empty string`);
13013
+ }
13014
+ return value.trim();
13015
+ }
13016
+ function validateObjectReference(value, path2) {
13017
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
13018
+ throw new Error(`${path2} must be an object reference`);
13019
+ }
13020
+ const reference = value;
13021
+ requireNonEmptyString(reference.className, `${path2}.className`);
13022
+ requireNonEmptyString(reference.id, `${path2}.id`);
13023
+ if (reference.path !== void 0) {
13024
+ requireNonEmptyString(reference.path, `${path2}.path`);
13025
+ }
13026
+ }
13027
+ function validateScalarRecord(value, path2) {
13028
+ if (!value || typeof value !== "object" || Array.isArray(value) || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) {
13029
+ throw new Error(`${path2} must be an object of scalar values`);
13030
+ }
13031
+ for (const [key, item] of Object.entries(value)) {
13032
+ if (item !== null && typeof item !== "string" && typeof item !== "number" && typeof item !== "boolean") {
13033
+ throw new Error(
13034
+ `${path2}.${key} must be a string, finite number, boolean, or null`
13035
+ );
13036
+ }
13037
+ if (typeof item === "number" && !Number.isFinite(item)) {
13038
+ throw new Error(`${path2}.${key} must be finite`);
13039
+ }
13040
+ }
13041
+ }
13042
+ function validateProjectedRecord(value, path2) {
13043
+ validateObjectReference(value, path2);
13044
+ const record = value;
13045
+ if (record.label !== void 0) {
13046
+ if (typeof record.label !== "string") {
13047
+ throw new Error(`${path2}.label must be a string`);
13048
+ }
13049
+ if (record.label.length > MAX_SAFE_TEXT_LENGTH) {
13050
+ throw new Error(
13051
+ `${path2}.label exceeds ${MAX_SAFE_TEXT_LENGTH} characters`
13052
+ );
13053
+ }
13054
+ }
13055
+ validateScalarRecord(record.fields, `${path2}.fields`);
13056
+ }
13057
+ function validateBoundedJson(value, path2, depth = 0) {
13058
+ if (depth > 12) throw new Error(`${path2} is nested too deeply`);
13059
+ if (value === null || typeof value === "boolean") return;
13060
+ if (typeof value === "number") {
13061
+ if (!Number.isFinite(value)) throw new Error(`${path2} must be finite`);
13062
+ return;
13063
+ }
13064
+ if (typeof value === "string") {
13065
+ if (value.length > MAX_SAFE_TEXT_LENGTH) {
13066
+ throw new Error(`${path2} exceeds ${MAX_SAFE_TEXT_LENGTH} characters`);
13067
+ }
13068
+ return;
13069
+ }
13070
+ if (Array.isArray(value)) {
13071
+ if (value.length > MAX_PROJECTION_CHANGES) {
13072
+ throw new Error(`${path2} contains too many values`);
13073
+ }
13074
+ value.forEach(
13075
+ (item, index) => validateBoundedJson(item, `${path2}[${index}]`, depth + 1)
13076
+ );
13077
+ return;
13078
+ }
13079
+ if (!value || typeof value !== "object") {
13080
+ throw new Error(`${path2} contains an unsupported value`);
13081
+ }
13082
+ const entries = Object.entries(value);
13083
+ if (entries.length > 256) throw new Error(`${path2} contains too many keys`);
13084
+ for (const [key, item] of entries) {
13085
+ if (/token|secret|password|authorization|cookie/i.test(key)) {
13086
+ throw new Error(`${path2}.${key} is not allowed in a commit projection`);
13087
+ }
13088
+ validateBoundedJson(item, `${path2}.${key}`, depth + 1);
13089
+ }
13090
+ }
13091
+ function validateProjectionResult(declaration, projection) {
13092
+ if (!projection || typeof projection !== "object") {
13093
+ throw new Error("Projection mapper must return an object");
13094
+ }
13095
+ requireNonEmptyString(
13096
+ projection.source?.reference,
13097
+ "projection.source.reference"
13098
+ );
13099
+ if (projection.source.version !== void 0) {
13100
+ requireNonEmptyString(
13101
+ projection.source.version,
13102
+ "projection.source.version"
13103
+ );
13104
+ }
13105
+ if (!Array.isArray(projection.changes)) {
13106
+ throw new Error("projection.changes must be an array");
13107
+ }
13108
+ if (projection.changes.length > MAX_PROJECTION_CHANGES) {
13109
+ throw new Error(
13110
+ `projection.changes exceeds the ${MAX_PROJECTION_CHANGES} change limit`
13111
+ );
13112
+ }
13113
+ if (projection.primaryTarget) {
13114
+ validateObjectReference(
13115
+ projection.primaryTarget,
13116
+ "projection.primaryTarget"
13117
+ );
13118
+ }
13119
+ if (projection.safeSummary) {
13120
+ const entries = Object.entries(projection.safeSummary);
13121
+ if (entries.length > MAX_SAFE_SUMMARY_KEYS) {
13122
+ throw new Error(
13123
+ `projection.safeSummary exceeds the ${MAX_SAFE_SUMMARY_KEYS} key limit`
13124
+ );
13125
+ }
13126
+ validateBoundedJson(projection.safeSummary, "projection.safeSummary");
13127
+ validateScalarRecord(projection.safeSummary, "projection.safeSummary");
13128
+ }
13129
+ projection.changes.forEach((change, index) => {
13130
+ const changePath = `projection.changes[${index}]`;
13131
+ validateBoundedJson(change, changePath);
13132
+ if (!change || typeof change !== "object" || Array.isArray(change)) {
13133
+ throw new Error(`${changePath} must be an object`);
13134
+ }
13135
+ const rawChange = change;
13136
+ const kind = requireNonEmptyString(rawChange.kind, `${changePath}.kind`);
13137
+ if (kind === "object") {
13138
+ const operation = requireNonEmptyString(
13139
+ rawChange.operation,
13140
+ `${changePath}.operation`
13141
+ );
13142
+ if (operation !== "created" && operation !== "updated" && operation !== "deleted") {
13143
+ throw new Error(
13144
+ `${changePath}.operation must be created, updated, or deleted`
13145
+ );
13146
+ }
13147
+ if (operation === "deleted") {
13148
+ validateObjectReference(rawChange.target, `${changePath}.target`);
13149
+ } else {
13150
+ validateProjectedRecord(rawChange.record, `${changePath}.record`);
13151
+ }
13152
+ } else if (kind === "relationship") {
13153
+ const operation = requireNonEmptyString(
13154
+ rawChange.operation,
13155
+ `${changePath}.operation`
13156
+ );
13157
+ if (operation !== "connected" && operation !== "disconnected") {
13158
+ throw new Error(
13159
+ `${changePath}.operation must be connected or disconnected`
13160
+ );
13161
+ }
13162
+ requireNonEmptyString(
13163
+ rawChange.relationship,
13164
+ `${changePath}.relationship`
13165
+ );
13166
+ validateObjectReference(rawChange.from, `${changePath}.from`);
13167
+ validateObjectReference(rawChange.to, `${changePath}.to`);
13168
+ } else if (kind === "state_observation") {
13169
+ if (rawChange.operation !== void 0) {
13170
+ throw new Error(
13171
+ `${changePath}.operation is not valid for an observation`
13172
+ );
13173
+ }
13174
+ validateObjectReference(rawChange.target, `${changePath}.target`);
13175
+ requireNonEmptyString(rawChange.machine, `${changePath}.machine`);
13176
+ requireNonEmptyString(rawChange.state, `${changePath}.state`);
13177
+ } else {
13178
+ throw new Error(`${changePath}.kind is unsupported: ${kind}`);
13179
+ }
13180
+ });
13181
+ const objectOperations = /* @__PURE__ */ new Map();
13182
+ for (const change of projection.changes) {
13183
+ if (change.kind !== "object") continue;
13184
+ const reference = change.operation === "deleted" ? change.target : change.record;
13185
+ const key = `${reference.className}\0${reference.id}`;
13186
+ const operations = objectOperations.get(key) || {
13187
+ deleted: false,
13188
+ upserted: false
13189
+ };
13190
+ if (change.operation === "deleted") operations.deleted = true;
13191
+ else operations.upserted = true;
13192
+ if (operations.deleted && operations.upserted) {
13193
+ throw new Error(
13194
+ `projection.changes cannot both delete and upsert ${reference.className}/${reference.id}; return only its canonical final state`
13195
+ );
13196
+ }
13197
+ objectOperations.set(key, operations);
13198
+ }
13199
+ const outcome = projection.outcome;
13200
+ if (declaration.kind === "transition") {
13201
+ if (!outcome) {
13202
+ throw new Error("A transition projection must return an outcome");
13203
+ }
13204
+ requireNonEmptyString(outcome.key, "projection.outcome.key");
13205
+ if (outcome.error) {
13206
+ validateBoundedJson(outcome.error, "projection.outcome.error");
13207
+ }
13208
+ } else if (projection.outcome !== void 0) {
13209
+ throw new Error("An effect projection cannot declare a transition outcome");
13210
+ }
13211
+ }
13212
+ function canonicalizeCommitValue(value) {
13213
+ const normalize = (current) => {
13214
+ if (current === null || typeof current === "boolean" || typeof current === "string") {
13215
+ return typeof current === "string" ? current.normalize("NFC") : current;
13216
+ }
13217
+ if (typeof current === "number") {
13218
+ if (!Number.isFinite(current)) {
13219
+ throw new Error("Cannot canonicalize a non-finite number");
13220
+ }
13221
+ return Object.is(current, -0) ? 0 : current;
13222
+ }
13223
+ if (Array.isArray(current)) return current.map(normalize);
13224
+ if (current && typeof current === "object") {
13225
+ return Object.fromEntries(
13226
+ Object.entries(current).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key.normalize("NFC"), normalize(item)])
13227
+ );
13228
+ }
13229
+ throw new Error(`Cannot canonicalize ${typeof current}`);
13230
+ };
13231
+ return JSON.stringify(normalize(value));
13232
+ }
13233
+
12986
13234
  // src/effect-runtime.ts
12987
13235
  function computeEffectKey(effect) {
12988
13236
  const attachedClass = effect.className?.trim();
@@ -13047,6 +13295,45 @@ function resolveInvocationMode(context) {
13047
13295
  }
13048
13296
  return "execute";
13049
13297
  }
13298
+ async function sha256Hex(value) {
13299
+ const digest = await globalThis.crypto.subtle.digest(
13300
+ "SHA-256",
13301
+ new TextEncoder().encode(value)
13302
+ );
13303
+ return Array.from(
13304
+ new Uint8Array(digest),
13305
+ (byte) => byte.toString(16).padStart(2, "0")
13306
+ ).join("");
13307
+ }
13308
+ async function resolveInvocationIdempotencyKey(request) {
13309
+ const supplied = request.context?.idempotencyKey?.trim();
13310
+ if (supplied) return supplied;
13311
+ const invocationId = request.context?.invocationId?.trim();
13312
+ if (!invocationId) {
13313
+ throw new Error(
13314
+ `Committed effect ${request.effectKey} requires an invocation id`
13315
+ );
13316
+ }
13317
+ const digest = await sha256Hex(
13318
+ canonicalizeCommitValue({
13319
+ sandboxId: request.context?.sandboxId || "",
13320
+ environmentId: request.context?.environmentId || "",
13321
+ effectKey: request.effectKey,
13322
+ invocationId,
13323
+ input: request.input
13324
+ })
13325
+ );
13326
+ return `gci_${digest}`;
13327
+ }
13328
+ function createUnavailableCommitContext(message) {
13329
+ const unavailable = async () => {
13330
+ throw new Error(message);
13331
+ };
13332
+ return {
13333
+ effect: unavailable,
13334
+ transition: unavailable
13335
+ };
13336
+ }
13050
13337
  function resolveReverseEffect(effectMap, currentEffect, request, behaviors) {
13051
13338
  const explicitReverseHandler = request.context?.invocation?.reverseHandler?.trim();
13052
13339
  const configuredReverseHandler = behaviors.reverse?.handler?.trim();
@@ -13118,22 +13405,32 @@ function resolveHandlerForMode(effectMap, effect, request) {
13118
13405
  throw new Error(`Dry run is not supported for ${request.effectKey}`);
13119
13406
  }
13120
13407
  if (mode === "reverse") {
13408
+ const sourceEffectKey = request.context?.invocation?.sourceEffectKey?.trim();
13409
+ if (sourceEffectKey && sourceEffectKey !== request.effectKey) {
13410
+ return { effect, mode, handler: effect.handler };
13411
+ }
13412
+ const namedReverseHandler = request.context?.invocation?.reverseHandler?.trim() || behaviors.reverse?.handler?.trim();
13413
+ if (namedReverseHandler) {
13414
+ const reverseEffect = resolveReverseEffect(
13415
+ effectMap,
13416
+ effect,
13417
+ request,
13418
+ behaviors
13419
+ );
13420
+ if (reverseEffect) {
13421
+ return {
13422
+ effect: reverseEffect,
13423
+ mode,
13424
+ handler: reverseEffect.handler
13425
+ };
13426
+ }
13427
+ throw new Error(
13428
+ `Reverse effect ${namedReverseHandler} is not registered for ${request.effectKey}`
13429
+ );
13430
+ }
13121
13431
  if (effect.reverseHandler) {
13122
13432
  return { effect, mode, handler: effect.reverseHandler };
13123
13433
  }
13124
- const reverseEffect = resolveReverseEffect(
13125
- effectMap,
13126
- effect,
13127
- request,
13128
- behaviors
13129
- );
13130
- if (reverseEffect) {
13131
- return {
13132
- effect: reverseEffect,
13133
- mode,
13134
- handler: reverseEffect.reverseHandler || reverseEffect.handler
13135
- };
13136
- }
13137
13434
  throw new Error(
13138
13435
  `Reverse execution is not supported for ${request.effectKey}`
13139
13436
  );
@@ -13234,18 +13531,142 @@ async function invokeRegisteredEffect(effectMap, request, options = {}) {
13234
13531
  throw new Error(`Effect handler not found: ${request.effectKey}`);
13235
13532
  }
13236
13533
  const resolved = resolveHandlerForMode(effectMap, effect, request);
13534
+ const isPreResolvedNamedReverse = resolved.mode === "reverse" && Boolean(request.context?.invocation?.sourceEffectKey?.trim()) && request.context?.invocation?.sourceEffectKey?.trim() !== request.effectKey;
13535
+ const resolvedEffectKey = computeEffectKey(resolved.effect);
13536
+ if (resolved.mode === "reverse" && resolved.effect !== effect && (effect.commit || resolved.effect.commit)) {
13537
+ throw new Error(
13538
+ `Committed named reverse effect ${resolvedEffectKey} must be reserved and dispatched using its own effect key; customer code was not invoked`
13539
+ );
13540
+ }
13541
+ const declaration = resolved.effect.commit;
13542
+ const commitTransport = options.commit;
13543
+ const commitRequired = Boolean(declaration) && (resolved.mode === "execute" || resolved.mode === "reverse");
13544
+ if (commitRequired && !commitTransport) {
13545
+ throw new Error(
13546
+ `Committed effect ${request.effectKey} has no durable commit transport; customer code was not invoked`
13547
+ );
13548
+ }
13549
+ if (commitRequired && declaration?.kind === "transition" && !request.context?.invocation?.transition) {
13550
+ throw new Error(
13551
+ `Transition effect ${request.effectKey} has no resolved transition context; customer code was not invoked`
13552
+ );
13553
+ }
13554
+ const idempotencyKey = commitRequired ? await resolveInvocationIdempotencyKey(request) : request.context?.idempotencyKey;
13555
+ let commitStarted = false;
13556
+ let commitPromise = null;
13557
+ const beginCommit = (requestedKind, productResult) => {
13558
+ if (!declaration || !commitRequired || !commitTransport) {
13559
+ return Promise.reject(
13560
+ new Error(
13561
+ `Effect ${request.effectKey} does not declare an active ${requestedKind} commit`
13562
+ )
13563
+ );
13564
+ }
13565
+ if (declaration.kind !== requestedKind) {
13566
+ return Promise.reject(
13567
+ new Error(
13568
+ `Effect ${request.effectKey} declares ${declaration.kind} commit, not ${requestedKind}`
13569
+ )
13570
+ );
13571
+ }
13572
+ if (commitStarted) {
13573
+ return Promise.reject(
13574
+ new Error(
13575
+ `Effect invocation ${request.context?.invocationId || request.effectKey} already emitted its commit`
13576
+ )
13577
+ );
13578
+ }
13579
+ commitStarted = true;
13580
+ commitPromise = (async () => {
13581
+ let projection;
13582
+ try {
13583
+ projection = declaration.project(productResult);
13584
+ validateProjectionResult(declaration, projection);
13585
+ } catch (error) {
13586
+ const message = error instanceof Error ? error.message : String(error);
13587
+ if (commitTransport.mappingFailed) {
13588
+ await commitTransport.mappingFailed({
13589
+ effectKey: request.effectKey,
13590
+ effectName: request.effectName,
13591
+ invocationId: request.context?.invocationId || "",
13592
+ idempotencyKey: idempotencyKey || "",
13593
+ environmentId: request.context?.environmentId || "",
13594
+ message
13595
+ });
13596
+ }
13597
+ throw new Error(
13598
+ `Product mutation may have succeeded, but its commit projection is invalid: ${message}`
13599
+ );
13600
+ }
13601
+ if (requestedKind === "transition") {
13602
+ const transition = request.context?.invocation?.transition;
13603
+ const outcome = projection.outcome;
13604
+ if (!transition || !outcome?.key) {
13605
+ throw new Error(
13606
+ `Transition effect ${request.effectKey} did not resolve an authored outcome`
13607
+ );
13608
+ }
13609
+ if (!Object.prototype.hasOwnProperty.call(
13610
+ transition.outcomes,
13611
+ outcome.key
13612
+ )) {
13613
+ throw new Error(
13614
+ `Transition ${transition.machine}.${transition.transition} does not declare outcome ${outcome.key}`
13615
+ );
13616
+ }
13617
+ }
13618
+ const invocationId = request.context?.invocationId || "";
13619
+ const environmentId = request.context?.environmentId || "";
13620
+ const sandboxId = request.context?.sandboxId || "";
13621
+ if (!invocationId || !environmentId || !sandboxId || !idempotencyKey) {
13622
+ throw new Error(
13623
+ `Committed effect ${request.effectKey} is missing its trusted invocation scope`
13624
+ );
13625
+ }
13626
+ const commitRequest = {
13627
+ kind: requestedKind,
13628
+ effectKey: request.effectKey,
13629
+ effectName: request.effectName,
13630
+ operationLabel: resolved.effect.label || resolved.effect.name,
13631
+ invocationId,
13632
+ idempotencyKey,
13633
+ sandboxId,
13634
+ environmentId,
13635
+ ...request.context?.sessionId ? { sessionId: request.context.sessionId } : {},
13636
+ ...request.context?.jobId ? { jobId: request.context.jobId } : {},
13637
+ ...request.context?.buildId ? { buildId: request.context.buildId } : {},
13638
+ ...request.context?.invocation?.artifactId ? { artifactId: request.context.invocation.artifactId } : {},
13639
+ projection,
13640
+ ...requestedKind === "transition" && request.context?.invocation?.transition ? { transition: request.context.invocation.transition } : {}
13641
+ };
13642
+ const receipt = await commitTransport.persist(commitRequest);
13643
+ await options.commitAcknowledged?.(receipt);
13644
+ return receipt;
13645
+ })();
13646
+ return commitPromise;
13647
+ };
13648
+ const commitContext = commitRequired ? {
13649
+ effect: (productResult) => beginCommit("effect", productResult),
13650
+ transition: (productResult) => beginCommit("transition", productResult)
13651
+ } : createUnavailableCommitContext(
13652
+ `Effect ${request.effectKey} is not executing a declared product commit`
13653
+ );
13237
13654
  const context = {
13238
13655
  ...request.context || {},
13656
+ ...idempotencyKey ? { idempotencyKey } : {},
13657
+ commit: commitContext,
13239
13658
  behaviors: normalizeEffectBehaviors(
13240
13659
  request.context?.behaviors || effect.metamodels || void 0
13241
13660
  ),
13242
13661
  invocation: {
13243
13662
  mode: resolved.mode,
13244
- sourceEffectKey: request.effectKey,
13245
- sourceEffectName: request.effectName,
13663
+ ...commitRequired && declaration ? { commitKind: declaration.kind } : {},
13664
+ sourceEffectKey: isPreResolvedNamedReverse ? request.context?.invocation?.sourceEffectKey || request.effectKey : request.effectKey,
13665
+ sourceEffectName: isPreResolvedNamedReverse ? request.context?.invocation?.sourceEffectName || request.effectName : request.effectName,
13246
13666
  ...request.context?.invocation?.reverseHandler ? { reverseHandler: request.context.invocation.reverseHandler } : {},
13247
13667
  ...request.context?.invocation?.artifactId ? { artifactId: request.context.invocation.artifactId } : {},
13248
- ...request.context?.invocation?.artifactOptions ? { artifactOptions: request.context.invocation.artifactOptions } : {}
13668
+ ...request.context?.invocation?.artifactOptions ? { artifactOptions: request.context.invocation.artifactOptions } : {},
13669
+ ...request.context?.invocation?.transition ? { transition: request.context.invocation.transition } : {}
13249
13670
  }
13250
13671
  };
13251
13672
  const feedbackContext = options.feedback ? createInvocationFeedbackContext(options.feedback) : null;
@@ -13269,6 +13690,16 @@ async function invokeRegisteredEffect(effectMap, request, options = {}) {
13269
13690
  handlerFailed = true;
13270
13691
  handlerError = error;
13271
13692
  }
13693
+ let commitError;
13694
+ let commitFailed = false;
13695
+ if (commitPromise) {
13696
+ try {
13697
+ await commitPromise;
13698
+ } catch (error) {
13699
+ commitFailed = true;
13700
+ commitError = error;
13701
+ }
13702
+ }
13272
13703
  let feedbackError;
13273
13704
  let feedbackFailed = false;
13274
13705
  if (feedbackContext) {
@@ -13282,9 +13713,17 @@ async function invokeRegisteredEffect(effectMap, request, options = {}) {
13282
13713
  if (handlerFailed) {
13283
13714
  throw handlerError;
13284
13715
  }
13716
+ if (commitFailed) {
13717
+ throw commitError;
13718
+ }
13285
13719
  if (feedbackFailed) {
13286
13720
  throw feedbackError;
13287
13721
  }
13722
+ if (commitRequired && !commitStarted) {
13723
+ throw new Error(
13724
+ `Mutating effect ${request.effectKey} returned without acknowledging its product mutation`
13725
+ );
13726
+ }
13288
13727
  return handlerResult;
13289
13728
  }
13290
13729
 
@@ -13353,12 +13792,7 @@ function toRecordSearchResult(className, node) {
13353
13792
  (field) => normalizeGraphPathSegment(field.name) === "real_id" && typeof field.value === "string" && field.value.trim()
13354
13793
  );
13355
13794
  const id = typeof realIdField?.value === "string" ? realIdField.value.trim() : graphPathId;
13356
- const rawLabel = typeof node.label === "string" && node.label.trim() ? node.label : "";
13357
- if (fields.length === 0 && rawLabel && isPlaceholderRecordLabel(rawLabel, graphPathId, path2)) {
13358
- return null;
13359
- }
13360
- const fallbackLabel = displayLabelFromFields(fields);
13361
- const label = rawLabel && !isPlaceholderRecordLabel(rawLabel, id, path2) ? rawLabel : fallbackLabel || rawLabel || id;
13795
+ const label = typeof node.label === "string" && node.label.trim() ? node.label.trim() : path2 || id;
13362
13796
  return {
13363
13797
  path: path2,
13364
13798
  className,
@@ -13368,30 +13802,6 @@ function toRecordSearchResult(className, node) {
13368
13802
  fields
13369
13803
  };
13370
13804
  }
13371
- function isPlaceholderRecordLabel(label, id, path2) {
13372
- const normalizedLabel = normalizeGraphPathSegment(label);
13373
- return normalizedLabel === normalizeGraphPathSegment(id) || normalizedLabel === normalizeGraphPathSegment(path2);
13374
- }
13375
- function displayLabelFromFields(fields) {
13376
- const preferredFieldNames = [
13377
- "name",
13378
- "title",
13379
- "label",
13380
- "display_name",
13381
- "file_name",
13382
- "number",
13383
- "code"
13384
- ];
13385
- for (const preferred of preferredFieldNames) {
13386
- const match = fields.find(
13387
- (field) => normalizeGraphPathSegment(field.name) === preferred && typeof field.value === "string" && field.value.trim()
13388
- );
13389
- if (typeof match?.value === "string") {
13390
- return match.value.trim();
13391
- }
13392
- }
13393
- return null;
13394
- }
13395
13805
  function normalizeRecordSearchText(value) {
13396
13806
  return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, " ").replace(/\s+/g, " ").trim();
13397
13807
  }
@@ -14354,7 +14764,8 @@ function normalizeStateMachines(values) {
14354
14764
  requirements: parseJsonRecord(transition?.requirements) || parseJsonRecord(transition?.requirements_json),
14355
14765
  permission: parseJsonValue(transition?.permission) ?? parseJsonValue(transition?.permission_json),
14356
14766
  risk: transition?.risk === "low" || transition?.risk === "medium" || transition?.risk === "high" ? transition.risk : null,
14357
- expectedOutcome: parseJsonValue(transition?.expectedOutcome) ?? parseJsonValue(transition?.expected_outcome_json)
14767
+ expectedOutcome: parseJsonValue(transition?.expectedOutcome) ?? parseJsonValue(transition?.expected_outcome_json),
14768
+ outcomes: parseJsonRecord(transition?.outcomes) || parseJsonRecord(transition?.outcomes_json)
14358
14769
  })).filter(
14359
14770
  (transition) => transition.name.length > 0 && transition.from.length > 0 && transition.to.length > 0
14360
14771
  );
@@ -14447,6 +14858,11 @@ function transitionMetadataGraphqlArgs(transition) {
14447
14858
  `expected_outcome_json: ${JSON.stringify(JSON.stringify(transition.expectedOutcome))}`
14448
14859
  );
14449
14860
  }
14861
+ if (transition.outcomes) {
14862
+ args.push(
14863
+ `outcomes_json: ${JSON.stringify(JSON.stringify(transition.outcomes))}`
14864
+ );
14865
+ }
14450
14866
  return args.length > 0 ? `, ${args.join(", ")}` : "";
14451
14867
  }
14452
14868
  function buildStateMachineModelMutations(modelPath, machines) {
@@ -14725,7 +15141,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14725
15141
  name: String!
14726
15142
  state_machine: StateMachine!
14727
15143
  add_state(name: String!, is_final: Boolean, label: String, description: String): StateMachineMutation!
14728
- 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!
15144
+ 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!
14729
15145
  activate_transition(name: String!): StateMachineMutation!
14730
15146
  }
14731
15147
 
@@ -14742,6 +15158,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14742
15158
  type StateMachineSnapshotMutation {
14743
15159
  snapshot: StateMachineSnapshot!
14744
15160
  activate_transition(name: String!): StateMachineSnapshotMutation!
15161
+ commit_transition(name: String!, outcome: String!, to: String!, commit_id: String!, source_version: String): StateMachineSnapshotMutation!
14745
15162
  observe_state(state: String!, force: Boolean, source: String): StateMachineSnapshotMutation!
14746
15163
  }
14747
15164
 
@@ -14788,6 +15205,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14788
15205
  permission_json: String
14789
15206
  risk: String
14790
15207
  expected_outcome_json: String
15208
+ outcomes_json: String
14791
15209
  }
14792
15210
 
14793
15211
  type StateMachinePath {
@@ -14798,6 +15216,10 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14798
15216
  type StateMachineTransitionEvent {
14799
15217
  sequence: Int!
14800
15218
  occurred_at: Float!
15219
+ commit_id: String
15220
+ outcome: String
15221
+ source_version: String
15222
+ projected_from_mismatch: String
14801
15223
  transition: StateMachineTransition!
14802
15224
  from: StateMachineState!
14803
15225
  to: StateMachineState!
@@ -14863,7 +15285,8 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14863
15285
  requirements_json,
14864
15286
  permission_json,
14865
15287
  risk,
14866
- expected_outcome_json
15288
+ expected_outcome_json,
15289
+ outcomes_json
14867
15290
  }) => {
14868
15291
  await run(
14869
15292
  value.target.add_state_machine_transition(
@@ -14879,7 +15302,8 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14879
15302
  requirementsJson: requirements_json,
14880
15303
  permissionJson: permission_json,
14881
15304
  risk,
14882
- expectedOutcomeJson: expected_outcome_json
15305
+ expectedOutcomeJson: expected_outcome_json,
15306
+ outcomesJson: outcomes_json
14883
15307
  }
14884
15308
  )
14885
15309
  );
@@ -14900,6 +15324,19 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14900
15324
  );
14901
15325
  return value;
14902
15326
  },
15327
+ commit_transition: async (value, { name, outcome, to, commit_id, source_version }) => {
15328
+ await run(
15329
+ value.target.commit_state_machine_transition(
15330
+ value.name,
15331
+ name,
15332
+ outcome,
15333
+ to,
15334
+ commit_id,
15335
+ source_version
15336
+ )
15337
+ );
15338
+ return value;
15339
+ },
14903
15340
  observe_state: async (value, { state, force, source }) => {
14904
15341
  await run(
14905
15342
  value.target.observe_state_machine_state(
@@ -14929,7 +15366,8 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14929
15366
  requirements_json: (value) => value.requirements_json || null,
14930
15367
  permission_json: (value) => value.permission_json || null,
14931
15368
  risk: (value) => value.risk || null,
14932
- expected_outcome_json: (value) => value.expected_outcome_json || null
15369
+ expected_outcome_json: (value) => value.expected_outcome_json || null,
15370
+ outcomes_json: (value) => value.outcomes_json || null
14933
15371
  },
14934
15372
  StateMachinePath: {
14935
15373
  states: (value) => value.states,
@@ -14938,6 +15376,10 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14938
15376
  StateMachineTransitionEvent: {
14939
15377
  sequence: (value) => value.sequence,
14940
15378
  occurred_at: (value) => value.occurred_at,
15379
+ commit_id: (value) => value.commit_id || null,
15380
+ outcome: (value) => value.outcome || null,
15381
+ source_version: (value) => value.source_version || null,
15382
+ projected_from_mismatch: (value) => value.projected_from_mismatch || null,
14941
15383
  transition: (value) => value.transition,
14942
15384
  from: (value) => value.from,
14943
15385
  to: (value) => value.to
@@ -15011,6 +15453,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
15011
15453
  permission_json
15012
15454
  risk
15013
15455
  expected_outcome_json
15456
+ outcomes_json
15014
15457
  }
15015
15458
  }`
15016
15459
  ]
@@ -15640,6 +16083,133 @@ var Environment = class _Environment {
15640
16083
  getAwaitingCount: async () => this.getAwaitingRecordCount()
15641
16084
  };
15642
16085
  }
16086
+ /**
16087
+ * Acknowledge a declared product mutation that already happened outside a
16088
+ * Granular-run effect (for example, in a webhook consumer). These methods
16089
+ * run the declaration's pure projection mapper; they never call its handler.
16090
+ */
16091
+ get commit() {
16092
+ return {
16093
+ effect: async (effect, productResult, options = {}) => this.persistExternalEffect(effect, productResult, options),
16094
+ transition: async (transition, productResult, options = {}) => this.persistExternalTransition(transition, productResult, options),
16095
+ get: async (commitId) => this.controlPlaneRequest(
16096
+ `/control/environments/${this.environmentId}/commits/${encodeURIComponent(
16097
+ commitId
16098
+ )}`
16099
+ ),
16100
+ retry: async (commitId) => this.controlPlaneRequest(
16101
+ `/control/environments/${this.environmentId}/commits/${encodeURIComponent(
16102
+ commitId
16103
+ )}/retry`,
16104
+ { method: "POST" }
16105
+ )
16106
+ };
16107
+ }
16108
+ /** Inspect or retry the agent synchronization of a product snapshot. */
16109
+ get observation() {
16110
+ return {
16111
+ get: async (observationId) => this.controlPlaneRequest(
16112
+ `/control/environments/${this.environmentId}/observations/${encodeURIComponent(
16113
+ observationId
16114
+ )}`
16115
+ ),
16116
+ retry: async (observationId) => this.controlPlaneRequest(
16117
+ `/control/environments/${this.environmentId}/observations/${encodeURIComponent(
16118
+ observationId
16119
+ )}/retry`,
16120
+ { method: "POST" }
16121
+ )
16122
+ };
16123
+ }
16124
+ async persistExternalEffect(effect, productResult, options) {
16125
+ const projection = effect.commit.project(productResult);
16126
+ validateProjectionResult(effect.commit, projection);
16127
+ this.requireExternalIdentity(projection.source.version, options);
16128
+ return this.controlPlaneRequest(
16129
+ `/control/environments/${this.environmentId}/external-commits`,
16130
+ {
16131
+ method: "POST",
16132
+ body: JSON.stringify({
16133
+ kind: "effect",
16134
+ effectKey: computeEffectKey2(effect),
16135
+ ...options.sourceEventId?.trim() ? { sourceEventId: options.sourceEventId.trim() } : {},
16136
+ projection
16137
+ })
16138
+ }
16139
+ );
16140
+ }
16141
+ async persistExternalTransition(transition, productResult, options) {
16142
+ const metadata = getDefinedTransitionMetadata(transition);
16143
+ if (!metadata) {
16144
+ throw new Error(
16145
+ "environment.commit.transition requires a transition returned by defineStateMachine"
16146
+ );
16147
+ }
16148
+ const projection = transition.effect.commit.project(productResult);
16149
+ validateProjectionResult(transition.effect.commit, projection);
16150
+ this.requireExternalIdentity(projection.source.version, options);
16151
+ if (!Object.prototype.hasOwnProperty.call(
16152
+ transition.outcomes,
16153
+ projection.outcome.key
16154
+ )) {
16155
+ throw new Error(
16156
+ `Transition ${metadata.machine}.${metadata.transition} does not declare outcome ${projection.outcome.key}`
16157
+ );
16158
+ }
16159
+ if (!projection.primaryTarget) {
16160
+ throw new Error(
16161
+ "An external transition projection requires primaryTarget to identify the transitioned product record"
16162
+ );
16163
+ }
16164
+ return this.controlPlaneRequest(
16165
+ `/control/environments/${this.environmentId}/external-commits`,
16166
+ {
16167
+ method: "POST",
16168
+ body: JSON.stringify({
16169
+ kind: "transition",
16170
+ effectKey: computeEffectKey2(transition.effect),
16171
+ ...options.sourceEventId?.trim() ? { sourceEventId: options.sourceEventId.trim() } : {},
16172
+ projection,
16173
+ transition: {
16174
+ className: projection.primaryTarget.className,
16175
+ objectId: projection.primaryTarget.id,
16176
+ ...projection.primaryTarget.path ? { objectPath: projection.primaryTarget.path } : {},
16177
+ machine: metadata.machine,
16178
+ transition: metadata.transition,
16179
+ from: transition.from
16180
+ }
16181
+ })
16182
+ }
16183
+ );
16184
+ }
16185
+ requireExternalIdentity(sourceVersion, options) {
16186
+ if (!options.sourceEventId?.trim() && !sourceVersion?.trim()) {
16187
+ throw new Error(
16188
+ "An external commit requires sourceEventId or a source version from its projection mapper"
16189
+ );
16190
+ }
16191
+ }
16192
+ /**
16193
+ * Synchronize a versioned product snapshot without claiming an effect or
16194
+ * lifecycle transition. This records no transition history.
16195
+ */
16196
+ async observe(mapper, productResult) {
16197
+ const declaration = { kind: "effect"};
16198
+ const projection = mapper(productResult);
16199
+ validateProjectionResult(declaration, projection);
16200
+ if (!projection.source.version?.trim()) {
16201
+ throw new Error(
16202
+ "environment.observe requires a monotonic source version or serialized adapter sequence"
16203
+ );
16204
+ }
16205
+ return this.controlPlaneRequest(
16206
+ `/control/environments/${this.environmentId}/observations`,
16207
+ {
16208
+ method: "POST",
16209
+ body: JSON.stringify({ projection })
16210
+ }
16211
+ );
16212
+ }
15643
16213
  /**
15644
16214
  * Mirror product-owned workflow state into Granular without making Granular
15645
16215
  * own the customer application's state machine.
@@ -17046,6 +17616,14 @@ var EnvironmentSession = class extends Session {
17046
17616
  }
17047
17617
  };
17048
17618
  }
17619
+ get mutations() {
17620
+ return {
17621
+ list: (options = {}) => this.sessionDataRequest(
17622
+ "/mutations",
17623
+ options
17624
+ )
17625
+ };
17626
+ }
17049
17627
  get artifacts() {
17050
17628
  return {
17051
17629
  list: (options = {}) => {
@@ -18545,6 +19123,7 @@ var Granular = class _Granular {
18545
19123
  const serialized = {
18546
19124
  effectKey: computeEffectKey2(effect),
18547
19125
  name: effect.name,
19126
+ ...effect.label ? { label: effect.label } : {},
18548
19127
  description: effect.description,
18549
19128
  inputSchema: effect.inputSchema,
18550
19129
  stability: effect.stability || "stable",
@@ -18568,6 +19147,9 @@ var Granular = class _Granular {
18568
19147
  if (effect.metamodels !== void 0) {
18569
19148
  serialized.metamodels = effect.metamodels;
18570
19149
  }
19150
+ if (effect.commit !== void 0) {
19151
+ serialized.commit = { kind: effect.commit.kind };
19152
+ }
18571
19153
  return serialized;
18572
19154
  }
18573
19155
  async publishSandboxEffectCatalog(host) {
@@ -18788,16 +19370,60 @@ var Granular = class _Granular {
18788
19370
  };
18789
19371
  wsClient.registerRpcHandler("effect.invoke", async (params) => {
18790
19372
  const request = params;
18791
- return invokeRegisteredEffect(
19373
+ let commitReceipt;
19374
+ const result = await invokeRegisteredEffect(
18792
19375
  this.getSandboxEffectMap(sandboxId),
18793
19376
  request,
18794
19377
  {
19378
+ commitAcknowledged: (receipt) => {
19379
+ commitReceipt = receipt;
19380
+ },
19381
+ commit: {
19382
+ persist: (commitRequest) => this.request(
19383
+ `/control/environments/${encodeURIComponent(
19384
+ commitRequest.environmentId
19385
+ )}/commits`,
19386
+ {
19387
+ method: "POST",
19388
+ body: JSON.stringify({
19389
+ kind: commitRequest.kind,
19390
+ invocationId: commitRequest.invocationId,
19391
+ idempotencyKey: commitRequest.idempotencyKey,
19392
+ effectKey: commitRequest.effectKey,
19393
+ projection: commitRequest.projection
19394
+ })
19395
+ }
19396
+ ),
19397
+ mappingFailed: (failure) => this.request(
19398
+ `/control/environments/${encodeURIComponent(
19399
+ failure.environmentId
19400
+ )}/commit-invocations/${encodeURIComponent(
19401
+ failure.invocationId
19402
+ )}`,
19403
+ {
19404
+ method: "PATCH",
19405
+ body: JSON.stringify({
19406
+ status: "mapping_failed",
19407
+ error: {
19408
+ code: "commit_projection_mapping_failed",
19409
+ message: failure.message,
19410
+ retryable: false
19411
+ }
19412
+ })
19413
+ }
19414
+ )
19415
+ },
18795
19416
  feedback: {
18796
19417
  invocationId: request.callId,
18797
19418
  publish: (method, publishParams) => wsClient.call(method, publishParams)
18798
19419
  }
18799
19420
  }
18800
19421
  );
19422
+ return {
19423
+ __granularEffectInvocationResult: true,
19424
+ result,
19425
+ ...commitReceipt ? { commit: commitReceipt } : {}
19426
+ };
18801
19427
  });
18802
19428
  wsClient.on("open", () => {
18803
19429
  void this.synchronizeEffectHost(host).catch((error) => {