@lotics/app-sdk 0.114.0 → 0.115.0

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
@@ -23971,6 +23971,180 @@ function everyOptionMarked(nameOf2) {
23971
23971
  };
23972
23972
  }
23973
23973
 
23974
+ // ../shared/src/schemas/iam.ts
23975
+ var jsonSchemaSchema = zod_default.string();
23976
+ var organizationSchema = zod_default.object({
23977
+ id: zod_default.string().describe("Unique identifier for the organization"),
23978
+ name: zod_default.string().describe("Display name of the organization"),
23979
+ slug: zod_default.string().nullable().describe("Slug for the organization")
23980
+ });
23981
+ var memberAccountTypeSchema = zod_default.enum(["self_managed", "admin_managed", "service"]).describe(
23982
+ "How the account is credentialed. admin_managed means an admin chose the sign-in name and set the password; the account has NO email address at all, so nothing can be mailed to it until the member claims one they control. service means no credential exists at all \u2014 the membership is an integration's identity, reached only by an API key bound to it."
23983
+ );
23984
+ var ALL_ACCOUNT_TYPES = memberAccountTypeSchema.options;
23985
+ var memberSchema = zod_default.object({
23986
+ member_id: zod_default.string().describe("Unique identifier for the member"),
23987
+ organization_id: zod_default.string().describe("Organization ID the member belongs to"),
23988
+ name: zod_default.string().describe("Display name of the member"),
23989
+ email_address: zod_default.string().nullable().describe(
23990
+ "What the member types to sign in: an email address for a self-managed member, an administrator-issued name (e.g. giamdoc.sunrise) for an admin-managed one. The latter is NOT an address and cannot receive mail \u2014 the field name predates the split. Null for a service identity, which holds no credential and signs in nowhere."
23991
+ ),
23992
+ role: zod_default.enum(["owner", "admin", "member"]).describe("Organization role"),
23993
+ image: zod_default.string().nullable().optional().describe("URL to the member's profile picture"),
23994
+ account_type: memberAccountTypeSchema,
23995
+ archived: zod_default.boolean().optional().describe(
23996
+ "True when this person has been removed from the organization. The membership survives removal so records naming them keep resolving; it grants nothing. Optional because an older server omits it \u2014 absent means active."
23997
+ ),
23998
+ joined_at: zod_default.string().optional().describe(
23999
+ "ISO timestamp of when this membership began. Optional because an older server omits it \u2014 absent means not told, never 'joined at the epoch'."
24000
+ )
24001
+ });
24002
+ var pendingInvitationSchema = zod_default.object({
24003
+ invitation_id: zod_default.string().describe("Unique identifier for the pending invitation"),
24004
+ organization_id: zod_default.string().describe("Organization the invitee will join on accept"),
24005
+ email: zod_default.string().describe("Email the invitation was sent to"),
24006
+ name: zod_default.string().nullable().describe("Admin-supplied display name (carried onto member.name on accept; invitee can override at accept time)"),
24007
+ role: zod_default.enum(["owner", "admin", "member"]).describe("Role the invitee will receive on accept"),
24008
+ expires_at: zod_default.string().describe("ISO timestamp when the invitation expires")
24009
+ });
24010
+ var ianaTimezoneSchema = zod_default.string().refine(
24011
+ (tz) => {
24012
+ try {
24013
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
24014
+ return true;
24015
+ } catch {
24016
+ return false;
24017
+ }
24018
+ },
24019
+ { message: "must be an IANA timezone name, e.g. Asia/Ho_Chi_Minh or America/New_York" }
24020
+ ).describe("IANA timezone name (e.g., 'America/New_York', 'Asia/Ho_Chi_Minh')");
24021
+ var currencyCodeSchema = zod_default.string().trim().toUpperCase().regex(/^[A-Z]{3}$/, "must be an ISO 4217 currency code, e.g. VND, USD, EUR").describe("ISO 4217 currency code (e.g. 'VND', 'USD', 'EUR'). Upper-cased.");
24022
+ var workspaceSchema = zod_default.object({
24023
+ id: zod_default.string().describe("Unique identifier for the workspace"),
24024
+ name: zod_default.string().describe("Display name of the workspace"),
24025
+ default_currency: zod_default.string().describe("Default currency code for the workspace"),
24026
+ // Plain string on the way OUT, deliberately. This is a response schema, so
24027
+ // validating here would make the server reject its own stored rows — and on
24028
+ // the workspace LIST one bad row fails the entire array. The zone is enforced
24029
+ // where it is written; a read is too late to do anything about it anyway.
24030
+ timezone: zod_default.string().describe("IANA timezone name (e.g., 'America/New_York', 'Asia/Ho_Chi_Minh')"),
24031
+ organization_id: zod_default.string().describe("Organization ID this workspace belongs to"),
24032
+ created_at: zod_default.string().describe("Timestamp when workspace was created")
24033
+ });
24034
+ var memberGroupSchema = zod_default.object({
24035
+ id: zod_default.string().describe("Unique identifier for the member group"),
24036
+ name: zod_default.string().describe("Display name of the group"),
24037
+ description: zod_default.string().optional().describe("Optional description of the group's purpose"),
24038
+ member_ids: zod_default.array(zod_default.string()).describe("Array of member IDs in this group"),
24039
+ invitation_ids: zod_default.array(zod_default.string()).describe("Array of pending invitation IDs pre-granted membership; migrated to member_ids on accept"),
24040
+ organization_id: zod_default.string().describe("ID of the organization this group belongs to"),
24041
+ created_at: zod_default.string().describe("Timestamp when group was created"),
24042
+ created_by: zod_default.string().describe("Member ID of the user who created this group")
24043
+ });
24044
+ var principalGroupSchema = zod_default.object({
24045
+ id: zod_default.string().describe("Unique identifier for the member group"),
24046
+ name: zod_default.string().describe("Display name of the group"),
24047
+ description: zod_default.string().optional().describe("Optional description of the group's purpose"),
24048
+ member_count: zod_default.number().int().describe("Number of accepted members in the group"),
24049
+ invitation_count: zod_default.number().int().describe("Number of pending invitations pre-granted membership")
24050
+ });
24051
+ var PUBLIC_PRINCIPAL_ID = "public";
24052
+ var principalSchema = zod_default.discriminatedUnion("type", [
24053
+ zod_default.object({
24054
+ type: zod_default.literal("member").describe("Principal type: individual member"),
24055
+ id: zod_default.string().describe("Member ID")
24056
+ }),
24057
+ zod_default.object({
24058
+ type: zod_default.literal("member_group").describe("Principal type: group of members"),
24059
+ id: zod_default.string().describe("Member group ID")
24060
+ }),
24061
+ zod_default.object({
24062
+ type: zod_default.literal("organization").describe("Principal type: all members in organization"),
24063
+ id: zod_default.string().describe("Organization ID")
24064
+ }),
24065
+ zod_default.object({
24066
+ type: zod_default.literal("public").describe("Principal type: anyone, including unauthenticated visitors"),
24067
+ id: zod_default.literal(PUBLIC_PRINCIPAL_ID).describe("Always the `public` sentinel \u2014 no entity")
24068
+ }),
24069
+ // Pending invitation as principal: lets admins pre-grant group membership and
24070
+ // resource access before the invitee accepts. Never resolved as a Subject —
24071
+ // a pending invitee cannot authenticate, so these bindings stay dormant until
24072
+ // accept-time migration rewrites them to type:"member".
24073
+ zod_default.object({
24074
+ type: zod_default.literal("invitation").describe("Principal type: pending invitation (not yet accepted)"),
24075
+ id: zod_default.string().describe("Invitation ID")
24076
+ })
24077
+ ]);
24078
+ var subjectSchema = zod_default.discriminatedUnion("type", [
24079
+ zod_default.object({
24080
+ type: zod_default.literal("member").describe("Subject type: authenticated member"),
24081
+ id: zod_default.string().describe("Member ID")
24082
+ }),
24083
+ zod_default.object({
24084
+ type: zod_default.literal("public").describe("Subject type: unauthenticated visitor")
24085
+ })
24086
+ ]);
24087
+ var resourceTypeSchema = zod_default.enum(["table", "table_view", "app", "automation", "skill", "document_template", "instruction", "knowledge_doc", "connected_account"]);
24088
+ var RESOURCE_ROLE_ACTIONS = {
24089
+ table: {
24090
+ editor: ["read_data", "write_data"],
24091
+ viewer: ["read_data"]
24092
+ },
24093
+ app: { manager: ["use", "manage"], user: ["use"] },
24094
+ skill: { manager: ["use", "manage"], user: ["use"] },
24095
+ document_template: { manager: ["use", "manage"], user: ["use"] },
24096
+ instruction: { manager: ["use", "manage"], user: ["use"] },
24097
+ knowledge_doc: { manager: ["use", "manage"], user: ["use"] },
24098
+ connected_account: { manager: ["use", "manage"], user: ["use"] },
24099
+ table_view: {},
24100
+ automation: {}
24101
+ };
24102
+ function shareableRolesFor(type) {
24103
+ return Object.keys(RESOURCE_ROLE_ACTIONS[type]);
24104
+ }
24105
+ var SHAREABLE_RESOURCE_TYPES = resourceTypeSchema.options.filter(
24106
+ (type) => shareableRolesFor(type).length > 0
24107
+ );
24108
+ var accessPrincipalSchema = zod_default.discriminatedUnion("type", [
24109
+ zod_default.object({
24110
+ type: zod_default.literal("member").describe("Principal type: an individual member"),
24111
+ id: zod_default.string().describe("Member ID")
24112
+ }),
24113
+ zod_default.object({
24114
+ type: zod_default.literal("member_group").describe("Principal type: a member group"),
24115
+ id: zod_default.string().describe("Group ID")
24116
+ }),
24117
+ zod_default.object({
24118
+ type: zod_default.literal("organization").describe("Principal type: everyone in the organization"),
24119
+ id: zod_default.string().describe("Organization ID")
24120
+ })
24121
+ ]);
24122
+ var resourceAccessSchema = zod_default.object({
24123
+ resource_id: zod_default.string().describe("ID of the resource these principals can reach"),
24124
+ principals: zod_default.array(accessPrincipalSchema).describe("Members, groups and the organization that have been granted access")
24125
+ });
24126
+ var contentResourceTypeSchema = zod_default.enum(["knowledge_doc", "document_template"]);
24127
+ var contentItemSchema = zod_default.object({
24128
+ id: zod_default.string().describe("ID of the knowledge doc or template"),
24129
+ name: zod_default.string().describe("Its name"),
24130
+ owner_member_id: zod_default.string().nullable().describe("Member who owns it; null when nobody does"),
24131
+ created_at: zod_default.string().describe("When it was created"),
24132
+ updated_at: zod_default.string().nullable().describe("When a knowledge doc was last changed; null for a template"),
24133
+ hidden_at: zod_default.string().nullable().describe("When a knowledge doc was hidden; null when it is in use, and for a template"),
24134
+ template_type: zod_default.string().nullable().describe("A template's type (html, email, word, excel, pdf-form); null for a knowledge doc"),
24135
+ can_open: zod_default.boolean().describe("Whether the caller may open it; false means it is not shared with them"),
24136
+ principals: zod_default.array(accessPrincipalSchema).describe("Members, groups and the organization it has been shared with, besides its owner")
24137
+ });
24138
+ var resourceRoleBindingSchema = zod_default.object({
24139
+ id: zod_default.string().describe("Unique identifier for the role binding"),
24140
+ principal: principalSchema.describe(
24141
+ "Who this role binding applies to (member, group, or organization)"
24142
+ ),
24143
+ resource_type: resourceTypeSchema.describe("Type of resource being accessed"),
24144
+ resource_id: zod_default.string().describe("ID of the specific resource"),
24145
+ role: zod_default.string().describe("Role name (e.g., 'viewer', 'editor', 'manager', 'user')")
24146
+ });
24147
+
23974
24148
  // ../shared/src/diacritics.ts
