@clue-ai/cli 0.0.5 → 0.0.7
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 +18 -7
- package/bin/clue-cli.mjs +898 -762
- package/commands/claude-code/clue-init.md +9 -2
- package/commands/codex/clue-init.md +9 -2
- package/package.json +1 -1
- package/src/ai-provider.mjs +147 -0
- package/src/command-spec.mjs +9 -7
- package/src/contracts.mjs +51 -16
- package/src/init-tool.mjs +158 -125
- package/src/lifecycle-init.mjs +180 -205
- package/src/public-schema.cjs +48 -1
- package/src/semantic-agent-runner.mjs +157 -0
- package/src/semantic-ai-config.mjs +17 -0
- package/src/semantic-ci.mjs +525 -204
- package/src/setup-check.mjs +399 -372
- package/src/setup-prepare.mjs +361 -147
- package/src/setup-tool.mjs +379 -229
package/src/semantic-ci.mjs
CHANGED
|
@@ -4,8 +4,20 @@ import {
|
|
|
4
4
|
validateSemanticSnapshotRequest,
|
|
5
5
|
validateSemanticSnapshotResponse,
|
|
6
6
|
} from "./contracts.mjs";
|
|
7
|
+
import {
|
|
8
|
+
callJsonAiProvider,
|
|
9
|
+
resolveAiProviderConfig,
|
|
10
|
+
} from "./ai-provider.mjs";
|
|
7
11
|
import { analyzeFastApiRoutes } from "./fastapi-analyzer.mjs";
|
|
8
12
|
import { listAllowedPythonFiles } from "./path-policy.mjs";
|
|
13
|
+
import {
|
|
14
|
+
SEMANTIC_AGENT_ROLE_IDS,
|
|
15
|
+
SEMANTIC_AGENT_RUNNER_VERSION,
|
|
16
|
+
semanticAgentPromptEnvelope,
|
|
17
|
+
semanticAgentSkillBundle,
|
|
18
|
+
validateSemanticAgentSkillBundle,
|
|
19
|
+
} from "./semantic-agent-runner.mjs";
|
|
20
|
+
import { clueClientCiSemanticAiRuntime } from "./semantic-ai-config.mjs";
|
|
9
21
|
|
|
10
22
|
const sha256 = (value) =>
|
|
11
23
|
`sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
@@ -68,6 +80,12 @@ const ACTUAL_OUTCOME_ACTIONS = new Set([
|
|
|
68
80
|
]);
|
|
69
81
|
|
|
70
82
|
const snakeSegmentPattern = /^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/;
|
|
83
|
+
const SEMANTIC_SNAPSHOT_SCHEMA_VERSION = 2;
|
|
84
|
+
const SEMANTIC_ANALYZER_VERSION = "fastapi-route-analyzer-v1";
|
|
85
|
+
const SEMANTIC_ROUTE_PROMPT_CONTRACT_VERSION = "route-semantics-v1";
|
|
86
|
+
const SEMANTIC_REUSE_PROMPT_CONTRACT_VERSION = "route-reuse-v1";
|
|
87
|
+
const SEMANTIC_SELECTOR_PROMPT_CONTRACT_VERSION = "semantic-selector-v1";
|
|
88
|
+
const SEMANTIC_PRIVACY_SANITIZER_VERSION = "privacy-sanitizer-v1";
|
|
71
89
|
|
|
72
90
|
const semanticSnapshotHashScope = (request) =>
|
|
73
91
|
[
|
|
@@ -77,6 +95,15 @@ const semanticSnapshotHashScope = (request) =>
|
|
|
77
95
|
request.service.service_key,
|
|
78
96
|
].join(":");
|
|
79
97
|
|
|
98
|
+
const semanticGenerationContract = () => ({
|
|
99
|
+
schema_version: SEMANTIC_SNAPSHOT_SCHEMA_VERSION,
|
|
100
|
+
analyzer_version: SEMANTIC_ANALYZER_VERSION,
|
|
101
|
+
route_prompt_contract_version: SEMANTIC_ROUTE_PROMPT_CONTRACT_VERSION,
|
|
102
|
+
reuse_prompt_contract_version: SEMANTIC_REUSE_PROMPT_CONTRACT_VERSION,
|
|
103
|
+
selector_prompt_contract_version: SEMANTIC_SELECTOR_PROMPT_CONTRACT_VERSION,
|
|
104
|
+
privacy_sanitizer_version: SEMANTIC_PRIVACY_SANITIZER_VERSION,
|
|
105
|
+
});
|
|
106
|
+
|
|
80
107
|
const canonicalJson = (value) => {
|
|
81
108
|
if (Array.isArray(value)) {
|
|
82
109
|
return `[${value.map((entry) => canonicalJson(entry)).join(",")}]`;
|
|
@@ -149,9 +176,14 @@ const fallbackSemantics = (route, reason, hashScope) => ({
|
|
|
149
176
|
confidence_reason: reason,
|
|
150
177
|
});
|
|
151
178
|
|
|
152
|
-
const buildAiPrompt = (routes) =>
|
|
179
|
+
const buildAiPrompt = ({ routes, generationContract, agentSkills }) =>
|
|
153
180
|
JSON.stringify({
|
|
181
|
+
...semanticAgentPromptEnvelope({
|
|
182
|
+
bundle: agentSkills,
|
|
183
|
+
roleId: SEMANTIC_AGENT_ROLE_IDS.routeSemanticGenerator,
|
|
184
|
+
}),
|
|
154
185
|
task: "Generate privacy-safe semantic meaning candidates for FastAPI operation sources. Return JSON only.",
|
|
186
|
+
generation_contract: generationContract,
|
|
155
187
|
rules: [
|
|
156
188
|
"Do not create operation_source_key values; only copy the provided keys.",
|
|
157
189
|
"Do not create ids, refs, hashes, source fingerprints, or field paths.",
|
|
@@ -199,9 +231,20 @@ const buildAiPrompt = (routes) =>
|
|
|
199
231
|
})),
|
|
200
232
|
});
|
|
201
233
|
|
|
202
|
-
const buildAiReuseDecisionPrompt = ({
|
|
234
|
+
const buildAiReuseDecisionPrompt = ({
|
|
235
|
+
route,
|
|
236
|
+
previousRoute,
|
|
237
|
+
currentHash,
|
|
238
|
+
generationContract,
|
|
239
|
+
agentSkills,
|
|
240
|
+
}) =>
|
|
203
241
|
JSON.stringify({
|
|
242
|
+
...semanticAgentPromptEnvelope({
|
|
243
|
+
bundle: agentSkills,
|
|
244
|
+
roleId: SEMANTIC_AGENT_ROLE_IDS.reuseJudge,
|
|
245
|
+
}),
|
|
204
246
|
task: "Decide whether previous privacy-safe route semantics still apply after a route source change. Return JSON only.",
|
|
247
|
+
generation_contract: generationContract,
|
|
205
248
|
rules: [
|
|
206
249
|
"Only decide for the provided operation_source_key.",
|
|
207
250
|
"Return decision as reuse_previous, regenerate, or needs_review.",
|
|
@@ -234,6 +277,28 @@ const buildAiReuseDecisionPrompt = ({ route, previousRoute, currentHash }) =>
|
|
|
234
277
|
});
|
|
235
278
|
|
|
236
279
|
const normalizeReuseDecision = ({ parsed, operationSourceKey }) => {
|
|
280
|
+
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.decisions)) {
|
|
281
|
+
return {
|
|
282
|
+
decision: "needs_review",
|
|
283
|
+
confidence: 0,
|
|
284
|
+
reason: "AI reuse decision did not return the required decisions array.",
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const unknownDecision = parsed.decisions.find(
|
|
288
|
+
(entry) =>
|
|
289
|
+
!entry ||
|
|
290
|
+
typeof entry !== "object" ||
|
|
291
|
+
typeof entry.operation_source_key !== "string" ||
|
|
292
|
+
(entry.operation_source_key !== operationSourceKey &&
|
|
293
|
+
entry.operation_source_key.trim() !== ""),
|
|
294
|
+
);
|
|
295
|
+
if (unknownDecision) {
|
|
296
|
+
return {
|
|
297
|
+
decision: "needs_review",
|
|
298
|
+
confidence: 0,
|
|
299
|
+
reason: "AI reuse decision returned an unexpected route key.",
|
|
300
|
+
};
|
|
301
|
+
}
|
|
237
302
|
const decision = safeArray(parsed?.decisions).find(
|
|
238
303
|
(entry) => entry?.operation_source_key === operationSourceKey,
|
|
239
304
|
);
|
|
@@ -244,9 +309,11 @@ const normalizeReuseDecision = ({ parsed, operationSourceKey }) => {
|
|
|
244
309
|
reason: "AI reuse decision omitted this changed route.",
|
|
245
310
|
};
|
|
246
311
|
}
|
|
247
|
-
const normalizedDecision = [
|
|
248
|
-
|
|
249
|
-
|
|
312
|
+
const normalizedDecision = [
|
|
313
|
+
"reuse_previous",
|
|
314
|
+
"regenerate",
|
|
315
|
+
"needs_review",
|
|
316
|
+
].includes(decision.decision)
|
|
250
317
|
? decision.decision
|
|
251
318
|
: "needs_review";
|
|
252
319
|
return {
|
|
@@ -263,109 +330,104 @@ const normalizeReuseDecision = ({ parsed, operationSourceKey }) => {
|
|
|
263
330
|
|
|
264
331
|
const callAiReuseDecisionProvider = async ({
|
|
265
332
|
request,
|
|
333
|
+
env,
|
|
266
334
|
apiKey,
|
|
267
335
|
route,
|
|
268
336
|
previousRoute,
|
|
269
337
|
currentHash,
|
|
338
|
+
generationContract,
|
|
339
|
+
agentSkills,
|
|
270
340
|
}) => {
|
|
271
|
-
const response = await fetch(
|
|
272
|
-
`${request.ai_provider_base_url.replace(/\/+$/, "")}/chat/completions`,
|
|
273
|
-
{
|
|
274
|
-
method: "POST",
|
|
275
|
-
headers: {
|
|
276
|
-
"content-type": "application/json",
|
|
277
|
-
authorization: `Bearer ${apiKey}`,
|
|
278
|
-
},
|
|
279
|
-
body: JSON.stringify({
|
|
280
|
-
model: request.ai_model,
|
|
281
|
-
messages: [
|
|
282
|
-
{
|
|
283
|
-
role: "system",
|
|
284
|
-
content:
|
|
285
|
-
"You judge whether previous route semantics can be reused from privacy-safe evidence.",
|
|
286
|
-
},
|
|
287
|
-
{
|
|
288
|
-
role: "user",
|
|
289
|
-
content: buildAiReuseDecisionPrompt({
|
|
290
|
-
route,
|
|
291
|
-
previousRoute,
|
|
292
|
-
currentHash,
|
|
293
|
-
}),
|
|
294
|
-
},
|
|
295
|
-
],
|
|
296
|
-
response_format: { type: "json_object" },
|
|
297
|
-
}),
|
|
298
|
-
},
|
|
299
|
-
);
|
|
300
|
-
if (!response.ok) {
|
|
301
|
-
return {
|
|
302
|
-
decision: "needs_review",
|
|
303
|
-
confidence: 0,
|
|
304
|
-
reason: `AI reuse decision failed: provider_${response.status}.`,
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
const body = await response.json();
|
|
308
|
-
const content = body?.choices?.[0]?.message?.content;
|
|
309
|
-
if (typeof content !== "string" || content.trim() === "") {
|
|
310
|
-
return {
|
|
311
|
-
decision: "needs_review",
|
|
312
|
-
confidence: 0,
|
|
313
|
-
reason: "AI reuse decision returned empty content.",
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
341
|
try {
|
|
342
|
+
const parsed = await callJsonAiProvider({
|
|
343
|
+
config: resolveAiProviderConfig({ env, request, apiKey }),
|
|
344
|
+
system:
|
|
345
|
+
"You judge whether previous route semantics can be reused from privacy-safe evidence.",
|
|
346
|
+
user: buildAiReuseDecisionPrompt({
|
|
347
|
+
route,
|
|
348
|
+
previousRoute,
|
|
349
|
+
currentHash,
|
|
350
|
+
generationContract,
|
|
351
|
+
agentSkills,
|
|
352
|
+
}),
|
|
353
|
+
toolName: "return_reuse_decision",
|
|
354
|
+
toolDescription:
|
|
355
|
+
"Return whether the previous route semantics can be reused.",
|
|
356
|
+
failureMessage: "AI reuse decision failed",
|
|
357
|
+
emptyMessage: "AI reuse decision returned empty content.",
|
|
358
|
+
});
|
|
317
359
|
return normalizeReuseDecision({
|
|
318
|
-
parsed
|
|
360
|
+
parsed,
|
|
319
361
|
operationSourceKey: route.operation_source_key,
|
|
320
362
|
});
|
|
321
|
-
} catch {
|
|
363
|
+
} catch (error) {
|
|
364
|
+
const providerStatusMatch =
|
|
365
|
+
error instanceof Error
|
|
366
|
+
? /AI reuse decision failed: (?<status>\d+)/.exec(error.message)
|
|
367
|
+
: null;
|
|
322
368
|
return {
|
|
323
369
|
decision: "needs_review",
|
|
324
370
|
confidence: 0,
|
|
325
|
-
reason:
|
|
371
|
+
reason: providerStatusMatch?.groups?.status
|
|
372
|
+
? `AI reuse decision failed: provider_${providerStatusMatch.groups.status}.`
|
|
373
|
+
: "AI reuse decision returned malformed JSON.",
|
|
326
374
|
};
|
|
327
375
|
}
|
|
328
376
|
};
|
|
329
377
|
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
378
|
+
const assertAiRouteResponseContract = ({ parsed, expectedRouteKeys }) => {
|
|
379
|
+
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.routes)) {
|
|
380
|
+
throw new SyntaxError("AI route semantics response must include routes");
|
|
381
|
+
}
|
|
382
|
+
const expected = new Set(expectedRouteKeys);
|
|
383
|
+
const actual = new Set();
|
|
384
|
+
for (const route of parsed.routes) {
|
|
385
|
+
if (!route || typeof route !== "object") {
|
|
386
|
+
throw new SyntaxError("AI route semantics response includes invalid route");
|
|
387
|
+
}
|
|
388
|
+
if (
|
|
389
|
+
typeof route.operation_source_key !== "string" ||
|
|
390
|
+
!expected.has(route.operation_source_key)
|
|
391
|
+
) {
|
|
392
|
+
throw new SyntaxError(
|
|
393
|
+
"AI route semantics response included an unexpected route key",
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
if (actual.has(route.operation_source_key)) {
|
|
397
|
+
throw new SyntaxError(
|
|
398
|
+
"AI route semantics response included a duplicate route key",
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
actual.add(route.operation_source_key);
|
|
402
|
+
if (!route.semantics || typeof route.semantics !== "object") {
|
|
403
|
+
throw new SyntaxError(
|
|
404
|
+
"AI route semantics response omitted required semantics",
|
|
405
|
+
);
|
|
406
|
+
}
|
|
334
407
|
}
|
|
335
|
-
return apiKey;
|
|
336
408
|
};
|
|
337
409
|
|
|
338
|
-
const callAiProvider = async ({
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
);
|
|
360
|
-
if (!response.ok) {
|
|
361
|
-
throw new Error(`AI provider failed: ${response.status}`);
|
|
362
|
-
}
|
|
363
|
-
const body = await response.json();
|
|
364
|
-
const content = body?.choices?.[0]?.message?.content;
|
|
365
|
-
if (typeof content !== "string" || content.trim() === "") {
|
|
366
|
-
throw new Error("AI provider returned empty semantic generation content");
|
|
367
|
-
}
|
|
368
|
-
const parsed = JSON.parse(content);
|
|
410
|
+
const callAiProvider = async ({
|
|
411
|
+
request,
|
|
412
|
+
env,
|
|
413
|
+
apiKey,
|
|
414
|
+
routes,
|
|
415
|
+
generationContract,
|
|
416
|
+
agentSkills,
|
|
417
|
+
}) => {
|
|
418
|
+
const parsed = await callJsonAiProvider({
|
|
419
|
+
config: resolveAiProviderConfig({ env, request, apiKey }),
|
|
420
|
+
system: "You generate privacy-safe route semantics JSON.",
|
|
421
|
+
user: buildAiPrompt({ routes, generationContract, agentSkills }),
|
|
422
|
+
toolName: "return_route_semantics",
|
|
423
|
+
toolDescription: "Return privacy-safe route semantics JSON.",
|
|
424
|
+
failureMessage: "AI provider failed",
|
|
425
|
+
emptyMessage: "AI provider returned empty semantic generation content",
|
|
426
|
+
});
|
|
427
|
+
assertAiRouteResponseContract({
|
|
428
|
+
parsed,
|
|
429
|
+
expectedRouteKeys: routes.map((route) => route.operation_source_key),
|
|
430
|
+
});
|
|
369
431
|
return new Map(
|
|
370
432
|
(parsed.routes ?? []).map((route) => [route.operation_source_key, route]),
|
|
371
433
|
);
|
|
@@ -413,14 +475,24 @@ const assertSnapshotRouteCoverage = ({ routes, snapshotRoutes }) => {
|
|
|
413
475
|
}
|
|
414
476
|
};
|
|
415
477
|
|
|
416
|
-
const generateAiRoutesPerRoute = async ({
|
|
478
|
+
const generateAiRoutesPerRoute = async ({
|
|
479
|
+
request,
|
|
480
|
+
env,
|
|
481
|
+
apiKey,
|
|
482
|
+
routes,
|
|
483
|
+
generationContract,
|
|
484
|
+
agentSkills,
|
|
485
|
+
}) => {
|
|
417
486
|
const aiRoutes = new Map();
|
|
418
487
|
for (const route of routes) {
|
|
419
488
|
try {
|
|
420
489
|
const routeAiRoutes = await callAiProvider({
|
|
421
490
|
request,
|
|
491
|
+
env,
|
|
422
492
|
apiKey,
|
|
423
493
|
routes: [route],
|
|
494
|
+
generationContract,
|
|
495
|
+
agentSkills,
|
|
424
496
|
});
|
|
425
497
|
const aiRoute = routeAiRoutes.get(route.operation_source_key);
|
|
426
498
|
if (aiRoute?.semantics) {
|
|
@@ -447,9 +519,18 @@ const generateAiRoutesPerRoute = async ({ request, apiKey, routes }) => {
|
|
|
447
519
|
return aiRoutes;
|
|
448
520
|
};
|
|
449
521
|
|
|
450
|
-
const buildAiSelectionPrompt = (
|
|
522
|
+
const buildAiSelectionPrompt = ({
|
|
523
|
+
selectionCandidates,
|
|
524
|
+
generationContract,
|
|
525
|
+
agentSkills,
|
|
526
|
+
}) =>
|
|
451
527
|
JSON.stringify({
|
|
528
|
+
...semanticAgentPromptEnvelope({
|
|
529
|
+
bundle: agentSkills,
|
|
530
|
+
roleId: SEMANTIC_AGENT_ROLE_IDS.semanticSelector,
|
|
531
|
+
}),
|
|
452
532
|
task: "Choose adopted semantic values from deterministic and AI candidates. Return JSON only.",
|
|
533
|
+
generation_contract: generationContract,
|
|
453
534
|
rules: [
|
|
454
535
|
"Only copy operation_source_key, candidate_index, and parameter_name values from the provided selection_candidates.",
|
|
455
536
|
"For each semantic field, return selected_source as deterministic or ai.",
|
|
@@ -476,56 +557,62 @@ const buildAiSelectionPrompt = (selectionCandidates) =>
|
|
|
476
557
|
|
|
477
558
|
const callAiSelectionProvider = async ({
|
|
478
559
|
request,
|
|
560
|
+
env,
|
|
479
561
|
apiKey,
|
|
480
562
|
selectionCandidates,
|
|
481
563
|
routeBySelectionKey,
|
|
564
|
+
generationContract,
|
|
565
|
+
agentSkills,
|
|
482
566
|
}) => {
|
|
483
567
|
if (selectionCandidates.length === 0) {
|
|
484
568
|
return new Map();
|
|
485
569
|
}
|
|
486
|
-
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
"You select adopted semantic values from privacy-safe candidates.",
|
|
502
|
-
},
|
|
503
|
-
{
|
|
504
|
-
role: "user",
|
|
505
|
-
content: buildAiSelectionPrompt(selectionCandidates),
|
|
506
|
-
},
|
|
507
|
-
],
|
|
508
|
-
response_format: { type: "json_object" },
|
|
509
|
-
}),
|
|
510
|
-
},
|
|
511
|
-
);
|
|
512
|
-
if (!response.ok) {
|
|
513
|
-
throw new Error(`AI selector failed: ${response.status}`);
|
|
514
|
-
}
|
|
515
|
-
const body = await response.json();
|
|
516
|
-
const content = body?.choices?.[0]?.message?.content;
|
|
517
|
-
if (typeof content !== "string" || content.trim() === "") {
|
|
518
|
-
throw new Error("AI selector returned empty semantic selection content");
|
|
519
|
-
}
|
|
520
|
-
const parsed = JSON.parse(content);
|
|
570
|
+
assertNoRawCodeStructureForSelectionCandidates(selectionCandidates);
|
|
571
|
+
const parsed = await callJsonAiProvider({
|
|
572
|
+
config: resolveAiProviderConfig({ env, request, apiKey }),
|
|
573
|
+
system: "You select adopted semantic values from privacy-safe candidates.",
|
|
574
|
+
user: buildAiSelectionPrompt({
|
|
575
|
+
selectionCandidates,
|
|
576
|
+
generationContract,
|
|
577
|
+
agentSkills,
|
|
578
|
+
}),
|
|
579
|
+
toolName: "return_semantic_selection",
|
|
580
|
+
toolDescription:
|
|
581
|
+
"Return selected semantic values from privacy-safe candidates.",
|
|
582
|
+
failureMessage: "AI selector failed",
|
|
583
|
+
emptyMessage: "AI selector returned empty semantic selection content",
|
|
584
|
+
});
|
|
521
585
|
return normalizeAiSelectionResponse({
|
|
522
586
|
parsed,
|
|
523
587
|
routeBySelectionKey,
|
|
588
|
+
validSelectionParametersByKey: validSelectionParametersByKey(
|
|
589
|
+
selectionCandidates,
|
|
590
|
+
),
|
|
524
591
|
});
|
|
525
592
|
};
|
|
526
593
|
|
|
527
|
-
const buildSemanticSnapshotVersion = ({
|
|
528
|
-
|
|
594
|
+
const buildSemanticSnapshotVersion = ({
|
|
595
|
+
request,
|
|
596
|
+
generationContract,
|
|
597
|
+
snapshotRoutes,
|
|
598
|
+
}) =>
|
|
599
|
+
`snap_${shortHash(
|
|
600
|
+
canonicalJson({
|
|
601
|
+
project_key: request.project_key,
|
|
602
|
+
repository_id: request.repository.repository_id,
|
|
603
|
+
service_key: request.service.service_key,
|
|
604
|
+
generation_contract: generationContract,
|
|
605
|
+
routes: snapshotRoutes
|
|
606
|
+
.map((route) => ({
|
|
607
|
+
operation_source_key: route.operation_source_key,
|
|
608
|
+
route_input_hash: route.route_input_hash,
|
|
609
|
+
route_semantic_hash: route.route_semantic_hash,
|
|
610
|
+
}))
|
|
611
|
+
.sort((left, right) =>
|
|
612
|
+
left.operation_source_key.localeCompare(right.operation_source_key),
|
|
613
|
+
),
|
|
614
|
+
}),
|
|
615
|
+
)}`;
|
|
529
616
|
|
|
530
617
|
const normalizeSnakeSegment = (value) => {
|
|
531
618
|
const normalized = String(value ?? "")
|
|
@@ -888,7 +975,30 @@ const candidateHasUnsafeSemanticText = (candidate) =>
|
|
|
888
975
|
const selectionKeyForCandidate = (route, candidateIndex) =>
|
|
889
976
|
`${route.operation_source_key}#${candidateIndex}`;
|
|
890
977
|
|
|
891
|
-
const
|
|
978
|
+
const validSelectionParametersByKey = (selectionCandidates) =>
|
|
979
|
+
new Map(
|
|
980
|
+
selectionCandidates.map((candidate) => [
|
|
981
|
+
selectionKeyForCandidate(candidate, candidate.candidate_index),
|
|
982
|
+
new Set(
|
|
983
|
+
safeArray(candidate.semantic_fields)
|
|
984
|
+
.map((field) => field?.parameter_name)
|
|
985
|
+
.filter((parameterName) => typeof parameterName === "string"),
|
|
986
|
+
),
|
|
987
|
+
]),
|
|
988
|
+
);
|
|
989
|
+
|
|
990
|
+
const normalizeAiSelectionResponse = ({
|
|
991
|
+
parsed,
|
|
992
|
+
routeBySelectionKey,
|
|
993
|
+
validSelectionParametersByKey,
|
|
994
|
+
}) => {
|
|
995
|
+
if (
|
|
996
|
+
!parsed ||
|
|
997
|
+
typeof parsed !== "object" ||
|
|
998
|
+
!Array.isArray(parsed.field_selections)
|
|
999
|
+
) {
|
|
1000
|
+
throw new SyntaxError("AI selector response must include field_selections");
|
|
1001
|
+
}
|
|
892
1002
|
const decisionsByCandidate = new Map();
|
|
893
1003
|
for (const decision of safeArray(parsed?.field_selections)) {
|
|
894
1004
|
if (!decision || typeof decision !== "object") {
|
|
@@ -907,6 +1017,10 @@ const normalizeAiSelectionResponse = ({ parsed, routeBySelectionKey }) => {
|
|
|
907
1017
|
if (!route) {
|
|
908
1018
|
continue;
|
|
909
1019
|
}
|
|
1020
|
+
const validParameters = validSelectionParametersByKey.get(selectionKey);
|
|
1021
|
+
if (!validParameters?.has(decision.parameter_name)) {
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
910
1024
|
const candidateDecisions =
|
|
911
1025
|
decisionsByCandidate.get(selectionKey) ?? new Map();
|
|
912
1026
|
candidateDecisions.set(decision.parameter_name, {
|
|
@@ -1072,7 +1186,8 @@ const buildSelectorCandidates = ({ routes, aiRoutes, hashScope }) => {
|
|
|
1072
1186
|
candidateHasUnsafeSemanticText(candidate) ||
|
|
1073
1187
|
!values.rawTargetObjectText ||
|
|
1074
1188
|
!values.targetKeyCandidate ||
|
|
1075
|
-
!values.aiExpectedDomainEffect
|
|
1189
|
+
(!values.aiExpectedDomainEffect &&
|
|
1190
|
+
!values.deterministicExpectedDomainEffect) ||
|
|
1076
1191
|
!hasBehaviorEvidence(values.sourceEvidenceRefs, routeEvidenceRefs) ||
|
|
1077
1192
|
!candidateHasTargetSpecificEvidence({
|
|
1078
1193
|
route,
|
|
@@ -1638,13 +1753,12 @@ const buildSnapshot = ({
|
|
|
1638
1753
|
routePlans = new Map(),
|
|
1639
1754
|
previousSnapshot,
|
|
1640
1755
|
deletedRouteCount = 0,
|
|
1756
|
+
generationContract,
|
|
1757
|
+
aiRuntime,
|
|
1641
1758
|
}) => {
|
|
1642
1759
|
const generatedAt = new Date().toISOString();
|
|
1643
1760
|
const hashScope = semanticSnapshotHashScope(request);
|
|
1644
|
-
const
|
|
1645
|
-
request,
|
|
1646
|
-
generatedAt,
|
|
1647
|
-
});
|
|
1761
|
+
const provisionalSemanticSnapshotVersion = "snap_pending";
|
|
1648
1762
|
const targetObjectProfiles = [];
|
|
1649
1763
|
const targetObjectMappings = [];
|
|
1650
1764
|
const sourceEvidenceRefs = [];
|
|
@@ -1678,7 +1792,7 @@ const buildSnapshot = ({
|
|
|
1678
1792
|
const snapshotRoutes = routes.map((route) => {
|
|
1679
1793
|
const routeWithVersion = {
|
|
1680
1794
|
...route,
|
|
1681
|
-
semantic_snapshot_version:
|
|
1795
|
+
semantic_snapshot_version: provisionalSemanticSnapshotVersion,
|
|
1682
1796
|
};
|
|
1683
1797
|
const plan = routePlans.get(route.operation_source_key) ?? {
|
|
1684
1798
|
origin: "new_route_ai_generated",
|
|
@@ -1697,7 +1811,7 @@ const buildSnapshot = ({
|
|
|
1697
1811
|
return rebasePreviousRoute({
|
|
1698
1812
|
previousRoute: plan.previous_route,
|
|
1699
1813
|
currentRoute: route,
|
|
1700
|
-
semanticSnapshotVersion,
|
|
1814
|
+
semanticSnapshotVersion: provisionalSemanticSnapshotVersion,
|
|
1701
1815
|
plan,
|
|
1702
1816
|
});
|
|
1703
1817
|
}
|
|
@@ -1710,31 +1824,28 @@ const buildSnapshot = ({
|
|
|
1710
1824
|
? `changed_route_needs_review: ${plan.semantic_change_reason}`
|
|
1711
1825
|
: (aiRoute?.confidence_reason ??
|
|
1712
1826
|
"AI semantics were unavailable for this route.");
|
|
1713
|
-
const baseRoute =
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
aiRoute.layer_evidence,
|
|
1727
|
-
route,
|
|
1728
|
-
hashScope,
|
|
1729
|
-
),
|
|
1730
|
-
confidence_reason: sanitizeText(
|
|
1731
|
-
String(
|
|
1732
|
-
aiRoute.confidence_reason ||
|
|
1733
|
-
"Generated by AI from privacy-safe route evidence.",
|
|
1827
|
+
const baseRoute =
|
|
1828
|
+
!aiRoute?.semantics || plan.origin === "changed_route_needs_review"
|
|
1829
|
+
? fallbackSemantics(routeWithVersion, unavailableReason, hashScope)
|
|
1830
|
+
: {
|
|
1831
|
+
operation_source_key: route.operation_source_key,
|
|
1832
|
+
semantic_snapshot_version: provisionalSemanticSnapshotVersion,
|
|
1833
|
+
method: route.method,
|
|
1834
|
+
path_template: route.path_template,
|
|
1835
|
+
semantics: sanitizeSemantics(aiRoute.semantics, route),
|
|
1836
|
+
layer_evidence: sanitizeLayerEvidence(
|
|
1837
|
+
aiRoute.layer_evidence,
|
|
1838
|
+
route,
|
|
1839
|
+
hashScope,
|
|
1734
1840
|
),
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1841
|
+
confidence_reason: sanitizeText(
|
|
1842
|
+
String(
|
|
1843
|
+
aiRoute.confidence_reason ||
|
|
1844
|
+
"Generated by AI from privacy-safe route evidence.",
|
|
1845
|
+
),
|
|
1846
|
+
route,
|
|
1847
|
+
),
|
|
1848
|
+
};
|
|
1738
1849
|
const assignmentCollections = buildOperationAssignmentCollections({
|
|
1739
1850
|
route,
|
|
1740
1851
|
aiRoute,
|
|
@@ -1794,7 +1905,20 @@ const buildSnapshot = ({
|
|
|
1794
1905
|
);
|
|
1795
1906
|
|
|
1796
1907
|
const uniqueSourceEvidenceRefs = [];
|
|
1797
|
-
addUniqueBy(
|
|
1908
|
+
addUniqueBy(
|
|
1909
|
+
uniqueSourceEvidenceRefs,
|
|
1910
|
+
sourceEvidenceRefs,
|
|
1911
|
+
(entry) => entry.id,
|
|
1912
|
+
);
|
|
1913
|
+
const semanticSnapshotVersion = buildSemanticSnapshotVersion({
|
|
1914
|
+
request,
|
|
1915
|
+
generationContract,
|
|
1916
|
+
snapshotRoutes,
|
|
1917
|
+
});
|
|
1918
|
+
const versionedSnapshotRoutes = snapshotRoutes.map((route) => ({
|
|
1919
|
+
...route,
|
|
1920
|
+
semantic_snapshot_version: semanticSnapshotVersion,
|
|
1921
|
+
}));
|
|
1798
1922
|
|
|
1799
1923
|
const snapshot = validateSemanticSnapshotRequest({
|
|
1800
1924
|
project_key: request.project_key,
|
|
@@ -1803,43 +1927,48 @@ const buildSnapshot = ({
|
|
|
1803
1927
|
),
|
|
1804
1928
|
schema_version: 2,
|
|
1805
1929
|
semantic_snapshot_version: semanticSnapshotVersion,
|
|
1930
|
+
generation_contract: generationContract,
|
|
1931
|
+
ai_runtime: aiRuntime,
|
|
1806
1932
|
generated_at: generatedAt,
|
|
1807
1933
|
repository: request.repository,
|
|
1808
|
-
service:
|
|
1934
|
+
service: {
|
|
1935
|
+
service_key: request.service.service_key,
|
|
1936
|
+
framework: request.service.framework,
|
|
1937
|
+
language: request.service.language,
|
|
1938
|
+
},
|
|
1809
1939
|
analysis_summary: {
|
|
1810
1940
|
total_routes_scanned: routes.length,
|
|
1811
|
-
routes_generated:
|
|
1941
|
+
routes_generated: versionedSnapshotRoutes.filter(
|
|
1812
1942
|
(route) => route.semantics.route_confidence > 0,
|
|
1813
1943
|
).length,
|
|
1814
|
-
uncertain_routes:
|
|
1944
|
+
uncertain_routes: versionedSnapshotRoutes.filter(
|
|
1815
1945
|
(route) => route.semantics.route_confidence < 0.5,
|
|
1816
1946
|
).length,
|
|
1817
1947
|
failed_routes: failedRouteCount,
|
|
1818
|
-
routes_reused:
|
|
1819
|
-
[
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
].includes(route.semantic_origin),
|
|
1948
|
+
routes_reused: versionedSnapshotRoutes.filter((route) =>
|
|
1949
|
+
["unchanged_route_reused", "changed_route_semantic_reused"].includes(
|
|
1950
|
+
route.semantic_origin,
|
|
1951
|
+
),
|
|
1823
1952
|
).length,
|
|
1824
|
-
routes_ai_generated:
|
|
1953
|
+
routes_ai_generated: versionedSnapshotRoutes.filter((route) =>
|
|
1825
1954
|
[
|
|
1826
1955
|
"new_route_ai_generated",
|
|
1827
1956
|
"changed_route_semantic_regenerated",
|
|
1828
1957
|
].includes(route.semantic_origin),
|
|
1829
1958
|
).length,
|
|
1830
1959
|
routes_deleted: deletedRouteCount,
|
|
1831
|
-
routes_needs_review:
|
|
1960
|
+
routes_needs_review: versionedSnapshotRoutes.filter(
|
|
1832
1961
|
(route) => route.semantic_origin === "changed_route_needs_review",
|
|
1833
1962
|
).length,
|
|
1834
|
-
changed_routes_semantic_reused:
|
|
1963
|
+
changed_routes_semantic_reused: versionedSnapshotRoutes.filter(
|
|
1835
1964
|
(route) => route.semantic_origin === "changed_route_semantic_reused",
|
|
1836
1965
|
).length,
|
|
1837
|
-
changed_routes_semantic_regenerated:
|
|
1966
|
+
changed_routes_semantic_regenerated: versionedSnapshotRoutes.filter(
|
|
1838
1967
|
(route) =>
|
|
1839
1968
|
route.semantic_origin === "changed_route_semantic_regenerated",
|
|
1840
1969
|
).length,
|
|
1841
1970
|
},
|
|
1842
|
-
routes:
|
|
1971
|
+
routes: versionedSnapshotRoutes,
|
|
1843
1972
|
target_object_profiles: targetObjectProfiles,
|
|
1844
1973
|
target_object_catalog: [...catalogByGroup.values()],
|
|
1845
1974
|
target_object_mappings: targetObjectMappings,
|
|
@@ -2039,10 +2168,13 @@ const buildRouteEvidencePromptEntry = ({ route, hashScope }) => ({
|
|
|
2039
2168
|
|
|
2040
2169
|
const classifyRoutesForSnapshot = async ({
|
|
2041
2170
|
request,
|
|
2171
|
+
env,
|
|
2042
2172
|
routes,
|
|
2043
2173
|
previousSnapshot,
|
|
2044
2174
|
aiProviderApiKey,
|
|
2045
2175
|
hashScope,
|
|
2176
|
+
generationContract,
|
|
2177
|
+
agentSkills,
|
|
2046
2178
|
}) => {
|
|
2047
2179
|
const previousRoutes = previousRouteMap(previousSnapshot);
|
|
2048
2180
|
const plans = new Map();
|
|
@@ -2076,12 +2208,15 @@ const classifyRoutesForSnapshot = async ({
|
|
|
2076
2208
|
|
|
2077
2209
|
const decision = await callAiReuseDecisionProvider({
|
|
2078
2210
|
request,
|
|
2211
|
+
env,
|
|
2079
2212
|
apiKey: aiProviderApiKey,
|
|
2080
2213
|
route: buildRouteEvidencePromptEntry({ route, hashScope }),
|
|
2081
2214
|
previousRoute,
|
|
2082
2215
|
currentHash,
|
|
2216
|
+
generationContract,
|
|
2217
|
+
agentSkills,
|
|
2083
2218
|
});
|
|
2084
|
-
if (decision.decision === "reuse_previous") {
|
|
2219
|
+
if (decision.decision === "reuse_previous" && decision.confidence >= 0.75) {
|
|
2085
2220
|
plans.set(route.operation_source_key, {
|
|
2086
2221
|
origin: "changed_route_semantic_reused",
|
|
2087
2222
|
route_input_hash: currentHash,
|
|
@@ -2093,6 +2228,21 @@ const classifyRoutesForSnapshot = async ({
|
|
|
2093
2228
|
});
|
|
2094
2229
|
continue;
|
|
2095
2230
|
}
|
|
2231
|
+
if (decision.decision === "reuse_previous") {
|
|
2232
|
+
plans.set(route.operation_source_key, {
|
|
2233
|
+
origin: "changed_route_needs_review",
|
|
2234
|
+
route_input_hash: currentHash,
|
|
2235
|
+
previous_route: previousRoute,
|
|
2236
|
+
previous_route_input_hash: previousRoute.route_input_hash,
|
|
2237
|
+
previous_route_semantic_hash:
|
|
2238
|
+
previousRoute.route_semantic_hash ?? routeSemanticHash(previousRoute),
|
|
2239
|
+
semantic_change_reason: sanitizeText(
|
|
2240
|
+
`AI reuse decision confidence was too low for automatic reuse: ${decision.reason}`,
|
|
2241
|
+
route,
|
|
2242
|
+
),
|
|
2243
|
+
});
|
|
2244
|
+
continue;
|
|
2245
|
+
}
|
|
2096
2246
|
if (decision.decision === "regenerate") {
|
|
2097
2247
|
plans.set(route.operation_source_key, {
|
|
2098
2248
|
origin: "changed_route_semantic_regenerated",
|
|
@@ -2158,7 +2308,13 @@ const rebasePreviousRoute = ({
|
|
|
2158
2308
|
};
|
|
2159
2309
|
};
|
|
2160
2310
|
|
|
2161
|
-
const fieldPathMatchesKeys = ({
|
|
2311
|
+
const fieldPathMatchesKeys = ({
|
|
2312
|
+
fieldPath,
|
|
2313
|
+
routeKeys,
|
|
2314
|
+
profileIds,
|
|
2315
|
+
mappingIds,
|
|
2316
|
+
catalogKeys,
|
|
2317
|
+
}) => {
|
|
2162
2318
|
for (const routeKey of routeKeys) {
|
|
2163
2319
|
if (fieldPath.startsWith(`routes[${routeKey}]`)) {
|
|
2164
2320
|
return true;
|
|
@@ -2197,7 +2353,9 @@ const reusablePreviousContext = ({ previousSnapshot, reusedRouteKeys }) => {
|
|
|
2197
2353
|
...safeArray(previousSnapshot?.routes)
|
|
2198
2354
|
.filter((route) => routeKeys.has(route.operation_source_key))
|
|
2199
2355
|
.flatMap((route) =>
|
|
2200
|
-
safeArray(route.operation_effects).map(
|
|
2356
|
+
safeArray(route.operation_effects).map(
|
|
2357
|
+
(effect) => effect.target_object_key,
|
|
2358
|
+
),
|
|
2201
2359
|
),
|
|
2202
2360
|
]);
|
|
2203
2361
|
const catalogs = safeArray(previousSnapshot?.target_object_catalog).filter(
|
|
@@ -2274,9 +2432,9 @@ const reusablePreviousContext = ({ previousSnapshot, reusedRouteKeys }) => {
|
|
|
2274
2432
|
sourceEvidenceIds.add(ref);
|
|
2275
2433
|
}
|
|
2276
2434
|
}
|
|
2277
|
-
const sourceEvidenceRefs = safeArray(
|
|
2278
|
-
|
|
2279
|
-
);
|
|
2435
|
+
const sourceEvidenceRefs = safeArray(
|
|
2436
|
+
previousSnapshot?.source_evidence_refs,
|
|
2437
|
+
).filter((entry) => sourceEvidenceIds.has(entry.id));
|
|
2280
2438
|
return {
|
|
2281
2439
|
profiles,
|
|
2282
2440
|
mappings,
|
|
@@ -2384,6 +2542,32 @@ const assertNoRawCodeStructure = (value, path = "snapshot") => {
|
|
|
2384
2542
|
}
|
|
2385
2543
|
};
|
|
2386
2544
|
|
|
2545
|
+
const assertNoRawCodeStructureForSelectionCandidates = (
|
|
2546
|
+
selectionCandidates,
|
|
2547
|
+
) => {
|
|
2548
|
+
for (const candidate of selectionCandidates) {
|
|
2549
|
+
safeArray(candidate.evidence_summaries).forEach((entry, index) => {
|
|
2550
|
+
assertNoRawCodeStructure(
|
|
2551
|
+
{
|
|
2552
|
+
kind: entry?.kind,
|
|
2553
|
+
summary: entry?.summary,
|
|
2554
|
+
evidence_strength: entry?.evidence_strength,
|
|
2555
|
+
},
|
|
2556
|
+
`semantic_selection_prompt[${index}].evidence_summaries`,
|
|
2557
|
+
);
|
|
2558
|
+
});
|
|
2559
|
+
safeArray(candidate.semantic_fields).forEach((field, index) => {
|
|
2560
|
+
assertNoRawCodeStructure(
|
|
2561
|
+
{
|
|
2562
|
+
deterministic_candidate: field?.deterministic_candidate,
|
|
2563
|
+
ai_candidate: field?.ai_candidate,
|
|
2564
|
+
},
|
|
2565
|
+
`semantic_selection_prompt[${index}].semantic_fields`,
|
|
2566
|
+
);
|
|
2567
|
+
});
|
|
2568
|
+
}
|
|
2569
|
+
};
|
|
2570
|
+
|
|
2387
2571
|
const semanticSnapshotUrl = (baseUrl) => {
|
|
2388
2572
|
const normalized = baseUrl.replace(/\/+$/, "");
|
|
2389
2573
|
if (normalized.endsWith("/api/v1/semantic-snapshots")) {
|
|
@@ -2396,7 +2580,9 @@ const semanticSnapshotUrl = (baseUrl) => {
|
|
|
2396
2580
|
};
|
|
2397
2581
|
|
|
2398
2582
|
const semanticSnapshotLatestUrl = (request) => {
|
|
2399
|
-
const url = new URL(
|
|
2583
|
+
const url = new URL(
|
|
2584
|
+
`${semanticSnapshotUrl(request.clue_api_base_url)}/latest`,
|
|
2585
|
+
);
|
|
2400
2586
|
url.searchParams.set("project_key", request.project_key);
|
|
2401
2587
|
url.searchParams.set("environment", request.environment);
|
|
2402
2588
|
url.searchParams.set("repository_id", request.repository.repository_id);
|
|
@@ -2409,6 +2595,8 @@ const fetchLatestSnapshot = async ({ request, env }) => {
|
|
|
2409
2595
|
if (!apiKey) {
|
|
2410
2596
|
return null;
|
|
2411
2597
|
}
|
|
2598
|
+
const allowFullRegeneration =
|
|
2599
|
+
env.CLUE_SEMANTIC_ALLOW_FULL_REGEN_WITHOUT_CACHE === "1";
|
|
2412
2600
|
let response;
|
|
2413
2601
|
try {
|
|
2414
2602
|
response = await fetch(semanticSnapshotLatestUrl(request), {
|
|
@@ -2417,19 +2605,34 @@ const fetchLatestSnapshot = async ({ request, env }) => {
|
|
|
2417
2605
|
authorization: `Bearer ${apiKey}`,
|
|
2418
2606
|
},
|
|
2419
2607
|
});
|
|
2420
|
-
} catch {
|
|
2421
|
-
|
|
2608
|
+
} catch (error) {
|
|
2609
|
+
if (allowFullRegeneration) {
|
|
2610
|
+
return null;
|
|
2611
|
+
}
|
|
2612
|
+
throw new Error(
|
|
2613
|
+
`previous semantic snapshot lookup failed: ${
|
|
2614
|
+
error instanceof Error ? error.message : String(error)
|
|
2615
|
+
}`,
|
|
2616
|
+
);
|
|
2422
2617
|
}
|
|
2423
2618
|
if (response.status === 404 || response.status === 204) {
|
|
2424
2619
|
return null;
|
|
2425
2620
|
}
|
|
2426
2621
|
if (!response.ok) {
|
|
2427
|
-
|
|
2622
|
+
if (allowFullRegeneration) {
|
|
2623
|
+
return null;
|
|
2624
|
+
}
|
|
2625
|
+
throw new Error(
|
|
2626
|
+
`previous semantic snapshot lookup failed: ${response.status}`,
|
|
2627
|
+
);
|
|
2428
2628
|
}
|
|
2429
2629
|
const body = await response.json();
|
|
2430
2630
|
const candidate = body?.snapshot ?? body;
|
|
2431
2631
|
if (!candidate || !Array.isArray(candidate.routes)) {
|
|
2432
|
-
|
|
2632
|
+
if (allowFullRegeneration) {
|
|
2633
|
+
return null;
|
|
2634
|
+
}
|
|
2635
|
+
throw new Error("previous semantic snapshot lookup returned invalid body");
|
|
2433
2636
|
}
|
|
2434
2637
|
const parsed = validateSemanticSnapshotRequest(candidate);
|
|
2435
2638
|
assertNoRawCodeStructure(parsed);
|
|
@@ -2475,14 +2678,89 @@ const sendSnapshot = async ({ request, env, snapshot }) => {
|
|
|
2475
2678
|
}
|
|
2476
2679
|
};
|
|
2477
2680
|
|
|
2681
|
+
const assertSemanticSnapshotAudit = ({
|
|
2682
|
+
routes,
|
|
2683
|
+
snapshot,
|
|
2684
|
+
generationContract,
|
|
2685
|
+
aiRuntime,
|
|
2686
|
+
}) => {
|
|
2687
|
+
const auditedSnapshot = validateSemanticSnapshotRequest(snapshot);
|
|
2688
|
+
assertSnapshotRouteCoverage({ routes, snapshotRoutes: auditedSnapshot.routes });
|
|
2689
|
+
assertNoRawCodeStructure(auditedSnapshot);
|
|
2690
|
+
if (canonicalJson(auditedSnapshot.generation_contract) !== canonicalJson(generationContract)) {
|
|
2691
|
+
throw new Error("semantic snapshot generation contract mismatch");
|
|
2692
|
+
}
|
|
2693
|
+
if (canonicalJson(auditedSnapshot.ai_runtime) !== canonicalJson(aiRuntime)) {
|
|
2694
|
+
throw new Error("semantic snapshot AI runtime metadata mismatch");
|
|
2695
|
+
}
|
|
2696
|
+
const routeByKey = new Map(
|
|
2697
|
+
routes.map((route) => [route.operation_source_key, route]),
|
|
2698
|
+
);
|
|
2699
|
+
const allowedOrigins = new Set([
|
|
2700
|
+
"new_route_ai_generated",
|
|
2701
|
+
"unchanged_route_reused",
|
|
2702
|
+
"changed_route_semantic_reused",
|
|
2703
|
+
"changed_route_semantic_regenerated",
|
|
2704
|
+
"changed_route_needs_review",
|
|
2705
|
+
]);
|
|
2706
|
+
for (const route of auditedSnapshot.routes) {
|
|
2707
|
+
const currentRoute = routeByKey.get(route.operation_source_key);
|
|
2708
|
+
if (!currentRoute) {
|
|
2709
|
+
throw new Error(
|
|
2710
|
+
`semantic snapshot audit found unknown route: ${route.operation_source_key}`,
|
|
2711
|
+
);
|
|
2712
|
+
}
|
|
2713
|
+
const expectedInputHash = routeInputHash(currentRoute);
|
|
2714
|
+
if (route.route_input_hash !== expectedInputHash) {
|
|
2715
|
+
throw new Error(
|
|
2716
|
+
`semantic snapshot audit found stale route_input_hash: ${route.operation_source_key}`,
|
|
2717
|
+
);
|
|
2718
|
+
}
|
|
2719
|
+
if (!allowedOrigins.has(route.semantic_origin)) {
|
|
2720
|
+
throw new Error(
|
|
2721
|
+
`semantic snapshot audit found invalid semantic_origin: ${route.operation_source_key}`,
|
|
2722
|
+
);
|
|
2723
|
+
}
|
|
2724
|
+
if (route.route_semantic_hash !== routeSemanticHash(route)) {
|
|
2725
|
+
throw new Error(
|
|
2726
|
+
`semantic snapshot audit found stale route_semantic_hash: ${route.operation_source_key}`,
|
|
2727
|
+
);
|
|
2728
|
+
}
|
|
2729
|
+
if (
|
|
2730
|
+
(route.semantic_origin === "unchanged_route_reused" ||
|
|
2731
|
+
route.semantic_origin === "changed_route_semantic_reused") &&
|
|
2732
|
+
(!route.previous_route_input_hash ||
|
|
2733
|
+
!route.previous_route_semantic_hash ||
|
|
2734
|
+
!route.previous_semantic_snapshot_version)
|
|
2735
|
+
) {
|
|
2736
|
+
throw new Error(
|
|
2737
|
+
`semantic snapshot audit found incomplete reuse metadata: ${route.operation_source_key}`,
|
|
2738
|
+
);
|
|
2739
|
+
}
|
|
2740
|
+
if (
|
|
2741
|
+
route.semantic_origin === "changed_route_needs_review" &&
|
|
2742
|
+
!route.semantic_change_reason
|
|
2743
|
+
) {
|
|
2744
|
+
throw new Error(
|
|
2745
|
+
`semantic snapshot audit found missing review reason: ${route.operation_source_key}`,
|
|
2746
|
+
);
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
};
|
|
2750
|
+
|
|
2478
2751
|
export const runSemanticCi = async ({
|
|
2479
2752
|
repoRoot,
|
|
2480
2753
|
request: rawRequest,
|
|
2481
2754
|
env,
|
|
2482
2755
|
previousSnapshot: providedPreviousSnapshot,
|
|
2756
|
+
agentSkills: rawAgentSkills,
|
|
2483
2757
|
}) => {
|
|
2484
2758
|
const request = validateSemanticCiRequest(rawRequest);
|
|
2485
2759
|
const hashScope = semanticSnapshotHashScope(request);
|
|
2760
|
+
const generationContract = semanticGenerationContract();
|
|
2761
|
+
const agentSkills = validateSemanticAgentSkillBundle(
|
|
2762
|
+
rawAgentSkills ?? semanticAgentSkillBundle(),
|
|
2763
|
+
);
|
|
2486
2764
|
const files = await listAllowedPythonFiles({
|
|
2487
2765
|
repoRoot,
|
|
2488
2766
|
allowedSourcePaths: request.allowed_source_paths,
|
|
@@ -2494,6 +2772,9 @@ export const runSemanticCi = async ({
|
|
|
2494
2772
|
"semantic CI discovered zero routes; check framework, backend root path, and allowed_source_paths",
|
|
2495
2773
|
);
|
|
2496
2774
|
}
|
|
2775
|
+
if (!env.CLUE_API_KEY) {
|
|
2776
|
+
throw new Error("CLUE_API_KEY is required");
|
|
2777
|
+
}
|
|
2497
2778
|
const previousSnapshot =
|
|
2498
2779
|
providedPreviousSnapshot === undefined
|
|
2499
2780
|
? await fetchLatestSnapshot({ request, env })
|
|
@@ -2503,37 +2784,69 @@ export const runSemanticCi = async ({
|
|
|
2503
2784
|
const previousRoute = previousRoutes.get(route.operation_source_key);
|
|
2504
2785
|
return !previousRoute || previousRoute.route_input_hash !== routeInputHash(route);
|
|
2505
2786
|
});
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2787
|
+
if (routeNeedsAi && !env.CLUE_AI_PROVIDER_API_KEY) {
|
|
2788
|
+
throw new Error("CLUE_AI_PROVIDER_API_KEY is required for semantic generation");
|
|
2789
|
+
}
|
|
2790
|
+
const aiProviderConfig = routeNeedsAi
|
|
2791
|
+
? resolveAiProviderConfig({ env, request })
|
|
2792
|
+
: null;
|
|
2793
|
+
const aiRuntime =
|
|
2794
|
+
aiProviderConfig === null && previousSnapshot?.ai_runtime
|
|
2795
|
+
? previousSnapshot.ai_runtime
|
|
2796
|
+
: clueClientCiSemanticAiRuntime({
|
|
2797
|
+
provider: aiProviderConfig?.provider ?? "not_used",
|
|
2798
|
+
model: aiProviderConfig?.model ?? "not_used",
|
|
2799
|
+
roleIds: SEMANTIC_AGENT_ROLE_IDS
|
|
2800
|
+
? Object.values(SEMANTIC_AGENT_ROLE_IDS)
|
|
2801
|
+
: [],
|
|
2802
|
+
runnerVersion: SEMANTIC_AGENT_RUNNER_VERSION,
|
|
2803
|
+
});
|
|
2804
|
+
const {
|
|
2805
|
+
plans: routePlans,
|
|
2806
|
+
routesRequiringGeneration,
|
|
2807
|
+
deletedRouteCount,
|
|
2808
|
+
} = await classifyRoutesForSnapshot({
|
|
2809
|
+
request,
|
|
2810
|
+
env,
|
|
2811
|
+
routes,
|
|
2812
|
+
previousSnapshot,
|
|
2813
|
+
aiProviderApiKey: aiProviderConfig?.apiKey ?? null,
|
|
2814
|
+
hashScope,
|
|
2815
|
+
generationContract,
|
|
2816
|
+
agentSkills,
|
|
2817
|
+
});
|
|
2515
2818
|
const promptRoutes = routesRequiringGeneration.map((route) =>
|
|
2516
2819
|
buildRouteEvidencePromptEntry({ route, hashScope }),
|
|
2517
2820
|
);
|
|
2518
2821
|
const aiRoutes = await generateAiRoutesPerRoute({
|
|
2519
2822
|
request,
|
|
2520
|
-
|
|
2823
|
+
env,
|
|
2824
|
+
apiKey: aiProviderConfig?.apiKey ?? null,
|
|
2521
2825
|
routes: promptRoutes,
|
|
2826
|
+
generationContract,
|
|
2827
|
+
agentSkills,
|
|
2522
2828
|
});
|
|
2523
2829
|
const { selectionCandidates, routeBySelectionKey } = buildSelectorCandidates({
|
|
2524
2830
|
routes: routesRequiringGeneration,
|
|
2525
2831
|
aiRoutes,
|
|
2526
2832
|
hashScope,
|
|
2527
2833
|
});
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2834
|
+
let aiSelectionDecisions = new Map();
|
|
2835
|
+
if (selectionCandidates.length > 0) {
|
|
2836
|
+
try {
|
|
2837
|
+
aiSelectionDecisions = await callAiSelectionProvider({
|
|
2838
|
+
request,
|
|
2839
|
+
env,
|
|
2840
|
+
apiKey: aiProviderConfig?.apiKey ?? null,
|
|
2841
|
+
selectionCandidates,
|
|
2842
|
+
routeBySelectionKey,
|
|
2843
|
+
generationContract,
|
|
2844
|
+
agentSkills,
|
|
2845
|
+
});
|
|
2846
|
+
} catch {
|
|
2847
|
+
aiSelectionDecisions = new Map();
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2537
2850
|
const snapshot = buildSnapshot({
|
|
2538
2851
|
request,
|
|
2539
2852
|
routes,
|
|
@@ -2542,6 +2855,14 @@ export const runSemanticCi = async ({
|
|
|
2542
2855
|
routePlans,
|
|
2543
2856
|
previousSnapshot,
|
|
2544
2857
|
deletedRouteCount,
|
|
2858
|
+
generationContract,
|
|
2859
|
+
aiRuntime,
|
|
2860
|
+
});
|
|
2861
|
+
assertSemanticSnapshotAudit({
|
|
2862
|
+
routes,
|
|
2863
|
+
snapshot,
|
|
2864
|
+
generationContract,
|
|
2865
|
+
aiRuntime,
|
|
2545
2866
|
});
|
|
2546
2867
|
const upload = await sendSnapshot({ request, env, snapshot });
|
|
2547
2868
|
return {
|