fullstackgtm 0.31.0 → 0.33.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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,60 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
5
  and the project adheres to [Semantic Versioning](https://semver.org/).
6
6
  The path to 1.0 is planned in [docs/roadmap-to-1.0.md](./docs/roadmap-to-1.0.md).
7
7
 
8
+ ## [0.33.0] — 2026-06-18
9
+
10
+ ### Added
11
+
12
+ - **Market sourcing helpers (`marketSourcing.ts`)** — find the right page to
13
+ capture per vendor, detect acquired/redirected vendors, and extract brand logos,
14
+ with zero coupling to any transport:
15
+ - `pickCategoryPage(html, baseUrl, keywords)` — follow a vendor's own nav to its
16
+ category page (so multi-product companies like SAP/Salesforce are captured on
17
+ the product page, not the corporate homepage). Pure.
18
+ - `findCategoryPageInSitemap(rootUrl, keywords, fetchText?)` — sitemap fallback
19
+ for JS-mega-menu sites whose product links aren't in the rendered homepage.
20
+ - `findCategoryPage(...)` — nav-scan then sitemap, combined.
21
+ - `detectDrift(url, srcHost, resolve?)` / `resolveFinalUrl(url)` — skip vendors
22
+ whose site redirects to a different company (an acquired/defunct product).
23
+ - `extractLogoUrl(html, baseUrl)` + `fetchLogoDataUri(homepageUrl, html?, …)` —
24
+ a vendor logo as a self-contained `data:` URI for `MarketVendor.logo`.
25
+ - `categoryKeywords()` + `registrableDomain()` utilities.
26
+
27
+ Pure functions operate on already-fetched HTML; fetching helpers default to the
28
+ package's SSRF-guarded `assertPublicUrl` + `fetch` but accept an injectable
29
+ fetcher (testable offline, browser-render-friendly). 8 tests.
30
+
31
+ ## [0.32.0] — 2026-06-18
32
+
33
+ ### Added
34
+
35
+ - **Brand logos plot inside the market-map scatter bubbles.** When a vendor has
36
+ a `data:image/…` logo and its dot is big enough to read it (r ≥ 12), the logo
37
+ becomes the in-bubble label: a white disc clipped to the circle carries the
38
+ logo, a colored rim still ties the dot to its legend color, and the legend
39
+ number moves just above so the cross-reference holds. Smaller or logo-less
40
+ dots keep the numbered-bubble treatment. Same `img-src data:` safety as the
41
+ legend/header logos — only `data:` URIs are honored; anything else is refused
42
+ and never emitted as an `<image>`. Render stays byte-deterministic.
43
+
44
+ - **Call-type classification + type-specific coaching rubrics.** New `call
45
+ classify` verb runs a deterministic, key-free signal classifier over a
46
+ transcript (12 call types: prospecting, discovery, demo, technical_validation,
47
+ negotiation, closing, onboarding, check_in, renewal_expansion, qbr, internal,
48
+ other), returning the type with a confidence and a written reason; `--llm`
49
+ adds a model tiebreak for the ambiguous middle, `--list` prints the taxonomy.
50
+ `call score` now **auto-selects the type-specific rubric** from that
51
+ classification instead of applying one generic discovery rubric to every call
52
+ — override with `--call-type <type>` or `--rubric`. The `Rubric` type gains
53
+ optional `name`, `callType`, `bands`, and per-dimension `anchorsHigh` /
54
+ `anchorsLow` / `evidenceCues` / `coachingPrompts`; anchored examples are
55
+ threaded into the scoring prompt (cuts variance) and the weighted overall is
56
+ mapped to a qualitative band, all computed client-side. Ten built-in presets
57
+ (`call score --list-rubrics`). New exports: `classifyCall`, `classifyCallLlm`,
58
+ `rubricForCallType`, `rubricPresets`, `bandForScore`, `CALL_TYPES`,
59
+ `CALL_TYPE_IDS`, `BANDS_5`, and the `CallType` / `CallTypeDef` /
60
+ `CallClassification` / `RubricDimension` / `ScoreBand` types.
61
+
8
62
  ## [0.31.0] — 2026-06-18
9
63
 
10
64
  ### Added
package/README.md CHANGED
@@ -59,14 +59,18 @@ Calls are where pipeline truth lives. `call parse` normalizes any transcript dia
59
59
 
60
60
  **Extraction is LLM-powered by default, with your own key.** The first time you run `call parse` or `call score`, the CLI asks for an Anthropic or OpenAI API key (auto-detected from the prefix), validates it against the provider, and stores it in the 0600 credential store — same treatment as CRM logins. Or skip the prompt: set `ANTHROPIC_API_KEY`/`OPENAI_API_KEY`, or `echo "$KEY" | fullstackgtm login anthropic` (or `openai`). The key talks directly to your provider — raw fetch, no SDK, no middleman. `--model` overrides the defaults (claude-haiku-4-5 / gpt-4o-mini). LLM insights carry verbatim-quote evidence and, for next steps, owner/deadline/commitment. Pass `--deterministic` for the free, instant, byte-stable keyword baseline (no key needed — right for CI and warehouse bulk loads). Every insight is provenance-marked (`extractor: "llm:anthropic:…"` vs `"deterministic"`).
61
61
 
62
- **`call score`** rates the call against a coaching rubric five built-in dimensions (discovery, next steps, stakeholders, value, objections) or your own via `--rubric rubric.json` (`{ scale, dimensions: [{ name, weight, rubric }] }`); the weighted overall is computed deterministically client-side, every dimension score is evidence-quoted, and the rubric file is where your client-specific coaching framework lives.
62
+ **`call classify`** picks the call type before scoring, because a renewal scored on "Depth of Discovery" is just wrong. It runs a **deterministic, key-free** signal classifier over the transcript — 12 call types (prospecting, discovery, demo, technical_validation, negotiation, closing, onboarding, check_in, renewal_expansion, qbr, internal, other) returning the type with a confidence and a written reason; pass `--llm` for a model tiebreak on the ambiguous middle. It needs no API key, so it runs free in CI and on warehouse bulk loads (`--list` prints the taxonomy).
63
+
64
+ **`call score`** rates the call against a coaching rubric. By default it **auto-selects the type-specific rubric** from the classification — each call type ships its own dimensions with anchored high/low examples and evidence cues (cuts scoring variance). Override with `--call-type <type>` to force a preset, or `--rubric rubric.json` for your own (`{ scale, name?, callType?, bands?, dimensions: [{ name, weight, rubric, anchorsHigh?, anchorsLow?, evidenceCues?, coachingPrompts? }] }`). The weighted overall is computed deterministically client-side, mapped to a qualitative band, every dimension score is evidence-quoted, and the rubric file is where your client-specific coaching framework lives (`--list-rubrics` prints the built-in presets).
63
65
 
64
66
  `call link` answers "which deal was this call about" from attendee domains (account domain or contact emails → open deals, most recent activity first, with confidence + reason). `call plan` turns next-step insights into the same governed plan lifecycle as everything else.
65
67
 
66
68
  ```bash
67
- # Coaching pipeline (Slack + CRM): parse → score → link → govern the writeback
69
+ # Coaching pipeline (Slack + CRM): parse → classify → score → link → govern the writeback
68
70
  fullstackgtm call parse --transcript call.txt --title "Acme disco" --out parsed.json # LLM extraction
69
- fullstackgtm call score --call parsed.json --rubric team-rubric.json # evidence-quoted scorecard
71
+ fullstackgtm call classify --call parsed.json # deterministic call type
72
+ fullstackgtm call score --call parsed.json # auto-selects the type's rubric
73
+ fullstackgtm call score --call parsed.json --rubric team-rubric.json # …or bring your own framework
70
74
  fullstackgtm call link --attendees jane@acme.com --provider hubspot # → deal id + reason
71
75
  fullstackgtm call plan --call parsed.json --deal 123 --provider hubspot --save
72
76
  # review → plans approve → apply: deal.next_step + follow-up tasks, compare-and-set protected
@@ -0,0 +1,72 @@
1
+ import type { Rubric, ScoreBand } from "./llm.ts";
2
+ /**
3
+ * Call-type taxonomy, a deterministic signal-based classifier, and one
4
+ * type-specific coaching rubric per call type.
5
+ *
6
+ * The design mirrors the rest of the package: deterministic by default,
7
+ * explainable, and byte-stable. `classifyCall` reads a transcript and returns
8
+ * a ranked classification with a written reason — no LLM, no key, no network —
9
+ * so it runs free in CI and on warehouse bulk loads. The LLM tiebreak
10
+ * (`classifyCallLlm` in llm.ts) is opt-in, for the ambiguous middle.
11
+ *
12
+ * Scoring a renewal against a discovery rubric is simply wrong, so each call
13
+ * type carries its own rubric: the dimensions that matter for *that*
14
+ * conversation, with anchored high/low examples and evidence cues that sharpen
15
+ * the verbatim-quote grounding the scorer already requires.
16
+ */
17
+ export type CallType = "prospecting" | "discovery" | "demo" | "technical_validation" | "negotiation" | "closing" | "onboarding" | "check_in" | "renewal_expansion" | "qbr" | "internal" | "other";
18
+ export declare const CALL_TYPE_IDS: CallType[];
19
+ export type CallTypeDef = {
20
+ id: CallType;
21
+ name: string;
22
+ phase: "pre_sale" | "post_sale" | "neither";
23
+ definition: string;
24
+ /** Phrases that, when present, argue *for* this type (lowercased, substring match). */
25
+ signals: string[];
26
+ /** Phrases that argue *against* this type even if a signal matched. */
27
+ negativeSignals?: string[];
28
+ /** Types this is commonly confused with — surfaced in the classifier reason. */
29
+ confusedWith?: CallType[];
30
+ };
31
+ /**
32
+ * Our own taxonomy. Signals are authored phrases, not lifted from any third
33
+ * party; they are intentionally conservative (high-precision verbs and nouns
34
+ * that rarely appear off-topic) so the deterministic pass is trustworthy and
35
+ * the ambiguous remainder is handed to the LLM tiebreak rather than guessed.
36
+ */
37
+ export declare const CALL_TYPES: CallTypeDef[];
38
+ export type CallClassification = {
39
+ type: CallType;
40
+ confidence: "high" | "low" | "none";
41
+ reason: string;
42
+ /** All types that matched at least one signal, strongest first. */
43
+ candidates: Array<{
44
+ type: CallType;
45
+ score: number;
46
+ matched: string[];
47
+ }>;
48
+ method: "deterministic";
49
+ };
50
+ /**
51
+ * Classify a call from its transcript using authored phrase signals. Pure,
52
+ * deterministic, key-free. Score = distinct positive signals matched minus
53
+ * distinct negative signals; the strongest type wins. Confidence is `high`
54
+ * only when one type clearly leads (>=2 signals and a >=2 margin over the
55
+ * runner-up); a single weak signal yields `low`; no signal yields `other`/none.
56
+ */
57
+ export declare function classifyCall(transcript: string): CallClassification;
58
+ /** Shared qualitative bands over a 1-5 weighted overall. */
59
+ export declare const BANDS_5: ScoreBand[];
60
+ /**
61
+ * Pick the qualitative band for a weighted overall: the highest band whose
62
+ * `min` the score reaches. Deterministic, client-side — same input, same band.
63
+ */
64
+ export declare function bandForScore(score: number, bands: ScoreBand[]): ScoreBand | undefined;
65
+ /**
66
+ * The type-specific rubric for a call type, or the generic fallback. The
67
+ * generic rubric is intentionally discovery-leaning (the most common scored
68
+ * call) and is what `other`/`internal`/unknown resolve to.
69
+ */
70
+ export declare function rubricForCallType(type: CallType, fallback: Rubric): Rubric;
71
+ /** All authored presets, for `--list` and docs. */
72
+ export declare function rubricPresets(): Rubric[];