geovouch 0.2.0 → 0.3.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/dist/index.d.ts +134 -86
- package/dist/index.js +47 -14
- package/package.json +38 -22
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ApiRefusal, RecordActionBody } from "geovouch-schema/api";
|
|
2
|
+
export type { ActionKind, ApiRefusal, EngineScore, Gap, GapsResponse, LiftResponse, ProjectRef, ProjectsResponse, Readiness, RecordActionBody, RecordActionResponse, ScoreboardResponse } from "geovouch-schema/api";
|
|
1
3
|
export type GeoVouchOptions = {
|
|
2
4
|
apiKey: string;
|
|
3
5
|
/** Override for self-hosted or preview deployments. */
|
|
@@ -11,7 +13,11 @@ export type GeoVouchOptions = {
|
|
|
11
13
|
export declare class GeoVouchError extends Error {
|
|
12
14
|
readonly status: number;
|
|
13
15
|
readonly body: unknown;
|
|
14
|
-
|
|
16
|
+
/** The machine-readable refusal, when the server sent one — switch on this, not on `message`. */
|
|
17
|
+
readonly refused?: ApiRefusal["refused"];
|
|
18
|
+
constructor(status: number, message: string, body: unknown,
|
|
19
|
+
/** The machine-readable refusal, when the server sent one — switch on this, not on `message`. */
|
|
20
|
+
refused?: ApiRefusal["refused"]);
|
|
15
21
|
}
|
|
16
22
|
export declare class GeoVouch {
|
|
17
23
|
#private;
|
|
@@ -24,20 +30,106 @@ export declare class GeoVouch {
|
|
|
24
30
|
name: string;
|
|
25
31
|
scanEnabled: boolean;
|
|
26
32
|
}[];
|
|
27
|
-
keyScope:
|
|
33
|
+
keyScope: "project" | "organization";
|
|
28
34
|
}>;
|
|
29
35
|
/** Where you stand, per engine. Never averaged across engines — they diverge by ~46x, so one
|
|
30
36
|
* blended score is the mean of four different instruments. */
|
|
31
|
-
scoreboard(): Promise<
|
|
37
|
+
scoreboard(): Promise<{
|
|
38
|
+
project: {
|
|
39
|
+
id: string;
|
|
40
|
+
slug: string;
|
|
41
|
+
name: string;
|
|
42
|
+
};
|
|
43
|
+
readiness: {
|
|
44
|
+
actionable: boolean;
|
|
45
|
+
reasons: string[];
|
|
46
|
+
};
|
|
47
|
+
engines: ({
|
|
48
|
+
engine: "gemini" | "chatgpt-search" | "perplexity" | "google-ai-overview";
|
|
49
|
+
status: "unmeasured";
|
|
50
|
+
observations: number;
|
|
51
|
+
} | {
|
|
52
|
+
engine: "gemini" | "chatgpt-search" | "perplexity" | "google-ai-overview";
|
|
53
|
+
status: "measured";
|
|
54
|
+
observations: number;
|
|
55
|
+
shareOfVoicePct: number | null;
|
|
56
|
+
presenceRatePct: number;
|
|
57
|
+
sourcedRatePct: number | null;
|
|
58
|
+
confidence95: {
|
|
59
|
+
shareOfVoicePct: {
|
|
60
|
+
low: number;
|
|
61
|
+
high: number;
|
|
62
|
+
} | null;
|
|
63
|
+
presenceRatePct: {
|
|
64
|
+
low: number;
|
|
65
|
+
high: number;
|
|
66
|
+
} | null;
|
|
67
|
+
};
|
|
68
|
+
lowConfidence: boolean;
|
|
69
|
+
rivalsNamed: number;
|
|
70
|
+
topRivals: {
|
|
71
|
+
label: string;
|
|
72
|
+
count: number;
|
|
73
|
+
}[];
|
|
74
|
+
})[];
|
|
75
|
+
}>;
|
|
32
76
|
/** The buyer questions you are losing, split by whether the evidence can carry a decision.
|
|
33
77
|
* `actionable` cleared precision, the 50% boundary and an anytime-valid familywise threshold;
|
|
34
78
|
* `researchOnly` did not, and acting on it is acting on a draw. */
|
|
35
|
-
gaps(): Promise<
|
|
79
|
+
gaps(): Promise<{
|
|
80
|
+
project: {
|
|
81
|
+
id: string;
|
|
82
|
+
slug: string;
|
|
83
|
+
name: string;
|
|
84
|
+
};
|
|
85
|
+
readiness: {
|
|
86
|
+
actionable: boolean;
|
|
87
|
+
reasons: string[];
|
|
88
|
+
};
|
|
89
|
+
actionable: {
|
|
90
|
+
[x: string]: unknown;
|
|
91
|
+
question: string;
|
|
92
|
+
actionable: boolean;
|
|
93
|
+
missedEngines: string[];
|
|
94
|
+
wonEngines: string[];
|
|
95
|
+
competitors: {
|
|
96
|
+
label: string;
|
|
97
|
+
count: number;
|
|
98
|
+
}[];
|
|
99
|
+
coverage: number;
|
|
100
|
+
observations: number;
|
|
101
|
+
provenMissedEngines: string[];
|
|
102
|
+
researchReasons: string[];
|
|
103
|
+
}[];
|
|
104
|
+
researchOnly: {
|
|
105
|
+
[x: string]: unknown;
|
|
106
|
+
question: string;
|
|
107
|
+
actionable: boolean;
|
|
108
|
+
missedEngines: string[];
|
|
109
|
+
wonEngines: string[];
|
|
110
|
+
competitors: {
|
|
111
|
+
label: string;
|
|
112
|
+
count: number;
|
|
113
|
+
}[];
|
|
114
|
+
coverage: number;
|
|
115
|
+
observations: number;
|
|
116
|
+
provenMissedEngines: string[];
|
|
117
|
+
researchReasons: string[];
|
|
118
|
+
}[];
|
|
119
|
+
}>;
|
|
36
120
|
/** What to write for one actionable question. */
|
|
37
|
-
brief(question: string): Promise<
|
|
121
|
+
brief(question: string): Promise<{
|
|
122
|
+
[x: string]: unknown;
|
|
123
|
+
project: {
|
|
124
|
+
id: string;
|
|
125
|
+
slug: string;
|
|
126
|
+
name: string;
|
|
127
|
+
};
|
|
128
|
+
}>;
|
|
38
129
|
/** Ranked next-best-actions. An EMPTY list means measured-and-nothing-to-do; it never means the
|
|
39
130
|
* gate was shut — that arrives as a `collect_more_data` action naming the blocker. */
|
|
40
131
|
recommend(): Promise<{
|
|
132
|
+
[x: string]: unknown;
|
|
41
133
|
actions: unknown[];
|
|
42
134
|
}>;
|
|
43
135
|
/** TELL US WHAT YOU DID. This is the verb that closes the loop: we never publish on your behalf, so
|
|
@@ -45,91 +137,47 @@ export declare class GeoVouch {
|
|
|
45
137
|
*
|
|
46
138
|
* `publishedAt` is when the work went LIVE, not when you are reporting it — it is the before/after
|
|
47
139
|
* pivot, and reporting Friday about a Tuesday page would count three days of "after" as before. */
|
|
48
|
-
recordAction(input: {
|
|
49
|
-
question: string;
|
|
50
|
-
kind: ActionKind;
|
|
51
|
-
url?: string;
|
|
140
|
+
recordAction(input: Omit<RecordActionBody, "publishedAt"> & {
|
|
52
141
|
publishedAt: Date | string;
|
|
53
|
-
note?: string;
|
|
54
142
|
}): Promise<{
|
|
55
|
-
|
|
143
|
+
project: {
|
|
144
|
+
id: string;
|
|
145
|
+
slug: string;
|
|
146
|
+
name: string;
|
|
147
|
+
};
|
|
148
|
+
action: {
|
|
149
|
+
[x: string]: unknown;
|
|
150
|
+
id: string;
|
|
151
|
+
question: string;
|
|
152
|
+
kind: "answer_page" | "page_strengthened" | "structured_data" | "docs_page" | "community_answer" | "third_party_listicle" | "backlink";
|
|
153
|
+
url: string | null;
|
|
154
|
+
publishedAt: Date;
|
|
155
|
+
};
|
|
56
156
|
next: string;
|
|
57
157
|
}>;
|
|
58
158
|
/** Did it work? Control-adjusted, per engine, against a holdout arm nothing ever targets.
|
|
59
159
|
* `unmeasured` means a window is missing or there is no control — it is not a zero. */
|
|
60
|
-
lift(): Promise<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
} | null;
|
|
81
|
-
};
|
|
82
|
-
lowConfidence?: boolean;
|
|
83
|
-
rivalsNamed?: number;
|
|
84
|
-
topRivals?: {
|
|
85
|
-
label: string;
|
|
86
|
-
count: number;
|
|
87
|
-
}[];
|
|
88
|
-
};
|
|
89
|
-
export type Readiness = {
|
|
90
|
-
actionable: boolean;
|
|
91
|
-
reasons: string[];
|
|
92
|
-
};
|
|
93
|
-
export type ProjectRef = {
|
|
94
|
-
id: string;
|
|
95
|
-
slug: string;
|
|
96
|
-
name: string;
|
|
97
|
-
};
|
|
98
|
-
export type ScoreboardResponse = {
|
|
99
|
-
project: ProjectRef;
|
|
100
|
-
readiness: Readiness;
|
|
101
|
-
engines: EngineScore[];
|
|
102
|
-
};
|
|
103
|
-
export type GapsResponse = {
|
|
104
|
-
project: ProjectRef;
|
|
105
|
-
readiness: Readiness;
|
|
106
|
-
actionable: Gap[];
|
|
107
|
-
researchOnly: Gap[];
|
|
108
|
-
};
|
|
109
|
-
export type Gap = {
|
|
110
|
-
question: string;
|
|
111
|
-
actionable: boolean;
|
|
112
|
-
citedRate?: number | null;
|
|
113
|
-
researchReasons?: string[];
|
|
114
|
-
competitors?: string[];
|
|
115
|
-
};
|
|
116
|
-
export type LiftResponse = {
|
|
117
|
-
project: ProjectRef;
|
|
118
|
-
holdoutArm: string[];
|
|
119
|
-
assets: {
|
|
120
|
-
slug: string;
|
|
121
|
-
question: string;
|
|
122
|
-
publishedAt: string;
|
|
123
|
-
/** Non-null means the after-window is not full yet. That resolves with TIME, not more content —
|
|
124
|
-
* the distinction between "no effect" and "too early to say". */
|
|
125
|
-
daysUntilComparable: number | null;
|
|
126
|
-
engines: {
|
|
127
|
-
engine: string;
|
|
128
|
-
status: string;
|
|
129
|
-
proven: boolean;
|
|
130
|
-
netCiteLift: number | null;
|
|
131
|
-
verdict: string;
|
|
160
|
+
lift(): Promise<{
|
|
161
|
+
project: {
|
|
162
|
+
id: string;
|
|
163
|
+
slug: string;
|
|
164
|
+
name: string;
|
|
165
|
+
};
|
|
166
|
+
holdoutArm: string[];
|
|
167
|
+
assets: {
|
|
168
|
+
slug: string;
|
|
169
|
+
question: string;
|
|
170
|
+
publishedAt: string;
|
|
171
|
+
daysUntilComparable: number | null;
|
|
172
|
+
engines: {
|
|
173
|
+
[x: string]: unknown;
|
|
174
|
+
engine: string;
|
|
175
|
+
status: string;
|
|
176
|
+
proven: boolean;
|
|
177
|
+
netCiteLift: number | null;
|
|
178
|
+
verdict: string;
|
|
179
|
+
}[];
|
|
132
180
|
}[];
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
181
|
+
guidance: string;
|
|
182
|
+
}>;
|
|
183
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,32 @@
|
|
|
1
|
+
// geovouch — the client. You bring an API key; we bring the measurement.
|
|
2
|
+
//
|
|
3
|
+
// THIS PACKAGE DELIBERATELY EXPOSES NO TECHNIQUE. No database, no migrations, no vendor token, no
|
|
4
|
+
// engine list to configure, no statistical parameter to tune. Those are ours, and keeping them ours
|
|
5
|
+
// is what lets the numbers get better without you redeploying — the previous shape of this SDK was a
|
|
6
|
+
// bag of pure functions, which meant every consumer assembled their own instrument and every one of
|
|
7
|
+
// them drifted from ours.
|
|
8
|
+
//
|
|
9
|
+
// What you get instead is the four verbs of a closed loop: where you stand, what to do, a receipt for
|
|
10
|
+
// what you did, and whether it worked.
|
|
11
|
+
import { ActionKind, ApiRefusal, GapsResponse, LiftResponse, ProjectsResponse, RecordActionBody, RecordActionResponse, ScoreboardResponse, } from "geovouch-schema/api";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
// Two payloads whose full shape belongs to the recommender, not the wire: loose records, so a richer
|
|
14
|
+
// server never has fields STRIPPED by the client. Parse may reject; it may never rewrite.
|
|
15
|
+
const Brief = z.object({ project: z.object({ id: z.string(), slug: z.string(), name: z.string() }) }).loose();
|
|
16
|
+
const Recommend = z.object({ actions: z.array(z.unknown()) }).loose();
|
|
1
17
|
/** Every failure arrives as this, with the server's own sentence. A thrown string would make callers
|
|
2
18
|
* parse prose to find out whether to retry. */
|
|
3
19
|
export class GeoVouchError extends Error {
|
|
4
20
|
status;
|
|
5
21
|
body;
|
|
6
|
-
|
|
22
|
+
refused;
|
|
23
|
+
constructor(status, message, body,
|
|
24
|
+
/** The machine-readable refusal, when the server sent one — switch on this, not on `message`. */
|
|
25
|
+
refused) {
|
|
7
26
|
super(message);
|
|
8
27
|
this.status = status;
|
|
9
28
|
this.body = body;
|
|
29
|
+
this.refused = refused;
|
|
10
30
|
this.name = "GeoVouchError";
|
|
11
31
|
}
|
|
12
32
|
}
|
|
@@ -24,7 +44,10 @@ export class GeoVouch {
|
|
|
24
44
|
this.#project = options.project;
|
|
25
45
|
this.#fetch = options.fetchImpl ?? fetch;
|
|
26
46
|
}
|
|
27
|
-
|
|
47
|
+
/** Every response is PARSED through the shared wire schema, never cast. The server ran the same
|
|
48
|
+
* object before answering, so the two ends are incapable of disagreeing about a field — and a drift
|
|
49
|
+
* surfaces here as a thrown `GeoVouchError`, not as `undefined` in a customer's dashboard. */
|
|
50
|
+
async #call(path, schema, init) {
|
|
28
51
|
const url = new URL(`${this.#base}/api/v1/${path}`);
|
|
29
52
|
if (this.#project)
|
|
30
53
|
url.searchParams.set("project", this.#project);
|
|
@@ -46,34 +69,41 @@ export class GeoVouch {
|
|
|
46
69
|
throw new GeoVouchError(res.status, `geovouch: ${res.status} returned a non-JSON body`, text);
|
|
47
70
|
}
|
|
48
71
|
if (!res.ok) {
|
|
49
|
-
const
|
|
50
|
-
|
|
72
|
+
const refusal = ApiRefusal.safeParse(parsed);
|
|
73
|
+
const message = refusal.success ? refusal.data.error : `geovouch: request failed (${res.status})`;
|
|
74
|
+
throw new GeoVouchError(res.status, message, parsed, refusal.success ? refusal.data.refused : undefined);
|
|
51
75
|
}
|
|
52
|
-
|
|
76
|
+
const checked = schema.safeParse(parsed);
|
|
77
|
+
if (!checked.success) {
|
|
78
|
+
// The server answered 200 with a shape this client does not recognise. That is a version skew
|
|
79
|
+
// between the two ends, and saying so beats returning a partially-undefined object.
|
|
80
|
+
throw new GeoVouchError(res.status, `geovouch: ${path} answered a shape this client does not understand — upgrade the \`geovouch\` package`, parsed);
|
|
81
|
+
}
|
|
82
|
+
return checked.data;
|
|
53
83
|
}
|
|
54
84
|
/** Which projects this key can read. */
|
|
55
85
|
projects() {
|
|
56
|
-
return this.#call("projects");
|
|
86
|
+
return this.#call("projects", ProjectsResponse);
|
|
57
87
|
}
|
|
58
88
|
/** Where you stand, per engine. Never averaged across engines — they diverge by ~46x, so one
|
|
59
89
|
* blended score is the mean of four different instruments. */
|
|
60
90
|
scoreboard() {
|
|
61
|
-
return this.#call("scoreboard");
|
|
91
|
+
return this.#call("scoreboard", ScoreboardResponse);
|
|
62
92
|
}
|
|
63
93
|
/** The buyer questions you are losing, split by whether the evidence can carry a decision.
|
|
64
94
|
* `actionable` cleared precision, the 50% boundary and an anytime-valid familywise threshold;
|
|
65
95
|
* `researchOnly` did not, and acting on it is acting on a draw. */
|
|
66
96
|
gaps() {
|
|
67
|
-
return this.#call("gaps");
|
|
97
|
+
return this.#call("gaps", GapsResponse);
|
|
68
98
|
}
|
|
69
99
|
/** What to write for one actionable question. */
|
|
70
100
|
brief(question) {
|
|
71
|
-
return this.#call("brief", { query: { question } });
|
|
101
|
+
return this.#call("brief", Brief, { query: { question } });
|
|
72
102
|
}
|
|
73
103
|
/** Ranked next-best-actions. An EMPTY list means measured-and-nothing-to-do; it never means the
|
|
74
104
|
* gate was shut — that arrives as a `collect_more_data` action naming the blocker. */
|
|
75
105
|
recommend() {
|
|
76
|
-
return this.#call("recommend");
|
|
106
|
+
return this.#call("recommend", Recommend);
|
|
77
107
|
}
|
|
78
108
|
/** TELL US WHAT YOU DID. This is the verb that closes the loop: we never publish on your behalf, so
|
|
79
109
|
* without a receipt your work is invisible to the step that measures whether it landed.
|
|
@@ -81,14 +111,17 @@ export class GeoVouch {
|
|
|
81
111
|
* `publishedAt` is when the work went LIVE, not when you are reporting it — it is the before/after
|
|
82
112
|
* pivot, and reporting Friday about a Tuesday page would count three days of "after" as before. */
|
|
83
113
|
recordAction(input) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
114
|
+
// Validated CLIENT-SIDE too, with the same object the server runs: a bad receipt fails here with a
|
|
115
|
+
// field-keyed error instead of a round trip, and the two validators cannot disagree.
|
|
116
|
+
const body = RecordActionBody.parse({
|
|
117
|
+
...input,
|
|
118
|
+
publishedAt: input.publishedAt instanceof Date ? input.publishedAt.toISOString() : input.publishedAt,
|
|
87
119
|
});
|
|
120
|
+
return this.#call("actions", RecordActionResponse, { method: "POST", body });
|
|
88
121
|
}
|
|
89
122
|
/** Did it work? Control-adjusted, per engine, against a holdout arm nothing ever targets.
|
|
90
123
|
* `unmeasured` means a window is missing or there is no control — it is not a zero. */
|
|
91
124
|
lift() {
|
|
92
|
-
return this.#call("lift");
|
|
125
|
+
return this.#call("lift", LiftResponse);
|
|
93
126
|
}
|
|
94
127
|
}
|
package/package.json
CHANGED
|
@@ -1,28 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "geovouch",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "GeoVouch client
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "GeoVouch client \u2014 measure whether AI assistants name your brand, know when the evidence can carry a decision, and prove whether what you shipped worked. Bring an API key; the storage, the engines and the statistics are ours.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
8
|
-
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
8
|
+
"node": ">=20"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/theinfluencecompany/geovouch.git",
|
|
18
|
+
"directory": "libs/client"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"geo",
|
|
22
|
+
"aeo",
|
|
23
|
+
"generative-engine-optimization",
|
|
24
|
+
"ai-search",
|
|
25
|
+
"share-of-voice",
|
|
26
|
+
"llm",
|
|
27
|
+
"brand-visibility"
|
|
28
|
+
],
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"default": "./dist/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
37
|
+
"build": "tsc -p tsconfig.build.json",
|
|
38
|
+
"test": "node --test --experimental-transform-types test/*.test.ts"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"geovouch-schema": "0.1.0",
|
|
42
|
+
"zod": "^4.5.4"
|
|
43
|
+
}
|
|
28
44
|
}
|