{
  "version": 3,
  "sources": ["../../../../node_modules/@sniptt/guards/lib/guards/primitives.ts", "../../../../node_modules/@sniptt/guards/lib/guards/structural.ts", "../../../../node_modules/@sniptt/guards/lib/guards/convenience.ts", "../../../../node_modules/@sniptt/guards/lib/index.ts", "../../../../src/logic-functions/unsuppress-lead.logic-function.ts", "twenty-sdk-define-stub:__twenty-sdk-define-stub__", "../../../../node_modules/twenty-shared/dist/is-record-object-schema-CwzshFdt.mjs", "../../../../node_modules/twenty-shared/dist/logic-function.mjs", "../../../../node_modules/twenty-sdk/src/sdk/logic-function/connections/errors/app-connection-auth-failed.error.ts", "../../../../node_modules/twenty-shared/dist/FieldMetadataType-PppCGM82.mjs", "../../../../node_modules/twenty-shared/dist/get-system-view-universal-identifier.util-CJoglbKX.mjs", "../../../../node_modules/twenty-shared/dist/application.mjs", "../../../../node_modules/twenty-sdk/src/sdk/logic-function/utils/post-graphql-request.util.ts", "../../../../node_modules/twenty-sdk/src/sdk/logic-function/connections/get-connection.ts", "../../../../node_modules/twenty-sdk/src/sdk/logic-function/connections/list-connections.ts", "../../../../node_modules/twenty-sdk/src/sdk/logic-function/connections/find-connection-for-request.ts", "../../../../node_modules/twenty-sdk/src/sdk/logic-function/agents/run-agent.ts", "../../../../node_modules/twenty-sdk/src/sdk/logic-function/jobs/enqueue-job.ts", "../../../../node_modules/twenty-sdk/src/sdk/logic-function/key-value/kv.ts", "../../../../node_modules/twenty-sdk/src/sdk/logic-function/response.ts", "../../../../src/constants/gate-queue-identifiers.ts", "../../../../src/logic-functions/actor-identity.ts", "../../../../src/identity/actor.ts", "../../../../src/logic-functions/greenlight-api.ts", "../../../../src/gate/release-decision.ts", "../../../../src/gate/suppression-decision.ts", "../../../../src/logic-functions/suppression-run.ts"],
  "sourcesContent": [null, null, null, null, "import { CoreApiClient } from 'twenty-client-sdk/core';\nimport { defineLogicFunction } from 'twenty-sdk/define';\nimport { Response, type RoutePayload } from 'twenty-sdk/logic-function';\n\nimport {\n  UNSUPPRESS_LEAD_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER,\n  UNSUPPRESS_LEAD_ROUTE_PATH,\n} from 'src/constants/gate-queue-identifiers';\nimport { resolveActorDisplayName } from 'src/logic-functions/actor-identity';\nimport { runUnsuppressLead } from 'src/logic-functions/suppression-run';\n\n/**\n * \"Allow contact again\" \u2014 lift a suppression.\n *\n * The more sensitive of the two block-list actions, because it re-enables\n * contact with somebody who is on record asking us to stop. Three things make it\n * heavier than its counterpart, and all three are enforced server-side in\n * `parseUnsuppressRequest` rather than in the panel, because a client-side guard\n * on a compliance action is decoration:\n *\n *   1. A reason from a **separate** closed list, every entry of which is a claim\n *      that the suppression itself was wrong or has been superseded.\n *   2. A **mandatory** free-text note. This is the opposite of the release\n *      override's policy and the asymmetry is the point: a daily action collects\n *      the word \"ok\" in a required text box, a rare and consequential one\n *      collects the actual reason.\n *   3. An explicit acknowledgement that contact is being re-enabled.\n *\n * The audit row records the reason the person was suppressed *under*, not only\n * the reason given for lifting it \u2014 \"which legal-request blocks has this\n * workspace lifted, and who lifted them\" is the question the trail exists to\n * answer, and it is unanswerable from the removal reason alone.\n *\n * Note what this action is **not**: it is not a route around the compliance gate.\n * A blocked lead still cannot be released by hand (`ALLOW_COMPLIANCE_OVERRIDE`\n * in `src/gate/release-decision.ts`). This clears the underlying *data*, and the\n * lead then re-scores and clears itself through evidence \u2014 which is exactly the\n * escape hatch that file describes as the only legitimate one.\n *\n * ## Identity\n *\n * \"Who lifted them\" is half of the question above, and until now the row\n * answered it with `Workspace member <uuid>` \u2014 which is to say it did not answer\n * it. `resolveActorDisplayName` resolves the name from the request's own\n * authenticated identity, never from its body; a workspace whose lifted\n * legal-request blocks all name the same unreadable id has a list, not a trail.\n */\nconst handler = async (event: RoutePayload) => {\n  const result = await runUnsuppressLead({\n    client: new CoreApiClient(),\n    body: event.body,\n    now: new Date(),\n    actorDisplayName: await resolveActorDisplayName(event),\n  });\n\n  return new Response(result.body, { status: result.status });\n};\n\nexport default defineLogicFunction({\n  universalIdentifier: UNSUPPRESS_LEAD_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER,\n  name: 'greenlight-unsuppress-lead',\n  description:\n    'Removes a person from the Greenlight do-not-contact list. Requires a reason, a written note and an explicit acknowledgement, and appends the removal to the audit log.',\n  timeoutSeconds: 15,\n  handler,\n  httpRouteTriggerSettings: {\n    path: UNSUPPRESS_LEAD_ROUTE_PATH,\n    httpMethod: 'POST',\n    isAuthRequired: true,\n  },\n});\n", "\n// Auto-generated stub for twenty-sdk/define injected by the SDK CLI build.\n// Real implementations would pull in zod, twenty-shared and ~1MB of code; at\n// runtime only `default.config.handler` is consumed, so tiny no-ops suffice.\nconst __defineFactoryStub = (config) => ({\n  success: true,\n  config,\n  errors: [],\n});\n\nconst __anyHandler = {\n  get(_target, prop) {\n    if (prop === '__esModule') return true;\n    if (prop === Symbol.toPrimitive) return () => '';\n    if (typeof prop === 'symbol') return undefined;\n    return new Proxy(() => undefined, __anyHandler);\n  },\n  apply() {\n    return new Proxy(() => undefined, __anyHandler);\n  },\n};\nconst __anyStub = new Proxy(() => undefined, __anyHandler);\n\nexport const createValidationResult = __defineFactoryStub;\nexport const defineAgent = __defineFactoryStub;\nexport const defineApplication = __defineFactoryStub;\nexport const defineApplicationRole = __defineFactoryStub;\nexport const defineCommandMenuItem = __defineFactoryStub;\nexport const defineConnectionProvider = __defineFactoryStub;\nexport const defineField = __defineFactoryStub;\nexport const defineFrontComponent = __defineFactoryStub;\nexport const defineIndex = __defineFactoryStub;\nexport const defineLogicFunction = __defineFactoryStub;\nexport const defineNavigationMenuItem = __defineFactoryStub;\nexport const defineObject = __defineFactoryStub;\nexport const definePageLayout = __defineFactoryStub;\nexport const definePageLayoutTab = __defineFactoryStub;\nexport const definePermissionFlag = __defineFactoryStub;\nexport const definePostInstallLogicFunction = __defineFactoryStub;\nexport const definePreInstallLogicFunction = __defineFactoryStub;\nexport const defineRole = __defineFactoryStub;\nexport const defineSettingsFrontComponent = __defineFactoryStub;\nexport const defineSkill = __defineFactoryStub;\nexport const defineUninstallLogicFunction = __defineFactoryStub;\nexport const defineView = __defineFactoryStub;\nexport const defineViewField = __defineFactoryStub;\nexport const AggregateOperations = __anyStub;\nexport const DateDisplayFormat = __anyStub;\nexport const FieldMetadataSettingsOnClickAction = __anyStub;\nexport const FieldType = __anyStub;\nexport const HTTPMethod = __anyStub;\nexport const NavigationMenuItemType = __anyStub;\nexport const NumberDataType = __anyStub;\nexport const ObjectRecordGroupByDateGranularity = __anyStub;\nexport const OnDeleteAction = __anyStub;\nexport const PageLayoutTabLayoutMode = __anyStub;\nexport const PageLayoutType = __anyStub;\nexport const RelationType = __anyStub;\nexport const RowLevelPermissionPredicateGroupLogicalOperator = __anyStub;\nexport const RowLevelPermissionPredicateOperand = __anyStub;\nexport const STANDARD_OBJECT = __anyStub;\nexport const STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS = __anyStub;\nexport const STANDARD_PAGE_LAYOUT = __anyStub;\nexport const STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS = __anyStub;\nexport const SystemPermissionFlag = __anyStub;\nexport const ViewCalendarLayout = __anyStub;\nexport const ViewFilterGroupLogicalOperator = __anyStub;\nexport const ViewFilterOperand = __anyStub;\nexport const ViewKey = __anyStub;\nexport const ViewOpenRecordIn = __anyStub;\nexport const ViewSortDirection = __anyStub;\nexport const ViewType = __anyStub;\nexport const ViewVisibility = __anyStub;\nexport const canAccessFullAdminPanel = __anyStub;\nexport const canImpersonate = __anyStub;\nexport const every = __anyStub;\nexport const everyDefined = __anyStub;\nexport const everyEquals = __anyStub;\nexport const favoriteRecordIds = __anyStub;\nexport const featureFlags = __anyStub;\nexport const getFieldUniversalIdentifier = __anyStub;\nexport const getSystemRelationFieldUniversalIdentifier = __anyStub;\nexport const getSystemViewFieldUniversalIdentifier = __anyStub;\nexport const getSystemViewUniversalIdentifier = __anyStub;\nexport const hasAnySoftDeleteFilterOnView = __anyStub;\nexport const includes = __anyStub;\nexport const includesEvery = __anyStub;\nexport const isDashboardPageLayoutInEditMode = __anyStub;\nexport const isDefined = __anyStub;\nexport const isInSidePanel = __anyStub;\nexport const isLayoutCustomizationModeEnabled = __anyStub;\nexport const isNonEmptyString = __anyStub;\nexport const isSelectAll = __anyStub;\nexport const none = __anyStub;\nexport const noneDefined = __anyStub;\nexport const noneEquals = __anyStub;\nexport const numberOfSelectedRecords = __anyStub;\nexport const objectMetadataItem = __anyStub;\nexport const objectMetadataLabel = __anyStub;\nexport const objectPermissions = __anyStub;\nexport const pageType = __anyStub;\nexport const selectedRecords = __anyStub;\nexport const some = __anyStub;\nexport const someDefined = __anyStub;\nexport const someEquals = __anyStub;\nexport const someNonEmptyString = __anyStub;\nexport const targetObjectReadPermissions = __anyStub;\nexport const targetObjectWritePermissions = __anyStub;\nexport const validateFields = __anyStub;\n", "import { isNonEmptyString as e } from \"@sniptt/guards\";\n//#region src/logic-function/is-record-object-schema.ts\nvar t = (t) => (t?.type === \"record\" || t?.type === \"object\") && e(t.objectUniversalIdentifier);\n//#endregion\nexport { t };\n", "import { t as e } from \"./isDefined-Dtu5EYqP.mjs\";\nimport { t } from \"./is-record-object-schema-CwzshFdt.mjs\";\nimport { isNonEmptyString as n, isObject as r } from \"@sniptt/guards\";\n//#region src/logic-function/build-tool-input-json-schema.ts\nvar i = (e, t) => {\n\tlet r = n(e) ? t?.(e) : void 0;\n\treturn `Id of the ${n(r) ? r : \"linked\"} record`;\n}, a = (n, o) => {\n\tif (t(n)) return {\n\t\ttype: \"string\",\n\t\tdescription: i(n.objectUniversalIdentifier, o)\n\t};\n\tif (n.type === \"records\") return {\n\t\ttype: \"array\",\n\t\titems: {\n\t\t\ttype: \"string\",\n\t\t\tdescription: i(n.objectUniversalIdentifier, o)\n\t\t}\n\t};\n\tlet { objectUniversalIdentifier: s, multiline: c, label: l, items: u, properties: d, additionalProperties: f, ...p } = n, m = { ...p };\n\treturn e(u) && (m.items = a(u, o)), e(d) && (m.properties = Object.fromEntries(Object.entries(d).map(([e, t]) => [e, a(t, o)]))), e(f) && (m.additionalProperties = r(f) ? a(f, o) : f), m;\n}, o = {\n\ttype: \"object\",\n\tproperties: {}\n}, s = (e) => {\n\tlet t = { type: \"unknown\" };\n\tswitch (e.type) {\n\t\tcase \"string\":\n\t\t\tt.type = \"string\";\n\t\t\tbreak;\n\t\tcase \"number\":\n\t\tcase \"integer\":\n\t\t\tt.type = \"number\";\n\t\t\tbreak;\n\t\tcase \"boolean\":\n\t\t\tt.type = \"boolean\";\n\t\t\tbreak;\n\t\tcase \"array\":\n\t\t\tt.type = \"array\", e.items && (t.items = s(e.items));\n\t\t\tbreak;\n\t\tcase \"object\":\n\t\t\tt.type = \"object\", e.properties && (t.properties = Object.fromEntries(Object.entries(e.properties).map(([e, t]) => [e, s(t)])));\n\t\t\tbreak;\n\t\tcase \"record\":\n\t\t\tt.type = \"record\";\n\t\t\tbreak;\n\t\tcase \"records\":\n\t\t\tt.type = \"records\";\n\t\t\tbreak;\n\t\tdefault: t.type = \"unknown\";\n\t}\n\treturn Array.isArray(e.enum) && (t.enum = e.enum.filter((e) => typeof e == \"string\")), e.multiline === !0 && (t.multiline = !0), n(e.label) && (t.label = e.label), n(e.objectUniversalIdentifier) && (t.objectUniversalIdentifier = e.objectUniversalIdentifier), t;\n}, c = (e) => [s(e)], l = { inputSchema: c({\n\ttype: \"object\",\n\tproperties: {\n\t\ta: { type: \"string\" },\n\t\tb: { type: \"number\" }\n\t}\n}) }, u = async (t) => {\n\tlet { getFunctionInputSchema: n } = await import(\"./get-function-input-schema-GNk3NRLJ.mjs\"), r = n(t)[0];\n\treturn r?.type === \"object\" && e(r.properties) ? {\n\t\ttype: \"object\",\n\t\tproperties: r.properties\n\t} : o;\n}, d = (t) => !e(t) || t === null ? \"unknown\" : typeof t == \"string\" ? \"string\" : typeof t == \"number\" ? \"number\" : typeof t == \"boolean\" ? \"boolean\" : Array.isArray(t) ? \"array\" : \"unknown\", f = (e) => e ? Object.entries(e).reduce((e, [t, n]) => (r(n) && !Array.isArray(n) ? e[t] = {\n\tisLeaf: !1,\n\ttype: \"object\",\n\tlabel: t,\n\tvalue: f(n)\n} : e[t] = {\n\tisLeaf: !0,\n\tvalue: n,\n\ttype: d(n),\n\tlabel: t\n}, e), {}) : {}, p = (e, t) => e ? `${e}.${t}` : t, m = (t, n, r = \"\") => {\n\tlet i = [];\n\tfor (let [a, o] of Object.entries(n)) {\n\t\tlet n = p(r, a), s = t[a];\n\t\tif (!e(s)) {\n\t\t\ti.push(`Missing key \"${n}\" in declared output schema.`);\n\t\t\tcontinue;\n\t\t}\n\t\tif (o.isLeaf !== s.isLeaf) {\n\t\t\ti.push(`Type mismatch at \"${n}\": expected ${o.isLeaf ? o.type : \"object\"} but declared ${s.isLeaf ? s.type : \"object\"}.`);\n\t\t\tcontinue;\n\t\t}\n\t\tif (!o.isLeaf && !s.isLeaf) {\n\t\t\ti.push(...m(s.value, o.value, n));\n\t\t\tcontinue;\n\t\t}\n\t\to.isLeaf && s.isLeaf && o.type !== \"unknown\" && s.type !== \"unknown\" && o.type !== s.type && i.push(`Type mismatch at \"${n}\": expected ${o.type} but declared ${s.type}.`);\n\t}\n\treturn i;\n}, h = [\n\t\"string\",\n\t\"number\",\n\t\"boolean\",\n\t\"array\",\n\t\"unknown\"\n], g = (e) => h.includes(e), _ = (e, t) => {\n\tlet n = t.label ?? e;\n\treturn t.type === \"record\" ? {\n\t\tisLeaf: !0,\n\t\ttype: \"string\",\n\t\tlabel: n,\n\t\tvalue: null\n\t} : t.type === \"records\" ? {\n\t\tisLeaf: !0,\n\t\ttype: \"array\",\n\t\tlabel: n,\n\t\tvalue: null\n\t} : t.type === \"object\" ? {\n\t\tisLeaf: !1,\n\t\ttype: \"object\",\n\t\tlabel: n,\n\t\tvalue: r(t.properties) ? v(t.properties) : {}\n\t} : {\n\t\tisLeaf: !0,\n\t\ttype: g(t.type) ? t.type : \"unknown\",\n\t\tlabel: n,\n\t\tvalue: null\n\t};\n}, v = (e) => Object.entries(e).reduce((e, [t, n]) => (e[t] = _(t, n), e), {}), y = (e) => {\n\tlet t = e[0];\n\treturn t?.type !== \"object\" || !r(t.properties) ? {} : v(t.properties);\n}, b = (e) => e?.type === \"records\" && n(e.objectUniversalIdentifier) || e?.type === \"array\" && t(e?.items);\n//#endregion\nexport { o as DEFAULT_TOOL_INPUT_SCHEMA, l as SEED_WORKFLOW_ACTION_TRIGGER_SETTINGS, a as buildToolInputJsonSchema, u as getInputSchemaFromSourceCode, f as getOutputSchemaFromValue, m as getOutputSchemaMismatchIssues, y as inputSchemaToOutputSchema, b as isRecordArraySchema, t as isRecordObjectSchema, c as jsonSchemaToInputSchema };\n", "// Thrown when the platform asks the SDK to operate on a connection whose\n// OAuth refresh failed permanently (`authFailedAt` is set). The end user\n// must reconnect from the app's settings tab — the app cannot recover on\n// its own.\n//\n// `listConnections` filters these out by default (the user can't act on\n// them anyway). `getConnection` throws this when the looked-up connection\n// is in this state, so a stored connection id can be safely retried until\n// it works again.\nexport class AppConnectionAuthFailedError extends Error {\n  readonly connectionId: string;\n\n  constructor(connectionId: string) {\n    super(\n      `App connection ${connectionId} requires the user to reconnect ` +\n        `(authFailedAt is set). Surface a \"Reconnect\" prompt in your UI.`,\n    );\n    this.name = 'AppConnectionAuthFailedError';\n    this.connectionId = connectionId;\n  }\n}\n", "//#region src/types/FieldMetadataType.ts\nvar e = /* @__PURE__ */ function(e) {\n\treturn e.ACTOR = \"ACTOR\", e.ADDRESS = \"ADDRESS\", e.ARRAY = \"ARRAY\", e.BOOLEAN = \"BOOLEAN\", e.CURRENCY = \"CURRENCY\", e.DATE = \"DATE\", e.DATE_TIME = \"DATE_TIME\", e.EMAILS = \"EMAILS\", e.FILES = \"FILES\", e.FULL_NAME = \"FULL_NAME\", e.LINKS = \"LINKS\", e.MORPH_RELATION = \"MORPH_RELATION\", e.MULTI_SELECT = \"MULTI_SELECT\", e.NUMBER = \"NUMBER\", e.NUMERIC = \"NUMERIC\", e.PHONES = \"PHONES\", e.POSITION = \"POSITION\", e.RATING = \"RATING\", e.RAW_JSON = \"RAW_JSON\", e.RELATION = \"RELATION\", e.RICH_TEXT = \"RICH_TEXT\", e.SELECT = \"SELECT\", e.TEXT = \"TEXT\", e.TS_VECTOR = \"TS_VECTOR\", e.UUID = \"UUID\", e;\n}({});\n//#endregion\nexport { e as t };\n", "import { v5 as e } from \"uuid\";\n//#region src/application/constants/TwentyStandardApplicationUniversalIdentifier.ts\nvar t = \"20202020-64aa-4b6f-b003-9c74b97cee20\", n = ({ entityNamespace: t, value: n, applicationUniversalIdentifier: r }) => e(`${t}:${n}`, r), r = ({ applicationUniversalIdentifier: e, objectUniversalIdentifier: t, name: r }) => n({\n\tentityNamespace: \"fieldMetadata\",\n\tvalue: `${t}:${r}`,\n\tapplicationUniversalIdentifier: e\n}), i = ({ applicationUniversalIdentifier: e, objectUniversalIdentifier: t, relationTargetObjectUniversalIdentifier: r }) => n({\n\tentityNamespace: \"fieldMetadata\",\n\tvalue: `${t}:systemRelation:${r}`,\n\tapplicationUniversalIdentifier: e\n}), a = ({ fieldMetadataApplicationUniversalIdentifier: e, viewUniversalIdentifier: t, fieldMetadataUniversalIdentifier: r }) => n({\n\tentityNamespace: \"viewField\",\n\tvalue: `${t}:${r}`,\n\tapplicationUniversalIdentifier: e\n}), o = ({ objectMetadataApplicationUniversalIdentifier: e, objectUniversalIdentifier: t, viewKey: r }) => n({\n\tentityNamespace: \"view\",\n\tvalue: `${t}:${r}`,\n\tapplicationUniversalIdentifier: e\n});\n//#endregion\nexport { n as a, r as i, a as n, t as o, i as r, o as t };\n", "import { t as e } from \"./FieldMetadataType-PppCGM82.mjs\";\nimport { a as t, i as n, n as r, o as i, r as a, t as o } from \"./get-system-view-universal-identifier.util-CJoglbKX.mjs\";\n//#region src/application/applicationCategoryType.ts\nvar s = [\n\t\"Communication\",\n\t\"Productivity\",\n\t\"Product management\",\n\t\"Sales\",\n\t\"Marketing\",\n\t\"Enrichment\",\n\t\"Data\",\n\t\"Search\",\n\t\"Other\"\n], c = (e) => s.includes(e), l = [\n\te.TEXT,\n\te.ARRAY,\n\te.BOOLEAN,\n\te.DATE,\n\te.DATE_TIME,\n\te.NUMBER,\n\te.NUMERIC,\n\te.RAW_JSON,\n\te.RICH_TEXT,\n\te.SELECT,\n\te.MULTI_SELECT\n], u = \"public\", d = \"TWENTY_API_KEY\", f = \"TWENTY_API_URL\", p = \"TWENTY_APP_ACCESS_TOKEN\", m = \"TWENTY_FUNCTIONS_URL\", h = \"generated\", g = { js: \"import { createRequire as __createRequire } from 'module';\\nconst require = __createRequire(import.meta.url);\" }, _ = \".twenty/output\", v = \"Standard\", y = ({ applicationUniversalIdentifier: e, name: n }) => t({\n\tentityNamespace: \"agent\",\n\tvalue: n,\n\tapplicationUniversalIdentifier: e\n}), b = ({ applicationUniversalIdentifier: e, key: n }) => t({\n\tentityNamespace: \"applicationVariable\",\n\tvalue: n,\n\tapplicationUniversalIdentifier: e\n}), x = \"navigation\", S = ({ applicationUniversalIdentifier: e, engineComponentKey: n }) => t({\n\tentityNamespace: \"commandMenuItem\",\n\tvalue: `GLOBAL:${n}`,\n\tapplicationUniversalIdentifier: e\n}), C = ({ applicationUniversalIdentifier: e, engineComponentKey: n }) => t({\n\tentityNamespace: \"commandMenuItem\",\n\tvalue: `GLOBAL_OBJECT_CONTEXT:${n}`,\n\tapplicationUniversalIdentifier: e\n}), w = ({ applicationUniversalIdentifier: e, engineComponentKey: n, objectUniversalIdentifier: r }) => t({\n\tentityNamespace: \"commandMenuItem\",\n\tvalue: `RECORD_SELECTION:${n}:${r ?? \"\"}`,\n\tapplicationUniversalIdentifier: e\n}), T = ({ applicationUniversalIdentifier: e, objectUniversalIdentifier: n }) => t({\n\tentityNamespace: \"commandMenuItem\",\n\tvalue: `${n}:${x}`,\n\tapplicationUniversalIdentifier: e\n}), E = ({ applicationUniversalIdentifier: e, name: n }) => t({\n\tentityNamespace: \"connectionProvider\",\n\tvalue: n,\n\tapplicationUniversalIdentifier: e\n}), D = ({ applicationUniversalIdentifier: e, roleUniversalIdentifier: n, fieldUniversalIdentifier: r }) => t({\n\tentityNamespace: \"fieldPermission\",\n\tvalue: `${n}:${r}`,\n\tapplicationUniversalIdentifier: e\n}), O = ({ applicationUniversalIdentifier: e, pageLayoutWidgetUniversalIdentifier: n }) => t({\n\tentityNamespace: \"view\",\n\tvalue: n,\n\tapplicationUniversalIdentifier: e\n}), k = ({ applicationUniversalIdentifier: e, componentName: n }) => t({\n\tentityNamespace: \"frontComponent\",\n\tvalue: n,\n\tapplicationUniversalIdentifier: e\n}), A = ({ applicationUniversalIdentifier: e, objectUniversalIdentifier: n, name: r }) => t({\n\tentityNamespace: \"index\",\n\tvalue: `${n}:${r}`,\n\tapplicationUniversalIdentifier: e\n}), j = ({ applicationUniversalIdentifier: e, name: n }) => t({\n\tentityNamespace: \"logicFunction\",\n\tvalue: n,\n\tapplicationUniversalIdentifier: e\n}), ee = ({ applicationUniversalIdentifier: e, name: n }) => t({\n\tentityNamespace: \"navigationMenuItem\",\n\tvalue: `FOLDER:${n}`,\n\tapplicationUniversalIdentifier: e\n}), M = ({ applicationUniversalIdentifier: e, objectUniversalIdentifier: n }) => t({\n\tentityNamespace: \"navigationMenuItem\",\n\tvalue: `OBJECT:${n}`,\n\tapplicationUniversalIdentifier: e\n}), N = ({ applicationUniversalIdentifier: e, viewUniversalIdentifier: n }) => t({\n\tentityNamespace: \"navigationMenuItem\",\n\tvalue: `VIEW:${n}`,\n\tapplicationUniversalIdentifier: e\n}), P = ({ applicationUniversalIdentifier: e, link: n }) => t({\n\tentityNamespace: \"navigationMenuItem\",\n\tvalue: `LINK:${n}`,\n\tapplicationUniversalIdentifier: e\n}), F = ({ applicationUniversalIdentifier: e, roleUniversalIdentifier: n, objectUniversalIdentifier: r }) => t({\n\tentityNamespace: \"objectPermission\",\n\tvalue: `${n}:${r}`,\n\tapplicationUniversalIdentifier: e\n}), I = ({ applicationUniversalIdentifier: e, nameSingular: n }) => t({\n\tentityNamespace: \"objectMetadata\",\n\tvalue: n,\n\tapplicationUniversalIdentifier: e\n}), L = ({ applicationUniversalIdentifier: e, pageLayoutUniversalIdentifier: n, title: r }) => t({\n\tentityNamespace: \"pageLayoutTab\",\n\tvalue: `${n}:${r}`,\n\tapplicationUniversalIdentifier: e\n}), R = \"RECORD_PAGE\", z = ({ applicationUniversalIdentifier: e, objectUniversalIdentifier: n, name: r }) => t({\n\tentityNamespace: \"pageLayout\",\n\tvalue: n ? `${n}:${r}` : r,\n\tapplicationUniversalIdentifier: e\n}), B = ({ applicationUniversalIdentifier: e, objectUniversalIdentifier: n }) => t({\n\tentityNamespace: \"pageLayout\",\n\tvalue: `${n}:${R}`,\n\tapplicationUniversalIdentifier: e\n}), V = ({ applicationUniversalIdentifier: e, pageLayoutTabUniversalIdentifier: n, title: r }) => t({\n\tentityNamespace: \"pageLayoutWidget\",\n\tvalue: `${n}:${r}`,\n\tapplicationUniversalIdentifier: e\n}), H = ({ applicationUniversalIdentifier: e, key: n }) => t({\n\tentityNamespace: \"permissionFlag\",\n\tvalue: n,\n\tapplicationUniversalIdentifier: e\n}), U = ({ applicationUniversalIdentifier: e, roleUniversalIdentifier: n, permissionFlagUniversalIdentifier: r }) => t({\n\tentityNamespace: \"rolePermissionFlag\",\n\tvalue: `${n}:${r}`,\n\tapplicationUniversalIdentifier: e\n}), W = ({ applicationUniversalIdentifier: e, agentUniversalIdentifier: n }) => t({\n\tentityNamespace: \"roleTarget\",\n\tvalue: n,\n\tapplicationUniversalIdentifier: e\n}), G = ({ applicationUniversalIdentifier: e, label: n }) => t({\n\tentityNamespace: \"role\",\n\tvalue: n,\n\tapplicationUniversalIdentifier: e\n}), K = ({ applicationUniversalIdentifier: e, fieldMetadataUniversalIdentifier: n }) => t({\n\tentityNamespace: \"searchFieldMetadata\",\n\tvalue: n,\n\tapplicationUniversalIdentifier: e\n}), q = ({ applicationUniversalIdentifier: e, fieldUniversalIdentifier: n, value: r }) => t({\n\tentityNamespace: \"selectOption\",\n\tvalue: `${n}:${r}`,\n\tapplicationUniversalIdentifier: e\n}), J = ({ applicationUniversalIdentifier: e, name: n }) => t({\n\tentityNamespace: \"skill\",\n\tvalue: n,\n\tapplicationUniversalIdentifier: e\n}), Y = ({ applicationUniversalIdentifier: e, viewUniversalIdentifier: n, name: r }) => t({\n\tentityNamespace: \"viewFieldGroup\",\n\tvalue: `${n}:${r}`,\n\tapplicationUniversalIdentifier: e\n}), X = ({ applicationUniversalIdentifier: e, viewUniversalIdentifier: n, fieldMetadataUniversalIdentifier: r }) => t({\n\tentityNamespace: \"viewField\",\n\tvalue: `${n}:${r}`,\n\tapplicationUniversalIdentifier: e\n}), Z = ({ applicationUniversalIdentifier: e, viewUniversalIdentifier: n, fieldMetadataUniversalIdentifier: r, operand: i, subFieldName: a }) => t({\n\tentityNamespace: \"viewFilter\",\n\tvalue: `${n}:${r}:${i}:${a ?? \"\"}`,\n\tapplicationUniversalIdentifier: e\n}), Q = ({ applicationUniversalIdentifier: e, viewUniversalIdentifier: n, fieldValue: r }) => t({\n\tentityNamespace: \"viewGroup\",\n\tvalue: `${n}:${r}`,\n\tapplicationUniversalIdentifier: e\n}), $ = ({ applicationUniversalIdentifier: e, viewUniversalIdentifier: n, fieldMetadataUniversalIdentifier: r }) => t({\n\tentityNamespace: \"viewSort\",\n\tvalue: `${n}:${r}`,\n\tapplicationUniversalIdentifier: e\n}), te = ({ applicationUniversalIdentifier: e, objectUniversalIdentifier: n, name: r }) => t({\n\tentityNamespace: \"view\",\n\tvalue: `${n}:${r}`,\n\tapplicationUniversalIdentifier: e\n}), ne = /* @__PURE__ */ function(e) {\n\treturn e.Object = \"object\", e.Field = \"field\", e.LogicFunction = \"logicFunction\", e.FrontComponent = \"frontComponent\", e.Role = \"role\", e.Skill = \"skill\", e.Agent = \"agent\", e.ConnectionProvider = \"connectionProvider\", e.View = \"view\", e.ViewField = \"viewField\", e.NavigationMenuItem = \"navigationMenuItem\", e.PageLayout = \"pageLayout\", e.PageLayoutTab = \"pageLayoutTab\", e.CommandMenuItem = \"commandMenuItem\", e;\n}({}), re = (t, n = e.TEXT) => {\n\tif (t == null) return \"\";\n\tswitch (n) {\n\t\tcase e.BOOLEAN: return String(t) === \"true\" ? \"true\" : \"false\";\n\t\tcase e.NUMBER:\n\t\tcase e.NUMERIC: return String(t);\n\t\tcase e.ARRAY:\n\t\tcase e.MULTI_SELECT:\n\t\t\tif (Array.isArray(t)) return JSON.stringify(t);\n\t\t\tif (typeof t == \"string\") {\n\t\t\t\ttry {\n\t\t\t\t\tlet e = JSON.parse(t);\n\t\t\t\t\tif (Array.isArray(e)) return t;\n\t\t\t\t} catch {}\n\t\t\t\treturn JSON.stringify([t]);\n\t\t\t}\n\t\t\treturn JSON.stringify(t);\n\t\tcase e.RAW_JSON:\n\t\tcase e.RICH_TEXT: return typeof t == \"string\" ? t : JSON.stringify(t);\n\t\tdefault: return typeof t == \"string\" ? t : String(t);\n\t}\n}, ie = (t, n = e.TEXT) => {\n\tif (t === \"\") return n === e.ARRAY || n === e.MULTI_SELECT ? [] : \"\";\n\tswitch (n) {\n\t\tcase e.BOOLEAN: return t === \"true\";\n\t\tcase e.NUMBER:\n\t\tcase e.NUMERIC: {\n\t\t\tlet e = Number(t);\n\t\t\treturn Number.isNaN(e) ? t : e;\n\t\t}\n\t\tcase e.ARRAY:\n\t\tcase e.MULTI_SELECT: try {\n\t\t\tlet e = JSON.parse(t);\n\t\t\treturn Array.isArray(e) ? e : [];\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t\tcase e.RAW_JSON:\n\t\tcase e.RICH_TEXT: try {\n\t\t\treturn JSON.parse(t);\n\t\t} catch {\n\t\t\treturn t;\n\t\t}\n\t\tdefault: return t;\n\t}\n};\n//#endregion\nexport { s as APPLICATION_CATEGORIES, l as APPLICATION_VARIABLE_FIELD_METADATA_TYPES, u as ASSETS_DIR, d as DEFAULT_API_KEY_NAME, f as DEFAULT_API_URL_NAME, p as DEFAULT_APP_ACCESS_TOKEN_NAME, m as DEFAULT_FUNCTIONS_URL_NAME, h as GENERATED_DIR, g as NODE_ESM_CJS_BANNER, _ as OUTPUT_DIR, ne as SyncableEntity, v as TWENTY_STANDARD_APPLICATION_NAME, i as TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER, t as computeDeterministicUuid, ie as deserializeApplicationVariableValue, y as getAgentUniversalIdentifier, b as getApplicationVariableUniversalIdentifier, E as getConnectionProviderUniversalIdentifier, D as getFieldPermissionUniversalIdentifier, n as getFieldUniversalIdentifier, O as getFieldsWidgetViewUniversalIdentifier, ee as getFolderNavigationMenuItemUniversalIdentifier, k as getFrontComponentUniversalIdentifier, S as getGlobalCommandMenuItemUniversalIdentifier, C as getGlobalObjectContextCommandMenuItemUniversalIdentifier, A as getIndexUniversalIdentifier, P as getLinkNavigationMenuItemUniversalIdentifier, j as getLogicFunctionUniversalIdentifier, T as getNavigationCommandUniversalIdentifier, M as getObjectNavigationMenuItemUniversalIdentifier, F as getObjectPermissionUniversalIdentifier, I as getObjectUniversalIdentifier, L as getPageLayoutTabUniversalIdentifier, z as getPageLayoutUniversalIdentifier, V as getPageLayoutWidgetUniversalIdentifier, H as getPermissionFlagUniversalIdentifier, B as getRecordPageLayoutUniversalIdentifier, w as getRecordSelectionCommandMenuItemUniversalIdentifier, U as getRolePermissionFlagUniversalIdentifier, W as getRoleTargetUniversalIdentifier, G as getRoleUniversalIdentifier, K as getSearchFieldUniversalIdentifier, q as getSelectOptionUniversalIdentifier, J as getSkillUniversalIdentifier, a as getSystemRelationFieldUniversalIdentifier, r as getSystemViewFieldUniversalIdentifier, o as getSystemViewUniversalIdentifier, Y as getViewFieldGroupUniversalIdentifier, X as getViewFieldUniversalIdentifier, Z as getViewFilterUniversalIdentifier, Q as getViewGroupUniversalIdentifier, N as getViewNavigationMenuItemUniversalIdentifier, $ as getViewSortUniversalIdentifier, te as getViewUniversalIdentifier, c as isKnownApplicationCategory, re as serializeApplicationVariableValue };\n", "import {\n  DEFAULT_API_URL_NAME,\n  DEFAULT_APP_ACCESS_TOKEN_NAME,\n} from 'twenty-shared/application';\n\nexport const postGraphqlRequest = async <TVariables, TData>({\n  query,\n  variables,\n  caller,\n}: {\n  query: string;\n  variables: TVariables;\n  caller: string;\n}): Promise<TData> => {\n  const apiUrl = process.env[DEFAULT_API_URL_NAME];\n  const accessToken = process.env[DEFAULT_APP_ACCESS_TOKEN_NAME];\n\n  if (!apiUrl || !accessToken) {\n    throw new Error(\n      `${caller}() requires the app runtime env vars ` +\n        `${DEFAULT_API_URL_NAME} and ${DEFAULT_APP_ACCESS_TOKEN_NAME}.`,\n    );\n  }\n\n  const response = await fetch(`${apiUrl}/metadata`, {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n      Authorization: `Bearer ${accessToken}`,\n    },\n    body: JSON.stringify({ query, variables }),\n  });\n\n  if (!response.ok) {\n    throw new Error(\n      `${caller}() failed: HTTP ${response.status} ${response.statusText}`,\n    );\n  }\n\n  const body = (await response.json()) as {\n    data?: TData;\n    errors?: { message: string }[];\n  };\n\n  if (body.errors && body.errors.length > 0) {\n    throw new Error(\n      `${caller}() failed: ${body.errors.map((error) => error.message).join(', ')}`,\n    );\n  }\n\n  if (!body.data) {\n    throw new Error(`${caller}() failed: response contained no data.`);\n  }\n\n  return body.data;\n};\n", "import { AppConnectionAuthFailedError } from '@/sdk/logic-function/connections/errors/app-connection-auth-failed.error';\nimport { type AppConnection } from '@/sdk/logic-function/connections/types/app-connection.type';\nimport { postGraphqlRequest } from '@/sdk/logic-function/utils/post-graphql-request.util';\n\nconst GET_APP_CONNECTION_QUERY = `\n  query GetAppConnection($id: ID!) {\n    appConnection(id: $id) {\n      id\n      providerName\n      name\n      handle\n      visibility\n      userWorkspaceId\n      accessToken\n      scopes\n      authFailedAt\n    }\n  }\n`;\n\nexport const getConnection = async (id: string): Promise<AppConnection> => {\n  const { appConnection } = await postGraphqlRequest<\n    { id: string },\n    { appConnection: AppConnection }\n  >({\n    query: GET_APP_CONNECTION_QUERY,\n    variables: { id },\n    caller: 'getConnection',\n  });\n\n  if (appConnection.authFailedAt !== null) {\n    throw new AppConnectionAuthFailedError(appConnection.id);\n  }\n\n  return appConnection;\n};\n", "import { type AppConnection } from '@/sdk/logic-function/connections/types/app-connection.type';\nimport { postGraphqlRequest } from '@/sdk/logic-function/utils/post-graphql-request.util';\n\nconst LIST_APP_CONNECTIONS_QUERY = `\n  query ListAppConnections($filter: ListAppConnectionsInput) {\n    appConnections(filter: $filter) {\n      id\n      providerName\n      name\n      handle\n      visibility\n      userWorkspaceId\n      accessToken\n      scopes\n      authFailedAt\n    }\n  }\n`;\n\nexport type ListConnectionsFilter = {\n  providerName?: string;\n  userWorkspaceId?: string;\n  visibility?: 'user' | 'workspace';\n};\n\nexport const listConnections = async (\n  filter: ListConnectionsFilter = {},\n): Promise<AppConnection[]> => {\n  const { appConnections } = await postGraphqlRequest<\n    { filter: ListConnectionsFilter },\n    { appConnections: AppConnection[] }\n  >({\n    query: LIST_APP_CONNECTIONS_QUERY,\n    variables: { filter },\n    caller: 'listConnections',\n  });\n\n  return appConnections;\n};\n", "import { type AppConnection } from '@/sdk/logic-function/connections/types/app-connection.type';\n\nexport const findConnectionForRequest = (\n  connections: AppConnection[],\n  event: { userWorkspaceId: string | null },\n): AppConnection | null => {\n  if (event.userWorkspaceId !== null) {\n    const personal = connections.find(\n      (connection) =>\n        connection.visibility === 'user' &&\n        connection.userWorkspaceId === event.userWorkspaceId,\n    );\n\n    if (personal) {\n      return personal;\n    }\n  }\n\n  const workspaceShared = connections.find(\n    (connection) => connection.visibility === 'workspace',\n  );\n\n  return workspaceShared ?? null;\n};\n", "import {\n  type RunAgentInput,\n  type RunAgentResult,\n} from 'twenty-shared/application';\n\nimport { postGraphqlRequest } from '@/sdk/logic-function/utils/post-graphql-request.util';\n\nconst RUN_AGENT_MUTATION = `\n  mutation RunAgent($input: RunAgentInput!) {\n    runAgent(input: $input) {\n      result\n      error\n      success\n    }\n  }\n`;\n\nexport const runAgent = async (\n  input: RunAgentInput,\n): Promise<RunAgentResult> => {\n  const { runAgent: result } = await postGraphqlRequest<\n    { input: RunAgentInput },\n    { runAgent: RunAgentResult }\n  >({\n    query: RUN_AGENT_MUTATION,\n    variables: { input },\n    caller: 'runAgent',\n  });\n\n  return result;\n};\n", "import { MetadataApiClient } from 'twenty-client-sdk/metadata';\nimport {\n  type EnqueueJobInput,\n  type EnqueueJobResult,\n} from 'twenty-shared/application';\n\nexport const enqueueJob = async (\n  input: EnqueueJobInput,\n): Promise<EnqueueJobResult> => {\n  const client = new MetadataApiClient();\n\n  const { enqueueJob: result } = await client.mutation({\n    enqueueJob: {\n      __args: { input },\n      enqueued: true,\n      logicFunctionUniversalIdentifier: true,\n    },\n  });\n\n  return result;\n};\n", "import {\n  type AppKeyValue,\n  type AppKeyValueScope,\n} from 'twenty-shared/application';\n\nimport { postGraphqlRequest } from '@/sdk/logic-function/utils/post-graphql-request.util';\n\nconst GET_APP_KEY_VALUE_QUERY = `\n  query GetAppKeyValue($key: String!, $scope: AppKeyValueScope) {\n    appKeyValue(key: $key, scope: $scope) {\n      key\n      value\n      scope\n    }\n  }\n`;\n\nconst SET_APP_KEY_VALUE_MUTATION = `\n  mutation SetAppKeyValue($input: SetAppKeyValueInput!) {\n    setAppKeyValue(input: $input) {\n      key\n      value\n      scope\n    }\n  }\n`;\n\nconst DELETE_APP_KEY_VALUE_MUTATION = `\n  mutation DeleteAppKeyValue($key: String!, $scope: AppKeyValueScope) {\n    deleteAppKeyValue(key: $key, scope: $scope)\n  }\n`;\n\nconst DEFAULT_APP_KEY_VALUE_SCOPE: AppKeyValueScope = 'WORKSPACE';\n\ntype KvOptions = {\n  scope?: AppKeyValueScope;\n};\n\nexport const kv = {\n  async get<TValue = unknown>(\n    key: string,\n    options?: KvOptions,\n  ): Promise<TValue | null> {\n    const { appKeyValue } = await postGraphqlRequest<\n      { key: string; scope: AppKeyValueScope },\n      { appKeyValue: AppKeyValue | null }\n    >({\n      query: GET_APP_KEY_VALUE_QUERY,\n      variables: { key, scope: options?.scope ?? DEFAULT_APP_KEY_VALUE_SCOPE },\n      caller: 'kv.get',\n    });\n\n    return (appKeyValue?.value ?? null) as TValue | null;\n  },\n\n  async set<TValue>(\n    key: string,\n    value: TValue,\n    options?: KvOptions,\n  ): Promise<void> {\n    await postGraphqlRequest<\n      {\n        input: { key: string; value: TValue; scope: AppKeyValueScope };\n      },\n      { setAppKeyValue: AppKeyValue }\n    >({\n      query: SET_APP_KEY_VALUE_MUTATION,\n      variables: {\n        input: {\n          key,\n          value,\n          scope: options?.scope ?? DEFAULT_APP_KEY_VALUE_SCOPE,\n        },\n      },\n      caller: 'kv.set',\n    });\n  },\n\n  async delete(key: string, options?: KvOptions): Promise<boolean> {\n    const { deleteAppKeyValue } = await postGraphqlRequest<\n      { key: string; scope: AppKeyValueScope },\n      { deleteAppKeyValue: boolean }\n    >({\n      query: DELETE_APP_KEY_VALUE_MUTATION,\n      variables: { key, scope: options?.scope ?? DEFAULT_APP_KEY_VALUE_SCOPE },\n      caller: 'kv.delete',\n    });\n\n    return deleteAppKeyValue;\n  },\n};\n", "import { type LogicFunctionHttpResponse } from 'twenty-shared/types';\n\nexport type ResponseInit = {\n  status?: number;\n  headers?: Record<string, string>;\n};\n\nexport class Response implements LogicFunctionHttpResponse {\n  readonly __twentyHttpResponse = true as const;\n  readonly body: unknown;\n  readonly status?: number;\n  readonly headers?: Record<string, string>;\n\n  constructor(body: unknown, init?: ResponseInit) {\n    this.body = body;\n    this.status = init?.status;\n    this.headers = init?.headers;\n  }\n}\n", "/**\n * Universal identifiers for the gate queue and the human override \u2014 the\n * ARCHITECTURE.md \"Path 4: Lead Override (Human Review)\" surface.\n *\n * Same permanence rule as `src/constants/universal-identifiers.ts`: every value\n * here is written into the customer's workspace at install time. Changing one\n * does not rename an entity, it orphans the old one and creates a new one \u2014 a\n * user's column widths, sort and sidebar position silently reset. Adding is\n * safe; editing or removing is a breaking change.\n *\n * These live in their own file rather than in `universal-identifiers.ts` purely\n * so the gate-queue surface can be developed without touching the data-model\n * constants that the scoring engine and the schema objects depend on.\n */\n\n/* -------------------------------------------------------------------------- */\n/* Gate queue \u2014 the leads a human has to judge                                 */\n/* -------------------------------------------------------------------------- */\n\nexport const GATE_QUEUE_VIEW_UNIVERSAL_IDENTIFIER =\n  '09ba374e-5b2b-42db-8aa5-0029bd2e051f';\n\nexport const GATE_QUEUE_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER =\n  '864dd25a-b6a2-44be-b59a-b1e505c3bf8e';\n\nexport const GATE_QUEUE_VIEW_FILTER_UNIVERSAL_IDENTIFIER =\n  '2b32b3d7-bf02-4663-9687-25f6237002d3';\n\nexport const GATE_QUEUE_VIEW_SORT_UNIVERSAL_IDENTIFIER =\n  'f8951745-619e-4b5e-b42d-ee2935a30821';\n\nexport const GATE_QUEUE_VIEW_FIELD_UNIVERSAL_IDENTIFIERS = {\n  name: 'd56b3545-b95b-44da-9781-74d2b75c0065',\n  greenlightScore: 'fca48afc-94b1-4d8a-80d6-a2c604b661fa',\n  greenlightDecision: '8200f88b-5e4b-4edb-99e1-5cdb85625aa1',\n  jobTitle: 'd9b0d26f-79df-4a7d-8ecc-4413db87952a',\n  company: '4b765b33-dd7f-442a-97c8-91eeeb616a42',\n  emails: '9a4e2ef4-e774-4716-9222-33fcbd0154ef',\n  createdAt: '8247c042-c969-4f8b-a761-2e8ee4fc7bcd',\n} as const;\n\n/* -------------------------------------------------------------------------- */\n/* Unscored queue \u2014 the leads the engine failed open on                        */\n/* -------------------------------------------------------------------------- */\n\nexport const UNSCORED_QUEUE_VIEW_UNIVERSAL_IDENTIFIER =\n  '31243a13-11a6-4000-8b3e-195031808425';\n\nexport const UNSCORED_QUEUE_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER =\n  '54943a27-cbf6-41f8-92fa-b848d7a348f7';\n\nexport const UNSCORED_QUEUE_VIEW_FILTER_UNIVERSAL_IDENTIFIER =\n  'a79d3182-44e2-4feb-8ecb-22081c89e2a8';\n\nexport const UNSCORED_QUEUE_VIEW_SORT_UNIVERSAL_IDENTIFIER =\n  '487d8c98-7232-4f82-8495-14b980bff1a2';\n\nexport const UNSCORED_QUEUE_VIEW_FIELD_UNIVERSAL_IDENTIFIERS = {\n  name: 'b0fedd30-f6fb-4309-ae4c-cc8a8e795d9e',\n  greenlightDecision: 'e16b0c6f-599c-4962-90ba-38e8ca87e8f3',\n  greenlightScore: '2c61b772-b3d8-4bd7-b58a-ea84e7775f22',\n  company: '94bfea98-6ec1-4826-a0af-c63c16bb02a3',\n  createdAt: '5e6892c9-2186-4894-9fa1-de88721dc2ff',\n  updatedAt: '51ae2440-5921-421e-aeb3-f0bdfd57efce',\n} as const;\n\n/* -------------------------------------------------------------------------- */\n/* Compliance-blocked queue \u2014 the leads a rep must not call                    */\n/* -------------------------------------------------------------------------- */\n\nexport const BLOCKED_QUEUE_VIEW_UNIVERSAL_IDENTIFIER =\n  '116ab2bc-074f-49b1-98cd-dc15e2d4f423';\n\nexport const BLOCKED_QUEUE_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER =\n  '819393c2-dcab-4b7b-a0f5-02ec4206c105';\n\nexport const BLOCKED_QUEUE_VIEW_FILTER_UNIVERSAL_IDENTIFIER =\n  '7b0406b7-d46b-4bb5-9045-f97c7a3d920d';\n\nexport const BLOCKED_QUEUE_VIEW_SORT_UNIVERSAL_IDENTIFIER =\n  'fdd064dc-e6be-40fc-b87d-afa04290e275';\n\nexport const BLOCKED_QUEUE_VIEW_FIELD_UNIVERSAL_IDENTIFIERS = {\n  name: 'e3540759-1b28-4c2f-bef4-d00cdc2d4370',\n  greenlightDecision: 'c30b0b2e-1baf-47b3-8e25-c73f437d38cd',\n  greenlightScore: '73b638bd-a0d1-46b0-924f-b9ad32941ed4',\n  company: '2e287ba3-a58f-4f96-bba2-21b926fa686f',\n  emails: '44272c90-b26f-4e6d-b205-a4df7539836a',\n  updatedAt: '68a75ac6-5900-4ab3-9dfb-0e1a1947239c',\n  // Appended: the view used to say only *that* a lead was blocked. \"Why, and\n  // since when\" is the question anyone auditing the list actually asks.\n  greenlightSuppressionReason: '72ba174f-cdab-4bc5-97b4-b68224de0829',\n  greenlightSuppressedAt: 'e4fbb8db-d634-4730-ba9d-42c48714ed5e',\n} as const;\n\n/* -------------------------------------------------------------------------- */\n/* Suppression list \u2014 everyone Greenlight is refusing to contact               */\n/* -------------------------------------------------------------------------- */\n\nexport const SUPPRESSION_LIST_VIEW_UNIVERSAL_IDENTIFIER =\n  'da0e90cd-6a75-474b-9386-25a54485b8b6';\n\nexport const SUPPRESSION_LIST_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER =\n  'c945216d-d6d2-4026-94de-fcfe51475661';\n\nexport const SUPPRESSION_LIST_VIEW_FILTER_UNIVERSAL_IDENTIFIER =\n  '8cbb2f41-9bac-4e5f-b8f7-75868c0d9174';\n\nexport const SUPPRESSION_LIST_VIEW_SORT_UNIVERSAL_IDENTIFIER =\n  '9fab9e4c-4c9e-487f-914e-cdf172edace8';\n\nexport const SUPPRESSION_LIST_VIEW_FIELD_UNIVERSAL_IDENTIFIERS = {\n  name: '47a5e99b-4c65-4f2d-878c-1ecb907599cf',\n  greenlightSuppressionReason: '68eb83c6-f68f-4677-955c-b29e5b2435f9',\n  greenlightSuppressedAt: '6dd94606-a8e7-46e5-9cc9-f5b5b45a2bcc',\n  greenlightSuppressionNote: '1f3be7ca-beaf-4f47-9af2-72c5721018dd',\n  emails: '0b13ae97-0451-495d-bdb1-4c4ccf897ad9',\n  company: '419019f6-bfcc-4f7c-8c57-27ea2d8f9c54',\n  greenlightDecision: '08567499-13ed-4541-9853-4c91a73ee976',\n} as const;\n\n/* -------------------------------------------------------------------------- */\n/* Override action \u2014 command menu item -> front component -> logic function    */\n/* -------------------------------------------------------------------------- */\n\nexport const RELEASE_LEAD_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER =\n  '0d3a5c2f-0f16-470a-bc58-e95995f4b6dd';\n\nexport const RELEASE_LEAD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER =\n  '12cff52f-5473-487b-a8f1-afa145c9ac79';\n\nexport const RELEASE_LEAD_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIER =\n  '8173aab9-2c24-4351-bb32-26022b70161b';\n\n/**\n * HTTP route the override logic function listens on. The front component posts\n * to this path prefixed with `/s`, which is how `RestApiClient` recognises an\n * app route rather than a core REST call.\n *\n * Declared once, here, because a mismatch between the two sides is a 404 that\n * only shows up at runtime in a live workspace.\n */\nexport const RELEASE_LEAD_ROUTE_PATH = '/greenlight/release-lead';\nexport const RELEASE_LEAD_CLIENT_PATH = `/s${RELEASE_LEAD_ROUTE_PATH}`;\n\n/* -------------------------------------------------------------------------- */\n/* Suppression actions \u2014 add to and remove from the block list                 */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Two actions rather than one toggle, and that is a deliberate cost.\n *\n * A single \"manage do-not-contact\" panel would be less code and one fewer\n * command in the menu. It would also make adding and removing a suppression the\n * same gesture, and they are not the same gesture: adding one stops outreach,\n * removing one *re-enables contact with someone who is on record asking us not\n * to*. Separate commands mean separate labels, separate confirmation copy, and\n * separate audit event types, so the sensitive one can never be reached by\n * muscle memory aimed at the safe one.\n */\nexport const SUPPRESS_LEAD_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER =\n  'c5b4efd8-4471-4ec6-8f94-a013115598ef';\n\nexport const SUPPRESS_LEAD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER =\n  'b0a557fb-8ae2-4acb-81f6-1f5c70f1d0e0';\n\nexport const SUPPRESS_LEAD_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIER =\n  'df3f5e55-74bd-4b5c-96a0-87be363d622d';\n\nexport const UNSUPPRESS_LEAD_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER =\n  '48eeda6a-2f2e-4892-8377-2efd0e460d43';\n\nexport const UNSUPPRESS_LEAD_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER =\n  '75d0a79c-c6fc-497c-979c-b59181af88c9';\n\nexport const UNSUPPRESS_LEAD_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIER =\n  'c5e5f193-ecdc-4e33-aff0-0cfd1d9f6b70';\n\nexport const SUPPRESS_LEAD_ROUTE_PATH = '/greenlight/suppress-lead';\nexport const SUPPRESS_LEAD_CLIENT_PATH = `/s${SUPPRESS_LEAD_ROUTE_PATH}`;\n\nexport const UNSUPPRESS_LEAD_ROUTE_PATH = '/greenlight/unsuppress-lead';\nexport const UNSUPPRESS_LEAD_CLIENT_PATH = `/s${UNSUPPRESS_LEAD_ROUTE_PATH}`;\n", "/**\n * Resolving \"which human is making this request\" \u2014 once per run, and never from\n * anything the caller sent.\n *\n * The wording of the answer lives in `src/identity/actor.ts`, which is pure.\n * This file is the I/O half: one metadata query, one cache, and a promise that\n * every failure ends in a string rather than an exception.\n *\n * ## Why `currentUser`, and why no new permission\n *\n * The obvious way to turn an id into a name is to read the workspace-member\n * directory, and it is the wrong one. `default-role.ts` records that argument in\n * full under \"grants that were argued down\"; the short version is that reading\n * every member of a customer's workspace to put one name on one row is the same\n * bad trade the `calendarEvent` grant was refused for.\n *\n * `currentUser` on the **metadata** API is the narrow alternative. It is not an\n * object query and it is not covered by `objectPermissions` at all: it returns\n * the identity of whoever authenticated the call and nothing else, so it cannot\n * be pointed at another member, cannot enumerate the directory, and needs no\n * grant on `workspaceMember`. Greenlight already reaches the metadata API for\n * exactly one other thing \u2014 `currentWorkspace` in `licence-workspace-identity.ts`\n * \u2014 and this is the second and last.\n *\n * That it works from inside a logic function was proved against a live instance\n * rather than assumed, which is what the previous attempt at this feature did\n * not do: the note it left behind said the lookup's \"API shape is not documented\n * for app-scoped tokens\", concluded the risk was not worth taking, and shipped a\n * UUID for two release cycles. The probe answered in one deploy. The request\n * carries the caller's own context, `currentUser.currentUserWorkspace.id` comes\n * back equal to `event.userWorkspaceId`, and `workspaceMember` on it is the same\n * record Twenty stamps into `createdBy.workspaceMemberId` on the audit row it\n * writes. An undocumented surface is a reason to verify, not a reason to guess.\n *\n * ## One lookup per run, not one per row\n *\n * A backfill writes thousands of audit rows and the ICP apply writes one per\n * decision; a lookup on each of them would turn a name into a rate limit. It\n * cannot happen by construction \u2014 each `define*` shell resolves the actor once,\n * before any work starts, and passes the resulting string down \u2014 but the cache\n * below makes it structurally impossible rather than merely conventional, and it\n * also collapses the burst of `status` polls the backfill and ICP panels make\n * while a run is in flight.\n *\n * The cache is keyed on `userWorkspaceId` because two different people using the\n * same warm container must not share an answer, and it stores only *successful*\n * resolutions: a transient metadata failure has to be retryable on the next\n * request rather than pinned into the container for its lifetime. Its staleness\n * window is the life of one function container \u2014 minutes \u2014 against a workspace\n * member renaming themselves, which is rare and whose worst outcome is one row\n * carrying the name that person had a few minutes earlier. An audit log records\n * what was true at the time; that is the correct trade in the direction it is\n * already made.\n *\n * ## Every failure is a string\n *\n * `resolveActorDisplayName` never throws and never returns an empty actor. A\n * metadata outage, a schema drift, an unauthenticated request, a runtime whose\n * `currentUser` disagrees with the request's own `userWorkspaceId` \u2014 all of them\n * land on the id-bearing string this app wrote before names existed. Losing the\n * name costs readability. Losing the row costs the evidence.\n */\n\nimport { MetadataApiClient } from 'twenty-client-sdk/metadata';\n\nimport {\n  formatActor,\n  readActorIdentityFrom,\n  type ActorIdentity,\n} from 'src/identity/actor';\nimport {\n  describeError,\n  logGreenlight,\n} from 'src/logic-functions/greenlight-api';\n\n/**\n * The lookup, isolated for the usual reason: `MetadataApiClient` reads its URL\n * and token from the process environment in its constructor, which no unit test\n * has. Same shape and same motivation as `WorkspaceIdentityPort`.\n */\nexport interface ActorIdentityPort {\n  read: (userWorkspaceId: string) => Promise<ActorIdentity | null>;\n}\n\nexport const metadataActorIdentity: ActorIdentityPort = {\n  read: async (userWorkspaceId) => {\n    try {\n      const client = new MetadataApiClient();\n      const response = (await client.query({\n        currentUser: {\n          // Asked for so `readActorIdentityFrom` can refuse an identity that is\n          // not this request's. See the security note on that function.\n          currentUserWorkspace: { id: true },\n          workspaceMember: {\n            id: true,\n            name: { firstName: true, lastName: true },\n            userEmail: true,\n          },\n        },\n      })) as unknown;\n\n      const identity = readActorIdentityFrom(response, userWorkspaceId);\n\n      if (identity === null) {\n        logGreenlight('actor_identity_unresolved', {\n          note: 'currentUser did not return a named workspace member for this request; the audit row will carry the id instead',\n        });\n      }\n\n      return identity;\n    } catch (error) {\n      logGreenlight('actor_identity_failed', { error: describeError(error) });\n\n      return null;\n    }\n  },\n};\n\n/**\n * Successful resolutions only, keyed by the id the platform put on the request.\n *\n * Module scope, so it lives exactly as long as the function container does.\n * Nothing invalidates it and nothing needs to: see the staleness argument in the\n * file header.\n */\nconst resolved = new Map<string, string>();\n\n/** Exposed for tests, which must not inherit a name from the case before. */\nexport const clearActorCache = (): void => {\n  resolved.clear();\n};\n\n/**\n * The actor for this request, as it will appear on every row the request writes.\n *\n * Takes the whole event rather than the id so that no caller can pass one it\n * assembled itself. `userWorkspaceId` is the only server-derived identity on a\n * `RoutePayload`, and keeping the read of it inside this function is what makes\n * \"the actor never comes from the request body\" a property of the code rather\n * than of six shells all remembering the same rule.\n */\nexport const resolveActorDisplayName = async (\n  event: { readonly userWorkspaceId?: string | null },\n  port: ActorIdentityPort = metadataActorIdentity,\n): Promise<string> => {\n  const userWorkspaceId = event.userWorkspaceId ?? null;\n\n  if (userWorkspaceId === null || userWorkspaceId === '') {\n    return formatActor(null, userWorkspaceId);\n  }\n\n  const cached = resolved.get(userWorkspaceId);\n\n  if (cached !== undefined) {\n    return cached;\n  }\n\n  // The supplied port already swallows its own failures. Catching here as well\n  // is not belt-and-braces for its own sake: this function's contract is that it\n  // returns a string, and a contract that holds only while every implementation\n  // of a pluggable port remembers to be quiet is not a contract.\n  const identity = await port\n    .read(userWorkspaceId)\n    .catch((error: unknown): null => {\n      logGreenlight('actor_identity_failed', { error: describeError(error) });\n\n      return null;\n    });\n\n  const actor = formatActor(identity, userWorkspaceId);\n\n  if (identity !== null) {\n    resolved.set(userWorkspaceId, actor);\n  }\n\n  return actor;\n};\n", "/**\n * Numaya Greenlight \u2014 who a row in the audit log is about.\n *\n * Everything in this file is pure: no SDK import, no network, no clock read.\n * The lookup that produces the raw identity lives in\n * `src/logic-functions/actor-identity.ts`; this file decides what the identity\n * *reads* as, which is the part that has to be exhaustively testable without a\n * live workspace \u2014 for the same reason `src/gate/release-decision.ts` is pure.\n * The override row is the GDPR Art. 22 evidence that a named human overruled an\n * automated decision, and \"named\" is a property of the string this file builds.\n *\n * ## The mistake this corrects\n *\n * Every human-triggered action in this app used to write its actor as\n *\n *     Workspace member 20202020-9e3b-46d4-a556-88b9ddc2b035\n *\n * built in six separate `define*` shells from `event.userWorkspaceId`. The\n * reasoning recorded at the time \u2014 in `release-lead.logic-function.ts` \u2014 was that\n * resolving a friendly name \"needs a workspace-member lookup whose API shape is\n * not documented for app-scoped tokens, and a wrong guess here would break every\n * override\". The caution was right; the conclusion was not, and it survived two\n * rounds of demo recording because a UUID *looks* like a working audit trail.\n *\n * It is not one. The audit log exists to answer \"who did this, and why\" months\n * later, usually to somebody outside the sales team \u2014 a DPO, an auditor, a\n * customer asking why they were contacted. A UUID answers neither half. Worse,\n * the id in that string is not even the identifier a Twenty admin can look\n * up: `userWorkspaceId` is the id of the **user-workspace membership row**, not\n * of the workspace member, so the label was wrong as well as unreadable.\n *\n * ## What replaced it\n *\n *     Priya Raman (workspace member 3f2c1b6e-\u2026)\n *\n * Both halves are load-bearing and neither is decoration:\n *\n *   - **The name** is what a human reads. It is the whole point of the change.\n *   - **The id** is what survives the name. \"Priya Raman\" is ambiguous across two\n *     employees with the same name, and across a member who has since left and\n *     been removed from the directory \u2014 at which point the name is all that is\n *     left of them and it no longer resolves to anybody. The workspace-member id\n *     is the durable account key, it is the same identifier Twenty stamps into\n *     its own `createdBy.workspaceMemberId` on the row, and it is deliberately\n *     not abbreviated: a truncated UUID in an audit trail is a UUID that cannot\n *     be pasted into a search box.\n *\n * ## Failing soft, on purpose\n *\n * `formatActor` takes `null` and answers with the *old* id-bearing string rather\n * than an empty actor or a thrown error. An audit row that names an id is poor.\n * An audit row that does not exist, because the name lookup failed and took the\n * write with it, is the thing this app promises never to produce \u2014 the same\n * fail-open reasoning `licence-workspace-identity.ts` applies to the workspace\n * fingerprint, and `release-lead.logic-function.ts` to the release marker.\n */\n\n/**\n * A workspace member, reduced to the two things an audit row needs.\n *\n * `displayName` is already resolved to a non-empty string by the time it gets\n * here \u2014 see `readActorIdentityFrom`, which refuses to build an identity it\n * cannot name. That keeps `formatActor` free of a \"what if the name is blank\"\n * branch, which is the branch that would otherwise quietly emit\n * `\" (workspace member 3f2c\u2026)\"` with a hole at the front of it.\n */\nexport interface ActorIdentity {\n  readonly workspaceMemberId: string;\n  readonly displayName: string;\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n  typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst readString = (value: unknown): string =>\n  typeof value === 'string' ? value.trim() : '';\n\n/**\n * \"Priya Raman\" from Twenty's `FullName` composite, or the address if the\n * directory has no name on the member.\n *\n * A workspace member created by invitation carries an email long before anybody\n * fills in a first and last name, so an address is a real and common state\n * rather than a defensive branch \u2014 and `priya@northwind.example` in an audit row\n * is still a person, where a blank is not. Joining with a filter rather than a\n * template keeps \"Priya\" (no surname recorded) reading as `Priya` and not as\n * `Priya ` with a trailing space that no reader can see and every string\n * comparison can.\n */\nconst nameFrom = (member: Record<string, unknown>): string => {\n  const name = member['name'];\n  const parts = isRecord(name)\n    ? [readString(name['firstName']), readString(name['lastName'])]\n    : [];\n  const joined = parts.filter((part) => part !== '').join(' ');\n\n  return joined !== '' ? joined : readString(member['userEmail']);\n};\n\n/**\n * The `currentUser` response, narrowed \u2014 and checked against the request that\n * asked for it.\n *\n * The check is the security property, not a nicety. The actor on an audit row\n * must be the human the platform authenticated, never a value the caller\n * supplied and never an ambient identity the runtime happened to have lying\n * around. `event.userWorkspaceId` is the server-derived half of that guarantee,\n * and this function will not return an identity unless the directory agrees:\n * `currentUser.currentUserWorkspace.id` has to equal the `userWorkspaceId` the\n * platform put on this request. If the two ever disagree \u2014 a pooled client, a\n * cached token, a future runtime that resolves `currentUser` from something\n * other than the caller \u2014 the answer is `null` and the row falls back to the id\n * that *is* known to belong to this request. Naming the wrong person in an audit\n * log is a worse failure than naming nobody, and it is a failure that would\n * never be noticed.\n *\n * Everything else here is shape-tolerance of the kind `readConnectionNodes` and\n * `readWorkspaceIdentityFrom` already apply: a schema drift degrades to \"no\n * name\", never to an exception.\n */\nexport const readActorIdentityFrom = (\n  payload: unknown,\n  userWorkspaceId: string,\n): ActorIdentity | null => {\n  if (!isRecord(payload)) {\n    return null;\n  }\n\n  const currentUser = payload['currentUser'];\n\n  if (!isRecord(currentUser)) {\n    return null;\n  }\n\n  const userWorkspace = currentUser['currentUserWorkspace'];\n\n  if (\n    !isRecord(userWorkspace) ||\n    readString(userWorkspace['id']) !== userWorkspaceId ||\n    userWorkspaceId === ''\n  ) {\n    return null;\n  }\n\n  const member = currentUser['workspaceMember'];\n\n  if (!isRecord(member)) {\n    return null;\n  }\n\n  const workspaceMemberId = readString(member['id']);\n  const displayName = nameFrom(member);\n\n  return workspaceMemberId === '' || displayName === ''\n    ? null\n    : { workspaceMemberId, displayName };\n};\n\n/**\n * The one string every audit row, release marker and backfill record puts in\n * `actorDisplayName`.\n *\n * Three outcomes, and the two unhappy ones are byte-for-byte what this app wrote\n * before the name lookup existed. That is deliberate: the fallback is not a new\n * failure mode to be discovered in production, it is the state the product\n * shipped in, so the worst a broken lookup can do is put the audit trail back\n * where it already was.\n */\nexport const formatActor = (\n  identity: ActorIdentity | null,\n  userWorkspaceId: string | null | undefined,\n): string => {\n  if (identity !== null) {\n    return `${identity.displayName} (workspace member ${identity.workspaceMemberId})`;\n  }\n\n  return userWorkspaceId === null ||\n    userWorkspaceId === undefined ||\n    userWorkspaceId === ''\n    ? 'Unidentified workspace member'\n    : `Workspace member ${userWorkspaceId}`;\n};\n", "/**\n * The narrowest possible view of Twenty's Core API client.\n *\n * `CoreApiClient` from `twenty-client-sdk/core` is typed `any` until\n * `dev:generate-client` regenerates it against the installed workspace schema,\n * so depending on it directly buys no type safety and costs testability \u2014 the\n * class reads `API_URL` / `APP_ACCESS_TOKEN` from the process environment in its\n * constructor, which no unit test has.\n *\n * Everything in this folder therefore talks to `GreenlightApiClient`. The\n * `define*` files are the only place `new CoreApiClient()` appears, and they do\n * nothing but hand it to a handler that can be driven by a fake in tests.\n */\n\nexport interface GreenlightApiClient {\n  query(request: Record<string, unknown>): Promise<unknown>;\n  mutation(request: Record<string, unknown>): Promise<unknown>;\n}\n\nexport const isPlainRecord = (\n  value: unknown,\n): value is Record<string, unknown> =>\n  typeof value === 'object' && value !== null && !Array.isArray(value);\n\n/**\n * Twenty returns connections as `{ edges: [{ node }] }`. Pull the nodes out\n * without assuming the shape is correct \u2014 a schema drift should degrade to\n * \"no records found\", never throw.\n */\nexport const readConnectionNodes = (\n  payload: unknown,\n  connectionName: string,\n): Record<string, unknown>[] => {\n  if (!isPlainRecord(payload)) {\n    return [];\n  }\n\n  const connection = payload[connectionName];\n\n  if (!isPlainRecord(connection)) {\n    return [];\n  }\n\n  const edges = connection['edges'];\n\n  if (!Array.isArray(edges)) {\n    return [];\n  }\n\n  return edges.flatMap((edge): Record<string, unknown>[] => {\n    if (!isPlainRecord(edge)) {\n      return [];\n    }\n\n    const node = edge['node'];\n\n    return isPlainRecord(node) ? [node] : [];\n  });\n};\n\n/** ISO-8601 sorts lexicographically, so this needs no Date parsing. */\nexport const oldestByCreatedAt = (\n  records: readonly Record<string, unknown>[],\n): Record<string, unknown> | null => {\n  let oldest: Record<string, unknown> | null = null;\n  let oldestKey: string | null = null;\n\n  for (const record of records) {\n    const createdAt = record['createdAt'];\n    const key = typeof createdAt === 'string' ? createdAt : '';\n\n    if (oldest === null || oldestKey === null || key < oldestKey) {\n      oldest = record;\n      oldestKey = key;\n    }\n  }\n\n  return oldest;\n};\n\n/**\n * Structured log line. Logic functions have no logger injected \u2014 stdout is what\n * `yarn twenty dev:function:logs` shows \u2014 so everything Greenlight emits is a\n * single JSON object with a stable `event` key that support can grep for.\n */\nexport const logGreenlight = (\n  event: string,\n  detail: Record<string, unknown>,\n): void => {\n  // eslint-disable-next-line no-console\n  console.log(JSON.stringify({ app: 'numaya-greenlight', event, ...detail }));\n};\n\nexport const describeError = (error: unknown): string => {\n  if (error instanceof Error) {\n    return `${error.name}: ${error.message}`;\n  }\n\n  if (typeof error === 'string') {\n    return error;\n  }\n\n  try {\n    return JSON.stringify(error) ?? 'unknown error';\n  } catch {\n    return 'unknown error';\n  }\n};\n", "/**\n * Numaya Greenlight \u2014 the decision half of the human override.\n *\n * Everything in this file is pure: no SDK import, no network, no clock read.\n * The logic function is a thin shell that fetches state, calls `decideRelease`,\n * and executes whatever it is told. That split exists because the override is\n * the GDPR Art. 22 load-bearing part of the product \u2014 the rules about what may\n * and may not be released have to be exhaustively testable without a live\n * Twenty workspace.\n *\n * Naming note: the engine's four decision states (`approved` / `gated` /\n * `blocked` / `unscored`, see `src/scoring/types.ts`) now map one-to-one onto\n * the `greenlightDecision` SELECT field on Person, which ships four options\n * (`PASS` / `GATE` / `BLOCKED` / `UNSCORED`). A compliance stop is therefore\n * readable straight off the field. Records scored before the `BLOCKED` option\n * existed still read `GATE`, so `isComplianceBlocked` survives as a fallback \u2014\n * see the comment on `blockedFromLead` for exactly when each one answers.\n */\n\n/* -------------------------------------------------------------------------- */\n/* The compliance question                                                     */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Whether a lead that the engine marked `blocked` may be released by a human.\n *\n * **True \u2014 product decision, 2026-08-02.** The opt-out register is maintained by\n * the sales team inside Twenty, and the business trusts the person maintaining\n * it to also be the person who corrects it. An earlier iteration refused this\n * and required the contact to re-consent by email; that was withdrawn, because\n * Twenty sends no email and building an outbound provider to gate a correction\n * the sales team is trusted to make anyway is disproportionate.\n *\n * What this restores: the golden rule in `ARCHITECTURE.md` and the promise in\n * `PRODUCT_SPEC.md` that a human can always release a held lead, with no\n * exception carved out for compliance.\n *\n * What it costs, stated plainly rather than buried:\n *\n *   1. A released compliance block leaves the record in a **contradictory\n *      state** \u2014 `greenlightDecision` says the lead is cleared while the\n *      suppression fields still say do-not-contact. The audit row is the only\n *      thing that explains it. Prefer clearing the suppression itself (\"Allow\n *      contact again\") when the block was recorded in error: that re-scores the\n *      lead and clears the gate through the data, which reads far better in a\n *      DPA conversation than an override sitting on top of a live opt-out.\n *   2. Under GDPR Art. 21(3) an objection to direct marketing is absolute, and\n *      \"a salesperson approved it\" is not a lawful basis on its own. This\n *      setting assumes the release reflects a real-world correction \u2014 the\n *      contact never opted out, or asked to be added back \u2014 not a decision to\n *      market to someone who said stop. That assumption is the customer's to\n *      hold, and every release is audited with who, when and why so it can be\n *      evidenced.\n *\n * Setting this back to `false` re-refuses compliance releases; the refusal\n * branch and its tests are the only readers.\n */\nexport const ALLOW_COMPLIANCE_OVERRIDE = true;\n\n/* -------------------------------------------------------------------------- */\n/* Reasons                                                                     */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The override reason is a **mandatory choice from a closed list**, plus an\n * optional free-text note.\n *\n * `ARCHITECTURE.md` leaves this as \"reason/none\". Neither extreme works:\n *\n *   - Optional reason. The whole Art. 22 argument is that a human applied\n *     judgement. An override with no recorded reason is indistinguishable in\n *     the audit trail from an automated release, so it evidences nothing.\n *\n *   - Mandatory free text. A required text box gets \"ok\" typed into it, which\n *     is worse than nothing: it manufactures the appearance of a reasoned\n *     decision without the substance, and it is unanalysable in aggregate. It\n *     is also a data-protection liability in its own right \u2014 an uncontrolled\n *     free-text field on an append-only log that nobody ever deletes is exactly\n *     where a rep eventually types something that should never have been\n *     recorded about a person.\n *\n * A required pick from five categories is a real judgement, cheap to make,\n * impossible to fake into meaninglessness, and analysable: 200 releases tagged\n * `ICP_TOO_NARROW` are not 200 judgement calls, they are one misconfigured ICP,\n * and the queue can say so. The optional note carries the specifics when there\n * are any.\n */\nexport const RELEASE_REASONS = [\n  {\n    code: 'ICP_TOO_NARROW',\n    label: 'Scoring rules are too narrow for this lead',\n    hint: 'The lead is fine; the ICP or rule configuration is what is wrong.',\n  },\n  {\n    code: 'DATA_INCOMPLETE',\n    label: 'Lead data is incomplete but I know this is a good lead',\n    hint: 'Missing fields dragged the score down. You have the context the record does not.',\n  },\n  {\n    code: 'KNOWN_ACCOUNT',\n    label: 'Existing relationship or inbound request',\n    hint: 'They asked us to get in touch, or there is a live thread already.',\n  },\n  {\n    code: 'STRATEGIC_EXCEPTION',\n    label: 'Deliberate exception I am accounting for',\n    hint: 'The gate is right and you are overriding it anyway, on purpose.',\n  },\n  {\n    code: 'TESTING',\n    label: 'Testing Greenlight',\n    hint: 'Keeps test releases out of the real override statistics.',\n  },\n] as const;\n\nexport type ReleaseReasonCode = (typeof RELEASE_REASONS)[number]['code'];\n\nexport const RELEASE_REASON_CODES: readonly ReleaseReasonCode[] =\n  RELEASE_REASONS.map((reason) => reason.code);\n\n/** Free-text notes are capped: an audit field is not a place to write an essay. */\nexport const MAX_REASON_NOTE_LENGTH = 500;\n\nconst findReason = (code: string) =>\n  RELEASE_REASONS.find((reason) => reason.code === code) ?? null;\n\n/* -------------------------------------------------------------------------- */\n/* Requests                                                                    */\n/* -------------------------------------------------------------------------- */\n\nexport interface ReleaseRequest {\n  readonly leadObjectNameSingular: string;\n  readonly recordId: string;\n  readonly reasonCode: ReleaseReasonCode;\n  /** Empty string when the reviewer did not add one. */\n  readonly reasonNote: string;\n}\n\nexport type ParseResult =\n  | { readonly ok: true; readonly request: ReleaseRequest }\n  | { readonly ok: false; readonly message: string };\n\nconst UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\n\n/**\n * v0.1 physically ships the score fields on Person only (`defineField` binds at\n * build time \u2014 see `greenlight-score.field.ts`). Accepting any other object name\n * would mean writing `greenlightDecision` to an object that does not have it.\n * The parameter exists so the Layer 3 seam is visible in the wire format, not\n * because it is configurable yet.\n */\nexport const SUPPORTED_LEAD_OBJECTS: readonly string[] = ['person'];\n\nconst asString = (value: unknown): string | null =>\n  typeof value === 'string' ? value : null;\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n  typeof value === 'object' && value !== null && !Array.isArray(value);\n\nexport const parseReleaseRequest = (body: unknown): ParseResult => {\n  if (typeof body !== 'object' || body === null || Array.isArray(body)) {\n    return { ok: false, message: 'Release request body must be an object.' };\n  }\n\n  const raw = body as Record<string, unknown>;\n\n  const recordId = asString(raw['recordId'])?.trim() ?? '';\n\n  if (!UUID.test(recordId)) {\n    return { ok: false, message: 'Release request needs a valid recordId.' };\n  }\n\n  const leadObjectNameSingular =\n    asString(raw['leadObjectNameSingular'])?.trim() || 'person';\n\n  if (!SUPPORTED_LEAD_OBJECTS.includes(leadObjectNameSingular)) {\n    return {\n      ok: false,\n      message: `Greenlight v0.1 can only release leads on ${SUPPORTED_LEAD_OBJECTS.join(', ')}, not ${leadObjectNameSingular}.`,\n    };\n  }\n\n  const reasonCode = asString(raw['reasonCode'])?.trim() ?? '';\n  const reason = findReason(reasonCode);\n\n  if (reason === null) {\n    return {\n      ok: false,\n      message:\n        'Choose why you are releasing this lead. A recorded reason is what makes the override a human decision rather than an automated one.',\n    };\n  }\n\n  const reasonNote = (asString(raw['reasonNote']) ?? '')\n    .trim()\n    .slice(0, MAX_REASON_NOTE_LENGTH);\n\n  return {\n    ok: true,\n    request: {\n      leadObjectNameSingular,\n      recordId,\n      reasonCode: reason.code,\n      reasonNote,\n    },\n  };\n};\n\n/** The audit-trail rendering of a reason: machine code first, prose after. */\nexport const formatOverrideReason = (\n  reasonCode: ReleaseReasonCode,\n  reasonNote: string,\n): string => {\n  const reason = findReason(reasonCode);\n  const label = reason === null ? reasonCode : `${reason.code} \u00B7 ${reason.label}`;\n\n  return reasonNote === '' ? label : `${label} \u2014 ${reasonNote}`;\n};\n\n/* -------------------------------------------------------------------------- */\n/* Current state of the lead                                                   */\n/* -------------------------------------------------------------------------- */\n\n/** Values of the `greenlightDecision` SELECT on Person. */\nexport const DECISION_PASS = 'PASS';\nexport const DECISION_GATE = 'GATE';\nexport const DECISION_BLOCKED = 'BLOCKED';\nexport const DECISION_UNSCORED = 'UNSCORED';\n\nconst KNOWN_DECISIONS: readonly string[] = [\n  DECISION_PASS,\n  DECISION_GATE,\n  DECISION_BLOCKED,\n  DECISION_UNSCORED,\n];\n\n/** `greenlightDecision` as stored, normalised. Null when it is not a value we ship. */\nexport const normaliseDecision = (raw: string | null): string | null => {\n  const value = raw?.trim().toUpperCase() ?? '';\n\n  return KNOWN_DECISIONS.includes(value) ? value : null;\n};\n\n/**\n * The decision an audit row should record as the state the lead was left in.\n *\n * A refused release leaves the lead exactly where it was, and \"where it was\"\n * is now a distinction worth keeping: a refusal against a `BLOCKED` record and\n * a refusal against a `GATE` record are different compliance stories. Anything\n * unrecognised degrades to `GATE` rather than being written through, because\n * this value goes into a SELECT that would reject an unknown option and lose\n * the row.\n */\nexport const auditDecisionValue = (\n  currentDecision: string | null,\n  shouldClearGate: boolean,\n): string =>\n  shouldClearGate\n    ? DECISION_PASS\n    : (normaliseDecision(currentDecision) ?? DECISION_GATE);\n\n/** Values of the `band` SELECT on GreenlightAuditLog. */\nexport const BAND_UNSCORED = 'UNSCORED';\n\nconst KNOWN_BANDS: readonly string[] = [\n  'EXCELLENT',\n  'GOOD',\n  'FAIR',\n  'POOR',\n  BAND_UNSCORED,\n];\n\n/**\n * The band an override's audit row should record.\n *\n * The release path used to write no band at all, so every `RELEASED` row took\n * the field's `UNSCORED` default while the `GATED` row for the same lead read\n * `POOR`. Two contradictory bands for one lead is not a compliance record, it is\n * a bug with a paper trail \u2014 a reviewer comparing the two rows cannot tell which\n * one is lying.\n *\n * The band is **read out of the trace the scoring run left on the record**, not\n * recomputed here. The engine's bands are configurable (`bandExcellentThreshold`\n * and friends on GreenlightConfig), so deriving a band from the score with the\n * shipped defaults would produce a *different* wrong answer in any workspace\n * that moved a threshold \u2014 and it would disagree with the `GATED` row all over\n * again. The trace carries the band that run actually chose, which is the only\n * value that can agree with the earlier row by construction.\n *\n * `UNSCORED` remains the answer when the trace is missing or unreadable, but it\n * now means what the option says: nothing here knows the band. That is the state\n * of a lead released before it was ever scored, which the queue does allow.\n */\nexport const auditBandValue = (trace: unknown): string => {\n  if (!isRecord(trace)) {\n    return BAND_UNSCORED;\n  }\n\n  const value = asString(trace['band'])?.trim().toUpperCase() ?? '';\n\n  return KNOWN_BANDS.includes(value) ? value : BAND_UNSCORED;\n};\n\nexport interface CurrentLeadState {\n  readonly recordId: string;\n  /** Denormalised into the audit row so the trail survives the record. */\n  readonly displayName: string;\n  /** Raw `greenlightDecision`. Null when the lead has never been scored. */\n  readonly decision: string | null;\n  readonly score: number | null;\n  /** Raw `greenlightTrace`, whatever shape the engine wrote. */\n  readonly trace: unknown;\n}\n\n/**\n * A human release, recorded outside the record so the scoring function can see\n * it. Written to app key-value storage by the override; see the contract note\n * on `RELEASE_MARKER_SCOPE` below.\n */\nexport interface ReleaseMarker {\n  readonly leadObjectNameSingular: string;\n  readonly recordId: string;\n  /** ISO 8601. */\n  readonly releasedAt: string;\n  readonly releasedBy: string;\n  readonly reasonCode: string;\n}\n\nexport const releaseMarkerKey = (\n  leadObjectNameSingular: string,\n  recordId: string,\n): string => `greenlight:release:${leadObjectNameSingular}:${recordId}`;\n\n/**\n * Two releases of the same lead inside this window are the same release \u2014 a\n * double-click, or Twenty re-delivering the request. Mirrors the 5s event\n * de-duplication window ARCHITECTURE.md \u00A7 \"Idempotency & Retry Safety\" already\n * assumes, with headroom for a slow round trip.\n *\n * This is a *narrow race* guard only. The authoritative double-release check is\n * the decision field itself: if the lead already reads PASS it is not held, so\n * there is nothing to release, whatever the marker says. The marker can be\n * stale \u2014 a lead released on Monday and legitimately re-gated on Tuesday after\n * its email was deleted must be releasable again on Tuesday.\n */\nexport const DUPLICATE_RELEASE_WINDOW_MS = 10_000;\n\n/* -------------------------------------------------------------------------- */\n/* Compliance block detection                                                  */\n/* -------------------------------------------------------------------------- */\n\nconst entryIsBlocking = (entry: unknown): boolean => {\n  if (!isRecord(entry)) {\n    return false;\n  }\n\n  const severity = asString(entry['severity'])?.toLowerCase() ?? '';\n  const outcome = asString(entry['outcome'])?.toLowerCase() ?? '';\n\n  return severity === 'blocking' && outcome === 'fail';\n};\n\nconst traceEntries = (trace: unknown): readonly unknown[] => {\n  if (Array.isArray(trace)) {\n    return trace;\n  }\n\n  if (!isRecord(trace)) {\n    return [];\n  }\n\n  for (const key of ['rules', 'trace', 'entries']) {\n    const candidate = trace[key];\n\n    if (Array.isArray(candidate)) {\n      return candidate;\n    }\n  }\n\n  return [];\n};\n\n/**\n * Recover the engine's `blocked` state from the trace.\n *\n * **This is the legacy path.** `greenlightDecision` now ships a `BLOCKED`\n * option, so a lead scored by any current build states its compliance stop on\n * the field itself and never reaches this function. It is kept because nothing\n * backfills a SELECT option: every lead scored before the option existed still\n * reads `GATE`, and reading the trace is the only way to tell those apart. See\n * `blockedFromLead`.\n *\n * **It is the one place the queue depends on a shape the scoring function\n * writes, and it is deliberately forgiving about that shape**, because the trace\n * field's exact envelope is not pinned down anywhere in the spec. Accepted, in\n * order:\n *\n *   - a top-level `decision` of `blocked` (any case) on the trace object;\n *   - an array of rule-trace entries, or `{ rules: [...] }` / `{ trace: [...] }`\n *     / `{ entries: [...] }`, containing an entry with\n *     `severity: 'blocking'` and `outcome: 'fail'` \u2014 the exact discriminator\n *     `src/scoring/engine.ts` itself uses to choose `blocked`.\n *\n * If the trace is missing or unreadable the answer is `false`: an unreadable\n * trace must not silently turn every held lead into an unreleasable one. That\n * is the fail-open direction for *this* check, and it is the right one \u2014 the\n * compliance rule that produced the block also wrote an audit row, and a lead\n * whose trace we cannot read is a lead we cannot claim anything about.\n */\nexport const isComplianceBlocked = (trace: unknown): boolean => {\n  if (isRecord(trace)) {\n    const declared = asString(trace['decision'])?.toLowerCase() ?? '';\n\n    if (declared === 'blocked') {\n      return true;\n    }\n  }\n\n  return traceEntries(trace).some(entryIsBlocking);\n};\n\n/**\n * Is this lead held on compliance grounds?\n *\n * Field first, trace second, and the order is the whole point:\n *\n *   - **The field** is the current, cheap, unambiguous answer for anything\n *     scored since `BLOCKED` shipped. One string comparison, no assumptions\n *     about a RAW_JSON envelope.\n *   - **The trace** answers for records scored before that, which still read\n *     `GATE` because adding a SELECT option does not rewrite existing rows.\n *     Dropping this fallback would quietly make every historic opt-out\n *     releasable by hand on the day the option was added \u2014 the exact failure the\n *     compliance refusal exists to prevent. It can go once a workspace has\n *     re-scored every lead, which is not something this code can know.\n */\nconst blockedFromLead = (lead: CurrentLeadState): boolean =>\n  normaliseDecision(lead.decision) === DECISION_BLOCKED ||\n  isComplianceBlocked(lead.trace);\n\n/* -------------------------------------------------------------------------- */\n/* The decision                                                                */\n/* -------------------------------------------------------------------------- */\n\nexport type ReleaseOutcome =\n  /** The gate was cleared and an audit row must be written. */\n  | 'released'\n  /** Already `PASS`. Not an error \u2014 the rep got what they wanted. */\n  | 'already_released'\n  /** `UNSCORED`: the engine failed open, the lead was never held. */\n  | 'not_held'\n  /** No decision at all: never scored, so never gated. */\n  | 'not_scored'\n  /** A second request inside the de-duplication window. */\n  | 'duplicate_request'\n  /** Compliance stop. See `ALLOW_COMPLIANCE_OVERRIDE`. */\n  | 'refused_compliance_block';\n\nexport interface ReleaseDecision {\n  readonly outcome: ReleaseOutcome;\n  /** Shown to the reviewer. Written for a salesperson, not a developer. */\n  readonly message: string;\n  /** Whether `greenlightDecision` must be written to `PASS`. */\n  readonly shouldClearGate: boolean;\n  /** Whether a `GreenlightAuditLog` row must be appended. */\n  readonly shouldWriteAuditRow: boolean;\n  /** `RELEASED` for a real release, `SKIPPED` for a recorded refusal. */\n  readonly auditEventType: 'RELEASED' | 'SKIPPED' | null;\n  /** Rendered reason for the audit row. Empty when no row is written. */\n  readonly overrideReason: string;\n  /** Whether the release marker must be written for the scoring function. */\n  readonly shouldWriteMarker: boolean;\n}\n\nexport interface DecideReleaseInput {\n  readonly request: ReleaseRequest;\n  readonly lead: CurrentLeadState;\n  /** Previous release of this lead, if app storage had one. */\n  readonly marker: ReleaseMarker | null;\n  /** The clock, passed in \u2014 this module never reads it. */\n  readonly now: Date;\n}\n\nconst noop = (\n  outcome: ReleaseOutcome,\n  message: string,\n): ReleaseDecision => ({\n  outcome,\n  message,\n  shouldClearGate: false,\n  shouldWriteAuditRow: false,\n  auditEventType: null,\n  overrideReason: '',\n  shouldWriteMarker: false,\n});\n\nconst isWithinDuplicateWindow = (\n  marker: ReleaseMarker | null,\n  now: Date,\n): boolean => {\n  if (marker === null) {\n    return false;\n  }\n\n  const releasedAt = Date.parse(marker.releasedAt);\n\n  if (Number.isNaN(releasedAt)) {\n    return false;\n  }\n\n  const elapsed = now.getTime() - releasedAt;\n\n  return elapsed >= 0 && elapsed < DUPLICATE_RELEASE_WINDOW_MS;\n};\n\n/**\n * Decide what the override should do. Never throws.\n *\n * Order matters and is not arbitrary:\n *\n *   1. **Not held** beats everything. If the gate is already open there is\n *      nothing to release, so we neither write the record nor append an audit\n *      row \u2014 a rep double-clicking must not manufacture history. This is the\n *      \"safe against releasing something already approved\" guarantee, and it is\n *      checked against the record's own field rather than against any cached\n *      marker, because the field is the only authoritative statement of whether\n *      the lead is held right now.\n *   2. **Compliance** beats the reviewer. Read from `greenlightDecision` when it\n *      says `BLOCKED`, and from the trace otherwise so pre-`BLOCKED` records\n *      still refuse (`blockedFromLead`). Checked before the duplicate window so\n *      that every attempt against a blocked lead is recorded, including repeat\n *      attempts \u2014 repeated attempts to release an opt-out are exactly the\n *      pattern a DPO would want to see.\n *   3. **Duplicate window** catches the narrow double-submit race that the\n *      field check cannot see, because both requests read `GATE` before either\n *      wrote `PASS`.\n */\nexport const decideRelease = (input: DecideReleaseInput): ReleaseDecision => {\n  const { request, lead, marker, now } = input;\n\n  const decision = lead.decision?.trim().toUpperCase() ?? null;\n\n  if (decision === null || decision === '') {\n    return noop(\n      'not_scored',\n      'This lead has no Greenlight decision yet, so it is not being held. Nothing to release.',\n    );\n  }\n\n  if (decision === DECISION_PASS) {\n    return noop(\n      'already_released',\n      'This lead has already cleared the gate. No change made, and no second audit entry written.',\n    );\n  }\n\n  if (decision === DECISION_UNSCORED) {\n    return noop(\n      'not_held',\n      'Greenlight could not score this lead, so it was let through unscored rather than held. There is no gate to clear.',\n    );\n  }\n\n  if (decision !== DECISION_GATE && decision !== DECISION_BLOCKED) {\n    return noop(\n      'not_held',\n      `This lead is in an unrecognised Greenlight state (${decision}), so the override left it alone.`,\n    );\n  }\n\n  const blocked = blockedFromLead(lead);\n\n  if (blocked && !ALLOW_COMPLIANCE_OVERRIDE) {\n    return {\n      outcome: 'refused_compliance_block',\n      message:\n        'This lead is held on compliance grounds \u2014 the record carries a recorded opt-out. That is the contact\\'s own instruction, not a scoring judgement, so it cannot be overridden here. Correct the opt-out data on the record if it is wrong; the lead will re-score and clear itself.',\n      shouldClearGate: false,\n      // The refusal *is* recorded. An attempt to release an opted-out contact\n      // is a compliance-relevant human action even though nothing changed.\n      shouldWriteAuditRow: true,\n      auditEventType: 'SKIPPED',\n      overrideReason: `REFUSED_COMPLIANCE_BLOCK \u2014 release attempted with reason ${formatOverrideReason(\n        request.reasonCode,\n        request.reasonNote,\n      )}`,\n      shouldWriteMarker: false,\n    };\n  }\n\n  if (isWithinDuplicateWindow(marker, now)) {\n    return noop(\n      'duplicate_request',\n      'This lead was released moments ago. Treating this as a duplicate submission and leaving the audit trail alone.',\n    );\n  }\n\n  return {\n    outcome: 'released',\n    message: 'Released. The lead is cleared to work and the override is on record.',\n    shouldClearGate: true,\n    shouldWriteAuditRow: true,\n    auditEventType: 'RELEASED',\n    overrideReason: formatOverrideReason(request.reasonCode, request.reasonNote),\n    shouldWriteMarker: true,\n  };\n};\n\n/** One-line summary used as the audit row's record label. */\nexport const buildAuditSummary = (\n  eventType: 'RELEASED' | 'SKIPPED',\n  lead: CurrentLeadState,\n): string => {\n  const name = lead.displayName.trim() === '' ? 'Unnamed lead' : lead.displayName;\n  const score = lead.score === null ? 'unscored' : String(lead.score);\n\n  return `${eventType} \u00B7 ${name} \u00B7 ${score}`;\n};\n", "/**\n * Numaya Greenlight \u2014 the decision half of the suppression / block list.\n *\n * Everything in this file is pure: no SDK import, no network, no clock read.\n * The two logic functions are thin shells that fetch state, call `decideSuppress`\n * or `decideUnsuppress`, and execute whatever they are told. Same split, and the\n * same reason, as `src/gate/release-decision.ts`: this is the part a regulator\n * would read, so it has to be exhaustively testable without a live workspace.\n *\n * ## Why Greenlight owns this data at all\n *\n * Twenty does no email management. There is no unsubscribe handling, no bounce\n * tracking and no do-not-contact list anywhere in the product. Greenlight's\n * compliance rule used to read an opt-out flag purely through the customer's\n * Layer 3 field mapping (`optOutFieldNames`), and on a stock workspace that\n * mapping resolves to nothing at all \u2014 so the rule returned `pass`, awarded its\n * full 15 points, and could never once fire. The one compliance check in the\n * product was decorative on a default install.\n *\n * The four `greenlightSuppress*` fields on Person are the fix. They are the\n * register Twenty does not have, they are Greenlight's own, and \u2014 unlike the\n * score fields \u2014 they are UI-editable, because they are human-maintained data\n * rather than engine output.\n *\n * ## Union, not override\n *\n * A customer who already has an opt-out column keeps it working. The compliance\n * rule reads Greenlight's field *and* the customer's mapping and any source\n * saying \"suppressed\" wins. Nothing in this file assumes Greenlight's field is\n * the only one; it is only the only one Greenlight can write.\n */\n\n/* -------------------------------------------------------------------------- */\n/* The Person fields this feature owns                                         */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The suppression columns on the lead-bearing object.\n *\n * Declared here rather than in `scoring-run.ts` because both the scoring side\n * (which must re-score when they change) and the two action logic functions\n * (which write them) need the same strings, and a drift between the two is an\n * infinite loop or a silently ignored block, neither of which shows up in a\n * type error.\n */\nexport const SUPPRESSION_FIELD_SUPPRESSED = 'greenlightSuppressed';\nexport const SUPPRESSION_FIELD_REASON = 'greenlightSuppressionReason';\nexport const SUPPRESSION_FIELD_SUPPRESSED_AT = 'greenlightSuppressedAt';\nexport const SUPPRESSION_FIELD_NOTE = 'greenlightSuppressionNote';\n\nexport const SUPPRESSION_PERSON_FIELDS: readonly string[] = [\n  SUPPRESSION_FIELD_SUPPRESSED,\n  SUPPRESSION_FIELD_REASON,\n  SUPPRESSION_FIELD_SUPPRESSED_AT,\n  SUPPRESSION_FIELD_NOTE,\n];\n\n/**\n * The subset whose change can move the score, i.e. the ones the re-score trigger\n * listens on.\n *\n * `greenlightSuppressedAt` and `greenlightSuppressionNote` are excluded: neither\n * is read by any rule, both are always written in the same mutation as the flag,\n * and every name on a trigger allow-list is another chance for a write loop.\n */\nexport const SUPPRESSION_SCORING_INPUT_FIELDS: readonly string[] = [\n  SUPPRESSION_FIELD_SUPPRESSED,\n  SUPPRESSION_FIELD_REASON,\n];\n\n/* -------------------------------------------------------------------------- */\n/* Why someone is suppressed                                                   */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The closed list of suppression reasons.\n *\n * Closed rather than free text for the same reason the release override's reason\n * list is closed (see `release-decision.ts`), plus one this list has and that one\n * does not: these categories carry different *legal* weight. \"Unsubscribed\" and\n * \"legal or erasure request\" are both do-not-contact, but only one of them\n * survives a change of marketing platform, and only one of them makes lifting the\n * suppression a decision somebody senior should be making. A free-text column\n * cannot be filtered, counted or escalated on.\n *\n * The list is deliberately the union of the three sources of suppression a\n * mailing operation actually has \u2014 the contact's own instruction (unsubscribed,\n * spam complaint, legal request), the mail system's verdict (hard bounce), and a\n * human's or an import's judgement (manual block, imported do-not-contact list).\n *\n * Appending to this list is safe: SELECT option identity derives from the option\n * `value`, not from `position`. Editing or removing a `code` is not.\n */\nexport const SUPPRESSION_REASONS = [\n  {\n    code: 'UNSUBSCRIBED',\n    label: 'Unsubscribed',\n    hint: 'They used an unsubscribe link, replied STOP, or asked a rep to stop emailing them.',\n  },\n  {\n    code: 'HARD_BOUNCE',\n    label: 'Hard bounce',\n    hint: 'The mailbox does not exist. Sending again damages the sending domain.',\n  },\n  {\n    code: 'SPAM_COMPLAINT',\n    label: 'Spam complaint',\n    hint: 'They reported a message as spam. The most expensive signal on this list.',\n  },\n  {\n    code: 'MANUAL_BLOCK',\n    label: 'Manual block',\n    hint: 'A person decided not to contact this record. Say why in the note.',\n  },\n  {\n    code: 'LEGAL_REQUEST',\n    label: 'Legal or erasure request',\n    hint: 'A GDPR erasure or objection request, or anything counsel has told us to honour.',\n  },\n  {\n    code: 'IMPORTED_DNC',\n    label: 'Imported do-not-contact list',\n    hint: 'Came in on a suppression file \u2014 a previous CRM, a partner list, a TPS-style register.',\n  },\n] as const;\n\nexport type SuppressionReasonCode = (typeof SUPPRESSION_REASONS)[number]['code'];\n\nexport const SUPPRESSION_REASON_CODES: readonly SuppressionReasonCode[] =\n  SUPPRESSION_REASONS.map((reason) => reason.code);\n\nconst findSuppressionReason = (code: string) =>\n  SUPPRESSION_REASONS.find((reason) => reason.code === code) ?? null;\n\nexport const suppressionReasonLabel = (code: string | null): string => {\n  if (code === null || code.trim() === '') {\n    return 'no reason recorded';\n  }\n\n  return findSuppressionReason(code.trim().toUpperCase())?.label ?? code.trim();\n};\n\n/* -------------------------------------------------------------------------- */\n/* Why a suppression is being lifted                                           */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The closed list of *removal* reasons \u2014 a separate vocabulary on purpose.\n *\n * Removing a suppression re-enables contact with somebody who is on record\n * asking not to be contacted. There is no reason on the list above that is also\n * a good reason to undo it, so sharing one list would let a rep lift a spam\n * complaint by picking \"spam complaint\", which reads in the audit trail as\n * nonsense a year later.\n *\n * Every entry here is a claim that the *suppression itself* was wrong or has\n * been superseded. That is the only honest ground for removal.\n */\nexport const UNSUPPRESSION_REASONS = [\n  {\n    code: 'RECORDED_IN_ERROR',\n    label: 'The suppression was recorded in error',\n    hint: 'Nobody asked us to stop; the flag was set by mistake or by a bad import.',\n  },\n  {\n    code: 'CONSENT_RENEWED',\n    label: 'They have opted back in',\n    hint: 'They asked to start hearing from us again. Say where that is evidenced.',\n  },\n  {\n    code: 'WRONG_PERSON',\n    label: 'It was applied to the wrong record',\n    hint: 'A duplicate or a namesake was suppressed instead of the person who asked.',\n  },\n  {\n    code: 'BOUNCE_RESOLVED',\n    label: 'The bounce was a mail-server fault and is fixed',\n    hint: 'Only for hard bounces. Never for an unsubscribe or a complaint.',\n  },\n  {\n    code: 'TESTING',\n    label: 'Testing Greenlight',\n    hint: 'Keeps test removals out of the real compliance statistics.',\n  },\n] as const;\n\nexport type UnsuppressionReasonCode =\n  (typeof UNSUPPRESSION_REASONS)[number]['code'];\n\nexport const UNSUPPRESSION_REASON_CODES: readonly UnsuppressionReasonCode[] =\n  UNSUPPRESSION_REASONS.map((reason) => reason.code);\n\nconst findUnsuppressionReason = (code: string) =>\n  UNSUPPRESSION_REASONS.find((reason) => reason.code === code) ?? null;\n\n/* -------------------------------------------------------------------------- */\n/* Notes                                                                       */\n/* -------------------------------------------------------------------------- */\n\nexport const MAX_SUPPRESSION_NOTE_LENGTH = 500;\n\n/**\n * A removal note is **mandatory**, and a suppression note is not.\n *\n * This is the opposite of the release override's policy, and deliberately so.\n * The argument against mandatory free text is that a high-frequency action\n * collects the word \"ok\"; releasing a gated lead is a daily action and the\n * closed reason list carries the meaning. Lifting a do-not-contact entry is\n * rare, individually consequential, and the one thing a DPO will ask to see the\n * working for. Friction is the point.\n */\nexport const MIN_UNSUPPRESSION_NOTE_LENGTH = 10;\n\n/* -------------------------------------------------------------------------- */\n/* Requests                                                                    */\n/* -------------------------------------------------------------------------- */\n\n/** Mirrors `SUPPORTED_LEAD_OBJECTS` in `release-decision.ts`, and for the same reason. */\nexport const SUPPRESSION_SUPPORTED_LEAD_OBJECTS: readonly string[] = ['person'];\n\nconst UUID =\n  /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\n\nconst asString = (value: unknown): string | null =>\n  typeof value === 'string' ? value : null;\n\nexport interface SuppressRequest {\n  readonly leadObjectNameSingular: string;\n  readonly recordId: string;\n  readonly reasonCode: SuppressionReasonCode;\n  /** Empty string when nothing was added. */\n  readonly note: string;\n}\n\nexport interface UnsuppressRequest {\n  readonly leadObjectNameSingular: string;\n  readonly recordId: string;\n  readonly reasonCode: UnsuppressionReasonCode;\n  /** Never empty \u2014 see `MIN_UNSUPPRESSION_NOTE_LENGTH`. */\n  readonly note: string;\n  /** The reviewer ticked \"I understand this re-enables contact\". */\n  readonly acknowledged: true;\n}\n\nexport type ParseResult<T> =\n  | { readonly ok: true; readonly request: T }\n  | { readonly ok: false; readonly message: string };\n\ninterface CommonFields {\n  readonly leadObjectNameSingular: string;\n  readonly recordId: string;\n  readonly note: string;\n}\n\nconst parseCommon = (body: unknown): ParseResult<CommonFields> => {\n  if (typeof body !== 'object' || body === null || Array.isArray(body)) {\n    return { ok: false, message: 'Request body must be an object.' };\n  }\n\n  const raw = body as Record<string, unknown>;\n  const recordId = asString(raw['recordId'])?.trim() ?? '';\n\n  if (!UUID.test(recordId)) {\n    return { ok: false, message: 'This request needs a valid recordId.' };\n  }\n\n  const leadObjectNameSingular =\n    asString(raw['leadObjectNameSingular'])?.trim() || 'person';\n\n  if (!SUPPRESSION_SUPPORTED_LEAD_OBJECTS.includes(leadObjectNameSingular)) {\n    return {\n      ok: false,\n      message: `Greenlight v0.1 keeps its block list on ${SUPPRESSION_SUPPORTED_LEAD_OBJECTS.join(\n        ', ',\n      )}, not ${leadObjectNameSingular}.`,\n    };\n  }\n\n  const note = (asString(raw['note']) ?? '')\n    .trim()\n    .slice(0, MAX_SUPPRESSION_NOTE_LENGTH);\n\n  return { ok: true, request: { leadObjectNameSingular, recordId, note } };\n};\n\nexport const parseSuppressRequest = (\n  body: unknown,\n): ParseResult<SuppressRequest> => {\n  const common = parseCommon(body);\n\n  if (!common.ok) {\n    return common;\n  }\n\n  const raw = body as Record<string, unknown>;\n  const reason = findSuppressionReason(\n    asString(raw['reasonCode'])?.trim().toUpperCase() ?? '',\n  );\n\n  if (reason === null) {\n    return {\n      ok: false,\n      message:\n        'Choose why this person must not be contacted. A block list with no recorded reasons cannot be audited, cleaned up, or defended.',\n    };\n  }\n\n  return {\n    ok: true,\n    request: { ...common.request, reasonCode: reason.code },\n  };\n};\n\nexport const parseUnsuppressRequest = (\n  body: unknown,\n): ParseResult<UnsuppressRequest> => {\n  const common = parseCommon(body);\n\n  if (!common.ok) {\n    return common;\n  }\n\n  const raw = body as Record<string, unknown>;\n  const reason = findUnsuppressionReason(\n    asString(raw['reasonCode'])?.trim().toUpperCase() ?? '',\n  );\n\n  if (reason === null) {\n    return {\n      ok: false,\n      message:\n        'Choose why this suppression should be lifted. Every reason on the list is a claim that the suppression itself was wrong or has been superseded \u2014 if none of them is true, do not lift it.',\n    };\n  }\n\n  if (common.request.note.length < MIN_UNSUPPRESSION_NOTE_LENGTH) {\n    return {\n      ok: false,\n      message: `Write down what happened, in at least ${MIN_UNSUPPRESSION_NOTE_LENGTH} characters. Removing someone from the block list re-enables contact with a person who asked us to stop, and the note is the only place the evidence for that lives.`,\n    };\n  }\n\n  if (raw['acknowledged'] !== true) {\n    return {\n      ok: false,\n      message:\n        'Confirm that you understand this re-enables contact with this person.',\n    };\n  }\n\n  return {\n    ok: true,\n    request: {\n      ...common.request,\n      reasonCode: reason.code,\n      acknowledged: true,\n    },\n  };\n};\n\n/* -------------------------------------------------------------------------- */\n/* Current state                                                               */\n/* -------------------------------------------------------------------------- */\n\nexport interface CurrentSuppressionState {\n  readonly recordId: string;\n  /** Denormalised into the audit row so the trail survives the record. */\n  readonly displayName: string;\n  readonly suppressed: boolean;\n  /** Raw `greenlightSuppressionReason`. Null when nothing is recorded. */\n  readonly reasonCode: string | null;\n  /** Raw `greenlightSuppressedAt` as stored. Null when nothing is recorded. */\n  readonly suppressedAt: string | null;\n  readonly note: string;\n  /** Raw `greenlightDecision`, for the audit row's decision column. */\n  readonly decision: string | null;\n  readonly score: number | null;\n  /**\n   * Raw `greenlightTrace`, read only for the audit row's band column \u2014 see\n   * `auditBandValue` in `release-decision.ts`. Optional because nothing in this\n   * module's decision logic reads it: a suppression is decided from the flag and\n   * the reason, never from a score, and a caller that cannot supply a trace must\n   * still be able to suppress somebody.\n   */\n  readonly trace?: unknown;\n}\n\n/**\n * Is this record on the block list *as far as Greenlight's own fields go*?\n *\n * A recorded reason with the flag cleared counts as suppressed, and that is not\n * a bug. Half-clearing \u2014 untick the box, leave `SPAM_COMPLAINT` sitting in the\n * reason column \u2014 is the realistic hand-edit, and reading it as \"not suppressed\"\n * would silently re-enable outreach to somebody whose record still states, in\n * writing, why we stopped. The compliance rule takes the same view; see\n * `src/scoring/rules/compliance-opt-out.rule.ts`.\n */\nexport const isSuppressedState = (state: {\n  readonly suppressed: boolean;\n  readonly reasonCode: string | null;\n}): boolean =>\n  state.suppressed || (state.reasonCode ?? '').trim().length > 0;\n\n/* -------------------------------------------------------------------------- */\n/* Audit vocabulary                                                            */\n/* -------------------------------------------------------------------------- */\n\n/** New `GreenlightAuditLog.eventType` options \u2014 see `src/objects/greenlight-audit-log.ts`. */\nexport const AUDIT_EVENT_SUPPRESSED = 'SUPPRESSED';\nexport const AUDIT_EVENT_UNSUPPRESSED = 'UNSUPPRESSED';\n\nexport type SuppressionAuditEventType =\n  | typeof AUDIT_EVENT_SUPPRESSED\n  | typeof AUDIT_EVENT_UNSUPPRESSED;\n\n/** The audit-trail rendering of a reason: machine code first, prose after. */\nexport const formatSuppressionReason = (\n  reasonCode: string,\n  note: string,\n): string => {\n  const label =\n    findSuppressionReason(reasonCode)?.label ??\n    findUnsuppressionReason(reasonCode)?.label ??\n    null;\n  const rendered = label === null ? reasonCode : `${reasonCode} \u00B7 ${label}`;\n\n  return note === '' ? rendered : `${rendered} \u2014 ${note}`;\n};\n\n/** One-line summary used as the audit row's record label. */\nexport const buildSuppressionAuditSummary = (\n  eventType: SuppressionAuditEventType,\n  displayName: string,\n  reasonCode: string,\n): string => {\n  const name = displayName.trim() === '' ? 'Unnamed lead' : displayName.trim();\n\n  return `${eventType} \u00B7 ${name} \u00B7 ${reasonCode}`;\n};\n\n/* -------------------------------------------------------------------------- */\n/* The decisions                                                               */\n/* -------------------------------------------------------------------------- */\n\nexport type SuppressOutcome =\n  /** Newly added to the block list. */\n  | 'suppressed'\n  /** Already on the list under the same reason. Nothing written, nothing logged. */\n  | 'already_suppressed'\n  /** Already on the list under a different reason; the reason was amended. */\n  | 'reason_amended';\n\nexport type UnsuppressOutcome =\n  /** Removed from the block list. */\n  | 'unsuppressed'\n  /** Was not on the list. Nothing written, nothing logged. */\n  | 'not_suppressed';\n\nexport interface SuppressionWrite {\n  readonly [field: string]: string | boolean | null;\n}\n\nexport interface SuppressionDecision<TOutcome extends string> {\n  readonly outcome: TOutcome;\n  /** Shown to the reviewer. Written for a salesperson, not a developer. */\n  readonly message: string;\n  /** The Person patch to apply, or null when nothing changes. */\n  readonly write: SuppressionWrite | null;\n  readonly shouldWriteAuditRow: boolean;\n  readonly auditEventType: SuppressionAuditEventType | null;\n  /** Rendered reason for the audit row. Empty when no row is written. */\n  readonly auditReason: string;\n}\n\nexport interface DecideSuppressInput {\n  readonly request: SuppressRequest;\n  readonly lead: CurrentSuppressionState;\n  /** The clock, passed in \u2014 this module never reads it. */\n  readonly now: Date;\n}\n\nexport interface DecideUnsuppressInput {\n  readonly request: UnsuppressRequest;\n  readonly lead: CurrentSuppressionState;\n  readonly now: Date;\n}\n\n/**\n * Decide what adding to the block list should do. Never throws.\n *\n * Three cases, and the amendment case is the interesting one. A record already\n * suppressed as `MANUAL_BLOCK` that turns out to be a `LEGAL_REQUEST` must be\n * correctable *without* passing through an unsuppressed state \u2014 a remove-then-add\n * would re-enable contact for as long as it took somebody to do the second half,\n * and would put a spurious `UNSUPPRESSED` row in the compliance trail.\n *\n * `greenlightSuppressedAt` is **not** rewritten on an amendment. That column\n * answers \"since when has this person been off-limits\", and the answer does not\n * change because we relabelled why.\n */\nexport const decideSuppress = (\n  input: DecideSuppressInput,\n): SuppressionDecision<SuppressOutcome> => {\n  const { request, lead, now } = input;\n  const currentReason = (lead.reasonCode ?? '').trim().toUpperCase();\n  const alreadyOnList = isSuppressedState(lead);\n\n  if (alreadyOnList && currentReason === request.reasonCode) {\n    return {\n      outcome: 'already_suppressed',\n      message: `${\n        lead.displayName.trim() === '' ? 'This lead' : lead.displayName.trim()\n      } is already on the Greenlight block list for the same reason. Nothing changed, and no second audit entry was written.`,\n      write: null,\n      shouldWriteAuditRow: false,\n      auditEventType: null,\n      auditReason: '',\n    };\n  }\n\n  if (alreadyOnList) {\n    return {\n      outcome: 'reason_amended',\n      message: `Already blocked \u2014 the recorded reason has been changed from ${suppressionReasonLabel(\n        currentReason === '' ? null : currentReason,\n      )} to ${suppressionReasonLabel(\n        request.reasonCode,\n      )}. The original block date is unchanged and the amendment is on record.`,\n      write: {\n        [SUPPRESSION_FIELD_SUPPRESSED]: true,\n        [SUPPRESSION_FIELD_REASON]: request.reasonCode,\n        [SUPPRESSION_FIELD_NOTE]: request.note,\n      },\n      shouldWriteAuditRow: true,\n      auditEventType: AUDIT_EVENT_SUPPRESSED,\n      auditReason: `AMENDED from ${\n        currentReason === '' ? 'no recorded reason' : currentReason\n      } \u2014 ${formatSuppressionReason(request.reasonCode, request.note)}`,\n    };\n  }\n\n  return {\n    outcome: 'suppressed',\n    message:\n      'Added to the Greenlight block list. This person will not be contacted, the lead has been re-scored, and the block is on record.',\n    write: {\n      [SUPPRESSION_FIELD_SUPPRESSED]: true,\n      [SUPPRESSION_FIELD_REASON]: request.reasonCode,\n      [SUPPRESSION_FIELD_SUPPRESSED_AT]: now.toISOString(),\n      [SUPPRESSION_FIELD_NOTE]: request.note,\n    },\n    shouldWriteAuditRow: true,\n    auditEventType: AUDIT_EVENT_SUPPRESSED,\n    auditReason: formatSuppressionReason(request.reasonCode, request.note),\n  };\n};\n\n/**\n * Decide what removing from the block list should do. Never throws.\n *\n * Every field is cleared, not just the flag. Leaving the reason behind would\n * leave the record asserting \"spam complaint\" while claiming to be contactable,\n * and the compliance rule (correctly) reads a lingering reason as still\n * suppressed \u2014 so a partial clear would look like it worked and quietly do\n * nothing.\n *\n * The audit row records the reason the person was suppressed *under*, not just\n * the reason given for lifting it. \"Which legal-request suppressions has this\n * workspace lifted, and who lifted them\" is the question this trail exists to\n * answer, and it is unanswerable from the removal reason alone.\n */\nexport const decideUnsuppress = (\n  input: DecideUnsuppressInput,\n): SuppressionDecision<UnsuppressOutcome> => {\n  const { request, lead } = input;\n\n  if (!isSuppressedState(lead)) {\n    return {\n      outcome: 'not_suppressed',\n      message:\n        'This person is not on the Greenlight block list, so there was nothing to remove. No change made, and no audit entry written.',\n      write: null,\n      shouldWriteAuditRow: false,\n      auditEventType: null,\n      auditReason: '',\n    };\n  }\n\n  const originalReason =\n    (lead.reasonCode ?? '').trim() === ''\n      ? 'no recorded reason'\n      : (lead.reasonCode ?? '').trim().toUpperCase();\n\n  return {\n    outcome: 'unsuppressed',\n    message:\n      'Removed from the Greenlight block list. Contact is re-enabled, the lead has been re-scored, and your reason is on record and cannot be edited afterwards.',\n    write: {\n      [SUPPRESSION_FIELD_SUPPRESSED]: false,\n      [SUPPRESSION_FIELD_REASON]: null,\n      [SUPPRESSION_FIELD_SUPPRESSED_AT]: null,\n      [SUPPRESSION_FIELD_NOTE]: '',\n    },\n    shouldWriteAuditRow: true,\n    auditEventType: AUDIT_EVENT_UNSUPPRESSED,\n    auditReason: `LIFTED a ${originalReason} suppression recorded ${\n      lead.suppressedAt ?? 'at an unrecorded time'\n    } \u2014 ${formatSuppressionReason(request.reasonCode, request.note)}`,\n  };\n};\n", "/**\n * The I/O half of the block-list actions, kept out of the two `define*` files so\n * it can be exercised against a fake API client.\n *\n * Every rule about what may be suppressed, what may be lifted and what gets\n * recorded lives in `src/gate/suppression-decision.ts`, which is pure. This\n * module reads the lead, calls the decision, executes it, and appends the audit\n * row. The only impure things it touches are the client and the `now` it is\n * handed; there is no `new Date()` and no `new CoreApiClient()` below.\n *\n * ## Ordering: record first, write second\n *\n * The audit row is appended **before** the Person is written, which is the same\n * order `scoring-run.ts` uses and for the same reason. If the audit write\n * succeeds and the record write fails, the trail over-records \u2014 an entry exists\n * for a change that did not land, and the reviewer is told exactly that.\n * Reversed, a successful record write followed by a failed audit write would\n * leave somebody silently on or off a do-not-contact list with nothing saying\n * who put them there. An append-only trail may repeat itself; it may not lose an\n * entry.\n *\n * A failed audit write is therefore a *clean refusal*: nothing has touched the\n * lead yet, so the action is abandoned rather than performed unlogged.\n *\n * ## The re-score is not this function's job\n *\n * Writing `greenlightSuppressed` emits `person.updated`, and that field is on\n * `SCORING_TRIGGER_PERSON_FIELDS`, so the platform re-scores the lead on its own\n * and the engine writes the resulting `greenlightDecision`. This module\n * deliberately does not write that column: the gate decision is the engine's to\n * make, and two writers on one decision column is how a decision starts\n * disagreeing with the trace that is supposed to explain it.\n *\n * ## Both actions share one body\n *\n * Add and remove differ only in which parser and which decision function run.\n * Everything after that \u2014 the audit row, the ordering, the failure reporting \u2014\n * is identical, deliberately, because it is far too easy to let the rarer of two\n * near-identical paths quietly become the less well audited one. The asymmetry\n * that *should* exist (a mandatory note and an explicit acknowledgement to\n * remove someone) lives in the pure module, where it is testable.\n */\n\nimport { auditBandValue } from 'src/gate/release-decision';\nimport {\n  buildSuppressionAuditSummary,\n  decideSuppress,\n  decideUnsuppress,\n  parseSuppressRequest,\n  parseUnsuppressRequest,\n  SUPPRESSION_FIELD_NOTE,\n  SUPPRESSION_FIELD_REASON,\n  SUPPRESSION_FIELD_SUPPRESSED,\n  SUPPRESSION_FIELD_SUPPRESSED_AT,\n  type CurrentSuppressionState,\n  type ParseResult,\n  type SuppressionDecision,\n} from 'src/gate/suppression-decision';\nimport {\n  describeError,\n  isPlainRecord,\n  logGreenlight,\n  readConnectionNodes,\n  type GreenlightApiClient,\n} from 'src/logic-functions/greenlight-api';\n\n/* -------------------------------------------------------------------------- */\n/* Reads                                                                       */\n/* -------------------------------------------------------------------------- */\n\n/** The Person columns both actions need in order to decide anything. */\nexport const suppressionLeadSelection = (): Record<string, unknown> => ({\n  id: true,\n  name: { firstName: true, lastName: true },\n  greenlightDecision: true,\n  greenlightScore: true,\n  // Read for one reason: the audit row's band. Without it a SUPPRESSED row\n  // carried a real score beside a defaulted \"Unscored\" band, contradicting the\n  // GATED row for the same lead \u2014 the same defect the release path had.\n  greenlightTrace: true,\n  [SUPPRESSION_FIELD_SUPPRESSED]: true,\n  [SUPPRESSION_FIELD_REASON]: true,\n  [SUPPRESSION_FIELD_SUPPRESSED_AT]: true,\n  [SUPPRESSION_FIELD_NOTE]: true,\n});\n\nconst asTrimmedString = (value: unknown): string | null => {\n  if (typeof value !== 'string') {\n    return null;\n  }\n\n  const trimmed = value.trim();\n\n  return trimmed.length === 0 ? null : trimmed;\n};\n\n/**\n * `Person.name` is a FULL_NAME composite. Local rather than imported from\n * `scoring-run.ts` so these two functions do not bundle the whole scoring engine\n * to render one label \u2014 the same reason `release-lead.logic-function.ts` carries\n * its own copy.\n */\nconst displayNameOf = (node: Record<string, unknown>): string => {\n  const name = node['name'];\n\n  if (isPlainRecord(name)) {\n    const parts = [name['firstName'], name['lastName']]\n      .map((part) => (typeof part === 'string' ? part.trim() : ''))\n      .filter((part) => part.length > 0);\n\n    if (parts.length > 0) {\n      return parts.join(' ');\n    }\n  }\n\n  return typeof node['id'] === 'string' ? node['id'] : 'Unknown lead';\n};\n\nexport const toSuppressionState = (\n  recordId: string,\n  node: Record<string, unknown>,\n): CurrentSuppressionState => ({\n  recordId,\n  displayName: displayNameOf(node),\n  suppressed: node[SUPPRESSION_FIELD_SUPPRESSED] === true,\n  reasonCode: asTrimmedString(node[SUPPRESSION_FIELD_REASON]),\n  suppressedAt: asTrimmedString(node[SUPPRESSION_FIELD_SUPPRESSED_AT]),\n  note: asTrimmedString(node[SUPPRESSION_FIELD_NOTE]) ?? '',\n  decision: asTrimmedString(node['greenlightDecision']),\n  score:\n    typeof node['greenlightScore'] === 'number' ? node['greenlightScore'] : null,\n  trace: node['greenlightTrace'] ?? null,\n});\n\nconst fetchLead = async (\n  client: GreenlightApiClient,\n  recordId: string,\n): Promise<CurrentSuppressionState | null> => {\n  const response = await client.query({\n    people: {\n      __args: { filter: { id: { eq: recordId } }, first: 1 },\n      edges: { node: suppressionLeadSelection() },\n    },\n  });\n\n  const [node] = readConnectionNodes(response, 'people');\n\n  return node === undefined ? null : toSuppressionState(recordId, node);\n};\n\n/* -------------------------------------------------------------------------- */\n/* The run                                                                     */\n/* -------------------------------------------------------------------------- */\n\nexport interface SuppressionRunInput {\n  readonly client: GreenlightApiClient;\n  /** The parsed HTTP body, untyped on purpose. */\n  readonly body: unknown;\n  readonly now: Date;\n  /** Server-derived. Never taken from the request body. */\n  readonly actorDisplayName: string;\n}\n\nexport interface SuppressionRunResult {\n  readonly status: number;\n  readonly body: {\n    readonly outcome: string;\n    readonly changed: boolean;\n    readonly message: string;\n    readonly auditWritten: boolean;\n  };\n}\n\ninterface IdentifiedRequest {\n  readonly leadObjectNameSingular: string;\n  readonly recordId: string;\n}\n\ninterface ActionSpec<TRequest extends IdentifiedRequest, TOutcome extends string> {\n  readonly logEvent: string;\n  parse(body: unknown): ParseResult<TRequest>;\n  decide(input: {\n    request: TRequest;\n    lead: CurrentSuppressionState;\n    now: Date;\n  }): SuppressionDecision<TOutcome>;\n}\n\nconst refuse = (\n  outcome: string,\n  message: string,\n  status: number,\n): SuppressionRunResult => ({\n  status,\n  body: { outcome, changed: false, message, auditWritten: false },\n});\n\nconst run = async <TRequest extends IdentifiedRequest, TOutcome extends string>(\n  { client, body, now, actorDisplayName }: SuppressionRunInput,\n  spec: ActionSpec<TRequest, TOutcome>,\n): Promise<SuppressionRunResult> => {\n  const parsed = spec.parse(body);\n\n  if (!parsed.ok) {\n    return refuse('invalid_request', parsed.message, 400);\n  }\n\n  const request = parsed.request;\n\n  let lead: CurrentSuppressionState | null;\n\n  try {\n    lead = await fetchLead(client, request.recordId);\n  } catch (error) {\n    return refuse(\n      'lookup_failed',\n      `Greenlight could not read this lead, so nothing was changed. Try again. (${describeError(\n        error,\n      )})`,\n      502,\n    );\n  }\n\n  if (lead === null) {\n    return refuse('record_not_found', 'That lead no longer exists.', 404);\n  }\n\n  const decision = spec.decide({ request, lead, now });\n\n  if (decision.write === null) {\n    return {\n      status: 200,\n      body: {\n        outcome: decision.outcome,\n        changed: false,\n        message: decision.message,\n        auditWritten: false,\n      },\n    };\n  }\n\n  let auditWritten = false;\n\n  if (decision.shouldWriteAuditRow && decision.auditEventType !== null) {\n    try {\n      await client.mutation({\n        createGreenlightAuditLog: {\n          __args: {\n            data: {\n              name: buildSuppressionAuditSummary(\n                decision.auditEventType,\n                lead.displayName,\n                String(decision.write[SUPPRESSION_FIELD_REASON] ?? 'CLEARED'),\n              ),\n              eventType: decision.auditEventType,\n              occurredAt: now.toISOString(),\n              leadObjectNameSingular: request.leadObjectNameSingular,\n              leadRecordId: request.recordId,\n              leadDisplayName: lead.displayName,\n              actorType: 'HUMAN',\n              actorDisplayName,\n              ruleTrace: { rules: [] },\n              score: lead.score,\n              // Same rule as the release path: the band comes out of the trace\n              // the scoring run recorded, so one lead cannot end up with two\n              // bands across its own audit rows.\n              band: auditBandValue(lead.trace),\n              // The state the record was in when the human acted. The re-score\n              // this write triggers is what moves it, and that run appends its\n              // own row \u2014 this one must not pre-empt a decision it did not make.\n              decision: lead.decision ?? 'UNSCORED',\n              overrideReason: decision.auditReason,\n              traceId: `${decision.auditEventType.toLowerCase()}-${request.recordId}-${now.getTime()}`,\n            },\n          },\n          id: true,\n        },\n      });\n\n      auditWritten = true;\n    } catch (error) {\n      logGreenlight('suppression_audit_write_failed', {\n        leadRecordId: request.recordId,\n        action: spec.logEvent,\n        error: describeError(error),\n      });\n\n      return refuse(\n        'audit_failed',\n        'Greenlight could not write the audit entry, so it left the record alone too. An unrecorded change to a do-not-contact list is worse than no change at all \u2014 try again.',\n        502,\n      );\n    }\n  }\n\n  try {\n    await client.mutation({\n      updatePerson: {\n        __args: { id: request.recordId, data: { ...decision.write } },\n        id: true,\n      },\n    });\n  } catch (error) {\n    logGreenlight('suppression_write_failed', {\n      leadRecordId: request.recordId,\n      action: spec.logEvent,\n      auditWritten,\n      error: describeError(error),\n    });\n\n    return {\n      status: 502,\n      body: {\n        outcome: 'write_failed',\n        changed: false,\n        message:\n          'Greenlight recorded this action in the audit log but could not write it to the record, so it is not in force. Open the lead and check it before relying on either state.',\n        auditWritten,\n      },\n    };\n  }\n\n  logGreenlight(spec.logEvent, {\n    leadRecordId: request.recordId,\n    outcome: decision.outcome,\n    actorDisplayName,\n  });\n\n  return {\n    status: 200,\n    body: {\n      outcome: decision.outcome,\n      changed: true,\n      message: decision.message,\n      auditWritten,\n    },\n  };\n};\n\nexport const runSuppressLead = (\n  input: SuppressionRunInput,\n): Promise<SuppressionRunResult> =>\n  run(input, {\n    logEvent: 'lead_suppressed',\n    parse: parseSuppressRequest,\n    decide: decideSuppress,\n  });\n\nexport const runUnsuppressLead = (\n  input: SuppressionRunInput,\n): Promise<SuppressionRunResult> =>\n  run(input, {\n    logEvent: 'lead_unsuppressed',\n    parse: parseUnsuppressRequest,\n    decide: decideUnsuppress,\n  });\n"],
  "mappings": "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",
  "names": ["s", "n", "e", "t", "t", "isRecord", "isRecord", "asString", "asString"]
}
