@drawbridge/drawbridge-agents 0.1.58 → 0.1.60

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.
@@ -1,102 +1,235 @@
1
1
  ---
2
2
  name: drawbridge-add-connection
3
- description: Use when adding a vendor connection (a new manifest in drawbridge-utils), changing one's auth type or scopes, or adding a hook, link, or step to an existing manifest and when asked to check an existing connection against the connection checklist.
3
+ description: Use for ANY work touching connections, vendors or hooks in drawbridge-utils — adding or checking a manifest, adding a vendor or its credentials, changing auth shape or scopes, adding a hook/link/step, or moving a vendor's API calls out of another repo. Also use before answering questions about how connections work.
4
4
  ---
5
5
 
6
- # Add a connection
6
+ # Connections
7
7
 
8
- A connection is one manifest in `drawbridge-utils/lib/connections/providers/`.
9
- The manifest is the only place vendor facts live, and it carries its own
10
- proof: a `review` key the contract test refuses to accept incomplete. This
11
- skill is the order of work that fills it honestly.
8
+ **Invoke this before touching a manifest, a vendor file, or any code that calls a
9
+ vendor's API. Not after. Darren, 2026-09-13: "never make a connection without
10
+ using that skill."**
12
11
 
13
- **A manifest ships complete: every hook the vendor can serve, every link the
14
- dashboard exposes, every scope those need, copy humanized, walked on dev.
15
- Nothing is deferred for a deadline. Darren, 2026-09-11: "Don't skip shit, do
16
- this right, our production launch deadline is somewhat flexible." If the work
17
- does not fit the time, the time moves.**
12
+ ## Which job is this?
13
+
14
+ | What you are doing | Go to |
15
+ |---|---|
16
+ | adding a whole new vendor connection | **A. New connection** |
17
+ | adding credentials for a vendor | **B. Vendor** |
18
+ | a repo calls a vendor's API directly | **C. Vendor code outside a manifest** |
19
+ | checking an existing connection | **D. Check** |
20
+
21
+ They share one law, and it is the reason this file exists.
22
+
23
+ ## THE LAW
24
+
25
+ **A vendor's facts live on its manifest. Nothing else.** Not a constant in
26
+ drawbridge-sync, not a client in drawbridge-api, not an env var read at a call
27
+ site. If a line of code knows a vendor's endpoint, header, payload shape, error
28
+ string or id format, it belongs in a hook.
29
+
30
+ **A vendor is NAMED by the connection that spends it.** `vendors : [ … ]` on the
31
+ manifest. That is what makes the provider screen able to say what a credential is
32
+ for, and what makes `vendorSettings()` hand a hook its fields.
33
+
34
+ **Grep before you invent.** A key, a field, a hook or a collection that already
35
+ has a shape must reuse it. Read the contract and the scaffold first, every time.
36
+
37
+ ## STOP — the three mistakes that have actually been made
38
+
39
+ Each of these was made by someone who had read this file. They are here as
40
+ gates, not trivia.
41
+
42
+ **1. Inventing a key instead of using the mechanism.** A vendor card said nothing
43
+ about what its credentials were for, so a `usage` key was added to the vendor
44
+ scaffold and to all eight vendor files — when the answer was that the manifest
45
+ that spends it had not named it in `vendors`. The screen derives that. One list
46
+ entry, no new key. *(2026-09-13. Earlier: a flat `provider` string was invented
47
+ when `provider : { slug, id }` was already in five collections.)*
48
+
49
+ > **Before adding any key, answer out loud: what already answers this? If the
50
+ > answer is "a manifest would, if it declared the vendor / hook / field it
51
+ > already can", do that instead.**
18
52
 
