{
  "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/icp-control.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/icp-identifiers.ts", "../../../../src/scoring/rules/helpers.ts", "../../../../src/icp/icp-command.ts", "../../../../src/logic-functions/actor-identity.ts", "../../../../src/identity/actor.ts", "../../../../src/logic-functions/greenlight-api.ts", "../../../../src/enrichment/field-specs.ts", "../../../../src/scoring/defaults.ts", "../../../../src/scoring/field-access.ts", "../../../../src/scoring/rules/company-identified.rule.ts", "../../../../src/scoring/rules/compliance-opt-out.rule.ts", "../../../../src/scoring/rules/contact-decision-maker.rule.ts", "../../../../src/scoring/rules/contact-email-valid.rule.ts", "../../../../src/scoring/rules/contact-named-person.rule.ts", "../../../../src/scoring/rules/contact-phone-valid.rule.ts", "../../../../src/scoring/rules/contact-shared-inbox.rule.ts", "../../../../src/scoring/rules/data-freshness.rule.ts", "../../../../src/scoring/rules/icp-company-size.rule.ts", "../../../../src/scoring/rules/icp-industry.rule.ts", "../../../../src/scoring/rules/icp-region.rule.ts", "../../../../src/scoring/rules/index.ts", "../../../../src/scoring/types.ts", "../../../../src/scoring/config.ts", "../../../../src/scoring/engine.ts", "../../../../src/backfill/field-mapping-check.ts", "../../../../src/backfill/backfill-plan.ts", "../../../../src/icp/icp-analysis.ts", "../../../../src/logic-functions/greenlight-config-record.ts", "../../../../src/icp/icp-size-bands.ts", "../../../../src/icp/icp-proposal.ts", "../../../../src/icp/icp-status.ts", "../../../../src/icp/icp-impact.ts", "../../../../src/backfill/backfill-state.ts", "../../../../src/licensing/cache.ts", "../../../../src/calibration/state.ts", "../../../../src/gate/release-decision.ts", "../../../../src/gate/suppression-decision.ts", "../../../../src/logic-functions/scoring-run.ts", "../../../../src/logic-functions/backfill-run.ts", "../../../../src/logic-functions/icp-run.ts", "../../../../src/constants/backfill-identifiers.ts", "../../../../src/logic-functions/backfill-state-store.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  ICP_REVIEW_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER,\n  ICP_ROUTE_PATH,\n} from 'src/constants/icp-identifiers';\nimport { parseIcpCommand } from 'src/icp/icp-command';\nimport { resolveActorDisplayName } from 'src/logic-functions/actor-identity';\nimport { runIcpCommand } from 'src/logic-functions/icp-run';\nimport { kvBackfillStateStore } from 'src/logic-functions/backfill-state-store';\n\n/**\n * The admin's handle on the ideal-customer profile: what it is costing to leave\n * it empty, what the workspace's own data suggests it should be, what applying\n * that would do, and \u2014 only then \u2014 applying it.\n *\n * ## Why one route with five actions\n *\n * Because they are five views of one question and the panel has to hold them\n * together. A `propose` that could not also return the impact would put the\n * suggestion and its consequence on two round trips, and a UI that renders a\n * profile before it can render what the profile does is a UI that will be used\n * that way. `status` is the free, side-effect-free default for the same reason\n * the backfill route makes it the default: the two actions that change something\n * both have to be asked for by name, and `apply` additionally has to be\n * confirmed in the body.\n *\n * ## Identity\n *\n * `userWorkspaceId` comes off the authenticated request, so it is server-derived\n * and cannot be asserted by the caller \u2014 the same limitation and the same\n * reasoning as the release, suppression and backfill actions, and\n * `resolveActorDisplayName` turns it into that person's name without ever\n * reading the body. It is what lands in the audit row, so \"who narrowed the\n * profile that started gating half the pipeline\" has an answer with a name in\n * it.\n *\n * Resolved once here and carried through `requestedBy`. An `apply` can queue a\n * re-score of the whole workspace behind it, and every row that run writes reads\n * this one string rather than asking again.\n *\n * ## Timeout\n *\n * Sixty seconds, against a documented worst case of 32 requests for a `propose`\n * (fourteen schema probes, ten pages of companies, five of people, two counts and\n * a config read). The other actions cost 1 to 8. Sixty is roughly four times the\n * expected wall-clock and still fails while somebody is looking at the panel,\n * which is the property that matters: a hung propose has to end in an error\n * message, not in a spinner nobody can interpret.\n */\nconst handler = async (event: RoutePayload) => {\n  const parsed = parseIcpCommand(event.body);\n\n  if (!parsed.ok) {\n    return new Response(\n      {\n        outcome: 'invalid_request',\n        message: parsed.message,\n        status: null,\n        proposal: null,\n        impact: null,\n        selection: null,\n        warnings: [],\n        notes: [],\n        rescore: null,\n      },\n      { status: 400 },\n    );\n  }\n\n  const outcome = await runIcpCommand(parsed.command, {\n    client: new CoreApiClient(),\n    store: kvBackfillStateStore,\n    now: new Date(),\n    requestedBy: await resolveActorDisplayName(event),\n  });\n\n  return new Response(outcome.body, { status: outcome.status });\n};\n\nexport default defineLogicFunction({\n  universalIdentifier: ICP_REVIEW_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER,\n  name: 'greenlight-icp-review',\n  description:\n    'Reports how much of the Greenlight scoring model is idle for want of an ideal-customer profile, proposes one from the workspace\u2019s own companies, previews what applying it would change, and applies it.',\n  timeoutSeconds: 60,\n  handler,\n  httpRouteTriggerSettings: {\n    path: ICP_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 and route paths for the ICP review surface \u2014 the thing\n * that tells an admin their ideal-customer profile is empty *before* they start\n * trusting the scores.\n *\n * Same permanence rule as the other four constant files: every value here is\n * written into the customer's workspace at install time, so changing one does\n * not rename an entity, it orphans the old one and mints a second alongside it.\n * Adding is safe; editing is a breaking change.\n *\n * A fifth constants file rather than a section in one of the others, for the\n * reason the third and fourth exist: so this workstream can add entities without\n * touching a file another workstream is editing.\n * `src/icp/__tests__/icp-identifiers.test.ts` runs the uniqueness guard across\n * all five files at once, which is the failure mode splitting them introduces.\n */\n\n/* -------------------------------------------------------------------------- */\n/* Logic function                                                              */\n/* -------------------------------------------------------------------------- */\n\n/**\n * One authenticated route behind the whole surface: read the inert state,\n * derive a proposal from the workspace's own data, preview what applying it\n * would do, apply it, or record that the workspace deliberately left the profile\n * open.\n *\n * A route rather than a cron for the same reason as the backfill control: these\n * are things a human does at a moment of their choosing, and the panel needs a\n * synchronous answer to render.\n */\nexport const ICP_REVIEW_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER =\n  '4364eab9-7644-4896-9fca-888812048a09';\n\n/* -------------------------------------------------------------------------- */\n/* Admin surface                                                               */\n/* -------------------------------------------------------------------------- */\n\nexport const ICP_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER =\n  '76ed6b40-14dc-43d8-afbb-3403d923516e';\n\nexport const ICP_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIER =\n  '30d6de66-6bb7-44b6-9337-1994e512d701';\n\n/**\n * The sidebar entry, and the standalone page it opens.\n *\n * A page rather than only a command menu item because a command menu item has\n * to be searched for, and nobody searches for a problem they do not know they\n * have. The whole defect is that the inert ICP is invisible; a surface you must\n * already suspect exists does not fix an invisibility bug.\n */\nexport const ICP_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER =\n  '58ef8728-89b9-47a8-9090-159c11b53e28';\n\nexport const ICP_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER =\n  '97e2ff47-d59c-477a-ace1-74fd0f03b591';\n\nexport const ICP_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER =\n  '069e0b7d-fdfa-4f75-87de-f5b79d742ab1';\n\nexport const ICP_PAGE_WIDGET_UNIVERSAL_IDENTIFIER =\n  'c7478dec-4b20-453a-93e6-a4e243194e37';\n\n/* -------------------------------------------------------------------------- */\n/* New GreenlightConfig fields                                                 */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Two fields, and they answer one question the config record could not answer\n * before: *has a human ever decided this?*\n *\n * An empty `icpIndustries` list is ambiguous today. It means either \"nobody has\n * ever looked at this\" or \"we sell to everyone and left it open on purpose\", and\n * those two are the difference between a broken install and a correct one. The\n * shipped permissive default is right; what was missing is any record of whether\n * it was *chosen*. Without that, a warning surface can only nag forever, and a\n * warning that cannot be answered is a warning people learn to ignore.\n *\n * Declared here rather than in `universal-identifiers.ts` alongside the other\n * `GreenlightConfig` fields purely to keep this workstream out of a shared file;\n * they are ordinary fields on that object and nothing else about them is special.\n */\nexport const GREENLIGHT_CONFIG_ICP_REVIEW_FIELD_UNIVERSAL_IDENTIFIERS = {\n  icpReviewedAt: '80a3be71-f484-4a67-9ff9-6c68539bee24',\n  icpReviewDecision: '28562f63-9d60-4048-9c91-5117eca9c8bf',\n} as const;\n\n/** SELECT values for `icpReviewDecision`. Identity derives from the value. */\nexport const ICP_REVIEW_DECISION_VALUES = {\n  notReviewed: 'NOT_REVIEWED',\n  applied: 'APPLIED',\n  leftOpen: 'LEFT_OPEN',\n} as const;\n\n/* -------------------------------------------------------------------------- */\n/* HTTP route                                                                  */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Same two-constant shape as the gate-queue and backfill routes: the front\n * component posts to the `/s`-prefixed path, which is how `RestApiClient`\n * recognises an app route rather than a core REST call, and declaring both here\n * means a mismatch is a test failure rather than a 404 found in a live workspace.\n */\nexport const ICP_ROUTE_PATH = '/greenlight/icp';\nexport const ICP_CLIENT_PATH = `/s${ICP_ROUTE_PATH}`;\n", "/** Small shared primitives for rules. Pure, no I/O, no clock. */\n\nexport const normalise = (value: string): string =>\n  value.trim().toLowerCase().replace(/\\s+/g, ' ');\n\nconst escapeForRegex = (value: string): string =>\n  value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n/**\n * Whole-token keyword match, so \"lead\" does not match \"leadership\" and\n * \"vp\" does not match \"vps\".\n *\n * ## The boundary is Unicode-aware, and it has to be\n *\n * The boundary class is `\\p{L}\\p{N}` \u2014 any letter or number in any script \u2014 not\n * `a-z0-9`. The ASCII form was wrong, and it was wrong in the direction that\n * costs a lead: an accented letter is not in `a-z0-9`, so it counted as a word\n * *separator*, and every accented word silently split into fragments that could\n * match a keyword on their own.\n *\n * Found live, not by reasoning: with the multilingual calibration pack loaded, a\n * lead titled \"S\u00F3cio Propriet\u00E1rio\" matched the keyword **`cio`** \u2014 because the\n * \"\u00F3\" before it read as a boundary. The verdict happened to be right for that\n * title, but the same flaw promotes \"Gerente de Neg\u00F3cio\" to C-level, because\n * \"neg\u00F3cio\" ends in \"cio\" preceded by an accent. A mid-level manager scored as a\n * decision-maker is exactly the twenty-point error this rule exists to avoid.\n *\n * For pure-ASCII input the two forms are identical, which is why every\n * pre-existing test passes unchanged. The difference only appears where the\n * shipped English vocabulary never reached.\n */\nexport const containsKeyword = (haystack: string, keyword: string): boolean => {\n  const needle = normalise(keyword);\n\n  if (needle.length === 0) {\n    return false;\n  }\n\n  const pattern = new RegExp(\n    `(^|[^\\\\p{L}\\\\p{N}])${escapeForRegex(needle)}([^\\\\p{L}\\\\p{N}]|$)`,\n    'iu',\n  );\n\n  return pattern.test(normalise(haystack));\n};\n\nexport const firstKeywordMatch = (\n  haystack: string,\n  keywords: readonly string[],\n): string | null => keywords.find((keyword) => containsKeyword(haystack, keyword)) ?? null;\n\n/** Case-insensitive membership against a configured list. */\nexport const matchesList = (\n  values: readonly string[],\n  allowed: readonly string[],\n): string | null => {\n  const allowedSet = new Set(allowed.map(normalise));\n\n  return values.find((value) => allowedSet.has(normalise(value))) ?? null;\n};\n\nconst EMAIL_PATTERN =\n  /^[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?\\.)+[A-Za-z]{2,}$/;\n\nexport interface ParsedEmail {\n  readonly address: string;\n  readonly localPart: string;\n  readonly domain: string;\n}\n\nexport const parseEmail = (raw: string): ParsedEmail | null => {\n  const address = raw.trim();\n\n  if (address.length === 0 || address.length > 254) {\n    return null;\n  }\n\n  if (!EMAIL_PATTERN.test(address)) {\n    return null;\n  }\n\n  const separator = address.lastIndexOf('@');\n  const localPart = address.slice(0, separator);\n  const domain = address.slice(separator + 1);\n\n  if (localPart.length > 64) {\n    return null;\n  }\n\n  return { address, localPart: localPart.toLowerCase(), domain: domain.toLowerCase() };\n};\n\n/** Strip the local part down to its base, so `sales+eu` is still `sales`. */\nexport const emailLocalRoot = (localPart: string): string => {\n  const withoutTag = localPart.split('+')[0] ?? localPart;\n  return withoutTag.replace(/[._-]+$/, '');\n};\n\nexport const MILLISECONDS_PER_DAY = 86_400_000;\n\nexport const daysBetween = (earlier: Date, later: Date): number =>\n  (later.getTime() - earlier.getTime()) / MILLISECONDS_PER_DAY;\n\nexport const roundTo = (value: number, decimals: number): number => {\n  const factor = 10 ** decimals;\n  return Math.round(value * factor) / factor;\n};\n\nexport const clamp01 = (value: number): number => {\n  if (!Number.isFinite(value)) {\n    return 0;\n  }\n\n  return Math.min(1, Math.max(0, value));\n};\n", "/**\n * Parsing what the ICP panel asked for.\n *\n * Pure and separate from the route, for the same reason\n * `src/backfill/backfill-command.ts` is: what the server accepts is a contract,\n * and a contract asserted by unit tests is a contract. It is also the only place\n * a request body is trusted with anything, and the thing being trusted here is\n * larger than a mode string \u2014 it is the profile that will be written into the\n * record every future score is computed against.\n *\n * Three rules govern the whole file:\n *\n *   1. **The action list is closed.** An unrecognised action is refused, not\n *      defaulted to `status`: a client asking for something that does not exist\n *      has a bug, and answering it with a plausible success is how that bug\n *      reaches production.\n *   2. **`apply` requires `confirm: true` from the server's point of view.** The\n *      panel's checkbox is a courtesy; this is the guard. A one-click apply that\n *      re-gates a sales team's pipeline is exactly what this product exists to\n *      prevent, so \"I meant it\" is part of the request, not part of the UI.\n *   3. **Malformed entries are dropped *and reported*.** Silently discarding\n *      half a profile would write a config the admin did not choose and cannot\n *      see; refusing the whole request over one bad row would make a long\n *      hand-edited list impossible to submit. Dropping with a warning is the\n *      only option that leaves the human able to tell what happened.\n */\n\nimport { normalise } from 'src/scoring/rules/helpers';\nimport type { IcpSizeBand } from 'src/scoring';\n\nimport type { IcpSelection } from 'src/icp/icp-proposal';\n\nexport const ICP_ACTIONS = [\n  'status',\n  'propose',\n  'preview',\n  'apply',\n  'leave-open',\n] as const;\n\nexport type IcpAction = (typeof ICP_ACTIONS)[number];\n\n/**\n * Caps, so a malformed or hostile body cannot turn into a config record that\n * costs a second on every scored lead. Both are far above any real profile: a\n * workspace naming two hundred target industries has not described a profile.\n */\nexport const MAX_ICP_VALUES = 200;\nexport const MAX_ICP_BANDS = 20;\n\nexport type IcpCommand =\n  | { readonly action: 'status' }\n  | { readonly action: 'propose' }\n  | {\n      readonly action: 'preview';\n      readonly selection: IcpSelection;\n      readonly warnings: readonly string[];\n    }\n  | {\n      readonly action: 'apply';\n      readonly selection: IcpSelection;\n      readonly warnings: readonly string[];\n      /** Re-score existing leads afterwards, via the existing backfill. */\n      readonly rescore: boolean;\n    }\n  | { readonly action: 'leave-open' };\n\nexport type IcpCommandParseResult =\n  | { readonly ok: true; readonly command: IcpCommand }\n  | { readonly ok: false; readonly message: string };\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n  typeof value === 'object' && value !== null && !Array.isArray(value);\n\ninterface ParsedSelection {\n  readonly selection: IcpSelection;\n  readonly warnings: readonly string[];\n}\n\nconst parseValueList = (\n  raw: unknown,\n  label: string,\n  warnings: string[],\n): string[] => {\n  if (raw === undefined || raw === null) {\n    return [];\n  }\n\n  if (!Array.isArray(raw)) {\n    warnings.push(`The ${label} list was not a list, so it was ignored.`);\n    return [];\n  }\n\n  const seen = new Set<string>();\n  const values: string[] = [];\n  let dropped = 0;\n\n  for (const entry of raw) {\n    if (typeof entry !== 'string' || entry.trim().length === 0) {\n      dropped += 1;\n      continue;\n    }\n\n    // De-duplicated by the engine's own `normalise`, because that is what\n    // `matchesList` compares with \u2014 two entries that normalise the same are one\n    // target, and storing both would only make the record harder to read.\n    const key = normalise(entry);\n\n    if (key.length === 0 || seen.has(key)) {\n      continue;\n    }\n\n    if (values.length >= MAX_ICP_VALUES) {\n      dropped += 1;\n      continue;\n    }\n\n    seen.add(key);\n    values.push(entry.trim());\n  }\n\n  if (dropped > 0) {\n    warnings.push(\n      `${dropped} ${label} ${dropped === 1 ? 'entry was' : 'entries were'} empty, not text, or beyond the ${MAX_ICP_VALUES}-entry limit, and ${\n        dropped === 1 ? 'was' : 'were'\n      } left out.`,\n    );\n  }\n\n  return values;\n};\n\nconst isFiniteNumber = (value: unknown): value is number =>\n  typeof value === 'number' && Number.isFinite(value);\n\nconst parseBands = (raw: unknown, warnings: string[]): IcpSizeBand[] => {\n  if (raw === undefined || raw === null) {\n    return [];\n  }\n\n  if (!Array.isArray(raw)) {\n    warnings.push('The size bands were not a list, so they were ignored.');\n    return [];\n  }\n\n  const bands: IcpSizeBand[] = [];\n  let dropped = 0;\n\n  for (const entry of raw) {\n    if (!isRecord(entry) || bands.length >= MAX_ICP_BANDS) {\n      dropped += 1;\n      continue;\n    }\n\n    const min = entry['minEmployees'];\n    const rawMax = entry['maxEmployees'];\n\n    // Exactly the validation `resolveIcp` applies, applied here as well so that\n    // a band this parser accepts is a band the engine will keep. A band that\n    // survives the write and is then silently dropped at scoring time is worse\n    // than a rejected one: the record would show a profile the engine is not\n    // using.\n    if (!isFiniteNumber(min) || min < 0) {\n      dropped += 1;\n      continue;\n    }\n\n    const max = isFiniteNumber(rawMax) ? rawMax : null;\n\n    if (max !== null && max < min) {\n      dropped += 1;\n      continue;\n    }\n\n    const label =\n      typeof entry['label'] === 'string' && entry['label'].trim().length > 0\n        ? entry['label'].trim()\n        : `${Math.round(min)}\u2013${max === null ? '\u221E' : Math.round(max)} employees`;\n\n    bands.push({\n      label,\n      minEmployees: Math.round(min),\n      maxEmployees: max === null ? null : Math.round(max),\n    });\n  }\n\n  if (dropped > 0) {\n    warnings.push(\n      `${dropped} size ${dropped === 1 ? 'band was' : 'bands were'} missing a usable employee range and ${\n        dropped === 1 ? 'was' : 'were'\n      } left out. A band needs a minimum, and a maximum that is either a number or empty for \"no upper limit\".`,\n    );\n  }\n\n  return bands;\n};\n\nconst parseSelection = (raw: unknown): ParsedSelection | null => {\n  if (!isRecord(raw)) {\n    return null;\n  }\n\n  const warnings: string[] = [];\n\n  return {\n    selection: {\n      industries: parseValueList(raw['industries'], 'industry', warnings),\n      regions: parseValueList(raw['regions'], 'region', warnings),\n      sizeBands: parseBands(raw['sizeBands'], warnings),\n    },\n    warnings,\n  };\n};\n\nconst isEmpty = (selection: IcpSelection): boolean =>\n  selection.industries.length === 0 &&\n  selection.regions.length === 0 &&\n  selection.sizeBands.length === 0;\n\nexport const parseIcpCommand = (body: unknown): IcpCommandParseResult => {\n  // An empty body is a status read: the safe, read-only, side-effect-free action\n  // is the one you get for free, and every action that changes something has to\n  // be asked for by name. Same reasoning as the backfill route.\n  if (body === undefined || body === null) {\n    return { ok: true, command: { action: 'status' } };\n  }\n\n  if (!isRecord(body)) {\n    return { ok: false, message: 'ICP request body must be an object.' };\n  }\n\n  const rawAction = body['action'];\n  const action =\n    rawAction === undefined || rawAction === null\n      ? 'status'\n      : typeof rawAction === 'string'\n        ? rawAction.trim().toLowerCase()\n        : '';\n\n  if (!(ICP_ACTIONS as readonly string[]).includes(action)) {\n    return {\n      ok: false,\n      message: `Unknown ICP action. Expected one of ${ICP_ACTIONS.join(', ')}.`,\n    };\n  }\n\n  if (action === 'status' || action === 'propose' || action === 'leave-open') {\n    return { ok: true, command: { action } as IcpCommand };\n  }\n\n  const parsed = parseSelection(body['selection']);\n\n  if (parsed === null) {\n    return {\n      ok: false,\n      message:\n        'That request carried no profile to work with. Send a \"selection\" object with industries, regions and sizeBands.',\n    };\n  }\n\n  if (action === 'preview') {\n    return {\n      ok: true,\n      command: {\n        action: 'preview',\n        selection: parsed.selection,\n        warnings: parsed.warnings,\n      },\n    };\n  }\n\n  if (isEmpty(parsed.selection)) {\n    return {\n      ok: false,\n      message:\n        'That profile is empty, which is what the workspace already has. If you mean to leave the profile open on purpose, use \"Leave it open\" instead \u2014 that records the decision rather than pretending one was made.',\n    };\n  }\n\n  if (body['confirm'] !== true) {\n    return {\n      ok: false,\n      message:\n        'Applying a profile changes the decision on every lead scored from now on. Confirm the preview before applying.',\n    };\n  }\n\n  return {\n    ok: true,\n    command: {\n      action: 'apply',\n      selection: parsed.selection,\n      warnings: parsed.warnings,\n      rescore: body['rescore'] === true,\n    },\n  };\n};\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 * The enrichable field catalogue: what may be asked for, in what shape.\n *\n * One table, read by three consumers that must not disagree \u2014 the prompt (what\n * the model is asked), the extractor (what is accepted back) and the gap\n * analysis (what is worth asking about at all). Keeping them keyed off the same\n * array is what stops the classic drift where a field is prompted for and then\n * silently discarded, or accepted without ever being requested.\n *\n * Every entry answers a *firmographic* question. Nothing here is a contact\n * detail: see the note on `EnrichableFieldKey` for why that boundary is a hard\n * one rather than a starting point.\n */\n\nimport type {\n  EnrichableFieldKey,\n  EnrichableFieldSpec,\n} from 'src/enrichment/types';\n\nexport const ENRICHABLE_FIELD_SPECS: readonly EnrichableFieldSpec[] = [\n  {\n    key: 'industry',\n    label: 'Industry',\n    question: \"What industry does this person's employer operate in?\",\n    kind: 'text',\n    maxLength: 60,\n  },\n  {\n    key: 'region',\n    label: 'Region',\n    question: 'Which country or region is the employer headquartered in?',\n    kind: 'text',\n    maxLength: 60,\n  },\n  {\n    key: 'employeeCount',\n    label: 'Employee count',\n    question: 'Approximately how many people does the employer employ?',\n    kind: 'integer',\n    maxLength: 12,\n  },\n  {\n    key: 'jobTitle',\n    label: 'Job title',\n    question: 'What is this person\u2019s job title at that employer?',\n    kind: 'text',\n    maxLength: 120,\n  },\n  {\n    key: 'seniority',\n    label: 'Seniority',\n    question:\n      'What seniority level does that job title correspond to (for example: C-level, VP, Director, Manager, Individual contributor)?',\n    kind: 'text',\n    maxLength: 40,\n  },\n];\n\nconst SPEC_BY_KEY: ReadonlyMap<string, EnrichableFieldSpec> = new Map(\n  ENRICHABLE_FIELD_SPECS.map((spec) => [spec.key, spec]),\n);\n\nexport const findFieldSpec = (key: string): EnrichableFieldSpec | null =>\n  SPEC_BY_KEY.get(key) ?? null;\n\nexport const isEnrichableFieldKey = (key: unknown): key is EnrichableFieldKey =>\n  typeof key === 'string' && SPEC_BY_KEY.has(key);\n\n/**\n * The dotted paths a workspace adds to its Layer 3 field mapping to let the\n * deterministic scorer read enriched values.\n *\n * Published from here rather than hard-coded in the config seed because the\n * shape of the provenance blob is this module's business, and a path written out\n * by hand somewhere else is a path that goes stale the first time the blob's\n * version changes.\n *\n * The ordering is the important part: the enriched path goes **after** the\n * workspace's own candidates in every mapping, never before. `field-access.ts`\n * tries candidates in order and takes the first that yields a value, so a human\n * value always wins and enrichment only ever fills a hole. That is the same\n * promise the storage shape makes \u2014 enrichment never overwrites a person \u2014 held\n * at the read side as well as the write side.\n */\nexport const enrichedFieldMappingPath = (key: EnrichableFieldKey): string =>\n  `greenlightEnrichment.fields.${key}.parsedValue`;\n\nexport const ENRICHED_FIELD_MAPPING_PATHS: Readonly<\n  Record<EnrichableFieldKey, string>\n> = Object.freeze(\n  Object.fromEntries(\n    ENRICHABLE_FIELD_SPECS.map((spec) => [\n      spec.key,\n      enrichedFieldMappingPath(spec.key),\n    ]),\n  ) as Record<EnrichableFieldKey, string>,\n);\n", "/**\n * Seeded defaults.\n *\n * \"Defaults are the feature.\" A configurable product that ships empty is a\n * homework assignment \u2014 these values are what the post-install hook writes into\n * the config record, and they are also the fall-back the engine uses whenever\n * configuration is missing or unusable.\n *\n * The ICP lists start empty on purpose: an empty list means \"no opinion\", so a\n * fresh install is permissive and nothing is gated for being in the wrong\n * industry before an admin has said what the right industry is.\n */\n\nimport type {\n  FieldMapping,\n  IcpConfig,\n  ScoringBand,\n} from 'src/scoring/types';\n\n/**\n * Default paths into the lead record, tried in order. These are defaults only \u2014\n * rule logic never mentions a field name, it asks for a logical key and the\n * mapping decides where that comes from.\n */\nexport const DEFAULT_FIELD_MAPPING: FieldMapping = {\n  companyName: ['companyName', 'company.name', 'company', 'accountName'],\n  industry: ['industry', 'company.industry', 'sector'],\n  region: ['region', 'country', 'address.addressCountry', 'company.region'],\n  employeeCount: ['employees', 'employeeCount', 'company.employees', 'companySize'],\n  contactName: ['name', 'fullName', 'contactName', 'firstName'],\n  // `position` is deliberately NOT a candidate. On Twenty \u2014 the only platform\n  // this ships on \u2014 `position` is the row-ordering number, not a job title, so\n  // the fallback resolved a real value of `-5` and handed it to the\n  // decision-maker rule as somebody's role. It was visible in a demo recording\n  // before it was visible in a test.\n  //\n  // The subtler half: `src/enrichment/plan.ts` reads this same mapping to decide\n  // whether a human has already answered a field. A resolvable `position` meant\n  // every Twenty Person looked like it already had a job title, so job-title\n  // enrichment could never fire at all.\n  //\n  // A workspace that genuinely stores titles in a field called `position` can\n  // still say so through the Layer 3 mapping. Guessing it by default costs more\n  // than it ever paid.\n  jobTitle: ['jobTitle', 'title', 'role'],\n  seniority: ['seniority', 'seniorityLevel'],\n  email: ['emails', 'email', 'emails.primaryEmail', 'workEmail'],\n  phone: ['phones', 'phone', 'phones.primaryPhoneNumber', 'mobile'],\n  lastVerifiedAt: ['lastVerifiedAt', 'verifiedAt', 'updatedAt', 'createdAt'],\n\n  // Greenlight's own suppression columns. Single-candidate and not extended by\n  // the Layer 3 mapping, unlike every other key here: these are fields the app\n  // ships and owns, so \"where does this live\" has exactly one answer. A\n  // workspace's *own* opt-out columns are `optedOut` below, and the compliance\n  // rule takes the union of the two rather than letting either win.\n  suppressed: ['greenlightSuppressed'],\n  suppressionReason: ['greenlightSuppressionReason'],\n\n  optedOut: ['optedOut', 'doNotContact', 'emailOptOut', 'unsubscribed'],\n};\n\n/** Permissive by default: no industry/region/size opinion until an admin sets one. */\nexport const DEFAULT_ICP: IcpConfig = {\n  industries: [],\n  regions: [],\n  sizeBands: [],\n};\n\n/**\n * Evenly-spaced neutral bands. Published in full in the scoring-model explainer;\n * a scoring product that hides its model does not get trusted by the people\n * whose leads it rejects.\n */\nexport const DEFAULT_BANDS: readonly ScoringBand[] = [\n  { id: 'excellent', label: 'Excellent', minScore: 85 },\n  { id: 'good', label: 'Good', minScore: 70 },\n  { id: 'fair', label: 'Fair', minScore: 50 },\n  { id: 'poor', label: 'Poor', minScore: 0 },\n];\n\n/** The floor of the \"Fair\" band: fair and above is cleared to work. */\nexport const DEFAULT_GATE_THRESHOLD = 50;\n\nexport const DEFAULT_SHELF_LIFE_DAYS = 30;\n\nexport const DEFAULT_DECISION_MAKER_TITLES: readonly string[] = [\n  'ceo',\n  'cto',\n  'cfo',\n  'coo',\n  'cmo',\n  'ciso',\n  'cio',\n  'chief',\n  'founder',\n  'co-founder',\n  'cofounder',\n  'owner',\n  'proprietor',\n  'president',\n  'partner',\n  'principal',\n  'vp',\n  'vice president',\n  'svp',\n  'evp',\n  'head of',\n  'director',\n  'managing director',\n  'general manager',\n  'board member',\n];\n\nexport const DEFAULT_INFLUENCER_TITLES: readonly string[] = [\n  'manager',\n  'lead',\n  'team lead',\n  'supervisor',\n  'architect',\n  'consultant',\n  'coordinator',\n  'buyer',\n  'procurement',\n];\n\nexport const DEFAULT_ROLE_INBOX_LOCAL_PARTS: readonly string[] = [\n  'info',\n  'sales',\n  'support',\n  'admin',\n  'contact',\n  'hello',\n  'hi',\n  'office',\n  'enquiries',\n  'enquiry',\n  'inquiries',\n  'inquiry',\n  'marketing',\n  'help',\n  'billing',\n  'accounts',\n  'accounting',\n  'finance',\n  'careers',\n  'jobs',\n  'hr',\n  'team',\n  'mail',\n  'general',\n  'noreply',\n  'no-reply',\n  'donotreply',\n  'webmaster',\n  'postmaster',\n  'abuse',\n];\n\n/** Values that mean \"somebody typed something rather than nothing\". */\nexport const PLACEHOLDER_VALUES: readonly string[] = [\n  '-',\n  '--',\n  '.',\n  'n/a',\n  'na',\n  'n.a.',\n  'none',\n  'null',\n  'nil',\n  'unknown',\n  'tbd',\n  'tba',\n  'test',\n  'testing',\n  'asdf',\n  'xxx',\n  '???',\n  'no name',\n  'not provided',\n  'not set',\n];\n", "/**\n * Reading values out of a lead record.\n *\n * Two problems are solved here, both of them schema-flexibility problems:\n *\n * 1. Customers name fields differently, so every read goes through the\n *    configured mapping (a list of candidate dotted paths, tried in order).\n * 2. CRM fields are often composite objects (`{ primaryEmail, additionalEmails }`,\n *    `{ firstName, lastName }`). Values are flattened generically by walking\n *    string leaves in key order \u2014 no field name is hard-coded anywhere.\n */\n\nimport { PLACEHOLDER_VALUES } from 'src/scoring/defaults';\nimport type {\n  FieldMapping,\n  FieldObservation,\n  FieldReader,\n  LeadFieldKey,\n  LeadRecord,\n} from 'src/scoring/types';\n\nconst MAX_LEAF_DEPTH = 4;\n\n/** Fail-open coercion of anything the caller has into a `LeadRecord`. */\nexport const toLeadRecord = (record: unknown): LeadRecord => {\n  if (!isPlainObject(record)) {\n    return { id: null, fields: {} };\n  }\n\n  const id = typeof record['id'] === 'string' ? record['id'] : null;\n\n  return { id, fields: record };\n};\n\nexport const isPlainObject = (\n  value: unknown,\n): value is Record<string, unknown> =>\n  typeof value === 'object' &&\n  value !== null &&\n  !Array.isArray(value) &&\n  !(value instanceof Date);\n\n/**\n * Resolve a dotted path against an object. Exact key match first, then a\n * case-insensitive match, so `address.addresscountry` still finds\n * `address.addressCountry`.\n */\nexport const getByPath = (source: unknown, path: string): unknown => {\n  const segments = path.split('.').filter((segment) => segment.length > 0);\n\n  let cursor: unknown = source;\n\n  for (const segment of segments) {\n    if (cursor === null || cursor === undefined) {\n      return undefined;\n    }\n\n    if (Array.isArray(cursor)) {\n      const index = Number(segment);\n      cursor = Number.isInteger(index) ? cursor[index] : undefined;\n      continue;\n    }\n\n    if (!isPlainObject(cursor)) {\n      return undefined;\n    }\n\n    if (segment in cursor) {\n      cursor = cursor[segment];\n      continue;\n    }\n\n    const lowered = segment.toLowerCase();\n    const matchedKey = Object.keys(cursor).find(\n      (key) => key.toLowerCase() === lowered,\n    );\n\n    cursor = matchedKey === undefined ? undefined : cursor[matchedKey];\n  }\n\n  return cursor;\n};\n\n/** Every non-empty string leaf under a value, in key order. */\nexport const stringLeaves = (value: unknown, depth = 0): string[] => {\n  if (depth > MAX_LEAF_DEPTH) {\n    return [];\n  }\n\n  if (typeof value === 'string') {\n    const trimmed = value.trim();\n    return trimmed.length > 0 ? [trimmed] : [];\n  }\n\n  if (typeof value === 'number' && Number.isFinite(value)) {\n    return [String(value)];\n  }\n\n  if (Array.isArray(value)) {\n    return value.flatMap((entry) => stringLeaves(entry, depth + 1));\n  }\n\n  if (value instanceof Date) {\n    return Number.isNaN(value.getTime()) ? [] : [value.toISOString()];\n  }\n\n  if (isPlainObject(value)) {\n    return Object.keys(value).flatMap((key) =>\n      stringLeaves(value[key], depth + 1),\n    );\n  }\n\n  return [];\n};\n\nconst isEmptyValue = (value: unknown): boolean =>\n  value === null ||\n  value === undefined ||\n  (typeof value === 'string' && value.trim().length === 0) ||\n  (Array.isArray(value) && value.length === 0) ||\n  (typeof value === 'number' && Number.isNaN(value)) ||\n  (isPlainObject(value) && stringLeaves(value).length === 0);\n\n/**\n * Whether a value is one of the strings that mean \"somebody typed something\n * rather than nothing\" \u2014 `n/a`, `tbd`, `xxx`, `keine angabe`.\n *\n * `known` defaults to the shipped list so every existing caller keeps its exact\n * behaviour. A rule that has a resolved config passes `config.placeholderValues`\n * instead, which is how a licence-delivered lexicon reaches this check without\n * the rule learning anything about licensing: the list is data, resolved by\n * `resolveConfig`, exactly like `decisionMakerTitles` already was.\n */\nexport const isPlaceholder = (\n  value: string,\n  known: readonly string[] = PLACEHOLDER_VALUES,\n): boolean => known.includes(value.trim().toLowerCase());\n\nexport const toNumberValue = (value: unknown): number | null => {\n  if (typeof value === 'number') {\n    return Number.isFinite(value) ? value : null;\n  }\n\n  if (typeof value === 'boolean') {\n    return null;\n  }\n\n  const leaves = stringLeaves(value);\n\n  for (const leaf of leaves) {\n    // Tolerate \"1,200\", \"1 200\", \"250+\", \"50-200\" (takes the lower bound).\n    const cleaned = leaf.replace(/[,\\s]/g, '');\n    const match = /-?\\d+(\\.\\d+)?/.exec(cleaned);\n\n    if (match !== null) {\n      const parsed = Number(match[0]);\n\n      if (Number.isFinite(parsed)) {\n        return parsed;\n      }\n    }\n  }\n\n  return null;\n};\n\nexport const toDateValue = (value: unknown): Date | null => {\n  if (value instanceof Date) {\n    return Number.isNaN(value.getTime()) ? null : value;\n  }\n\n  if (typeof value === 'number' && Number.isFinite(value)) {\n    const fromEpoch = new Date(value);\n    return Number.isNaN(fromEpoch.getTime()) ? null : fromEpoch;\n  }\n\n  const leaves = stringLeaves(value);\n\n  for (const leaf of leaves) {\n    const parsed = new Date(leaf);\n\n    if (!Number.isNaN(parsed.getTime())) {\n      return parsed;\n    }\n  }\n\n  return null;\n};\n\nconst TRUTHY_TOKENS = new Set([\n  'true',\n  'yes',\n  'y',\n  '1',\n  'opted_out',\n  'opted-out',\n  'optedout',\n  'unsubscribed',\n  'do_not_contact',\n  'do-not-contact',\n  'donotcontact',\n  'dnc',\n  'blocked',\n]);\n\nconst FALSY_TOKENS = new Set([\n  'false',\n  'no',\n  'n',\n  '0',\n  'subscribed',\n  'opted_in',\n  'opted-in',\n  'optedin',\n]);\n\nexport const toBooleanValue = (value: unknown): boolean | null => {\n  if (typeof value === 'boolean') {\n    return value;\n  }\n\n  if (typeof value === 'number' && Number.isFinite(value)) {\n    return value !== 0;\n  }\n\n  const leaves = stringLeaves(value);\n\n  for (const leaf of leaves) {\n    const token = leaf.toLowerCase();\n\n    if (TRUTHY_TOKENS.has(token)) {\n      return true;\n    }\n\n    if (FALSY_TOKENS.has(token)) {\n      return false;\n    }\n  }\n\n  return null;\n};\n\nconst renderDisplay = (value: unknown): string => {\n  if (typeof value === 'boolean') {\n    return value ? 'true' : 'false';\n  }\n\n  const leaves = stringLeaves(value);\n\n  if (leaves.length === 0) {\n    return '(not set)';\n  }\n\n  return leaves.slice(0, 3).join(', ');\n};\n\ninterface Resolution {\n  readonly mappedTo: string | null;\n  readonly candidates: readonly string[];\n  readonly value: unknown;\n  readonly present: boolean;\n}\n\n/**\n * Builds the reader handed to rules. Every lookup is recorded, so a trace entry\n * can tell a salesperson exactly which field was consulted and what was in it.\n */\nexport const createFieldReader = (\n  lead: LeadRecord,\n  mapping: FieldMapping,\n): FieldReader => {\n  const seen = new Map<LeadFieldKey, FieldObservation>();\n\n  const candidatesFor = (key: LeadFieldKey): readonly string[] => {\n    const configured = mapping[key];\n    return Array.isArray(configured) ? configured : [];\n  };\n\n  const resolveAll = (key: LeadFieldKey): Resolution[] => {\n    const candidates = candidatesFor(key);\n\n    return candidates.map((path) => {\n      const value = getByPath(lead.fields, path);\n\n      return {\n        mappedTo: path,\n        candidates,\n        value,\n        present: !isEmptyValue(value),\n      };\n    });\n  };\n\n  const resolve = (key: LeadFieldKey): Resolution => {\n    const candidates = candidatesFor(key);\n    const found = resolveAll(key).find((entry) => entry.present);\n\n    if (found !== undefined) {\n      return found;\n    }\n\n    return { mappedTo: null, candidates, value: undefined, present: false };\n  };\n\n  const record = (key: LeadFieldKey, resolution: Resolution): void => {\n    seen.set(key, {\n      key,\n      mappedTo: resolution.mappedTo,\n      candidates: resolution.candidates,\n      present: resolution.present,\n      display: resolution.present ? renderDisplay(resolution.value) : '(not set)',\n    });\n  };\n\n  const readResolved = (key: LeadFieldKey): Resolution => {\n    const resolution = resolve(key);\n    record(key, resolution);\n    return resolution;\n  };\n\n  return {\n    text: (key) => {\n      const leaves = stringLeaves(readResolved(key).value);\n      return leaves.length > 0 ? leaves.join(' ') : null;\n    },\n    textList: (key) => stringLeaves(readResolved(key).value),\n    number: (key) => toNumberValue(readResolved(key).value),\n    date: (key) => toDateValue(readResolved(key).value),\n    booleanAny: (key) => {\n      const all = resolveAll(key);\n      const present = all.filter((entry) => entry.present);\n      const truthy = present.find((entry) => toBooleanValue(entry.value) === true);\n\n      if (truthy !== undefined) {\n        record(key, truthy);\n        return true;\n      }\n\n      const first = present[0];\n\n      if (first !== undefined) {\n        record(key, first);\n        return toBooleanValue(first.value) === true ? true : false;\n      }\n\n      record(key, {\n        mappedTo: null,\n        candidates: candidatesFor(key),\n        value: undefined,\n        present: false,\n      });\n\n      return null;\n    },\n    observations: () => Array.from(seen.values()),\n  };\n};\n", "import { isPlaceholder } from 'src/scoring/field-access';\nimport type { ScoringRule } from 'src/scoring/types';\n\nexport const companyIdentifiedRule: ScoringRule = {\n  id: 'company.identified',\n  name: 'Company named',\n  category: 'contact',\n  question: 'Do we know which company this lead works for?',\n  why: 'Every other qualification question \u2014 industry, size, region, territory, existing customer \u2014 depends on knowing the company. A lead with no company is a name floating in space, and a rep has nothing to research before they call.',\n  defaultEnabled: true,\n  defaultSeverity: 'major',\n  defaultWeight: 10,\n  reads: ['companyName'],\n\n  evaluate: ({ config, read }) => {\n    const name = read.text('companyName');\n\n    if (name === null) {\n      return {\n        outcome: 'fail',\n        explanation: 'No company is recorded on this lead.',\n        remedy: 'Add the company name, or link the lead to a company record.',\n      };\n    }\n\n    if (isPlaceholder(name, config.placeholderValues) || name.trim().length < 2) {\n      return {\n        outcome: 'fail',\n        explanation: `\"${name}\" is a placeholder rather than a real company name.`,\n        remedy: 'Replace it with the real company name.',\n        detail: { companyName: name },\n      };\n    }\n\n    return {\n      outcome: 'pass',\n      explanation: `The lead is attributed to ${name}.`,\n      detail: { companyName: name },\n    };\n  },\n};\n", "import type { RuleVerdict, ScoringRule } from 'src/scoring/types';\n\n/**\n * The one check in the product that is not about lead quality.\n *\n * ## What changed, and why it had to\n *\n * This rule used to read a single logical key, `optedOut`, resolved purely\n * through the customer's Layer 3 field mapping. Twenty ships no email management\n * \u2014 no unsubscribe handling, no bounce tracking, no do-not-contact list \u2014 so on\n * a stock workspace that mapping resolves to nothing at all,\n * `read.booleanAny('optedOut')` returned `null`, and the rule returned `pass`\n * with full credit. The single compliance check in Greenlight could not fire on\n * a default install, and it inflated every score by 15 points while failing to.\n *\n * Greenlight now ships the register itself (`greenlightSuppressed` and friends on\n * Person), and this rule reads it. Two consequences, both deliberate:\n *\n * ## 1. Union, not override\n *\n * Greenlight's field is read *first*, the customer's mapping second, and **any**\n * source saying \"suppressed\" fails the rule. A workspace that already had an\n * opt-out column keeps it working exactly as before, and gains a second register\n * rather than having its own replaced. There is no precedence question, because\n * for a compliance stop there is no argument in which \"the other column says it\n * is fine\" is a reason to send the email.\n *\n * A recorded suppression *reason* counts as suppression on its own, even with\n * the boolean cleared. Untick-the-box-but-leave-`SPAM_COMPLAINT`-behind is the\n * realistic hand edit, and reading it as \"contactable\" would silently re-enable\n * outreach to somebody whose own record still states, in writing, why we\n * stopped. The remedy names both fields so the fix is obvious.\n *\n * ## 2. \"Nobody knows\" is no longer the same answer as \"confirmed clear\"\n *\n * The old rule collapsed two very different states onto `pass` with full marks:\n * *this person has not opted out*, and *nothing anywhere in this workspace\n * records whether they have*. The second is not evidence of compliance; it is\n * the absence of a compliance system, and paying 15 points for it is how a\n * decorative check comes to look like a working one.\n *\n * They are now distinguished:\n *\n *   - **Confirmed clear** \u2014 Greenlight's register says no entry, or the\n *     customer's own opt-out column says false. `pass`, full credit. `false` on\n *     a Greenlight-owned column is a real assertion about a real register, not\n *     an absence.\n *   - **Genuinely unknown** \u2014 neither register is present on the record at all.\n *     `not_applicable`: the engine excludes it from *both* sides of the score, so\n *     the lead is neither credited for evidence it does not have nor punished for\n *     a check nobody could run. The remaining rules are renormalised over the\n *     weight that could actually be evaluated, and the trace says why.\n *\n * `not_applicable` rather than `fail` matters: only a blocking `fail` produces\n * the `blocked` decision, so an unknown never holds a lead. ARCHITECTURE.md's\n * golden rule is that a lead is never lost, and \"we could not check\" is not a\n * reason to stop somebody working a lead \u2014 it is a reason to stop pretending we\n * checked.\n *\n * In practice the unknown branch is what a workspace sees before the suppression\n * fields have synced, or when the engine is driven as a library over a record\n * shape that has neither register. On a synced workspace the field is present on\n * every Person, which is precisely the point: the fix for \"this rule cannot fail\"\n * is shipping somewhere for the answer to live, not re-weighting the rule.\n */\n\n/** `HARD_BOUNCE` -> `hard bounce`, without importing the gate layer's vocabulary. */\nconst humaniseReason = (code: string): string =>\n  code.trim().toLowerCase().replace(/[_-]+/g, ' ');\n\nconst failVerdict = (\n  greenlightSays: boolean,\n  customerSays: boolean,\n  reason: string | null,\n): RuleVerdict => {\n  const because =\n    reason === null ? '' : ` The recorded reason is \"${humaniseReason(reason)}\".`;\n\n  const explanation = greenlightSays\n    ? customerSays\n      ? `This person is on Greenlight's do-not-contact list and is also flagged as opted out in your own CRM field.${because} Contacting them is a compliance breach.`\n      : `This person is on Greenlight's do-not-contact list.${because} Contacting them is a compliance breach.`\n    : 'Your own opt-out field says this person has opted out of contact. Reaching out anyway is a compliance breach.';\n\n  return {\n    outcome: 'fail',\n    explanation,\n    remedy: greenlightSays\n      ? 'Do not contact this lead. If the block was recorded in error, use \"Allow contact again (Greenlight)\" \u2014 it clears both the flag and the reason, records who lifted it and why, and re-scores the lead.'\n      : 'Do not contact this lead. If the opt-out was recorded in error, correct your own opt-out field on the record and say why; the lead re-scores and clears itself.',\n    detail: {\n      suppressed: true,\n      suppressedByGreenlight: greenlightSays,\n      suppressedByCustomerField: customerSays,\n      suppressionReason: reason,\n      // Kept under the original key so anything already reading the trace for\n      // this rule \u2014 dashboards, exports, the release path's fallback \u2014 is not\n      // broken by the new field set.\n      optOutRecorded: true,\n    },\n  };\n};\n\nexport const complianceOptOutRule: ScoringRule = {\n  id: 'compliance.opt-out',\n  name: 'Not opted out',\n  category: 'compliance',\n  question: 'Has this person asked not to be contacted?',\n  why: 'Contacting someone who has opted out is a compliance breach and the fastest way to lose a domain reputation. This is the one check that is not about lead quality at all \u2014 it is about not doing something you are not allowed to do. Greenlight keeps its own do-not-contact register because Twenty has none, and reads any opt-out field the workspace already had alongside it.',\n  defaultEnabled: true,\n  defaultSeverity: 'blocking',\n  defaultWeight: 15,\n  reads: ['suppressed', 'suppressionReason', 'optedOut'],\n\n  evaluate: ({ read }) => {\n    // Greenlight's own register first.\n    const suppressedFlag = read.booleanAny('suppressed');\n    const suppressionReason = read.text('suppressionReason');\n    // Then the customer's, wherever their mapping points.\n    const optedOut = read.booleanAny('optedOut');\n\n    const greenlightSays = suppressedFlag === true || suppressionReason !== null;\n    const customerSays = optedOut === true;\n\n    if (greenlightSays || customerSays) {\n      return failVerdict(greenlightSays, customerSays, suppressionReason);\n    }\n\n    // Neither register is present on this record. Not a pass \u2014 an unanswered\n    // question, excluded from the score rather than paid out at full marks.\n    if (suppressedFlag === null && optedOut === null) {\n      return {\n        outcome: 'not_applicable',\n        explanation:\n          'Nothing on this record says whether this person has asked not to be contacted, so the compliance check could not be answered and was left out of the score rather than passed by default.',\n        remedy:\n          'Greenlight ships a \"Do not contact\" field for exactly this. If it is missing here, the app\u2019s fields have not reached this record yet \u2014 or point Greenlight at your own opt-out column under Opt-out fields in Greenlight settings.',\n        detail: {\n          suppressed: false,\n          suppressedByGreenlight: false,\n          suppressedByCustomerField: false,\n          suppressionReason: null,\n          optOutRecorded: false,\n          suppressionDataAvailable: false,\n        },\n      };\n    }\n\n    return {\n      outcome: 'pass',\n      explanation:\n        suppressedFlag === false\n          ? 'Greenlight\u2019s do-not-contact list has no entry for this person, so outreach is permitted.'\n          : 'Your opt-out field is clear on this person, so outreach is permitted.',\n      detail: {\n        suppressed: false,\n        suppressedByGreenlight: false,\n        suppressedByCustomerField: false,\n        suppressionReason: null,\n        optOutRecorded: false,\n        suppressionDataAvailable: true,\n      },\n    };\n  },\n};\n", "import { firstKeywordMatch } from 'src/scoring/rules/helpers';\nimport type { ScoringRule } from 'src/scoring/types';\n\nexport const contactDecisionMakerRule: ScoringRule = {\n  id: 'contact.decision-maker',\n  name: 'Decision-maker',\n  category: 'contact',\n  question: 'Can this person say yes, or at least get you in front of the person who can?',\n  why: 'The single biggest predictor of a deal is talking to someone with budget authority. A perfect-fit company represented by someone with no say costs a rep the same hours as a real opportunity and closes none of them.',\n  defaultEnabled: true,\n  defaultSeverity: 'major',\n  defaultWeight: 20,\n  reads: ['seniority', 'jobTitle'],\n\n  evaluate: ({ config, read }) => {\n    const seniority = read.text('seniority');\n    const jobTitle = read.text('jobTitle');\n    const subject = seniority ?? jobTitle;\n\n    if (subject === null) {\n      return {\n        outcome: 'fail',\n        explanation:\n          'No job title is recorded, so we cannot tell whether this person can authorise a purchase.',\n        remedy: \"Add the contact's job title.\",\n      };\n    }\n\n    const decisionMaker = firstKeywordMatch(subject, config.decisionMakerTitles);\n\n    if (decisionMaker !== null) {\n      return {\n        outcome: 'pass',\n        explanation: `\"${subject}\" indicates buying authority.`,\n        detail: { title: subject, matchedKeyword: decisionMaker },\n      };\n    }\n\n    const influencer = firstKeywordMatch(subject, config.influencerTitles);\n\n    if (influencer !== null) {\n      return {\n        outcome: 'partial',\n        credit: 0.5,\n        explanation: `\"${subject}\" is likely an influencer rather than the person who signs.`,\n        remedy:\n          'Work this contact as a route in, and find the budget holder above them before forecasting the deal.',\n        detail: { title: subject, matchedKeyword: influencer },\n      };\n    }\n\n    return {\n      outcome: 'fail',\n      explanation: `\"${subject}\" does not look like someone who can authorise a purchase.`,\n      remedy:\n        'Find a contact with budget authority at this company, or add their title keyword to your decision-maker list if it belongs there.',\n      detail: { title: subject },\n    };\n  },\n};\n", "import { parseEmail } from 'src/scoring/rules/helpers';\nimport type { ScoringRule } from 'src/scoring/types';\n\nexport const contactEmailValidRule: ScoringRule = {\n  id: 'contact.email-valid',\n  name: 'Usable email address',\n  category: 'contact',\n  question: 'Is there an email address that will actually deliver?',\n  why: 'Email is how most first contact happens. A missing or malformed address means the outreach silently fails and the rep never learns why \u2014 the lead just looks unresponsive.',\n  defaultEnabled: true,\n  defaultSeverity: 'critical',\n  defaultWeight: 15,\n  reads: ['email'],\n\n  evaluate: ({ read }) => {\n    const candidates = read.textList('email');\n\n    if (candidates.length === 0) {\n      return {\n        outcome: 'fail',\n        explanation: 'No email address is recorded, so this lead cannot be emailed.',\n        remedy: \"Add the contact's email address.\",\n      };\n    }\n\n    const parsed = candidates\n      .map((candidate) => parseEmail(candidate))\n      .find((candidate) => candidate !== null);\n\n    if (parsed === undefined || parsed === null) {\n      return {\n        outcome: 'fail',\n        explanation: `\"${candidates[0] ?? ''}\" is not a valid email address, so anything sent to it will bounce.`,\n        remedy: 'Correct the email address.',\n        detail: { email: candidates[0] ?? null },\n      };\n    }\n\n    return {\n      outcome: 'pass',\n      explanation: `${parsed.address} is a well-formed email address.`,\n      detail: { email: parsed.address, domain: parsed.domain },\n    };\n  },\n};\n", "import { isPlaceholder } from 'src/scoring/field-access';\nimport type { ScoringRule } from 'src/scoring/types';\n\nexport const contactNamedPersonRule: ScoringRule = {\n  id: 'contact.named-person',\n  name: 'Named contact',\n  category: 'contact',\n  question: 'Is there a real human being to call?',\n  why: 'A rep cannot open a conversation with an organisation. Leads without a named person become \"whoever picks up\", which is the lowest-converting outreach there is.',\n  defaultEnabled: true,\n  defaultSeverity: 'minor',\n  defaultWeight: 8,\n  reads: ['contactName'],\n\n  evaluate: ({ config, read }) => {\n    const name = read.text('contactName');\n\n    if (name === null) {\n      return {\n        outcome: 'fail',\n        explanation: 'No contact name is recorded, so there is no one to address.',\n        remedy: 'Add the first and last name of the person to contact.',\n      };\n    }\n\n    if (isPlaceholder(name, config.placeholderValues) || !/[a-z]/i.test(name)) {\n      return {\n        outcome: 'fail',\n        explanation: `\"${name}\" is a placeholder rather than a person's name.`,\n        remedy: \"Replace it with the contact's real name.\",\n        detail: { contactName: name },\n      };\n    }\n\n    const parts = name\n      .trim()\n      .split(/\\s+/)\n      .filter((part) => /[a-z]/i.test(part));\n\n    if (parts.length < 2) {\n      return {\n        outcome: 'partial',\n        credit: 0.5,\n        explanation: `Only a first name (\"${name}\") is recorded \u2014 enough to greet someone, not enough to find them.`,\n        remedy: 'Add the surname so the rep can verify the person before reaching out.',\n        detail: { contactName: name },\n      };\n    }\n\n    return {\n      outcome: 'pass',\n      explanation: `${name} is a named contact.`,\n      detail: { contactName: name },\n    };\n  },\n};\n", "import type { ScoringRule } from 'src/scoring/types';\n\nconst MIN_DIGITS = 7;\nconst MAX_DIGITS = 15;\n\nexport const contactPhoneValidRule: ScoringRule = {\n  id: 'contact.phone-valid',\n  name: 'Dialable phone number',\n  category: 'contact',\n  question: 'Is there a phone number a rep could actually dial?',\n  why: 'Phone is the fastest path to a real conversation and the fallback when email goes unanswered. A number with too few digits or a note typed into the field looks like coverage and gives none.',\n  defaultEnabled: true,\n  defaultSeverity: 'minor',\n  defaultWeight: 5,\n  reads: ['phone'],\n\n  evaluate: ({ read }) => {\n    const candidates = read.textList('phone');\n\n    if (candidates.length === 0) {\n      return {\n        outcome: 'fail',\n        explanation: 'No phone number is recorded, so this lead can only be reached by email.',\n        remedy: \"Add the contact's phone number.\",\n      };\n    }\n\n    const usable = candidates.find((candidate) => {\n      const stripped = candidate.replace(/[\\s().\\-/]/g, '').replace(/^\\+/, '');\n\n      if (!/^\\d+$/.test(stripped)) {\n        return false;\n      }\n\n      return stripped.length >= MIN_DIGITS && stripped.length <= MAX_DIGITS;\n    });\n\n    if (usable === undefined) {\n      return {\n        outcome: 'fail',\n        explanation: `\"${candidates[0] ?? ''}\" is not a number a rep could dial.`,\n        remedy: `Correct the phone number \u2014 it needs between ${MIN_DIGITS} and ${MAX_DIGITS} digits, optionally with a country code.`,\n        detail: { phone: candidates[0] ?? null },\n      };\n    }\n\n    return {\n      outcome: 'pass',\n      explanation: `${usable} is a dialable phone number.`,\n      detail: { phone: usable },\n    };\n  },\n};\n", "import { emailLocalRoot, parseEmail } from 'src/scoring/rules/helpers';\nimport type { ScoringRule } from 'src/scoring/types';\n\nexport const contactSharedInboxRule: ScoringRule = {\n  id: 'contact.shared-inbox',\n  name: 'Personal mailbox',\n  category: 'contact',\n  question: 'Does the email address belong to a person, or to a shared inbox?',\n  why: 'A shared inbox \u2014 info@, sales@, enquiries@ \u2014 has no owner and no accountability, so replies are nobody\\'s job. These addresses look like a contactable lead in a report and behave like a dead end in practice.',\n  defaultEnabled: true,\n  defaultSeverity: 'minor',\n  defaultWeight: 7,\n  reads: ['email'],\n\n  evaluate: ({ config, read }) => {\n    const candidates = read.textList('email');\n    const parsed = candidates\n      .map((candidate) => parseEmail(candidate))\n      .find((candidate) => candidate !== null);\n\n    if (parsed === undefined || parsed === null) {\n      return {\n        outcome: 'not_applicable',\n        explanation:\n          'There is no usable email address to judge, so this check was skipped \u2014 the email rule already covers it.',\n      };\n    }\n\n    const root = emailLocalRoot(parsed.localPart);\n\n    if (config.roleInboxLocalParts.includes(root)) {\n      return {\n        outcome: 'fail',\n        explanation: `${parsed.address} is a shared \"${root}@\" inbox, so no particular person owns a reply.`,\n        remedy:\n          'Find a named person at this company and use their direct address. Keep the shared inbox as a fallback only.',\n        detail: { email: parsed.address, mailbox: root },\n      };\n    }\n\n    return {\n      outcome: 'pass',\n      explanation: `${parsed.address} looks like a personal mailbox, so a named person will see it.`,\n      detail: { email: parsed.address, mailbox: root },\n    };\n  },\n};\n", "import { clamp01, daysBetween, roundTo } from 'src/scoring/rules/helpers';\nimport type { ScoringRule } from 'src/scoring/types';\n\n/** Tolerance for clock skew between systems, in days. */\nconst FUTURE_TOLERANCE_DAYS = 1;\n\nexport const dataFreshnessRule: ScoringRule = {\n  id: 'data.freshness',\n  name: 'Data still in date',\n  category: 'data-quality',\n  question: 'Was this lead verified recently enough to trust?',\n  why: 'People change jobs, companies move, numbers get reassigned. A contact captured two years ago is not a lead, it is a historical record \u2014 and a rep who works it wastes the call and looks unprepared.',\n  defaultEnabled: true,\n  defaultSeverity: 'minor',\n  defaultWeight: 10,\n  reads: ['lastVerifiedAt'],\n\n  evaluate: ({ config, now, read }) => {\n    if (now === null) {\n      return {\n        outcome: 'not_applicable',\n        explanation:\n          'The current date was not available for this run, so the age of this lead could not be checked.',\n      };\n    }\n\n    const shelfLifeDays =\n      config.fieldShelfLifeDays.lastVerifiedAt ?? config.defaultShelfLifeDays;\n\n    const verifiedAt = read.date('lastVerifiedAt');\n\n    if (verifiedAt === null) {\n      return {\n        outcome: 'fail',\n        explanation:\n          'Nothing on this lead records when its details were last checked, so we have to assume they are stale.',\n        remedy: `Set a last-verified date, or map Greenlight's \"last verified\" field to a date your workspace already maintains. Your shelf life is ${shelfLifeDays} days.`,\n        detail: { shelfLifeDays },\n      };\n    }\n\n    const ageDays = daysBetween(verifiedAt, now);\n\n    if (ageDays < -FUTURE_TOLERANCE_DAYS) {\n      return {\n        outcome: 'fail',\n        explanation: `This lead says it was verified on ${verifiedAt.toISOString().slice(0, 10)}, which is in the future \u2014 the date is wrong.`,\n        remedy: 'Correct the last-verified date.',\n        detail: { verifiedAt: verifiedAt.toISOString(), shelfLifeDays },\n      };\n    }\n\n    const age = Math.max(0, ageDays);\n\n    if (age <= shelfLifeDays) {\n      return {\n        outcome: 'pass',\n        explanation: `Last verified ${roundTo(age, 1)} days ago, inside your ${shelfLifeDays}-day shelf life.`,\n        detail: { ageDays: roundTo(age, 1), shelfLifeDays },\n      };\n    }\n\n    if (age < shelfLifeDays * 2) {\n      const credit = clamp01(1 - (age - shelfLifeDays) / shelfLifeDays);\n\n      return {\n        outcome: 'partial',\n        credit,\n        explanation: `Last verified ${roundTo(age, 1)} days ago, past your ${shelfLifeDays}-day shelf life but not yet twice over.`,\n        remedy: 'Re-check the contact details before a rep spends time on this lead.',\n        detail: { ageDays: roundTo(age, 1), shelfLifeDays },\n      };\n    }\n\n    return {\n      outcome: 'fail',\n      explanation: `Last verified ${roundTo(age, 1)} days ago \u2014 more than twice your ${shelfLifeDays}-day shelf life.`,\n      remedy: 'Re-verify the contact and company details, or retire the lead.',\n      detail: { ageDays: roundTo(age, 1), shelfLifeDays },\n    };\n  },\n};\n", "import type { ScoringRule } from 'src/scoring/types';\n\nexport const icpCompanySizeRule: ScoringRule = {\n  id: 'icp.company-size',\n  name: 'Target company size',\n  category: 'icp',\n  question: 'Is this company the size you sell to?',\n  why: 'Company size decides whether your pricing, contract and onboarding fit at all. Too small and the deal will not clear your floor; too large and the sales motion is a different product.',\n  defaultEnabled: true,\n  defaultSeverity: 'minor',\n  defaultWeight: 10,\n  reads: ['employeeCount'],\n\n  evaluate: ({ config, read }) => {\n    const bands = config.icp.sizeBands;\n\n    if (bands.length === 0) {\n      return {\n        outcome: 'not_applicable',\n        explanation:\n          'You have not defined any target company-size bands, so every company size is accepted.',\n        remedy:\n          'Define your target size bands in the Greenlight ICP settings to start filtering on headcount.',\n      };\n    }\n\n    const employees = read.number('employeeCount');\n\n    if (employees === null) {\n      return {\n        outcome: 'fail',\n        explanation:\n          'This lead has no company size recorded, so we cannot tell whether the company is the size you sell to.',\n        remedy: 'Set the employee count on the lead or its company record.',\n      };\n    }\n\n    const matched = bands.find(\n      (band) =>\n        employees >= band.minEmployees &&\n        (band.maxEmployees === null || employees <= band.maxEmployees),\n    );\n\n    if (matched !== undefined) {\n      return {\n        outcome: 'pass',\n        explanation: `${employees} employees puts this company in your \"${matched.label}\" target band.`,\n        detail: { employees, band: matched.label },\n      };\n    }\n\n    return {\n      outcome: 'fail',\n      explanation: `${employees} employees falls outside every target size band you defined.`,\n      remedy: `Widen a size band if you do sell to companies this size. Your bands are: ${bands\n        .map((band) => band.label)\n        .join(', ')}.`,\n      detail: { employees },\n    };\n  },\n};\n", "import { matchesList } from 'src/scoring/rules/helpers';\nimport type { ScoringRule } from 'src/scoring/types';\n\nexport const icpIndustryRule: ScoringRule = {\n  id: 'icp.industry',\n  name: 'Target industry',\n  category: 'icp',\n  question: 'Is this company in an industry you sell to?',\n  why: 'Reps burn the most time on companies that were never going to buy. Industry is the cheapest, most reliable filter you have, and it is the one your team argues about most.',\n  defaultEnabled: true,\n  defaultSeverity: 'major',\n  defaultWeight: 20,\n  reads: ['industry', 'companyName'],\n\n  evaluate: ({ config, read }) => {\n    const targets = config.icp.industries;\n\n    if (targets.length === 0) {\n      return {\n        outcome: 'not_applicable',\n        explanation:\n          'You have not listed any target industries, so every industry is accepted.',\n        remedy:\n          'Add your target industries to the Greenlight ICP settings to start filtering on industry.',\n      };\n    }\n\n    const values = read.textList('industry');\n\n    if (values.length === 0) {\n      return {\n        outcome: 'fail',\n        explanation:\n          'This lead has no industry recorded, so we cannot tell whether it is in a market you sell to.',\n        remedy: 'Set the industry on the lead or its company record.',\n        detail: { targetIndustries: targets.join(', ') },\n      };\n    }\n\n    const matched = matchesList(values, targets);\n\n    if (matched !== null) {\n      return {\n        outcome: 'pass',\n        explanation: `${matched} is one of your target industries.`,\n        detail: { industry: matched },\n      };\n    }\n\n    return {\n      outcome: 'fail',\n      explanation: `${values.join(', ')} is not one of your target industries.`,\n      remedy: `Add it to your target industries if you do sell there. Your current list is: ${targets.join(', ')}.`,\n      detail: { industry: values.join(', '), targetIndustries: targets.join(', ') },\n    };\n  },\n};\n", "import { matchesList } from 'src/scoring/rules/helpers';\nimport type { ScoringRule } from 'src/scoring/types';\n\nexport const icpRegionRule: ScoringRule = {\n  id: 'icp.region',\n  name: 'Target region',\n  category: 'icp',\n  question: 'Is this company somewhere you can actually sell and deliver?',\n  why: 'A perfect-fit company in a territory you do not cover is a perfect-fit company for someone else. Region also drives who the lead should be routed to.',\n  defaultEnabled: true,\n  defaultSeverity: 'major',\n  defaultWeight: 10,\n  reads: ['region'],\n\n  evaluate: ({ config, read }) => {\n    const targets = config.icp.regions;\n\n    if (targets.length === 0) {\n      return {\n        outcome: 'not_applicable',\n        explanation:\n          'You have not listed any target regions, so every region is accepted.',\n        remedy:\n          'Add the regions you sell into to the Greenlight ICP settings to start filtering on region.',\n      };\n    }\n\n    const values = read.textList('region');\n\n    if (values.length === 0) {\n      return {\n        outcome: 'fail',\n        explanation:\n          'This lead has no country or region recorded, so we cannot tell whether you cover it.',\n        remedy: 'Set the country or region on the lead.',\n        detail: { targetRegions: targets.join(', ') },\n      };\n    }\n\n    const matched = matchesList(values, targets);\n\n    if (matched !== null) {\n      return {\n        outcome: 'pass',\n        explanation: `${matched} is a region you sell into.`,\n        detail: { region: matched },\n      };\n    }\n\n    return {\n      outcome: 'fail',\n      explanation: `${values.join(', ')} is outside the regions you sell into.`,\n      remedy: `Add it to your target regions if you do cover it. Your current list is: ${targets.join(', ')}.`,\n      detail: { region: values.join(', '), targetRegions: targets.join(', ') },\n    };\n  },\n};\n", "/**\n * The rule catalogue.\n *\n * Adding a rule is a local change: write a `ScoringRule` in its own file, add\n * it to `ALL_RULES`, and it inherits default settings, config plumbing,\n * weighting, tracing and fail-open error handling for free. Removing one is\n * equally local \u2014 a config record referencing a rule that no longer exists is\n * ignored rather than fatal.\n */\n\nimport { companyIdentifiedRule } from 'src/scoring/rules/company-identified.rule';\nimport { complianceOptOutRule } from 'src/scoring/rules/compliance-opt-out.rule';\nimport { contactDecisionMakerRule } from 'src/scoring/rules/contact-decision-maker.rule';\nimport { contactEmailValidRule } from 'src/scoring/rules/contact-email-valid.rule';\nimport { contactNamedPersonRule } from 'src/scoring/rules/contact-named-person.rule';\nimport { contactPhoneValidRule } from 'src/scoring/rules/contact-phone-valid.rule';\nimport { contactSharedInboxRule } from 'src/scoring/rules/contact-shared-inbox.rule';\nimport { dataFreshnessRule } from 'src/scoring/rules/data-freshness.rule';\nimport { icpCompanySizeRule } from 'src/scoring/rules/icp-company-size.rule';\nimport { icpIndustryRule } from 'src/scoring/rules/icp-industry.rule';\nimport { icpRegionRule } from 'src/scoring/rules/icp-region.rule';\nimport type { RuleCatalogueEntry, ScoringRule } from 'src/scoring/types';\n\nexport const ALL_RULES: readonly ScoringRule[] = [\n  complianceOptOutRule,\n  icpIndustryRule,\n  icpRegionRule,\n  icpCompanySizeRule,\n  companyIdentifiedRule,\n  contactNamedPersonRule,\n  contactDecisionMakerRule,\n  contactEmailValidRule,\n  contactSharedInboxRule,\n  contactPhoneValidRule,\n  dataFreshnessRule,\n];\n\nexport {\n  companyIdentifiedRule,\n  complianceOptOutRule,\n  contactDecisionMakerRule,\n  contactEmailValidRule,\n  contactNamedPersonRule,\n  contactPhoneValidRule,\n  contactSharedInboxRule,\n  dataFreshnessRule,\n  icpCompanySizeRule,\n  icpIndustryRule,\n  icpRegionRule,\n};\n\n/**\n * The published rule catalogue \u2014 \"each rule: what it checks, why, what fixes\n * it\". The same text belongs in the docs and in the in-app help.\n */\nexport const describeRuleCatalogue = (\n  rules: readonly ScoringRule[] = ALL_RULES,\n): readonly RuleCatalogueEntry[] =>\n  rules.map((rule) => ({\n    id: rule.id,\n    name: rule.name,\n    category: rule.category,\n    question: rule.question,\n    why: rule.why,\n    reads: rule.reads,\n    defaultEnabled: rule.defaultEnabled,\n    defaultSeverity: rule.defaultSeverity,\n    defaultWeight: rule.defaultWeight,\n  }));\n", "/**\n * Numaya Greenlight \u2014 deterministic scoring engine types.\n *\n * This module is intentionally free of any Twenty SDK import, any network call,\n * any filesystem access and any clock read. Everything the engine needs arrives\n * through `scoreLead()`'s input, including `now`. That is what makes the whole\n * scoring path unit-testable in complete isolation.\n */\n\n/** Bumped whenever a change alters the score a given lead would receive. */\nexport const SCORING_ENGINE_VERSION = '0.1.0';\n\n/* -------------------------------------------------------------------------- */\n/* Lead input                                                                  */\n/* -------------------------------------------------------------------------- */\n\n/**\n * A lead as the engine sees it: an opaque bag of field values plus an optional\n * record id used only for the trace. The engine never assumes a field name \u2014\n * every read goes through the configured {@link FieldMapping}.\n */\nexport interface LeadRecord {\n  readonly id?: string | null;\n  readonly fields: Readonly<Record<string, unknown>>;\n}\n\n/**\n * Logical field keys. Rules only ever speak in these; the customer's actual\n * column names live in the config's field mapping.\n */\nexport type LeadFieldKey =\n  | 'companyName'\n  | 'industry'\n  | 'region'\n  | 'employeeCount'\n  | 'contactName'\n  | 'jobTitle'\n  | 'seniority'\n  | 'email'\n  | 'phone'\n  | 'lastVerifiedAt'\n  /** Greenlight's own block-list flag. Its mapping is not customer-configurable. */\n  | 'suppressed'\n  /** Greenlight's own block-list reason code. Read for the explanation, not the verdict. */\n  | 'suppressionReason'\n  /** The *customer's* opt-out column(s), wherever the Layer 3 mapping points. */\n  | 'optedOut';\n\nexport const LEAD_FIELD_KEYS: readonly LeadFieldKey[] = [\n  'companyName',\n  'industry',\n  'region',\n  'employeeCount',\n  'contactName',\n  'jobTitle',\n  'seniority',\n  'email',\n  'phone',\n  'lastVerifiedAt',\n  'suppressed',\n  'suppressionReason',\n  'optedOut',\n];\n\n/**\n * One or more dotted paths into the lead record, tried in order. Multiple\n * candidates let a workspace keep a preferred field with sane fallbacks\n * (e.g. `lastVerifiedAt` \u2192 `updatedAt` \u2192 `createdAt`).\n */\nexport type FieldMapping = Readonly<\n  Record<LeadFieldKey, readonly string[]>\n>;\n\n/** What a rule actually looked at, so the trace can show its working. */\nexport interface FieldObservation {\n  readonly key: LeadFieldKey;\n  /** The candidate path that produced a value, or null when none did. */\n  readonly mappedTo: string | null;\n  readonly candidates: readonly string[];\n  readonly present: boolean;\n  /** Human-readable rendering of the value, e.g. `Manufacturing` / `(not set)`. */\n  readonly display: string;\n}\n\n/**\n * Field reader handed to every rule. All lookups are recorded so the trace\n * entry can name the exact fields consulted and the values seen.\n */\nexport interface FieldReader {\n  text(key: LeadFieldKey): string | null;\n  textList(key: LeadFieldKey): readonly string[];\n  number(key: LeadFieldKey): number | null;\n  date(key: LeadFieldKey): Date | null;\n  /** True when any mapped candidate resolves truthy; null when none are set. */\n  booleanAny(key: LeadFieldKey): boolean | null;\n  observations(): readonly FieldObservation[];\n}\n\n/* -------------------------------------------------------------------------- */\n/* Configuration                                                               */\n/* -------------------------------------------------------------------------- */\n\nexport type RuleSeverity =\n  /** A failure blocks the lead outright (compliance), whatever the score. */\n  | 'blocking'\n  /** A failure gates the lead even if the score clears the threshold. */\n  | 'critical'\n  | 'major'\n  | 'minor'\n  /** Reported in the trace but never contributes to the score. */\n  | 'advisory';\n\nexport const RULE_SEVERITIES: readonly RuleSeverity[] = [\n  'blocking',\n  'critical',\n  'major',\n  'minor',\n  'advisory',\n];\n\nexport type RuleCategory = 'icp' | 'contact' | 'data-quality' | 'compliance';\n\nexport interface IcpSizeBand {\n  readonly label: string;\n  readonly minEmployees: number;\n  /** `null` means unbounded. */\n  readonly maxEmployees: number | null;\n}\n\nexport interface IcpConfig {\n  /** Empty list means \"no opinion\" \u2014 the matching rule reports not-applicable. */\n  readonly industries: readonly string[];\n  readonly regions: readonly string[];\n  readonly sizeBands: readonly IcpSizeBand[];\n}\n\nexport interface ScoringBand {\n  readonly id: string;\n  readonly label: string;\n  /** Inclusive lower bound on the 0-100 score. */\n  readonly minScore: number;\n}\n\nexport interface RuleSetting {\n  readonly enabled: boolean;\n  readonly severity: RuleSeverity;\n  /** Relative weight. Only meaningful against the other enabled rules. */\n  readonly weight: number;\n}\n\nexport interface ResolvedScoringConfig {\n  readonly icp: IcpConfig;\n  readonly rules: Readonly<Record<string, RuleSetting>>;\n  /** Sorted high \u2192 low by `minScore`. */\n  readonly bands: readonly ScoringBand[];\n  /** Score at or above which a lead is approved. */\n  readonly gateThreshold: number;\n  readonly defaultShelfLifeDays: number;\n  readonly fieldShelfLifeDays: Readonly<Partial<Record<LeadFieldKey, number>>>;\n  /** Title keywords that indicate authority to buy. */\n  readonly decisionMakerTitles: readonly string[];\n  /** Title keywords that indicate influence but not authority (partial credit). */\n  readonly influencerTitles: readonly string[];\n  /** Mailbox local-parts that mean \"shared inbox, nobody owns replies\". */\n  readonly roleInboxLocalParts: readonly string[];\n  /** Values that mean \"somebody typed something rather than nothing\". */\n  readonly placeholderValues: readonly string[];\n  readonly fieldMapping: FieldMapping;\n}\n\n/** A deeply-optional config, i.e. whatever came out of the CRM config record. */\nexport type ScoringConfigInput = unknown;\n\n/**\n * Vocabulary that replaces the shipped defaults as the *fall-back* for a\n * workspace that has not expressed its own opinion.\n *\n * Structurally identical to the calibration baseline in `src/calibration/`, and\n * deliberately declared here rather than imported from there: the engine must\n * not be able to tell where a baseline came from, and a `src/scoring/` import of\n * `src/calibration/` would invert a dependency direction that is load-bearing\n * (see `src/calibration/index.ts`). Every field is optional \u2014 an absent one\n * leaves the corresponding shipped default exactly as it is.\n *\n * It carries no weights, no thresholds and no rule identifiers, and there is no\n * field here through which one could be smuggled. The worst a baseline can do is\n * change which strings a rule matches.\n */\nexport interface ScoringVocabularyBaseline {\n  readonly decisionMakerTitles?: readonly string[];\n  readonly influencerTitles?: readonly string[];\n  readonly roleInboxLocalParts?: readonly string[];\n  readonly placeholderValues?: readonly string[];\n  /** Canonical industry term \u2192 equivalent free-text variants. */\n  readonly industrySynonyms?: Readonly<Record<string, readonly string[]>>;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Fail-open reporting                                                         */\n/* -------------------------------------------------------------------------- */\n\nexport type DegradationCode =\n  | 'config_missing'\n  | 'config_malformed'\n  | 'icp_malformed'\n  | 'rules_malformed'\n  | 'rule_setting_malformed'\n  | 'weight_malformed'\n  | 'severity_malformed'\n  | 'bands_malformed'\n  | 'gate_threshold_malformed'\n  | 'shelf_life_malformed'\n  | 'field_mapping_malformed'\n  | 'title_list_malformed'\n  | 'lead_malformed'\n  | 'now_malformed'\n  | 'rule_errored'\n  | 'no_scorable_rules'\n  | 'engine_errored';\n\n/**\n * A recorded fall-back. Every degradation means the engine chose to keep going\n * with a safe default rather than fail \u2014 the product's core promise is that a\n * lead is never lost and never silently dropped.\n */\nexport interface Degradation {\n  readonly code: DegradationCode;\n  /** Written for a CRM admin, not a developer. */\n  readonly message: string;\n  readonly detail?: string;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Rules                                                                       */\n/* -------------------------------------------------------------------------- */\n\nexport type RuleOutcome =\n  | 'pass'\n  | 'partial'\n  | 'fail'\n  /** The rule has nothing to judge (e.g. the ICP list is empty). Not scored. */\n  | 'not_applicable'\n  /** Turned off in configuration. Not scored. */\n  | 'skipped'\n  /** The rule threw. Not scored \u2014 a broken rule never drags a lead down. */\n  | 'errored';\n\nexport type RuleDetail = Readonly<\n  Record<string, string | number | boolean | null | undefined>\n>;\n\n/** What a rule returns. `credit` defaults to 1 for pass and 0 for fail. */\nexport interface RuleVerdict {\n  readonly outcome: 'pass' | 'partial' | 'fail' | 'not_applicable';\n  /** 0-1. Required for `partial`; ignored elsewhere unless supplied. */\n  readonly credit?: number;\n  /** One sentence a salesperson understands. */\n  readonly explanation: string;\n  /** What to do about it, when there is something to do. */\n  readonly remedy?: string;\n  readonly detail?: RuleDetail;\n}\n\nexport interface RuleContext {\n  readonly lead: LeadRecord;\n  readonly config: ResolvedScoringConfig;\n  /** Null when the caller supplied an unusable `now`; time-based rules opt out. */\n  readonly now: Date | null;\n  readonly read: FieldReader;\n}\n\n/**\n * A single deterministic check. Adding a rule is a local change: write one of\n * these, add it to the catalogue array, done \u2014 defaults, config plumbing,\n * weighting, tracing and fail-open handling are all inherited.\n */\nexport interface ScoringRule {\n  readonly id: string;\n  /** Short label shown in the CRM, e.g. \"Decision-maker\". */\n  readonly name: string;\n  readonly category: RuleCategory;\n  /** The question the rule answers, in the buyer's words. */\n  readonly question: string;\n  /** Why the rule exists. Published in the rule catalogue and shown in-app. */\n  readonly why: string;\n  readonly defaultEnabled: boolean;\n  readonly defaultSeverity: RuleSeverity;\n  readonly defaultWeight: number;\n  readonly reads: readonly LeadFieldKey[];\n  evaluate(context: RuleContext): RuleVerdict;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Result                                                                      */\n/* -------------------------------------------------------------------------- */\n\nexport type GateDecision =\n  /** Cleared to work. */\n  | 'approved'\n  /** Held in the greenlight queue: visible, explained, human-overridable. */\n  | 'gated'\n  /** Compliance stop (e.g. opted out). Still visible and overridable. */\n  | 'blocked'\n  /** Nothing could be scored. Fail-open: the lead passes through, flagged. */\n  | 'unscored';\n\nexport interface RuleTraceEntry {\n  readonly ruleId: string;\n  readonly ruleName: string;\n  readonly category: RuleCategory;\n  readonly question: string;\n  readonly outcome: RuleOutcome;\n  readonly severity: RuleSeverity;\n  /** 0-1 share of this rule's weight that the lead earned. */\n  readonly credit: number;\n  readonly weight: number;\n  readonly pointsEarned: number;\n  readonly pointsPossible: number;\n  /** False for skipped / not-applicable / errored / advisory rules. */\n  readonly contributed: boolean;\n  readonly explanation: string;\n  readonly remedy?: string;\n  readonly detail?: RuleDetail;\n  readonly observations: readonly FieldObservation[];\n  readonly error?: string;\n}\n\nexport interface ScoringResult {\n  /** 0-100, rounded to one decimal. Null only when nothing was scorable. */\n  readonly score: number | null;\n  readonly band: ScoringBand | null;\n  readonly decision: GateDecision;\n  readonly gateThreshold: number;\n  /** One sentence for the top of the lead record. */\n  readonly summary: string;\n  /** The headline reasons behind the decision, worst first. */\n  readonly reasons: readonly string[];\n  readonly trace: readonly RuleTraceEntry[];\n  readonly degradations: readonly Degradation[];\n  readonly configSource: ConfigSource;\n  /** ISO timestamp of the `now` that was passed in. */\n  readonly scoredAt: string | null;\n  readonly leadId: string | null;\n  readonly engineVersion: string;\n  readonly totalWeight: number;\n  readonly earnedWeight: number;\n}\n\nexport type ConfigSource = 'provided' | 'defaults' | 'repaired';\n\nexport interface ConfigResolution {\n  readonly config: ResolvedScoringConfig;\n  readonly source: ConfigSource;\n  readonly degradations: readonly Degradation[];\n}\n\nexport interface ScoreLeadInput {\n  readonly lead: LeadRecord;\n  /**\n   * The clock, passed in. The engine never reads the clock itself, so every\n   * run is reproducible from its inputs alone.\n   */\n  readonly now: Date;\n  /** Raw config record straight from the CRM. Anything unusable is repaired. */\n  readonly config?: ScoringConfigInput;\n  /** Override the rule catalogue (tests, future per-workspace rule packs). */\n  readonly rules?: readonly ScoringRule[];\n  /**\n   * Licence-delivered vocabulary, if any. Absent \u2014 which is the unlicensed,\n   * offline and signature-failing case \u2014 scores identically to a build that\n   * never had this parameter.\n   */\n  readonly baseline?: ScoringVocabularyBaseline | null;\n}\n\n/** Public documentation shape for the rule catalogue (docs + in-app help). */\nexport interface RuleCatalogueEntry {\n  readonly id: string;\n  readonly name: string;\n  readonly category: RuleCategory;\n  readonly question: string;\n  readonly why: string;\n  readonly reads: readonly LeadFieldKey[];\n  readonly defaultEnabled: boolean;\n  readonly defaultSeverity: RuleSeverity;\n  readonly defaultWeight: number;\n}\n", "/**\n * Configuration resolution.\n *\n * `resolveConfig` takes whatever came out of the CRM config record \u2014 including\n * `undefined`, `null`, a string, or an object with half its fields the wrong\n * type \u2014 and always returns a usable config plus a list of the fall-backs it\n * had to make. Nothing in here throws. Missing config falls back to seeded\n * defaults; malformed weights fall back to neutral weights.\n */\n\nimport { ENRICHED_FIELD_MAPPING_PATHS } from 'src/enrichment/field-specs';\nimport {\n  DEFAULT_BANDS,\n  DEFAULT_DECISION_MAKER_TITLES,\n  DEFAULT_FIELD_MAPPING,\n  DEFAULT_GATE_THRESHOLD,\n  DEFAULT_ICP,\n  DEFAULT_INFLUENCER_TITLES,\n  DEFAULT_ROLE_INBOX_LOCAL_PARTS,\n  DEFAULT_SHELF_LIFE_DAYS,\n  PLACEHOLDER_VALUES,\n} from 'src/scoring/defaults';\nimport { isPlainObject } from 'src/scoring/field-access';\nimport { ALL_RULES } from 'src/scoring/rules';\nimport {\n  LEAD_FIELD_KEYS,\n  RULE_SEVERITIES,\n  type ConfigResolution,\n  type Degradation,\n  type DegradationCode,\n  type FieldMapping,\n  type IcpConfig,\n  type IcpSizeBand,\n  type LeadFieldKey,\n  type ResolvedScoringConfig,\n  type RuleSetting,\n  type RuleSeverity,\n  type ScoringBand,\n  type ScoringConfigInput,\n  type ScoringRule,\n  type ScoringVocabularyBaseline,\n} from 'src/scoring/types';\n\n/**\n * Set equality over normalised strings.\n *\n * Exported because `src/calibration/apply.ts` needs exactly this comparison and\n * the dependency may only run in that direction \u2014 calibration imports scoring,\n * never the reverse. Duplicating ten lines across that boundary would be two\n * implementations of one rule, and the rule decides whether a workspace's\n * configuration is honoured.\n */\nexport const isSameStringSet = (\n  left: readonly string[],\n  right: readonly string[],\n): boolean => {\n  const leftSet = new Set(left.map((entry) => entry.trim().toLowerCase().replace(/\\s+/g, ' ')));\n  const rightSet = new Set(right.map((entry) => entry.trim().toLowerCase().replace(/\\s+/g, ' ')));\n\n  if (leftSet.size !== rightSet.size) {\n    return false;\n  }\n\n  for (const entry of rightSet) {\n    if (!leftSet.has(entry)) {\n      return false;\n    }\n  }\n\n  return true;\n};\n\n/**\n * Expand a workspace's ICP industries through a synonym taxonomy.\n *\n * Declared here rather than imported from `src/calibration/` for the dependency\n * reason above, and applied at resolution rather than inside\n * `icp-industry.rule.ts` so the rule stays a pure set-membership test. The rule\n * is untouched by this feature; it simply finds a longer list of acceptable\n * strings. See `src/calibration/apply.ts` for the full argument.\n */\nexport const expandIndustriesWithSynonyms = (\n  industries: readonly string[],\n  synonyms: Readonly<Record<string, readonly string[]>> | undefined,\n): readonly string[] => {\n  if (synonyms === undefined || industries.length === 0) {\n    return industries;\n  }\n\n  const clusters = Object.entries(synonyms);\n\n  if (clusters.length === 0) {\n    return industries;\n  }\n\n  const variantToCanonical = new Map<string, string>();\n\n  for (const [canonical, variants] of clusters) {\n    const canonicalKey = normaliseTerm(canonical);\n    variantToCanonical.set(canonicalKey, canonicalKey);\n\n    for (const variant of variants) {\n      const variantKey = normaliseTerm(variant);\n\n      // A variant listed under two canonicals is a defect in the pack, and the\n      // resolution has to be independent of object key order or the same pack\n      // would behave differently in two builds. Two rules, in this order:\n      // **a canonical always wins over a variant** (the unconditional `set`\n      // above), and among competing variants the **first cluster wins**. Both\n      // are order-independent given a fixed pack, which is the property that\n      // matters.\n      if (!variantToCanonical.has(variantKey)) {\n        variantToCanonical.set(variantKey, canonicalKey);\n      }\n    }\n  }\n\n  // The workspace's own terms first and unmodified \u2014 expansion only ever adds.\n  const expanded = new Set(industries.map(normaliseTerm));\n\n  for (const industry of industries) {\n    const canonical = variantToCanonical.get(normaliseTerm(industry));\n\n    if (canonical === undefined) {\n      continue;\n    }\n\n    expanded.add(canonical);\n\n    for (const variant of synonyms[canonical] ?? []) {\n      expanded.add(normaliseTerm(variant));\n    }\n  }\n\n  return [...expanded];\n};\n\nconst normaliseTerm = (value: string): string =>\n  value.trim().toLowerCase().replace(/\\s+/g, ' ');\n\n/**\n * Append Greenlight's own enriched-value paths to the end of every enrichable\n * key's candidate list.\n *\n * ## Why this lives here and not in the config record\n *\n * `greenlightEnrichment` is a column Greenlight ships, owns and writes, so\n * \"where does an enriched industry live\" has exactly one answer and it is not\n * the customer's to give. There is also nowhere in `GreenlightConfig` to say it:\n * the record exposes precisely two mapping settings (`decisionMakerFieldName`,\n * `optOutFieldNames`), so seeding these five paths would mean minting columns\n * for values no admin should ever edit \u2014 and a seeded value is a value that can\n * be cleared, which would put us straight back to enrichment writing data\n * nothing reads. Doing it here also covers the workspace with *no* config record\n * at all, where `resolveConfig` short-circuits to `buildDefaultConfig` and never\n * looks at a field mapping the record could have carried.\n *\n * ## Appended, not pinned\n *\n * `PINNED_FIELD_MAPPING_KEYS` below *rejects* a workspace's mapping outright.\n * That is right for `suppressed`, whose key names a Greenlight-only column, and\n * wrong here: `industry`, `region`, `employeeCount`, `jobTitle` and `seniority`\n * are genuine Layer 3 seams and a workspace must keep saying where its own\n * columns live. The treatment is therefore the weaker half of pinning \u2014 the\n * workspace owns the head of the list, Greenlight owns the tail:\n *\n *   - **Unconditional**, so a config typo cannot silently disconnect enrichment.\n *     That is the same safety property pinning buys, without the cost.\n *   - **Always last**, so `field-access.ts` \u2014 which takes the first candidate\n *     that yields a value \u2014 reaches it only when every human-held candidate is\n *     empty. Enrichment fills a hole; it never overwrites a person.\n *\n * `DEFAULT_FIELD_MAPPING` itself is deliberately left alone: `src/enrichment/\n * plan.ts` reads it to answer \"has a human already answered this field\", and an\n * enriched value visible there would read as human input and never refresh.\n */\nconst withEnrichedFallbacks = (mapping: FieldMapping): FieldMapping => {\n  const next: Record<LeadFieldKey, readonly string[]> = { ...mapping };\n\n  for (const [key, path] of Object.entries(ENRICHED_FIELD_MAPPING_PATHS)) {\n    const leadKey = key as LeadFieldKey;\n    const candidates = next[leadKey] ?? [];\n\n    next[leadKey] = candidates.includes(path)\n      ? candidates\n      : [...candidates, path];\n  }\n\n  return next;\n};\n\n/**\n * The complete seeded configuration. Also what the install hook should write.\n *\n * The optional `baseline` supplies licence-delivered vocabulary in place of the\n * shipped lists. It is *only* consulted for the four vocabulary lists \u2014 bands,\n * weights, thresholds, shelf lives, the ICP and the field mapping are read from\n * the shipped constants unconditionally, and there is no argument here through\n * which a baseline could reach them.\n */\nexport const buildDefaultConfig = (\n  rules: readonly ScoringRule[] = ALL_RULES,\n  baseline?: ScoringVocabularyBaseline | null,\n): ResolvedScoringConfig => ({\n  icp: DEFAULT_ICP,\n  rules: Object.fromEntries(\n    rules.map((rule) => [\n      rule.id,\n      {\n        enabled: rule.defaultEnabled,\n        severity: rule.defaultSeverity,\n        weight: rule.defaultWeight,\n      } satisfies RuleSetting,\n    ]),\n  ),\n  bands: DEFAULT_BANDS,\n  gateThreshold: DEFAULT_GATE_THRESHOLD,\n  defaultShelfLifeDays: DEFAULT_SHELF_LIFE_DAYS,\n  fieldShelfLifeDays: {},\n  decisionMakerTitles:\n    baseline?.decisionMakerTitles ?? DEFAULT_DECISION_MAKER_TITLES,\n  influencerTitles: baseline?.influencerTitles ?? DEFAULT_INFLUENCER_TITLES,\n  roleInboxLocalParts:\n    baseline?.roleInboxLocalParts ?? DEFAULT_ROLE_INBOX_LOCAL_PARTS,\n  placeholderValues: baseline?.placeholderValues ?? PLACEHOLDER_VALUES,\n  fieldMapping: withEnrichedFallbacks(DEFAULT_FIELD_MAPPING),\n});\n\nconst isFiniteNumber = (value: unknown): value is number =>\n  typeof value === 'number' && Number.isFinite(value);\n\nconst toStringList = (value: unknown): string[] | null => {\n  if (typeof value === 'string') {\n    const trimmed = value.trim();\n    return trimmed.length > 0 ? [trimmed] : [];\n  }\n\n  if (!Array.isArray(value)) {\n    return null;\n  }\n\n  return value\n    .filter((entry): entry is string => typeof entry === 'string')\n    .map((entry) => entry.trim())\n    .filter((entry) => entry.length > 0);\n};\n\nconst lowerAll = (values: readonly string[]): string[] =>\n  values.map((value) => value.toLowerCase());\n\nclass DegradationLog {\n  private readonly entries: Degradation[] = [];\n\n  add(code: DegradationCode, message: string, detail?: string): void {\n    this.entries.push(detail === undefined ? { code, message } : { code, message, detail });\n  }\n\n  get list(): readonly Degradation[] {\n    return this.entries;\n  }\n\n  get count(): number {\n    return this.entries.length;\n  }\n}\n\nconst resolveIcp = (raw: unknown, log: DegradationLog): IcpConfig => {\n  if (raw === undefined || raw === null) {\n    return DEFAULT_ICP;\n  }\n\n  if (!isPlainObject(raw)) {\n    log.add(\n      'icp_malformed',\n      'Your ideal-customer-profile settings could not be read, so no industry, region or size filtering was applied.',\n    );\n    return DEFAULT_ICP;\n  }\n\n  const industries = toStringList(raw['industries']);\n  const regions = toStringList(raw['regions']);\n\n  if (raw['industries'] !== undefined && industries === null) {\n    log.add(\n      'icp_malformed',\n      'Your ICP industry list could not be read, so every industry was accepted.',\n    );\n  }\n\n  if (raw['regions'] !== undefined && regions === null) {\n    log.add(\n      'icp_malformed',\n      'Your ICP region list could not be read, so every region was accepted.',\n    );\n  }\n\n  const rawBands = raw['sizeBands'];\n  let sizeBands: IcpSizeBand[] = [];\n\n  if (rawBands !== undefined && rawBands !== null) {\n    if (!Array.isArray(rawBands)) {\n      log.add(\n        'icp_malformed',\n        'Your ICP company-size bands could not be read, so every company size was accepted.',\n      );\n    } else {\n      sizeBands = rawBands.flatMap((entry): IcpSizeBand[] => {\n        if (!isPlainObject(entry)) {\n          return [];\n        }\n\n        const min = entry['minEmployees'];\n        const max = entry['maxEmployees'];\n\n        if (!isFiniteNumber(min) || min < 0) {\n          return [];\n        }\n\n        const resolvedMax = isFiniteNumber(max) ? max : null;\n\n        if (resolvedMax !== null && resolvedMax < min) {\n          return [];\n        }\n\n        const label =\n          typeof entry['label'] === 'string' && entry['label'].trim().length > 0\n            ? entry['label'].trim()\n            : `${min}-${resolvedMax ?? '\u221E'} employees`;\n\n        return [{ label, minEmployees: min, maxEmployees: resolvedMax }];\n      });\n\n      if (sizeBands.length !== rawBands.length) {\n        log.add(\n          'icp_malformed',\n          'Some ICP company-size bands were incomplete and were ignored.',\n          `${rawBands.length - sizeBands.length} of ${rawBands.length} size bands dropped`,\n        );\n      }\n    }\n  }\n\n  return {\n    industries: industries ?? [],\n    regions: regions ?? [],\n    sizeBands,\n  };\n};\n\nconst readRuleOverrides = (\n  raw: unknown,\n  log: DegradationLog,\n): Map<string, Record<string, unknown>> => {\n  const overrides = new Map<string, Record<string, unknown>>();\n\n  if (raw === undefined || raw === null) {\n    return overrides;\n  }\n\n  // Accept both a keyed object and an array of { id, ... } records, because a\n  // CRM config record can reasonably be modelled either way.\n  if (Array.isArray(raw)) {\n    for (const entry of raw) {\n      if (!isPlainObject(entry)) {\n        continue;\n      }\n\n      const id = entry['id'] ?? entry['ruleId'];\n\n      if (typeof id === 'string' && id.length > 0) {\n        overrides.set(id, entry);\n      }\n    }\n\n    return overrides;\n  }\n\n  if (!isPlainObject(raw)) {\n    log.add(\n      'rules_malformed',\n      'Your rule settings could not be read, so every rule ran at its default setting.',\n    );\n    return overrides;\n  }\n\n  for (const [id, entry] of Object.entries(raw)) {\n    if (isPlainObject(entry)) {\n      overrides.set(id, entry);\n      continue;\n    }\n\n    if (typeof entry === 'boolean') {\n      overrides.set(id, { enabled: entry });\n      continue;\n    }\n\n    log.add(\n      'rule_setting_malformed',\n      `The settings for rule \"${id}\" could not be read, so the rule ran at its default setting.`,\n    );\n  }\n\n  return overrides;\n};\n\nconst resolveRuleSettings = (\n  raw: unknown,\n  rules: readonly ScoringRule[],\n  log: DegradationLog,\n): Record<string, RuleSetting> => {\n  const overrides = readRuleOverrides(raw, log);\n  const settings: Record<string, RuleSetting> = {};\n\n  for (const rule of rules) {\n    const override = overrides.get(rule.id);\n\n    let enabled = rule.defaultEnabled;\n    let severity: RuleSeverity = rule.defaultSeverity;\n    let weight = rule.defaultWeight;\n\n    if (override !== undefined) {\n      const rawEnabled = override['enabled'];\n\n      if (typeof rawEnabled === 'boolean') {\n        enabled = rawEnabled;\n      } else if (rawEnabled !== undefined && rawEnabled !== null) {\n        log.add(\n          'rule_setting_malformed',\n          `\"${rule.name}\" had an unreadable on/off setting, so it stayed at its default.`,\n          `rule ${rule.id}`,\n        );\n      }\n\n      const rawSeverity = override['severity'];\n\n      if (\n        typeof rawSeverity === 'string' &&\n        (RULE_SEVERITIES as readonly string[]).includes(rawSeverity)\n      ) {\n        severity = rawSeverity as RuleSeverity;\n      } else if (rawSeverity !== undefined && rawSeverity !== null) {\n        log.add(\n          'severity_malformed',\n          `\"${rule.name}\" had an unrecognised severity, so its default severity was used.`,\n          `rule ${rule.id}`,\n        );\n      }\n\n      const rawWeight = override['weight'];\n\n      if (isFiniteNumber(rawWeight) && rawWeight >= 0) {\n        weight = rawWeight;\n      } else if (rawWeight !== undefined && rawWeight !== null) {\n        log.add(\n          'weight_malformed',\n          `\"${rule.name}\" had an unusable weight, so its default weight was used instead.`,\n          `rule ${rule.id}`,\n        );\n      }\n    }\n\n    settings[rule.id] = { enabled, severity, weight };\n  }\n\n  // Neutral-weight fall-back: if nothing that can score carries any weight, the\n  // score would be undefined. Give every rule the same voice instead.\n  const scorable = rules.filter((rule) => {\n    const setting = settings[rule.id];\n    return setting !== undefined && setting.enabled && setting.severity !== 'advisory';\n  });\n\n  const totalWeight = scorable.reduce(\n    (sum, rule) => sum + (settings[rule.id]?.weight ?? 0),\n    0,\n  );\n\n  if (scorable.length > 0 && totalWeight <= 0) {\n    log.add(\n      'weight_malformed',\n      'None of your enabled rules carried any weight, so every rule was given equal weight.',\n    );\n\n    for (const rule of scorable) {\n      const current = settings[rule.id];\n\n      if (current !== undefined) {\n        settings[rule.id] = { ...current, weight: 1 };\n      }\n    }\n  }\n\n  return settings;\n};\n\nconst resolveBands = (raw: unknown, log: DegradationLog): readonly ScoringBand[] => {\n  if (raw === undefined || raw === null) {\n    return DEFAULT_BANDS;\n  }\n\n  if (!Array.isArray(raw)) {\n    log.add(\n      'bands_malformed',\n      'Your score bands could not be read, so the standard Excellent/Good/Fair/Poor bands were used.',\n    );\n    return DEFAULT_BANDS;\n  }\n\n  const bands = raw.flatMap((entry): ScoringBand[] => {\n    if (!isPlainObject(entry)) {\n      return [];\n    }\n\n    const minScore = entry['minScore'];\n\n    if (!isFiniteNumber(minScore)) {\n      return [];\n    }\n\n    const id =\n      typeof entry['id'] === 'string' && entry['id'].trim().length > 0\n        ? entry['id'].trim()\n        : null;\n\n    if (id === null) {\n      return [];\n    }\n\n    const label =\n      typeof entry['label'] === 'string' && entry['label'].trim().length > 0\n        ? entry['label'].trim()\n        : id;\n\n    return [{ id, label, minScore: Math.min(100, Math.max(0, minScore)) }];\n  });\n\n  if (bands.length === 0) {\n    log.add(\n      'bands_malformed',\n      'No usable score bands were configured, so the standard Excellent/Good/Fair/Poor bands were used.',\n    );\n    return DEFAULT_BANDS;\n  }\n\n  if (bands.length !== raw.length) {\n    log.add(\n      'bands_malformed',\n      'Some score bands were incomplete and were ignored.',\n      `${raw.length - bands.length} of ${raw.length} bands dropped`,\n    );\n  }\n\n  return [...bands].sort((a, b) => b.minScore - a.minScore);\n};\n\nconst resolveGateThreshold = (raw: unknown, log: DegradationLog): number => {\n  if (raw === undefined || raw === null) {\n    return DEFAULT_GATE_THRESHOLD;\n  }\n\n  if (!isFiniteNumber(raw) || raw < 0 || raw > 100) {\n    log.add(\n      'gate_threshold_malformed',\n      `Your gate threshold was not a score between 0 and 100, so the default of ${DEFAULT_GATE_THRESHOLD} was used.`,\n    );\n    return DEFAULT_GATE_THRESHOLD;\n  }\n\n  return raw;\n};\n\nconst resolveShelfLife = (raw: unknown, log: DegradationLog): number => {\n  if (raw === undefined || raw === null) {\n    return DEFAULT_SHELF_LIFE_DAYS;\n  }\n\n  if (!isFiniteNumber(raw) || raw <= 0) {\n    log.add(\n      'shelf_life_malformed',\n      `Your data shelf life was not a positive number of days, so the default of ${DEFAULT_SHELF_LIFE_DAYS} days was used.`,\n    );\n    return DEFAULT_SHELF_LIFE_DAYS;\n  }\n\n  return raw;\n};\n\nconst resolveFieldShelfLives = (\n  raw: unknown,\n  log: DegradationLog,\n): Partial<Record<LeadFieldKey, number>> => {\n  if (raw === undefined || raw === null) {\n    return {};\n  }\n\n  if (!isPlainObject(raw)) {\n    log.add(\n      'shelf_life_malformed',\n      'Your per-field shelf lives could not be read, so the single default shelf life was used for every field.',\n    );\n    return {};\n  }\n\n  const resolved: Partial<Record<LeadFieldKey, number>> = {};\n\n  for (const key of LEAD_FIELD_KEYS) {\n    const value = raw[key];\n\n    if (value === undefined || value === null) {\n      continue;\n    }\n\n    if (isFiniteNumber(value) && value > 0) {\n      resolved[key] = value;\n    } else {\n      log.add(\n        'shelf_life_malformed',\n        `The shelf life for \"${key}\" was not a positive number of days, so the default shelf life was used.`,\n      );\n    }\n  }\n\n  return resolved;\n};\n\n/**\n * Resolve one vocabulary list against the three-layer precedence.\n *\n * `shipped` is the in-source constant and `baseline` is the licence-delivered\n * replacement for it, or `undefined` when there is none. The workspace's own\n * value still wins over both \u2014 with one carefully-bounded exception.\n *\n * ## The exception: an unedited seed is not a choice\n *\n * The post-install hook copies the shipped decision-maker list *into* the config\n * record so an admin can see and edit it. Every fresh install therefore holds an\n * \"explicit\" list that is really just the default, and treating it as a choice\n * would let it shadow calibration permanently \u2014 calibration would arrive, verify,\n * publish, and change nothing.\n *\n * So a stored list whose set of entries is exactly the shipped set is recognised\n * as an untouched seed and steps aside for the baseline. Adding or removing a\n * single entry makes it a genuine curation that nothing will override. The\n * comparison is exact set equality: no subset test, no size heuristic, nothing\n * that could mistake a real edit for a seed. And it only ever applies when a\n * baseline exists \u2014 with no calibration, the shipped list is what the seed\n * resolves to either way, so the branch is unreachable in an unlicensed install.\n */\nconst resolveTitleList = (\n  raw: unknown,\n  shipped: readonly string[],\n  baseline: readonly string[] | undefined,\n  label: string,\n  log: DegradationLog,\n): readonly string[] => {\n  const fallback = baseline ?? shipped;\n\n  if (raw === undefined || raw === null) {\n    return fallback;\n  }\n\n  const list = toStringList(raw);\n\n  if (list === null || list.length === 0) {\n    log.add(\n      'title_list_malformed',\n      `Your ${label} list could not be read, so the built-in list was used.`,\n    );\n    return fallback;\n  }\n\n  if (baseline !== undefined && isSameStringSet(list, shipped)) {\n    return baseline;\n  }\n\n  return lowerAll(list);\n};\n\n/**\n * Keys the customer's field mapping is not allowed to move.\n *\n * These are Greenlight's own suppression columns: the app ships them, owns them,\n * and knows exactly where they live. Every other key here is a genuine Layer 3\n * seam because the field belongs to the customer's schema \u2014 these two do not,\n * and a config blob able to point `suppressed` somewhere else is a config blob\n * able to switch the block list off by mistyping a column name. The workspace's\n * *own* opt-out columns remain fully mappable under `optedOut`, and the\n * compliance rule takes the union of the two.\n */\nconst PINNED_FIELD_MAPPING_KEYS: readonly LeadFieldKey[] = [\n  'suppressed',\n  'suppressionReason',\n];\n\n/**\n * The workspace's mapping, resolved and repaired \u2014 before the enriched tail is\n * appended. Split out so `withEnrichedFallbacks` runs on *every* return path,\n * including the two that bail out early on unreadable configuration.\n */\nconst readConfiguredFieldMapping = (\n  raw: unknown,\n  log: DegradationLog,\n): FieldMapping => {\n  const mapping: Record<LeadFieldKey, readonly string[]> = {\n    ...DEFAULT_FIELD_MAPPING,\n  };\n\n  if (raw === undefined || raw === null) {\n    return mapping;\n  }\n\n  if (!isPlainObject(raw)) {\n    log.add(\n      'field_mapping_malformed',\n      'Your field mapping could not be read, so the default field names were used.',\n    );\n    return mapping;\n  }\n\n  for (const key of LEAD_FIELD_KEYS) {\n    const value = raw[key];\n\n    if (value === undefined || value === null) {\n      continue;\n    }\n\n    if (PINNED_FIELD_MAPPING_KEYS.includes(key)) {\n      log.add(\n        'field_mapping_malformed',\n        `\"${key}\" is one of Greenlight's own do-not-contact fields and cannot be remapped, so the mapping you supplied for it was ignored. Use \"Opt-out fields\" to add your own opt-out columns alongside it.`,\n      );\n      continue;\n    }\n\n    const paths = toStringList(value);\n\n    if (paths === null || paths.length === 0) {\n      log.add(\n        'field_mapping_malformed',\n        `The field mapping for \"${key}\" was unusable, so the default field names were used.`,\n      );\n      continue;\n    }\n\n    mapping[key] = paths;\n  }\n\n  return mapping;\n};\n\nconst resolveFieldMapping = (raw: unknown, log: DegradationLog): FieldMapping =>\n  withEnrichedFallbacks(readConfiguredFieldMapping(raw, log));\n\n/**\n * Turn an untrusted config value into a usable configuration.\n *\n * Never throws. The returned `source` tells the caller whether the workspace's\n * own configuration was used as-is (`provided`), had to be patched\n * (`repaired`), or was absent entirely (`defaults`).\n *\n * ## The baseline argument\n *\n * `baseline` is licence-delivered vocabulary \u2014 see\n * `src/calibration/apply.ts`. It is a *fall-back* replacement, never an\n * override: it stands in for the shipped constants when the workspace has said\n * nothing, and it is invisible when the workspace has said something. Passing\n * `undefined` or `null` \u2014 the unlicensed, offline, stale and\n * signature-failing cases, which is to say every case where anything went wrong\n * \u2014 makes this function behave exactly as it did before the parameter existed.\n * `__tests__/config.test.ts` asserts that identity rather than asserting a\n * promise about it.\n *\n * The baseline never reaches `source`. A calibrated workspace with an otherwise\n * clean config record still reports `provided`, because calibration is not a\n * repair and an admin reading the trace should not see one.\n */\nexport const resolveConfig = (\n  raw: ScoringConfigInput,\n  rules: readonly ScoringRule[] = ALL_RULES,\n  baseline?: ScoringVocabularyBaseline | null,\n): ConfigResolution => {\n  const log = new DegradationLog();\n  const vocabulary = baseline ?? undefined;\n\n  if (raw === undefined || raw === null) {\n    return {\n      config: buildDefaultConfig(rules, vocabulary),\n      source: 'defaults',\n      degradations: [\n        {\n          code: 'config_missing',\n          message:\n            'No Greenlight configuration was found, so this lead was scored with the built-in defaults.',\n        },\n      ],\n    };\n  }\n\n  if (!isPlainObject(raw)) {\n    return {\n      config: buildDefaultConfig(rules, vocabulary),\n      source: 'defaults',\n      degradations: [\n        {\n          code: 'config_malformed',\n          message:\n            'The Greenlight configuration record could not be read, so this lead was scored with the built-in defaults.',\n          detail: `expected an object, received ${Array.isArray(raw) ? 'array' : typeof raw}`,\n        },\n      ],\n    };\n  }\n\n  const resolvedIcp = resolveIcp(raw['icp'], log);\n\n  const config: ResolvedScoringConfig = {\n    // Expansion is applied to the *resolved* ICP, so a workspace that listed\n    // \"SaaS\" keeps \"SaaS\" at the head of the list and gains its cluster behind\n    // it. An empty list is returned untouched \u2014 \"no opinion\" must not become\n    // \"an opinion about eighty industries\".\n    icp: {\n      ...resolvedIcp,\n      industries: expandIndustriesWithSynonyms(\n        resolvedIcp.industries,\n        vocabulary?.industrySynonyms,\n      ),\n    },\n    rules: resolveRuleSettings(raw['rules'], rules, log),\n    bands: resolveBands(raw['bands'], log),\n    gateThreshold: resolveGateThreshold(raw['gateThreshold'], log),\n    defaultShelfLifeDays: resolveShelfLife(raw['defaultShelfLifeDays'], log),\n    fieldShelfLifeDays: resolveFieldShelfLives(raw['fieldShelfLifeDays'], log),\n    decisionMakerTitles: lowerAll(\n      resolveTitleList(\n        raw['decisionMakerTitles'],\n        DEFAULT_DECISION_MAKER_TITLES,\n        vocabulary?.decisionMakerTitles,\n        'decision-maker title',\n        log,\n      ),\n    ),\n    influencerTitles: lowerAll(\n      resolveTitleList(\n        raw['influencerTitles'],\n        DEFAULT_INFLUENCER_TITLES,\n        vocabulary?.influencerTitles,\n        'influencer title',\n        log,\n      ),\n    ),\n    roleInboxLocalParts: lowerAll(\n      resolveTitleList(\n        raw['roleInboxLocalParts'],\n        DEFAULT_ROLE_INBOX_LOCAL_PARTS,\n        vocabulary?.roleInboxLocalParts,\n        'shared-inbox',\n        log,\n      ),\n    ),\n    placeholderValues: lowerAll(\n      resolveTitleList(\n        raw['placeholderValues'],\n        PLACEHOLDER_VALUES,\n        vocabulary?.placeholderValues,\n        'placeholder value',\n        log,\n      ),\n    ),\n    fieldMapping: resolveFieldMapping(raw['fieldMapping'], log),\n  };\n\n  return {\n    config,\n    source: log.count > 0 ? 'repaired' : 'provided',\n    degradations: log.list,\n  };\n};\n", "/**\n * The scoring engine.\n *\n * Pure: no SDK import, no network, no I/O, no clock read. `now` arrives as a\n * parameter so every run is reproducible from its inputs alone.\n *\n * Fail-open is a hard product rule, and it is implemented at four levels:\n *   1. missing or unusable config     \u2192 seeded defaults / neutral weights\n *   2. a rule that throws             \u2192 recorded as `errored`, excluded from\n *                                       the score, never fatal\n *   3. nothing scorable at all        \u2192 decision `unscored`, lead passes\n *                                       through flagged rather than lost\n *   4. anything else that throws      \u2192 caught at the boundary, same as (3)\n *\n * A lead is never lost and never silently dropped.\n */\n\nimport { createFieldReader, isPlainObject } from 'src/scoring/field-access';\nimport { resolveConfig } from 'src/scoring/config';\nimport { ALL_RULES } from 'src/scoring/rules';\nimport { clamp01, roundTo } from 'src/scoring/rules/helpers';\nimport {\n  SCORING_ENGINE_VERSION,\n  type Degradation,\n  type GateDecision,\n  type LeadRecord,\n  type ResolvedScoringConfig,\n  type RuleSetting,\n  type RuleTraceEntry,\n  type RuleVerdict,\n  type ScoreLeadInput,\n  type ScoringBand,\n  type ScoringResult,\n  type ScoringRule,\n} from 'src/scoring/types';\n\nconst MAX_REASONS = 5;\n\nconst isUsableDate = (value: unknown): value is Date =>\n  value instanceof Date && !Number.isNaN(value.getTime());\n\nconst errorMessage = (error: unknown): string => {\n  if (error instanceof Error) {\n    return error.message;\n  }\n\n  return typeof error === 'string' ? error : 'unknown error';\n};\n\nconst creditFor = (verdict: RuleVerdict): number => {\n  switch (verdict.outcome) {\n    case 'pass':\n      return verdict.credit === undefined ? 1 : clamp01(verdict.credit);\n    case 'partial':\n      return verdict.credit === undefined ? 0.5 : clamp01(verdict.credit);\n    case 'fail':\n      return verdict.credit === undefined ? 0 : clamp01(verdict.credit);\n    case 'not_applicable':\n    default:\n      return 0;\n  }\n};\n\nconst settingFor = (\n  config: ResolvedScoringConfig,\n  rule: ScoringRule,\n): RuleSetting =>\n  config.rules[rule.id] ?? {\n    enabled: rule.defaultEnabled,\n    severity: rule.defaultSeverity,\n    weight: rule.defaultWeight,\n  };\n\nconst bandFor = (\n  score: number | null,\n  bands: readonly ScoringBand[],\n): ScoringBand | null => {\n  if (score === null) {\n    return null;\n  }\n\n  return (\n    [...bands]\n      .sort((a, b) => b.minScore - a.minScore)\n      .find((band) => score >= band.minScore) ?? null\n  );\n};\n\nconst SEVERITY_RANK: Record<string, number> = {\n  blocking: 0,\n  critical: 1,\n  major: 2,\n  minor: 3,\n  advisory: 4,\n};\n\nconst collectReasons = (trace: readonly RuleTraceEntry[]): string[] =>\n  trace\n    .filter(\n      (entry) =>\n        entry.contributed &&\n        (entry.outcome === 'fail' || entry.outcome === 'partial'),\n    )\n    .sort((a, b) => {\n      const bySeverity =\n        (SEVERITY_RANK[a.severity] ?? 9) - (SEVERITY_RANK[b.severity] ?? 9);\n\n      if (bySeverity !== 0) {\n        return bySeverity;\n      }\n\n      return b.pointsPossible - b.pointsEarned - (a.pointsPossible - a.pointsEarned);\n    })\n    .slice(0, MAX_REASONS)\n    .map((entry) => entry.explanation);\n\nconst summarise = (\n  decision: GateDecision,\n  score: number | null,\n  band: ScoringBand | null,\n  gateThreshold: number,\n  blockingReason: string | null,\n): string => {\n  switch (decision) {\n    case 'blocked':\n      return blockingReason ?? 'This lead is blocked from outreach on compliance grounds.';\n    case 'unscored':\n      return 'Greenlight could not score this lead, so it has been let through and flagged for a human to look at.';\n    case 'approved':\n      return `Cleared to work \u2014 scored ${score ?? 0}/100${\n        band === null ? '' : ` (${band.label})`\n      }, at or above your ${gateThreshold} threshold.`;\n    case 'gated':\n    default:\n      return `Held for review \u2014 scored ${score ?? 0}/100${\n        band === null ? '' : ` (${band.label})`\n      }, below your ${gateThreshold} threshold. Anyone can release it.`;\n  }\n};\n\nconst normaliseLead = (\n  lead: unknown,\n  degradations: Degradation[],\n): LeadRecord => {\n  if (!isPlainObject(lead)) {\n    degradations.push({\n      code: 'lead_malformed',\n      message:\n        'The lead record could not be read, so it was scored as if every field were empty. It has been let through and flagged.',\n    });\n\n    return { id: null, fields: {} };\n  }\n\n  const id = typeof lead['id'] === 'string' ? lead['id'] : null;\n  const fields = lead['fields'];\n\n  if (!isPlainObject(fields)) {\n    degradations.push({\n      code: 'lead_malformed',\n      message:\n        'The lead record had no readable fields, so it was scored as if every field were empty. It has been let through and flagged.',\n    });\n\n    return { id, fields: {} };\n  }\n\n  return { id, fields };\n};\n\nconst evaluateRule = (\n  rule: ScoringRule,\n  config: ResolvedScoringConfig,\n  lead: LeadRecord,\n  now: Date | null,\n  degradations: Degradation[],\n): RuleTraceEntry => {\n  const setting = settingFor(config, rule);\n  const reader = createFieldReader(lead, config.fieldMapping);\n\n  const base = {\n    ruleId: rule.id,\n    ruleName: rule.name,\n    category: rule.category,\n    question: rule.question,\n    severity: setting.severity,\n    weight: setting.weight,\n  } as const;\n\n  if (!setting.enabled) {\n    return {\n      ...base,\n      outcome: 'skipped',\n      credit: 0,\n      pointsEarned: 0,\n      pointsPossible: 0,\n      contributed: false,\n      explanation: `\"${rule.name}\" is switched off in your Greenlight settings, so it did not affect this score.`,\n      observations: [],\n    };\n  }\n\n  let verdict: RuleVerdict;\n\n  try {\n    verdict = rule.evaluate({ lead, config, now, read: reader });\n  } catch (error) {\n    // Fail-open level 2: a broken rule is reported, never fatal, and never\n    // drags the lead's score down.\n    const message = errorMessage(error);\n\n    degradations.push({\n      code: 'rule_errored',\n      message: `The \"${rule.name}\" check could not run, so it was left out of this score.`,\n      detail: `${rule.id}: ${message}`,\n    });\n\n    return {\n      ...base,\n      outcome: 'errored',\n      credit: 0,\n      pointsEarned: 0,\n      pointsPossible: 0,\n      contributed: false,\n      explanation: `The \"${rule.name}\" check could not run and was left out of this score, so the lead was not penalised for it.`,\n      remedy: 'Report this to your Greenlight administrator \u2014 it is a fault in the app, not in the lead.',\n      observations: reader.observations(),\n      error: message,\n    };\n  }\n\n  const isScorable =\n    verdict.outcome === 'pass' ||\n    verdict.outcome === 'partial' ||\n    verdict.outcome === 'fail';\n\n  const contributed = isScorable && setting.severity !== 'advisory';\n  const credit = creditFor(verdict);\n  const pointsPossible = contributed ? setting.weight : 0;\n  const pointsEarned = contributed ? roundTo(setting.weight * credit, 4) : 0;\n\n  const entry: RuleTraceEntry = {\n    ...base,\n    outcome: verdict.outcome,\n    credit,\n    pointsEarned,\n    pointsPossible,\n    contributed,\n    explanation: verdict.explanation,\n    observations: reader.observations(),\n  };\n\n  return {\n    ...entry,\n    ...(verdict.remedy === undefined ? {} : { remedy: verdict.remedy }),\n    ...(verdict.detail === undefined ? {} : { detail: verdict.detail }),\n  };\n};\n\nconst runScoring = (input: ScoreLeadInput): ScoringResult => {\n  const degradations: Degradation[] = [];\n  const rules = input.rules ?? ALL_RULES;\n\n  const lead = normaliseLead(input.lead, degradations);\n\n  let now: Date | null = null;\n\n  if (isUsableDate(input.now)) {\n    now = input.now;\n  } else {\n    degradations.push({\n      code: 'now_malformed',\n      message:\n        'The run had no usable date, so any check that depends on how old the data is was skipped.',\n    });\n  }\n\n  // The baseline is licence-delivered vocabulary and is threaded straight\n  // through to config resolution \u2014 the engine never inspects it, and no rule\n  // ever sees it as anything but the ordinary resolved lists. An absent\n  // baseline resolves exactly as this call did before the parameter existed.\n  const resolution = resolveConfig(input.config, rules, input.baseline);\n  degradations.push(...resolution.degradations);\n\n  const { config } = resolution;\n\n  const trace = rules.map((rule) =>\n    evaluateRule(rule, config, lead, now, degradations),\n  );\n\n  const contributing = trace.filter((entry) => entry.contributed);\n  const totalWeight = roundTo(\n    contributing.reduce((sum, entry) => sum + entry.pointsPossible, 0),\n    4,\n  );\n  const earnedWeight = roundTo(\n    contributing.reduce((sum, entry) => sum + entry.pointsEarned, 0),\n    4,\n  );\n\n  let score: number | null = null;\n\n  if (totalWeight > 0) {\n    score = roundTo((earnedWeight / totalWeight) * 100, 1);\n  } else {\n    // Fail-open level 3: nothing could be scored. The lead is not held on a\n    // technicality \u2014 it goes through, flagged.\n    degradations.push({\n      code: 'no_scorable_rules',\n      message:\n        'No Greenlight check was able to score this lead, so it was let through unscored and flagged for review.',\n    });\n  }\n\n  const band = bandFor(score, config.bands);\n\n  const blockingFailure = trace.find(\n    (entry) => entry.severity === 'blocking' && entry.outcome === 'fail',\n  );\n  const criticalFailure = trace.find(\n    (entry) => entry.severity === 'critical' && entry.outcome === 'fail',\n  );\n\n  let decision: GateDecision;\n\n  if (blockingFailure !== undefined) {\n    decision = 'blocked';\n  } else if (score === null) {\n    decision = 'unscored';\n  } else if (criticalFailure !== undefined) {\n    decision = 'gated';\n  } else {\n    decision = score >= config.gateThreshold ? 'approved' : 'gated';\n  }\n\n  const reasons = collectReasons(trace);\n\n  return {\n    score,\n    band,\n    decision,\n    gateThreshold: config.gateThreshold,\n    summary: summarise(\n      decision,\n      score,\n      band,\n      config.gateThreshold,\n      blockingFailure?.explanation ?? null,\n    ),\n    reasons,\n    trace,\n    degradations,\n    configSource: resolution.source,\n    scoredAt: now === null ? null : now.toISOString(),\n    leadId: lead.id ?? null,\n    engineVersion: SCORING_ENGINE_VERSION,\n    totalWeight,\n    earnedWeight,\n  };\n};\n\n/**\n * Score a lead and decide whether it is cleared to work.\n *\n * This is the entire public entry point. It never throws and always returns a\n * result \u2014 that guarantee is the product, not an implementation detail.\n */\nexport const scoreLead = (input: ScoreLeadInput): ScoringResult => {\n  try {\n    return runScoring(input);\n  } catch (error) {\n    // Fail-open level 4: the engine itself broke. The lead still passes.\n    const leadId =\n      isPlainObject(input?.lead) && typeof input.lead['id'] === 'string'\n        ? input.lead['id']\n        : null;\n\n    return {\n      score: null,\n      band: null,\n      decision: 'unscored',\n      gateThreshold: 0,\n      summary:\n        'Greenlight hit an unexpected error and could not score this lead, so it has been let through and flagged for a human to look at.',\n      reasons: ['Greenlight could not complete a scoring run for this lead.'],\n      trace: [],\n      degradations: [\n        {\n          code: 'engine_errored',\n          message:\n            'Greenlight hit an unexpected error while scoring. The lead was let through rather than held.',\n          detail: errorMessage(error),\n        },\n      ],\n      configSource: 'defaults',\n      scoredAt: isUsableDate(input?.now) ? input.now.toISOString() : null,\n      leadId,\n      engineVersion: SCORING_ENGINE_VERSION,\n      totalWeight: 0,\n      earnedWeight: 0,\n    };\n  }\n};\n", "/**\n * Numaya Greenlight \u2014 does the Layer 3 field mapping name fields that exist?\n *\n * Pure, like `backfill-plan.ts` and `backfill-state.ts` beside it, and for the\n * same reason: the request shape below and the reading of what came back are the\n * two things most likely to be wrong against a real schema, and a pure module is\n * the only place they can be asserted without a workspace. The one piece of I/O\n * the check needs \u2014 actually asking Twenty \u2014 is issued by `backfill-run.ts`.\n *\n * ============================================================================\n * ## The defect this exists to make visible\n *\n * An admin maps logical keys to their own field names on the `GreenlightConfig`\n * record: `decisionMakerFieldName`, `readyForOutreachFieldName`,\n * `optOutFieldNames`. Those names reach exactly one place in a request \u2014 the\n * **selection set** (`configuredSelectionFields` in `backfill-plan.ts`).\n *\n * It was believed, and written down in this repository, that a name which does\n * not exist would fail the query and trip the backfill's retry ladder. That was\n * wrong. Proved against a live instance in `src/__live__/api-contract.live-test.ts`:\n *\n *   - Twenty **validates arguments** \u2014 `filter`, `orderBy`, a mutation's `data`.\n *     A field the object does not have is rejected outright.\n *   - Twenty **does not validate selection sets**. A column the object does not\n *     have is selected without complaint and comes back `null`.\n *\n * So a single mistyped character in a settings field does not fail anything. The\n * page succeeds, the column arrives `null`, and the engine scores the record as\n * though a human had deliberately left that field blank. The backfill runs to\n * completion, reports success, and every score in the workspace is quietly worse\n * than it should be. No error, no notice, nothing for an admin to find.\n *\n * That is the worst class of defect this product can have, because it makes the\n * scores wrong while the product looks healthy, and the scores are what\n * customers buy.\n *\n * ============================================================================\n * ## Why the probe is a filter and never a selection\n *\n * Probing by selection is the bug itself: `{ people { edges { node { nonesuch } } } }`\n * answers happily on every workspace in existence, so a probe built that way\n * reports every name as present and is worse than no probe at all. Two field\n * probes in this repository were written that way and both answered `true`\n * everywhere \u2014 `probeOpportunityOwner` in `pipeline-store.ts` and `probeField` in\n * `icp-run.ts`, the latter reporting an ICP derived from industry columns that\n * mostly do not exist.\n *\n * The technique that works is the one `icp-run.ts` was corrected to use: put the\n * name in the half of the query Twenty *does* check.\n *\n *     people(first: 1, filter: { <name>: { is: \"NOT_NULL\" } }) { edges { node { id } } }\n *\n * `NOT_NULL` over `NULL` only because it reads as the question being asked: is\n * there such a thing. The rows are never looked at.\n *\n * ============================================================================\n * ## Silence is the safe answer, and this module is built to prefer it\n *\n * A validation that can break scoring is worse than the bug it was written to\n * catch, so nothing here is allowed to guess. A name is reported missing on one\n * condition only: Twenty rejected the probe with a message that names *that*\n * field as one the object does not have. Every other outcome \u2014 a composite type,\n * a transport failure, a permission error, an empty response, a message this\n * module does not recognise \u2014 resolves to `inconclusive`, which says nothing to\n * anybody and leaves scoring exactly as it is today.\n *\n * The composite case is worth stating on its own, because it is the one that\n * would produce a *false accusation* rather than a missed one. A configured name\n * pointing at a composite (`CURRENCY`, `ADDRESS`, `LINKS`, `emails`, `phones`)\n * is rejected with *\"Sub field 'is' not found for composite type: \u2026\"*. The field\n * plainly exists \u2014 the probe simply cannot ask about it this way \u2014 so that shape\n * is read as **present**. `icp-run.ts` makes the opposite trade for its own\n * candidates and is right to: it is choosing between candidate columns and\n * \"no industry field answered\" is a true thing to say about a currency-typed\n * industry. Here the output is a sentence telling an admin their configuration is\n * wrong, and telling them that about a field they can see on the record page is\n * the kind of mistake that gets a warning ignored forever afterwards.\n */\n\nimport { DEFAULT_FIELD_MAPPING } from 'src/scoring';\n\nimport { isPlainRecord } from 'src/logic-functions/greenlight-api';\n\n/* -------------------------------------------------------------------------- */\n/* Which settings carry a field name                                           */\n/* -------------------------------------------------------------------------- */\n\n/** The `GreenlightConfig` settings whose value is a field name on the lead. */\nexport type MappedFieldSource =\n  | 'decisionMakerFieldName'\n  | 'readyForOutreachFieldName'\n  | 'optOutFieldNames';\n\ninterface MappedFieldSourceDescriptor {\n  /** The setting's label on the record page, so the sentence is findable. */\n  readonly label: string;\n  /**\n   * The engine's logical key this setting feeds, or `null` for a setting that\n   * only widens the selection. Naming the key is half of what makes the notice\n   * actionable: \"this name is wrong\" is a fault report, \"this name is wrong *so\n   * the decision-maker signal is being read as empty*\" is a decision.\n   */\n  readonly logicalKey: keyof typeof DEFAULT_FIELD_MAPPING | null;\n}\n\nexport const MAPPED_FIELD_SOURCES: Readonly<\n  Record<MappedFieldSource, MappedFieldSourceDescriptor>\n> = {\n  decisionMakerFieldName: {\n    label: 'Decision-maker field',\n    logicalKey: 'jobTitle',\n  },\n  readyForOutreachFieldName: {\n    label: 'Ready-for-outreach field',\n    logicalKey: null,\n  },\n  optOutFieldNames: { label: 'Opt-out fields', logicalKey: 'optedOut' },\n};\n\nexport interface MappedFieldName {\n  readonly name: string;\n  readonly source: MappedFieldSource;\n}\n\n/**\n * A plain GraphQL identifier, and nothing else, ever.\n *\n * Config values are admin-supplied text one query away from the schema, so a\n * name that is not an identifier is dropped rather than interpolated. Dropping\n * is also why the check below can be honest about its own coverage: a name this\n * regex rejects never reaches a request, so it cannot be silently read as\n * `null`, and there is nothing to report about it.\n */\nconst FIELD_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\nconst asFieldName = (value: unknown): string | null => {\n  if (typeof value !== 'string') {\n    return null;\n  }\n\n  const trimmed = value.trim();\n\n  return FIELD_NAME.test(trimmed) ? trimmed : null;\n};\n\n/**\n * Every field name the workspace has pointed Greenlight at, with the setting it\n * came from.\n *\n * The provenance is the point. `configuredSelectionFields` in `backfill-plan.ts`\n * needs only the strings and is derived from this, so the two can never disagree\n * about which names reach a request \u2014 but a notice that says *\"\u2018jobTitl\u2019 is not\n * a field\"* without saying which box it was typed into leaves an admin hunting\n * through a record page, and a notice nobody can act on is furniture.\n *\n * De-duplicated by name: the same string in two settings is one column to probe,\n * and it keeps the first setting that mentioned it because that is the reading\n * order of the record page.\n */\nexport const mappedFieldNames = (\n  configRecord: unknown,\n): MappedFieldName[] => {\n  if (!isPlainRecord(configRecord)) {\n    return [];\n  }\n\n  const found: MappedFieldName[] = [];\n  const seen = new Set<string>();\n\n  const add = (value: unknown, source: MappedFieldSource): void => {\n    const name = asFieldName(value);\n\n    if (name === null || seen.has(name)) {\n      return;\n    }\n\n    seen.add(name);\n    found.push({ name, source });\n  };\n\n  add(configRecord['decisionMakerFieldName'], 'decisionMakerFieldName');\n  add(configRecord['readyForOutreachFieldName'], 'readyForOutreachFieldName');\n\n  const optOutFields = configRecord['optOutFieldNames'];\n\n  if (Array.isArray(optOutFields)) {\n    for (const value of optOutFields) {\n      add(value, 'optOutFieldNames');\n    }\n  }\n\n  return found;\n};\n\n/* -------------------------------------------------------------------------- */\n/* The probe                                                                   */\n/* -------------------------------------------------------------------------- */\n\n/**\n * How many names one check is allowed to ask about.\n *\n * `optOutFieldNames` is a free-text array an admin can paste anything into, and a\n * check that scaled with it would let a fifty-entry list turn one click on Start\n * into fifty requests against a 100-a-minute ceiling the backfill's own chunk\n * arithmetic already spends 62 of. A workspace configuring more than eight\n * mapped columns is not the shape this product is sold in, and the names past\n * the limit are simply not asked about \u2014 they are reported as unchecked in the\n * log rather than quietly assumed correct.\n *\n * Eight is comfortably above the realistic ceiling: one decision-maker field,\n * one ready-for-outreach field and up to six workspace opt-out columns.\n */\nexport const FIELD_MAPPING_PROBE_LIMIT = 8;\n\n/**\n * The cheapest question that means \"does this column exist\", as a request.\n *\n * `first: 1` and a selection of nothing but `id`: the answer is carried entirely\n * by whether Twenty accepted the argument, so fetching a row is incidental and\n * fetching a wide one would be waste.\n */\nexport const buildFieldExistenceProbe = (\n  connection: string,\n  fieldName: string,\n): Record<string, unknown> => ({\n  [connection]: {\n    __args: { first: 1, filter: { [fieldName]: { is: 'NOT_NULL' } } },\n    edges: { node: { id: true } },\n  },\n});\n\n/**\n * `absent` is the only verdict that ever reaches an admin. The other two are\n * both silence, and are kept apart only so the log can say which kind it was.\n */\nexport type FieldProbeVerdict = 'present' | 'absent' | 'inconclusive';\n\n/**\n * Did the response actually answer, or did it merely come back?\n *\n * A rejected request normally throws out of the client, but a transport that\n * returns `{ errors, data: null }` without throwing would make every probe look\n * like a success. Requiring the connection to be present costs nothing and\n * closes that path \u2014 and because an unrecognised shape resolves to\n * `inconclusive` rather than `absent`, closing it errs towards saying nothing.\n */\nexport const probeAnswered = (\n  connection: string,\n  response: unknown,\n): boolean =>\n  isPlainRecord(response) && isPlainRecord(response[connection]);\n\n/**\n * Twenty's way of saying \"that object has no such field\", in the two spellings\n * this app has actually seen come back.\n *\n * The first is the one that cost this repository months of wrong pipeline\n * criteria: `Object taskTarget doesn't have any \"opportunityId\" field.` The\n * second is the standard GraphQL rejection for a filter input that has no such\n * member. Both are matched only in company with the field's own name, so a\n * message about some *other* field \u2014 a nested error, a batched response \u2014 can\n * never be read as a verdict on this one.\n */\nconst ABSENT_PHRASES: readonly RegExp[] = [\n  /does(?:n['\u2019]t| not) have any/i,\n  /is not defined by type/i,\n  /unknown field/i,\n];\n\n/**\n * The field exists; the probe simply may not ask about it this way. See the\n * header: reporting a visible column as missing is the failure mode that gets\n * every future notice ignored, so this shape is read as present.\n */\nconst COMPOSITE_PHRASE = /sub ?field .* (?:not found|is not defined)/i;\n\n/**\n * Read a rejection. Anything unrecognised is `inconclusive`, on purpose.\n *\n * This is where fail-open is actually implemented, so it is worth being blunt\n * about the trade: a Twenty release that reworded its errors turns this check\n * off \u2014 every probe becomes `inconclusive`, nothing is reported, and scoring\n * behaves precisely as it does today. It does not turn the check into a source\n * of false accusations, and it cannot stop a backfill. The live contract test is\n * what would notice the rewording; this function is what makes the rewording\n * survivable in the meantime.\n */\nexport const classifyProbeRejection = (\n  fieldName: string,\n  message: string,\n): FieldProbeVerdict => {\n  if (COMPOSITE_PHRASE.test(message)) {\n    return 'present';\n  }\n\n  if (!message.includes(fieldName)) {\n    return 'inconclusive';\n  }\n\n  return ABSENT_PHRASES.some((phrase) => phrase.test(message))\n    ? 'absent'\n    : 'inconclusive';\n};\n\n/* -------------------------------------------------------------------------- */\n/* What an admin is told                                                       */\n/* -------------------------------------------------------------------------- */\n\nexport interface MappingFinding {\n  readonly name: string;\n  readonly source: MappedFieldSource;\n}\n\nexport interface FieldMappingCheckResult {\n  /** Names Twenty explicitly said the object does not have. */\n  readonly missing: readonly MappingFinding[];\n  /** How many names were asked about. */\n  readonly probed: number;\n  /** Asked, but the answer could not be trusted either way. */\n  readonly inconclusive: number;\n  /** Past `FIELD_MAPPING_PROBE_LIMIT`, or the check never ran. */\n  readonly unchecked: number;\n}\n\nexport const EMPTY_FIELD_MAPPING_CHECK: FieldMappingCheckResult = {\n  missing: [],\n  probed: 0,\n  inconclusive: 0,\n  unchecked: 0,\n};\n\n/** The candidates the engine still reads when a configured name is not there. */\nconst fallbacksFor = (finding: MappingFinding): readonly string[] => {\n  const key = MAPPED_FIELD_SOURCES[finding.source].logicalKey;\n\n  if (key === null) {\n    return [];\n  }\n\n  return DEFAULT_FIELD_MAPPING[key].filter((path) => path !== finding.name);\n};\n\n/** \"a, b and c\" \u2014 an English list, because this is a sentence a human reads. */\nconst asList = (values: readonly string[]): string => {\n  if (values.length <= 1) {\n    return values[0] ?? '';\n  }\n\n  return `${values.slice(0, -1).join(', ')} and ${values[values.length - 1]}`;\n};\n\n/**\n * What this particular name being absent actually costs.\n *\n * Precision here is the whole requirement. \"Invalid config\" is useless, and so\n * is overstating it: the Layer 3 mapping *prepends* a configured name to the\n * engine's shipped candidates rather than replacing them (see `readFieldMapping`\n * in `greenlight-config-record.ts`), so a mistyped decision-maker field does not\n * blank the signal for every lead \u2014 it blanks it for the leads whose seniority\n * lives only in the workspace's own column. Saying \"no lead has a job title\"\n * would be a bigger claim than the code makes, and an admin who checks one\n * record and finds it scored fine will stop believing the next notice too.\n */\nconst consequenceOf = (finding: MappingFinding): string => {\n  const fallbacks = fallbacksFor(finding);\n\n  switch (finding.source) {\n    case 'decisionMakerFieldName':\n      return fallbacks.length === 0\n        ? 'the decision-maker rule has nothing left to read, so seniority scores nothing on any lead'\n        : `Greenlight falls back to ${asList(fallbacks)}, so a lead whose seniority is recorded only in that column is scored as though it had no job title`;\n\n    case 'optOutFieldNames':\n      return fallbacks.length === 0\n        ? 'nothing is left for the compliance rule to read, so no lead can be recognised as opted out'\n        : `Greenlight falls back to ${asList(fallbacks)}, so a lead who has opted out only in that column is not recognised as opted out and can be released for outreach`;\n\n    case 'readyForOutreachFieldName':\n    default:\n      return 'no rule reads that setting in this release, so no score changes \u2014 but the setting is not pointing at anything, and it will matter as soon as one does';\n  }\n};\n\n/**\n * The paragraph the admin reads, or `null` when there is nothing to say.\n *\n * One paragraph rather than a list because the panel renders this string as\n * prose, and a bullet list that arrives as a run-on line is worse than a\n * sentence written to be one. It leads with the consequence, names each field\n * and the box it was typed into, and ends with the two things to do \u2014 because a\n * notice that stops at \"something is wrong\" makes the reader do the work of\n * working out whether it matters.\n *\n * It never suggests the admin made a mistake. A field can vanish from a\n * workspace long after somebody typed its name correctly, and the sentence is\n * true either way.\n */\nexport const describeFieldMappingFindings = (\n  missing: readonly MappingFinding[],\n  objectLabel: string,\n): string | null => {\n  if (missing.length === 0) {\n    return null;\n  }\n\n  const head =\n    missing.length === 1\n      ? `Greenlight\u2019s field mapping names a field ${objectLabel} does not have, so this backfill reads it as empty on every lead.`\n      : `Greenlight\u2019s field mapping names ${missing.length} fields ${objectLabel} does not have, so this backfill reads them as empty on every lead.`;\n\n  const lines = missing.map(\n    (finding) =>\n      `\u201C${finding.name}\u201D (${MAPPED_FIELD_SOURCES[finding.source].label}) is not a field on ${objectLabel} \u2014 ${consequenceOf(finding)}.`,\n  );\n\n  return [\n    head,\n    ...lines,\n    'Correct the name on the Greenlight Configuration record, then run a backfill over all records to re-score what this run has already done.',\n  ].join(' ');\n};\n\n/** The audit row's name \u2014 what the log shows before anybody opens the row. */\nexport const fieldMappingAuditName = (\n  missing: readonly MappingFinding[],\n  objectLabel: string,\n): string =>\n  missing.length === 1 && missing[0] !== undefined\n    ? `ERROR \u00B7 Greenlight field mapping \u00B7 \u201C${missing[0].name}\u201D is not a field on ${objectLabel}`\n    : `ERROR \u00B7 Greenlight field mapping \u00B7 ${missing.length} configured names are not fields on ${objectLabel}`;\n\n/**\n * The structured half of the audit row, so the trail carries the facts and not\n * only the sentence. Same split as everywhere else here: `name` is for reading,\n * `ruleTrace` is for answering questions later.\n */\nexport const fieldMappingAuditDetail = (\n  result: FieldMappingCheckResult,\n  objectNameSingular: string,\n): Record<string, unknown> => ({\n  action: 'field_mapping_check',\n  leadObjectNameSingular: objectNameSingular,\n  missing: result.missing.map((finding) => ({\n    name: finding.name,\n    setting: finding.source,\n    settingLabel: MAPPED_FIELD_SOURCES[finding.source].label,\n    logicalKey: MAPPED_FIELD_SOURCES[finding.source].logicalKey,\n    remainingCandidates: fallbacksFor(finding),\n  })),\n  probed: result.probed,\n  inconclusive: result.inconclusive,\n  unchecked: result.unchecked,\n});\n", "/**\n * Numaya Greenlight \u2014 how one backfill tick decides what to read.\n *\n * Pure, like `backfill-state.ts` and for the same reason: the request shapes\n * below are the part most likely to be wrong against a real schema, and a pure\n * module is the only place they can be asserted without a workspace.\n *\n * Three things live here \u2014 the size of a chunk and the arithmetic that fixes it,\n * the Person selection and why it is exactly that wide, and the keyset cursor.\n */\n\nimport { mappedFieldNames } from 'src/backfill/field-mapping-check';\nimport { isPlainRecord } from 'src/logic-functions/greenlight-api';\n\nimport type { BackfillCursor, BackfillMode } from 'src/backfill/backfill-state';\n\n/* -------------------------------------------------------------------------- */\n/* Chunk size \u2014 the rate-limit arithmetic                                      */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Twenty's documented ceiling. Stated here rather than left implicit because\n * every number below is derived from it and a future change to it must land in\n * one place.\n */\nexport const TWENTY_REQUESTS_PER_MINUTE = 100;\n\n/**\n * Records handled per cron tick, and the cron fires once a minute.\n *\n * ## The arithmetic\n *\n * Per tick, against the Core API:\n *\n *     1   page query      (this module's `buildPageRequest`)\n *     1   config query    (`loadConfigRecord`, hoisted out of the per-record path)\n *   2\u00D7N   record writes   (`createGreenlightAuditLog` + `updatePerson`)\n *     \u2500\u2500\u2500\u2500\u2500\n *     2 + 2N requests\n *\n * `2\u00D7N` is the worst case: it is what a record whose outcome actually *changed*\n * costs. A record guard 3 recognises as unchanged costs zero writes, so a re-run\n * over an already-scored workspace issues 2 requests a minute, not 62.\n *\n * At N = 30:  2 + 60 = **62 Core API requests per minute**, 62% of the ceiling.\n *\n * Against app key-value storage, per tick: 1 state read, 2 state writes (claim\n * the lease, commit the chunk), and up to N release-marker reads \u2014 but only for\n * records the engine wants to *hold*, since `pinReleasedDecision` does not spend\n * a read on a passing lead. Worst case 33 key-value operations. If Twenty ever\n * counts those against the same bucket the total is 95, still inside 100.\n *\n * ## Why not 49, which is what 100 requests would actually buy\n *\n * Because the backfill is not the only thing talking to the API. The create and\n * update scoring hooks keep firing while it runs, each costing up to 3 requests,\n * and a workspace importing leads during a backfill is not a hypothetical \u2014 it is\n * the most likely time for one. Roughly 38 requests a minute of headroom is\n * twelve concurrent live scoring events, which is a busy import.\n *\n * Spending the whole budget would make the backfill throttle the live gate. A\n * backfill that finishes an hour sooner by making new leads score late has\n * optimised the wrong thing: new leads are the ones a rep is about to call.\n *\n * ## What it costs in wall-clock\n *\n * 30/minute = 1,800/hour. The 1,205-person dev workspace: ~40 minutes. A\n * 10,000-contact workspace: ~5.6 hours. Both unattended, both resumable, and\n * both visible while they run \u2014 which is the requirement, rather than speed.\n */\nexport const BACKFILL_CHUNK_SIZE = 30;\n\n/**\n * Twenty's page ceiling \u2014 the documented batch size. A page may therefore be\n * twice a chunk, and the difference is spent on the cursor boundary rather than\n * on scoring: see `pageSizeFor`.\n */\nexport const BACKFILL_PAGE_SIZE = 60;\n\n/**\n * Stop starting new records after this much of the tick has gone.\n *\n * The cron function is registered with `timeoutSeconds: 55`. Being killed by the\n * platform is survivable \u2014 the cursor simply does not advance and the next tick\n * redoes the chunk \u2014 but it is wasteful, and it makes the panel's counters lag\n * reality by a whole lease. Stopping early and committing the prefix that was\n * actually finished is strictly better: the position is recorded, and the tail is\n * re-fetched rather than re-scored.\n */\nexport const BACKFILL_TICK_BUDGET_MS = 40_000;\n\n/* -------------------------------------------------------------------------- */\n/* What to read off a Person                                                   */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The Person columns a bulk score reads.\n *\n * ## The governing constraint is parity, not completeness\n *\n * The event path hands `scoring-run.ts` `event.properties.after` \u2014 every column\n * on the record \u2014 and then hydrates the company with exactly `{ id, name }` (see\n * `hydrateCompany`). A backfilled lead and a freshly-created lead must land on\n * the same score, or the gate queue becomes incoherent: the same person scores\n * one way if they were imported yesterday and another if they arrive tomorrow,\n * and nobody can explain the queue to the rep working it.\n *\n * So this selection deliberately does **not** pull richer company data than the\n * event path can see. `company.industry` and `company.employees` would raise ICP\n * scores \u2014 and would raise them only for backfilled records. Being consistently\n * conservative beats being inconsistently generous.\n *\n * ## The three that are not about scoring\n *\n *   - `greenlightTrace` carries the stored outcome fingerprint. Without it guard\n *     3 cannot fire, and every backfill pass would rewrite every record and\n *     append a duplicate audit row. This one field is what makes re-running the\n *     backfill idempotent.\n *   - `greenlightDecision` and `greenlightScore` are what the modes filter and\n *     sort on, and what the panel counts.\n *   - `createdAt` is the cursor. `updatedAt` is also the freshness rule's input.\n *\n * ## The suppression columns are not optional\n *\n * `greenlightSuppressed` / `greenlightSuppressionReason` are the compliance\n * rule's primary input (`DEFAULT_FIELD_MAPPING.suppressed`). Omitting them would\n * make every bulk-scored record answer the compliance question with \"nothing on\n * this record says\" \u2014 and a workspace's whole do-not-contact register would be\n * silently ignored for exactly the records nobody has checked yet. That is the\n * single worst thing this feature could do, so they are listed first among the\n * Greenlight fields and asserted in the tests.\n */\nexport const backfillPersonSelection = (\n  extraFieldNames: readonly string[] = [],\n): Record<string, unknown> => {\n  const selection: Record<string, unknown> = {\n    id: true,\n    createdAt: true,\n    updatedAt: true,\n    name: { firstName: true, lastName: true },\n    emails: { primaryEmail: true, additionalEmails: true },\n    phones: {\n      primaryPhoneNumber: true,\n      primaryPhoneCallingCode: true,\n      primaryPhoneCountryCode: true,\n      additionalPhones: true,\n    },\n    jobTitle: true,\n    city: true,\n    linkedinLink: { primaryLinkUrl: true, primaryLinkLabel: true },\n    // Both, for the same reason `SCORING_TRIGGER_PERSON_FIELDS` lists both: the\n    // relation and its foreign key are reported under different names across\n    // versions. Supplying `company` is also what stops `hydrateCompany` spending\n    // an extra query per record \u2014 the difference between 2 and 3 requests each.\n    companyId: true,\n    company: { id: true, name: true },\n    greenlightSuppressed: true,\n    greenlightSuppressionReason: true,\n    // A scoring *input*, not bookkeeping: the resolved field mapping appends\n    // `greenlightEnrichment.fields.<key>.parsedValue` to every enrichable key,\n    // so a page fetched without this column scores an enriched lead as though it\n    // had never been enriched \u2014 and a bulk pass would then disagree with the\n    // event path on the same record. The two sibling columns are deliberately\n    // absent; no rule reads a date or a status.\n    greenlightEnrichment: true,\n    greenlightScore: true,\n    greenlightDecision: true,\n    greenlightTrace: true,\n  };\n\n  for (const name of extraFieldNames) {\n    if (!(name in selection)) {\n      selection[name] = true;\n    }\n  }\n\n  return selection;\n};\n\n/**\n * The Layer 3 field names a workspace has pointed Greenlight at, so a customised\n * workspace scores the same in bulk as it does on an event.\n *\n * The event path gets every column for free; this one has to ask by name, and the\n * names it does not know are exactly the ones an admin configured. Anything that\n * is not a plain GraphQL identifier is dropped rather than interpolated \u2014 a\n * config field is admin-supplied text, and it is one query away from the schema.\n * That filtering, and the provenance of each name, now live in\n * `field-mapping-check.ts`, which needs to know not just *which* names reach a\n * request but *which setting* each one was typed into. One list, one owner: the\n * names this function puts in a selection are by construction the names that\n * module probes.\n *\n * ## What a typo in a configured name actually does\n *\n * This used to read: \"a name that passes the regex but does not exist still fails\n * the query, which is why `backfill-run.ts` retries the page without extras\n * before giving up.\" That is wrong, and it was wrong in the direction that hides\n * the problem.\n *\n * Proved against a live instance (`src/__live__/api-contract.live-test.ts`):\n * **Twenty validates arguments and does not validate selection sets.** A `filter`\n * or `orderBy` naming a column the object lacks is rejected outright; a\n * *selected* column the object lacks is accepted and comes back `null`.\n *\n * These names only ever reach the selection. So a typo in a settings field does\n * not fail the page and does not trip the retry ladder \u2014 the query succeeds, the\n * column arrives as `null`, and the engine scores the record as though the human\n * had left that field blank. The backfill runs to completion, reports success,\n * and produces quietly worse scores across the whole workspace.\n *\n * It is still not this function's job to fix that \u2014 a page builder that started\n * probing the schema would put a round trip on the path a tick takes every\n * minute. It is fixed one level up and once per run: `startBackfill` in\n * `backfill-run.ts` asks Twenty whether each of these names exists, using the\n * half of a query Twenty actually checks, and reports the ones that do not to the\n * panel and the audit log. See `field-mapping-check.ts` for why the probe is a\n * filter and never a selection.\n */\nexport const configuredSelectionFields = (configRecord: unknown): string[] =>\n  mappedFieldNames(configRecord).map((mapped) => mapped.name);\n\n/* -------------------------------------------------------------------------- */\n/* The page request                                                            */\n/* -------------------------------------------------------------------------- */\n\nexport interface PageRequestInput {\n  readonly mode: BackfillMode;\n  readonly cursor: BackfillCursor;\n  readonly chunkSize: number;\n  readonly selection: Record<string, unknown>;\n  /**\n   * False drops `orderBy` **and** the cursor together \u2014 they are one mechanism\n   * and a cursor without an order is a random walk. See `PAGE_ATTEMPTS`.\n   */\n  readonly ordered: boolean;\n}\n\n/**\n * `is: 'NULL'` rather than a comparison against a value: a Person that predates\n * the app has no `greenlightDecision` at all, which is a different state from the\n * `UNSCORED` value the fail-open path writes. See `BACKFILL_MODES`.\n */\nexport const buildModeFilter = (mode: BackfillMode): Record<string, unknown> =>\n  mode === 'unscored' ? { greenlightDecision: { is: 'NULL' } } : {};\n\n/**\n * `gte`, not `gt`, and the boundary ids in the cursor are what make that correct.\n *\n * `createdAt` is not unique \u2014 a CSV import stamps hundreds of records within the\n * same millisecond. `gt` would step straight over every record sharing the\n * boundary timestamp with the last one handled, silently leaving them unscored:\n * the exact class of bug this whole feature exists to fix, reintroduced inside\n * the fix. `gte` re-reads the boundary and `cursor.ids` removes the ones already\n * done.\n */\nexport const buildPageFilter = ({\n  mode,\n  cursor,\n  ordered,\n}: Pick<PageRequestInput, 'mode' | 'cursor' | 'ordered'>): Record<\n  string,\n  unknown\n> => {\n  const filter: Record<string, unknown> = { ...buildModeFilter(mode) };\n\n  if (ordered && cursor.createdAt !== null) {\n    filter['createdAt'] = { gte: cursor.createdAt };\n  }\n\n  return filter;\n};\n\n/**\n * How many rows to ask for, given how many the page will have to skip.\n *\n * A `gte` cursor re-reads every record sharing the boundary timestamp, and\n * `cursor.ids` is how those are recognised and skipped. If the page were sized to\n * the chunk, those skips would eat the chunk's budget \u2014 and in the worst case a\n * page could be *entirely* boundary records, examine nothing, advance nothing,\n * and repeat forever. That is head-of-line blocking, and it is not hypothetical:\n * Postgres `now()` is constant within a transaction, so an import writes rows\n * with identical `createdAt`.\n *\n * Over-fetching by exactly the boundary size means a page always contains a full\n * chunk of *new* records, right up to Twenty's page ceiling. It costs nothing \u2014\n * it is the same single request either way, which is why the rate arithmetic on\n * `BACKFILL_CHUNK_SIZE` is unaffected.\n *\n * `BACKFILL_MAX_CURSOR_IDS` is what stops the boundary set outgrowing the\n * headroom; past that the walk can only re-read records, never skip one, and\n * `BACKFILL_MAX_STALLS` turns a walk that has stopped advancing into a visible\n * failure rather than a silent spin.\n */\nexport const pageSizeFor = (\n  chunkSize: number,\n  cursor: BackfillCursor,\n): number => Math.min(BACKFILL_PAGE_SIZE, chunkSize + cursor.ids.length);\n\nexport const buildPageRequest = ({\n  mode,\n  cursor,\n  chunkSize,\n  selection,\n  ordered,\n}: PageRequestInput): Record<string, unknown> => {\n  const filter = buildPageFilter({ mode, cursor, ordered });\n\n  const args: Record<string, unknown> = {\n    first: ordered ? pageSizeFor(chunkSize, cursor) : chunkSize,\n  };\n\n  if (Object.keys(filter).length > 0) {\n    args['filter'] = filter;\n  }\n\n  if (ordered) {\n    args['orderBy'] = [{ createdAt: 'AscNullsFirst' }];\n  }\n\n  return { people: { __args: args, edges: { node: selection } } };\n};\n\n/**\n * The ladder `backfill-run.ts` walks when a page query is rejected, in order.\n *\n * Each rung drops the next-most-likely cause and keeps everything below it:\n *\n *   1. **Everything.** Ordered, cursored, with the workspace's configured field\n *      names in the selection.\n *   2. **Without the configured names.** This rung is, on Twenty 2.26,\n *      **unreachable**, and the comment that used to be here said the opposite \u2014\n *      \"by far the likeliest failure\". Configured names only ever enter the\n *      *selection*, and Twenty does not validate selection sets: an unknown\n *      column is accepted and returns `null`. So a mistyped mapping cannot reject\n *      a page, and this rung cannot be the thing that saves it. See\n *      `configuredSelectionFields` for what a typo does instead, which is worse\n *      than a failed page and much quieter.\n *\n *      It stays in the ladder for two reasons. It is free \u2014 a rung that never\n *      fires costs one unreached branch \u2014 and it is the correct response if a\n *      future Twenty starts validating selections, which is the stricter and more\n *      likely direction for that behaviour to move. The live contract test\n *      exercises every rung, so this note stops being true the moment it stops\n *      being true.\n *   3. **Unordered and uncursored.** Only reachable in `unscored` mode, and it\n *      works there because that mode's filter *is* the queue: a record the tick\n *      scores stops matching `greenlightDecision is NULL`, so repeatedly asking\n *      for \"the next 30 unscored\" drains the workspace with no ordering support\n *      whatsoever. In `all` mode there is no such self-consuming filter, so there\n *      is no honest fallback and the run records the error and stalls where an\n *      admin can see it \u2014 which is the correct outcome, not a worse one.\n *\n * The ladder exists because `orderBy` direction enums and cursor comparators are\n * the two things in this file that cannot be verified without a live schema, and\n * an app that stops scoring a whole workspace because of an enum spelling is a\n * bad trade against ~30 lines of degradation.\n */\nexport const PAGE_ATTEMPTS = [\n  { useExtraFields: true, ordered: true },\n  { useExtraFields: false, ordered: true },\n  { useExtraFields: false, ordered: false },\n] as const;\n\n/** Counting is a nicety: it drives the progress bar and nothing else. */\nexport const buildCountRequest = (\n  mode: BackfillMode,\n): Record<string, unknown> => {\n  const filter = buildModeFilter(mode);\n\n  return {\n    people: {\n      ...(Object.keys(filter).length > 0 ? { __args: { filter } } : {}),\n      totalCount: true,\n    },\n  };\n};\n\nexport const readTotalCount = (response: unknown): number | null => {\n  if (!isPlainRecord(response)) {\n    return null;\n  }\n\n  const connection = response['people'];\n\n  if (!isPlainRecord(connection)) {\n    return null;\n  }\n\n  const total = connection['totalCount'];\n\n  return typeof total === 'number' && Number.isFinite(total) && total >= 0\n    ? Math.floor(total)\n    : null;\n};\n\n/* -------------------------------------------------------------------------- */\n/* The cursor                                                                  */\n/* -------------------------------------------------------------------------- */\n\nexport const readNodeId = (node: unknown): string | null =>\n  isPlainRecord(node) && typeof node['id'] === 'string' && node['id'].length > 0\n    ? node['id']\n    : null;\n\nexport const readNodeCreatedAt = (node: unknown): string | null =>\n  isPlainRecord(node) &&\n  typeof node['createdAt'] === 'string' &&\n  node['createdAt'].length > 0\n    ? node['createdAt']\n    : null;\n\n/** Has this record already been handled at the current cursor boundary? */\nexport const isAlreadyHandled = (\n  cursor: BackfillCursor,\n  node: unknown,\n): boolean => {\n  const id = readNodeId(node);\n\n  return id !== null && cursor.ids.includes(id);\n};\n\n/**\n * Move the cursor over the records this tick actually reached.\n *\n * `handled` must be a **prefix** of the page in `createdAt` order \u2014 the records\n * the tick got through before its budget ran out, including any it skipped\n * because they were already at the boundary. Passing the whole page when only\n * part of it was processed is how a backfill silently skips records, so\n * `backfill-run.ts` builds the prefix as it goes rather than after the fact.\n *\n * The boundary id set carries forward only when the timestamp did not move; a\n * new timestamp starts a fresh, and usually tiny, set.\n */\nexport const advanceCursor = (\n  previous: BackfillCursor,\n  handled: readonly unknown[],\n): BackfillCursor => {\n  if (handled.length === 0) {\n    return previous;\n  }\n\n  const last = handled[handled.length - 1];\n  const createdAt = readNodeCreatedAt(last) ?? previous.createdAt;\n\n  const carried =\n    previous.createdAt === createdAt ? previous.ids : ([] as readonly string[]);\n\n  const atBoundary = handled\n    .filter((node) => (readNodeCreatedAt(node) ?? createdAt) === createdAt)\n    .map(readNodeId)\n    .filter((id): id is string => id !== null);\n\n  return {\n    createdAt,\n    ids: [...new Set([...carried, ...atBoundary])],\n  };\n};\n\n/* -------------------------------------------------------------------------- */\n/* Handing a record to the scoring run                                         */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Dress a fetched Person as the database-event payload `runPersonScoring` reads.\n *\n * This is the whole of the \"reuse, do not duplicate\" story: the bulk path builds\n * the same envelope the platform would have delivered and calls the same\n * function, so every guard, the release-marker pin, the audit row, the fail-open\n * flag and the trace payload are the ones the live path uses \u2014 not a second\n * implementation that will drift.\n *\n * `updatedFields` is deliberately absent. `isGreenlightAuthoredWrite` reads an\n * absent list as \"we do not know what changed\", which means \"score it\" \u2014 exactly\n * right for a record nobody has ever scored. Supplying a synthetic list would be\n * inventing history, and supplying the Greenlight-owned names would make guard 2\n * swallow every single record in the backfill.\n */\nexport const toScoringEvent = (\n  node: Record<string, unknown>,\n): Record<string, unknown> => ({\n  recordId: readNodeId(node),\n  properties: { after: node },\n});\n", "/**\n * Reading the workspace's own Company records and finding the shape in them.\n *\n * Pure \u2014 no client, no clock, no SDK \u2014 for the same reason `src/scoring/` is:\n * this is the part that has to behave sensibly against data nobody has seen, and\n * the only way to assert that is to hand it the awkward shapes directly. The\n * awkward shapes are not hypothetical: no companies at all, an `industry` field\n * that does not exist on the object, an `industry` field that exists and is\n * blank on every record, free text where \"SaaS\", \"saas \" and \"SaaS \" are three\n * strings and one industry, one industry holding 90% of the book, and a tail of\n * two hundred industries with two companies each.\n *\n * ## What this module refuses to do\n *\n * It never invents a value. Every cluster it reports is a string somebody in the\n * workspace typed, counted, with the raw variants kept. Greenlight cannot know a\n * customer's target market, and the moment this file starts mapping \"Software\"\n * onto a taxonomy of our choosing it is guessing on the customer's behalf \u2014 which\n * is the thing the permissive default exists to avoid.\n *\n * ## Clustering, and why the raw variants are carried forward\n *\n * `icp.industry` matches with `matchesList`, which compares\n * `normalise(value) === normalise(target)` \u2014 trim, lowercase, collapse runs of\n * whitespace, and nothing else. So \"Software\" and \"software \" are one target;\n * \"Software\" and \"Software Development\" are two, and writing only the first would\n * silently fail every lead carrying the second.\n *\n * Clustering here is therefore for *presentation and evidence* only. A cluster\n * groups variants a human would call one industry so the panel can say \"Software\n * \u2014 214 companies (31%)\" instead of listing nine spellings, and it carries every\n * raw variant with it so that accepting the cluster writes all nine into the\n * config. Merge for the human, match on the literals.\n */\n\nimport { getByPath, isPlainObject, stringLeaves } from 'src/scoring/field-access';\nimport { normalise } from 'src/scoring/rules/helpers';\n\n/* -------------------------------------------------------------------------- */\n/* Where the facts live on a Company                                           */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Industry field names worth probing for, in order of how likely they are to be\n * the one.\n *\n * Twenty's stock Company object has **no industry field**. It ships `name`,\n * `domainName`, `address`, `employees`, `linkedinLink`, `xLink`,\n * `annualRecurringRevenue`, `idealCustomerProfile` and `accountOwner`. Industry,\n * where it exists at all, is a custom field the workspace added, so its name is\n * unknowable at build time and has to be discovered against the live schema \u2014\n * see `probeCompanyFields` in `src/logic-functions/icp-run.ts`.\n *\n * A single-token list, not dotted paths: each of these is probed with its own\n * cheap query, and one probe per candidate is the only way to learn which ones\n * exist.\n *\n * The stated reason used to be \"an unknown field name fails the whole GraphQL\n * request\". It does not. Twenty validates arguments \u2014 `filter`, `orderBy`,\n * mutation `data` \u2014 and does **not** validate selection sets, so an unknown\n * selected column is accepted and returns `null`. Probing by selection therefore\n * answered \"yes\" for every candidate on every workspace, which is exactly the bug\n * `probeCompanyFields` was fixed for: it named five industry fields to an admin\n * whose Company object had none of them, and the one useful message \u2014 that this\n * workspace has no industry field at all \u2014 could never fire.\n *\n * The design is unchanged and still correct; only the reason was wrong. The probe\n * now asks the half Twenty checks (`filter: { <field>: { is: 'NOT_NULL' } }`), and\n * one query per candidate is still needed because a single query naming all five\n * would be rejected on the first absent one without saying which others exist.\n */\nexport const COMPANY_INDUSTRY_FIELD_CANDIDATES: readonly string[] = [\n  'industry',\n  'industries',\n  'sector',\n  'vertical',\n  'companyIndustry',\n  'businessType',\n];\n\n/**\n * Region paths, deliberately the same candidates the scoring engine's default\n * field mapping uses for `region`, minus the ones that only make sense on a\n * Person. `address` is a stock composite, so `address.addressCountry` is the one\n * that usually answers.\n */\nexport const COMPANY_REGION_FIELD_CANDIDATES: readonly string[] = [\n  'region',\n  'country',\n];\n\n/** Read after `address` has been selected as a composite. */\nexport const COMPANY_ADDRESS_REGION_PATH = 'address.addressCountry';\n\n/**\n * Headcount candidates. `employees` is the stock field; the rest are the names\n * workspaces that outgrew it tend to pick.\n */\nexport const COMPANY_SIZE_FIELD_CANDIDATES: readonly string[] = [\n  'employees',\n  'employeeCount',\n  'numberOfEmployees',\n  'headcount',\n  'companySize',\n];\n\n/* -------------------------------------------------------------------------- */\n/* Facts                                                                       */\n/* -------------------------------------------------------------------------- */\n\n/** One company, reduced to the three things an ICP is made of. */\nexport interface CompanyFacts {\n  readonly id: string | null;\n  readonly name: string | null;\n  /** Every industry-ish string on the record, raw and untouched. */\n  readonly industries: readonly string[];\n  readonly regions: readonly string[];\n  /** Null when absent, unparseable, or not a plausible headcount. */\n  readonly employeeCount: number | null;\n}\n\n/** Which field names the live schema actually answered to. */\nexport interface CompanyFieldPaths {\n  readonly industry: readonly string[];\n  readonly region: readonly string[];\n  readonly size: readonly string[];\n}\n\nconst readStrings = (\n  node: Record<string, unknown>,\n  paths: readonly string[],\n): string[] => {\n  const seen = new Set<string>();\n  const values: string[] = [];\n\n  for (const path of paths) {\n    for (const leaf of stringLeaves(getByPath(node, path))) {\n      const key = normalise(leaf);\n\n      // A placeholder is worse than a blank: it clusters, it looks like\n      // evidence, and it would be written into the config as a target industry.\n      if (key.length === 0 || IGNORED_VALUES.has(key)) {\n        continue;\n      }\n\n      if (!seen.has(key)) {\n        seen.add(key);\n        values.push(leaf.trim());\n      }\n    }\n  }\n\n  return values;\n};\n\n/**\n * Values that mean \"somebody typed something rather than nothing\". A subset of\n * the engine's `PLACEHOLDER_VALUES`, plus the two an import tool writes into an\n * industry column.\n */\nconst IGNORED_VALUES = new Set([\n  '-',\n  '--',\n  '.',\n  'n/a',\n  'na',\n  'n.a.',\n  'none',\n  'null',\n  'nil',\n  'unknown',\n  'other',\n  'others',\n  'tbd',\n  'tba',\n  'test',\n  'not set',\n  'not provided',\n]);\n\n/**\n * A headcount below 1 is not a small company, it is a blank somebody stored as a\n * zero \u2014 and treating it as a real value would drag a proposed band's floor down\n * to include every company with no data at all. Anything above a million is a\n * typo. Both are reported as unknown rather than dropped silently: the coverage\n * figure is what tells the human how much of the proposal rests on real data.\n */\nconst MAX_PLAUSIBLE_HEADCOUNT = 1_000_000;\n\nconst readEmployeeCount = (\n  node: Record<string, unknown>,\n  paths: readonly string[],\n): number | null => {\n  for (const path of paths) {\n    const raw = getByPath(node, path);\n    const value =\n      typeof raw === 'number'\n        ? raw\n        : typeof raw === 'string' && raw.trim().length > 0\n          ? Number(raw.trim())\n          : Number.NaN;\n\n    if (\n      Number.isFinite(value) &&\n      value >= 1 &&\n      value <= MAX_PLAUSIBLE_HEADCOUNT\n    ) {\n      return Math.round(value);\n    }\n  }\n\n  return null;\n};\n\n/** Reduce one fetched Company node to facts. Never throws. */\nexport const toCompanyFacts = (\n  node: unknown,\n  paths: CompanyFieldPaths,\n): CompanyFacts => {\n  if (!isPlainObject(node)) {\n    return {\n      id: null,\n      name: null,\n      industries: [],\n      regions: [],\n      employeeCount: null,\n    };\n  }\n\n  return {\n    id: typeof node['id'] === 'string' ? node['id'] : null,\n    name: typeof node['name'] === 'string' ? node['name'] : null,\n    industries: readStrings(node, paths.industry),\n    regions: readStrings(node, paths.region),\n    employeeCount: readEmployeeCount(node, paths.size),\n  };\n};\n\n/* -------------------------------------------------------------------------- */\n/* Clustering                                                                  */\n/* -------------------------------------------------------------------------- */\n\n/**\n * A group of raw values a human would call one thing.\n *\n * `values` is what gets written into the config, and it is every variant seen \u2014\n * see the note at the top of this file. `label` is only ever shown.\n */\nexport interface IcpValueCluster {\n  /** Display form: the most frequent raw spelling in the cluster. */\n  readonly label: string;\n  /** Every raw variant, deduped by the engine's own `normalise`. */\n  readonly values: readonly string[];\n  /** Companies carrying any variant in this cluster. */\n  readonly count: number;\n  /** `count` as a share of companies that have any value for this dimension. */\n  readonly share: number;\n  /** Up to three company names, so the human can sanity-check the cluster. */\n  readonly examples: readonly string[];\n}\n\nexport interface DimensionAnalysis {\n  /** Companies with at least one usable value. */\n  readonly known: number;\n  /** Companies examined. */\n  readonly total: number;\n  /** `known / total`, 0 when nothing was examined. */\n  readonly coverage: number;\n  /** Distinct values before clustering \u2014 the free-text-sprawl number. */\n  readonly distinctValues: number;\n  /** Sorted by `count` descending, then label, so output is deterministic. */\n  readonly clusters: readonly IcpValueCluster[];\n}\n\n/**\n * Generic words that turn one industry into two spellings.\n *\n * Stripped only when building the *merge* key, never from the stored values, and\n * only when something is left over: \"Services\" on its own stays \"Services\".\n * Deliberately short. Every entry here is a decision that two strings mean the\n * same thing, and being wrong about that silently merges two real markets \u2014 so\n * the list holds only words that are pure noise in an industry name.\n */\nconst GENERIC_SUFFIXES: readonly string[] = [\n  'services',\n  'service',\n  'industry',\n  'industries',\n  'sector',\n  'sectors',\n  'solutions',\n];\n\n/**\n * The key two spellings must share to be treated as one industry.\n *\n * Four conservative steps on top of the engine's `normalise`: `&` becomes `and`,\n * punctuation becomes a word break, a single trailing generic word is dropped, a\n * trailing plural `s` is dropped from any word of five or more characters, and\n * what is left is joined with no separator at all. That last step is what merges\n * spellings that differ only in where the spaces and hyphens fell. Together they\n * merge \"SaaS\"/\"saas\", \"E-Commerce\"/\"ecommerce\", \"Health Care\"/\"Healthcare\",\n * \"Food & Beverage\"/\"food and beverage\", \"Software Services\"/\"Software\" and\n * \"Logistics\"/\"Logistic\".\n *\n * They deliberately do **not** merge \"Finance\" and \"Financial Services\", or\n * \"Health\" and \"Healthcare\": those look related to a reader and are different\n * words to a matcher, and a merge that is wrong writes a market the customer\n * does not sell to into their config. Both spellings are still offered as\n * separate rows, so a human who considers them one thing can tick both.\n */\nexport const clusterKey = (value: string): string => {\n  const base = normalise(value)\n    .replace(/&/g, ' and ')\n    .replace(/[^a-z0-9]+/g, ' ')\n    .replace(/\\s+/g, ' ')\n    .trim();\n\n  if (base.length === 0) {\n    return '';\n  }\n\n  const words = base.split(' ');\n  const last = words[words.length - 1] ?? '';\n\n  const withoutGeneric =\n    words.length > 1 && GENERIC_SUFFIXES.includes(last)\n      ? words.slice(0, -1)\n      : words;\n\n  return withoutGeneric\n    .map((word) =>\n      word.length >= 5 && word.endsWith('s') ? word.slice(0, -1) : word,\n    )\n    .join('');\n};\n\ninterface ClusterAccumulator {\n  key: string;\n  /** Raw spelling -> how many companies used it. */\n  spellings: Map<string, { display: string; count: number }>;\n  companies: Set<string>;\n  count: number;\n  examples: string[];\n}\n\nconst identityKey = (value: string): string => normalise(value);\n\n/**\n * Cluster one dimension across a company sample.\n *\n * `merge` is false for regions: \"US\" and \"USA\" are not merged, because a region\n * list is matched literally against whatever a lead carries and quietly folding\n * country spellings together would put a target in the config the workspace\n * never named. Both spellings appear as their own clusters and the human picks.\n */\nexport const analyseDimension = (\n  companies: readonly CompanyFacts[],\n  pick: (facts: CompanyFacts) => readonly string[],\n  { merge }: { merge: boolean },\n): DimensionAnalysis => {\n  const clusters = new Map<string, ClusterAccumulator>();\n  const distinct = new Set<string>();\n  let known = 0;\n\n  for (const [index, company] of companies.entries()) {\n    const values = pick(company);\n\n    if (values.length === 0) {\n      continue;\n    }\n\n    known += 1;\n\n    const companyKey = company.id ?? `#${index}`;\n\n    for (const value of values) {\n      distinct.add(identityKey(value));\n\n      const key = merge ? clusterKey(value) : identityKey(value);\n\n      if (key.length === 0) {\n        continue;\n      }\n\n      let cluster = clusters.get(key);\n\n      if (cluster === undefined) {\n        cluster = {\n          key,\n          spellings: new Map(),\n          companies: new Set(),\n          count: 0,\n          examples: [],\n        };\n        clusters.set(key, cluster);\n      }\n\n      const spellingKey = identityKey(value);\n      const spelling = cluster.spellings.get(spellingKey);\n\n      if (spelling === undefined) {\n        cluster.spellings.set(spellingKey, { display: value.trim(), count: 1 });\n      } else {\n        spelling.count += 1;\n      }\n\n      if (!cluster.companies.has(companyKey)) {\n        cluster.companies.add(companyKey);\n        cluster.count += 1;\n\n        if (cluster.examples.length < 3 && company.name !== null) {\n          cluster.examples.push(company.name);\n        }\n      }\n    }\n  }\n\n  const built = [...clusters.values()].map((cluster): IcpValueCluster => {\n    const spellings = [...cluster.spellings.values()].sort(\n      (a, b) => b.count - a.count || a.display.localeCompare(b.display),\n    );\n\n    return {\n      label: spellings[0]?.display ?? cluster.key,\n      values: spellings.map((spelling) => spelling.display),\n      count: cluster.count,\n      share: known === 0 ? 0 : cluster.count / known,\n      examples: cluster.examples,\n    };\n  });\n\n  return {\n    known,\n    total: companies.length,\n    coverage: companies.length === 0 ? 0 : known / companies.length,\n    distinctValues: distinct.size,\n    clusters: built.sort(\n      (a, b) => b.count - a.count || a.label.localeCompare(b.label),\n    ),\n  };\n};\n\n/* -------------------------------------------------------------------------- */\n/* Values the workspace holds that the profile does not list                   */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The gap between what is written on the records and what the profile accepts.\n *\n * This exists because of the single most confusing outcome this product can\n * produce: enrichment fills `industry` in, the rep watches the score refuse to\n * move, and nothing anywhere says why. The lead is not at fault and no amount of\n * work on the lead will fix it \u2014 `icp.industry` is exact set membership, the\n * value is simply not on the list. The remedy is a configuration change, so it\n * belongs on the page that owns the configuration rather than on every card that\n * happens to be affected.\n *\n * Deliberately **not** a matching change. Loosening `matchesList` to token\n * overlap would re-score every existing lead in every workspace silently, and\n * would match \"security guard services\" against a profile meaning information\n * security. Naming the mismatch costs nobody a point and leaves the decision\n * with the person who knows which markets they sell to.\n */\nexport interface UnmatchedValues {\n  /** Clusters carrying no variant the profile accepts, most common first. */\n  readonly clusters: readonly IcpValueCluster[];\n  /** Companies carrying at least one unmatched value. */\n  readonly companies: number;\n  /** `companies` as a share of those with any value for this dimension. */\n  readonly share: number;\n}\n\nconst EMPTY_UNMATCHED: UnmatchedValues = {\n  clusters: [],\n  companies: 0,\n  share: 0,\n};\n\n/**\n * Clusters whose values the configured profile would reject.\n *\n * An empty target list returns nothing rather than everything: the rule reports\n * `not_applicable` in that state and accepts every industry, so there is no\n * mismatch to report and claiming several hundred would be false.\n *\n * A cluster counts as matched when **any** of its raw variants matches, because\n * that is what the engine does \u2014 `matchesList` is given every value on the\n * record and passes on the first hit.\n */\nexport const findUnmatchedValues = (\n  analysis: DimensionAnalysis,\n  targets: readonly string[],\n): UnmatchedValues => {\n  const accepted = new Set(targets.map((target) => normalise(target)));\n\n  if (accepted.size === 0) {\n    return EMPTY_UNMATCHED;\n  }\n\n  const clusters = analysis.clusters.filter(\n    (cluster) => !cluster.values.some((value) => accepted.has(normalise(value))),\n  );\n\n  if (clusters.length === 0) {\n    return EMPTY_UNMATCHED;\n  }\n\n  // Counts are per cluster and a company can only carry one value per cluster,\n  // but it can carry values in two different clusters. Summing therefore\n  // over-counts companies; it is the right number for \"records affected\" and is\n  // named as such rather than being passed off as a distinct-company count.\n  const companies = clusters.reduce((sum, cluster) => sum + cluster.count, 0);\n\n  return {\n    clusters,\n    companies,\n    share: analysis.known === 0 ? 0 : companies / analysis.known,\n  };\n};\n\n/* -------------------------------------------------------------------------- */\n/* Size distribution                                                           */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Conventional headcount boundaries, not boundaries derived from the data.\n *\n * A percentile-derived band (\"your 10th to 90th percentile is 7 to 340\n * employees\") is more faithful to the sample and much worse as a *profile*: it\n * encodes sampling noise as a hard cut-off, and it produces edges nobody can\n * defend in a sales meeting. These six buckets are the ones every sales\n * organisation already argues in, so the evidence attaches to a shape the human\n * already has an opinion about.\n */\nexport const SIZE_BUCKETS: readonly {\n  label: string;\n  minEmployees: number;\n  maxEmployees: number | null;\n}[] = [\n  { label: '1\u201310 employees', minEmployees: 1, maxEmployees: 10 },\n  { label: '11\u201350 employees', minEmployees: 11, maxEmployees: 50 },\n  { label: '51\u2013200 employees', minEmployees: 51, maxEmployees: 200 },\n  { label: '201\u20131,000 employees', minEmployees: 201, maxEmployees: 1000 },\n  { label: '1,001\u20135,000 employees', minEmployees: 1001, maxEmployees: 5000 },\n  { label: 'Over 5,000 employees', minEmployees: 5001, maxEmployees: null },\n];\n\nexport interface SizeBucketCount {\n  readonly label: string;\n  readonly minEmployees: number;\n  readonly maxEmployees: number | null;\n  readonly count: number;\n  readonly share: number;\n}\n\nexport interface SizeAnalysis {\n  readonly known: number;\n  readonly total: number;\n  readonly coverage: number;\n  readonly buckets: readonly SizeBucketCount[];\n  readonly smallest: number | null;\n  readonly largest: number | null;\n  readonly median: number | null;\n}\n\nexport const analyseSizes = (\n  companies: readonly CompanyFacts[],\n): SizeAnalysis => {\n  const counts = companies\n    .map((company) => company.employeeCount)\n    .filter((count): count is number => count !== null)\n    .sort((a, b) => a - b);\n\n  const known = counts.length;\n\n  const buckets = SIZE_BUCKETS.map((bucket): SizeBucketCount => {\n    const count = counts.filter(\n      (value) =>\n        value >= bucket.minEmployees &&\n        (bucket.maxEmployees === null || value <= bucket.maxEmployees),\n    ).length;\n\n    return { ...bucket, count, share: known === 0 ? 0 : count / known };\n  });\n\n  return {\n    known,\n    total: companies.length,\n    coverage: companies.length === 0 ? 0 : known / companies.length,\n    buckets,\n    smallest: counts[0] ?? null,\n    largest: counts[known - 1] ?? null,\n    median: known === 0 ? null : (counts[Math.floor((known - 1) / 2)] ?? null),\n  };\n};\n\n/* -------------------------------------------------------------------------- */\n/* The whole picture                                                           */\n/* -------------------------------------------------------------------------- */\n\nexport interface IcpAnalysis {\n  /** Companies actually examined. */\n  readonly sampled: number;\n  /** Companies in the workspace, when countable. Null means the count failed. */\n  readonly totalCompanies: number | null;\n  readonly industry: DimensionAnalysis;\n  readonly region: DimensionAnalysis;\n  readonly size: SizeAnalysis;\n  /** Field names the schema answered to, so the panel can say where this came from. */\n  readonly paths: CompanyFieldPaths;\n}\n\nexport const analyseCompanies = ({\n  companies,\n  totalCompanies,\n  paths,\n}: {\n  readonly companies: readonly CompanyFacts[];\n  readonly totalCompanies: number | null;\n  readonly paths: CompanyFieldPaths;\n}): IcpAnalysis => ({\n  sampled: companies.length,\n  totalCompanies,\n  industry: analyseDimension(companies, (facts) => facts.industries, {\n    merge: true,\n  }),\n  region: analyseDimension(companies, (facts) => facts.regions, {\n    merge: false,\n  }),\n  size: analyseSizes(companies),\n  paths,\n});\n", "/**\n * The adapter between the `GreenlightConfig` CRM record and the scoring engine.\n *\n * These are two different shapes on purpose and the gap has to live somewhere:\n *\n *   GreenlightConfig  is designed for a human with a record page. Bands are three\n *                     separate NUMBER fields because a typo inside a JSON blob\n *                     would silently gate a whole workspace; enable/severity and\n *                     weight are separate RAW_JSON maps because they are two\n *                     different decisions an admin makes at different times.\n *\n *   ResolvedScoringConfig  is designed for the engine. Bands are one sorted\n *                     array; every rule carries enabled + severity + weight\n *                     together.\n *\n * This file is the only place that knows both. Everything here is a pure\n * function of its input \u2014 no API client, no clock \u2014 so the whole\n * seed / read / merge story is unit-testable without a Twenty instance.\n */\n\nimport { buildDefaultConfig, DEFAULT_FIELD_MAPPING } from 'src/scoring';\nimport type { ResolvedScoringConfig } from 'src/scoring';\n\nimport { isPlainRecord } from 'src/logic-functions/greenlight-api';\n\n/**\n * The `GreenlightConfig.leadObjectNameSingular` SELECT value for Person.\n *\n * Duplicated from `src/objects/greenlight-config.ts` rather than imported: that\n * module pulls in `twenty-sdk/define`, and a logic-function bundle has no\n * business carrying the whole manifest-authoring toolkit. The value is a SELECT\n * option and is as permanent as the field identifier itself.\n */\nexport const DEFAULT_LEAD_OBJECT_SELECT_VALUE = 'PERSON';\n\n/** Lowercase API name matching the SELECT value above, for the audit trail. */\nexport const LEAD_OBJECT_SELECT_TO_NAME_SINGULAR: Readonly<\n  Record<string, string>\n> = {\n  PERSON: 'person',\n  COMPANY: 'company',\n  OPPORTUNITY: 'opportunity',\n};\n\n/** Every field this app reads from or writes to the config record. */\nexport const GREENLIGHT_CONFIG_FIELD_NAMES = [\n  'id',\n  'createdAt',\n  'name',\n  'isGateEnabled',\n  'icpIndustries',\n  'icpRegions',\n  'icpSizeBands',\n  'ruleSettings',\n  'scoreWeights',\n  'bandExcellentThreshold',\n  'bandGoodThreshold',\n  'bandFairThreshold',\n  'gateThreshold',\n  'defaultShelfLifeDays',\n  'fieldShelfLives',\n  'leadObjectNameSingular',\n  'decisionMakerFieldName',\n  'decisionMakerMatchValues',\n  'readyForOutreachFieldName',\n  'readyForOutreachValue',\n  'optOutFieldNames',\n] as const;\n\n/** GraphQL selection set for the fields above. */\nexport const greenlightConfigSelection = (): Record<string, boolean> =>\n  Object.fromEntries(GREENLIGHT_CONFIG_FIELD_NAMES.map((name) => [name, true]));\n\nexport type GreenlightConfigSeed = Record<string, unknown>;\n\nconst bandMinScore = (\n  config: ResolvedScoringConfig,\n  bandId: string,\n): number | undefined =>\n  config.bands.find((band) => band.id === bandId)?.minScore;\n\n/**\n * The record the post-install hook writes on a fresh install.\n *\n * Every value is set **explicitly** rather than left to the field manifest's\n * `defaultValue`, for three reasons:\n *\n *  1. The manifest defaults and the engine defaults disagree. The field\n *     manifests carry bands 80/60/40 and gate 40; `buildDefaultConfig()` \u2014 the\n *     thing that actually scores leads \u2014 carries 85/70/50 and gate 50. Whichever\n *     is \"right\", they must not differ, and the engine is the one that decides\n *     outcomes, so the engine wins. Leaving the record blank would ship a\n *     workspace whose visible configuration lies about its own behaviour.\n *     `decisionMakerMatchValues` has the same problem: 12 titles in the manifest,\n *     25 in the engine.\n *  2. `ruleSettings` and `scoreWeights` cannot come from a manifest default at\n *     all \u2014 they are keyed by the rule catalogue, which the engine owns and which\n *     grows every release.\n *  3. A `defaultValue` only applies at column creation. It is not a value a\n *     later read can distinguish from \"the admin set it to that\", so relying on\n *     it would make the upgrade-merge below unable to tell missing from chosen.\n *\n * The rule of thumb: if the engine has an opinion, seed it explicitly; the field\n * manifest `defaultValue` is then only a safety net for records created by hand.\n */\nexport const buildGreenlightConfigSeed = (): GreenlightConfigSeed => {\n  const defaults = buildDefaultConfig();\n\n  const ruleSettings: Record<string, { enabled: boolean; severity: string }> =\n    {};\n  const scoreWeights: Record<string, number> = {};\n\n  for (const [ruleId, setting] of Object.entries(defaults.rules)) {\n    ruleSettings[ruleId] = {\n      enabled: setting.enabled,\n      severity: setting.severity,\n    };\n    scoreWeights[ruleId] = setting.weight;\n  }\n\n  return {\n    name: 'Default',\n    isGateEnabled: true,\n\n    icpIndustries: [...defaults.icp.industries],\n    icpRegions: [...defaults.icp.regions],\n    icpSizeBands: { bands: [...defaults.icp.sizeBands] },\n\n    ruleSettings,\n    scoreWeights,\n\n    bandExcellentThreshold: bandMinScore(defaults, 'excellent') ?? 85,\n    bandGoodThreshold: bandMinScore(defaults, 'good') ?? 70,\n    bandFairThreshold: bandMinScore(defaults, 'fair') ?? 50,\n    gateThreshold: defaults.gateThreshold,\n\n    defaultShelfLifeDays: defaults.defaultShelfLifeDays,\n    fieldShelfLives: { ...defaults.fieldShelfLifeDays },\n\n    leadObjectNameSingular: DEFAULT_LEAD_OBJECT_SELECT_VALUE,\n    decisionMakerFieldName: 'jobTitle',\n    decisionMakerMatchValues: [...defaults.decisionMakerTitles],\n\n    // Stock Twenty's Person has no stage field, so there is nothing honest to\n    // point these at. Blank means \"the release action only clears the gate\".\n    readyForOutreachFieldName: null,\n    readyForOutreachValue: null,\n\n    // Empty, not absent: the engine's default field mapping already probes\n    // `optedOut` / `doNotContact` / `emailOptOut` / `unsubscribed`. This list is\n    // for *extra* workspace-specific opt-out fields.\n    optOutFieldNames: [],\n  };\n};\n\n/* -------------------------------------------------------------------------- */\n/* Upgrade merge                                                               */\n/* -------------------------------------------------------------------------- */\n\nconst isMissing = (value: unknown): boolean =>\n  value === undefined || value === null;\n\n/**\n * Whether writing `seedValue` over a missing field would actually change\n * anything once Twenty has stored it.\n *\n * Found by running a real `app:install` upgrade rather than by a unit test, and\n * unit tests structurally cannot see it: **Twenty persists an empty `RAW_JSON`\n * as `null`.** So `fieldShelfLives`, seeded `{}`, reads back as `null`,\n * `isMissing` says missing, the patch writes `{}` again, and it reads back\n * `null` again \u2014 forever. Every single upgrade patched exactly one key and filed\n * an audit row claiming a merge that had not happened, which quietly makes the\n * upgrade trail untrustworthy for the merges that are real.\n *\n * A patch that cannot converge is not a merge, it is a loop with an audit trail.\n * An empty seed value written over a missing field is a no-op by definition, so\n * it is skipped and the key is left to the field manifest's own default.\n */\nconst isNoOpSeed = (value: unknown): boolean => {\n  if (Array.isArray(value)) {\n    return value.length === 0;\n  }\n\n  if (isPlainRecord(value)) {\n    return Object.keys(value).length === 0;\n  }\n\n  return false;\n};\n\n/**\n * The patch an upgrade should apply to an existing config record.\n *\n * \"Missing\" means `null` or `undefined` only. An empty array, an empty object\n * and a zero are all *choices* a workspace made \u2014 a permissive ICP is the whole\n * point of the shipped default \u2014 and resetting them to seed values would be the\n * exact behaviour ARCHITECTURE.md's upgrade section forbids (\"config merge, not\n * replace\").\n *\n * The interesting case is `ruleSettings` / `scoreWeights`: a release that adds a\n * rule must make that rule appear in the workspace's config at its shipped\n * default, without disturbing a single existing entry. Both maps are therefore\n * merged key-by-key rather than compared wholesale.\n *\n * Returns an empty object when there is nothing to do, so the caller can skip\n * the mutation entirely \u2014 which is what makes re-running the hook a no-op.\n */\nexport const buildConfigUpgradePatch = (\n  existing: unknown,\n  seed: GreenlightConfigSeed = buildGreenlightConfigSeed(),\n): Record<string, unknown> => {\n  const record = isPlainRecord(existing) ? existing : {};\n  const patch: Record<string, unknown> = {};\n\n  for (const [key, seedValue] of Object.entries(seed)) {\n    // `null` is the intended seeded value for these two, so a null on the\n    // record is indistinguishable from the seed and must never be \"repaired\".\n    if (seedValue === null) {\n      continue;\n    }\n\n    if (key === 'ruleSettings' || key === 'scoreWeights') {\n      continue;\n    }\n\n    if (isMissing(record[key]) && !isNoOpSeed(seedValue)) {\n      patch[key] = seedValue;\n    }\n  }\n\n  const ruleSettingsPatch = mergeKeyedMap(\n    record['ruleSettings'],\n    seed['ruleSettings'],\n  );\n\n  if (ruleSettingsPatch !== null) {\n    patch['ruleSettings'] = ruleSettingsPatch;\n  }\n\n  const scoreWeightsPatch = mergeKeyedMap(\n    record['scoreWeights'],\n    seed['scoreWeights'],\n  );\n\n  if (scoreWeightsPatch !== null) {\n    patch['scoreWeights'] = scoreWeightsPatch;\n  }\n\n  return patch;\n};\n\n/**\n * Merge shipped keys into a stored map without overwriting anything present.\n * Returns `null` when the stored map already covers every shipped key \u2014 the\n * signal that no write is needed.\n */\nconst mergeKeyedMap = (\n  stored: unknown,\n  shipped: unknown,\n): Record<string, unknown> | null => {\n  if (!isPlainRecord(shipped)) {\n    return null;\n  }\n\n  if (!isPlainRecord(stored)) {\n    // Absent or unreadable: replacing it with the shipped map loses nothing,\n    // because there was nothing legible there to lose.\n    return { ...shipped };\n  }\n\n  const merged: Record<string, unknown> = { ...stored };\n  let added = false;\n\n  for (const [key, value] of Object.entries(shipped)) {\n    if (isMissing(merged[key])) {\n      merged[key] = value;\n      added = true;\n    }\n  }\n\n  return added ? merged : null;\n};\n\n/* -------------------------------------------------------------------------- */\n/* Record -> engine config                                                     */\n/* -------------------------------------------------------------------------- */\n\nconst asFiniteNumber = (value: unknown): number | undefined =>\n  typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n\nconst asStringList = (value: unknown): string[] | undefined => {\n  if (!Array.isArray(value)) {\n    return undefined;\n  }\n\n  const list = value\n    .filter((entry): entry is string => typeof entry === 'string')\n    .map((entry) => entry.trim())\n    .filter((entry) => entry.length > 0);\n\n  return list;\n};\n\n/** `icpSizeBands` ships as `{ bands: [...] }`; tolerate a bare array too. */\nconst readSizeBands = (value: unknown): unknown => {\n  if (Array.isArray(value)) {\n    return value;\n  }\n\n  if (isPlainRecord(value) && Array.isArray(value['bands'])) {\n    return value['bands'];\n  }\n\n  return undefined;\n};\n\n/**\n * Recombine `ruleSettings` (enable + severity) and `scoreWeights` (weight) into\n * the single per-rule shape the engine reads.\n *\n * `isEnabled` is accepted as an alias for `enabled` because the field's own\n * description in `src/objects/greenlight-config.ts` documents that spelling.\n * The engine reads `enabled`; rather than let a hand-edited record silently do\n * nothing, both are honoured here. (The field description is stale \u2014 see the\n * report accompanying this change.)\n */\nconst readRules = (\n  ruleSettings: unknown,\n  scoreWeights: unknown,\n): Record<string, unknown> | undefined => {\n  const settings = isPlainRecord(ruleSettings) ? ruleSettings : undefined;\n  const weights = isPlainRecord(scoreWeights) ? scoreWeights : undefined;\n\n  if (settings === undefined && weights === undefined) {\n    return undefined;\n  }\n\n  const ruleIds = new Set<string>([\n    ...Object.keys(settings ?? {}),\n    ...Object.keys(weights ?? {}),\n  ]);\n\n  const rules: Record<string, unknown> = {};\n\n  for (const ruleId of ruleIds) {\n    const setting = settings?.[ruleId];\n    const merged: Record<string, unknown> = isPlainRecord(setting)\n      ? { ...setting }\n      : {};\n\n    if (merged['enabled'] === undefined && merged['isEnabled'] !== undefined) {\n      merged['enabled'] = merged['isEnabled'];\n    }\n\n    const weight = asFiniteNumber(weights?.[ruleId]);\n\n    if (weight !== undefined) {\n      merged['weight'] = weight;\n    }\n\n    rules[ruleId] = merged;\n  }\n\n  return rules;\n};\n\n/**\n * Rebuild the engine's band array from the three threshold fields.\n *\n * Returns `undefined` when no threshold is usable, which makes `resolveConfig`\n * fall back to the shipped bands without logging a degradation \u2014 the right\n * outcome, because \"the admin never touched this\" is not a fault.\n */\nconst readBands = (record: Record<string, unknown>): unknown => {\n  const excellent = asFiniteNumber(record['bandExcellentThreshold']);\n  const good = asFiniteNumber(record['bandGoodThreshold']);\n  const fair = asFiniteNumber(record['bandFairThreshold']);\n\n  if (excellent === undefined && good === undefined && fair === undefined) {\n    return undefined;\n  }\n\n  const bands: { id: string; label: string; minScore: number }[] = [];\n\n  if (excellent !== undefined) {\n    bands.push({ id: 'excellent', label: 'Excellent', minScore: excellent });\n  }\n\n  if (good !== undefined) {\n    bands.push({ id: 'good', label: 'Good', minScore: good });\n  }\n\n  if (fair !== undefined) {\n    bands.push({ id: 'fair', label: 'Fair', minScore: fair });\n  }\n\n  // Poor is the floor and has no threshold field: it is whatever is left.\n  bands.push({ id: 'poor', label: 'Poor', minScore: 0 });\n\n  return bands;\n};\n\n/**\n * The Layer 3 field mapping, expressed as *additional* candidate paths in front\n * of the engine's defaults rather than as a replacement.\n *\n * Prepending matters: a workspace that points `decisionMakerFieldName` at a\n * custom field still wants `jobTitle` probed as a fallback for the Person\n * records where the custom field is blank. De-duplicated so that pointing the\n * setting at the default field name produces the default list unchanged rather\n * than a list that probes the same path twice.\n */\nconst prepend = (\n  extra: readonly string[],\n  defaults: readonly string[],\n): string[] => [...new Set([...extra, ...defaults])];\n\nconst readFieldMapping = (\n  record: Record<string, unknown>,\n): Record<string, string[]> | undefined => {\n  const mapping: Record<string, string[]> = {};\n\n  const decisionMakerField = record['decisionMakerFieldName'];\n\n  if (typeof decisionMakerField === 'string' && decisionMakerField.trim()) {\n    // Read the shipped candidates rather than repeating them. They were\n    // duplicated here, and the two copies drifted the moment `position` was\n    // removed from the engine's list for reading Twenty's row-ordering number\n    // as a job title \u2014 this copy would have quietly kept the bug alive for any\n    // workspace that had configured a decision-maker field.\n    mapping['jobTitle'] = prepend(\n      [decisionMakerField.trim()],\n      DEFAULT_FIELD_MAPPING.jobTitle,\n    );\n  }\n\n  const optOutFields = asStringList(record['optOutFieldNames']);\n\n  if (optOutFields !== undefined && optOutFields.length > 0) {\n    mapping['optedOut'] = prepend(optOutFields, [\n      'optedOut',\n      'doNotContact',\n      'emailOptOut',\n      'unsubscribed',\n    ]);\n  }\n\n  return Object.keys(mapping).length > 0 ? mapping : undefined;\n};\n\n/**\n * Turn a `GreenlightConfig` record into the raw config `scoreLead` accepts.\n *\n * Deliberately lenient: anything unreadable is left out of the returned object\n * so `resolveConfig` supplies its own default. This function never throws and\n * never validates \u2014 validation is the engine's job and it already reports what\n * it had to repair.\n */\nexport const toScoringConfigInput = (record: unknown): unknown => {\n  if (!isPlainRecord(record)) {\n    return undefined;\n  }\n\n  const industries = asStringList(record['icpIndustries']);\n  const regions = asStringList(record['icpRegions']);\n  const sizeBands = readSizeBands(record['icpSizeBands']);\n\n  const icp =\n    industries !== undefined || regions !== undefined || sizeBands !== undefined\n      ? {\n          industries: industries ?? [],\n          regions: regions ?? [],\n          sizeBands: sizeBands ?? [],\n        }\n      : undefined;\n\n  const decisionMakerTitles = asStringList(record['decisionMakerMatchValues']);\n\n  return {\n    icp,\n    rules: readRules(record['ruleSettings'], record['scoreWeights']),\n    bands: readBands(record),\n    gateThreshold: asFiniteNumber(record['gateThreshold']),\n    defaultShelfLifeDays: asFiniteNumber(record['defaultShelfLifeDays']),\n    fieldShelfLifeDays: isPlainRecord(record['fieldShelfLives'])\n      ? record['fieldShelfLives']\n      : undefined,\n    // Empty means \"the admin cleared the list\", which disables the\n    // decision-maker rule on purpose. Only an unreadable value falls back.\n    decisionMakerTitles,\n    fieldMapping: readFieldMapping(record),\n  };\n};\n\n/** Is the gate switched on? Absent or unreadable reads as on. */\nexport const isGateEnabled = (record: unknown): boolean =>\n  !(isPlainRecord(record) && record['isGateEnabled'] === false);\n\n/** Which object the workspace calls the lead, as a lowercase API name. */\nexport const readLeadObjectNameSingular = (record: unknown): string => {\n  const raw = isPlainRecord(record) ? record['leadObjectNameSingular'] : null;\n\n  if (typeof raw !== 'string' || raw.trim().length === 0) {\n    return LEAD_OBJECT_SELECT_TO_NAME_SINGULAR[\n      DEFAULT_LEAD_OBJECT_SELECT_VALUE\n    ] as string;\n  }\n\n  const normalised = raw.trim().toUpperCase();\n\n  return LEAD_OBJECT_SELECT_TO_NAME_SINGULAR[normalised] ?? raw.trim();\n};\n", "/**\n * Turning ticked headcount buckets into the bands that get written.\n *\n * A module of its own, importing nothing, for one reason: both the proposal\n * (server side) and the panel (browser side) have to produce **byte-identical**\n * bands from the same ticks. The panel disables Apply until the impact on screen\n * was measured against the exact profile on screen, and it decides \"exact\" by\n * comparing the serialised selection \u2014 so a panel that merged buckets even\n * slightly differently from the server would mark a freshly-computed impact\n * stale and make the user re-measure a profile nothing had changed about.\n *\n * Matching is by `label` rather than by array index or object identity, because\n * the panel's copy of these buckets has been through JSON and is not the same\n * object the server merged.\n */\n\nexport interface HeadcountBucket {\n  readonly label: string;\n  readonly minEmployees: number;\n  /** `null` means unbounded \u2014 the top bucket. */\n  readonly maxEmployees: number | null;\n  readonly count: number;\n  readonly share: number;\n}\n\nexport interface MergedSizeBand {\n  readonly label: string;\n  readonly minEmployees: number;\n  readonly maxEmployees: number | null;\n  readonly count: number;\n  readonly share: number;\n}\n\n/**\n * Fuse adjacent buckets into one band a human would name.\n *\n * Adjacency is decided against `all`, so ticking 1\u201310 and 51\u2013200 while leaving\n * 11\u201350 alone produces two bands rather than one that quietly swallows the gap.\n * That gap is a real statement \u2014 \"we sell to the very small and the mid-market,\n * not the bit in between\" \u2014 and merging over it would write a profile the person\n * did not choose.\n */\nexport const mergeBucketsIntoBands = (\n  chosenLabels: readonly string[],\n  all: readonly HeadcountBucket[],\n): MergedSizeBand[] => {\n  const chosen = new Set(chosenLabels);\n  const bands: MergedSizeBand[] = [];\n  let run: HeadcountBucket[] = [];\n\n  const flush = () => {\n    const first = run[0];\n    const last = run[run.length - 1];\n\n    if (first === undefined || last === undefined) {\n      run = [];\n      return;\n    }\n\n    bands.push({\n      label:\n        last.maxEmployees === null\n          ? `${first.minEmployees.toLocaleString('en-US')}+ employees`\n          : `${first.minEmployees.toLocaleString('en-US')}\u2013${last.maxEmployees.toLocaleString('en-US')} employees`,\n      minEmployees: first.minEmployees,\n      maxEmployees: last.maxEmployees,\n      count: run.reduce((sum, bucket) => sum + bucket.count, 0),\n      share: run.reduce((sum, bucket) => sum + bucket.share, 0),\n    });\n\n    run = [];\n  };\n\n  for (const bucket of all) {\n    if (chosen.has(bucket.label)) {\n      run.push(bucket);\n      continue;\n    }\n\n    flush();\n  }\n\n  flush();\n\n  return bands;\n};\n\n/** The three keys the config record and the scoring engine care about. */\nexport const toPlainBand = ({\n  label,\n  minEmployees,\n  maxEmployees,\n}: MergedSizeBand): {\n  label: string;\n  minEmployees: number;\n  maxEmployees: number | null;\n} => ({ label, minEmployees, maxEmployees });\n", "/**\n * Turning an analysis of the workspace's companies into a *proposal a human\n * accepts, edits or rejects* \u2014 never into configuration.\n *\n * Nothing in this file writes anything. `proposeIcp` returns suggestions with\n * the evidence attached, and `toIcpConfigPatch` turns whatever the human ended\n * up selecting into the exact record shape the ICP rules parse. The gap between\n * those two functions is where a person has to stand, and it is deliberate: an\n * ICP is a commercial decision about who the business sells to, and no amount of\n * arithmetic over a CRM export makes it ours to take.\n *\n * ## The thresholds, and why each one refuses rather than guesses\n *\n * Every constant below exists to make this module decline. A profile derived\n * from twelve companies, or from a field set on 3% of records, or from a tail of\n * two hundred industries with two companies each, is not a weak profile \u2014 it is\n * an arbitrary one, and applying it would gate real leads on noise. Declining is\n * a *result*: the panel says how much data there was and why it was not enough,\n * which is a fact the admin can act on (fill the field in, or configure the ICP\n * by hand from knowledge that is not in the CRM at all).\n */\n\nimport type { IcpSizeBand } from 'src/scoring';\n\nimport {\n  analyseCompanies,\n  type DimensionAnalysis,\n  type IcpAnalysis,\n  type IcpValueCluster,\n  type SizeAnalysis,\n  type SizeBucketCount,\n} from 'src/icp/icp-analysis';\nimport {\n  mergeBucketsIntoBands,\n  toPlainBand,\n} from 'src/icp/icp-size-bands';\n\n/* -------------------------------------------------------------------------- */\n/* Thresholds                                                                  */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Below this many companies carrying a value, any pattern is an accident of who\n * happened to fill the field in. Ten is not a statistical claim; it is the point\n * below which a human reading the evidence would say \"that is not enough\" \u2014 and\n * they would be right, so the module says it for them.\n */\nexport const MIN_KNOWN_VALUES = 10;\n\n/** One company is an anecdote. Two is the floor for calling something a pattern. */\nexport const MIN_CLUSTER_COMPANIES = 2;\n\n/** Stop adding suggestions once this share of the known values is covered. */\nexport const COVERAGE_TARGET = 0.8;\n\n/**\n * More than a dozen target industries is not a profile, it is the absence of\n * one \u2014 and an ICP that accepts everything scores exactly like the empty ICP\n * this whole feature exists to fix.\n */\nexport const MAX_SUGGESTIONS = 12;\n\n/**\n * If the single largest cluster is smaller than this, the data has no shape.\n * This is the \"long uniform tail\" case: two hundred industries, two companies\n * each, no winner. Proposing the top twelve there would be proposing the\n * alphabetically lucky.\n */\nexport const MIN_TOP_SHARE = 0.05;\n\n/** A bucket smaller than this is not worth widening a band for. */\nexport const MIN_BUCKET_SHARE = 0.05;\n\n/* -------------------------------------------------------------------------- */\n/* Shapes                                                                      */\n/* -------------------------------------------------------------------------- */\n\nexport type IcpDimension = 'industry' | 'region' | 'size';\n\n/**\n * How much the evidence is worth. Reported next to every suggestion, because a\n * suggestion drawn from 6% of the records and one drawn from 80% of them look\n * identical once they are a list of chips.\n */\nexport type IcpConfidence = 'none' | 'low' | 'moderate' | 'high';\n\nexport interface IcpValueDimensionProposal {\n  readonly dimension: 'industry' | 'region';\n  readonly proposed: boolean;\n  readonly confidence: IcpConfidence;\n  /** Plain words, always present \u2014 including, especially, when nothing is proposed. */\n  readonly reason: string;\n  readonly known: number;\n  readonly sampled: number;\n  readonly coverage: number;\n  readonly distinctValues: number;\n  /** Share of companies-with-a-value that the suggestions below would match. */\n  readonly coveredShare: number;\n  readonly suggestions: readonly IcpValueCluster[];\n  /** Everything that did not make the cut, so a human can add it back. */\n  readonly alternatives: readonly IcpValueCluster[];\n}\n\nexport interface IcpSizeBandSuggestion extends IcpSizeBand {\n  readonly count: number;\n  readonly share: number;\n}\n\nexport interface IcpSizeDimensionProposal {\n  readonly dimension: 'size';\n  readonly proposed: boolean;\n  readonly confidence: IcpConfidence;\n  readonly reason: string;\n  readonly known: number;\n  readonly sampled: number;\n  readonly coverage: number;\n  readonly coveredShare: number;\n  readonly median: number | null;\n  readonly smallest: number | null;\n  readonly largest: number | null;\n  readonly bands: readonly IcpSizeBandSuggestion[];\n  /** Every bucket with its count, so the human can see what was left out. */\n  readonly buckets: readonly SizeBucketCount[];\n  /** Companies with a known headcount that no proposed band would accept. */\n  readonly outsideCount: number;\n  readonly outsideShare: number;\n}\n\n/** Exactly the three lists an ICP is, flattened and ready to be written. */\nexport interface IcpSelection {\n  readonly industries: readonly string[];\n  readonly regions: readonly string[];\n  readonly sizeBands: readonly IcpSizeBand[];\n}\n\nexport interface IcpProposal {\n  readonly industry: IcpValueDimensionProposal;\n  readonly region: IcpValueDimensionProposal;\n  readonly size: IcpSizeDimensionProposal;\n  /** What accepting the proposal unedited would write. */\n  readonly selection: IcpSelection;\n  readonly analysis: IcpAnalysis;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Confidence                                                                  */\n/* -------------------------------------------------------------------------- */\n\nconst confidenceFor = (known: number, coverage: number): IcpConfidence => {\n  if (known === 0) {\n    return 'none';\n  }\n\n  if (known < MIN_KNOWN_VALUES) {\n    return 'low';\n  }\n\n  if (coverage >= 0.6 && known >= 30) {\n    return 'high';\n  }\n\n  if (coverage >= 0.25) {\n    return 'moderate';\n  }\n\n  return 'low';\n};\n\nconst percent = (share: number): string => `${Math.round(share * 100)}%`;\n\n/* -------------------------------------------------------------------------- */\n/* Industry and region                                                         */\n/* -------------------------------------------------------------------------- */\n\nconst DIMENSION_NOUN: Record<'industry' | 'region', string> = {\n  industry: 'industry',\n  region: 'region',\n};\n\n/**\n * \"an industry\", \"a region\".\n *\n * The nouns are interpolated into sentences an admin reads on the ICP screen, and\n * \"has a industry recorded\" is the kind of wrongness that makes a user distrust\n * the numbers next to it. Keyed off the dimension rather than sniffing the first\n * letter because the set is closed and two entries are cheaper than a rule with\n * exceptions (\"an hour\", \"a university\").\n */\nconst DIMENSION_NOUN_WITH_ARTICLE: Record<'industry' | 'region', string> = {\n  industry: 'an industry',\n  region: 'a region',\n};\n\nconst declineValueDimension = (\n  dimension: 'industry' | 'region',\n  analysis: DimensionAnalysis,\n  reason: string,\n): IcpValueDimensionProposal => ({\n  dimension,\n  proposed: false,\n  confidence: confidenceFor(analysis.known, analysis.coverage),\n  reason,\n  known: analysis.known,\n  sampled: analysis.total,\n  coverage: analysis.coverage,\n  distinctValues: analysis.distinctValues,\n  coveredShare: 0,\n  suggestions: [],\n  alternatives: analysis.clusters,\n});\n\nexport const proposeValueDimension = (\n  dimension: 'industry' | 'region',\n  analysis: DimensionAnalysis,\n): IcpValueDimensionProposal => {\n  const noun = DIMENSION_NOUN[dimension];\n  const nounWithArticle = DIMENSION_NOUN_WITH_ARTICLE[dimension];\n\n  if (analysis.total === 0) {\n    return declineValueDimension(\n      dimension,\n      analysis,\n      'There are no companies in this workspace to learn from, so Greenlight has nothing to propose. Set your target ' +\n        `${noun}s by hand, or import your customers first.`,\n    );\n  }\n\n  if (analysis.known === 0) {\n    return declineValueDimension(\n      dimension,\n      analysis,\n      `None of the ${analysis.total} companies examined has ${nounWithArticle} recorded, so there is nothing to derive a profile from.`,\n    );\n  }\n\n  if (analysis.known < MIN_KNOWN_VALUES) {\n    return declineValueDimension(\n      dimension,\n      analysis,\n      `Only ${analysis.known} of ${analysis.total} companies has ${nounWithArticle} recorded. That is too few to tell a pattern from a coincidence, so nothing is proposed \u2014 the ${analysis.known} value${\n        analysis.known === 1 ? '' : 's'\n      } found are listed below if you want to start from them.`,\n    );\n  }\n\n  const eligible = analysis.clusters.filter(\n    (cluster) => cluster.count >= MIN_CLUSTER_COMPANIES,\n  );\n\n  const top = eligible[0];\n\n  if (top === undefined || top.share < MIN_TOP_SHARE) {\n    return declineValueDimension(\n      dimension,\n      analysis,\n      `Your companies are spread across ${analysis.distinctValues} ${noun} values with no clear cluster \u2014 the largest holds ${\n        top === undefined ? 'a single company' : percent(top.share)\n      } of the ${analysis.known} that have one. A profile built from that would be arbitrary, so nothing is proposed.`,\n    );\n  }\n\n  const suggestions: IcpValueCluster[] = [];\n  let covered = 0;\n\n  for (const cluster of eligible) {\n    if (\n      suggestions.length >= MAX_SUGGESTIONS ||\n      covered >= COVERAGE_TARGET * analysis.known\n    ) {\n      break;\n    }\n\n    suggestions.push(cluster);\n    covered += cluster.count;\n  }\n\n  const suggested = new Set(suggestions.map((cluster) => cluster.label));\n  const coveredShare = analysis.known === 0 ? 0 : covered / analysis.known;\n\n  return {\n    dimension,\n    proposed: true,\n    confidence: confidenceFor(analysis.known, analysis.coverage),\n    reason: `${suggestions.length} ${noun}${\n      suggestions.length === 1 ? '' : 's'\n    } cover ${percent(coveredShare)} of the ${analysis.known} companies that have one recorded (${percent(\n      analysis.coverage,\n    )} of the ${analysis.total} examined). Everything else found is listed below.`,\n    known: analysis.known,\n    sampled: analysis.total,\n    coverage: analysis.coverage,\n    distinctValues: analysis.distinctValues,\n    coveredShare,\n    suggestions,\n    alternatives: analysis.clusters.filter(\n      (cluster) => !suggested.has(cluster.label),\n    ),\n  };\n};\n\n/* -------------------------------------------------------------------------- */\n/* Size                                                                        */\n/* -------------------------------------------------------------------------- */\n\nexport const proposeSizeDimension = (\n  analysis: SizeAnalysis,\n): IcpSizeDimensionProposal => {\n  const base = {\n    dimension: 'size' as const,\n    known: analysis.known,\n    sampled: analysis.total,\n    coverage: analysis.coverage,\n    median: analysis.median,\n    smallest: analysis.smallest,\n    largest: analysis.largest,\n    buckets: analysis.buckets,\n    confidence: confidenceFor(analysis.known, analysis.coverage),\n  };\n\n  const decline = (reason: string): IcpSizeDimensionProposal => ({\n    ...base,\n    proposed: false,\n    reason,\n    coveredShare: 0,\n    bands: [],\n    outsideCount: analysis.known,\n    outsideShare: analysis.known === 0 ? 0 : 1,\n  });\n\n  if (analysis.total === 0) {\n    return decline(\n      'There are no companies in this workspace to learn from, so Greenlight has nothing to propose.',\n    );\n  }\n\n  if (analysis.known === 0) {\n    return decline(\n      `None of the ${analysis.total} companies examined has an employee count recorded, so there is nothing to derive size bands from.`,\n    );\n  }\n\n  if (analysis.known < MIN_KNOWN_VALUES) {\n    return decline(\n      `Only ${analysis.known} of ${analysis.total} companies has an employee count recorded \u2014 too few to draw a band around.`,\n    );\n  }\n\n  const ranked = [...analysis.buckets]\n    .filter((bucket) => bucket.share >= MIN_BUCKET_SHARE)\n    .sort((a, b) => b.count - a.count);\n\n  const chosen: SizeBucketCount[] = [];\n  let covered = 0;\n\n  for (const bucket of ranked) {\n    if (covered >= COVERAGE_TARGET) {\n      break;\n    }\n\n    chosen.push(bucket);\n    covered += bucket.share;\n  }\n\n  if (chosen.length === 0) {\n    return decline(\n      `The ${analysis.known} companies with a headcount are spread thinly across every size, with no band holding more than ${percent(\n        Math.max(...analysis.buckets.map((bucket) => bucket.share)),\n      )} of them. Nothing is proposed.`,\n    );\n  }\n\n  const bands = mergeBucketsIntoBands(\n    chosen.map((bucket) => bucket.label),\n    analysis.buckets,\n  );\n  const coveredShare = bands.reduce((sum, band) => sum + band.share, 0);\n  const outsideShare = Math.max(0, 1 - coveredShare);\n\n  return {\n    ...base,\n    proposed: true,\n    reason: `${bands.length === 1 ? 'This band covers' : `These ${bands.length} bands cover`} ${percent(\n      coveredShare,\n    )} of the ${analysis.known} companies with a headcount recorded (${percent(\n      analysis.coverage,\n    )} of the ${analysis.total} examined). The median is ${\n      analysis.median === null\n        ? 'unknown'\n        : `${analysis.median.toLocaleString('en-US')} employees`\n    }.`,\n    coveredShare,\n    bands,\n    outsideCount: Math.round(outsideShare * analysis.known),\n    outsideShare,\n  };\n};\n\n/* -------------------------------------------------------------------------- */\n/* The proposal                                                                */\n/* -------------------------------------------------------------------------- */\n\nexport const selectionFrom = ({\n  industry,\n  region,\n  size,\n}: {\n  readonly industry: IcpValueDimensionProposal;\n  readonly region: IcpValueDimensionProposal;\n  readonly size: IcpSizeDimensionProposal;\n}): IcpSelection => ({\n  // Every raw spelling, not the cluster label \u2014 see the note at the top of\n  // `icp-analysis.ts`. The label is a display string; these are what the rule\n  // compares against.\n  industries: industry.suggestions.flatMap((cluster) => [...cluster.values]),\n  regions: region.suggestions.flatMap((cluster) => [...cluster.values]),\n  // `toPlainBand`, and the panel uses the same helper on the same buckets, so\n  // the two selections serialise identically \u2014 which is what lets the panel tell\n  // a freshly-measured impact from a stale one.\n  sizeBands: size.bands.map(toPlainBand),\n});\n\nexport const proposeIcp = (analysis: IcpAnalysis): IcpProposal => {\n  const industry = proposeValueDimension('industry', analysis.industry);\n  const region = proposeValueDimension('region', analysis.region);\n  const size = proposeSizeDimension(analysis.size);\n\n  return {\n    industry,\n    region,\n    size,\n    selection: selectionFrom({ industry, region, size }),\n    analysis,\n  };\n};\n\n/** Convenience for tests and for the one caller that has raw companies. */\nexport const proposeIcpFromCompanies = (\n  input: Parameters<typeof analyseCompanies>[0],\n): IcpProposal => proposeIcp(analyseCompanies(input));\n\n/* -------------------------------------------------------------------------- */\n/* What actually gets written                                                  */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The `GreenlightConfig` patch for a selection.\n *\n * The shapes here are not a choice \u2014 they are dictated by what the other side\n * parses, and both sides were read rather than assumed:\n *\n *   - `icpIndustries` / `icpRegions` are `FieldType.ARRAY` on the record and\n *     reach the engine through `asStringList`, then `resolveIcp`, then\n *     `matchesList`, which compares `normalise(value) === normalise(target)`.\n *     Flat arrays of the raw strings, therefore.\n *   - `icpSizeBands` is `FieldType.RAW_JSON` and is seeded as `{ bands: [...] }`.\n *     `readSizeBands` accepts either that or a bare array; the wrapped form is\n *     what the install hook writes, so it is what this writes too.\n *   - Each band needs `minEmployees` (finite, >= 0) and `maxEmployees` (finite\n *     or `null` for unbounded). `resolveIcp` silently drops any band missing\n *     those, so a band that looks right and scores nothing is exactly the\n *     failure this shape is copied to avoid.\n *\n * The field's own description mentions a `fit` weight. `resolveIcp` does not\n * read one and `icp.company-size` does not use one, so writing it would be\n * writing a lie into the record. Flagged in the report.\n */\nexport const toIcpConfigPatch = (\n  selection: IcpSelection,\n): Record<string, unknown> => ({\n  icpIndustries: [...selection.industries],\n  icpRegions: [...selection.regions],\n  icpSizeBands: {\n    bands: selection.sizeBands.map(\n      ({ label, minEmployees, maxEmployees }) => ({\n        label,\n        minEmployees,\n        maxEmployees,\n      }),\n    ),\n  },\n});\n\n/**\n * The same selection expressed as the `icp` block of a raw scoring-config input,\n * so a preview can score a lead against a profile that has not been saved.\n * `resolveIcp` reads `industries`, `regions` and a bare `sizeBands` array.\n */\nexport const applyIcpToConfigInput = (\n  base: unknown,\n  selection: IcpSelection,\n): Record<string, unknown> => ({\n  ...(typeof base === 'object' && base !== null && !Array.isArray(base)\n    ? (base as Record<string, unknown>)\n    : {}),\n  icp: {\n    industries: [...selection.industries],\n    regions: [...selection.regions],\n    sizeBands: selection.sizeBands.map(\n      ({ label, minEmployees, maxEmployees }) => ({\n        label,\n        minEmployees,\n        maxEmployees,\n      }),\n    ),\n  },\n});\n", "/**\n * \"How much of the scoring model is switched off?\" \u2014 answered in points, from\n * the workspace's own configuration, with no hard-coded numbers.\n *\n * ## Why this is arithmetic and not a boolean\n *\n * The engine's handling of an empty ICP is correct and should not change: an\n * empty list means \"no opinion\", the rule reports `not_applicable`, and the\n * weighting drops it from *both* sides of the fraction so a workspace with no\n * industry opinion is not punished for it. Nothing is broken.\n *\n * What is missing is that the consequence is invisible. `not_applicable` on\n * three rules means industry, region and company size contribute nothing, so\n * every score in the workspace is built out of \"do we hold contact details\"\n * rather than \"is this a fit\" \u2014 and the score still looks like a score. A\n * boolean (\"ICP not configured\") does not convey that. A number does: 40 of the\n * 130 points this engine can award are idle, which is 31% of the model, and it\n * is the 31% that decides whether a lead is worth calling.\n *\n * The numbers are read out of the resolved configuration rather than restated\n * here, so a workspace that reweighted its rules gets its own arithmetic and not\n * ours. Pure: no client, no clock.\n */\n\nimport { ALL_RULES, resolveConfig, type ResolvedScoringConfig } from 'src/scoring';\nimport { toScoringConfigInput } from 'src/logic-functions/greenlight-config-record';\n\nimport {\n  ICP_REVIEW_DECISION_VALUES,\n  type GREENLIGHT_CONFIG_ICP_REVIEW_FIELD_UNIVERSAL_IDENTIFIERS,\n} from 'src/constants/icp-identifiers';\n\n/* -------------------------------------------------------------------------- */\n/* Which rule reads which list                                                 */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The contract between a rule id and the config list that switches it on.\n *\n * Hard-coded, and guarded by a test that fails if the rule catalogue ever grows\n * an `icp` rule that is not in this map. Deriving it would mean the rules\n * declaring their own configuration dependency, which is a change to\n * `src/scoring/` this workstream does not own \u2014 and a missing entry here would\n * otherwise report an unconfigured dimension as configured, which is the one\n * error this whole surface exists to prevent.\n */\nexport const ICP_RULE_DIMENSIONS = {\n  'icp.industry': 'industries',\n  'icp.region': 'regions',\n  'icp.company-size': 'sizeBands',\n} as const satisfies Record<string, keyof ResolvedScoringConfig['icp']>;\n\nexport type IcpRuleId = keyof typeof ICP_RULE_DIMENSIONS;\n\n/* -------------------------------------------------------------------------- */\n/* Review state                                                                */\n/* -------------------------------------------------------------------------- */\n\nexport type IcpReviewDecision =\n  (typeof ICP_REVIEW_DECISION_VALUES)[keyof typeof ICP_REVIEW_DECISION_VALUES];\n\nexport interface IcpReviewState {\n  readonly decision: IcpReviewDecision;\n  readonly reviewedAt: string | null;\n}\n\ntype IcpReviewFieldName =\n  keyof typeof GREENLIGHT_CONFIG_ICP_REVIEW_FIELD_UNIVERSAL_IDENTIFIERS;\n\nexport const ICP_REVIEW_FIELD_NAMES: readonly IcpReviewFieldName[] = [\n  'icpReviewedAt',\n  'icpReviewDecision',\n];\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n  typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst KNOWN_DECISIONS: readonly string[] = Object.values(\n  ICP_REVIEW_DECISION_VALUES,\n);\n\n/**\n * An empty ICP means one of two completely different things \u2014 \"nobody has ever\n * looked at this\" or \"we sell to everyone and left it open on purpose\" \u2014 and the\n * record could not tell them apart before these fields existed. A record from\n * before the upgrade has neither field set, which reads as never reviewed. That\n * is the correct answer for it: nobody had been asked.\n */\nexport const readReviewState = (record: unknown): IcpReviewState => {\n  if (!isRecord(record)) {\n    return {\n      decision: ICP_REVIEW_DECISION_VALUES.notReviewed,\n      reviewedAt: null,\n    };\n  }\n\n  const raw = record['icpReviewDecision'];\n  const decision =\n    typeof raw === 'string' && KNOWN_DECISIONS.includes(raw.trim().toUpperCase())\n      ? (raw.trim().toUpperCase() as IcpReviewDecision)\n      : ICP_REVIEW_DECISION_VALUES.notReviewed;\n\n  const reviewedAtRaw = record['icpReviewedAt'];\n  const reviewedAt =\n    typeof reviewedAtRaw === 'string' && reviewedAtRaw.trim().length > 0\n      ? reviewedAtRaw\n      : null;\n\n  return { decision, reviewedAt };\n};\n\n/* -------------------------------------------------------------------------- */\n/* Status                                                                      */\n/* -------------------------------------------------------------------------- */\n\nexport interface IcpDimensionStatus {\n  readonly ruleId: IcpRuleId;\n  readonly name: string;\n  readonly question: string;\n  /** Does this dimension carry at least one entry? */\n  readonly configured: boolean;\n  readonly entryCount: number;\n  readonly enabled: boolean;\n  /** Points this rule would be worth if it were configured and enabled. */\n  readonly weight: number;\n  /** Enabled, not advisory, and unconfigured: points that exist and do nothing. */\n  readonly idle: boolean;\n}\n\nexport interface IcpStatus {\n  /** At least one of the three dimensions carries an entry. */\n  readonly configured: boolean;\n  readonly fullyConfigured: boolean;\n  /** Weight belonging to enabled ICP rules with nothing to judge. */\n  readonly idlePoints: number;\n  /** Weight belonging to every other rule that can score. */\n  readonly activePoints: number;\n  readonly totalPoints: number;\n  /** `idlePoints / totalPoints`, 0 when the workspace has no scorable rules. */\n  readonly idleShare: number;\n  readonly dimensions: readonly IcpDimensionStatus[];\n  readonly review: IcpReviewState;\n  /** One sentence for the top of the panel. Never hedged, never a warning icon alone. */\n  readonly headline: string;\n}\n\nconst roundTo = (value: number, decimals: number): number => {\n  const factor = 10 ** decimals;\n  return Math.round(value * factor) / factor;\n};\n\nexport const readIcpStatus = (configRecord: unknown): IcpStatus => {\n  const { config } = resolveConfig(toScoringConfigInput(configRecord));\n\n  const dimensions: IcpDimensionStatus[] = [];\n  let idlePoints = 0;\n  let activePoints = 0;\n\n  for (const rule of ALL_RULES) {\n    const setting = config.rules[rule.id];\n    const enabled = setting?.enabled ?? rule.defaultEnabled;\n    const severity = setting?.severity ?? rule.defaultSeverity;\n    const weight = setting?.weight ?? rule.defaultWeight;\n\n    // Advisory rules are reported and never scored, so they are not points at\n    // all \u2014 the engine excludes them from the denominator and so does this.\n    const scorable = enabled && severity !== 'advisory';\n\n    const dimensionKey =\n      rule.id in ICP_RULE_DIMENSIONS\n        ? ICP_RULE_DIMENSIONS[rule.id as IcpRuleId]\n        : null;\n\n    if (dimensionKey === null) {\n      if (scorable) {\n        activePoints += weight;\n      }\n\n      continue;\n    }\n\n    const entryCount = config.icp[dimensionKey].length;\n    const configured = entryCount > 0;\n    const idle = scorable && !configured;\n\n    if (scorable) {\n      if (configured) {\n        activePoints += weight;\n      } else {\n        idlePoints += weight;\n      }\n    }\n\n    dimensions.push({\n      ruleId: rule.id as IcpRuleId,\n      name: rule.name,\n      question: rule.question,\n      configured,\n      entryCount,\n      enabled,\n      weight,\n      idle,\n    });\n  }\n\n  const totalPoints = idlePoints + activePoints;\n  const idleShare = totalPoints === 0 ? 0 : idlePoints / totalPoints;\n  const configured = dimensions.some((dimension) => dimension.configured);\n\n  return {\n    configured,\n    fullyConfigured: dimensions.every((dimension) => dimension.configured),\n    idlePoints,\n    activePoints,\n    totalPoints,\n    idleShare: roundTo(idleShare, 4),\n    dimensions,\n    review: readReviewState(configRecord),\n    headline: buildHeadline({\n      dimensions,\n      idlePoints,\n      totalPoints,\n      idleShare,\n    }),\n  };\n};\n\nconst listNames = (names: readonly string[]): string => {\n  if (names.length <= 1) {\n    return names[0] ?? '';\n  }\n\n  return `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}`;\n};\n\nconst buildHeadline = ({\n  dimensions,\n  idlePoints,\n  totalPoints,\n  idleShare,\n}: {\n  readonly dimensions: readonly IcpDimensionStatus[];\n  readonly idlePoints: number;\n  readonly totalPoints: number;\n  readonly idleShare: number;\n}): string => {\n  const idleNames = dimensions\n    .filter((dimension) => dimension.idle)\n    .map((dimension) => dimension.name.toLowerCase());\n\n  if (idleNames.length === 0) {\n    return 'Every part of your ideal-customer profile is configured and scoring.';\n  }\n\n  return (\n    `${listNames(idleNames)} ${idleNames.length === 1 ? 'is' : 'are'} not configured, so ` +\n    `${idleNames.length === 1 ? 'it scores' : 'they score'} nothing. That is ${idlePoints} of the ` +\n    `${totalPoints} points Greenlight can award \u2014 ${Math.round(idleShare * 100)}% of the model, and ` +\n    'the part that decides whether a lead is a fit. Every score in this workspace is currently built ' +\n    'from whether you hold contact details.'\n  );\n};\n", "/**\n * What applying a profile would actually do \u2014 measured, before anything is\n * written.\n *\n * ## Why this is not optional\n *\n * Writing an ICP changes every future score in the workspace. Three rules that\n * were reporting `not_applicable` and scoring 0/0 start reporting pass or fail\n * and scoring 0/40, and a lead that has no industry recorded fails a `major`\n * rule it was previously exempt from. Applied to a live pipeline that is not a\n * settings change, it is a re-gating of every rep's working list, and the\n * difference between \"this sharpens the queue\" and \"this holds nine hundred\n * leads a team was working from\" is not knowable by reading the profile. It is\n * only knowable by scoring against it.\n *\n * So this module scores the sample twice \u2014 once with the workspace's current\n * configuration and once with the proposed profile patched into it \u2014 and reports\n * the transitions. The engine is pure and never throws, so this can be done\n * exactly, in-process, with no writes and no side effects.\n *\n * ## The second number, which matters more than the first\n *\n * `readability` counts, per dimension, how many sampled leads Greenlight can\n * even *read* the value from. It is separate from \"how many match\" for a reason\n * that the first live measurement made unavoidable: a profile can be perfectly\n * correct about the customer's market and still fail every lead, because the\n * scoring path never sees the field it needs. When that is the situation, an\n * impact preview that only reported \"would gate 1,190 leads\" would be true and\n * useless \u2014 the admin would conclude their ICP is wrong. It is not; the plumbing\n * is missing. The two numbers together say which.\n */\n\nimport {\n  scoreLead,\n  toLeadRecord,\n  type GateDecision,\n  type LeadFieldKey,\n  type ScoringResult,\n} from 'src/scoring';\nimport { toScoringConfigInput } from 'src/logic-functions/greenlight-config-record';\n\nimport {\n  applyIcpToConfigInput,\n  type IcpSelection,\n} from 'src/icp/icp-proposal';\nimport { ICP_RULE_DIMENSIONS, type IcpRuleId } from 'src/icp/icp-status';\n\n/** Which logical field each ICP rule needs to be able to read. */\nexport const ICP_RULE_FIELD_KEYS = {\n  'icp.industry': 'industry',\n  'icp.region': 'region',\n  'icp.company-size': 'employeeCount',\n} as const satisfies Record<IcpRuleId, LeadFieldKey>;\n\nexport interface IcpReadability {\n  readonly ruleId: IcpRuleId;\n  readonly fieldKey: LeadFieldKey;\n  /** The rule was in play at all \u2014 i.e. the proposal configures this dimension. */\n  readonly active: boolean;\n  /** Leads where some mapped field resolved to a value. */\n  readonly readable: number;\n  /** Leads the rule would pass. */\n  readonly matched: number;\n  /** Leads that carry a value which is not in the profile. */\n  readonly mismatched: number;\n  /** Leads where nothing could be read, so the rule fails for want of data. */\n  readonly unreadable: number;\n  /** Field paths the engine tried, in order \u2014 what to point at or fill in. */\n  readonly candidates: readonly string[];\n  /** The path that answered, on the leads where one did. */\n  readonly resolvedPaths: readonly string[];\n}\n\nexport interface IcpImpactExample {\n  readonly leadId: string | null;\n  readonly display: string;\n  readonly scoreBefore: number | null;\n  readonly scoreAfter: number | null;\n  readonly reason: string;\n}\n\nexport interface IcpImpact {\n  readonly sampled: number;\n  /** Leads in the workspace, when countable. Drives the extrapolation only. */\n  readonly totalLeads: number | null;\n  readonly approvedBefore: number;\n  readonly approvedAfter: number;\n  readonly gatedBefore: number;\n  readonly gatedAfter: number;\n  readonly blocked: number;\n  /** Cleared today, held after. The number that decides whether this is safe. */\n  readonly newlyGated: number;\n  /** Held today, cleared after \u2014 possible, because a matching lead earns points. */\n  readonly newlyApproved: number;\n  readonly unchanged: number;\n  readonly averageScoreBefore: number | null;\n  readonly averageScoreAfter: number | null;\n  /** `newlyGated` scaled to the whole workspace. Clearly an estimate; label it. */\n  readonly estimatedNewlyGated: number | null;\n  readonly readability: readonly IcpReadability[];\n  readonly examples: readonly IcpImpactExample[];\n  /** True when the profile is empty, i.e. there is nothing to preview. */\n  readonly empty: boolean;\n}\n\nconst EXAMPLE_LIMIT = 5;\n\nconst displayName = (node: unknown): string => {\n  if (typeof node !== 'object' || node === null) {\n    return 'Unnamed lead';\n  }\n\n  const record = node as Record<string, unknown>;\n  const name = record['name'];\n\n  if (typeof name === 'object' && name !== null) {\n    const parts = name as Record<string, unknown>;\n    const joined = [parts['firstName'], parts['lastName']]\n      .filter((part): part is string => typeof part === 'string')\n      .join(' ')\n      .trim();\n\n    if (joined.length > 0) {\n      return joined;\n    }\n  }\n\n  if (typeof name === 'string' && name.trim().length > 0) {\n    return name.trim();\n  }\n\n  const emails = record['emails'];\n\n  if (typeof emails === 'object' && emails !== null) {\n    const primary = (emails as Record<string, unknown>)['primaryEmail'];\n\n    if (typeof primary === 'string' && primary.trim().length > 0) {\n      return primary.trim();\n    }\n  }\n\n  return 'Unnamed lead';\n};\n\nconst isEmptySelection = (selection: IcpSelection): boolean =>\n  selection.industries.length === 0 &&\n  selection.regions.length === 0 &&\n  selection.sizeBands.length === 0;\n\nconst dimensionIsActive = (\n  ruleId: IcpRuleId,\n  selection: IcpSelection,\n): boolean => {\n  const key = ICP_RULE_DIMENSIONS[ruleId];\n\n  return key === 'industries'\n    ? selection.industries.length > 0\n    : key === 'regions'\n      ? selection.regions.length > 0\n      : selection.sizeBands.length > 0;\n};\n\nconst average = (values: readonly number[]): number | null =>\n  values.length === 0\n    ? null\n    : Math.round(\n        (values.reduce((sum, value) => sum + value, 0) / values.length) * 10,\n      ) / 10;\n\ninterface ReadabilityAccumulator {\n  readable: number;\n  matched: number;\n  mismatched: number;\n  unreadable: number;\n  candidates: string[];\n  resolvedPaths: Set<string>;\n}\n\nconst observeReadability = (\n  result: ScoringResult,\n  ruleId: IcpRuleId,\n  accumulator: ReadabilityAccumulator,\n): void => {\n  const entry = result.trace.find((traceEntry) => traceEntry.ruleId === ruleId);\n\n  if (entry === undefined || entry.outcome === 'not_applicable') {\n    return;\n  }\n\n  const fieldKey = ICP_RULE_FIELD_KEYS[ruleId];\n  const observation = entry.observations.find(\n    (candidate) => candidate.key === fieldKey,\n  );\n\n  if (observation !== undefined && accumulator.candidates.length === 0) {\n    accumulator.candidates = [...observation.candidates];\n  }\n\n  const present = observation?.present ?? false;\n\n  if (present) {\n    accumulator.readable += 1;\n\n    if (observation?.mappedTo !== null && observation?.mappedTo !== undefined) {\n      accumulator.resolvedPaths.add(observation.mappedTo);\n    }\n  } else {\n    accumulator.unreadable += 1;\n  }\n\n  if (entry.outcome === 'pass' || entry.outcome === 'partial') {\n    accumulator.matched += 1;\n  } else if (entry.outcome === 'fail' && present) {\n    accumulator.mismatched += 1;\n  }\n};\n\nexport interface IcpImpactInput {\n  /** Raw Person nodes, fetched with the same selection the scoring path uses. */\n  readonly leads: readonly unknown[];\n  /** The `GreenlightConfig` record as read, unmodified. */\n  readonly configRecord: unknown;\n  readonly selection: IcpSelection;\n  /** Injected, like everywhere else in this codebase. */\n  readonly now: Date;\n  readonly totalLeads: number | null;\n}\n\n/**\n * Score the sample twice and report the difference. Never throws \u2014 `scoreLead`\n * does not, and nothing else here can.\n */\nexport const previewIcpImpact = ({\n  leads,\n  configRecord,\n  selection,\n  now,\n  totalLeads,\n}: IcpImpactInput): IcpImpact => {\n  const baseConfig = toScoringConfigInput(configRecord);\n  const proposedConfig = applyIcpToConfigInput(baseConfig, selection);\n\n  const readability = new Map<IcpRuleId, ReadabilityAccumulator>();\n\n  for (const ruleId of Object.keys(ICP_RULE_DIMENSIONS) as IcpRuleId[]) {\n    readability.set(ruleId, {\n      readable: 0,\n      matched: 0,\n      mismatched: 0,\n      unreadable: 0,\n      candidates: [],\n      resolvedPaths: new Set<string>(),\n    });\n  }\n\n  const tally: Record<GateDecision, number> = {\n    approved: 0,\n    gated: 0,\n    blocked: 0,\n    unscored: 0,\n  };\n  const tallyAfter: Record<GateDecision, number> = {\n    approved: 0,\n    gated: 0,\n    blocked: 0,\n    unscored: 0,\n  };\n\n  const scoresBefore: number[] = [];\n  const scoresAfter: number[] = [];\n  const examples: IcpImpactExample[] = [];\n\n  let newlyGated = 0;\n  let newlyApproved = 0;\n  let unchanged = 0;\n\n  for (const node of leads) {\n    const lead = toLeadRecord(node);\n\n    const before = scoreLead({ lead, now, config: baseConfig });\n    const after = scoreLead({ lead, now, config: proposedConfig });\n\n    tally[before.decision] += 1;\n    tallyAfter[after.decision] += 1;\n\n    if (before.score !== null) {\n      scoresBefore.push(before.score);\n    }\n\n    if (after.score !== null) {\n      scoresAfter.push(after.score);\n    }\n\n    for (const ruleId of readability.keys()) {\n      const accumulator = readability.get(ruleId);\n\n      if (accumulator !== undefined) {\n        observeReadability(after, ruleId, accumulator);\n      }\n    }\n\n    if (before.decision === after.decision) {\n      unchanged += 1;\n      continue;\n    }\n\n    if (before.decision === 'approved' && after.decision === 'gated') {\n      newlyGated += 1;\n\n      if (examples.length < EXAMPLE_LIMIT) {\n        examples.push({\n          leadId: lead.id ?? null,\n          display: displayName(node),\n          scoreBefore: before.score,\n          scoreAfter: after.score,\n          reason: after.reasons[0] ?? after.summary,\n        });\n      }\n\n      continue;\n    }\n\n    if (before.decision === 'gated' && after.decision === 'approved') {\n      newlyApproved += 1;\n    }\n  }\n\n  return {\n    sampled: leads.length,\n    totalLeads,\n    approvedBefore: tally.approved,\n    approvedAfter: tallyAfter.approved,\n    gatedBefore: tally.gated,\n    gatedAfter: tallyAfter.gated,\n    blocked: tally.blocked,\n    newlyGated,\n    newlyApproved,\n    unchanged,\n    averageScoreBefore: average(scoresBefore),\n    averageScoreAfter: average(scoresAfter),\n    estimatedNewlyGated:\n      totalLeads === null || leads.length === 0\n        ? null\n        : Math.round((newlyGated / leads.length) * totalLeads),\n    readability: (Object.keys(ICP_RULE_DIMENSIONS) as IcpRuleId[]).map(\n      (ruleId): IcpReadability => {\n        const accumulator = readability.get(ruleId);\n\n        return {\n          ruleId,\n          fieldKey: ICP_RULE_FIELD_KEYS[ruleId],\n          active: dimensionIsActive(ruleId, selection),\n          readable: accumulator?.readable ?? 0,\n          matched: accumulator?.matched ?? 0,\n          mismatched: accumulator?.mismatched ?? 0,\n          unreadable: accumulator?.unreadable ?? 0,\n          candidates: accumulator?.candidates ?? [],\n          resolvedPaths: [...(accumulator?.resolvedPaths ?? [])],\n        };\n      },\n    ),\n    examples,\n    empty: isEmptySelection(selection),\n  };\n};\n", "/**\n * Numaya Greenlight \u2014 the durable state of a backfill run.\n *\n * Everything in this file is pure: no SDK import, no network, no clock read.\n * `src/logic-functions/backfill-run.ts` is the shell that reads this state out of\n * Twenty's app key-value storage, hands it to the functions below, and writes the\n * result back. That split exists for the same reason it exists in\n * `src/gate/release-decision.ts`: resumability is the property this feature lives\n * or dies by, and it has to be exhaustively testable without a live workspace and\n * without waiting a real minute between cron ticks.\n *\n * ## Why there is state at all\n *\n * The scoring trigger fires on Person create and update. Records that already\n * existed when Greenlight was installed emit neither, so they are never scored \u2014\n * measured on a dev workspace, 1200 of 1205 people, while the gate queue sat\n * empty. An empty queue reads as \"everything has been checked and is clean\". It\n * was not: nothing had been checked.\n *\n * Fixing that means walking every existing Person, and a workspace of ten\n * thousand cannot be walked inside one function invocation at 100 requests a\n * minute. The walk therefore spans many invocations, and anything that spans\n * invocations needs its progress somewhere a crash cannot take with it.\n *\n * ## What \"resumable\" means precisely here\n *\n * A crashed or timed-out tick loses **the chunk it was in the middle of**, not\n * the run. The cursor is only advanced over records the tick actually reached, so\n * the next tick re-fetches the tail it did not get to. Records it *did* reach are\n * re-fetched too when the crash landed mid-chunk \u2014 and re-scoring them costs\n * nothing, because `scoring-run.ts`'s outcome fingerprint (guard 3) recognises an\n * unchanged outcome and writes neither the record nor an audit row. Idempotency\n * is not something this module has to provide; it only has to avoid *losing*\n * position, and it is allowed to lose a little.\n *\n * ## The lease\n *\n * `kv` offers no compare-and-set, so two overlapping ticks could both read the\n * same cursor. `leaseUntil` narrows that window: a tick claims the lease before\n * it does any work and releases it when it commits. It is a courtesy, not a\n * mutex \u2014 the read-modify-write race is still theoretically open.\n *\n * That is acceptable because the lease is not what makes the run safe. Guard 3\n * is. Two ticks processing the same page produce one set of writes and one set of\n * audit rows; the second finds every fingerprint already stored and does nothing.\n * The lease exists to stop us *wasting* the rate-limit budget, not to stop us\n * corrupting anything.\n *\n * The lease also has to expire, and that is the whole point: a tick the platform\n * killed on timeout never releases it, so a permanent lease would deadlock the\n * run at the first timeout. `BACKFILL_LEASE_MS` is therefore the maximum a crash\n * can cost \u2014 see the constant.\n */\n\n/* -------------------------------------------------------------------------- */\n/* Vocabulary                                                                  */\n/* -------------------------------------------------------------------------- */\n\n/**\n * What a run is allowed to look at.\n *\n *   - `unscored` \u2014 Person records with **no** `greenlightDecision` at all. This\n *     is the defect: never seen by the engine. It is the default and it is the\n *     only mode that runs itself on install.\n *   - `all` \u2014 every Person, re-scored. Needed after a rule or weight change, and\n *     needed to move records scored before `BLOCKED` existed off their legacy\n *     `GATE` value. Never automatic: it rewrites decisions a workspace may have\n *     been working from, so a human asks for it explicitly.\n *\n * Note what is *not* here: a mode that re-scores `UNSCORED` records. Those are\n * fail-opens the engine already looked at and could not score, they already have\n * a view of their own, and a mode that retried them on a schedule would append an\n * ERROR audit row per record per pass forever. `all` covers them when a human\n * decides the underlying cause is fixed.\n */\nexport const BACKFILL_MODES = ['unscored', 'all'] as const;\n\nexport type BackfillMode = (typeof BACKFILL_MODES)[number];\n\nexport const isBackfillMode = (value: unknown): value is BackfillMode =>\n  typeof value === 'string' &&\n  (BACKFILL_MODES as readonly string[]).includes(value);\n\n/**\n * `running` is the only state the cron acts on. The other three are terminal and\n * exist so the panel can say which of them happened \u2014 \"finished\", \"you stopped\n * it\" and \"it broke\" are three different things an admin needs told apart, and\n * collapsing them into \"not running\" is the same species of silence this whole\n * feature exists to remove.\n */\nexport type BackfillStatus = 'running' | 'completed' | 'cancelled' | 'failed';\n\n/* -------------------------------------------------------------------------- */\n/* Timings                                                                     */\n/* -------------------------------------------------------------------------- */\n\n/**\n * How long a tick holds the lease.\n *\n * Sized at roughly twice the cron function's `timeoutSeconds` (55). Shorter than\n * the timeout and a slow-but-healthy tick would have its lease stolen by the next\n * one, which is the overlap the lease exists to prevent. Much longer and a tick\n * the platform killed would idle the run for no reason: this is the exact cost of\n * a crash, so it is deliberately the smallest value that is safely above one full\n * invocation.\n */\nexport const BACKFILL_LEASE_MS = 120_000;\n\n/**\n * Ceiling on how many boundary ids the cursor carries.\n *\n * `cursor.ids` holds the records already handled at the cursor's exact timestamp,\n * so the `gte` filter does not re-process them. It grows whenever more records\n * than one chunk share a single `createdAt` \u2014 which is not exotic: Postgres\n * `now()` is constant within a transaction, so a bulk import writes rows with\n * byte-identical timestamps.\n *\n * The value is not arbitrary. `backfill-plan.ts` over-fetches a page by exactly\n * this many rows so that skipping the boundary does not eat the chunk's budget,\n * and Twenty's page ceiling is 60, so this is `60 \u2212 BACKFILL_CHUNK_SIZE`. It is\n * declared here rather than imported from there only because `backfill-plan.ts`\n * imports *this* module; `backfill-plan.test.ts` asserts the two agree.\n *\n * Beyond the cap the walk still cannot skip a record \u2014 it can only re-read one,\n * which guard 3 makes free. What it can do is stop making progress, and\n * `BACKFILL_MAX_STALLS` is what turns that into something visible.\n */\nexport const BACKFILL_MAX_CURSOR_IDS = 30;\n\n/**\n * Consecutive ticks that fetched records but got through none of them before the\n * run is declared failed.\n *\n * The only way this happens is a `createdAt` boundary wider than a whole page:\n * more than sixty records sharing one timestamp, where the order among ties is\n * not stable enough for the boundary set to cover them. In `unscored` mode it\n * self-corrects, because a scored record leaves the filter. In `all` mode there\n * is no self-correction, and without this the run would spin against the rate\n * limit forever, reporting \"running\", making no progress, and looking exactly\n * like a healthy backfill on a large workspace.\n *\n * Three rather than one: tie ordering can legitimately shuffle between pages, so\n * a single unproductive tick is not evidence of anything. Three in a row is.\n */\nexport const BACKFILL_MAX_STALLS = 3;\n\n/* -------------------------------------------------------------------------- */\n/* The state                                                                   */\n/* -------------------------------------------------------------------------- */\n\n/** Where the walk has got to. Split out because it is advanced as a unit. */\nexport interface BackfillCursor {\n  /** ISO 8601 `createdAt` of the last record handled. Null before the first. */\n  readonly createdAt: string | null;\n  /** Ids already handled that share exactly `createdAt`. */\n  readonly ids: readonly string[];\n}\n\nexport interface BackfillTally {\n  /** Records fetched and looked at, including ones nothing was written for. */\n  readonly examined: number;\n  /** `scoring-run` wrote a record and an audit row. */\n  readonly scored: number;\n  /** Guard 3 recognised the outcome; nothing was written. */\n  readonly unchanged: number;\n  /** `scoring-run` declined to score (unreadable event, authored write). */\n  readonly skipped: number;\n  /** `scoring-run` failed open: flagged UNSCORED, ERROR row appended. */\n  readonly failed: number;\n}\n\nexport const EMPTY_TALLY: BackfillTally = {\n  examined: 0,\n  scored: 0,\n  unchanged: 0,\n  skipped: 0,\n  failed: 0,\n};\n\nexport interface BackfillState {\n  /** Bumped only for a shape change the reader below cannot absorb. */\n  readonly version: 1;\n  /** Derived from the start time, so it is unique without a random source. */\n  readonly runId: string;\n  readonly mode: BackfillMode;\n  readonly status: BackfillStatus;\n  readonly startedAt: string;\n  readonly updatedAt: string;\n  readonly finishedAt: string | null;\n  /** ISO 8601. Null when no tick holds the lease. */\n  readonly leaseUntil: string | null;\n  readonly cursor: BackfillCursor;\n  /**\n   * How many records matched the mode when the run started. Null when the count\n   * query failed \u2014 a count is a nicety and must never stop a run beginning.\n   */\n  readonly totalAtStart: number | null;\n  readonly tally: BackfillTally;\n  /** Ticks that committed work. Distinguishes \"slow\" from \"stuck\". */\n  readonly chunks: number;\n  /** Consecutive ticks that fetched records and got through none. */\n  readonly stalls: number;\n  /** The most recent tick-level failure, kept so the panel can show it. */\n  readonly lastError: string | null;\n  /** Server-derived, or `'install'` for the automatic run. */\n  readonly requestedBy: string;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Reading it back                                                             */\n/* -------------------------------------------------------------------------- */\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n  typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst asString = (value: unknown, fallback: string): string =>\n  typeof value === 'string' && value.length > 0 ? value : fallback;\n\nconst asNullableString = (value: unknown): string | null =>\n  typeof value === 'string' && value.length > 0 ? value : null;\n\nconst asCount = (value: unknown): number =>\n  typeof value === 'number' && Number.isFinite(value) && value >= 0\n    ? Math.floor(value)\n    : 0;\n\nconst asIdList = (value: unknown): string[] =>\n  Array.isArray(value)\n    ? value.filter((entry): entry is string => typeof entry === 'string')\n    : [];\n\nconst STATUSES: readonly string[] = [\n  'running',\n  'completed',\n  'cancelled',\n  'failed',\n];\n\n/**\n * Coerce whatever came out of key-value storage into a state, or `null`.\n *\n * Deliberately lenient about every field except the two that decide behaviour \u2014\n * `status` and `mode`. An unreadable counter is cosmetic and defaults to zero; an\n * unreadable *status* would let a corrupt blob keep a cron loop alive against a\n * cursor nobody can interpret, so anything it does not recognise is refused\n * outright and the run reads as \"there is no backfill\", which is the state an\n * admin can act on.\n *\n * Refusing rather than repairing is the right direction here specifically because\n * the recovery is cheap and visible: the panel shows no run, and starting a fresh\n * one costs one click and re-walks records that are almost all already scored \u2014\n * which guard 3 makes nearly free.\n */\nexport const toBackfillState = (value: unknown): BackfillState | null => {\n  if (!isRecord(value)) {\n    return null;\n  }\n\n  const status = value['status'];\n  const mode = value['mode'];\n\n  if (typeof status !== 'string' || !STATUSES.includes(status)) {\n    return null;\n  }\n\n  if (!isBackfillMode(mode)) {\n    return null;\n  }\n\n  const startedAt = asString(value['startedAt'], '');\n\n  if (startedAt === '') {\n    return null;\n  }\n\n  const cursor = isRecord(value['cursor']) ? value['cursor'] : {};\n  const tally = isRecord(value['tally']) ? value['tally'] : {};\n\n  return {\n    version: 1,\n    runId: asString(value['runId'], startedAt),\n    mode,\n    status: status as BackfillStatus,\n    startedAt,\n    updatedAt: asString(value['updatedAt'], startedAt),\n    finishedAt: asNullableString(value['finishedAt']),\n    leaseUntil: asNullableString(value['leaseUntil']),\n    cursor: {\n      createdAt: asNullableString(cursor['createdAt']),\n      ids: asIdList(cursor['ids']),\n    },\n    totalAtStart:\n      typeof value['totalAtStart'] === 'number' &&\n      Number.isFinite(value['totalAtStart'])\n        ? Math.floor(value['totalAtStart'])\n        : null,\n    tally: {\n      examined: asCount(tally['examined']),\n      scored: asCount(tally['scored']),\n      unchanged: asCount(tally['unchanged']),\n      skipped: asCount(tally['skipped']),\n      failed: asCount(tally['failed']),\n    },\n    chunks: asCount(value['chunks']),\n    stalls: asCount(value['stalls']),\n    lastError: asNullableString(value['lastError']),\n    requestedBy: asString(value['requestedBy'], 'unknown'),\n  };\n};\n\n/* -------------------------------------------------------------------------- */\n/* Transitions                                                                 */\n/* -------------------------------------------------------------------------- */\n\nexport interface BeginBackfillInput {\n  readonly mode: BackfillMode;\n  readonly now: Date;\n  readonly requestedBy: string;\n  readonly totalAtStart: number | null;\n}\n\n/**\n * The run id is derived from the start instant rather than drawn from a random\n * source, so this module stays pure and a test can assert the exact id. Two runs\n * cannot start in the same millisecond \u2014 `beginBackfill` is only reachable from\n * the control route, which refuses while a run is `running`.\n */\nexport const beginBackfill = ({\n  mode,\n  now,\n  requestedBy,\n  totalAtStart,\n}: BeginBackfillInput): BackfillState => {\n  const startedAt = now.toISOString();\n\n  return {\n    version: 1,\n    runId: `backfill-${startedAt}`,\n    mode,\n    status: 'running',\n    startedAt,\n    updatedAt: startedAt,\n    finishedAt: null,\n    // Left unclaimed so the very next cron tick picks the run up rather than\n    // waiting out a lease nobody is holding.\n    leaseUntil: null,\n    cursor: { createdAt: null, ids: [] },\n    totalAtStart,\n    tally: EMPTY_TALLY,\n    chunks: 0,\n    stalls: 0,\n    lastError: null,\n    requestedBy,\n  };\n};\n\nexport const isLeaseHeld = (state: BackfillState, now: Date): boolean => {\n  if (state.leaseUntil === null) {\n    return false;\n  }\n\n  const until = Date.parse(state.leaseUntil);\n\n  // An unparseable lease is treated as expired. The alternative \u2014 treating it as\n  // held \u2014 would strand the run permanently on one bad write, and the cost of\n  // being wrong is one duplicated chunk that writes nothing.\n  return !Number.isNaN(until) && until > now.getTime();\n};\n\nexport const claimLease = (state: BackfillState, now: Date): BackfillState => ({\n  ...state,\n  updatedAt: now.toISOString(),\n  leaseUntil: new Date(now.getTime() + BACKFILL_LEASE_MS).toISOString(),\n});\n\n/** Give the lease back without changing anything else. Used on a failed tick. */\nexport const releaseLease = (\n  state: BackfillState,\n  now: Date,\n  lastError: string | null,\n): BackfillState => ({\n  ...state,\n  updatedAt: now.toISOString(),\n  leaseUntil: null,\n  lastError,\n});\n\nconst addTally = (left: BackfillTally, right: BackfillTally): BackfillTally => ({\n  examined: left.examined + right.examined,\n  scored: left.scored + right.scored,\n  unchanged: left.unchanged + right.unchanged,\n  skipped: left.skipped + right.skipped,\n  failed: left.failed + right.failed,\n});\n\nexport interface CommitChunkInput {\n  readonly now: Date;\n  readonly cursor: BackfillCursor;\n  readonly tally: BackfillTally;\n  readonly lastError: string | null;\n  /** True when the page proved there is nothing left to walk. */\n  readonly exhausted: boolean;\n  /** True when records came back and the tick got through none of them. */\n  readonly stalled: boolean;\n}\n\nconst STALL_MESSAGE =\n  'The backfill stopped making progress. More records share one creation timestamp than a single page can hold, so the walk cannot get past them. Run the backfill over unscored leads only \u2014 that mode drains regardless \u2014 or contact support.';\n\n/**\n * Fold one tick's work into the run and release the lease.\n *\n * The cursor is replaced wholesale rather than merged: the caller has already\n * worked out where the walk actually reached, and it is the only party that can \u2014\n * see `advanceCursor` in `backfill-plan.ts`.\n *\n * Three ways out of `running`, and it matters that they are distinguishable:\n * `exhausted` is the walk finishing, `stalled` past its limit is the walk being\n * unable to continue, and neither is a cancel. A run that quietly stayed\n * \"running\" while achieving nothing would be the same defect as an empty gate\n * queue over an unscored workspace \u2014 a surface that looks healthy and is not.\n */\nexport const commitChunk = (\n  state: BackfillState,\n  { now, cursor, tally, lastError, exhausted, stalled }: CommitChunkInput,\n): BackfillState => {\n  const at = now.toISOString();\n  const stalls = stalled ? state.stalls + 1 : 0;\n  const wedged = stalls >= BACKFILL_MAX_STALLS;\n\n  return {\n    ...state,\n    status: exhausted ? 'completed' : wedged ? 'failed' : state.status,\n    updatedAt: at,\n    finishedAt: exhausted || wedged ? at : state.finishedAt,\n    leaseUntil: null,\n    cursor: {\n      createdAt: cursor.createdAt,\n      ids: cursor.ids.slice(-BACKFILL_MAX_CURSOR_IDS),\n    },\n    tally: addTally(state.tally, tally),\n    chunks: state.chunks + 1,\n    stalls,\n    lastError: wedged ? STALL_MESSAGE : lastError,\n  };\n};\n\nexport const cancelBackfill = (\n  state: BackfillState,\n  now: Date,\n  requestedBy: string,\n): BackfillState => ({\n  ...state,\n  status: 'cancelled',\n  updatedAt: now.toISOString(),\n  finishedAt: now.toISOString(),\n  // Released so a cancel taken while a tick is mid-flight does not leave a lease\n  // behind on a run nobody will ever pick up again.\n  leaseUntil: null,\n  requestedBy,\n});\n\n/* -------------------------------------------------------------------------- */\n/* What the admin is shown                                                     */\n/* -------------------------------------------------------------------------- */\n\nexport interface BackfillProgress {\n  readonly runId: string;\n  readonly mode: BackfillMode;\n  readonly status: BackfillStatus;\n  readonly startedAt: string;\n  readonly updatedAt: string;\n  readonly finishedAt: string | null;\n  readonly totalAtStart: number | null;\n  readonly examined: number;\n  readonly scored: number;\n  readonly unchanged: number;\n  readonly skipped: number;\n  readonly failed: number;\n  readonly chunks: number;\n  readonly lastError: string | null;\n  readonly requestedBy: string;\n  /** 0-100, or null when the total was never known. */\n  readonly percent: number | null;\n  /** Whole minutes, or null when there is nothing to estimate from. */\n  readonly minutesRemaining: number | null;\n  /** True when a tick is currently holding the lease. */\n  readonly working: boolean;\n}\n\n/**\n * Render the state for a human.\n *\n * `percent` is capped at 100 and floored at 0 rather than reported raw. The total\n * is a snapshot taken when the run started, and records created *during* a run\n * are walked too, so the honest arithmetic can exceed the denominator. A progress\n * bar reading 114% is a bug report; the cap is not hiding anything the counters\n * beside it do not already state exactly.\n *\n * `minutesRemaining` is deliberately derived from the chunk *rate this run has\n * actually achieved* rather than from the nominal chunk size, so a workspace\n * whose ticks are being skipped is told the truth about how long it will take.\n */\nexport const describeBackfill = (\n  state: BackfillState,\n  now: Date,\n): BackfillProgress => {\n  const { tally, totalAtStart } = state;\n\n  const percent =\n    totalAtStart === null || totalAtStart <= 0\n      ? null\n      : Math.max(0, Math.min(100, Math.round((tally.examined / totalAtStart) * 100)));\n\n  const elapsedMs = Math.max(\n    0,\n    Date.parse(state.updatedAt) - Date.parse(state.startedAt),\n  );\n\n  const remaining =\n    totalAtStart === null ? null : Math.max(0, totalAtStart - tally.examined);\n\n  const minutesRemaining =\n    state.status !== 'running' ||\n    remaining === null ||\n    remaining === 0 ||\n    tally.examined === 0 ||\n    elapsedMs <= 0\n      ? null\n      : Math.ceil(remaining / (tally.examined / (elapsedMs / 60_000)));\n\n  return {\n    runId: state.runId,\n    mode: state.mode,\n    status: state.status,\n    startedAt: state.startedAt,\n    updatedAt: state.updatedAt,\n    finishedAt: state.finishedAt,\n    totalAtStart,\n    examined: tally.examined,\n    scored: tally.scored,\n    unchanged: tally.unchanged,\n    skipped: tally.skipped,\n    failed: tally.failed,\n    chunks: state.chunks,\n    lastError: state.lastError,\n    requestedBy: state.requestedBy,\n    percent,\n    minutesRemaining:\n      minutesRemaining === null || !Number.isFinite(minutesRemaining)\n        ? null\n        : minutesRemaining,\n    working: isLeaseHeld(state, now),\n  };\n};\n", "/**\n * Cache freshness and offline-grace arithmetic.\n *\n * Pure: `now` is passed in, never read. The scoring engine holds the same line\n * for the same reason \u2014 a clock read inside a decision function makes the\n * decision untestable at its boundaries, and every interesting case here *is* a\n * boundary.\n *\n * ---------------------------------------------------------------------------\n * ## The windows, and why they are these windows\n *\n * From the error-state table in `LICENSING_INTEGRATION.md`:\n *\n * | Cache age        | Behaviour                                     |\n * |------------------|-----------------------------------------------|\n * | `< 24h`          | Cached entitlement used as-is                 |\n * | `24h \u2013 72h`      | Rules-only, \"grace period\"                    |\n * | `> 72h`          | Rules-only + visible warning                  |\n *\n * Note that grace **ends at 72h from `cachedAt`**, not at 24h + 72h. The prose\n * elsewhere reads \"24h cache, then a 72h offline grace window\", which would put\n * the cliff at 96h; the table is the specific claim and the table is what is\n * implemented. The difference is 24 hours of degraded-but-warned operation and\n * it changes nothing about safety, because **nothing is ever blocked at any\n * point on this ladder** \u2014 past 72h the product is still scoring leads, it is\n * just scoring them without enrichment and saying so loudly.\n *\n * ## Why grace disables enrichment rather than extending it\n *\n * Because the two failure modes are not symmetric. Trusting a stale entitlement\n * for three days means potentially serving a paid feature to a revoked licence,\n * and revocation is the one lever that has to work. Turning enrichment off means\n * the customer loses an optional enhancement during a Numaya outage. The second\n * is recoverable in seconds when the service returns; the first is not\n * recoverable at all.\n *\n * The 24h band is the exception, and it is bounded precisely so that \"Numaya\n * restarted a container\" never costs a customer anything.\n * ---------------------------------------------------------------------------\n */\n\nconst HOUR_MS = 60 * 60 * 1000;\n\n/** Entitlements younger than this are used exactly as if they were live. */\nexport const LICENCE_CACHE_TTL_MS = 24 * HOUR_MS;\n\n/**\n * Age at which the offline grace window closes, measured from `cachedAt`.\n * Past this the product warns visibly \u2014 it does not stop.\n */\nexport const LICENCE_OFFLINE_GRACE_LIMIT_MS = 72 * HOUR_MS;\n\nexport type CacheFreshness = 'fresh' | 'grace' | 'expired';\n\n/**\n * Age of a cached entitlement in milliseconds, or `null` when `cachedAt` is\n * missing or unparseable.\n *\n * A negative age \u2014 a cache written by a machine whose clock is ahead \u2014 is\n * clamped to `0` rather than rejected. The alternative is that a small clock\n * skew on the licensing service makes a workspace look like it has never\n * validated, which is a worse outcome than briefly treating a slightly-future\n * entitlement as fresh.\n */\nexport const cacheAgeMs = (\n  cachedAt: string | null | undefined,\n  now: Date,\n): number | null => {\n  if (typeof cachedAt !== 'string' || cachedAt.length === 0) {\n    return null;\n  }\n\n  const cachedMs = Date.parse(cachedAt);\n\n  if (Number.isNaN(cachedMs)) {\n    return null;\n  }\n\n  return Math.max(0, now.getTime() - cachedMs);\n};\n\n/**\n * Boundaries are inclusive at the *lower* edge of the worse band: an entitlement\n * exactly 24h old is already in grace, and one exactly 72h old is already\n * expired. Erring towards the stricter band on the boundary keeps the rule\n * \"fresh means strictly under a day old\" true as stated.\n */\nexport const classifyCacheAge = (ageMs: number): CacheFreshness => {\n  if (ageMs < LICENCE_CACHE_TTL_MS) {\n    return 'fresh';\n  }\n\n  if (ageMs < LICENCE_OFFLINE_GRACE_LIMIT_MS) {\n    return 'grace';\n  }\n\n  return 'expired';\n};\n\n/**\n * `null` when there is no usable cache at all \u2014 which is not the same as an\n * expired one, and the admin notice says so differently.\n */\nexport const classifyCache = (\n  cachedAt: string | null | undefined,\n  now: Date,\n): { readonly freshness: CacheFreshness | null; readonly ageMs: number | null } => {\n  const ageMs = cacheAgeMs(cachedAt, now);\n\n  return {\n    freshness: ageMs === null ? null : classifyCacheAge(ageMs),\n    ageMs,\n  };\n};\n", "/**\n * Resolving \"am I running shipped defaults or calibration vNN, and when did it\n * last refresh?\" \u2014 one pure function, `now` injected, no I/O.\n *\n * ===========================================================================\n * ## The same ladder as the entitlement, for the same reasons\n *\n * Calibration is cached beside the entitlement in workspace key-value storage\n * and walks the identical 24h / 72h freshness ladder from\n * `src/licensing/cache.ts`. Sharing the arithmetic is not laziness \u2014 the two\n * caches are refreshed by the *same nightly run*, so any second ladder would\n * differ from this one only by drifting out of step with it.\n *\n * Where the two differ is what the far end of the ladder means, and the\n * difference is the whole point:\n *\n *   | Age        | Entitlement                  | Calibration                 |\n *   |------------|------------------------------|-----------------------------|\n *   | `< 24h`    | used as-is                   | used as-is                  |\n *   | `24\u201372h`   | rules-only (\"grace\")         | **still used**              |\n *   | `> 72h`    | rules-only + loud warning    | shipped defaults            |\n *\n * The entitlement tightens at 24h because trusting a stale one risks serving a\n * paid feature to a revoked licence, and revocation has to work. Calibration\n * carries no such risk: it is a word list. Dropping it the moment Numaya has a\n * bad night would make a customer's scores move for a reason that has nothing to\n * do with their data, which is a worse outcome than a slightly old vocabulary.\n * So calibration survives the grace window and is only abandoned past 72h, at\n * which point \"we have not spoken to Numaya in three days\" is a real statement\n * about the install and the admin should be seeing shipped-default behaviour\n * they can reason about.\n *\n * Note the asymmetry is safe in both directions: past 72h calibration falls back\n * to the shipped defaults, which is a *working product*, not a degraded one.\n * There is no branch below that can stop a lead being scored.\n * ===========================================================================\n */\n\nimport { classifyCache } from 'src/licensing/cache';\nimport {\n  type CachedCalibration,\n  type Calibration,\n  type CalibrationCounts,\n  type CalibrationReason,\n  type CalibrationState,\n} from 'src/calibration/types';\n\nexport const countCalibration = (\n  calibration: Calibration,\n): CalibrationCounts => ({\n  decisionMakerTitles: calibration.decisionMakerTitles.length,\n  influencerTitles: calibration.influencerTitles.length,\n  roleInboxLocalParts: calibration.roleInboxLocalParts.length,\n  placeholderValues: calibration.placeholderValues.length,\n  industrySynonyms: Object.keys(calibration.industrySynonyms).length,\n});\n\n/**\n * The state to publish when no usable calibration exists.\n *\n * Every field that describes a payload is `null` rather than zero or empty\n * string: \"there is no calibration\" and \"there is a calibration with nothing in\n * it\" must not render the same in the admin panel, and a zero count is exactly\n * how the second one would look.\n */\nexport const shippedDefaultsState = (\n  reason: CalibrationReason,\n  now: Date,\n): CalibrationState => ({\n  status: 'shipped_defaults',\n  reason,\n  source: 'none',\n  calibrationVersion: null,\n  issuedAt: null,\n  refreshedAt: null,\n  checkedAt: now.toISOString(),\n  cacheAgeMs: null,\n  keyId: null,\n  counts: null,\n});\n\nexport const calibratedState = (\n  entry: CachedCalibration,\n  source: 'live' | 'cache',\n  cacheAgeMs: number | null,\n  now: Date,\n): CalibrationState => ({\n  status: 'calibrated',\n  reason: 'calibrated',\n  source,\n  calibrationVersion: entry.calibration.calibrationVersion,\n  issuedAt: entry.calibration.issuedAt.length > 0 ? entry.calibration.issuedAt : null,\n  refreshedAt: entry.verifiedAt,\n  checkedAt: now.toISOString(),\n  cacheAgeMs,\n  keyId: entry.keyId,\n  counts: countCalibration(entry.calibration),\n});\n\n/**\n * Age at which a cached calibration stops being used. Deliberately the\n * entitlement's *outer* limit, not its inner one \u2014 see the table above.\n */\nexport const CALIBRATION_MAX_AGE_MS = 72 * 60 * 60 * 1000;\n\nexport interface ResolveCalibrationInput {\n  /** The cached payload, or `null` when none has ever been stored. */\n  readonly cached: CachedCalibration | null;\n  readonly now: Date;\n}\n\nexport interface ResolvedCalibration {\n  readonly state: CalibrationState;\n  /** The payload the scoring path should use, or `null` for shipped defaults. */\n  readonly calibration: Calibration | null;\n}\n\n/**\n * What the scoring path should use right now, and what the admin should be told.\n *\n * ===========================================================================\n * ## Why there is no entitlement check here\n *\n * Because the *existence of the cache is* the entitlement check, and moving it\n * to the writer is what keeps a licence lookup out of the per-lead scoring path.\n *\n * `refreshCalibration` writes the cache only while the licence resolves to\n * `full`, and **deletes it** the moment a nightly run finds the entitlement\n * gone. So a present, fresh cache already means \"this workspace was entitled at\n * its last successful licence check\". Re-deriving that per lead would mean a\n * second key-value read on the hot path to re-establish a fact the nightly run\n * has already written down.\n *\n * The consequence is a revocation latency of at most 24 hours \u2014 one nightly\n * cycle \u2014 which is exactly the latency enrichment revocation already has and is\n * documented as having, for the same reason: the alternative is putting a\n * network call to Numaya in front of a customer's scoring path, which is the\n * coupling the fail-open rule exists to prevent.\n *\n * The reverse case costs the customer one night. A renewed licence re-fetches on\n * the next run, and until then the workspace scores on shipped defaults \u2014 the\n * full working product, exactly as an unlicensed install always has.\n * ===========================================================================\n */\nexport const resolveCalibration = (\n  input: ResolveCalibrationInput,\n): ResolvedCalibration => {\n  if (input.cached === null) {\n    return {\n      state: shippedDefaultsState('not_provisioned', input.now),\n      calibration: null,\n    };\n  }\n\n  const { ageMs } = classifyCache(input.cached.cachedAt, input.now);\n\n  // An unreadable `cachedAt` is treated as expired rather than as fresh. The\n  // opposite choice would make a corrupt timestamp mean \"trust this forever\",\n  // which is the one thing a freshness check must never be talked into.\n  if (ageMs === null || ageMs >= CALIBRATION_MAX_AGE_MS) {\n    return {\n      state: {\n        ...shippedDefaultsState('stale', input.now),\n        cacheAgeMs: ageMs,\n        calibrationVersion: input.cached.calibration.calibrationVersion,\n        keyId: input.cached.keyId,\n      },\n      calibration: null,\n    };\n  }\n\n  return {\n    state: calibratedState(\n      input.cached,\n      ageMs === 0 ? 'live' : 'cache',\n      ageMs,\n      input.now,\n    ),\n    calibration: input.cached.calibration,\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 Person scoring run \u2014 everything the two database-event registrations share.\n *\n * Kept out of the `define*` files so it can be exercised against a fake API\n * client. The only impure things it touches are the client it is handed and the\n * `now` it is passed; there is no `new Date()` and no `new CoreApiClient()`\n * anywhere below.\n *\n * ## Not retriggering ourselves\n *\n * Writing `greenlightScore` back to a Person emits `person.updated`, which is\n * the event that made us score in the first place. Three independent guards stop\n * that becoming a loop, in order of how much they can be trusted:\n *\n *  1. **The trigger allow-list** (`SCORING_TRIGGER_PERSON_FIELDS`, declared in\n *     `score-person-updated.logic-function.ts`). Twenty only dispatches an update\n *     event to a function whose `databaseEventTriggerSettings.updatedFields`\n *     names a field that actually changed. None of Greenlight's three fields is\n *     on that list, so the platform never delivers our own write back to us.\n *     This guard runs outside our process and is the one doing the real work.\n *\n *  2. **The authored-write check** (`isGreenlightAuthoredWrite`). If an event\n *     arrives anyway \u2014 a bulk edit that touched a scoring field *and* a\n *     Greenlight field, a future SDK that treats `updatedFields` as advisory \u2014\n *     and every field it reports is one we own, the run exits before any read or\n *     write. Cheap, and it does not depend on the platform honouring anything.\n *\n *  3. **The outcome fingerprint** (`fingerprintOutcome`). Even if both guards\n *     above were bypassed, the second pass over an unchanged lead produces an\n *     identical fingerprint to the one already stored on the record, so nothing\n *     is written and the chain terminates after exactly one extra iteration.\n *     This is also what makes a retried event idempotent: no second audit row,\n *     no second write.\n *\n * The allow-list in guard 1 does carry three Greenlight-named fields \u2014\n * `greenlightSuppressed`, `greenlightSuppressionReason` and\n * `greenlightEnrichment`. All three are safe for the same structural reason:\n * this run never writes any of them, so its own write-back still wakes nothing.\n * Both exceptions, and the bounded enrichment cycle the third one does close,\n * are argued in full on `SCORING_TRIGGER_PERSON_FIELDS` below.\n *\n * Guard 3 fingerprints the *outcome*, not the inputs, on purpose. The engine's\n * default field mapping reads `lastVerifiedAt` from `updatedAt`, which changes\n * on every write \u2014 an input hash would therefore differ on every pass and could\n * never terminate. The outcome is stable by construction.\n *\n * ## Not undoing a human override\n *\n * The guards above stop *our own* write from re-scoring a lead. They do nothing\n * about the other half of the contract with `release-lead.logic-function.ts`: a\n * rep releases a gated lead, then edits `jobTitle` seconds later, which is a\n * legitimate re-score that would re-gate the lead the human just released.\n *\n * The release writes a marker to app key-value storage. This run reads it before\n * writing a holding decision and pins the decision to `PASS` when one is present\n * \u2014 the score and the whole trace still update, only the decision is pinned. See\n * `pinReleasedDecision` for the exceptions and the failure directions.\n */\n\nimport {\n  SCORING_ENGINE_VERSION,\n  scoreLead,\n  toLeadRecord,\n  type GateDecision,\n  type ScoringResult,\n} from 'src/scoring';\n\nimport {\n  NO_SEAL,\n  resolveCalibration,\n  sealedCalibrationBody,\n  toScoringBaseline,\n  type CalibrationReaderPort,\n  type CalibrationSealPort,\n} from 'src/calibration';\nimport {\n  releaseMarkerKey,\n  type ReleaseMarker,\n} from 'src/gate/release-decision';\nimport { SUPPRESSION_SCORING_INPUT_FIELDS } from 'src/gate/suppression-decision';\nimport {\n  describeError,\n  isPlainRecord,\n  logGreenlight,\n  oldestByCreatedAt,\n  readConnectionNodes,\n  type GreenlightApiClient,\n} from 'src/logic-functions/greenlight-api';\nimport {\n  greenlightConfigSelection,\n  isGateEnabled,\n  readLeadObjectNameSingular,\n  toScoringConfigInput,\n} from 'src/logic-functions/greenlight-config-record';\n\n/* -------------------------------------------------------------------------- */\n/* Field vocabulary                                                            */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The Person fields **this run writes**. Nothing here may ever appear in a\n * `databaseEventTriggerSettings.updatedFields` list \u2014 see guard 1 above.\n *\n * \"Managed\" means written by the scoring run, not merely shipped by Greenlight.\n * The distinction started mattering when the app grew fields it *owns* but never\n * writes: the four `greenlightSuppress*` columns are Greenlight's own schema and\n * are human-maintained input, so two of them are on the trigger list below and\n * none of them belongs here. Adding one of them to this list would make guard 2\n * swallow the very event the feature depends on.\n */\nexport const GREENLIGHT_MANAGED_PERSON_FIELDS: readonly string[] = [\n  'greenlightScore',\n  'greenlightDecision',\n  'greenlightTrace',\n];\n\n/**\n * Fields whose change can move the score, i.e. the ones worth re-scoring for.\n *\n * `updatedAt` is deliberately absent even though the freshness rule reads it:\n * it changes on *every* write, including ours, which would defeat guard 1\n * entirely. Freshness is re-evaluated whenever anything else changes, and a lead\n * that goes stale without being touched is a reporting problem, not an event.\n *\n * `company` and `companyId` are both listed because Twenty reports a relation\n * change under the foreign-key name in some versions and the relation name in\n * others; a name that never fires costs nothing.\n *\n * ## The suppression exception\n *\n * `SUPPRESSION_SCORING_INPUT_FIELDS` appends `greenlightSuppressed` and\n * `greenlightSuppressionReason`. These are the only Greenlight-named fields on\n * this list, and the exception is mandatory rather than convenient: suppression\n * is a *blocking* input to the compliance rule, so a change that does not\n * re-score leaves a person who has just asked us to stop sitting in the queue\n * still marked `PASS`. A block list nobody re-scores against is not a block list.\n *\n * The loop still terminates, and the argument is the same three guards, checked\n * against the new field:\n *\n *   1. **Guard 1 still holds because the two sets are disjoint.** The scoring run\n *      writes exactly `GREENLIGHT_MANAGED_PERSON_FIELDS`, and no name on that\n *      list is on this one. A suppression change wakes the scorer; the scorer's\n *      own write-back does not wake anything, because it touches none of these\n *      names. The cycle would only close if the run wrote a suppression field,\n *      which it never does \u2014 the two block-list actions are the only writers, and\n *      they are human-triggered. `scoring-run.test.ts` asserts both halves.\n *   2. **Guard 2 is unaffected.** `isGreenlightAuthoredWrite` tests membership of\n *      the managed list, not of this one, so an event carrying only suppression\n *      fields is correctly *not* treated as our own write and is scored.\n *   3. **Guard 3 is the backstop and gets stronger, not weaker.** Suppression\n *      moves the compliance rule's outcome, so the first run after a change has a\n *      genuinely different fingerprint and writes; any replay of that same event\n *      fingerprints identically and writes nothing.\n *\n * ## The enrichment exception\n *\n * `greenlightEnrichment` is on this list for the same kind of reason and it is\n * equally mandatory. `resolveFieldMapping` appends\n * `greenlightEnrichment.fields.<key>.parsedValue` to every enrichable key, so an\n * enriched value is a *scoring input*. Leaving it off meant enrichment could\n * only ever benefit a lead on its next unrelated edit \u2014 the enriched industry\n * sat on the record, read by nothing, until somebody happened to change a phone\n * number. That is enrichment being cosmetic, which is the bug this closes.\n *\n * The two sibling columns are deliberately **not** here. `greenlightEnrichedAt`\n * and `greenlightEnrichmentStatus` are written by the same mutation but no rule\n * reads either, so waking the scorer for them would buy a guaranteed-identical\n * fingerprint and nothing else.\n *\n * ### Why this terminates\n *\n * Unlike suppression, this one *does* close a cycle: enrichment's own trigger is\n * `greenlightDecision`, which is a field this run writes. The cycle is real,\n * bounded, and cannot reach a provider more than a fixed number of times.\n *\n *   1. **The common case never starts.** Guard 3 is checked before any write. If\n *      enrichment changed no value the mapping reads \u2014 it accepted nothing, or\n *      the field already had a human value ahead of the enriched path \u2014 the score,\n *      band, decision and every rule verdict are identical, the fingerprint\n *      matches the one stored on the record, and this run writes *nothing*.\n *      `greenlightDecision` never changes, so enrichment is never woken. The\n *      chain is one hop long.\n *   2. **When the score does move**, this run writes `greenlightDecision` and\n *      enrichment wakes. Its gap analysis then finds every enrichable field\n *      either freshly cached (the run that just fired filled it) or inside the\n *      unresolved back-off window (it asked and found nothing), requests nothing,\n *      and returns `no_gaps` **before any provider call and before any write**.\n *      No write means no `greenlightEnrichment` change, which means the scorer is\n *      not woken again. The chain is two hops long and costs one key-value read.\n *   3. **The pathological case still terminates.** Even if a later enrichment run\n *      did find a fresh gap, every hop strictly shrinks the set of unfilled\n *      enrichable fields \u2014 a field that is sourced becomes fresh, and a field that\n *      is not becomes unresolved \u2014 so the chain is bounded by the five entries in\n *      `ENRICHABLE_FIELD_SPECS`, and the monthly spend cap bounds it again from\n *      outside.\n *\n * Guard 2 stays correct throughout: `isGreenlightAuthoredWrite` tests membership\n * of `GREENLIGHT_MANAGED_PERSON_FIELDS`, which this run writes and enrichment\n * does not, so an enrichment write is correctly *not* mistaken for our own and\n * is scored. `__tests__/enrich-score-cycle.test.ts` drives the whole loop against\n * both runs and asserts it settles rather than trusting this comment.\n */\nexport const SCORING_TRIGGER_PERSON_FIELDS: readonly string[] = [\n  'name',\n  'emails',\n  'phones',\n  'jobTitle',\n  'linkedinLink',\n  'company',\n  'companyId',\n  // The enriched-value blob. See \"The enrichment exception\" above \u2014 this is a\n  // scoring input, and the cycle it closes is bounded and proved by test.\n  'greenlightEnrichment',\n  ...SUPPRESSION_SCORING_INPUT_FIELDS,\n];\n\n/** `Person.greenlightDecision` / `GreenlightAuditLog.decision` SELECT values. */\nexport type DecisionValue = 'PASS' | 'GATE' | 'BLOCKED' | 'UNSCORED';\n\n/** `GreenlightAuditLog.eventType` SELECT values used by the scoring path. */\ntype AuditEventType = 'SCORED' | 'GATED' | 'SKIPPED' | 'ERROR';\n\n/* -------------------------------------------------------------------------- */\n/* Guards                                                                      */\n/* -------------------------------------------------------------------------- */\n\n/**\n * True when an update event carries nothing but fields Greenlight itself wrote.\n *\n * An empty or absent `updatedFields` returns false: \"we do not know what\n * changed\" must mean \"score it\", because the alternative is silently skipping a\n * real edit. Over-scoring is harmless \u2014 guard 3 makes the redundant run a no-op.\n */\nexport const isGreenlightAuthoredWrite = (\n  updatedFields: unknown,\n): boolean => {\n  if (!Array.isArray(updatedFields) || updatedFields.length === 0) {\n    return false;\n  }\n\n  return updatedFields.every(\n    (field) =>\n      typeof field === 'string' &&\n      GREENLIGHT_MANAGED_PERSON_FIELDS.includes(field),\n  );\n};\n\n/* -------------------------------------------------------------------------- */\n/* Result -> CRM vocabulary                                                    */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Map the engine's four-valued decision onto the SELECT the schema ships.\n *\n * The mapping is now one-to-one: `greenlightDecision` carries a `BLOCKED` option\n * of its own (see `src/fields/greenlight-decision.field.ts`), so a compliance\n * stop is no longer indistinguishable from a low score. Before that option\n * existed `blocked` collapsed onto `GATE` and every consumer had to\n * reverse-engineer the difference out of `greenlightTrace`.\n *\n * When the gate is switched off, `gated` is downgraded to `PASS` per the\n * `isGateEnabled` field's contract (\"nothing is held for review\"). `blocked` is\n * **not** downgraded: that state only arises from a compliance stop such as an\n * opt-out, and quietly marking an opted-out contact as ready to call is not a\n * behaviour any switch on a settings page should be able to buy.\n */\nexport const toDecisionValue = (\n  decision: GateDecision,\n  gateEnabled: boolean,\n): DecisionValue => {\n  switch (decision) {\n    case 'approved':\n      return 'PASS';\n    case 'gated':\n      return gateEnabled ? 'GATE' : 'PASS';\n    case 'blocked':\n      return 'BLOCKED';\n    default:\n      return 'UNSCORED';\n  }\n};\n\n/** The decision values that mean \"this lead is being held from a rep\". */\nconst HOLDING_DECISIONS: readonly DecisionValue[] = ['GATE', 'BLOCKED'];\n\nconst KNOWN_BAND_VALUES: readonly string[] = [\n  'EXCELLENT',\n  'GOOD',\n  'FAIR',\n  'POOR',\n];\n\n/**\n * Band ids are configurable, so an unrecognised one has to degrade rather than\n * be written into a SELECT that would reject it.\n */\nexport const toBandValue = (result: ScoringResult): string => {\n  const id = result.band?.id;\n\n  if (typeof id !== 'string') {\n    return 'UNSCORED';\n  }\n\n  const value = id.trim().toUpperCase();\n\n  return KNOWN_BAND_VALUES.includes(value) ? value : 'UNSCORED';\n};\n\nexport const toAuditEventType = (\n  decision: DecisionValue,\n  result: ScoringResult,\n): AuditEventType => {\n  if (result.decision === 'unscored') {\n    return 'SKIPPED';\n  }\n\n  // GATED covers both holding states. The audit log's `eventType` vocabulary is\n  // about what happened to the lead \u2014 it was held \u2014 and the row's `decision`\n  // column already says which of the two states it was held in.\n  return HOLDING_DECISIONS.includes(decision) ? 'GATED' : 'SCORED';\n};\n\n/* -------------------------------------------------------------------------- */\n/* Fingerprint + trace id                                                      */\n/* -------------------------------------------------------------------------- */\n\n/** FNV-1a, 32-bit. Short, dependency-free, and stable across Node versions. */\nconst hash32 = (input: string): string => {\n  let hash = 0x811c9dc5;\n\n  for (let index = 0; index < input.length; index += 1) {\n    hash ^= input.charCodeAt(index);\n    hash = Math.imul(hash, 0x01000193) >>> 0;\n  }\n\n  return hash.toString(16).padStart(8, '0');\n};\n\n/**\n * A stable digest of everything about a run that a human would call \"the\n * outcome\": the score, the decision, the band, and the verdict of every rule.\n *\n * Explicitly excludes `scoredAt`, the trace prose and the degradation list, so\n * re-scoring an untouched lead a day later produces the same fingerprint and\n * writes nothing.\n */\nexport const fingerprintOutcome = (\n  result: ScoringResult,\n  decisionValue: string,\n): string => {\n  const parts = [\n    result.engineVersion,\n    decisionValue,\n    result.score === null ? 'null' : result.score.toFixed(1),\n    result.band?.id ?? 'none',\n    result.gateThreshold.toString(),\n    ...result.trace.map(\n      (entry) => `${entry.ruleId}:${entry.outcome}:${entry.credit.toFixed(3)}`,\n    ),\n  ];\n\n  return hash32(parts.join('|'));\n};\n\n/**\n * Deterministic per (lead, outcome). A retry that re-writes the same decision\n * reuses the id, so duplicate audit rows are recognisable as one run rather than\n * looking like the gate flip-flopped.\n */\nexport const buildTraceId = (\n  leadRecordId: string,\n  fingerprint: string,\n): string => `gl-${hash32(leadRecordId)}-${fingerprint}`;\n\n/** The fingerprint already stored on the Person, if any. */\nexport const readStoredFingerprint = (trace: unknown): string | null => {\n  if (!isPlainRecord(trace)) {\n    return null;\n  }\n\n  const value = trace['fingerprint'];\n\n  return typeof value === 'string' && value.length > 0 ? value : null;\n};\n\n/* -------------------------------------------------------------------------- */\n/* Display helpers                                                             */\n/* -------------------------------------------------------------------------- */\n\n/** Person.name is a FULL_NAME composite. Fall back to email, then to the id. */\nexport const readLeadDisplayName = (person: unknown): string => {\n  if (!isPlainRecord(person)) {\n    return 'Unknown lead';\n  }\n\n  const name = person['name'];\n\n  if (isPlainRecord(name)) {\n    const parts = [name['firstName'], name['lastName']]\n      .filter((part): part is string => typeof part === 'string')\n      .map((part) => part.trim())\n      .filter((part) => part.length > 0);\n\n    if (parts.length > 0) {\n      return parts.join(' ');\n    }\n  }\n\n  if (typeof name === 'string' && name.trim().length > 0) {\n    return name.trim();\n  }\n\n  const emails = person['emails'];\n\n  if (isPlainRecord(emails)) {\n    const primary = emails['primaryEmail'];\n\n    if (typeof primary === 'string' && primary.trim().length > 0) {\n      return primary.trim();\n    }\n  }\n\n  const id = person['id'];\n\n  return typeof id === 'string' ? id : 'Unknown lead';\n};\n\n/* -------------------------------------------------------------------------- */\n/* The run                                                                     */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The slice of app key-value storage this run needs, as a port.\n *\n * `kv` is only importable from `twenty-sdk/logic-function`, and this module is\n * deliberately SDK-free so it can be driven by a fake in tests \u2014 the same reason\n * `GreenlightApiClient` exists. `src/logic-functions/release-marker-store.ts`\n * holds the one real implementation.\n *\n * It is a *required* input rather than an optional one on purpose. A run without\n * a store silently re-gates every released lead, which is exactly the bug this\n * port exists to close; making it required means the compiler, not a reviewer,\n * catches a registration that forgot to wire it.\n */\nexport interface ReleaseMarkerStore {\n  get(key: string): Promise<ReleaseMarker | null>;\n  delete(key: string): Promise<void>;\n}\n\n/**\n * A config record the caller has already read, so the run does not read it again.\n *\n * Wrapped in an object rather than passed as `Record | null` because `null` is a\n * *legitimate answer* here \u2014 it means \"this workspace has no config record, score\n * on the engine's defaults\" \u2014 and would otherwise be indistinguishable from \"the\n * caller did not supply one\". The wrapper makes absent and empty different types\n * instead of different values.\n *\n * It exists for the backfill (`src/logic-functions/backfill-run.ts`), which scores\n * thirty records per cron tick against one config that cannot change between\n * them. Reading it per record would treble the run's share of the rate limit \u2014\n * 30 requests a minute buying thirty identical answers \u2014 and would be the single\n * largest cost in a bulk pass.\n *\n * Optional on purpose: the two event registrations pass nothing and keep the\n * exact behaviour they had, which is what makes this an additive change rather\n * than a rewrite of the hot path.\n */\nexport interface PreloadedConfig {\n  readonly record: Record<string, unknown> | null;\n}\n\nexport interface ScoringRunInput {\n  readonly client: GreenlightApiClient;\n  /** The `DatabaseEventPayload` as delivered, untyped on purpose. */\n  readonly event: unknown;\n  readonly now: Date;\n  readonly markers: ReleaseMarkerStore;\n  /** Omitted by the event registrations; supplied by the bulk backfill. */\n  readonly config?: PreloadedConfig;\n  /**\n   * Read-only access to the licence-delivered calibration cache.\n   *\n   * Optional, and its absence means \"shipped defaults\" \u2014 the same outcome as an\n   * unlicensed workspace, an expired cache, or a payload whose signature did not\n   * verify. That is what makes this additive: every existing caller that does\n   * not pass it keeps its exact previous behaviour, and the compiler does not\n   * have to be trusted on the point because there is nothing to break.\n   */\n  readonly calibration?: CalibrationReaderPort | null;\n  /**\n   * Checks that the cached calibration belongs to this workspace's licence key.\n   *\n   * Defaults to `NO_SEAL`, which accepts nothing \u2014 so a caller that wires the\n   * reader but forgets the seal gets shipped defaults rather than an unchecked\n   * cache. See `src/calibration/seal.ts` for what the seal does and does not\n   * prove.\n   */\n  readonly calibrationSeal?: CalibrationSealPort;\n}\n\nexport type ScoringRunOutcome =\n  | { status: 'skipped'; reason: string }\n  | { status: 'unchanged'; leadRecordId: string; fingerprint: string }\n  | {\n      status: 'scored';\n      leadRecordId: string;\n      traceId: string;\n      decision: string;\n      score: number | null;\n    }\n  | { status: 'failed'; leadRecordId: string | null; error: string };\n\nconst readEventRecord = (event: unknown): Record<string, unknown> | null => {\n  if (!isPlainRecord(event)) {\n    return null;\n  }\n\n  const properties = event['properties'];\n\n  if (!isPlainRecord(properties)) {\n    return null;\n  }\n\n  const after = properties['after'];\n\n  return isPlainRecord(after) ? after : null;\n};\n\nconst readEventUpdatedFields = (event: unknown): unknown => {\n  if (!isPlainRecord(event)) {\n    return undefined;\n  }\n\n  const properties = event['properties'];\n\n  return isPlainRecord(properties) ? properties['updatedFields'] : undefined;\n};\n\nconst readRecordId = (\n  event: unknown,\n  record: Record<string, unknown> | null,\n): string | null => {\n  if (isPlainRecord(event) && typeof event['recordId'] === 'string') {\n    return event['recordId'];\n  }\n\n  if (record !== null && typeof record['id'] === 'string') {\n    return record['id'];\n  }\n\n  return null;\n};\n\n/** Load the single config record. Absent or unreadable yields `null`. */\nexport const loadConfigRecord = async (\n  client: GreenlightApiClient,\n): Promise<Record<string, unknown> | null> => {\n  const response = await client.query({\n    greenlightConfigs: { edges: { node: greenlightConfigSelection() } },\n  });\n\n  return oldestByCreatedAt(readConnectionNodes(response, 'greenlightConfigs'));\n};\n\n/**\n * Best-effort company hydration.\n *\n * `event.properties.after` carries the Person's own columns and the company\n * foreign key, but not the company's name \u2014 and \"which company does this lead\n * work for\" is a rule worth 10 points. One extra read buys it. If the read\n * fails for any reason the lead is scored without it, because a lead scored\n * slightly low is recoverable and a lead not scored at all is not.\n */\nconst hydrateCompany = async (\n  client: GreenlightApiClient,\n  person: Record<string, unknown>,\n): Promise<Record<string, unknown>> => {\n  const companyId = person['companyId'];\n\n  if (typeof companyId !== 'string' || companyId.length === 0) {\n    return person;\n  }\n\n  if (isPlainRecord(person['company'])) {\n    return person;\n  }\n\n  try {\n    const response = await client.query({\n      companies: {\n        __args: { filter: { id: { eq: companyId } } },\n        edges: { node: { id: true, name: true } },\n      },\n    });\n\n    const [company] = readConnectionNodes(response, 'companies');\n\n    return company === undefined ? person : { ...person, company };\n  } catch (error) {\n    logGreenlight('company_hydration_failed', {\n      companyId,\n      error: describeError(error),\n    });\n\n    return person;\n  }\n};\n\n/* -------------------------------------------------------------------------- */\n/* The human override                                                          */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The object name the release marker is keyed on.\n *\n * Hard-coded rather than taken from the config's `leadObjectNameSingular`\n * because the override can only ever write a `person` key \u2014\n * `SUPPORTED_LEAD_OBJECTS` in `release-decision.ts` has one entry, and both\n * scoring registrations are bound to `person.*` events. Deriving the key from\n * configuration would mean a workspace that renamed its lead object silently\n * looked up a key the release side never writes, which is the same silent\n * self-undoing override this whole mechanism exists to prevent.\n */\nconst RELEASE_MARKER_OBJECT = 'person';\n\n/**\n * A compliance stop, using the exact discriminator `src/scoring/engine.ts` uses\n * to choose `blocked` \u2014 checked from the trace as well as the decision so a\n * blocking failure is still recognised if the engine ever reports the two\n * inconsistently.\n */\nconst hasBlockingFailure = (result: ScoringResult): boolean =>\n  result.decision === 'blocked' ||\n  result.trace.some(\n    (entry) => entry.severity === 'blocking' && entry.outcome === 'fail',\n  );\n\ninterface ReleasePin {\n  /** The decision to actually write. */\n  readonly decision: DecisionValue;\n  /** Non-null only when a marker was honoured and the decision was pinned. */\n  readonly honoured: ReleaseMarker | null;\n}\n\n/**\n * Honour a human release when re-scoring would otherwise hold the lead again.\n *\n * The contract with `release-lead.logic-function.ts`: if a release marker\n * exists, the score and the trace still update freely, but `greenlightDecision`\n * stays at `PASS`. Three things are worth being explicit about.\n *\n * **A newly blocking compliance failure outranks the release.** The marker is\n * deleted and the lead is re-held. \"Newly\" needs no timestamp comparison: the\n * override refuses to release a compliance-blocked lead at all\n * (`ALLOW_COMPLIANCE_OVERRIDE === false`) and writes no marker when it refuses,\n * so any marker that exists was written for a lead that was *not* blocked at\n * release time. A blocking failure seen now is therefore newer by construction.\n *\n * **A failed key-value read re-gates rather than releases.** Falling back to\n * \"no marker\" means a released lead can be wrongly held for a moment \u2014 an\n * annoyance a human fixes by releasing it again, with the whole trail recording\n * what happened. The opposite fallback, treating an unreadable store as \"a\n * marker probably exists\", would let a storage outage quietly release leads the\n * gate is holding, including leads held on compliance grounds. That is not\n * recoverable by a human, because nobody knows it happened. The failure is\n * logged rather than swallowed, under `release_marker_read_failed`.\n *\n * **A failed delete is self-correcting.** If the marker survives a re-gating\n * compliance failure, the *next* run re-evaluates the same blocking rule and\n * re-holds the lead again \u2014 a stale marker only ever wins once the compliance\n * failure has genuinely gone away, which is the state a release should win in.\n */\nconst pinReleasedDecision = async (\n  markers: ReleaseMarkerStore,\n  leadRecordId: string,\n  decisionValue: DecisionValue,\n  result: ScoringResult,\n): Promise<ReleasePin> => {\n  if (!HOLDING_DECISIONS.includes(decisionValue)) {\n    // Nothing is being held, so there is nothing to pin and no reason to spend\n    // a read. A marker left behind by an earlier release is harmless: it is only\n    // ever consulted on a run that would hold the lead.\n    return { decision: decisionValue, honoured: null };\n  }\n\n  const key = releaseMarkerKey(RELEASE_MARKER_OBJECT, leadRecordId);\n\n  let marker: ReleaseMarker | null = null;\n\n  try {\n    marker = await markers.get(key);\n  } catch (error) {\n    logGreenlight('release_marker_read_failed', {\n      leadRecordId,\n      decision: decisionValue,\n      error: describeError(error),\n    });\n\n    return { decision: decisionValue, honoured: null };\n  }\n\n  if (marker === null) {\n    return { decision: decisionValue, honoured: null };\n  }\n\n  if (hasBlockingFailure(result)) {\n    try {\n      await markers.delete(key);\n    } catch (error) {\n      logGreenlight('release_marker_delete_failed', {\n        leadRecordId,\n        error: describeError(error),\n      });\n    }\n\n    logGreenlight('release_marker_overruled', {\n      leadRecordId,\n      decision: decisionValue,\n      releasedAt: marker.releasedAt,\n    });\n\n    return { decision: decisionValue, honoured: null };\n  }\n\n  return { decision: 'PASS', honoured: marker };\n};\n\nconst buildTracePayload = (\n  result: ScoringResult,\n  traceId: string,\n  fingerprint: string,\n  decisionValue: string,\n  bandValue: string,\n  releaseOverride: Record<string, unknown> | null,\n  /**\n   * Which calibration produced this score, or `null` for shipped defaults.\n   *\n   * Recorded on the lead itself rather than only in the admin panel, because\n   * \"why did this lead score 68 last week and 82 today\" is a question asked\n   * about one record months later, and the panel only ever shows *now*. It is\n   * the version number and nothing else \u2014 the payload is a few hundred\n   * kilobytes of word lists and does not belong on every Person row.\n   */\n  calibrationVersion: number | null,\n): Record<string, unknown> => ({\n  calibration:\n    calibrationVersion === null\n      ? { source: 'shipped_defaults' }\n      : { source: 'licensed', version: calibrationVersion },\n  traceId,\n  fingerprint,\n  engineVersion: result.engineVersion,\n  scoredAt: result.scoredAt,\n  score: result.score,\n  band: bandValue,\n  decision: decisionValue,\n  gateThreshold: result.gateThreshold,\n  summary: result.summary,\n  reasons: [...result.reasons],\n  configSource: result.configSource,\n  degradations: result.degradations.map((entry) => ({ ...entry })),\n  rules: result.trace.map((entry) => ({ ...entry })),\n  // Present only on a pinned run. Without it the trace would show a lead\n  // scoring 12 against a threshold of 40 sitting at PASS with nothing on the\n  // record explaining why, which is the same silence the marker exists to fix.\n  ...(releaseOverride === null ? {} : { releaseOverride }),\n});\n\n/**\n * Score one Person event end to end.\n *\n * Never throws. Every failure path returns a `failed` outcome after making a\n * best-effort attempt to leave the lead flagged `UNSCORED` with an ERROR audit\n * row \u2014 ARCHITECTURE.md's golden rule is that a lead is never blocked from\n * reaching a rep, so a broken run must leave a visible, explained record rather\n * than a silent gap.\n */\n/**\n * Read the cached calibration and reduce it to the vocabulary baseline the\n * engine accepts.\n *\n * Never throws and never blocks: a store that errors, a cache that is absent\n * (which is also what a lapsed entitlement leaves behind \u2014 the nightly run\n * clears it), and a payload that has aged past 72 hours all return `null`,\n * which resolves to the in-source defaults. There is no branch here that can\n * stop a lead being scored, and there is deliberately no way to write one \u2014 the\n * port has no method but `read`.\n *\n * No licence state is consulted. That is not an omission: see\n * `src/calibration/state.ts`, \"Why there is no entitlement check here\".\n */\ninterface AppliedCalibration {\n  readonly baseline: ReturnType<typeof toScoringBaseline>;\n  readonly version: number;\n}\n\n/**\n * Return the entry only if its seal verifies under this workspace's licence key.\n *\n * A failure is logged once and reported as `null`, i.e. as \"no calibration\",\n * which is the shipped-defaults path. It is deliberately not an error: a cache\n * that fails its seal is most often a licence key that was changed, and a rep\n * scoring a lead is not the person to tell about it. The nightly run rewrites a\n * correctly-sealed entry and the condition clears itself.\n */\nconst verifySeal = async (\n  stored: Awaited<ReturnType<CalibrationReaderPort['read']>>,\n  seal: CalibrationSealPort,\n): Promise<typeof stored> => {\n  if (stored === null) {\n    return null;\n  }\n\n  const canonical = sealedCalibrationBody(stored);\n\n  if (canonical === null) {\n    return null;\n  }\n\n  const sealed = await seal.verify(canonical, stored.sealTag);\n\n  if (!sealed) {\n    logGreenlight('calibration_cache_seal_mismatch', {\n      calibrationVersion: stored.calibration.calibrationVersion,\n      keyId: stored.keyId,\n    });\n\n    return null;\n  }\n\n  return stored;\n};\n\nconst readCalibrationBaseline = async (\n  calibration: CalibrationReaderPort | null | undefined,\n  seal: CalibrationSealPort,\n  now: Date,\n): Promise<AppliedCalibration | null> => {\n  if (calibration === null || calibration === undefined) {\n    return null;\n  }\n\n  try {\n    const stored = await calibration.read();\n\n    // The seal is checked before the freshness ladder, not after. A cache\n    // lifted from another workspace should be refused on the grounds that it is\n    // not ours, whatever its age \u2014 and checking age first would mean a stolen\n    // cache and an expired one produce the same reason in the log, which is the\n    // one distinction worth keeping here.\n    const cached = await verifySeal(stored, seal);\n\n    const resolved = resolveCalibration({ cached, now });\n\n    return resolved.calibration === null\n      ? null\n      : {\n          baseline: toScoringBaseline(resolved.calibration),\n          version: resolved.calibration.calibrationVersion,\n        };\n  } catch (error) {\n    logGreenlight('calibration_read_failed', { error: describeError(error) });\n\n    return null;\n  }\n};\n\nexport const runPersonScoring = async ({\n  client,\n  event,\n  now,\n  markers,\n  config,\n  calibration,\n  calibrationSeal = NO_SEAL,\n}: ScoringRunInput): Promise<ScoringRunOutcome> => {\n  const person = readEventRecord(event);\n  const leadRecordId = readRecordId(event, person);\n\n  // Guard 2. Runs before anything else so a Greenlight-authored write costs one\n  // array scan, not two API round trips.\n  if (isGreenlightAuthoredWrite(readEventUpdatedFields(event))) {\n    return { status: 'skipped', reason: 'greenlight_authored_write' };\n  }\n\n  if (person === null || leadRecordId === null) {\n    logGreenlight('event_unreadable', { leadRecordId });\n\n    return { status: 'skipped', reason: 'event_unreadable' };\n  }\n\n  try {\n    // A supplied config is used as-is, including a supplied `null`. Only an\n    // absent wrapper triggers the read \u2014 see `PreloadedConfig`.\n    const configRecord =\n      config === undefined ? await loadConfigRecord(client) : config.record;\n    const gateEnabled = isGateEnabled(configRecord);\n    const leadObjectNameSingular = readLeadObjectNameSingular(configRecord);\n\n    const hydrated = await hydrateCompany(client, person);\n\n    const applied = await readCalibrationBaseline(\n      calibration,\n      calibrationSeal,\n      now,\n    );\n\n    // `scoreLead` never throws and repairs anything unusable in the config, so\n    // a missing record needs no special case here \u2014 it becomes `configSource:\n    // 'defaults'` and a degradation entry in the trace. A `null` baseline is\n    // likewise not a special case: it is what the parameter means when there is\n    // no calibration, and it scores identically to the build that predates it.\n    const result = scoreLead({\n      lead: toLeadRecord(hydrated),\n      now,\n      config: toScoringConfigInput(configRecord),\n      baseline: applied?.baseline ?? null,\n    });\n\n    const engineDecision = toDecisionValue(result.decision, gateEnabled);\n    const pin = await pinReleasedDecision(\n      markers,\n      leadRecordId,\n      engineDecision,\n      result,\n    );\n    const decisionValue = pin.decision;\n    const bandValue = toBandValue(result);\n\n    // The fingerprint is taken over the decision actually written, not the one\n    // the engine proposed. That keeps guard 3 honest in both directions: the\n    // first pinned run differs from the stored GATE fingerprint and writes, and\n    // every identical pinned run afterwards matches and writes nothing. A\n    // fingerprint over `engineDecision` would record a decision the record does\n    // not carry.\n    const fingerprint = fingerprintOutcome(result, decisionValue);\n\n    // Guard 3.\n    if (readStoredFingerprint(person['greenlightTrace']) === fingerprint) {\n      return { status: 'unchanged', leadRecordId, fingerprint };\n    }\n\n    const traceId = buildTraceId(leadRecordId, fingerprint);\n    const leadDisplayName = readLeadDisplayName(person);\n    const eventType = toAuditEventType(decisionValue, result);\n    const tracePayload = buildTracePayload(\n      result,\n      traceId,\n      fingerprint,\n      decisionValue,\n      bandValue,\n      pin.honoured === null\n        ? null\n        : {\n            engineDecision,\n            releasedAt: pin.honoured.releasedAt,\n            releasedBy: pin.honoured.releasedBy,\n            reasonCode: pin.honoured.reasonCode,\n          },\n      applied?.version ?? null,\n    );\n    // A pinned run is still a SYSTEM action \u2014 the system honouring a human\n    // decision taken earlier \u2014 so `actorType` stays SYSTEM and the human's\n    // fingerprints go in the reason, where the audit log already looks for them.\n    const overrideReason =\n      pin.honoured === null\n        ? ''\n        : `RELEASE_MARKER_HONOURED \u2014 engine decision ${engineDecision} held at PASS by the release recorded ${pin.honoured.releasedAt} (${pin.honoured.releasedBy}, ${pin.honoured.reasonCode})`;\n\n    // Audit row first, Person second \u2014 and the order is load-bearing.\n    //\n    // If the audit write succeeds and the Person write fails, the next event\n    // re-scores, finds the stored fingerprint still absent, and writes both\n    // again: the trail over-records. If the order were reversed, the Person\n    // would carry the new fingerprint, guard 3 would suppress every subsequent\n    // run, and the decision would never be recorded at all. An append-only\n    // trail is allowed to repeat itself; it is not allowed to lose an entry.\n    await client.mutation({\n      createGreenlightAuditLog: {\n        __args: {\n          data: {\n            name: `${eventType} \u00B7 ${leadDisplayName} \u00B7 ${result.score ?? '\u2014'}`,\n            eventType,\n            occurredAt: now.toISOString(),\n            leadObjectNameSingular,\n            leadRecordId,\n            leadDisplayName,\n            actorType: 'SYSTEM',\n            actorDisplayName: `Greenlight engine ${SCORING_ENGINE_VERSION}`,\n            ruleTrace: tracePayload,\n            score: result.score,\n            band: bandValue,\n            decision: decisionValue,\n            overrideReason,\n            traceId,\n          },\n        },\n        id: true,\n      },\n    });\n\n    await client.mutation({\n      updatePerson: {\n        __args: {\n          id: leadRecordId,\n          data: {\n            greenlightScore: result.score,\n            greenlightDecision: decisionValue,\n            greenlightTrace: tracePayload,\n          },\n        },\n        id: true,\n      },\n    });\n\n    logGreenlight('lead_scored', {\n      leadRecordId,\n      traceId,\n      score: result.score,\n      band: bandValue,\n      decision: decisionValue,\n      engineDecision,\n      releasePinned: pin.honoured !== null,\n      configSource: result.configSource,\n      calibrationVersion: applied?.version ?? null,\n      degradations: result.degradations.length,\n    });\n\n    return {\n      status: 'scored',\n      leadRecordId,\n      traceId,\n      decision: decisionValue,\n      score: result.score,\n    };\n  } catch (error) {\n    const message = describeError(error);\n\n    logGreenlight('scoring_failed', { leadRecordId, error: message });\n\n    await flagUnscored(client, leadRecordId, person, now, message);\n\n    return { status: 'failed', leadRecordId, error: message };\n  }\n};\n\n/**\n * Last-ditch fail-open write. Each half is independently guarded: a lead left\n * with no flag is bad, a logic function that throws out to the platform because\n * its error handler also failed is worse.\n */\nconst flagUnscored = async (\n  client: GreenlightApiClient,\n  leadRecordId: string,\n  person: Record<string, unknown>,\n  now: Date,\n  error: string,\n): Promise<void> => {\n  const traceId = buildTraceId(leadRecordId, hash32(error));\n  const leadDisplayName = readLeadDisplayName(person);\n\n  const tracePayload = {\n    traceId,\n    // No fingerprint: an errored run must not suppress the next attempt.\n    engineVersion: SCORING_ENGINE_VERSION,\n    scoredAt: now.toISOString(),\n    score: null,\n    band: 'UNSCORED',\n    decision: 'UNSCORED',\n    summary:\n      'Greenlight could not score this lead. It has been passed through unscored and flagged for review.',\n    reasons: [error],\n    rules: [],\n  };\n\n  try {\n    await client.mutation({\n      createGreenlightAuditLog: {\n        __args: {\n          data: {\n            name: `ERROR \u00B7 ${leadDisplayName} \u00B7 \u2014`,\n            eventType: 'ERROR',\n            occurredAt: now.toISOString(),\n            leadObjectNameSingular: 'person',\n            leadRecordId,\n            leadDisplayName,\n            actorType: 'SYSTEM',\n            actorDisplayName: `Greenlight engine ${SCORING_ENGINE_VERSION}`,\n            ruleTrace: tracePayload,\n            score: null,\n            band: 'UNSCORED',\n            decision: 'UNSCORED',\n            overrideReason: '',\n            traceId,\n          },\n        },\n        id: true,\n      },\n    });\n  } catch (auditError) {\n    logGreenlight('fail_open_audit_write_failed', {\n      leadRecordId,\n      error: describeError(auditError),\n    });\n  }\n\n  try {\n    await client.mutation({\n      updatePerson: {\n        __args: {\n          id: leadRecordId,\n          data: {\n            greenlightDecision: 'UNSCORED',\n            greenlightTrace: tracePayload,\n          },\n        },\n        id: true,\n      },\n    });\n  } catch (updateError) {\n    logGreenlight('fail_open_person_write_failed', {\n      leadRecordId,\n      error: describeError(updateError),\n    });\n  }\n};\n", "/**\n * The I/O half of the backfill, kept out of the `define*` files so the whole\n * resume-after-crash story can be exercised against a fake API client and a fake\n * key-value store.\n *\n * Everything about *what* to read is in `src/backfill/backfill-plan.ts` and\n * everything about *where the walk has got to* is in\n * `src/backfill/backfill-state.ts`, both pure. This module reads state, fetches a\n * page, calls the existing scoring run once per record, and writes the position\n * back. The only impure things it touches are the client, the store and the `now`\n * it is handed \u2014 plus one elapsed-time reading, injected, so a test can make the\n * tick run out of budget without waiting forty seconds.\n *\n * ## Why a cron rather than the alternatives\n *\n * **Not inline in the post-install hook.** A 10,000-contact workspace at 100\n * requests a minute needs hours. The hook would time out, and the platform would\n * retry it \u2014 from the beginning, three times, each attempt re-walking whatever\n * the last one managed. That is not a slow backfill, it is a backfill that can\n * never finish and that spends its whole life re-scoring the first few hundred\n * records.\n *\n * **Not a self-enqueueing chain** (`enqueueJob` with a delay, each chunk booking\n * the next). Tempting, and it would pace itself precisely. But the chain has\n * exactly one thread of liveness: if any single link fails to enqueue \u2014 a\n * transient error in the one call that is not retried, because it *is* the retry\n * \u2014 the run stops silently and forever, and nothing in the system is left to\n * notice. A backfill that can die quietly is the same class of defect as a\n * scoring trigger that never fires on existing records. Fixing invisibility with\n * something else invisible is not a fix.\n *\n * **A cron draining a durable cursor.** The schedule is owned by the platform,\n * not by the job, so liveness is supplied from outside: every minute, something\n * asks \"is there work?\" whatever happened last time. A crashed tick costs one\n * lease and nothing else. And because progress lives in storage rather than in an\n * in-flight process, \"is a backfill running and how far has it got\" is a question\n * that can be *answered* \u2014 by the cron, by the admin panel, and by the next tick\n * \u2014 instead of inferred from the absence of a log line.\n *\n * The cost is that the cron ticks forever on workspaces with no backfill running.\n * That tick is one key-value read which returns null. It is the cheapest possible\n * price for the property that matters.\n *\n * ## Never throwing\n *\n * No path here throws out to the platform. A tick that fails releases its lease,\n * records the reason where the panel can show it, and returns; the next tick\n * tries again. A *record* that fails is logged and skipped \u2014 `runPersonScoring`\n * has already flagged it `UNSCORED` with an ERROR audit row on the way out, which\n * is ARCHITECTURE.md's golden rule doing its job. One bad record must not stall\n * the queue, and in `unscored` mode it cannot even be retried into a loop: the\n * fail-open write moves it off `greenlightDecision is NULL`, so it leaves the\n * queue by being flagged rather than by being fixed.\n */\n\nimport {\n  advanceCursor,\n  BACKFILL_CHUNK_SIZE,\n  BACKFILL_TICK_BUDGET_MS,\n  buildCountRequest,\n  buildPageRequest,\n  backfillPersonSelection,\n  configuredSelectionFields,\n  isAlreadyHandled,\n  PAGE_ATTEMPTS,\n  pageSizeFor,\n  readTotalCount,\n  toScoringEvent,\n} from 'src/backfill/backfill-plan';\nimport {\n  buildFieldExistenceProbe,\n  classifyProbeRejection,\n  describeFieldMappingFindings,\n  EMPTY_FIELD_MAPPING_CHECK,\n  fieldMappingAuditDetail,\n  fieldMappingAuditName,\n  FIELD_MAPPING_PROBE_LIMIT,\n  mappedFieldNames,\n  probeAnswered,\n  type FieldMappingCheckResult,\n  type FieldProbeVerdict,\n  type MappingFinding,\n} from 'src/backfill/field-mapping-check';\nimport {\n  beginBackfill,\n  cancelBackfill as cancelBackfillState,\n  claimLease,\n  commitChunk,\n  describeBackfill,\n  EMPTY_TALLY,\n  isLeaseHeld,\n  releaseLease,\n  toBackfillState,\n  type BackfillCursor,\n  type BackfillMode,\n  type BackfillProgress,\n  type BackfillState,\n  type BackfillTally,\n} from 'src/backfill/backfill-state';\nimport {\n  describeError,\n  logGreenlight,\n  readConnectionNodes,\n  type GreenlightApiClient,\n} from 'src/logic-functions/greenlight-api';\nimport {\n  type CalibrationReaderPort,\n  type CalibrationSealPort,\n} from 'src/calibration';\nimport { readLeadObjectNameSingular } from 'src/logic-functions/greenlight-config-record';\nimport {\n  loadConfigRecord,\n  runPersonScoring,\n  type ReleaseMarkerStore,\n} from 'src/logic-functions/scoring-run';\nimport { SCORING_ENGINE_VERSION } from 'src/scoring';\n\n/* -------------------------------------------------------------------------- */\n/* Ports                                                                       */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The slice of app key-value storage the backfill needs, as a port \u2014 same shape\n * and same justification as `ReleaseMarkerStore` in `scoring-run.ts`.\n * `src/logic-functions/backfill-state-store.ts` holds the one real implementation.\n */\nexport interface BackfillStateStore {\n  read(): Promise<unknown>;\n  write(state: BackfillState): Promise<void>;\n}\n\nexport interface BackfillDeps {\n  readonly client: GreenlightApiClient;\n  readonly store: BackfillStateStore;\n  readonly now: Date;\n  /**\n   * Elapsed-time source for the tick budget, injected so a test can expire the\n   * budget instantly. `now` cannot serve: it is a fixed instant on purpose, and\n   * the whole point of the budget is that real time passes during a chunk.\n   */\n  readonly clock?: () => number;\n}\n\nexport interface BackfillChunkDeps extends BackfillDeps {\n  readonly markers: ReleaseMarkerStore;\n  /**\n   * Read-only calibration cache, passed straight through to each record's\n   * scoring run so a bulk pass calibrates identically to a live event. Omitted\n   * means shipped defaults, which is what an unlicensed backfill has always\n   * produced.\n   */\n  readonly calibration?: CalibrationReaderPort | null;\n  /** Checks the cached calibration belongs to this workspace's licence key. */\n  readonly calibrationSeal?: CalibrationSealPort;\n  /** Overridden only by tests; production uses the rate-limit-derived default. */\n  readonly chunkSize?: number;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Shared helpers                                                              */\n/* -------------------------------------------------------------------------- */\n\nconst readState = async (\n  store: BackfillStateStore,\n): Promise<{ ok: true; state: BackfillState | null } | { ok: false; error: string }> => {\n  try {\n    return { ok: true, state: toBackfillState(await store.read()) };\n  } catch (error) {\n    return { ok: false, error: describeError(error) };\n  }\n};\n\n/** Best effort. A count drives a progress bar; it must never stop a run. */\nconst countMatching = async (\n  client: GreenlightApiClient,\n  mode: BackfillMode,\n): Promise<number | null> => {\n  try {\n    return readTotalCount(await client.query(buildCountRequest(mode)));\n  } catch (error) {\n    logGreenlight('backfill_count_failed', { mode, error: describeError(error) });\n\n    return null;\n  }\n};\n\n/**\n * A run-level row in the audit log, so \"was this workspace ever backfilled, and\n * when\" survives the key-value blob and is answerable from inside the CRM.\n *\n * Filed under `BACKFILL`, which the object's `eventType` SELECT now carries. It\n * used to borrow `CONFIG_CHANGED` \u2014 the precedent `install-run.ts` set for a\n * lifecycle event that is not about one lead \u2014 because appending a SELECT option\n * belonged to another workstream. It does not any more: a backfill is not a\n * configuration change, and an admin filtering for \"what did this workspace do\n * in bulk\" should not have to read row names to find out.\n *\n * Existing `CONFIG_CHANGED` rows are left exactly as written. Option identity in\n * Twenty derives from the option `value`, so appending disturbed nothing, and a\n * row that recorded what it recorded at the time is not something to rewrite.\n *\n * Swallows every failure, like its counterpart in `install-run.ts`: a missing\n * provenance row is not worth failing a backfill over.\n *\n * `eventType` is a parameter with a default rather than a constant because this\n * module files rows of two kinds. A backfill starting, progressing and finishing\n * is `BACKFILL`. A configured field name that does not exist is not a backfill\n * event at all \u2014 it is a fault an admin has to act on, and an admin filtering the\n * audit log for faults filters for `ERROR`. Making it share `BACKFILL` would bury\n * the one row in this app that says \"your scores are wrong\" underneath the\n * routine ones that say \"bulk scoring happened\", which is the same argument the\n * object's own `eventType` comment makes about SUPPRESSED and UNSUPPRESSED.\n */\nconst writeBackfillAuditRow = async (\n  client: GreenlightApiClient,\n  now: Date,\n  summary: string,\n  detail: Record<string, unknown>,\n  eventType: 'BACKFILL' | 'ERROR' = 'BACKFILL',\n): Promise<void> => {\n  try {\n    await client.mutation({\n      createGreenlightAuditLog: {\n        __args: {\n          data: {\n            name: summary,\n            eventType,\n            occurredAt: now.toISOString(),\n            leadObjectNameSingular: '',\n            leadRecordId: null,\n            leadDisplayName: '',\n            actorType: 'SYSTEM',\n            actorDisplayName: `Greenlight backfill ${SCORING_ENGINE_VERSION}`,\n            ruleTrace: detail,\n            score: null,\n            band: 'UNSCORED',\n            decision: 'UNSCORED',\n            overrideReason: '',\n            traceId: String(detail['runId'] ?? ''),\n          },\n        },\n        id: true,\n      },\n    });\n  } catch (error) {\n    logGreenlight('backfill_audit_write_failed', {\n      summary,\n      error: describeError(error),\n    });\n  }\n};\n\n/* -------------------------------------------------------------------------- */\n/* Does the configured field mapping name fields that exist?                   */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The connection the backfill actually walks, and what to call it on screen.\n *\n * Hard-coded rather than derived from `leadObjectNameSingular`, and deliberately:\n * `buildPageRequest` walks `people` and nothing else, and v0.1 writes score,\n * decision and trace fields to Person only. Probing whichever object the SELECT\n * happens to name would ask about a schema this run is not going to read, and\n * would tell an admin their mapping is fine for records that are not being\n * scored at all. The configured object name still reaches the audit row, where\n * \"which object did it mean\" is a question worth being able to answer.\n */\nconst LEAD_CONNECTION = 'people';\nconst LEAD_OBJECT_LABEL = 'Person';\n\n/**\n * One name, one question, in the half of a query Twenty validates.\n *\n * A response that came back but carries no connection is `inconclusive`, not\n * `absent`: see `probeAnswered`. Every rejection is handed to the pure\n * classifier, which is where the decision to stay quiet unless certain lives.\n */\nconst probeFieldExists = async (\n  client: GreenlightApiClient,\n  fieldName: string,\n): Promise<FieldProbeVerdict> => {\n  try {\n    const response = await client.query(\n      buildFieldExistenceProbe(LEAD_CONNECTION, fieldName),\n    );\n\n    return probeAnswered(LEAD_CONNECTION, response) ? 'present' : 'inconclusive';\n  } catch (error) {\n    return classifyProbeRejection(fieldName, describeError(error));\n  }\n};\n\n/**\n * Ask about every configured name, one at a time.\n *\n * Serial rather than `Promise.all`, for the same reason `icp-run.ts` probes\n * serially: eight simultaneous requests is a burst against a per-minute ceiling\n * the live scoring path is also drawing on, and there is nothing to hurry for \u2014\n * this runs once, on a click, before a job that will take hours.\n *\n * Exported so the check can be unit-tested against a fake client without going\n * through a whole run.\n */\nexport const checkFieldMapping = async (\n  client: GreenlightApiClient,\n  configRecord: unknown,\n): Promise<FieldMappingCheckResult> => {\n  const mapped = mappedFieldNames(configRecord);\n  const asked = mapped.slice(0, FIELD_MAPPING_PROBE_LIMIT);\n\n  const missing: MappingFinding[] = [];\n  let inconclusive = 0;\n\n  for (const candidate of asked) {\n    const verdict = await probeFieldExists(client, candidate.name);\n\n    if (verdict === 'absent') {\n      missing.push(candidate);\n    } else if (verdict === 'inconclusive') {\n      inconclusive += 1;\n    }\n  }\n\n  return {\n    missing,\n    probed: asked.length,\n    inconclusive,\n    unchecked: mapped.length - asked.length,\n  };\n};\n\n/**\n * The whole check, wrapped so that it cannot cost the caller anything.\n *\n * Reads the config record itself rather than taking one, because the only caller\n * is `startBackfill`, which has no reason to hold a config otherwise \u2014 and\n * because the read has to be inside the `try` that makes this fail-open. If\n * anything at all goes wrong, from the config read to the last probe, the result\n * is the empty one: no findings, no notice, no audit row, and a backfill that\n * behaves precisely as it did before this check existed.\n *\n * That is the ordering rule the whole feature is built on. A validation that can\n * break scoring is worse than the defect it was written to catch.\n */\nconst runFieldMappingCheck = async (\n  client: GreenlightApiClient,\n): Promise<{\n  readonly result: FieldMappingCheckResult;\n  readonly configRecord: Record<string, unknown> | null;\n}> => {\n  try {\n    const configRecord = await loadConfigRecord(client);\n\n    return {\n      result: await checkFieldMapping(client, configRecord),\n      configRecord,\n    };\n  } catch (error) {\n    logGreenlight('backfill_field_mapping_check_failed', {\n      error: describeError(error),\n    });\n\n    return { result: EMPTY_FIELD_MAPPING_CHECK, configRecord: null };\n  }\n};\n\n/* -------------------------------------------------------------------------- */\n/* One tick                                                                    */\n/* -------------------------------------------------------------------------- */\n\nexport type BackfillChunkOutcome =\n  /** No run has ever been requested on this workspace. */\n  | { status: 'no_run' }\n  /** A run exists but is finished, cancelled or failed. */\n  | { status: 'not_running'; runStatus: BackfillState['status'] }\n  /** Another tick is mid-chunk. */\n  | { status: 'locked'; leaseUntil: string | null }\n  | { status: 'state_unreadable'; error: string }\n  | { status: 'lease_not_taken'; error: string }\n  | { status: 'config_unavailable'; error: string }\n  | { status: 'page_failed'; error: string }\n  | {\n      status: 'progressed';\n      runId: string;\n      tally: BackfillTally;\n      exhausted: boolean;\n      degraded: boolean;\n    };\n\ninterface PageResult {\n  readonly nodes: Record<string, unknown>[];\n  /** False on the fallback rung that drops ordering and the cursor together. */\n  readonly ordered: boolean;\n  readonly degraded: boolean;\n}\n\n/**\n * Walk `PAGE_ATTEMPTS` until one rung answers. See that constant for why each\n * rung exists; the `all`-mode filter below is why the last one is skipped there.\n */\nconst fetchPage = async (\n  client: GreenlightApiClient,\n  state: BackfillState,\n  chunkSize: number,\n  extraFields: readonly string[],\n): Promise<{ ok: true; page: PageResult } | { ok: false; error: string }> => {\n  const attempts = PAGE_ATTEMPTS.filter(\n    (attempt) => attempt.ordered || state.mode === 'unscored',\n  );\n\n  let lastError = 'no page attempt was made';\n\n  for (const [index, attempt] of attempts.entries()) {\n    const selection = backfillPersonSelection(\n      attempt.useExtraFields ? extraFields : [],\n    );\n\n    try {\n      const response = await client.query(\n        buildPageRequest({\n          mode: state.mode,\n          cursor: state.cursor,\n          chunkSize,\n          selection,\n          ordered: attempt.ordered,\n        }),\n      );\n\n      return {\n        ok: true,\n        page: {\n          nodes: readConnectionNodes(response, 'people'),\n          ordered: attempt.ordered,\n          degraded: index > 0,\n        },\n      };\n    } catch (error) {\n      lastError = describeError(error);\n\n      logGreenlight('backfill_page_attempt_failed', {\n        runId: state.runId,\n        mode: state.mode,\n        attempt: index,\n        ordered: attempt.ordered,\n        useExtraFields: attempt.useExtraFields,\n        error: lastError,\n      });\n    }\n  }\n\n  return { ok: false, error: lastError };\n};\n\n/**\n * Score one bounded chunk and record where the walk reached. Never throws.\n *\n * The ordering of the steps is the resumability argument, so it is worth stating\n * flatly:\n *\n *   1. Read the state. No run, or not `running` \u2014 return.\n *   2. Refuse if another tick holds the lease.\n *   3. Take the lease **and write it** before touching a single record. A tick\n *      that cannot record that it started must not start.\n *   4. Read the config once for the whole chunk. A failure here **abandons the\n *      tick** rather than falling back to engine defaults: the event path treats\n *      an unreadable config as a per-lead fail-open, which is right for one lead,\n *      but scoring thirty leads against defaults a workspace has deliberately\n *      moved away from would bulk-gate or bulk-pass them on configuration nobody\n *      chose. A minute of delay costs nothing; thirty wrong decisions do.\n *   5. Fetch the page, degrading through `PAGE_ATTEMPTS`.\n *   6. Score records in order, stopping when the tick budget runs out, and build\n *      the *prefix actually reached* as we go.\n *   7. Commit: advance the cursor over that prefix only, fold in the tally,\n *      release the lease.\n *\n * A crash anywhere in 4-6 leaves the cursor exactly where step 3 found it, so the\n * next tick after the lease expires re-fetches the same page. Records the crashed\n * tick had already scored are re-scored \u2014 and write nothing, because guard 3\n * recognises the stored fingerprint. That is the whole of the crash story: at\n * most one chunk of duplicated *reads*, never a duplicated write, never a\n * duplicated audit row, and never a skipped record.\n */\nexport const runBackfillChunk = async ({\n  client,\n  store,\n  markers,\n  calibration,\n  calibrationSeal,\n  now,\n  clock = () => Date.now(),\n  chunkSize = BACKFILL_CHUNK_SIZE,\n}: BackfillChunkDeps): Promise<BackfillChunkOutcome> => {\n  const read = await readState(store);\n\n  if (!read.ok) {\n    logGreenlight('backfill_state_unreadable', { error: read.error });\n\n    return { status: 'state_unreadable', error: read.error };\n  }\n\n  const state = read.state;\n\n  if (state === null) {\n    return { status: 'no_run' };\n  }\n\n  if (state.status !== 'running') {\n    return { status: 'not_running', runStatus: state.status };\n  }\n\n  if (isLeaseHeld(state, now)) {\n    return { status: 'locked', leaseUntil: state.leaseUntil };\n  }\n\n  const claimed = claimLease(state, now);\n\n  try {\n    await store.write(claimed);\n  } catch (error) {\n    const message = describeError(error);\n\n    logGreenlight('backfill_lease_write_failed', {\n      runId: state.runId,\n      error: message,\n    });\n\n    return { status: 'lease_not_taken', error: message };\n  }\n\n  const commit = async (\n    cursor: BackfillCursor,\n    tally: BackfillTally,\n    lastError: string | null,\n    exhausted: boolean,\n    stalled: boolean,\n  ): Promise<void> => {\n    const next = commitChunk(claimed, {\n      now,\n      cursor,\n      tally,\n      lastError,\n      exhausted,\n      stalled,\n    });\n\n    try {\n      await store.write(next);\n    } catch (error) {\n      // Nothing to be done but say so. The lease expires on its own, and the\n      // chunk that was not recorded is simply redone \u2014 which writes nothing.\n      logGreenlight('backfill_commit_failed', {\n        runId: state.runId,\n        error: describeError(error),\n      });\n\n      return;\n    }\n\n    if (exhausted) {\n      logGreenlight('backfill_completed', {\n        runId: next.runId,\n        mode: next.mode,\n        ...next.tally,\n        chunks: next.chunks,\n      });\n\n      await writeBackfillAuditRow(\n        client,\n        now,\n        `BACKFILL \u00B7 Greenlight backfill finished \u00B7 ${next.tally.examined} records examined`,\n        {\n          action: 'backfill_completed',\n          runId: next.runId,\n          mode: next.mode,\n          startedAt: next.startedAt,\n          finishedAt: next.finishedAt,\n          chunks: next.chunks,\n          ...next.tally,\n          engineVersion: SCORING_ENGINE_VERSION,\n        },\n      );\n    }\n  };\n\n  const abandon = async (lastError: string): Promise<void> => {\n    try {\n      await store.write(releaseLease(claimed, now, lastError));\n    } catch (error) {\n      logGreenlight('backfill_release_failed', {\n        runId: state.runId,\n        error: describeError(error),\n      });\n    }\n  };\n\n  let configRecord: Record<string, unknown> | null;\n\n  try {\n    configRecord = await loadConfigRecord(client);\n  } catch (error) {\n    const message = describeError(error);\n\n    logGreenlight('backfill_config_unavailable', {\n      runId: state.runId,\n      error: message,\n    });\n\n    await abandon(message);\n\n    return { status: 'config_unavailable', error: message };\n  }\n\n  const page = await fetchPage(\n    client,\n    state,\n    chunkSize,\n    configuredSelectionFields(configRecord),\n  );\n\n  if (!page.ok) {\n    await abandon(page.error);\n\n    return { status: 'page_failed', error: page.error };\n  }\n\n  const { nodes, ordered, degraded } = page.page;\n\n  const tally = { ...EMPTY_TALLY };\n  const handled: Record<string, unknown>[] = [];\n  const deadline = clock() + BACKFILL_TICK_BUDGET_MS;\n\n  for (const node of nodes) {\n    // The page is deliberately larger than the chunk \u2014 the surplus pays for\n    // skipping the cursor boundary, not for extra scoring. The rate arithmetic\n    // is sized on the chunk, so the chunk is what bounds the writes.\n    if (tally.examined >= chunkSize) {\n      break;\n    }\n\n    // At least one record is always attempted, so a clock that is already past\n    // the budget cannot stall the run forever.\n    if (tally.examined > 0 && clock() > deadline) {\n      break;\n    }\n\n    // Only meaningful on an ordered page: the boundary ids exist to stop `gte`\n    // re-processing records at the cursor's exact timestamp. The unordered rung\n    // has no cursor, and its filter already excludes anything scored.\n    if (ordered && isAlreadyHandled(state.cursor, node)) {\n      handled.push(node);\n      continue;\n    }\n\n    tally.examined += 1;\n\n    try {\n      const outcome = await runPersonScoring({\n        client,\n        event: toScoringEvent(node),\n        now,\n        markers,\n        // Read once per chunk, not once per record. The single largest saving in\n        // the whole design \u2014 see `PreloadedConfig` in `scoring-run.ts`.\n        config: { record: configRecord },\n        calibration,\n        calibrationSeal,\n      });\n\n      switch (outcome.status) {\n        case 'scored':\n          tally.scored += 1;\n          break;\n        case 'unchanged':\n          tally.unchanged += 1;\n          break;\n        case 'skipped':\n          tally.skipped += 1;\n          break;\n        default:\n          tally.failed += 1;\n          logGreenlight('backfill_record_failed', {\n            runId: state.runId,\n            leadRecordId: outcome.leadRecordId,\n            error: outcome.error,\n          });\n          break;\n      }\n    } catch (error) {\n      // `runPersonScoring` is documented never to throw, and it does not. This\n      // exists so that if it ever starts to, one malformed record costs one\n      // record rather than the whole queue.\n      tally.failed += 1;\n\n      logGreenlight('backfill_record_threw', {\n        runId: state.runId,\n        error: describeError(error),\n      });\n    }\n\n    handled.push(node);\n  }\n\n  // Only a page that came back short of what was asked for *and* was fully\n  // worked through proves there is nothing left. A page cut off by the chunk\n  // budget or the tick budget proves nothing.\n  const requested = ordered ? pageSizeFor(chunkSize, state.cursor) : chunkSize;\n  const exhausted =\n    nodes.length < requested && handled.length === nodes.length;\n\n  // Records came back and the tick got through none of them. See\n  // `BACKFILL_MAX_STALLS`: repeated, this is a walk that cannot advance, and it\n  // has to become visible rather than spin.\n  const stalled = nodes.length > 0 && tally.examined === 0;\n\n  const cursor = ordered ? advanceCursor(state.cursor, handled) : state.cursor;\n\n  await commit(\n    cursor,\n    tally,\n    degraded ? 'Ran with a reduced query. See logs.' : null,\n    exhausted,\n    stalled,\n  );\n\n  logGreenlight('backfill_chunk', {\n    runId: state.runId,\n    mode: state.mode,\n    requested,\n    fetched: nodes.length,\n    handled: handled.length,\n    ...tally,\n    exhausted,\n    stalled,\n    degraded,\n  });\n\n  return {\n    status: 'progressed',\n    runId: state.runId,\n    tally,\n    exhausted,\n    degraded,\n  };\n};\n\n/* -------------------------------------------------------------------------- */\n/* Control surface                                                             */\n/* -------------------------------------------------------------------------- */\n\nexport interface BackfillControlResult {\n  readonly status: number;\n  readonly body: {\n    readonly outcome: string;\n    readonly message: string;\n    /** Null when no backfill has ever been requested on this workspace. */\n    readonly run: BackfillProgress | null;\n    /** Person records with no Greenlight decision at all. Null if uncountable. */\n    readonly neverScoredCount: number | null;\n    /** So the panel can state the cadence without hard-coding it twice. */\n    readonly chunkSize: number;\n  };\n}\n\nconst control = (\n  status: number,\n  outcome: string,\n  message: string,\n  run: BackfillProgress | null,\n  neverScoredCount: number | null,\n): BackfillControlResult => ({\n  status,\n  body: {\n    outcome,\n    message,\n    run,\n    neverScoredCount,\n    chunkSize: BACKFILL_CHUNK_SIZE,\n  },\n});\n\n/**\n * What the admin panel renders.\n *\n * Deliberately answers all four questions in one round trip \u2014 how many are\n * unscored, is something running, how far has it got, when did it last finish \u2014\n * because a surface that needs three calls to tell you whether the gate is\n * working is a surface people stop opening.\n */\nexport const readBackfillStatus = async ({\n  client,\n  store,\n  now,\n}: BackfillDeps): Promise<BackfillControlResult> => {\n  const read = await readState(store);\n  const neverScoredCount = await countMatching(client, 'unscored');\n\n  if (!read.ok) {\n    return control(\n      200,\n      'state_unreadable',\n      'Greenlight could not read the backfill\u2019s progress. Scoring of new leads is unaffected. Try again, and start a backfill if this persists.',\n      null,\n      neverScoredCount,\n    );\n  }\n\n  if (read.state === null) {\n    return control(\n      200,\n      'no_run',\n      neverScoredCount === null || neverScoredCount === 0\n        ? 'No backfill has run on this workspace.'\n        : `No backfill has run on this workspace. ${neverScoredCount} people have never been scored \u2014 until they are, an empty gate queue does not mean they are clean.`,\n      null,\n      neverScoredCount,\n    );\n  }\n\n  return control(\n    200,\n    read.state.status,\n    '',\n    describeBackfill(read.state, now),\n    neverScoredCount,\n  );\n};\n\nexport interface StartBackfillInput extends BackfillDeps {\n  readonly mode: BackfillMode;\n  /** Server-derived. Never taken from the request body. */\n  readonly requestedBy: string;\n}\n\n/**\n * Begin a run, or refuse because one is already going.\n *\n * The refusal is a `200` carrying the running run's progress rather than a `409`:\n * the caller asked for a backfill and there is a backfill, so the honest answer\n * is to show them the one that exists. Starting a second would double the request\n * rate against a limit the first is already sized against.\n *\n * ============================================================================\n * ## Why the field-mapping check happens here\n *\n * A configured field name that does not exist comes back `null` rather than\n * failing anything (`field-mapping-check.ts` has the whole story). Somewhere has\n * to notice. Four places could:\n *\n *   - **On the per-record scoring path.** Never. Scoring one lead must not gain a\n *     schema round trip to answer a question whose answer changes about once a\n *     year. The mapping changes rarely; that path runs constantly.\n *   - **On each backfill tick.** Same objection in slower motion. A run is\n *     thousands of ticks, the answer is identical on every one of them, and the\n *     probes would be spent against the same per-minute ceiling the chunk\n *     arithmetic already budgets 62 of.\n *   - **On the config record's write path.** There is no such path to hook. The\n *     record is edited by a human on Twenty's own record page and this app\n *     registers no database-event function against it; adding one would mean a\n *     schema probe on every field save, and it would still say nothing about the\n *     workspaces that were already mis-configured before this shipped.\n *   - **On the panel's status poll.** The panel polls `readBackfillStatus` while\n *     a run is going. A probe there is a schema query every few seconds, for a\n *     fact that has not moved.\n *\n * **Here**, once per run, is the moment the question is actually being asked:\n * this is where a whole workspace gets scored against this mapping in one go, so\n * it is where a wrong mapping does its damage all at once. It is also the moment\n * an admin is looking \u2014 they pressed Start and are reading the reply. And because\n * `requestInstallBackfill` routes through this function, install and upgrade are\n * covered by the same code without a second implementation, as is the re-score\n * `icp-run.ts` kicks off after a profile is applied.\n *\n * ## What it costs\n *\n * One config read plus one `first: 1` query per configured name, capped at\n * `FIELD_MAPPING_PROBE_LIMIT`, once, on a human's click. A stock workspace has a\n * single configured name (`decisionMakerFieldName`, seeded `jobTitle`), so the\n * usual bill is two requests before a job that will run for hours. Nothing here\n * is on a schedule and nothing here can fail the start.\n */\nexport const startBackfill = async ({\n  client,\n  store,\n  now,\n  mode,\n  requestedBy,\n}: StartBackfillInput): Promise<BackfillControlResult> => {\n  const read = await readState(store);\n\n  if (read.ok && read.state !== null && read.state.status === 'running') {\n    return control(\n      200,\n      'already_running',\n      'A backfill is already running. Watch its progress here, or stop it first if you need to change the mode.',\n      describeBackfill(read.state, now),\n      await countMatching(client, 'unscored'),\n    );\n  }\n\n  const totalAtStart = await countMatching(client, mode);\n  const state = beginBackfill({ mode, now, requestedBy, totalAtStart });\n\n  try {\n    await store.write(state);\n  } catch (error) {\n    const message = describeError(error);\n\n    logGreenlight('backfill_start_failed', { mode, error: message });\n\n    return control(\n      502,\n      'start_failed',\n      `Greenlight could not record the backfill, so it did not start one. Nothing has changed. (${message})`,\n      null,\n      totalAtStart,\n    );\n  }\n\n  logGreenlight('backfill_started', {\n    runId: state.runId,\n    mode,\n    requestedBy,\n    totalAtStart,\n  });\n\n  // After the run is recorded, never before. A configured name that does not\n  // exist is a reason to tell somebody, not a reason to refuse to start: the\n  // backfill still fills in blanks, still respects suppression, and still leaves\n  // the workspace better off than the unscored state it was in. Refusing would\n  // trade a quiet defect for a loud one.\n  const mapping = await runFieldMappingCheck(client);\n  const mappingNotice = describeFieldMappingFindings(\n    mapping.result.missing,\n    LEAD_OBJECT_LABEL,\n  );\n\n  if (mapping.result.missing.length > 0) {\n    const detail = {\n      ...fieldMappingAuditDetail(\n        mapping.result,\n        readLeadObjectNameSingular(mapping.configRecord),\n      ),\n      runId: state.runId,\n      mode,\n      requestedBy,\n      engineVersion: SCORING_ENGINE_VERSION,\n    };\n\n    logGreenlight('backfill_field_mapping_missing', detail);\n\n    await writeBackfillAuditRow(\n      client,\n      now,\n      fieldMappingAuditName(mapping.result.missing, LEAD_OBJECT_LABEL),\n      detail,\n      'ERROR',\n    );\n  } else if (mapping.result.inconclusive > 0 || mapping.result.unchecked > 0) {\n    // Nothing an admin can act on, so nothing an admin is shown. Logged because\n    // \"the check ran and could not answer\" and \"the check ran and found nothing\n    // wrong\" are different states, and support should not have to guess which\n    // one a quiet run was in.\n    logGreenlight('backfill_field_mapping_unverified', {\n      runId: state.runId,\n      probed: mapping.result.probed,\n      inconclusive: mapping.result.inconclusive,\n      unchecked: mapping.result.unchecked,\n    });\n  }\n\n  await writeBackfillAuditRow(\n    client,\n    now,\n    `BACKFILL \u00B7 Greenlight backfill started \u00B7 ${mode}`,\n    {\n      action: 'backfill_started',\n      runId: state.runId,\n      mode,\n      requestedBy,\n      totalAtStart,\n      chunkSize: BACKFILL_CHUNK_SIZE,\n      engineVersion: SCORING_ENGINE_VERSION,\n      fieldMappingChecked: mapping.result.probed,\n      fieldMappingMissing: mapping.result.missing.length,\n    },\n  );\n\n  const started =\n    totalAtStart === null\n      ? 'Backfill started. It runs about one batch a minute in the background; you can close this panel.'\n      : `Backfill started over ${totalAtStart} records. It runs ${BACKFILL_CHUNK_SIZE} a minute in the background; you can close this panel.`;\n\n  return control(\n    200,\n    'started',\n    // The notice leads. The confirmation ends with \"you can close this panel\",\n    // and a warning placed after that sentence is a warning nobody reads.\n    mappingNotice === null ? started : `${mappingNotice} ${started}`,\n    describeBackfill(state, now),\n    await countMatching(client, 'unscored'),\n  );\n};\n\nexport const stopBackfill = async ({\n  client,\n  store,\n  now,\n  requestedBy,\n}: BackfillDeps & { requestedBy: string }): Promise<BackfillControlResult> => {\n  const read = await readState(store);\n  const neverScoredCount = await countMatching(client, 'unscored');\n\n  if (!read.ok || read.state === null) {\n    return control(\n      200,\n      'no_run',\n      'There is no backfill to stop.',\n      null,\n      neverScoredCount,\n    );\n  }\n\n  if (read.state.status !== 'running') {\n    return control(\n      200,\n      'not_running',\n      'That backfill is not running any more.',\n      describeBackfill(read.state, now),\n      neverScoredCount,\n    );\n  }\n\n  const cancelled = cancelBackfillState(read.state, now, requestedBy);\n\n  try {\n    await store.write(cancelled);\n  } catch (error) {\n    return control(\n      502,\n      'stop_failed',\n      `Greenlight could not stop the backfill. (${describeError(error)})`,\n      describeBackfill(read.state, now),\n      neverScoredCount,\n    );\n  }\n\n  logGreenlight('backfill_cancelled', {\n    runId: cancelled.runId,\n    requestedBy,\n    ...cancelled.tally,\n  });\n\n  return control(\n    200,\n    'stopped',\n    'Backfill stopped. Records already scored keep their scores; the rest are untouched. Starting again begins from the beginning, which is cheap \u2014 records that are already scored are recognised and left alone.',\n    describeBackfill(cancelled, now),\n    neverScoredCount,\n  );\n};\n\n/* -------------------------------------------------------------------------- */\n/* The install entry point                                                     */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Request the automatic first backfill. **This is the only function the\n * post-install hook needs to call.**\n *\n * ## Idempotency across install *and* upgrade\n *\n * The post-install hook runs on a fresh install, on every version upgrade\n * (`shouldRunOnVersionUpgrade: true`), and up to three times on each of those\n * because the async queue retries. So the question is not \"has a backfill run\",\n * it is \"has one ever been *requested*\" \u2014 and the presence of any state at all,\n * in any status, is the durable answer. A completed run, a cancelled run and a\n * run still going all mean the same thing here: a human has been given the\n * surface, do not start another behind their back.\n *\n * That deliberately includes a run somebody cancelled. Re-starting a backfill an\n * admin stopped, on the next patch release, is the app overruling a decision it\n * was told about. If they want another they can start one; the panel is one\n * command away and says exactly how many records are still unscored.\n *\n * ## Why `unscored` and never `all`\n *\n * An install-triggered `all` would rewrite `greenlightDecision` on every record in\n * the workspace, including ones a rep has already worked from \u2014 on an upgrade,\n * without anyone asking. `unscored` only ever fills in a blank, and a blank is\n * unambiguously wrong: nothing has looked at that lead.\n *\n * ## Never throws\n *\n * The post-install hook deliberately does not catch, so that a transient failure\n * is retried by the platform. That is right for seeding the config, which is what\n * that hook is *for*. It is not right for this: a backfill that could not be\n * requested must not fail the install or burn the hook's three retries, because\n * the workspace is perfectly functional without it and the admin can start one by\n * hand. Every failure below is logged and swallowed.\n */\nexport const requestInstallBackfill = async ({\n  client,\n  store,\n  now,\n}: BackfillDeps): Promise<\n  | { status: 'requested'; runId: string; totalAtStart: number | null }\n  | { status: 'already_requested'; runStatus: BackfillState['status'] }\n  | { status: 'nothing_to_do' }\n  | { status: 'failed'; error: string }\n> => {\n  try {\n    const read = await readState(store);\n\n    if (!read.ok) {\n      logGreenlight('backfill_install_state_unreadable', { error: read.error });\n\n      return { status: 'failed', error: read.error };\n    }\n\n    if (read.state !== null) {\n      logGreenlight('backfill_install_skipped', {\n        reason: 'already_requested',\n        runId: read.state.runId,\n        runStatus: read.state.status,\n      });\n\n      return { status: 'already_requested', runStatus: read.state.status };\n    }\n\n    const totalAtStart = await countMatching(client, 'unscored');\n\n    // A fresh workspace with nothing to walk gets no run and no audit row. The\n    // cron would finish it on the first tick anyway; not starting is tidier and\n    // leaves the panel saying \"no backfill has run\", which is true and correct.\n    if (totalAtStart === 0) {\n      logGreenlight('backfill_install_skipped', {\n        reason: 'nothing_unscored',\n      });\n\n      return { status: 'nothing_to_do' };\n    }\n\n    const result = await startBackfill({\n      client,\n      store,\n      now,\n      mode: 'unscored',\n      requestedBy: 'install',\n    });\n\n    if (result.body.outcome !== 'started' || result.body.run === null) {\n      return { status: 'failed', error: result.body.outcome };\n    }\n\n    return {\n      status: 'requested',\n      runId: result.body.run.runId,\n      totalAtStart,\n    };\n  } catch (error) {\n    const message = describeError(error);\n\n    logGreenlight('backfill_install_request_failed', { error: message });\n\n    return { status: 'failed', error: message };\n  }\n};\n", "/**\n * The I/O half of the ICP review, kept out of the `define*` file so the whole\n * propose \u2192 preview \u2192 apply story can be exercised against a fake API client.\n *\n * Everything about *what the data means* is pure and lives in `src/icp/`. This\n * module does four things and no thinking: discover which fields the workspace's\n * Company object actually has, read a bounded sample of Companies and People,\n * write the profile the human accepted, and hand a re-score to the backfill that\n * already exists.\n *\n * ## Discovering the industry field, and why it needs discovering\n *\n * Twenty's stock Company object has no industry field. Where a workspace has\n * one, it is a custom field whose name that workspace chose, and an unknown\n * field name fails the entire GraphQL request rather than coming back empty. So\n * the candidates are probed one at a time with the cheapest possible query, and\n * the ones that answer are the ones the sample selects. Fourteen probes at\n * `first: 1` is the price of not requiring the admin to tell us their schema\n * before we can tell them anything \u2014 and it is paid once, when a human clicks\n * Suggest, never on a scoring path.\n *\n * ## Request budget\n *\n * A `propose` costs at most 14 probes + 10 company pages + 5 lead pages + 2\n * counts + 1 config read = 32 requests against a 100/minute limit, once, on a\n * human's click. `status` and `preview` cost 1 and 7. Nothing here runs on a\n * schedule, so none of it competes with live scoring except while somebody is\n * looking at the panel.\n *\n * ## Nothing here throws\n *\n * Every read degrades to \"less evidence, and a note saying so\"; every write\n * failure is reported to the panel as a failure with the record unchanged. A\n * profile that could not be written must never look like one that was.\n */\n\nimport {\n  buildCountRequest,\n  backfillPersonSelection,\n  configuredSelectionFields,\n  readTotalCount,\n} from 'src/backfill/backfill-plan';\nimport {\n  COMPANY_ADDRESS_REGION_PATH,\n  COMPANY_INDUSTRY_FIELD_CANDIDATES,\n  COMPANY_REGION_FIELD_CANDIDATES,\n  COMPANY_SIZE_FIELD_CANDIDATES,\n  analyseCompanies,\n  findUnmatchedValues,\n  toCompanyFacts,\n  type CompanyFacts,\n  type CompanyFieldPaths,\n  type UnmatchedValues,\n} from 'src/icp/icp-analysis';\nimport { previewIcpImpact, type IcpImpact } from 'src/icp/icp-impact';\nimport {\n  proposeIcp,\n  toIcpConfigPatch,\n  type IcpProposal,\n  type IcpSelection,\n} from 'src/icp/icp-proposal';\nimport {\n  ICP_REVIEW_FIELD_NAMES,\n  readIcpStatus,\n  type IcpStatus,\n} from 'src/icp/icp-status';\nimport type { IcpCommand } from 'src/icp/icp-command';\nimport { ICP_REVIEW_DECISION_VALUES } from 'src/constants/icp-identifiers';\nimport {\n  startBackfill,\n  type BackfillStateStore,\n} from 'src/logic-functions/backfill-run';\nimport {\n  describeError,\n  isPlainRecord,\n  logGreenlight,\n  oldestByCreatedAt,\n  readConnectionNodes,\n  type GreenlightApiClient,\n} from 'src/logic-functions/greenlight-api';\nimport {\n  greenlightConfigSelection,\n  toScoringConfigInput,\n} from 'src/logic-functions/greenlight-config-record';\nimport { SCORING_ENGINE_VERSION, resolveConfig } from 'src/scoring';\n\n/* -------------------------------------------------------------------------- */\n/* Sample sizes                                                                */\n/* -------------------------------------------------------------------------- */\n\n/** Twenty's documented page ceiling. */\nconst PAGE_SIZE = 60;\n\n/**\n * Ten pages of companies and five of people.\n *\n * Six hundred companies is enough that a real cluster is unmistakable and a\n * fabricated one is not, and three hundred leads puts the impact estimate's\n * sampling error at a couple of percent \u2014 well inside the precision anybody\n * needs to answer \"is this safe to apply\". Both numbers are reported alongside\n * every figure derived from them, so a workspace larger than the sample sees\n * plainly that it is looking at a sample.\n */\nexport const COMPANY_SAMPLE_PAGES = 10;\nexport const LEAD_SAMPLE_PAGES = 5;\n\nexport const COMPANY_SAMPLE_LIMIT = COMPANY_SAMPLE_PAGES * PAGE_SIZE;\nexport const LEAD_SAMPLE_LIMIT = LEAD_SAMPLE_PAGES * PAGE_SIZE;\n\n/* -------------------------------------------------------------------------- */\n/* Result shape                                                                */\n/* -------------------------------------------------------------------------- */\n\nexport interface IcpControlBody {\n  readonly outcome: string;\n  readonly message: string;\n  /** How much of the model is idle. Always present when the config is readable. */\n  readonly status: IcpStatus | null;\n  readonly proposal: IcpProposal | null;\n  readonly impact: IcpImpact | null;\n  /** What a preview or apply was actually computed against. */\n  readonly selection: IcpSelection | null;\n  /**\n   * Values the workspace's own records carry that the configured profile\n   * rejects. Present on `propose`, which is the only action that reads\n   * Companies. Null everywhere else rather than an empty shape, so \"not looked\"\n   * and \"looked and found none\" stay distinguishable.\n   */\n  readonly unmatched: {\n    readonly industry: UnmatchedValues;\n    readonly region: UnmatchedValues;\n  } | null;\n  /** Entries the parser had to drop, in the admin's words. */\n  readonly warnings: readonly string[];\n  /** What the data could not tell us, and why. Never silent. */\n  readonly notes: readonly string[];\n  readonly rescore: { readonly started: boolean; readonly message: string } | null;\n}\n\nexport interface IcpControlResult {\n  readonly status: number;\n  readonly body: IcpControlBody;\n}\n\nconst result = (\n  status: number,\n  body: Partial<IcpControlBody> & { outcome: string; message: string },\n): IcpControlResult => ({\n  status,\n  body: {\n    status: null,\n    proposal: null,\n    impact: null,\n    selection: null,\n    unmatched: null,\n    warnings: [],\n    notes: [],\n    rescore: null,\n    ...body,\n  },\n});\n\n/* -------------------------------------------------------------------------- */\n/* Config record                                                               */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The config record, including the two review columns this feature adds.\n *\n * A selection of its own rather than `greenlightConfigSelection()` for one\n * reason: a workspace upgraded from a release before those columns existed still\n * has them, because metadata sync creates them \u2014 but a workspace running an\n * older *manifest* does not, and this file should need no knowledge of which\n * release the workspace is on.\n *\n * ## The fall-back below cannot fire on Twenty 2.26, and that is fine\n *\n * It was written believing that asking for a column that is not there fails the\n * whole query. It does not: Twenty validates arguments and not selection sets,\n * so an absent `icpReviewedAt` comes back `null` and the extended read succeeds\n * everywhere. See `probeField` above for the proof and for what that assumption\n * cost when something *depended* on it.\n *\n * Nothing here depends on it. The rung is kept rather than deleted for two\n * reasons: it costs one request only in a case that cannot occur, and the\n * asymmetry it guards against is a property of this Twenty release rather than\n * of GraphQL \u2014 a server that tightened selection validation would make it live\n * again. What it must not do is masquerade as the thing protecting the *write*.\n * That is `writeIcpConfig` further down, which tries the patch with the review\n * columns and retries without them, and which works because a mutation's `data`\n * **is** an argument and therefore **is** validated:\n *\n *     Object greenlightConfig doesn't have any \"icpReviewDecision\" field.\n *\n * `reviewFieldsAvailable` is reported for the log's benefit and read by nothing.\n */\nconst loadIcpConfigRecord = async (\n  client: GreenlightApiClient,\n): Promise<{\n  record: Record<string, unknown> | null;\n  reviewFieldsAvailable: boolean;\n}> => {\n  const extended = {\n    ...greenlightConfigSelection(),\n    ...Object.fromEntries(ICP_REVIEW_FIELD_NAMES.map((name) => [name, true])),\n  };\n\n  try {\n    const response = await client.query({\n      greenlightConfigs: { edges: { node: extended } },\n    });\n\n    return {\n      record: oldestByCreatedAt(readConnectionNodes(response, 'greenlightConfigs')),\n      reviewFieldsAvailable: true,\n    };\n  } catch (error) {\n    logGreenlight('icp_config_extended_read_failed', {\n      error: describeError(error),\n    });\n  }\n\n  const response = await client.query({\n    greenlightConfigs: { edges: { node: greenlightConfigSelection() } },\n  });\n\n  return {\n    record: oldestByCreatedAt(readConnectionNodes(response, 'greenlightConfigs')),\n    reviewFieldsAvailable: false,\n  };\n};\n\n/* -------------------------------------------------------------------------- */\n/* Field discovery                                                             */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Did that query actually answer, or did it come back without data?\n *\n * A rejected GraphQL request normally throws out of the client, but a transport\n * that returns `{ errors, data: null }` without throwing would make every probe\n * look like a success and put non-existent field names into the sample query \u2014\n * where, since Twenty does not validate selection sets, they would come back\n * `null` rather than failing, and the panel would report a profile derived from\n * columns that are not there. Checking that the connection is present costs\n * nothing and closes that path.\n */\nconst answered = (response: unknown): boolean =>\n  isPlainRecord(response) && isPlainRecord(response['companies']);\n\n/**\n * A selection turned into the filter that asks whether its fields exist.\n *\n * `{ industry: true }` becomes `{ industry: { is: 'NOT_NULL' } }`; the composite\n * probe `{ address: { addressCountry: true } }` becomes\n * `{ address: { addressCountry: { is: 'NOT_NULL' } } }`. The nesting is carried\n * over unchanged, which matters: Twenty rejects an operator applied to a\n * composite root \u2014 `Sub field \"is\" not found for composite type: ADDRESS` \u2014 so a\n * composite has to be probed at the sub-path, and the sub-path is exactly what\n * the caller's selection already spells out.\n *\n * The value is never used. `NOT_NULL` over `NULL` only because it reads as the\n * question being asked: is there such a thing.\n */\nconst existenceFilter = (\n  selection: Record<string, unknown>,\n): Record<string, unknown> =>\n  Object.fromEntries(\n    Object.entries(selection).map(([name, value]) => [\n      name,\n      isPlainRecord(value) ? existenceFilter(value) : { is: 'NOT_NULL' },\n    ]),\n  );\n\n/**\n * Does this workspace's Company have these fields?\n *\n * ## Why this asks in the filter and not in the selection\n *\n * It used to put the candidate in the **selection set** and read \"the request\n * was not rejected\" as \"the field is there\". Against a live Twenty that is\n * always true, because:\n *\n *     Twenty validates arguments against the object's schema.\n *     Twenty does not validate selection sets.\n *\n *     { companies(first: 1) { edges { node { id sector } } } }\n *     \u2192 { \"node\": { \"id\": \"\u2026\", \"sector\": null } }        // no such column\n *\n *     { companies(filter: { sector: { is: \"NOT_NULL\" } }) { \u2026 } }\n *     \u2192 Object company doesn't have any \"sector\" field.  // rejected\n *\n * So every candidate answered `true` on every workspace. `probeCompanyFields`\n * then reported *\"Industry read from Company.industry, Company.sector,\n * Company.vertical, Company.companyIndustry, Company.businessType\"* \u2014 naming\n * five columns that mostly do not exist \u2014 and put all five into the sample\n * selection, where each came back `null`. The proposal was therefore built from\n * nothing and explained by a note that was flatly untrue, while the one note an\n * admin could have acted on (\"this workspace has no industry field on Company \u2026\n * nothing can be proposed until one exists\") was unreachable by construction.\n *\n * This is the same defect as the `taskTargets` filter in `pipeline-store.ts`,\n * read from the other end: there, a column that came back on every row turned\n * out not to be filterable; here, a filter that Twenty checks turns out to be\n * the only half of a query it checks at all. Both were invisible to the unit\n * suite because the fakes modelled the same assumption as the code.\n *\n * One field type is probed conservatively: a candidate that happens to be a\n * composite (`CURRENCY`, `ADDRESS`, `LINKS`) is rejected with *\"Sub field 'is'\n * not found for composite type\"* and reported absent, because the probe cannot\n * know which sub-field to ask for. A currency-typed \"industry\" is not an\n * industry field in any useful sense, and reporting \"no industry field\n * answered\" is a true and actionable thing to say about it.\n */\nconst probeField = async (\n  client: GreenlightApiClient,\n  selection: Record<string, unknown>,\n): Promise<boolean> => {\n  try {\n    const response = await client.query({\n      companies: {\n        __args: { first: 1, filter: existenceFilter(selection) },\n        edges: { node: { id: true } },\n      },\n    });\n\n    return answered(response);\n  } catch {\n    return false;\n  }\n};\n\nexport interface CompanyProbe {\n  readonly paths: CompanyFieldPaths;\n  /** Field names to put in the sample's selection set. */\n  readonly selection: Record<string, unknown>;\n  readonly notes: readonly string[];\n}\n\n/**\n * Find out which of the candidate fields this workspace's Company object has.\n *\n * Reported rather than assumed: the panel says \"industry was read from\n * `sector`\", or says that no industry-like field exists at all, which is a\n * different and much more actionable problem than \"we found no industries\".\n */\nexport const probeCompanyFields = async (\n  client: GreenlightApiClient,\n): Promise<CompanyProbe> => {\n  const selection: Record<string, unknown> = { id: true, name: true };\n  const industry: string[] = [];\n  const region: string[] = [];\n  const size: string[] = [];\n  const notes: string[] = [];\n\n  for (const candidate of COMPANY_INDUSTRY_FIELD_CANDIDATES) {\n    if (await probeField(client, { [candidate]: true })) {\n      selection[candidate] = true;\n      industry.push(candidate);\n    }\n  }\n\n  for (const candidate of COMPANY_REGION_FIELD_CANDIDATES) {\n    if (await probeField(client, { [candidate]: true })) {\n      selection[candidate] = true;\n      region.push(candidate);\n    }\n  }\n\n  if (await probeField(client, { address: { addressCountry: true } })) {\n    selection['address'] = { addressCountry: true };\n    region.push(COMPANY_ADDRESS_REGION_PATH);\n  }\n\n  for (const candidate of COMPANY_SIZE_FIELD_CANDIDATES) {\n    if (await probeField(client, { [candidate]: true })) {\n      selection[candidate] = true;\n      size.push(candidate);\n    }\n  }\n\n  if (industry.length === 0) {\n    notes.push(\n      'This workspace has no industry field on Company \u2014 Twenty does not ship one, and no custom field named industry, sector, vertical or similar was found. Nothing can be proposed for industry until one exists and is filled in.',\n    );\n  } else {\n    notes.push(`Industry read from Company.${industry.join(', Company.')}.`);\n  }\n\n  if (region.length === 0) {\n    notes.push(\n      'No country or region field answered on Company, so nothing can be proposed for region.',\n    );\n  } else {\n    notes.push(`Region read from Company.${region.join(', Company.')}.`);\n  }\n\n  if (size.length === 0) {\n    notes.push(\n      'No employee-count field answered on Company, so nothing can be proposed for company size.',\n    );\n  } else {\n    notes.push(`Company size read from Company.${size.join(', Company.')}.`);\n  }\n\n  return { paths: { industry, region, size }, selection, notes };\n};\n\n/* -------------------------------------------------------------------------- */\n/* Samples                                                                     */\n/* -------------------------------------------------------------------------- */\n\ninterface SampleResult {\n  readonly nodes: readonly Record<string, unknown>[];\n  readonly notes: readonly string[];\n}\n\n/**\n * Walk a connection newest-first, keyed on `createdAt`.\n *\n * Newest-first, and stated in the panel copy, because if only part of the book\n * can be examined the recent part is the more honest answer to \"who do we sell\n * to now\". A dead segment from four years ago is exactly the kind of evidence\n * that should not be shaping today's profile.\n *\n * If the ordered query is rejected \u2014 `orderBy` enum spellings are the one thing\n * in here that cannot be checked without a live schema \u2014 it falls back to a\n * single unordered page and says so, because a smaller honest sample beats a\n * blank panel.\n */\nconst fetchSample = async (\n  client: GreenlightApiClient,\n  {\n    connection,\n    selection,\n    pages,\n    label,\n  }: {\n    connection: string;\n    selection: Record<string, unknown>;\n    pages: number;\n    label: string;\n  },\n): Promise<SampleResult> => {\n  const notes: string[] = [];\n  const nodes: Record<string, unknown>[] = [];\n  const seen = new Set<string>();\n  let cursor: string | null = null;\n\n  for (let page = 0; page < pages; page += 1) {\n    const args: Record<string, unknown> = {\n      first: PAGE_SIZE,\n      orderBy: [{ createdAt: 'DescNullsLast' }],\n    };\n\n    if (cursor !== null) {\n      args['filter'] = { createdAt: { lte: cursor } };\n    }\n\n    let batch: Record<string, unknown>[];\n\n    try {\n      const response = await client.query({\n        [connection]: { __args: args, edges: { node: selection } },\n      });\n\n      batch = readConnectionNodes(response, connection);\n    } catch (error) {\n      logGreenlight('icp_sample_page_failed', {\n        connection,\n        page,\n        error: describeError(error),\n      });\n\n      if (page > 0) {\n        notes.push(\n          `Reading ${label} stopped early after ${nodes.length} records; the figures below are based on those.`,\n        );\n        break;\n      }\n\n      const fallback = await fetchUnorderedPage(client, connection, selection);\n\n      notes.push(\n        fallback.length === 0\n          ? `Greenlight could not read ${label} from this workspace just now.`\n          : `Greenlight could not page through ${label}, so it examined ${fallback.length} records rather than up to ${pages * PAGE_SIZE}.`,\n      );\n\n      return { nodes: fallback, notes };\n    }\n\n    let added = 0;\n\n    for (const node of batch) {\n      const id = typeof node['id'] === 'string' ? node['id'] : null;\n\n      if (id !== null && seen.has(id)) {\n        continue;\n      }\n\n      if (id !== null) {\n        seen.add(id);\n      }\n\n      nodes.push(node);\n      added += 1;\n    }\n\n    const last = batch[batch.length - 1];\n    const lastCreatedAt =\n      last !== undefined && typeof last['createdAt'] === 'string'\n        ? last['createdAt']\n        : null;\n\n    // Short page, no usable cursor, or a page that advanced nothing: all three\n    // mean the walk is finished. The last one is what stops an unmoving cursor\n    // spinning for ten pages over the same sixty records.\n    if (batch.length < PAGE_SIZE || lastCreatedAt === null || added === 0) {\n      break;\n    }\n\n    cursor = lastCreatedAt;\n  }\n\n  return { nodes, notes };\n};\n\nconst fetchUnorderedPage = async (\n  client: GreenlightApiClient,\n  connection: string,\n  selection: Record<string, unknown>,\n): Promise<Record<string, unknown>[]> => {\n  try {\n    const response = await client.query({\n      [connection]: { __args: { first: PAGE_SIZE }, edges: { node: selection } },\n    });\n\n    return readConnectionNodes(response, connection);\n  } catch (error) {\n    logGreenlight('icp_sample_fallback_failed', {\n      connection,\n      error: describeError(error),\n    });\n\n    return [];\n  }\n};\n\n/** Best effort. A total drives an extrapolation; it must never stop the panel. */\nconst countConnection = async (\n  client: GreenlightApiClient,\n  connection: 'companies' | 'people',\n): Promise<number | null> => {\n  try {\n    const request =\n      connection === 'people'\n        ? buildCountRequest('all')\n        : { companies: { totalCount: true } };\n\n    const response = await client.query(request);\n\n    if (connection === 'people') {\n      return readTotalCount(response);\n    }\n\n    const node = isPlainRecord(response) ? response['companies'] : null;\n    const total = isPlainRecord(node) ? node['totalCount'] : null;\n\n    return typeof total === 'number' && Number.isFinite(total) && total >= 0\n      ? Math.floor(total)\n      : null;\n  } catch (error) {\n    logGreenlight('icp_count_failed', {\n      connection,\n      error: describeError(error),\n    });\n\n    return null;\n  }\n};\n\n/* -------------------------------------------------------------------------- */\n/* Deps                                                                        */\n/* -------------------------------------------------------------------------- */\n\nexport interface IcpDeps {\n  readonly client: GreenlightApiClient;\n  /** Only touched when the admin asks for a re-score. */\n  readonly store: BackfillStateStore;\n  readonly now: Date;\n  /** Server-derived. Never taken from the request body. */\n  readonly requestedBy: string;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Actions                                                                     */\n/* -------------------------------------------------------------------------- */\n\nconst CONFIG_MISSING_MESSAGE =\n  'Greenlight has no configuration record on this workspace yet, so there is nothing to review. It is created when the app is installed; if this persists, re-run the install hook.';\n\nexport const readIcpReviewStatus = async ({\n  client,\n}: Pick<IcpDeps, 'client'>): Promise<IcpControlResult> => {\n  let record: Record<string, unknown> | null;\n\n  try {\n    record = (await loadIcpConfigRecord(client)).record;\n  } catch (error) {\n    return result(200, {\n      outcome: 'config_unreadable',\n      message: `Greenlight could not read its configuration record, so it cannot tell you whether your profile is set. Scoring is unaffected. (${describeError(error)})`,\n    });\n  }\n\n  if (record === null) {\n    return result(200, { outcome: 'config_missing', message: CONFIG_MISSING_MESSAGE });\n  }\n\n  return result(200, {\n    outcome: 'ok',\n    message: '',\n    status: readIcpStatus(record),\n  });\n};\n\n/**\n * Derive a profile from the workspace's own data and measure what applying it\n * would do. Nothing is written.\n */\nexport const proposeIcpForWorkspace = async ({\n  client,\n  now,\n}: Pick<IcpDeps, 'client' | 'now'>): Promise<IcpControlResult> => {\n  let record: Record<string, unknown> | null;\n\n  try {\n    record = (await loadIcpConfigRecord(client)).record;\n  } catch (error) {\n    return result(200, {\n      outcome: 'config_unreadable',\n      message: `Greenlight could not read its configuration record. (${describeError(error)})`,\n    });\n  }\n\n  if (record === null) {\n    return result(200, { outcome: 'config_missing', message: CONFIG_MISSING_MESSAGE });\n  }\n\n  const status = readIcpStatus(record);\n  const probe = await probeCompanyFields(client);\n  const totalCompanies = await countConnection(client, 'companies');\n\n  const sample = await fetchSample(client, {\n    connection: 'companies',\n    selection: { ...probe.selection, createdAt: true },\n    pages: COMPANY_SAMPLE_PAGES,\n    label: 'your companies',\n  });\n\n  const companies: CompanyFacts[] = sample.nodes.map((node) =>\n    toCompanyFacts(node, probe.paths),\n  );\n\n  const analysis = analyseCompanies({\n    companies,\n    totalCompanies,\n    paths: probe.paths,\n  });\n  const proposal = proposeIcp(analysis);\n\n  const notes = [...probe.notes, ...sample.notes];\n\n  /*\n   * Values the workspace already holds that the profile rejects.\n   *\n   * This is the answer to the worst outcome enrichment can produce: the\n   * industry field fills in, the score does not move, and nothing says why.\n   * `icp.industry` is exact set membership, so the lead is not at fault and no\n   * work on the lead will ever fix it - the list is simply missing a value.\n   * Saying so here, on the page that owns the list, is the whole fix. Nothing\n   * about matching or scoring changes.\n   *\n   * Read from the resolved config rather than from `status`, which reports\n   * whether a dimension is configured but not what it was configured with.\n   */\n  const configured = resolveConfig(toScoringConfigInput(record)).config.icp;\n  const unmatched = {\n    industry: findUnmatchedValues(analysis.industry, configured.industries),\n    region: findUnmatchedValues(analysis.region, configured.regions),\n  };\n\n  if (\n    totalCompanies !== null &&\n    totalCompanies > companies.length &&\n    companies.length > 0\n  ) {\n    notes.push(\n      `Based on the ${companies.length.toLocaleString('en-US')} most recently added of your ${totalCompanies.toLocaleString('en-US')} companies.`,\n    );\n  }\n\n  const impact = await measureImpact({\n    client,\n    record,\n    now,\n    selection: proposal.selection,\n  });\n\n  return result(200, {\n    outcome: 'proposed',\n    message: '',\n    status,\n    proposal,\n    impact: impact.impact,\n    selection: proposal.selection,\n    unmatched,\n    notes: [...notes, ...impact.notes],\n  });\n};\n\nconst measureImpact = async ({\n  client,\n  record,\n  now,\n  selection,\n}: {\n  client: GreenlightApiClient;\n  record: Record<string, unknown>;\n  now: Date;\n  selection: IcpSelection;\n}): Promise<{ impact: IcpImpact; notes: readonly string[] }> => {\n  const totalLeads = await countConnection(client, 'people');\n\n  const sample = await fetchSample(client, {\n    connection: 'people',\n    // Parity with the scoring path is the whole point: the same selection the\n    // backfill uses, including the workspace's own configured field names, so a\n    // preview measures what scoring will actually see rather than a richer\n    // record this module could have fetched and the engine never will.\n    selection: {\n      ...backfillPersonSelection(configuredSelectionFields(record)),\n      createdAt: true,\n    },\n    pages: LEAD_SAMPLE_PAGES,\n    label: 'your leads',\n  });\n\n  const notes = [...sample.notes];\n\n  if (\n    totalLeads !== null &&\n    totalLeads > sample.nodes.length &&\n    sample.nodes.length > 0\n  ) {\n    notes.push(\n      `Impact measured on the ${sample.nodes.length.toLocaleString('en-US')} most recently added of your ${totalLeads.toLocaleString('en-US')} leads, then scaled.`,\n    );\n  }\n\n  return {\n    impact: previewIcpImpact({\n      leads: sample.nodes,\n      configRecord: record,\n      selection,\n      now,\n      totalLeads,\n    }),\n    notes,\n  };\n};\n\n/** Score a sample against an edited profile. Nothing is written. */\nexport const previewIcpForWorkspace = async ({\n  client,\n  now,\n  selection,\n  warnings,\n}: Pick<IcpDeps, 'client' | 'now'> & {\n  selection: IcpSelection;\n  warnings: readonly string[];\n}): Promise<IcpControlResult> => {\n  let record: Record<string, unknown> | null;\n\n  try {\n    record = (await loadIcpConfigRecord(client)).record;\n  } catch (error) {\n    return result(200, {\n      outcome: 'config_unreadable',\n      message: `Greenlight could not read its configuration record. (${describeError(error)})`,\n    });\n  }\n\n  if (record === null) {\n    return result(200, { outcome: 'config_missing', message: CONFIG_MISSING_MESSAGE });\n  }\n\n  const impact = await measureImpact({ client, record, now, selection });\n\n  return result(200, {\n    outcome: 'previewed',\n    message: '',\n    status: readIcpStatus(record),\n    impact: impact.impact,\n    selection,\n    warnings,\n    notes: impact.notes,\n  });\n};\n\n/* -------------------------------------------------------------------------- */\n/* Writing                                                                     */\n/* -------------------------------------------------------------------------- */\n\nconst writeIcpAuditRow = async (\n  client: GreenlightApiClient,\n  now: Date,\n  summary: string,\n  detail: Record<string, unknown>,\n): Promise<void> => {\n  try {\n    await client.mutation({\n      createGreenlightAuditLog: {\n        __args: {\n          data: {\n            name: summary,\n            // `CONFIG_CHANGED` is what it is: the configuration changed. Same\n            // precedent as the install hook and the backfill, and the row's\n            // `ruleTrace` carries the profile that was written together with the\n            // impact estimate it was accepted against \u2014 so \"who decided this,\n            // when, and what were they told it would do\" has one answer.\n            eventType: 'CONFIG_CHANGED',\n            occurredAt: now.toISOString(),\n            leadObjectNameSingular: '',\n            leadRecordId: null,\n            leadDisplayName: '',\n            actorType: 'USER',\n            actorDisplayName: String(detail['requestedBy'] ?? 'Unknown'),\n            ruleTrace: detail,\n            score: null,\n            band: 'UNSCORED',\n            decision: 'UNSCORED',\n            overrideReason: '',\n            traceId: '',\n          },\n        },\n        id: true,\n      },\n    });\n  } catch (error) {\n    logGreenlight('icp_audit_write_failed', {\n      summary,\n      error: describeError(error),\n    });\n  }\n};\n\n/**\n * Patch the config record, tolerating a workspace whose metadata predates the\n * two review columns.\n *\n * The profile is what matters; the review stamp is provenance. If the stamp\n * cannot be written the profile still must be, so the write is retried without\n * it rather than abandoned \u2014 and the caller is told, because a panel that keeps\n * saying \"not reviewed\" after a successful apply needs an explanation on screen\n * rather than a shrug.\n */\nconst patchConfig = async (\n  client: GreenlightApiClient,\n  configId: string,\n  patch: Record<string, unknown>,\n  reviewPatch: Record<string, unknown>,\n): Promise<{ ok: true; reviewRecorded: boolean } | { ok: false; error: string }> => {\n  try {\n    await client.mutation({\n      updateGreenlightConfig: {\n        __args: { id: configId, data: { ...patch, ...reviewPatch } },\n        id: true,\n      },\n    });\n\n    return { ok: true, reviewRecorded: true };\n  } catch (error) {\n    logGreenlight('icp_config_write_with_review_failed', {\n      configId,\n      error: describeError(error),\n    });\n  }\n\n  if (Object.keys(patch).length === 0) {\n    return { ok: false, error: 'the review columns are not available' };\n  }\n\n  try {\n    await client.mutation({\n      updateGreenlightConfig: { __args: { id: configId, data: patch }, id: true },\n    });\n\n    return { ok: true, reviewRecorded: false };\n  } catch (error) {\n    return { ok: false, error: describeError(error) };\n  }\n};\n\nconst configIdOf = (record: Record<string, unknown>): string | null =>\n  typeof record['id'] === 'string' && record['id'].length > 0\n    ? record['id']\n    : null;\n\nexport const applyIcpToWorkspace = async ({\n  client,\n  store,\n  now,\n  requestedBy,\n  selection,\n  warnings,\n  rescore,\n}: IcpDeps & {\n  selection: IcpSelection;\n  warnings: readonly string[];\n  rescore: boolean;\n}): Promise<IcpControlResult> => {\n  let record: Record<string, unknown> | null;\n\n  try {\n    record = (await loadIcpConfigRecord(client)).record;\n  } catch (error) {\n    return result(502, {\n      outcome: 'config_unreadable',\n      message: `Greenlight could not read its configuration record, so nothing was changed. (${describeError(error)})`,\n    });\n  }\n\n  if (record === null) {\n    return result(200, { outcome: 'config_missing', message: CONFIG_MISSING_MESSAGE });\n  }\n\n  const configId = configIdOf(record);\n\n  if (configId === null) {\n    return result(502, {\n      outcome: 'config_unusable',\n      message:\n        'Greenlight found a configuration record without an id, so it did not write to it. Nothing has changed.',\n    });\n  }\n\n  // Measured again here, server-side, rather than trusting the number the panel\n  // was showing. The sample it is measured on is the current one, and the\n  // figure lands in the audit row \u2014 so the record says what the person was told\n  // this would do at the moment they said yes, not what a screen said ten\n  // minutes earlier.\n  const impact = await measureImpact({ client, record, now, selection });\n\n  const before = {\n    icpIndustries: record['icpIndustries'] ?? null,\n    icpRegions: record['icpRegions'] ?? null,\n    icpSizeBands: record['icpSizeBands'] ?? null,\n  };\n\n  const written = await patchConfig(\n    client,\n    configId,\n    toIcpConfigPatch(selection),\n    {\n      icpReviewedAt: now.toISOString(),\n      icpReviewDecision: ICP_REVIEW_DECISION_VALUES.applied,\n    },\n  );\n\n  if (!written.ok) {\n    return result(502, {\n      outcome: 'write_failed',\n      message: `Greenlight could not write the profile, so nothing has changed. (${written.error})`,\n      status: readIcpStatus(record),\n      impact: impact.impact,\n      selection,\n      warnings,\n      notes: impact.notes,\n    });\n  }\n\n  logGreenlight('icp_applied', {\n    configId,\n    requestedBy,\n    industries: selection.industries.length,\n    regions: selection.regions.length,\n    sizeBands: selection.sizeBands.length,\n    newlyGatedInSample: impact.impact.newlyGated,\n    sampled: impact.impact.sampled,\n  });\n\n  await writeIcpAuditRow(\n    client,\n    now,\n    `CONFIG_CHANGED \u00B7 Greenlight ICP applied \u00B7 ${selection.industries.length} industries, ${selection.regions.length} regions, ${selection.sizeBands.length} size bands`,\n    {\n      action: 'icp_applied',\n      requestedBy,\n      before,\n      after: toIcpConfigPatch(selection),\n      impact: {\n        sampled: impact.impact.sampled,\n        newlyGated: impact.impact.newlyGated,\n        newlyApproved: impact.impact.newlyApproved,\n        estimatedNewlyGated: impact.impact.estimatedNewlyGated,\n        averageScoreBefore: impact.impact.averageScoreBefore,\n        averageScoreAfter: impact.impact.averageScoreAfter,\n      },\n      rescoreRequested: rescore,\n      engineVersion: SCORING_ENGINE_VERSION,\n    },\n  );\n\n  const rescoreOutcome = rescore\n    ? await requestRescore({ client, store, now, requestedBy })\n    : {\n        started: false,\n        message:\n          'Existing leads keep the scores they already have. New and edited leads are judged against the new profile from now on; re-score existing leads from \"Score existing leads\" whenever you are ready.',\n      };\n\n  const updated = { ...record, ...toIcpConfigPatch(selection) };\n\n  return result(200, {\n    outcome: 'applied',\n    message: written.reviewRecorded\n      ? 'Profile saved. Every lead scored from now on is judged against it.'\n      : 'Profile saved. Greenlight could not stamp the review date on the record \u2014 the profile itself is in place, and this panel may keep showing the profile as unreviewed until the app metadata is next synced.',\n    status: readIcpStatus(\n      written.reviewRecorded\n        ? {\n            ...updated,\n            icpReviewedAt: now.toISOString(),\n            icpReviewDecision: ICP_REVIEW_DECISION_VALUES.applied,\n          }\n        : updated,\n    ),\n    impact: impact.impact,\n    selection,\n    warnings,\n    notes: impact.notes,\n    rescore: rescoreOutcome,\n  });\n};\n\n/**\n * Re-score existing leads by starting the backfill that already exists, in `all`\n * mode.\n *\n * Reused rather than reimplemented, and that is the whole point: the backfill\n * already paces itself against the rate limit, resumes after a crash, honours\n * decisions a colleague released by hand, and is visible while it runs. A second\n * bulk path built here would have to re-earn every one of those properties and\n * would get at most one of them right.\n */\nconst requestRescore = async ({\n  client,\n  store,\n  now,\n  requestedBy,\n}: IcpDeps): Promise<{ started: boolean; message: string }> => {\n  try {\n    const started = await startBackfill({\n      client,\n      store,\n      now,\n      mode: 'all',\n      requestedBy: `${requestedBy} (ICP change)`,\n    });\n\n    return {\n      started: started.body.outcome === 'started',\n      message: started.body.message,\n    };\n  } catch (error) {\n    logGreenlight('icp_rescore_start_failed', { error: describeError(error) });\n\n    return {\n      started: false,\n      message: `The profile was saved, but Greenlight could not start the re-score. Start it yourself from \"Score existing leads\". (${describeError(error)})`,\n    };\n  }\n};\n\n/**\n * Record that the workspace looked at this and chose to leave the profile open.\n *\n * This is not a dismissal and it is not \"remind me later\". The permissive\n * default is a legitimate answer \u2014 plenty of businesses genuinely do sell to\n * every industry in every country \u2014 and until now the record could not tell that\n * answer apart from nobody ever having been asked. A warning that cannot be\n * answered is a warning people learn to scroll past, which is how the original\n * defect would come back wearing a new surface.\n */\nexport const leaveIcpOpen = async ({\n  client,\n  now,\n  requestedBy,\n}: Pick<IcpDeps, 'client' | 'now' | 'requestedBy'>): Promise<IcpControlResult> => {\n  let record: Record<string, unknown> | null;\n\n  try {\n    record = (await loadIcpConfigRecord(client)).record;\n  } catch (error) {\n    return result(502, {\n      outcome: 'config_unreadable',\n      message: `Greenlight could not read its configuration record, so nothing was changed. (${describeError(error)})`,\n    });\n  }\n\n  if (record === null) {\n    return result(200, { outcome: 'config_missing', message: CONFIG_MISSING_MESSAGE });\n  }\n\n  const configId = configIdOf(record);\n\n  if (configId === null) {\n    return result(502, {\n      outcome: 'config_unusable',\n      message:\n        'Greenlight found a configuration record without an id, so it did not write to it.',\n    });\n  }\n\n  const written = await patchConfig(\n    client,\n    configId,\n    {},\n    {\n      icpReviewedAt: now.toISOString(),\n      icpReviewDecision: ICP_REVIEW_DECISION_VALUES.leftOpen,\n    },\n  );\n\n  if (!written.ok) {\n    return result(502, {\n      outcome: 'write_failed',\n      message: `Greenlight could not record that decision, so this panel will keep asking. (${written.error})`,\n      status: readIcpStatus(record),\n    });\n  }\n\n  logGreenlight('icp_left_open', { configId, requestedBy });\n\n  await writeIcpAuditRow(\n    client,\n    now,\n    'CONFIG_CHANGED \u00B7 Greenlight ICP deliberately left open',\n    {\n      action: 'icp_left_open',\n      requestedBy,\n      note: 'No industry, region or size filtering. Scores are built from contact quality and compliance only.',\n      engineVersion: SCORING_ENGINE_VERSION,\n    },\n  );\n\n  const updated = {\n    ...record,\n    icpReviewedAt: now.toISOString(),\n    icpReviewDecision: ICP_REVIEW_DECISION_VALUES.leftOpen,\n  };\n\n  return result(200, {\n    outcome: 'left_open',\n    message:\n      'Recorded. Industry, region and company size stay switched off, and scores continue to be built from contact quality and compliance alone. You can change this at any time.',\n    status: readIcpStatus(updated),\n  });\n};\n\n/* -------------------------------------------------------------------------- */\n/* Entry point                                                                 */\n/* -------------------------------------------------------------------------- */\n\nexport const runIcpCommand = async (\n  command: IcpCommand,\n  deps: IcpDeps,\n): Promise<IcpControlResult> => {\n  switch (command.action) {\n    case 'propose':\n      return proposeIcpForWorkspace(deps);\n    case 'preview':\n      return previewIcpForWorkspace({\n        ...deps,\n        selection: command.selection,\n        warnings: command.warnings,\n      });\n    case 'apply':\n      return applyIcpToWorkspace({\n        ...deps,\n        selection: command.selection,\n        warnings: command.warnings,\n        rescore: command.rescore,\n      });\n    case 'leave-open':\n      return leaveIcpOpen(deps);\n    default:\n      return readIcpReviewStatus(deps);\n  }\n};\n", "/**\n * Universal identifiers and durable keys for the backfill \u2014 the surface that\n * scores the leads which already existed when Greenlight was installed.\n *\n * Same permanence rule as `src/constants/universal-identifiers.ts` and\n * `src/constants/gate-queue-identifiers.ts`: every value here is written into\n * the customer's workspace at install time. Changing one does not rename an\n * entity, it orphans the old one and creates a second one alongside it \u2014 and for\n * a **cron** logic function that means two schedulers draining the same queue\n * against the same rate limit. Adding is safe; editing is a breaking change.\n *\n * A third constants file rather than a section in the second one, for the same\n * reason the second one exists: so the backfill can be developed without\n * touching a file the gate-queue and data-model workstreams are both editing.\n * `src/backfill/__tests__/backfill-identifiers.test.ts` runs the uniqueness\n * guard across all three files together, which is the failure mode splitting\n * them introduces.\n */\n\n/* -------------------------------------------------------------------------- */\n/* Logic functions                                                             */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The cron drain. One tick, one bounded chunk \u2014 see `BACKFILL_CHUNK_SIZE` in\n * `src/backfill/backfill-plan.ts` for the request arithmetic that sizes it.\n */\nexport const BACKFILL_CRON_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER =\n  'cb419da0-360d-4539-90a7-18b9a95a87fc';\n\n/**\n * The admin control surface: start, cancel, and read progress. An HTTP route\n * rather than a second cron because these are things a human does at a moment of\n * their choosing, and because the panel needs a synchronous answer to render.\n */\nexport const BACKFILL_CONTROL_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER =\n  'b4ed46c6-db60-4994-b499-2257c3a0615b';\n\n/* -------------------------------------------------------------------------- */\n/* Admin surface \u2014 command menu item -> front component -> logic function       */\n/* -------------------------------------------------------------------------- */\n\nexport const BACKFILL_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER =\n  '31e867f6-c35f-415d-9bcd-014ba9e8b365';\n\nexport const BACKFILL_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIER =\n  'bf258f37-9673-4a8d-870f-fecc4ab44d43';\n\n/**\n * HTTP route the control function listens on. Same two-constant shape as the\n * gate-queue actions: the front component posts to the `/s`-prefixed path, which\n * is how `RestApiClient` recognises an app route rather than a core REST call,\n * and declaring both here means a mismatch is a test failure rather than a 404\n * discovered in a live workspace.\n */\nexport const BACKFILL_ROUTE_PATH = '/greenlight/backfill';\nexport const BACKFILL_CLIENT_PATH = `/s${BACKFILL_ROUTE_PATH}`;\n\n/* -------------------------------------------------------------------------- */\n/* Never-scored view \u2014 the defect, made visible                                */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Distinct from the existing \"Greenlight Unscored (Failed Open)\" view, and the\n * distinction is the whole bug report.\n *\n *   - `greenlightDecision = UNSCORED` means *the engine looked and could not\n *     score it*. That view is an error backlog.\n *   - `greenlightDecision` **empty** means *nothing has ever looked at it*.\n *     Before this feature existed there was no such view, so 1200 of 1205 people\n *     on a dev workspace sat in a state with no name and no surface, while the\n *     gate queue read empty \u2014 which looks exactly like \"everything is clean\".\n *\n * Collapsing the two into one view would have hidden the second inside the\n * first's error semantics. They are different questions with different answers.\n */\nexport const NEVER_SCORED_VIEW_UNIVERSAL_IDENTIFIER =\n  'eff19c4d-408f-40f4-ab25-99b24658a191';\n\nexport const NEVER_SCORED_NAVIGATION_MENU_ITEM_UNIVERSAL_IDENTIFIER =\n  '0b08bfea-6fc5-4219-ac28-d55775d36be5';\n\nexport const NEVER_SCORED_VIEW_FILTER_UNIVERSAL_IDENTIFIER =\n  '4de7f659-091d-4fba-9488-4530b1a79ac0';\n\nexport const NEVER_SCORED_VIEW_SORT_UNIVERSAL_IDENTIFIER =\n  '58b70c61-855c-4d4a-b824-91eed23c37c9';\n\nexport const NEVER_SCORED_VIEW_FIELD_UNIVERSAL_IDENTIFIERS = {\n  name: '4b1f9d1c-3f13-4d36-913d-4a23b6a3895c',\n  greenlightDecision: '4a6f7fc7-6a99-48be-9d52-af900b1a1900',\n  emails: 'b7593fb9-d8fe-47b8-b0eb-6c84ef9c8d0a',\n  company: '65fd67f1-da62-4b76-b64b-36933e03d02c',\n  createdAt: '8d5e193c-dd82-46b6-b28b-9e1876a70f7f',\n} as const;\n\n/* -------------------------------------------------------------------------- */\n/* Durable state keys                                                          */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Where the backfill's progress lives, in Twenty's app key-value storage.\n *\n * `scope: 'WORKSPACE'` is load-bearing and matches `release-marker-store.ts`:\n * progress is per-workspace, and a read under a different scope silently finds\n * nothing \u2014 which would read as \"no backfill has ever run\" and restart one.\n *\n * The key is versioned (`:v1`) because the stored shape is a contract between\n * releases. A future release that changes the state shape incompatibly bumps the\n * suffix rather than trying to migrate a half-finished run in place; the old key\n * is then simply an orphan, and an orphan is a much better failure than a run\n * that resumes against a cursor it no longer understands.\n */\nexport const BACKFILL_STATE_KEY = 'greenlight:backfill:state:v1';\n", "import { kv } from 'twenty-sdk/logic-function';\n\nimport { BACKFILL_STATE_KEY } from 'src/constants/backfill-identifiers';\nimport type { BackfillStateStore } from 'src/logic-functions/backfill-run';\n\n/**\n * The real backfill state store: Twenty's app key-value storage.\n *\n * Four lines in a file of its own, exactly like `release-marker-store.ts` and for\n * exactly the same reason: `kv` is only importable from\n * `twenty-sdk/logic-function`, and `backfill-run.ts` stays SDK-free so the whole\n * resume-after-crash story can be driven by a fake in unit tests. A backfill's\n * correctness is entirely about what happens across process boundaries, and a\n * test that cannot simulate a process boundary cannot test it.\n *\n * `scope: 'WORKSPACE'` is not a detail. Progress is per-workspace, and a read\n * under a different scope silently returns `null` \u2014 which this code reads as \"no\n * backfill has ever run\" and would answer by starting another one, forever.\n *\n * Neither method catches. The caller decides what an unreadable or unwritable\n * store means, and it decides differently for the two: a failed read is \"there is\n * no run, do nothing this tick\", while a failed *write* must abandon the tick\n * before it does any scoring, because work whose position cannot be recorded is\n * work that will be done again on every tick from now on.\n */\nexport const kvBackfillStateStore: BackfillStateStore = {\n  read: () => kv.get<unknown>(BACKFILL_STATE_KEY, { scope: 'WORKSPACE' }),\n  write: async (state) => {\n    await kv.set(BACKFILL_STATE_KEY, state, { scope: 'WORKSPACE' });\n  },\n};\n"],
  "mappings": "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",
  "names": ["s", "n", "e", "t", "t", "n", "r", "e", "t", "n", "isRecord", "displayName", "isFiniteNumber", "b", "resolved", "b", "result", "b", "b", "isRecord", "roundTo", "configured", "result", "isRecord", "percent", "response"]
}