23975
24149
  var COMBINING_MARKS = /[̀-ͯ]/g;
23976
24150
  function foldDiacritics(value) {
@@ -24831,7 +25005,7 @@ var tableFieldTypeConfigSchema = zod_default.discriminatedUnion("type", [
24831
25005
  zod_default.object({
24832
25006
  type: zod_default.literal("number"),
24833
25007
  format: numberFormatSchema.default("number"),
24834
- currency: zod_default.string().optional().describe("ISO 4217 code"),
25008
+ currency: currencyCodeSchema.optional(),
24835
25009
  unit: zod_default.string().optional().describe(UNIT_DESCRIPTION),
24836
25010
  unit_field: zod_default.string().optional().describe(UNIT_FIELD_DESCRIPTION),
24837
25011
  currency_field: zod_default.string().optional().describe(CURRENCY_FIELD_DESCRIPTION),
@@ -24900,7 +25074,7 @@ var tableFieldTypeConfigSchema = zod_default.discriminatedUnion("type", [
24900
25074
  }),
24901
25075
  zod_default.object({
24902
25076
  type: zod_default.literal("formula"),
24903
- formula: formulaInputSchema
25077
+ formula: formulaInputSchema.extend({ currency: currencyCodeSchema.optional() })
24904
25078
  }).superRefine((field, ctx) => figureIssue(["formula"])(field.formula, ctx)),
24905
25079
  // `button` is deliberately absent: button fields are legacy — this union
24906
25080
  // shapes CREATE input only, and new buttons can no longer be created
@@ -24972,173 +25146,6 @@ function projectCell(field, value) {
24972
25146
  }
24973
25147
  }
24974
25148
 
24975
- // ../shared/src/walk_workflow_expression.ts
24976
- var LINKED_CALL = "linked";
24977
- var GRAMMAR_NAME_SHAPE = `${LINKED_CALL}(record["fld_link"])[0] fetches one linked row`;
24978
- var RUNTIME_KEYS = /* @__PURE__ */ new Set([
24979
- "timezone",
24980
- "workflow_id",
24981
- "execution_id",
24982
- "workspace_id",
24983
- "organization_id",
24984
- "now",
24985
- "change_origin",
24986
- "execution_principal",
24987
- "triggered_by_member_id"
24988
- ]);
24989
- var CURRENT_MEMBER_IN_ANY_GROUP = "current_member_in_any_group";
24990
- var PARKED_RUNTIME_KEYS = /* @__PURE__ */ new Map([
24991
- [
24992
- "execution_principal",
24993
- `runtime.execution_principal is not readable \u2014 a workflow runs under the OWNER's authority, so it never answers "is the person who triggered this allowed?". Authorize the caller with ${CURRENT_MEMBER_IN_ANY_GROUP}(["grp_\u2026"]) or read runtime.triggered_by_member_id.`
24994
- ]
24995
- ]);
24996
- var READABLE_RUNTIME_KEYS = Array.from(RUNTIME_KEYS).filter(
24997
- (key) => !PARKED_RUNTIME_KEYS.has(key)
24998
- );
24999
- var INLINE_HELPERS = [
25000
- "formatCurrency",
25001
- "randomNumber",
25002
- "randomAlphaNumeric",
25003
- "sample",
25004
- // Actor authorization predicate — resolves the triggering member's groups
25005
- // server-side (bound in the backend helper registry, not a pure shared
25006
- // helper).
25007
- CURRENT_MEMBER_IN_ANY_GROUP
25008
- ];
25009
- var HELPERS = /* @__PURE__ */ new Set([
25010
- ...Object.keys(expression_array_exports),
25011
- ...Object.keys(expression_string_exports),
25012
- ...Object.keys(expression_math_exports),
25013
- ...Object.keys(expression_object_exports),
25014
- ...Object.keys(expression_type_exports),
25015
- ...Object.keys(getDateExpressionFunctions()),
25016
- ...INLINE_HELPERS
25017
- ]);
25018
- function isHelper(name) {
25019
- return HELPERS.has(name);
25020
- }
25021
- var HELPER_MENU = [
25022
- `text \u2014 ${Object.keys(expression_string_exports).sort().join(", ")}`,
25023
- `number \u2014 ${Object.keys(expression_math_exports).sort().join(", ")}`,
25024
- `array \u2014 ${Object.keys(expression_array_exports).sort().join(", ")}`,
25025
- `object \u2014 ${Object.keys(expression_object_exports).sort().join(", ")}`,
25026
- `type/null \u2014 ${Object.keys(expression_type_exports).sort().join(", ")}`,
25027
- `date \u2014 ${Object.keys(getDateExpressionFunctions()).sort().join(", ")}`,
25028
- `other \u2014 ${[...INLINE_HELPERS].sort().join(", ")}`
25029
- ].join("; ");
25030
- var AWAIT_IF_FORM = "Write the if form: let x = null; if (<condition>) { x = await tool({...}); }";
25031
- var AWAIT_IN_OPTIONAL_CHAIN = `An await after \`?.\` runs only when the value before it is not null. ${AWAIT_IF_FORM}`;
25032
- var CALLBACK_HELPERS = /* @__PURE__ */ new Set([
25033
- "filter",
25034
- "find",
25035
- "pluck",
25036
- "sortBy",
25037
- "sumBy",
25038
- "meanBy",
25039
- "minBy",
25040
- "maxBy",
25041
- "countBy",
25042
- "groupBy",
25043
- "uniqueBy",
25044
- "some",
25045
- "every",
25046
- "differenceBy",
25047
- "intersectionBy",
25048
- "reduce"
25049
- ]);
25050
- var LAMBDA_POSITION_HINT = `Function expressions are not supported here. A lambda \u2014 either \`(x) => <expr>\` or \`function (x) { return <expr>; }\` \u2014 is legal only INLINE as a callback argument to ${[...CALLBACK_HELPERS].sort().join(" / ")}, e.g. pluck(rows, (r) => r.qty * r.price). To NAME reusable logic, declare it at the top level instead: \`function toLine(l = {}) { return { ... }; }\` \u2014 every parameter needs a default, and the body is inlined at each call site. Binding a lambda to a name (const f = (x) => ...) or calling one directly stays unsupported.`;
25051
- var HELPER_ALIASES = /* @__PURE__ */ new Map([
25052
- ["String", "toString"],
25053
- ["Number", "toNumber"],
25054
- ["parseFloat", "toNumber"],
25055
- // faithful: toNumber IS parseFloat semantics
25056
- ["toUpperCase", "upper"],
25057
- ["toLowerCase", "lower"],
25058
- ["map", "pluck"],
25059
- // arr.map(x => …) ≡ pluck(arr, x => …)
25060
- ["toFixed", "formatNumber"]
25061
- // x.toFixed(2) ≡ formatNumber(x, 2)
25062
- ]);
25063
- var REJECTED_HELPERS = /* @__PURE__ */ new Map([
25064
- [
25065
- "parseInt",
25066
- {
25067
- message: "parseInt is not supported \u2014 parse with toNumber(x), then apply floor(...), ceil(...), or round(...) to get a whole number. floor rounds down (toward negative infinity), so for negative values choose the rounding deliberately.",
25068
- construct: "parse_int"
25069
- }
25070
- ]
25071
- ]);
25072
- var NAMESPACE_HELPERS = /* @__PURE__ */ new Map([
25073
- ["JSON", /* @__PURE__ */ new Map([["parse", "parseJson"], ["stringify", "toJson"]])],
25074
- ["Array", /* @__PURE__ */ new Map([["isArray", "isArray"]])],
25075
- ["Object", /* @__PURE__ */ new Map([["keys", "keys"], ["values", "values"], ["entries", "entries"]])],
25076
- ["Number", /* @__PURE__ */ new Map([["parseFloat", "toNumber"]])]
25077
- ]);
25078
- var METHOD_FORM_NAMES = /* @__PURE__ */ new Set([
25079
- // String.prototype
25080
- "trim",
25081
- "toUpperCase",
25082
- "toLowerCase",
25083
- "replace",
25084
- "replaceAll",
25085
- "split",
25086
- "substring",
25087
- "padStart",
25088
- "padEnd",
25089
- "startsWith",
25090
- "endsWith",
25091
- // String.prototype + Array.prototype
25092
- "includes",
25093
- "slice",
25094
- "at",
25095
- "concat",
25096
- // Array.prototype
25097
- "join",
25098
- "reverse",
25099
- "map",
25100
- "filter",
25101
- "find",
25102
- "some",
25103
- "every",
25104
- "reduce",
25105
- // Number.prototype / Object.prototype
25106
- "toFixed",
25107
- "toString"
25108
- ]);
25109
- var METHOD_FORM_MENU = [...METHOD_FORM_NAMES].sort().join(", ");
25110
- var PROPERTY_HELPERS = /* @__PURE__ */ new Map([["length", "length"]]);
25111
- var HELPER_SPELLINGS = (() => {
25112
- const out = /* @__PURE__ */ new Map();
25113
- const add = (helper, spelling) => {
25114
- if (isHelper(helper) && !out.has(helper)) out.set(helper, spelling);
25115
- };
25116
- for (const name of METHOD_FORM_NAMES) add(HELPER_ALIASES.get(name) ?? name, { kind: "method", name });
25117
- for (const [name, helper] of PROPERTY_HELPERS) add(helper, { kind: "property", name });
25118
- for (const [namespace, members2] of NAMESPACE_HELPERS) {
25119
- for (const [member, helper] of members2) add(helper, { kind: "namespace", namespace, member });
25120
- }
25121
- for (const fn of Object.keys(expression_math_exports)) {
25122
- if (fn !== "random" && typeof Reflect.get(Math, fn) === "function") {
25123
- add(fn, { kind: "namespace", namespace: "Math", member: fn });
25124
- }
25125
- }
25126
- return out;
25127
- })();
25128
- var RESERVED_CALLABLE_NAMES = /* @__PURE__ */ new Set([
25129
- ...REJECTED_HELPERS.keys(),
25130
- ...NAMESPACE_HELPERS.keys(),
25131
- "Math",
25132
- "Boolean"
25133
- ]);
25134
- var LINKED_ARITY = `${LINKED_CALL}(...) takes exactly one argument \u2014 the read of the link field, e.g. ${LINKED_CALL}(record["fld_link"])[0] for one row or ${LINKED_CALL}(record["fld_link"]) for every row.`;
25135
- var LINKED_ARGUMENT_SHAPE = `The argument of ${LINKED_CALL}(...) must END on the link field, because that field is what it follows \u2014 ${LINKED_CALL}(record["fld_link"])[0]. A value that names no field (a binding, a literal, a helper result) cannot say which link to follow; read the field on it: ${LINKED_CALL}(ids["fld_link"]).`;
25136
- var LINKED_OPTIONAL_RESULT = `${LINKED_CALL}(...) always returns an array, never null, so \`?.\` on its result can never fire. The argument takes a guard \u2014 ${LINKED_CALL}(record?.["fld_link"])[0] \u2014 and so does the fetched row, which is null where the linked record is deleted or unreadable: ${LINKED_CALL}(record["fld_link"])[0]?.["fld_x"].`;
25137
- var LINKED_METHOD_FORM = `${LINKED_CALL} has no method form \u2014 write ${LINKED_CALL}(record["fld_link"])[0], with the link read as the argument.`;
25138
- var LINKED_ON_NON_RECORD = `${LINKED_CALL}(...) follows a link field on a record, and this value is not one. Read the record first, then follow the link: ${LINKED_CALL}(record["fld_link"])[0].`;
25139
- var LINKED_ON_CHANGES = `A change diff is not a record, so ${LINKED_CALL}(...) cannot follow a link off it. Read the post-state with ${LINKED_CALL}(record["fld_link"]) or the pre-state with ${LINKED_CALL}(prev_record["fld_link"]).`;
25140
- var READ_ROOT_HINT = `A read starts from a name: a reserved identifier (record, prev_record, changes, runtime, trigger, index), a binding, or ${LINKED_CALL}(record["fld_link"]). Bind the value first \u2014 const x = <expression>; \u2014 then read x.y.`;
25141
-
25142
25149
  // ../shared/src/schemas/workflow_expressions.ts
25143
25150
  var runtimeKeySchema = zod_default.enum([
25144
25151
  "timezone",
@@ -25148,15 +25155,6 @@ var runtimeKeySchema = zod_default.enum([
25148
25155
  "organization_id",
25149
25156
  "now",
25150
25157
  "change_origin",
25151
- // PARKED — accepted in stored ASTs, rejected for new saves at parse (see
25152
- // `PARKED_RUNTIME_KEYS` in walk_workflow_expression.ts). It is the OWNER's
25153
- // principal (a workflow runs under owner authority), so it can't answer the
25154
- // caller-authorization question anyone reads it for; the generated `.d.ts`
25155
- // omits it and the docs point at `current_member_in_any_group` /
25156
- // `runtime.triggered_by_member_id` instead. The entry stays so any stored
25157
- // AST keeps validating and executing — drop it only once a prod probe shows
25158
- // zero references (`workflows.steps_v2::text LIKE '%execution_principal%'`).
25159
- "execution_principal",
25160
25158
  "triggered_by_member_id"
25161
25159
  ]);
25162
25160
  var nestedExpressionSchema = zod_default.lazy(
@@ -25329,6 +25327,154 @@ var templatePartSchema = zod_default.lazy(
25329
25327
  ])
25330
25328
  );
25331
25329
 
25330
+ // ../shared/src/walk_workflow_expression.ts
25331
+ var LINKED_CALL = "linked";
25332
+ var GRAMMAR_NAME_SHAPE = `${LINKED_CALL}(record["fld_link"])[0] fetches one linked row`;
25333
+ var RUNTIME_KEYS = new Set(runtimeKeySchema.options);
25334
+ var CURRENT_MEMBER_IN_ANY_GROUP = "current_member_in_any_group";
25335
+ var INLINE_HELPERS = [
25336
+ "formatCurrency",
25337
+ "randomNumber",
25338
+ "randomAlphaNumeric",
25339
+ "sample",
25340
+ // Actor authorization predicate — resolves the triggering member's groups
25341
+ // server-side (bound in the backend helper registry, not a pure shared
25342
+ // helper).
25343
+ CURRENT_MEMBER_IN_ANY_GROUP
25344
+ ];
25345
+ var HELPERS = /* @__PURE__ */ new Set([
25346
+ ...Object.keys(expression_array_exports),
25347
+ ...Object.keys(expression_string_exports),
25348
+ ...Object.keys(expression_math_exports),
25349
+ ...Object.keys(expression_object_exports),
25350
+ ...Object.keys(expression_type_exports),
25351
+ ...Object.keys(getDateExpressionFunctions()),
25352
+ ...INLINE_HELPERS
25353
+ ]);
25354
+ function isHelper(name) {
25355
+ return HELPERS.has(name);
25356
+ }
25357
+ var HELPER_MENU = [
25358
+ `text \u2014 ${Object.keys(expression_string_exports).sort().join(", ")}`,
25359
+ `number \u2014 ${Object.keys(expression_math_exports).sort().join(", ")}`,
25360
+ `array \u2014 ${Object.keys(expression_array_exports).sort().join(", ")}`,
25361
+ `object \u2014 ${Object.keys(expression_object_exports).sort().join(", ")}`,
25362
+ `type/null \u2014 ${Object.keys(expression_type_exports).sort().join(", ")}`,
25363
+ `date \u2014 ${Object.keys(getDateExpressionFunctions()).sort().join(", ")}`,
25364
+ `other \u2014 ${[...INLINE_HELPERS].sort().join(", ")}`
25365
+ ].join("; ");
25366
+ var AWAIT_IF_FORM = "Write the if form: let x = null; if (<condition>) { x = await tool({...}); }";
25367
+ var AWAIT_IN_OPTIONAL_CHAIN = `An await after \`?.\` runs only when the value before it is not null. ${AWAIT_IF_FORM}`;
25368
+ var CALLBACK_HELPERS = /* @__PURE__ */ new Set([
25369
+ "filter",
25370
+ "find",
25371
+ "pluck",
25372
+ "sortBy",
25373
+ "sumBy",
25374
+ "meanBy",
25375
+ "minBy",
25376
+ "maxBy",
25377
+ "countBy",
25378
+ "groupBy",
25379
+ "uniqueBy",
25380
+ "some",
25381
+ "every",
25382
+ "differenceBy",
25383
+ "intersectionBy",
25384
+ "reduce"
25385
+ ]);
25386
+ var LAMBDA_POSITION_HINT = `Function expressions are not supported here. A lambda \u2014 either \`(x) => <expr>\` or \`function (x) { return <expr>; }\` \u2014 is legal only INLINE as a callback argument to ${[...CALLBACK_HELPERS].sort().join(" / ")}, e.g. pluck(rows, (r) => r.qty * r.price). To NAME reusable logic, declare it at the top level instead: \`function toLine(l = {}) { return { ... }; }\` \u2014 every parameter needs a default, and the body is inlined at each call site. Binding a lambda to a name (const f = (x) => ...) or calling one directly stays unsupported.`;
25387
+ var HELPER_ALIASES = /* @__PURE__ */ new Map([
25388
+ ["String", "toString"],
25389
+ ["Number", "toNumber"],
25390
+ ["parseFloat", "toNumber"],
25391
+ // faithful: toNumber IS parseFloat semantics
25392
+ ["toUpperCase", "upper"],
25393
+ ["toLowerCase", "lower"],
25394
+ ["map", "pluck"],
25395
+ // arr.map(x => …) ≡ pluck(arr, x => …)
25396
+ ["toFixed", "formatNumber"]
25397
+ // x.toFixed(2) ≡ formatNumber(x, 2)
25398
+ ]);
25399
+ var REJECTED_HELPERS = /* @__PURE__ */ new Map([
25400
+ [
25401
+ "parseInt",
25402
+ {
25403
+ message: "parseInt is not supported \u2014 parse with toNumber(x), then apply floor(...), ceil(...), or round(...) to get a whole number. floor rounds down (toward negative infinity), so for negative values choose the rounding deliberately.",
25404
+ construct: "parse_int"
25405
+ }
25406
+ ]
25407
+ ]);
25408
+ var NAMESPACE_HELPERS = /* @__PURE__ */ new Map([
25409
+ ["JSON", /* @__PURE__ */ new Map([["parse", "parseJson"], ["stringify", "toJson"]])],
25410
+ ["Array", /* @__PURE__ */ new Map([["isArray", "isArray"]])],
25411
+ ["Object", /* @__PURE__ */ new Map([["keys", "keys"], ["values", "values"], ["entries", "entries"]])],
25412
+ ["Number", /* @__PURE__ */ new Map([["parseFloat", "toNumber"]])]
25413
+ ]);
25414
+ var METHOD_FORM_NAMES = /* @__PURE__ */ new Set([
25415
+ // String.prototype
25416
+ "trim",
25417
+ "toUpperCase",
25418
+ "toLowerCase",
25419
+ "replace",
25420
+ "replaceAll",
25421
+ "split",
25422
+ "substring",
25423
+ "padStart",
25424
+ "padEnd",
25425
+ "startsWith",
25426
+ "endsWith",
25427
+ // String.prototype + Array.prototype
25428
+ "includes",
25429
+ "slice",
25430
+ "at",
25431
+ "concat",
25432
+ // Array.prototype
25433
+ "join",
25434
+ "reverse",
25435
+ "map",
25436
+ "filter",
25437
+ "find",
25438
+ "some",
25439
+ "every",
25440
+ "reduce",
25441
+ // Number.prototype / Object.prototype
25442
+ "toFixed",
25443
+ "toString"
25444
+ ]);
25445
+ var METHOD_FORM_MENU = [...METHOD_FORM_NAMES].sort().join(", ");
25446
+ var PROPERTY_HELPERS = /* @__PURE__ */ new Map([["length", "length"]]);
25447
+ var HELPER_SPELLINGS = (() => {
25448
+ const out = /* @__PURE__ */ new Map();
25449
+ const add = (helper, spelling) => {
25450
+ if (isHelper(helper) && !out.has(helper)) out.set(helper, spelling);
25451
+ };
25452
+ for (const name of METHOD_FORM_NAMES) add(HELPER_ALIASES.get(name) ?? name, { kind: "method", name });
25453
+ for (const [name, helper] of PROPERTY_HELPERS) add(helper, { kind: "property", name });
25454
+ for (const [namespace, members2] of NAMESPACE_HELPERS) {
25455
+ for (const [member, helper] of members2) add(helper, { kind: "namespace", namespace, member });
25456
+ }
25457
+ for (const fn of Object.keys(expression_math_exports)) {
25458
+ if (fn !== "random" && typeof Reflect.get(Math, fn) === "function") {
25459
+ add(fn, { kind: "namespace", namespace: "Math", member: fn });
25460
+ }
25461
+ }
25462
+ return out;
25463
+ })();
25464
+ var RESERVED_CALLABLE_NAMES = /* @__PURE__ */ new Set([
25465
+ ...REJECTED_HELPERS.keys(),
25466
+ ...NAMESPACE_HELPERS.keys(),
25467
+ "Math",
25468
+ "Boolean"
25469
+ ]);
25470
+ var LINKED_ARITY = `${LINKED_CALL}(...) takes exactly one argument \u2014 the read of the link field, e.g. ${LINKED_CALL}(record["fld_link"])[0] for one row or ${LINKED_CALL}(record["fld_link"]) for every row.`;
25471
+ var LINKED_ARGUMENT_SHAPE = `The argument of ${LINKED_CALL}(...) must END on the link field, because that field is what it follows \u2014 ${LINKED_CALL}(record["fld_link"])[0]. A value that names no field (a binding, a literal, a helper result) cannot say which link to follow; read the field on it: ${LINKED_CALL}(ids["fld_link"]).`;
25472
+ var LINKED_OPTIONAL_RESULT = `${LINKED_CALL}(...) always returns an array, never null, so \`?.\` on its result can never fire. The argument takes a guard \u2014 ${LINKED_CALL}(record?.["fld_link"])[0] \u2014 and so does the fetched row, which is null where the linked record is deleted or unreadable: ${LINKED_CALL}(record["fld_link"])[0]?.["fld_x"].`;
25473
+ var LINKED_METHOD_FORM = `${LINKED_CALL} has no method form \u2014 write ${LINKED_CALL}(record["fld_link"])[0], with the link read as the argument.`;
25474
+ var LINKED_ON_NON_RECORD = `${LINKED_CALL}(...) follows a link field on a record, and this value is not one. Read the record first, then follow the link: ${LINKED_CALL}(record["fld_link"])[0].`;
25475
+ var LINKED_ON_CHANGES = `A change diff is not a record, so ${LINKED_CALL}(...) cannot follow a link off it. Read the post-state with ${LINKED_CALL}(record["fld_link"]) or the pre-state with ${LINKED_CALL}(prev_record["fld_link"]).`;
25476
+ var READ_ROOT_HINT = `A read starts from a name: a reserved identifier (record, prev_record, changes, runtime, trigger, index), a binding, or ${LINKED_CALL}(record["fld_link"]). Bind the value first \u2014 const x = <expression>; \u2014 then read x.y.`;
25477
+
25332
25478
  // ../shared/src/chat_models.ts
25333
25479
  var MODEL_TIERS = ["haiku", "sonnet", "opus"];
25334
25480
  var TIER_MODELS = {
@@ -26976,36 +27122,46 @@ var WorkflowEvalError = class extends Error {
26976
27122
  detail;
26977
27123
  };
26978
27124
  var WorkflowTimeBudgetError = class extends Error {
26979
- constructor(subject, budgetMs) {
26980
- super(`${subject} exceeded its time budget of ${budgetMs / 1e3}s.`);
27125
+ /** `waitedFor`: what the run's deadline passed waiting on, when that was not the run's own work. */
27126
+ constructor(subject, budgetMs, waitedFor) {
27127
+ super(`${subject} exceeded its time budget of ${budgetMs / 1e3}s${waitedFor === void 0 ? "" : ` waiting for ${waitedFor}`}.`);
26981
27128
  this.name = "WorkflowTimeBudgetError";
26982
27129
  }
26983
27130
  };
26984
27131
  var WorkflowExpressionBudgetError = class extends Error {
26985
- constructor(exceeded) {
26986
- super(
26987
- exceeded === "nodes" ? `Expression evaluated more than ${MAX_EVALUATION_NODES} nodes. A callback nested in another runs once per pair of elements.` : `The run built more than ${MAX_RUN_BUILT_BYTES / 1024 / 1024}MB of arrays, objects and text. Build long text with join() rather than a fold of +.`
26988
- );
26989
- this.exceeded = exceeded;
27132
+ constructor(budget) {
27133
+ super(budgetMessage(budget));
27134
+ this.budget = budget;
26990
27135
  this.name = "WorkflowExpressionBudgetError";
26991
27136
  }
26992
- exceeded;
27137
+ budget;
26993
27138
  };
26994
- var MAX_EVALUATION_NODES = 2e7;
27139
+ function budgetMessage(budget) {
27140
+ switch (budget.exceeded) {
27141
+ case "bytes":
27142
+ return `The run built more than ${MAX_RUN_BUILT_BYTES / 1024 / 1024}MB of arrays, objects and text.`;
27143
+ case "heap":
27144
+ return `The expression used more than ${budget.heap_mb}MB of memory.`;
27145
+ case "compute":
27146
+ return `The expression computed for more than ${budget.compute_ms / 1e3}s.`;
27147
+ }
27148
+ }
26995
27149
  var SLOT_BYTES = 8;
26996
27150
  var CHAR_BYTES = 2;
27151
+ var OBJECT_BYTES = 56;
26997
27152
  var EVALUATION_SLICE_MS = 20;
26998
27153
  var turn = {
26999
27154
  slice_started_ms: performance.now(),
27000
27155
  pause: null
27001
27156
  };
27002
- function startRunBudget(subject, budgetMs) {
27003
- return { deadline_ms: Date.now() + budgetMs, budget_ms: budgetMs, subject, built_bytes: 0 };
27157
+ function startRunBudget(subject, budgetMs, path) {
27158
+ return { host: "shared", deadline_ms: Date.now() + budgetMs, budget_ms: budgetMs, subject, path, built_bytes: 0 };
27004
27159
  }
27005
27160
  function runClock(run2) {
27006
27161
  if (Date.now() > run2.deadline_ms) {
27007
27162
  throw new WorkflowTimeBudgetError(run2.subject, run2.budget_ms);
27008
27163
  }
27164
+ if (run2.host === "isolated") return null;
27009
27165
  if (turn.pause === null && performance.now() - turn.slice_started_ms >= EVALUATION_SLICE_MS) {
27010
27166
  turn.pause = new Promise((resolve) => setTimeout(resolve, 0)).then(() => {
27011
27167
  turn.pause = null;
@@ -27015,15 +27171,16 @@ function runClock(run2) {
27015
27171
  return turn.pause;
27016
27172
  }
27017
27173
  function chargeBuilt(run2, bytes) {
27174
+ if (run2.host === "isolated") return;
27018
27175
  run2.built_bytes += bytes;
27019
27176
  if (run2.built_bytes > MAX_RUN_BUILT_BYTES) {
27020
- throw new WorkflowExpressionBudgetError("bytes");
27177
+ throw new WorkflowExpressionBudgetError({ exceeded: "bytes" });
27021
27178
  }
27022
27179
  }
27023
27180
  function builtSize(value) {
27024
27181
  if (typeof value === "string") return value.length * CHAR_BYTES;
27025
- if (Array.isArray(value)) return value.length * SLOT_BYTES;
27026
- if (typeof value === "object" && value !== null) return Object.keys(value).length * SLOT_BYTES;
27182
+ if (Array.isArray(value)) return OBJECT_BYTES + value.length * SLOT_BYTES;
27183
+ if (typeof value === "object" && value !== null) return OBJECT_BYTES + Object.keys(value).length * SLOT_BYTES;
27027
27184
  return 0;
27028
27185
  }
27029
27186
  var PARSED_NODE_BYTES = 40;
@@ -27053,13 +27210,13 @@ function callbackWorkers() {
27053
27210
  };
27054
27211
  }
27055
27212
  function evaluateWorkflowExpression(expr2, ctx) {
27056
- const budget = { nodes: 0, workers: callbackWorkers(), settled: false };
27213
+ const budget = { workers: callbackWorkers(), settled: false };
27057
27214
  return evaluateExpression(expr2, { ...ctx, evaluation_budget: budget }).finally(() => {
27058
27215
  budget.settled = true;
27059
27216
  });
27060
27217
  }
27061
27218
  async function evaluateExpression(expr2, ctx) {
27062
- for (let pause = spendNode(ctx); pause !== null; pause = checkClock(ctx)) await pause;
27219
+ for (let pause = checkClock(ctx); pause !== null; pause = checkClock(ctx)) await pause;
27063
27220
  switch (expr2.kind) {
27064
27221
  case "lit":
27065
27222
  return expr2.value;
@@ -27096,15 +27253,6 @@ async function evaluateExpression(expr2, ctx) {
27096
27253
  return assertNever(expr2);
27097
27254
  }
27098
27255
  }
27099
- function spendNode(ctx) {
27100
- const budget = ctx.evaluation_budget;
27101
- assertUnsettled(budget);
27102
- budget.nodes += 1;
27103
- if (budget.nodes > MAX_EVALUATION_NODES) {
27104
- throw new WorkflowExpressionBudgetError("nodes");
27105
- }
27106
- return runClock(ctx.run_budget);
27107
- }
27108
27256
  function checkClock(ctx) {
27109
27257
  assertUnsettled(ctx.evaluation_budget);
27110
27258
  return runClock(ctx.run_budget);
@@ -28096,9 +28244,6 @@ function clientRuntime(input) {
28096
28244
  },
28097
28245
  get change_origin() {
28098
28246
  return unread("change_origin");
28099
- },
28100
- get execution_principal() {
28101
- return unread("execution_principal");
28102
28247
  }
28103
28248
  };
28104
28249
  }
@@ -28131,7 +28276,7 @@ async function predictWorkflowEffects(input) {
28131
28276
  trigger_table: null,
28132
28277
  member_lookup: async () => unread("member display"),
28133
28278
  lexical_bindings: [{}],
28134
- run_budget: startRunBudget("Write prediction", PREDICTION_BUDGET_MS)
28279
+ run_budget: startRunBudget("Write prediction", PREDICTION_BUDGET_MS, "write")
28135
28280
  };
28136
28281
  const walk = { ctx, effects: [], written, input, unsure: false, aligned: true };
28137
28282
  try {
package/docs/mutations.md CHANGED
@@ -397,9 +397,8 @@ Why a full-form snapshot save is a bug, not a style choice — three independent
397
397
  moment *any* field is touched — there is no value-equality pass (deep equality over stored
398
398
  shapes would be a silent lock bypass). A snapshot touches every field, so it turns every
399
399
  save against a locked record into a total rejection even when nothing changed.
400
- 2. **`before_update` table workflows fire on the write.** Tables can carry `before_update`
401
- workflows (autofills, validations, side effects). They run on every write that touches the
402
- record — a snapshot save fires them for fields the user never edited.
400
+ 2. **A table's checks and `after_update` workflows run on the write.** They run on every
401
+ write that touches the record — a snapshot save runs them for fields the user never edited.
403
402
  3. **Snapshots clobber concurrent edits.** An update writes exactly the fields present in the
404
403
  payload; unsent fields keep their current value. A diff therefore coexists with a
405
404
  colleague's concurrent edit to a *different* field — a snapshot overwrites it with the
package/docs/workflows.md CHANGED
@@ -344,8 +344,8 @@ try {
344
344
 
345
345
  `e` is shaped `{ message, type, step_id?, detail? }`. Caught: tool errors and expression errors
346
346
  thrown inside the body. **Not caught:** `validate` failures — a check that throws included — and
347
- `return` (control flow), the run's own bounds (its time, its step count, and the node and 512 MB
348
- bounds in *Traps*), and errors from steps that run on *resume* after a `wait` inside the body —
347
+ `return` (control flow), the run's own bounds (its time, its step count, and the compute and
348
+ memory bounds in *Traps*), and errors from steps that run on *resume* after a `wait` inside the body —
349
349
  the wrapper spans one execution pass. There is no `finally`: put always-run steps after the `try`,
350
350
  always-on-failure steps in the `catch`.
351
351
 
@@ -611,7 +611,7 @@ key preserves. So passing a possibly-null read straight through is safe. Use
611
611
  `set_skip_null` is what turns a bag of optional workflow inputs into a diff-write without a guard
612
612
  per field; naming the same field in both `set` and `set_skip_null` is an error. A call whose
613
613
  surfaces all resolve to nothing — an all-dropped `set_skip_null`, an op object with no entries —
614
- is a no-op: no records touched, no `before_update` hooks. So a conditionally-built op object needs
614
+ is a no-op: no records touched, no table check asked. So a conditionally-built op object needs
615
615
  no guard around the call. Why diffs and not snapshots, plus locked records and the
616
616
  `request_locked_record_change` path: [mutations](./mutations.md#diff-before-update--send-only-what-changed).
617
617
 
@@ -791,9 +791,6 @@ The rules that are easy to get wrong because the failing code looks correct.
791
791
  init/update included. Resume re-enters *after* the whole loop, so a wait inside would run
792
792
  iteration 0 and silently skip every remaining iteration and its mutations. Move the wait out,
793
793
  or split the loop into separately triggered runs.
794
- - **Waits and `agent` steps are disallowed in `before_*` table workflows.** Those run inside the
795
- mutation pipeline and must complete synchronously; an LLM loop cannot return an accept/reject
796
- verdict in time.
797
794
  - **String contexts coerce to display text.** A read inside a template or a string-typed argument
798
795
  renders the *label*; everywhere else it is the raw key. So
799
796
  `` `Status: ${record["fld_status"]}` `` prints "Done" while
@@ -827,11 +824,14 @@ The rules that are easy to get wrong because the failing code looks correct.
827
824
  takes an array of at most 100,000. `join`, `replaceAll`, `padStart`, `padEnd`, `toString`,
828
825
  `toJson` and a template string write at most 10,000,000 characters; `randomNumber` /
829
826
  `randomAlphaNumeric` take a length of at most 1,000. Past one of those the call throws an
830
- expression error, which `try`/`catch` catches, and so does a step whose result would take what
831
- the run keeps past 67,108,864 characters of JSON. One evaluation runs at most 20,000,000 nodes —
832
- a callback nested in another runs once per pair of elements — and the run's expressions build at
833
- most 512 MB of arrays, objects and text in all, every temporary counted: past either, the run
834
- fails whatever catches it. Build long text with `join`, never a fold of `+`.
827
+ expression error, which `try`/`catch` catches, and so does an expression whose value would write
828
+ more than 67,108,864 characters of JSON, or a step whose result would take what the run keeps
829
+ past that. One evaluation computes for at most 6 seconds — waiting on a linked row, a member's
830
+ name or `current_member_in_any_group` does not count — and uses at most 256 MB of memory (512 MB
831
+ on a private instance with 4 GB or more). The values a run's expressions answer come to at most
832
+ 512 MB in all, counted each time one is answered, a text at every place the value holds it.
833
+ Past any of these, the run fails whatever catches it. A callback nested in another runs once
834
+ per pair of elements.
835
835
 
836
836
  ## The bright line
837
837
 
@@ -860,8 +860,7 @@ genuinely recursive.
860
860
  `set_app_workflow` with `verify_only: true` gives the verdict a save would, and writes nothing. The
861
861
  server runs every check a save runs — the subset parse, the type pass against the alias's
862
862
  declared inputs, tool and kwarg resolution, field, option, table and group resolution, `switch`
863
- case validation, the literal `formatDate` probe, waits in loops, the `before_*` restrictions,
864
- table reach, the `field_key` rule and the published-API guard. **Green means the save will take
863
+ case validation, the literal `formatDate` probe, waits in loops, table reach, the `field_key` rule and the published-API guard. **Green means the save will take
865
864
  it, red means real** — do not push through a red check. Lint comes back as warnings, which do not
866
865
  fail it.
867
866
 
@@ -900,7 +899,7 @@ check's message joined with `"; "`, and one `field_errors` entry per failing che
900
899
  iterations of a `while` / `do_while` / `c_for`, 10 000 steps, 50 live reads, and 20 seconds — and
901
900
  tripping any of them appends an entry naming the bound and ending "the plan below is incomplete",
902
901
  then stops. It also stops, with an entry saying why, where the live run would fail: at a
903
- `validate` check that throws, an evaluation past the run's node or 512 MB bound (*Traps*), or a
902
+ `validate` check that throws, an evaluation past the run's compute or memory bounds (*Traps*), or a
904
903
  return too long to write.
905
904
  So read `evaluation_errors` before reading `planned_calls`: an entry there may be a
906
905
  truncated plan rather than a bug in your body. A `foreach` is deliberately NOT capped at 1 000 —
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/app-sdk",
3
- "version": "0.114.0",
3
+ "version": "0.115.0",
4
4
  "description": "The SDK a Lotics custom-code app reads and writes through \u2014 typed hooks over the host bridge, cell readers, mount() and AppRouter",
5
5
  "type": "module",
6
6
  "exports": {