fullstackgtm 0.43.0 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/CHANGELOG.md +378 -1
  2. package/README.md +37 -14
  3. package/dist/audit.d.ts +3 -1
  4. package/dist/audit.js +29 -2
  5. package/dist/cli/audit.d.ts +15 -0
  6. package/dist/cli/audit.js +392 -0
  7. package/dist/cli/auth.d.ts +80 -0
  8. package/dist/cli/auth.js +500 -0
  9. package/dist/cli/call.d.ts +1 -0
  10. package/dist/cli/call.js +373 -0
  11. package/dist/cli/capabilities.d.ts +30 -0
  12. package/dist/cli/capabilities.js +197 -0
  13. package/dist/cli/draft.d.ts +7 -0
  14. package/dist/cli/draft.js +87 -0
  15. package/dist/cli/enrich.d.ts +8 -0
  16. package/dist/cli/enrich.js +788 -0
  17. package/dist/cli/fix.d.ts +33 -0
  18. package/dist/cli/fix.js +344 -0
  19. package/dist/cli/help.d.ts +25 -0
  20. package/dist/cli/help.js +609 -0
  21. package/dist/cli/icp.d.ts +7 -0
  22. package/dist/cli/icp.js +229 -0
  23. package/dist/cli/init.d.ts +7 -0
  24. package/dist/cli/init.js +58 -0
  25. package/dist/cli/market.d.ts +1 -0
  26. package/dist/cli/market.js +391 -0
  27. package/dist/cli/plans.d.ts +5 -0
  28. package/dist/cli/plans.js +454 -0
  29. package/dist/cli/schedule.d.ts +8 -0
  30. package/dist/cli/schedule.js +479 -0
  31. package/dist/cli/shared.d.ts +70 -0
  32. package/dist/cli/shared.js +331 -0
  33. package/dist/cli/signals.d.ts +7 -0
  34. package/dist/cli/signals.js +412 -0
  35. package/dist/cli/suggest.d.ts +49 -0
  36. package/dist/cli/suggest.js +135 -0
  37. package/dist/cli/tam.d.ts +9 -0
  38. package/dist/cli/tam.js +387 -0
  39. package/dist/cli/ui.d.ts +121 -0
  40. package/dist/cli/ui.js +375 -0
  41. package/dist/cli.d.ts +1 -57
  42. package/dist/cli.js +104 -4556
  43. package/dist/connector.d.ts +15 -0
  44. package/dist/connector.js +35 -0
  45. package/dist/connectors/hubspot.d.ts +3 -1
  46. package/dist/connectors/hubspot.js +10 -3
  47. package/dist/connectors/outboxChannel.d.ts +64 -0
  48. package/dist/connectors/outboxChannel.js +170 -0
  49. package/dist/connectors/prospectSources.d.ts +22 -0
  50. package/dist/connectors/prospectSources.js +43 -0
  51. package/dist/connectors/salesforce.d.ts +3 -1
  52. package/dist/connectors/salesforce.js +12 -5
  53. package/dist/connectors/signalSources.d.ts +105 -0
  54. package/dist/connectors/signalSources.js +264 -0
  55. package/dist/connectors/theirstack.d.ts +84 -0
  56. package/dist/connectors/theirstack.js +125 -0
  57. package/dist/icp.d.ts +52 -4
  58. package/dist/icp.js +112 -35
  59. package/dist/index.d.ts +1 -0
  60. package/dist/index.js +1 -0
  61. package/dist/init.js +3 -0
  62. package/dist/judge.d.ts +2 -0
  63. package/dist/judge.js +6 -0
  64. package/dist/judgeEval.d.ts +7 -0
  65. package/dist/judgeEval.js +8 -1
  66. package/dist/marketClassify.d.ts +2 -0
  67. package/dist/marketClassify.js +7 -1
  68. package/dist/mcp-bin.js +2 -2
  69. package/dist/mcp.js +259 -166
  70. package/dist/runReport.d.ts +26 -0
  71. package/dist/runReport.js +15 -0
  72. package/dist/schedule.d.ts +80 -2
  73. package/dist/schedule.js +272 -5
  74. package/dist/signals.d.ts +54 -0
  75. package/dist/signals.js +64 -0
  76. package/dist/spoolFiles.d.ts +44 -0
  77. package/dist/spoolFiles.js +114 -0
  78. package/dist/tam.d.ts +225 -0
  79. package/dist/tam.js +470 -0
  80. package/dist/types.d.ts +11 -0
  81. package/docs/api.md +91 -11
  82. package/docs/outbox-format.md +92 -0
  83. package/docs/recipes.md +37 -0
  84. package/docs/roadmap-to-1.0.md +31 -1
  85. package/docs/signal-spool-format.md +175 -0
  86. package/docs/tam.md +195 -0
  87. package/llms.txt +83 -11
  88. package/package.json +1 -1
  89. package/skills/fullstackgtm/SKILL.md +4 -3
  90. package/src/audit.ts +27 -1
  91. package/src/cli/audit.ts +447 -0
  92. package/src/cli/auth.ts +549 -0
  93. package/src/cli/call.ts +398 -0
  94. package/src/cli/capabilities.ts +215 -0
  95. package/src/cli/draft.ts +101 -0
  96. package/src/cli/enrich.ts +885 -0
  97. package/src/cli/fix.ts +372 -0
  98. package/src/cli/help.ts +664 -0
  99. package/src/cli/icp.ts +265 -0
  100. package/src/cli/init.ts +65 -0
  101. package/src/cli/market.ts +423 -0
  102. package/src/cli/plans.ts +523 -0
  103. package/src/cli/schedule.ts +526 -0
  104. package/src/cli/shared.ts +375 -0
  105. package/src/cli/signals.ts +434 -0
  106. package/src/cli/suggest.ts +151 -0
  107. package/src/cli/tam.ts +435 -0
  108. package/src/cli/ui.ts +426 -0
  109. package/src/cli.ts +103 -5170
  110. package/src/connector.ts +46 -0
  111. package/src/connectors/hubspot.ts +14 -2
  112. package/src/connectors/outboxChannel.ts +202 -0
  113. package/src/connectors/prospectSources.ts +57 -0
  114. package/src/connectors/salesforce.ts +18 -2
  115. package/src/connectors/signalSources.ts +314 -0
  116. package/src/connectors/theirstack.ts +170 -0
  117. package/src/icp.ts +120 -34
  118. package/src/index.ts +32 -0
  119. package/src/init.ts +3 -0
  120. package/src/judge.ts +7 -0
  121. package/src/judgeEval.ts +8 -1
  122. package/src/marketClassify.ts +8 -1
  123. package/src/mcp-bin.ts +2 -2
  124. package/src/mcp.ts +130 -57
  125. package/src/runReport.ts +39 -0
  126. package/src/schedule.ts +330 -7
  127. package/src/signals.ts +90 -0
  128. package/src/spoolFiles.ts +116 -0
  129. package/src/tam.ts +654 -0
  130. package/src/types.ts +12 -0
