@drawbridge/drawbridge-agents 0.1.28 → 0.1.31

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.
@@ -21,6 +21,7 @@
21
21
  "../drawbridge-docs",
22
22
  "../drawbridge-emails",
23
23
  "../drawbridge-games",
24
+ "../drawbridge-growth",
24
25
  "../drawbridge-mongodb",
25
26
  "../drawbridge-redis",
26
27
  "../drawbridge-share",
@@ -68,13 +68,20 @@ skill) so it stops living only in tribal memory.
68
68
  after it produced five distinct defects, ending in a 60-second line-window overlap that billed
69
69
  one correctly-delivered event twice. Do not reintroduce meter events, and do not "fix" a billing
70
70
  discrepancy by reporting usage to Stripe — corrections are `billing.trueup` ledger rows.
71
- - **`items.actions` is the record of what the customer was SOLD** — `{ limit,
72
- overageCentsPerAction }`, written by drawbridge-stripe (≥0.1.44) at create/upgrade/downgrade and
73
- read by drawbridge-sync at cycle close. plans.js is the default for NEW sales only; editing it
74
- does not reprice existing customers (that is `repriceActionsSnapshot`, explicit and dry-runnable).
75
- Anything adding custom/enterprise pricing must gate the plan-change paths and the reprice script
76
- or negotiated values get silently overwritten. The rate is a NUMBER in cents the string
77
- `'2.5'` shape exists only at the Stripe `unit_amount_decimal` boundary.
71
+ - **`subscription.overrides` is the single home for sold/negotiated terms** —
72
+ `{ actions : { limit, overages }, conversion }`. `overrides.actions` is ALWAYS stamped at sale by
73
+ drawbridge-stripe (≥0.1.46) and re-stamped on plan change; `overrides.conversion` exists only when
74
+ an admin overrides the plan default, and api carries it onto the new document across plan changes.
75
+ Resolution is PER FIELD: `overrides.actions.X` legacy `items.actions.X` (transitional)
76
+ plans.js; conversion resolves ONLY through `conversionRate()` in utils (`overrides.conversion`
77
+ legacy top-level `conversion` plan free; `??` throughout, 0 is a valid override). `overages`
78
+ is a NUMBER in cents per action — the string `'2.5'` shape exists only at the Stripe
79
+ `unit_amount_decimal` boundary. Deleting a key restores plan flow, which is why the admin PUT
80
+ unsets the legacy keys it supersedes. plans.js is the default for NEW sales only; repricing
81
+ existing customers is `repriceActionsSnapshot` (explicit, dry-runnable — and it overwrites
82
+ admin-negotiated actions values, so read the dry run). The legacy keys are copy-only until the
83
+ overrides cleanup release, and sync (reader) deploys before stripe (writer) on any shape change.
84
+ Detail: drawbridge-docs `reference/billing.md`.
78
85
  - **No Stripe product backs actions (≥0.1.45).** Invoice items carry
79
86
  currency/quantity/unit_amount_decimal/tax_behavior/description directly; the account keeps exactly
80
87
  one product — the plan product behind `STRIPE_PRICE_*`. Never reintroduce a product-presence guard
@@ -219,6 +226,20 @@ skill) so it stops living only in tribal memory.
219
226
  - The `write_resource_feedbacks` and `read_publications` scopes live in drawbridge-shopify-app's
220
227
  `shopify.app.*.toml` but power other repos: sync's product ResourceFeedback sends and the
221
228
  embedded app's publishing section. Removing either → silent Shopify userErrors, no crash.
229
+ - **No Shopify webhook reaches the embedded app.** Every `[[webhooks.subscriptions]]` in
230
+ drawbridge-shopify-app's `shopify.app.*.toml` — `app/uninstalled` included — is delivered to
231
+ drawbridge-webhooks (`route/shopify.js`, topic-agnostic → `buffer` doc), which drawbridge-sync
232
+ consumes (`lib/buffer.js` `shopify.app/uninstalled` → delete connections, hard-delete the `shop`
233
+ record). So `app/routes/webhooks.app.uninstalled.tsx` is unreachable and **nothing in the
234
+ embedded app may hold per-shop state that needs invalidating on uninstall** — no memo, no cache,
235
+ no "already done" set. Violated once: `ensureChannel` cached "this shop is bound to our channel
236
+ spec" in a process-local Set cleared only by that dead route, so uninstall → reinstall skipped
237
+ `channelCreate` and the merchant silently lost the Drawbridge sales channel (2026-08-07 QA).
238
+ - Shopify Admin GraphQL failures come in **two shapes**, and sales-channel code must handle both:
239
+ domain failures land in `userErrors` (HTTP 200), while a revoked/expired offline token,
240
+ throttling, or a field missing on the API version land in a **top-level `errors`** — a bare
241
+ string for a 401, an array of objects otherwise — with a null `data`. Reading only `userErrors`
242
+ turns an auth failure into a silent success.
222
243
  - `SHOPIFY_REQUIRED_SCOPES` (`@drawbridge/shopify` `lib/constants.js`) must stay in lockstep with
