@listen-fire/engine-contracts 0.12.0-rc.2 → 0.12.0-rc.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/adapter.d.ts +1651 -0
  2. package/dist/adapter.js +157 -0
  3. package/dist/awaitable.d.ts +65 -0
  4. package/dist/awaitable.js +13 -0
  5. package/dist/catalog.d.ts +1443 -0
  6. package/dist/catalog.js +1541 -0
  7. package/dist/conformance/recording.d.ts +12 -12
  8. package/dist/document_sources.d.ts +14 -0
  9. package/dist/document_sources.js +30 -0
  10. package/dist/event.d.ts +2 -2
  11. package/dist/expression/expression.d.ts +10 -0
  12. package/dist/expression/expression.js +207 -0
  13. package/dist/expression/schemas.d.ts +869 -0
  14. package/dist/expression/schemas.js +266 -0
  15. package/dist/handbook_section.d.ts +79 -0
  16. package/dist/handbook_section.js +83 -0
  17. package/dist/index.d.ts +14 -0
  18. package/dist/index.js +21 -3
  19. package/dist/model_names.d.ts +77 -0
  20. package/dist/model_names.js +53 -0
  21. package/dist/mutation_context.d.ts +55 -0
  22. package/dist/mutation_context.js +91 -0
  23. package/dist/plugin_spec.d.ts +36 -0
  24. package/dist/plugin_spec.js +157 -0
  25. package/dist/protocol.d.ts +298 -16
  26. package/dist/protocol.js +151 -16
  27. package/dist/slots/browser.d.ts +131 -0
  28. package/dist/slots/browser.js +79 -0
  29. package/dist/slots/files.d.ts +31 -0
  30. package/dist/slots/files.js +29 -0
  31. package/dist/slots/index.d.ts +7 -0
  32. package/dist/slots/index.js +26 -0
  33. package/dist/slots/judge.d.ts +26 -0
  34. package/dist/slots/judge.js +26 -0
  35. package/dist/slots/models.d.ts +58 -0
  36. package/dist/slots/models.js +38 -0
  37. package/dist/slots/ocr.d.ts +28 -0
  38. package/dist/slots/ocr.js +37 -0
  39. package/dist/slots/wake.d.ts +12 -0
  40. package/dist/slots/wake.js +14 -0
  41. package/dist/slots/wire.d.ts +47 -0
  42. package/dist/slots/wire.js +46 -0
  43. package/dist/trigger_config.d.ts +154 -0
  44. package/dist/trigger_config.js +116 -0
  45. package/dist/uniqueness.d.ts +77 -0
  46. package/dist/uniqueness.js +114 -0
  47. package/package.json +14 -4
