fullstackgtm 0.15.0 → 0.17.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 +84 -0
- package/dist/cli.js +240 -2
- package/dist/connectors/hubspot.js +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/llm.d.ts +7 -0
- package/dist/llm.js +7 -1
- package/dist/market.d.ts +166 -0
- package/dist/market.js +395 -0
- package/dist/marketClassify.d.ts +49 -0
- package/dist/marketClassify.js +201 -0
- package/dist/marketReport.d.ts +3 -0
- package/dist/marketReport.js +233 -0
- package/dist/mcp.js +45 -0
- package/dist/resolve.js +36 -17
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
- package/src/cli.ts +264 -3
- package/src/connectors/hubspot.ts +1 -1
- package/src/index.ts +38 -0
- package/src/llm.ts +7 -1
- package/src/market.ts +559 -0
- package/src/marketClassify.ts +286 -0
- package/src/marketReport.ts +272 -0
- package/src/mcp.ts +65 -0
- package/src/resolve.ts +39 -19
- package/src/types.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,90 @@ 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.17.0] — 2026-06-11
|
|
9
|
+
|
|
10
|
+
Market map classification: intensity readings become a one-command step, and
|
|
11
|
+
the verbatim-quote rule becomes a mechanical gate instead of a prompt
|
|
12
|
+
instruction.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`fullstackgtm market classify`** — LLM intensity readings for every
|
|
17
|
+
vendor × claim cell from the stored captures, through the same
|
|
18
|
+
bring-your-own-key constrained-tool-call seam as `call parse`
|
|
19
|
+
(provenance `extractor: "llm:<provider>:<model>"`). Vendors with no
|
|
20
|
+
usable captures score UNOBSERVABLE deterministically, without an LLM
|
|
21
|
+
call. `--vendor` classifies one vendor to `--out` for hand-merging;
|
|
22
|
+
`--model` overrides the provider default.
|
|
23
|
+
- **Mechanical span verification** — because market sources are *stored*
|
|
24
|
+
captures (unlike transcripts, which pass through), every quoted evidence
|
|
25
|
+
span is checked character-for-character (whitespace-normalized) against
|
|
26
|
+
the capture it cites. Readings that fail bounce back to the model once
|
|
27
|
+
with the failures named; persistent failures abort with nothing stored.
|
|
28
|
+
The same gate now guards `market observe` (escape hatch: `--unverified`,
|
|
29
|
+
for sets whose captures genuinely live elsewhere) and the MCP submission
|
|
30
|
+
path — every proposal channel passes the same gate.
|
|
31
|
+
- **`fullstackgtm market worksheet --vendor <id>`** — the no-key channel:
|
|
32
|
+
a self-contained packet (claims with judging definitions, surface rule,
|
|
33
|
+
captured page texts) for an agent or human to classify by hand and
|
|
34
|
+
submit via `observe`.
|
|
35
|
+
- **`fullstackgtm market refresh`** — capture → classify → front drift →
|
|
36
|
+
HTML field report, one command. The weekly refresh is now a single
|
|
37
|
+
invocation (schedule it however you schedule things).
|
|
38
|
+
- **MCP**: `fullstackgtm_market_worksheet` and `fullstackgtm_market_observe`
|
|
39
|
+
(validates + verifies + appends; returns the computed front states on
|
|
40
|
+
acceptance).
|
|
41
|
+
- `forcedToolCall` exported from `llm.ts` — the one seam every LLM feature
|
|
42
|
+
in the package goes through.
|
|
43
|
+
|
|
44
|
+
## [0.16.0] — 2026-06-11
|
|
45
|
+
|
|
46
|
+
The market map: a live model of the competitive category a company sells
|
|
47
|
+
into — claim taxonomy as reviewable config, append-only observations with
|
|
48
|
+
verbatim-quote evidence, deterministic front states and drift.
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
|
|
52
|
+
- **`fullstackgtm market`** — `init` scaffolds a `market.config.json`
|
|
53
|
+
(vendor registry + claim taxonomy + the LOUD/QUIET/ABSENT surface rule);
|
|
54
|
+
`capture` fetches vendor pages into a content-addressed text cache (the
|
|
55
|
+
change detector, replay buffer, and evidence chain); `observe --from`
|
|
56
|
+
ingests intensity readings after validating full coverage and the
|
|
57
|
+
verbatim-evidence rule (a loud/quiet reading with no quote is rejected);
|
|
58
|
+
`fronts` computes deterministic front states (open / contested / owned /
|
|
59
|
+
saturated / vacant) and `--diff` reports drift between runs; `report`
|
|
60
|
+
renders the claim × vendor matrix as markdown or a self-contained
|
|
61
|
+
printable HTML field report with an evidence appendix.
|
|
62
|
+
- **Division of labor matches call intelligence:** intensity readings are
|
|
63
|
+
proposals (provenance-marked extractor, always with quoted evidence);
|
|
64
|
+
everything downstream is deterministic over the stored observations —
|
|
65
|
+
same observations, same map. A failed capture reads as UNOBSERVABLE,
|
|
66
|
+
never as absence.
|
|
67
|
+
- **Profile-scoped storage:** captures and observations live under
|
|
68
|
+
`~/.fullstackgtm/market/<category>` (or the active profile's home), so one
|
|
69
|
+
client org's category intel never bleeds into another's.
|
|
70
|
+
- `ObservationStore` contract + file implementation (append-only, one JSON
|
|
71
|
+
document per run) — like the plan store, the file layout and the hosted
|
|
72
|
+
backend are two implementations of the same contract.
|
|
73
|
+
- `"web"` joined `GtmEvidenceSourceSystem`: market observations carry
|
|
74
|
+
standard `GtmEvidence` with `metadata.url` + `metadata.captureHash`.
|
|
75
|
+
|
|
76
|
+
## [0.15.1] — 2026-06-11
|
|
77
|
+
|
|
78
|
+
Fixes from the 0.15.0 journey verification (4 agents, 26 checks).
|
|
79
|
+
|
|
80
|
+
### Fixed
|
|
81
|
+
|
|
82
|
+
- **Name-only deal resolution is gated**: `resolve deal --name X` without
|
|
83
|
+
`--account-id` returned safe_to_create even when open deals named X
|
|
84
|
+
existed — a gate that ignores name collisions protects nobody. It now
|
|
85
|
+
returns ambiguous with the colliding open deals and tells the caller to
|
|
86
|
+
supply `--account-id` for a definitive answer.
|
|
87
|
+
- The closed-deals-share-name note is account-scoped (it previously counted
|
|
88
|
+
closed deals on *other* accounts).
|
|
89
|
+
- The `hs_object_source_detail_2` stamp on CLI-created companies now
|
|
90
|
+
carries the operation id for precise attribution.
|
|
91
|
+
|
|
8
92
|
## [0.15.0] — 2026-06-11
|
|
9
93
|
|
|
10
94
|
The Prevent layer: stop creating duplicates, and name the writer that does.
|
package/dist/cli.js
CHANGED
|
@@ -18,6 +18,9 @@ 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, loadCaptureTexts, loadMarketConfig, starterMarketConfig, validateObservationSet, verifyEvidenceSpans, } from "./market.js";
|
|
22
|
+
import { buildWorksheet, classifyMarket } from "./marketClassify.js";
|
|
23
|
+
import { marketMapToHtml, marketMapToMarkdown } from "./marketReport.js";
|
|
21
24
|
import { DEFAULT_RUBRIC, detectProviderFromKey, extractInsightsLlm, parseRubric, resolveLlmCredential, scoreCallLlm, validateLlmKey, } from "./llm.js";
|
|
22
25
|
import { resolveRecord } from "./resolve.js";
|
|
23
26
|
import { suggestValues } from "./suggest.js";
|
|
@@ -55,6 +58,20 @@ Usage:
|
|
|
55
58
|
fullstackgtm resolve <account|contact|deal> [--name N] [--domain D] [--email E] [--account-id A] [source options] [--json]
|
|
56
59
|
the create gate: exit 0 = safe to create, exit 2 = match
|
|
57
60
|
found (exists/ambiguous) — call before ANY record creation
|
|
61
|
+
fullstackgtm market init --category <name> start a market map: vendors + claim taxonomy as reviewable config
|
|
62
|
+
fullstackgtm market capture [--config <path>] [--run <label>]
|
|
63
|
+
fullstackgtm market classify [--run <label>] [--vendor <id>] [--model m] [--out <path>]
|
|
64
|
+
fullstackgtm market worksheet --vendor <id> [--out <path>]
|
|
65
|
+
fullstackgtm market observe --from <observations.json> [--unverified]
|
|
66
|
+
fullstackgtm market fronts [--run <label>] [--diff <prior-run>] [--json]
|
|
67
|
+
fullstackgtm market report [--run <label>] [--format md|html] [--out <path>]
|
|
68
|
+
fullstackgtm market refresh [--run <label>] [--model m]
|
|
69
|
+
the live competitive map: capture vendor pages (content-addressed),
|
|
70
|
+
classify intensity per claim (LLM bring-your-own-key, or fill the
|
|
71
|
+
worksheet with any agent) — every quoted span is verified verbatim
|
|
72
|
+
against the stored capture it cites before it's accepted — then
|
|
73
|
+
compute deterministic front states and drift, render the field
|
|
74
|
+
report. refresh = capture → classify → drift → report in one step
|
|
58
75
|
fullstackgtm suggest --plan-id <id> | --plan <path> [source options] [--json] [--out <path>]
|
|
59
76
|
derive values for requires_human_* placeholders
|
|
60
77
|
from snapshot evidence, with confidence + reasons
|
|
@@ -606,9 +623,14 @@ async function requireLlmCredential(command = "parse") {
|
|
|
606
623
|
if (resolved)
|
|
607
624
|
return resolved;
|
|
608
625
|
// Scoring is inherently LLM work — there is no keyword fallback to suggest.
|
|
609
|
-
const fallbackHint = command === "parse"
|
|
626
|
+
const fallbackHint = command === "parse"
|
|
627
|
+
? ", or pass --deterministic for the free keyword baseline"
|
|
628
|
+
: command === "score"
|
|
629
|
+
? " (call score has no non-LLM mode)"
|
|
630
|
+
: ", or classify by hand: `market worksheet --vendor <id>` then `market observe --from`";
|
|
631
|
+
const work = command === "score" ? "scoring" : command === "parse" ? "extraction" : "classification";
|
|
610
632
|
if (!process.stdin.isTTY) {
|
|
611
|
-
throw new Error(`LLM ${
|
|
633
|
+
throw new Error(`LLM ${work} needs an API key. Set ANTHROPIC_API_KEY or OPENAI_API_KEY, or run \`echo "$KEY" | fullstackgtm login anthropic\` (or \`login openai\`) once${fallbackHint}.`);
|
|
612
634
|
}
|
|
613
635
|
console.error("LLM parsing needs an API key (Anthropic or OpenAI) — yours, used directly with the provider.");
|
|
614
636
|
console.error(`Paste it once; it is validated and stored at ${credentialsPath()} (file mode 0600), like CRM logins.`);
|
|
@@ -715,6 +737,218 @@ function buildCallPlan(parsed, deal, proposed, current, extraNextSteps) {
|
|
|
715
737
|
operations,
|
|
716
738
|
};
|
|
717
739
|
}
|
|
740
|
+
/**
|
|
741
|
+
* The market map: claim taxonomy in a reviewable config file, page captures
|
|
742
|
+
* and append-only observations under the profile home, deterministic front
|
|
743
|
+
* states and reports computed from the store. Intensity readings enter as
|
|
744
|
+
* proposals through two channels — `classify` (LLM, bring-your-own-key, the
|
|
745
|
+
* call-intelligence pattern) and `worksheet`/`observe` (an agent or human
|
|
746
|
+
* fills the worksheet) — and BOTH pass the same mechanical gate: every quoted
|
|
747
|
+
* span is verified verbatim against the stored capture it cites.
|
|
748
|
+
*/
|
|
749
|
+
async function marketCommand(args) {
|
|
750
|
+
const [subcommand, ...rest] = args;
|
|
751
|
+
const configPath = () => resolve(process.cwd(), option(rest, "--config") ?? "market.config.json");
|
|
752
|
+
if (!subcommand || subcommand === "--help") {
|
|
753
|
+
console.log(`Usage:
|
|
754
|
+
market init --category <name> [--out <path>] write a starter market.config.json
|
|
755
|
+
market capture [--config <path>] [--run <label>]
|
|
756
|
+
market classify [--run <label>] [--capture-run <label>] [--vendor <id>] [--model m] [--out <path>]
|
|
757
|
+
market worksheet --vendor <id> [--capture-run <label>] [--out <path>]
|
|
758
|
+
market observe --from <observations.json> [--unverified]
|
|
759
|
+
market fronts [--config <path>] [--run <label>] [--diff <prior-run>] [--json]
|
|
760
|
+
market report [--config <path>] [--run <label>] [--format md|html] [--out <path>]
|
|
761
|
+
market refresh [--run <label>] [--model m] capture → classify → fronts drift → HTML report
|
|
762
|
+
|
|
763
|
+
classify uses your Anthropic/OpenAI key (like call parse) to read the stored
|
|
764
|
+
captures and propose intensity readings; worksheet is the no-key path (an
|
|
765
|
+
agent or human fills it, submits via observe). Either way, every quoted span
|
|
766
|
+
is verified character-for-character against the capture it cites before the
|
|
767
|
+
observation is accepted — quotes that aren't on the page bounce.
|
|
768
|
+
|
|
769
|
+
The taxonomy (vendors + claims) is config you review and version; captures
|
|
770
|
+
and observations live under ~/.fullstackgtm/market/<category> (profile-scoped,
|
|
771
|
+
one client's category intel never bleeds into another's). Front states are
|
|
772
|
+
recomputed deterministically on every invocation — never stored.`);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
if (subcommand === "init") {
|
|
776
|
+
const category = option(rest, "--category");
|
|
777
|
+
if (!category)
|
|
778
|
+
throw new Error("market init requires --category <name>");
|
|
779
|
+
const outPath = resolve(process.cwd(), option(rest, "--out") ?? "market.config.json");
|
|
780
|
+
if (existsSync(outPath))
|
|
781
|
+
throw new Error(`${outPath} already exists — refusing to overwrite`);
|
|
782
|
+
writeFileSync(outPath, `${JSON.stringify(starterMarketConfig(category), null, 2)}\n`);
|
|
783
|
+
console.log(`Wrote ${outPath}. Fill in vendors and claims, then: fullstackgtm market capture`);
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
const config = loadMarketConfig(configPath());
|
|
787
|
+
const store = createFileObservationStore(config.category);
|
|
788
|
+
if (subcommand === "capture") {
|
|
789
|
+
const result = await captureMarket(config, { runLabel: option(rest, "--run") ?? "run-1" });
|
|
790
|
+
for (const entry of result.entries) {
|
|
791
|
+
const flag = entry.captureHash && entry.textChars > 500 ? "" : " <-- thin/empty";
|
|
792
|
+
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}`);
|
|
793
|
+
}
|
|
794
|
+
console.log(`\nmanifest: ${result.manifestPath}`);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
if (subcommand === "observe") {
|
|
798
|
+
const fromPath = option(rest, "--from");
|
|
799
|
+
if (!fromPath)
|
|
800
|
+
throw new Error("market observe requires --from <observations.json>");
|
|
801
|
+
const set = JSON.parse(readFileSync(resolve(process.cwd(), fromPath), "utf8"));
|
|
802
|
+
const problems = validateObservationSet(config, set);
|
|
803
|
+
if (problems.length > 0) {
|
|
804
|
+
console.error(`Rejected: ${problems.length} problem(s)`);
|
|
805
|
+
for (const problem of problems.slice(0, 20))
|
|
806
|
+
console.error(` - ${problem}`);
|
|
807
|
+
process.exitCode = 1;
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
if (!rest.includes("--unverified")) {
|
|
811
|
+
const { textByHash } = loadCaptureTexts(config.category);
|
|
812
|
+
const failures = verifyEvidenceSpans(set.observations, textByHash);
|
|
813
|
+
if (failures.length > 0) {
|
|
814
|
+
console.error(`Rejected: ${failures.length} evidence span(s) failed verification against the stored captures`);
|
|
815
|
+
for (const failure of failures.slice(0, 20)) {
|
|
816
|
+
console.error(` - ${failure.vendorId} × ${failure.claimId}: ${failure.problem}`);
|
|
817
|
+
}
|
|
818
|
+
console.error("Quotes must be copied verbatim from the captured pages. (--unverified skips this gate when the captures genuinely live elsewhere.)");
|
|
819
|
+
process.exitCode = 1;
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
await store.append(set);
|
|
824
|
+
console.log(`Appended ${set.runLabel}: ${set.observations.length} observations (${set.extractor})`);
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
if (subcommand === "worksheet") {
|
|
828
|
+
const vendorId = option(rest, "--vendor");
|
|
829
|
+
if (!vendorId)
|
|
830
|
+
throw new Error("market worksheet requires --vendor <id>");
|
|
831
|
+
const worksheet = buildWorksheet(config, vendorId, { captureRun: option(rest, "--capture-run") ?? undefined });
|
|
832
|
+
const outPath = option(rest, "--out");
|
|
833
|
+
const payload = `${JSON.stringify(worksheet, null, 2)}\n`;
|
|
834
|
+
if (outPath) {
|
|
835
|
+
writeFileSync(resolve(process.cwd(), outPath), payload);
|
|
836
|
+
console.log(`Wrote ${outPath} (${worksheet.pages.length} captured pages, ${worksheet.claims.length} claims)`);
|
|
837
|
+
}
|
|
838
|
+
else {
|
|
839
|
+
console.log(payload);
|
|
840
|
+
}
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
if (subcommand === "classify") {
|
|
844
|
+
const credential = await requireLlmCredential("market classify");
|
|
845
|
+
const vendorFilter = option(rest, "--vendor");
|
|
846
|
+
const outPath = option(rest, "--out");
|
|
847
|
+
if (vendorFilter && !outPath) {
|
|
848
|
+
throw new Error("market classify --vendor produces a partial set (coverage validation would reject it) — pass --out <path> to inspect/merge it by hand");
|
|
849
|
+
}
|
|
850
|
+
const result = await classifyMarket(config, {
|
|
851
|
+
llm: { ...credential, model: option(rest, "--model") ?? undefined },
|
|
852
|
+
runLabel: option(rest, "--run") ?? option(rest, "--capture-run") ?? "run-1",
|
|
853
|
+
captureRun: option(rest, "--capture-run") ?? undefined,
|
|
854
|
+
vendors: vendorFilter ? [vendorFilter] : undefined,
|
|
855
|
+
});
|
|
856
|
+
if (result.retriedVendorIds.length > 0) {
|
|
857
|
+
console.error(`Span verification bounced ${result.retriedVendorIds.join(", ")} once; retry passed.`);
|
|
858
|
+
}
|
|
859
|
+
if (outPath) {
|
|
860
|
+
writeFileSync(resolve(process.cwd(), outPath), `${JSON.stringify(result.set, null, 2)}\n`);
|
|
861
|
+
console.log(`Wrote ${outPath}: ${result.set.observations.length} verified observations (${result.set.extractor})`);
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
const problems = validateObservationSet(config, result.set);
|
|
865
|
+
if (problems.length > 0) {
|
|
866
|
+
throw new Error(`Classified set failed validation: ${problems.slice(0, 5).join("; ")}`);
|
|
867
|
+
}
|
|
868
|
+
await store.append(result.set);
|
|
869
|
+
console.log(`Appended ${result.set.runLabel}: ${result.set.observations.length} observations, every span verified (${result.set.extractor})`);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
if (subcommand === "refresh") {
|
|
873
|
+
const credential = await requireLlmCredential("market classify");
|
|
874
|
+
const runLabel = option(rest, "--run") ?? `run-${new Date().toISOString().slice(0, 10)}`;
|
|
875
|
+
const prior = await store.latest();
|
|
876
|
+
console.log(`Capturing ${config.vendors.length} vendors as ${runLabel}…`);
|
|
877
|
+
const captured = await captureMarket(config, { runLabel });
|
|
878
|
+
const failed = captured.entries.filter((entry) => !entry.captureHash);
|
|
879
|
+
if (failed.length > 0)
|
|
880
|
+
console.log(`${failed.length} page(s) failed/empty — affected cells will verify against remaining pages or read unobservable.`);
|
|
881
|
+
console.log(`Classifying with ${credential.provider}…`);
|
|
882
|
+
const result = await classifyMarket(config, {
|
|
883
|
+
llm: { ...credential, model: option(rest, "--model") ?? undefined },
|
|
884
|
+
runLabel,
|
|
885
|
+
captureRun: runLabel,
|
|
886
|
+
});
|
|
887
|
+
await store.append(result.set);
|
|
888
|
+
const fronts = computeFrontStates(config, result.set);
|
|
889
|
+
if (prior) {
|
|
890
|
+
const drift = diffFrontStates(computeFrontStates(config, prior), fronts);
|
|
891
|
+
if (drift.length === 0)
|
|
892
|
+
console.log(`No front changes since ${prior.runLabel}.`);
|
|
893
|
+
for (const change of drift)
|
|
894
|
+
console.log(`CHANGED ${change.claimId}: ${change.before} → ${change.after}`);
|
|
895
|
+
}
|
|
896
|
+
const outPath = option(rest, "--out") ?? `${config.category}-${runLabel}.html`;
|
|
897
|
+
writeFileSync(resolve(process.cwd(), outPath), marketMapToHtml(config, result.set));
|
|
898
|
+
console.log(`Wrote ${outPath}`);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
const loadSet = async () => {
|
|
902
|
+
const runLabel = option(rest, "--run");
|
|
903
|
+
const set = runLabel ? await store.get(runLabel) : await store.latest();
|
|
904
|
+
if (!set) {
|
|
905
|
+
throw new Error(runLabel
|
|
906
|
+
? `No observation run "${runLabel}" for ${config.category}`
|
|
907
|
+
: `No observations stored for ${config.category} — run market observe --from <file> first`);
|
|
908
|
+
}
|
|
909
|
+
return set;
|
|
910
|
+
};
|
|
911
|
+
if (subcommand === "fronts") {
|
|
912
|
+
const set = await loadSet();
|
|
913
|
+
const fronts = computeFrontStates(config, set);
|
|
914
|
+
const priorLabel = option(rest, "--diff");
|
|
915
|
+
const prior = priorLabel ? await store.get(priorLabel) : null;
|
|
916
|
+
if (priorLabel && !prior)
|
|
917
|
+
throw new Error(`No observation run "${priorLabel}" to diff against`);
|
|
918
|
+
const drift = prior ? diffFrontStates(computeFrontStates(config, prior), fronts) : null;
|
|
919
|
+
if (rest.includes("--json")) {
|
|
920
|
+
console.log(JSON.stringify({ runLabel: set.runLabel, fronts, drift }, null, 2));
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
for (const front of fronts) {
|
|
924
|
+
const owner = front.state === "owned" ? ` → ${front.loudVendorIds[0]}` : "";
|
|
925
|
+
console.log(`${front.state.toUpperCase().padEnd(10)} ${front.claimId}${owner}`);
|
|
926
|
+
}
|
|
927
|
+
if (drift) {
|
|
928
|
+
console.log("");
|
|
929
|
+
if (drift.length === 0)
|
|
930
|
+
console.log(`No front changes since ${priorLabel}.`);
|
|
931
|
+
for (const change of drift)
|
|
932
|
+
console.log(`CHANGED ${change.claimId}: ${change.before} → ${change.after}`);
|
|
933
|
+
}
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
if (subcommand === "report") {
|
|
937
|
+
const set = await loadSet();
|
|
938
|
+
const format = option(rest, "--format") ?? "md";
|
|
939
|
+
const output = format === "html" ? marketMapToHtml(config, set) : marketMapToMarkdown(config, set);
|
|
940
|
+
const outPath = option(rest, "--out");
|
|
941
|
+
if (outPath) {
|
|
942
|
+
writeFileSync(resolve(process.cwd(), outPath), output);
|
|
943
|
+
console.log(`Wrote ${outPath}`);
|
|
944
|
+
}
|
|
945
|
+
else {
|
|
946
|
+
console.log(output);
|
|
947
|
+
}
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
throw new Error(`Unknown market subcommand: ${subcommand} (try: init, capture, classify, worksheet, observe, fronts, report, refresh)`);
|
|
951
|
+
}
|
|
718
952
|
/**
|
|
719
953
|
* The resolve gate: exit 0 = safe to create, exit 2 = match found (exists or
|
|
720
954
|
* ambiguous — do NOT blind-create), exit 1 = error. Built for sync jobs and
|
|
@@ -1496,6 +1730,10 @@ export async function runCli(argv) {
|
|
|
1496
1730
|
await resolveCommand(args);
|
|
1497
1731
|
return;
|
|
1498
1732
|
}
|
|
1733
|
+
if (command === "market") {
|
|
1734
|
+
await marketCommand(args);
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1499
1737
|
if (command === "profiles") {
|
|
1500
1738
|
profilesCommand(args);
|
|
1501
1739
|
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:
|
|
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,8 @@ 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, loadCaptureTexts, loadMarketConfig, marketHome, normalizeForMatch, observationId, parseMarketConfig, starterMarketConfig, validateObservationSet, verifyEvidenceSpans, 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, type SpanVerificationFailure, } from "./market.ts";
|
|
23
|
+
export { buildWorksheet, classifyMarket, type ClassifyMarketOptions, type ClassifyMarketResult, type MarketWorksheet, } from "./marketClassify.ts";
|
|
24
|
+
export { marketMapToHtml, marketMapToMarkdown } from "./marketReport.ts";
|
|
22
25
|
export { suggestValues, type SuggestionConfidence, type ValueSuggestion } from "./suggest.ts";
|
|
23
26
|
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,7 @@ 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, loadCaptureTexts, loadMarketConfig, marketHome, normalizeForMatch, observationId, parseMarketConfig, starterMarketConfig, validateObservationSet, verifyEvidenceSpans, } from "./market.js";
|
|
23
|
+
export { buildWorksheet, classifyMarket, } from "./marketClassify.js";
|
|
24
|
+
export { marketMapToHtml, marketMapToMarkdown } from "./marketReport.js";
|
|
22
25
|
export { suggestValues } from "./suggest.js";
|
package/dist/llm.d.ts
CHANGED
|
@@ -64,6 +64,13 @@ export declare function scoreCallLlm(transcript: string, rubric: Rubric, options
|
|
|
64
64
|
title?: string;
|
|
65
65
|
}): Promise<CallScorecard>;
|
|
66
66
|
export declare function parseRubric(json: string): Rubric;
|
|
67
|
+
/**
|
|
68
|
+
* Shared constrained-tool-call plumbing: force the model to answer through a
|
|
69
|
+
* single tool whose input_schema is the output contract. Exported for other
|
|
70
|
+
* semi-deterministic features (market classification) — every LLM feature in
|
|
71
|
+
* the package goes through this one seam.
|
|
72
|
+
*/
|
|
73
|
+
export declare function forcedToolCall(prompt: string, toolName: string, schema: object, model: string, options: LlmCallOptions): Promise<unknown>;
|
|
67
74
|
/** Cheap key validation against the provider's model-list endpoint. Status line only. */
|
|
68
75
|
export declare function validateLlmKey(provider: LlmProvider, apiKey: string, fetchImpl?: typeof fetch): Promise<{
|
|
69
76
|
ok: boolean;
|
package/dist/llm.js
CHANGED
|
@@ -158,7 +158,13 @@ export function parseRubric(json) {
|
|
|
158
158
|
};
|
|
159
159
|
}
|
|
160
160
|
// ── Provider plumbing (raw fetch, forced tool calls) ───────────────────────
|
|
161
|
-
|
|
161
|
+
/**
|
|
162
|
+
* Shared constrained-tool-call plumbing: force the model to answer through a
|
|
163
|
+
* single tool whose input_schema is the output contract. Exported for other
|
|
164
|
+
* semi-deterministic features (market classification) — every LLM feature in
|
|
165
|
+
* the package goes through this one seam.
|
|
166
|
+
*/
|
|
167
|
+
export async function forcedToolCall(prompt, toolName, schema, model, options) {
|
|
162
168
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
163
169
|
if (options.provider === "anthropic") {
|
|
164
170
|
const response = await llmFetch(fetchImpl, ANTHROPIC_URL, {
|
package/dist/market.d.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
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 declare function loadCaptureTexts(category: string, directory?: string): {
|
|
130
|
+
entries: CaptureEntry[];
|
|
131
|
+
textByHash: Map<string, string>;
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* Whitespace-only normalization for span matching, plus one extraction
|
|
135
|
+
* artifact: the HTML-to-text step can emit a line break before punctuation
|
|
136
|
+
* that follows an inline tag ("placements\n. Districts"), which no honest
|
|
137
|
+
* quoter would reproduce — so whitespace *before* punctuation is dropped
|
|
138
|
+
* too. Words, casing, and characters must still match the page exactly.
|
|
139
|
+
*/
|
|
140
|
+
export declare function normalizeForMatch(value: string): string;
|
|
141
|
+
export type SpanVerificationFailure = {
|
|
142
|
+
vendorId: string;
|
|
143
|
+
claimId: string;
|
|
144
|
+
quote: string;
|
|
145
|
+
problem: string;
|
|
146
|
+
};
|
|
147
|
+
export declare function verifyEvidenceSpans(observations: MarketObservation[], textByHash: Map<string, string>): SpanVerificationFailure[];
|
|
148
|
+
export type ClaimFront = {
|
|
149
|
+
claimId: string;
|
|
150
|
+
state: FrontState;
|
|
151
|
+
loudVendorIds: string[];
|
|
152
|
+
quietVendorIds: string[];
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* Front rule v1: 0 loud → open (if anyone is quiet) or vacant; 1 loud →
|
|
156
|
+
* owned; 2–3 loud → contested; ≥4 loud → saturated. Unobservable cells are
|
|
157
|
+
* excluded — a failed capture never reads as absence.
|
|
158
|
+
*/
|
|
159
|
+
export declare function computeFrontStates(config: MarketConfig, set: ObservationSet): ClaimFront[];
|
|
160
|
+
export type FrontDrift = {
|
|
161
|
+
claimId: string;
|
|
162
|
+
before: FrontState;
|
|
163
|
+
after: FrontState;
|
|
164
|
+
};
|
|
165
|
+
/** What changed in the category between two runs — the refresh's whole point. */
|
|
166
|
+
export declare function diffFrontStates(before: ClaimFront[], after: ClaimFront[]): FrontDrift[];
|