fullstackgtm 0.15.0 → 0.16.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,54 @@ 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.16.0] — 2026-06-11
9
+
10
+ The market map: a live model of the competitive category a company sells
11
+ into — claim taxonomy as reviewable config, append-only observations with
12
+ verbatim-quote evidence, deterministic front states and drift.
13
+
14
+ ### Added
15
+
16
+ - **`fullstackgtm market`** — `init` scaffolds a `market.config.json`
17
+ (vendor registry + claim taxonomy + the LOUD/QUIET/ABSENT surface rule);
18
+ `capture` fetches vendor pages into a content-addressed text cache (the
19
+ change detector, replay buffer, and evidence chain); `observe --from`
20
+ ingests intensity readings after validating full coverage and the
21
+ verbatim-evidence rule (a loud/quiet reading with no quote is rejected);
22
+ `fronts` computes deterministic front states (open / contested / owned /
23
+ saturated / vacant) and `--diff` reports drift between runs; `report`
24
+ renders the claim × vendor matrix as markdown or a self-contained
25
+ printable HTML field report with an evidence appendix.
26
+ - **Division of labor matches call intelligence:** intensity readings are
27
+ proposals (provenance-marked extractor, always with quoted evidence);
28
+ everything downstream is deterministic over the stored observations —
29
+ same observations, same map. A failed capture reads as UNOBSERVABLE,
30
+ never as absence.
31
+ - **Profile-scoped storage:** captures and observations live under
32
+ `~/.fullstackgtm/market/<category>` (or the active profile's home), so one
33
+ client org's category intel never bleeds into another's.
34
+ - `ObservationStore` contract + file implementation (append-only, one JSON
35
+ document per run) — like the plan store, the file layout and the hosted
36
+ backend are two implementations of the same contract.
37
+ - `"web"` joined `GtmEvidenceSourceSystem`: market observations carry
38
+ standard `GtmEvidence` with `metadata.url` + `metadata.captureHash`.
39
+
40
+ ## [0.15.1] — 2026-06-11
41
+
42
+ Fixes from the 0.15.0 journey verification (4 agents, 26 checks).
43
+
44
+ ### Fixed
45
+
46
+ - **Name-only deal resolution is gated**: `resolve deal --name X` without
47
+ `--account-id` returned safe_to_create even when open deals named X
48
+ existed — a gate that ignores name collisions protects nobody. It now
49
+ returns ambiguous with the colliding open deals and tells the caller to
50
+ supply `--account-id` for a definitive answer.
51
+ - The closed-deals-share-name note is account-scoped (it previously counted
52
+ closed deals on *other* accounts).
53
+ - The `hs_object_source_detail_2` stamp on CLI-created companies now
54
+ carries the operation id for precise attribution.
55
+
8
56
  ## [0.15.0] — 2026-06-11
9
57
 
10
58
  The Prevent layer: stop creating duplicates, and name the writer that does.
package/dist/cli.js CHANGED
@@ -18,6 +18,8 @@ import { auditReportToHtml, auditReportToMarkdown } from "./report.js";
18
18
  import { builtinAuditRules } from "./rules.js";
19
19
  import { sampleSnapshot } from "./sampleData.js";
20
20
  import { normalizeTranscript, parseCall, suggestCallDeal } from "./calls.js";
21
+ import { captureMarket, computeFrontStates, createFileObservationStore, diffFrontStates, loadMarketConfig, starterMarketConfig, validateObservationSet, } from "./market.js";
22
+ import { marketMapToHtml, marketMapToMarkdown } from "./marketReport.js";
21
23
  import { DEFAULT_RUBRIC, detectProviderFromKey, extractInsightsLlm, parseRubric, resolveLlmCredential, scoreCallLlm, validateLlmKey, } from "./llm.js";
22
24
  import { resolveRecord } from "./resolve.js";
23
25
  import { suggestValues } from "./suggest.js";
@@ -55,6 +57,15 @@ Usage:
55
57
  fullstackgtm resolve <account|contact|deal> [--name N] [--domain D] [--email E] [--account-id A] [source options] [--json]
56
58
  the create gate: exit 0 = safe to create, exit 2 = match
57
59
  found (exists/ambiguous) — call before ANY record creation
60
+ fullstackgtm market init --category <name> start a market map: vendors + claim taxonomy as reviewable config
61
+ fullstackgtm market capture [--config <path>] [--run <label>]
62
+ fullstackgtm market observe --from <observations.json>
63
+ fullstackgtm market fronts [--run <label>] [--diff <prior-run>] [--json]
64
+ fullstackgtm market report [--run <label>] [--format md|html] [--out <path>]
65
+ the live competitive map: capture vendor pages (content-addressed),
66
+ ingest intensity readings with verbatim-quote evidence, compute
67
+ deterministic front states (open/contested/owned/saturated) and
68
+ drift between runs, render the client-ready field report
58
69
  fullstackgtm suggest --plan-id <id> | --plan <path> [source options] [--json] [--out <path>]
59
70
  derive values for requires_human_* placeholders
60
71
  from snapshot evidence, with confidence + reasons
@@ -715,6 +726,120 @@ function buildCallPlan(parsed, deal, proposed, current, extraNextSteps) {
715
726
  operations,
716
727
  };
717
728
  }