@@ -0,0 +1,1651 @@
1
+ import type { ExternalRecordRef, FieldEvidence, Resource } from './records';
2
+ import type { TriggerEvent, TriggerType } from './event';
3
+ import type { MutationContext } from './mutation_context';
4
+ import type { AwaitableCapability } from './awaitable';
5
+ import type { Actor } from './mutation_context';
6
+ import type { UniquenessConstraints } from './uniqueness';
7
+ import type { ConfigBlock } from './trigger_config';
8
+ import type { HandbookSection } from './handbook_section';
9
+ import type { WireCheckInboundInputShape } from './protocol';
10
+ import type { SchemaEntryPoint, SchemaTypeDescriptor, SourcePosition } from './catalog';
11
+ import type { Expression } from '@listen-fire/movement-lang/expression/types';
12
+ export type { ConfigBlock, TriggerConfigField } from './trigger_config';
13
+ export { fileDescriptorOf, fileHandle, fileValue, fileValueOf, filesAtType, isFileValue, ownedFileSource } from './file_ref';
14
+ export type { EarlierFileSource, FileRef, FileSource, FileValue } from './file_ref';
15
+ export type { ExternalRecordRef, Fact, FieldEvidence, Resource, ResourceProvenance } from './records';
16
+ /**
17
+ * Minimal user descriptor returned by Listen-Fire's `resolveActingUser`
18
+ * (acting_user/resolve.ts) and consumed by the engine's meta resolver to
19
+ * populate `@user_email`,
20
+ * `@user_name`, and `@user_id`. Kept shape-loose (name optional) so
21
+ * adapters that can only surface an email don't have to fabricate a
22
+ * display name.
23
+ *
24
+ */
25
+ export interface ActingUser {
26
+ id: string;
27
+ email: string;
28
+ name?: string | null;
29
+ }
30
+ /**
31
+ * Raw event-side actor identity returned by `Adapter.extractActor`. Distinct
32
+ * from `ActingUser`: an `ActorIdentity` is the external originator of the
33
+ * event (the `From:` address on an email, the Slack user that posted, the
34
+ * Attio actor that wrote a record) regardless of whether they are a
35
+ * registered Listen-Fire team member. Powers `@actor_email` / `@actor_name` /
36
+ * `@actor_id` so authors can compare "the team member responsible for this
37
+ * dispatch" (`@user_*`) with "who actually originated the event" (`@actor_*`).
38
+ *
39
+ * Parsed from the event payload — no DB lookups — because actor identity
40
+ * is a parse, not an auth decision. The method is async only so a remote
41
+ * adapter can answer it over the wire; local impls are still pure parses.
42
+ *
43
+ */
44
+ export interface ActorIdentity {
45
+ /** Primary key in the source system (email address, phone number,
46
+ * Slack user id, Attio actor id, etc.). */
47
+ identifier: string;
48
+ /** How this identity resolves to a Listen-Fire team user — the only thing the
49
+ * resolution logic cares about, independent of which adapter produced it:
50
+ * • `'email'` — resolvable to a team user by email (the address is on
51
+ * `identifier` or `email`).
52
+ * • `'phone'` — resolvable to a team user by phone number.
53
+ * • `'opaque'` — neither; a raw external id with no resolved email/phone,
54
+ * so it maps to no Listen-Fire user (e.g. a bare Slack/Attio id from the
55
+ * pure-parse `extractActor`). Resolvers skip it rather than guessing. */
56
+ scheme: 'email' | 'phone' | 'opaque';
57
+ /** Which adapter sourced this identity — provenance only; the resolution
58
+ * logic never branches on it (it branches on `scheme`). */
59
+ adapterType?: string;
60
+ /** Email address when the source carries one (every email event; Slack
61
+ * messages don't have this without an API call). */
62
+ email?: string;
63
+ /** Display name (the friendly part of `"Ada" <h@x>`; Slack display
64
+ * name; etc.). */
65
+ name?: string;
66
+ /** Fallback display label when `name` isn't available. */
67
+ label?: string;
68
+ }
69
+ /**
70
+ * An ordered actor candidate parsed from an inbound event by the adapter's
71
+ * `getActorCandidates`. Distinct from `ActorIdentity` (the raw event
72
+ * originator surfaced via `@actor_*`): a candidate is a *resolution input*
73
+ * Listen-Fire's `resolveActingUser` maps to a team user. The `source` tag drives
74
+ * the resolution order — `originator` candidates (who sent it) are matched
75
+ * against non-service users first; `relay` candidates (the forwarding /
76
+ * service inbox that delivered it) are matched against service users only.
77
+ *
78
+ */
79
+ export interface ActorCandidate {
80
+ identity: ActorIdentity;
81
+ /** who-sent-it (`originator`) vs forwarding/service-inbox (`relay`). */
82
+ source: 'originator' | 'relay';
83
+ }
84
+ /**
85
+ * The protocol method names a write goes through. Used as the capability
86
+ * basis for "is this adapter a valid TG target" — a manifest whose `methods`
87
+ * include any of these writes meaningfully (`createRecord` is the canonical
88
+ * one). Single source of truth, so writability can't drift from what the
89
+ * adapter actually implements.
90
+ *
91
+ */
92
+ export declare const WRITE_METHODS: readonly ["createRecord", "updateRecord", "deleteRecord"];
93
+ /**
94
+ * Static, construction-free description of an adapter — identical in shape for
95
+ * LOCAL (built-in) and REMOTE (installed) adapters. Lets the framework
96
+ * enumerate + filter adapters in bulk without constructing them (the prior
97
+ * `WRITABLE_ADAPTER_TYPES` / `ADAPTER_CREDENTIAL_TYPE` whitelists existed only
98
+ * because capabilities were instance fields that needed construction).
99
+ *
100
+ * `methods[]` is THE capability basis: it lists the protocol method names the
101
+ * adapter genuinely implements (mirroring the remote `WireManifestResult.methods`).
102
+ * `writable` / `readable` and every optional-method gate derive from it — no
103
+ * separate flags, so they can't drift from the implementation. A local adapter
104
+ * exports one of these as a static const registered alongside its factory; a
105
+ * remote adapter's stored `WireManifestResult` maps onto it. Read uniformly via
106
+ * `getAdapterManifest` / `listAdapterManifests`.
107
+ *
108
+ */
109
+ /**
110
+ * Functional grouping for the public integrations grid — lets the marketing
111
+ * site sort adapters by what they're for rather than alphabetically.
112
+ */
113
+ export type IntegrationCategory = 'CRM' | 'Email' | 'Messaging' | 'Files' | 'Spreadsheets' | 'Meetings';
114
+ export interface AdapterManifest {
115
+ /** Stable slug the engine routes on — matches `Adapter.adapterType`. */
116
+ adapterType: string;
117
+ /** Human label for the picker, free of internal jargon. */
118
+ displayName: string;
119
+ /**
120
+ * The provider's public website — surfaced as a link on the integrations
121
+ * grid. Omitted for built-ins with no external provider (e.g. inbound email).
122
+ */
123
+ website?: string;
124
+ /**
125
+ * Functional grouping for the integrations grid (CRM, Files, …). Omitted ⇒
126
+ * the integration falls into an "Other" bucket.
127
+ */
128
+ category?: IntegrationCategory;
129
+ /**
130
+ * One or two honest sentences about what the external system is and what
131
+ * connecting it lets the team do. Surfaced on the Adapters page, the
132
+ * public integrations grid, and any other catalogue UI; written for users,
133
+ * not engineers. Optional only so remote/wire manifests without one stay
134
+ * admissible — every built-in adapter should declare it.
135
+ */
136
+ description?: string;
137
+ /**
138
+ * Movement-authoring guidance for the agent — adapter-specific tips,
139
+ * caveats, or field-usage hints that help the authoring agent write correct
140
+ * movements. NOT surfaced on any public or user-facing page (the integrations
141
+ * grid and the adapters catalogue both omit it). Included only where the agent
142
+ * reads adapter info at authoring time (e.g. `describeMovementInstance`).
143
+ * Optional; omit when the description alone is sufficient.
144
+ */
145
+ authoringHints?: string;
146
+ /**
147
+ * A conceptual handbook section for this system — what an author should know
148
+ * BEFORE instantiating it (idioms, the choice between two approaches, the
149
+ * behaviour that surprises). Assembled into the automation handbook as a
150
+ * `system:<adapterType>` chapter, so it is bound by the same prose and
151
+ * probe contract as every hand-written chapter.
152
+ *
153
+ * A different tier from `authoringHints`, which is per-instance guidance
154
+ * served AFTER instantiation (`describeMovementInstance`); both may be
155
+ * declared.
156
+ */
157
+ handbookSection?: HandbookSection;
158
+ /** Triggers this adapter declares it can fire (mirrors `Adapter.supportedTriggers`). */
159
+ supportedTriggers: readonly TriggerType[];
160
+ /**
161
+ * Protocol method names the adapter implements — the capability basis.
162
+ * `writable` (includes a write method) and optional-method gating both read
163
+ * this. Mirrors the remote `WireManifestResult.methods`.
164
+ */
165
+ methods: readonly string[];
166
+ /**
167
+ * External-service credential type "enough to connect". Omitted ⇒ the adapter
168
+ * needs no credential (the KG is intrinsic; email / WhatsApp carry inbound
169
+ * creds on the legacy `pipeline_input`). Relocated from `ADAPTER_CREDENTIAL_TYPE`.
170
+ */
171
+ requiredCredentialType?: string;
172
+ /**
173
+ * The credentials a module declares it needs, by name (its protocol
174
+ * manifest's `credentials`). A connection to it names one credential that
175
+ * fills one of these — the provider credential, never the install's own
176
+ * server secret. Absent ⇒ the module declares none.
177
+ */
178
+ declaredCredentials?: readonly string[];
179
+ /**
180
+ * `recordType` of the adapter's `meta` root — the signal that the adapter
181
+ * supports a one-off meta-rooted fan-out ("sync everything now" → the Manual
182
+ * source). Mirrors `Adapter.metaRecordType`; omitted by adapters with no meta
183
+ * root.
184
+ */
185
+ metaRecordType?: string;
186
+ /**
187
+ * Trigger-kind aliases (uppercase `PipelineInputType`-style values like
188
+ * `CUSTOM_EMAIL`, `ATTIO`) that route to this adapter. The registry builds
189
+ * its kind→slug alias map from these, so the aliasing can't drift from what
190
+ * the adapters declare. Multiple kinds may share one adapter (all
191
+ * email-style inbound channels resolve to the email adapter); omit when the
192
+ * adapter has no inbound trigger kind. The canonical slug
193
+ * (`adapterType`) is always self-aliasing and need not be listed.
194
+ */
195
+ triggerKinds?: readonly string[];
196
+ /**
197
+ * Other names this same adapter answers to, in movement source and
198
+ * everywhere else a name is resolved. Unlike `triggerKinds` (uppercase
199
+ * dispatch values on stored trigger rows) these are names an AUTHOR writes:
200
+ * email declares `mailgun` and `resend` so a deployment can say which
201
+ * provider its mail runs through without that changing what the adapter
202
+ * does. Registered like a kind alias, and held to the same uniqueness —
203
+ * no two adapters may answer to one name.
204
+ */
205
+ aliases?: readonly string[];
206
+ /**
207
+ * A deployment-level setting this adapter needs and does not have.
208
+ *
209
+ * Not a credential (`requiredCredentialType` is that, and it is per team):
210
+ * this is configuration of the installation itself, like the address inbound
211
+ * mail is delivered to. A movement that references the adapter is recorded
212
+ * as UNVERIFIED while it is missing, so an author is told rather than
213
+ * shipping something that can never fire. Absent ⇒ nothing missing.
214
+ */
215
+ configurationGap?: string;
216
+ /**
217
+ * Config BLOCKS rendered for this adapter's TRIGGER role — the per-trigger
218
+ * config on the automation detail page. An ordered {@link ConfigBlock} list:
219
+ * value blocks (input/slug/select) collect values that persist to
220
+ * `automations.trigger.config`; presentation blocks (section) frame them; an
221
+ * action block (rare here) dispatches to an app handler. Carried on the
222
+ * manifest so construction-free readers (the movement catalog's listen
223
+ * vocabulary, provisioning, the adapters/trigger views) can see it without
224
+ * building the adapter. The adapter instance's `triggerConfig` references it.
225
+ *
226
+ * This is the widened successor to the former `triggerConfigSchema`
227
+ * (`TriggerConfigField[]`) — value blocks ARE that union; section/action are
228
+ * the new arms. It is also where an adapter declares its INBOUND ROUTING KEY
229
+ * (a value block marked `routingKey`) — the de-named successor to the former
230
+ * `inboundChannel: 'forwarding-address'` flag. An adapter whose events arrive
231
+ * addressed by a key (email's plus-suffix `key`) declares that block here,
232
+ * and the framework routes generically off the declaration; there is no named
233
+ * "forwarding address" concept in the engine.
234
+ *
235
+ * Omitted ⇒ the adapter's trigger role needs no user-editable config.
236
+ *
237
+ */
238
+ triggerConfig?: readonly ConfigBlock[];
239
+ /**
240
+ * Config BLOCKS rendered for this adapter's CONSTRUCTION role — shown at an
241
+ * adapter-instance slot in the movement editor (and the chat equivalent).
242
+ * Value blocks bind construction args; ACTION blocks are the connect
243
+ * affordances (the generalised "+"). Reserved here as the single home for
244
+ * the construction-arg UI and the connect actions; the first ship uses it
245
+ * only for `action` blocks. Omitted ⇒ no construction-time config UI.
246
+ *
247
+ */
248
+ construction?: readonly ConfigBlock[];
249
+ /**
250
+ * The adapter's subscribable-event vocabulary — the named events external
251
+ * subscriptions can be registered for (attio: `record.created` /
252
+ * `record.updated` / `record.deleted`; the KG: its mutation change kinds
253
+ * `create` / `update` / `delete`). One declaration drives:
254
+ * - listen-config validation: a `listen to <instance> { events: […] }`
255
+ * value outside this set is rejected at check time (the movement
256
+ * catalog projects it as the `events` key's value vocabulary);
257
+ * - subscription provisioning: `ensureEventSubscription` receives the
258
+ * union of the events the team's listens actually want.
259
+ * Omitted ⇒ the adapter declares no subscribable events (its channel is
260
+ * push-free or interpreted elsewhere — inbound email routed by its
261
+ * `key`, the web Send box).
262
+ */
263
+ subscribableEvents?: readonly string[];
264
+ /**
265
+ * The `events:` selection a listen WITHOUT one is subscribed to — the
266
+ * adapter's own dispatch default, declared so the checker derives the same
267
+ * event type the runtime will deliver. WhatsApp is the motivating case: a
268
+ * config-less listen receives MESSAGES ONLY (`triggerAcceptsWhatsappKind`),
269
+ * so reactions/locations never fire a movement that didn't opt in — and the
270
+ * checker must not type a config-less listen as delivering them. Absent ⇒
271
+ * every subscribable event (the attio/airtable registration default).
272
+ */
273
+ defaultSubscribedEvents?: readonly string[];
274
+ /**
275
+ * Listen-config keys whose VALUES jointly scope an external subscription
276
+ * channel, beyond the (adapter, credential) pair. Airtable's `['base',
277
+ * 'table']` is the motivating case: webhooks are per-(base, table), so two
278
+ * listens on different tables need distinct registrations. The listen
279
+ * reconciler folds these config values into the channel key (one
280
+ * `webhook_subscription` row + external webhook per distinct scope) and passes
281
+ * them as `EnsureEventSubscriptionInput.scope`. Omitted ⇒ one channel per
282
+ * (adapter, credential) — the Attio default.
283
+ *
284
+ */
285
+ subscriptionScopeKeys?: readonly string[];
286
+ /**
287
+ * Additional listen routing-config keys this adapter declares, beyond
288
+ * the derived ones (the inbound routing `key` from `triggerConfigSchema`,
289
+ * the subscribable `events`). Each entry names the key, whether a listen MUST carry it,
290
+ * and an optional value format the checker validates statically
291
+ * (`'cron'` — a five-field cron expression, see movement-lang/cron).
292
+ * The cron adapter's `schedule` is the motivating case:
293
+ * `listen to timer { schedule: "0 9 * * 1" } fire digest`.
294
+ */
295
+ listenConfig?: readonly ListenConfigKey[];
296
+ /**
297
+ * Plain-language truth about WHAT a listener on this adapter actually fires
298
+ * on — surfaced to the authoring agent via `describeInstance` so it grounds
299
+ * its claims instead of inventing them. The motivating bug: an agent told a
300
+ * user a Slack listener runs on "every message in any channel" when Slack
301
+ * only delivers the events the bot is subscribed to (typically @-mentions).
302
+ * Write the honest envelope and the caveats: what arrives by default, what
303
+ * needs extra setup/scope, and "don't assume X — confirm" where the real
304
+ * behaviour depends on external config the framework can't see. Omitted ⇒
305
+ * the agent has no trigger prose and must confirm scope with the user
306
+ * rather than asserting it.
307
+ */
308
+ triggerExpectation?: string;
309
+ /**
310
+ * Ownership gating for inbound events (message-write-unification §6.4b):
311
+ * when true, the dispatcher processes an inbound event ONLY if its actor
312
+ * resolves to a REGISTERED team user (the adapter's `getActorCandidates`
313
+ * chain matched by `resolveActingUser`, WITHOUT trigger context — creator
314
+ * override/fallback never pass the gate). The bot is never a registered
315
+ * user, so self-echo drops for free; an unregistered person is dropped by
316
+ * the same rule. Dropped events are recorded (receipt marked suppressed,
317
+ * replayable), never silent. Set by adapters whose inbound channel is
318
+ * globally addressable (Slack: anyone in the workspace can @-mention the
319
+ * bot); adapters whose inbound is already identity-gated upstream omit it
320
+ * (WhatsApp's sender→team resolution; Telegram's /start handshake binding
321
+ * ON THE BUILT-IN SHARED BOT — a team running its own bot has no such
322
+ * gate, so a BYO-bot team's inbound is not actually identity-gated here).
323
+ */
324
+ inboundRequiresRegisteredActor?: boolean;
325
+ /** True when this adapter's schema comes from LIVE introspection of the
326
+ * connected workspace (attio, airtable, sheets, affinity) — a
327
+ * describeInstance costs upstream API calls, so callers should scope with
328
+ * `types`. Absent/false = the schema is static and describing is free. */
329
+ introspectedSchema?: boolean;
330
+ /**
331
+ * Construction args that pick the instance's ENTRY POSITION — the node the
332
+ * cursor starts at, instead of the meta node (Sheets' `spreadsheet:`,
333
+ * Airtable's `base:`). Each names a construction arg and the meta-node
334
+ * COLLECTION whose members are both its value enum and the node it lands on.
335
+ * Optional args: omitted ⇒ the default (meta) position. The catalog surfaces
336
+ * them as accepted construction args, types their value against the
337
+ * collection, and threads the chosen value into construction (introspection
338
+ * AND runtime). Omitted ⇒ the adapter has no entry position.
339
+ */
340
+ positionArgs?: readonly PositionArg[];
341
+ /**
342
+ * Per-adapter DISPLAY vocabulary for the read-only movement renderer and
343
+ * any other human-facing surface (the integrations grid, the story page) —
344
+ * same ownership story as {@link AdapterManifest.handbookSection}: the
345
+ * adapter is the only thing that knows how to talk about itself, so the
346
+ * renderer core stays adapter-blind and consumes only what is declared
347
+ * here. Optional; a missing field renders a generic composed fallback
348
+ * rather than failing.
349
+ *
350
+ */
351
+ vocabulary?: AdapterVocabulary;
352
+ }
353
+ /**
354
+ * Monochrome brand mark for an adapter — the integrations grid and (later)
355
+ * the movement renderer render it with `fill: currentColor` unless `fill` is
356
+ * false (stroke marks). Ported verbatim from the product app's
357
+ * `service-icon.tsx`; the web app keeps its own copy (a later sweep unifies
358
+ * them) so this shape must stay byte-identical to what `apps/web` expects.
359
+ */
360
+ export interface BrandIcon {
361
+ d: string;
362
+ fill?: boolean;
363
+ viewBox?: string;
364
+ }
365
+ /**
366
+ * One candidate sentence for an adapter event. `{slot}` tokens are filled
367
+ * from the trigger/listen config using the ADAPTER'S OWN config key names
368
+ * (its listen-config / narrowing / routing keys) — the resolver
369
+ * (`services/translation_graph/vocabulary.ts`) does plain string
370
+ * substitution only, never interprets what a slot means. A template whose
371
+ * slot has no matching (non-empty string) config value is skipped, so an
372
+ * adapter can express "prefer the fuller phrasing, fall back to the terser
373
+ * one" as an ORDERED list (email: full address → routing tag → bare).
374
+ */
375
+ export interface EventPhraseTemplate {
376
+ template: string;
377
+ }
378
+ /**
379
+ * The adapter's event-phrasing vocabulary — "When an email arrives at …",
380
+ * "When a record changes in Attio". Keyed by the adapter's own
381
+ * `subscribableEvents` names where it has discrete events (Attio's
382
+ * `record.created`, the KG's `create`); `default` is rendered when the
383
+ * config's selected events don't match a more specific key, or the adapter
384
+ * has no discrete event vocabulary at all (email, Slack — one implicit
385
+ * channel). A manifest with no entry for the resolved key falls through to
386
+ * the renderer core's generic composed phrase, never a per-adapter case
387
+ * living outside the manifest.
388
+ */
389
+ export type AdapterEventPhraseVocabulary = Record<string, readonly EventPhraseTemplate[]>;
390
+ interface AdapterVocabulary {
391
+ /** Absorbs the former core-side `BRAND_ICONS` map — see
392
+ * `adapters/registry.ts`'s `getBrandIcon`. */
393
+ icon?: BrandIcon;
394
+ /** Absorbs `describeSource()`'s per-adapter switch cases. */
395
+ eventPhrase?: AdapterEventPhraseVocabulary;
396
+ }
397
+ /** One entry-position construction arg — see {@link AdapterManifest.positionArgs}. */
398
+ interface PositionArg {
399
+ /** The construction-arg name, e.g. `spreadsheet`. */
400
+ name: string;
401
+ /** The meta-node collection whose members are the value enum and the landed
402
+ * node (Sheets: `Spreadsheet`). */
403
+ optionsFrom: string;
404
+ /** Author-facing label (defaults to `name`). */
405
+ label?: string;
406
+ }
407
+ /** One adapter-declared listen routing-config key (see
408
+ * `AdapterManifest.listenConfig`). */
409
+ export interface ListenConfigKey {
410
+ key: string;
411
+ /** A listen on this adapter must carry the key. */
412
+ required?: boolean;
413
+ /** Static value validation the movement checker applies. `'cron'` = a
414
+ * five-field cron expression; `'timezone'` = an IANA time-zone id;
415
+ * `'fields'` = a list of BARE property names, validated against the position
416
+ * the listen's address lands on (the changed-attribute filter — any adapter
417
+ * with a "only fire when one of these changed" surface declares it);
418
+ * `'strings'` = a non-empty list of non-empty quoted strings (Gmail's
419
+ * `labels`). */
420
+ format?: 'cron' | 'timezone' | 'fields' | 'strings';
421
+ /**
422
+ * This key is one HOP of the address that narrows the event's record edge.
423
+ *
424
+ * A listen is shorthand for a WHERE. `listen to at { base: "appDevLoop",
425
+ * table: "tblDeals" }` says which table the event's `record` edge lands on,
426
+ * so the author must not have to restate it in the movement — the config IS
427
+ * the address.
428
+ *
429
+ * The keys that declare `narrows` form a PATH, in declaration order, each hop
430
+ * selecting a member of `collection` whose published data has `matchField`
431
+ * equal to the config value:
432
+ *
433
+ * -[:Base WHERE `Id` == "appDevLoop"]->-[:Table WHERE `Id` == "tblDeals"]->
434
+ *
435
+ * A path, NOT one hop with a compound WHERE — that is the whole cost
436
+ * argument. Read as one hop, "narrowed by (base, table)" must know the valid
437
+ * (base, table) pairs, which is a `listTables` per base: the 1+N this plan
438
+ * exists to kill. As a path the base resolves first (1 call) and only THAT
439
+ * base's tables are walked (1 call).
440
+ *
441
+ * The final hop's `collection` is the type the record edge narrows FROM —
442
+ * an event edge targeting it is retargeted at whatever the walk landed on.
443
+ *
444
+ */
445
+ narrows?: {
446
+ /** The meta-graph collection whose members this hop selects from
447
+ * (Airtable's `Base`, then `Table`). */
448
+ collection: string;
449
+ /** The field of a member's PUBLISHED DATA the config value is matched
450
+ * against. A listen names things by ID while type-space addresses by NAME,
451
+ * so this is normally the id field — and the adapter must publish it on
452
+ * the member's position or nothing can narrow. */
453
+ matchField: string;
454
+ };
455
+ }
456
+ export interface EnsureEventSubscriptionInput {
457
+ /** The events the subscription should now cover — the union of every
458
+ * listen's `events` config on this (adapter, credential[, scope]) channel,
459
+ * defaulting to the manifest's full `subscribableEvents`. */
460
+ events: string[];
461
+ /** The stable per-channel URL the source should POST to. */
462
+ callbackUrl: string;
463
+ /** The registration currently held for this URL, when one exists — the
464
+ * diff-sync input. `events` is what the source is registered for now. */
465
+ current?: {
466
+ externalId?: string;
467
+ events: string[];
468
+ };
469
+ /**
470
+ * The channel's adapter-declared scope (the `subscriptionScopeKeys` config
471
+ * values) — e.g. Airtable's `{ base, table }`. Empty/absent for adapters
472
+ * whose channel is just (adapter, credential). The adapter reads what it
473
+ * declared; the reconciler passes it through opaquely.
474
+ */
475
+ scope?: Record<string, string>;
476
+ }
477
+ /** What the source issued — the platform persists it on the subscription
478
+ * row (the secret verifies inbound signatures). Returning `undefined`
479
+ * means "nothing registered" (a documented no-op adapter). */
480
+ export interface EventSubscriptionRegistration {
481
+ /** Source-issued subscription id, when registered via API. */
482
+ externalId?: string;
483
+ /** HMAC secret for inbound signature verification, when issued. */
484
+ secret?: string;
485
+ }
486
+ export interface RemoveEventSubscriptionInput {
487
+ callbackUrl: string;
488
+ /** The source-issued id held for this URL, when one exists. */
489
+ externalId?: string;
490
+ /** The channel's adapter-declared scope (mirrors
491
+ * {@link EnsureEventSubscriptionInput.scope}) — Airtable's per-base delete
492
+ * needs the `base` to address the webhook. Absent for unscoped channels. */
493
+ scope?: Record<string, string>;
494
+ }
495
+ export interface Adapter {
496
+ /** Stable identifier — matches `MutationContext.source.adapterType`. */
497
+ readonly adapterType: string;
498
+ /**
499
+ * The AWAITABLE capability (asks-as-adapter §A) — present ONLY on adapters
500
+ * that advertise awaitable edges (`await x-[:E]->`). It answers whether an
501
+ * awaited edge has resolved and, if not, the key that will wake it; the engine
502
+ * keeps the correlation itself. Absent on every ordinary adapter.
503
+ * Delivery-orthogonal (P12) — this is not rendering or delivery.
504
+ */
505
+ readonly awaitable?: AwaitableCapability;
506
+ /**
507
+ * Triggers this adapter declares it can fire (P11). Poll is universal
508
+ * fallback, not framework default. Snapshot/changes-feed/webhook/mutation
509
+ * are per-adapter opt-in.
510
+ */
511
+ readonly supportedTriggers: readonly TriggerType[];
512
+ /**
513
+ * Whole-adapter capability declaration — can the source traverse incoming
514
+ * edges, carry edge properties, expose resources? Static (no credential),
515
+ * so it's a plain method returning a constant. The engine consults it at
516
+ * evaluation time; per-edge / per-field capability lives on `describe()`.
517
+ *
518
+ */
519
+ runtimeCapabilities(): RuntimeCapabilities;
520
+ /**
521
+ * Lightweight catalog of entry-point types: those the editor's "Add
522
+ * action" picker can offer as a starting point (writable types) plus
523
+ * any types that author-time UI needs to enumerate (e.g. Attio's
524
+ * webhook_event meta type). NO per-type API calls — adapters
525
+ * answer this from a single `listObjects` call (or hardcoded for
526
+ * the KG / synthetic types).
527
+ */
528
+ listEntryPoints(): Promise<SchemaEntryPoint[]>;
529
+ /**
530
+ * Full descriptor for a single type — fields, references, scope,
531
+ * labelTemplate. The editor calls this on-demand as the user
532
+ * navigates the schema; adapters fetch the per-type attributes
533
+ * lazily. Returns null when the typeId isn't recognized.
534
+ */
535
+ describe(typeId: string): Promise<SchemaTypeDescriptor | null>;
536
+ /**
537
+ * Forget whatever this adapter has cached about its system's SHAPE — the
538
+ * field catalog, the collections, the workspace identity.
539
+ *
540
+ * Called where a person has just had reason to change it: the forced refresh
541
+ * behind `describeConnection`, and a credential being (re)connected. An
542
+ * adapter that caches nothing has nothing to do here and may leave it out.
543
+ */
544
+ invalidateSchemaCache?(): void | Promise<void>;
545
+ /**
546
+ * Walk the META-GRAPH of types (optional).
547
+ *
548
+ * Introspection is a walk, not a dump: from the schema root (`meta`), the
549
+ * types reachable independently; from a type node, its fields and the edges
550
+ * leaving it; and so on, recursively. `edgesFrom` is that one primitive —
551
+ * `listEntryPoints()` is `edgesFrom(meta)` and `describe(T)` is
552
+ * `edgesFrom(T)`, which is why the three eventually collapse into it.
553
+ *
554
+ * A POSITION IS A PATH from the root. That is the whole point: it carries
555
+ * whatever identifies the node in the source graph, so following an edge
556
+ * costs one upstream call rather than a re-derivation. An Airtable table's
557
+ * position carries its base id, because you can only reach a table through
558
+ * its base — so the caller never re-walks the workspace to route a name.
559
+ * Callers do not construct positions; they echo back one this method handed
560
+ * them on a previous hop's `targetPositions`.
561
+ *
562
+ * These are META-graph positions (type nodes), NOT data-graph positions
563
+ * (records). An adapter keeps them in its own meta vocabulary — Airtable's
564
+ * `Base` / `Table` — so a type node can never be mistaken for a record of
565
+ * that type. The framework's NAME for the type stays the edge's
566
+ * `targetTypeId`; the position is only how the adapter gets back there.
567
+ *
568
+ * Optional, and additive: an adapter of UNIFORM schema (one type-edge per
569
+ * type, however many records sit behind it) is already correct via
570
+ * `listEntryPoints` + `describe` and needs no `edgesFrom`. Only adapters
571
+ * whose schema VARIES per instance — two Airtable bases hold different
572
+ * tables, so they are two types, not two instances of one — have a
573
+ * meta-graph deep enough to walk. Callers fall back to `describe`.
574
+ *
575
+ * Returns null when the position isn't recognised (the same contract as
576
+ * `describe`'s unknown typeId).
577
+ *
578
+ */
579
+ edgesFrom?(position: SourcePosition, cursor?: unknown): Promise<EdgesFromResult | null>;
580
+ /**
581
+ * This adapter's meta graph has CONTAINERS — an Airtable base holding
582
+ * tables, a spreadsheet holding sheets, a list holding its entries. So
583
+ * describing its published entry list is not a local lookup: it walks every
584
+ * container, one call each.
585
+ *
586
+ * That cost is the reason a full-surface describe is refused for these
587
+ * adapters (`instance_cache.ts`), and it is a fact about the SHAPE of the
588
+ * graph, not about whether the adapter implements `edgesFrom`. Keying the
589
+ * refusal off `edgesFrom` conflated the two and silently hollowed out the
590
+ * describe of every uniform adapter the moment it learned to walk —
591
+ * positions came back `undescribed`, with no properties and no error.
592
+ *
593
+ * Absent ⇒ uniform: every type is located by its type id, `describe` is a
594
+ * local lookup, and there is nothing to fan out over.
595
+ *
596
+ */
597
+ readonly walksContainers?: boolean;
598
+ /**
599
+ * Type id for this adapter's webhook/synthetic event positions — the
600
+ * engine resolves an unstable position's type to this. Pure config;
601
+ * declared in the manifest for remote adapters. A stable position
602
+ * resolves to its own `recordType`; an unstable position resolves to
603
+ * this id (e.g. Attio's `attio:webhook_event`). Adapters with no
604
+ * synthetic event positions leave it undefined.
605
+ *
606
+ * Consumed by the generic `resolvePositionTypeId` engine resolver
607
+ * (`engine/position_type.ts`), which replaced the former per-adapter
608
+ * `getPositionTypeId` method now that the logic is purely config.
609
+ */
610
+ readonly webhookEventTypeId?: string;
611
+ /**
612
+ * `recordType` value of this adapter's `meta` root that should resolve
613
+ * to null (rather than `webhookEventTypeId`) when carried on an
614
+ * unstable position. Only adapters whose meta root flows through
615
+ * position-type resolution need it (Listen-Fire Valuations); pure config.
616
+ */
617
+ readonly metaRecordType?: string;
618
+ /**
619
+ * Config blocks this adapter exposes for its trigger role — the
620
+ * {@link ConfigBlock} list a user's automation-detail form renders for a
621
+ * `automations.trigger` whose `kind` resolves to this adapter (e.g. the
622
+ * inbound-email forwarding-address framing + slug). One declaration drives
623
+ * the form, the write-path validation (`updateTriggerConfig`), and the
624
+ * runtime config-validity check. Pure config; omitted by adapters whose
625
+ * trigger role needs no user-editable settings. Mirrors the manifest's
626
+ * `triggerConfig`.
627
+ *
628
+ */
629
+ readonly triggerConfig?: readonly ConfigBlock[];
630
+ /**
631
+ * Ensure the source system delivers the named events to `callbackUrl` —
632
+ * the listen-reconciliation seam (see the module-level event-subscription
633
+ * notes above `EnsureEventSubscriptionInput`). Idempotent: called with
634
+ * the full desired event set every time the team's listens change; the
635
+ * adapter diffs against `current` and registers / updates as needed.
636
+ * Implemented for real where the source has a subscription API (attio
637
+ * webhooks); a documented no-op (return undefined) where it doesn't;
638
+ * omitted where events need no external registration at all.
639
+ */
640
+ ensureEventSubscription?(input: EnsureEventSubscriptionInput): Promise<EventSubscriptionRegistration | undefined>;
641
+ /** Tear down the registration `ensureEventSubscription` created — called
642
+ * when the last listen on the (adapter, credential) channel retires. */
643
+ removeEventSubscription?(input: RemoveEventSubscriptionInput): Promise<void>;
644
+ /**
645
+ * Match an inbound record against existing linked_objects. The framework
646
+ * supplies candidates filtered by adapterType + recordType; the adapter
647
+ * decides match/no-match using whatever natural keys fit.
648
+ */
649
+ resolveEntity(input: ResolveEntityInput): Promise<ResolveEntityResult>;
650
+ /**
651
+ * Read a single field value at a position. For in-memory sources this is
652
+ * a local lookup; for external sources it may consult cached payload data
653
+ * within the position itself.
654
+ */
655
+ getFieldValue(input: GetFieldValueInput): Promise<unknown>;
656
+ /**
657
+ * Resolve a reference field at a position to one or more related positions.
658
+ * For in-memory sources: local graph traversal. For external sources: may
659
+ * invoke API calls; any fetch caching is internal to the adapter.
660
+ */
661
+ getRelated(input: GetRelatedInput): Promise<RelatedResult[]>;
662
+ /**
663
+ * Streaming variant of `getRelated` — adapters that can yield results
664
+ * incrementally implement this so the engine doesn't have to hold the
665
+ * full result set in memory. Used primarily for the snapshot fan-out
666
+ * (`getRelated(adapter-meta, collection)`) where the collection may
667
+ * contain millions of records. When this method is present, the engine
668
+ * prefers it over `getRelated`; absent, the engine falls back to the
669
+ * eager collection path.
670
+ *
671
+ * Adapters with no genuine streaming source (in-memory KG traversal,
672
+ * single-record webhook lookups) can leave this undefined — the engine
673
+ * just collects via `getRelated`.
674
+ */
675
+ iterateRelated?(input: GetRelatedInput): AsyncIterable<RelatedResult>;
676
+ /**
677
+ * One bounded page of `iterateRelated`, for adapters whose source pages
678
+ * natively (an offset, a continuation token). `cursor` is absent on the
679
+ * first call; the adapter answers `nextCursor` while more remain and the
680
+ * caller echoes it back. The cursor is opaque to everything but the adapter
681
+ * that minted it.
682
+ *
683
+ * This is what lets a traversal cross the wire in bounded pages: the
684
+ * protocol server serves `iterateRelatedPage` from it, where an adapter
685
+ * with only `iterateRelated` has to be drained into one page. An adapter
686
+ * implementing both keeps them one implementation (`iterateRelatedPages`).
687
+ */
688
+ iterateRelatedPage?(input: GetRelatedInput, cursor?: unknown): Promise<RelatedPage>;
689
+ /**
690
+ * Unsolicited push. Turn a raw inbound signal into a stream of discriminable
691
+ * events, doing whatever work that needs — including an async API fetch to
692
+ * pull the actual changes when the signal is only a thin "something changed"
693
+ * ping (Airtable), or to type a thin payload (in which case it sets each
694
+ * event's `tag`). A provider that delivers full payloads in the body (Attio)
695
+ * just splits the batch with no I/O.
696
+ *
697
+ * `checkpoint` carries the per-subscription cursor from the prior delivery
698
+ * (opaque to the framework); the returned `checkpoint` is persisted and fed
699
+ * back next time. Push providers that hold no cursor ignore it and return it
700
+ * unset. This mirrors `PollSource.getEvents` — the two seams differ only in
701
+ * what wakes them (a delivery vs a timer).
702
+ */
703
+ preprocessInbound?(input: {
704
+ raw: unknown;
705
+ checkpoint?: unknown;
706
+ }): Promise<{
707
+ events: DiscriminableEvent[];
708
+ checkpoint?: unknown;
709
+ }>;
710
+ /**
711
+ * The wake door, separate from `preprocessInbound`'s events: the correlation
712
+ * keys (as `awaitable.resolveAwait` names them when pending) that this raw
713
+ * inbound payload may have resolved. The engine resumes whatever is parked on
714
+ * them; a key nothing waits on is a no-op. No ordering against the events —
715
+ * a resumed await re-checks live, so it never relies on the payload.
716
+ */
717
+ wakeKeys?(input: {
718
+ raw: unknown;
719
+ }): Promise<string[]>;
720
+ /**
721
+ * The module checks its own inbound requests (its manifest declares
722
+ * `inbound.scheme: module`): one request at its door, raw, in place of the
723
+ * host's signature check, `preprocessInbound` and `wakeKeys`. The answer is
724
+ * its events, a refusal, or a reply the host sends as it is (a handshake).
725
+ */
726
+ checkInbound?(input: InboundCheckInput): Promise<InboundCheck>;
727
+ /**
728
+ * The instance-aware union of event types this connection can emit. The
729
+ * engine discriminates `DiscriminableEvent`s against it (and the editor runs
730
+ * author-time type logic over it). Connection-level, like `listEntryPoints`.
731
+ */
732
+ listEventTypes?(): Promise<EventType[]>;
733
+ createRecord(input: WriteInput): Promise<WriteResult>;
734
+ /**
735
+ * Write an EXISTING record by id. Beyond the fields, an update answers for
736
+ * the parent association the write named (`UpdateWriteResult.association`):
737
+ * a matched record whose fields were all unchanged is reported to the author
738
+ * as an attach, and that report is only honest if the adapter says whether
739
+ * the attach happened.
740
+ */
741
+ updateRecord(input: UpdateInput): Promise<UpdateResult>;
742
+ deleteRecord(input: DeleteInput): Promise<DeleteResult>;
743
+ /**
744
+ * Assert a relationship between two records that BOTH already exist —
745
+ * the write primitive behind the movement language's standalone
746
+ * `edge a -[:e]-> b` statement. Record *creation* wires its links via
747
+ * `WriteInput.parentLinks`; this covers the rare case where neither side
748
+ * is being written.
749
+ *
750
+ * `edgeName` arrives in the adapter's own currency, resolved against the
751
+ * FROM side's schema (KG: the ontology edge display name, exactly what
752
+ * `ParentLink.edgeName` already resolves; external systems: the reference
753
+ * fieldId `describe(from.recordType).references` publishes). Idempotent:
754
+ * an already-present link returns `created: false` with no side effects.
755
+ *
756
+ * Optional — adapters without a uniform "link two records" call (their
757
+ * reference-value shapes don't support a free-standing assert) omit it;
758
+ * callers reject up front with the adapter named. Manifests list
759
+ * `linkRecords` in `methods` where genuinely implemented.
760
+ */
761
+ linkRecords?(input: LinkRecordsInput): Promise<LinkRecordsResult>;
762
+ /**
763
+ * Sever a relationship between two existing records — the inverse of
764
+ * `linkRecords`, behind the movement language's `unlink a -[:e]-> b`
765
+ * statement. Same input currency (from / edgeName / to, the edge resolved
766
+ * against the FROM side's schema). Idempotent: an absent link returns
767
+ * `removed: false` with no side effects.
768
+ *
769
+ * - KG: delete the resolved edge row (its evidence cascades) and emit
770
+ * one adjacency-changed mutation event per endpoint — the mirror of
771
+ * `linkRecords`' insert.
772
+ * - External systems: read-modify-write the from side's reference
773
+ * field — remove the target from a multi-reference, clear a
774
+ * single-reference that points at it.
775
+ *
776
+ * Optional — adapters without a free-standing unlink omit it; callers
777
+ * reject up front with the adapter named. Manifests list `unlinkRecords`
778
+ * in `methods` where genuinely implemented.
779
+ */
780
+ unlinkRecords?(input: UnlinkRecordsInput): Promise<UnlinkRecordsResult>;
781
+ /**
782
+ * Run an adapter-provided expression function scoped to a writable field
783
+ * (the bodies behind a field's advertised `functions`, see
784
+ * `SchemaFieldDescriptor.functions`). Called by the evaluator when a field
785
+ * mapping's expression invokes a non-built-in function that the target
786
+ * field advertises — e.g. Slack's `SLACK_MESSAGE`. Adapters without field
787
+ * functions leave this undefined.
788
+ *
789
+ * The function is ordinary in every way but two (P8): its scope is the
790
+ * field, and its body is here rather than in the engine's built-in table.
791
+ * Input + output are serialisable so a remote adapter answers over the
792
+ * wire. The body gets no roster/channel — anything channel-scoped (Slack
793
+ * mention resolution) happens later at the write boundary.
794
+ *
795
+ */
796
+ invokeFieldFunction?(input: InvokeFieldFunctionInput): Promise<unknown>;
797
+ /**
798
+ * Adapters that can read cheaply implement this so the engine can skip
799
+ * writes whose values haven't changed. Adapters without it: writes
800
+ * unconditionally; adapterType provenance still prevents echo loops.
801
+ */
802
+ readRecord?(input: ReadInput): Promise<Record<string, unknown> | null>;
803
+ /**
804
+ * Resolve an opaque adapter-internal id to a human-readable label.
805
+ * Powers `prettyForm` in picker results — `event.id.object_id == '<uuid>'`
806
+ * gets rendered as "object is Companies" instead of "object is <uuid>".
807
+ *
808
+ * Optional. When absent, opaque ids surface as their raw value.
809
+ */
810
+ describeOpaqueId?(input: {
811
+ /** The field/path whose value is being described (e.g. `id.object_id`).
812
+ * Adapters use this to pick the right catalog (objects vs lists vs
813
+ * attributes). */
814
+ fieldPath: string;
815
+ /** The id value to look up. */
816
+ value: string;
817
+ }): Promise<string | null>;
818
+ /**
819
+ * Translate a filter expression into the adapter's native query body, plus
820
+ * a residual expression carrying anything that couldn't be pushed down.
821
+ *
822
+ * Default policy when the residual is non-null: throw at the boundary
823
+ * unless the caller explicitly opts into client-side fallback. Loud
824
+ * failure prevents the "fetch a million records to filter in memory"
825
+ * footgun that comes with quiet fallback.
826
+ *
827
+ * Adapters that publish nothing in `capabilities.pushdown` should not
828
+ * implement this — callers will skip pushdown entirely and ask for an
829
+ * unfiltered fetch.
830
+ */
831
+ translateFilter?(input: {
832
+ /** The filter expression authored by the user / generated by the AI
833
+ * builder. The framework guarantees only that its node kinds are a
834
+ * subset of `capabilities.pushdown.expressionKinds`. */
835
+ expression: Expression;
836
+ /** The entity type the filter applies to (e.g. `attio:companies`).
837
+ * Adapters use this to scope the native query body. */
838
+ entityType: string;
839
+ }): Promise<FilterTranslationResult>;
840
+ /**
841
+ * Return the type's in-batch dedup rules — the uniqueness constraints
842
+ * the framework uses to collapse co-extracted ephemeral duplicates
843
+ * before C9 apply (W6-D1).
844
+ *
845
+ * Adapters return their existing uniqueness metadata projected into
846
+ * an ephemeral-vs-ephemeral form: an OR-of-AND of property-keyed
847
+ * entries the framework can evaluate against `EphemeralNode.data`
848
+ * pairs. Entries the framework can't resolve in dedup time (e.g.
849
+ * KG `edge_to:` references that require a materialised graph
850
+ * context) are simply omitted — the framework's pair evaluator
851
+ * bails on such entries and falls through to the next constraint.
852
+ *
853
+ * Adapters that don't want dedup (Slack, Email) return `null`; the
854
+ * framework skips the type entirely.
855
+ *
856
+ */
857
+ getDedupRules?(input: {
858
+ typeRef: string;
859
+ }): Promise<DedupRules | null>;
860
+ /**
861
+ * Parse ordered actor candidates from an inbound event — a pure parse of
862
+ * who-sent-it / who-relayed-it, with NO Listen-Fire DB access (external-system
863
+ * API enrichment IS allowed: Slack `users.info`, Attio
864
+ * `/v2/workspace_members/{id}`, etc., to surface the actor's email).
865
+ *
866
+ * This is the adapter half of the acting-user split (the extensible
867
+ * adapter protocol, 2026-05-29). The adapter no longer reaches into
868
+ * Listen-Fire's user tables — it only describes the candidates; Listen-Fire's
869
+ * `resolveActingUser` (acting_user/resolve.ts) runs the identical
870
+ * resolution chain (creator-override → originator/non-service →
871
+ * relay/service → creator-fallback → null) by mapping these candidates
872
+ * to a team user. Splitting parse from resolution lets the parse cross a
873
+ * remote-adapter boundary that the DB chain never could.
874
+ *
875
+ * Candidate ordering and `source` tags reproduce the prior per-adapter
876
+ * chains exactly:
877
+ * - Email: sender (`From:` / `sender`) as `originator`; each parsed
878
+ * forwarding-header recipient as `relay`.
879
+ * - Slack / Attio: the runtime-resolved actor email as a single
880
+ * `originator` candidate (empty array when no email resolves).
881
+ *
882
+ * The terminal recipient on the deployment's own inbound address is ROUTING ONLY and is never a
883
+ * candidate — auth comes from who SENT it or which inbox RELAYED it.
884
+ *
885
+ * Optional — adapters with no acting-user concept omit it; the resolver
886
+ * then yields null (no `@user_*`). `extractActor` (`@actor_*`) is
887
+ * independent of this method.
888
+ *
889
+ */
890
+ getActorCandidates?(input: {
891
+ event: TriggerEvent;
892
+ }): Promise<ActorCandidate[]>;
893
+ /**
894
+ * Extract the raw actor identity from an inbound event. A pure parse,
895
+ * no DB lookups — actor identity is "who originated this event
896
+ * in the source system" regardless of whether they map to a Listen-Fire
897
+ * user. Powers `@actor_email` / `@actor_name` / `@actor_id` so authors
898
+ * can attribute events to their raw originator independently of the
899
+ * authenticated team member.
900
+ *
901
+ * Returns `null` when the event has no actor-shaped originator (e.g.
902
+ * a snapshot replay with no per-record provenance). Adapters that
903
+ * don't implement get `@actor_*` = null at dispatch time, gracefully.
904
+ *
905
+ */
906
+ extractActor?(input: {
907
+ event: TriggerEvent;
908
+ }): Promise<ActorIdentity | null>;
909
+ didWeAuthor?(input: {
910
+ event: TriggerEvent;
911
+ }): Promise<boolean | null>;
912
+ }
913
+ /**
914
+ * Reserved reference fieldId for a type's attached resources (the `resources`
915
+ * edge). A resource-bearing source type declares a reference with this fieldId
916
+ * in `describe`; the adapter resolves it via `getRelated` to resource positions
917
+ * (each `position.data` carrying a `Resource`). The engine's `resources` edge
918
+ * and the `resource_traverse` expression walk it.
919
+ *
920
+ * The label is `_resources` (a leading-underscore identifier, no `#` prefix) so
921
+ * it parses everywhere an edge name can appear — the movement language treats
922
+ * `#` as a comment outside traversal-hop brackets, so a `#`-prefixed label was
923
+ * swallowed in any other position. The leading underscore is a valid identifier
924
+ * char that also signals "reserved/meta" and keeps the name clear of
925
+ * extraction-derived edge/node names. `_resources` is a reserved resource-hop
926
+ * keyword in `-[:_resources]->`.
927
+ *
928
+ * This replaces the retired `getResources` method — resources are "just an
929
+ * edge" on the uniform traversal path.
930
+ *
931
+ */
932
+ export declare const RESOURCES_REFERENCE_FIELD_ID = "_resources";
933
+ export interface ResourceFilter {
934
+ resourceType?: 'URL' | 'EMAIL' | 'WHATSAPP' | 'FILE' | 'TEXT';
935
+ hasDocument?: boolean;
936
+ mimeType?: string;
937
+ namePattern?: string;
938
+ }
939
+ /**
940
+ * @deprecated Use `Resource`. Retained as a structural alias for
941
+ * source-compatibility while the wave-1 R4a/b/c adapter impls migrate.
942
+ */
943
+ export type ResourcePosition = Resource;
944
+ /**
945
+ * In-batch dedup rules surfaced by an adapter for one target type.
946
+ * Consumed by the framework's W6-D1 dedup phase: per ephemeral pair,
947
+ * walk OR-of-AND constraints; one matching constraint = pair merges
948
+ * (or routes to the LLM judge when the matching constraint contained
949
+ * any fuzzy entry).
950
+ *
951
+ * Adapters that don't model dedup return `null` from `getDedupRules`.
952
+ * Adapters whose existing uniqueness shape is constraint-of-property
953
+ * (KG) just project their stored rules into this form by name; adapters
954
+ * with richer (cross-record / cross-edge) constraints omit the parts
955
+ * that can't be evaluated in ephemeral-vs-ephemeral form.
956
+ *
957
+ */
958
+ export interface DedupRules {
959
+ /** OR-list of constraints; any one matching means the pair merges. */
960
+ constraints: DedupConstraint[];
961
+ }
962
+ export interface DedupConstraint {
963
+ /** AND-list of entries; every entry must fire for the constraint to
964
+ * match. Empty entries lists never fire. */
965
+ entries: DedupConstraintEntry[];
966
+ }
967
+ interface DedupConstraintEntry {
968
+ /** Adapter-neutral property identifier the framework maps to an
969
+ * ephemeral data field name via the bundle's synthetic schema
970
+ * (every `FieldShape.propertyTypeId` is stamped during W3-F4
971
+ * pre-collection). Entries whose id has no matching FieldShape on
972
+ * the ephemeral's site are silently skipped — the constraint then
973
+ * bails for the pair. */
974
+ propertyTypeId: string;
975
+ /** When true, values within similarity threshold count as a match
976
+ * and the matching constraint is flagged as fuzzy — pairs whose
977
+ * only matching constraint is fuzzy route to the LLM judge rather
978
+ * than auto-merge. */
979
+ fuzzy: boolean;
980
+ }
981
+ /** A prior correspondence the host holds for an adapter type and record
982
+ * type — an external record and the node it stands for — with the fields
983
+ * `resolveEntity` reads off a candidate (the protocol's `WireLinkedObject`,
984
+ * plus when it was made). A stored row carries more, which an adapter does
985
+ * not read. */
986
+ export interface LinkedObjectCandidate {
987
+ id: string;
988
+ node_id: string;
989
+ adapter_type: string;
990
+ external_id: string;
991
+ external_object_type?: string | null;
992
+ data?: unknown;
993
+ /** When the correspondence was made; absent over the wire. */
994
+ created_at?: Date | string;
995
+ }
996
+ export interface ResolveEntityInput {
997
+ /**
998
+ * The fields the engine intends to write, keyed by the adapter's
999
+ * `targetField` (e.g. KG property type id, Attio attribute slug).
1000
+ * Adapters interpret these values against their own identity model:
1001
+ * - KG-as-target: feeds the node type's `uniqueness_constraints`
1002
+ * (see `apps/api/src/lib/knowledge/uniqueness_constraints.ts`).
1003
+ * - External-as-target: native rules — e.g. Attio's per-attribute
1004
+ * `is_unique` flag (deferred today).
1005
+ *
1006
+ * Identity is the *target system's* concern; the framework no longer
1007
+ * tags individual mappings as identity-critical. Adapters that can't
1008
+ * search by identity return no candidates and the engine falls through
1009
+ * to create.
1010
+ */
1011
+ record: Record<string, unknown>;
1012
+ /** A typeId in this adapter's SchemaDescriptor. */
1013
+ recordType: string;
1014
+ /** Prior linked_objects matching adapterType + recordType. */
1015
+ candidates: LinkedObjectCandidate[];
1016
+ /**
1017
+ * Effective uniqueness constraints — the opaque union of the adapter's
1018
+ * native constraints (from `describe(recordType).uniquenessConstraints`)
1019
+ * and any author-defined constraints on the TG action node. An OR-of-AND
1020
+ * of the adapter's own field names (3b §3.2); the adapter compiles it to
1021
+ * its native search. The engine pre-combines the two sets and also uses
1022
+ * them to arbitrate exactness over the returned candidates — the adapter
1023
+ * just compiles + searches.
1024
+ */
1025
+ constraints: UniquenessConstraints;
1026
+ }
1027
+ export interface ResolveEntityResult {
1028
+ /**
1029
+ * Ordered candidate shortlist from the target system. Empty when no
1030
+ * existing record matches. Flat `ExternalRecordRef`s — same shape as
1031
+ * write results.
1032
+ *
1033
+ * Engine semantics:
1034
+ * 0 candidates → create new record
1035
+ * 1 candidate → match it (single result is unambiguous by definition)
1036
+ * N candidates → the engine arbitrates (exactness over the held
1037
+ * constraints, then the LLM judge over each candidate's `data`)
1038
+ */
1039
+ candidates: ExternalRecordRef[];
1040
+ }
1041
+ export interface GetFieldValueInput {
1042
+ position: SourcePosition;
1043
+ fieldId: string;
1044
+ }
1045
+ export interface GetRelatedInput {
1046
+ position: SourcePosition;
1047
+ fieldId: string;
1048
+ /**
1049
+ * Direction of traversal. 'outgoing' is universal — every adapter must
1050
+ * support it. 'incoming' is gated by `capabilities.incomingEdges`; the
1051
+ * engine checks the capability before passing 'incoming' through.
1052
+ */
1053
+ direction: 'outgoing' | 'incoming';
1054
+ /**
1055
+ * Native filter pushed down by the engine — the output of the source
1056
+ * adapter's `translateFilter` for the step's `expressionFilter`.
1057
+ * Adapter-opaque (each adapter knows its own shape). Adapters that
1058
+ * don't support pushdown can ignore this; the engine still
1059
+ * post-filters in that case. Always `undefined` when no
1060
+ * `expressionFilter` is set on the step or when the adapter doesn't
1061
+ * publish `translateFilter`.
1062
+ */
1063
+ nativeFilter?: unknown;
1064
+ /**
1065
+ * The hop's WHERE predicate, pushed for the adapter to NARROW THE FETCH
1066
+ * (adapter-capability-contract chunk 7) — so an unbounded collection hits the
1067
+ * provider's filtered query API instead of paging every record. Only ever a
1068
+ * PURE predicate (the checker gates impure WHEREs to bounded edges; the engine
1069
+ * pushes only pure ones). An adapter that can't translate it may ignore it and
1070
+ * over-fetch — the engine still satisfies the predicate via the shared filter
1071
+ * unit over what comes back, so results are correct either way. The win is
1072
+ * purely fetching less.
1073
+ */
1074
+ where?: Expression;
1075
+ /** The hop's ORDER BY, pushed so the adapter can sort at the source.
1076
+ * `fieldId` is the surface property name; the adapter resolves it. An
1077
+ * adapter whose API cannot sort that way ignores it and the engine sorts
1078
+ * what comes back. */
1079
+ orderBy?: {
1080
+ fieldId: string;
1081
+ direction: 'asc' | 'desc';
1082
+ };
1083
+ /**
1084
+ * The hop's LIMIT, pushed so the adapter can bound the fetch.
1085
+ *
1086
+ * THE CONTRACT: honour `limit` only when you also honoured `orderBy`, or
1087
+ * when no `orderBy` was given. A limit taken WITHOUT the sort it came with
1088
+ * returns an arbitrary n of the matching set, and the engine's sort over
1089
+ * those n then answers a different question than the author asked — "the
1090
+ * newest two" becomes "the two we happened to page first, newest first".
1091
+ * Over-fetching is always safe (the engine sorts and slices); truncating
1092
+ * without the sort is not.
1093
+ * plans/ordering-primitives-2026-09-04/1_decisions.md D1
1094
+ */
1095
+ limit?: number;
1096
+ }
1097
+ /**
1098
+ * One hop of the meta-graph walk (`Adapter.edgesFrom`) — the node you landed
1099
+ * on, and the paths on from it.
1100
+ *
1101
+ * The node itself is a plain `SchemaTypeDescriptor`, byte-for-byte what
1102
+ * `describe` returns: fields and edges (`references`). That is deliberate —
1103
+ * it keeps ONE shape for "what is this type", so a caller that falls back to
1104
+ * `describe` handles the same descriptor, and the eventual collapse of
1105
+ * `describe` into `edgesFrom` adds paths and paging rather than a second
1106
+ * vocabulary for the same facts.
1107
+ */
1108
+ /**
1109
+ * What an edge says about WHERE IT LANDS: the target node whole — its fields,
1110
+ * what is writable, what is required — minus the target's own onward edges.
1111
+ *
1112
+ * The omission is the contract, not an oversight. It buys the asymmetry the
1113
+ * walk runs on: an agent can decide **"write here"** with no further call,
1114
+ * because it already holds the landing's fields; deciding **"walk further"**
1115
+ * costs exactly one hop. Writing is answered locally, exploring is paid for a
1116
+ * hop at a time — which is what lets an agent author in one shot without a
1117
+ * speculative describe per candidate edge.
1118
+ *
1119
+ * `Omit<…, 'references'>` rather than a descriptor with an empty array: "this
1120
+ * node has no edges" and "we deliberately did not tell you its edges" are
1121
+ * different statements, and only one of them is true here.
1122
+ *
1123
+ * DELIBERATELY ITS OWN TYPE so the lookahead can shrink. Hydrating every
1124
+ * target of every edge is the expensive read in this design and may not
1125
+ * survive a large workspace; narrowing this alias to a stub (name and type
1126
+ * only) must not change the walk, the call, or any caller's control flow.
1127
+ * That is why callers read `targetNodes[edge].<x>` and never reach for the
1128
+ * same fact somewhere else.
1129
+ *
1130
+ */
1131
+ export type EdgeTargetNode = DescribedTargetNode | StubTargetNode;
1132
+ /** The target, described: its fields, what is writable, what is required —
1133
+ * everything but its own onward edges. An agent holding this can author a
1134
+ * write with no further call. */
1135
+ export type DescribedTargetNode = Omit<SchemaTypeDescriptor, 'references'> & {
1136
+ /** Present and false-y so a reader never has to infer which kind it holds. */
1137
+ stub?: false;
1138
+ };
1139
+ /**
1140
+ * The target, NAMED but not described — "there is a node here; hop to see its
1141
+ * fields".
1142
+ *
1143
+ * Whether to stub is the ADAPTER's call, edge by edge, because the adapter is
1144
+ * the only thing that knows what a describe costs it. Attio pays an attribute
1145
+ * fetch per object, so hydrating its root means fetching the whole workspace to
1146
+ * answer "what is in this connection"; email's describes are local, so it
1147
+ * hydrates freely. Neither is more correct.
1148
+ *
1149
+ * `stub: true` is EXPLICIT and never inferred from an empty field list,
1150
+ * because "hop to see the fields" and "this node has no fields" are different
1151
+ * facts. A stub that read as the latter would be silence dressed as an answer —
1152
+ * the exact failure this model exists to remove.
1153
+ *
1154
+ */
1155
+ export interface StubTargetNode {
1156
+ stub: true;
1157
+ typeId: string;
1158
+ displayName: string;
1159
+ description?: string;
1160
+ }
1161
+ /**
1162
+ * A described type as an edge's landing — everything it says about itself,
1163
+ * minus its onward edges. ONE implementation so "what does an edge tell you
1164
+ * about its target" has a single answer across every adapter, and so scaling
1165
+ * the lookahead back to a stub is an edit here rather than in sixteen places.
1166
+ */
1167
+ export declare function targetNodeOf(descriptor: SchemaTypeDescriptor): DescribedTargetNode;
1168
+ /** The target as a NAME only — for an edge the adapter judges too expensive to
1169
+ * describe up front. Costs nothing to produce, and says so. */
1170
+ export declare function stubTargetOf(input: {
1171
+ typeId: string;
1172
+ displayName?: string;
1173
+ description?: string;
1174
+ }): StubTargetNode;
1175
+ export interface EdgesFromResult {
1176
+ /** The node at `position` — its fields and the edges leaving it. */
1177
+ descriptor: SchemaTypeDescriptor;
1178
+ /**
1179
+ * What each edge LANDS ON, keyed by the edge's `fieldId` — the same keying
1180
+ * as `targetPositions`, which says how to get there while this says what is
1181
+ * there.
1182
+ *
1183
+ * Absent/partial is legitimate: an edge with no entry is one whose target
1184
+ * the caller must describe by walking to it. That tolerance is what lets the
1185
+ * hydration be scaled back later without a contract break.
1186
+ */
1187
+ targetNodes?: Record<string, EdgeTargetNode>;
1188
+ /**
1189
+ * Path to each edge's TARGET type node, keyed by the edge's `fieldId` (the
1190
+ * `references` entry it belongs to). The caller echoes one back as
1191
+ * `edgesFrom`'s position to follow that edge.
1192
+ *
1193
+ * A POLYMORPHIC edge is ONE reference with MANY members, so its members
1194
+ * ride here under their own keys with no reference row each: the member's
1195
+ * `recordType` says which polymorphic edge it belongs to, and the label the
1196
+ * adapter minted onto its data (`{ Name }`, `{ Title }` — `positionLabel`)
1197
+ * is the name that addresses it. Narrowing evaluates its predicate over
1198
+ * exactly these members (4_polymorphic_edges.md).
1199
+ *
1200
+ * A side map rather than a field on the reference descriptor: a position is
1201
+ * the walk's currency, not part of a type's published shape, and `describe`
1202
+ * has no paths to state. Absent/partial is fine — an edge with no path is
1203
+ * simply one the caller must reach by name.
1204
+ */
1205
+ targetPositions?: Record<string, SourcePosition>;
1206
+ /**
1207
+ * Opaque continuation for a wide hop. Pagination is in the contract from the
1208
+ * start as the general bound on fan-out: "each instance is a type-edge" is
1209
+ * only safe if a hop with very many instances can be walked incrementally
1210
+ * instead of returned whole. Absent ⇒ this page is the whole hop.
1211
+ */
1212
+ nextCursor?: unknown;
1213
+ }
1214
+ export interface RelatedResult {
1215
+ /** The resolved related position (the node on the other end of the edge). */
1216
+ position: SourcePosition;
1217
+ /**
1218
+ * Edge identifier when the source has first-class edges. Populated by KG
1219
+ * sources; left undefined by adapters where references are plain record
1220
+ * fields with no separate edge identity.
1221
+ */
1222
+ edgeId?: string;
1223
+ /**
1224
+ * The walked edge's properties, inline (3b §2). The engine captures these
1225
+ * during traversal so an `edge_property` expression reads them directly —
1226
+ * generalising edge-property filtering to any adapter (gated by
1227
+ * `capabilities.traversal.edgeProperties`) and retiring the bespoke
1228
+ * `getEdgeFieldValue`. Keyed by the adapter's field id (KG: property type
1229
+ * id). Omitted by adapters without edge properties.
1230
+ */
1231
+ edgeProperties?: Record<string, unknown>;
1232
+ }
1233
+ /** One page of a traversal — see `Adapter.iterateRelatedPage`. */
1234
+ export interface RelatedPage {
1235
+ items: RelatedResult[];
1236
+ /** Absent on the last page. */
1237
+ nextCursor?: unknown;
1238
+ }
1239
+ /**
1240
+ * Walk a paged traversal to its end as a stream: the first page is fetched
1241
+ * cursorless and each page's `nextCursor` is echoed back until a page
1242
+ * carries none. A page's items may be lazy (an async iterable), so a caller
1243
+ * that stops early pays only for what it read.
1244
+ *
1245
+ * Cursors come from servers we don't control, so a cursor handed back
1246
+ * unchanged (compared as JSON — cursors ride the wire as JSON) or a walk
1247
+ * past `MAX_RELATED_PAGES` fails loudly instead of looping forever.
1248
+ */
1249
+ export declare function iterateRelatedPages(page: (cursor: unknown) => Promise<{
1250
+ items: Iterable<RelatedResult> | AsyncIterable<RelatedResult>;
1251
+ nextCursor?: unknown;
1252
+ }>): AsyncIterable<RelatedResult>;
1253
+ /**
1254
+ * The small, honest whole-adapter capability set — facts that hold for the
1255
+ * adapter as a whole (not per-edge / per-field). These are read by the engine
1256
+ * at evaluation time: a `traverse direction=incoming` step, an `edge_property`
1257
+ * read, or a `resource` / `resource_traverse` step against an adapter that
1258
+ * doesn't support them raises `UnsupportedSourceCapabilityError`.
1259
+ *
1260
+ * Answered statically by `Adapter.runtimeCapabilities()` — no credential
1261
+ * needed, so it can be a plain method returning a constant. Per-edge filter /
1262
+ * order / limit live on the `describe()` output, NOT here (principle 1 —
1263
+ * capability at grain).
1264
+ *
1265
+ */
1266
+ export interface RuntimeCapabilities {
1267
+ /** Traversal direction + edge-property support. `outgoing` is universal
1268
+ * (every adapter must implement it) and therefore implicit. `incoming`
1269
+ * lets `traverse`/`exists` walk inverse edges (KG yes; most external
1270
+ * systems no). `edgeProperties` lets `edge_property` expressions read
1271
+ * fields off walked edges. */
1272
+ traversal: {
1273
+ incoming: boolean;
1274
+ edgeProperties: boolean;
1275
+ };
1276
+ /** Resources (files, URLs, content) attached to source positions, plus
1277
+ * `resource_traverse` step support. Typically only the KG adapter. */
1278
+ resources: boolean;
1279
+ }
1280
+ /**
1281
+ * The base/default whole-adapter capability set — what a plain external
1282
+ * adapter supports: outgoing-only traversal, no edge properties, no
1283
+ * resources. The KG overrides to enable all three.
1284
+ */
1285
+ export declare const BASE_RUNTIME_CAPABILITIES: RuntimeCapabilities;
1286
+ /**
1287
+ * Result of `Adapter.translateFilter` — a partial-pushdown contract. The
1288
+ * `native` value goes to the adapter's query API; the `residual` (if any)
1289
+ * carries the part of the filter that couldn't be pushed down.
1290
+ *
1291
+ * Default policy: `residual !== null` is a loud failure unless the caller
1292
+ * opts into client-side fallback (which has to be explicit because it can
1293
+ * mean "fetch everything and filter in memory").
1294
+ */
1295
+ export interface FilterTranslationResult {
1296
+ /** Adapter-opaque native query/filter body. */
1297
+ native: unknown;
1298
+ /** The portion of the filter the adapter couldn't push down. */
1299
+ residual: Expression | null;
1300
+ }
1301
+ /**
1302
+ * Input to `Adapter.invokeFieldFunction` — a fully-evaluated field-function
1303
+ * call. `instructions` is `args[0]` coerced to a string (the brief); `data`
1304
+ * is `args[1..]` as bare values in author order, passed through verbatim. No
1305
+ * ontology, no roster, no channel.
1306
+ *
1307
+ */
1308
+ export interface InvokeFieldFunctionInput {
1309
+ recordType: string;
1310
+ fieldId: string;
1311
+ functionName: string;
1312
+ args: {
1313
+ instructions: string;
1314
+ data: unknown[];
1315
+ };
1316
+ }
1317
+ /** One parent → child connection a write carries (`WriteInput.parentLinks`). */
1318
+ export interface ParentLink {
1319
+ /** The parent's type. Adapters need this to scope edge / reference
1320
+ * resolution to the parent's type. */
1321
+ recordType: string;
1322
+ /** External id of the parent record (returned by the parent's
1323
+ * createRecord call). */
1324
+ externalId: string;
1325
+ /** The user-facing name of the edge that connects parent → child.
1326
+ * Adapter resolves this to a concrete edge / reference per its
1327
+ * schema. Outbound and inbound KG names are first-class equals;
1328
+ * adapter references match by `fieldId`. */
1329
+ edgeName: string;
1330
+ /** The parent's payload, in the PARENT adapter's own shape: a handle
1331
+ * parent contributes its WriteResult `data` (e.g. Slack's
1332
+ * `{ channelId, ts }`), a traversed/event parent its position `data`
1333
+ * (e.g. the inbound Slack event's `{ channel, ts, … }`). Adapters read
1334
+ * only their OWN parents' data — no cross-adapter shape assumptions.
1335
+ * Absent when the parent carried none. */
1336
+ data?: Record<string, unknown>;
1337
+ }
1338
+ /** The parent-link set of a write — a list of 0/1/N parents. A linked
1339
+ * write is just the 1-element case of the general N-parent write, so the
1340
+ * engine always provides `parentLinks` and this is a stable accessor. */
1341
+ export declare function writeParentLinks(input: Pick<WriteInput, 'parentLinks'>): ParentLink[];
1342
+ /** The lone parent of a write, for adapters whose record types attach to
1343
+ * exactly one parent (a note/file/folder/list-entry belongs to a single
1344
+ * owner). Collapses the general N-parent list to that one case: returns
1345
+ * `undefined` for a root write (empty list) and throws if the program
1346
+ * somehow handed a single-parent type more than one parent — a misconfig
1347
+ * the adapter can't honour. */
1348
+ export declare function singleParentLink(input: Pick<WriteInput, 'parentLinks'>): ParentLink | undefined;
1349
+ export interface WriteInput {
1350
+ recordType: string;
1351
+ fields: Record<string, unknown>;
1352
+ /**
1353
+ * Per-field provenance, keyed by the adapter's target field name (the same
1354
+ * keys as `fields`). The record and its evidence persist as one
1355
+ * transactional unit (3b §3.3) — no post-structural `writeEvidence`. The
1356
+ * engine only includes a field here when its value is provenance-faithful
1357
+ * (un-transformed from an extraction / property read, 3b §3.4); adapters
1358
+ * without an evidence sink ignore it.
1359
+ */
1360
+ evidence?: Record<string, FieldEvidence>;
1361
+ /**
1362
+ * Source material this record drew from — node-level provenance (3b §3.3 /
1363
+ * `4d_resources.md`). Every resource that contributed to *any* of this
1364
+ * record's fields, each carrying its stable engine-stamped `id`, its
1365
+ * `facts`, and its `provenance`. The adapter persists / dedupes on `id`
1366
+ * (KG: resource row + `node_resource` link + `extraction_fact`); adapters
1367
+ * without a resource sink ignore it. Replaces the standalone `writeResource`
1368
+ * call — resources persist as part of the record's own write.
1369
+ */
1370
+ resources?: Resource[];
1371
+ /**
1372
+ * Mutation context applied to the write at the adapter boundary. The KG
1373
+ * adapter uses this to populate evidence.mutation_context; external
1374
+ * adapters use it for linked_object creation and adapterType tagging.
1375
+ */
1376
+ mutationContext: MutationContext;
1377
+ /**
1378
+ * Parent context when this write is the child of another action.
1379
+ * Adapters use this to wire the parent → child relationship in the
1380
+ * target system:
1381
+ *
1382
+ * - KG adapter: resolves `edgeName` against the ontology (outbound
1383
+ * name match → outgoing edge from parent; inbound name match →
1384
+ * incoming edge to parent) and inserts the corresponding edge
1385
+ * between parent and new child.
1386
+ * - External adapters: resolve `edgeName` against the parent
1387
+ * type's references in their schema descriptor and set the
1388
+ * matching reference field (or per-entity parent fields like
1389
+ * Attio's `parent_record_id`) on the create payload.
1390
+ *
1391
+ * A list of 0/1/N parents — a linked write is just the 1-element case
1392
+ * of the general N-parent write (the movement language's tuple-path
1393
+ * form: `write (company-[:investments]->, investor-[:investments]->)
1394
+ * { … }`). ONE create carries every parent link: the KG inserts each
1395
+ * connecting edge inside the record's own transaction; external
1396
+ * adapters set each matching reference on the create payload (or link
1397
+ * the parent side afterwards). Adapters read it via `writeParentLinks`
1398
+ * (or `singleParentLink` for single-parent record types).
1399
+ *
1400
+ * Empty for root actions and for child actions whose authored
1401
+ * relationship predates the edge-aware picker (no `edgeName`
1402
+ * recorded); adapters then create the record with no parent linking.
1403
+ */
1404
+ parentLinks?: ParentLink[];
1405
+ /**
1406
+ * Provenance from an external trigger. Set by the engine when a
1407
+ * webhook / snapshot / poll event fires a TG and the target side is
1408
+ * the KG (or an adapter that wants to record the bridge). Adapters
1409
+ * use this to insert a `linked_object` row alongside the new record so
1410
+ * future events from the same external record resolve back to this
1411
+ * write rather than creating a duplicate.
1412
+ *
1413
+ * Undefined when the trigger source is the KG itself (mutation
1414
+ * trigger) or when the engine has no record id (e.g. the trigger fires
1415
+ * but doesn't reference a single source record).
1416
+ */
1417
+ bridgeToExternal?: {
1418
+ /** Source adapter that produced the trigger event (e.g. 'attio'). */
1419
+ adapterType: string;
1420
+ /** The source's record id. */
1421
+ recordId: string;
1422
+ /** Source-side object type label, for `linked_object.external_object_type`. */
1423
+ recordType?: string;
1424
+ };
1425
+ }
1426
+ export interface UpdateInput extends WriteInput {
1427
+ externalId: string;
1428
+ }
1429
+ export interface DeleteInput {
1430
+ recordType: string;
1431
+ externalId: string;
1432
+ mutationContext: MutationContext;
1433
+ }
1434
+ /** One end of a `linkRecords` assert — an existing record, by the same
1435
+ * (recordType, externalId) coordinates every write method speaks. */
1436
+ interface LinkRecordEndpoint {
1437
+ /** A typeId in this adapter's schema (KG: a NodeTypeId). */
1438
+ recordType: string;
1439
+ /** The record's id in this adapter's system (KG: the node id). */
1440
+ externalId: string;
1441
+ }
1442
+ /** Input to `Adapter.linkRecords` — assert `from -[edgeName]-> to`. */
1443
+ export interface LinkRecordsInput {
1444
+ from: LinkRecordEndpoint;
1445
+ /**
1446
+ * The edge connecting from → to, resolved against the FROM side's
1447
+ * schema, in the adapter's own currency: the KG resolves it like
1448
+ * `ParentLink.edgeName` (outbound/inbound ontology names); external
1449
+ * adapters match it against the from type's reference fieldIds.
1450
+ */
1451
+ edgeName: string;
1452
+ to: LinkRecordEndpoint;
1453
+ /** Provenance context — evidence-free (an edge assert writes no field
1454
+ * values), but the KG threads it onto the mutation events it emits. */
1455
+ mutationContext: MutationContext;
1456
+ }
1457
+ export interface LinkRecordsResult {
1458
+ /** True when the link was newly asserted; false when it already existed
1459
+ * (the call is idempotent). */
1460
+ created: boolean;
1461
+ }
1462
+ /** Input to `Adapter.unlinkRecords` — sever `from -[edgeName]-> to`. Same
1463
+ * currency as the link assert it inverts. */
1464
+ export type UnlinkRecordsInput = LinkRecordsInput;
1465
+ export interface UnlinkRecordsResult {
1466
+ /** True when a link existed and was severed; false when none was present
1467
+ * (the call is idempotent). */
1468
+ removed: boolean;
1469
+ }
1470
+ /**
1471
+ * A write's return — the written record as the flat `ExternalRecordRef`
1472
+ * currency (the write return *is* the display data; `getDisplayData` is
1473
+ * gone). The engine snapshots `data` onto `linked_object.data` for the UI
1474
+ * prettifier when it bridges KG → external.
1475
+ */
1476
+ export type WriteResult = ExternalRecordRef;
1477
+ /**
1478
+ * The id-based-write NOT-FOUND signal — the one adapter-side requirement of
1479
+ * the engine-owned binding model (3b_explicit_link_model.md). An
1480
+ * `updateRecord` (a write BY id) targets a record the caller believes exists;
1481
+ * when the underlying record is gone (deleted externally), the adapter MUST
1482
+ * report it through this typed signal rather than throwing an opaque error, so
1483
+ * the engine's bind self-heal can deterministically distinguish "wrote fine"
1484
+ * from "target is gone → drop the stale binding and re-mint."
1485
+ *
1486
+ * This carries NO correspondence concept — it is purely "the record you named
1487
+ * by id does not exist." Every adapter implementing `updateRecord` translates
1488
+ * its system's not-found (Attio/Valuations 404, a missing KG node, a missing
1489
+ * Airtable/Sheets row, …) into this signal. Adapters whose `updateRecord`
1490
+ * unconditionally throws (Slack: posts aren't updatable) never need it.
1491
+ *
1492
+ */
1493
+ export interface UpdateNotFound {
1494
+ notFound: true;
1495
+ }
1496
+ /**
1497
+ * What became of the parent association an update named. Every `updateRecord`
1498
+ * states one, because "the engine sent a parent-only update" and "the record
1499
+ * is attached to its parent" are different facts, and only the adapter knows
1500
+ * which one happened. A matched record whose fields were all unchanged reports
1501
+ * itself as an `attach` on the strength of this value, so a guess here would
1502
+ * be a run that claims a relationship nobody made.
1503
+ *
1504
+ * - `made` — the association was absent and now exists.
1505
+ * - `already` — it was already there; nothing needed sending.
1506
+ * - `unsupported` — this system cannot attach an EXISTING record along the
1507
+ * edge the write named. The engine FAILS the write: the author asked for
1508
+ * a relationship the system has no way to make, and a quiet success would
1509
+ * be the very lie this value exists to prevent.
1510
+ * - `none` — the write named no parent, so nothing was asked.
1511
+ */
1512
+ export type ParentAssociation = 'made' | 'already' | 'unsupported' | 'none';
1513
+ /** The association an adapter reports when it has no way to attach a MATCHED
1514
+ * record to a parent at all: nothing was asked of it (`none`), or something
1515
+ * was and it cannot deliver (`unsupported`). */
1516
+ export declare function unsupportedAssociation(input: Pick<WriteInput, 'parentLinks'>): ParentAssociation;
1517
+ /** The association an adapter reports when its parents are CONTAINERS the
1518
+ * child cannot leave — a row in a tab, an entry that IS its (list, member)
1519
+ * pair. Having matched the record through the container is already the whole
1520
+ * of the relationship, so there is nothing left to make. */
1521
+ export declare function containerAssociation(input: Pick<WriteInput, 'parentLinks'>): ParentAssociation;
1522
+ /**
1523
+ * The success arm of an id-based write: the written record in the flat
1524
+ * `ExternalRecordRef` currency, plus what became of the parent association the
1525
+ * write named. `association` is REQUIRED — an adapter that stays silent about
1526
+ * a parent it was handed is exactly the silent degradation this field removes,
1527
+ * and a new adapter is caught by the compiler rather than by a missing edge in
1528
+ * production.
1529
+ */
1530
+ export interface UpdateWriteResult extends ExternalRecordRef {
1531
+ association: ParentAssociation;
1532
+ }
1533
+ /**
1534
+ * An id-based write returns the written record (`UpdateWriteResult` —
1535
+ * `externalId` echoes the input id) OR the typed not-found signal. Discriminate
1536
+ * with `'notFound' in result` (or the `updateRecordSucceeded` helper).
1537
+ */
1538
+ export type UpdateResult = UpdateWriteResult | UpdateNotFound;
1539
+ /** Narrow an `UpdateResult` to the success arm — `false` when the target was
1540
+ * reported not-found. The single discrimination point engine + callers use. */
1541
+ export declare function updateRecordSucceeded(result: UpdateResult): result is UpdateWriteResult;
1542
+ /** A delete's return carries no payload — the absence of a throw IS the
1543
+ * result. Kept as a named type so the `Adapter` signature stays readable. */
1544
+ export type DeleteResult = Record<string, never>;
1545
+ /** One request at a module-checked inbound door, as it arrived (the wire's
1546
+ * `checkInbound` params: the body is base64). */
1547
+ export type InboundCheckInput = WireCheckInboundInputShape;
1548
+ /** What a module decided about one inbound request (`checkInbound`). */
1549
+ export type InboundCheck = {
1550
+ outcome: 'accepted';
1551
+ events: DiscriminableEvent[];
1552
+ wakeKeys?: string[];
1553
+ checkpoint?: unknown;
1554
+ }
1555
+ /** The host answers `status` (401 when absent) with `signature_invalid`. */
1556
+ | {
1557
+ outcome: 'refused';
1558
+ status?: number;
1559
+ }
1560
+ /** Sent as it is, nothing run: `text` as text/plain, `json` as JSON. */
1561
+ | {
1562
+ outcome: 'reply';
1563
+ status: number;
1564
+ text?: string;
1565
+ json?: unknown;
1566
+ };
1567
+ export interface ReadInput {
1568
+ recordType: string;
1569
+ externalId: string;
1570
+ /** When set, request only these fields (adapter may ignore and return all). */
1571
+ fieldIds?: string[];
1572
+ }
1573
+ /**
1574
+ * A typed event from an UNSOLICITED inbound signal (`Adapter.preprocessInbound`)
1575
+ * or a SOLICITED poll (`PollSource.getEvents`). The engine discriminates it
1576
+ * against the adapter's `listEventTypes()` union into a typed root
1577
+ * `SourcePosition` and runs the trigger. See `4b_inbound_pseudocode.md`.
1578
+ */
1579
+ export interface DiscriminableEvent {
1580
+ /** The event body — what an `EventType.match` is evaluated against, and what
1581
+ * becomes the seeded position's `data`. */
1582
+ payload: unknown;
1583
+ /** Re-identifying external key for the record this event concerns; the
1584
+ * engine uses it for the KG bridge / correspondence lookup. */
1585
+ externalId?: string;
1586
+ /** The source's own per-DELIVERY id (Slack envelope `event_id`, …), stable
1587
+ * across at-least-once redeliveries — the receipt store dedupes on it so a
1588
+ * retry is a no-op, not a duplicate run. Distinct from `externalId` (the
1589
+ * record id, which repeats across legitimate distinct deliveries). Absent
1590
+ * ⇒ not deduped. */
1591
+ idempotencyKey?: string;
1592
+ /** Set when the adapter already classified the event (e.g. `preprocessInbound`
1593
+ * fetched a thin payload to type it): the matching `EventType.tag`. When
1594
+ * absent, the engine discriminates via each type's `match`. */
1595
+ tag?: string;
1596
+ actor?: Actor;
1597
+ occurredAt?: string;
1598
+ changeType?: 'create' | 'update' | 'delete';
1599
+ /** The source's own event-type string (e.g. Attio `record.created`), when it
1600
+ * has one. The inbound dispatcher narrows a webhook delivery to the triggers
1601
+ * whose `listen { events: [...] }` selection includes it — so authors (and
1602
+ * Airtable's registration) only pay for the change types they chose. Poll
1603
+ * events leave it unset. */
1604
+ eventType?: string;
1605
+ /** External-side record type the source names directly (e.g. Attio's object
1606
+ * id). Carried onto the `TriggerEvent.externalRecordRef.recordType` for the
1607
+ * KG bridge. When absent, the dispatch layer falls back to the discriminated
1608
+ * `rootRecordType`. */
1609
+ recordType?: string;
1610
+ /** Adapter-specific ids of the fields that changed, when the source surfaces
1611
+ * field-level change info (e.g. Attio attribute UUIDs). Threaded onto the
1612
+ * `TriggerEvent` for edge pruning; absent ⇒ "possibly affected" for every
1613
+ * edge. */
1614
+ changedFields?: string[];
1615
+ }
1616
+ /**
1617
+ * A member of an adapter's declared event-type union (`listEventTypes`). The
1618
+ * engine matches a `DiscriminableEvent` against these — by `tag` when the event
1619
+ * carries one, else by evaluating `match` over its payload — to assign the
1620
+ * concrete position type up front (no `webhook_event` meta-type + per-target-edge
1621
+ * hop). Instance-aware: for Attio the union is the connection's object types.
1622
+ */
1623
+ export interface EventType {
1624
+ /** Stable discriminator, e.g. `company.updated`. */
1625
+ tag: string;
1626
+ /** Concrete typeId the event yields, e.g. `attio:companies`. */
1627
+ positionType: string;
1628
+ /** Declarative recogniser the engine evaluates over `DiscriminableEvent.payload`.
1629
+ * OPTIONAL: an adapter whose normalized payload carries no literal
1630
+ * discriminator (Telegram, WhatsApp — they classify at parse time)
1631
+ * omits it and sets `DiscriminableEvent.tag` instead; a match-less type
1632
+ * is selectable ONLY by tag, never by payload probing. */
1633
+ match?: {
1634
+ path: string;
1635
+ equals: string | readonly string[];
1636
+ };
1637
+ }
1638
+ /**
1639
+ * Input to an adapter's internal collection-paging generator (Attio,
1640
+ * Valuations). Not an engine-facing method — the engine reaches a
1641
+ * collection through the meta-position `getRelated`/`iterateRelated`
1642
+ * fan-out, and those adapters call their own pager from there.
1643
+ */
1644
+ export interface SnapshotInput {
1645
+ pipelineInputId: string;
1646
+ recordType: string;
1647
+ /** Pagination state, opaque to framework. Adapter controls shape. */
1648
+ cursor?: unknown;
1649
+ /** Adapter-specific filter (e.g., subset by status, owner, etc.). */
1650
+ filter?: unknown;
1651
+ }