19
- ## Steps
53
+ **2. "This vendor has no connection, so it has no manifest."** Bright Data was
54
+ written as a vendor nothing named, on the grounds that no merchant links a Bright
55
+ Data account. Neither does anyone link our SendGrid account. **Who a credential
56
+ belongs to is not who spends it.** Drawbridge's own vendors are spent by the
57
+ `drawbridge` manifest and named in its `vendors` list — SendGrid, Twilio, HubSpot,
58
+ Bright Data. A vendor no manifest names is a bug, not a category.
59
+
60
+ **3. Leaving the vendor's calls behind.** Bright Data became a provider row and
61
+ its API calls stayed in `drawbridge-sync/lib/scrape.js` for another day — an
62
+ endpoint constant, a bearer header and a request body inline in a 3,798-line
63
+ file. Moving the credential is half the job. **The calls are the job.**
64
+
65
+ ## A. New connection
66
+
67
+ **Copy `_scaffold.js`. Answer every key. Delete nothing.**
68
+ `drawbridge-utils/lib/connections/_scaffold.js` is three things at once: the file
69
+ you copy, the reference for what each key may hold, and **the schema `build()`
70
+ reads**. A manifest that omits a key or invents one cannot be imported — it fails
71
+ before any test runs, in every repo that pulls the package, naming the key.
72
+
73
+ So "did I miss something" is answered by the suite. What follows is judgement,
74
+ which a schema cannot check.
75
+
76
+ **A manifest ships complete: every hook the vendor can serve, every link its
77
+ dashboard exposes, every scope those need, copy humanized, walked on dev.
78
+ Nothing is deferred for a deadline. Darren, 2026-09-11: "Don't skip shit, do this
79
+ right, our production launch deadline is somewhat flexible." If the work does not
80
+ fit the time, the time moves.**
20
81
 
21
82
  1. **Read the vendor twice, and write down where.** Fetch, never recall — a
22
- consent url once written from memory pointed at the api host and stalled
23
- every merchant on a blank page.
83
+ consent url written from memory pointed at the api host and stalled every
84
+ merchant on a blank page.
24
85
  - **API reference**: auth mechanisms and who the vendor says each is for
25
86
  (your own account vs accounts outside your organization — Drawbridge is
26
- always the second), partner or public-app program, OAuth urls and
87
+ always the second), partner or public-app program, OAuth endpoints and
27
88
  transport, token lifetime, refresh, every endpoint a hook will call.
28
- - **Dashboard docs** (help centre, app UI): the page url for each object
29
- we will hold an id for (list, segment, tag, contact, order), where the
30
- vendor says the id appears, what the vendor calls each thing, and what a
31
- disconnect actually does on their side. This read fills `urls.*` and the
32
- merchant copy. A vendor whose UI urls are not documented gets an
33
- observed form, marked observed, confirmed on dev before promote.
34
- `review.api` and `review.dashboard` take the urls read.
35
-
36
- 2. **Scope for the whole manifest, not today's hooks.** List every scope the
37
- vendor offers with its url in `review.scopes`, then request every scope any
38
- hook in the contract could need for this vendor contacts, segments,
39
- tags, custom fields, lists, webhooks whether or not that hook ships
40
- now. Scopes are app-level at Klaviyo and Attentive: adding one later costs
41
- every connected merchant a re-consent. "Minimum for what I built today" is
42
- the wrong minimum.
43
-
44
- 3. **Pick `auth.type` from the vendor's own framing:**
45
-
46
- | vendor's shape | type |
89
+ - **Dashboard docs**: the page url for each object we hold an id for, where
90
+ the vendor says that id appears, the vendor's own names for things, and what
91
+ a disconnect really does on their side. This read fills `urls.*` and the
92
+ merchant copy.
93
+
94
+ Both urls go on **the vendor file's `urls`**, beside its `scopes` link.
95
+ `build()` throws if a `review` key reappears on a manifest.
96
+
97
+ 2. **Scope for the whole manifest, not today's hooks.** Request every scope any
98
+ hook in the contract could need for this vendor, whether or not it ships now.
99
+ Scopes are app-level at Klaviyo and Attentive: adding one later costs every
100
+ connected merchant a re-consent. Declare them in `auth.oauth.scopes`
101
+ **without it a vendor cannot detect drift**, which is the bug this whole
102
+ rework came out of.
103
+
104
+ 3. **The auth SHAPE says which kind it is. There is no `auth.type` to pick.**
105
+ All five options are present and exactly one is non-false:
106
+
107
+ | the vendor's shape | the truthy key |
47
108
  |---|---|
