fullstackgtm 0.40.0 → 0.42.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.
@@ -0,0 +1,209 @@
1
+ import { type LlmCallOptions } from "./llm.ts";
2
+ import { type Icp } from "./icp.ts";
3
+ import type { Signal, SignalBucket, SignalOutcome } from "./signals.ts";
4
+ import { computeWeights } from "./signals.ts";
5
+ import type { CanonicalGtmSnapshot } from "./types.ts";
6
+ /**
7
+ * The judge layer: turn raw signals into a short ranked list of decisions on
8
+ * `timing × fit × memory`. Every other layer answers "is this CRM record
9
+ * correct?"; the judge answers "did something change at this account this week
10
+ * worth reaching out *about*, to the right person, whom we haven't already
11
+ * chased?" — and, crucially, says **no** when the answer is no.
12
+ *
13
+ * GOVERNANCE — structural, forever: judge is read-only. It reads signals + CRM
14
+ * history and writes a ranked DECISION artifact to the LOCAL judge store; it
15
+ * emits no `PatchOperation`, opens no plan, touches no provider record. `--save`
16
+ * persists a `JudgeRun` and stamps the consumed signals `judgedBy`. The write
17
+ * side is downstream and human-gated (`draft` → `plans approve` → `apply`).
18
+ *
19
+ * EVIDENCE GATE — structural: a decision's `whyNow` must be a verbatim
20
+ * normalized-whitespace substring of a credited signal's stored `quote`. The
21
+ * deterministic baseline takes the why-now verbatim from the top signal's quote
22
+ * (so it is grounded by construction); the LLM path is gated after the call —
23
+ * an ungrounded why-now is rejected (one retry, then fall back to the
24
+ * deterministic why-now). The brain NEVER stores an ungrounded why-now.
25
+ */
26
+ export type JudgeDecisionKind = "send" | "nurture" | "skip";
27
+ export type JudgeDecision = {
28
+ accountDomain: string;
29
+ /** 0-100. */
30
+ score: number;
31
+ decision: JudgeDecisionKind;
32
+ /** MUST be a verbatim normalized-whitespace substring of a credited signal.quote. */
33
+ whyNow: string;
34
+ /** One line a rep could say (LLM), or "" (deterministic baseline). */
35
+ play: string;
36
+ /** Credited signal ids — the evidence anchors for this decision. */
37
+ evidence: string[];
38
+ skippedReason?: string;
39
+ /** "deterministic" | "llm:<provider>:<model>". */
40
+ producedBy: string;
41
+ };
42
+ export type JudgeRun = {
43
+ id: string;
44
+ runLabel: string;
45
+ /** The signal run this judgement consumed. */
46
+ signalRunLabel: string;
47
+ createdAt: string;
48
+ decisions: JudgeDecision[];
49
+ };
50
+ export type JudgeBand = "strong" | "good" | "weak" | "noise";
51
+ export declare function scoreBand(score: number): JudgeBand;
52
+ export declare function judgeRunId(runLabel: string): string;
53
+ /**
54
+ * Whitespace/punctuation-spacing-normalized, lowercased match — the EXACT rule
55
+ * `call`/`market` use for verbatim evidence gates (llm.ts normalizeSpan). A
56
+ * local copy keeps this file importable without the LLM engine (the cross-module
57
+ * duplication precedent in signals.ts/market.ts).
58
+ */
59
+ export declare function normalizeSpan(value: string): string;
60
+ /** Verbatim evidence gate: `produced` must be a ≥12-char normalized substring of `source`. */
61
+ export declare function isGroundedSpan(produced: string, source: string): boolean;
62
+ export type AccountScore = {
63
+ accountDomain: string;
64
+ /** 0-100. */
65
+ score: number;
66
+ band: JudgeBand;
67
+ decision: JudgeDecisionKind;
68
+ /** The single highest-credited signal — the why-now anchor. */
69
+ topSignal: Signal;
70
+ /** All credited signal ids (the cluster). */
71
+ evidence: string[];
72
+ /** Raw 0..1 fit against the best-matching contact. */
73
+ fit: number;
74
+ /** Raw timing component (max bucket weight × recency [+cluster bonus]). */
75
+ timing: number;
76
+ /** True when the account was touched within the memory window (caps to nurture/skip). */
77
+ recentlyTouched: boolean;
78
+ skippedReason?: string;
79
+ /**
80
+ * Internal: every credited signal indexed by id, attached by `judgeSignals`
81
+ * so the LLM path can resolve quotes beyond `topSignal`. Not part of the
82
+ * stored artifact.
83
+ */
84
+ _signalsById?: Map<string, Signal>;
85
+ };
86
+ /**
87
+ * Score one account on timing × fit × memory and map to a 0-100 score + band +
88
+ * decision. Deterministic.
89
+ *
90
+ * - **timing**: each credited signal's bucket weight (learned overlay over
91
+ * config defaults via `computeWeights`) × recency decay × its already-baked
92
+ * repost boost; the account's timing is the MAX credited signal weight, plus a
93
+ * cluster bonus when ≥2 signals are fresh (the article's compounding "fresh
94
+ * round AND a live req" case → 80-100 band).
95
+ * - **fit**: `scoreProspectAgainstIcp(bestContact, icp)` (0..1). No ICP / no
96
+ * contact → a neutral 0.5 so a strong, well-grounded timing signal can still
97
+ * surface (fit then doesn't sink an otherwise-hot account).
98
+ * - **memory** (`recentlyTouched`): touched within `TOUCHED_WINDOW_DAYS` → never
99
+ * pile on: cap the decision at `nurture` (or `skip` if already lukewarm).
100
+ *
101
+ * `decision` from the band: strong→send, good→nurture, weak/noise→skip — then
102
+ * the memory cap applies. `skip` is a first-class output: a lone low-intent
103
+ * signal (e.g. a single `social` blip) lands in `weak`/`noise` and is skipped
104
+ * with a reason, not mailed.
105
+ */
106
+ export declare function scoreAccount(opts: {
107
+ accountDomain: string;
108
+ signals: Signal[];
109
+ weights: Record<SignalBucket, number>;
110
+ icp?: Icp;
111
+ bestContact?: {
112
+ jobTitle?: string;
113
+ jobLevel?: string;
114
+ jobDepartment?: string;
115
+ headline?: string;
116
+ };
117
+ recentlyTouched?: boolean;
118
+ now?: Date;
119
+ }): AccountScore;
120
+ /**
121
+ * Has this account been touched within `TOUCHED_WINDOW_DAYS`? Reads the snapshot:
122
+ * the account record's `lastActivityAt`, plus any `activities` whose `occurredAt`
123
+ * is recent and that link to the account (directly or via a contact at it). Pure;
124
+ * with no snapshot, returns false (the no-history path — caller passes `false`).
125
+ */
126
+ export declare function accountRecentlyTouched(accountDomain: string, snapshot: CanonicalGtmSnapshot, now?: Date, windowDays?: number): boolean;
127
+ /**
128
+ * Find the best-matching contact for an account from the snapshot, for fit
129
+ * scoring: the account's contacts, preferring one with a title (a title is what
130
+ * fit scores on). Returns undefined when the account/contact isn't in snapshot.
131
+ */
132
+ export declare function bestContactForAccount(accountDomain: string, snapshot: CanonicalGtmSnapshot): {
133
+ jobTitle?: string;
134
+ jobLevel?: string;
135
+ jobDepartment?: string;
136
+ headline?: string;
137
+ } | undefined;
138
+ /**
139
+ * Build a `JudgeDecision` from an `AccountScore` with the deterministic baseline:
140
+ * whyNow taken VERBATIM from the top credited signal's quote (grounded by
141
+ * construction), no `play`, producedBy "deterministic".
142
+ */
143
+ export declare function deterministicDecision(score: AccountScore): JudgeDecision;
144
+ /**
145
+ * A grounded why-now drawn verbatim from the signal's quote — capped to a
146
+ * sentence-ish span so it reads as a why-now, not the whole JD. We take the
147
+ * leading clause up to the first sentence boundary (or the whole quote if
148
+ * short), guaranteeing `isGroundedSpan(whyNow, quote)` holds.
149
+ */
150
+ export declare function deterministicWhyNow(signal: Signal): string;
151
+ export declare const JUDGE_SCHEMA: {
152
+ readonly type: "object";
153
+ readonly required: readonly ["decision", "whyNow", "play"];
154
+ readonly properties: {
155
+ readonly decision: {
156
+ readonly type: "string";
157
+ readonly enum: readonly ["send", "nurture", "skip"];
158
+ };
159
+ readonly whyNow: {
160
+ readonly type: "string";
161
+ readonly description: string;
162
+ };
163
+ readonly play: {
164
+ readonly type: "string";
165
+ readonly description: "One line a rep could say out loud, grounded in the trigger. Max 25 words. No em dashes.";
166
+ };
167
+ readonly skippedReason: {
168
+ readonly type: "string";
169
+ readonly description: "skip only: one phrase why this is not a send.";
170
+ };
171
+ };
172
+ };
173
+ export declare const DEFAULT_JUDGE_PROMPT = "You are a GTM judge deciding whether to reach out to an account based on fresh signals.\nScore bands (the operator owns these \u2014 edit this prompt to change them):\n- 80-100 (send): strong fit AND high intent \u2014 a live demand search, fresh funding, or >=2 clustered signals.\n- 50-79 (nurture): good fit and one solid signal, but not enough to interrupt cold.\n- 20-49 (skip): a weak or lone low-intent signal.\n- 0-19 (skip): off-ICP or noise.\nRules:\n- whyNow MUST be a verbatim span copied from one of the signal quotes below. If you cannot ground a why-now in a real quote, you cannot send.\n- play is one line a rep could say out loud, tied to the trigger. No greeting, no \"Hi {{firstName}}\". No em dashes.\n- Saying \"skip\" is a first-class, valuable answer. A judge that skips a lukewarm account is doing its job.";
174
+ /**
175
+ * The LLM judge for one account. Forced tool call with `JUDGE_SCHEMA` + an
176
+ * editable prompt; then the verbatim-quote GATE: `whyNow` must be a normalized
177
+ * ≥12-char substring of one of the credited signal quotes. Retry once with
178
+ * corrective feedback (the market classify idiom), then fall back to the
179
+ * deterministic why-now — NEVER store an ungrounded why-now. The deterministic
180
+ * `score`/`band`/`decision`/`evidence` are authoritative; the model contributes
181
+ * `whyNow` (gated) and `play` only — score is not delegated to the model.
182
+ */
183
+ export declare function judgeDecisionLlm(score: AccountScore, promptTemplate: string, llm: LlmCallOptions): Promise<JudgeDecision>;
184
+ /**
185
+ * Judge a batch of signals: group by account, score each, and produce one
186
+ * decision per account (deterministic when no LLM options, gated-LLM otherwise).
187
+ * `--min-score` filtering and the snapshot/ICP wiring happen in the caller; this
188
+ * takes resolved inputs. Returns decisions sorted by score desc (the ranked list).
189
+ */
190
+ export declare function judgeSignals(opts: {
191
+ signals: Signal[];
192
+ outcomes?: SignalOutcome[];
193
+ config: Parameters<typeof computeWeights>[0];
194
+ icp?: Icp;
195
+ snapshot?: CanonicalGtmSnapshot;
196
+ withHistory?: boolean;
197
+ promptTemplate?: string;
198
+ llm?: LlmCallOptions;
199
+ now?: Date;
200
+ }): Promise<JudgeDecision[]>;
201
+ export declare function judgeDir(baseDir?: string): string;
202
+ export interface JudgeStore {
203
+ /** Append a new judge run; refuses an existing label (runs are append-only). */
204
+ appendRun(run: JudgeRun): Promise<JudgeRun>;
205
+ getRun(runLabel: string): Promise<JudgeRun | null>;
206
+ listRuns(): Promise<JudgeRun[]>;
207
+ latestRun(): Promise<JudgeRun | null>;
208
+ }
209
+ export declare function createFileJudgeStore(directory?: string): JudgeStore;