729
+ /**
730
+ * The market map: claim taxonomy in a reviewable config file, page captures
731
+ * and append-only observations under the profile home, deterministic front
732
+ * states and reports computed from the store. Classification (LLM intensity
733
+ * readings) lands in a later change; until then `market observe --from`
734
+ * ingests proposal files produced by an agent or a human.
735
+ */
736
+ async function marketCommand(args) {
737
+ const [subcommand, ...rest] = args;
738
+ const configPath = () => resolve(process.cwd(), option(rest, "--config") ?? "market.config.json");
739
+ if (!subcommand || subcommand === "--help") {
740
+ console.log(`Usage:
741
+ market init --category <name> [--out <path>] write a starter market.config.json
742
+ market capture [--config <path>] [--run <label>]
743
+ market observe --from <observations.json> [--config <path>]
744
+ market fronts [--config <path>] [--run <label>] [--diff <prior-run>] [--json]
745
+ market report [--config <path>] [--run <label>] [--format md|html] [--out <path>]
746
+
747
+ The taxonomy (vendors + claims) is config you review and version; captures
748
+ and observations live under ~/.fullstackgtm/market/<category> (profile-scoped,
749
+ one client's category intel never bleeds into another's). Front states are
750
+ recomputed deterministically on every invocation — never stored.`);
751
+ return;
752
+ }
753
+ if (subcommand === "init") {
754
+ const category = option(rest, "--category");
755
+ if (!category)
756
+ throw new Error("market init requires --category <name>");
757
+ const outPath = resolve(process.cwd(), option(rest, "--out") ?? "market.config.json");
758
+ if (existsSync(outPath))
759
+ throw new Error(`${outPath} already exists — refusing to overwrite`);
760
+ writeFileSync(outPath, `${JSON.stringify(starterMarketConfig(category), null, 2)}\n`);
761
+ console.log(`Wrote ${outPath}. Fill in vendors and claims, then: fullstackgtm market capture`);
762
+ return;
763
+ }
764
+ const config = loadMarketConfig(configPath());
765
+ const store = createFileObservationStore(config.category);
766
+ if (subcommand === "capture") {
767
+ const result = await captureMarket(config, { runLabel: option(rest, "--run") ?? "run-1" });
768
+ for (const entry of result.entries) {
769
+ const flag = entry.captureHash && entry.textChars > 500 ? "" : " <-- thin/empty";
770
+ console.log(`${entry.vendorId.padEnd(16)} ${entry.kind.padEnd(8)} ${String(entry.httpStatus ?? "ERR").padEnd(4)} ${String(entry.textChars).padStart(7)} chars ${entry.url}${flag}`);
771
+ }
772
+ console.log(`\nmanifest: ${result.manifestPath}`);
773
+ return;
774
+ }
775
+ if (subcommand === "observe") {
776
+ const fromPath = option(rest, "--from");
777
+ if (!fromPath)
778
+ throw new Error("market observe requires --from <observations.json>");
779
+ const set = JSON.parse(readFileSync(resolve(process.cwd(), fromPath), "utf8"));
780
+ const problems = validateObservationSet(config, set);
781
+ if (problems.length > 0) {
782
+ console.error(`Rejected: ${problems.length} problem(s)`);
783
+ for (const problem of problems.slice(0, 20))
784
+ console.error(` - ${problem}`);
785
+ process.exitCode = 1;
786
+ return;
787
+ }
788
+ await store.append(set);
789
+ console.log(`Appended ${set.runLabel}: ${set.observations.length} observations (${set.extractor})`);
790
+ return;
791
+ }
792
+ const loadSet = async () => {
793
+ const runLabel = option(rest, "--run");
794
+ const set = runLabel ? await store.get(runLabel) : await store.latest();
795
+ if (!set) {
796
+ throw new Error(runLabel
797
+ ? `No observation run "${runLabel}" for ${config.category}`
798
+ : `No observations stored for ${config.category} — run market observe --from <file> first`);
799
+ }
800
+ return set;
801
+ };
802
+ if (subcommand === "fronts") {
803
+ const set = await loadSet();
804
+ const fronts = computeFrontStates(config, set);
805
+ const priorLabel = option(rest, "--diff");
806
+ const prior = priorLabel ? await store.get(priorLabel) : null;
807
+ if (priorLabel && !prior)
808
+ throw new Error(`No observation run "${priorLabel}" to diff against`);
809
+ const drift = prior ? diffFrontStates(computeFrontStates(config, prior), fronts) : null;
810
+ if (rest.includes("--json")) {
811
+ console.log(JSON.stringify({ runLabel: set.runLabel, fronts, drift }, null, 2));
812
+ return;
813
+ }
814
+ for (const front of fronts) {
815
+ const owner = front.state === "owned" ? ` → ${front.loudVendorIds[0]}` : "";
816
+ console.log(`${front.state.toUpperCase().padEnd(10)} ${front.claimId}${owner}`);
817
+ }
818
+ if (drift) {
819
+ console.log("");
820
+ if (drift.length === 0)
821
+ console.log(`No front changes since ${priorLabel}.`);
822
+ for (const change of drift)
823
+ console.log(`CHANGED ${change.claimId}: ${change.before} → ${change.after}`);
824
+ }
825
+ return;
826
+ }
827
+ if (subcommand === "report") {
828
+ const set = await loadSet();
829
+ const format = option(rest, "--format") ?? "md";
830
+ const output = format === "html" ? marketMapToHtml(config, set) : marketMapToMarkdown(config, set);
831
+ const outPath = option(rest, "--out");
832
+ if (outPath) {
833
+ writeFileSync(resolve(process.cwd(), outPath), output);
834
+ console.log(`Wrote ${outPath}`);
835
+ }
836
+ else {
837
+ console.log(output);
838
+ }
839
+ return;
840
+ }
841
+ throw new Error(`Unknown market subcommand: ${subcommand} (try: init, capture, observe, fronts, report)`);
842
+ }
718
843
  /**
719
844
  * The resolve gate: exit 0 = safe to create, exit 2 = match found (exists or
720
845
  * ambiguous — do NOT blind-create), exit 1 = error. Built for sync jobs and
@@ -1496,6 +1621,10 @@ export async function runCli(argv) {
1496
1621
  await resolveCommand(args);
1497
1622
  return;
1498
1623
  }
1624
+ if (command === "market") {
1625
+ await marketCommand(args);
1626
+ return;
1627
+ }
1499
1628
  if (command === "profiles") {
1500
1629
  profilesCommand(args);
1501
1630
  return;
@@ -334,7 +334,7 @@ export function createHubspotConnector(options) {
334
334
  created = await request(`/crm/v3/objects/companies`, {
335
335
  method: "POST",
336
336
  body: JSON.stringify({
337
- properties: { name, hs_object_source_detail_2: "fullstackgtm create: operation" },
337
+ properties: { name, hs_object_source_detail_2: `fullstackgtm create: (${operation.id})` },
338
338
  }),
339
339
  });
340
340
  }
package/dist/index.d.ts CHANGED
@@ -19,5 +19,7 @@ export { extractCallInsights, normalizeTranscript, parseCall, parseTranscript, s
19
19
  export { sampleSnapshot } from "./sampleData.ts";
20
20
  export { DEFAULT_MODELS, DEFAULT_RUBRIC, detectProviderFromKey, extractInsightsLlm, parseRubric, resolveLlmCredential, scoreCallLlm, validateLlmKey, type CallScorecard, type LlmCredential, type LlmExtractedInsight, type LlmProvider, type Rubric, type ScoredDimension, } from "./llm.ts";
21
21
  export { resolveRecord, type ResolveCandidate, type ResolveMatch, type ResolveResult } from "./resolve.ts";
22
+ export { captureMarket, computeFrontStates, createFileObservationStore, diffFrontStates, extractReadableText, loadMarketConfig, marketHome, observationId, parseMarketConfig, starterMarketConfig, validateObservationSet, type CaptureEntry, type CaptureOptions, type ClaimFront, type ClaimIntensity, type FrontDrift, type FrontState, type MarketClaim, type MarketConfig, type MarketObservation, type MarketVendor, type ObservationConfidence, type ObservationSet, type ObservationStore, } from "./market.ts";
23
+ export { marketMapToHtml, marketMapToMarkdown } from "./marketReport.ts";
22
24
  export { suggestValues, type SuggestionConfidence, type ValueSuggestion } from "./suggest.ts";
23
25
  export type { ApprovalStatus, AuditFinding, AuditFindingSeverity, CanonicalAccount, CanonicalActivity, CanonicalContact, CanonicalDeal, CanonicalGtmSnapshot, CanonicalUser, CrmProvider, GtmAuditRule, GtmConnector, GtmEvidence, GtmEvidenceSourceSystem, GtmObjectType, GtmPolicy, GtmRuleContext, GtmRuleResult, GtmSnapshotIndex, PatchOperation, PatchOperationResult, PatchOperationType, PatchPlan, PatchPlanRun, PatchPlanRunStatus, PatchVerification, PipelineFinding, PipelineFindingStatus, PipelineFindingType, ProviderIdentity, RiskLevel, SourceFreshness, } from "./types.ts";
package/dist/index.js CHANGED
@@ -19,4 +19,6 @@ export { extractCallInsights, normalizeTranscript, parseCall, parseTranscript, s
19
19
  export { sampleSnapshot } from "./sampleData.js";
20
20
  export { DEFAULT_MODELS, DEFAULT_RUBRIC, detectProviderFromKey, extractInsightsLlm, parseRubric, resolveLlmCredential, scoreCallLlm, validateLlmKey, } from "./llm.js";
21
21
  export { resolveRecord } from "./resolve.js";
22
+ export { captureMarket, computeFrontStates, createFileObservationStore, diffFrontStates, extractReadableText, loadMarketConfig, marketHome, observationId, parseMarketConfig, starterMarketConfig, validateObservationSet, } from "./market.js";
23
+ export { marketMapToHtml, marketMapToMarkdown } from "./marketReport.js";
22
24
  export { suggestValues } from "./suggest.js";
@@ -0,0 +1,147 @@
1
+ import type { GtmEvidence } from "./types.ts";
2
+ /**
3
+ * The Market Map: a live model of the competitive category a company sells
4
+ * into. Vendors publish claims constantly (pricing pages, feature pages,
5
+ * hero copy); each (vendor × claim) cell gets a messaging-intensity reading,
6
+ * and each claim row gets a derived front state. Observations are
7
+ * append-only — history is the product; "what changed since last run" is a
8
+ * first-class question.
9
+ *
10
+ * Division of labor mirrors call intelligence: intensity readings are
11
+ * *proposals* (LLM or human, always with verbatim quoted evidence), while
12
+ * everything downstream — front states, drift, the report — is deterministic
13
+ * over the stored observations. Same stored observations, same map.
14
+ *
15
+ * The claim taxonomy and vendor registry live in a reviewable config file
16
+ * (git-friendly, analyst-edited); captures and observations live under the
17
+ * profile home so one client's category intel never bleeds into another's.
18
+ */
19
+ export type ClaimIntensity = "loud" | "quiet" | "absent" | "unobservable";
20
+ export type ObservationConfidence = "high" | "medium" | "low";
21
+ export type FrontState = "open" | "contested" | "owned" | "saturated" | "vacant";
22
+ export type MarketClaim = {
23
+ id: string;
24
+ /** The capability being claimed, precise enough to judge loud/quiet/absent. */
25
+ capability: string;
26
+ /** Which ICP the claim cell addresses (category-specific vocabulary). */
27
+ icp: string;
28
+ /** Which pricing structure the claim cell implies (category-specific). */
29
+ pricingStructure: string;
30
+ /** Operational definition: how a reader judges LOUD vs QUIET vs ABSENT. */
31
+ definition: string;
32
+ };
33
+ export type MarketVendor = {
34
+ id: string;
35
+ name: string;
36
+ urls: {
37
+ home: string;
38
+ /** null is itself an observation: no public pricing surface. */
39
+ pricing: string | null;
40
+ product: string[];
41
+ };
42
+ notes?: string;
43
+ };
44
+ export type MarketConfig = {
45
+ category: string;
46
+ anchorVendor?: string;
47
+ vendors: MarketVendor[];
48
+ claims: MarketClaim[];
49
+ /** The LOUD/QUIET/ABSENT/UNOBSERVABLE judging rule, stated for reviewers. */
50
+ surfaceRule?: string;
51
+ };
52
+ export type MarketObservation = {
53
+ /** stableHash(category, runLabel, vendorId, claimId) — deterministic. */
54
+ id: string;
55
+ vendorId: string;
56
+ claimId: string;
57
+ observedAt: string;
58
+ intensity: ClaimIntensity;
59
+ confidence: ObservationConfidence;
60
+ /** Reviewer-facing: why the reading is what it is. */
61
+ reason: string;
62
+ /**
63
+ * Verbatim quoted spans grounding any non-absent reading
64
+ * (sourceSystem "web", metadata.url + metadata.captureHash).
65
+ */
66
+ evidence: GtmEvidence[];
67
+ };
68
+ export type ObservationSet = {
69
+ id: string;
70
+ category: string;
71
+ runLabel: string;
72
+ runAt: string;
73
+ /** What produced the readings: "manual" or "llm:<provider>:<model>". */
74
+ extractor: string;
75
+ observations: MarketObservation[];
76
+ };
77
+ export type CaptureEntry = {
78
+ runLabel: string;
79
+ vendorId: string;
80
+ kind: "home" | "pricing" | "product";
81
+ url: string;
82
+ fetchedAt: string;
83
+ httpStatus: number | null;
84
+ /** sha256 of the extracted text; null when the fetch failed or was empty. */
85
+ captureHash: string | null;
86
+ textChars: number;
87
+ };
88
+ export declare function observationId(category: string, runLabel: string, vendorId: string, claimId: string): string;
89
+ export declare function parseMarketConfig(raw: string): MarketConfig;
90
+ export declare function loadMarketConfig(path: string): MarketConfig;
91
+ export declare function starterMarketConfig(category: string): MarketConfig;
92
+ export declare function marketHome(category: string, baseDir?: string): string;
93
+ export declare function extractReadableText(html: string): string;
94
+ export type FetchPage = (url: string) => Promise<{
95
+ status: number;
96
+ body: string;
97
+ }>;
98
+ export type CaptureOptions = {
99
+ /** Directory for captures; defaults to <marketHome>/captures. */
100
+ dir?: string;
101
+ runLabel?: string;
102
+ /** Injectable for tests; defaults to global fetch. */
103
+ fetchPage?: FetchPage;
104
+ now?: () => Date;
105
+ };
106
+ export type CaptureResult = {
107
+ entries: CaptureEntry[];
108
+ manifestPath: string;
109
+ };
110
+ export declare function captureMarket(config: MarketConfig, options?: CaptureOptions): Promise<CaptureResult>;
111
+ export interface ObservationStore {
112
+ append(set: ObservationSet): Promise<ObservationSet>;
113
+ get(runLabel: string): Promise<ObservationSet | null>;
114
+ list(): Promise<Array<{
115
+ runLabel: string;
116
+ runAt: string;
117
+ observations: number;
118
+ }>>;
119
+ latest(): Promise<ObservationSet | null>;
120
+ }
121
+ export declare function createFileObservationStore(category: string, directory?: string): ObservationStore;
122
+ /**
123
+ * Validate a proposed observation set against the config before it enters
124
+ * the store: known vendors/claims, full coverage, legal readings, and the
125
+ * verbatim-evidence rule (non-absent readings must quote something).
126
+ * Returns problems; an empty array means accept.
127
+ */
128
+ export declare function validateObservationSet(config: MarketConfig, set: ObservationSet): string[];
129
+ export type ClaimFront = {
130
+ claimId: string;
131
+ state: FrontState;
132
+ loudVendorIds: string[];
133
+ quietVendorIds: string[];
134
+ };
135
+ /**
136
+ * Front rule v1: 0 loud → open (if anyone is quiet) or vacant; 1 loud →
137
+ * owned; 2–3 loud → contested; ≥4 loud → saturated. Unobservable cells are
138
+ * excluded — a failed capture never reads as absence.
139
+ */
140
+ export declare function computeFrontStates(config: MarketConfig, set: ObservationSet): ClaimFront[];
141
+ export type FrontDrift = {
142
+ claimId: string;
143
+ before: FrontState;
144
+ after: FrontState;
145
+ };
146
+ /** What changed in the category between two runs — the refresh's whole point. */
147
+ export declare function diffFrontStates(before: ClaimFront[], after: ClaimFront[]): FrontDrift[];