@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.js CHANGED
@@ -6555,6 +6555,11 @@ var Session = class {
6555
6555
  }
6556
6556
  }
6557
6557
  buildDirectedInvocationEffectContext(params, feedbackContext) {
6558
+ const commitUnavailable = async () => {
6559
+ throw new Error(
6560
+ "This directed browser tool invocation has no product commit transport"
6561
+ );
6562
+ };
6558
6563
  return {
6559
6564
  effectClientId: this.clientId,
6560
6565
  sandboxId: params.sandboxId || "",
@@ -6567,6 +6572,10 @@ var Session = class {
6567
6572
  userId: "",
6568
6573
  subjectId: ""
6569
6574
  },
6575
+ commit: {
6576
+ effect: commitUnavailable,
6577
+ transition: commitUnavailable
6578
+ },
6570
6579
  ...feedbackContext ? {
6571
6580
  feedback: feedbackContext.feedback,
6572
6581
  transientFeedback: feedbackContext.transientFeedback
@@ -12338,6 +12347,12 @@ var StateTransitionExpectedOutcomeSchema = external_exports.union([
12338
12347
  summary: external_exports.string().optional()
12339
12348
  }).strict()
12340
12349
  ]);
12350
+ var StateTransitionOutcomeSchema = external_exports.object({
12351
+ label: external_exports.string().min(1).optional(),
12352
+ to: external_exports.string().min(1),
12353
+ primary: external_exports.boolean().optional(),
12354
+ disposition: external_exports.enum(["continue", "error"])
12355
+ }).strict();
12341
12356
  var StateMachineTransitionSchema = external_exports.object({
12342
12357
  name: external_exports.string().min(1),
12343
12358
  from: external_exports.string().min(1),
@@ -12349,7 +12364,8 @@ var StateMachineTransitionSchema = external_exports.object({
12349
12364
  requirements: StateTransitionRequirementsSchema.optional(),
12350
12365
  permission: StateTransitionPermissionSchema.optional(),
12351
12366
  risk: external_exports.enum(["low", "medium", "high"]).optional(),
12352
- expectedOutcome: StateTransitionExpectedOutcomeSchema.optional()
12367
+ expectedOutcome: StateTransitionExpectedOutcomeSchema.optional(),
12368
+ outcomes: external_exports.record(external_exports.string().min(1), StateTransitionOutcomeSchema).optional()
12353
12369
  }).strict();
12354
12370
  external_exports.object({
12355
12371
  name: external_exports.string().min(1),
@@ -13030,6 +13046,289 @@ var effectBehaviorsMetamodelPackage = defineMetamodelPackage({
13030
13046
  }
13031
13047
  });
13032
13048
 
13049
+ // src/commit.ts
13050
+ var MAX_PROJECTION_CHANGES = 1e3;
13051
+ var MAX_SAFE_SUMMARY_KEYS = 32;
13052
+ var MAX_SAFE_TEXT_LENGTH = 2e3;
13053
+ var definedTransitionMetadata = /* @__PURE__ */ new WeakMap();
13054
+ function getDefinedTransitionMetadata(transition) {
13055
+ return definedTransitionMetadata.get(transition);
13056
+ }
13057
+ function defineProjection(mapper) {
13058
+ return mapper;
13059
+ }
13060
+ function defineEffect(effect) {
13061
+ return effect;
13062
+ }
13063
+ function defineStateMachine(definition) {
13064
+ const stateNames = new Set(Object.keys(definition.states));
13065
+ for (const [transitionName, transition] of Object.entries(
13066
+ definition.transitions
13067
+ )) {
13068
+ if (!stateNames.has(transition.from)) {
13069
+ throw new Error(
13070
+ `Transition ${transitionName} starts at undeclared state ${transition.from}`
13071
+ );
13072
+ }
13073
+ if (transition.effect.commit?.kind !== "transition") {
13074
+ throw new Error(
13075
+ `Transition ${transitionName} must use a transition-commit effect`
13076
+ );
13077
+ }
13078
+ const outcomes = Object.entries(transition.outcomes);
13079
+ const primary = outcomes.filter(([, outcome]) => outcome.primary === true);
13080
+ if (primary.length !== 1) {
13081
+ throw new Error(
13082
+ `Transition ${transitionName} must declare exactly one primary outcome`
13083
+ );
13084
+ }
13085
+ for (const [outcomeKey, outcome] of outcomes) {
13086
+ if (outcome.to !== "$current" && !stateNames.has(outcome.to)) {
13087
+ throw new Error(
13088
+ `Transition ${transitionName} outcome ${outcomeKey} targets undeclared state ${outcome.to}`
13089
+ );
13090
+ }
13091
+ if (outcome.to === "$current" && outcome.disposition !== "error") {
13092
+ throw new Error(
13093
+ `Transition ${transitionName} outcome ${outcomeKey} may use $current only with error disposition`
13094
+ );
13095
+ }
13096
+ }
13097
+ }
13098
+ for (const [transitionName, transition] of Object.entries(
13099
+ definition.transitions
13100
+ )) {
13101
+ definedTransitionMetadata.set(transition, {
13102
+ machine: definition.name,
13103
+ transition: transitionName
13104
+ });
13105
+ }
13106
+ return definition;
13107
+ }
13108
+ function requireNonEmptyString(value, path) {
13109
+ if (typeof value !== "string" || value.trim().length === 0) {
13110
+ throw new Error(`${path} must be a non-empty string`);
13111
+ }
13112
+ return value.trim();
13113
+ }
13114
+ function validateObjectReference(value, path) {
13115
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
13116
+ throw new Error(`${path} must be an object reference`);
13117
+ }
13118
+ const reference = value;
13119
+ requireNonEmptyString(reference.className, `${path}.className`);
13120
+ requireNonEmptyString(reference.id, `${path}.id`);
13121
+ if (reference.path !== void 0) {
13122
+ requireNonEmptyString(reference.path, `${path}.path`);
13123
+ }
13124
+ }
13125
+ function validateScalarRecord(value, path) {
13126
+ if (!value || typeof value !== "object" || Array.isArray(value) || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) {
13127
+ throw new Error(`${path} must be an object of scalar values`);
13128
+ }
13129
+ for (const [key, item] of Object.entries(value)) {
13130
+ if (item !== null && typeof item !== "string" && typeof item !== "number" && typeof item !== "boolean") {
13131
+ throw new Error(
13132
+ `${path}.${key} must be a string, finite number, boolean, or null`
13133
+ );
13134
+ }
13135
+ if (typeof item === "number" && !Number.isFinite(item)) {
13136
+ throw new Error(`${path}.${key} must be finite`);
13137
+ }
13138
+ }
13139
+ }
13140
+ function validateProjectedRecord(value, path) {
13141
+ validateObjectReference(value, path);
13142
+ const record = value;
13143
+ if (record.label !== void 0) {
13144
+ if (typeof record.label !== "string") {
13145
+ throw new Error(`${path}.label must be a string`);
13146
+ }
13147
+ if (record.label.length > MAX_SAFE_TEXT_LENGTH) {
13148
+ throw new Error(
13149
+ `${path}.label exceeds ${MAX_SAFE_TEXT_LENGTH} characters`
13150
+ );
13151
+ }
13152
+ }
13153
+ validateScalarRecord(record.fields, `${path}.fields`);
13154
+ }
13155
+ function validateBoundedJson(value, path, depth = 0) {
13156
+ if (depth > 12) throw new Error(`${path} is nested too deeply`);
13157
+ if (value === null || typeof value === "boolean") return;
13158
+ if (typeof value === "number") {
13159
+ if (!Number.isFinite(value)) throw new Error(`${path} must be finite`);
13160
+ return;
13161
+ }
13162
+ if (typeof value === "string") {
13163
+ if (value.length > MAX_SAFE_TEXT_LENGTH) {
13164
+ throw new Error(`${path} exceeds ${MAX_SAFE_TEXT_LENGTH} characters`);
13165
+ }
13166
+ return;
13167
+ }
13168
+ if (Array.isArray(value)) {
13169
+ if (value.length > MAX_PROJECTION_CHANGES) {
13170
+ throw new Error(`${path} contains too many values`);
13171
+ }
13172
+ value.forEach(
13173
+ (item, index) => validateBoundedJson(item, `${path}[${index}]`, depth + 1)
13174
+ );
13175
+ return;
13176
+ }
13177
+ if (!value || typeof value !== "object") {
13178
+ throw new Error(`${path} contains an unsupported value`);
13179
+ }
13180
+ const entries = Object.entries(value);
13181
+ if (entries.length > 256) throw new Error(`${path} contains too many keys`);
13182
+ for (const [key, item] of entries) {
13183
+ if (/token|secret|password|authorization|cookie/i.test(key)) {
13184
+ throw new Error(`${path}.${key} is not allowed in a commit projection`);
13185
+ }
13186
+ validateBoundedJson(item, `${path}.${key}`, depth + 1);
13187
+ }
13188
+ }
13189
+ function validateProjectionResult(declaration, projection) {
13190
+ if (!projection || typeof projection !== "object") {
13191
+ throw new Error("Projection mapper must return an object");
13192
+ }
13193
+ requireNonEmptyString(
13194
+ projection.source?.reference,
13195
+ "projection.source.reference"
13196
+ );
13197
+ if (projection.source.version !== void 0) {
13198
+ requireNonEmptyString(
13199
+ projection.source.version,
13200
+ "projection.source.version"
13201
+ );
13202
+ }
13203
+ if (!Array.isArray(projection.changes)) {
13204
+ throw new Error("projection.changes must be an array");
13205
+ }
13206
+ if (projection.changes.length > MAX_PROJECTION_CHANGES) {
13207
+ throw new Error(
13208
+ `projection.changes exceeds the ${MAX_PROJECTION_CHANGES} change limit`
13209
+ );
13210
+ }
13211
+ if (projection.primaryTarget) {
13212
+ validateObjectReference(
13213
+ projection.primaryTarget,
13214
+ "projection.primaryTarget"
13215
+ );
13216
+ }
13217
+ if (projection.safeSummary) {
13218
+ const entries = Object.entries(projection.safeSummary);
13219
+ if (entries.length > MAX_SAFE_SUMMARY_KEYS) {
13220
+ throw new Error(
13221
+ `projection.safeSummary exceeds the ${MAX_SAFE_SUMMARY_KEYS} key limit`
13222
+ );
13223
+ }
13224
+ validateBoundedJson(projection.safeSummary, "projection.safeSummary");
13225
+ validateScalarRecord(projection.safeSummary, "projection.safeSummary");
13226
+ }
13227
+ projection.changes.forEach((change, index) => {
13228
+ const changePath = `projection.changes[${index}]`;
13229
+ validateBoundedJson(change, changePath);
13230
+ if (!change || typeof change !== "object" || Array.isArray(change)) {
13231
+ throw new Error(`${changePath} must be an object`);
13232
+ }
13233
+ const rawChange = change;
13234
+ const kind = requireNonEmptyString(rawChange.kind, `${changePath}.kind`);
13235
+ if (kind === "object") {
13236
+ const operation = requireNonEmptyString(
13237
+ rawChange.operation,
13238
+ `${changePath}.operation`
13239
+ );
13240
+ if (operation !== "created" && operation !== "updated" && operation !== "deleted") {
13241
+ throw new Error(
13242
+ `${changePath}.operation must be created, updated, or deleted`
13243
+ );
13244
+ }
13245
+ if (operation === "deleted") {
13246
+ validateObjectReference(rawChange.target, `${changePath}.target`);
13247
+ } else {
13248
+ validateProjectedRecord(rawChange.record, `${changePath}.record`);
13249
+ }
13250
+ } else if (kind === "relationship") {
13251
+ const operation = requireNonEmptyString(
13252
+ rawChange.operation,
13253
+ `${changePath}.operation`
13254
+ );
13255
+ if (operation !== "connected" && operation !== "disconnected") {
13256
+ throw new Error(
13257
+ `${changePath}.operation must be connected or disconnected`
13258
+ );
13259
+ }
13260
+ requireNonEmptyString(
13261
+ rawChange.relationship,
13262
+ `${changePath}.relationship`
13263
+ );
13264
+ validateObjectReference(rawChange.from, `${changePath}.from`);
13265
+ validateObjectReference(rawChange.to, `${changePath}.to`);
13266
+ } else if (kind === "state_observation") {
13267
+ if (rawChange.operation !== void 0) {
13268
+ throw new Error(
13269
+ `${changePath}.operation is not valid for an observation`
13270
+ );
13271
+ }
13272
+ validateObjectReference(rawChange.target, `${changePath}.target`);
13273
+ requireNonEmptyString(rawChange.machine, `${changePath}.machine`);
13274
+ requireNonEmptyString(rawChange.state, `${changePath}.state`);
13275
+ } else {
13276
+ throw new Error(`${changePath}.kind is unsupported: ${kind}`);
13277
+ }
13278
+ });
13279
+ const objectOperations = /* @__PURE__ */ new Map();
13280
+ for (const change of projection.changes) {
13281
+ if (change.kind !== "object") continue;
13282
+ const reference = change.operation === "deleted" ? change.target : change.record;
13283
+ const key = `${reference.className}\0${reference.id}`;
13284
+ const operations = objectOperations.get(key) || {
13285
+ deleted: false,
13286
+ upserted: false
13287
+ };
13288
+ if (change.operation === "deleted") operations.deleted = true;
13289
+ else operations.upserted = true;
13290
+ if (operations.deleted && operations.upserted) {
13291
+ throw new Error(
13292
+ `projection.changes cannot both delete and upsert ${reference.className}/${reference.id}; return only its canonical final state`
13293
+ );
13294
+ }
13295
+ objectOperations.set(key, operations);
13296
+ }
13297
+ const outcome = projection.outcome;
13298
+ if (declaration.kind === "transition") {
13299
+ if (!outcome) {
13300
+ throw new Error("A transition projection must return an outcome");
13301
+ }
13302
+ requireNonEmptyString(outcome.key, "projection.outcome.key");
13303
+ if (outcome.error) {
13304
+ validateBoundedJson(outcome.error, "projection.outcome.error");
13305
+ }
13306
+ } else if (projection.outcome !== void 0) {
13307
+ throw new Error("An effect projection cannot declare a transition outcome");
13308
+ }
13309
+ }
13310
+ function canonicalizeCommitValue(value) {
13311
+ const normalize = (current) => {
13312
+ if (current === null || typeof current === "boolean" || typeof current === "string") {
13313
+ return typeof current === "string" ? current.normalize("NFC") : current;
13314
+ }
13315
+ if (typeof current === "number") {
13316
+ if (!Number.isFinite(current)) {
13317
+ throw new Error("Cannot canonicalize a non-finite number");
13318
+ }
13319
+ return Object.is(current, -0) ? 0 : current;
13320
+ }
13321
+ if (Array.isArray(current)) return current.map(normalize);
13322
+ if (current && typeof current === "object") {
13323
+ return Object.fromEntries(
13324
+ Object.entries(current).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key.normalize("NFC"), normalize(item)])
13325
+ );
13326
+ }
13327
+ throw new Error(`Cannot canonicalize ${typeof current}`);
13328
+ };
13329
+ return JSON.stringify(normalize(value));
13330
+ }
13331
+
13033
13332
  // src/effect-runtime.ts
13034
13333
  function computeEffectKey(effect) {
13035
13334
  const attachedClass = effect.className?.trim();
@@ -13094,6 +13393,45 @@ function resolveInvocationMode(context) {
13094
13393
  }
13095
13394
  return "execute";
13096
13395
  }
13396
+ async function sha256Hex(value) {
13397
+ const digest = await globalThis.crypto.subtle.digest(
13398
+ "SHA-256",
13399
+ new TextEncoder().encode(value)
13400
+ );
13401
+ return Array.from(
13402
+ new Uint8Array(digest),
13403
+ (byte) => byte.toString(16).padStart(2, "0")
13404
+ ).join("");
13405
+ }
13406
+ async function resolveInvocationIdempotencyKey(request) {
13407
+ const supplied = request.context?.idempotencyKey?.trim();
13408
+ if (supplied) return supplied;
13409
+ const invocationId = request.context?.invocationId?.trim();
13410
+ if (!invocationId) {
13411
+ throw new Error(
13412
+ `Committed effect ${request.effectKey} requires an invocation id`
13413
+ );
13414
+ }
13415
+ const digest = await sha256Hex(
13416
+ canonicalizeCommitValue({
13417
+ sandboxId: request.context?.sandboxId || "",
13418
+ environmentId: request.context?.environmentId || "",
13419
+ effectKey: request.effectKey,
13420
+ invocationId,
13421
+ input: request.input
13422
+ })
13423
+ );
13424
+ return `gci_${digest}`;
13425
+ }
13426
+ function createUnavailableCommitContext(message) {
13427
+ const unavailable = async () => {
13428
+ throw new Error(message);
13429
+ };
13430
+ return {
13431
+ effect: unavailable,
13432
+ transition: unavailable
13433
+ };
13434
+ }
13097
13435
  function resolveReverseEffect(effectMap, currentEffect, request, behaviors) {
13098
13436
  const explicitReverseHandler = request.context?.invocation?.reverseHandler?.trim();
13099
13437
  const configuredReverseHandler = behaviors.reverse?.handler?.trim();
@@ -13165,22 +13503,32 @@ function resolveHandlerForMode(effectMap, effect, request) {
13165
13503
  throw new Error(`Dry run is not supported for ${request.effectKey}`);
13166
13504
  }
13167
13505
  if (mode === "reverse") {
13506
+ const sourceEffectKey = request.context?.invocation?.sourceEffectKey?.trim();
13507
+ if (sourceEffectKey && sourceEffectKey !== request.effectKey) {
13508
+ return { effect, mode, handler: effect.handler };
13509
+ }
13510
+ const namedReverseHandler = request.context?.invocation?.reverseHandler?.trim() || behaviors.reverse?.handler?.trim();
13511
+ if (namedReverseHandler) {
13512
+ const reverseEffect = resolveReverseEffect(
13513
+ effectMap,
13514
+ effect,
13515
+ request,
13516
+ behaviors
13517
+ );
13518
+ if (reverseEffect) {
13519
+ return {
13520
+ effect: reverseEffect,
13521
+ mode,
13522
+ handler: reverseEffect.handler
13523
+ };
13524
+ }
13525
+ throw new Error(
13526
+ `Reverse effect ${namedReverseHandler} is not registered for ${request.effectKey}`
13527
+ );
13528
+ }
13168
13529
  if (effect.reverseHandler) {
13169
13530
  return { effect, mode, handler: effect.reverseHandler };
13170
13531
  }
13171
- const reverseEffect = resolveReverseEffect(
13172
- effectMap,
13173
- effect,
13174
- request,
13175
- behaviors
13176
- );
13177
- if (reverseEffect) {
13178
- return {
13179
- effect: reverseEffect,
13180
- mode,
13181
- handler: reverseEffect.reverseHandler || reverseEffect.handler
13182
- };
13183
- }
13184
13532
  throw new Error(
13185
13533
  `Reverse execution is not supported for ${request.effectKey}`
13186
13534
  );
@@ -13281,18 +13629,142 @@ async function invokeRegisteredEffect(effectMap, request, options = {}) {
13281
13629
  throw new Error(`Effect handler not found: ${request.effectKey}`);
13282
13630
  }
13283
13631
  const resolved = resolveHandlerForMode(effectMap, effect, request);
13632
+ const isPreResolvedNamedReverse = resolved.mode === "reverse" && Boolean(request.context?.invocation?.sourceEffectKey?.trim()) && request.context?.invocation?.sourceEffectKey?.trim() !== request.effectKey;
13633
+ const resolvedEffectKey = computeEffectKey(resolved.effect);
13634
+ if (resolved.mode === "reverse" && resolved.effect !== effect && (effect.commit || resolved.effect.commit)) {
13635
+ throw new Error(
13636
+ `Committed named reverse effect ${resolvedEffectKey} must be reserved and dispatched using its own effect key; customer code was not invoked`
13637
+ );
13638
+ }
13639
+ const declaration = resolved.effect.commit;
13640
+ const commitTransport = options.commit;
13641
+ const commitRequired = Boolean(declaration) && (resolved.mode === "execute" || resolved.mode === "reverse");
13642
+ if (commitRequired && !commitTransport) {
13643
+ throw new Error(
13644
+ `Committed effect ${request.effectKey} has no durable commit transport; customer code was not invoked`
13645
+ );
13646
+ }
13647
+ if (commitRequired && declaration?.kind === "transition" && !request.context?.invocation?.transition) {
13648
+ throw new Error(
13649
+ `Transition effect ${request.effectKey} has no resolved transition context; customer code was not invoked`
13650
+ );
13651
+ }
13652
+ const idempotencyKey = commitRequired ? await resolveInvocationIdempotencyKey(request) : request.context?.idempotencyKey;
13653
+ let commitStarted = false;
13654
+ let commitPromise = null;
13655
+ const beginCommit = (requestedKind, productResult) => {
13656
+ if (!declaration || !commitRequired || !commitTransport) {
13657
+ return Promise.reject(
13658
+ new Error(
13659
+ `Effect ${request.effectKey} does not declare an active ${requestedKind} commit`
13660
+ )
13661
+ );
13662
+ }
13663
+ if (declaration.kind !== requestedKind) {
13664
+ return Promise.reject(
13665
+ new Error(
13666
+ `Effect ${request.effectKey} declares ${declaration.kind} commit, not ${requestedKind}`
13667
+ )
13668
+ );
13669
+ }
13670
+ if (commitStarted) {
13671
+ return Promise.reject(
13672
+ new Error(
13673
+ `Effect invocation ${request.context?.invocationId || request.effectKey} already emitted its commit`
13674
+ )
13675
+ );
13676
+ }
13677
+ commitStarted = true;
13678
+ commitPromise = (async () => {
13679
+ let projection;
13680
+ try {
13681
+ projection = declaration.project(productResult);
13682
+ validateProjectionResult(declaration, projection);
13683
+ } catch (error) {
13684
+ const message = error instanceof Error ? error.message : String(error);
13685
+ if (commitTransport.mappingFailed) {
13686
+ await commitTransport.mappingFailed({
13687
+ effectKey: request.effectKey,
13688
+ effectName: request.effectName,
13689
+ invocationId: request.context?.invocationId || "",
13690
+ idempotencyKey: idempotencyKey || "",
13691
+ environmentId: request.context?.environmentId || "",
13692
+ message
13693
+ });
13694
+ }
13695
+ throw new Error(
13696
+ `Product mutation may have succeeded, but its commit projection is invalid: ${message}`
13697
+ );
13698
+ }
13699
+ if (requestedKind === "transition") {
13700
+ const transition = request.context?.invocation?.transition;
13701
+ const outcome = projection.outcome;
13702
+ if (!transition || !outcome?.key) {
13703
+ throw new Error(
13704
+ `Transition effect ${request.effectKey} did not resolve an authored outcome`
13705
+ );
13706
+ }
13707
+ if (!Object.prototype.hasOwnProperty.call(
13708
+ transition.outcomes,
13709
+ outcome.key
13710
+ )) {
13711
+ throw new Error(
13712
+ `Transition ${transition.machine}.${transition.transition} does not declare outcome ${outcome.key}`
13713
+ );
13714
+ }
13715
+ }
13716
+ const invocationId = request.context?.invocationId || "";
13717
+ const environmentId = request.context?.environmentId || "";
13718
+ const sandboxId = request.context?.sandboxId || "";
13719
+ if (!invocationId || !environmentId || !sandboxId || !idempotencyKey) {
13720
+ throw new Error(
13721
+ `Committed effect ${request.effectKey} is missing its trusted invocation scope`
13722
+ );
13723
+ }
13724
+ const commitRequest = {
13725
+ kind: requestedKind,
13726
+ effectKey: request.effectKey,
13727
+ effectName: request.effectName,
13728
+ operationLabel: resolved.effect.label || resolved.effect.name,
13729
+ invocationId,
13730
+ idempotencyKey,
13731
+ sandboxId,
13732
+ environmentId,
13733
+ ...request.context?.sessionId ? { sessionId: request.context.sessionId } : {},
13734
+ ...request.context?.jobId ? { jobId: request.context.jobId } : {},
13735
+ ...request.context?.buildId ? { buildId: request.context.buildId } : {},
13736
+ ...request.context?.invocation?.artifactId ? { artifactId: request.context.invocation.artifactId } : {},
13737
+ projection,
13738
+ ...requestedKind === "transition" && request.context?.invocation?.transition ? { transition: request.context.invocation.transition } : {}
13739
+ };
13740
+ const receipt = await commitTransport.persist(commitRequest);
13741
+ await options.commitAcknowledged?.(receipt);
13742
+ return receipt;
13743
+ })();
13744
+ return commitPromise;
13745
+ };
13746
+ const commitContext = commitRequired ? {
13747
+ effect: (productResult) => beginCommit("effect", productResult),
13748
+ transition: (productResult) => beginCommit("transition", productResult)
13749
+ } : createUnavailableCommitContext(
13750
+ `Effect ${request.effectKey} is not executing a declared product commit`
13751
+ );
13284
13752
  const context = {
13285
13753
  ...request.context || {},
13754
+ ...idempotencyKey ? { idempotencyKey } : {},
13755
+ commit: commitContext,
13286
13756
  behaviors: normalizeEffectBehaviors(
13287
13757
  request.context?.behaviors || effect.metamodels || void 0
13288
13758
  ),
13289
13759
  invocation: {
13290
13760
  mode: resolved.mode,
13291
- sourceEffectKey: request.effectKey,
13292
- sourceEffectName: request.effectName,
13761
+ ...commitRequired && declaration ? { commitKind: declaration.kind } : {},
13762
+ sourceEffectKey: isPreResolvedNamedReverse ? request.context?.invocation?.sourceEffectKey || request.effectKey : request.effectKey,
13763
+ sourceEffectName: isPreResolvedNamedReverse ? request.context?.invocation?.sourceEffectName || request.effectName : request.effectName,
13293
13764
  ...request.context?.invocation?.reverseHandler ? { reverseHandler: request.context.invocation.reverseHandler } : {},
13294
13765
  ...request.context?.invocation?.artifactId ? { artifactId: request.context.invocation.artifactId } : {},
13295
- ...request.context?.invocation?.artifactOptions ? { artifactOptions: request.context.invocation.artifactOptions } : {}
13766
+ ...request.context?.invocation?.artifactOptions ? { artifactOptions: request.context.invocation.artifactOptions } : {},
13767
+ ...request.context?.invocation?.transition ? { transition: request.context.invocation.transition } : {}
13296
13768
  }
13297
13769
  };
13298
13770
  const feedbackContext = options.feedback ? createInvocationFeedbackContext(options.feedback) : null;
@@ -13316,6 +13788,16 @@ async function invokeRegisteredEffect(effectMap, request, options = {}) {
13316
13788
  handlerFailed = true;
13317
13789
  handlerError = error;
13318
13790
  }
13791
+ let commitError;
13792
+ let commitFailed = false;
13793
+ if (commitPromise) {
13794
+ try {
13795
+ await commitPromise;
13796
+ } catch (error) {
13797
+ commitFailed = true;
13798
+ commitError = error;
13799
+ }
13800
+ }
13319
13801
  let feedbackError;
13320
13802
  let feedbackFailed = false;
13321
13803
  if (feedbackContext) {
@@ -13329,9 +13811,17 @@ async function invokeRegisteredEffect(effectMap, request, options = {}) {
13329
13811
  if (handlerFailed) {
13330
13812
  throw handlerError;
13331
13813
  }
13814
+ if (commitFailed) {
13815
+ throw commitError;
13816
+ }
13332
13817
  if (feedbackFailed) {
13333
13818
  throw feedbackError;
13334
13819
  }
13820
+ if (commitRequired && !commitStarted) {
13821
+ throw new Error(
13822
+ `Mutating effect ${request.effectKey} returned without acknowledging its product mutation`
13823
+ );
13824
+ }
13335
13825
  return handlerResult;
13336
13826
  }
13337
13827
 
@@ -13400,12 +13890,7 @@ function toRecordSearchResult(className, node) {
13400
13890
  (field) => normalizeGraphPathSegment(field.name) === "real_id" && typeof field.value === "string" && field.value.trim()
13401
13891
  );
13402
13892
  const id = typeof realIdField?.value === "string" ? realIdField.value.trim() : graphPathId;
13403
- const rawLabel = typeof node.label === "string" && node.label.trim() ? node.label : "";
13404
- if (fields.length === 0 && rawLabel && isPlaceholderRecordLabel(rawLabel, graphPathId, path)) {
13405
- return null;
13406
- }
13407
- const fallbackLabel = displayLabelFromFields(fields);
13408
- const label = rawLabel && !isPlaceholderRecordLabel(rawLabel, id, path) ? rawLabel : fallbackLabel || rawLabel || id;
13893
+ const label = typeof node.label === "string" && node.label.trim() ? node.label.trim() : path || id;
13409
13894
  return {
13410
13895
  path,
13411
13896
  className,
@@ -13415,30 +13900,6 @@ function toRecordSearchResult(className, node) {
13415
13900
  fields
13416
13901
  };
13417
13902
  }
13418
- function isPlaceholderRecordLabel(label, id, path) {
13419
- const normalizedLabel = normalizeGraphPathSegment(label);
13420
- return normalizedLabel === normalizeGraphPathSegment(id) || normalizedLabel === normalizeGraphPathSegment(path);
13421
- }
13422
- function displayLabelFromFields(fields) {
13423
- const preferredFieldNames = [
13424
- "name",
13425
- "title",
13426
- "label",
13427
- "display_name",
13428
- "file_name",
13429
- "number",
13430
- "code"
13431
- ];
13432
- for (const preferred of preferredFieldNames) {
13433
- const match = fields.find(
13434
- (field) => normalizeGraphPathSegment(field.name) === preferred && typeof field.value === "string" && field.value.trim()
13435
- );
13436
- if (typeof match?.value === "string") {
13437
- return match.value.trim();
13438
- }
13439
- }
13440
- return null;
13441
- }
13442
13903
  function normalizeRecordSearchText(value) {
13443
13904
  return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, " ").replace(/\s+/g, " ").trim();
13444
13905
  }
@@ -14401,7 +14862,8 @@ function normalizeStateMachines(values) {
14401
14862
  requirements: parseJsonRecord(transition?.requirements) || parseJsonRecord(transition?.requirements_json),
14402
14863
  permission: parseJsonValue(transition?.permission) ?? parseJsonValue(transition?.permission_json),
14403
14864
  risk: transition?.risk === "low" || transition?.risk === "medium" || transition?.risk === "high" ? transition.risk : null,
14404
- expectedOutcome: parseJsonValue(transition?.expectedOutcome) ?? parseJsonValue(transition?.expected_outcome_json)
14865
+ expectedOutcome: parseJsonValue(transition?.expectedOutcome) ?? parseJsonValue(transition?.expected_outcome_json),
14866
+ outcomes: parseJsonRecord(transition?.outcomes) || parseJsonRecord(transition?.outcomes_json)
14405
14867
  })).filter(
14406
14868
  (transition) => transition.name.length > 0 && transition.from.length > 0 && transition.to.length > 0
14407
14869
  );
@@ -14494,6 +14956,11 @@ function transitionMetadataGraphqlArgs(transition) {
14494
14956
  `expected_outcome_json: ${JSON.stringify(JSON.stringify(transition.expectedOutcome))}`
14495
14957
  );
14496
14958
  }