223
244
  `[access_scopes]` in drawbridge-shopify-app's `shopify.app.*.toml` (both files). drawbridge-api
224
245
  diffs each store's granted scopes against it to flag "update permissions" on the connections
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drawbridge/drawbridge-agents",
3
- "version": "0.1.28",
3
+ "version": "0.1.31",
4
4
  "description": "Shared agent-instruction content (rules, code style, conventions) for the drawbridge-* monorepo.",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {
@@ -0,0 +1,88 @@
1
+ // What communication channels are actually flowing, HubSpot's count against ours.
2
+ //
3
+ // Exists because "the integration is connected" and "the data is arriving" are
4
+ // different claims, and the second is the only one that matters here. HubSpot's
5
+ // communication object holds SMS, WhatsApp and LinkedIn messages, told apart by
6
+ // channel — so whatever writes LinkedIn messages into that object, this pipeline
7
+ // reads. The question is never whether we can read them; it is whether anything is
8
+ // writing them.
9
+ //
10
+ // Same shape as the `missing` figure on the contact ingest, which is what caught 50
11
+ // contacts that had been silently absent for the life of the project.
12
+
13
+ const { growth } = require( '../lib/mongodb' );
14
+ const { enabled, request } = require( '../lib/hubspot' );
15
+ const { run } = require( '../lib/cycle' );
16
+
17
+ // The channels HubSpot files under the communication object. PHYSICAL_MAIL is
18
+ // hidden on this portal but counted anyway, because a hidden property still holds
19
+ // records.
20
+ const CHANNELS = [
21
+ 'LINKEDIN_MESSAGE',
22
+ 'SMS',
23
+ 'WHATS_APP',
24
+ 'PHYSICAL_MAIL',
25
+ 'CUSTOM_CHANNEL_CONVERSATION'
26
+ ];
27
+
28
+ run( 'channels', async () => {
29
+
30
+ if( ! enabled() ) return { skipped : 'HUBSPOT_ACCESS_TOKEN not set' };
31
+
32
+ const { db, database } = await growth();
33
+
34
+ const report = {};
35
+
36
+ for( const channel of CHANNELS ){
37
+
38
+ const body = await request({
39
+ body : {
40
+ filterGroups : [
41
+ {
42
+ filters : [
43
+ {
44
+ operator : 'EQ',
45
+ propertyName : 'hs_communication_channel_type',
46
+ value : channel
47
+ }
48
+ ]
49
+ }
50
+ ],
51
+ limit : 1
52
+ },
53
+ method : 'POST',
54
+ path : '/crm/v3/objects/communications/search'
55
+ });
56
+
57
+ const inHubspot = Number( body?.total || 0 );
58
+
59
+ // Ours is counted on the touch, which is one row per contact on the message,
60
+ // so it can legitimately exceed HubSpot's count of messages.
61
+ const held = await database.collection( 'touch' ).countDocuments({
62
+ channel,
63
+ type : 'communication'
64
+ });
65
+
66
+ report[ channel ] = {
67
+ held,
68
+ inHubspot,
69
+ // The number that answers "is anything writing this channel".
70
+ missing : inHubspot === 0 ? 0 : Math.max( 0, inHubspot - held )
71
+ };
72
+
73
+ }
74
+
75
+ const flowing = Object.entries( report )
76
+ .filter( ( [ , value ] ) => value.inHubspot > 0 )
77
+ .map( ( [ key ] ) => key );
78
+
79
+ return {
80
+ byChannel : report,
81
+ // Empty means nothing outside email, calls, meetings, notes and tasks is
82
+ // reaching HubSpot at all - which is a setup answer, not a pipeline answer.
83
+ flowing : flowing.length ? flowing : 'nothing beyond email/calls/meetings/notes/tasks',
84
+ note : 'LINKEDIN_MESSAGE at 0 means no tool is writing LinkedIn messages into'
85
+ + ' HubSpot. Whatever writes them, this pipeline already reads them.'
86
+ };
87
+
88
+ });