@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.
- package/dist/adapter.d.ts +1651 -0
- package/dist/adapter.js +157 -0
- package/dist/awaitable.d.ts +65 -0
- package/dist/awaitable.js +13 -0
- package/dist/catalog.d.ts +1443 -0
- package/dist/catalog.js +1541 -0
- package/dist/conformance/recording.d.ts +12 -12
- package/dist/document_sources.d.ts +14 -0
- package/dist/document_sources.js +30 -0
- package/dist/event.d.ts +2 -2
- package/dist/expression/expression.d.ts +10 -0
- package/dist/expression/expression.js +207 -0
- package/dist/expression/schemas.d.ts +869 -0
- package/dist/expression/schemas.js +266 -0
- package/dist/handbook_section.d.ts +79 -0
- package/dist/handbook_section.js +83 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +21 -3
- package/dist/model_names.d.ts +77 -0
- package/dist/model_names.js +53 -0
- package/dist/mutation_context.d.ts +55 -0
- package/dist/mutation_context.js +91 -0
- package/dist/plugin_spec.d.ts +36 -0
- package/dist/plugin_spec.js +157 -0
- package/dist/protocol.d.ts +298 -16
- package/dist/protocol.js +151 -16
- package/dist/slots/browser.d.ts +131 -0
- package/dist/slots/browser.js +79 -0
- package/dist/slots/files.d.ts +31 -0
- package/dist/slots/files.js +29 -0
- package/dist/slots/index.d.ts +7 -0
- package/dist/slots/index.js +26 -0
- package/dist/slots/judge.d.ts +26 -0
- package/dist/slots/judge.js +26 -0
- package/dist/slots/models.d.ts +58 -0
- package/dist/slots/models.js +38 -0
- package/dist/slots/ocr.d.ts +28 -0
- package/dist/slots/ocr.js +37 -0
- package/dist/slots/wake.d.ts +12 -0
- package/dist/slots/wake.js +14 -0
- package/dist/slots/wire.d.ts +47 -0
- package/dist/slots/wire.js +46 -0
- package/dist/trigger_config.d.ts +154 -0
- package/dist/trigger_config.js +116 -0
- package/dist/uniqueness.d.ts +77 -0
- package/dist/uniqueness.js +114 -0
- 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
|
+
}
|