@clue-ai/cli 0.0.3

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.
@@ -0,0 +1,1830 @@
1
+ import { createHash, createHmac } from "node:crypto";
2
+ import {
3
+ validateSemanticCiRequest,
4
+ validateSemanticSnapshotRequest,
5
+ } from "./contracts.mjs";
6
+ import { analyzeFastApiRoutes } from "./fastapi-analyzer.mjs";
7
+ import { listAllowedPythonFiles } from "./path-policy.mjs";
8
+
9
+ const sha256 = (value) =>
10
+ `sha256:${createHash("sha256").update(value).digest("hex")}`;
11
+ const hmacSha256 = (key, value) =>
12
+ `sha256:${createHmac("sha256", key).update(value).digest("hex")}`;
13
+ const shortHash = (value) =>
14
+ createHash("sha256").update(value).digest("hex").slice(0, 12);
15
+
16
+ const EFFECT_KIND_VALUES = new Set([
17
+ "data_create",
18
+ "data_update",
19
+ "data_delete",
20
+ "data_upsert",
21
+ "data_read",
22
+ "data_query",
23
+ "data_import",
24
+ "data_export",
25
+ "state_transition",
26
+ "validation_check",
27
+ "access_check",
28
+ "communication_send",
29
+ "audit_record",
30
+ "event_emit",
31
+ "background_task_schedule",
32
+ "external_service_call",
33
+ "payment_operation",
34
+ "ai_inference",
35
+ "file_process",
36
+ "cache_change",
37
+ "system_config_change",
38
+ "unknown",
39
+ ]);
40
+
41
+ const RESOLUTION_BASIS_VALUES = new Set([
42
+ "route_contract",
43
+ "direct_data_mutation",
44
+ "repository_call",
45
+ "orm_model_call",
46
+ "sql_statement",
47
+ "read_query",
48
+ "external_sdk_call",
49
+ "message_publish",
50
+ "job_enqueue",
51
+ "file_operation",
52
+ "validation_logic",
53
+ "access_control_logic",
54
+ "ai_inferred_code_behavior",
55
+ "explicit_semantic_config",
56
+ "unknown",
57
+ ]);
58
+
59
+ const SELECTED_SOURCE_VALUES = new Set(["deterministic", "ai"]);
60
+
61
+ const ACTUAL_OUTCOME_ACTIONS = new Set([
62
+ "completed",
63
+ "failed",
64
+ "validation_error",
65
+ "blocked",
66
+ "abandoned",
67
+ ]);
68
+
69
+ const snakeSegmentPattern = /^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/;
70
+
71
+ const semanticSnapshotHashScope = (request) =>
72
+ [
73
+ "clue_tools_semantic_snapshot",
74
+ request.project_key,
75
+ request.repository.repository_id,
76
+ request.repository.merge_commit,
77
+ request.repository.workflow_run_id ?? "no_workflow_run",
78
+ ].join(":");
79
+
80
+ const opaqueRouteHash = (route, purpose, hashScope) =>
81
+ hmacSha256(
82
+ hashScope,
83
+ [purpose, route.operation_source_key, route.source.source_fingerprint].join(
84
+ ":",
85
+ ),
86
+ );
87
+
88
+ const fallbackSemantics = (route, reason, hashScope) => ({
89
+ operation_source_key: route.operation_source_key,
90
+ semantic_snapshot_version: route.semantic_snapshot_version,
91
+ method: route.method,
92
+ path_template: route.path_template,
93
+ semantics: {
94
+ route_summary: "unknown",
95
+ action_candidates: [],
96
+ outcome_candidates: [],
97
+ value_event_candidates: [],
98
+ route_confidence: 0,
99
+ },
100
+ layer_evidence: {
101
+ data_effects: [],
102
+ side_effects: [],
103
+ failure_surfaces: ["unknown"],
104
+ validation_field_paths: [],
105
+ permission_scopes: [],
106
+ component_fingerprints: [
107
+ opaqueRouteHash(route, "route_contract_component", hashScope),
108
+ opaqueRouteHash(route, "abstract_behavior_component", hashScope),
109
+ opaqueRouteHash(route, "source_component", hashScope),
110
+ ],
111
+ },
112
+ operation_effects: [],
113
+ unresolved_operation_effects: [],
114
+ source_evidence_refs: [],
115
+ confidence_reason: reason,
116
+ });
117
+
118
+ const buildAiPrompt = (routes) =>
119
+ JSON.stringify({
120
+ task: "Generate privacy-safe semantic meaning candidates for FastAPI operation sources. Return JSON only.",
121
+ rules: [
122
+ "Do not create operation_source_key values; only copy the provided keys.",
123
+ "Do not create ids, refs, hashes, source fingerprints, or field paths.",
124
+ "Do not include raw source code, raw SQL, file paths, function names, class names, prompts, or completions in the output.",
125
+ "Use the provided privacy-safe evidence summaries only.",
126
+ "Return operation_effect_candidates only when source evidence supports a schema-valid target object and effect action.",
127
+ "Keep multiple effects separate when one route produces multiple meaningful effects.",
128
+ "Do not encode actual outcomes such as completed, failed, blocked, validation_error, or abandoned into operation_effect_key or expected_domain_effect.",
129
+ ],
130
+ output_shape: {
131
+ routes: [
132
+ {
133
+ operation_source_key: "route.POST./reports",
134
+ semantics: {
135
+ route_summary: "creates report-related business records",
136
+ action_candidates: [],
137
+ outcome_candidates: [],
138
+ route_confidence: 0.8,
139
+ },
140
+ confidence_reason:
141
+ "Privacy-safe source evidence supports the route meaning.",
142
+ operation_effect_candidates: [
143
+ {
144
+ raw_target_object_text: "Reports",
145
+ concept: "business record produced in the service",
146
+ business_role: "core record users create and review",
147
+ effect_kind: "data_create",
148
+ effect_action: "create",
149
+ resolution_basis: "route_contract",
150
+ resolution_reason:
151
+ "The route contract and abstract evidence indicate a create effect.",
152
+ missing_context: [],
153
+ reasoning_summary:
154
+ "Abstract explanation without raw implementation details.",
155
+ },
156
+ ],
157
+ },
158
+ ],
159
+ },
160
+ routes: routes.map((route) => ({
161
+ operation_source_key: route.operation_source_key,
162
+ method: route.method,
163
+ path_template: route.path_template,
164
+ evidence_summaries: route.evidence_summaries,
165
+ })),
166
+ });
167
+
168
+ const requireAiProviderApiKey = (env) => {
169
+ const apiKey = env.AI_PROVIDER_API_KEY;
170
+ if (!apiKey) {
171
+ throw new Error("AI_PROVIDER_API_KEY is required for semantic generation");
172
+ }
173
+ return apiKey;
174
+ };
175
+
176
+ const callAiProvider = async ({ request, apiKey, routes }) => {
177
+ const response = await fetch(
178
+ `${request.ai_provider_base_url.replace(/\/+$/, "")}/chat/completions`,
179
+ {
180
+ method: "POST",
181
+ headers: {
182
+ "content-type": "application/json",
183
+ authorization: `Bearer ${apiKey}`,
184
+ },
185
+ body: JSON.stringify({
186
+ model: request.ai_model,
187
+ messages: [
188
+ {
189
+ role: "system",
190
+ content: "You generate privacy-safe route semantics JSON.",
191
+ },
192
+ { role: "user", content: buildAiPrompt(routes) },
193
+ ],
194
+ response_format: { type: "json_object" },
195
+ }),
196
+ },
197
+ );
198
+ if (!response.ok) {
199
+ throw new Error(`AI provider failed: ${response.status}`);
200
+ }
201
+ const body = await response.json();
202
+ const content = body?.choices?.[0]?.message?.content;
203
+ if (typeof content !== "string" || content.trim() === "") {
204
+ throw new Error("AI provider returned empty semantic generation content");
205
+ }
206
+ const parsed = JSON.parse(content);
207
+ return new Map(
208
+ (parsed.routes ?? []).map((route) => [route.operation_source_key, route]),
209
+ );
210
+ };
211
+
212
+ const buildAiSelectionPrompt = (selectionCandidates) =>
213
+ JSON.stringify({
214
+ task: "Choose adopted semantic values from deterministic and AI candidates. Return JSON only.",
215
+ rules: [
216
+ "Only copy operation_source_key, candidate_index, and parameter_name values from the provided selection_candidates.",
217
+ "For each semantic field, return selected_source as deterministic or ai.",
218
+ "Do not create ids, refs, hashes, source fingerprints, field paths, source locations, raw code, raw SQL, prompts, or completions.",
219
+ "Prefer the AI candidate when it adds supported business meaning from privacy-safe evidence.",
220
+ "Prefer deterministic when both candidates are equivalent or the AI candidate is unknown, unsupported, or less schema-safe.",
221
+ "If neither candidate is safe, include missing_context instead of inventing a value.",
222
+ ],
223
+ output_shape: {
224
+ field_selections: [
225
+ {
226
+ operation_source_key: "route.POST./reports",
227
+ candidate_index: 0,
228
+ parameter_name: "concept",
229
+ selected_source: "ai",
230
+ selection_reason:
231
+ "AI candidate gives the more specific business concept.",
232
+ missing_context: [],
233
+ },
234
+ ],
235
+ },
236
+ selection_candidates: selectionCandidates,
237
+ });
238
+
239
+ const callAiSelectionProvider = async ({
240
+ request,
241
+ apiKey,
242
+ selectionCandidates,
243
+ routeBySelectionKey,
244
+ }) => {
245
+ if (selectionCandidates.length === 0) {
246
+ return new Map();
247
+ }
248
+ assertNoRawCodeStructure(selectionCandidates, "semantic_selection_prompt");
249
+ const response = await fetch(
250
+ `${request.ai_provider_base_url.replace(/\/+$/, "")}/chat/completions`,
251
+ {
252
+ method: "POST",
253
+ headers: {
254
+ "content-type": "application/json",
255
+ authorization: `Bearer ${apiKey}`,
256
+ },
257
+ body: JSON.stringify({
258
+ model: request.ai_model,
259
+ messages: [
260
+ {
261
+ role: "system",
262
+ content:
263
+ "You select adopted semantic values from privacy-safe candidates.",
264
+ },
265
+ {
266
+ role: "user",
267
+ content: buildAiSelectionPrompt(selectionCandidates),
268
+ },
269
+ ],
270
+ response_format: { type: "json_object" },
271
+ }),
272
+ },
273
+ );
274
+ if (!response.ok) {
275
+ throw new Error(`AI selector failed: ${response.status}`);
276
+ }
277
+ const body = await response.json();
278
+ const content = body?.choices?.[0]?.message?.content;
279
+ if (typeof content !== "string" || content.trim() === "") {
280
+ throw new Error("AI selector returned empty semantic selection content");
281
+ }
282
+ const parsed = JSON.parse(content);
283
+ return normalizeAiSelectionResponse({
284
+ parsed,
285
+ routeBySelectionKey,
286
+ });
287
+ };
288
+
289
+ const buildSemanticSnapshotVersion = ({ request, generatedAt }) =>
290
+ `snap_${shortHash(`${request.project_key}:${request.repository.repository_id}:${request.repository.merge_commit}:${request.repository.workflow_run_id ?? generatedAt}`)}`;
291
+
292
+ const normalizeSnakeSegment = (value) => {
293
+ const normalized = String(value ?? "")
294
+ .normalize("NFKD")
295
+ .replace(/[\u0300-\u036f]/g, "")
296
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
297
+ .toLowerCase()
298
+ .replace(/[^a-z0-9]+/g, "_")
299
+ .replace(/^_+|_+$/g, "")
300
+ .replace(/_+/g, "_");
301
+ if (!normalized || !/^[a-z]/.test(normalized)) {
302
+ return null;
303
+ }
304
+ return normalized;
305
+ };
306
+
307
+ const slugSegment = (value) => {
308
+ const normalized = normalizeSnakeSegment(value);
309
+ if (!normalized) {
310
+ return null;
311
+ }
312
+ if (
313
+ !normalized.endsWith("s") &&
314
+ !normalized.endsWith("data") &&
315
+ !normalized.endsWith("status")
316
+ ) {
317
+ return `${normalized}s`;
318
+ }
319
+ return normalized;
320
+ };
321
+
322
+ const splitIdentifierTokens = (value) =>
323
+ String(value ?? "")
324
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
325
+ .toLowerCase()
326
+ .split(/[^a-z0-9]+|_+/)
327
+ .filter((token) => token.length > 0);
328
+
329
+ const TECHNICAL_TARGET_TOKENS = new Set([
330
+ "request",
331
+ "response",
332
+ "dto",
333
+ "schema",
334
+ "payload",
335
+ "input",
336
+ "output",
337
+ "params",
338
+ "parameter",
339
+ "parameters",
340
+ "body",
341
+ "model",
342
+ "service",
343
+ "repository",
344
+ "controller",
345
+ "handler",
346
+ "usecase",
347
+ ]);
348
+
349
+ const EFFECT_ACTION_TOKENS = new Set([
350
+ "create",
351
+ "update",
352
+ "delete",
353
+ "upsert",
354
+ "read",
355
+ "query",
356
+ "list",
357
+ "get",
358
+ "send",
359
+ "record",
360
+ "schedule",
361
+ "publish",
362
+ "emit",
363
+ "import",
364
+ "export",
365
+ "process",
366
+ "generate",
367
+ "save",
368
+ "write",
369
+ "assign",
370
+ "transition",
371
+ "approve",
372
+ "cancel",
373
+ "archive",
374
+ "refund",
375
+ "checkout",
376
+ "submit",
377
+ "complete",
378
+ ]);
379
+
380
+ const FORBIDDEN_TARGET_TEXT_PATTERNS = [
381
+ /\b[A-Za-z0-9_./-]+\.py\b/i,
382
+ /\b(from\s+[A-Za-z_][A-Za-z0-9_.]*\s+import|import\s+[A-Za-z_][A-Za-z0-9_.]*)\b/i,
383
+ /\b(class|def)\s+[A-Za-z_][A-Za-z0-9_]*/,
384
+ /\b(select|insert|update|delete)\s+.+\b(from|into|set)\b/i,
385
+ /\b(system|user|assistant)\s*:\s*.+\b(prompt|completion|transcript)\b/i,
386
+ /\b[A-Z][A-Z0-9_]*(?:API_KEY|SECRET|TOKEN|PASSWORD|PRIVATE_KEY)\b(?:\s*=\s*["']?[^"'\s,;}]+)?/,
387
+ /\b[A-Z][A-Z0-9_]{2,}\s*=\s*["']?[^"'\s,;}]+/,
388
+ /\b(?:api[_-]?key|secret|token|password)\s*[:=]\s*["']?[^"'\s,;}]+/i,
389
+ /\b(?:bind|param|parameter)[ _-]?[A-Za-z0-9_]*\s*[:=]\s*["']?[^"'\s,;}]+/i,
390
+ /[:@$][A-Za-z_][A-Za-z0-9_]*\s*=\s*["']?[^"'\s,;}]+/,
391
+ /\bBearer\s+(?!\[secret\])[A-Za-z0-9._~+/-]+=*/i,
392
+ /\bsk-[A-Za-z0-9_-]{8,}\b/,
393
+ ];
394
+
395
+ const hasForbiddenTargetText = (value) =>
396
+ FORBIDDEN_TARGET_TEXT_PATTERNS.some((pattern) =>
397
+ pattern.test(String(value ?? "")),
398
+ );
399
+
400
+ const normalizeTargetObjectText = (value) => {
401
+ if (hasForbiddenTargetText(value)) {
402
+ return null;
403
+ }
404
+ const tokens = splitIdentifierTokens(value).filter(
405
+ (token) => !TECHNICAL_TARGET_TOKENS.has(token),
406
+ );
407
+ while (tokens.length > 1 && EFFECT_ACTION_TOKENS.has(tokens[0])) {
408
+ tokens.shift();
409
+ }
410
+ while (
411
+ tokens.length > 1 &&
412
+ EFFECT_ACTION_TOKENS.has(tokens[tokens.length - 1])
413
+ ) {
414
+ tokens.pop();
415
+ }
416
+ const targetKey = slugSegment(tokens.join("_"));
417
+ return targetKey ? displayNameFromKey(targetKey) : null;
418
+ };
419
+
420
+ const displayNameFromKey = (key) =>
421
+ key
422
+ .split("_")
423
+ .map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
424
+ .join(" ");
425
+
426
+ const EFFECT_ACTION_ALIASES = new Map([
427
+ ["creates", "create"],
428
+ ["created", "create"],
429
+ ["updates", "update"],
430
+ ["updated", "update"],
431
+ ["deletes", "delete"],
432
+ ["deleted", "delete"],
433
+ ["reads", "read"],
434
+ ["queries", "query"],
435
+ ["imports", "import"],
436
+ ["exports", "export"],
437
+ ["sends", "send"],
438
+ ["sent", "send"],
439
+ ["records", "record"],
440
+ ["recorded", "record"],
441
+ ["emits", "emit"],
442
+ ["emitted", "emit"],
443
+ ["schedules", "schedule"],
444
+ ["scheduled", "schedule"],
445
+ ["calls", "call"],
446
+ ["called", "call"],
447
+ ["processes", "process"],
448
+ ["processed", "process"],
449
+ ["infers", "infer"],
450
+ ["inferred", "infer"],
451
+ ["changes", "change"],
452
+ ["changed", "change"],
453
+ ["transitions", "transition"],
454
+ ["transitioned", "transition"],
455
+ ["checks", "check"],
456
+ ["checked", "check"],
457
+ ]);
458
+
459
+ const normalizeEffectAction = (value, effectKind) => {
460
+ if (!hasForbiddenTargetText(value)) {
461
+ const candidate = normalizeSnakeSegment(value);
462
+ const normalizedCandidate =
463
+ EFFECT_ACTION_ALIASES.get(candidate) ?? candidate;
464
+ if (normalizedCandidate && snakeSegmentPattern.test(normalizedCandidate)) {
465
+ return normalizedCandidate;
466
+ }
467
+ }
468
+ const fromKind = {
469
+ data_create: "create",
470
+ data_update: "update",
471
+ data_delete: "delete",
472
+ data_upsert: "upsert",
473
+ data_read: "read",
474
+ data_query: "query",
475
+ data_import: "import",
476
+ data_export: "export",
477
+ communication_send: "send",
478
+ audit_record: "record",
479
+ event_emit: "emit",
480
+ background_task_schedule: "schedule",
481
+ external_service_call: "call",
482
+ payment_operation: "process",
483
+ ai_inference: "infer",
484
+ file_process: "process",
485
+ cache_change: "change",
486
+ system_config_change: "change",
487
+ state_transition: "transition",
488
+ validation_check: "check",
489
+ access_check: "check",
490
+ }[effectKind];
491
+ return fromKind ?? null;
492
+ };
493
+
494
+ const isValidEffectKey = (key, targetObjectKey) => {
495
+ if (typeof key !== "string" || !key.includes(".")) {
496
+ return false;
497
+ }
498
+ const [prefix, action] = key.split(".");
499
+ return (
500
+ prefix === targetObjectKey &&
501
+ snakeSegmentPattern.test(prefix) &&
502
+ snakeSegmentPattern.test(action) &&
503
+ prefix !== "unknown" &&
504
+ !ACTUAL_OUTCOME_ACTIONS.has(action)
505
+ );
506
+ };
507
+
508
+ const safeArray = (value) => (Array.isArray(value) ? value : []);
509
+
510
+ const firstNonEmpty = (...values) => {
511
+ for (const value of values) {
512
+ if (typeof value === "string" && value.trim()) {
513
+ return value.trim();
514
+ }
515
+ }
516
+ return null;
517
+ };
518
+
519
+ const buildSourceEvidenceRefs = (route, hashScope) => {
520
+ const baseId = `evidence_${shortHash(route.operation_source_key)}_route_contract`;
521
+ const domainCallTargets = safeArray(
522
+ route.ai_context?.code_structure?.call_targets,
523
+ ).filter(
524
+ (target) =>
525
+ typeof target === "string" && isDomainBehaviorCallTarget(target),
526
+ );
527
+ const refs = [
528
+ {
529
+ id: baseId,
530
+ kind: "route_contract",
531
+ summary: `${route.method ?? "UNKNOWN"} route exposes a product operation source contract.`,
532
+ evidence_strength:
533
+ route.method && route.method !== "GET" ? "medium" : "weak",
534
+ source_ref_hash: opaqueRouteHash(
535
+ route,
536
+ "route_contract_source",
537
+ hashScope,
538
+ ),
539
+ location_hash: opaqueRouteHash(
540
+ route,
541
+ "route_contract_location",
542
+ hashScope,
543
+ ),
544
+ metadata: {
545
+ extractor: "fastapi_route_inventory",
546
+ schema_version: 2,
547
+ },
548
+ },
549
+ ];
550
+
551
+ if (domainCallTargets.length > 0) {
552
+ refs.push({
553
+ id: `evidence_${shortHash(route.operation_source_key)}_abstract_behavior`,
554
+ kind: "abstract_domain_behavior",
555
+ summary:
556
+ "Route handler invokes domain behavior that may affect business objects or side effects.",
557
+ evidence_strength: "medium",
558
+ source_ref_hash: opaqueRouteHash(
559
+ route,
560
+ "abstract_behavior_source",
561
+ hashScope,
562
+ ),
563
+ location_hash: opaqueRouteHash(
564
+ route,
565
+ "abstract_behavior_location",
566
+ hashScope,
567
+ ),
568
+ metadata: {
569
+ extractor: "fastapi_route_inventory",
570
+ schema_version: 2,
571
+ domain_call_target_count: domainCallTargets.length,
572
+ },
573
+ });
574
+ }
575
+
576
+ return refs;
577
+ };
578
+
579
+ const isDomainBehaviorCallTarget = (target) => {
580
+ const normalized = target.toLowerCase();
581
+ if (
582
+ /\b(logger|logging|metric|metrics|telemetry|trace|tracer|span|debug|print|cache|uuid|datetime|time|json)\b/.test(
583
+ normalized,
584
+ )
585
+ ) {
586
+ return false;
587
+ }
588
+ const lastSegment = normalized.split(".").at(-1) ?? normalized;
589
+ if (/^(__|to_|from_|as_)/.test(lastSegment)) {
590
+ return false;
591
+ }
592
+ return /^[a-z_][a-z0-9_]*$/.test(lastSegment);
593
+ };
594
+
595
+ const tokenStemsFromText = (value) =>
596
+ String(value ?? "")
597
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
598
+ .toLowerCase()
599
+ .split(/[^a-z0-9]+|_+/)
600
+ .filter((token) => token.length >= 3)
601
+ .flatMap((token) => [
602
+ token,
603
+ token.endsWith("s") ? token.slice(0, -1) : `${token}s`,
604
+ ]);
605
+
606
+ const localTargetEvidenceStems = (route) => {
607
+ const codeStructure = route.ai_context?.code_structure ?? {};
608
+ const localEvidenceValues = [
609
+ ...safeArray(codeStructure.call_targets),
610
+ ...safeArray(codeStructure.parameter_annotations),
611
+ ...safeArray(codeStructure.dependency_hints),
612
+ codeStructure.response_annotation,
613
+ ];
614
+ return new Set(localEvidenceValues.flatMap(tokenStemsFromText));
615
+ };
616
+
617
+ const candidateHasTargetSpecificEvidence = ({ route, rawTargetObjectText }) => {
618
+ const targetStems = new Set(tokenStemsFromText(rawTargetObjectText));
619
+ if (targetStems.size === 0) {
620
+ return false;
621
+ }
622
+ const evidenceStems = localTargetEvidenceStems(route);
623
+ return [...targetStems].some((stem) => evidenceStems.has(stem));
624
+ };
625
+
626
+ const normalizeAiOperationEffectCandidates = (aiRoute) =>
627
+ safeArray(
628
+ aiRoute?.operation_effect_candidates ?? aiRoute?.operation_effects,
629
+ ).filter((candidate) => candidate && typeof candidate === "object");
630
+
631
+ const candidateHasUnsafeSemanticText = (candidate) =>
632
+ [
633
+ candidate?.concept,
634
+ candidate?.business_role,
635
+ candidate?.raw_target_object_text,
636
+ candidate?.target_object_text,
637
+ candidate?.effect_kind,
638
+ candidate?.effect_action,
639
+ candidate?.expected_domain_effect,
640
+ candidate?.resolution_basis,
641
+ candidate?.resolution_reason,
642
+ candidate?.reasoning_summary,
643
+ ...safeArray(candidate?.missing_context),
644
+ ].some((value) =>
645
+ FORBIDDEN_TARGET_TEXT_PATTERNS.some((pattern) =>
646
+ pattern.test(String(value ?? "")),
647
+ ),
648
+ );
649
+
650
+ const selectionKeyForCandidate = (route, candidateIndex) =>
651
+ `${route.operation_source_key}#${candidateIndex}`;
652
+
653
+ const normalizeAiSelectionResponse = ({ parsed, routeBySelectionKey }) => {
654
+ const decisionsByCandidate = new Map();
655
+ for (const decision of safeArray(parsed?.field_selections)) {
656
+ if (!decision || typeof decision !== "object") {
657
+ continue;
658
+ }
659
+ if (
660
+ typeof decision.operation_source_key !== "string" ||
661
+ !Number.isInteger(decision.candidate_index) ||
662
+ typeof decision.parameter_name !== "string" ||
663
+ !SELECTED_SOURCE_VALUES.has(decision.selected_source)
664
+ ) {
665
+ continue;
666
+ }
667
+ const selectionKey = `${decision.operation_source_key}#${decision.candidate_index}`;
668
+ const route = routeBySelectionKey.get(selectionKey);
669
+ if (!route) {
670
+ continue;
671
+ }
672
+ const candidateDecisions =
673
+ decisionsByCandidate.get(selectionKey) ?? new Map();
674
+ candidateDecisions.set(decision.parameter_name, {
675
+ selected_source: decision.selected_source,
676
+ selection_reason: firstNonEmpty(
677
+ sanitizeCandidateText(decision.selection_reason, route),
678
+ "AI selector chose the adopted semantic value from deterministic and AI candidates.",
679
+ ),
680
+ missing_context: stringArray(decision.missing_context, route),
681
+ });
682
+ decisionsByCandidate.set(selectionKey, candidateDecisions);
683
+ }
684
+ return decisionsByCandidate;
685
+ };
686
+
687
+ const sourceEvidenceRefsForCandidate = ({ candidate, routeEvidenceRefs }) => {
688
+ const behaviorEvidence = routeEvidenceRefs.filter(
689
+ (entry) => entry.kind !== "route_contract",
690
+ );
691
+ return (
692
+ behaviorEvidence.length > 0 ? behaviorEvidence : [routeEvidenceRefs[0]]
693
+ ).map((entry) => entry.id);
694
+ };
695
+
696
+ const sanitizeCandidateText = (value, route) =>
697
+ sanitizeText(String(value ?? ""), route).trim();
698
+
699
+ const hasBehaviorEvidence = (sourceEvidenceRefs, routeEvidenceRefs) => {
700
+ const routeEvidenceById = new Map(
701
+ routeEvidenceRefs.map((entry) => [entry.id, entry]),
702
+ );
703
+ return sourceEvidenceRefs.some(
704
+ (ref) => routeEvidenceById.get(ref)?.kind !== "route_contract",
705
+ );
706
+ };
707
+
708
+ const deterministicEffectKindForRoute = (route) => {
709
+ switch (route.method) {
710
+ case "POST":
711
+ return "data_create";
712
+ case "PUT":
713
+ case "PATCH":
714
+ return "data_update";
715
+ case "DELETE":
716
+ return "data_delete";
717
+ case "GET":
718
+ case "HEAD":
719
+ case "OPTIONS":
720
+ return "data_query";
721
+ default:
722
+ return "unknown";
723
+ }
724
+ };
725
+
726
+ const deterministicResolutionBasisForRoute = (
727
+ sourceEvidenceRefs,
728
+ routeEvidenceRefs,
729
+ ) => {
730
+ if (hasBehaviorEvidence(sourceEvidenceRefs, routeEvidenceRefs)) {
731
+ return "ai_inferred_code_behavior";
732
+ }
733
+ return "route_contract";
734
+ };
735
+
736
+ const buildCandidateSemanticValues = ({
737
+ route,
738
+ candidate,
739
+ routeEvidenceRefs,
740
+ }) => {
741
+ const sourceEvidenceRefs = sourceEvidenceRefsForCandidate({
742
+ candidate,
743
+ routeEvidenceRefs,
744
+ });
745
+ const rawTargetObjectText = firstNonEmpty(
746
+ normalizeTargetObjectText(candidate.raw_target_object_text),
747
+ normalizeTargetObjectText(candidate.target_object_text),
748
+ );
749
+ const deterministicConcept =
750
+ "object associated with a route operation source";
751
+ const aiConcept = firstNonEmpty(
752
+ sanitizeCandidateText(candidate.concept, route),
753
+ "domain object affected by this operation source",
754
+ );
755
+ const deterministicBusinessRole =
756
+ "object participating in the customer workflow";
757
+ const aiBusinessRole = firstNonEmpty(
758
+ sanitizeCandidateText(candidate.business_role, route),
759
+ "object participating in the customer workflow",
760
+ );
761
+ const aiEffectKind = EFFECT_KIND_VALUES.has(candidate.effect_kind)
762
+ ? candidate.effect_kind
763
+ : "unknown";
764
+ const deterministicEffectKind = deterministicEffectKindForRoute(route);
765
+ const aiResolutionBasis = RESOLUTION_BASIS_VALUES.has(
766
+ candidate.resolution_basis,
767
+ )
768
+ ? candidate.resolution_basis
769
+ : "unknown";
770
+ const deterministicResolutionBasis = deterministicResolutionBasisForRoute(
771
+ sourceEvidenceRefs,
772
+ routeEvidenceRefs,
773
+ );
774
+ const deterministicResolutionReason =
775
+ "Operation effect was detected from privacy-safe source evidence.";
776
+ const aiResolutionReason = firstNonEmpty(
777
+ sanitizeCandidateText(candidate.resolution_reason, route),
778
+ deterministicResolutionReason,
779
+ );
780
+ const targetKeyCandidate = rawTargetObjectText
781
+ ? slugSegment(rawTargetObjectText)
782
+ : null;
783
+ const aiEffectAction = normalizeEffectAction(
784
+ candidate.effect_action,
785
+ aiEffectKind,
786
+ );
787
+ const deterministicEffectAction = normalizeEffectAction(
788
+ candidate.effect_action,
789
+ deterministicEffectKind,
790
+ );
791
+ const aiExpectedDomainEffect =
792
+ targetKeyCandidate && aiEffectAction
793
+ ? `${targetKeyCandidate}.${aiEffectAction}`
794
+ : null;
795
+ const deterministicExpectedDomainEffect =
796
+ targetKeyCandidate && deterministicEffectAction
797
+ ? `${targetKeyCandidate}.${deterministicEffectAction}`
798
+ : null;
799
+
800
+ return {
801
+ sourceEvidenceRefs,
802
+ rawTargetObjectText,
803
+ targetKeyCandidate,
804
+ deterministicConcept,
805
+ aiConcept,
806
+ deterministicBusinessRole,
807
+ aiBusinessRole,
808
+ deterministicEffectKind,
809
+ aiEffectKind,
810
+ deterministicResolutionBasis,
811
+ aiResolutionBasis,
812
+ deterministicResolutionReason,
813
+ aiResolutionReason,
814
+ deterministicExpectedDomainEffect,
815
+ aiExpectedDomainEffect,
816
+ };
817
+ };
818
+
819
+ const buildSelectorCandidates = ({ routes, aiRoutes, hashScope }) => {
820
+ const selectionCandidates = [];
821
+ const routeBySelectionKey = new Map();
822
+
823
+ for (const route of routes) {
824
+ const routeEvidenceRefs = buildSourceEvidenceRefs(route, hashScope);
825
+ const aiRoute = aiRoutes.get(route.operation_source_key);
826
+ const candidates = normalizeAiOperationEffectCandidates(aiRoute);
827
+ candidates.forEach((candidate, candidateIndex) => {
828
+ const values = buildCandidateSemanticValues({
829
+ route,
830
+ candidate,
831
+ routeEvidenceRefs,
832
+ });
833
+ if (
834
+ candidateHasUnsafeSemanticText(candidate) ||
835
+ !values.rawTargetObjectText ||
836
+ !values.targetKeyCandidate ||
837
+ !values.aiExpectedDomainEffect ||
838
+ !hasBehaviorEvidence(values.sourceEvidenceRefs, routeEvidenceRefs) ||
839
+ !candidateHasTargetSpecificEvidence({
840
+ route,
841
+ rawTargetObjectText: values.rawTargetObjectText,
842
+ })
843
+ ) {
844
+ return;
845
+ }
846
+ const selectionKey = selectionKeyForCandidate(route, candidateIndex);
847
+ routeBySelectionKey.set(selectionKey, route);
848
+ const evidenceById = new Map(
849
+ routeEvidenceRefs.map((entry) => [entry.id, entry]),
850
+ );
851
+ selectionCandidates.push({
852
+ operation_source_key: route.operation_source_key,
853
+ candidate_index: candidateIndex,
854
+ evidence_summaries: values.sourceEvidenceRefs.map(
855
+ (ref, evidenceIndex) => {
856
+ const evidence = evidenceById.get(ref);
857
+ return {
858
+ evidence_index: evidenceIndex,
859
+ kind: evidence.kind,
860
+ summary: evidence.summary,
861
+ evidence_strength: evidence.evidence_strength,
862
+ };
863
+ },
864
+ ),
865
+ semantic_fields: [
866
+ {
867
+ parameter_name: "concept",
868
+ deterministic_candidate: values.deterministicConcept,
869
+ ai_candidate: values.aiConcept,
870
+ },
871
+ {
872
+ parameter_name: "business_role",
873
+ deterministic_candidate: values.deterministicBusinessRole,
874
+ ai_candidate: values.aiBusinessRole,
875
+ },
876
+ {
877
+ parameter_name: "effect_kind",
878
+ deterministic_candidate: values.deterministicEffectKind,
879
+ ai_candidate: values.aiEffectKind,
880
+ },
881
+ {
882
+ parameter_name: "expected_domain_effect",
883
+ deterministic_candidate: values.deterministicExpectedDomainEffect,
884
+ ai_candidate: values.aiExpectedDomainEffect,
885
+ },
886
+ {
887
+ parameter_name: "resolution.basis",
888
+ deterministic_candidate: values.deterministicResolutionBasis,
889
+ ai_candidate: values.aiResolutionBasis,
890
+ },
891
+ {
892
+ parameter_name: "resolution.reason",
893
+ deterministic_candidate: values.deterministicResolutionReason,
894
+ ai_candidate: values.aiResolutionReason,
895
+ },
896
+ ],
897
+ });
898
+ });
899
+ }
900
+
901
+ return { selectionCandidates, routeBySelectionKey };
902
+ };
903
+
904
+ const canAdoptCandidateValue = (value) =>
905
+ value !== undefined && value !== null && value !== "" && value !== "unknown";
906
+
907
+ const selectSemanticSource = ({
908
+ deterministicValue,
909
+ aiValue,
910
+ parameterName,
911
+ aiSelectionDecision,
912
+ }) => {
913
+ if (aiSelectionDecision?.selected_source === "ai") {
914
+ return canAdoptCandidateValue(aiValue) ? "ai" : null;
915
+ }
916
+ if (aiSelectionDecision?.selected_source === "deterministic") {
917
+ return canAdoptCandidateValue(deterministicValue) ? "deterministic" : null;
918
+ }
919
+ if (
920
+ parameterName === "effect_kind" &&
921
+ deterministicValue !== "unknown" &&
922
+ deterministicValue === aiValue
923
+ ) {
924
+ return "deterministic";
925
+ }
926
+ if (
927
+ parameterName === "resolution.basis" &&
928
+ deterministicValue !== "unknown" &&
929
+ deterministicValue === aiValue
930
+ ) {
931
+ return "deterministic";
932
+ }
933
+ return "ai";
934
+ };
935
+
936
+ const adoptSemanticValue = ({
937
+ deterministicValue,
938
+ aiValue,
939
+ parameterName,
940
+ aiSelectionDecision,
941
+ }) => {
942
+ const selectedSource = selectSemanticSource({
943
+ deterministicValue,
944
+ aiValue,
945
+ parameterName,
946
+ aiSelectionDecision,
947
+ });
948
+ if (selectedSource === "deterministic") {
949
+ return deterministicValue;
950
+ }
951
+ if (selectedSource === "ai") {
952
+ return aiValue;
953
+ }
954
+ return null;
955
+ };
956
+
957
+ const buildSemanticMeaningCandidate = ({
958
+ id,
959
+ fieldPath,
960
+ parameterName,
961
+ deterministicValue,
962
+ aiValue,
963
+ selectedSource,
964
+ selectionReason,
965
+ sourceEvidenceRefs,
966
+ aiInferenceEvidenceRef,
967
+ selectionAiInferenceEvidenceRef,
968
+ }) => ({
969
+ id,
970
+ field_path: fieldPath,
971
+ parameter_name: parameterName,
972
+ deterministic_candidate: {
973
+ value: deterministicValue,
974
+ source_evidence_refs: sourceEvidenceRefs,
975
+ missing_context: [],
976
+ },
977
+ ai_candidate: {
978
+ value: aiValue,
979
+ source_evidence_refs: sourceEvidenceRefs,
980
+ ai_inference_evidence_ref: aiInferenceEvidenceRef,
981
+ missing_context: [],
982
+ },
983
+ selected_source: selectedSource,
984
+ selection_reason:
985
+ selectionReason ??
986
+ (selectedSource === "ai"
987
+ ? "AI candidate was selected from privacy-safe evidence for this semantic field."
988
+ : "Deterministic candidate was selected because it satisfied the schema and evidence boundary."),
989
+ selection_ai_inference_evidence_ref: selectionAiInferenceEvidenceRef,
990
+ });
991
+
992
+ const unresolvedEffect = ({
993
+ route,
994
+ candidate,
995
+ sourceEvidenceRefs,
996
+ reason,
997
+ missingContext,
998
+ }) => ({
999
+ id: `unresolved_effect_${shortHash(`${route.operation_source_key}:${JSON.stringify(candidate)}`)}`,
1000
+ operation_source_key: route.operation_source_key,
1001
+ reason,
1002
+ source_evidence_refs: sourceEvidenceRefs,
1003
+ missing_context: safeArray(missingContext ?? candidate?.missing_context)
1004
+ .filter((entry) => typeof entry === "string" && entry.trim())
1005
+ .map((entry) => sanitizeText(entry.trim(), route)),
1006
+ });
1007
+
1008
+ const buildOperationAssignmentCollections = ({
1009
+ route,
1010
+ aiRoute,
1011
+ routeEvidenceRefs,
1012
+ catalogByGroup,
1013
+ aiSelectionDecisions,
1014
+ }) => {
1015
+ const operationEffects = [];
1016
+ const unresolvedOperationEffects = [];
1017
+ const profiles = [];
1018
+ const mappings = [];
1019
+ const aiInferenceEvidence = [];
1020
+ const semanticMeaningCandidates = [];
1021
+ const emittedEffectKeys = new Set();
1022
+
1023
+ const candidates = normalizeAiOperationEffectCandidates(aiRoute);
1024
+ for (
1025
+ let candidateIndex = 0;
1026
+ candidateIndex < candidates.length;
1027
+ candidateIndex += 1
1028
+ ) {
1029
+ const candidate = candidates[candidateIndex];
1030
+ const selectionDecisions =
1031
+ aiSelectionDecisions.get(
1032
+ selectionKeyForCandidate(route, candidateIndex),
1033
+ ) ?? new Map();
1034
+ const values = buildCandidateSemanticValues({
1035
+ route,
1036
+ candidate,
1037
+ routeEvidenceRefs,
1038
+ });
1039
+ const sourceEvidenceRefs = values.sourceEvidenceRefs;
1040
+ if (candidateHasUnsafeSemanticText(candidate)) {
1041
+ unresolvedOperationEffects.push(
1042
+ unresolvedEffect({
1043
+ route,
1044
+ candidate,
1045
+ sourceEvidenceRefs,
1046
+ reason:
1047
+ "AI operation effect candidate contained raw implementation or secret-like text, so no formal operation effect was emitted.",
1048
+ }),
1049
+ );
1050
+ continue;
1051
+ }
1052
+ const rawTargetObjectText = values.rawTargetObjectText;
1053
+ const targetKeyCandidate = values.targetKeyCandidate;
1054
+ const deterministicConcept = values.deterministicConcept;
1055
+ const aiConcept = values.aiConcept;
1056
+ const concept = adoptSemanticValue({
1057
+ deterministicValue: deterministicConcept,
1058
+ aiValue: aiConcept,
1059
+ parameterName: "concept",
1060
+ aiSelectionDecision: selectionDecisions.get("concept"),
1061
+ });
1062
+ const deterministicBusinessRole = values.deterministicBusinessRole;
1063
+ const aiBusinessRole = values.aiBusinessRole;
1064
+ const businessRole = adoptSemanticValue({
1065
+ deterministicValue: deterministicBusinessRole,
1066
+ aiValue: aiBusinessRole,
1067
+ parameterName: "business_role",
1068
+ aiSelectionDecision: selectionDecisions.get("business_role"),
1069
+ });
1070
+ const aiEffectKind = values.aiEffectKind;
1071
+ const deterministicEffectKind = values.deterministicEffectKind;
1072
+ const aiResolutionBasis = values.aiResolutionBasis;
1073
+ const deterministicResolutionBasis = values.deterministicResolutionBasis;
1074
+ const effectKind = adoptSemanticValue({
1075
+ deterministicValue: deterministicEffectKind,
1076
+ aiValue: aiEffectKind,
1077
+ parameterName: "effect_kind",
1078
+ aiSelectionDecision: selectionDecisions.get("effect_kind"),
1079
+ });
1080
+ const resolutionBasis = adoptSemanticValue({
1081
+ deterministicValue: deterministicResolutionBasis,
1082
+ aiValue: aiResolutionBasis,
1083
+ parameterName: "resolution.basis",
1084
+ aiSelectionDecision: selectionDecisions.get("resolution.basis"),
1085
+ });
1086
+ const deterministicResolutionReason = values.deterministicResolutionReason;
1087
+ const aiResolutionReason = values.aiResolutionReason;
1088
+ const resolutionReason = adoptSemanticValue({
1089
+ deterministicValue: deterministicResolutionReason,
1090
+ aiValue: aiResolutionReason,
1091
+ parameterName: "resolution.reason",
1092
+ aiSelectionDecision: selectionDecisions.get("resolution.reason"),
1093
+ });
1094
+ const expectedDomainEffect = adoptSemanticValue({
1095
+ deterministicValue: values.deterministicExpectedDomainEffect,
1096
+ aiValue: values.aiExpectedDomainEffect,
1097
+ parameterName: "expected_domain_effect",
1098
+ aiSelectionDecision: selectionDecisions.get("expected_domain_effect"),
1099
+ });
1100
+ const effectAction = expectedDomainEffect?.split(".")[1] ?? null;
1101
+
1102
+ if (
1103
+ !rawTargetObjectText ||
1104
+ !targetKeyCandidate ||
1105
+ !effectAction ||
1106
+ !hasBehaviorEvidence(sourceEvidenceRefs, routeEvidenceRefs) ||
1107
+ !candidateHasTargetSpecificEvidence({
1108
+ route,
1109
+ rawTargetObjectText,
1110
+ })
1111
+ ) {
1112
+ unresolvedOperationEffects.push(
1113
+ unresolvedEffect({
1114
+ route,
1115
+ candidate,
1116
+ sourceEvidenceRefs,
1117
+ reason:
1118
+ "Target object, effect action, or supporting behavior evidence was insufficient, so no formal operation effect was emitted.",
1119
+ }),
1120
+ );
1121
+ continue;
1122
+ }
1123
+
1124
+ const requiredSelectionParameters = [
1125
+ "concept",
1126
+ "business_role",
1127
+ "effect_kind",
1128
+ "expected_domain_effect",
1129
+ "resolution.basis",
1130
+ "resolution.reason",
1131
+ ];
1132
+ const missingSelectionParameters = requiredSelectionParameters.filter(
1133
+ (parameterName) => !selectionDecisions.has(parameterName),
1134
+ );
1135
+ if (missingSelectionParameters.length > 0) {
1136
+ unresolvedOperationEffects.push(
1137
+ unresolvedEffect({
1138
+ route,
1139
+ candidate,
1140
+ sourceEvidenceRefs,
1141
+ reason:
1142
+ "Higher-level AI selector did not return complete field-level selection decisions, so no formal operation effect was emitted.",
1143
+ }),
1144
+ );
1145
+ continue;
1146
+ }
1147
+ const selectionMissingContext = requiredSelectionParameters.flatMap(
1148
+ (parameterName) =>
1149
+ selectionDecisions.get(parameterName)?.missing_context ?? [],
1150
+ );
1151
+ if (selectionMissingContext.length > 0) {
1152
+ unresolvedOperationEffects.push(
1153
+ unresolvedEffect({
1154
+ route,
1155
+ candidate,
1156
+ sourceEvidenceRefs,
1157
+ reason:
1158
+ "Higher-level AI selector reported missing context, so no formal operation effect was emitted.",
1159
+ missingContext: selectionMissingContext,
1160
+ }),
1161
+ );
1162
+ continue;
1163
+ }
1164
+ if (
1165
+ !canAdoptCandidateValue(concept) ||
1166
+ !canAdoptCandidateValue(businessRole) ||
1167
+ !canAdoptCandidateValue(effectKind) ||
1168
+ !canAdoptCandidateValue(expectedDomainEffect) ||
1169
+ !canAdoptCandidateValue(resolutionBasis) ||
1170
+ !canAdoptCandidateValue(resolutionReason)
1171
+ ) {
1172
+ unresolvedOperationEffects.push(
1173
+ unresolvedEffect({
1174
+ route,
1175
+ candidate,
1176
+ sourceEvidenceRefs,
1177
+ reason:
1178
+ "Higher-level AI selector chose an unsafe or unknown value, so no formal operation effect was emitted.",
1179
+ }),
1180
+ );
1181
+ continue;
1182
+ }
1183
+
1184
+ const groupKey = `${concept.toLowerCase()}::${businessRole.toLowerCase()}`;
1185
+ const existingCatalog = catalogByGroup.get(groupKey);
1186
+ const targetObjectKey =
1187
+ existingCatalog?.target_object_key ?? targetKeyCandidate;
1188
+ const operationEffectKey = `${targetObjectKey}.${effectAction}`;
1189
+ if (!isValidEffectKey(operationEffectKey, targetObjectKey)) {
1190
+ unresolvedOperationEffects.push(
1191
+ unresolvedEffect({
1192
+ route,
1193
+ candidate,
1194
+ sourceEvidenceRefs,
1195
+ reason:
1196
+ "Generated effect key would violate the target object key naming contract.",
1197
+ }),
1198
+ );
1199
+ continue;
1200
+ }
1201
+ if (emittedEffectKeys.has(operationEffectKey)) {
1202
+ unresolvedOperationEffects.push(
1203
+ unresolvedEffect({
1204
+ route,
1205
+ candidate,
1206
+ sourceEvidenceRefs,
1207
+ reason:
1208
+ "Duplicate operation effect key was detected for this route, so the duplicate candidate was preserved as unresolved evidence.",
1209
+ }),
1210
+ );
1211
+ continue;
1212
+ }
1213
+ emittedEffectKeys.add(operationEffectKey);
1214
+
1215
+ const suffix = `${targetObjectKey}_${effectAction}_${shortHash(route.operation_source_key)}`;
1216
+ const profileId = `target_profile_${suffix}`;
1217
+ const mappingId = `target_mapping_${suffix}`;
1218
+ const aiProfileEvidenceId = `ai_inference_profile_${suffix}`;
1219
+ const aiSelectionEvidenceId = `ai_inference_select_${suffix}`;
1220
+ const profileFieldPrefix = `target_object_profiles[${profileId}]`;
1221
+ const effectFieldPrefix = `routes[${route.operation_source_key}].operation_effects[${operationEffectKey}]`;
1222
+
1223
+ profiles.push({
1224
+ id: profileId,
1225
+ operation_source_key: route.operation_source_key,
1226
+ operation_effect_key: operationEffectKey,
1227
+ raw_target_object_text: rawTargetObjectText,
1228
+ concept,
1229
+ business_role: businessRole,
1230
+ evidence_refs: sourceEvidenceRefs,
1231
+ source_evidence_refs: sourceEvidenceRefs,
1232
+ missing_context: [],
1233
+ });
1234
+
1235
+ aiInferenceEvidence.push({
1236
+ id: aiProfileEvidenceId,
1237
+ task: "target_object_profile_extraction",
1238
+ input_source_evidence_refs: sourceEvidenceRefs,
1239
+ output_field_paths: [
1240
+ `${profileFieldPrefix}.raw_target_object_text`,
1241
+ `${profileFieldPrefix}.concept`,
1242
+ `${profileFieldPrefix}.business_role`,
1243
+ ],
1244
+ reasoning_summary: firstNonEmpty(
1245
+ sanitizeCandidateText(candidate.reasoning_summary, route),
1246
+ "AI candidate described the target object profile from privacy-safe source evidence.",
1247
+ ),
1248
+ missing_context: [],
1249
+ });
1250
+ aiInferenceEvidence.push({
1251
+ id: aiSelectionEvidenceId,
1252
+ task: "semantic_meaning_candidate_selection",
1253
+ input_source_evidence_refs: sourceEvidenceRefs,
1254
+ output_field_paths: [
1255
+ `${profileFieldPrefix}.concept`,
1256
+ `${profileFieldPrefix}.business_role`,
1257
+ `${effectFieldPrefix}.effect_kind`,
1258
+ `${effectFieldPrefix}.expected_domain_effect`,
1259
+ `${effectFieldPrefix}.resolution.basis`,
1260
+ `${effectFieldPrefix}.resolution.reason`,
1261
+ ],
1262
+ reasoning_summary:
1263
+ "Higher-level AI selector adopted schema-valid semantic values while preserving deterministic and AI candidates separately.",
1264
+ missing_context: [],
1265
+ });
1266
+
1267
+ const candidateSpecs = [
1268
+ {
1269
+ parameterName: "concept",
1270
+ fieldPath: `${profileFieldPrefix}.concept`,
1271
+ deterministicValue: deterministicConcept,
1272
+ aiValue: aiConcept,
1273
+ selectedValue: concept,
1274
+ },
1275
+ {
1276
+ parameterName: "business_role",
1277
+ fieldPath: `${profileFieldPrefix}.business_role`,
1278
+ deterministicValue: deterministicBusinessRole,
1279
+ aiValue: aiBusinessRole,
1280
+ selectedValue: businessRole,
1281
+ },
1282
+ {
1283
+ parameterName: "effect_kind",
1284
+ fieldPath: `${effectFieldPrefix}.effect_kind`,
1285
+ deterministicValue: deterministicEffectKind,
1286
+ aiValue: aiEffectKind,
1287
+ selectedValue: effectKind,
1288
+ },
1289
+ {
1290
+ parameterName: "expected_domain_effect",
1291
+ fieldPath: `${effectFieldPrefix}.expected_domain_effect`,
1292
+ deterministicValue: values.deterministicExpectedDomainEffect,
1293
+ aiValue: values.aiExpectedDomainEffect,
1294
+ selectedValue: expectedDomainEffect,
1295
+ },
1296
+ {
1297
+ parameterName: "resolution.basis",
1298
+ fieldPath: `${effectFieldPrefix}.resolution.basis`,
1299
+ deterministicValue: deterministicResolutionBasis,
1300
+ aiValue: aiResolutionBasis,
1301
+ selectedValue: resolutionBasis,
1302
+ },
1303
+ {
1304
+ parameterName: "resolution.reason",
1305
+ fieldPath: `${effectFieldPrefix}.resolution.reason`,
1306
+ deterministicValue: deterministicResolutionReason,
1307
+ aiValue: aiResolutionReason,
1308
+ selectedValue: resolutionReason,
1309
+ },
1310
+ ];
1311
+
1312
+ for (const spec of candidateSpecs) {
1313
+ semanticMeaningCandidates.push(
1314
+ buildSemanticMeaningCandidate({
1315
+ id: `semantic_candidate_${shortHash(`${route.operation_source_key}:${operationEffectKey}:${spec.parameterName}`)}`,
1316
+ fieldPath: spec.fieldPath,
1317
+ parameterName: spec.parameterName,
1318
+ deterministicValue: spec.deterministicValue,
1319
+ aiValue: spec.aiValue,
1320
+ selectedSource: selectSemanticSource({
1321
+ deterministicValue: spec.deterministicValue,
1322
+ aiValue: spec.aiValue,
1323
+ parameterName: spec.parameterName,
1324
+ aiSelectionDecision: selectionDecisions.get(spec.parameterName),
1325
+ }),
1326
+ selectionReason: selectionDecisions.get(spec.parameterName)
1327
+ ?.selection_reason,
1328
+ sourceEvidenceRefs,
1329
+ aiInferenceEvidenceRef: aiProfileEvidenceId,
1330
+ selectionAiInferenceEvidenceRef: aiSelectionEvidenceId,
1331
+ }),
1332
+ );
1333
+ }
1334
+
1335
+ const catalogEntry = existingCatalog ?? {
1336
+ target_object_key: targetObjectKey,
1337
+ display_name: displayNameFromKey(targetObjectKey),
1338
+ concept,
1339
+ business_role: businessRole,
1340
+ aliases: [],
1341
+ raw_target_object_profile_refs: [],
1342
+ grouping_evidence_refs: [],
1343
+ missing_context: [],
1344
+ };
1345
+ if (!catalogEntry.aliases.includes(rawTargetObjectText)) {
1346
+ catalogEntry.aliases.push(rawTargetObjectText);
1347
+ }
1348
+ if (!catalogEntry.raw_target_object_profile_refs.includes(profileId)) {
1349
+ catalogEntry.raw_target_object_profile_refs.push(profileId);
1350
+ }
1351
+ for (const ref of sourceEvidenceRefs) {
1352
+ if (!catalogEntry.grouping_evidence_refs.includes(ref)) {
1353
+ catalogEntry.grouping_evidence_refs.push(ref);
1354
+ }
1355
+ }
1356
+ catalogByGroup.set(groupKey, catalogEntry);
1357
+
1358
+ mappings.push({
1359
+ id: mappingId,
1360
+ operation_source_key: route.operation_source_key,
1361
+ operation_effect_key: operationEffectKey,
1362
+ raw_target_object_profile_ref: profileId,
1363
+ target_object_key: targetObjectKey,
1364
+ grouping_evidence_ref: sourceEvidenceRefs[0],
1365
+ mapping_reason: `The ${operationEffectKey} effect maps to the normalized ${targetObjectKey} catalog entry through privacy-safe source evidence.`,
1366
+ missing_context: [],
1367
+ });
1368
+
1369
+ operationEffects.push({
1370
+ operation_effect_key: operationEffectKey,
1371
+ effect_kind: effectKind,
1372
+ expected_domain_effect: operationEffectKey,
1373
+ target_object_key: targetObjectKey,
1374
+ target_object_profile_ref: profileId,
1375
+ target_object_mapping_ref: mappingId,
1376
+ resolution: {
1377
+ basis: resolutionBasis,
1378
+ reason: resolutionReason,
1379
+ },
1380
+ source_evidence_refs: sourceEvidenceRefs,
1381
+ missing_context: [],
1382
+ });
1383
+ }
1384
+
1385
+ return {
1386
+ operationEffects,
1387
+ unresolvedOperationEffects,
1388
+ profiles,
1389
+ mappings,
1390
+ aiInferenceEvidence,
1391
+ semanticMeaningCandidates,
1392
+ };
1393
+ };
1394
+
1395
+ const buildSnapshot = ({ request, routes, aiRoutes, aiSelectionDecisions }) => {
1396
+ const generatedAt = new Date().toISOString();
1397
+ const hashScope = semanticSnapshotHashScope(request);
1398
+ const semanticSnapshotVersion = buildSemanticSnapshotVersion({
1399
+ request,
1400
+ generatedAt,
1401
+ });
1402
+ const targetObjectProfiles = [];
1403
+ const targetObjectMappings = [];
1404
+ const sourceEvidenceRefs = [];
1405
+ const aiInferenceEvidence = [];
1406
+ const semanticMeaningCandidates = [];
1407
+ const catalogByGroup = new Map();
1408
+
1409
+ const snapshotRoutes = routes.map((route) => {
1410
+ const routeWithVersion = {
1411
+ ...route,
1412
+ semantic_snapshot_version: semanticSnapshotVersion,
1413
+ };
1414
+ const routeEvidenceRefs = buildSourceEvidenceRefs(
1415
+ routeWithVersion,
1416
+ hashScope,
1417
+ );
1418
+ sourceEvidenceRefs.push(...routeEvidenceRefs);
1419
+ const aiRoute = aiRoutes.get(route.operation_source_key);
1420
+ const baseRoute = !aiRoute?.semantics
1421
+ ? fallbackSemantics(
1422
+ routeWithVersion,
1423
+ "AI semantics were unavailable for this route.",
1424
+ hashScope,
1425
+ )
1426
+ : {
1427
+ operation_source_key: route.operation_source_key,
1428
+ semantic_snapshot_version: semanticSnapshotVersion,
1429
+ method: route.method,
1430
+ path_template: route.path_template,
1431
+ semantics: sanitizeSemantics(aiRoute.semantics, route),
1432
+ layer_evidence: sanitizeLayerEvidence(
1433
+ aiRoute.layer_evidence,
1434
+ route,
1435
+ hashScope,
1436
+ ),
1437
+ confidence_reason: sanitizeText(
1438
+ String(
1439
+ aiRoute.confidence_reason ||
1440
+ "Generated by AI from privacy-safe route evidence.",
1441
+ ),
1442
+ route,
1443
+ ),
1444
+ };
1445
+ const assignmentCollections = buildOperationAssignmentCollections({
1446
+ route,
1447
+ aiRoute,
1448
+ routeEvidenceRefs,
1449
+ catalogByGroup,
1450
+ aiSelectionDecisions,
1451
+ });
1452
+ targetObjectProfiles.push(...assignmentCollections.profiles);
1453
+ targetObjectMappings.push(...assignmentCollections.mappings);
1454
+ aiInferenceEvidence.push(...assignmentCollections.aiInferenceEvidence);
1455
+ semanticMeaningCandidates.push(
1456
+ ...assignmentCollections.semanticMeaningCandidates,
1457
+ );
1458
+ return {
1459
+ ...baseRoute,
1460
+ operation_effects: assignmentCollections.operationEffects,
1461
+ unresolved_operation_effects:
1462
+ assignmentCollections.unresolvedOperationEffects,
1463
+ source_evidence_refs: routeEvidenceRefs.map((entry) => entry.id),
1464
+ };
1465
+ });
1466
+
1467
+ return validateSemanticSnapshotRequest({
1468
+ project_key: request.project_key,
1469
+ idempotency_key: sha256(
1470
+ `${request.project_key}:${request.repository.repository_id}:${request.repository.merge_commit}:${request.repository.workflow_run_id ?? generatedAt}`,
1471
+ ),
1472
+ schema_version: 2,
1473
+ semantic_snapshot_version: semanticSnapshotVersion,
1474
+ generated_at: generatedAt,
1475
+ repository: request.repository,
1476
+ service: request.service,
1477
+ analysis_summary: {
1478
+ total_routes_scanned: routes.length,
1479
+ routes_generated: snapshotRoutes.filter(
1480
+ (route) => route.semantics.route_confidence > 0,
1481
+ ).length,
1482
+ uncertain_routes: snapshotRoutes.filter(
1483
+ (route) => route.semantics.route_confidence < 0.5,
1484
+ ).length,
1485
+ failed_routes: 0,
1486
+ },
1487
+ routes: snapshotRoutes,
1488
+ target_object_profiles: targetObjectProfiles,
1489
+ target_object_catalog: [...catalogByGroup.values()],
1490
+ target_object_mappings: targetObjectMappings,
1491
+ source_evidence_refs: sourceEvidenceRefs,
1492
+ ai_inference_evidence: aiInferenceEvidence,
1493
+ semantic_meaning_candidates: semanticMeaningCandidates,
1494
+ });
1495
+ };
1496
+
1497
+ const sanitizeText = (value, route) => {
1498
+ let result = value;
1499
+ const forbiddenTokens = [
1500
+ route.ai_context.handler_name,
1501
+ route.ai_context.relative_path,
1502
+ ].filter((token) => typeof token === "string" && token.length > 0);
1503
+ for (const token of forbiddenTokens) {
1504
+ result = result.split(token).join("[component]");
1505
+ }
1506
+ result = result
1507
+ .replace(
1508
+ /\b(select|insert|update|delete)\s+.+?\b(from|into|set)\b[^"',;}]+/gi,
1509
+ "[sql]",
1510
+ )
1511
+ .replace(
1512
+ /\b(system|user|assistant)\s*:\s*[^"',;}]*(prompt|completion|transcript)[^"',;}]*/gi,
1513
+ "[prompt]",
1514
+ )
1515
+ .replace(
1516
+ /\b[A-Za-z_][A-Za-z0-9_]*(Controller|Service|Repository|UseCase|Handler|Model)\b/g,
1517
+ "[component]",
1518
+ )
1519
+ .replace(
1520
+ /\b[A-Za-z_][A-Za-z0-9_]*(Request|Response|DTO|Dto|Schema|Payload|Input|Output|Params|Parameters|Body)\b/g,
1521
+ "[component]",
1522
+ )
1523
+ .replace(
1524
+ /\b[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*){2,}\b/g,
1525
+ "[component]",
1526
+ )
1527
+ .replace(
1528
+ /\b[A-Z][A-Z0-9_]*(?:API_KEY|SECRET|TOKEN|PASSWORD|PRIVATE_KEY)\b(?:\s*=\s*["']?[^"'\s,;}]+)?/g,
1529
+ "[secret]",
1530
+ )
1531
+ .replace(/\b[A-Z][A-Z0-9_]{2,}\s*=\s*["']?[^"'\s,;}]+/g, "[env]")
1532
+ .replace(
1533
+ /\b(?:api[_-]?key|secret|token|password)\s*[:=]\s*["']?[^"'\s,;}]+/gi,
1534
+ "[secret]",
1535
+ )
1536
+ .replace(
1537
+ /\b(?:bind|param|parameter)[ _-]?[A-Za-z0-9_]*\s*[:=]\s*["']?[^"'\s,;}]+/gi,
1538
+ "[bind]",
1539
+ )
1540
+ .replace(/[:@$][A-Za-z_][A-Za-z0-9_]*\s*=\s*["']?[^"'\s,;}]+/g, "[bind]")
1541
+ .replace(/\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi, "Bearer [secret]")
1542
+ .replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, "[secret]")
1543
+ .replace(/\$[A-Z][A-Z0-9_]+/g, "[env]");
1544
+ return result;
1545
+ };
1546
+
1547
+ const sanitizeSemantics = (value, route) => {
1548
+ const record = sanitizeForClueStorage(value, route);
1549
+ const safeRecord =
1550
+ record && typeof record === "object" && !Array.isArray(record)
1551
+ ? record
1552
+ : {};
1553
+ return {
1554
+ route_summary:
1555
+ typeof safeRecord.route_summary === "string" &&
1556
+ safeRecord.route_summary.trim()
1557
+ ? safeRecord.route_summary
1558
+ : "unknown",
1559
+ action_candidates: Array.isArray(safeRecord.action_candidates)
1560
+ ? safeRecord.action_candidates
1561
+ : [],
1562
+ outcome_candidates: Array.isArray(safeRecord.outcome_candidates)
1563
+ ? safeRecord.outcome_candidates
1564
+ : [],
1565
+ value_event_candidates: [],
1566
+ route_confidence: Number.isFinite(Number(safeRecord.route_confidence))
1567
+ ? Math.max(0, Math.min(1, Number(safeRecord.route_confidence)))
1568
+ : 0,
1569
+ };
1570
+ };
1571
+
1572
+ const sanitizeForClueStorage = (value, route) => {
1573
+ if (typeof value === "string") {
1574
+ return sanitizeText(value, route);
1575
+ }
1576
+ if (Array.isArray(value)) {
1577
+ return value.map((entry) => sanitizeForClueStorage(entry, route));
1578
+ }
1579
+ if (value && typeof value === "object") {
1580
+ return Object.fromEntries(
1581
+ Object.entries(value).map(([key, entry]) => [
1582
+ key,
1583
+ sanitizeForClueStorage(entry, route),
1584
+ ]),
1585
+ );
1586
+ }
1587
+ return value;
1588
+ };
1589
+
1590
+ const stringArray = (value, route) =>
1591
+ Array.isArray(value)
1592
+ ? value
1593
+ .filter((entry) => typeof entry === "string" && entry.trim())
1594
+ .map((entry) => sanitizeText(entry.trim(), route))
1595
+ : [];
1596
+
1597
+ const confidenceOrUndefined = (value) => {
1598
+ const numberValue = Number(value);
1599
+ if (!Number.isFinite(numberValue)) {
1600
+ return undefined;
1601
+ }
1602
+ return Math.max(0, Math.min(1, numberValue));
1603
+ };
1604
+
1605
+ const normalizeMutationKind = (value) =>
1606
+ ["create", "update", "delete", "upsert", "read", "unknown"].includes(value)
1607
+ ? value
1608
+ : "unknown";
1609
+
1610
+ const sanitizeLayerEvidence = (value, route, hashScope) => {
1611
+ const record =
1612
+ value && typeof value === "object" && !Array.isArray(value) ? value : {};
1613
+ const dataEffects = Array.isArray(record.data_effects)
1614
+ ? record.data_effects
1615
+ .filter(
1616
+ (entry) =>
1617
+ entry &&
1618
+ typeof entry === "object" &&
1619
+ typeof entry.entity_label === "string",
1620
+ )
1621
+ .map((entry) => ({
1622
+ entity_label: sanitizeText(entry.entity_label, route),
1623
+ ...(typeof entry.mutation_kind === "string"
1624
+ ? { mutation_kind: normalizeMutationKind(entry.mutation_kind) }
1625
+ : {}),
1626
+ ...(confidenceOrUndefined(entry.confidence) === undefined
1627
+ ? {}
1628
+ : { confidence: confidenceOrUndefined(entry.confidence) }),
1629
+ }))
1630
+ : [];
1631
+ const sideEffects = Array.isArray(record.side_effects)
1632
+ ? record.side_effects
1633
+ .filter(
1634
+ (entry) =>
1635
+ entry &&
1636
+ typeof entry === "object" &&
1637
+ typeof entry.side_effect_kind === "string",
1638
+ )
1639
+ .map((entry) => ({
1640
+ side_effect_kind: sanitizeText(entry.side_effect_kind, route),
1641
+ ...(confidenceOrUndefined(entry.confidence) === undefined
1642
+ ? {}
1643
+ : { confidence: confidenceOrUndefined(entry.confidence) }),
1644
+ }))
1645
+ : [];
1646
+
1647
+ return {
1648
+ data_effects: dataEffects,
1649
+ side_effects: sideEffects,
1650
+ failure_surfaces: stringArray(record.failure_surfaces, route),
1651
+ validation_field_paths: stringArray(record.validation_field_paths, route),
1652
+ permission_scopes: stringArray(record.permission_scopes, route),
1653
+ component_fingerprints: [
1654
+ opaqueRouteHash(route, "route_contract_component", hashScope),
1655
+ opaqueRouteHash(route, "abstract_behavior_component", hashScope),
1656
+ opaqueRouteHash(route, "source_component", hashScope),
1657
+ ],
1658
+ };
1659
+ };
1660
+
1661
+ const FORBIDDEN_CODE_STRUCTURE_PATTERNS = [
1662
+ { label: "file path", pattern: /\b[A-Za-z0-9_./-]+\.py\b/i },
1663
+ {
1664
+ label: "import statement",
1665
+ pattern:
1666
+ /\b(from\s+[A-Za-z_][A-Za-z0-9_.]*\s+import|import\s+[A-Za-z_][A-Za-z0-9_.]*)\b/i,
1667
+ },
1668
+ {
1669
+ label: "code definition",
1670
+ pattern: /\b(class|def)\s+[A-Za-z_][A-Za-z0-9_]*/,
1671
+ },
1672
+ {
1673
+ label: "internal component name",
1674
+ pattern:
1675
+ /\b[A-Za-z_][A-Za-z0-9_]*(Controller|Service|Repository|UseCase|Handler|Model)\b/,
1676
+ },
1677
+ {
1678
+ label: "code artifact identifier",
1679
+ pattern:
1680
+ /\b[A-Za-z_][A-Za-z0-9_]*(Request|Response|DTO|Dto|Schema|Payload|Input|Output|Params|Parameters|Body)\b/,
1681
+ },
1682
+ {
1683
+ label: "module path",
1684
+ pattern: /\b[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*){2,}\b/,
1685
+ },
1686
+ {
1687
+ label: "raw sql",
1688
+ pattern: /\b(select|insert|update|delete)\s+.+\b(from|into|set)\b/i,
1689
+ },
1690
+ {
1691
+ label: "raw prompt",
1692
+ pattern:
1693
+ /\b(system|user|assistant)\s*:\s*.+\b(prompt|completion|transcript)\b/i,
1694
+ },
1695
+ {
1696
+ label: "secret",
1697
+ pattern:
1698
+ /\b[A-Z][A-Z0-9_]*(?:API_KEY|SECRET|TOKEN|PASSWORD|PRIVATE_KEY)\b(?:\s*=\s*["']?[^"'\s,;}]+)?/,
1699
+ },
1700
+ {
1701
+ label: "environment variable",
1702
+ pattern: /\b[A-Z][A-Z0-9_]{2,}\s*=\s*["']?[^"'\s,;}]+/,
1703
+ },
1704
+ {
1705
+ label: "credential",
1706
+ pattern:
1707
+ /\b(?:api[_-]?key|secret|token|password)\s*[:=]\s*["']?[^"'\s,;}]+/i,
1708
+ },
1709
+ {
1710
+ label: "bind value",
1711
+ pattern:
1712
+ /\b(?:bind|param|parameter)[ _-]?[A-Za-z0-9_]*\s*[:=]\s*["']?[^"'\s,;}]+/i,
1713
+ },
1714
+ {
1715
+ label: "bind value",
1716
+ pattern: /[:@$][A-Za-z_][A-Za-z0-9_]*\s*=\s*["']?[^"'\s,;}]+/,
1717
+ },
1718
+ {
1719
+ label: "bearer token",
1720
+ pattern: /\bBearer\s+(?!\[secret\])[A-Za-z0-9._~+/-]+=*/i,
1721
+ },
1722
+ { label: "secret token", pattern: /\bsk-[A-Za-z0-9_-]{8,}\b/ },
1723
+ ];
1724
+
1725
+ const assertNoRawCodeStructure = (value, path = "snapshot") => {
1726
+ if (typeof value === "string") {
1727
+ for (const rule of FORBIDDEN_CODE_STRUCTURE_PATTERNS) {
1728
+ if (rule.pattern.test(value)) {
1729
+ throw new Error(`semantic snapshot contains ${rule.label} at ${path}`);
1730
+ }
1731
+ }
1732
+ return;
1733
+ }
1734
+ if (Array.isArray(value)) {
1735
+ value.forEach((entry, index) =>
1736
+ assertNoRawCodeStructure(entry, `${path}[${index}]`),
1737
+ );
1738
+ return;
1739
+ }
1740
+ if (value && typeof value === "object") {
1741
+ for (const [key, entry] of Object.entries(value)) {
1742
+ assertNoRawCodeStructure(entry, `${path}.${key}`);
1743
+ }
1744
+ }
1745
+ };
1746
+
1747
+ const semanticSnapshotUrl = (baseUrl) => {
1748
+ const normalized = baseUrl.replace(/\/+$/, "");
1749
+ if (normalized.endsWith("/api/v1/semantic-snapshots")) {
1750
+ return normalized;
1751
+ }
1752
+ if (normalized.endsWith("/api/v1")) {
1753
+ return `${normalized}/semantic-snapshots`;
1754
+ }
1755
+ return `${normalized}/api/v1/semantic-snapshots`;
1756
+ };
1757
+
1758
+ const sendSnapshot = async ({ request, env, snapshot }) => {
1759
+ const apiKey = env.CLUE_API_KEY;
1760
+ if (!apiKey) {
1761
+ throw new Error("CLUE_API_KEY is required");
1762
+ }
1763
+ const validatedSnapshot = validateSemanticSnapshotRequest(snapshot);
1764
+ assertNoRawCodeStructure(validatedSnapshot);
1765
+ const response = await fetch(semanticSnapshotUrl(request.clue_api_base_url), {
1766
+ method: "POST",
1767
+ headers: {
1768
+ "content-type": "application/json",
1769
+ authorization: `Bearer ${apiKey}`,
1770
+ },
1771
+ body: JSON.stringify(validatedSnapshot),
1772
+ });
1773
+ if (!response.ok) {
1774
+ throw new Error(`Clue semantic snapshot upload failed: ${response.status}`);
1775
+ }
1776
+ return response.json();
1777
+ };
1778
+
1779
+ export const runSemanticCi = async ({ repoRoot, request: rawRequest, env }) => {
1780
+ const request = validateSemanticCiRequest(rawRequest);
1781
+ const hashScope = semanticSnapshotHashScope(request);
1782
+ const files = await listAllowedPythonFiles({
1783
+ repoRoot,
1784
+ allowedSourcePaths: request.allowed_source_paths,
1785
+ excludedSourcePaths: request.excluded_source_paths,
1786
+ });
1787
+ const routes = await analyzeFastApiRoutes({ repoRoot, files });
1788
+ const promptRoutes = routes.map((route) => ({
1789
+ operation_source_key: route.operation_source_key,
1790
+ method: route.method,
1791
+ path_template: route.path_template,
1792
+ evidence_summaries: buildSourceEvidenceRefs(route, hashScope).map(
1793
+ (entry, index) => ({
1794
+ evidence_index: index,
1795
+ kind: entry.kind,
1796
+ summary: entry.summary,
1797
+ evidence_strength: entry.evidence_strength,
1798
+ }),
1799
+ ),
1800
+ }));
1801
+ const aiProviderApiKey = requireAiProviderApiKey(env);
1802
+ const aiRoutes = await callAiProvider({
1803
+ request,
1804
+ apiKey: aiProviderApiKey,
1805
+ routes: promptRoutes,
1806
+ });
1807
+ const { selectionCandidates, routeBySelectionKey } = buildSelectorCandidates({
1808
+ routes,
1809
+ aiRoutes,
1810
+ hashScope,
1811
+ });
1812
+ const aiSelectionDecisions = await callAiSelectionProvider({
1813
+ request,
1814
+ apiKey: aiProviderApiKey,
1815
+ selectionCandidates,
1816
+ routeBySelectionKey,
1817
+ });
1818
+ const snapshot = buildSnapshot({
1819
+ request,
1820
+ routes,
1821
+ aiRoutes,
1822
+ aiSelectionDecisions,
1823
+ });
1824
+ const upload = await sendSnapshot({ request, env, snapshot });
1825
+ return {
1826
+ accepted: upload.accepted === true,
1827
+ route_count: snapshot.routes.length,
1828
+ upload,
1829
+ };
1830
+ };