@@ -0,0 +1,92 @@
1
+ # Outbox format (governed send queue)
2
+
3
+ The **outbox** is the send-side mirror of the [signal spool](./signal-spool-format.md).
4
+ It is how an *approved* drafted opener leaves the CLI — as a governed artifact a
5
+ downstream sender picks up, **not** as a transmitted message.
6
+
7
+ `fullstackgtm` **drafts everything and transmits nothing.** The CLI never opens
8
+ an SMTP or messaging-API connection. `apply --channel outbox` renders each
9
+ approved opener to a local JSONL file; a sender you run (the hosted product, or
10
+ your own MTA / ESP integration) drains the outbox and performs the actual send.
11
+ This is the send-side half of the open-core boundary: the governed artifact and
12
+ its format are open; always-on transmission infrastructure is hosted/opt-in.
13
+
14
+ ## The loop
15
+
16
+ ```
17
+ signals fetch → icp judge → draft → plans approve → apply --channel outbox → <sender>
18
+ (detect) (decide) (write) (human gate) (render, no send) (transmit)
19
+ ```
20
+
21
+ `draft` stages a `needs_approval` plan of `create_task` operations, each carrying
22
+ one opener grounded in a verbatim signal quote. After `plans approve`, applying
23
+ the plan **through the outbox channel** (instead of a CRM provider) renders each
24
+ approved operation to the outbox. Nothing is sent, and no CRM record is written.
25
+
26
+ ```bash
27
+ fullstackgtm draft --from-judge latest --channel email --save
28
+ fullstackgtm plans approve <planId> --operations all
29
+ fullstackgtm apply --plan-id <planId> --channel outbox # renders; transmits nothing
30
+ ```
31
+
32
+ Applying the same plan through a CRM provider instead (`--provider hubspot`)
33
+ logs the opener as a CRM task — the existing behavior. The two are alternatives:
34
+ a plan applies to one target.
35
+
36
+ ## Where the outbox lives
37
+
38
+ ```
39
+ <profile home>/signals/outbox/<channel>.jsonl (default: ~/.fullstackgtm/signals/outbox/)
40
+ ```
41
+
42
+ One file per channel (`email.jsonl`, `linkedin.jsonl`, `task.jsonl`), matching the
43
+ draft op's channel. Profile-scoped, owner-only (0600). A sender reads the file(s)
44
+ for the channel it handles and drains them on its own schedule.
45
+
46
+ ## The format
47
+
48
+ Newline-delimited JSON (JSONL), one **outbox entry** per approved opener:
49
+
50
+ | Field | Meaning |
51
+ |---|---|
52
+ | `id` | The source operation id — the **idempotency key**. Re-rendering the same op never duplicates a row. |
53
+ | `channel` | `email` \| `linkedin` \| `task` — from the draft op. |
54
+ | `objectType` | `contact` \| `account` — the CRM object the opener is addressed to. |
55
+ | `objectId` | The CRM record id. A sender with CRM access resolves it to an email / profile. |
56
+ | `body` | The **approved opener, verbatim** as it was signed in the plan operation. |
57
+ | `reason` | The draft op's human-readable reason (carries the account and the trigger). |
58
+ | `evidenceIds` | Ids of the evidence the opener was grounded in (the verbatim signal quote). |
59
+ | `renderedAt` | ISO 8601 — when the CLI rendered this to the outbox. **Not** a send time. |
60
+
61
+ Example (`email.jsonl`):
62
+
63
+ ```jsonl
64
+ {"id":"op_draft_abc","channel":"email","objectType":"contact","objectId":"C123","body":"Saw the Series B — congrats. Worth a quick chat on revops?","reason":"Signal-grounded opener for globex.com -> contact vp@globex.com","evidenceIds":["ev_globex_funding"],"renderedAt":"2026-06-25T13:08:17.176Z"}
65
+ ```
66
+
67
+ The entry carries the CRM `objectId`, not a resolved email address — the CLI does
68
+ not resolve recipients (that is the sender's job, against the live CRM). The
69
+ `body` is exactly what a human approved; a sender may template around it but the
70
+ approved span is the source of truth.
71
+
72
+ ## Governance and what the channel will not do
73
+
74
+ - **Only drafted openers.** The outbox channel renders `create_task` operations
75
+ produced by `draft` (policy `draft:<channel>`). Any other operation is
76
+ `skipped` — it is not a general CRM writer. Apply CRM changes through a CRM
77
+ provider.
78
+ - **Approval-gated and integrity-checked.** Rendering reuses the same apply path
79
+ as a CRM write: only operations whose plan is `approved` are applied, and a
80
+ stored plan's approval signatures are verified before anything is rendered.
81
+ - **Idempotent.** Re-rendering an operation already in the outbox is a no-op.
82
+ - **Draining and retention are the sender's job.** The CLI appends; it never
83
+ truncates the outbox. Your sender removes or archives entries after it
84
+ transmits them.
85
+
86
+ ## See also
87
+
88
+ - [signal-spool-format.md](./signal-spool-format.md) — the receive-side mirror.
89
+ - `docs/api.md` — the `apply`/`draft` verbs and the connector contract.
90
+ - The connector taxonomy and the hosted/open split (the managed sender stays
91
+ closed) are in the maintainer design doc
92
+ (monorepo `docs/spec-connectors-signals-outbound.md`).
package/docs/recipes.md CHANGED
@@ -149,6 +149,43 @@ accounts distort the queue.
149
149
 
150
150
  ---
151
151
 
152
+ ## Recipe 6 — Size your TAM, then fill it on a schedule
153
+
154
+ **Goal:** turn the ICP into a defensible market size, then close the gap to it
155
+ over weeks — with a number for "how far along am I."
156
+
157
+ ```bash
158
+ # 1. Estimate the universe FROM the ICP. Either probe a provider for the
159
+ # ICP-match count, or pass --accounts from a known segment. Record a baseline.
160
+ fullstackgtm tam estimate --source explorium --provider hubspot --acv 180000
161
+ # → accounts: matching COMPANIES (Explorium /v1/businesses) = the account universe.
162
+ # → ACV: a real ANNUAL figure you confirm (--acv), or --acv-from-crm
163
+ # --deal-period monthly|quarterly|annual to derive+annualize from closed-won.
164
+ # (--provider is your coverage source, not your ACV — it won't auto-set it.)
165
+ # → buyers/account: CRM avg-contacts/account (or --buyers-per-account); labeled.
166
+ # → TAM model: accounts × ACV = $; contacts = accounts × buyers (population target)
167
+
168
+ # 2. Schedule the population — plan-only acquire, apply stays a human gate.
169
+ fullstackgtm tam populate --cron "0 7 * * 1-5" --source pipe0 --provider hubspot
170
+ fullstackgtm schedule install
171
+
172
+ # 3. Each cycle: approve the morning's lead plan (Recipe 1's tail), then apply.
173
+ fullstackgtm plans list && fullstackgtm plans approve <id> --operations all
174
+ fullstackgtm apply --plan-id <id> --provider hubspot
175
+
176
+ # 4. Track where you're getting to — stamp a coverage reading; once a couple
177
+ # accumulate, report projects a burn-up + ETA to full coverage.
178
+ fullstackgtm tam status --provider hubspot --save
179
+ fullstackgtm tam report > tam-report.md
180
+ ```
181
+
182
+ The estimate is honest about its source (provider count vs. assumption), the
183
+ population never auto-writes (the meter is charged only at apply), and the ETA is
184
+ refused rather than faked until the timeline shows real movement. See
185
+ [tam.md](./tam.md).
186
+
187
+ ---
188
+
152
189
  ## The boundary, restated
153
190
 
154
191
  - **Read freely. Write only through `plans approve → apply`.** Never bypass it.
@@ -140,8 +140,38 @@ outward, one layer per release, each consolidating before the next expanded:
140
140
  broker-https enforcement, the build/CI dist-integrity gate (published dist is
141
141
  provably from source), Salesforce-merge capability matrix.
142
142
 
143
+ ## 0.29 → 0.43 — from hygiene to the full outbound loop, as shipped
144
+
145
+ The next train extended the governed spine from *fixing the CRM* to *filling
146
+ it and acting on it*, without ever loosening the write gate:
147
+
148
+ - **0.29–0.34** — market-map maturation (config suggestion + taxonomy types,
149
+ sourcing helpers, competitor discovery, vendor logos/scale in the report)
150
+ plus call-type classification with type-specific coaching rubrics (0.32);
151
+ Salesforce `dedupe`/`merge_records` via SOAP and pipeline-aware closed/won
152
+ detection (0.30).
153
+ - **0.35–0.36** — DX cohesion: progressive-disclosure help, summary-first
154
+ `audit` with next-step guidance; the engagement workspace — a deterministic
155
+ `health` verb over a per-profile timeline.
156
+ - **0.37–0.39** — acquire: `enrich acquire` (net-new, ICP-targeted,
157
+ resolve-first deduped, metered lead creation), lead assignment policies so
158
+ leads are never born ownerless, the LinkedIn (HeyReach) discovery source
159
+ with email resolution, and run observability for broker-paired CLIs.
160
+ - **0.40** — Salesforce reaches full write parity with HubSpot
161
+ (`create_record`); HubSpot date-field writes no longer false-conflict.
162
+ - **0.41** — signal-based outbound: `signals fetch/list/outcome/weights`,
163
+ `icp judge`/`icp eval` (timing × fit × memory, golden-set calibration
164
+ gate), `draft` (trigger-grounded openers as governed plans), and the LLM
165
+ base-URL override.
166
+ - **0.42** — bulk apply for lead creation (HubSpot batch + Salesforce
167
+ composite).
168
+ - **0.43** — the on-ramp and the seams: `init` workspace scaffolding,
169
+ `docs/recipes.md` (five composable plays), and the contact/account seam
170
+ campaign — contact-granular outbound end to end, account-level acquire for
171
+ ABM, `health` by object type.
172
+
143
173
  The known-gaps list below predates these layers and has been re-verified
144
- against the 0.28 surface: still accurate, still open.
174
+ against the 0.43 surface (2026-07): still accurate, still open.
145
175
 
146
176
  ## Known real-portal gaps to close before 1.0
147
177
 
@@ -0,0 +1,175 @@
1
+ # Signal spool format (webhook landing zone)
2
+
3
+ The **spool** is how push-based signal platforms — website de-anonymization
4
+ (RB2B), social/job listening (Trigify), form-submission webhooks (HubSpot) —
5
+ reach `fullstackgtm`. The CLI is a deterministic batch tool, not an always-on
6
+ server, so it does not receive webhooks itself. Instead:
7
+
8
+ 1. A **receiver** you run (any HTTP endpoint — 20 lines, or an off-the-shelf
9
+ webhook-to-file tool) appends one JSON row per event to a spool file.
10
+ 2. `fullstackgtm signals fetch --connector file` reads the spool on its next run,
11
+ turning each row into a ranked signal through the same evidence-gate → dedup →
12
+ weight pipeline as every other source.
13
+
14
+ The spool format and the `file` connector that reads it are open source. The
15
+ package ships **no receiver** — operating an always-on endpoint is out of scope
16
+ for the CLI (a managed receiver is part of the hosted product). This doc is the
17
+ contract your receiver writes to.
18
+
19
+ ## The format
20
+
21
+ A spool file is **newline-delimited JSON (JSONL)**: one JSON object per line,
22
+ append-only. (A single `.json` file containing a JSON array is also accepted, for
23
+ convenience when staging by hand.) Each object is a **staged signal row**:
24
+
25
+ | Field | Required | Meaning |
26
+ |---|---|---|
27
+ | `bucket` | yes¹ | One of `demand`, `funding`, `job`, `company`, `social`. |
28
+ | `accountDomain` | yes | The company domain. `domain` is accepted as an alias. Normalized (protocol/`www`/path stripped, lowercased) on read. |
29
+ | `trigger` | yes | Short human label, e.g. `"visited pricing page"`, `"raised Series B"`. |
30
+ | `quote` | yes | **Verbatim evidence** — the exact text a human can verify (a headline, a page title, the submitted form + email). A row with no quote is **dropped**, never faked. |
31
+ | `sourceUrl` | no | Link to the evidence. |
32
+ | `firstSeen` | no | ISO 8601 timestamp. Defaults to the fetch time. |
33
+ | `weight` | no | Explicit weight; defaults to the bucket's configured weight. |
34
+
35
+ ¹ `bucket` may be omitted in a spool file — the `file` connector fills its
36
+ default (`company`). Set it explicitly per row when a source spans buckets.
37
+
38
+ Example spool file (`rb2b.jsonl`):
39
+
40
+ ```jsonl
41
+ {"bucket":"company","accountDomain":"globex.com","trigger":"visited pricing 3x","quote":"globex.com — viewed /pricing and /demo, 3 sessions","sourceUrl":"https://app.rb2b.com/v/abc123"}
42
+ {"bucket":"company","accountDomain":"initech.com","trigger":"visited docs","quote":"initech.com — viewed /docs/api","sourceUrl":"https://app.rb2b.com/v/def456"}
43
+ ```
44
+
45
+ A row that fails validation (unknown bucket, missing domain/trigger/quote) makes
46
+ the fetch error and names the offending row — a corrupt spool is surfaced, not
47
+ silently skipped. Keep your receiver's mapping faithful to this table.
48
+
49
+ ## Where the spool lives
50
+
51
+ `--connector file` with **no path** reads the conventional landing zone:
52
+
53
+ ```
54
+ <profile home>/signals/spool/ (default: ~/.fullstackgtm/signals/spool/)
55
+ ```
56
+
57
+ Every `*.jsonl` (and `*.json`) file in that directory is read and concatenated,
58
+ name-sorted — so each platform can append to its own file and they all land in
59
+ one fetch:
60
+
61
+ ```
62
+ ~/.fullstackgtm/signals/spool/rb2b.jsonl
63
+ ~/.fullstackgtm/signals/spool/trigify.jsonl
64
+ ~/.fullstackgtm/signals/spool/hubspot.jsonl
65
+ ```
66
+
67
+ Point at a different file or directory with `--connector-opt path=<path>`:
68
+
69
+ ```bash
70
+ # the conventional landing zone (zero-arg)
71
+ fullstackgtm signals fetch --connector file --save
72
+
73
+ # a specific file or directory
74
+ fullstackgtm signals fetch --connector file --connector-opt path=./events.jsonl --save
75
+ fullstackgtm signals fetch --connector file --connector-opt path=/var/spool/fsgtm --save
76
+ ```
77
+
78
+ The CLI **never writes** to the spool — your receiver does. The directory is
79
+ profile-scoped, so spools for different client workspaces stay separate.
80
+
81
+ ### Re-reads and retention
82
+
83
+ Re-reading the same spool is safe: signals dedup on `(accountDomain, bucket,
84
+ trigger)` within the configured window (`dedupWindowDays`, default 30), so a row
85
+ that's still in the spool on the next fetch is deduped, not double-counted. The
86
+ CLI does not consume or truncate the spool — **retention is yours**: rotate or
87
+ truncate the files on your own schedule (e.g. nightly, or after a successful
88
+ `--save`). Unbounded spool growth is a self-host concern the CLI doesn't manage.
89
+
90
+ ## Writing a receiver
91
+
92
+ A receiver is whatever turns a platform's webhook POST into spool rows. The
93
+ minimum: verify the platform's signature, map the payload to the fields above,
94
+ append one JSON line. Pseudocode:
95
+
96
+ ```js
97
+ // POST handler for your platform's webhook
98
+ function onWebhook(req) {
99
+ verifySignature(req); // platform's HMAC / shared secret
100
+ const row = mapToStagedRow(req.body); // per-platform mapping (below)
101
+ if (!row.quote) return 204; // no verifiable evidence -> skip
102
+ appendLine("~/.fullstackgtm/signals/spool/<platform>.jsonl", JSON.stringify(row));
103
+ return 204;
104
+ }
105
+ ```
106
+
107
+ Keep the receiver dumb: no ranking, no CRM writes, no judgment. All of that is
108
+ the CLI's job, gated and deterministic, on the next `signals fetch`.
109
+
110
+ ## Per-platform mapping
111
+
112
+ These map each platform's webhook payload to a staged row. Payload shapes drift —
113
+ verify against the platform's current webhook docs before relying on a field.
114
+
115
+ ### RB2B (website visitor de-anonymization)
116
+
117
+ RB2B delivers an identified visitor (person + company) via outbound webhook.
118
+ Use the **company domain** as `accountDomain` and the visited pages as the
119
+ verbatim `quote`.
120
+
121
+ | Spool field | From RB2B payload |
122
+ |---|---|
123
+ | `bucket` | `"company"` (or `"demand"` for high-intent pages) |
124
+ | `accountDomain` | the visitor's company domain |
125
+ | `trigger` | e.g. `"visited pricing"` derived from the top page |
126
+ | `quote` | `"<domain> — <pages viewed>"` (verbatim page paths) |
127
+ | `sourceUrl` | the RB2B visitor permalink |
128
+
129
+ ### Trigify (social / job-change listening)
130
+
131
+ Trigify pushes social and job-change events. Map the monitored account's domain;
132
+ the post/job text is the evidence.
133
+
134
+ | Spool field | From Trigify payload |
135
+ |---|---|
136
+ | `bucket` | `"social"` or `"job"` |
137
+ | `accountDomain` | the account/company domain |
138
+ | `trigger` | e.g. `"new role: VP Sales"` or `"posted about <topic>"` |
139
+ | `quote` | the verbatim post text or job title + snippet |
140
+ | `sourceUrl` | the source post / listing URL |
141
+
142
+ ### HubSpot form-submission webhook
143
+
144
+ The `hubspot-forms` **pull** connector already covers recent submissions via the
145
+ Forms API (Phase 1). For real-time, subscribe to HubSpot's form-submission
146
+ webhook and append a `demand` row per submission — the same shape the pull
147
+ connector produces, so both paths dedup against each other.
148
+
149
+ | Spool field | From HubSpot submission |
150
+ |---|---|
151
+ | `bucket` | `"demand"` |
152
+ | `accountDomain` | corporate domain of the submitted email (drop free-mail) |
153
+ | `trigger` | `"form: <form name>"` |
154
+ | `quote` | `"Submitted \"<form name>\" — <email>"` |
155
+ | `firstSeen` | submission timestamp (ISO 8601) |
156
+
157
+ ## One container convention across staged intake
158
+
159
+ The spool CONTAINER (JSONL one-object-per-line; JSON array accepted; a
160
+ directory means every `*.jsonl` / `*.json` file in it, name-sorted) is shared
161
+ by every staged-file intake path, not just `--connector file`:
162
+
163
+ - `signals fetch --from <path>` — rows are staged signal rows (this doc's table);
164
+ - `enrich ingest <path> --source <id>` — rows are that source's enrichment rows;
165
+ - `market observe --from <path>` — each object is one observation-set envelope.
166
+
167
+ Each verb keeps its original single-file `.json` / `.csv` handling unchanged;
168
+ the row schema stays the verb's own. Shared reader: `src/spoolFiles.ts`.
169
+
170
+ ## See also
171
+
172
+ - `docs/api.md` — the `signals` verb and the `SignalSourceConnector` contract.
173
+ - The connector taxonomy and the hosted/open-source split for the receiver are
174
+ described in the maintainer design doc (monorepo
175
+ `docs/spec-connectors-signals-outbound.md`).
package/docs/tam.md ADDED
@@ -0,0 +1,195 @@
1
+ # TAM mapping — size the reachable market, then fill it
2
+
3
+ `tam` answers a longer-horizon question than `market` (the competitive map): **how
4
+ big is the reachable market for my ICP, and how far along am I in actually putting
5
+ it in the CRM?** It is deliberately *not* a one-shot "$10B TAM" headline — it pairs
6
+ a defensible estimate with an iterative, governed population loop and a coverage
7
+ timeline so "this will take a while" becomes a quantified ETA.
8
+
9
+ ```
10
+ tam estimate → tam populate → (approve/apply each cycle) → tam status --save → tam report
11
+ model schedule governed acquire coverage point burn-up + ETA
12
+ ```
13
+
14
+ ## The model
15
+
16
+ `tam estimate` derives the universe **from your ICP** (`icp.json`):
17
+
18
+ ```
19
+ universe accounts = a real count for the ICP filter
20
+ contacts (population target) = accounts × buyers/account
21
+ TAM $ = accounts × ACV (account/per-logo basis — the default)
22
+ = contacts × ACV (buyer/per-seat basis, --acv-basis buyer)
23
+ ```
24
+
25
+ The account count comes from one of three places, always **labeled** in the model:
26
+
27
+ - `--source theirstack` — **the real, technographically-targeted universe** (and the
28
+ only one that returns an actual list). TheirStack counts companies that *use* a
29
+ CRM/MAP — set `firmographics.technologies` (e.g. `["salesforce","hubspot",
30
+ "pipedrive"]`) — plus geo + employee bounds. `accountsSource:
31
+ "provider:theirstack (uses-CRM)"`. This is the buying signal for a CRM-hygiene/
32
+ RevOps tool: "company runs a CRM", not "company is a software publisher". **And
33
+ `tam accounts --source theirstack` pulls the actual company list** (real names +
34
+ domains) you can review, save (`--out <csv>`), and acquire into the CRM.
35
+ (Counting is cheap but **not free** — TheirStack rejects `limit:0`, so a count
36
+ returns 1 row ≈ 3 credits; a list pull is ~3 credits/company. `login theirstack`.)
37
+ - `--source explorium` — a firmographic **company count** from Explorium
38
+ `/v1/businesses` (geo + employee bands + NAICS). A count only — **Explorium 403s
39
+ when you try to pull the list**, and it can't target by CRM-usage, so it's a rough
40
+ size estimate, not a list. `accountsSource: "provider:explorium"`.
41
+ - `--accounts <n>` — an explicit assumption (e.g. a number from a provider export).
42
+ `accountsSource: "assumption"`.
43
+
44
+ > **Why technographic, and why a count isn't enough** (verified live, 2026-06-25/26):
45
+ > lead/people endpoints don't report a real universe — Explorium `/v1/prospects`
46
+ > `total_results` equals the requested page size (1→1, 10→10), and pipe0/Crustdata
47
+ > returns only a pagination cursor with no total. Explorium `/v1/businesses` counts
48
+ > companies (e.g. US mid-market software ≈ 19,000) — **as long as you don't send a
49
+ > `size` field, which silently caps the total** — but it can't filter by CRM-usage
50
+ > and **403s when you try to pull the list**. TheirStack is the technographic
51
+ > source that does both (target by tech, return real records). pipe0 stays a
52
+ > discovery/population source (`tam populate`), not a counting one; `--source pipe0`
53
+ > for `estimate` is rejected with that explanation.
54
+
55
+ Explorium caps `total_results` at **60,000** (a `provider:explorium (≥60k cap —
56
+ floor)` label + a ⚠). TheirStack has no such cap but list-pull costs credits, so
57
+ `tam accounts --max <n>` pages it.
58
+
59
+ ### List-pull cost (never spend by surprise)
60
+
61
+ `tam accounts` pulls real records at **~3 TheirStack credits/company**, so it always
62
+ prints the cost up front and guards big pulls:
63
+
64
+ - `tam accounts --dry-run [--usd-per-credit <rate>]` — prices the pull AND the full
65
+ TAM (e.g. *"up to 100 companies ≈ 300 credits. Full TAM (~43,517) ≈ 130,551
66
+ credits (~$5,483) to materialize."*) and spends **0 credits**. The dollar figure
67
+ appears only when you give a rate — TheirStack pricing is ~$0.04–$0.11/credit by
68
+ tier, and the tool won't fabricate a default.
69
+ - A pull above `--max-credits` (default **150** ≈ 50 companies) requires `--confirm`
70
+ — so a large pull is always a deliberate choice, not an accident.
71
+
72
+ So the natural pattern is **count freely, pull in confirmed batches** (`--max N`),
73
+ acquire, measure coverage, repeat — rather than one large up-front spend.
74
+
75
+ **ACV must be a real, ANNUAL figure that you confirm** — never a band default, and
76
+ never silently lifted from the CRM. `--provider` is your **coverage** source, not
77
+ your ACV: the account universe is Explorium, coverage is the CRM, and the TAM's
78
+ economics are yours to confirm. Two confirmed paths, always labeled (`acv.source`):
79
+
80
+ - `--acv <annual-usd>` — your confirmed annualized ACV (`source: "explicit (annual)"`).
81
+ - `--acv-from-crm --deal-period monthly|quarterly|annual` — derive the **median
82
+ closed-won deal amount** and **annualize** it by the period you specify
83
+ (`source: "crm:closed-won (N deals, median $X/monthly ×12 = annual)"`). The period
84
+ is **required** because a deal amount can be MRR, quarterly, or annual — guessing
85
+ it is a 4–12× error (a $15k/mo deal is a $180k ACV). The deal count is in the
86
+ label, so a thin signal (e.g. "1 deal") is visible — don't over-trust it.
87
+
88
+ If you give neither, `estimate` errors rather than invent a number — and a bare
89
+ `--provider` does NOT auto-set ACV.
90
+
91
+ **Buyers/account** (the contact population target = accounts × buyers) gets the same
92
+ treatment: `--buyers-per-account <n>`, else the CRM's **average contacts per account**
93
+ (`crm:avg-contacts/account (N)`), else a clearly-labeled minimal assumption of 1
94
+ (`assumption:1-buyer`) — never a silent guess. `--acv-basis buyer` makes ACV per-seat
95
+ (TAM = contacts × ACV) instead of per-logo.
96
+
97
+ Optional citable **cross-checks** (`--cross-checks <file.json>`, an array of
98
+ `{claim, valueUsd?, sourceUrl, quote, asOf?}`) sit alongside the bottom-up number as a
99
+ top-down sanity check — the market-research flavor, evidence-attached.
100
+
101
+ `estimate` also records a **coverage baseline** (the CRM account/contact counts right
102
+ now, when a `--provider`/`--input`/`--demo` source is given) so `status` can attribute
103
+ what *this campaign* added versus what was already there.
104
+
105
+ ## Populate it (governed, never auto-writes)
106
+
107
+ `tam populate --cron "<expr>"` wires a recurring **`enrich acquire --source <s> --save`**
108
+ into the scheduler. Each firing queues a `needs_approval` lead plan — it does **not**
109
+ write to the CRM, and the acquire meter is charged only at apply. Apply stays a separate
110
+ human gate (`apply --plan-id <id>`, re-checked approved at every firing). So a scheduled
111
+ TAM populates by *accumulating proposals you approve*, never by surprise leads.
112
+
113
+ ```bash
114
+ fullstackgtm tam populate --cron "0 7 * * 1-5" --source pipe0 --provider hubspot
115
+ fullstackgtm schedule install # activate the crontab entry
116
+ # each cycle:
117
+ fullstackgtm plans list # the morning's lead plan
118
+ fullstackgtm plans approve <id> --operations all
119
+ fullstackgtm apply --plan-id <id> --provider hubspot
120
+ ```
121
+
122
+ This is the one place the schedule allowlist permits a create-side verb, and only in
123
+ its plan-producing `--save` form (scheduling `enrich acquire` without `--save` is
124
+ rejected — it would queue nothing).
125
+
126
+ ## Track coverage (where you're getting to)
127
+
128
+ Coverage is measured on the **intersection** of your CRM and the TAM's ICP — not a
129
+ raw account count. `tam estimate` stores the ICP filter on the model, and `tam
130
+ status` classifies every domain-bearing CRM account against it:
131
+
132
+ - **in-TAM** — matches every CRM-checkable criterion (size, industry) with no
133
+ contradiction. *Only these count toward coverage.*
134
+ - **out-of-TAM** — contradicts a criterion (wrong size/industry). Reported
135
+ separately; never inflates coverage. So 50k accounts loaded from other sources
136
+ that aren't your target market don't fake a high number.
137
+ - **unknown** — the record lacks the fields to judge. Surfaced, never silently
138
+ counted — it tells you what to enrich.
139
+
140
+ ```bash
141
+ fullstackgtm tam status --provider hubspot --save # classified coverage + a timeline reading
142
+ fullstackgtm tam report # markdown: model + in/out/unknown + burn-up + ETA
143
+ ```
144
+
145
+ > **Honest limit:** geo and "uses a CRM" aren't on a CRM account record, so an
146
+ > in-TAM classification means "matches what the CRM lets us check" (size + industry),
147
+ > not a full technographic match. A `--reverify` pass (re-confirm geo/tech per
148
+ > domain via TheirStack) is planned; until then, status labels what it checked.
149
+
150
+ **Bottom-up vs top-down — squaring "more in CRM than estimated."** The estimate is a
151
+ *lower-bound prior*, not a ceiling — TheirStack only knows ~740k US firms, so your
152
+ multi-source CRM can legitimately hold more valid in-TAM accounts than it predicted.
153
+ Every in-TAM account is a *proven* member of the real market, so the in-TAM count is
154
+ the strongest floor on the true universe. When `in-TAM ≥ estimate`, `status` stops
155
+ reporting a fake "100%" and flips the signal: **"the estimate was a floor — your real
156
+ market is at least N; re-estimate (broader ICP / better source) to find the
157
+ headroom."** `reconciledUniverse = max(estimate, in-TAM)`.
158
+
159
+ Once two-plus readings show movement, `report`/`status` project an **ETA** on the
160
+ **in-TAM** burn rate (off-ICP accounts loaded elsewhere don't count as progress) →
161
+ days remaining → a projected fill date. Fewer than two readings, a flat rate, or no
162
+ elapsed time returns an honest "not enough history yet," never a fabricated date.
163
+
164
+ ## Storage
165
+
166
+ Profile-scoped under `<home>/tam/<name>/`:
167
+
168
+ - `model.json` — the TAM model (universe, ACV, cross-checks, baseline). 0600.
169
+ - `coverage.jsonl` — append-only coverage readings (one per `status --save`). 0600.
170
+
171
+ Scope per client with `--profile <name>`; name multiple TAMs with `--name <n>`
172
+ (default `default`).
173
+
174
+ ## Invariants
175
+
176
+ - **Estimate is honest about its source.** Every model says whether the account count
177
+ is a `provider:*` probe or an `assumption`; the probe errors rather than silently
178
+ guessing when the provider returns no count.
179
+ - **No fabricated economics.** ACV must be explicit or CRM-derived (closed-won) — there
180
+ are no band defaults, and `estimate` refuses without a real ACV. Buyers/account and
181
+ ACV both carry a `source` label so a thin signal is visible, never hidden.
182
+ - **Population never auto-writes.** `tam populate` schedules plan-only acquire; apply
183
+ is a separate gate, the meter is charged only at apply, and the allowlist requires
184
+ `--save`.
185
+ - **Coverage is deterministic** from the CRM snapshot + the model baseline — no LLM,
186
+ byte-stable, comparable run-over-run.
187
+ - **ETA is refused when unprojectable** rather than faked.
188
+
189
+ ## Library
190
+
191
+ `estimateTam`, `computeCoverage`, `coverageCountsFromSnapshot`, `projectEta`,
192
+ `tamReportToMarkdown`, the store (`saveTamModel`/`loadTamModel`/`appendCoverage`/
193
+ `readCoverageTimeline`), the count probe (`probeExploriumBusinessCount` +
194
+ `EXPLORIUM_BUSINESS_COUNT_CAP`), and the ICP→firmographic filter
195
+ (`icpToExploriumBusinessFilters`) are all exported for programmatic use.