48
- | installed from the vendor's app store, never redirects back | `install` |
49
- | registered app, merchant consents on the vendor's screen | `oauth` |
50
- | the vendor offers ONLY per-account credentials | `keys` |
51
- | no third party behind it at all | `generated` |
52
-
53
- `keys` is only for a vendor that offers nothing else to third parties.
54
- Where OAuth exists, build OAuth; a registration that has not landed ships
55
- the manifest dormant (`requires` names the client vars), never as keys.
56
-
57
- 4. **Implement every hook the vendor can serve.** The only `false` is "the
58
- vendor has no endpoint for this", with the one-line reason and the page
59
- that shows the absence. Time is never a reason. "Flip later" is how segment
60
- labels shipped as tags with no id kept. Every `urls` entry the dashboard
61
- read found is implemented too.
62
-
63
- 5. **Write the merchant copy from the two reads, then run the humanizer.**
64
- Excerpt, description, guide, confirm, error copy: the vendor's own names
65
- for things, the guide's navigation path from the help centre, and what
66
- disconnect really does. Then invoke `humanizer:humanizer` on the copy
67
- before commit and put the date in `review.content`. Not "if time".
68
-
69
- 6. **Build against the checklist** in
70
- `drawbridge-docs/reference/connection-hooks.md` → "Adding a connection".
71
- Its guards are the enforcement: directory registration, feature grant,
72
- `build()`, the `review` contract test. Trust a named failure over your
73
- memory of the steps.
74
-
75
- 7. **Walk it on dev, then date `review.verified`.** Connect a real account,
76
- pick the resource, sync one contact and one opted-out contact, open each
77
- `urls.*` link. `verified` is `null` on the publish that first carries the
78
- manifest (the walk needs it deployed) and dated on the next; a manifest
79
- still `null` at promote does not promote. A green suite is not the walk.
80
-
81
- ## Checking an existing connection
82
-
83
- Same steps, read as questions: is each `review` entry a real url or date, do
84
- the requested scopes cover every hook the contract lists, does each `urls.*`
85
- entry exist and open, was the copy humanized. A gap is a manifest fix, not a
86
- note.
109
+ | installed from the vendor's app store, never redirects back | `auth.install` |
110
+ | registered app, merchant consents on the vendor's screen | `auth.oauth` |
111
+ | the vendor offers ONLY per-account credentials | `auth.keys` |
112
+ | Drawbridge mints the credential, no third party | `auth.generated` |
113
+ | nothing to authenticate | `auth.none` |
114
+
115
+ The other four are `false` **carrying the reason**. `type` is INJECTED by
116
+ `build()` from the shape; leave it out a declared `type` that disagrees
117
+ with the shape throws.
118
+
119
+ 4. **Implement every hook the vendor can serve.** The only `false` is "the vendor
120
+ has no endpoint for this", with the reason and the page that shows the absence.
121
+ Time is never a reason.
122
+
123
+ **`lifecycle.health` is REQUIRED of every `oauth` and `install` connection** —
124
+ both end in a token somebody else can revoke, expire or narrow, and none of
125
+ those tells us.
126
+
127
+ **A hook describes its effects; it never performs them.** It receives
128
+ `( props, options )` and returns the answer envelope; `perform()` does the
129
+ writing. **Read `lib/connections/_hooks.js` before writing your first one** —
130
+ it carries a complete annotated hook and runs it.
131
+
132
+ 5. **Write the merchant copy from the two reads, then run `humanizer:humanizer`
133
+ on it before commit.** Excerpt, description, guide, confirm, error copy: the
134
+ vendor's own names, the help centre's navigation path, what disconnect really
135
+ does. `content.errors` names the vendor in both arms — a merchant connecting
136
+ two accounts in one sitting needs to know which one failed.
137
+
138
+ 6. **Let the enforcement check you.** `build()` refuses, at import: a missing or
139
+ invented top-level key, a missing hook, a hook prop its slot does not carry, an
140
+ unknown trigger, an oauth vendor declining `lifecycle.health`, two truthy auth
141
+ kinds, a step whose type disagrees with its position. Trust a named failure
142
+ over your memory of these steps.
143
+
144
+ 7. **Walk it on dev.** Connect a real account, pick the resource, sync one contact
145
+ and one opted-out contact, break the grant and confirm the error reaches the
146
+ card, the page and the checklist, reconnect and confirm it clears *immediately*,
147
+ then open each `urls.*` link. **A green suite is not the walk.** Record it in
148
+ the release doc for the change that carries the manifest — there is no
149
+ `review.verified` any more.
150
+
151
+ ## B. Vendor
152
+
153
+ One vendor, one file: `lib/connections/vendors/<vendor>.js`, carrying `fields`,
154
+ `icon`, `name`, `slug` and `urls`. However many connections spend it.
155
+
156
+ **Then name it.** Add the slug to the `vendors` list of every manifest that
157
+ spends it. **This is not optional bookkeeping** — it is what makes the provider
158
+ screen say who spends the credential, and what makes `vendorSettings({ slug })`
159
+ return the fields to that connection's hooks.
160
+
161
+ - A vendor merchants connect → named by that connection (`klaviyo`, `shopify`).
162
+ - **A vendor DRAWBRIDGE spends → named by the `drawbridge` manifest**, beside
163
+ SendGrid, Twilio, HubSpot and Bright Data. There is no such thing as a vendor
164
+ with no connection; there are vendors no MERCHANT connects.
165
+ - Each field's `credential` names the env var it falls back to. That is the only
166
+ place that name is written — `requires` derives from it, and so does the test
167
+ env fixture.
168
+
169
+ A vendor that is named by nothing renders a credential form saying nothing about
170
+ what it powers. If you find yourself wanting a key to explain a vendor, you have
171
+ found an unnamed vendor instead.
172
+
173
+ ## C. Vendor code outside a manifest
174
+
175
+ You found an endpoint, a token header, a payload shape or a vendor's error string
176
+ in drawbridge-sync, drawbridge-api, drawbridge-webhooks or anywhere else.
177
+
178
+ **It becomes a hook.** Not "later", not "it has no connection", not "it needs a
179
+ dependency the package cannot have".
180
+
181
+ 1. **Which manifest spends this vendor?** If a merchant connects it, theirs. If
182
+ Drawbridge pays for it, `drawbridge`. Name it in `vendors` if it is not there.
183
+ 2. **Which domain?** Read `HOOKS` in `contract.js` first. If nothing fits, add a
184
+ domain — with the reasoning for why it is not one of the existing ones, and
185
+ slots named for what every vendor of that kind does rather than for what this
186
+ one calls it. Every other manifest then answers the new domain `false` with a
187
+ reason; `build()` will not let you forget one.
188
+ 3. **Split vendor from Drawbridge, explicitly.** This is the judgement call and
189
+ it is the whole design. The vendor's facts move; our policy stays. From the
190
+ Bright Data move: the endpoint and the unblocking request moved; the tier
191
+ ladder that chooses between renderers, and the extraction that runs on whatever
192
+ comes back, did not.
193
+ 4. **A dependency the package must not carry is not an exemption.** Published
194
+ utils is imported by app-web and share, so it holds no browser, no BullMQ, no
195
+ socket server. The hook answers the part the vendor knows and the shell does
196
+ the rest — `scrape.browser` answers an ENDPOINT and the shell drives it. An
197
+ injected `fetcher` covers plain HTTP.
198
+ 5. **Sweep, and pin the sweep.** A test that greps this repo for the vendor's
199
+ endpoint and env names, failing on any that survive. Both
200
+ `connection-slug-neutral.test.js` (api) and `commerce-slug-neutral.test.js`
201
+ (sync) are the pattern, exemptions named with reasons and each one re-checked
202
+ for whether it is still earning its place.
203
+
204
+ ## D. Check
205
+
206
+ Same steps read as questions: do the requested scopes cover every hook the
207
+ contract lists, does each `urls.*` entry open, does the vendor file carry its
208
+ api/dashboard/scopes links, **is the vendor named by the connection that spends
209
+ it**, **is any of its wire detail still outside the manifest**, was the copy
210
+ humanized, has it been walked. A gap is a fix, not a note.
87
211
 
