fullstackgtm 0.16.0 → 0.18.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 +69 -0
- package/INSTALL_FOR_AGENTS.md +10 -5
- package/README.md +17 -0
- package/dist/cli.js +141 -12
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/llm.d.ts +7 -0
- package/dist/llm.js +7 -1
- package/dist/market.d.ts +35 -0
- package/dist/market.js +100 -0
- package/dist/marketAxes.d.ts +77 -0
- package/dist/marketAxes.js +199 -0
- package/dist/marketClassify.d.ts +49 -0
- package/dist/marketClassify.js +201 -0
- package/dist/marketReport.js +114 -1
- package/dist/mcp.js +45 -0
- package/docs/api.md +29 -2
- package/llms.txt +16 -0
- package/package.json +1 -1
- package/src/cli.ts +150 -12
- package/src/index.ts +24 -0
- package/src/llm.ts +7 -1
- package/src/market.ts +130 -0
- package/src/marketAxes.ts +268 -0
- package/src/marketClassify.ts +286 -0
- package/src/marketReport.ts +134 -1
- package/src/mcp.ts +65 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,75 @@ 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.18.0] — 2026-06-11
|
|
9
|
+
|
|
10
|
+
Axis discovery: earn a strategic 2×2 from the observations instead of
|
|
11
|
+
asserting one.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **Axes as config** — `axes` in `market.config.json`: each axis is a
|
|
16
|
+
claim-scoring rubric (`{ id, label, poles, rubric, status, claimScores }`,
|
|
17
|
+
null = axis doesn't apply to that claim); a vendor's position is the
|
|
18
|
+
intensity-weighted mean (loud=1, quiet=½) of the claims it voices.
|
|
19
|
+
`primaryAxes: [x, y]` picks the report's strategic map. Config validation
|
|
20
|
+
rejects axes scoring unknown claims.
|
|
21
|
+
- **`fullstackgtm market axes`** — the discovery math, pure and
|
|
22
|
+
dependency-free: PCA (power iteration) over the vendor × claim intensity
|
|
23
|
+
matrix — PC1 is the category's own primary axis, PC2 the
|
|
24
|
+
maximum-differentiation direction orthogonal to it; triangulation of every
|
|
25
|
+
configured axis against the PCs (a real axis is *derivable* from the data,
|
|
26
|
+
not just felt); and an orthogonality screen (|r| ≥ 0.75 = one axis twice —
|
|
27
|
+
sometimes the finding: the category couples the ideas and the empty
|
|
28
|
+
quadrant is the white space). Fully-unobservable vendors are excluded,
|
|
29
|
+
never zeroed.
|
|
30
|
+
- **Report: strategic map** — section 03 renders the primary 2×2 (positions
|
|
31
|
+
computed, not asserted; dot size = LOUD count; axis status in the caption)
|
|
32
|
+
when axes are configured; the evidence appendix renumbers accordingly. The
|
|
33
|
+
report deliberately carries only the one earned 2×2 — best foot forward
|
|
34
|
+
for the client; axis exploration (every pairing, r, verdicts) is `market
|
|
35
|
+
axes` territory for the analyst or agent doing the iterating.
|
|
36
|
+
- **Golden regression**: the 280-cell creative-intelligence validation
|
|
37
|
+
dataset ships as a test fixture — PCA must recover the buyer axis as PC1
|
|
38
|
+
(|r| ≥ 0.9) and value-mode as PC2 (|r| ≥ 0.85), and flag the documented
|
|
39
|
+
buyer × operating-model redundancy.
|
|
40
|
+
|
|
41
|
+
## [0.17.0] — 2026-06-11
|
|
42
|
+
|
|
43
|
+
Market map classification: intensity readings become a one-command step, and
|
|
44
|
+
the verbatim-quote rule becomes a mechanical gate instead of a prompt
|
|
45
|
+
instruction.
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- **`fullstackgtm market classify`** — LLM intensity readings for every
|
|
50
|
+
vendor × claim cell from the stored captures, through the same
|
|
51
|
+
bring-your-own-key constrained-tool-call seam as `call parse`
|
|
52
|
+
(provenance `extractor: "llm:<provider>:<model>"`). Vendors with no
|
|
53
|
+
usable captures score UNOBSERVABLE deterministically, without an LLM
|
|
54
|
+
call. `--vendor` classifies one vendor to `--out` for hand-merging;
|
|
55
|
+
`--model` overrides the provider default.
|
|
56
|
+
- **Mechanical span verification** — because market sources are *stored*
|
|
57
|
+
captures (unlike transcripts, which pass through), every quoted evidence
|
|
58
|
+
span is checked character-for-character (whitespace-normalized) against
|
|
59
|
+
the capture it cites. Readings that fail bounce back to the model once
|
|
60
|
+
with the failures named; persistent failures abort with nothing stored.
|
|
61
|
+
The same gate now guards `market observe` (escape hatch: `--unverified`,
|
|
62
|
+
for sets whose captures genuinely live elsewhere) and the MCP submission
|
|
63
|
+
path — every proposal channel passes the same gate.
|
|
64
|
+
- **`fullstackgtm market worksheet --vendor <id>`** — the no-key channel:
|
|
65
|
+
a self-contained packet (claims with judging definitions, surface rule,
|
|
66
|
+
captured page texts) for an agent or human to classify by hand and
|
|
67
|
+
submit via `observe`.
|
|
68
|
+
- **`fullstackgtm market refresh`** — capture → classify → front drift →
|
|
69
|
+
HTML field report, one command. The weekly refresh is now a single
|
|
70
|
+
invocation (schedule it however you schedule things).
|
|
71
|
+
- **MCP**: `fullstackgtm_market_worksheet` and `fullstackgtm_market_observe`
|
|
72
|
+
(validates + verifies + appends; returns the computed front states on
|
|
73
|
+
acceptance).
|
|
74
|
+
- `forcedToolCall` exported from `llm.ts` — the one seam every LLM feature
|
|
75
|
+
in the package goes through.
|
|
76
|
+
|
|
8
77
|
## [0.16.0] — 2026-06-11
|
|
9
78
|
|
|
10
79
|
The market map: a live model of the competitive category a company sells
|
package/INSTALL_FOR_AGENTS.md
CHANGED
|
@@ -49,11 +49,16 @@ In an agent sandbox, prefer rung 1 or 2. Never echo tokens into argv —
|
|
|
49
49
|
environments — login flows then print verification URLs instead of opening
|
|
50
50
|
the OS browser.
|
|
51
51
|
|
|
52
|
-
LLM calls (`call parse`, `call score`): set
|
|
53
|
-
`OPENAI_API_KEY` in the environment, or have the human
|
|
54
|
-
`echo "$KEY" | fullstackgtm login anthropic` once. Without a key, use
|
|
55
|
-
`call parse --deterministic` (free keyword baseline, no prompt)
|
|
56
|
-
|
|
52
|
+
LLM calls (`call parse`, `call score`, `market classify`): set
|
|
53
|
+
`ANTHROPIC_API_KEY` or `OPENAI_API_KEY` in the environment, or have the human
|
|
54
|
+
run `echo "$KEY" | fullstackgtm login anthropic` once. Without a key, use
|
|
55
|
+
`call parse --deterministic` (free keyword baseline, no prompt) — or, for the
|
|
56
|
+
market map, classify it yourself: `fullstackgtm market worksheet --vendor <id>`
|
|
57
|
+
returns the claims, judging rules, and captured page texts; submit your
|
|
58
|
+
readings via `market observe --from <file>`. Quote evidence VERBATIM from the
|
|
59
|
+
page texts — every span is checked character-for-character against the stored
|
|
60
|
+
capture, and paraphrased quotes are rejected. In non-interactive contexts the
|
|
61
|
+
CLI never prompts — it fails with this guidance.
|
|
57
62
|
|
|
58
63
|
Provider prerequisites (what the human must create, and which scopes) are in
|
|
59
64
|
the README's **"Connect your CRM"** section: HubSpot needs a private app with
|
package/README.md
CHANGED
|
@@ -109,6 +109,23 @@ npx fullstackgtm report --provider hubspot --client "Acme" --out acme-health.htm
|
|
|
109
109
|
|
|
110
110
|
`report` renders the same audit as a deliverable — severity counts up front, a prose summary, per-rule detail with example records, and next steps — as markdown or self-contained HTML (printable, emailable, no external assets).
|
|
111
111
|
|
|
112
|
+
## The market map: the category, observed
|
|
113
|
+
|
|
114
|
+
Your CRM records what happened in your own deals; nothing records the shape of the category you sell into. The **market map** does: vendors and a claim taxonomy live in a reviewable `market.config.json`, vendor pages are captured into a content-addressed cache, every vendor × claim cell gets a messaging-intensity reading (LOUD / QUIET / ABSENT — with UNOBSERVABLE for failed captures, never a fake absence), and deterministic front states fall out per claim: open, contested, owned, saturated.
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
fullstackgtm market init --category creative-intelligence # seed vendors + claims, edit by hand
|
|
118
|
+
fullstackgtm market capture # fetch pages → content-addressed captures
|
|
119
|
+
fullstackgtm market classify # LLM readings (BYO key), every quote verified
|
|
120
|
+
fullstackgtm market fronts --diff run-1 # what changed since last run
|
|
121
|
+
fullstackgtm market report --format html --out map.html # the client-ready field report
|
|
122
|
+
fullstackgtm market refresh # all of the above, weekly, one command
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The discipline matches the rest of the tool. Intensity readings are *proposals* — from the LLM (`classify`, same bring-your-own-key seam as `call parse`, provenance-marked) or from any agent/human (`market worksheet` → `market observe`) — and **every quoted evidence span is verified character-for-character against the stored capture it cites** before an observation is accepted. Quotes that aren't on the page bounce. Everything downstream of the store is deterministic: same observations, same map.
|
|
126
|
+
|
|
127
|
+
`market axes` is for earning a strategic 2×2 instead of asserting one: PCA over the intensity matrix (PC1 = the category's own primary axis, PC2 = the most differentiating direction orthogonal to it), triangulation of your configured axes against the data, and an orthogonality screen that flags two axes that are secretly one. Axes are claim-scoring rubrics in the config; the report renders the primary pair as the strategic map. Captures and observations are profile-scoped (`~/.fullstackgtm/market/<category>`), so one client's category intel never bleeds into another's.
|
|
128
|
+
|
|
112
129
|
### Working across organizations
|
|
113
130
|
|
|
114
131
|
Consultants and fractional operators hold credentials for several CRMs at once. A profile scopes stored logins *and* stored plans to one organization:
|
package/dist/cli.js
CHANGED
|
@@ -18,7 +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, loadMarketConfig, starterMarketConfig, validateObservationSet, } from "./market.js";
|
|
21
|
+
import { captureMarket, computeFrontStates, createFileObservationStore, diffFrontStates, loadCaptureTexts, loadMarketConfig, starterMarketConfig, validateObservationSet, verifyEvidenceSpans, } from "./market.js";
|
|
22
|
+
import { assessAxes, axesReportToText } from "./marketAxes.js";
|
|
23
|
+
import { buildWorksheet, classifyMarket } from "./marketClassify.js";
|
|
22
24
|
import { marketMapToHtml, marketMapToMarkdown } from "./marketReport.js";
|
|
23
25
|
import { DEFAULT_RUBRIC, detectProviderFromKey, extractInsightsLlm, parseRubric, resolveLlmCredential, scoreCallLlm, validateLlmKey, } from "./llm.js";
|
|
24
26
|
import { resolveRecord } from "./resolve.js";
|
|
@@ -59,13 +61,19 @@ Usage:
|
|
|
59
61
|
found (exists/ambiguous) — call before ANY record creation
|
|
60
62
|
fullstackgtm market init --category <name> start a market map: vendors + claim taxonomy as reviewable config
|
|
61
63
|
fullstackgtm market capture [--config <path>] [--run <label>]
|
|
62
|
-
fullstackgtm market
|
|
64
|
+
fullstackgtm market classify [--run <label>] [--vendor <id>] [--model m] [--out <path>]
|
|
65
|
+
fullstackgtm market worksheet --vendor <id> [--out <path>]
|
|
66
|
+
fullstackgtm market observe --from <observations.json> [--unverified]
|
|
63
67
|
fullstackgtm market fronts [--run <label>] [--diff <prior-run>] [--json]
|
|
68
|
+
fullstackgtm market axes [--run <label>] [--json]
|
|
64
69
|
fullstackgtm market report [--run <label>] [--format md|html] [--out <path>]
|
|
70
|
+
fullstackgtm market refresh [--run <label>] [--model m]
|
|
65
71
|
the live competitive map: capture vendor pages (content-addressed),
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
72
|
+
classify intensity per claim (LLM bring-your-own-key, or fill the
|
|
73
|
+
worksheet with any agent) — every quoted span is verified verbatim
|
|
74
|
+
against the stored capture it cites before it's accepted — then
|
|
75
|
+
compute deterministic front states and drift, render the field
|
|
76
|
+
report. refresh = capture → classify → drift → report in one step
|
|
69
77
|
fullstackgtm suggest --plan-id <id> | --plan <path> [source options] [--json] [--out <path>]
|
|
70
78
|
derive values for requires_human_* placeholders
|
|
71
79
|
from snapshot evidence, with confidence + reasons
|
|
@@ -617,9 +625,14 @@ async function requireLlmCredential(command = "parse") {
|
|
|
617
625
|
if (resolved)
|
|
618
626
|
return resolved;
|
|
619
627
|
// Scoring is inherently LLM work — there is no keyword fallback to suggest.
|
|
620
|
-
const fallbackHint = command === "parse"
|
|
628
|
+
const fallbackHint = command === "parse"
|
|
629
|
+
? ", or pass --deterministic for the free keyword baseline"
|
|
630
|
+
: command === "score"
|
|
631
|
+
? " (call score has no non-LLM mode)"
|
|
632
|
+
: ", or classify by hand: `market worksheet --vendor <id>` then `market observe --from`";
|
|
633
|
+
const work = command === "score" ? "scoring" : command === "parse" ? "extraction" : "classification";
|
|
621
634
|
if (!process.stdin.isTTY) {
|
|
622
|
-
throw new Error(`LLM ${
|
|
635
|
+
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}.`);
|
|
623
636
|
}
|
|
624
637
|
console.error("LLM parsing needs an API key (Anthropic or OpenAI) — yours, used directly with the provider.");
|
|
625
638
|
console.error(`Paste it once; it is validated and stored at ${credentialsPath()} (file mode 0600), like CRM logins.`);
|
|
@@ -729,9 +742,11 @@ function buildCallPlan(parsed, deal, proposed, current, extraNextSteps) {
|
|
|
729
742
|
/**
|
|
730
743
|
* The market map: claim taxonomy in a reviewable config file, page captures
|
|
731
744
|
* and append-only observations under the profile home, deterministic front
|
|
732
|
-
* states and reports computed from the store.
|
|
733
|
-
*
|
|
734
|
-
*
|
|
745
|
+
* states and reports computed from the store. Intensity readings enter as
|
|
746
|
+
* proposals through two channels — `classify` (LLM, bring-your-own-key, the
|
|
747
|
+
* call-intelligence pattern) and `worksheet`/`observe` (an agent or human
|
|
748
|
+
* fills the worksheet) — and BOTH pass the same mechanical gate: every quoted
|
|
749
|
+
* span is verified verbatim against the stored capture it cites.
|
|
735
750
|
*/
|
|
736
751
|
async function marketCommand(args) {
|
|
737
752
|
const [subcommand, ...rest] = args;
|
|
@@ -740,9 +755,26 @@ async function marketCommand(args) {
|
|
|
740
755
|
console.log(`Usage:
|
|
741
756
|
market init --category <name> [--out <path>] write a starter market.config.json
|
|
742
757
|
market capture [--config <path>] [--run <label>]
|
|
743
|
-
market
|
|
758
|
+
market classify [--run <label>] [--capture-run <label>] [--vendor <id>] [--model m] [--out <path>]
|
|
759
|
+
market worksheet --vendor <id> [--capture-run <label>] [--out <path>]
|
|
760
|
+
market observe --from <observations.json> [--unverified]
|
|
744
761
|
market fronts [--config <path>] [--run <label>] [--diff <prior-run>] [--json]
|
|
762
|
+
market axes [--config <path>] [--run <label>] [--json]
|
|
745
763
|
market report [--config <path>] [--run <label>] [--format md|html] [--out <path>]
|
|
764
|
+
market refresh [--run <label>] [--model m] capture → classify → fronts drift → HTML report
|
|
765
|
+
|
|
766
|
+
axes runs the axis-discovery math: PCA over the vendor × claim intensity
|
|
767
|
+
matrix (PC1 = the category's primary axis, PC2 = the max-differentiation
|
|
768
|
+
direction orthogonal to it), triangulation of configured axes against the
|
|
769
|
+
PCs, and an orthogonality screen (|r|>0.75 = one axis twice). Axes live in
|
|
770
|
+
the config as claim-scoring rubrics; the report's strategic map and axis
|
|
771
|
+
lab render from them.
|
|
772
|
+
|
|
773
|
+
classify uses your Anthropic/OpenAI key (like call parse) to read the stored
|
|
774
|
+
captures and propose intensity readings; worksheet is the no-key path (an
|
|
775
|
+
agent or human fills it, submits via observe). Either way, every quoted span
|
|
776
|
+
is verified character-for-character against the capture it cites before the
|
|
777
|
+
observation is accepted — quotes that aren't on the page bounce.
|
|
746
778
|
|
|
747
779
|
The taxonomy (vendors + claims) is config you review and version; captures
|
|
748
780
|
and observations live under ~/.fullstackgtm/market/<category> (profile-scoped,
|
|
@@ -785,10 +817,97 @@ recomputed deterministically on every invocation — never stored.`);
|
|
|
785
817
|
process.exitCode = 1;
|
|
786
818
|
return;
|
|
787
819
|
}
|
|
820
|
+
if (!rest.includes("--unverified")) {
|
|
821
|
+
const { textByHash } = loadCaptureTexts(config.category);
|
|
822
|
+
const failures = verifyEvidenceSpans(set.observations, textByHash);
|
|
823
|
+
if (failures.length > 0) {
|
|
824
|
+
console.error(`Rejected: ${failures.length} evidence span(s) failed verification against the stored captures`);
|
|
825
|
+
for (const failure of failures.slice(0, 20)) {
|
|
826
|
+
console.error(` - ${failure.vendorId} × ${failure.claimId}: ${failure.problem}`);
|
|
827
|
+
}
|
|
828
|
+
console.error("Quotes must be copied verbatim from the captured pages. (--unverified skips this gate when the captures genuinely live elsewhere.)");
|
|
829
|
+
process.exitCode = 1;
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
788
833
|
await store.append(set);
|
|
789
834
|
console.log(`Appended ${set.runLabel}: ${set.observations.length} observations (${set.extractor})`);
|
|
790
835
|
return;
|
|
791
836
|
}
|
|
837
|
+
if (subcommand === "worksheet") {
|
|
838
|
+
const vendorId = option(rest, "--vendor");
|
|
839
|
+
if (!vendorId)
|
|
840
|
+
throw new Error("market worksheet requires --vendor <id>");
|
|
841
|
+
const worksheet = buildWorksheet(config, vendorId, { captureRun: option(rest, "--capture-run") ?? undefined });
|
|
842
|
+
const outPath = option(rest, "--out");
|
|
843
|
+
const payload = `${JSON.stringify(worksheet, null, 2)}\n`;
|
|
844
|
+
if (outPath) {
|
|
845
|
+
writeFileSync(resolve(process.cwd(), outPath), payload);
|
|
846
|
+
console.log(`Wrote ${outPath} (${worksheet.pages.length} captured pages, ${worksheet.claims.length} claims)`);
|
|
847
|
+
}
|
|
848
|
+
else {
|
|
849
|
+
console.log(payload);
|
|
850
|
+
}
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
if (subcommand === "classify") {
|
|
854
|
+
const credential = await requireLlmCredential("market classify");
|
|
855
|
+
const vendorFilter = option(rest, "--vendor");
|
|
856
|
+
const outPath = option(rest, "--out");
|
|
857
|
+
if (vendorFilter && !outPath) {
|
|
858
|
+
throw new Error("market classify --vendor produces a partial set (coverage validation would reject it) — pass --out <path> to inspect/merge it by hand");
|
|
859
|
+
}
|
|
860
|
+
const result = await classifyMarket(config, {
|
|
861
|
+
llm: { ...credential, model: option(rest, "--model") ?? undefined },
|
|
862
|
+
runLabel: option(rest, "--run") ?? option(rest, "--capture-run") ?? "run-1",
|
|
863
|
+
captureRun: option(rest, "--capture-run") ?? undefined,
|
|
864
|
+
vendors: vendorFilter ? [vendorFilter] : undefined,
|
|
865
|
+
});
|
|
866
|
+
if (result.retriedVendorIds.length > 0) {
|
|
867
|
+
console.error(`Span verification bounced ${result.retriedVendorIds.join(", ")} once; retry passed.`);
|
|
868
|
+
}
|
|
869
|
+
if (outPath) {
|
|
870
|
+
writeFileSync(resolve(process.cwd(), outPath), `${JSON.stringify(result.set, null, 2)}\n`);
|
|
871
|
+
console.log(`Wrote ${outPath}: ${result.set.observations.length} verified observations (${result.set.extractor})`);
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
const problems = validateObservationSet(config, result.set);
|
|
875
|
+
if (problems.length > 0) {
|
|
876
|
+
throw new Error(`Classified set failed validation: ${problems.slice(0, 5).join("; ")}`);
|
|
877
|
+
}
|
|
878
|
+
await store.append(result.set);
|
|
879
|
+
console.log(`Appended ${result.set.runLabel}: ${result.set.observations.length} observations, every span verified (${result.set.extractor})`);
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
if (subcommand === "refresh") {
|
|
883
|
+
const credential = await requireLlmCredential("market classify");
|
|
884
|
+
const runLabel = option(rest, "--run") ?? `run-${new Date().toISOString().slice(0, 10)}`;
|
|
885
|
+
const prior = await store.latest();
|
|
886
|
+
console.log(`Capturing ${config.vendors.length} vendors as ${runLabel}…`);
|
|
887
|
+
const captured = await captureMarket(config, { runLabel });
|
|
888
|
+
const failed = captured.entries.filter((entry) => !entry.captureHash);
|
|
889
|
+
if (failed.length > 0)
|
|
890
|
+
console.log(`${failed.length} page(s) failed/empty — affected cells will verify against remaining pages or read unobservable.`);
|
|
891
|
+
console.log(`Classifying with ${credential.provider}…`);
|
|
892
|
+
const result = await classifyMarket(config, {
|
|
893
|
+
llm: { ...credential, model: option(rest, "--model") ?? undefined },
|
|
894
|
+
runLabel,
|
|
895
|
+
captureRun: runLabel,
|
|
896
|
+
});
|
|
897
|
+
await store.append(result.set);
|
|
898
|
+
const fronts = computeFrontStates(config, result.set);
|
|
899
|
+
if (prior) {
|
|
900
|
+
const drift = diffFrontStates(computeFrontStates(config, prior), fronts);
|
|
901
|
+
if (drift.length === 0)
|
|
902
|
+
console.log(`No front changes since ${prior.runLabel}.`);
|
|
903
|
+
for (const change of drift)
|
|
904
|
+
console.log(`CHANGED ${change.claimId}: ${change.before} → ${change.after}`);
|
|
905
|
+
}
|
|
906
|
+
const outPath = option(rest, "--out") ?? `${config.category}-${runLabel}.html`;
|
|
907
|
+
writeFileSync(resolve(process.cwd(), outPath), marketMapToHtml(config, result.set));
|
|
908
|
+
console.log(`Wrote ${outPath}`);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
792
911
|
const loadSet = async () => {
|
|
793
912
|
const runLabel = option(rest, "--run");
|
|
794
913
|
const set = runLabel ? await store.get(runLabel) : await store.latest();
|
|
@@ -838,7 +957,17 @@ recomputed deterministically on every invocation — never stored.`);
|
|
|
838
957
|
}
|
|
839
958
|
return;
|
|
840
959
|
}
|
|
841
|
-
|
|
960
|
+
if (subcommand === "axes") {
|
|
961
|
+
const set = await loadSet();
|
|
962
|
+
const report = assessAxes(config, set);
|
|
963
|
+
if (rest.includes("--json")) {
|
|
964
|
+
console.log(JSON.stringify(report, null, 2));
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
console.log(axesReportToText(report));
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
throw new Error(`Unknown market subcommand: ${subcommand} (try: init, capture, classify, worksheet, observe, fronts, axes, report, refresh)`);
|
|
842
971
|
}
|
|
843
972
|
/**
|
|
844
973
|
* The resolve gate: exit 0 = safe to create, exit 2 = match found (exists or
|
package/dist/index.d.ts
CHANGED
|
@@ -19,7 +19,9 @@ 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";
|
|
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 MarketAxis, type MarketClaim, type MarketConfig, type MarketObservation, type MarketVendor, type ObservationConfidence, type ObservationSet, type ObservationStore, type SpanVerificationFailure, } from "./market.ts";
|
|
23
|
+
export { assessAxes, axesReportToText, axisPosition, messageBreadth, pcaTop2, pearson, type AxesReport, type AxisAssessment, type AxisPairing, type PrincipalComponent, } from "./marketAxes.ts";
|
|
24
|
+
export { buildWorksheet, classifyMarket, type ClassifyMarketOptions, type ClassifyMarketResult, type MarketWorksheet, } from "./marketClassify.ts";
|
|
23
25
|
export { marketMapToHtml, marketMapToMarkdown } from "./marketReport.ts";
|
|
24
26
|
export { suggestValues, type SuggestionConfidence, type ValueSuggestion } from "./suggest.ts";
|
|
25
27
|
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,6 +19,8 @@ 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";
|
|
22
|
+
export { captureMarket, computeFrontStates, createFileObservationStore, diffFrontStates, extractReadableText, loadCaptureTexts, loadMarketConfig, marketHome, normalizeForMatch, observationId, parseMarketConfig, starterMarketConfig, validateObservationSet, verifyEvidenceSpans, } from "./market.js";
|
|
23
|
+
export { assessAxes, axesReportToText, axisPosition, messageBreadth, pcaTop2, pearson, } from "./marketAxes.js";
|
|
24
|
+
export { buildWorksheet, classifyMarket, } from "./marketClassify.js";
|
|
23
25
|
export { marketMapToHtml, marketMapToMarkdown } from "./marketReport.js";
|
|
24
26
|
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
CHANGED
|
@@ -41,6 +41,18 @@ export type MarketVendor = {
|
|
|
41
41
|
};
|
|
42
42
|
notes?: string;
|
|
43
43
|
};
|
|
44
|
+
export type MarketAxis = {
|
|
45
|
+
id: string;
|
|
46
|
+
label: string;
|
|
47
|
+
negativePole: string;
|
|
48
|
+
positivePole: string;
|
|
49
|
+
/** How a human scores a claim on this axis — the axis IS this rubric. */
|
|
50
|
+
rubric: string;
|
|
51
|
+
/** e.g. "validated", "proposal", "proposal (PC2-validated)". Reviewer-facing. */
|
|
52
|
+
status?: string;
|
|
53
|
+
/** claimId → score in [-1, 1]; null = the axis does not apply to this claim. */
|
|
54
|
+
claimScores: Record<string, number | null>;
|
|
55
|
+
};
|
|
44
56
|
export type MarketConfig = {
|
|
45
57
|
category: string;
|
|
46
58
|
anchorVendor?: string;
|
|
@@ -48,6 +60,10 @@ export type MarketConfig = {
|
|
|
48
60
|
claims: MarketClaim[];
|
|
49
61
|
/** The LOUD/QUIET/ABSENT/UNOBSERVABLE judging rule, stated for reviewers. */
|
|
50
62
|
surfaceRule?: string;
|
|
63
|
+
/** Strategic axes as claim-scoring rubrics — config, not code. */
|
|
64
|
+
axes?: MarketAxis[];
|
|
65
|
+
/** [xAxisId, yAxisId] for the report's strategic map. */
|
|
66
|
+
primaryAxes?: [string, string];
|
|
51
67
|
};
|
|
52
68
|
export type MarketObservation = {
|
|
53
69
|
/** stableHash(category, runLabel, vendorId, claimId) — deterministic. */
|
|
@@ -126,6 +142,25 @@ export declare function createFileObservationStore(category: string, directory?:
|
|
|
126
142
|
* Returns problems; an empty array means accept.
|
|
127
143
|
*/
|
|
128
144
|
export declare function validateObservationSet(config: MarketConfig, set: ObservationSet): string[];
|
|
145
|
+
export declare function loadCaptureTexts(category: string, directory?: string): {
|
|
146
|
+
entries: CaptureEntry[];
|
|
147
|
+
textByHash: Map<string, string>;
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Whitespace-only normalization for span matching, plus one extraction
|
|
151
|
+
* artifact: the HTML-to-text step can emit a line break before punctuation
|
|
152
|
+
* that follows an inline tag ("placements\n. Districts"), which no honest
|
|
153
|
+
* quoter would reproduce — so whitespace *before* punctuation is dropped
|
|
154
|
+
* too. Words, casing, and characters must still match the page exactly.
|
|
155
|
+
*/
|
|
156
|
+
export declare function normalizeForMatch(value: string): string;
|
|
157
|
+
export type SpanVerificationFailure = {
|
|
158
|
+
vendorId: string;
|
|
159
|
+
claimId: string;
|
|
160
|
+
quote: string;
|
|
161
|
+
problem: string;
|
|
162
|
+
};
|
|
163
|
+
export declare function verifyEvidenceSpans(observations: MarketObservation[], textByHash: Map<string, string>): SpanVerificationFailure[];
|
|
129
164
|
export type ClaimFront = {
|
|
130
165
|
claimId: string;
|
|
131
166
|
state: FrontState;
|
package/dist/market.js
CHANGED
|
@@ -49,6 +49,30 @@ export function parseMarketConfig(raw) {
|
|
|
49
49
|
if (config.anchorVendor && !config.vendors.some((v) => v.id === config.anchorVendor)) {
|
|
50
50
|
throw new Error(`market config: anchorVendor "${config.anchorVendor}" is not in vendors`);
|
|
51
51
|
}
|
|
52
|
+
if (config.axes) {
|
|
53
|
+
const claimIds = new Set(config.claims.map((claim) => claim.id));
|
|
54
|
+
const axisIds = new Set();
|
|
55
|
+
for (const axis of config.axes) {
|
|
56
|
+
if (!axis.id)
|
|
57
|
+
throw new Error("market config: axis missing id");
|
|
58
|
+
if (axisIds.has(axis.id))
|
|
59
|
+
throw new Error(`market config: duplicate axis id "${axis.id}"`);
|
|
60
|
+
axisIds.add(axis.id);
|
|
61
|
+
for (const claimId of Object.keys(axis.claimScores ?? {})) {
|
|
62
|
+
if (!claimIds.has(claimId)) {
|
|
63
|
+
throw new Error(`market config: axis "${axis.id}" scores unknown claim "${claimId}"`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (config.primaryAxes) {
|
|
68
|
+
if (config.primaryAxes.length !== 2 || config.primaryAxes.some((id) => !axisIds.has(id))) {
|
|
69
|
+
throw new Error(`market config: primaryAxes must name two configured axes (got ${JSON.stringify(config.primaryAxes)})`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else if (config.primaryAxes) {
|
|
74
|
+
throw new Error("market config: primaryAxes set but no axes configured");
|
|
75
|
+
}
|
|
52
76
|
return config;
|
|
53
77
|
}
|
|
54
78
|
export function loadMarketConfig(path) {
|
|
@@ -270,6 +294,82 @@ export function validateObservationSet(config, set) {
|
|
|
270
294
|
}
|
|
271
295
|
return problems;
|
|
272
296
|
}
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
// Evidence span verification — the deterministic gate that makes the
|
|
299
|
+
// verbatim-quote rule mechanical instead of a prompt instruction. Because the
|
|
300
|
+
// source documents are *stored* (unlike call transcripts, which pass through),
|
|
301
|
+
// every quoted span can be checked against the capture it cites before the
|
|
302
|
+
// observation is accepted. Comparison is whitespace-normalized only: case and
|
|
303
|
+
// wording must match the page exactly.
|
|
304
|
+
export function loadCaptureTexts(category, directory) {
|
|
305
|
+
const dir = directory ?? join(marketHome(category), "captures");
|
|
306
|
+
const manifestPath = join(dir, "manifest.json");
|
|
307
|
+
const entries = existsSync(manifestPath)
|
|
308
|
+
? JSON.parse(readFileSync(manifestPath, "utf8"))
|
|
309
|
+
: [];
|
|
310
|
+
const textByHash = new Map();
|
|
311
|
+
for (const entry of entries) {
|
|
312
|
+
if (entry.captureHash && !textByHash.has(entry.captureHash)) {
|
|
313
|
+
try {
|
|
314
|
+
textByHash.set(entry.captureHash, readFileSync(join(dir, `${entry.captureHash}.txt`), "utf8"));
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
// Missing capture file: verification of anything citing it will fail loudly.
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return { entries, textByHash };
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Whitespace-only normalization for span matching, plus one extraction
|
|
325
|
+
* artifact: the HTML-to-text step can emit a line break before punctuation
|
|
326
|
+
* that follows an inline tag ("placements\n. Districts"), which no honest
|
|
327
|
+
* quoter would reproduce — so whitespace *before* punctuation is dropped
|
|
328
|
+
* too. Words, casing, and characters must still match the page exactly.
|
|
329
|
+
*/
|
|
330
|
+
export function normalizeForMatch(value) {
|
|
331
|
+
return value
|
|
332
|
+
.replace(/\s+([.,;:!?])/g, "$1")
|
|
333
|
+
.replace(/\s+/g, " ")
|
|
334
|
+
.trim();
|
|
335
|
+
}
|
|
336
|
+
export function verifyEvidenceSpans(observations, textByHash) {
|
|
337
|
+
const failures = [];
|
|
338
|
+
for (const obs of observations) {
|
|
339
|
+
for (const evidence of obs.evidence) {
|
|
340
|
+
const quote = evidence.text ?? "";
|
|
341
|
+
const hash = String(evidence.metadata?.captureHash ?? "");
|
|
342
|
+
if (!hash) {
|
|
343
|
+
failures.push({
|
|
344
|
+
vendorId: obs.vendorId,
|
|
345
|
+
claimId: obs.claimId,
|
|
346
|
+
quote,
|
|
347
|
+
problem: "evidence has no captureHash — spans must cite a stored capture",
|
|
348
|
+
});
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
const captureText = textByHash.get(hash);
|
|
352
|
+
if (captureText === undefined) {
|
|
353
|
+
failures.push({
|
|
354
|
+
vendorId: obs.vendorId,
|
|
355
|
+
claimId: obs.claimId,
|
|
356
|
+
quote,
|
|
357
|
+
problem: `capture ${hash.slice(0, 12)} not found — evidence must stay resolvable`,
|
|
358
|
+
});
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (!normalizeForMatch(captureText).includes(normalizeForMatch(quote))) {
|
|
362
|
+
failures.push({
|
|
363
|
+
vendorId: obs.vendorId,
|
|
364
|
+
claimId: obs.claimId,
|
|
365
|
+
quote,
|
|
366
|
+
problem: `quote not found verbatim in capture ${hash.slice(0, 12)}`,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return failures;
|
|
372
|
+
}
|
|
273
373
|
/**
|
|
274
374
|
* Front rule v1: 0 loud → open (if anyone is quiet) or vacant; 1 loud →
|
|
275
375
|
* owned; 2–3 loud → contested; ≥4 loud → saturated. Unobservable cells are
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { MarketAxis, MarketConfig, MarketObservation, ObservationSet } from "./market.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Axis discovery for a market map — the method that earns a strategic 2x2
|
|
4
|
+
* instead of asserting one. Axes are claim-scoring rubrics in the config
|
|
5
|
+
* (reviewable, versioned); a vendor's position on an axis is the
|
|
6
|
+
* intensity-weighted mean of the scores of claims it voices. Two checks keep
|
|
7
|
+
* axes honest, both computed deterministically from the stored observations:
|
|
8
|
+
*
|
|
9
|
+
* 1. Triangulation — PCA over the vendor × claim intensity matrix gives the
|
|
10
|
+
* category's own top variance directions; a real axis correlates with a
|
|
11
|
+
* principal component (it is derivable from the data, not just felt).
|
|
12
|
+
* 2. Orthogonality — two configured axes that correlate ≥ ~0.75 at the
|
|
13
|
+
* vendor level are one axis twice. Sometimes that redundancy is the
|
|
14
|
+
* finding: the category couples the two ideas, and the empty quadrant is
|
|
15
|
+
* the strategic white space.
|
|
16
|
+
*
|
|
17
|
+
* Everything here is pure math over the store: same observations, same map.
|
|
18
|
+
*/
|
|
19
|
+
export declare const VOICE_WEIGHT: Record<string, number>;
|
|
20
|
+
/**
|
|
21
|
+
* Intensity-weighted mean of claim scores over claims the vendor voices.
|
|
22
|
+
* Claims scored null on the axis are excluded; returns null if the vendor
|
|
23
|
+
* voices nothing scoreable (e.g. fully unobservable).
|
|
24
|
+
*/
|
|
25
|
+
export declare function axisPosition(vendorId: string, claimScores: Record<string, number | null>, observations: MarketObservation[]): number | null;
|
|
26
|
+
/** Share of the claim space voiced (loud + half-weight quiet) over observable claims. */
|
|
27
|
+
export declare function messageBreadth(vendorId: string, observations: MarketObservation[]): {
|
|
28
|
+
breadth: number | null;
|
|
29
|
+
loudCount: number;
|
|
30
|
+
};
|
|
31
|
+
export declare function pearson(xs: number[], ys: number[]): number;
|
|
32
|
+
export type PrincipalComponent = {
|
|
33
|
+
/** claimId → loading. Sign is arbitrary; read poles from the extremes. */
|
|
34
|
+
loadings: Array<{
|
|
35
|
+
claimId: string;
|
|
36
|
+
loading: number;
|
|
37
|
+
}>;
|
|
38
|
+
/** vendorId → score on this component. */
|
|
39
|
+
scores: Array<{
|
|
40
|
+
vendorId: string;
|
|
41
|
+
score: number;
|
|
42
|
+
}>;
|
|
43
|
+
};
|
|
44
|
+
export declare function pcaTop2(config: MarketConfig, set: ObservationSet): {
|
|
45
|
+
vendors: string[];
|
|
46
|
+
pc1: PrincipalComponent;
|
|
47
|
+
pc2: PrincipalComponent;
|
|
48
|
+
};
|
|
49
|
+
export type AxisVendorPosition = {
|
|
50
|
+
vendorId: string;
|
|
51
|
+
position: number | null;
|
|
52
|
+
};
|
|
53
|
+
export type AxisAssessment = {
|
|
54
|
+
axis: MarketAxis;
|
|
55
|
+
positions: AxisVendorPosition[];
|
|
56
|
+
/** Standard deviation of placeable vendor positions — does the axis separate anyone? */
|
|
57
|
+
spread: number;
|
|
58
|
+
rVsPc1: number;
|
|
59
|
+
rVsPc2: number;
|
|
60
|
+
};
|
|
61
|
+
export type AxisPairing = {
|
|
62
|
+
aId: string;
|
|
63
|
+
bId: string;
|
|
64
|
+
r: number;
|
|
65
|
+
verdict: "near-orthogonal" | "correlated — weak pair" | "redundant — same axis twice";
|
|
66
|
+
};
|
|
67
|
+
export type AxesReport = {
|
|
68
|
+
vendors: string[];
|
|
69
|
+
pc1: PrincipalComponent;
|
|
70
|
+
pc2: PrincipalComponent;
|
|
71
|
+
assessments: AxisAssessment[];
|
|
72
|
+
/** Includes the derived breadth axis in pairings. */
|
|
73
|
+
pairings: AxisPairing[];
|
|
74
|
+
};
|
|
75
|
+
export declare function pairingVerdict(r: number): AxisPairing["verdict"];
|
|
76
|
+
export declare function assessAxes(config: MarketConfig, set: ObservationSet): AxesReport;
|
|
77
|
+
export declare function axesReportToText(report: AxesReport): string;
|