@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.
package/dist/index.mjs CHANGED
@@ -6533,6 +6533,11 @@ var Session = class {
6533
6533
  }
6534
6534
  }
6535
6535
  buildDirectedInvocationEffectContext(params, feedbackContext) {
6536
+ const commitUnavailable = async () => {
6537
+ throw new Error(
6538
+ "This directed browser tool invocation has no product commit transport"
6539
+ );
6540
+ };
6536
6541
  return {
6537
6542
  effectClientId: this.clientId,
6538
6543
  sandboxId: params.sandboxId || "",
@@ -6545,6 +6550,10 @@ var Session = class {
6545
6550
  userId: "",
6546
6551
  subjectId: ""
6547
6552
  },
6553
+ commit: {
6554
+ effect: commitUnavailable,
6555
+ transition: commitUnavailable
6556
+ },
6548
6557
  ...feedbackContext ? {
6549
6558
  feedback: feedbackContext.feedback,
6550
6559
  transientFeedback: feedbackContext.transientFeedback
@@ -12316,6 +12325,12 @@ var StateTransitionExpectedOutcomeSchema = external_exports.union([
12316
12325
  summary: external_exports.string().optional()
12317
12326
  }).strict()
12318
12327
  ]);
12328
+ var StateTransitionOutcomeSchema = external_exports.object({
12329
+ label: external_exports.string().min(1).optional(),
12330
+ to: external_exports.string().min(1),
12331
+ primary: external_exports.boolean().optional(),
12332
+ disposition: external_exports.enum(["continue", "error"])
12333
+ }).strict();
12319
12334
  var StateMachineTransitionSchema = external_exports.object({
12320
12335
  name: external_exports.string().min(1),
12321
12336
  from: external_exports.string().min(1),
@@ -12327,7 +12342,8 @@ var StateMachineTransitionSchema = external_exports.object({
12327
12342
  requirements: StateTransitionRequirementsSchema.optional(),
12328
12343
  permission: StateTransitionPermissionSchema.optional(),
12329
12344
  risk: external_exports.enum(["low", "medium", "high"]).optional(),
12330
- expectedOutcome: StateTransitionExpectedOutcomeSchema.optional()
12345
+ expectedOutcome: StateTransitionExpectedOutcomeSchema.optional(),
12346
+ outcomes: external_exports.record(external_exports.string().min(1), StateTransitionOutcomeSchema).optional()
12331
12347
  }).strict();
12332
12348
  external_exports.object({
12333
12349
  name: external_exports.string().min(1),
@@ -13008,6 +13024,289 @@ var effectBehaviorsMetamodelPackage = defineMetamodelPackage({
13008
13024
  }
13009
13025
  });
13010
13026
 
13027
+ // src/commit.ts
13028
+ var MAX_PROJECTION_CHANGES = 1e3;
13029
+ var MAX_SAFE_SUMMARY_KEYS = 32;
13030
+ var MAX_SAFE_TEXT_LENGTH = 2e3;
13031
+ var definedTransitionMetadata = /* @__PURE__ */ new WeakMap();
13032
+ function getDefinedTransitionMetadata(transition) {
13033
+ return definedTransitionMetadata.get(transition);
13034
+ }
13035
+ function defineProjection(mapper) {
13036
+ return mapper;
13037
+ }
13038
+ function defineEffect(effect) {
13039
+ return effect;
13040
+ }
13041
+ function defineStateMachine(definition) {
13042
+ const stateNames = new Set(Object.keys(definition.states));
13043
+ for (const [transitionName, transition] of Object.entries(
13044
+ definition.transitions
13045
+ )) {
13046
+ if (!stateNames.has(transition.from)) {
13047
+ throw new Error(
13048
+ `Transition ${transitionName} starts at undeclared state ${transition.from}`
13049
+ );
13050
+ }
13051
+ if (transition.effect.commit?.kind !== "transition") {
13052
+ throw new Error(
13053
+ `Transition ${transitionName} must use a transition-commit effect`
13054
+ );
13055
+ }
13056
+ const outcomes = Object.entries(transition.outcomes);
13057
+ const primary = outcomes.filter(([, outcome]) => outcome.primary === true);
13058
+ if (primary.length !== 1) {
13059
+ throw new Error(
13060
+ `Transition ${transitionName} must declare exactly one primary outcome`
13061
+ );
13062
+ }
13063
+ for (const [outcomeKey, outcome] of outcomes) {
13064
+ if (outcome.to !== "$current" && !stateNames.has(outcome.to)) {
13065
+ throw new Error(
13066
+ `Transition ${transitionName} outcome ${outcomeKey} targets undeclared state ${outcome.to}`
13067
+ );
13068
+ }
13069
+ if (outcome.to === "$current" && outcome.disposition !== "error") {
13070
+ throw new Error(
13071
+ `Transition ${transitionName} outcome ${outcomeKey} may use $current only with error disposition`
13072
+ );
13073
+ }
13074
+ }
13075
+ }
13076
+ for (const [transitionName, transition] of Object.entries(
13077
+ definition.transitions
13078
+ )) {
13079
+ definedTransitionMetadata.set(transition, {
13080
+ machine: definition.name,
13081
+ transition: transitionName
13082
+ });
13083
+ }
13084
+ return definition;
13085
+ }
13086
+ function requireNonEmptyString(value, path) {
13087
+ if (typeof value !== "string" || value.trim().length === 0) {
13088
+ throw new Error(`${path} must be a non-empty string`);
13089
+ }
13090
+ return value.trim();
13091
+ }
13092
+ function validateObjectReference(value, path) {
13093
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
13094
+ throw new Error(`${path} must be an object reference`);
13095
+ }
13096
+ const reference = value;
13097
+ requireNonEmptyString(reference.className, `${path}.className`);
13098
+ requireNonEmptyString(reference.id, `${path}.id`);
13099
+ if (reference.path !== void 0) {
13100
+ requireNonEmptyString(reference.path, `${path}.path`);
13101
+ }
13102
+ }
13103
+ function validateScalarRecord(value, path) {
13104
+ if (!value || typeof value !== "object" || Array.isArray(value) || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) {
13105
+ throw new Error(`${path} must be an object of scalar values`);
13106
+ }
13107
+ for (const [key, item] of Object.entries(value)) {
13108
+ if (item !== null && typeof item !== "string" && typeof item !== "number" && typeof item !== "boolean") {
13109
+ throw new Error(
13110
+ `${path}.${key} must be a string, finite number, boolean, or null`
13111
+ );
13112
+ }
13113
+ if (typeof item === "number" && !Number.isFinite(item)) {
13114
+ throw new Error(`${path}.${key} must be finite`);
13115
+ }
13116
+ }
13117
+ }
13118
+ function validateProjectedRecord(value, path) {
13119
+ validateObjectReference(value, path);
13120
+ const record = value;
13121
+ if (record.label !== void 0) {
13122
+ if (typeof record.label !== "string") {
13123
+ throw new Error(`${path}.label must be a string`);
13124
+ }
13125
+ if (record.label.length > MAX_SAFE_TEXT_LENGTH) {
13126
+ throw new Error(
13127
+ `${path}.label exceeds ${MAX_SAFE_TEXT_LENGTH} characters`
13128
+ );
13129
+ }
13130
+ }
13131
+ validateScalarRecord(record.fields, `${path}.fields`);
13132
+ }
13133
+ function validateBoundedJson(value, path, depth = 0) {
13134
+ if (depth > 12) throw new Error(`${path} is nested too deeply`);
13135
+ if (value === null || typeof value === "boolean") return;
13136
+ if (typeof value === "number") {
13137
+ if (!Number.isFinite(value)) throw new Error(`${path} must be finite`);
13138
+ return;
13139
+ }
13140
+ if (typeof value === "string") {
13141
+ if (value.length > MAX_SAFE_TEXT_LENGTH) {
13142
+ throw new Error(`${path} exceeds ${MAX_SAFE_TEXT_LENGTH} characters`);
13143
+ }
13144
+ return;
13145
+ }
13146
+ if (Array.isArray(value)) {
13147
+ if (value.length > MAX_PROJECTION_CHANGES) {
13148
+ throw new Error(`${path} contains too many values`);
13149
+ }
13150
+ value.forEach(
13151
+ (item, index) => validateBoundedJson(item, `${path}[${index}]`, depth + 1)
13152
+ );
13153
+ return;
13154
+ }
13155
+ if (!value || typeof value !== "object") {
13156
+ throw new Error(`${path} contains an unsupported value`);
13157
+ }
13158
+ const entries = Object.entries(value);
13159
+ if (entries.length > 256) throw new Error(`${path} contains too many keys`);
13160
+ for (const [key, item] of entries) {
13161
+ if (/token|secret|password|authorization|cookie/i.test(key)) {
13162
+ throw new Error(`${path}.${key} is not allowed in a commit projection`);
13163
+ }
13164
+ validateBoundedJson(item, `${path}.${key}`, depth + 1);
13165
+ }
13166
+ }
13167
+ function validateProjectionResult(declaration, projection) {
13168
+ if (!projection || typeof projection !== "object") {
13169
+ throw new Error("Projection mapper must return an object");
13170
+ }
13171
+ requireNonEmptyString(
13172
+ projection.source?.reference,
13173
+ "projection.source.reference"
13174
+ );
13175
+ if (projection.source.version !== void 0) {
13176
+ requireNonEmptyString(
13177
+ projection.source.version,
13178
+ "projection.source.version"
13179
+ );
13180
+ }
13181
+ if (!Array.isArray(projection.changes)) {
13182
+ throw new Error("projection.changes must be an array");
13183
+ }
13184
+ if (projection.changes.length > MAX_PROJECTION_CHANGES) {
13185
+ throw new Error(
13186
+ `projection.changes exceeds the ${MAX_PROJECTION_CHANGES} change limit`
13187
+ );
13188
+ }
13189
+ if (projection.primaryTarget) {
13190
+ validateObjectReference(
13191
+ projection.primaryTarget,
13192
+ "projection.primaryTarget"
13193
+ );
13194
+ }
13195
+ if (projection.safeSummary) {
13196
+ const entries = Object.entries(projection.safeSummary);
13197
+ if (entries.length > MAX_SAFE_SUMMARY_KEYS) {
13198
+ throw new Error(
13199
+ `projection.safeSummary exceeds the ${MAX_SAFE_SUMMARY_KEYS} key limit`
13200
+ );
13201
+ }
13202
+ validateBoundedJson(projection.safeSummary, "projection.safeSummary");
13203
+ validateScalarRecord(projection.safeSummary, "projection.safeSummary");
13204
+ }
13205
+ projection.changes.forEach((change, index) => {
13206
+ const changePath = `projection.changes[${index}]`;
13207
+ validateBoundedJson(change, changePath);
13208
+ if (!change || typeof change !== "object" || Array.isArray(change)) {
13209
+ throw new Error(`${changePath} must be an object`);
13210
+ }
13211
+ const rawChange = change;
13212
+ const kind = requireNonEmptyString(rawChange.kind, `${changePath}.kind`);
13213
+ if (kind === "object") {
13214
+ const operation = requireNonEmptyString(
13215
+ rawChange.operation,
13216
+ `${changePath}.operation`
13217
+ );
13218
+ if (operation !== "created" && operation !== "updated" && operation !== "deleted") {
13219
+ throw new Error(
13220
+ `${changePath}.operation must be created, updated, or deleted`
13221
+ );
13222
+ }
13223
+ if (operation === "deleted") {
13224
+ validateObjectReference(rawChange.target, `${changePath}.target`);
13225
+ } else {
13226
+ validateProjectedRecord(rawChange.record, `${changePath}.record`);
13227
+ }
13228
+ } else if (kind === "relationship") {
13229
+ const operation = requireNonEmptyString(
13230
+ rawChange.operation,
13231
+ `${changePath}.operation`
13232
+ );
13233
+ if (operation !== "connected" && operation !== "disconnected") {
13234
+ throw new Error(
13235
+ `${changePath}.operation must be connected or disconnected`
13236
+ );
13237
+ }
13238
+ requireNonEmptyString(
13239
+ rawChange.relationship,
13240
+ `${changePath}.relationship`
13241
+ );
13242
+ validateObjectReference(rawChange.from, `${changePath}.from`);
13243
+ validateObjectReference(rawChange.to, `${changePath}.to`);
13244
+ } else if (kind === "state_observation") {
13245
+ if (rawChange.operation !== void 0) {
13246
+ throw new Error(
13247
+ `${changePath}.operation is not valid for an observation`
13248
+ );
13249
+ }
13250
+ validateObjectReference(rawChange.target, `${changePath}.target`);
13251
+ requireNonEmptyString(rawChange.machine, `${changePath}.machine`);
13252
+ requireNonEmptyString(rawChange.state, `${changePath}.state`);
13253
+ } else {
13254
+ throw new Error(`${changePath}.kind is unsupported: ${kind}`);
13255
+ }
13256
+ });
13257
+ const objectOperations = /* @__PURE__ */ new Map();
13258
+ for (const change of projection.changes) {
13259
+ if (change.kind !== "object") continue;
13260
+ const reference = change.operation === "deleted" ? change.target : change.record;
13261
+ const key = `${reference.className}\0${reference.id}`;
13262
+ const operations = objectOperations.get(key) || {
13263
+ deleted: false,
13264
+ upserted: false
13265
+ };
13266
+ if (change.operation === "deleted") operations.deleted = true;
13267
+ else operations.upserted = true;
13268
+ if (operations.deleted && operations.upserted) {
13269
+ throw new Error(
13270
+ `projection.changes cannot both delete and upsert ${reference.className}/${reference.id}; return only its canonical final state`
13271
+ );
13272
+ }
13273
+ objectOperations.set(key, operations);
13274
+ }
13275
+ const outcome = projection.outcome;
13276
+ if (declaration.kind === "transition") {
13277
+ if (!outcome) {
13278
+ throw new Error("A transition projection must return an outcome");
13279
+ }
13280
+ requireNonEmptyString(outcome.key, "projection.outcome.key");
13281
+ if (outcome.error) {
13282
+ validateBoundedJson(outcome.error, "projection.outcome.error");
13283
+ }
13284
+ } else if (projection.outcome !== void 0) {
13285
+ throw new Error("An effect projection cannot declare a transition outcome");
13286
+ }
13287
+ }
13288
+ function canonicalizeCommitValue(value) {
13289
+ const normalize = (current) => {
13290
+ if (current === null || typeof current === "boolean" || typeof current === "string") {
13291
+ return typeof current === "string" ? current.normalize("NFC") : current;
13292
+ }
13293
+ if (typeof current === "number") {
13294
+ if (!Number.isFinite(current)) {
13295
+ throw new Error("Cannot canonicalize a non-finite number");
13296
+ }
13297
+ return Object.is(current, -0) ? 0 : current;
13298
+ }
13299
+ if (Array.isArray(current)) return current.map(normalize);
13300
+ if (current && typeof current === "object") {
13301
+ return Object.fromEntries(
13302
+ Object.entries(current).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key.normalize("NFC"), normalize(item)])
13303
+ );
13304
+ }
13305
+ throw new Error(`Cannot canonicalize ${typeof current}`);
13306
+ };
13307
+ return JSON.stringify(normalize(value));
13308
+ }
13309
+
13011
13310
  // src/effect-runtime.ts
13012
13311
  function computeEffectKey(effect) {
13013
13312
  const attachedClass = effect.className?.trim();
@@ -13072,6 +13371,45 @@ function resolveInvocationMode(context) {
13072
13371
  }
13073
13372
  return "execute";
13074
13373
  }
13374
+ async function sha256Hex(value) {
13375
+ const digest = await globalThis.crypto.subtle.digest(
13376
+ "SHA-256",
13377
+ new TextEncoder().encode(value)
13378
+ );
13379
+ return Array.from(
13380
+ new Uint8Array(digest),
13381
+ (byte) => byte.toString(16).padStart(2, "0")
13382
+ ).join("");
13383
+ }
13384
+ async function resolveInvocationIdempotencyKey(request) {
13385
+ const supplied = request.context?.idempotencyKey?.trim();
13386
+ if (supplied) return supplied;
13387
+ const invocationId = request.context?.invocationId?.trim();
13388
+ if (!invocationId) {
13389
+ throw new Error(
13390
+ `Committed effect ${request.effectKey} requires an invocation id`
13391
+ );
13392
+ }
13393
+ const digest = await sha256Hex(
13394
+ canonicalizeCommitValue({
13395
+ sandboxId: request.context?.sandboxId || "",
13396
+ environmentId: request.context?.environmentId || "",
13397
+ effectKey: request.effectKey,
13398
+ invocationId,
13399
+ input: request.input
13400
+ })
13401
+ );
13402
+ return `gci_${digest}`;
13403
+ }
13404
+ function createUnavailableCommitContext(message) {
13405
+ const unavailable = async () => {
13406
+ throw new Error(message);
13407
+ };
13408
+ return {
13409
+ effect: unavailable,
13410
+ transition: unavailable
13411
+ };
13412
+ }
13075
13413
  function resolveReverseEffect(effectMap, currentEffect, request, behaviors) {
13076
13414
  const explicitReverseHandler = request.context?.invocation?.reverseHandler?.trim();
13077
13415
  const configuredReverseHandler = behaviors.reverse?.handler?.trim();
@@ -13143,22 +13481,32 @@ function resolveHandlerForMode(effectMap, effect, request) {
13143
13481
  throw new Error(`Dry run is not supported for ${request.effectKey}`);
13144
13482
  }
13145
13483
  if (mode === "reverse") {
13484
+ const sourceEffectKey = request.context?.invocation?.sourceEffectKey?.trim();
13485
+ if (sourceEffectKey && sourceEffectKey !== request.effectKey) {
13486
+ return { effect, mode, handler: effect.handler };
13487
+ }
13488
+ const namedReverseHandler = request.context?.invocation?.reverseHandler?.trim() || behaviors.reverse?.handler?.trim();
13489
+ if (namedReverseHandler) {
13490
+ const reverseEffect = resolveReverseEffect(
13491
+ effectMap,
13492
+ effect,
13493
+ request,
13494
+ behaviors
13495
+ );
13496
+ if (reverseEffect) {
13497
+ return {
13498
+ effect: reverseEffect,
13499
+ mode,
13500
+ handler: reverseEffect.handler
13501
+ };
13502
+ }
13503
+ throw new Error(
13504
+ `Reverse effect ${namedReverseHandler} is not registered for ${request.effectKey}`
13505
+ );
13506
+ }
13146
13507
  if (effect.reverseHandler) {
13147
13508
  return { effect, mode, handler: effect.reverseHandler };
13148
13509
  }
13149
- const reverseEffect = resolveReverseEffect(
13150
- effectMap,
13151
- effect,
13152
- request,
13153
- behaviors
13154
- );
13155
- if (reverseEffect) {
13156
- return {
13157
- effect: reverseEffect,
13158
- mode,
13159
- handler: reverseEffect.reverseHandler || reverseEffect.handler
13160
- };
13161
- }
13162
13510
  throw new Error(
13163
13511
  `Reverse execution is not supported for ${request.effectKey}`
13164
13512
  );
@@ -13259,18 +13607,142 @@ async function invokeRegisteredEffect(effectMap, request, options = {}) {
13259
13607
  throw new Error(`Effect handler not found: ${request.effectKey}`);
13260
13608
  }
13261
13609
  const resolved = resolveHandlerForMode(effectMap, effect, request);
13610
+ const isPreResolvedNamedReverse = resolved.mode === "reverse" && Boolean(request.context?.invocation?.sourceEffectKey?.trim()) && request.context?.invocation?.sourceEffectKey?.trim() !== request.effectKey;
13611
+ const resolvedEffectKey = computeEffectKey(resolved.effect);
13612
+ if (resolved.mode === "reverse" && resolved.effect !== effect && (effect.commit || resolved.effect.commit)) {
13613
+ throw new Error(
13614
+ `Committed named reverse effect ${resolvedEffectKey} must be reserved and dispatched using its own effect key; customer code was not invoked`
13615
+ );
13616
+ }
13617
+ const declaration = resolved.effect.commit;
13618
+ const commitTransport = options.commit;
13619
+ const commitRequired = Boolean(declaration) && (resolved.mode === "execute" || resolved.mode === "reverse");
13620
+ if (commitRequired && !commitTransport) {
13621
+ throw new Error(
13622
+ `Committed effect ${request.effectKey} has no durable commit transport; customer code was not invoked`
13623
+ );
13624
+ }
13625
+ if (commitRequired && declaration?.kind === "transition" && !request.context?.invocation?.transition) {
13626
+ throw new Error(
13627
+ `Transition effect ${request.effectKey} has no resolved transition context; customer code was not invoked`
13628
+ );
13629
+ }
13630
+ const idempotencyKey = commitRequired ? await resolveInvocationIdempotencyKey(request) : request.context?.idempotencyKey;
13631
+ let commitStarted = false;
13632
+ let commitPromise = null;
13633
+ const beginCommit = (requestedKind, productResult) => {
13634
+ if (!declaration || !commitRequired || !commitTransport) {
13635
+ return Promise.reject(
13636
+ new Error(
13637
+ `Effect ${request.effectKey} does not declare an active ${requestedKind} commit`
13638
+ )
13639
+ );
13640
+ }
13641
+ if (declaration.kind !== requestedKind) {
13642
+ return Promise.reject(
13643
+ new Error(
13644
+ `Effect ${request.effectKey} declares ${declaration.kind} commit, not ${requestedKind}`
13645
+ )
13646
+ );
13647
+ }
13648
+ if (commitStarted) {
13649
+ return Promise.reject(
13650
+ new Error(
13651
+ `Effect invocation ${request.context?.invocationId || request.effectKey} already emitted its commit`
13652
+ )
13653
+ );
13654
+ }
13655
+ commitStarted = true;
13656
+ commitPromise = (async () => {
13657
+ let projection;
13658
+ try {
13659
+ projection = declaration.project(productResult);
13660
+ validateProjectionResult(declaration, projection);
13661
+ } catch (error) {
13662
+ const message = error instanceof Error ? error.message : String(error);
13663
+ if (commitTransport.mappingFailed) {
13664
+ await commitTransport.mappingFailed({
13665
+ effectKey: request.effectKey,
13666
+ effectName: request.effectName,
13667
+ invocationId: request.context?.invocationId || "",
13668
+ idempotencyKey: idempotencyKey || "",
13669
+ environmentId: request.context?.environmentId || "",
13670
+ message
13671
+ });
13672
+ }
13673
+ throw new Error(
13674
+ `Product mutation may have succeeded, but its commit projection is invalid: ${message}`
13675
+ );
13676
+ }
13677
+ if (requestedKind === "transition") {
13678
+ const transition = request.context?.invocation?.transition;
13679
+ const outcome = projection.outcome;
13680
+ if (!transition || !outcome?.key) {
13681
+ throw new Error(
13682
+ `Transition effect ${request.effectKey} did not resolve an authored outcome`
13683
+ );
13684
+ }
13685
+ if (!Object.prototype.hasOwnProperty.call(
13686
+ transition.outcomes,
13687
+ outcome.key
13688
+ )) {
13689
+ throw new Error(
13690
+ `Transition ${transition.machine}.${transition.transition} does not declare outcome ${outcome.key}`
13691
+ );
13692
+ }
13693
+ }
13694
+ const invocationId = request.context?.invocationId || "";
13695
+ const environmentId = request.context?.environmentId || "";
13696
+ const sandboxId = request.context?.sandboxId || "";
13697
+ if (!invocationId || !environmentId || !sandboxId || !idempotencyKey) {
13698
+ throw new Error(
13699
+ `Committed effect ${request.effectKey} is missing its trusted invocation scope`
13700
+ );
13701
+ }
13702
+ const commitRequest = {
13703
+ kind: requestedKind,
13704
+ effectKey: request.effectKey,
13705
+ effectName: request.effectName,
13706
+ operationLabel: resolved.effect.label || resolved.effect.name,
13707
+ invocationId,
13708
+ idempotencyKey,
13709
+ sandboxId,
13710
+ environmentId,
13711
+ ...request.context?.sessionId ? { sessionId: request.context.sessionId } : {},
13712
+ ...request.context?.jobId ? { jobId: request.context.jobId } : {},
13713
+ ...request.context?.buildId ? { buildId: request.context.buildId } : {},
13714
+ ...request.context?.invocation?.artifactId ? { artifactId: request.context.invocation.artifactId } : {},
13715
+ projection,
13716
+ ...requestedKind === "transition" && request.context?.invocation?.transition ? { transition: request.context.invocation.transition } : {}
13717
+ };
13718
+ const receipt = await commitTransport.persist(commitRequest);
13719
+ await options.commitAcknowledged?.(receipt);
13720
+ return receipt;
13721
+ })();
13722
+ return commitPromise;
13723
+ };
13724
+ const commitContext = commitRequired ? {
13725
+ effect: (productResult) => beginCommit("effect", productResult),
13726
+ transition: (productResult) => beginCommit("transition", productResult)
13727
+ } : createUnavailableCommitContext(
13728
+ `Effect ${request.effectKey} is not executing a declared product commit`
13729
+ );
13262
13730
  const context = {
13263
13731
  ...request.context || {},
13732
+ ...idempotencyKey ? { idempotencyKey } : {},
13733
+ commit: commitContext,
13264
13734
  behaviors: normalizeEffectBehaviors(
13265
13735
  request.context?.behaviors || effect.metamodels || void 0
13266
13736
  ),
13267
13737
  invocation: {
13268
13738
  mode: resolved.mode,
13269
- sourceEffectKey: request.effectKey,
13270
- sourceEffectName: request.effectName,
13739
+ ...commitRequired && declaration ? { commitKind: declaration.kind } : {},
13740
+ sourceEffectKey: isPreResolvedNamedReverse ? request.context?.invocation?.sourceEffectKey || request.effectKey : request.effectKey,
13741
+ sourceEffectName: isPreResolvedNamedReverse ? request.context?.invocation?.sourceEffectName || request.effectName : request.effectName,
13271
13742
  ...request.context?.invocation?.reverseHandler ? { reverseHandler: request.context.invocation.reverseHandler } : {},
13272
13743
  ...request.context?.invocation?.artifactId ? { artifactId: request.context.invocation.artifactId } : {},
13273
- ...request.context?.invocation?.artifactOptions ? { artifactOptions: request.context.invocation.artifactOptions } : {}
13744
+ ...request.context?.invocation?.artifactOptions ? { artifactOptions: request.context.invocation.artifactOptions } : {},
13745
+ ...request.context?.invocation?.transition ? { transition: request.context.invocation.transition } : {}
13274
13746
  }
13275
13747
  };
13276
13748
  const feedbackContext = options.feedback ? createInvocationFeedbackContext(options.feedback) : null;
@@ -13294,6 +13766,16 @@ async function invokeRegisteredEffect(effectMap, request, options = {}) {
13294
13766
  handlerFailed = true;
13295
13767
  handlerError = error;
13296
13768
  }
13769
+ let commitError;
13770
+ let commitFailed = false;
13771
+ if (commitPromise) {
13772
+ try {
13773
+ await commitPromise;
13774
+ } catch (error) {
13775
+ commitFailed = true;
13776
+ commitError = error;
13777
+ }
13778
+ }
13297
13779
  let feedbackError;
13298
13780
  let feedbackFailed = false;
13299
13781
  if (feedbackContext) {
@@ -13307,9 +13789,17 @@ async function invokeRegisteredEffect(effectMap, request, options = {}) {
13307
13789
  if (handlerFailed) {
13308
13790
  throw handlerError;
13309
13791
  }
13792
+ if (commitFailed) {
13793
+ throw commitError;
13794
+ }
13310
13795
  if (feedbackFailed) {
13311
13796
  throw feedbackError;
13312
13797
  }
13798
+ if (commitRequired && !commitStarted) {
13799
+ throw new Error(
13800
+ `Mutating effect ${request.effectKey} returned without acknowledging its product mutation`
13801
+ );
13802
+ }
13313
13803
  return handlerResult;
13314
13804
  }
13315
13805
 
@@ -13378,12 +13868,7 @@ function toRecordSearchResult(className, node) {
13378
13868
  (field) => normalizeGraphPathSegment(field.name) === "real_id" && typeof field.value === "string" && field.value.trim()
13379
13869
  );
13380
13870
  const id = typeof realIdField?.value === "string" ? realIdField.value.trim() : graphPathId;
13381
- const rawLabel = typeof node.label === "string" && node.label.trim() ? node.label : "";
13382
- if (fields.length === 0 && rawLabel && isPlaceholderRecordLabel(rawLabel, graphPathId, path)) {
13383
- return null;
13384
- }
13385
- const fallbackLabel = displayLabelFromFields(fields);
13386
- const label = rawLabel && !isPlaceholderRecordLabel(rawLabel, id, path) ? rawLabel : fallbackLabel || rawLabel || id;
13871
+ const label = typeof node.label === "string" && node.label.trim() ? node.label.trim() : path || id;
13387
13872
  return {
13388
13873
  path,
13389
13874
  className,
@@ -13393,30 +13878,6 @@ function toRecordSearchResult(className, node) {
13393
13878
  fields
13394
13879
  };
13395
13880
  }
13396
- function isPlaceholderRecordLabel(label, id, path) {
13397
- const normalizedLabel = normalizeGraphPathSegment(label);
13398
- return normalizedLabel === normalizeGraphPathSegment(id) || normalizedLabel === normalizeGraphPathSegment(path);
13399
- }
13400
- function displayLabelFromFields(fields) {
13401
- const preferredFieldNames = [
13402
- "name",
13403
- "title",
13404
- "label",
13405
- "display_name",
13406
- "file_name",
13407
- "number",
13408
- "code"
13409
- ];
13410
- for (const preferred of preferredFieldNames) {
13411
- const match = fields.find(
13412
- (field) => normalizeGraphPathSegment(field.name) === preferred && typeof field.value === "string" && field.value.trim()
13413
- );
13414
- if (typeof match?.value === "string") {
13415
- return match.value.trim();
13416
- }
13417
- }
13418
- return null;
13419
- }
13420
13881
  function normalizeRecordSearchText(value) {
13421
13882
  return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, " ").replace(/\s+/g, " ").trim();
13422
13883
  }
@@ -14379,7 +14840,8 @@ function normalizeStateMachines(values) {
14379
14840
  requirements: parseJsonRecord(transition?.requirements) || parseJsonRecord(transition?.requirements_json),
14380
14841
  permission: parseJsonValue(transition?.permission) ?? parseJsonValue(transition?.permission_json),
14381
14842
  risk: transition?.risk === "low" || transition?.risk === "medium" || transition?.risk === "high" ? transition.risk : null,
14382
- expectedOutcome: parseJsonValue(transition?.expectedOutcome) ?? parseJsonValue(transition?.expected_outcome_json)
14843
+ expectedOutcome: parseJsonValue(transition?.expectedOutcome) ?? parseJsonValue(transition?.expected_outcome_json),
14844
+ outcomes: parseJsonRecord(transition?.outcomes) || parseJsonRecord(transition?.outcomes_json)
14383
14845
  })).filter(
14384
14846
  (transition) => transition.name.length > 0 && transition.from.length > 0 && transition.to.length > 0
14385
14847
  );
@@ -14472,6 +14934,11 @@ function transitionMetadataGraphqlArgs(transition) {
14472
14934
  `expected_outcome_json: ${JSON.stringify(JSON.stringify(transition.expectedOutcome))}`
14473
14935
  );
14474
14936
  }