88
212
  ## Red flags — stop
89
213
 
90
214
  - "Minimum scopes for now" — re-consent for every merchant later.
91
- - "Help centre only for the guide" `urls.*` will be empty.
92
- - "Humanizer if time" it is a step with a date, not a polish.
93
- - "Verified in the header comment" — `review.verified` is the record.
94
- - "Flip that hook later", "skipped because of the deadline" — nothing is
95
- skipped. Say the time is short and keep going.
215
+ - "Humanizer if time" it is a step, not a polish.
216
+ - "Flip that hook later", "skipped because of the deadline" nothing is skipped.
96
217
  - "Keys as an interim, OAuth after the demo" — the OAuth build is the work.
218
+ - "I'll add `review.verified`" — the key is gone and `build()` throws on it.
219
+ - "The hook can just write it" — a hook holds no controller. Describe the write.
220
+ - "`false` because nobody has asked for it yet" — `false` is a decision about the
221
+ VENDOR, with a reason.
222
+ - **"This vendor has no connection"** — it has no MERCHANT. Name it on
223
+ `drawbridge`.
224
+ - **"I'll add a key for that"** — what already answers it? Almost always a
225
+ manifest declaring something it already can.
226
+ - **"The credentials moved, that's the migration"** — the calls are the job.
97
227
 
