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/docs/api.md
CHANGED
|
@@ -58,7 +58,9 @@ release.
|
|
|
58
58
|
## CLI
|
|
59
59
|
|
|
60
60
|
Commands: `login` / `logout`, `snapshot`, `audit`, `report`, `diff`, `merge`, `plans`,
|
|
61
|
-
`apply`, `
|
|
61
|
+
`apply`, `suggest`, `call` (`parse` / `score` / `link` / `plan`), `resolve`,
|
|
62
|
+
`market` (`init` / `capture` / `classify` / `worksheet` / `observe` / `fronts` /
|
|
63
|
+
`axes` / `report` / `refresh`), `rules`, `profiles`, `doctor`.
|
|
62
64
|
Exit codes: `0` success · `1` error · `2` findings/regressions at the requested gate
|
|
63
65
|
(`--fail-on`, `--fail-on-new-findings`). `--json` everywhere; JSON output shapes are stable.
|
|
64
66
|
|
|
@@ -78,7 +80,32 @@ deliverable in markdown or self-contained HTML: severity counts, prose summary,
|
|
|
78
80
|
per-rule detail with capped examples, and next steps. `auditReportToMarkdown` /
|
|
79
81
|
`auditReportToHtml` expose the same rendering programmatically.
|
|
80
82
|
|
|
83
|
+
## Market map
|
|
84
|
+
|
|
85
|
+
Newer surface (0.16–0.18); shapes are settling toward the 1.0 contract. A live
|
|
86
|
+
model of the competitive category: claim taxonomy + vendor registry as a
|
|
87
|
+
reviewable `market.config.json` (`MarketConfig`, `MarketClaim`, `MarketVendor`,
|
|
88
|
+
`MarketAxis`), content-addressed page captures (`captureMarket`,
|
|
89
|
+
`loadCaptureTexts`), append-only observations (`ObservationSet`,
|
|
90
|
+
`MarketObservation`, `ObservationStore` / `createFileObservationStore` —
|
|
91
|
+
profile-scoped under `<home>/market/<category>`), and deterministic
|
|
92
|
+
derivations: `computeFrontStates` / `diffFrontStates` (front rule v1),
|
|
93
|
+
`assessAxes` / `pcaTop2` / `axisPosition` (axis discovery), and
|
|
94
|
+
`marketMapToMarkdown` / `marketMapToHtml` (the field report; renders the
|
|
95
|
+
primary strategic 2×2 when `axes` / `primaryAxes` are configured).
|
|
96
|
+
|
|
97
|
+
Intensity readings are proposals: `classifyMarket` (LLM, bring-your-own-key,
|
|
98
|
+
provenance-marked) or `buildWorksheet` + `market observe` (agent/human). Every
|
|
99
|
+
quoted evidence span is mechanically verified verbatim
|
|
100
|
+
(`verifyEvidenceSpans`; whitespace and punctuation-spacing normalized) against
|
|
101
|
+
the stored capture it cites before a set is accepted; failed captures read as
|
|
102
|
+
`unobservable`, never `absent`.
|
|
103
|
+
|
|
81
104
|
## MCP
|
|
82
105
|
|
|
83
106
|
Tools: `fullstackgtm_audit`, `fullstackgtm_rules`, `fullstackgtm_apply`
|
|
84
|
-
(requires explicit `approvedOperationIds`)
|
|
107
|
+
(requires explicit `approvedOperationIds`), `fullstackgtm_suggest`,
|
|
108
|
+
`fullstackgtm_call_parse`, `fullstackgtm_resolve`,
|
|
109
|
+
`fullstackgtm_market_worksheet`, `fullstackgtm_market_observe` (validates,
|
|
110
|
+
verifies quoted spans against the stored captures, appends, returns front
|
|
111
|
+
states). Input schemas are stable.
|
package/llms.txt
CHANGED
|
@@ -31,6 +31,22 @@ coaching scorecards; `call link` suggests the deal with confidence + reason;
|
|
|
31
31
|
`call plan` proposes governed next-step writes through the standard
|
|
32
32
|
approve/apply lifecycle.
|
|
33
33
|
|
|
34
|
+
## Key invariants (market map)
|
|
35
|
+
|
|
36
|
+
`fullstackgtm market` models the competitive category: vendors + claim
|
|
37
|
+
taxonomy in `market.config.json`; `capture` stores vendor pages
|
|
38
|
+
content-addressed; `classify` (BYO key, same ladder as calls) or
|
|
39
|
+
`worksheet` + `observe` (agent/human channel) propose LOUD/QUIET/ABSENT
|
|
40
|
+
intensity readings per vendor × claim. Every quoted evidence span is
|
|
41
|
+
mechanically verified verbatim against the stored capture it cites;
|
|
42
|
+
unverifiable quotes are rejected (`--unverified` only when captures live
|
|
43
|
+
elsewhere). Failed captures read UNOBSERVABLE, never ABSENT. `fronts --diff`
|
|
44
|
+
= deterministic front states + drift between runs; `axes` = PCA axis
|
|
45
|
+
discovery + orthogonality screen; `report` = self-contained HTML field
|
|
46
|
+
report; `refresh` = capture → classify → drift → report in one command.
|
|
47
|
+
Storage is profile-scoped under `<home>/market/<category>`. MCP:
|
|
48
|
+
`fullstackgtm_market_worksheet`, `fullstackgtm_market_observe`.
|
|
49
|
+
|
|
34
50
|
## Key invariants
|
|
35
51
|
|
|
36
52
|
- Reads are safe by default; nothing is written without explicit `--approve`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fullstackgtm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Open-source agentic GTM ops framework: canonical GTM data model, pluggable deterministic audits, reviewable dry-run patch plans, approval-gated write-back with conflict detection, and cross-system entity resolution. HubSpot, Salesforce, and Stripe connectors included.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Full Stack GTM",
|
package/src/cli.ts
CHANGED
|
@@ -44,11 +44,15 @@ import {
|
|
|
44
44
|
computeFrontStates,
|
|
45
45
|
createFileObservationStore,
|
|
46
46
|
diffFrontStates,
|
|
47
|
+
loadCaptureTexts,
|
|
47
48
|
loadMarketConfig,
|
|
48
49
|
starterMarketConfig,
|
|
49
50
|
validateObservationSet,
|
|
51
|
+
verifyEvidenceSpans,
|
|
50
52
|
type ObservationSet,
|
|
51
53
|
} from "./market.ts";
|
|
54
|
+
import { assessAxes, axesReportToText } from "./marketAxes.ts";
|
|
55
|
+
import { buildWorksheet, classifyMarket } from "./marketClassify.ts";
|
|
52
56
|
import { marketMapToHtml, marketMapToMarkdown } from "./marketReport.ts";
|
|
53
57
|
import {
|
|
54
58
|
DEFAULT_RUBRIC,
|
|
@@ -107,13 +111,19 @@ Usage:
|
|
|
107
111
|
found (exists/ambiguous) — call before ANY record creation
|
|
108
112
|
fullstackgtm market init --category <name> start a market map: vendors + claim taxonomy as reviewable config
|
|
109
113
|
fullstackgtm market capture [--config <path>] [--run <label>]
|
|
110
|
-
fullstackgtm market
|
|
114
|
+
fullstackgtm market classify [--run <label>] [--vendor <id>] [--model m] [--out <path>]
|
|
115
|
+
fullstackgtm market worksheet --vendor <id> [--out <path>]
|
|
116
|
+
fullstackgtm market observe --from <observations.json> [--unverified]
|
|
111
117
|
fullstackgtm market fronts [--run <label>] [--diff <prior-run>] [--json]
|
|
118
|
+
fullstackgtm market axes [--run <label>] [--json]
|
|
112
119
|
fullstackgtm market report [--run <label>] [--format md|html] [--out <path>]
|
|
120
|
+
fullstackgtm market refresh [--run <label>] [--model m]
|
|
113
121
|
the live competitive map: capture vendor pages (content-addressed),
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
122
|
+
classify intensity per claim (LLM bring-your-own-key, or fill the
|
|
123
|
+
worksheet with any agent) — every quoted span is verified verbatim
|
|
124
|
+
against the stored capture it cites before it's accepted — then
|
|
125
|
+
compute deterministic front states and drift, render the field
|
|
126
|
+
report. refresh = capture → classify → drift → report in one step
|
|
117
127
|
fullstackgtm suggest --plan-id <id> | --plan <path> [source options] [--json] [--out <path>]
|
|
118
128
|
derive values for requires_human_* placeholders
|
|
119
129
|
from snapshot evidence, with confidence + reasons
|
|
@@ -696,15 +706,22 @@ the free keyword baseline; score always needs a key (scoring is LLM work).`);
|
|
|
696
706
|
* TTY a missing key is captured once (validated, stored 0600 like provider
|
|
697
707
|
* logins). Non-interactive contexts get an actionable error instead.
|
|
698
708
|
*/
|
|
699
|
-
async function requireLlmCredential(
|
|
709
|
+
async function requireLlmCredential(
|
|
710
|
+
command: "parse" | "score" | "market classify" = "parse",
|
|
711
|
+
): Promise<{ provider: LlmProvider; apiKey: string }> {
|
|
700
712
|
const resolved = resolveLlmCredential();
|
|
701
713
|
if (resolved) return resolved;
|
|
702
714
|
// Scoring is inherently LLM work — there is no keyword fallback to suggest.
|
|
703
715
|
const fallbackHint =
|
|
704
|
-
command === "parse"
|
|
716
|
+
command === "parse"
|
|
717
|
+
? ", or pass --deterministic for the free keyword baseline"
|
|
718
|
+
: command === "score"
|
|
719
|
+
? " (call score has no non-LLM mode)"
|
|
720
|
+
: ", or classify by hand: `market worksheet --vendor <id>` then `market observe --from`";
|
|
721
|
+
const work = command === "score" ? "scoring" : command === "parse" ? "extraction" : "classification";
|
|
705
722
|
if (!process.stdin.isTTY) {
|
|
706
723
|
throw new Error(
|
|
707
|
-
`LLM ${
|
|
724
|
+
`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}.`,
|
|
708
725
|
);
|
|
709
726
|
}
|
|
710
727
|
console.error("LLM parsing needs an API key (Anthropic or OpenAI) — yours, used directly with the provider.");
|
|
@@ -824,9 +841,11 @@ function buildCallPlan(
|
|
|
824
841
|
/**
|
|
825
842
|
* The market map: claim taxonomy in a reviewable config file, page captures
|
|
826
843
|
* and append-only observations under the profile home, deterministic front
|
|
827
|
-
* states and reports computed from the store.
|
|
828
|
-
*
|
|
829
|
-
*
|
|
844
|
+
* states and reports computed from the store. Intensity readings enter as
|
|
845
|
+
* proposals through two channels — `classify` (LLM, bring-your-own-key, the
|
|
846
|
+
* call-intelligence pattern) and `worksheet`/`observe` (an agent or human
|
|
847
|
+
* fills the worksheet) — and BOTH pass the same mechanical gate: every quoted
|
|
848
|
+
* span is verified verbatim against the stored capture it cites.
|
|
830
849
|
*/
|
|
831
850
|
async function marketCommand(args: string[]) {
|
|
832
851
|
const [subcommand, ...rest] = args;
|
|
@@ -836,9 +855,26 @@ async function marketCommand(args: string[]) {
|
|
|
836
855
|
console.log(`Usage:
|
|
837
856
|
market init --category <name> [--out <path>] write a starter market.config.json
|
|
838
857
|
market capture [--config <path>] [--run <label>]
|
|
839
|
-
market
|
|
858
|
+
market classify [--run <label>] [--capture-run <label>] [--vendor <id>] [--model m] [--out <path>]
|
|
859
|
+
market worksheet --vendor <id> [--capture-run <label>] [--out <path>]
|
|
860
|
+
market observe --from <observations.json> [--unverified]
|
|
840
861
|
market fronts [--config <path>] [--run <label>] [--diff <prior-run>] [--json]
|
|
862
|
+
market axes [--config <path>] [--run <label>] [--json]
|
|
841
863
|
market report [--config <path>] [--run <label>] [--format md|html] [--out <path>]
|
|
864
|
+
market refresh [--run <label>] [--model m] capture → classify → fronts drift → HTML report
|
|
865
|
+
|
|
866
|
+
axes runs the axis-discovery math: PCA over the vendor × claim intensity
|
|
867
|
+
matrix (PC1 = the category's primary axis, PC2 = the max-differentiation
|
|
868
|
+
direction orthogonal to it), triangulation of configured axes against the
|
|
869
|
+
PCs, and an orthogonality screen (|r|>0.75 = one axis twice). Axes live in
|
|
870
|
+
the config as claim-scoring rubrics; the report's strategic map and axis
|
|
871
|
+
lab render from them.
|
|
872
|
+
|
|
873
|
+
classify uses your Anthropic/OpenAI key (like call parse) to read the stored
|
|
874
|
+
captures and propose intensity readings; worksheet is the no-key path (an
|
|
875
|
+
agent or human fills it, submits via observe). Either way, every quoted span
|
|
876
|
+
is verified character-for-character against the capture it cites before the
|
|
877
|
+
observation is accepted — quotes that aren't on the page bounce.
|
|
842
878
|
|
|
843
879
|
The taxonomy (vendors + claims) is config you review and version; captures
|
|
844
880
|
and observations live under ~/.fullstackgtm/market/<category> (profile-scoped,
|
|
@@ -883,11 +919,100 @@ recomputed deterministically on every invocation — never stored.`);
|
|
|
883
919
|
process.exitCode = 1;
|
|
884
920
|
return;
|
|
885
921
|
}
|
|
922
|
+
if (!rest.includes("--unverified")) {
|
|
923
|
+
const { textByHash } = loadCaptureTexts(config.category);
|
|
924
|
+
const failures = verifyEvidenceSpans(set.observations, textByHash);
|
|
925
|
+
if (failures.length > 0) {
|
|
926
|
+
console.error(`Rejected: ${failures.length} evidence span(s) failed verification against the stored captures`);
|
|
927
|
+
for (const failure of failures.slice(0, 20)) {
|
|
928
|
+
console.error(` - ${failure.vendorId} × ${failure.claimId}: ${failure.problem}`);
|
|
929
|
+
}
|
|
930
|
+
console.error("Quotes must be copied verbatim from the captured pages. (--unverified skips this gate when the captures genuinely live elsewhere.)");
|
|
931
|
+
process.exitCode = 1;
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
886
935
|
await store.append(set);
|
|
887
936
|
console.log(`Appended ${set.runLabel}: ${set.observations.length} observations (${set.extractor})`);
|
|
888
937
|
return;
|
|
889
938
|
}
|
|
890
939
|
|
|
940
|
+
if (subcommand === "worksheet") {
|
|
941
|
+
const vendorId = option(rest, "--vendor");
|
|
942
|
+
if (!vendorId) throw new Error("market worksheet requires --vendor <id>");
|
|
943
|
+
const worksheet = buildWorksheet(config, vendorId, { captureRun: option(rest, "--capture-run") ?? undefined });
|
|
944
|
+
const outPath = option(rest, "--out");
|
|
945
|
+
const payload = `${JSON.stringify(worksheet, null, 2)}\n`;
|
|
946
|
+
if (outPath) {
|
|
947
|
+
writeFileSync(resolve(process.cwd(), outPath), payload);
|
|
948
|
+
console.log(`Wrote ${outPath} (${worksheet.pages.length} captured pages, ${worksheet.claims.length} claims)`);
|
|
949
|
+
} else {
|
|
950
|
+
console.log(payload);
|
|
951
|
+
}
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (subcommand === "classify") {
|
|
956
|
+
const credential = await requireLlmCredential("market classify");
|
|
957
|
+
const vendorFilter = option(rest, "--vendor");
|
|
958
|
+
const outPath = option(rest, "--out");
|
|
959
|
+
if (vendorFilter && !outPath) {
|
|
960
|
+
throw new Error(
|
|
961
|
+
"market classify --vendor produces a partial set (coverage validation would reject it) — pass --out <path> to inspect/merge it by hand",
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
const result = await classifyMarket(config, {
|
|
965
|
+
llm: { ...credential, model: option(rest, "--model") ?? undefined },
|
|
966
|
+
runLabel: option(rest, "--run") ?? option(rest, "--capture-run") ?? "run-1",
|
|
967
|
+
captureRun: option(rest, "--capture-run") ?? undefined,
|
|
968
|
+
vendors: vendorFilter ? [vendorFilter] : undefined,
|
|
969
|
+
});
|
|
970
|
+
if (result.retriedVendorIds.length > 0) {
|
|
971
|
+
console.error(`Span verification bounced ${result.retriedVendorIds.join(", ")} once; retry passed.`);
|
|
972
|
+
}
|
|
973
|
+
if (outPath) {
|
|
974
|
+
writeFileSync(resolve(process.cwd(), outPath), `${JSON.stringify(result.set, null, 2)}\n`);
|
|
975
|
+
console.log(`Wrote ${outPath}: ${result.set.observations.length} verified observations (${result.set.extractor})`);
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
const problems = validateObservationSet(config, result.set);
|
|
979
|
+
if (problems.length > 0) {
|
|
980
|
+
throw new Error(`Classified set failed validation: ${problems.slice(0, 5).join("; ")}`);
|
|
981
|
+
}
|
|
982
|
+
await store.append(result.set);
|
|
983
|
+
console.log(
|
|
984
|
+
`Appended ${result.set.runLabel}: ${result.set.observations.length} observations, every span verified (${result.set.extractor})`,
|
|
985
|
+
);
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
if (subcommand === "refresh") {
|
|
990
|
+
const credential = await requireLlmCredential("market classify");
|
|
991
|
+
const runLabel = option(rest, "--run") ?? `run-${new Date().toISOString().slice(0, 10)}`;
|
|
992
|
+
const prior = await store.latest();
|
|
993
|
+
console.log(`Capturing ${config.vendors.length} vendors as ${runLabel}…`);
|
|
994
|
+
const captured = await captureMarket(config, { runLabel });
|
|
995
|
+
const failed = captured.entries.filter((entry) => !entry.captureHash);
|
|
996
|
+
if (failed.length > 0) console.log(`${failed.length} page(s) failed/empty — affected cells will verify against remaining pages or read unobservable.`);
|
|
997
|
+
console.log(`Classifying with ${credential.provider}…`);
|
|
998
|
+
const result = await classifyMarket(config, {
|
|
999
|
+
llm: { ...credential, model: option(rest, "--model") ?? undefined },
|
|
1000
|
+
runLabel,
|
|
1001
|
+
captureRun: runLabel,
|
|
1002
|
+
});
|
|
1003
|
+
await store.append(result.set);
|
|
1004
|
+
const fronts = computeFrontStates(config, result.set);
|
|
1005
|
+
if (prior) {
|
|
1006
|
+
const drift = diffFrontStates(computeFrontStates(config, prior), fronts);
|
|
1007
|
+
if (drift.length === 0) console.log(`No front changes since ${prior.runLabel}.`);
|
|
1008
|
+
for (const change of drift) console.log(`CHANGED ${change.claimId}: ${change.before} → ${change.after}`);
|
|
1009
|
+
}
|
|
1010
|
+
const outPath = option(rest, "--out") ?? `${config.category}-${runLabel}.html`;
|
|
1011
|
+
writeFileSync(resolve(process.cwd(), outPath), marketMapToHtml(config, result.set));
|
|
1012
|
+
console.log(`Wrote ${outPath}`);
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
891
1016
|
const loadSet = async (): Promise<ObservationSet> => {
|
|
892
1017
|
const runLabel = option(rest, "--run");
|
|
893
1018
|
const set = runLabel ? await store.get(runLabel) : await store.latest();
|
|
@@ -938,7 +1063,20 @@ recomputed deterministically on every invocation — never stored.`);
|
|
|
938
1063
|
return;
|
|
939
1064
|
}
|
|
940
1065
|
|
|
941
|
-
|
|
1066
|
+
if (subcommand === "axes") {
|
|
1067
|
+
const set = await loadSet();
|
|
1068
|
+
const report = assessAxes(config, set);
|
|
1069
|
+
if (rest.includes("--json")) {
|
|
1070
|
+
console.log(JSON.stringify(report, null, 2));
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
console.log(axesReportToText(report));
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
throw new Error(
|
|
1078
|
+
`Unknown market subcommand: ${subcommand} (try: init, capture, classify, worksheet, observe, fronts, axes, report, refresh)`,
|
|
1079
|
+
);
|
|
942
1080
|
}
|
|
943
1081
|
|
|
944
1082
|
/**
|
package/src/index.ts
CHANGED
|
@@ -136,18 +136,22 @@ export {
|
|
|
136
136
|
createFileObservationStore,
|
|
137
137
|
diffFrontStates,
|
|
138
138
|
extractReadableText,
|
|
139
|
+
loadCaptureTexts,
|
|
139
140
|
loadMarketConfig,
|
|
140
141
|
marketHome,
|
|
142
|
+
normalizeForMatch,
|
|
141
143
|
observationId,
|
|
142
144
|
parseMarketConfig,
|
|
143
145
|
starterMarketConfig,
|
|
144
146
|
validateObservationSet,
|
|
147
|
+
verifyEvidenceSpans,
|
|
145
148
|
type CaptureEntry,
|
|
146
149
|
type CaptureOptions,
|
|
147
150
|
type ClaimFront,
|
|
148
151
|
type ClaimIntensity,
|
|
149
152
|
type FrontDrift,
|
|
150
153
|
type FrontState,
|
|
154
|
+
type MarketAxis,
|
|
151
155
|
type MarketClaim,
|
|
152
156
|
type MarketConfig,
|
|
153
157
|
type MarketObservation,
|
|
@@ -155,7 +159,27 @@ export {
|
|
|
155
159
|
type ObservationConfidence,
|
|
156
160
|
type ObservationSet,
|
|
157
161
|
type ObservationStore,
|
|
162
|
+
type SpanVerificationFailure,
|
|
158
163
|
} from "./market.ts";
|
|
164
|
+
export {
|
|
165
|
+
assessAxes,
|
|
166
|
+
axesReportToText,
|
|
167
|
+
axisPosition,
|
|
168
|
+
messageBreadth,
|
|
169
|
+
pcaTop2,
|
|
170
|
+
pearson,
|
|
171
|
+
type AxesReport,
|
|
172
|
+
type AxisAssessment,
|
|
173
|
+
type AxisPairing,
|
|
174
|
+
type PrincipalComponent,
|
|
175
|
+
} from "./marketAxes.ts";
|
|
176
|
+
export {
|
|
177
|
+
buildWorksheet,
|
|
178
|
+
classifyMarket,
|
|
179
|
+
type ClassifyMarketOptions,
|
|
180
|
+
type ClassifyMarketResult,
|
|
181
|
+
type MarketWorksheet,
|
|
182
|
+
} from "./marketClassify.ts";
|
|
159
183
|
export { marketMapToHtml, marketMapToMarkdown } from "./marketReport.ts";
|
|
160
184
|
export { suggestValues, type SuggestionConfidence, type ValueSuggestion } from "./suggest.ts";
|
|
161
185
|
export type {
|
package/src/llm.ts
CHANGED
|
@@ -239,7 +239,13 @@ export function parseRubric(json: string): Rubric {
|
|
|
239
239
|
|
|
240
240
|
// ── Provider plumbing (raw fetch, forced tool calls) ───────────────────────
|
|
241
241
|
|
|
242
|
-
|
|
242
|
+
/**
|
|
243
|
+
* Shared constrained-tool-call plumbing: force the model to answer through a
|
|
244
|
+
* single tool whose input_schema is the output contract. Exported for other
|
|
245
|
+
* semi-deterministic features (market classification) — every LLM feature in
|
|
246
|
+
* the package goes through this one seam.
|
|
247
|
+
*/
|
|
248
|
+
export async function forcedToolCall(
|
|
243
249
|
prompt: string,
|
|
244
250
|
toolName: string,
|
|
245
251
|
schema: object,
|
package/src/market.ts
CHANGED
|
@@ -52,6 +52,19 @@ export type MarketVendor = {
|
|
|
52
52
|
notes?: string;
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
+
export type MarketAxis = {
|
|
56
|
+
id: string;
|
|
57
|
+
label: string;
|
|
58
|
+
negativePole: string;
|
|
59
|
+
positivePole: string;
|
|
60
|
+
/** How a human scores a claim on this axis — the axis IS this rubric. */
|
|
61
|
+
rubric: string;
|
|
62
|
+
/** e.g. "validated", "proposal", "proposal (PC2-validated)". Reviewer-facing. */
|
|
63
|
+
status?: string;
|
|
64
|
+
/** claimId → score in [-1, 1]; null = the axis does not apply to this claim. */
|
|
65
|
+
claimScores: Record<string, number | null>;
|
|
66
|
+
};
|
|
67
|
+
|
|
55
68
|
export type MarketConfig = {
|
|
56
69
|
category: string;
|
|
57
70
|
anchorVendor?: string;
|
|
@@ -59,6 +72,10 @@ export type MarketConfig = {
|
|
|
59
72
|
claims: MarketClaim[];
|
|
60
73
|
/** The LOUD/QUIET/ABSENT/UNOBSERVABLE judging rule, stated for reviewers. */
|
|
61
74
|
surfaceRule?: string;
|
|
75
|
+
/** Strategic axes as claim-scoring rubrics — config, not code. */
|
|
76
|
+
axes?: MarketAxis[];
|
|
77
|
+
/** [xAxisId, yAxisId] for the report's strategic map. */
|
|
78
|
+
primaryAxes?: [string, string];
|
|
62
79
|
};
|
|
63
80
|
|
|
64
81
|
export type MarketObservation = {
|
|
@@ -148,6 +165,27 @@ export function parseMarketConfig(raw: string): MarketConfig {
|
|
|
148
165
|
if (config.anchorVendor && !config.vendors.some((v) => v.id === config.anchorVendor)) {
|
|
149
166
|
throw new Error(`market config: anchorVendor "${config.anchorVendor}" is not in vendors`);
|
|
150
167
|
}
|
|
168
|
+
if (config.axes) {
|
|
169
|
+
const claimIds = new Set(config.claims.map((claim) => claim.id));
|
|
170
|
+
const axisIds = new Set<string>();
|
|
171
|
+
for (const axis of config.axes) {
|
|
172
|
+
if (!axis.id) throw new Error("market config: axis missing id");
|
|
173
|
+
if (axisIds.has(axis.id)) throw new Error(`market config: duplicate axis id "${axis.id}"`);
|
|
174
|
+
axisIds.add(axis.id);
|
|
175
|
+
for (const claimId of Object.keys(axis.claimScores ?? {})) {
|
|
176
|
+
if (!claimIds.has(claimId)) {
|
|
177
|
+
throw new Error(`market config: axis "${axis.id}" scores unknown claim "${claimId}"`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (config.primaryAxes) {
|
|
182
|
+
if (config.primaryAxes.length !== 2 || config.primaryAxes.some((id) => !axisIds.has(id))) {
|
|
183
|
+
throw new Error(`market config: primaryAxes must name two configured axes (got ${JSON.stringify(config.primaryAxes)})`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} else if (config.primaryAxes) {
|
|
187
|
+
throw new Error("market config: primaryAxes set but no axes configured");
|
|
188
|
+
}
|
|
151
189
|
return config;
|
|
152
190
|
}
|
|
153
191
|
|
|
@@ -408,6 +446,98 @@ export function validateObservationSet(config: MarketConfig, set: ObservationSet
|
|
|
408
446
|
return problems;
|
|
409
447
|
}
|
|
410
448
|
|
|
449
|
+
// ---------------------------------------------------------------------------
|
|
450
|
+
// Evidence span verification — the deterministic gate that makes the
|
|
451
|
+
// verbatim-quote rule mechanical instead of a prompt instruction. Because the
|
|
452
|
+
// source documents are *stored* (unlike call transcripts, which pass through),
|
|
453
|
+
// every quoted span can be checked against the capture it cites before the
|
|
454
|
+
// observation is accepted. Comparison is whitespace-normalized only: case and
|
|
455
|
+
// wording must match the page exactly.
|
|
456
|
+
|
|
457
|
+
export function loadCaptureTexts(
|
|
458
|
+
category: string,
|
|
459
|
+
directory?: string,
|
|
460
|
+
): { entries: CaptureEntry[]; textByHash: Map<string, string> } {
|
|
461
|
+
const dir = directory ?? join(marketHome(category), "captures");
|
|
462
|
+
const manifestPath = join(dir, "manifest.json");
|
|
463
|
+
const entries: CaptureEntry[] = existsSync(manifestPath)
|
|
464
|
+
? (JSON.parse(readFileSync(manifestPath, "utf8")) as CaptureEntry[])
|
|
465
|
+
: [];
|
|
466
|
+
const textByHash = new Map<string, string>();
|
|
467
|
+
for (const entry of entries) {
|
|
468
|
+
if (entry.captureHash && !textByHash.has(entry.captureHash)) {
|
|
469
|
+
try {
|
|
470
|
+
textByHash.set(entry.captureHash, readFileSync(join(dir, `${entry.captureHash}.txt`), "utf8"));
|
|
471
|
+
} catch {
|
|
472
|
+
// Missing capture file: verification of anything citing it will fail loudly.
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return { entries, textByHash };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Whitespace-only normalization for span matching, plus one extraction
|
|
481
|
+
* artifact: the HTML-to-text step can emit a line break before punctuation
|
|
482
|
+
* that follows an inline tag ("placements\n. Districts"), which no honest
|
|
483
|
+
* quoter would reproduce — so whitespace *before* punctuation is dropped
|
|
484
|
+
* too. Words, casing, and characters must still match the page exactly.
|
|
485
|
+
*/
|
|
486
|
+
export function normalizeForMatch(value: string): string {
|
|
487
|
+
return value
|
|
488
|
+
.replace(/\s+([.,;:!?])/g, "$1")
|
|
489
|
+
.replace(/\s+/g, " ")
|
|
490
|
+
.trim();
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export type SpanVerificationFailure = {
|
|
494
|
+
vendorId: string;
|
|
495
|
+
claimId: string;
|
|
496
|
+
quote: string;
|
|
497
|
+
problem: string;
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
export function verifyEvidenceSpans(
|
|
501
|
+
observations: MarketObservation[],
|
|
502
|
+
textByHash: Map<string, string>,
|
|
503
|
+
): SpanVerificationFailure[] {
|
|
504
|
+
const failures: SpanVerificationFailure[] = [];
|
|
505
|
+
for (const obs of observations) {
|
|
506
|
+
for (const evidence of obs.evidence) {
|
|
507
|
+
const quote = evidence.text ?? "";
|
|
508
|
+
const hash = String(evidence.metadata?.captureHash ?? "");
|
|
509
|
+
if (!hash) {
|
|
510
|
+
failures.push({
|
|
511
|
+
vendorId: obs.vendorId,
|
|
512
|
+
claimId: obs.claimId,
|
|
513
|
+
quote,
|
|
514
|
+
problem: "evidence has no captureHash — spans must cite a stored capture",
|
|
515
|
+
});
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
const captureText = textByHash.get(hash);
|
|
519
|
+
if (captureText === undefined) {
|
|
520
|
+
failures.push({
|
|
521
|
+
vendorId: obs.vendorId,
|
|
522
|
+
claimId: obs.claimId,
|
|
523
|
+
quote,
|
|
524
|
+
problem: `capture ${hash.slice(0, 12)} not found — evidence must stay resolvable`,
|
|
525
|
+
});
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
if (!normalizeForMatch(captureText).includes(normalizeForMatch(quote))) {
|
|
529
|
+
failures.push({
|
|
530
|
+
vendorId: obs.vendorId,
|
|
531
|
+
claimId: obs.claimId,
|
|
532
|
+
quote,
|
|
533
|
+
problem: `quote not found verbatim in capture ${hash.slice(0, 12)}`,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return failures;
|
|
539
|
+
}
|
|
540
|
+
|
|
411
541
|
// ---------------------------------------------------------------------------
|
|
412
542
|
// Front states — deterministic, recomputed every time, never stored.
|
|
413
543
|
|