14937
+ if (transition.outcomes) {
14938
+ args.push(
14939
+ `outcomes_json: ${JSON.stringify(JSON.stringify(transition.outcomes))}`
14940
+ );
14941
+ }
14475
14942
  return args.length > 0 ? `, ${args.join(", ")}` : "";
14476
14943
  }
14477
14944
  function buildStateMachineModelMutations(modelPath, machines) {
@@ -14534,7 +15001,7 @@ function buildMachineTypes(classSummary, machine) {
14534
15001
  ];
14535
15002
  }
14536
15003
  function buildMachineMethods(classSummary, machine) {
14537
- const stateName = stateTypeName(classSummary.name, machine.name);
15004
+ const stateName2 = stateTypeName(classSummary.name, machine.name);
14538
15005
  const transitionName = transitionTypeName(classSummary.name, machine.name);
14539
15006
  pathTypeName(classSummary.name, machine.name);
14540
15007
  const docsPrefix = `${classSummary.name}.${machine.name}`;
@@ -14544,12 +15011,12 @@ function buildMachineMethods(classSummary, machine) {
14544
15011
  docs: [`Get the current ${docsPrefix} state.`],
14545
15012
  static: false,
14546
15013
  params: [],
14547
- returnType: `Promise<${stateName} | null>`,
15014
+ returnType: `Promise<${stateName2} | null>`,
14548
15015
  runtime: {
14549
15016
  kind: "state_machine",
14550
15017
  machineName: machine.name,
14551
15018
  className: classSummary.name,
14552
- stateTypeName: stateName,
15019
+ stateTypeName: stateName2,
14553
15020
  transitionTypeName: transitionName,
14554
15021
  operation: "get_current"
14555
15022
  }
@@ -14560,13 +15027,13 @@ function buildMachineMethods(classSummary, machine) {
14560
15027
  `Reach a ${docsPrefix} state through the shortest allowed transition path.`
14561
15028
  ],
14562
15029
  static: false,
14563
- params: [{ name: "target", type: stateName }],
15030
+ params: [{ name: "target", type: stateName2 }],
14564
15031
  returnType: `Promise<${toPascalCase2(classSummary.name)}>`,
14565
15032
  runtime: {
14566
15033
  kind: "state_machine",
14567
15034
  machineName: machine.name,
14568
15035
  className: classSummary.name,
14569
- stateTypeName: stateName,
15036
+ stateTypeName: stateName2,
14570
15037
  transitionTypeName: transitionName,
14571
15038
  operation: "reach"
14572
15039
  }
@@ -14581,7 +15048,7 @@ function buildMachineMethods(classSummary, machine) {
14581
15048
  kind: "state_machine",
14582
15049
  machineName: machine.name,
14583
15050
  className: classSummary.name,
14584
- stateTypeName: stateName,
15051
+ stateTypeName: stateName2,
14585
15052
  transitionTypeName: transitionName,
14586
15053
  operation: "list_transitions"
14587
15054
  }
@@ -14591,12 +15058,12 @@ function buildMachineMethods(classSummary, machine) {
14591
15058
  docs: [`List reachable states for ${docsPrefix} from the current state.`],
14592
15059
  static: false,
14593
15060
  params: [],
14594
- returnType: `Promise<${stateName}[]>`,
15061
+ returnType: `Promise<${stateName2}[]>`,
14595
15062
  runtime: {
14596
15063
  kind: "state_machine",
14597
15064
  machineName: machine.name,
14598
15065
  className: classSummary.name,
14599
- stateTypeName: stateName,
15066
+ stateTypeName: stateName2,
14600
15067
  transitionTypeName: transitionName,
14601
15068
  operation: "list_reachable_states"
14602
15069
  }
@@ -14611,7 +15078,7 @@ function buildMachineMethods(classSummary, machine) {
14611
15078
  kind: "state_machine",
14612
15079
  machineName: machine.name,
14613
15080
  className: classSummary.name,
14614
- stateTypeName: stateName,
15081
+ stateTypeName: stateName2,
14615
15082
  transitionTypeName: transitionName,
14616
15083
  operation: "is_final"
14617
15084
  }
@@ -14622,13 +15089,13 @@ function buildMachineMethods(classSummary, machine) {
14622
15089
  `List shortest transition paths from the current ${docsPrefix} state to a target state.`
14623
15090
  ],
14624
15091
  static: false,
14625
- params: [{ name: "target", type: stateName }],
14626
- returnType: `Promise<Array<{ states: ${stateName}[]; transitions: ${transitionName}[] }>>`,
15092
+ params: [{ name: "target", type: stateName2 }],
15093
+ returnType: `Promise<Array<{ states: ${stateName2}[]; transitions: ${transitionName}[] }>>`,
14627
15094
  runtime: {
14628
15095
  kind: "state_machine",
14629
15096
  machineName: machine.name,
14630
15097
  className: classSummary.name,
14631
- stateTypeName: stateName,
15098
+ stateTypeName: stateName2,
14632
15099
  transitionTypeName: transitionName,
14633
15100
  operation: "paths_to"
14634
15101
  }
@@ -14652,7 +15119,7 @@ function buildMachineMethods(classSummary, machine) {
14652
15119
  kind: "state_machine",
14653
15120
  machineName: machine.name,
14654
15121
  className: classSummary.name,
14655
- stateTypeName: stateName,
15122
+ stateTypeName: stateName2,
14656
15123
  transitionTypeName: transitionName,
14657
15124
  operation: "reach",
14658
15125
  targetState: stateNameValue,
@@ -14671,7 +15138,7 @@ function buildMachineMethods(classSummary, machine) {
14671
15138
  kind: "state_machine",
14672
15139
  machineName: machine.name,
14673
15140
  className: classSummary.name,
14674
- stateTypeName: stateName,
15141
+ stateTypeName: stateName2,
14675
15142
  transitionTypeName: transitionName,
14676
15143
  operation: "prepare_reach",
14677
15144
  targetState: stateNameValue,
@@ -14684,7 +15151,7 @@ function buildMachineMethods(classSummary, machine) {
14684
15151
  kind: "state_machine",
14685
15152
  machineName: machine.name,
14686
15153
  className: classSummary.name,
14687
- stateTypeName: stateName,
15154
+ stateTypeName: stateName2,
14688
15155
  transitionTypeName: transitionName,
14689
15156
  operation: "prepare_create_reach",
14690
15157
  targetState: stateNameValue,
@@ -14750,7 +15217,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14750
15217
  name: String!
14751
15218
  state_machine: StateMachine!
14752
15219
  add_state(name: String!, is_final: Boolean, label: String, description: String): StateMachineMutation!
14753
- 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!
15220
+ 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!
14754
15221
  activate_transition(name: String!): StateMachineMutation!
14755
15222
  }
14756
15223
 
@@ -14767,6 +15234,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14767
15234
  type StateMachineSnapshotMutation {
14768
15235
  snapshot: StateMachineSnapshot!
14769
15236
  activate_transition(name: String!): StateMachineSnapshotMutation!
15237
+ commit_transition(name: String!, outcome: String!, to: String!, commit_id: String!, source_version: String): StateMachineSnapshotMutation!
14770
15238
  observe_state(state: String!, force: Boolean, source: String): StateMachineSnapshotMutation!
14771
15239
  }
14772
15240
 
@@ -14813,6 +15281,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14813
15281
  permission_json: String
14814
15282
  risk: String
14815
15283
  expected_outcome_json: String
15284
+ outcomes_json: String
14816
15285
  }
14817
15286
 
14818
15287
  type StateMachinePath {
@@ -14823,6 +15292,10 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14823
15292
  type StateMachineTransitionEvent {
14824
15293
  sequence: Int!
14825
15294
  occurred_at: Float!
15295
+ commit_id: String
15296
+ outcome: String
15297
+ source_version: String
15298
+ projected_from_mismatch: String
14826
15299
  transition: StateMachineTransition!
14827
15300
  from: StateMachineState!
14828
15301
  to: StateMachineState!
@@ -14888,7 +15361,8 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14888
15361
  requirements_json,
14889
15362
  permission_json,
14890
15363
  risk,
14891
- expected_outcome_json
15364
+ expected_outcome_json,
15365
+ outcomes_json
14892
15366
  }) => {
14893
15367
  await run(
14894
15368
  value.target.add_state_machine_transition(
@@ -14904,7 +15378,8 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14904
15378
  requirementsJson: requirements_json,
14905
15379
  permissionJson: permission_json,
14906
15380
  risk,
14907
- expectedOutcomeJson: expected_outcome_json
15381
+ expectedOutcomeJson: expected_outcome_json,
15382
+ outcomesJson: outcomes_json
14908
15383
  }
14909
15384
  )
14910
15385
  );
@@ -14925,6 +15400,19 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14925
15400
  );
14926
15401
  return value;
14927
15402
  },
15403
+ commit_transition: async (value, { name, outcome, to, commit_id, source_version }) => {
15404
+ await run(
15405
+ value.target.commit_state_machine_transition(
15406
+ value.name,
15407
+ name,
15408
+ outcome,
15409
+ to,
15410
+ commit_id,
15411
+ source_version
15412
+ )
15413
+ );
15414
+ return value;
15415
+ },
14928
15416
  observe_state: async (value, { state, force, source }) => {
14929
15417
  await run(
14930
15418
  value.target.observe_state_machine_state(
@@ -14954,7 +15442,8 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14954
15442
  requirements_json: (value) => value.requirements_json || null,
14955
15443
  permission_json: (value) => value.permission_json || null,
14956
15444
  risk: (value) => value.risk || null,
14957
- expected_outcome_json: (value) => value.expected_outcome_json || null
15445
+ expected_outcome_json: (value) => value.expected_outcome_json || null,
15446
+ outcomes_json: (value) => value.outcomes_json || null
14958
15447
  },
14959
15448
  StateMachinePath: {
14960
15449
  states: (value) => value.states,
@@ -14963,6 +15452,10 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14963
15452
  StateMachineTransitionEvent: {
14964
15453
  sequence: (value) => value.sequence,
14965
15454
  occurred_at: (value) => value.occurred_at,
15455
+ commit_id: (value) => value.commit_id || null,
15456
+ outcome: (value) => value.outcome || null,
15457
+ source_version: (value) => value.source_version || null,
15458
+ projected_from_mismatch: (value) => value.projected_from_mismatch || null,
14966
15459
  transition: (value) => value.transition,
14967
15460
  from: (value) => value.from,
14968
15461
  to: (value) => value.to
@@ -15036,6 +15529,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
15036
15529
  permission_json
15037
15530
  risk
15038
15531
  expected_outcome_json
15532
+ outcomes_json
15039
15533
  }
15040
15534
  }`
15041
15535
  ]
@@ -15763,6 +16257,133 @@ var Environment = class _Environment {
15763
16257
  getAwaitingCount: async () => this.getAwaitingRecordCount()
15764
16258
  };
15765
16259
  }
16260
+ /**
16261
+ * Acknowledge a declared product mutation that already happened outside a
16262
+ * Granular-run effect (for example, in a webhook consumer). These methods
16263
+ * run the declaration's pure projection mapper; they never call its handler.
16264
+ */
16265
+ get commit() {
16266
+ return {
16267
+ effect: async (effect, productResult, options = {}) => this.persistExternalEffect(effect, productResult, options),
16268
+ transition: async (transition, productResult, options = {}) => this.persistExternalTransition(transition, productResult, options),
16269
+ get: async (commitId) => this.controlPlaneRequest(
16270
+ `/control/environments/${this.environmentId}/commits/${encodeURIComponent(
16271
+ commitId
16272
+ )}`
16273
+ ),
16274
+ retry: async (commitId) => this.controlPlaneRequest(
16275
+ `/control/environments/${this.environmentId}/commits/${encodeURIComponent(
16276
+ commitId
16277
+ )}/retry`,
16278
+ { method: "POST" }
16279
+ )
16280
+ };
16281
+ }
16282
+ /** Inspect or retry the agent synchronization of a product snapshot. */
16283
+ get observation() {
16284
+ return {
16285
+ get: async (observationId) => this.controlPlaneRequest(
16286
+ `/control/environments/${this.environmentId}/observations/${encodeURIComponent(
16287
+ observationId
16288
+ )}`
16289
+ ),
16290
+ retry: async (observationId) => this.controlPlaneRequest(
16291
+ `/control/environments/${this.environmentId}/observations/${encodeURIComponent(
16292
+ observationId
16293
+ )}/retry`,
16294
+ { method: "POST" }
16295
+ )
16296
+ };
16297
+ }
16298
+ async persistExternalEffect(effect, productResult, options) {
16299
+ const projection = effect.commit.project(productResult);
16300
+ validateProjectionResult(effect.commit, projection);
16301
+ this.requireExternalIdentity(projection.source.version, options);
16302
+ return this.controlPlaneRequest(
16303
+ `/control/environments/${this.environmentId}/external-commits`,
16304
+ {
16305
+ method: "POST",
16306
+ body: JSON.stringify({
16307
+ kind: "effect",
16308
+ effectKey: computeEffectKey2(effect),
16309
+ ...options.sourceEventId?.trim() ? { sourceEventId: options.sourceEventId.trim() } : {},
16310
+ projection
16311
+ })
16312
+ }
16313
+ );
16314
+ }
16315
+ async persistExternalTransition(transition, productResult, options) {
16316
+ const metadata = getDefinedTransitionMetadata(transition);
16317
+ if (!metadata) {
16318
+ throw new Error(
16319
+ "environment.commit.transition requires a transition returned by defineStateMachine"
16320
+ );
16321
+ }
16322
+ const projection = transition.effect.commit.project(productResult);
16323
+ validateProjectionResult(transition.effect.commit, projection);
16324
+ this.requireExternalIdentity(projection.source.version, options);
16325
+ if (!Object.prototype.hasOwnProperty.call(
16326
+ transition.outcomes,
16327
+ projection.outcome.key
16328
+ )) {
16329
+ throw new Error(
16330
+ `Transition ${metadata.machine}.${metadata.transition} does not declare outcome ${projection.outcome.key}`
16331
+ );
16332
+ }
16333
+ if (!projection.primaryTarget) {
16334
+ throw new Error(
16335
+ "An external transition projection requires primaryTarget to identify the transitioned product record"
16336
+ );
16337
+ }
16338
+ return this.controlPlaneRequest(
16339
+ `/control/environments/${this.environmentId}/external-commits`,
16340
+ {
16341
+ method: "POST",
16342
+ body: JSON.stringify({
16343
+ kind: "transition",
16344
+ effectKey: computeEffectKey2(transition.effect),
16345
+ ...options.sourceEventId?.trim() ? { sourceEventId: options.sourceEventId.trim() } : {},
16346
+ projection,
16347
+ transition: {
16348
+ className: projection.primaryTarget.className,
16349
+ objectId: projection.primaryTarget.id,
16350
+ ...projection.primaryTarget.path ? { objectPath: projection.primaryTarget.path } : {},
16351
+ machine: metadata.machine,
16352
+ transition: metadata.transition,
16353
+ from: transition.from
16354
+ }
16355
+ })
16356
+ }
16357
+ );
16358
+ }
16359
+ requireExternalIdentity(sourceVersion, options) {
16360
+ if (!options.sourceEventId?.trim() && !sourceVersion?.trim()) {
16361
+ throw new Error(
16362
+ "An external commit requires sourceEventId or a source version from its projection mapper"
16363
+ );
16364
+ }
16365
+ }
16366
+ /**
16367
+ * Synchronize a versioned product snapshot without claiming an effect or
16368
+ * lifecycle transition. This records no transition history.
16369
+ */
16370
+ async observe(mapper, productResult) {
16371
+ const declaration = { kind: "effect"};
16372
+ const projection = mapper(productResult);
16373
+ validateProjectionResult(declaration, projection);
16374
+ if (!projection.source.version?.trim()) {
16375
+ throw new Error(
16376
+ "environment.observe requires a monotonic source version or serialized adapter sequence"
16377
+ );
16378
+ }
16379
+ return this.controlPlaneRequest(
16380
+ `/control/environments/${this.environmentId}/observations`,
16381
+ {
16382
+ method: "POST",
16383
+ body: JSON.stringify({ projection })
16384
+ }
16385
+ );
16386
+ }
15766
16387
  /**
15767
16388
  * Mirror product-owned workflow state into Granular without making Granular
15768
16389
  * own the customer application's state machine.
@@ -15802,8 +16423,8 @@ var Environment = class _Environment {
15802
16423
  * `await env.recordState({ className: "spend_request", id, machine: "lifecycle", state: "policy_review", source: "customer_backend" })`
15803
16424
  */
15804
16425
  state(target) {
15805
- const observe = async (machineName, stateName, input = {}) => {
15806
- const observedState = input.observedState || input.state || stateName;
16426
+ const observe = async (machineName, stateName2, input = {}) => {
16427
+ const observedState = input.observedState || input.state || stateName2;
15807
16428
  if (!observedState) {
15808
16429
  throw new Error("State observation requires a target state");
15809
16430
  }
@@ -15829,7 +16450,7 @@ var Environment = class _Environment {
15829
16450
  {
15830
16451
  get: (_machineTarget, stateProperty) => {
15831
16452
  if (stateProperty === "to") {
15832
- return (stateName, input) => observe(machineProperty, stateName, input || {});
16453
+ return (stateName2, input) => observe(machineProperty, stateName2, input || {});
15833
16454
  }
15834
16455
  if (typeof stateProperty !== "string") return void 0;
15835
16456
  return (input) => observe(
@@ -17169,6 +17790,14 @@ var EnvironmentSession = class extends Session {
17169
17790
  }
17170
17791
  };
17171
17792
  }
17793
+ get mutations() {
17794
+ return {
17795
+ list: (options = {}) => this.sessionDataRequest(
17796
+ "/mutations",
17797
+ options
17798
+ )
17799
+ };
17800
+ }
17172
17801
  get artifacts() {
17173
17802
  return {
17174
17803
  list: (options = {}) => {
@@ -18668,6 +19297,7 @@ var Granular = class _Granular {
18668
19297
  const serialized = {
18669
19298
  effectKey: computeEffectKey2(effect),
18670
19299
  name: effect.name,
19300
+ ...effect.label ? { label: effect.label } : {},
18671
19301
  description: effect.description,
18672
19302
  inputSchema: effect.inputSchema,
18673
19303
  stability: effect.stability || "stable",
@@ -18691,6 +19321,9 @@ var Granular = class _Granular {
18691
19321
  if (effect.metamodels !== void 0) {
18692
19322
  serialized.metamodels = effect.metamodels;
18693
19323
  }
19324
+ if (effect.commit !== void 0) {
19325
+ serialized.commit = { kind: effect.commit.kind };
19326
+ }
18694
19327
  return serialized;
18695
19328
  }
18696
19329
  async publishSandboxEffectCatalog(host) {
@@ -18911,16 +19544,60 @@ var Granular = class _Granular {
18911
19544
  };
18912
19545
  wsClient.registerRpcHandler("effect.invoke", async (params) => {
18913
19546
  const request = params;
18914
- return invokeRegisteredEffect(
19547
+ let commitReceipt;
19548
+ const result = await invokeRegisteredEffect(
18915
19549
  this.getSandboxEffectMap(sandboxId),
18916
19550
  request,
18917
19551
  {
19552
+ commitAcknowledged: (receipt) => {
19553
+ commitReceipt = receipt;
19554
+ },
19555
+ commit: {
19556
+ persist: (commitRequest) => this.request(
19557
+ `/control/environments/${encodeURIComponent(
19558
+ commitRequest.environmentId
19559
+ )}/commits`,
19560
+ {
19561
+ method: "POST",
19562
+ body: JSON.stringify({
19563
+ kind: commitRequest.kind,
19564
+ invocationId: commitRequest.invocationId,
19565
+ idempotencyKey: commitRequest.idempotencyKey,
19566
+ effectKey: commitRequest.effectKey,
19567
+ projection: commitRequest.projection
19568
+ })
19569
+ }
19570
+ ),
19571
+ mappingFailed: (failure) => this.request(
19572
+ `/control/environments/${encodeURIComponent(
19573
+ failure.environmentId
19574
+ )}/commit-invocations/${encodeURIComponent(
19575
+ failure.invocationId
19576
+ )}`,
19577
+ {
19578
+ method: "PATCH",
19579
+ body: JSON.stringify({
19580
+ status: "mapping_failed",
19581
+ error: {
19582
+ code: "commit_projection_mapping_failed",
19583
+ message: failure.message,
19584
+ retryable: false
19585
+ }
19586
+ })
19587
+ }
19588
+ )
19589
+ },
18918
19590
  feedback: {
18919
19591
  invocationId: request.callId,
18920
19592
  publish: (method, publishParams) => wsClient.call(method, publishParams)
18921
19593
  }
18922
19594
  }
18923
19595
  );
19596
+ return {
19597
+ __granularEffectInvocationResult: true,
19598
+ result,
19599
+ ...commitReceipt ? { commit: commitReceipt } : {}
19600
+ };
18924
19601
  });
18925
19602
  wsClient.on("open", () => {
18926
19603
  void this.synchronizeEffectHost(host).catch((error) => {
@@ -19492,6 +20169,275 @@ var Granular = class _Granular {
19492
20169
  }
19493
20170
  };
19494
20171
 
20172
+ // src/record-snapshot-projection.ts
20173
+ function stableValue(value) {
20174
+ if (Array.isArray(value)) return value.map(stableValue);
20175
+ if (value && typeof value === "object") {
20176
+ return Object.fromEntries(
20177
+ Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, stableValue(item)])
20178
+ );
20179
+ }
20180
+ return value;
20181
+ }
20182
+ function stableJson(value) {
20183
+ return JSON.stringify(stableValue(value));
20184
+ }
20185
+ function recordKey(record) {
20186
+ return `${record.className}\0${record.id}`;
20187
+ }
20188
+ function recordReference(className, id) {
20189
+ return { className, id };
20190
+ }
20191
+ function changedRecordSnapshots(beforeRecords, afterRecords) {
20192
+ const before = new Map(
20193
+ beforeRecords.map((record) => [recordKey(record), record])
20194
+ );
20195
+ const after = new Map(
20196
+ afterRecords.map((record) => [recordKey(record), record])
20197
+ );
20198
+ const changedKeys = /* @__PURE__ */ new Set();
20199
+ for (const key of /* @__PURE__ */ new Set([...before.keys(), ...after.keys()])) {
20200
+ if (stableJson(before.get(key)) !== stableJson(after.get(key))) {
20201
+ changedKeys.add(key);
20202
+ }
20203
+ }
20204
+ return {
20205
+ beforeRecords: beforeRecords.filter(
20206
+ (record) => changedKeys.has(recordKey(record))
20207
+ ),
20208
+ afterRecords: afterRecords.filter(
20209
+ (record) => changedKeys.has(recordKey(record))
20210
+ )
20211
+ };
20212
+ }
20213
+ function relationshipTargets(manifest) {
20214
+ const targets = /* @__PURE__ */ new Map();
20215
+ for (const volume of manifest.volumes) {
20216
+ for (const operation of volume.operations) {
20217
+ const relationship = operation.defineRelationship;
20218
+ if (!relationship) continue;
20219
+ targets.set(`${relationship.left}\0${relationship.leftSubmodel}`, {
20220
+ className: relationship.right
20221
+ });
20222
+ targets.set(`${relationship.right}\0${relationship.rightSubmodel}`, {
20223
+ className: relationship.left
20224
+ });
20225
+ }
20226
+ }
20227
+ return targets;
20228
+ }
20229
+ function stateName(value) {
20230
+ return typeof value === "string" ? value : value.state;
20231
+ }
20232
+ function asIds(value) {
20233
+ return value === void 0 ? [] : Array.isArray(value) ? value : [value];
20234
+ }
20235
+ function changedObjectProjection(before, after) {
20236
+ const beforeFields = before?.fields || {};
20237
+ const afterFields = after.fields || {};
20238
+ const fields = { ...afterFields };
20239
+ for (const fieldName of Object.keys(beforeFields)) {
20240
+ if (!(fieldName in afterFields)) fields[fieldName] = null;
20241
+ }
20242
+ if (before && before.label === after.label && stableJson(beforeFields) === stableJson(afterFields)) {
20243
+ return null;
20244
+ }
20245
+ return {
20246
+ kind: "object",
20247
+ operation: before ? "updated" : "created",
20248
+ record: {
20249
+ className: after.className,
20250
+ id: after.id,
20251
+ ...after.label !== void 0 ? { label: after.label } : {},
20252
+ fields
20253
+ }
20254
+ };
20255
+ }
20256
+ function projectionChanges(input) {
20257
+ const before = new Map(
20258
+ input.beforeRecords.map((record) => [recordKey(record), record])
20259
+ );
20260
+ const after = new Map(
20261
+ input.afterRecords.map((record) => [recordKey(record), record])
20262
+ );
20263
+ const targets = relationshipTargets(input.manifest);
20264
+ const changes = [];
20265
+ for (const record of input.afterRecords) {
20266
+ const previous = before.get(recordKey(record));
20267
+ const objectChange = changedObjectProjection(previous, record);
20268
+ if (objectChange) changes.push(objectChange);
20269
+ const relationshipNames = /* @__PURE__ */ new Set([
20270
+ ...Object.keys(previous?.relationships || {}),
20271
+ ...Object.keys(record.relationships || {})
20272
+ ]);
20273
+ for (const relationshipName of [...relationshipNames].sort()) {
20274
+ const target = targets.get(
20275
+ `${record.className}\0${relationshipName}`
20276
+ );
20277
+ if (!target) {
20278
+ throw new Error(
20279
+ `No ontology relationship target is declared for ${record.className}.${relationshipName}`
20280
+ );
20281
+ }
20282
+ const previousIds = new Set(
20283
+ asIds(previous?.relationships?.[relationshipName])
20284
+ );
20285
+ const currentIds = new Set(
20286
+ asIds(record.relationships?.[relationshipName])
20287
+ );
20288
+ for (const id of [...previousIds].sort()) {
20289
+ if (currentIds.has(id)) continue;
20290
+ changes.push({
20291
+ kind: "relationship",
20292
+ operation: "disconnected",
20293
+ relationship: relationshipName,
20294
+ from: recordReference(record.className, record.id),
20295
+ to: recordReference(target.className, id)
20296
+ });
20297
+ }
20298
+ for (const id of [...currentIds].sort()) {
20299
+ if (previousIds.has(id)) continue;
20300
+ changes.push({
20301
+ kind: "relationship",
20302
+ operation: "connected",
20303
+ relationship: relationshipName,
20304
+ from: recordReference(record.className, record.id),
20305
+ to: recordReference(target.className, id)
20306
+ });
20307
+ }
20308
+ }
20309
+ const machines = /* @__PURE__ */ new Set([
20310
+ ...Object.keys(previous?.states || {}),
20311
+ ...Object.keys(record.states || {})
20312
+ ]);
20313
+ for (const machine of [...machines].sort()) {
20314
+ const next = record.states?.[machine];
20315
+ if (next === void 0) continue;
20316
+ const prior = previous?.states?.[machine];
20317
+ if (prior !== void 0 && stateName(prior) === stateName(next)) continue;
20318
+ if (input.transition && input.transition.className === record.className && input.transition.objectId === record.id && input.transition.machine === machine) {
20319
+ continue;
20320
+ }
20321
+ changes.push({
20322
+ kind: "state_observation",
20323
+ target: recordReference(record.className, record.id),
20324
+ machine,
20325
+ state: stateName(next)
20326
+ });
20327
+ }
20328
+ }
20329
+ for (const record of input.beforeRecords) {
20330
+ if (after.has(recordKey(record))) continue;
20331
+ changes.push({
20332
+ kind: "object",
20333
+ operation: "deleted",
20334
+ target: recordReference(record.className, record.id)
20335
+ });
20336
+ }
20337
+ return changes;
20338
+ }
20339
+ function valueAtPath(value, path) {
20340
+ return path.split(".").filter(Boolean).reduce((current, segment) => {
20341
+ if (!current || typeof current !== "object" || Array.isArray(current)) {
20342
+ return void 0;
20343
+ }
20344
+ return current[segment];
20345
+ }, value);
20346
+ }
20347
+ function primaryTarget(input) {
20348
+ if (input.transition) {
20349
+ return recordReference(
20350
+ input.transition.className,
20351
+ input.transition.objectId
20352
+ );
20353
+ }
20354
+ const creates = input.effect.metamodels?.creates;
20355
+ if (creates) {
20356
+ const declaration = typeof creates === "string" ? { className: creates } : creates;
20357
+ const id = declaration.idPath ? valueAtPath(input.result, declaration.idPath) : void 0;
20358
+ if (typeof id === "string" && id) {
20359
+ return recordReference(declaration.className, id);
20360
+ }
20361
+ }
20362
+ if (input.effect.className && !input.effect.static) {
20363
+ const objectId = input.effectInput && typeof input.effectInput === "object" && !Array.isArray(input.effectInput) ? input.effectInput._objectId : void 0;
20364
+ if (typeof objectId === "string" && objectId) {
20365
+ return recordReference(input.effect.className, objectId);
20366
+ }
20367
+ }
20368
+ const created = input.changes.filter(
20369
+ (change) => change.kind === "object" && change.operation === "created"
20370
+ );
20371
+ const onlyCreated = created.length === 1 ? created[0] : void 0;
20372
+ return onlyCreated ? recordReference(onlyCreated.record.className, onlyCreated.record.id) : void 0;
20373
+ }
20374
+ function transitionOutcome(transition, afterRecords) {
20375
+ if (!transition) {
20376
+ throw new Error("A transition commit has no authored transition context.");
20377
+ }
20378
+ const target = afterRecords.find(
20379
+ (record) => record.className === transition.className && record.id === transition.objectId
20380
+ );
20381
+ const observed = target?.states?.[transition.machine];
20382
+ if (!observed) {
20383
+ throw new Error(
20384
+ `The product result did not expose ${transition.className}:${transition.objectId}.${transition.machine}`
20385
+ );
20386
+ }
20387
+ const finalState = stateName(observed);
20388
+ const matches = Object.entries(transition.outcomes).filter(
20389
+ ([, outcome]) => outcome.to === finalState || outcome.to === "$current" && finalState === transition.from
20390
+ );
20391
+ if (matches.length !== 1) {
20392
+ throw new Error(
20393
+ `Product state ${finalState} maps to ${matches.length} authored outcomes for ${transition.machine}.${transition.transition}`
20394
+ );
20395
+ }
20396
+ const [key, declaration] = matches[0];
20397
+ return {
20398
+ key,
20399
+ ...declaration.disposition === "error" ? {
20400
+ error: {
20401
+ code: `product_outcome_${key}`,
20402
+ message: declaration.label || `The product completed in ${finalState} instead of continuing.`,
20403
+ retryable: false
20404
+ }
20405
+ } : {}
20406
+ };
20407
+ }
20408
+ function projectRecordSnapshotMutation(input) {
20409
+ const changes = projectionChanges({
20410
+ manifest: input.manifest,
20411
+ beforeRecords: input.source.beforeRecords,
20412
+ afterRecords: input.source.afterRecords,
20413
+ transition: input.transition
20414
+ });
20415
+ const target = primaryTarget({
20416
+ effect: input.effect,
20417
+ effectInput: input.effectInput,
20418
+ result: input.result,
20419
+ changes,
20420
+ transition: input.transition
20421
+ });
20422
+ const base = {
20423
+ source: {
20424
+ reference: input.source.reference,
20425
+ ...input.source.version ? { version: input.source.version } : {}
20426
+ },
20427
+ ...target ? { primaryTarget: target } : {},
20428
+ changes,
20429
+ safeSummary: {
20430
+ effect: input.effect.name,
20431
+ affectedChanges: changes.length
20432
+ }
20433
+ };
20434
+ if (input.commitKind === "effect") return base;
20435
+ return {
20436
+ ...base,
20437
+ outcome: transitionOutcome(input.transition, input.source.afterRecords)
20438
+ };
20439
+ }
20440
+
19495
20441
  // src/agent-harness-templates/action-presentation/0.1.0/manifest.json
19496
20442
  var manifest_default = {
19497
20443
  id: "action-presentation",
@@ -22500,6 +23446,6 @@ function calculateOpenAITokenSpend(model, rawUsage) {
22500
23446
  };
22501
23447
  }
22502
23448
 
22503
- export { Environment, EnvironmentSession, GRANULAR_FEED_DIAGNOSTIC_EVENT, Granular, OPENAI_MODEL_PRICING_USD_PER_MILLION, OntologyHandle, Session, SessionFeedController, WSClient, buildContinuationInstruction, buildContinuationInstructionFromTemplate, buildGranularAgentCheckpointBlock, buildGranularAgentDomainBlock, buildGranularAgentFileBlock, buildGranularAgentHeapBlock, buildGranularAgentLoopBlock, buildGranularAgentManualActionBlock, buildGranularAgentManualActionMemorySummary, buildGranularAgentReferentBlock, buildGranularAgentRuntimeImportsBlock, buildGranularAgentSessionBlock, buildGranularAgentSystemPrompt, buildGranularAgentSystemPromptFromTemplate, buildGranularAgentToolBlock, buildGranularAgentWorkflowBlock, buildOpenAISpendEventId, buildSessionTranscript, buildSessionTranscriptFromFeedItems, calculateOpenAITokenSpend, consumeGranularReasoningOnlyChunk, consumeGranularReasoningTraceChunk, createFeedPublisher, createHarnessVerifierSnapshot, emitFeedDiagnostic, emitFeedDiagnosticToDefaultSink, emptyFeedSnapshot, evaluateContinuation, evaluateValidationRule, extractPromptTokens, getCurrentClosureId, getDefaultHarnessTemplateId, getExclusivePromptTarget, getOpenAIModelPricing, hasCanonicalSessionFeedActivation, hasOpenPrompt, hashHarnessTemplateValue, invokeRegisteredEffect, isCanonicalSessionFeedDocument, isLocalApiUrl, listHarnessTemplates, mergeFeedItemsBySequence, normalizeEffectBehaviors, normalizeFeedDiagnostic, normalizeFeedDiagnosticKind, normalizeFeedPage, normalizeOpenAIUsage, normalizePrompt, normalizePromptChoiceOption, normalizePromptText, normalizePromptType, orderTransientFeedItems, projectConversationReferentFocus, projectConversationReferentSummary, projectHeapSummary, projectLoopSummary, projectSessionFileSummary, projectWorkflowFocus, projectWorkflowSummary, readSessionFeedSnapshot, recordOpenAIUsageSpend, renderContinuationInstructionFromTemplate, renderGranularAgentSystemPromptFromTemplate, resolveApiUrl, resolveAuthTokenForApiUrl, resolveHarnessTemplate, resolveJobPresentation, resolvePromptAnswer, reviewGeneratedJobCode, scorePromptChoiceMatch, stripGranularReasoningTrace, toGranularHttpBase, validateHarnessTemplateManifest, validationRuleFailureMessage };
23449
+ export { Environment, EnvironmentSession, GRANULAR_FEED_DIAGNOSTIC_EVENT, Granular, OPENAI_MODEL_PRICING_USD_PER_MILLION, OntologyHandle, Session, SessionFeedController, WSClient, buildContinuationInstruction, buildContinuationInstructionFromTemplate, buildGranularAgentCheckpointBlock, buildGranularAgentDomainBlock, buildGranularAgentFileBlock, buildGranularAgentHeapBlock, buildGranularAgentLoopBlock, buildGranularAgentManualActionBlock, buildGranularAgentManualActionMemorySummary, buildGranularAgentReferentBlock, buildGranularAgentRuntimeImportsBlock, buildGranularAgentSessionBlock, buildGranularAgentSystemPrompt, buildGranularAgentSystemPromptFromTemplate, buildGranularAgentToolBlock, buildGranularAgentWorkflowBlock, buildOpenAISpendEventId, buildSessionTranscript, buildSessionTranscriptFromFeedItems, calculateOpenAITokenSpend, canonicalizeCommitValue, changedRecordSnapshots, consumeGranularReasoningOnlyChunk, consumeGranularReasoningTraceChunk, createFeedPublisher, createHarnessVerifierSnapshot, defineEffect, defineProjection, defineStateMachine, emitFeedDiagnostic, emitFeedDiagnosticToDefaultSink, emptyFeedSnapshot, evaluateContinuation, evaluateValidationRule, extractPromptTokens, getCurrentClosureId, getDefaultHarnessTemplateId, getExclusivePromptTarget, getOpenAIModelPricing, hasCanonicalSessionFeedActivation, hasOpenPrompt, hashHarnessTemplateValue, invokeRegisteredEffect, isCanonicalSessionFeedDocument, isLocalApiUrl, listHarnessTemplates, mergeFeedItemsBySequence, normalizeEffectBehaviors, normalizeFeedDiagnostic, normalizeFeedDiagnosticKind, normalizeFeedPage, normalizeOpenAIUsage, normalizePrompt, normalizePromptChoiceOption, normalizePromptText, normalizePromptType, orderTransientFeedItems, projectConversationReferentFocus, projectConversationReferentSummary, projectHeapSummary, projectLoopSummary, projectRecordSnapshotMutation, projectSessionFileSummary, projectWorkflowFocus, projectWorkflowSummary, readSessionFeedSnapshot, recordOpenAIUsageSpend, renderContinuationInstructionFromTemplate, renderGranularAgentSystemPromptFromTemplate, resolveApiUrl, resolveAuthTokenForApiUrl, resolveHarnessTemplate, resolveJobPresentation, resolvePromptAnswer, reviewGeneratedJobCode, scorePromptChoiceMatch, stripGranularReasoningTrace, toGranularHttpBase, validateHarnessTemplateManifest, validateProjectionResult, validationRuleFailureMessage };
22504
23450
  //# sourceMappingURL=index.mjs.map
22505
23451
  //# sourceMappingURL=index.mjs.map