98
228
  ## Notes
99
229
 
100
230
  - A fact that must hold across repos goes through `drawbridge-record-contract`.
101
- - If the vendor's list endpoint filters by name server-side, the field must
102
- NOT declare `search : false`; a test pins that for Attentive.
231
+ - If the vendor's list endpoint filters by name server-side, the field must NOT
232
+ declare `search : false`; a test pins that for Attentive.
233
+ - The reference doc's tables are GENERATED from the registry
234
+ (`lib/connections/reference.js`) and a test fails if they drift. Regenerate,
235
+ never hand-edit.
package/claude/CLAUDE.md CHANGED
@@ -8,6 +8,7 @@
8
8
  @../conventions/jsx-fragments.md
9
9
  @../conventions/transactions.md
10
10
  @../conventions/cascade-cleanup.md
11
+ @../conventions/migrations.md
11
12
  @../conventions/dry.md
12
13
  @../conventions/property-shorthand.md
13
14
  @../conventions/drawbridge-packages.md
@@ -478,6 +478,28 @@ failure never does. Sync's step-runner errors the connection on that code during
478
478
  anywhere transient flips healthy connections to error on a blip; renaming it in either repo
479
479
  means dead grants retry into Sentry forever (the Shopify 08-16 incident, ecosystem-wide).
480
480
 
481
+ **A segment's `connections` rows have one writer, and the api ships first (utils ↔ sync ↔ api
482
+ ↔ app-web).** `segment.connections[]` — `{ connection, id, slug, type, url }`, one row per
483
+ connection that publishes the segment — is written ONLY by drawbridge-sync's
484
+ `step.segment.register` (cleared by `step.segment.remove`), through the shared descriptors in
485
+ utils `lib/connections/segment-rows.js`. api and app-web read the rows raw; the `url` is built
486
+ inside the hook, where the settings are decrypted, so nothing downstream may recompute it. Two
487
+ deploy orders, both silent when violated: the api must have **booted** with `schema/segment.js`
488
+ before any sync writes a row (strict validator, `additionalProperties : false` → Mongo 121, row
489
+ lost), and the api's pin must land before sync provisions a workflow, because `enums.step.type`
490
+ derives from the installed utils and the workflow route validates against it ("Document failed
491
+ validation", naming no field). Detail: `drawbridge-docs/reference/connection-hooks.md`.
492
+
493
+ **A system step's workflow is provisioned from the manifest's `trigger` (utils ↔ sync).**
494
+ `ensureSystemWorkflows` in sync walks every system step a manifest declares and creates one
495
+ workflow per step that names a `trigger` — no trigger means nothing fires it, which is how
496
+ Shopify's audit-only token steps stay undispatched. No PUBLISHED manifest carries a trigger yet,
497
+ so sync must not merge ahead of the utils pin bump: on the older package the walk finds no
498
+ trigger, provisions nothing, and Klaviyo connections silently stop getting their daily health
499
+ workflow. Provisioning is idempotent on `(connection, system, title)` with nothing
500
+ unique-indexing it, so a system step's `key` must stay static — a key that varied by account
501
+ would provision a second workflow every time it changed.
502
+
481
503
  **The SMS inbound URL is one string two repos must agree on (api ↔ sync ↔ webhooks).**
482
504
  `APP_CLIENT_WEBHOOKS_URI + '/connection/drawbridge/sms'` is written to each purchased
483
505
  number by api (`route/organization-networking.js`) and re-asserted by sync's daily identity
@@ -497,6 +519,12 @@ api's identity/organization routes); the merged keys keep the old names. Reading
497
519
  `providerSettings({ slug : 'drawbridge' })` anywhere finds no row — it was split on dev
498
520
  2026-09-08 and prod is seeded per-vendor. Detail: `drawbridge-docs/reference/providers.md`.
499
521
 
522
+ - **The embedded app's install paths ARE `INSTALL_ENDPOINTS`** (utils `lib/connections/contract.js`
523
+ ↔ drawbridge-shopify-app `app/drawbridge.server.ts`). The api's `POST /connection/:vendor/*`
524
+ allows exactly those thirteen and 404s anything else; shopify-app does not depend on utils, so
525
+ `app/drawbridge.server.test.ts` carries the list verbatim and fails on drift. Rename one side,
526
+ rename both and the test — a mismatch is an embedded app whose organization list silently 404s.
527
+
500
528
  ## Import surfaces
