@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/cli/index.js CHANGED
@@ -14167,6 +14167,12 @@ var StateTransitionExpectedOutcomeSchema = external_exports.union([
14167
14167
  summary: external_exports.string().optional()
14168
14168
  }).strict()
14169
14169
  ]);
14170
+ var StateTransitionOutcomeSchema = external_exports.object({
14171
+ label: external_exports.string().min(1).optional(),
14172
+ to: external_exports.string().min(1),
14173
+ primary: external_exports.boolean().optional(),
14174
+ disposition: external_exports.enum(["continue", "error"])
14175
+ }).strict();
14170
14176
  var StateMachineTransitionSchema = external_exports.object({
14171
14177
  name: external_exports.string().min(1),
14172
14178
  from: external_exports.string().min(1),
@@ -14178,7 +14184,8 @@ var StateMachineTransitionSchema = external_exports.object({
14178
14184
  requirements: StateTransitionRequirementsSchema.optional(),
14179
14185
  permission: StateTransitionPermissionSchema.optional(),
14180
14186
  risk: external_exports.enum(["low", "medium", "high"]).optional(),
14181
- expectedOutcome: StateTransitionExpectedOutcomeSchema.optional()
14187
+ expectedOutcome: StateTransitionExpectedOutcomeSchema.optional(),
14188
+ outcomes: external_exports.record(external_exports.string().min(1), StateTransitionOutcomeSchema).optional()
14182
14189
  }).strict();
14183
14190
  external_exports.object({
14184
14191
  name: external_exports.string().min(1),
@@ -15529,7 +15536,8 @@ function normalizeStateMachines(values) {
15529
15536
  requirements: parseJsonRecord(transition?.requirements) || parseJsonRecord(transition?.requirements_json),
15530
15537
  permission: parseJsonValue(transition?.permission) ?? parseJsonValue(transition?.permission_json),
15531
15538
  risk: transition?.risk === "low" || transition?.risk === "medium" || transition?.risk === "high" ? transition.risk : null,
15532
- expectedOutcome: parseJsonValue(transition?.expectedOutcome) ?? parseJsonValue(transition?.expected_outcome_json)
15539
+ expectedOutcome: parseJsonValue(transition?.expectedOutcome) ?? parseJsonValue(transition?.expected_outcome_json),
15540
+ outcomes: parseJsonRecord(transition?.outcomes) || parseJsonRecord(transition?.outcomes_json)
15533
15541
  })).filter(
15534
15542
  (transition) => transition.name.length > 0 && transition.from.length > 0 && transition.to.length > 0
15535
15543
  );
@@ -15622,6 +15630,11 @@ function transitionMetadataGraphqlArgs(transition) {
15622
15630
  `expected_outcome_json: ${JSON.stringify(JSON.stringify(transition.expectedOutcome))}`
15623
15631
  );
15624
15632
  }
15633
+ if (transition.outcomes) {
15634
+ args.push(
15635
+ `outcomes_json: ${JSON.stringify(JSON.stringify(transition.outcomes))}`
15636
+ );
15637
+ }
15625
15638
  return args.length > 0 ? `, ${args.join(", ")}` : "";
15626
15639
  }
15627
15640
  function buildStateMachineModelMutations(modelPath, machines) {
@@ -15900,7 +15913,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
15900
15913
  name: String!
15901
15914
  state_machine: StateMachine!
15902
15915
  add_state(name: String!, is_final: Boolean, label: String, description: String): StateMachineMutation!
15903
- 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!
15916
+ 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!
15904
15917
  activate_transition(name: String!): StateMachineMutation!
15905
15918
  }
15906
15919
 
@@ -15917,6 +15930,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
15917
15930
  type StateMachineSnapshotMutation {
15918
15931
  snapshot: StateMachineSnapshot!
15919
15932
  activate_transition(name: String!): StateMachineSnapshotMutation!
15933
+ commit_transition(name: String!, outcome: String!, to: String!, commit_id: String!, source_version: String): StateMachineSnapshotMutation!
15920
15934
  observe_state(state: String!, force: Boolean, source: String): StateMachineSnapshotMutation!
15921
15935
  }
15922
15936
 
@@ -15963,6 +15977,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
15963
15977
  permission_json: String
15964
15978
  risk: String
15965
15979
  expected_outcome_json: String
15980
+ outcomes_json: String
15966
15981
  }
15967
15982
 
15968
15983
  type StateMachinePath {
@@ -15973,6 +15988,10 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
15973
15988
  type StateMachineTransitionEvent {
15974
15989
  sequence: Int!
15975
15990
  occurred_at: Float!
15991
+ commit_id: String
15992
+ outcome: String
15993
+ source_version: String
15994
+ projected_from_mismatch: String
15976
15995
  transition: StateMachineTransition!
15977
15996
  from: StateMachineState!
15978
15997
  to: StateMachineState!
@@ -16038,7 +16057,8 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
16038
16057
  requirements_json,
16039
16058
  permission_json,
16040
16059
  risk,
16041
- expected_outcome_json
16060
+ expected_outcome_json,
16061
+ outcomes_json
16042
16062
  }) => {
16043
16063
  await run(
16044
16064
  value.target.add_state_machine_transition(
@@ -16054,7 +16074,8 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
16054
16074
  requirementsJson: requirements_json,
16055
16075
  permissionJson: permission_json,
16056
16076
  risk,
16057
- expectedOutcomeJson: expected_outcome_json
16077
+ expectedOutcomeJson: expected_outcome_json,
16078
+ outcomesJson: outcomes_json
16058
16079
  }
16059
16080
  )
16060
16081
  );
