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/src/cli.ts
CHANGED
|
@@ -39,6 +39,20 @@ import { auditReportToHtml, auditReportToMarkdown, type ReportOptions } from "./
|
|
|
39
39
|
import { builtinAuditRules } from "./rules.ts";
|
|
40
40
|
import { sampleSnapshot } from "./sampleData.ts";
|
|
41
41
|
import { normalizeTranscript, parseCall, suggestCallDeal, type ExtractedCallInsight, type ParsedCall } from "./calls.ts";
|
|
42
|
+
import {
|
|
43
|
+
captureMarket,
|
|
44
|
+
computeFrontStates,
|
|
45
|
+
createFileObservationStore,
|
|
46
|
+
diffFrontStates,
|
|
47
|
+
loadCaptureTexts,
|
|
48
|
+
loadMarketConfig,
|
|
49
|
+
starterMarketConfig,
|
|
50
|
+
validateObservationSet,
|
|
51
|
+
verifyEvidenceSpans,
|
|
52
|
+
type ObservationSet,
|
|
53
|
+
} from "./market.ts";
|
|
54
|
+
import { buildWorksheet, classifyMarket } from "./marketClassify.ts";
|
|
55
|
+
import { marketMapToHtml, marketMapToMarkdown } from "./marketReport.ts";
|
|
42
56
|
import {
|
|
43
57
|
DEFAULT_RUBRIC,
|
|
44
58
|
detectProviderFromKey,
|
|
@@ -94,6 +108,20 @@ Usage:
|
|
|
94
108
|
fullstackgtm resolve <account|contact|deal> [--name N] [--domain D] [--email E] [--account-id A] [source options] [--json]
|
|
95
109
|
the create gate: exit 0 = safe to create, exit 2 = match
|
|
96
110
|
found (exists/ambiguous) — call before ANY record creation
|
|
111
|
+
fullstackgtm market init --category <name> start a market map: vendors + claim taxonomy as reviewable config
|
|
112
|
+
fullstackgtm market capture [--config <path>] [--run <label>]
|
|
113
|
+
fullstackgtm market classify [--run <label>] [--vendor <id>] [--model m] [--out <path>]
|
|
114
|
+
fullstackgtm market worksheet --vendor <id> [--out <path>]
|
|
115
|
+
fullstackgtm market observe --from <observations.json> [--unverified]
|
|
116
|
+
fullstackgtm market fronts [--run <label>] [--diff <prior-run>] [--json]
|
|
117
|
+
fullstackgtm market report [--run <label>] [--format md|html] [--out <path>]
|
|
118
|
+
fullstackgtm market refresh [--run <label>] [--model m]
|
|
119
|
+
the live competitive map: capture vendor pages (content-addressed),
|
|
120
|
+
classify intensity per claim (LLM bring-your-own-key, or fill the
|
|
121
|
+
worksheet with any agent) — every quoted span is verified verbatim
|
|
122
|
+
against the stored capture it cites before it's accepted — then
|
|
123
|
+
compute deterministic front states and drift, render the field
|
|
124
|
+
report. refresh = capture → classify → drift → report in one step
|
|
97
125
|
fullstackgtm suggest --plan-id <id> | --plan <path> [source options] [--json] [--out <path>]
|
|
98
126
|
derive values for requires_human_* placeholders
|
|
99
127
|
from snapshot evidence, with confidence + reasons
|
|
@@ -676,15 +704,22 @@ the free keyword baseline; score always needs a key (scoring is LLM work).`);
|
|
|
676
704
|
* TTY a missing key is captured once (validated, stored 0600 like provider
|
|
677
705
|
* logins). Non-interactive contexts get an actionable error instead.
|
|
678
706
|
*/
|
|
679
|
-
async function requireLlmCredential(
|
|
707
|
+
async function requireLlmCredential(
|
|
708
|
+
command: "parse" | "score" | "market classify" = "parse",
|
|
709
|
+
): Promise<{ provider: LlmProvider; apiKey: string }> {
|
|
680
710
|
const resolved = resolveLlmCredential();
|
|
681
711
|
if (resolved) return resolved;
|
|
682
712
|
// Scoring is inherently LLM work — there is no keyword fallback to suggest.
|
|
683
713
|
const fallbackHint =
|
|
684
|
-
command === "parse"
|
|
714
|
+
command === "parse"
|
|
715
|
+
? ", or pass --deterministic for the free keyword baseline"
|
|
716
|
+
: command === "score"
|
|
717
|
+
? " (call score has no non-LLM mode)"
|
|
718
|
+
: ", or classify by hand: `market worksheet --vendor <id>` then `market observe --from`";
|
|
719
|
+
const work = command === "score" ? "scoring" : command === "parse" ? "extraction" : "classification";
|
|
685
720
|
if (!process.stdin.isTTY) {
|
|
686
721
|
throw new Error(
|
|
687
|
-
`LLM ${
|
|
722
|
+
`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}.`,
|
|
688
723
|
);
|
|
689
724
|
}
|
|
690
725
|
console.error("LLM parsing needs an API key (Anthropic or OpenAI) — yours, used directly with the provider.");
|
|
@@ -801,6 +836,228 @@ function buildCallPlan(
|
|
|
801
836
|
};
|
|
802
837
|
}
|
|
803
838
|
|
|
839
|
+
/**
|
|
840
|
+
* The market map: claim taxonomy in a reviewable config file, page captures
|
|
841
|
+
* and append-only observations under the profile home, deterministic front
|
|
842
|
+
* states and reports computed from the store. Intensity readings enter as
|
|
843
|
+
* proposals through two channels — `classify` (LLM, bring-your-own-key, the
|
|
844
|
+
* call-intelligence pattern) and `worksheet`/`observe` (an agent or human
|
|
845
|
+
* fills the worksheet) — and BOTH pass the same mechanical gate: every quoted
|
|
846
|
+
* span is verified verbatim against the stored capture it cites.
|
|
847
|
+
*/
|
|
848
|
+
async function marketCommand(args: string[]) {
|
|
849
|
+
const [subcommand, ...rest] = args;
|
|
850
|
+
const configPath = () => resolve(process.cwd(), option(rest, "--config") ?? "market.config.json");
|
|
851
|
+
|
|
852
|
+
if (!subcommand || subcommand === "--help") {
|
|
853
|
+
console.log(`Usage:
|
|
854
|
+
market init --category <name> [--out <path>] write a starter market.config.json
|
|
855
|
+
market capture [--config <path>] [--run <label>]
|
|
856
|
+
market classify [--run <label>] [--capture-run <label>] [--vendor <id>] [--model m] [--out <path>]
|
|
857
|
+
market worksheet --vendor <id> [--capture-run <label>] [--out <path>]
|
|
858
|
+
market observe --from <observations.json> [--unverified]
|
|
859
|
+
market fronts [--config <path>] [--run <label>] [--diff <prior-run>] [--json]
|
|
860
|
+
market report [--config <path>] [--run <label>] [--format md|html] [--out <path>]
|
|
861
|
+
market refresh [--run <label>] [--model m] capture → classify → fronts drift → HTML report
|
|
862
|
+
|
|
863
|
+
classify uses your Anthropic/OpenAI key (like call parse) to read the stored
|
|
864
|
+
captures and propose intensity readings; worksheet is the no-key path (an
|
|
865
|
+
agent or human fills it, submits via observe). Either way, every quoted span
|
|
866
|
+
is verified character-for-character against the capture it cites before the
|
|
867
|
+
observation is accepted — quotes that aren't on the page bounce.
|
|
868
|
+
|
|
869
|
+
The taxonomy (vendors + claims) is config you review and version; captures
|
|
870
|
+
and observations live under ~/.fullstackgtm/market/<category> (profile-scoped,
|
|
871
|
+
one client's category intel never bleeds into another's). Front states are
|
|
872
|
+
recomputed deterministically on every invocation — never stored.`);
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (subcommand === "init") {
|
|
877
|
+
const category = option(rest, "--category");
|
|
878
|
+
if (!category) throw new Error("market init requires --category <name>");
|
|
879
|
+
const outPath = resolve(process.cwd(), option(rest, "--out") ?? "market.config.json");
|
|
880
|
+
if (existsSync(outPath)) throw new Error(`${outPath} already exists — refusing to overwrite`);
|
|
881
|
+
writeFileSync(outPath, `${JSON.stringify(starterMarketConfig(category), null, 2)}\n`);
|
|
882
|
+
console.log(`Wrote ${outPath}. Fill in vendors and claims, then: fullstackgtm market capture`);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const config = loadMarketConfig(configPath());
|
|
887
|
+
const store = createFileObservationStore(config.category);
|
|
888
|
+
|
|
889
|
+
if (subcommand === "capture") {
|
|
890
|
+
const result = await captureMarket(config, { runLabel: option(rest, "--run") ?? "run-1" });
|
|
891
|
+
for (const entry of result.entries) {
|
|
892
|
+
const flag = entry.captureHash && entry.textChars > 500 ? "" : " <-- thin/empty";
|
|
893
|
+
console.log(
|
|
894
|
+
`${entry.vendorId.padEnd(16)} ${entry.kind.padEnd(8)} ${String(entry.httpStatus ?? "ERR").padEnd(4)} ${String(entry.textChars).padStart(7)} chars ${entry.url}${flag}`,
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
console.log(`\nmanifest: ${result.manifestPath}`);
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
if (subcommand === "observe") {
|
|
902
|
+
const fromPath = option(rest, "--from");
|
|
903
|
+
if (!fromPath) throw new Error("market observe requires --from <observations.json>");
|
|
904
|
+
const set = JSON.parse(readFileSync(resolve(process.cwd(), fromPath), "utf8")) as ObservationSet;
|
|
905
|
+
const problems = validateObservationSet(config, set);
|
|
906
|
+
if (problems.length > 0) {
|
|
907
|
+
console.error(`Rejected: ${problems.length} problem(s)`);
|
|
908
|
+
for (const problem of problems.slice(0, 20)) console.error(` - ${problem}`);
|
|
909
|
+
process.exitCode = 1;
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
if (!rest.includes("--unverified")) {
|
|
913
|
+
const { textByHash } = loadCaptureTexts(config.category);
|
|
914
|
+
const failures = verifyEvidenceSpans(set.observations, textByHash);
|
|
915
|
+
if (failures.length > 0) {
|
|
916
|
+
console.error(`Rejected: ${failures.length} evidence span(s) failed verification against the stored captures`);
|
|
917
|
+
for (const failure of failures.slice(0, 20)) {
|
|
918
|
+
console.error(` - ${failure.vendorId} × ${failure.claimId}: ${failure.problem}`);
|
|
919
|
+
}
|
|
920
|
+
console.error("Quotes must be copied verbatim from the captured pages. (--unverified skips this gate when the captures genuinely live elsewhere.)");
|
|
921
|
+
process.exitCode = 1;
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
await store.append(set);
|
|
926
|
+
console.log(`Appended ${set.runLabel}: ${set.observations.length} observations (${set.extractor})`);
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (subcommand === "worksheet") {
|
|
931
|
+
const vendorId = option(rest, "--vendor");
|
|
932
|
+
if (!vendorId) throw new Error("market worksheet requires --vendor <id>");
|
|
933
|
+
const worksheet = buildWorksheet(config, vendorId, { captureRun: option(rest, "--capture-run") ?? undefined });
|
|
934
|
+
const outPath = option(rest, "--out");
|
|
935
|
+
const payload = `${JSON.stringify(worksheet, null, 2)}\n`;
|
|
936
|
+
if (outPath) {
|
|
937
|
+
writeFileSync(resolve(process.cwd(), outPath), payload);
|
|
938
|
+
console.log(`Wrote ${outPath} (${worksheet.pages.length} captured pages, ${worksheet.claims.length} claims)`);
|
|
939
|
+
} else {
|
|
940
|
+
console.log(payload);
|
|
941
|
+
}
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
if (subcommand === "classify") {
|
|
946
|
+
const credential = await requireLlmCredential("market classify");
|
|
947
|
+
const vendorFilter = option(rest, "--vendor");
|
|
948
|
+
const outPath = option(rest, "--out");
|
|
949
|
+
if (vendorFilter && !outPath) {
|
|
950
|
+
throw new Error(
|
|
951
|
+
"market classify --vendor produces a partial set (coverage validation would reject it) — pass --out <path> to inspect/merge it by hand",
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
const result = await classifyMarket(config, {
|
|
955
|
+
llm: { ...credential, model: option(rest, "--model") ?? undefined },
|
|
956
|
+
runLabel: option(rest, "--run") ?? option(rest, "--capture-run") ?? "run-1",
|
|
957
|
+
captureRun: option(rest, "--capture-run") ?? undefined,
|
|
958
|
+
vendors: vendorFilter ? [vendorFilter] : undefined,
|
|
959
|
+
});
|
|
960
|
+
if (result.retriedVendorIds.length > 0) {
|
|
961
|
+
console.error(`Span verification bounced ${result.retriedVendorIds.join(", ")} once; retry passed.`);
|
|
962
|
+
}
|
|
963
|
+
if (outPath) {
|
|
964
|
+
writeFileSync(resolve(process.cwd(), outPath), `${JSON.stringify(result.set, null, 2)}\n`);
|
|
965
|
+
console.log(`Wrote ${outPath}: ${result.set.observations.length} verified observations (${result.set.extractor})`);
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
const problems = validateObservationSet(config, result.set);
|
|
969
|
+
if (problems.length > 0) {
|
|
970
|
+
throw new Error(`Classified set failed validation: ${problems.slice(0, 5).join("; ")}`);
|
|
971
|
+
}
|
|
972
|
+
await store.append(result.set);
|
|
973
|
+
console.log(
|
|
974
|
+
`Appended ${result.set.runLabel}: ${result.set.observations.length} observations, every span verified (${result.set.extractor})`,
|
|
975
|
+
);
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
if (subcommand === "refresh") {
|
|
980
|
+
const credential = await requireLlmCredential("market classify");
|
|
981
|
+
const runLabel = option(rest, "--run") ?? `run-${new Date().toISOString().slice(0, 10)}`;
|
|
982
|
+
const prior = await store.latest();
|
|
983
|
+
console.log(`Capturing ${config.vendors.length} vendors as ${runLabel}…`);
|
|
984
|
+
const captured = await captureMarket(config, { runLabel });
|
|
985
|
+
const failed = captured.entries.filter((entry) => !entry.captureHash);
|
|
986
|
+
if (failed.length > 0) console.log(`${failed.length} page(s) failed/empty — affected cells will verify against remaining pages or read unobservable.`);
|
|
987
|
+
console.log(`Classifying with ${credential.provider}…`);
|
|
988
|
+
const result = await classifyMarket(config, {
|
|
989
|
+
llm: { ...credential, model: option(rest, "--model") ?? undefined },
|
|
990
|
+
runLabel,
|
|
991
|
+
captureRun: runLabel,
|
|
992
|
+
});
|
|
993
|
+
await store.append(result.set);
|
|
994
|
+
const fronts = computeFrontStates(config, result.set);
|
|
995
|
+
if (prior) {
|
|
996
|
+
const drift = diffFrontStates(computeFrontStates(config, prior), fronts);
|
|
997
|
+
if (drift.length === 0) console.log(`No front changes since ${prior.runLabel}.`);
|
|
998
|
+
for (const change of drift) console.log(`CHANGED ${change.claimId}: ${change.before} → ${change.after}`);
|
|
999
|
+
}
|
|
1000
|
+
const outPath = option(rest, "--out") ?? `${config.category}-${runLabel}.html`;
|
|
1001
|
+
writeFileSync(resolve(process.cwd(), outPath), marketMapToHtml(config, result.set));
|
|
1002
|
+
console.log(`Wrote ${outPath}`);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
const loadSet = async (): Promise<ObservationSet> => {
|
|
1007
|
+
const runLabel = option(rest, "--run");
|
|
1008
|
+
const set = runLabel ? await store.get(runLabel) : await store.latest();
|
|
1009
|
+
if (!set) {
|
|
1010
|
+
throw new Error(
|
|
1011
|
+
runLabel
|
|
1012
|
+
? `No observation run "${runLabel}" for ${config.category}`
|
|
1013
|
+
: `No observations stored for ${config.category} — run market observe --from <file> first`,
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
return set;
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
if (subcommand === "fronts") {
|
|
1020
|
+
const set = await loadSet();
|
|
1021
|
+
const fronts = computeFrontStates(config, set);
|
|
1022
|
+
const priorLabel = option(rest, "--diff");
|
|
1023
|
+
const prior = priorLabel ? await store.get(priorLabel) : null;
|
|
1024
|
+
if (priorLabel && !prior) throw new Error(`No observation run "${priorLabel}" to diff against`);
|
|
1025
|
+
const drift = prior ? diffFrontStates(computeFrontStates(config, prior), fronts) : null;
|
|
1026
|
+
if (rest.includes("--json")) {
|
|
1027
|
+
console.log(JSON.stringify({ runLabel: set.runLabel, fronts, drift }, null, 2));
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
for (const front of fronts) {
|
|
1031
|
+
const owner = front.state === "owned" ? ` → ${front.loudVendorIds[0]}` : "";
|
|
1032
|
+
console.log(`${front.state.toUpperCase().padEnd(10)} ${front.claimId}${owner}`);
|
|
1033
|
+
}
|
|
1034
|
+
if (drift) {
|
|
1035
|
+
console.log("");
|
|
1036
|
+
if (drift.length === 0) console.log(`No front changes since ${priorLabel}.`);
|
|
1037
|
+
for (const change of drift) console.log(`CHANGED ${change.claimId}: ${change.before} → ${change.after}`);
|
|
1038
|
+
}
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
if (subcommand === "report") {
|
|
1043
|
+
const set = await loadSet();
|
|
1044
|
+
const format = option(rest, "--format") ?? "md";
|
|
1045
|
+
const output = format === "html" ? marketMapToHtml(config, set) : marketMapToMarkdown(config, set);
|
|
1046
|
+
const outPath = option(rest, "--out");
|
|
1047
|
+
if (outPath) {
|
|
1048
|
+
writeFileSync(resolve(process.cwd(), outPath), output);
|
|
1049
|
+
console.log(`Wrote ${outPath}`);
|
|
1050
|
+
} else {
|
|
1051
|
+
console.log(output);
|
|
1052
|
+
}
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
throw new Error(
|
|
1057
|
+
`Unknown market subcommand: ${subcommand} (try: init, capture, classify, worksheet, observe, fronts, report, refresh)`,
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
804
1061
|
/**
|
|
805
1062
|
* The resolve gate: exit 0 = safe to create, exit 2 = match found (exists or
|
|
806
1063
|
* ambiguous — do NOT blind-create), exit 1 = error. Built for sync jobs and
|
|
@@ -1665,6 +1922,10 @@ export async function runCli(argv: string[]) {
|
|
|
1665
1922
|
await resolveCommand(args);
|
|
1666
1923
|
return;
|
|
1667
1924
|
}
|
|
1925
|
+
if (command === "market") {
|
|
1926
|
+
await marketCommand(args);
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1668
1929
|
if (command === "profiles") {
|
|
1669
1930
|
profilesCommand(args);
|
|
1670
1931
|
return;
|
|
@@ -431,7 +431,7 @@ export function createHubspotConnector(options: HubspotConnectorOptions): Requir
|
|
|
431
431
|
created = await request(`/crm/v3/objects/companies`, {
|
|
432
432
|
method: "POST",
|
|
433
433
|
body: JSON.stringify({
|
|
434
|
-
properties: { name, hs_object_source_detail_2:
|
|
434
|
+
properties: { name, hs_object_source_detail_2: `fullstackgtm create: (${operation.id})` },
|
|
435
435
|
}),
|
|
436
436
|
});
|
|
437
437
|
} catch {
|
package/src/index.ts
CHANGED
|
@@ -130,6 +130,44 @@ export {
|
|
|
130
130
|
type ScoredDimension,
|
|
131
131
|
} from "./llm.ts";
|
|
132
132
|
export { resolveRecord, type ResolveCandidate, type ResolveMatch, type ResolveResult } from "./resolve.ts";
|
|
133
|
+
export {
|
|
134
|
+
captureMarket,
|
|
135
|
+
computeFrontStates,
|
|
136
|
+
createFileObservationStore,
|
|
137
|
+
diffFrontStates,
|
|
138
|
+
extractReadableText,
|
|
139
|
+
loadCaptureTexts,
|
|
140
|
+
loadMarketConfig,
|
|
141
|
+
marketHome,
|
|
142
|
+
normalizeForMatch,
|
|
143
|
+
observationId,
|
|
144
|
+
parseMarketConfig,
|
|
145
|
+
starterMarketConfig,
|
|
146
|
+
validateObservationSet,
|
|
147
|
+
verifyEvidenceSpans,
|
|
148
|
+
type CaptureEntry,
|
|
149
|
+
type CaptureOptions,
|
|
150
|
+
type ClaimFront,
|
|
151
|
+
type ClaimIntensity,
|
|
152
|
+
type FrontDrift,
|
|
153
|
+
type FrontState,
|
|
154
|
+
type MarketClaim,
|
|
155
|
+
type MarketConfig,
|
|
156
|
+
type MarketObservation,
|
|
157
|
+
type MarketVendor,
|
|
158
|
+
type ObservationConfidence,
|
|
159
|
+
type ObservationSet,
|
|
160
|
+
type ObservationStore,
|
|
161
|
+
type SpanVerificationFailure,
|
|
162
|
+
} from "./market.ts";
|
|
163
|
+
export {
|
|
164
|
+
buildWorksheet,
|
|
165
|
+
classifyMarket,
|
|
166
|
+
type ClassifyMarketOptions,
|
|
167
|
+
type ClassifyMarketResult,
|
|
168
|
+
type MarketWorksheet,
|
|
169
|
+
} from "./marketClassify.ts";
|
|
170
|
+
export { marketMapToHtml, marketMapToMarkdown } from "./marketReport.ts";
|
|
133
171
|
export { suggestValues, type SuggestionConfidence, type ValueSuggestion } from "./suggest.ts";
|
|
134
172
|
export type {
|
|
135
173
|
ApprovalStatus,
|
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,
|