14959
+ if (transition.outcomes) {
14960
+ args.push(
14961
+ `outcomes_json: ${JSON.stringify(JSON.stringify(transition.outcomes))}`
14962
+ );
14963
+ }
14497
14964
  return args.length > 0 ? `, ${args.join(", ")}` : "";
14498
14965
  }
14499
14966
  function buildStateMachineModelMutations(modelPath, machines) {
@@ -14556,7 +15023,7 @@ function buildMachineTypes(classSummary, machine) {
14556
15023
  ];
14557
15024
  }
14558
15025
  function buildMachineMethods(classSummary, machine) {
14559
- const stateName = stateTypeName(classSummary.name, machine.name);
15026
+ const stateName2 = stateTypeName(classSummary.name, machine.name);
14560
15027
  const transitionName = transitionTypeName(classSummary.name, machine.name);
14561
15028
  pathTypeName(classSummary.name, machine.name);
14562
15029
  const docsPrefix = `${classSummary.name}.${machine.name}`;
@@ -14566,12 +15033,12 @@ function buildMachineMethods(classSummary, machine) {
14566
15033
  docs: [`Get the current ${docsPrefix} state.`],
14567
15034
  static: false,
14568
15035
  params: [],
14569
- returnType: `Promise<${stateName} | null>`,
15036
+ returnType: `Promise<${stateName2} | null>`,
14570
15037
  runtime: {
14571
15038
  kind: "state_machine",
14572
15039
  machineName: machine.name,
14573
15040
  className: classSummary.name,
14574
- stateTypeName: stateName,
15041
+ stateTypeName: stateName2,
14575
15042
  transitionTypeName: transitionName,
14576
15043
  operation: "get_current"
14577
15044
  }
@@ -14582,13 +15049,13 @@ function buildMachineMethods(classSummary, machine) {
14582
15049
  `Reach a ${docsPrefix} state through the shortest allowed transition path.`
14583
15050
  ],
14584
15051
  static: false,
14585
- params: [{ name: "target", type: stateName }],
15052
+ params: [{ name: "target", type: stateName2 }],
14586
15053
  returnType: `Promise<${toPascalCase2(classSummary.name)}>`,
14587
15054
  runtime: {
14588
15055
  kind: "state_machine",
14589
15056
  machineName: machine.name,
14590
15057
  className: classSummary.name,
14591
- stateTypeName: stateName,
15058
+ stateTypeName: stateName2,
14592
15059
  transitionTypeName: transitionName,
14593
15060
  operation: "reach"
14594
15061
  }
@@ -14603,7 +15070,7 @@ function buildMachineMethods(classSummary, machine) {
14603
15070
  kind: "state_machine",
14604
15071
  machineName: machine.name,
14605
15072
  className: classSummary.name,
14606
- stateTypeName: stateName,
15073
+ stateTypeName: stateName2,
14607
15074
  transitionTypeName: transitionName,
14608
15075
  operation: "list_transitions"
14609
15076
  }
@@ -14613,12 +15080,12 @@ function buildMachineMethods(classSummary, machine) {
14613
15080
  docs: [`List reachable states for ${docsPrefix} from the current state.`],
14614
15081
  static: false,
14615
15082
  params: [],
14616
- returnType: `Promise<${stateName}[]>`,
15083
+ returnType: `Promise<${stateName2}[]>`,
14617
15084
  runtime: {
14618
15085
  kind: "state_machine",
14619
15086
  machineName: machine.name,
14620
15087
  className: classSummary.name,
14621
- stateTypeName: stateName,
15088
+ stateTypeName: stateName2,
14622
15089
  transitionTypeName: transitionName,
14623
15090
  operation: "list_reachable_states"
14624
15091
  }
@@ -14633,7 +15100,7 @@ function buildMachineMethods(classSummary, machine) {
14633
15100
  kind: "state_machine",
14634
15101
  machineName: machine.name,
14635
15102
  className: classSummary.name,
14636
- stateTypeName: stateName,
15103
+ stateTypeName: stateName2,
14637
15104
  transitionTypeName: transitionName,
14638
15105
  operation: "is_final"
14639
15106
  }
@@ -14644,13 +15111,13 @@ function buildMachineMethods(classSummary, machine) {
14644
15111
  `List shortest transition paths from the current ${docsPrefix} state to a target state.`
14645
15112
  ],
14646
15113
  static: false,
14647
- params: [{ name: "target", type: stateName }],
14648
- returnType: `Promise<Array<{ states: ${stateName}[]; transitions: ${transitionName}[] }>>`,
15114
+ params: [{ name: "target", type: stateName2 }],
15115
+ returnType: `Promise<Array<{ states: ${stateName2}[]; transitions: ${transitionName}[] }>>`,
14649
15116
  runtime: {
14650
15117
  kind: "state_machine",
14651
15118
  machineName: machine.name,
14652
15119
  className: classSummary.name,
14653
- stateTypeName: stateName,
15120
+ stateTypeName: stateName2,
14654
15121
  transitionTypeName: transitionName,
14655
15122
  operation: "paths_to"
14656
15123
  }
@@ -14674,7 +15141,7 @@ function buildMachineMethods(classSummary, machine) {
14674
15141
  kind: "state_machine",
14675
15142
  machineName: machine.name,
14676
15143
  className: classSummary.name,
14677
- stateTypeName: stateName,
15144
+ stateTypeName: stateName2,
14678
15145
  transitionTypeName: transitionName,
14679
15146
  operation: "reach",
14680
15147
  targetState: stateNameValue,
@@ -14693,7 +15160,7 @@ function buildMachineMethods(classSummary, machine) {
14693
15160
  kind: "state_machine",
14694
15161
  machineName: machine.name,
14695
15162
  className: classSummary.name,
14696
- stateTypeName: stateName,
15163
+ stateTypeName: stateName2,
14697
15164
  transitionTypeName: transitionName,
14698
15165
  operation: "prepare_reach",
14699
15166
  targetState: stateNameValue,
@@ -14706,7 +15173,7 @@ function buildMachineMethods(classSummary, machine) {
14706
15173
  kind: "state_machine",
14707
15174
  machineName: machine.name,
14708
15175
  className: classSummary.name,
14709
- stateTypeName: stateName,
15176
+ stateTypeName: stateName2,
14710
15177
  transitionTypeName: transitionName,
14711
15178
  operation: "prepare_create_reach",
14712
15179
  targetState: stateNameValue,
@@ -14772,7 +15239,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14772
15239
  name: String!
14773
15240
  state_machine: StateMachine!
14774
15241
  add_state(name: String!, is_final: Boolean, label: String, description: String): StateMachineMutation!
14775
- 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!
15242
+ 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!
14776
15243
  activate_transition(name: String!): StateMachineMutation!
14777
15244
  }
14778
15245
 
@@ -14789,6 +15256,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14789
15256
  type StateMachineSnapshotMutation {
14790
15257
  snapshot: StateMachineSnapshot!
14791
15258
  activate_transition(name: String!): StateMachineSnapshotMutation!
15259
+ commit_transition(name: String!, outcome: String!, to: String!, commit_id: String!, source_version: String): StateMachineSnapshotMutation!
14792
15260
  observe_state(state: String!, force: Boolean, source: String): StateMachineSnapshotMutation!
14793
15261
  }
14794
15262
 
@@ -14835,6 +15303,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14835
15303
  permission_json: String
14836
15304
  risk: String
14837
15305
  expected_outcome_json: String
15306
+ outcomes_json: String
14838
15307
  }
14839
15308
 
14840
15309
  type StateMachinePath {
@@ -14845,6 +15314,10 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14845
15314
  type StateMachineTransitionEvent {
14846
15315
  sequence: Int!
14847
15316
  occurred_at: Float!
15317
+ commit_id: String
15318
+ outcome: String
15319
+ source_version: String
15320
+ projected_from_mismatch: String
14848
15321
  transition: StateMachineTransition!
14849
15322
  from: StateMachineState!
14850
15323
  to: StateMachineState!
@@ -14910,7 +15383,8 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14910
15383
  requirements_json,
14911
15384
  permission_json,
14912
15385
  risk,
14913
- expected_outcome_json
15386
+ expected_outcome_json,
15387
+ outcomes_json
14914
15388
  }) => {
14915
15389
  await run(
14916
15390
  value.target.add_state_machine_transition(
@@ -14926,7 +15400,8 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14926
15400
  requirementsJson: requirements_json,
14927
15401
  permissionJson: permission_json,
14928
15402
  risk,
14929
- expectedOutcomeJson: expected_outcome_json
15403
+ expectedOutcomeJson: expected_outcome_json,
15404
+ outcomesJson: outcomes_json
14930
15405
  }
14931
15406
  )
14932
15407
  );
@@ -14947,6 +15422,19 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14947
15422
  );
14948
15423
  return value;
14949
15424
  },
15425
+ commit_transition: async (value, { name, outcome, to, commit_id, source_version }) => {
15426
+ await run(
15427
+ value.target.commit_state_machine_transition(
15428
+ value.name,
15429
+ name,
15430
+ outcome,
15431
+ to,
15432
+ commit_id,
15433
+ source_version
15434
+ )
15435
+ );
15436
+ return value;
15437
+ },
14950
15438
  observe_state: async (value, { state, force, source }) => {
14951
15439
  await run(
14952
15440
  value.target.observe_state_machine_state(
@@ -14976,7 +15464,8 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14976
15464
  requirements_json: (value) => value.requirements_json || null,
14977
15465
  permission_json: (value) => value.permission_json || null,
14978
15466
  risk: (value) => value.risk || null,
14979
- expected_outcome_json: (value) => value.expected_outcome_json || null
15467
+ expected_outcome_json: (value) => value.expected_outcome_json || null,
15468
+ outcomes_json: (value) => value.outcomes_json || null
14980
15469
  },
14981
15470
  StateMachinePath: {
14982
15471
  states: (value) => value.states,
@@ -14985,6 +15474,10 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
14985
15474
  StateMachineTransitionEvent: {
14986
15475
  sequence: (value) => value.sequence,
14987
15476
  occurred_at: (value) => value.occurred_at,
15477
+ commit_id: (value) => value.commit_id || null,
15478
+ outcome: (value) => value.outcome || null,
15479
+ source_version: (value) => value.source_version || null,
15480
+ projected_from_mismatch: (value) => value.projected_from_mismatch || null,
14988
15481
  transition: (value) => value.transition,
14989
15482
  from: (value) => value.from,
14990
15483
  to: (value) => value.to
@@ -15058,6 +15551,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
15058
15551
  permission_json
15059
15552
  risk
15060
15553
  expected_outcome_json
15554
+ outcomes_json
15061
15555
  }
15062
15556
  }`
15063
15557
  ]
@@ -15785,6 +16279,133 @@ var Environment = class _Environment {
15785
16279
  getAwaitingCount: async () => this.getAwaitingRecordCount()
15786
16280
  };
15787
16281
  }
16282
+ /**
16283
+ * Acknowledge a declared product mutation that already happened outside a
16284
+ * Granular-run effect (for example, in a webhook consumer). These methods
16285
+ * run the declaration's pure projection mapper; they never call its handler.
16286
+ */
16287
+ get commit() {
16288
+ return {
16289
+ effect: async (effect, productResult, options = {}) => this.persistExternalEffect(effect, productResult, options),
16290
+ transition: async (transition, productResult, options = {}) => this.persistExternalTransition(transition, productResult, options),
16291
+ get: async (commitId) => this.controlPlaneRequest(
16292
+ `/control/environments/${this.environmentId}/commits/${encodeURIComponent(
16293
+ commitId
16294
+ )}`
16295
+ ),
16296
+ retry: async (commitId) => this.controlPlaneRequest(
16297
+ `/control/environments/${this.environmentId}/commits/${encodeURIComponent(
16298
+ commitId
16299
+ )}/retry`,
16300
+ { method: "POST" }
16301
+ )
16302
+ };
16303
+ }
16304
+ /** Inspect or retry the agent synchronization of a product snapshot. */
16305
+ get observation() {
16306
+ return {
16307
+ get: async (observationId) => this.controlPlaneRequest(
16308
+ `/control/environments/${this.environmentId}/observations/${encodeURIComponent(
16309
+ observationId
16310
+ )}`
16311
+ ),
16312
+ retry: async (observationId) => this.controlPlaneRequest(
16313
+ `/control/environments/${this.environmentId}/observations/${encodeURIComponent(
16314
+ observationId
16315
+ )}/retry`,
16316
+ { method: "POST" }
16317
+ )
16318
+ };
16319
+ }
16320
+ async persistExternalEffect(effect, productResult, options) {
16321
+ const projection = effect.commit.project(productResult);
16322
+ validateProjectionResult(effect.commit, projection);
16323
+ this.requireExternalIdentity(projection.source.version, options);
16324
+ return this.controlPlaneRequest(
16325
+ `/control/environments/${this.environmentId}/external-commits`,
16326
+ {
16327
+ method: "POST",
16328
+ body: JSON.stringify({
16329
+ kind: "effect",
16330
+ effectKey: computeEffectKey2(effect),
16331
+ ...options.sourceEventId?.trim() ? { sourceEventId: options.sourceEventId.trim() } : {},
16332
+ projection
16333
+ })
16334
+ }
16335
+ );
16336
+ }
16337
+ async persistExternalTransition(transition, productResult, options) {
16338
+ const metadata = getDefinedTransitionMetadata(transition);
16339
+ if (!metadata) {
16340
+ throw new Error(
16341
+ "environment.commit.transition requires a transition returned by defineStateMachine"
16342
+ );
16343
+ }
16344
+ const projection = transition.effect.commit.project(productResult);
16345
+ validateProjectionResult(transition.effect.commit, projection);
16346
+ this.requireExternalIdentity(projection.source.version, options);
16347
+ if (!Object.prototype.hasOwnProperty.call(
16348
+ transition.outcomes,
16349
+ projection.outcome.key
16350
+ )) {
16351
+ throw new Error(
16352
+ `Transition ${metadata.machine}.${metadata.transition} does not declare outcome ${projection.outcome.key}`
16353
+ );
16354
+ }
16355
+ if (!projection.primaryTarget) {
16356
+ throw new Error(
16357
+ "An external transition projection requires primaryTarget to identify the transitioned product record"
16358
+ );
16359
+ }
16360
+ return this.controlPlaneRequest(
16361
+ `/control/environments/${this.environmentId}/external-commits`,
16362
+ {
16363
+ method: "POST",
16364
+ body: JSON.stringify({
16365
+ kind: "transition",
16366
+ effectKey: computeEffectKey2(transition.effect),
16367
+ ...options.sourceEventId?.trim() ? { sourceEventId: options.sourceEventId.trim() } : {},
16368
+ projection,
16369
+ transition: {
16370
+ className: projection.primaryTarget.className,
16371
+ objectId: projection.primaryTarget.id,
16372
+ ...projection.primaryTarget.path ? { objectPath: projection.primaryTarget.path } : {},
16373
+ machine: metadata.machine,
16374
+ transition: metadata.transition,
16375
+ from: transition.from
16376
+ }
16377
+ })
16378
+ }
16379
+ );
16380
+ }
16381
+ requireExternalIdentity(sourceVersion, options) {
16382
+ if (!options.sourceEventId?.trim() && !sourceVersion?.trim()) {
16383
+ throw new Error(
16384
+ "An external commit requires sourceEventId or a source version from its projection mapper"
16385
+ );
16386
+ }
16387
+ }
16388
+ /**
16389
+ * Synchronize a versioned product snapshot without claiming an effect or
16390
+ * lifecycle transition. This records no transition history.
16391
+ */
16392
+ async observe(mapper, productResult) {
16393
+ const declaration = { kind: "effect"};
16394
+ const projection = mapper(productResult);
16395
+ validateProjectionResult(declaration, projection);
16396
+ if (!projection.source.version?.trim()) {
16397
+ throw new Error(
16398
+ "environment.observe requires a monotonic source version or serialized adapter sequence"
16399
+ );
16400
+ }
16401
+ return this.controlPlaneRequest(
16402
+ `/control/environments/${this.environmentId}/observations`,
16403
+ {
16404
+ method: "POST",
16405
+ body: JSON.stringify({ projection })
16406
+ }
16407
+ );
16408
+ }
15788
16409
  /**
15789
16410
  * Mirror product-owned workflow state into Granular without making Granular
15790
16411
  * own the customer application's state machine.
@@ -15824,8 +16445,8 @@ var Environment = class _Environment {
15824
16445
  * `await env.recordState({ className: "spend_request", id, machine: "lifecycle", state: "policy_review", source: "customer_backend" })`
15825
16446
  */
15826
16447
  state(target) {
15827
- const observe = async (machineName, stateName, input = {}) => {
15828
- const observedState = input.observedState || input.state || stateName;
16448
+ const observe = async (machineName, stateName2, input = {}) => {
16449
+ const observedState = input.observedState || input.state || stateName2;
15829
16450
  if (!observedState) {
15830
16451
  throw new Error("State observation requires a target state");
15831
16452
  }
@@ -15851,7 +16472,7 @@ var Environment = class _Environment {
15851
16472
  {
15852
16473
  get: (_machineTarget, stateProperty) => {
15853
16474
  if (stateProperty === "to") {
15854
- return (stateName, input) => observe(machineProperty, stateName, input || {});
16475
+ return (stateName2, input) => observe(machineProperty, stateName2, input || {});
15855
16476
  }
15856
16477
  if (typeof stateProperty !== "string") return void 0;
15857
16478
  return (input) => observe(
@@ -17191,6 +17812,14 @@ var EnvironmentSession = class extends Session {
17191
17812
  }
17192
17813
  };
17193
17814
  }
17815
+ get mutations() {
17816
+ return {
17817
+ list: (options = {}) => this.sessionDataRequest(
17818
+ "/mutations",
17819
+ options
17820
+ )
17821
+ };
17822
+ }
17194
17823
  get artifacts() {
17195
17824
  return {
17196
17825
  list: (options = {}) => {
@@ -18690,6 +19319,7 @@ var Granular = class _Granular {
18690
19319
  const serialized = {
18691
19320
  effectKey: computeEffectKey2(effect),
18692
19321
  name: effect.name,
19322
+ ...effect.label ? { label: effect.label } : {},
18693
19323
  description: effect.description,
18694
19324
  inputSchema: effect.inputSchema,
18695
19325
  stability: effect.stability || "stable",
@@ -18713,6 +19343,9 @@ var Granular = class _Granular {
18713
19343
  if (effect.metamodels !== void 0) {
18714
19344
  serialized.metamodels = effect.metamodels;
18715
19345
  }
19346
+ if (effect.commit !== void 0) {
19347
+ serialized.commit = { kind: effect.commit.kind };
19348
+ }
18716
19349
  return serialized;
18717
19350
  }
18718
19351
  async publishSandboxEffectCatalog(host) {
@@ -18933,16 +19566,60 @@ var Granular = class _Granular {
18933
19566
  };
18934
19567
  wsClient.registerRpcHandler("effect.invoke", async (params) => {
18935
19568
  const request = params;
18936
- return invokeRegisteredEffect(
19569
+ let commitReceipt;
19570
+ const result = await invokeRegisteredEffect(
18937
19571
  this.getSandboxEffectMap(sandboxId),
18938
19572
  request,
18939
19573
  {
19574
+ commitAcknowledged: (receipt) => {
19575
+ commitReceipt = receipt;
19576
+ },
19577
+ commit: {
19578
+ persist: (commitRequest) => this.request(
19579
+ `/control/environments/${encodeURIComponent(
19580
+ commitRequest.environmentId
19581
+ )}/commits`,
19582
+ {
19583
+ method: "POST",
19584
+ body: JSON.stringify({
19585
+ kind: commitRequest.kind,
19586
+ invocationId: commitRequest.invocationId,
19587
+ idempotencyKey: commitRequest.idempotencyKey,
19588
+ effectKey: commitRequest.effectKey,
19589
+ projection: commitRequest.projection
19590
+ })
19591
+ }
19592
+ ),
19593
+ mappingFailed: (failure) => this.request(
19594
+ `/control/environments/${encodeURIComponent(
19595
+ failure.environmentId
19596
+ )}/commit-invocations/${encodeURIComponent(
19597
+ failure.invocationId
19598
+ )}`,
19599
+ {
19600
+ method: "PATCH",
19601
+ body: JSON.stringify({
19602
+ status: "mapping_failed",
19603
+ error: {
19604
+ code: "commit_projection_mapping_failed",
19605
+ message: failure.message,
19606
+ retryable: false
19607
+ }
19608
+ })
19609
+ }
19610
+ )
19611
+ },
18940
19612
  feedback: {
18941
19613
  invocationId: request.callId,
18942
19614
  publish: (method, publishParams) => wsClient.call(method, publishParams)
18943
19615
  }
18944
19616
  }
18945
19617
  );
19618
+ return {
19619
+ __granularEffectInvocationResult: true,
19620
+ result,
19621
+ ...commitReceipt ? { commit: commitReceipt } : {}
19622
+ };
18946
19623
  });
18947
19624
  wsClient.on("open", () => {
18948
19625
  void this.synchronizeEffectHost(host).catch((error) => {
@@ -19514,6 +20191,275 @@ var Granular = class _Granular {
19514
20191
  }
19515
20192
  };
19516
20193
 
20194
+ // src/record-snapshot-projection.ts
20195
+ function stableValue(value) {
20196
+ if (Array.isArray(value)) return value.map(stableValue);
20197
+ if (value && typeof value === "object") {
20198
+ return Object.fromEntries(
20199
+ Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, stableValue(item)])
20200
+ );
20201
+ }
20202
+ return value;
20203
+ }
20204
+ function stableJson(value) {
20205
+ return JSON.stringify(stableValue(value));
20206
+ }
20207
+ function recordKey(record) {
20208
+ return `${record.className}\0${record.id}`;
20209
+ }
20210
+ function recordReference(className, id) {
20211
+ return { className, id };
20212
+ }
20213
+ function changedRecordSnapshots(beforeRecords, afterRecords) {
20214
+ const before = new Map(
20215
+ beforeRecords.map((record) => [recordKey(record), record])
20216
+ );
20217
+ const after = new Map(
20218
+ afterRecords.map((record) => [recordKey(record), record])
20219
+ );
20220
+ const changedKeys = /* @__PURE__ */ new Set();
20221
+ for (const key of /* @__PURE__ */ new Set([...before.keys(), ...after.keys()])) {
20222
+ if (stableJson(before.get(key)) !== stableJson(after.get(key))) {
20223
+ changedKeys.add(key);
20224
+ }
20225
+ }
20226
+ return {
20227
+ beforeRecords: beforeRecords.filter(
20228
+ (record) => changedKeys.has(recordKey(record))
20229
+ ),
20230
+ afterRecords: afterRecords.filter(
20231
+ (record) => changedKeys.has(recordKey(record))
20232
+ )
20233
+ };
20234
+ }
20235
+ function relationshipTargets(manifest) {
20236
+ const targets = /* @__PURE__ */ new Map();
20237
+ for (const volume of manifest.volumes) {
20238
+ for (const operation of volume.operations) {
20239
+ const relationship = operation.defineRelationship;
20240
+ if (!relationship) continue;
20241
+ targets.set(`${relationship.left}\0${relationship.leftSubmodel}`, {
20242
+ className: relationship.right
20243
+ });
20244
+ targets.set(`${relationship.right}\0${relationship.rightSubmodel}`, {
20245
+ className: relationship.left
20246
+ });
20247
+ }
20248
+ }
20249
+ return targets;
20250
+ }
20251
+ function stateName(value) {
20252
+ return typeof value === "string" ? value : value.state;
20253
+ }
20254
+ function asIds(value) {
20255
+ return value === void 0 ? [] : Array.isArray(value) ? value : [value];
20256
+ }
20257
+ function changedObjectProjection(before, after) {
20258
+ const beforeFields = before?.fields || {};
20259
+ const afterFields = after.fields || {};
20260
+ const fields = { ...afterFields };
20261
+ for (const fieldName of Object.keys(beforeFields)) {
20262
+ if (!(fieldName in afterFields)) fields[fieldName] = null;
20263
+ }
20264
+ if (before && before.label === after.label && stableJson(beforeFields) === stableJson(afterFields)) {
20265
+ return null;
20266
+ }
20267
+ return {
20268
+ kind: "object",
20269
+ operation: before ? "updated" : "created",
20270
+ record: {
20271
+ className: after.className,
20272
+ id: after.id,
20273
+ ...after.label !== void 0 ? { label: after.label } : {},
20274
+ fields
20275
+ }
20276
+ };
20277
+ }
20278
+ function projectionChanges(input) {
20279
+ const before = new Map(
20280
+ input.beforeRecords.map((record) => [recordKey(record), record])
20281
+ );
20282
+ const after = new Map(
20283
+ input.afterRecords.map((record) => [recordKey(record), record])
20284
+ );
20285
+ const targets = relationshipTargets(input.manifest);
20286
+ const changes = [];
20287
+ for (const record of input.afterRecords) {
20288
+ const previous = before.get(recordKey(record));
20289
+ const objectChange = changedObjectProjection(previous, record);
20290
+ if (objectChange) changes.push(objectChange);
20291
+ const relationshipNames = /* @__PURE__ */ new Set([
20292
+ ...Object.keys(previous?.relationships || {}),
20293
+ ...Object.keys(record.relationships || {})
20294
+ ]);
20295
+ for (const relationshipName of [...relationshipNames].sort()) {
20296
+ const target = targets.get(
20297
+ `${record.className}\0${relationshipName}`
20298
+ );
20299
+ if (!target) {
20300
+ throw new Error(
20301
+ `No ontology relationship target is declared for ${record.className}.${relationshipName}`
20302
+ );
20303
+ }
20304
+ const previousIds = new Set(
20305
+ asIds(previous?.relationships?.[relationshipName])
20306
+ );
20307
+ const currentIds = new Set(
20308
+ asIds(record.relationships?.[relationshipName])
20309
+ );
20310
+ for (const id of [...previousIds].sort()) {
20311
+ if (currentIds.has(id)) continue;
20312
+ changes.push({
20313
+ kind: "relationship",
20314
+ operation: "disconnected",
20315
+ relationship: relationshipName,
20316
+ from: recordReference(record.className, record.id),
20317
+ to: recordReference(target.className, id)
20318
+ });
20319
+ }
20320
+ for (const id of [...currentIds].sort()) {
20321
+ if (previousIds.has(id)) continue;
20322
+ changes.push({
20323
+ kind: "relationship",
20324
+ operation: "connected",
20325
+ relationship: relationshipName,
20326
+ from: recordReference(record.className, record.id),
20327
+ to: recordReference(target.className, id)
20328
+ });
20329
+ }
20330
+ }
20331
+ const machines = /* @__PURE__ */ new Set([
20332
+ ...Object.keys(previous?.states || {}),
20333
+ ...Object.keys(record.states || {})
20334
+ ]);
20335
+ for (const machine of [...machines].sort()) {
20336
+ const next = record.states?.[machine];
20337
+ if (next === void 0) continue;
20338
+ const prior = previous?.states?.[machine];
20339
+ if (prior !== void 0 && stateName(prior) === stateName(next)) continue;
20340
+ if (input.transition && input.transition.className === record.className && input.transition.objectId === record.id && input.transition.machine === machine) {
20341
+ continue;
20342
+ }
20343
+ changes.push({
20344
+ kind: "state_observation",
20345
+ target: recordReference(record.className, record.id),
20346
+ machine,
20347
+ state: stateName(next)
20348
+ });
20349
+ }
20350
+ }
20351
+ for (const record of input.beforeRecords) {
20352
+ if (after.has(recordKey(record))) continue;
20353
+ changes.push({
20354
+ kind: "object",
20355
+ operation: "deleted",
20356
+ target: recordReference(record.className, record.id)
20357
+ });
20358
+ }
20359
+ return changes;
20360
+ }
20361
+ function valueAtPath(value, path) {
20362
+ return path.split(".").filter(Boolean).reduce((current, segment) => {
20363
+ if (!current || typeof current !== "object" || Array.isArray(current)) {
20364
+ return void 0;
20365
+ }
20366
+ return current[segment];
20367
+ }, value);
20368
+ }
20369
+ function primaryTarget(input) {
20370
+ if (input.transition) {
20371
+ return recordReference(
20372
+ input.transition.className,
20373
+ input.transition.objectId
20374
+ );
20375
+ }
20376
+ const creates = input.effect.metamodels?.creates;
20377
+ if (creates) {
20378
+ const declaration = typeof creates === "string" ? { className: creates } : creates;
20379
+ const id = declaration.idPath ? valueAtPath(input.result, declaration.idPath) : void 0;
20380
+ if (typeof id === "string" && id) {
20381
+ return recordReference(declaration.className, id);
20382
+ }
20383
+ }
20384
+ if (input.effect.className && !input.effect.static) {
20385
+ const objectId = input.effectInput && typeof input.effectInput === "object" && !Array.isArray(input.effectInput) ? input.effectInput._objectId : void 0;
20386
+ if (typeof objectId === "string" && objectId) {
20387
+ return recordReference(input.effect.className, objectId);
20388
+ }
20389
+ }
20390
+ const created = input.changes.filter(
20391
+ (change) => change.kind === "object" && change.operation === "created"
20392
+ );
20393
+ const onlyCreated = created.length === 1 ? created[0] : void 0;
20394
+ return onlyCreated ? recordReference(onlyCreated.record.className, onlyCreated.record.id) : void 0;
20395
+ }
20396
+ function transitionOutcome(transition, afterRecords) {
20397
+ if (!transition) {
20398
+ throw new Error("A transition commit has no authored transition context.");
20399
+ }
20400
+ const target = afterRecords.find(
20401
+ (record) => record.className === transition.className && record.id === transition.objectId
20402
+ );
20403
+ const observed = target?.states?.[transition.machine];
20404
+ if (!observed) {
20405
+ throw new Error(
20406
+ `The product result did not expose ${transition.className}:${transition.objectId}.${transition.machine}`
20407
+ );
20408
+ }
20409
+ const finalState = stateName(observed);
20410
+ const matches = Object.entries(transition.outcomes).filter(
20411
+ ([, outcome]) => outcome.to === finalState || outcome.to === "$current" && finalState === transition.from
20412
+ );
20413
+ if (matches.length !== 1) {
20414
+ throw new Error(
20415
+ `Product state ${finalState} maps to ${matches.length} authored outcomes for ${transition.machine}.${transition.transition}`
20416
+ );
20417
+ }
20418
+ const [key, declaration] = matches[0];
20419
+ return {
20420
+ key,
20421
+ ...declaration.disposition === "error" ? {
20422
+ error: {
20423
+ code: `product_outcome_${key}`,
20424
+ message: declaration.label || `The product completed in ${finalState} instead of continuing.`,
20425
+ retryable: false
20426
+ }
20427
+ } : {}
20428
+ };
20429
+ }
20430
+ function projectRecordSnapshotMutation(input) {
20431
+ const changes = projectionChanges({
20432
+ manifest: input.manifest,
20433
+ beforeRecords: input.source.beforeRecords,
20434
+ afterRecords: input.source.afterRecords,
20435
+ transition: input.transition
20436
+ });
20437
+ const target = primaryTarget({
20438
+ effect: input.effect,
20439
+ effectInput: input.effectInput,
20440
+ result: input.result,
20441
+ changes,
20442
+ transition: input.transition
20443
+ });
20444
+ const base = {
20445
+ source: {
20446
+ reference: input.source.reference,
20447
+ ...input.source.version ? { version: input.source.version } : {}
20448
+ },
20449
+ ...target ? { primaryTarget: target } : {},
20450
+ changes,
20451
+ safeSummary: {
20452
+ effect: input.effect.name,
20453
+ affectedChanges: changes.length
20454
+ }
20455
+ };
20456
+ if (input.commitKind === "effect") return base;
20457
+ return {
20458
+ ...base,
20459
+ outcome: transitionOutcome(input.transition, input.source.afterRecords)
20460
+ };
20461
+ }
20462
+
19517
20463
  // src/agent-harness-templates/action-presentation/0.1.0/manifest.json
19518
20464
  var manifest_default = {
19519
20465
  id: "action-presentation",
@@ -22551,10 +23497,15 @@ exports.buildOpenAISpendEventId = buildOpenAISpendEventId;
22551
23497
  exports.buildSessionTranscript = buildSessionTranscript;
22552
23498
  exports.buildSessionTranscriptFromFeedItems = buildSessionTranscriptFromFeedItems;
22553
23499
  exports.calculateOpenAITokenSpend = calculateOpenAITokenSpend;
23500
+ exports.canonicalizeCommitValue = canonicalizeCommitValue;
23501
+ exports.changedRecordSnapshots = changedRecordSnapshots;
22554
23502
  exports.consumeGranularReasoningOnlyChunk = consumeGranularReasoningOnlyChunk;
22555
23503
  exports.consumeGranularReasoningTraceChunk = consumeGranularReasoningTraceChunk;
22556
23504
  exports.createFeedPublisher = createFeedPublisher;
22557
23505
  exports.createHarnessVerifierSnapshot = createHarnessVerifierSnapshot;
23506
+ exports.defineEffect = defineEffect;
23507
+ exports.defineProjection = defineProjection;
23508
+ exports.defineStateMachine = defineStateMachine;
22558
23509
  exports.emitFeedDiagnostic = emitFeedDiagnostic;
22559
23510
  exports.emitFeedDiagnosticToDefaultSink = emitFeedDiagnosticToDefaultSink;
22560
23511
  exports.emptyFeedSnapshot = emptyFeedSnapshot;
@@ -22587,6 +23538,7 @@ exports.projectConversationReferentFocus = projectConversationReferentFocus;
22587
23538
  exports.projectConversationReferentSummary = projectConversationReferentSummary;
22588
23539
  exports.projectHeapSummary = projectHeapSummary;
22589
23540
  exports.projectLoopSummary = projectLoopSummary;
23541
+ exports.projectRecordSnapshotMutation = projectRecordSnapshotMutation;
22590
23542
  exports.projectSessionFileSummary = projectSessionFileSummary;
22591
23543
  exports.projectWorkflowFocus = projectWorkflowFocus;
22592
23544
  exports.projectWorkflowSummary = projectWorkflowSummary;
@@ -22604,6 +23556,7 @@ exports.scorePromptChoiceMatch = scorePromptChoiceMatch;
22604
23556
  exports.stripGranularReasoningTrace = stripGranularReasoningTrace;
22605
23557
  exports.toGranularHttpBase = toGranularHttpBase;
22606
23558
  exports.validateHarnessTemplateManifest = validateHarnessTemplateManifest;
23559
+ exports.validateProjectionResult = validateProjectionResult;
22607
23560
  exports.validationRuleFailureMessage = validationRuleFailureMessage;
22608
23561
  //# sourceMappingURL=index.js.map
22609
23562
  //# sourceMappingURL=index.js.map