@@ -16075,6 +16096,19 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
16075
16096
  );
16076
16097
  return value;
16077
16098
  },
16099
+ commit_transition: async (value, { name, outcome, to, commit_id, source_version }) => {
16100
+ await run(
16101
+ value.target.commit_state_machine_transition(
16102
+ value.name,
16103
+ name,
16104
+ outcome,
16105
+ to,
16106
+ commit_id,
16107
+ source_version
16108
+ )
16109
+ );
16110
+ return value;
16111
+ },
16078
16112
  observe_state: async (value, { state, force, source }) => {
16079
16113
  await run(
16080
16114
  value.target.observe_state_machine_state(
@@ -16104,7 +16138,8 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
16104
16138
  requirements_json: (value) => value.requirements_json || null,
16105
16139
  permission_json: (value) => value.permission_json || null,
16106
16140
  risk: (value) => value.risk || null,
16107
- expected_outcome_json: (value) => value.expected_outcome_json || null
16141
+ expected_outcome_json: (value) => value.expected_outcome_json || null,
16142
+ outcomes_json: (value) => value.outcomes_json || null
16108
16143
  },
16109
16144
  StateMachinePath: {
16110
16145
  states: (value) => value.states,
@@ -16113,6 +16148,10 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
16113
16148
  StateMachineTransitionEvent: {
16114
16149
  sequence: (value) => value.sequence,
16115
16150
  occurred_at: (value) => value.occurred_at,
16151
+ commit_id: (value) => value.commit_id || null,
16152
+ outcome: (value) => value.outcome || null,
16153
+ source_version: (value) => value.source_version || null,
16154
+ projected_from_mismatch: (value) => value.projected_from_mismatch || null,
16116
16155
  transition: (value) => value.transition,
16117
16156
  from: (value) => value.from,
16118
16157
  to: (value) => value.to
@@ -16186,6 +16225,7 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
16186
16225
  permission_json
16187
16226
  risk
16188
16227
  expected_outcome_json
16228
+ outcomes_json
16189
16229
  }
16190
16230
  }`
16191
16231
  ]
@@ -22095,6 +22135,11 @@ var Session = class {
22095
22135
  }
22096
22136
  }
22097
22137
  buildDirectedInvocationEffectContext(params, feedbackContext) {
22138
+ const commitUnavailable = async () => {
22139
+ throw new Error(
22140
+ "This directed browser tool invocation has no product commit transport"
22141
+ );
22142
+ };
22098
22143
  return {
22099
22144
  effectClientId: this.clientId,
22100
22145
  sandboxId: params.sandboxId || "",
@@ -22107,6 +22152,10 @@ var Session = class {
22107
22152
  userId: "",
22108
22153
  subjectId: ""
22109
22154
  },
22155
+ commit: {
22156
+ effect: commitUnavailable,
22157
+ transition: commitUnavailable
22158
+ },
22110
22159
  ...feedbackContext ? {
22111
22160
  feedback: feedbackContext.feedback,
22112
22161
  transientFeedback: feedbackContext.transientFeedback
@@ -23636,6 +23685,238 @@ function buildSessionTranscript(input) {
23636
23685
  });
23637
23686
  }
23638
23687
 
23688
+ // src/commit.ts
23689
+ var MAX_PROJECTION_CHANGES = 1e3;
23690
+ var MAX_SAFE_SUMMARY_KEYS = 32;
23691
+ var MAX_SAFE_TEXT_LENGTH = 2e3;
23692
+ var definedTransitionMetadata = /* @__PURE__ */ new WeakMap();
23693
+ function getDefinedTransitionMetadata(transition) {
23694
+ return definedTransitionMetadata.get(transition);
23695
+ }
23696
+ function requireNonEmptyString(value, path7) {
23697
+ if (typeof value !== "string" || value.trim().length === 0) {
23698
+ throw new Error(`${path7} must be a non-empty string`);
23699
+ }
23700
+ return value.trim();
23701
+ }
23702
+ function validateObjectReference(value, path7) {
23703
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
23704
+ throw new Error(`${path7} must be an object reference`);
23705
+ }
23706
+ const reference = value;
23707
+ requireNonEmptyString(reference.className, `${path7}.className`);
23708
+ requireNonEmptyString(reference.id, `${path7}.id`);
23709
+ if (reference.path !== void 0) {
23710
+ requireNonEmptyString(reference.path, `${path7}.path`);
23711
+ }
23712
+ }
23713
+ function validateScalarRecord(value, path7) {
23714
+ if (!value || typeof value !== "object" || Array.isArray(value) || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) {
23715
+ throw new Error(`${path7} must be an object of scalar values`);
23716
+ }
23717
+ for (const [key, item] of Object.entries(value)) {
23718
+ if (item !== null && typeof item !== "string" && typeof item !== "number" && typeof item !== "boolean") {
23719
+ throw new Error(
23720
+ `${path7}.${key} must be a string, finite number, boolean, or null`
23721
+ );
23722
+ }
23723
+ if (typeof item === "number" && !Number.isFinite(item)) {
23724
+ throw new Error(`${path7}.${key} must be finite`);
23725
+ }
23726
+ }
23727
+ }
23728
+ function validateProjectedRecord(value, path7) {
23729
+ validateObjectReference(value, path7);
23730
+ const record = value;
23731
+ if (record.label !== void 0) {
23732
+ if (typeof record.label !== "string") {
23733
+ throw new Error(`${path7}.label must be a string`);
23734
+ }
23735
+ if (record.label.length > MAX_SAFE_TEXT_LENGTH) {
23736
+ throw new Error(
23737
+ `${path7}.label exceeds ${MAX_SAFE_TEXT_LENGTH} characters`
23738
+ );
23739
+ }
23740
+ }
23741
+ validateScalarRecord(record.fields, `${path7}.fields`);
23742
+ }
23743
+ function validateBoundedJson(value, path7, depth = 0) {
23744
+ if (depth > 12) throw new Error(`${path7} is nested too deeply`);
23745
+ if (value === null || typeof value === "boolean") return;
23746
+ if (typeof value === "number") {
23747
+ if (!Number.isFinite(value)) throw new Error(`${path7} must be finite`);
23748
+ return;
23749
+ }
23750
+ if (typeof value === "string") {
23751
+ if (value.length > MAX_SAFE_TEXT_LENGTH) {
23752
+ throw new Error(`${path7} exceeds ${MAX_SAFE_TEXT_LENGTH} characters`);
23753
+ }
23754
+ return;
23755
+ }
23756
+ if (Array.isArray(value)) {
23757
+ if (value.length > MAX_PROJECTION_CHANGES) {
23758
+ throw new Error(`${path7} contains too many values`);
23759
+ }
23760
+ value.forEach(
23761
+ (item, index) => validateBoundedJson(item, `${path7}[${index}]`, depth + 1)
23762
+ );
23763
+ return;
23764
+ }
23765
+ if (!value || typeof value !== "object") {
23766
+ throw new Error(`${path7} contains an unsupported value`);
23767
+ }
23768
+ const entries = Object.entries(value);
23769
+ if (entries.length > 256) throw new Error(`${path7} contains too many keys`);
23770
+ for (const [key, item] of entries) {
23771
+ if (/token|secret|password|authorization|cookie/i.test(key)) {
23772
+ throw new Error(`${path7}.${key} is not allowed in a commit projection`);
23773
+ }
23774
+ validateBoundedJson(item, `${path7}.${key}`, depth + 1);
23775
+ }
23776
+ }
23777
+ function validateProjectionResult(declaration, projection) {
23778
+ if (!projection || typeof projection !== "object") {
23779
+ throw new Error("Projection mapper must return an object");
23780
+ }
23781
+ requireNonEmptyString(
23782
+ projection.source?.reference,
23783
+ "projection.source.reference"
23784
+ );
23785
+ if (projection.source.version !== void 0) {
23786
+ requireNonEmptyString(
23787
+ projection.source.version,
23788
+ "projection.source.version"
23789
+ );
23790
+ }
23791
+ if (!Array.isArray(projection.changes)) {
23792
+ throw new Error("projection.changes must be an array");
23793
+ }
23794
+ if (projection.changes.length > MAX_PROJECTION_CHANGES) {
23795
+ throw new Error(
23796
+ `projection.changes exceeds the ${MAX_PROJECTION_CHANGES} change limit`
23797
+ );
23798
+ }
23799
+ if (projection.primaryTarget) {
23800
+ validateObjectReference(
23801
+ projection.primaryTarget,
23802
+ "projection.primaryTarget"
23803
+ );
23804
+ }
23805
+ if (projection.safeSummary) {
23806
+ const entries = Object.entries(projection.safeSummary);
23807
+ if (entries.length > MAX_SAFE_SUMMARY_KEYS) {
23808
+ throw new Error(
23809
+ `projection.safeSummary exceeds the ${MAX_SAFE_SUMMARY_KEYS} key limit`
23810
+ );
23811
+ }
23812
+ validateBoundedJson(projection.safeSummary, "projection.safeSummary");
23813
+ validateScalarRecord(projection.safeSummary, "projection.safeSummary");
23814
+ }
23815
+ projection.changes.forEach((change, index) => {
23816
+ const changePath = `projection.changes[${index}]`;
23817
+ validateBoundedJson(change, changePath);
23818
+ if (!change || typeof change !== "object" || Array.isArray(change)) {
23819
+ throw new Error(`${changePath} must be an object`);
23820
+ }
23821
+ const rawChange = change;
23822
+ const kind = requireNonEmptyString(rawChange.kind, `${changePath}.kind`);
23823
+ if (kind === "object") {
23824
+ const operation = requireNonEmptyString(
23825
+ rawChange.operation,
23826
+ `${changePath}.operation`
23827
+ );
23828
+ if (operation !== "created" && operation !== "updated" && operation !== "deleted") {
23829
+ throw new Error(
23830
+ `${changePath}.operation must be created, updated, or deleted`
23831
+ );
23832
+ }
23833
+ if (operation === "deleted") {
23834
+ validateObjectReference(rawChange.target, `${changePath}.target`);
23835
+ } else {
23836
+ validateProjectedRecord(rawChange.record, `${changePath}.record`);
23837
+ }
23838
+ } else if (kind === "relationship") {
23839
+ const operation = requireNonEmptyString(
23840
+ rawChange.operation,
23841
+ `${changePath}.operation`
23842
+ );
23843
+ if (operation !== "connected" && operation !== "disconnected") {
23844
+ throw new Error(
23845
+ `${changePath}.operation must be connected or disconnected`
23846
+ );
23847
+ }
23848
+ requireNonEmptyString(
23849
+ rawChange.relationship,
23850
+ `${changePath}.relationship`
23851
+ );
23852
+ validateObjectReference(rawChange.from, `${changePath}.from`);
23853
+ validateObjectReference(rawChange.to, `${changePath}.to`);
23854
+ } else if (kind === "state_observation") {
23855
+ if (rawChange.operation !== void 0) {
23856
+ throw new Error(
23857
+ `${changePath}.operation is not valid for an observation`
23858
+ );
23859
+ }
23860
+ validateObjectReference(rawChange.target, `${changePath}.target`);
23861
+ requireNonEmptyString(rawChange.machine, `${changePath}.machine`);
23862
+ requireNonEmptyString(rawChange.state, `${changePath}.state`);
23863
+ } else {
23864
+ throw new Error(`${changePath}.kind is unsupported: ${kind}`);
23865
+ }
23866
+ });
23867
+ const objectOperations = /* @__PURE__ */ new Map();
23868
+ for (const change of projection.changes) {
23869
+ if (change.kind !== "object") continue;
23870
+ const reference = change.operation === "deleted" ? change.target : change.record;
23871
+ const key = `${reference.className}\0${reference.id}`;
23872
+ const operations = objectOperations.get(key) || {
23873
+ deleted: false,
23874
+ upserted: false
23875
+ };
23876
+ if (change.operation === "deleted") operations.deleted = true;
23877
+ else operations.upserted = true;
23878
+ if (operations.deleted && operations.upserted) {
23879
+ throw new Error(
23880
+ `projection.changes cannot both delete and upsert ${reference.className}/${reference.id}; return only its canonical final state`
23881
+ );
23882
+ }
23883
+ objectOperations.set(key, operations);
23884
+ }
23885
+ const outcome = projection.outcome;
23886
+ if (declaration.kind === "transition") {
23887
+ if (!outcome) {
23888
+ throw new Error("A transition projection must return an outcome");
23889
+ }
23890
+ requireNonEmptyString(outcome.key, "projection.outcome.key");
23891
+ if (outcome.error) {
23892
+ validateBoundedJson(outcome.error, "projection.outcome.error");
23893
+ }
23894
+ } else if (projection.outcome !== void 0) {
23895
+ throw new Error("An effect projection cannot declare a transition outcome");
23896
+ }
23897
+ }
23898
+ function canonicalizeCommitValue(value) {
23899
+ const normalize = (current) => {
23900
+ if (current === null || typeof current === "boolean" || typeof current === "string") {
23901
+ return typeof current === "string" ? current.normalize("NFC") : current;
23902
+ }
23903
+ if (typeof current === "number") {
23904
+ if (!Number.isFinite(current)) {
23905
+ throw new Error("Cannot canonicalize a non-finite number");
23906
+ }
23907
+ return Object.is(current, -0) ? 0 : current;
23908
+ }
23909
+ if (Array.isArray(current)) return current.map(normalize);
23910
+ if (current && typeof current === "object") {
23911
+ return Object.fromEntries(
23912
+ Object.entries(current).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key.normalize("NFC"), normalize(item)])
23913
+ );
23914
+ }
23915
+ throw new Error(`Cannot canonicalize ${typeof current}`);
23916
+ };
23917
+ return JSON.stringify(normalize(value));
23918
+ }
23919
+
23639
23920
  // src/effect-runtime.ts
23640
23921
  function computeEffectKey(effect) {
23641
23922
  const attachedClass = effect.className?.trim();
@@ -23700,6 +23981,45 @@ function resolveInvocationMode(context) {
23700
23981
  }
23701
23982
  return "execute";
23702
23983
  }
23984
+ async function sha256Hex(value) {
23985
+ const digest = await globalThis.crypto.subtle.digest(
23986
+ "SHA-256",
23987
+ new TextEncoder().encode(value)
23988
+ );
23989
+ return Array.from(
23990
+ new Uint8Array(digest),
23991
+ (byte) => byte.toString(16).padStart(2, "0")
23992
+ ).join("");
23993
+ }
23994
+ async function resolveInvocationIdempotencyKey(request) {
23995
+ const supplied = request.context?.idempotencyKey?.trim();
23996
+ if (supplied) return supplied;
23997
+ const invocationId = request.context?.invocationId?.trim();
23998
+ if (!invocationId) {
23999
+ throw new Error(
24000
+ `Committed effect ${request.effectKey} requires an invocation id`
24001
+ );
24002
+ }
24003
+ const digest = await sha256Hex(
24004
+ canonicalizeCommitValue({
24005
+ sandboxId: request.context?.sandboxId || "",
24006
+ environmentId: request.context?.environmentId || "",
24007
+ effectKey: request.effectKey,
24008
+ invocationId,
24009
+ input: request.input
24010
+ })
24011
+ );
24012
+ return `gci_${digest}`;
24013
+ }
24014
+ function createUnavailableCommitContext(message) {
24015
+ const unavailable = async () => {
24016
+ throw new Error(message);
24017
+ };
24018
+ return {
24019
+ effect: unavailable,
24020
+ transition: unavailable
24021
+ };
24022
+ }
23703
24023
  function resolveReverseEffect(effectMap, currentEffect, request, behaviors) {
23704
24024
  const explicitReverseHandler = request.context?.invocation?.reverseHandler?.trim();
23705
24025
  const configuredReverseHandler = behaviors.reverse?.handler?.trim();
@@ -23771,22 +24091,32 @@ function resolveHandlerForMode(effectMap, effect, request) {
23771
24091
  throw new Error(`Dry run is not supported for ${request.effectKey}`);
23772
24092
  }
23773
24093
  if (mode === "reverse") {
24094
+ const sourceEffectKey = request.context?.invocation?.sourceEffectKey?.trim();
24095
+ if (sourceEffectKey && sourceEffectKey !== request.effectKey) {
24096
+ return { effect, mode, handler: effect.handler };
24097
+ }
24098
+ const namedReverseHandler = request.context?.invocation?.reverseHandler?.trim() || behaviors.reverse?.handler?.trim();
24099
+ if (namedReverseHandler) {
24100
+ const reverseEffect = resolveReverseEffect(
24101
+ effectMap,
24102
+ effect,
24103
+ request,
24104
+ behaviors
24105
+ );
24106
+ if (reverseEffect) {
24107
+ return {
24108
+ effect: reverseEffect,
24109
+ mode,
24110
+ handler: reverseEffect.handler
24111
+ };
24112
+ }
24113
+ throw new Error(
24114
+ `Reverse effect ${namedReverseHandler} is not registered for ${request.effectKey}`
24115
+ );
24116
+ }
23774
24117
  if (effect.reverseHandler) {
23775
24118
  return { effect, mode, handler: effect.reverseHandler };
23776
24119
  }
23777
- const reverseEffect = resolveReverseEffect(
23778
- effectMap,
23779
- effect,
23780
- request,
23781
- behaviors
23782
- );
23783
- if (reverseEffect) {
23784
- return {
23785
- effect: reverseEffect,
23786
- mode,
23787
- handler: reverseEffect.reverseHandler || reverseEffect.handler
23788
- };
23789
- }
23790
24120
  throw new Error(
23791
24121
  `Reverse execution is not supported for ${request.effectKey}`
23792
24122
  );
@@ -23887,18 +24217,142 @@ async function invokeRegisteredEffect(effectMap, request, options = {}) {
23887
24217
  throw new Error(`Effect handler not found: ${request.effectKey}`);
23888
24218
  }
23889
24219
  const resolved = resolveHandlerForMode(effectMap, effect, request);
24220
+ const isPreResolvedNamedReverse = resolved.mode === "reverse" && Boolean(request.context?.invocation?.sourceEffectKey?.trim()) && request.context?.invocation?.sourceEffectKey?.trim() !== request.effectKey;
24221
+ const resolvedEffectKey = computeEffectKey(resolved.effect);
24222
+ if (resolved.mode === "reverse" && resolved.effect !== effect && (effect.commit || resolved.effect.commit)) {
24223
+ throw new Error(
24224
+ `Committed named reverse effect ${resolvedEffectKey} must be reserved and dispatched using its own effect key; customer code was not invoked`
24225
+ );
24226
+ }
24227
+ const declaration = resolved.effect.commit;
24228
+ const commitTransport = options.commit;
24229
+ const commitRequired = Boolean(declaration) && (resolved.mode === "execute" || resolved.mode === "reverse");
24230
+ if (commitRequired && !commitTransport) {
24231
+ throw new Error(
24232
+ `Committed effect ${request.effectKey} has no durable commit transport; customer code was not invoked`
24233
+ );
24234
+ }
24235
+ if (commitRequired && declaration?.kind === "transition" && !request.context?.invocation?.transition) {
24236
+ throw new Error(
24237
+ `Transition effect ${request.effectKey} has no resolved transition context; customer code was not invoked`
24238
+ );
24239
+ }
24240
+ const idempotencyKey = commitRequired ? await resolveInvocationIdempotencyKey(request) : request.context?.idempotencyKey;
24241
+ let commitStarted = false;
24242
+ let commitPromise = null;
24243
+ const beginCommit = (requestedKind, productResult) => {
24244
+ if (!declaration || !commitRequired || !commitTransport) {
24245
+ return Promise.reject(
24246
+ new Error(
24247
+ `Effect ${request.effectKey} does not declare an active ${requestedKind} commit`
24248
+ )
24249
+ );
24250
+ }
24251
+ if (declaration.kind !== requestedKind) {
24252
+ return Promise.reject(
24253
+ new Error(
24254
+ `Effect ${request.effectKey} declares ${declaration.kind} commit, not ${requestedKind}`
24255
+ )
24256
+ );
24257
+ }
24258
+ if (commitStarted) {
24259
+ return Promise.reject(
24260
+ new Error(
24261
+ `Effect invocation ${request.context?.invocationId || request.effectKey} already emitted its commit`
24262
+ )
24263
+ );
24264
+ }
24265
+ commitStarted = true;
24266
+ commitPromise = (async () => {
24267
+ let projection;
24268
+ try {
24269
+ projection = declaration.project(productResult);
24270
+ validateProjectionResult(declaration, projection);
24271
+ } catch (error2) {
24272
+ const message = error2 instanceof Error ? error2.message : String(error2);
24273
+ if (commitTransport.mappingFailed) {
24274
+ await commitTransport.mappingFailed({
24275
+ effectKey: request.effectKey,
24276
+ effectName: request.effectName,
24277
+ invocationId: request.context?.invocationId || "",
24278
+ idempotencyKey: idempotencyKey || "",
24279
+ environmentId: request.context?.environmentId || "",
24280
+ message
24281
+ });
24282
+ }
24283
+ throw new Error(
24284
+ `Product mutation may have succeeded, but its commit projection is invalid: ${message}`
24285
+ );
24286
+ }
24287
+ if (requestedKind === "transition") {
24288
+ const transition = request.context?.invocation?.transition;
24289
+ const outcome = projection.outcome;
24290
+ if (!transition || !outcome?.key) {
24291
+ throw new Error(
24292
+ `Transition effect ${request.effectKey} did not resolve an authored outcome`
24293
+ );
24294
+ }
24295
+ if (!Object.prototype.hasOwnProperty.call(
24296
+ transition.outcomes,
24297
+ outcome.key
24298
+ )) {
24299
+ throw new Error(
24300
+ `Transition ${transition.machine}.${transition.transition} does not declare outcome ${outcome.key}`
24301
+ );
24302
+ }
24303
+ }
24304
+ const invocationId = request.context?.invocationId || "";
24305
+ const environmentId = request.context?.environmentId || "";
24306
+ const sandboxId = request.context?.sandboxId || "";
24307
+ if (!invocationId || !environmentId || !sandboxId || !idempotencyKey) {
24308
+ throw new Error(
24309
+ `Committed effect ${request.effectKey} is missing its trusted invocation scope`
24310
+ );
24311
+ }
24312
+ const commitRequest = {
24313
+ kind: requestedKind,
24314
+ effectKey: request.effectKey,
24315
+ effectName: request.effectName,
24316
+ operationLabel: resolved.effect.label || resolved.effect.name,
24317
+ invocationId,
24318
+ idempotencyKey,
24319
+ sandboxId,
24320
+ environmentId,
24321
+ ...request.context?.sessionId ? { sessionId: request.context.sessionId } : {},
24322
+ ...request.context?.jobId ? { jobId: request.context.jobId } : {},
24323
+ ...request.context?.buildId ? { buildId: request.context.buildId } : {},
24324
+ ...request.context?.invocation?.artifactId ? { artifactId: request.context.invocation.artifactId } : {},
24325
+ projection,
24326
+ ...requestedKind === "transition" && request.context?.invocation?.transition ? { transition: request.context.invocation.transition } : {}
24327
+ };
24328
+ const receipt = await commitTransport.persist(commitRequest);
24329
+ await options.commitAcknowledged?.(receipt);
24330
+ return receipt;
24331
+ })();
24332
+ return commitPromise;
24333
+ };
24334
+ const commitContext = commitRequired ? {
24335
+ effect: (productResult) => beginCommit("effect", productResult),
24336
+ transition: (productResult) => beginCommit("transition", productResult)
24337
+ } : createUnavailableCommitContext(
24338
+ `Effect ${request.effectKey} is not executing a declared product commit`
24339
+ );
23890
24340
  const context = {
23891
24341
  ...request.context || {},
24342
+ ...idempotencyKey ? { idempotencyKey } : {},
24343
+ commit: commitContext,
23892
24344
  behaviors: normalizeEffectBehaviors(
23893
24345
  request.context?.behaviors || effect.metamodels || void 0
23894
24346
  ),
23895
24347
  invocation: {
23896
24348
  mode: resolved.mode,
23897
- sourceEffectKey: request.effectKey,
23898
- sourceEffectName: request.effectName,
24349
+ ...commitRequired && declaration ? { commitKind: declaration.kind } : {},
24350
+ sourceEffectKey: isPreResolvedNamedReverse ? request.context?.invocation?.sourceEffectKey || request.effectKey : request.effectKey,
24351
+ sourceEffectName: isPreResolvedNamedReverse ? request.context?.invocation?.sourceEffectName || request.effectName : request.effectName,
23899
24352
  ...request.context?.invocation?.reverseHandler ? { reverseHandler: request.context.invocation.reverseHandler } : {},
23900
24353
  ...request.context?.invocation?.artifactId ? { artifactId: request.context.invocation.artifactId } : {},
23901
- ...request.context?.invocation?.artifactOptions ? { artifactOptions: request.context.invocation.artifactOptions } : {}
24354
+ ...request.context?.invocation?.artifactOptions ? { artifactOptions: request.context.invocation.artifactOptions } : {},
24355
+ ...request.context?.invocation?.transition ? { transition: request.context.invocation.transition } : {}
23902
24356
  }
23903
24357
  };
23904
24358
  const feedbackContext = options.feedback ? createInvocationFeedbackContext(options.feedback) : null;
@@ -23922,6 +24376,16 @@ async function invokeRegisteredEffect(effectMap, request, options = {}) {
23922
24376
  handlerFailed = true;
23923
24377
  handlerError = error2;
23924
24378
  }
24379
+ let commitError;
24380
+ let commitFailed = false;
24381
+ if (commitPromise) {
24382
+ try {
24383
+ await commitPromise;
24384
+ } catch (error2) {
24385
+ commitFailed = true;
24386
+ commitError = error2;
24387
+ }
24388
+ }
23925
24389
  let feedbackError;
23926
24390
  let feedbackFailed = false;
23927
24391
  if (feedbackContext) {
@@ -23935,9 +24399,17 @@ async function invokeRegisteredEffect(effectMap, request, options = {}) {
23935
24399
  if (handlerFailed) {
23936
24400
  throw handlerError;
23937
24401
  }
24402
+ if (commitFailed) {
24403
+ throw commitError;
24404
+ }
23938
24405
  if (feedbackFailed) {
23939
24406
  throw feedbackError;
23940
24407
  }
24408
+ if (commitRequired && !commitStarted) {
24409
+ throw new Error(
24410
+ `Mutating effect ${request.effectKey} returned without acknowledging its product mutation`
24411
+ );
24412
+ }
23941
24413
  return handlerResult;
23942
24414
  }
23943
24415
 
@@ -24006,12 +24478,7 @@ function toRecordSearchResult(className, node) {
24006
24478
  (field) => normalizeGraphPathSegment(field.name) === "real_id" && typeof field.value === "string" && field.value.trim()
24007
24479
  );
24008
24480
  const id = typeof realIdField?.value === "string" ? realIdField.value.trim() : graphPathId;
24009
- const rawLabel = typeof node.label === "string" && node.label.trim() ? node.label : "";
24010
- if (fields.length === 0 && rawLabel && isPlaceholderRecordLabel(rawLabel, graphPathId, path7)) {
24011
- return null;
24012
- }
24013
- const fallbackLabel = displayLabelFromFields(fields);
24014
- const label = rawLabel && !isPlaceholderRecordLabel(rawLabel, id, path7) ? rawLabel : fallbackLabel || rawLabel || id;
24481
+ const label = typeof node.label === "string" && node.label.trim() ? node.label.trim() : path7 || id;
24015
24482
  return {
24016
24483
  path: path7,
24017
24484
  className,
@@ -24021,30 +24488,6 @@ function toRecordSearchResult(className, node) {
24021
24488
  fields
24022
24489
  };
24023
24490
  }
24024
- function isPlaceholderRecordLabel(label, id, path7) {
24025
- const normalizedLabel = normalizeGraphPathSegment(label);
24026
- return normalizedLabel === normalizeGraphPathSegment(id) || normalizedLabel === normalizeGraphPathSegment(path7);
24027
- }
24028
- function displayLabelFromFields(fields) {
24029
- const preferredFieldNames = [
24030
- "name",
24031
- "title",
24032
- "label",
24033
- "display_name",
24034
- "file_name",
24035
- "number",
24036
- "code"
24037
- ];
24038
- for (const preferred of preferredFieldNames) {
24039
- const match = fields.find(
24040
- (field) => normalizeGraphPathSegment(field.name) === preferred && typeof field.value === "string" && field.value.trim()
24041
- );
24042
- if (typeof match?.value === "string") {
24043
- return match.value.trim();
24044
- }
24045
- }
24046
- return null;
24047
- }
24048
24491
  function normalizeRecordSearchText(value) {
24049
24492
  return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, " ").replace(/\s+/g, " ").trim();
24050
24493
  }
@@ -24674,6 +25117,133 @@ var Environment = class _Environment {
24674
25117
  getAwaitingCount: async () => this.getAwaitingRecordCount()
24675
25118
  };
24676
25119
  }
25120
+ /**
25121
+ * Acknowledge a declared product mutation that already happened outside a
25122
+ * Granular-run effect (for example, in a webhook consumer). These methods
25123
+ * run the declaration's pure projection mapper; they never call its handler.
25124
+ */
25125
+ get commit() {
25126
+ return {
25127
+ effect: async (effect, productResult, options = {}) => this.persistExternalEffect(effect, productResult, options),
25128
+ transition: async (transition, productResult, options = {}) => this.persistExternalTransition(transition, productResult, options),
25129
+ get: async (commitId) => this.controlPlaneRequest(
25130
+ `/control/environments/${this.environmentId}/commits/${encodeURIComponent(
25131
+ commitId
25132
+ )}`
25133
+ ),
25134
+ retry: async (commitId) => this.controlPlaneRequest(
25135
+ `/control/environments/${this.environmentId}/commits/${encodeURIComponent(
25136
+ commitId
25137
+ )}/retry`,
25138
+ { method: "POST" }
25139
+ )
25140
+ };
25141
+ }
25142
+ /** Inspect or retry the agent synchronization of a product snapshot. */
25143
+ get observation() {
25144
+ return {
25145
+ get: async (observationId) => this.controlPlaneRequest(
25146
+ `/control/environments/${this.environmentId}/observations/${encodeURIComponent(
25147
+ observationId
25148
+ )}`
25149
+ ),
25150
+ retry: async (observationId) => this.controlPlaneRequest(
25151
+ `/control/environments/${this.environmentId}/observations/${encodeURIComponent(
25152
+ observationId
25153
+ )}/retry`,
25154
+ { method: "POST" }
25155
+ )
25156
+ };
25157
+ }
25158
+ async persistExternalEffect(effect, productResult, options) {
25159
+ const projection = effect.commit.project(productResult);
25160
+ validateProjectionResult(effect.commit, projection);
25161
+ this.requireExternalIdentity(projection.source.version, options);
25162
+ return this.controlPlaneRequest(
25163
+ `/control/environments/${this.environmentId}/external-commits`,
25164
+ {
25165
+ method: "POST",
25166
+ body: JSON.stringify({
25167
+ kind: "effect",
25168
+ effectKey: computeEffectKey2(effect),
25169
+ ...options.sourceEventId?.trim() ? { sourceEventId: options.sourceEventId.trim() } : {},
25170
+ projection
25171
+ })
25172
+ }
25173
+ );
25174
+ }
25175
+ async persistExternalTransition(transition, productResult, options) {
25176
+ const metadata = getDefinedTransitionMetadata(transition);
25177
+ if (!metadata) {
25178
+ throw new Error(
25179
+ "environment.commit.transition requires a transition returned by defineStateMachine"
25180
+ );
25181
+ }
25182
+ const projection = transition.effect.commit.project(productResult);
25183
+ validateProjectionResult(transition.effect.commit, projection);
25184
+ this.requireExternalIdentity(projection.source.version, options);
25185
+ if (!Object.prototype.hasOwnProperty.call(
25186
+ transition.outcomes,
25187
+ projection.outcome.key
25188
+ )) {
25189
+ throw new Error(
25190
+ `Transition ${metadata.machine}.${metadata.transition} does not declare outcome ${projection.outcome.key}`
25191
+ );
25192
+ }
25193
+ if (!projection.primaryTarget) {
25194
+ throw new Error(
25195
+ "An external transition projection requires primaryTarget to identify the transitioned product record"
25196
+ );
25197
+ }
25198
+ return this.controlPlaneRequest(
25199
+ `/control/environments/${this.environmentId}/external-commits`,
25200
+ {
25201
+ method: "POST",
25202
+ body: JSON.stringify({
25203
+ kind: "transition",
25204
+ effectKey: computeEffectKey2(transition.effect),
25205
+ ...options.sourceEventId?.trim() ? { sourceEventId: options.sourceEventId.trim() } : {},
25206
+ projection,
25207
+ transition: {
25208
+ className: projection.primaryTarget.className,
25209
+ objectId: projection.primaryTarget.id,
25210
+ ...projection.primaryTarget.path ? { objectPath: projection.primaryTarget.path } : {},
25211
+ machine: metadata.machine,
25212
+ transition: metadata.transition,
25213
+ from: transition.from
25214
+ }
25215
+ })
25216
+ }
25217
+ );
25218
+ }
25219
+ requireExternalIdentity(sourceVersion, options) {
25220
+ if (!options.sourceEventId?.trim() && !sourceVersion?.trim()) {
25221
+ throw new Error(
25222
+ "An external commit requires sourceEventId or a source version from its projection mapper"
25223
+ );
25224
+ }
25225
+ }
25226
+ /**
25227
+ * Synchronize a versioned product snapshot without claiming an effect or
25228
+ * lifecycle transition. This records no transition history.
25229
+ */
25230
+ async observe(mapper, productResult) {
25231
+ const declaration = { kind: "effect"};
25232
+ const projection = mapper(productResult);
25233
+ validateProjectionResult(declaration, projection);
25234
+ if (!projection.source.version?.trim()) {
25235
+ throw new Error(
25236
+ "environment.observe requires a monotonic source version or serialized adapter sequence"
25237
+ );
25238
+ }
25239
+ return this.controlPlaneRequest(
25240
+ `/control/environments/${this.environmentId}/observations`,
25241
+ {
25242
+ method: "POST",
25243
+ body: JSON.stringify({ projection })
25244
+ }
25245
+ );
25246
+ }
24677
25247
  /**
24678
25248
  * Mirror product-owned workflow state into Granular without making Granular
24679
25249
  * own the customer application's state machine.
@@ -26080,6 +26650,14 @@ var EnvironmentSession = class extends Session {
26080
26650
  }
26081
26651
  };
26082
26652
  }
26653
+ get mutations() {
26654
+ return {
26655
+ list: (options = {}) => this.sessionDataRequest(
26656
+ "/mutations",
26657
+ options
26658
+ )
26659
+ };
26660
+ }
26083
26661
  get artifacts() {
26084
26662
  return {
26085
26663
  list: (options = {}) => {
@@ -27579,6 +28157,7 @@ var Granular = class _Granular {
27579
28157
  const serialized = {
27580
28158
  effectKey: computeEffectKey2(effect),
27581
28159
  name: effect.name,
28160
+ ...effect.label ? { label: effect.label } : {},
27582
28161
  description: effect.description,
27583
28162
  inputSchema: effect.inputSchema,
27584
28163
  stability: effect.stability || "stable",
@@ -27602,6 +28181,9 @@ var Granular = class _Granular {
27602
28181
  if (effect.metamodels !== void 0) {
27603
28182
  serialized.metamodels = effect.metamodels;
27604
28183
  }
28184
+ if (effect.commit !== void 0) {
28185
+ serialized.commit = { kind: effect.commit.kind };
28186
+ }
27605
28187
  return serialized;
27606
28188
  }
27607
28189
  async publishSandboxEffectCatalog(host) {
@@ -27822,16 +28404,60 @@ var Granular = class _Granular {
27822
28404
  };
27823
28405
  wsClient.registerRpcHandler("effect.invoke", async (params) => {
27824
28406
  const request = params;
27825
- return invokeRegisteredEffect(
28407
+ let commitReceipt;
28408
+ const result = await invokeRegisteredEffect(
27826
28409
  this.getSandboxEffectMap(sandboxId),
27827
28410
  request,
27828
28411
  {
28412
+ commitAcknowledged: (receipt) => {
28413
+ commitReceipt = receipt;
28414
+ },
28415
+ commit: {
28416
+ persist: (commitRequest) => this.request(
28417
+ `/control/environments/${encodeURIComponent(
28418
+ commitRequest.environmentId
28419
+ )}/commits`,
28420
+ {
28421
+ method: "POST",
28422
+ body: JSON.stringify({
28423
+ kind: commitRequest.kind,
28424
+ invocationId: commitRequest.invocationId,
28425
+ idempotencyKey: commitRequest.idempotencyKey,
28426
+ effectKey: commitRequest.effectKey,
28427
+ projection: commitRequest.projection
28428
+ })
28429
+ }
28430
+ ),
28431
+ mappingFailed: (failure) => this.request(
28432
+ `/control/environments/${encodeURIComponent(
28433
+ failure.environmentId
28434
+ )}/commit-invocations/${encodeURIComponent(
28435
+ failure.invocationId
28436
+ )}`,
28437
+ {
28438
+ method: "PATCH",
28439
+ body: JSON.stringify({
28440
+ status: "mapping_failed",
28441
+ error: {
28442
+ code: "commit_projection_mapping_failed",
28443
+ message: failure.message,
28444
+ retryable: false
28445
+ }
28446
+ })
28447
+ }
28448
+ )
28449
+ },
27829
28450
  feedback: {
27830
28451
  invocationId: request.callId,
27831
28452
  publish: (method, publishParams) => wsClient.call(method, publishParams)
27832
28453
  }
27833
28454
  }
27834
28455
  );
28456
+ return {
28457
+ __granularEffectInvocationResult: true,
28458
+ result,
28459
+ ...commitReceipt ? { commit: commitReceipt } : {}
28460
+ };
27835
28461
  });
27836
28462
  wsClient.on("open", () => {
27837
28463
  void this.synchronizeEffectHost(host).catch((error2) => {