501
529
 
502
530
  - Import names against a package's **actual exports** — a missing export resolves to `undefined`
@@ -505,10 +533,11 @@ api's identity/organization routes); the merged keys keep the old names. Reading
505
533
 
506
534
  ## Shopify sales channel
507
535
 
508
- - `@drawbridge/shopify` storefront `getProduct`/`getProductVariantsPage` throw the **exact
509
- message `'Shopify product not found'`** when the Storefront API can't see a product
536
+ - The Shopify storefront client (`@drawbridge/drawbridge-utils/shopify/storefront`, formerly its
537
+ own `@drawbridge/shopify` package) throws the **exact message `'Shopify product not found'`**
538
+ from `getProduct`/`getProductVariantsPage` when the Storefront API can't see a product
510
539
  (unpublished from the Drawbridge channel / deleted); transport and GraphQL failures throw
511
- `'Shopify storefront error: …'`. drawbridge-sync's product worker (`queue/product.js`)
540
+ `'Shopify storefront error: …'`. The Shopify manifest's `commerce.product` hook
512
541
  string-matches the not-found message to deactivate the product and send ResourceFeedback
513
542
  instead of retrying. Reword the message → unpublished products retry-loop forever and
514
543
  merchants get no feedback; drawbridge-api's shopifyProduct route relies on the not-found
@@ -524,7 +553,7 @@ api's identity/organization routes); the merged keys keep the old names. Reading
524
553
  - **Feedback must forward the product's own `updatedAt`, never a fresh timestamp.** Shopify
525
554
  versions feedback on `productUpdatedAt` and refuses a payload older than the version it holds,
526
555
  so stamping `new Date()` records a version ahead of the product and every later send comes back
527
- `'Feedback for a later version of this resource was already accepted'`. `@drawbridge/shopify`
556
+ `'Feedback for a later version of this resource was already accepted'`. The admin client
528
557
  reads it (costing one extra `read_products` query per send) so callers cannot get this wrong.
529
558
  - **ResourceFeedback messages are format-validated by Shopify, not by us**: one message, ≤ 100
530
559
  characters, leading capital, trailing period, not all caps. A breach fails the whole send.
@@ -548,7 +577,7 @@ api's identity/organization routes); the merged keys keep the old names. Reading
548
577
  throttling, or a field missing on the API version land in a **top-level `errors`** — a bare
549
578
  string for a 401, an array of objects otherwise — with a null `data`. Reading only `userErrors`
550
579
  turns an auth failure into a silent success.
551
- - `SHOPIFY_REQUIRED_SCOPES` (`@drawbridge/shopify` `lib/constants.js`) must stay in lockstep with
580
+ - `SHOPIFY_REQUIRED_SCOPES` (`@drawbridge/drawbridge-utils` `lib/shopify/constants.js`) must stay in lockstep with
552
581
  `[access_scopes]` in drawbridge-shopify-app's `shopify.app.*.toml` (both files). drawbridge-api
553
582
  diffs each store's granted scopes against it to flag "update permissions" on the connections
554
583
  page. A scope added to the toml but not the constant silently escapes the check; one added to
@@ -27,8 +27,9 @@ worked fine.
27
27
  ## Coordinating peer dependencies on bump
28
28
 
29
29
  When bumping any `@drawbridge/*` package, audit every sibling package's
30
- `peerDependencies` for a stale pin. Peer pins are version-exact (e.g.
31
- `"@drawbridge/shopify": "0.0.3"`), so bumping the package in consumer
30
+ `peerDependencies` for a stale pin. Consumer pins are version-exact (e.g.
31
+ `"@drawbridge/drawbridge-utils": "0.0.183"`; components' peer on utils is the
32
+ one range, `>=0.0.118`), so bumping the package in consumer
32
33
  apps while leaving a sibling's peer dep on the old version triggers
