twin3-sdk 0.2.20-pre-alpha → 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/README.md +4 -4
- package/index.d.ts +79 -0
- package/js/ai-visibility.js +616 -0
- package/js/discovery-evidence.js +2 -1
- package/js/discovery.d.ts +21 -0
- package/js/discovery.js +10 -0
- package/js/discovery.mjs +10 -0
- package/js/index.js +3 -0
- package/js/index.mjs +10 -0
- package/js/internal/discovery.js +54 -18
- package/js/runtime-session.js +75 -3
- package/package.json +4 -2
- package/runtime.d.ts +14 -0
- package/twin3/discovery/ai-visibility-catalog.json +68 -0
package/README.md
CHANGED
|
@@ -116,7 +116,7 @@ pip install -e /path/to/twin3-sdk
|
|
|
116
116
|
* Any protocol or algorithm improvement in `twin3-sdk` is reflected in both applications in **0 milliseconds**.
|
|
117
117
|
|
|
118
118
|
#### 2. Production Distribution
|
|
119
|
-
`v0.
|
|
119
|
+
`v0.3.0` is the current production release: `0.3.0` for npm and `0.3.0` for PyPI, published on the npm dist-tag `latest` as an immutable GitHub release for Google Cloud Marketplace GM-13. It is the first production, non-prerelease release and has no API changes from 0.2.21-pre-alpha. Consumers must pin the exact released version and lock its registry integrity; never use a mutable range for a cryptographic protocol dependency.
|
|
120
120
|
|
|
121
121
|
The additive `twin3-sdk/runtime` subpath provides a single-action Agent runtime candidate. Products retain authenticated authority, context egress consent, durable storage and trusted tool brokers. Python `twin3.runtime` provides compatible contracts and digests only. OpenShell, ADK and Hermes adapters are not included. See [runtime scope and limits](docs/architecture/PERSONAL-AGENT-RUNTIME-V1.md).
|
|
122
122
|
|
|
@@ -125,15 +125,15 @@ The additive `twin3-sdk/runtime` subpath provides a single-action Agent runtime
|
|
|
125
125
|
After publication, install the exact released version from the registry:
|
|
126
126
|
|
|
127
127
|
```bash
|
|
128
|
-
npm install twin3-sdk@0.
|
|
129
|
-
pip install twin3-sdk==0.
|
|
128
|
+
npm install twin3-sdk@0.3.0
|
|
129
|
+
pip install twin3-sdk==0.3.0
|
|
130
130
|
```
|
|
131
131
|
|
|
132
132
|
For local evaluation, you can also use the packed npm tarball. Do not install from a git URL or a version range:
|
|
133
133
|
|
|
134
134
|
```bash
|
|
135
135
|
npm pack --ignore-scripts
|
|
136
|
-
npm install ./twin3-sdk-0.
|
|
136
|
+
npm install ./twin3-sdk-0.3.0.tgz
|
|
137
137
|
```
|
|
138
138
|
|
|
139
139
|
---
|
package/index.d.ts
CHANGED
|
@@ -224,6 +224,85 @@ export interface EngineObservationReading {
|
|
|
224
224
|
export declare function normalizeEngineObservation(row: EngineObservationInput, options: { host: string }): EngineObservation;
|
|
225
225
|
export declare function summarizeEngineObservations(observations: EngineObservation[], options: { host: string; sourceLevel?: boolean }): EngineObservationReading;
|
|
226
226
|
export declare function summarizeCohortVisibility(readingsBySubject: Record<string, EngineObservationReading>, subjectId: string, options?: { minCohortSize?: number }): Record<string, any>;
|
|
227
|
+
export declare class AiVisibilityError extends Error { code: string; }
|
|
228
|
+
export declare const AI_REFERRAL_SCHEMA: "twin3-discovery-ai-referral-v1";
|
|
229
|
+
export declare const AI_CRAWLER_SCHEMA: "twin3-discovery-ai-crawler-v1";
|
|
230
|
+
export declare const AI_READINESS_SCHEMA: "twin3-discovery-ai-readiness-v1";
|
|
231
|
+
export declare const AI_TRAJECTORY_SCHEMA: "twin3-discovery-ai-trajectory-v1";
|
|
232
|
+
export type AiCrawlerPurpose = "training" | "search" | "user" | "agent" | "control";
|
|
233
|
+
export type AiTrajectoryStage = "crawled" | "search_indexed" | "user_fetched" | "retrieved" | "mentioned" | "cited" | "clicked";
|
|
234
|
+
export interface AiVisibilityCatalog {
|
|
235
|
+
version: number;
|
|
236
|
+
checked_on: string;
|
|
237
|
+
provenance: Array<{ name: string; url: string; license: string; used: string[] }>;
|
|
238
|
+
referral_sources: Array<{ key: string; operator: string; hosts: string[]; aliases: string[]; surfaces?: Array<{ key: string; medium: string }>; referrer_path?: string; utm?: boolean }>;
|
|
239
|
+
crawlers: Array<{ token: string; operator: string; purpose: AiCrawlerPurpose; robots: string; user_agent: boolean; claim?: string }>;
|
|
240
|
+
stages: Array<{ key: AiTrajectoryStage; en: string; "zh-TW": string }>;
|
|
241
|
+
}
|
|
242
|
+
export declare const AI_VISIBILITY_CATALOG: AiVisibilityCatalog;
|
|
243
|
+
export interface AiReferral {
|
|
244
|
+
schema: "twin3-discovery-ai-referral-v1";
|
|
245
|
+
source: string;
|
|
246
|
+
operator: string;
|
|
247
|
+
origin: "referrer" | "utm";
|
|
248
|
+
surface: string | null;
|
|
249
|
+
catalog_version: number;
|
|
250
|
+
}
|
|
251
|
+
export interface AiCrawler {
|
|
252
|
+
schema: "twin3-discovery-ai-crawler-v1";
|
|
253
|
+
token: string;
|
|
254
|
+
operator: string;
|
|
255
|
+
purpose: AiCrawlerPurpose;
|
|
256
|
+
robots: string;
|
|
257
|
+
catalog_version: number;
|
|
258
|
+
}
|
|
259
|
+
export interface AiReadinessCheck {
|
|
260
|
+
id: string;
|
|
261
|
+
priority: "P0" | "P1" | "P2" | "P3";
|
|
262
|
+
status: "pass" | "warn" | "fail" | "info" | "na";
|
|
263
|
+
evidence: Record<string, unknown>;
|
|
264
|
+
action: string | null;
|
|
265
|
+
}
|
|
266
|
+
export interface AiReadinessReport {
|
|
267
|
+
schema: "twin3-discovery-ai-readiness-v1";
|
|
268
|
+
host: string;
|
|
269
|
+
path: string;
|
|
270
|
+
catalog_version: number;
|
|
271
|
+
checks: AiReadinessCheck[];
|
|
272
|
+
crawler_access: Record<string, { operator: string; purpose: AiCrawlerPurpose; allowed: boolean; matched: "named" | "star" | "none" }>;
|
|
273
|
+
gate: { passed: boolean; blocking: string[] };
|
|
274
|
+
actions: Array<{ check: string; priority: "P0" | "P1" | "P2" | "P3"; action: string }>;
|
|
275
|
+
non_claims: string[];
|
|
276
|
+
digest: string;
|
|
277
|
+
}
|
|
278
|
+
export interface AiTrajectorySignal { state: "measured" | "withheld" | "unknown"; rate_bp: number | null; }
|
|
279
|
+
export interface AiTrajectoryOperator {
|
|
280
|
+
crawl: Record<"training" | "search" | "user" | "agent" | "other", number>;
|
|
281
|
+
refused: number;
|
|
282
|
+
engines: Record<string, Record<"retrieved" | "mentioned" | "cited", AiTrajectorySignal> & { answer_visibility_bp: number | null }>;
|
|
283
|
+
clicks: number;
|
|
284
|
+
click_sources: Record<string, number>;
|
|
285
|
+
click_surfaces: Record<string, number>;
|
|
286
|
+
stages: AiTrajectoryStage[];
|
|
287
|
+
}
|
|
288
|
+
export interface AiTrajectory {
|
|
289
|
+
schema: "twin3-discovery-ai-trajectory-v1";
|
|
290
|
+
host: string;
|
|
291
|
+
catalog_version: number;
|
|
292
|
+
operators: Record<string, AiTrajectoryOperator>;
|
|
293
|
+
totals: { fetched_requests: number; refused_requests: number; unclassified_requests: number; ai_sessions: number; non_ai_sessions: number };
|
|
294
|
+
reading_digest: string | null;
|
|
295
|
+
reading_valid: boolean | null;
|
|
296
|
+
unattributed_engines: string[];
|
|
297
|
+
score_weights: { mentioned: number; cited: number; divisor: number; source: string };
|
|
298
|
+
non_claims: string[];
|
|
299
|
+
digest: string;
|
|
300
|
+
}
|
|
301
|
+
export interface AiReferralRow { referrer?: string | null; landing_url?: string | null; utm_source?: string | null; sessions?: number; }
|
|
302
|
+
export declare function classifyAiReferral(referrer?: string | null, options?: { landingUrl?: string | null; utmSource?: string | null }): AiReferral | null;
|
|
303
|
+
export declare function classifyAiCrawler(userAgent: string): AiCrawler | null;
|
|
304
|
+
export declare function auditAiReadiness(options: { host: string; path?: string; robotsTxt?: string | null; html?: string | null; llmsTxt?: string | null }): AiReadinessReport;
|
|
305
|
+
export declare function summarizeAiTrajectory(options: { host: string; visits?: DiscoveryVisitEvidence[]; reading?: EngineObservationReading | null; referrals?: AiReferralRow[]; engineOperators?: Record<string, string> | null }): AiTrajectory;
|
|
227
306
|
export declare function buildSignedReferral(options: Record<string, any>): Record<string, any>;
|
|
228
307
|
export declare function verifySignedReferral(token: Record<string, any>, options: Record<string, any>): Record<string, any>;
|
|
229
308
|
|
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("node:crypto");
|
|
4
|
+
const CATALOG = require("../twin3/discovery/ai-visibility-catalog.json");
|
|
5
|
+
|
|
6
|
+
const CATALOG_VERSION = CATALOG.version;
|
|
7
|
+
const VISIT_EVIDENCE_SCHEMA = "twin3-discovery-visit-evidence-v1";
|
|
8
|
+
const ENGINE_READING_SCHEMA = "twin3-discovery-engine-reading-v1";
|
|
9
|
+
const AI_REFERRAL_SCHEMA = "twin3-discovery-ai-referral-v1";
|
|
10
|
+
const AI_CRAWLER_SCHEMA = "twin3-discovery-ai-crawler-v1";
|
|
11
|
+
const AI_READINESS_SCHEMA = "twin3-discovery-ai-readiness-v1";
|
|
12
|
+
const AI_TRAJECTORY_SCHEMA = "twin3-discovery-ai-trajectory-v1";
|
|
13
|
+
|
|
14
|
+
const PURPOSES = ["training", "search", "user", "agent", "other"];
|
|
15
|
+
const SIGNALS = ["retrieved", "mentioned", "cited"];
|
|
16
|
+
const STAGES = CATALOG.stages.map(stage => stage.key);
|
|
17
|
+
const MIN_VISIBLE_CHARS = 200;
|
|
18
|
+
const SCORE_WEIGHTS = {
|
|
19
|
+
mentioned: 12,
|
|
20
|
+
cited: 5,
|
|
21
|
+
divisor: 17,
|
|
22
|
+
source: "ansvisor visibility-score 0.60 mention / 0.25 citation, position term dropped",
|
|
23
|
+
};
|
|
24
|
+
const READINESS_NON_CLAIMS = [
|
|
25
|
+
"robots_allow_is_not_proof_of_indexing",
|
|
26
|
+
"readiness_is_not_visibility",
|
|
27
|
+
"unverified_user_agent_responses_not_tested",
|
|
28
|
+
"llms_txt_has_no_documented_ranking_effect",
|
|
29
|
+
];
|
|
30
|
+
const TRAJECTORY_NON_CLAIMS = [
|
|
31
|
+
"user_agent_claims_are_not_verified_operator_identity",
|
|
32
|
+
"referrer_counts_undercount_apps_that_strip_referrers",
|
|
33
|
+
"rates_do_not_pool_across_engines",
|
|
34
|
+
"stages_are_not_a_causal_funnel",
|
|
35
|
+
];
|
|
36
|
+
const ENTITY_TYPES = new Set([
|
|
37
|
+
"Organization", "Corporation", "LocalBusiness", "NGO", "Person", "ProfilePage", "Brand",
|
|
38
|
+
"SoftwareApplication", "WebApplication", "WebAPI", "Product", "Service",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
const WS = " \t\n\r\f";
|
|
42
|
+
const HOST = /^[a-z0-9](?:[a-z0-9.-]{0,251}[a-z0-9])?$/;
|
|
43
|
+
const OPERATOR = /^[A-Za-z0-9](?:[A-Za-z0-9 ._-]{0,78}[A-Za-z0-9.])?$/;
|
|
44
|
+
const ENGINE = /^[A-Za-z0-9._:/-](?:[A-Za-z0-9 ._:/-]{0,78}[A-Za-z0-9._:/-])?$/;
|
|
45
|
+
const SHA256 = /^[0-9a-f]{64}$/;
|
|
46
|
+
const DIRECTIVE = /^[a-z0-9_:-]{1,40}$/;
|
|
47
|
+
const TYPE_NAME = /^[A-Za-z0-9:/._#-]{1,100}$/;
|
|
48
|
+
const RAW_TEXT = /<(script|style|template)(?=[\t\n\f\r />])([^>]*)>([\s\S]*?)<\/\1[\t\n\f\r ]*>/gi;
|
|
49
|
+
const ATTR = /([A-Za-z_:][-A-Za-z0-9_:.]*)(?:[\t\n\f\r ]*=[\t\n\f\r ]*(?:"([^"]*)"|'([^']*)'|([^\t\n\f\r "'>`=<]+)))?/g;
|
|
50
|
+
const escapeRegex = value => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
51
|
+
const CRAWLER_PATTERNS = CATALOG.crawlers.filter(crawler => crawler.user_agent).map(crawler =>
|
|
52
|
+
[crawler, new RegExp("(?:^|[^A-Za-z0-9_-])" + escapeRegex(crawler.token) + "(?:$|[^A-Za-z0-9_@.-])", "i")]);
|
|
53
|
+
const CRAWLERS_BY_TOKEN = new Map(CATALOG.crawlers.map(crawler => [crawler.token, crawler]));
|
|
54
|
+
const PRIORITY = { P0: 0, P1: 1, P2: 2, P3: 3 };
|
|
55
|
+
const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
56
|
+
|
|
57
|
+
class AiVisibilityError extends Error {
|
|
58
|
+
constructor(code) {
|
|
59
|
+
super(code);
|
|
60
|
+
this.name = "AiVisibilityError";
|
|
61
|
+
this.code = code;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function requireValue(condition, code) {
|
|
66
|
+
if (!condition) throw new AiVisibilityError(code);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isRecord(value) {
|
|
70
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
71
|
+
&& [Object.prototype, null].includes(Object.getPrototypeOf(value));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function get(record, key) {
|
|
75
|
+
return Object.hasOwn(record, key) && record[key] !== undefined ? record[key] : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function asciiLower(value) {
|
|
79
|
+
return value.replace(/[A-Z]/g, char => String.fromCharCode(char.charCodeAt(0) + 32));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function strip(value, chars = WS) {
|
|
83
|
+
let start = 0;
|
|
84
|
+
let end = value.length;
|
|
85
|
+
while (start < end && chars.includes(value[start])) start += 1;
|
|
86
|
+
while (end > start && chars.includes(value[end - 1])) end -= 1;
|
|
87
|
+
return value.slice(start, end);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function codePoints(value) {
|
|
91
|
+
let count = 0;
|
|
92
|
+
for (const _ of value) count += 1;
|
|
93
|
+
return count;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function text(value, limit, code) {
|
|
97
|
+
requireValue(typeof value === "string" && value.length <= limit * 2 && !/[\ud800-\udfff]/u.test(value) && codePoints(value) <= limit, code);
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function optionalText(value, limit, code) {
|
|
102
|
+
return value === null || value === undefined ? null : text(value, limit, code);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function hostValue(value, code = "host_invalid") {
|
|
106
|
+
requireValue(typeof value === "string" && !/[^\x00-\x7f]/.test(value), code);
|
|
107
|
+
const host = asciiLower(strip(value, WS + "\v")).replace(/\.$/, "");
|
|
108
|
+
requireValue(HOST.test(host) && !host.includes(".."), code);
|
|
109
|
+
return host;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function count(value, code, minimum = 0) {
|
|
113
|
+
requireValue(Number.isSafeInteger(value) && minimum <= value && value <= 1_000_000_000, code);
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function stripWww(host) {
|
|
118
|
+
return host.startsWith("www.") ? host.slice(4) : host;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function under(value, host) {
|
|
122
|
+
return value === host || value.endsWith("." + host);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function urlParts(value) {
|
|
126
|
+
const match = /^([A-Za-z][A-Za-z0-9+.-]*):\/\/([^/?#]*)([^?#]*)(\?[^#]*)?(#[\s\S]*)?$/.exec(value);
|
|
127
|
+
if (match === null || !["http", "https"].includes(asciiLower(match[1]))) return null;
|
|
128
|
+
const authority = match[2];
|
|
129
|
+
if (authority.includes("@") || authority.includes("[")) return null;
|
|
130
|
+
let host;
|
|
131
|
+
try {
|
|
132
|
+
host = hostValue(authority.split(":")[0]);
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
return [host, match[3] || "/", (match[4] || "").slice(1)];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function landingMedium(landingUrl) {
|
|
140
|
+
const parts = landingUrl === null ? null : urlParts(landingUrl);
|
|
141
|
+
if (parts === null) return "";
|
|
142
|
+
for (const pair of parts[2].split("&")) {
|
|
143
|
+
const index = pair.indexOf("=");
|
|
144
|
+
const name = index < 0 ? pair : pair.slice(0, index);
|
|
145
|
+
if (asciiLower(name) === "utm_medium") return asciiLower(index < 0 ? "" : pair.slice(index + 1));
|
|
146
|
+
}
|
|
147
|
+
return "";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function surfaceOf(source, landingUrl) {
|
|
151
|
+
const medium = landingMedium(landingUrl);
|
|
152
|
+
if (!medium) return null;
|
|
153
|
+
const surface = (source.surfaces || []).find(item => item.medium === medium);
|
|
154
|
+
return surface ? surface.key : null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function classifyAiReferral(referrer = null, { landingUrl = null, utmSource = null } = {}) {
|
|
158
|
+
referrer = optionalText(referrer, 2048, "referral_invalid");
|
|
159
|
+
landingUrl = optionalText(landingUrl, 4096, "referral_invalid");
|
|
160
|
+
utmSource = optionalText(utmSource, 256, "referral_invalid");
|
|
161
|
+
let match = null;
|
|
162
|
+
if (referrer) {
|
|
163
|
+
let parts = referrer.includes("://") ? urlParts(referrer) : null;
|
|
164
|
+
if (parts === null && !referrer.includes("://")) {
|
|
165
|
+
try {
|
|
166
|
+
parts = [hostValue(referrer), null, ""];
|
|
167
|
+
} catch {
|
|
168
|
+
parts = null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (parts !== null) {
|
|
172
|
+
const host = stripWww(parts[0]);
|
|
173
|
+
const path = parts[1];
|
|
174
|
+
for (const source of CATALOG.referral_sources) {
|
|
175
|
+
if (!source.hosts.some(candidate => under(host, candidate))) continue;
|
|
176
|
+
const required = source.referrer_path;
|
|
177
|
+
if (required !== undefined && (path === null || !path.startsWith(required))) continue;
|
|
178
|
+
match = [source, "referrer"];
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (match === null && utmSource) {
|
|
184
|
+
const value = stripWww(asciiLower(strip(utmSource)));
|
|
185
|
+
for (const source of CATALOG.referral_sources) {
|
|
186
|
+
if (source.utm === false) continue;
|
|
187
|
+
if (source.aliases.includes(value) || source.hosts.some(candidate => under(value, candidate))) {
|
|
188
|
+
match = [source, "utm"];
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (match === null) return null;
|
|
194
|
+
const [source, origin] = match;
|
|
195
|
+
return {
|
|
196
|
+
schema: AI_REFERRAL_SCHEMA,
|
|
197
|
+
source: source.key,
|
|
198
|
+
operator: source.operator,
|
|
199
|
+
origin,
|
|
200
|
+
surface: surfaceOf(source, landingUrl),
|
|
201
|
+
catalog_version: CATALOG_VERSION,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function classifyAiCrawler(userAgent) {
|
|
206
|
+
userAgent = text(userAgent, 2048, "crawler_user_agent_invalid");
|
|
207
|
+
for (const [crawler, pattern] of CRAWLER_PATTERNS) {
|
|
208
|
+
if (pattern.test(userAgent)) {
|
|
209
|
+
return {
|
|
210
|
+
schema: AI_CRAWLER_SCHEMA,
|
|
211
|
+
token: crawler.token,
|
|
212
|
+
operator: crawler.operator,
|
|
213
|
+
purpose: crawler.purpose,
|
|
214
|
+
robots: crawler.robots,
|
|
215
|
+
catalog_version: CATALOG_VERSION,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function parseRobots(source) {
|
|
223
|
+
const groups = [];
|
|
224
|
+
let current = null;
|
|
225
|
+
let lastWasAgent = false;
|
|
226
|
+
for (const raw of source.replace(/^/, "").split(/\r\n|\n|\r/)) {
|
|
227
|
+
const line = strip(raw.split("#")[0], WS + "\v");
|
|
228
|
+
if (!line || !line.includes(":")) continue;
|
|
229
|
+
const index = line.indexOf(":");
|
|
230
|
+
const name = asciiLower(strip(line.slice(0, index), WS + "\v"));
|
|
231
|
+
const value = strip(line.slice(index + 1), WS + "\v");
|
|
232
|
+
if (name === "user-agent") {
|
|
233
|
+
if (current === null || !lastWasAgent) {
|
|
234
|
+
current = { agents: [], rules: [] };
|
|
235
|
+
groups.push(current);
|
|
236
|
+
}
|
|
237
|
+
current.agents.push(asciiLower(value));
|
|
238
|
+
lastWasAgent = true;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
lastWasAgent = false;
|
|
242
|
+
if (current !== null && (name === "allow" || name === "disallow")) current.rules.push([name, value]);
|
|
243
|
+
}
|
|
244
|
+
return groups;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function selectGroup(groups, token) {
|
|
248
|
+
const lowered = asciiLower(token);
|
|
249
|
+
const named = groups.filter(group => group.agents.includes(lowered));
|
|
250
|
+
const chosen = named.length ? named : groups.filter(group => group.agents.includes("*"));
|
|
251
|
+
const matched = named.length ? "named" : chosen.length ? "star" : "none";
|
|
252
|
+
return [matched, chosen.flatMap(group => group.rules)];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function patternMatches(pattern, path) {
|
|
256
|
+
const anchored = pattern.endsWith("$");
|
|
257
|
+
const parts = (anchored ? pattern.slice(0, -1) : pattern).split("*");
|
|
258
|
+
if (!path.startsWith(parts[0])) return false;
|
|
259
|
+
let position = parts[0].length;
|
|
260
|
+
if (parts.length === 1) return anchored ? position === path.length : true;
|
|
261
|
+
for (const part of parts.slice(1, -1)) {
|
|
262
|
+
const index = path.indexOf(part, position);
|
|
263
|
+
if (index < 0) return false;
|
|
264
|
+
position = index + part.length;
|
|
265
|
+
}
|
|
266
|
+
const last = parts[parts.length - 1];
|
|
267
|
+
if (anchored) return path.length - last.length >= position && path.endsWith(last);
|
|
268
|
+
return path.indexOf(last, position) >= 0;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function allowedBy(rules, path) {
|
|
272
|
+
let best = -1;
|
|
273
|
+
let allowed = true;
|
|
274
|
+
for (const [name, value] of rules) {
|
|
275
|
+
if (name === "disallow" && value === "") continue;
|
|
276
|
+
if (patternMatches(value, path) && (value.length > best || (value.length === best && name === "allow"))) {
|
|
277
|
+
best = value.length;
|
|
278
|
+
allowed = name === "allow";
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return allowed;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function attributes(tag) {
|
|
285
|
+
const inner = tag.replace(/^<[A-Za-z0-9]+/, "").replace(/[>/]+$/, "");
|
|
286
|
+
const found = {};
|
|
287
|
+
for (const match of inner.matchAll(ATTR)) {
|
|
288
|
+
const name = asciiLower(match[1]);
|
|
289
|
+
if (!Object.hasOwn(found, name)) {
|
|
290
|
+
const value = [match[2], match[3], match[4]].find(group => group !== undefined);
|
|
291
|
+
found[name] = value === undefined ? "" : value;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return found;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function tags(html, name) {
|
|
298
|
+
return [...html.matchAll(new RegExp("<" + name + "(?=[\\t\\n\\f\\r />])[^>]*>", "gi"))].map(match => attributes(match[0]));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function attr(record, key) {
|
|
302
|
+
return Object.hasOwn(record, key) ? record[key] : "";
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function jsonLd(blocks) {
|
|
306
|
+
const types = new Set();
|
|
307
|
+
let dated = false;
|
|
308
|
+
let invalid = 0;
|
|
309
|
+
for (const block of blocks.slice(0, 32)) {
|
|
310
|
+
let value;
|
|
311
|
+
try {
|
|
312
|
+
value = JSON.parse(block);
|
|
313
|
+
} catch {
|
|
314
|
+
invalid += 1;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
const stack = [[value, 0]];
|
|
318
|
+
while (stack.length) {
|
|
319
|
+
const [node, depth] = stack.pop();
|
|
320
|
+
if (depth > 64) continue;
|
|
321
|
+
if (node !== null && typeof node === "object" && !Array.isArray(node)) {
|
|
322
|
+
const kind = Object.hasOwn(node, "@type") ? node["@type"] : null;
|
|
323
|
+
for (const name of Array.isArray(kind) ? kind : [kind]) {
|
|
324
|
+
if (typeof name === "string" && TYPE_NAME.test(name)) types.add(name);
|
|
325
|
+
}
|
|
326
|
+
for (const key of ["dateModified", "datePublished"]) {
|
|
327
|
+
if (Object.hasOwn(node, key) && typeof node[key] === "string" && strip(node[key])) dated = true;
|
|
328
|
+
}
|
|
329
|
+
for (const child of Object.values(node)) stack.push([child, depth + 1]);
|
|
330
|
+
} else if (Array.isArray(node)) {
|
|
331
|
+
for (const child of node) stack.push([child, depth + 1]);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return [[...types].sort(compare), dated, invalid];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function shortType(name) {
|
|
339
|
+
const parts = name.split(/[/:#]/);
|
|
340
|
+
return parts[parts.length - 1];
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function check(id, priority, status, evidence, action = null) {
|
|
344
|
+
return { id, priority, status, evidence, action };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function crawlerChecks(robotsTxt, path) {
|
|
348
|
+
const groups = robotsTxt !== null ? parseRobots(robotsTxt) : [];
|
|
349
|
+
const present = robotsTxt !== null;
|
|
350
|
+
const checks = [check("robots_txt", "P2", present ? "pass" : "warn", { present }, present ? null : "publish_robots_txt")];
|
|
351
|
+
const access = {};
|
|
352
|
+
for (const crawler of CATALOG.crawlers) {
|
|
353
|
+
if (crawler.robots === "not_applicable") continue;
|
|
354
|
+
const [matched, rules] = selectGroup(groups, crawler.token);
|
|
355
|
+
const allowed = allowedBy(rules, path);
|
|
356
|
+
access[crawler.token] = { operator: crawler.operator, purpose: crawler.purpose, allowed, matched };
|
|
357
|
+
const evidence = { allowed, matched, purpose: crawler.purpose, claim: crawler.claim === undefined ? null : crawler.claim };
|
|
358
|
+
let result;
|
|
359
|
+
if (crawler.purpose === "search") result = allowed ? ["pass", "P0", null] : ["fail", "P0", "allow_ai_search_crawler"];
|
|
360
|
+
else if (crawler.purpose === "user") result = allowed ? ["pass", "P1", null] : ["warn", "P1", "allow_user_triggered_fetcher"];
|
|
361
|
+
else result = ["info", "P3", null];
|
|
362
|
+
checks.push(check("crawler:" + crawler.token, result[1], result[0], evidence, result[2]));
|
|
363
|
+
}
|
|
364
|
+
return [checks, access];
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function htmlChecks(source, host) {
|
|
368
|
+
if (source === null) return [check("html", "P0", "na", { present: false }, "supply_server_rendered_html")];
|
|
369
|
+
const html = source.replace(/<!--[\s\S]*?-->/g, " ");
|
|
370
|
+
const ldBlocks = [];
|
|
371
|
+
for (const match of html.matchAll(RAW_TEXT)) {
|
|
372
|
+
if (asciiLower(match[1]) !== "script") continue;
|
|
373
|
+
const kind = asciiLower(strip(attr(attributes("<script" + match[2] + ">"), "type")));
|
|
374
|
+
if (kind === "application/ld+json") ldBlocks.push(match[3]);
|
|
375
|
+
}
|
|
376
|
+
const body = html.replace(RAW_TEXT, " ");
|
|
377
|
+
let visible = 0;
|
|
378
|
+
for (const char of body.replace(/<[^>]*>/g, " ")) if (!(WS + "\v ").includes(char)) visible += 1;
|
|
379
|
+
const enough = visible >= MIN_VISIBLE_CHARS;
|
|
380
|
+
const checks = [check("ssr_text", "P0", enough ? "pass" : "fail", { visible_chars: visible, minimum: MIN_VISIBLE_CHARS },
|
|
381
|
+
enough ? null : "server_render_card_content")];
|
|
382
|
+
const title = /<title(?=[\t\n\f\r />])[^>]*>([\s\S]*?)<\/title[\t\n\f\r ]*>/i.exec(body);
|
|
383
|
+
const hasTitle = title !== null && Boolean(strip(title[1].replace(/<[^>]*>/g, "")));
|
|
384
|
+
checks.push(check("title", "P1", hasTitle ? "pass" : "fail", { present: hasTitle }, hasTitle ? null : "add_title"));
|
|
385
|
+
const metas = tags(body, "meta");
|
|
386
|
+
const content = (key, value) => metas.some(meta => asciiLower(attr(meta, key)) === value && Boolean(strip(attr(meta, "content"))));
|
|
387
|
+
const hasDescription = content("name", "description");
|
|
388
|
+
checks.push(check("meta_description", "P2", hasDescription ? "pass" : "warn", { present: hasDescription },
|
|
389
|
+
hasDescription ? null : "add_meta_description"));
|
|
390
|
+
const canonicalLink = tags(body, "link").find(link => asciiLower(attr(link, "rel")).split(/[ \t\n\r\f\v]+/).includes("canonical"));
|
|
391
|
+
const canonical = canonicalLink === undefined ? null : strip(attr(canonicalLink, "href"));
|
|
392
|
+
if (!canonical) {
|
|
393
|
+
checks.push(check("canonical", "P2", "warn", { present: false, same_host: null }, "add_canonical"));
|
|
394
|
+
} else {
|
|
395
|
+
const parts = canonical.includes("://") ? urlParts(canonical) : null;
|
|
396
|
+
const same = (canonical.startsWith("/") && !canonical.startsWith("//")) || (parts !== null && parts[0] === host);
|
|
397
|
+
checks.push(check("canonical", same ? "P2" : "P1", same ? "pass" : "warn", { present: true, same_host: same },
|
|
398
|
+
same ? null : "point_canonical_to_card_host"));
|
|
399
|
+
}
|
|
400
|
+
const directives = new Set();
|
|
401
|
+
for (const meta of metas) {
|
|
402
|
+
if (["robots", "googlebot"].includes(asciiLower(attr(meta, "name")))) {
|
|
403
|
+
for (const piece of asciiLower(attr(meta, "content")).split(",")) {
|
|
404
|
+
const part = strip(piece);
|
|
405
|
+
if (DIRECTIVE.test(part)) directives.add(part);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const sortedDirectives = [...directives].sort(compare);
|
|
410
|
+
if (directives.has("noindex") || directives.has("none")) {
|
|
411
|
+
checks.push(check("meta_robots", "P0", "fail", { directives: sortedDirectives }, "remove_noindex"));
|
|
412
|
+
} else if (directives.has("nosnippet")) {
|
|
413
|
+
checks.push(check("meta_robots", "P1", "warn", { directives: sortedDirectives }, "remove_nosnippet"));
|
|
414
|
+
} else {
|
|
415
|
+
checks.push(check("meta_robots", "P0", "pass", { directives: sortedDirectives }));
|
|
416
|
+
}
|
|
417
|
+
const [types, ldDated, invalid] = jsonLd(ldBlocks);
|
|
418
|
+
const entity = types.some(name => ENTITY_TYPES.has(shortType(name)));
|
|
419
|
+
const evidence = { blocks: ldBlocks.length, invalid, types, entity };
|
|
420
|
+
if (invalid) checks.push(check("json_ld", "P1", "fail", evidence, "fix_json_ld_syntax"));
|
|
421
|
+
else if (!ldBlocks.length) checks.push(check("json_ld", "P1", "warn", evidence, "add_entity_json_ld"));
|
|
422
|
+
else if (!entity) checks.push(check("json_ld", "P2", "warn", evidence, "add_entity_json_ld"));
|
|
423
|
+
else checks.push(check("json_ld", "P1", "pass", evidence));
|
|
424
|
+
const dated = ldDated || content("property", "article:modified_time") || content("property", "article:published_time")
|
|
425
|
+
|| tags(body, "time").some(time => Boolean(strip(attr(time, "datetime"))));
|
|
426
|
+
checks.push(check("freshness", "P2", dated ? "pass" : "warn", { dated }, dated ? null : "publish_dates"));
|
|
427
|
+
const headings = [...body.matchAll(/<h1(?=[\t\n\f\r />])/gi)].length;
|
|
428
|
+
checks.push(check("h1", headings === 0 ? "P2" : "P3", headings === 1 ? "pass" : "warn", { count: headings },
|
|
429
|
+
headings === 1 ? null : "use_one_h1"));
|
|
430
|
+
return checks;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function operatorEntry() {
|
|
434
|
+
return {
|
|
435
|
+
crawl: Object.fromEntries(PURPOSES.map(purpose => [purpose, 0])),
|
|
436
|
+
refused: 0,
|
|
437
|
+
engines: {},
|
|
438
|
+
clicks: 0,
|
|
439
|
+
click_sources: {},
|
|
440
|
+
click_surfaces: {},
|
|
441
|
+
stages: [],
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function signalOf(readingSignal) {
|
|
446
|
+
requireValue(isRecord(readingSignal), "trajectory_reading_invalid");
|
|
447
|
+
const state = get(readingSignal, "state");
|
|
448
|
+
const rate = get(readingSignal, "rate_bp");
|
|
449
|
+
requireValue(["measured", "withheld", "unknown"].includes(state), "trajectory_reading_invalid");
|
|
450
|
+
if (state === "measured") requireValue(Number.isInteger(rate) && rate >= 0 && rate <= 10000, "trajectory_reading_invalid");
|
|
451
|
+
else requireValue(rate === null, "trajectory_reading_invalid");
|
|
452
|
+
return { state, rate_bp: rate };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function createAiVisibilityApi(canonicalJsonBuffer) {
|
|
456
|
+
const digest = value => crypto.createHash("sha256").update(canonicalJsonBuffer(value)).digest("hex");
|
|
457
|
+
|
|
458
|
+
function verified(row, schema, host, code) {
|
|
459
|
+
requireValue(isRecord(row) && row.schema === schema && row.host === host, code);
|
|
460
|
+
const supplied = row.digest;
|
|
461
|
+
requireValue(typeof supplied === "string" && SHA256.test(supplied), code);
|
|
462
|
+
const { digest: _, ...body } = row;
|
|
463
|
+
let expected;
|
|
464
|
+
try {
|
|
465
|
+
expected = digest(body);
|
|
466
|
+
} catch {
|
|
467
|
+
throw new AiVisibilityError(code);
|
|
468
|
+
}
|
|
469
|
+
requireValue(crypto.timingSafeEqual(Buffer.from(supplied, "ascii"), Buffer.from(expected, "ascii")), code);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function auditAiReadiness({ host, path = "/", robotsTxt = null, html = null, llmsTxt = null } = {}) {
|
|
473
|
+
const owner = hostValue(host, "readiness_host_invalid");
|
|
474
|
+
requireValue(typeof path === "string" && path.startsWith("/") && path.length <= 2048
|
|
475
|
+
&& !/[^\x00-\x7f]/.test(path) && !/[\x00-\x20\x7f?#]/.test(path), "readiness_path_invalid");
|
|
476
|
+
robotsTxt = optionalText(robotsTxt, 512_000, "readiness_robots_invalid");
|
|
477
|
+
html = optionalText(html, 2_000_000, "readiness_html_invalid");
|
|
478
|
+
llmsTxt = optionalText(llmsTxt, 512_000, "readiness_llms_invalid");
|
|
479
|
+
const [checks, access] = crawlerChecks(robotsTxt, path);
|
|
480
|
+
checks.push(...htmlChecks(html, owner));
|
|
481
|
+
checks.push(check("llms_txt", "P3", "info", { present: Boolean(llmsTxt && strip(llmsTxt)) }));
|
|
482
|
+
const blocking = checks.filter(item => ["fail", "na"].includes(item.status) && item.priority === "P0").map(item => item.id).sort(compare);
|
|
483
|
+
const actions = checks
|
|
484
|
+
.filter(item => item.action && ["fail", "warn", "na"].includes(item.status))
|
|
485
|
+
.map(item => ({ check: item.id, priority: item.priority, action: item.action }))
|
|
486
|
+
.sort((a, b) => PRIORITY[a.priority] - PRIORITY[b.priority] || compare(a.check, b.check));
|
|
487
|
+
const body = {
|
|
488
|
+
schema: AI_READINESS_SCHEMA,
|
|
489
|
+
host: owner,
|
|
490
|
+
path,
|
|
491
|
+
catalog_version: CATALOG_VERSION,
|
|
492
|
+
checks,
|
|
493
|
+
crawler_access: access,
|
|
494
|
+
gate: { passed: !blocking.length, blocking },
|
|
495
|
+
actions,
|
|
496
|
+
non_claims: [...READINESS_NON_CLAIMS],
|
|
497
|
+
};
|
|
498
|
+
return { ...body, digest: digest(body) };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function summarizeAiTrajectory({ host, visits = [], reading = null, referrals = [], engineOperators = null } = {}) {
|
|
502
|
+
const owner = hostValue(host, "trajectory_host_invalid");
|
|
503
|
+
requireValue(Array.isArray(visits) && visits.length <= 100_000, "trajectory_visits_invalid");
|
|
504
|
+
requireValue(Array.isArray(referrals) && referrals.length <= 100_000, "trajectory_referrals_invalid");
|
|
505
|
+
const mapping = engineOperators === null ? {} : engineOperators;
|
|
506
|
+
requireValue(isRecord(mapping) && Object.entries(mapping).every(([engine, operator]) =>
|
|
507
|
+
ENGINE.test(engine) && typeof operator === "string" && OPERATOR.test(operator)), "trajectory_engine_operators_invalid");
|
|
508
|
+
const operators = {};
|
|
509
|
+
const ensure = name => (operators[name] = Object.hasOwn(operators, name) ? operators[name] : operatorEntry());
|
|
510
|
+
const totals = { fetched_requests: 0, refused_requests: 0, unclassified_requests: 0, ai_sessions: 0, non_ai_sessions: 0 };
|
|
511
|
+
for (const visit of visits) {
|
|
512
|
+
verified(visit, VISIT_EVIDENCE_SCHEMA, owner, "trajectory_visit_invalid");
|
|
513
|
+
const requests = count(get(visit, "request_count"), "trajectory_visit_invalid", 1);
|
|
514
|
+
const status = count(get(visit, "status"), "trajectory_visit_invalid");
|
|
515
|
+
const bot = get(visit, "bot");
|
|
516
|
+
if (bot === null) {
|
|
517
|
+
totals.unclassified_requests += requests;
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
requireValue(typeof bot === "string" && typeof get(visit, "provider") === "string", "trajectory_visit_invalid");
|
|
521
|
+
const crawler = CRAWLERS_BY_TOKEN.get(bot);
|
|
522
|
+
const entry = ensure(crawler ? crawler.operator : visit.provider);
|
|
523
|
+
const purpose = crawler && PURPOSES.includes(crawler.purpose) ? crawler.purpose : "other";
|
|
524
|
+
if (status >= 400) {
|
|
525
|
+
entry.refused += requests;
|
|
526
|
+
totals.refused_requests += requests;
|
|
527
|
+
} else {
|
|
528
|
+
entry.crawl[purpose] += requests;
|
|
529
|
+
totals.fetched_requests += requests;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
const unattributed = [];
|
|
533
|
+
let readingDigest = null;
|
|
534
|
+
let readingValid = null;
|
|
535
|
+
if (reading !== null) {
|
|
536
|
+
verified(reading, ENGINE_READING_SCHEMA, owner, "trajectory_reading_invalid");
|
|
537
|
+
requireValue(isRecord(get(reading, "by_engine")) && typeof get(reading, "valid") === "boolean", "trajectory_reading_invalid");
|
|
538
|
+
readingDigest = reading.digest;
|
|
539
|
+
readingValid = reading.valid;
|
|
540
|
+
for (const engine of Object.keys(reading.by_engine).sort(compare)) {
|
|
541
|
+
const entryIn = reading.by_engine[engine];
|
|
542
|
+
requireValue(ENGINE.test(engine), "trajectory_reading_invalid");
|
|
543
|
+
requireValue(isRecord(entryIn) && isRecord(get(entryIn, "signals")), "trajectory_reading_invalid");
|
|
544
|
+
const signals = Object.fromEntries(SIGNALS.map(name => [name, signalOf(get(entryIn.signals, name))]));
|
|
545
|
+
const { mentioned, cited } = signals;
|
|
546
|
+
const score = mentioned.state === "measured" && cited.state === "measured"
|
|
547
|
+
? Math.floor((SCORE_WEIGHTS.mentioned * mentioned.rate_bp + SCORE_WEIGHTS.cited * cited.rate_bp) / SCORE_WEIGHTS.divisor)
|
|
548
|
+
: null;
|
|
549
|
+
if (!Object.hasOwn(mapping, engine)) {
|
|
550
|
+
unattributed.push(engine);
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
ensure(mapping[engine]).engines[engine] = { ...signals, answer_visibility_bp: score };
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
const referralKeys = ["referrer", "landing_url", "utm_source", "sessions"];
|
|
557
|
+
for (const row of referrals) {
|
|
558
|
+
requireValue(isRecord(row) && Object.keys(row).every(key => referralKeys.includes(key)), "trajectory_referral_invalid");
|
|
559
|
+
const sessions = count(Object.hasOwn(row, "sessions") ? row.sessions : 1, "trajectory_referral_invalid", 1);
|
|
560
|
+
let referral;
|
|
561
|
+
try {
|
|
562
|
+
referral = classifyAiReferral(get(row, "referrer"), { landingUrl: get(row, "landing_url"), utmSource: get(row, "utm_source") });
|
|
563
|
+
} catch {
|
|
564
|
+
throw new AiVisibilityError("trajectory_referral_invalid");
|
|
565
|
+
}
|
|
566
|
+
if (referral === null) {
|
|
567
|
+
totals.non_ai_sessions += sessions;
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
const entry = ensure(referral.operator);
|
|
571
|
+
entry.clicks += sessions;
|
|
572
|
+
entry.click_sources[referral.source] = (entry.click_sources[referral.source] || 0) + sessions;
|
|
573
|
+
if (referral.surface !== null) entry.click_surfaces[referral.surface] = (entry.click_surfaces[referral.surface] || 0) + sessions;
|
|
574
|
+
totals.ai_sessions += sessions;
|
|
575
|
+
}
|
|
576
|
+
for (const entry of Object.values(operators)) {
|
|
577
|
+
const reached = {
|
|
578
|
+
crawled: Object.values(entry.crawl).some(value => value > 0),
|
|
579
|
+
search_indexed: entry.crawl.search > 0,
|
|
580
|
+
user_fetched: entry.crawl.user > 0,
|
|
581
|
+
clicked: entry.clicks > 0,
|
|
582
|
+
};
|
|
583
|
+
for (const name of SIGNALS) {
|
|
584
|
+
reached[name] = Object.values(entry.engines).some(engine => engine[name].state === "measured" && engine[name].rate_bp > 0);
|
|
585
|
+
}
|
|
586
|
+
entry.stages = STAGES.filter(stage => reached[stage]);
|
|
587
|
+
}
|
|
588
|
+
const body = {
|
|
589
|
+
schema: AI_TRAJECTORY_SCHEMA,
|
|
590
|
+
host: owner,
|
|
591
|
+
catalog_version: CATALOG_VERSION,
|
|
592
|
+
operators,
|
|
593
|
+
totals,
|
|
594
|
+
reading_digest: readingDigest,
|
|
595
|
+
reading_valid: readingValid,
|
|
596
|
+
unattributed_engines: unattributed,
|
|
597
|
+
score_weights: { ...SCORE_WEIGHTS },
|
|
598
|
+
non_claims: [...TRAJECTORY_NON_CLAIMS],
|
|
599
|
+
};
|
|
600
|
+
return { ...body, digest: digest(body) };
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return { auditAiReadiness, summarizeAiTrajectory };
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
module.exports = {
|
|
607
|
+
AI_VISIBILITY_CATALOG: CATALOG,
|
|
608
|
+
AI_REFERRAL_SCHEMA,
|
|
609
|
+
AI_CRAWLER_SCHEMA,
|
|
610
|
+
AI_READINESS_SCHEMA,
|
|
611
|
+
AI_TRAJECTORY_SCHEMA,
|
|
612
|
+
AiVisibilityError,
|
|
613
|
+
classifyAiReferral,
|
|
614
|
+
classifyAiCrawler,
|
|
615
|
+
createAiVisibilityApi,
|
|
616
|
+
};
|
package/js/discovery-evidence.js
CHANGED
|
@@ -20,8 +20,9 @@ const BOTS = [
|
|
|
20
20
|
["GPTBot", "OpenAI"],
|
|
21
21
|
["OAI-SearchBot", "OpenAI"],
|
|
22
22
|
["ChatGPT-User", "OpenAI"],
|
|
23
|
-
["
|
|
23
|
+
["Claude-SearchBot", "Anthropic"],
|
|
24
24
|
["Claude-User", "Anthropic"],
|
|
25
|
+
["ClaudeBot", "Anthropic"],
|
|
25
26
|
["PerplexityBot", "Perplexity"],
|
|
26
27
|
["Perplexity-User", "Perplexity"],
|
|
27
28
|
["Googlebot", "Google"],
|
package/js/discovery.d.ts
CHANGED
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
export {
|
|
2
|
+
AI_CRAWLER_SCHEMA,
|
|
3
|
+
AI_READINESS_SCHEMA,
|
|
4
|
+
AI_REFERRAL_SCHEMA,
|
|
5
|
+
AI_TRAJECTORY_SCHEMA,
|
|
6
|
+
AI_VISIBILITY_CATALOG,
|
|
7
|
+
AiCrawler,
|
|
8
|
+
AiCrawlerPurpose,
|
|
9
|
+
AiReadinessCheck,
|
|
10
|
+
AiReadinessReport,
|
|
11
|
+
AiReferral,
|
|
12
|
+
AiReferralRow,
|
|
13
|
+
AiTrajectory,
|
|
14
|
+
AiTrajectoryOperator,
|
|
15
|
+
AiTrajectorySignal,
|
|
16
|
+
AiTrajectoryStage,
|
|
17
|
+
AiVisibilityCatalog,
|
|
18
|
+
AiVisibilityError,
|
|
19
|
+
auditAiReadiness,
|
|
20
|
+
classifyAiCrawler,
|
|
21
|
+
classifyAiReferral,
|
|
22
|
+
summarizeAiTrajectory,
|
|
2
23
|
A2AAgentCardSignature,
|
|
3
24
|
A2AInterface,
|
|
4
25
|
A2AJwks,
|
package/js/discovery.js
CHANGED
|
@@ -17,6 +17,16 @@ module.exports = {
|
|
|
17
17
|
normalizeEngineObservation: impl.engineObservationApi.normalizeEngineObservation,
|
|
18
18
|
summarizeEngineObservations: impl.engineObservationApi.summarizeEngineObservations,
|
|
19
19
|
summarizeCohortVisibility: impl.engineObservationApi.summarizeCohortVisibility,
|
|
20
|
+
AI_VISIBILITY_CATALOG: impl.aiVisibility.AI_VISIBILITY_CATALOG,
|
|
21
|
+
AI_REFERRAL_SCHEMA: impl.aiVisibility.AI_REFERRAL_SCHEMA,
|
|
22
|
+
AI_CRAWLER_SCHEMA: impl.aiVisibility.AI_CRAWLER_SCHEMA,
|
|
23
|
+
AI_READINESS_SCHEMA: impl.aiVisibility.AI_READINESS_SCHEMA,
|
|
24
|
+
AI_TRAJECTORY_SCHEMA: impl.aiVisibility.AI_TRAJECTORY_SCHEMA,
|
|
25
|
+
AiVisibilityError: impl.aiVisibility.AiVisibilityError,
|
|
26
|
+
classifyAiReferral: impl.aiVisibility.classifyAiReferral,
|
|
27
|
+
classifyAiCrawler: impl.aiVisibility.classifyAiCrawler,
|
|
28
|
+
auditAiReadiness: impl.aiVisibilityApi.auditAiReadiness,
|
|
29
|
+
summarizeAiTrajectory: impl.aiVisibilityApi.summarizeAiTrajectory,
|
|
20
30
|
createSovereignCard: impl.createSovereignCard,
|
|
21
31
|
createTwin3AgentCard: impl.createTwin3AgentCard,
|
|
22
32
|
createAgentCard: impl.createAgentCard,
|
package/js/discovery.mjs
CHANGED
|
@@ -14,6 +14,16 @@ export const EngineObservationError = api.EngineObservationError;
|
|
|
14
14
|
export const normalizeEngineObservation = api.normalizeEngineObservation;
|
|
15
15
|
export const summarizeEngineObservations = api.summarizeEngineObservations;
|
|
16
16
|
export const summarizeCohortVisibility = api.summarizeCohortVisibility;
|
|
17
|
+
export const AI_VISIBILITY_CATALOG = api.AI_VISIBILITY_CATALOG;
|
|
18
|
+
export const AI_REFERRAL_SCHEMA = api.AI_REFERRAL_SCHEMA;
|
|
19
|
+
export const AI_CRAWLER_SCHEMA = api.AI_CRAWLER_SCHEMA;
|
|
20
|
+
export const AI_READINESS_SCHEMA = api.AI_READINESS_SCHEMA;
|
|
21
|
+
export const AI_TRAJECTORY_SCHEMA = api.AI_TRAJECTORY_SCHEMA;
|
|
22
|
+
export const AiVisibilityError = api.AiVisibilityError;
|
|
23
|
+
export const classifyAiReferral = api.classifyAiReferral;
|
|
24
|
+
export const classifyAiCrawler = api.classifyAiCrawler;
|
|
25
|
+
export const auditAiReadiness = api.auditAiReadiness;
|
|
26
|
+
export const summarizeAiTrajectory = api.summarizeAiTrajectory;
|
|
17
27
|
export const createSovereignCard = api.createSovereignCard;
|
|
18
28
|
export const createTwin3AgentCard = api.createTwin3AgentCard;
|
|
19
29
|
export const createAgentCard = api.createAgentCard;
|
package/js/index.js
CHANGED
|
@@ -45,6 +45,9 @@ module.exports = {
|
|
|
45
45
|
normalizeEngineObservation: discovery.normalizeEngineObservation,
|
|
46
46
|
summarizeEngineObservations: discovery.summarizeEngineObservations,
|
|
47
47
|
summarizeCohortVisibility: discovery.summarizeCohortVisibility,
|
|
48
|
+
AI_VISIBILITY_CATALOG: discovery.AI_VISIBILITY_CATALOG, AiVisibilityError: discovery.AiVisibilityError,
|
|
49
|
+
AI_REFERRAL_SCHEMA: discovery.AI_REFERRAL_SCHEMA, AI_CRAWLER_SCHEMA: discovery.AI_CRAWLER_SCHEMA, AI_READINESS_SCHEMA: discovery.AI_READINESS_SCHEMA, AI_TRAJECTORY_SCHEMA: discovery.AI_TRAJECTORY_SCHEMA,
|
|
50
|
+
classifyAiReferral: discovery.classifyAiReferral, classifyAiCrawler: discovery.classifyAiCrawler, auditAiReadiness: discovery.auditAiReadiness, summarizeAiTrajectory: discovery.summarizeAiTrajectory,
|
|
48
51
|
AuthorizationLeaseError: shield.AuthorizationLeaseError,
|
|
49
52
|
verifyAuthorizationLease: shield.verifyAuthorizationLease,
|
|
50
53
|
leaseClearanceContext: shield.leaseClearanceContext,
|
package/js/index.mjs
CHANGED
|
@@ -246,6 +246,16 @@ export const signEnvelopeWithSigner = api.signEnvelopeWithSigner;
|
|
|
246
246
|
export const summarizeCohortVisibility = api.summarizeCohortVisibility;
|
|
247
247
|
export const summarizeDiscoveryEvidence = api.summarizeDiscoveryEvidence;
|
|
248
248
|
export const summarizeEngineObservations = api.summarizeEngineObservations;
|
|
249
|
+
export const AI_VISIBILITY_CATALOG = api.AI_VISIBILITY_CATALOG;
|
|
250
|
+
export const AI_REFERRAL_SCHEMA = api.AI_REFERRAL_SCHEMA;
|
|
251
|
+
export const AI_CRAWLER_SCHEMA = api.AI_CRAWLER_SCHEMA;
|
|
252
|
+
export const AI_READINESS_SCHEMA = api.AI_READINESS_SCHEMA;
|
|
253
|
+
export const AI_TRAJECTORY_SCHEMA = api.AI_TRAJECTORY_SCHEMA;
|
|
254
|
+
export const AiVisibilityError = api.AiVisibilityError;
|
|
255
|
+
export const classifyAiReferral = api.classifyAiReferral;
|
|
256
|
+
export const classifyAiCrawler = api.classifyAiCrawler;
|
|
257
|
+
export const auditAiReadiness = api.auditAiReadiness;
|
|
258
|
+
export const summarizeAiTrajectory = api.summarizeAiTrajectory;
|
|
249
259
|
export const taskTrustLine = api.taskTrustLine;
|
|
250
260
|
export const toolAuthorityMatches = api.toolAuthorityMatches;
|
|
251
261
|
export const unresolvedPropositions = api.unresolvedPropositions;
|
package/js/internal/discovery.js
CHANGED
|
@@ -5,6 +5,7 @@ const trust = require("./trust.js");
|
|
|
5
5
|
|
|
6
6
|
const discoveryEvidence = require("../discovery-evidence.js");
|
|
7
7
|
const engineObservation = require("../engine-observation.js");
|
|
8
|
+
const aiVisibility = require("../ai-visibility.js");
|
|
8
9
|
const { validId } = require("../id-format.js");
|
|
9
10
|
const SOVEREIGN_CARD_SCHEMA = "twin3-discovery-sovereign-card-v1";
|
|
10
11
|
const CARD_CANONICALIZATION = "rfc8785";
|
|
@@ -1039,28 +1040,60 @@ function parseX402DnsTxt(records, host) {
|
|
|
1039
1040
|
scheme: (tags.scheme || "").split(",").filter(Boolean),
|
|
1040
1041
|
};
|
|
1041
1042
|
}
|
|
1042
|
-
function
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1043
|
+
function x402V4IsUnsafe(a, b, c) {
|
|
1044
|
+
return a === 0 || a === 10 || a === 127 || (a === 100 && b >= 64 && b <= 127) || (a === 169 && b === 254) || (a === 172 && b >= 16 && b <= 31)
|
|
1045
|
+
|| (a === 192 && b === 0 && c === 0) || (a === 192 && b === 0 && c === 2) || (a === 192 && b === 168)
|
|
1046
|
+
|| (a === 198 && (b === 18 || b === 19)) || (a === 198 && b === 51 && c === 100) || (a === 203 && b === 0 && c === 113)
|
|
1047
|
+
|| a >= 224;
|
|
1048
|
+
}
|
|
1049
|
+
function x402ParseV4(text) {
|
|
1046
1050
|
const v4 = text.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
1047
|
-
if (v4)
|
|
1048
|
-
|
|
1049
|
-
|
|
1051
|
+
if (!v4)
|
|
1052
|
+
return null;
|
|
1053
|
+
const octets = v4.slice(1).map(Number);
|
|
1054
|
+
if (octets.some((n) => n > 255))
|
|
1055
|
+
core.fail("x402_fetch_address_invalid");
|
|
1056
|
+
return octets;
|
|
1057
|
+
}
|
|
1058
|
+
function x402ParseV6(text) {
|
|
1059
|
+
if (!/^[0-9a-f:.]+$/.test(text) || (text.match(/::/g) || []).length > 1)
|
|
1060
|
+
core.fail("x402_fetch_address_invalid");
|
|
1061
|
+
let groups = text.split(":");
|
|
1062
|
+
const tail = groups[groups.length - 1];
|
|
1063
|
+
if (tail.includes(".")) {
|
|
1064
|
+
const octets = x402ParseV4(tail);
|
|
1065
|
+
if (!octets)
|
|
1050
1066
|
core.fail("x402_fetch_address_invalid");
|
|
1051
|
-
|
|
1052
|
-
|| (a === 192 && b === 0 && Number(v4[3]) === 0) || (a === 192 && b === 0 && Number(v4[3]) === 2) || (a === 192 && b === 168)
|
|
1053
|
-
|| (a === 198 && (b === 18 || b === 19)) || (a === 198 && b === 51 && Number(v4[3]) === 100) || (a === 203 && b === 0 && Number(v4[3]) === 113)
|
|
1054
|
-
|| a >= 224;
|
|
1067
|
+
groups = [...groups.slice(0, -1), ((octets[0] << 8) | octets[1]).toString(16), ((octets[2] << 8) | octets[3]).toString(16)];
|
|
1055
1068
|
}
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
const
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1069
|
+
const gap = groups.indexOf("");
|
|
1070
|
+
if (text.includes("::")) {
|
|
1071
|
+
const head = text.startsWith("::") ? [] : groups.slice(0, gap);
|
|
1072
|
+
const rest = groups.slice(groups.lastIndexOf("") + 1);
|
|
1073
|
+
if (head.length + rest.length > 7)
|
|
1074
|
+
core.fail("x402_fetch_address_invalid");
|
|
1075
|
+
groups = [...head, ...Array(8 - head.length - rest.length).fill("0"), ...rest];
|
|
1062
1076
|
}
|
|
1063
|
-
|
|
1077
|
+
if (groups.length !== 8 || groups.some((group) => !/^[0-9a-f]{1,4}$/.test(group)))
|
|
1078
|
+
core.fail("x402_fetch_address_invalid");
|
|
1079
|
+
return groups.map((group) => parseInt(group, 16));
|
|
1080
|
+
}
|
|
1081
|
+
function x402AddressIsUnsafe(address) {
|
|
1082
|
+
const text = String(address).trim().toLowerCase();
|
|
1083
|
+
const v4 = x402ParseV4(text);
|
|
1084
|
+
if (v4)
|
|
1085
|
+
return x402V4IsUnsafe(v4[0], v4[1], v4[2]);
|
|
1086
|
+
if (!text.includes(":"))
|
|
1087
|
+
core.fail("x402_fetch_address_invalid");
|
|
1088
|
+
const h = x402ParseV6(text);
|
|
1089
|
+
if (h.slice(0, 6).every((n) => n === 0))
|
|
1090
|
+
return true;
|
|
1091
|
+
if (h.slice(0, 5).every((n) => n === 0) && h[5] === 0xffff)
|
|
1092
|
+
return true;
|
|
1093
|
+
if ((h[0] === 0x64 && h[1] === 0xff9b) || h[0] === 0x2002)
|
|
1094
|
+
return true;
|
|
1095
|
+
return h[0] < 0x2000 || h[0] > 0x3fff || (h[0] === 0x2001 && (h[1] < 0x0200 || h[1] === 0x0db8))
|
|
1096
|
+
|| (h[0] === 0x3fff && h[1] < 0x1000);
|
|
1064
1097
|
}
|
|
1065
1098
|
function checkX402FetchTarget(url, host, { resolvedAddresses = [], hop = 0, maxRedirects = X402_FETCH_BOUNDS.max_redirects } = {}) {
|
|
1066
1099
|
const safeHost = x402Host(host);
|
|
@@ -1244,10 +1277,12 @@ function canStrangerDecide(declaration) {
|
|
|
1244
1277
|
}
|
|
1245
1278
|
const discoveryEvidenceApi = discoveryEvidence.createDiscoveryEvidenceApi(core.canonicalJsonBuffer);
|
|
1246
1279
|
const engineObservationApi = engineObservation.createEngineObservationApi(core.canonicalJsonBuffer);
|
|
1280
|
+
const aiVisibilityApi = aiVisibility.createAiVisibilityApi(core.canonicalJsonBuffer);
|
|
1247
1281
|
|
|
1248
1282
|
module.exports = {
|
|
1249
1283
|
discoveryEvidence,
|
|
1250
1284
|
engineObservation,
|
|
1285
|
+
aiVisibility,
|
|
1251
1286
|
validId,
|
|
1252
1287
|
SOVEREIGN_CARD_SCHEMA,
|
|
1253
1288
|
CARD_CANONICALIZATION,
|
|
@@ -1367,4 +1402,5 @@ module.exports = {
|
|
|
1367
1402
|
canStrangerDecide,
|
|
1368
1403
|
discoveryEvidenceApi,
|
|
1369
1404
|
engineObservationApi,
|
|
1405
|
+
aiVisibilityApi,
|
|
1370
1406
|
};
|
package/js/runtime-session.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const contracts = require("./runtime.js");
|
|
4
|
+
const { occurrenceContext, occurrenceId } = require("./shield.js");
|
|
4
5
|
|
|
5
6
|
const copy = value => JSON.parse(JSON.stringify(value));
|
|
6
7
|
const fail = code => { throw new contracts.RuntimeContractError(code); };
|
|
@@ -23,15 +24,17 @@ function taskSnapshot(state, replayed = false) {
|
|
|
23
24
|
return copy({ task_id: state.task_id, run_id: state.run_id, request_id: state.request_id, revision: state.revision,
|
|
24
25
|
execution_status: state.execution_status, approval_status: state.approval_status, effect_outcome: state.effect_outcome,
|
|
25
26
|
reply: state.reply, error: state.error, operation_id: state.operation_id, result: state.result,
|
|
26
|
-
preview: state.preview, available_actions: availableActions, events: state.events, cursor: state.revision, replayed
|
|
27
|
+
preview: state.preview, available_actions: availableActions, events: state.events, cursor: state.revision, replayed,
|
|
28
|
+
...(state.reconciliation ? { reconciliation: state.reconciliation } : {}) });
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
class AgentRuntime {
|
|
30
|
-
constructor({ store, resolvePrincipal, resolveContext, engine, resolveSkill, resolveExecution, evaluateAction = async () => "ask", clock = () => Math.floor(Date.now() / 1000), timeoutMs = 30000 }) {
|
|
32
|
+
constructor({ store, resolvePrincipal, resolveContext, engine, resolveSkill, resolveExecution, resolveOutcome = null, evaluateAction = async () => "ask", clock = () => Math.floor(Date.now() / 1000), timeoutMs = 30000 }) {
|
|
31
33
|
if (!store || typeof store.get !== "function" || typeof store.transact !== "function"
|
|
32
34
|
|| ![resolvePrincipal, resolveContext, engine, resolveSkill, resolveExecution, evaluateAction, clock].every(value => typeof value === "function")) fail("runtime_ports_required");
|
|
33
35
|
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 300000) fail("runtime_timeout_invalid");
|
|
34
|
-
|
|
36
|
+
if (resolveOutcome !== null && typeof resolveOutcome !== "function") fail("runtime_ports_required");
|
|
37
|
+
Object.assign(this, { store, resolvePrincipal, resolveContext, engine, resolveSkill, resolveExecution, resolveOutcome, evaluateAction, clock, timeoutMs });
|
|
35
38
|
this.controllers = new Map();
|
|
36
39
|
}
|
|
37
40
|
|
|
@@ -253,6 +256,75 @@ class AgentRuntime {
|
|
|
253
256
|
});
|
|
254
257
|
}
|
|
255
258
|
|
|
259
|
+
async reconcile(caller, { task_id: taskId, expected_revision: expectedRevision, request_id: requestId }) {
|
|
260
|
+
boundedText(requestId, 128);
|
|
261
|
+
if (!Number.isSafeInteger(expectedRevision)) fail("runtime_request_invalid");
|
|
262
|
+
if (!this.resolveOutcome) fail("runtime_reconciliation_unavailable");
|
|
263
|
+
const principal = await this.principal(caller), key = this.key(principal, taskId);
|
|
264
|
+
const command = contracts.runtimeDigest({ operation: "reconcile", task_id: taskId, expected_revision: expectedRevision, request_id: requestId });
|
|
265
|
+
const state = await this.store.get(key);
|
|
266
|
+
if (!state) fail("runtime_task_not_found");
|
|
267
|
+
if (Object.hasOwn(state.commands, requestId)) {
|
|
268
|
+
if (state.commands[requestId] !== command) fail("runtime_request_conflict");
|
|
269
|
+
if (contracts.runtimeDigest(await this.principal(caller)) !== contracts.runtimeDigest(principal)) fail("runtime_authority_changed");
|
|
270
|
+
return taskSnapshot(state, true);
|
|
271
|
+
}
|
|
272
|
+
if (state.revision !== expectedRevision) fail("runtime_revision_conflict");
|
|
273
|
+
if (state.execution_status !== "outcome_unknown" || state.effect_outcome !== "unknown" || !state.preview || !state.context) fail("runtime_reconciliation_not_available");
|
|
274
|
+
const preview = contracts.createExecutionPreview({ principal: state.principal, context: state.context, actions: state.preview.actions,
|
|
275
|
+
task_id: taskId, request_id: state.preview.request_id, revision: state.preview.revision, expires_at: state.preview.expires_at });
|
|
276
|
+
if (contracts.runtimeDigest(preview) !== contracts.runtimeDigest(state.preview) || preview.actions.length !== 1) fail("runtime_preview_changed");
|
|
277
|
+
const original = state.principal;
|
|
278
|
+
if (["kind", "tenant", "subject", "owner", "claim_generation"].some(field => original[field] !== principal[field])) fail("runtime_authority_changed");
|
|
279
|
+
const operationId = occurrenceId(occurrenceContext({ tenant: original.tenant, taskId, plan: preview.actions, index: 0, epoch: original.epoch }));
|
|
280
|
+
if (state.operation_id && state.operation_id !== operationId) fail("runtime_operation_changed");
|
|
281
|
+
const identity = { operation_id: operationId, action_digest: contracts.runtimeDigest(preview.actions[0]),
|
|
282
|
+
context_digest: contracts.runtimeDigest(state.context), principal_digest: contracts.runtimeDigest(original) };
|
|
283
|
+
const controller = new AbortController();
|
|
284
|
+
let timer;
|
|
285
|
+
const returned = await Promise.race([
|
|
286
|
+
Promise.resolve().then(() => this.resolveOutcome(caller, copy(identity), controller.signal)),
|
|
287
|
+
new Promise((_, reject) => { timer = setTimeout(() => { controller.abort(); reject(new contracts.RuntimeContractError("runtime_reconciliation_timeout")); }, this.timeoutMs); })
|
|
288
|
+
]).finally(() => clearTimeout(timer));
|
|
289
|
+
contracts.runtimeDigest(returned);
|
|
290
|
+
const outcome = copy(returned);
|
|
291
|
+
if (!outcome || !["confirmed", "not_submitted", "unknown"].includes(outcome.effect_outcome)
|
|
292
|
+
|| Object.keys(outcome).some(field => ![...Object.keys(identity), "effect_outcome", "evidence_ref", "result"].includes(field))
|
|
293
|
+
|| Object.entries(identity).some(([field, value]) => outcome[field] !== value)) fail("runtime_reconciliation_invalid");
|
|
294
|
+
if (contracts.runtimeDigest(await this.principal(caller)) !== contracts.runtimeDigest(principal)) fail("runtime_authority_changed");
|
|
295
|
+
if (outcome.effect_outcome === "unknown") {
|
|
296
|
+
const latest = await this.store.get(key);
|
|
297
|
+
if (contracts.runtimeDigest(await this.principal(caller)) !== contracts.runtimeDigest(principal)) fail("runtime_authority_changed");
|
|
298
|
+
return taskSnapshot(latest);
|
|
299
|
+
}
|
|
300
|
+
const evidence = boundedText(outcome.evidence_ref, 512);
|
|
301
|
+
if (/[\u0000-\u001f\u007f]/u.test(evidence) || outcome.effect_outcome === "confirmed" && !Object.hasOwn(outcome, "result")) fail("runtime_reconciliation_invalid");
|
|
302
|
+
const result = outcome.effect_outcome === "confirmed" ? outcome.result : null;
|
|
303
|
+
contracts.runtimeDigest(result);
|
|
304
|
+
if (Buffer.byteLength(JSON.stringify(result), "utf8") > 65536) fail("runtime_payload_too_large");
|
|
305
|
+
return this.store.transact(key, async current => {
|
|
306
|
+
if (!current) fail("runtime_task_not_found");
|
|
307
|
+
if (Object.hasOwn(current.commands, requestId)) {
|
|
308
|
+
if (current.commands[requestId] !== command) fail("runtime_request_conflict");
|
|
309
|
+
return { state: current, value: taskSnapshot(current, true) };
|
|
310
|
+
}
|
|
311
|
+
if (current.revision !== expectedRevision || current.execution_status !== "outcome_unknown"
|
|
312
|
+
|| contracts.runtimeDigest(current.preview) !== contracts.runtimeDigest(preview)
|
|
313
|
+
|| contracts.runtimeDigest(current.context) !== identity.context_digest
|
|
314
|
+
|| contracts.runtimeDigest(current.principal) !== identity.principal_digest) fail("runtime_revision_conflict");
|
|
315
|
+
if (contracts.runtimeDigest(await this.principal(caller)) !== contracts.runtimeDigest(principal)) fail("runtime_authority_changed");
|
|
316
|
+
current.commands = { ...current.commands, [requestId]: command };
|
|
317
|
+
current.operation_id = operationId;
|
|
318
|
+
current.effect_outcome = outcome.effect_outcome;
|
|
319
|
+
current.execution_status = outcome.effect_outcome === "confirmed" ? "completed" : "blocked";
|
|
320
|
+
current.result = result;
|
|
321
|
+
current.error = outcome.effect_outcome === "confirmed" ? null : "runtime_action_not_submitted";
|
|
322
|
+
current.reconciliation = { evidence_ref: evidence, ...identity };
|
|
323
|
+
appendEvent(current, "reconciled", this.clock());
|
|
324
|
+
return { state: current, value: taskSnapshot(current) };
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
256
328
|
async cancel(caller, { task_id: taskId, expected_revision: expectedRevision, request_id: requestId }) {
|
|
257
329
|
boundedText(requestId, 128);
|
|
258
330
|
const principal = await this.principal(caller);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "twin3-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Universal Capability Motherboard for Autonomous AI Agents and Self-Sovereign Humans (JS/Node runtime)",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./js/index.js",
|
|
@@ -83,9 +83,11 @@
|
|
|
83
83
|
"js/index.js",
|
|
84
84
|
"js/discovery-evidence.js",
|
|
85
85
|
"js/engine-observation.js",
|
|
86
|
+
"js/ai-visibility.js",
|
|
86
87
|
"js/id-format.js",
|
|
87
88
|
"twin3/discovery/id-format.v1.json",
|
|
88
89
|
"twin3/discovery/evidence-i18n.json",
|
|
90
|
+
"twin3/discovery/ai-visibility-catalog.json",
|
|
89
91
|
"js/comm/counterparty.js",
|
|
90
92
|
"js/comm/completion.js",
|
|
91
93
|
"twin3/comm/task-completion-command.schema.json",
|
|
@@ -138,7 +140,7 @@
|
|
|
138
140
|
"publishConfig": {
|
|
139
141
|
"access": "public",
|
|
140
142
|
"registry": "https://registry.npmjs.org/",
|
|
141
|
-
"tag": "
|
|
143
|
+
"tag": "latest"
|
|
142
144
|
},
|
|
143
145
|
"scripts": {
|
|
144
146
|
"test": "node --test tests/*.test.js"
|
package/runtime.d.ts
CHANGED
|
@@ -115,7 +115,19 @@ export interface RuntimeTaskSnapshot {
|
|
|
115
115
|
events: { event_id: string; task_id: string; run_id: string; cursor: number; type: string; at: number }[];
|
|
116
116
|
cursor: number;
|
|
117
117
|
replayed: boolean;
|
|
118
|
+
reconciliation?: RuntimeReconciliationIdentity & { evidence_ref: string };
|
|
118
119
|
}
|
|
120
|
+
export interface RuntimeReconciliationIdentity {
|
|
121
|
+
operation_id: string;
|
|
122
|
+
action_digest: string;
|
|
123
|
+
context_digest: string;
|
|
124
|
+
principal_digest: string;
|
|
125
|
+
}
|
|
126
|
+
export type RuntimeReconciliationOutcome = RuntimeReconciliationIdentity & (
|
|
127
|
+
| { effect_outcome: "unknown" }
|
|
128
|
+
| { effect_outcome: "not_submitted"; evidence_ref: string }
|
|
129
|
+
| { effect_outcome: "confirmed"; evidence_ref: string; result: JsonValue }
|
|
130
|
+
);
|
|
119
131
|
export interface RuntimeTaskStore {
|
|
120
132
|
get(key: string): Promise<unknown>;
|
|
121
133
|
transact<T>(key: string, update: (state: unknown) => Promise<{ state: unknown; value: T }> | { state: unknown; value: T }): Promise<T>;
|
|
@@ -129,6 +141,7 @@ export declare class AgentRuntime<Caller = unknown> {
|
|
|
129
141
|
engine(input: { intent: string; context: ContextEnvelope; principal: PrincipalContext; request_id: string; signal: AbortSignal }): Promise<{ reply: string; actions: unknown[] }>;
|
|
130
142
|
resolveSkill(caller: Caller, skillId: string): Promise<{ ready: boolean; manifest: SkillManifest } | null>;
|
|
131
143
|
resolveExecution(caller: Caller, state: unknown): Promise<{ gate: RuntimeGate; broker: RuntimeBroker } | null>;
|
|
144
|
+
resolveOutcome?(caller: Caller, identity: RuntimeReconciliationIdentity, signal: AbortSignal): Promise<RuntimeReconciliationOutcome>;
|
|
132
145
|
evaluateAction?(caller: Caller, action: SkillAction, context: ContextEnvelope): Promise<"allow" | "ask" | "deny">;
|
|
133
146
|
clock?: () => number;
|
|
134
147
|
timeoutMs?: number;
|
|
@@ -137,5 +150,6 @@ export declare class AgentRuntime<Caller = unknown> {
|
|
|
137
150
|
get(caller: Caller, taskId: string): Promise<RuntimeTaskSnapshot>;
|
|
138
151
|
decide(caller: Caller, input: { task_id: string; preview_id: string; expected_revision: number; decision: "approve" | "deny"; request_id: string }): Promise<RuntimeTaskSnapshot>;
|
|
139
152
|
execute(caller: Caller, input: { task_id: string; expected_revision: number; request_id: string }): Promise<RuntimeTaskSnapshot>;
|
|
153
|
+
reconcile(caller: Caller, input: { task_id: string; expected_revision: number; request_id: string }): Promise<RuntimeTaskSnapshot>;
|
|
140
154
|
cancel(caller: Caller, input: { task_id: string; expected_revision: number; request_id: string }): Promise<RuntimeTaskSnapshot>;
|
|
141
155
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"checked_on": "2026-10-27",
|
|
4
|
+
"provenance": [
|
|
5
|
+
{
|
|
6
|
+
"name": "ansvisor",
|
|
7
|
+
"url": "https://github.com/ansvisor/ansvisor",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"used": ["server/src/lib/ai-sources.json v3", "server/src/lib/ai-bots.json v1", "server/src/config/visibility-score.js weights"]
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"name": "claude-seo",
|
|
13
|
+
"url": "https://github.com/AgriciDaniel/claude-seo/tree/6be93f45064ffbadced572b5d5c95f55fd263af0",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"used": ["scripts/agentic_check.py AI_AGENTS and RFC 9309 group selection", "skills/seo-geo crawler claim table"]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"name": "GEOFlow",
|
|
19
|
+
"url": "https://github.com/yaojingang/GEOFlow",
|
|
20
|
+
"license": "AGPL-3.0",
|
|
21
|
+
"used": ["publish quality-gate concept only; no code or data copied"]
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"referral_sources": [
|
|
25
|
+
{ "key": "chatgpt.com", "operator": "OpenAI", "hosts": ["chatgpt.com", "chat.openai.com"], "aliases": ["chatgpt", "openai", "openai.com"], "surfaces": [{ "key": "shopping", "medium": "feed" }] },
|
|
26
|
+
{ "key": "perplexity.ai", "operator": "Perplexity", "hosts": ["perplexity.ai"], "aliases": ["perplexity"] },
|
|
27
|
+
{ "key": "claude.ai", "operator": "Anthropic", "hosts": ["claude.ai"], "aliases": ["claude"] },
|
|
28
|
+
{ "key": "gemini.google.com", "operator": "Google", "hosts": ["gemini.google.com", "bard.google.com"], "aliases": ["gemini", "bard"] },
|
|
29
|
+
{ "key": "copilot.microsoft.com", "operator": "Microsoft", "hosts": ["copilot.microsoft.com", "copilot.com"], "aliases": ["copilot"] },
|
|
30
|
+
{ "key": "grok.com", "operator": "xAI", "hosts": ["grok.com"], "aliases": ["grok"] },
|
|
31
|
+
{ "key": "deepseek.com", "operator": "DeepSeek", "hosts": ["deepseek.com"], "aliases": ["deepseek"] },
|
|
32
|
+
{ "key": "meta.ai", "operator": "Meta", "hosts": ["meta.ai"], "aliases": [] },
|
|
33
|
+
{ "key": "you.com", "operator": "You.com", "hosts": ["you.com"], "aliases": [] },
|
|
34
|
+
{ "key": "phind.com", "operator": "Phind", "hosts": ["phind.com"], "aliases": [] },
|
|
35
|
+
{ "key": "poe.com", "operator": "Quora", "hosts": ["poe.com"], "aliases": [] },
|
|
36
|
+
{ "key": "labs.google", "operator": "Google", "hosts": ["labs.google"], "aliases": [] },
|
|
37
|
+
{ "key": "bing.com", "operator": "Microsoft", "hosts": ["bing.com"], "aliases": [], "referrer_path": "/ck/a", "utm": false }
|
|
38
|
+
],
|
|
39
|
+
"crawlers": [
|
|
40
|
+
{ "token": "GPTBot", "operator": "OpenAI", "purpose": "training", "robots": "honours", "user_agent": true },
|
|
41
|
+
{ "token": "OAI-SearchBot", "operator": "OpenAI", "purpose": "search", "robots": "honours", "user_agent": true, "claim": "citable_in_chatgpt_search" },
|
|
42
|
+
{ "token": "ChatGPT-User", "operator": "OpenAI", "purpose": "user", "robots": "may_not_apply", "user_agent": true },
|
|
43
|
+
{ "token": "ClaudeBot", "operator": "Anthropic", "purpose": "training", "robots": "honours", "user_agent": true },
|
|
44
|
+
{ "token": "Claude-SearchBot", "operator": "Anthropic", "purpose": "search", "robots": "honours", "user_agent": true, "claim": "citable_in_claude_search" },
|
|
45
|
+
{ "token": "Claude-User", "operator": "Anthropic", "purpose": "user", "robots": "honours", "user_agent": true },
|
|
46
|
+
{ "token": "PerplexityBot", "operator": "Perplexity", "purpose": "search", "robots": "honours", "user_agent": true, "claim": "citable_in_perplexity" },
|
|
47
|
+
{ "token": "Perplexity-User", "operator": "Perplexity", "purpose": "user", "robots": "generally_ignores", "user_agent": true },
|
|
48
|
+
{ "token": "Googlebot", "operator": "Google", "purpose": "search", "robots": "honours", "user_agent": true, "claim": "eligible_for_google_ai_overviews" },
|
|
49
|
+
{ "token": "Google-Extended", "operator": "Google", "purpose": "control", "robots": "honours", "user_agent": false },
|
|
50
|
+
{ "token": "Google-Agent", "operator": "Google", "purpose": "user", "robots": "generally_ignores", "user_agent": true },
|
|
51
|
+
{ "token": "Google-CloudVertexBot", "operator": "Google", "purpose": "user", "robots": "honours", "user_agent": true },
|
|
52
|
+
{ "token": "Bingbot", "operator": "Microsoft", "purpose": "search", "robots": "honours", "user_agent": true, "claim": "eligible_for_copilot_and_bing" },
|
|
53
|
+
{ "token": "Applebot", "operator": "Apple", "purpose": "search", "robots": "honours", "user_agent": true },
|
|
54
|
+
{ "token": "Applebot-Extended", "operator": "Apple", "purpose": "control", "robots": "honours", "user_agent": false },
|
|
55
|
+
{ "token": "CCBot", "operator": "Common Crawl", "purpose": "training", "robots": "honours", "user_agent": true },
|
|
56
|
+
{ "token": "A2A-Client", "operator": "A2A", "purpose": "agent", "robots": "not_applicable", "user_agent": true },
|
|
57
|
+
{ "token": "WebMCP-Client", "operator": "WebMCP", "purpose": "agent", "robots": "not_applicable", "user_agent": true }
|
|
58
|
+
],
|
|
59
|
+
"stages": [
|
|
60
|
+
{ "key": "crawled", "en": "Fetched by an AI crawler", "zh-TW": "被 AI 爬蟲抓取" },
|
|
61
|
+
{ "key": "search_indexed", "en": "Fetched by an AI search crawler", "zh-TW": "被 AI 搜尋爬蟲收錄" },
|
|
62
|
+
{ "key": "user_fetched", "en": "Fetched for a person's question", "zh-TW": "因使用者提問被即時讀取" },
|
|
63
|
+
{ "key": "retrieved", "en": "Retrieved in an AI answer run", "zh-TW": "在 AI 回答中被檢索" },
|
|
64
|
+
{ "key": "mentioned", "en": "Mentioned in an AI answer", "zh-TW": "在 AI 回答中被提及" },
|
|
65
|
+
{ "key": "cited", "en": "Cited as a source", "zh-TW": "被列為引用來源" },
|
|
66
|
+
{ "key": "clicked", "en": "Visited from an AI answer", "zh-TW": "從 AI 回答點擊進站" }
|
|
67
|
+
]
|
|
68
|
+
}
|