unbrowse 3.1.0 → 3.2.1

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.
Files changed (102) hide show
  1. package/dist/cli.js +454 -96
  2. package/dist/index.js +2 -6
  3. package/dist/mcp.js +695 -46
  4. package/dist/server.js +25994 -0
  5. package/package.json +1 -2
  6. package/vendor/kuri/manifest.json +1 -1
  7. package/runtime-src/agent-outcome.ts +0 -166
  8. package/runtime-src/analytics-session.ts +0 -55
  9. package/runtime-src/api/browse-index.ts +0 -317
  10. package/runtime-src/api/browse-session.ts +0 -572
  11. package/runtime-src/api/browse-submit-prereqs.ts +0 -48
  12. package/runtime-src/api/browse-submit.ts +0 -1184
  13. package/runtime-src/api/routes.ts +0 -1823
  14. package/runtime-src/auth/browser-cookies.ts +0 -423
  15. package/runtime-src/auth/index.ts +0 -535
  16. package/runtime-src/auth/runtime.ts +0 -116
  17. package/runtime-src/browser/index.ts +0 -659
  18. package/runtime-src/browser/types.ts +0 -41
  19. package/runtime-src/build-info.generated.ts +0 -6
  20. package/runtime-src/capture/index.ts +0 -1794
  21. package/runtime-src/capture/prefetch.ts +0 -95
  22. package/runtime-src/capture/rsc.ts +0 -45
  23. package/runtime-src/cli/shortcuts.ts +0 -273
  24. package/runtime-src/cli.ts +0 -1572
  25. package/runtime-src/client/graph-client.ts +0 -100
  26. package/runtime-src/client/index.ts +0 -1425
  27. package/runtime-src/debug-trace.ts +0 -18
  28. package/runtime-src/domain.ts +0 -38
  29. package/runtime-src/execution/index.ts +0 -3397
  30. package/runtime-src/execution/retry.ts +0 -46
  31. package/runtime-src/execution/robots.ts +0 -167
  32. package/runtime-src/execution/search-forms.ts +0 -188
  33. package/runtime-src/extraction/index.ts +0 -1507
  34. package/runtime-src/foundry/publish-bundle.ts +0 -392
  35. package/runtime-src/graph/agent-augment.ts +0 -315
  36. package/runtime-src/graph/index.ts +0 -1524
  37. package/runtime-src/graph/local-fixtures.ts +0 -393
  38. package/runtime-src/graph/local-harness.ts +0 -646
  39. package/runtime-src/graph/planner.ts +0 -411
  40. package/runtime-src/graph/session.ts +0 -294
  41. package/runtime-src/graph/trace-store.ts +0 -136
  42. package/runtime-src/index.ts +0 -24
  43. package/runtime-src/indexer/index.ts +0 -465
  44. package/runtime-src/intent-match.ts +0 -1515
  45. package/runtime-src/kuri/client.ts +0 -1839
  46. package/runtime-src/logger.ts +0 -30
  47. package/runtime-src/marketplace/index.ts +0 -103
  48. package/runtime-src/mcp.ts +0 -1747
  49. package/runtime-src/orchestrator/browser-agent.ts +0 -374
  50. package/runtime-src/orchestrator/dag-advisor.ts +0 -59
  51. package/runtime-src/orchestrator/dag-feedback.ts +0 -257
  52. package/runtime-src/orchestrator/first-pass-action.ts +0 -403
  53. package/runtime-src/orchestrator/index.ts +0 -4480
  54. package/runtime-src/orchestrator/passive-publish.ts +0 -187
  55. package/runtime-src/orchestrator/timing-economics.ts +0 -80
  56. package/runtime-src/payments/cascade.ts +0 -137
  57. package/runtime-src/payments/index.ts +0 -270
  58. package/runtime-src/payments/lobster-pay.ts +0 -182
  59. package/runtime-src/payments/wallet.ts +0 -98
  60. package/runtime-src/publish/review-context.ts +0 -93
  61. package/runtime-src/publish/sanitize.ts +0 -197
  62. package/runtime-src/publish/schema-review.ts +0 -192
  63. package/runtime-src/publish-admission.ts +0 -388
  64. package/runtime-src/ratelimit/index.ts +0 -23
  65. package/runtime-src/reverse-engineer/bundle-scanner.ts +0 -127
  66. package/runtime-src/reverse-engineer/description-prompt.ts +0 -213
  67. package/runtime-src/reverse-engineer/index.ts +0 -1551
  68. package/runtime-src/router.ts +0 -17
  69. package/runtime-src/routing-telemetry.ts +0 -395
  70. package/runtime-src/runtime/browser-access.ts +0 -11
  71. package/runtime-src/runtime/browser-auth.ts +0 -12
  72. package/runtime-src/runtime/browser-host.ts +0 -48
  73. package/runtime-src/runtime/lifecycle.ts +0 -17
  74. package/runtime-src/runtime/local-server.ts +0 -311
  75. package/runtime-src/runtime/paths.ts +0 -99
  76. package/runtime-src/runtime/setup.ts +0 -251
  77. package/runtime-src/runtime/supervisor.ts +0 -69
  78. package/runtime-src/runtime/update-hints.ts +0 -351
  79. package/runtime-src/server.ts +0 -100
  80. package/runtime-src/session-logs.ts +0 -142
  81. package/runtime-src/settings.ts +0 -221
  82. package/runtime-src/single-binary.ts +0 -143
  83. package/runtime-src/site-policy.ts +0 -54
  84. package/runtime-src/stale-cleanup-runner.ts +0 -144
  85. package/runtime-src/stale-cleanup.ts +0 -133
  86. package/runtime-src/telemetry-attribution.ts +0 -120
  87. package/runtime-src/telemetry.ts +0 -253
  88. package/runtime-src/template-params.ts +0 -141
  89. package/runtime-src/transform/drift.ts +0 -60
  90. package/runtime-src/transform/index.ts +0 -277
  91. package/runtime-src/types/index.ts +0 -1
  92. package/runtime-src/types/skill.ts +0 -912
  93. package/runtime-src/vault/index.ts +0 -196
  94. package/runtime-src/verification/auth-gate.ts +0 -8
  95. package/runtime-src/verification/candidates.ts +0 -27
  96. package/runtime-src/verification/index.ts +0 -120
  97. package/runtime-src/verification/matrix.ts +0 -30
  98. package/runtime-src/version.ts +0 -148
  99. package/runtime-src/workflow/artifact.ts +0 -161
  100. package/runtime-src/workflow/compile.ts +0 -808
  101. package/runtime-src/workflow/publish.ts +0 -225
  102. package/runtime-src/workflow/runtime.ts +0 -213
@@ -1,120 +0,0 @@
1
- export const TELEMETRY_ATTRIBUTION_KEYS = [
2
- "utm_source",
3
- "utm_medium",
4
- "utm_campaign",
5
- "utm_content",
6
- "utm_term",
7
- "utm_id",
8
- "gclid",
9
- "wbraid",
10
- "gbraid",
11
- "fbclid",
12
- "twclid",
13
- "ttclid",
14
- "msclkid",
15
- "li_fat_id",
16
- "referrer_host",
17
- "channel",
18
- "campaign_id",
19
- "campaign_name",
20
- "content_id",
21
- "content_type",
22
- "creative_id",
23
- "ad_id",
24
- "adset_id",
25
- "inferred_icp",
26
- "variant_id",
27
- "experiment_id",
28
- "icp",
29
- ] as const;
30
-
31
- export type TelemetryAttributionKey = (typeof TELEMETRY_ATTRIBUTION_KEYS)[number];
32
-
33
- export interface TelemetryAttribution {
34
- utm_source?: string;
35
- utm_medium?: string;
36
- utm_campaign?: string;
37
- utm_content?: string;
38
- utm_term?: string;
39
- utm_id?: string;
40
- gclid?: string;
41
- wbraid?: string;
42
- gbraid?: string;
43
- fbclid?: string;
44
- twclid?: string;
45
- ttclid?: string;
46
- msclkid?: string;
47
- li_fat_id?: string;
48
- referrer_host?: string;
49
- channel?: string;
50
- campaign_id?: string;
51
- campaign_name?: string;
52
- content_id?: string;
53
- content_type?: string;
54
- creative_id?: string;
55
- ad_id?: string;
56
- adset_id?: string;
57
- inferred_icp?: string;
58
- variant_id?: string;
59
- experiment_id?: string;
60
- icp?: string;
61
- }
62
-
63
- const MAX_ATTRIBUTION_VALUE_LENGTH = 160;
64
-
65
- export function sanitizeAttributionValue(value: unknown): string | undefined {
66
- if (typeof value !== "string") return undefined;
67
- const trimmed = value.trim();
68
- if (!trimmed) return undefined;
69
- return trimmed.slice(0, MAX_ATTRIBUTION_VALUE_LENGTH);
70
- }
71
-
72
- export function hasTelemetryAttribution(value: Partial<TelemetryAttribution> | null | undefined): boolean {
73
- if (!value) return false;
74
- return TELEMETRY_ATTRIBUTION_KEYS.some((key) => Boolean(sanitizeAttributionValue(value[key])));
75
- }
76
-
77
- export function sanitizeTelemetryAttribution(
78
- raw: Partial<Record<TelemetryAttributionKey, unknown>> | null | undefined,
79
- ): TelemetryAttribution | undefined {
80
- if (!raw) return undefined;
81
- const cleaned: TelemetryAttribution = {};
82
- for (const key of TELEMETRY_ATTRIBUTION_KEYS) {
83
- const value = sanitizeAttributionValue(raw[key]);
84
- if (value) cleaned[key] = value;
85
- }
86
- return hasTelemetryAttribution(cleaned) ? cleaned : undefined;
87
- }
88
-
89
- export function mergeTelemetryAttribution(
90
- base?: Partial<TelemetryAttribution> | null,
91
- incoming?: Partial<TelemetryAttribution> | null,
92
- ): TelemetryAttribution | undefined {
93
- const merged = sanitizeTelemetryAttribution({
94
- ...(base ?? {}),
95
- ...(incoming ?? {}),
96
- });
97
- return merged;
98
- }
99
-
100
- export function mergeTelemetryProperties(
101
- properties?: Record<string, unknown>,
102
- attribution?: Partial<TelemetryAttribution> | null,
103
- ): Record<string, unknown> | undefined {
104
- const cleanedAttribution = sanitizeTelemetryAttribution(attribution);
105
- if (!cleanedAttribution) return properties;
106
- return {
107
- ...cleanedAttribution,
108
- ...(properties ?? {}),
109
- };
110
- }
111
-
112
- export function decodeTelemetryAttribution(value: string | undefined): TelemetryAttribution | undefined {
113
- if (!value) return undefined;
114
- try {
115
- const decoded = Buffer.from(value, "base64").toString("utf8");
116
- return sanitizeTelemetryAttribution(JSON.parse(decoded) as Partial<Record<TelemetryAttributionKey, unknown>>);
117
- } catch {
118
- return undefined;
119
- }
120
- }
@@ -1,253 +0,0 @@
1
- /**
2
- * Route trace telemetry — issue #28
3
- *
4
- * Persists anonymized routing artifacts to ~/.unbrowse/traces/ for future
5
- * ML training (RAG over traces, contextual bandits, KGE extraction).
6
- *
7
- * Anonymization rules (per spec):
8
- * NEVER store: raw cookies, auth tokens, CSRF tokens, full request/response
9
- * bodies, user-entered secrets, raw chain-of-thought, PII.
10
- * DO store: normalized binding names, hashed binding values, response
11
- * schema shape, response hashes, route fingerprints,
12
- * error class / failure reason taxonomy.
13
- */
14
-
15
- import { createHash } from "node:crypto";
16
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
17
- import { join } from "node:path";
18
- import type { RouteTraceArtifact, TraceFailureReason } from "./types/index.js";
19
- import { TRACE_VERSION } from "./version.js";
20
-
21
- function getTraceDir(): string {
22
- return process.env.UNBROWSE_TRACES_DIR ??
23
- join(process.env.HOME ?? "/tmp", ".unbrowse", "traces");
24
- }
25
-
26
- /** Whether trace emission is enabled (opt-out via env). */
27
- export function isTracingEnabled(): boolean {
28
- return process.env.UNBROWSE_DISABLE_TRACES !== "1";
29
- }
30
-
31
- /**
32
- * SHA-256 hex digest of a string value.
33
- * Used to hash binding values so dependency matching works without exposing
34
- * the actual value.
35
- */
36
- export function hashValue(value: string): string {
37
- return createHash("sha256").update(value).digest("hex").slice(0, 16);
38
- }
39
-
40
- /** Sensitive field name patterns — values of matching keys are stripped. */
41
- const SENSITIVE_PATTERNS = [
42
- /cookie/i, /token/i, /auth/i, /key/i, /secret/i, /password/i,
43
- /csrf/i, /session/i, /bearer/i, /credential/i, /apikey/i,
44
- ];
45
-
46
- function isSensitiveName(name: string): boolean {
47
- return SENSITIVE_PATTERNS.some((re) => re.test(name));
48
- }
49
-
50
- /**
51
- * Return only the binding *names* that are safe to surface in a trace.
52
- * Values are never included; sensitive names are omitted entirely.
53
- */
54
- export function safeBindingNames(bindings: Record<string, unknown>): string[] {
55
- return Object.keys(bindings).filter((k) => !isSensitiveName(k));
56
- }
57
-
58
- /**
59
- * Strip the query string and fragment from a URL so traces never contain
60
- * user-entered search terms or personal identifiers embedded in URLs.
61
- */
62
- export function anonymizeUrl(url: string): string {
63
- try {
64
- const u = new URL(url);
65
- return `${u.protocol}//${u.host}${u.pathname}`;
66
- } catch {
67
- return url.replace(/\?.*$/, "").replace(/#.*$/, "");
68
- }
69
- }
70
-
71
- /**
72
- * Produce a SHA-256 hex digest of the serialized result body.
73
- * This lets the backend detect duplicate responses without storing content.
74
- */
75
- export function hashResponseBody(result: unknown): string {
76
- const str = typeof result === "string" ? result : JSON.stringify(result ?? "");
77
- return createHash("sha256").update(str).digest("hex").slice(0, 32);
78
- }
79
-
80
- /**
81
- * Classify an error message into a `TraceFailureReason` taxonomy value so
82
- * negative traces carry structured labels instead of free-form strings.
83
- */
84
- export function classifyFailure(error: string | undefined): TraceFailureReason {
85
- if (!error) return "unknown";
86
- const e = error.toLowerCase();
87
- if (e.includes("auth") || e.includes("401") || e.includes("403")) return "auth_failed";
88
- if (e.includes("schema") || e.includes("mismatch")) return "schema_mismatch";
89
- if (e.includes("timeout") || e.includes("timed out")) return "timeout";
90
- if (e.includes("empty") || e.includes("no result")) return "empty_response";
91
- if (e.includes("missing") || e.includes("dependency")) return "dependency_missing";
92
- if (e.includes("robots") || e.includes("disallowed")) return "robots_disallowed";
93
- return "unknown";
94
- }
95
-
96
- export interface EmitTraceParams {
97
- trace_id: string;
98
- session_scope: string;
99
- goal: string;
100
- domain: string;
101
- started_at: string;
102
- skill_id?: string;
103
- endpoint_id?: string;
104
- source: RouteTraceArtifact["source"];
105
- status_code?: number;
106
- response_bytes: number;
107
- result?: unknown;
108
- schema_match?: boolean;
109
- candidates_considered: number;
110
- bindings_before: Record<string, unknown>;
111
- bindings_resolved: Record<string, unknown>;
112
- bindings_missing: string[];
113
- outcome: RouteTraceArtifact["outcome"];
114
- error?: string;
115
- rationale?: string;
116
- }
117
-
118
- /**
119
- * Write an anonymized `RouteTraceArtifact` to the local trace directory.
120
- * Returns the file path written, or null if tracing is disabled or the write fails.
121
- */
122
- export function emitRouteTrace(params: EmitTraceParams): string | null {
123
- if (!isTracingEnabled()) return null;
124
-
125
- const now = new Date().toISOString();
126
- const artifact: RouteTraceArtifact = {
127
- trace_id: params.trace_id,
128
- session_scope: params.session_scope,
129
- goal: params.goal,
130
- domain: params.domain,
131
- started_at: params.started_at,
132
- completed_at: now,
133
- latency_ms: Date.now() - new Date(params.started_at).getTime(),
134
- skill_id: params.skill_id,
135
- endpoint_id: params.endpoint_id,
136
- source: params.source,
137
- status_code: params.status_code,
138
- response_bytes: params.response_bytes,
139
- response_hash: params.result != null ? hashResponseBody(params.result) : undefined,
140
- schema_match: params.schema_match,
141
- candidates_considered: params.candidates_considered,
142
- bindings_before: safeBindingNames(params.bindings_before),
143
- bindings_resolved: safeBindingNames(params.bindings_resolved),
144
- bindings_missing: params.bindings_missing.filter((k) => !isSensitiveName(k)),
145
- outcome: params.outcome,
146
- failure_reason: params.outcome !== "success"
147
- ? classifyFailure(params.error)
148
- : undefined,
149
- rationale: params.rationale,
150
- trace_version: TRACE_VERSION,
151
- };
152
-
153
- try {
154
- const traceDir = getTraceDir();
155
- if (!existsSync(traceDir)) mkdirSync(traceDir, { recursive: true });
156
- const stamp = params.started_at.replace(/[:.]/g, "-");
157
- const file = join(traceDir, `${stamp}-${params.outcome}-${params.trace_id}.json`);
158
- writeFileSync(file, JSON.stringify(artifact, null, 2), "utf-8");
159
- return file;
160
- } catch {
161
- return null;
162
- }
163
- }
164
-
165
- // ---------------------------------------------------------------------------
166
- // Error accumulator — auto-file GitHub issues on repeated failures
167
- // ---------------------------------------------------------------------------
168
-
169
- interface AccumulatedError {
170
- count: number;
171
- firstSeen: string;
172
- lastSeen: string;
173
- error: string;
174
- intent: string;
175
- url?: string;
176
- domain: string;
177
- skillId?: string;
178
- endpointId?: string;
179
- }
180
-
181
- const errorAccumulator = new Map<string, AccumulatedError>();
182
- const AUTO_FILE_THRESHOLD = 3;
183
- const autoFiledKeys = new Set<string>();
184
-
185
- export interface FailureContext {
186
- skillId?: string;
187
- endpointId?: string;
188
- domain: string;
189
- intent: string;
190
- url?: string;
191
- error: string;
192
- }
193
-
194
- /**
195
- * Record a failure. After AUTO_FILE_THRESHOLD consecutive failures for the
196
- * same skill+endpoint, auto-file a GitHub issue via the backend.
197
- */
198
- export function recordFailure(ctx: FailureContext): void {
199
- const key = `${ctx.skillId ?? "unknown"}:${ctx.endpointId ?? "unknown"}`;
200
- const existing = errorAccumulator.get(key);
201
- const now = new Date().toISOString();
202
-
203
- if (existing) {
204
- existing.count++;
205
- existing.lastSeen = now;
206
- existing.error = ctx.error;
207
- } else {
208
- errorAccumulator.set(key, {
209
- count: 1,
210
- firstSeen: now,
211
- lastSeen: now,
212
- error: ctx.error,
213
- intent: ctx.intent,
214
- url: ctx.url,
215
- domain: ctx.domain,
216
- skillId: ctx.skillId,
217
- endpointId: ctx.endpointId,
218
- });
219
- }
220
-
221
- const entry = errorAccumulator.get(key)!;
222
- if (entry.count >= AUTO_FILE_THRESHOLD && !autoFiledKeys.has(key)) {
223
- autoFiledKeys.add(key);
224
- // Fire-and-forget auto-file
225
- import("./client/index.js")
226
- .then((client) =>
227
- client.autoFileIssue({
228
- skill_id: ctx.skillId ?? "unknown",
229
- endpoint_id: ctx.endpointId ?? "unknown",
230
- domain: ctx.domain,
231
- intent: ctx.intent,
232
- url: ctx.url,
233
- error: ctx.error,
234
- failure_count: entry.count,
235
- first_seen: entry.firstSeen,
236
- last_seen: entry.lastSeen,
237
- kuri_version: process.env.KURI_VERSION ?? "unknown",
238
- }),
239
- )
240
- .catch((err) =>
241
- console.warn(`[telemetry] auto-file failed: ${(err as Error).message}`),
242
- );
243
- }
244
- }
245
-
246
- export function getAccumulatedErrors(): Map<string, AccumulatedError> {
247
- return new Map(errorAccumulator);
248
- }
249
-
250
- export function resetErrorAccumulatorForTests(): void {
251
- errorAccumulator.clear();
252
- autoFiledKeys.clear();
253
- }
@@ -1,141 +0,0 @@
1
- function sanitizeBindingKey(key: string): string {
2
- return key
3
- .replace(/\[\]/g, "_item")
4
- .replace(/\[([^\]]+)\]/g, (_m, inner: string) => `_${inner}`)
5
- .replace(/[{}]/g, "")
6
- .replace(/[^a-zA-Z0-9_]+/g, "_")
7
- .replace(/_+/g, "_")
8
- .replace(/^_+|_+$/g, "");
9
- }
10
-
11
- export function normalizeQueryBindingKey(key: string): string {
12
- const normalized = sanitizeBindingKey(key);
13
- if (!normalized) return "query";
14
- return /^[0-9]/.test(normalized) ? `query_${normalized}` : normalized;
15
- }
16
-
17
- export function buildQueryBindingMap(keys: Iterable<string>): Record<string, string> {
18
- const out: Record<string, string> = {};
19
- const used = new Set<string>();
20
- for (const rawKey of keys) {
21
- if (!rawKey || out[rawKey]) continue;
22
- const base = normalizeQueryBindingKey(rawKey);
23
- let next = base;
24
- let suffix = 2;
25
- while (used.has(next)) next = `${base}_${suffix++}`;
26
- used.add(next);
27
- out[rawKey] = next;
28
- }
29
- return out;
30
- }
31
-
32
- export function extractTemplateQueryBindings(urlTemplate: string): Record<string, string> {
33
- try {
34
- const templateUrl = new URL(urlTemplate);
35
- const out: Record<string, string> = {};
36
- for (const [key, value] of templateUrl.searchParams.entries()) {
37
- const placeholder = value.match(/^\{([^}]+)\}$/)?.[1];
38
- if (placeholder) out[key] = placeholder;
39
- }
40
- return out;
41
- } catch {
42
- return {};
43
- }
44
- }
45
-
46
- export function deriveTemplateParamsFromContextUrl(
47
- urlTemplate: string,
48
- contextUrl?: string,
49
- ): Record<string, string> {
50
- if (!contextUrl) return {};
51
- try {
52
- const templateUrl = new URL(urlTemplate);
53
- const actualUrl = new URL(contextUrl);
54
- const out: Record<string, string> = {};
55
-
56
- const templateParts = templateUrl.pathname.split("/").filter(Boolean).map((part) => decodeURIComponent(part));
57
- const actualParts = actualUrl.pathname.split("/").filter(Boolean);
58
- if (templateParts.length === actualParts.length) {
59
- let compatible = true;
60
- for (let i = 0; i < templateParts.length; i++) {
61
- const templatePart = templateParts[i];
62
- const actualPart = actualParts[i];
63
- const placeholder = templatePart.match(/^\{([^}]+)\}$/)?.[1];
64
- if (placeholder) {
65
- out[placeholder] = decodeURIComponent(actualPart);
66
- continue;
67
- }
68
- if (templatePart !== actualPart) {
69
- compatible = false;
70
- break;
71
- }
72
- }
73
- if (!compatible) {
74
- for (const key of Object.keys(out)) delete out[key];
75
- }
76
- }
77
-
78
- for (const [key, value] of templateUrl.searchParams.entries()) {
79
- const placeholder = value.match(/^\{([^}]+)\}$/)?.[1];
80
- if (!placeholder) continue;
81
- const actualValue = actualUrl.searchParams.get(key);
82
- if (actualValue != null && actualValue !== "") {
83
- out[placeholder] = actualValue;
84
- }
85
- }
86
- return out;
87
- } catch {
88
- return {};
89
- }
90
- }
91
-
92
- function deriveSemanticTemplateParams(
93
- urlTemplate: string,
94
- contextUrl?: string,
95
- ): Record<string, string> {
96
- if (!contextUrl) return {};
97
- try {
98
- const templateNames = [...urlTemplate.matchAll(/\{([^}]+)\}/g)].map((m) => m[1]).filter(Boolean);
99
- if (templateNames.length === 0) return {};
100
- const actualUrl = new URL(contextUrl);
101
- const actualParts = actualUrl.pathname.split("/").filter(Boolean).map((part) => decodeURIComponent(part));
102
- const lowerParts = actualParts.map((part) => part.toLowerCase());
103
- const out: Record<string, string> = {};
104
-
105
- for (const name of templateNames) {
106
- const lower = name.toLowerCase();
107
- if (lower === "tag" || lower === "tags") {
108
- const prefixIndex = lowerParts.findIndex((part) => part === "t" || part === "tag" || part === "tags");
109
- if (prefixIndex >= 0 && actualParts[prefixIndex + 1]) out[name] = actualParts[prefixIndex + 1]!;
110
- }
111
- }
112
-
113
- return out;
114
- } catch {
115
- return {};
116
- }
117
- }
118
-
119
- export function mergeContextTemplateParams(
120
- params: Record<string, unknown>,
121
- urlTemplate: string,
122
- contextUrl?: string,
123
- ): Record<string, unknown> {
124
- const merged: Record<string, unknown> = { ...params };
125
- const inferred = deriveTemplateParamsFromContextUrl(urlTemplate, contextUrl);
126
- for (const [key, value] of Object.entries(inferred)) {
127
- if (merged[key] == null || merged[key] === "") merged[key] = value;
128
- }
129
- for (const [key, value] of Object.entries(deriveSemanticTemplateParams(urlTemplate, contextUrl))) {
130
- if (merged[key] == null || merged[key] === "") merged[key] = value;
131
- }
132
- for (const [rawKey, placeholder] of Object.entries(extractTemplateQueryBindings(urlTemplate))) {
133
- if ((merged[placeholder] == null || merged[placeholder] === "") && merged[rawKey] != null && merged[rawKey] !== "") {
134
- merged[placeholder] = merged[rawKey];
135
- }
136
- if ((merged[rawKey] == null || merged[rawKey] === "") && merged[placeholder] != null && merged[placeholder] !== "") {
137
- merged[rawKey] = merged[placeholder];
138
- }
139
- }
140
- return merged;
141
- }
@@ -1,60 +0,0 @@
1
- import type { DriftResult, ResponseSchema } from "../types/index.js";
2
- import { inferSchema } from "./index.js";
3
-
4
- /**
5
- * Collect all field paths from a schema (dot-notation).
6
- */
7
- function collectPaths(schema: ResponseSchema, prefix = ""): Map<string, string> {
8
- const paths = new Map<string, string>();
9
- if (schema.type === "object" && schema.properties) {
10
- for (const [key, val] of Object.entries(schema.properties)) {
11
- const fullPath = prefix ? `${prefix}.${key}` : key;
12
- paths.set(fullPath, val.type);
13
- if (val.type === "object" && val.properties) {
14
- for (const [p, t] of collectPaths(val, fullPath)) {
15
- paths.set(p, t);
16
- }
17
- }
18
- if (val.type === "array" && val.items?.type === "object") {
19
- for (const [p, t] of collectPaths(val.items, `${fullPath}[]`)) {
20
- paths.set(p, t);
21
- }
22
- }
23
- }
24
- }
25
- return paths;
26
- }
27
-
28
- /**
29
- * Compare stored schema against a fresh response sample.
30
- * Returns added/removed fields and type changes.
31
- */
32
- export function detectSchemaDrift(existing: ResponseSchema, newSample: unknown): DriftResult {
33
- const newSchema = inferSchema([newSample]);
34
- const oldPaths = collectPaths(existing);
35
- const newPaths = collectPaths(newSchema);
36
-
37
- const added_fields: string[] = [];
38
- const removed_fields: string[] = [];
39
- const type_changes: Array<{ path: string; was: string; now: string }> = [];
40
-
41
- for (const [path, type] of newPaths) {
42
- if (!oldPaths.has(path)) {
43
- added_fields.push(path);
44
- } else if (oldPaths.get(path) !== type) {
45
- type_changes.push({ path, was: oldPaths.get(path)!, now: type });
46
- }
47
- }
48
- for (const path of oldPaths.keys()) {
49
- if (!newPaths.has(path)) {
50
- removed_fields.push(path);
51
- }
52
- }
53
-
54
- return {
55
- drifted: added_fields.length > 0 || removed_fields.length > 0 || type_changes.length > 0,
56
- added_fields,
57
- removed_fields,
58
- type_changes,
59
- };
60
- }