33
34
  `ERESOLVE` at install time.
34
35
 
@@ -0,0 +1,73 @@
1
+ # Migrations live in the api
2
+
3
+ Every one-off migration and backfill goes in `drawbridge-api/scripts.js`.
4
+
5
+ **The only exception is media processing** — work that needs ffmpeg or sharp to
6
+ open an image or a video. Those dependencies exist in `drawbridge-sync` and
7
+ nowhere else, so a migration that re-encodes a file or extracts a video frame
8
+ has to run there. Nothing else qualifies.
9
+
10
+ "It dispatches queue work" is **not** an exception. The api has BullMQ and Redis
11
+ and already enqueues onto the workers' queues, so a migration that needs a worker
12
+ enqueues the job from the api and lets the worker do it. That is the same split
13
+ every live code path already uses: the api decides, the workers execute.
14
+
15
+ ## Why
16
+
17
+ A migration is a thing somebody runs once, under supervision, reading the dry run
18
+ before the real one. Keeping them in one file in one repo means there is one
19
+ place to look on release day, one run-list to work down, and one review surface
20
+ when someone asks what a release will do to production data. Splitting them
21
+ across two repos means the release plan has two homes, and the second one gets
22
+ forgotten — which is exactly what happened to the segment connection rows
23
+ migration.
24
+
25
+ It also means the file that runs migrations cannot break the worker. Sync
26
+ crashed on boot for several days because its `index.js` required a migrations
27
+ file the Dockerfile never copied, and App Platform rolled every deploy back so
28
+ quietly that dev served stale code the whole time.
29
+
30
+ ## How
31
+
32
+ - Add the function to `drawbridge-api/scripts.js`, alongside the others.
33
+ - Add a commented invocation to the numbered run-list at the bottom. Nothing runs
34
+ until somebody uncomments a line and deploys.
35
+ - Take `dryRun` and report what it would do. The first run of a migration is a
36
+ read.
37
+ - Add it to the release document in `drawbridge-docs/releases/` for the launch it
38
+ belongs to, with the ordering constraint stated: what has to be deployed before
39
+ it can run, and what breaks if it runs early.
40
+ - If the work needs a worker, enqueue the job the live path enqueues, through the
41
+ same helper the live path uses rather than a copy of it, so the payload and the
42
+ job id shape cannot drift.
43
+
44
+ ## Do not reach for a coalescing job id
45
+
46
+ A static job id looks like coalescing and is usually suppression. BullMQ drops an
47
+ add whose id still exists, and a queue that retains completed jobs will refuse a
48
+ re-add for as long as it keeps them — a week on the connection queue, a month for
49
+ failed ones. Worse, an id can be reused a layer down: `drawbridge-sync`'s
50
+ `queue/workflow.js` derives each step's job id from the orchestrator job's id, so
51
+ a static orchestrator id produced a static step id even though the orchestrator
52
+ job itself carried `removeOnComplete`.
53
+
54
+ That is not theoretical. It is what made a renamed segment never reach Mailchimp
55
+ or Klaviyo: the first register ran, and every later one for that segment was
56
+ dropped in silence for seven days.
57
+
58
+ **Make the id unique per dispatch and make the work idempotent instead.** A
59
+ register that writes the same row twice costs nothing. A register that is dropped
60
+ loses a change the merchant can see. Where a timestamp is the unique part, add a
61
+ counter too, because a loop dispatches many jobs inside one millisecond.
62
+
63
+ Reserve a static id for the case where running twice is genuinely harmful AND the
64
+ queue clears the id on both outcomes, and say in a comment which of the two you
65
+ are relying on.
66
+
67
+ ## Media migrations, the one exception
68
+
69
+ A media migration still follows everything above, except it lives in
70
+ `drawbridge-sync/scripts/` as a standalone script run by hand with `ENV_FILE`,
71
+ rather than in a `scripts.js` the service requires at boot. **Sync's `index.js`
72
+ must never require a migrations module** — that coupling is what took the worker
73
+ down.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drawbridge/drawbridge-agents",
3
- "version": "0.1.58",
3
+ "version": "0.1.60",
4
4
  "description": "Shared agent-instruction content (rules, code style, conventions) for the drawbridge-* monorepo.",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {