@clue-ai/cli 0.0.6 → 0.0.8
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 +11 -14
- package/bin/clue-cli.mjs +114 -21
- package/commands/claude-code/clue-init.md +6 -5
- package/commands/codex/clue-init.md +6 -5
- package/package.json +1 -1
- package/src/ai-provider.mjs +3 -2
- package/src/command-spec.mjs +2 -0
- package/src/contracts.mjs +5 -4
- package/src/init-tool.mjs +5 -6
- package/src/public-schema.cjs +47 -0
- package/src/semantic-agent-runner.mjs +157 -0
- package/src/semantic-ai-config.mjs +17 -0
- package/src/semantic-ci.mjs +419 -57
- package/src/setup-check.mjs +28 -2
- package/src/setup-prepare.mjs +181 -14
- package/src/setup-tool.mjs +159 -11
package/src/semantic-ci.mjs
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import { createHash, createHmac } from "node:crypto";
|
|
2
|
-
import { callJsonAiProvider, resolveAiProviderConfig } from "./ai-provider.mjs";
|
|
3
2
|
import {
|
|
4
3
|
validateSemanticCiRequest,
|
|
5
4
|
validateSemanticSnapshotRequest,
|
|
6
5
|
validateSemanticSnapshotResponse,
|
|
7
6
|
} from "./contracts.mjs";
|
|
7
|
+
import {
|
|
8
|
+
callJsonAiProvider,
|
|
9
|
+
resolveAiProviderConfig,
|
|
10
|
+
} from "./ai-provider.mjs";
|
|
8
11
|
import { analyzeFastApiRoutes } from "./fastapi-analyzer.mjs";
|
|
9
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";
|
|
10
21
|
|
|
11
22
|
const sha256 = (value) =>
|
|
12
23
|
`sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
@@ -69,6 +80,12 @@ const ACTUAL_OUTCOME_ACTIONS = new Set([
|
|
|
69
80
|
]);
|
|
70
81
|
|
|
71
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";
|
|
72
89
|
|
|
73
90
|
const semanticSnapshotHashScope = (request) =>
|
|
74
91
|
[
|
|
@@ -78,6 +95,15 @@ const semanticSnapshotHashScope = (request) =>
|
|
|
78
95
|
request.service.service_key,
|
|
79
96
|
].join(":");
|
|
80
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
|
+
|
|
81
107
|
const canonicalJson = (value) => {
|
|
82
108
|
if (Array.isArray(value)) {
|
|
83
109
|
return `[${value.map((entry) => canonicalJson(entry)).join(",")}]`;
|
|
@@ -150,9 +176,14 @@ const fallbackSemantics = (route, reason, hashScope) => ({
|
|
|
150
176
|
confidence_reason: reason,
|
|
151
177
|
});
|
|
152
178
|
|
|
153
|
-
const buildAiPrompt = (routes) =>
|
|
179
|
+
const buildAiPrompt = ({ routes, generationContract, agentSkills }) =>
|
|
154
180
|
JSON.stringify({
|
|
181
|
+
...semanticAgentPromptEnvelope({
|
|
182
|
+
bundle: agentSkills,
|
|
183
|
+
roleId: SEMANTIC_AGENT_ROLE_IDS.routeSemanticGenerator,
|
|
184
|
+
}),
|
|
155
185
|
task: "Generate privacy-safe semantic meaning candidates for FastAPI operation sources. Return JSON only.",
|
|
186
|
+
generation_contract: generationContract,
|
|
156
187
|
rules: [
|
|
157
188
|
"Do not create operation_source_key values; only copy the provided keys.",
|
|
158
189
|
"Do not create ids, refs, hashes, source fingerprints, or field paths.",
|
|
@@ -200,9 +231,20 @@ const buildAiPrompt = (routes) =>
|
|
|
200
231
|
})),
|
|
201
232
|
});
|
|
202
233
|
|
|
203
|
-
const buildAiReuseDecisionPrompt = ({
|
|
234
|
+
const buildAiReuseDecisionPrompt = ({
|
|
235
|
+
route,
|
|
236
|
+
previousRoute,
|
|
237
|
+
currentHash,
|
|
238
|
+
generationContract,
|
|
239
|
+
agentSkills,
|
|
240
|
+
}) =>
|
|
204
241
|
JSON.stringify({
|
|
242
|
+
...semanticAgentPromptEnvelope({
|
|
243
|
+
bundle: agentSkills,
|
|
244
|
+
roleId: SEMANTIC_AGENT_ROLE_IDS.reuseJudge,
|
|
245
|
+
}),
|
|
205
246
|
task: "Decide whether previous privacy-safe route semantics still apply after a route source change. Return JSON only.",
|
|
247
|
+
generation_contract: generationContract,
|
|
206
248
|
rules: [
|
|
207
249
|
"Only decide for the provided operation_source_key.",
|
|
208
250
|
"Return decision as reuse_previous, regenerate, or needs_review.",
|
|
@@ -235,6 +277,28 @@ const buildAiReuseDecisionPrompt = ({ route, previousRoute, currentHash }) =>
|
|
|
235
277
|
});
|
|
236
278
|
|
|
237
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
|
+
}
|
|
238
302
|
const decision = safeArray(parsed?.decisions).find(
|
|
239
303
|
(entry) => entry?.operation_source_key === operationSourceKey,
|
|
240
304
|
);
|
|
@@ -271,6 +335,8 @@ const callAiReuseDecisionProvider = async ({
|
|
|
271
335
|
route,
|
|
272
336
|
previousRoute,
|
|
273
337
|
currentHash,
|
|
338
|
+
generationContract,
|
|
339
|
+
agentSkills,
|
|
274
340
|
}) => {
|
|
275
341
|
try {
|
|
276
342
|
const parsed = await callJsonAiProvider({
|
|
@@ -281,6 +347,8 @@ const callAiReuseDecisionProvider = async ({
|
|
|
281
347
|
route,
|
|
282
348
|
previousRoute,
|
|
283
349
|
currentHash,
|
|
350
|
+
generationContract,
|
|
351
|
+
agentSkills,
|
|
284
352
|
}),
|
|
285
353
|
toolName: "return_reuse_decision",
|
|
286
354
|
toolDescription:
|
|
@@ -307,26 +375,59 @@ const callAiReuseDecisionProvider = async ({
|
|
|
307
375
|
}
|
|
308
376
|
};
|
|
309
377
|
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
+
}
|
|
316
407
|
}
|
|
317
|
-
return apiKey;
|
|
318
408
|
};
|
|
319
409
|
|
|
320
|
-
const callAiProvider = async ({
|
|
410
|
+
const callAiProvider = async ({
|
|
411
|
+
request,
|
|
412
|
+
env,
|
|
413
|
+
apiKey,
|
|
414
|
+
routes,
|
|
415
|
+
generationContract,
|
|
416
|
+
agentSkills,
|
|
417
|
+
}) => {
|
|
321
418
|
const parsed = await callJsonAiProvider({
|
|
322
419
|
config: resolveAiProviderConfig({ env, request, apiKey }),
|
|
323
420
|
system: "You generate privacy-safe route semantics JSON.",
|
|
324
|
-
user: buildAiPrompt(routes),
|
|
421
|
+
user: buildAiPrompt({ routes, generationContract, agentSkills }),
|
|
325
422
|
toolName: "return_route_semantics",
|
|
326
423
|
toolDescription: "Return privacy-safe route semantics JSON.",
|
|
327
424
|
failureMessage: "AI provider failed",
|
|
328
425
|
emptyMessage: "AI provider returned empty semantic generation content",
|
|
329
426
|
});
|
|
427
|
+
assertAiRouteResponseContract({
|
|
428
|
+
parsed,
|
|
429
|
+
expectedRouteKeys: routes.map((route) => route.operation_source_key),
|
|
430
|
+
});
|
|
330
431
|
return new Map(
|
|
331
432
|
(parsed.routes ?? []).map((route) => [route.operation_source_key, route]),
|
|
332
433
|
);
|
|
@@ -374,7 +475,14 @@ const assertSnapshotRouteCoverage = ({ routes, snapshotRoutes }) => {
|
|
|
374
475
|
}
|
|
375
476
|
};
|
|
376
477
|
|
|
377
|
-
const generateAiRoutesPerRoute = async ({
|
|
478
|
+
const generateAiRoutesPerRoute = async ({
|
|
479
|
+
request,
|
|
480
|
+
env,
|
|
481
|
+
apiKey,
|
|
482
|
+
routes,
|
|
483
|
+
generationContract,
|
|
484
|
+
agentSkills,
|
|
485
|
+
}) => {
|
|
378
486
|
const aiRoutes = new Map();
|
|
379
487
|
for (const route of routes) {
|
|
380
488
|
try {
|
|
@@ -383,6 +491,8 @@ const generateAiRoutesPerRoute = async ({ request, env, apiKey, routes }) => {
|
|
|
383
491
|
env,
|
|
384
492
|
apiKey,
|
|
385
493
|
routes: [route],
|
|
494
|
+
generationContract,
|
|
495
|
+
agentSkills,
|
|
386
496
|
});
|
|
387
497
|
const aiRoute = routeAiRoutes.get(route.operation_source_key);
|
|
388
498
|
if (aiRoute?.semantics) {
|
|
@@ -409,9 +519,18 @@ const generateAiRoutesPerRoute = async ({ request, env, apiKey, routes }) => {
|
|
|
409
519
|
return aiRoutes;
|
|
410
520
|
};
|
|
411
521
|
|
|
412
|
-
const buildAiSelectionPrompt = (
|
|
522
|
+
const buildAiSelectionPrompt = ({
|
|
523
|
+
selectionCandidates,
|
|
524
|
+
generationContract,
|
|
525
|
+
agentSkills,
|
|
526
|
+
}) =>
|
|
413
527
|
JSON.stringify({
|
|
528
|
+
...semanticAgentPromptEnvelope({
|
|
529
|
+
bundle: agentSkills,
|
|
530
|
+
roleId: SEMANTIC_AGENT_ROLE_IDS.semanticSelector,
|
|
531
|
+
}),
|
|
414
532
|
task: "Choose adopted semantic values from deterministic and AI candidates. Return JSON only.",
|
|
533
|
+
generation_contract: generationContract,
|
|
415
534
|
rules: [
|
|
416
535
|
"Only copy operation_source_key, candidate_index, and parameter_name values from the provided selection_candidates.",
|
|
417
536
|
"For each semantic field, return selected_source as deterministic or ai.",
|
|
@@ -442,15 +561,21 @@ const callAiSelectionProvider = async ({
|
|
|
442
561
|
apiKey,
|
|
443
562
|
selectionCandidates,
|
|
444
563
|
routeBySelectionKey,
|
|
564
|
+
generationContract,
|
|
565
|
+
agentSkills,
|
|
445
566
|
}) => {
|
|
446
567
|
if (selectionCandidates.length === 0) {
|
|
447
568
|
return new Map();
|
|
448
569
|
}
|
|
449
|
-
|
|
570
|
+
assertNoRawCodeStructureForSelectionCandidates(selectionCandidates);
|
|
450
571
|
const parsed = await callJsonAiProvider({
|
|
451
572
|
config: resolveAiProviderConfig({ env, request, apiKey }),
|
|
452
573
|
system: "You select adopted semantic values from privacy-safe candidates.",
|
|
453
|
-
user: buildAiSelectionPrompt(
|
|
574
|
+
user: buildAiSelectionPrompt({
|
|
575
|
+
selectionCandidates,
|
|
576
|
+
generationContract,
|
|
577
|
+
agentSkills,
|
|
578
|
+
}),
|
|
454
579
|
toolName: "return_semantic_selection",
|
|
455
580
|
toolDescription:
|
|
456
581
|
"Return selected semantic values from privacy-safe candidates.",
|
|
@@ -460,11 +585,34 @@ const callAiSelectionProvider = async ({
|
|
|
460
585
|
return normalizeAiSelectionResponse({
|
|
461
586
|
parsed,
|
|
462
587
|
routeBySelectionKey,
|
|
588
|
+
validSelectionParametersByKey: validSelectionParametersByKey(
|
|
589
|
+
selectionCandidates,
|
|
590
|
+
),
|
|
463
591
|
});
|
|
464
592
|
};
|
|
465
593
|
|
|
466
|
-
const buildSemanticSnapshotVersion = ({
|
|
467
|
-
|
|
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
|
+
)}`;
|
|
468
616
|
|
|
469
617
|
const normalizeSnakeSegment = (value) => {
|
|
470
618
|
const normalized = String(value ?? "")
|
|
@@ -827,7 +975,30 @@ const candidateHasUnsafeSemanticText = (candidate) =>
|
|
|
827
975
|
const selectionKeyForCandidate = (route, candidateIndex) =>
|
|
828
976
|
`${route.operation_source_key}#${candidateIndex}`;
|
|
829
977
|
|
|
830
|
-
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
|
+
}
|
|
831
1002
|
const decisionsByCandidate = new Map();
|
|
832
1003
|
for (const decision of safeArray(parsed?.field_selections)) {
|
|
833
1004
|
if (!decision || typeof decision !== "object") {
|
|
@@ -846,6 +1017,10 @@ const normalizeAiSelectionResponse = ({ parsed, routeBySelectionKey }) => {
|
|
|
846
1017
|
if (!route) {
|
|
847
1018
|
continue;
|
|
848
1019
|
}
|
|
1020
|
+
const validParameters = validSelectionParametersByKey.get(selectionKey);
|
|
1021
|
+
if (!validParameters?.has(decision.parameter_name)) {
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
849
1024
|
const candidateDecisions =
|
|
850
1025
|
decisionsByCandidate.get(selectionKey) ?? new Map();
|
|
851
1026
|
candidateDecisions.set(decision.parameter_name, {
|
|
@@ -1011,7 +1186,8 @@ const buildSelectorCandidates = ({ routes, aiRoutes, hashScope }) => {
|
|
|
1011
1186
|
candidateHasUnsafeSemanticText(candidate) ||
|
|
1012
1187
|
!values.rawTargetObjectText ||
|
|
1013
1188
|
!values.targetKeyCandidate ||
|
|
1014
|
-
!values.aiExpectedDomainEffect
|
|
1189
|
+
(!values.aiExpectedDomainEffect &&
|
|
1190
|
+
!values.deterministicExpectedDomainEffect) ||
|
|
1015
1191
|
!hasBehaviorEvidence(values.sourceEvidenceRefs, routeEvidenceRefs) ||
|
|
1016
1192
|
!candidateHasTargetSpecificEvidence({
|
|
1017
1193
|
route,
|
|
@@ -1577,13 +1753,12 @@ const buildSnapshot = ({
|
|
|
1577
1753
|
routePlans = new Map(),
|
|
1578
1754
|
previousSnapshot,
|
|
1579
1755
|
deletedRouteCount = 0,
|
|
1756
|
+
generationContract,
|
|
1757
|
+
aiRuntime,
|
|
1580
1758
|
}) => {
|
|
1581
1759
|
const generatedAt = new Date().toISOString();
|
|
1582
1760
|
const hashScope = semanticSnapshotHashScope(request);
|
|
1583
|
-
const
|
|
1584
|
-
request,
|
|
1585
|
-
generatedAt,
|
|
1586
|
-
});
|
|
1761
|
+
const provisionalSemanticSnapshotVersion = "snap_pending";
|
|
1587
1762
|
const targetObjectProfiles = [];
|
|
1588
1763
|
const targetObjectMappings = [];
|
|
1589
1764
|
const sourceEvidenceRefs = [];
|
|
@@ -1617,7 +1792,7 @@ const buildSnapshot = ({
|
|
|
1617
1792
|
const snapshotRoutes = routes.map((route) => {
|
|
1618
1793
|
const routeWithVersion = {
|
|
1619
1794
|
...route,
|
|
1620
|
-
semantic_snapshot_version:
|
|
1795
|
+
semantic_snapshot_version: provisionalSemanticSnapshotVersion,
|
|
1621
1796
|
};
|
|
1622
1797
|
const plan = routePlans.get(route.operation_source_key) ?? {
|
|
1623
1798
|
origin: "new_route_ai_generated",
|
|
@@ -1636,7 +1811,7 @@ const buildSnapshot = ({
|
|
|
1636
1811
|
return rebasePreviousRoute({
|
|
1637
1812
|
previousRoute: plan.previous_route,
|
|
1638
1813
|
currentRoute: route,
|
|
1639
|
-
semanticSnapshotVersion,
|
|
1814
|
+
semanticSnapshotVersion: provisionalSemanticSnapshotVersion,
|
|
1640
1815
|
plan,
|
|
1641
1816
|
});
|
|
1642
1817
|
}
|
|
@@ -1654,7 +1829,7 @@ const buildSnapshot = ({
|
|
|
1654
1829
|
? fallbackSemantics(routeWithVersion, unavailableReason, hashScope)
|
|
1655
1830
|
: {
|
|
1656
1831
|
operation_source_key: route.operation_source_key,
|
|
1657
|
-
semantic_snapshot_version:
|
|
1832
|
+
semantic_snapshot_version: provisionalSemanticSnapshotVersion,
|
|
1658
1833
|
method: route.method,
|
|
1659
1834
|
path_template: route.path_template,
|
|
1660
1835
|
semantics: sanitizeSemantics(aiRoute.semantics, route),
|
|
@@ -1735,6 +1910,15 @@ const buildSnapshot = ({
|
|
|
1735
1910
|
sourceEvidenceRefs,
|
|
1736
1911
|
(entry) => entry.id,
|
|
1737
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
|
+
}));
|
|
1738
1922
|
|
|
1739
1923
|
const snapshot = validateSemanticSnapshotRequest({
|
|
1740
1924
|
project_key: request.project_key,
|
|
@@ -1743,42 +1927,48 @@ const buildSnapshot = ({
|
|
|
1743
1927
|
),
|
|
1744
1928
|
schema_version: 2,
|
|
1745
1929
|
semantic_snapshot_version: semanticSnapshotVersion,
|
|
1930
|
+
generation_contract: generationContract,
|
|
1931
|
+
ai_runtime: aiRuntime,
|
|
1746
1932
|
generated_at: generatedAt,
|
|
1747
1933
|
repository: request.repository,
|
|
1748
|
-
service:
|
|
1934
|
+
service: {
|
|
1935
|
+
service_key: request.service.service_key,
|
|
1936
|
+
framework: request.service.framework,
|
|
1937
|
+
language: request.service.language,
|
|
1938
|
+
},
|
|
1749
1939
|
analysis_summary: {
|
|
1750
1940
|
total_routes_scanned: routes.length,
|
|
1751
|
-
routes_generated:
|
|
1941
|
+
routes_generated: versionedSnapshotRoutes.filter(
|
|
1752
1942
|
(route) => route.semantics.route_confidence > 0,
|
|
1753
1943
|
).length,
|
|
1754
|
-
uncertain_routes:
|
|
1944
|
+
uncertain_routes: versionedSnapshotRoutes.filter(
|
|
1755
1945
|
(route) => route.semantics.route_confidence < 0.5,
|
|
1756
1946
|
).length,
|
|
1757
1947
|
failed_routes: failedRouteCount,
|
|
1758
|
-
routes_reused:
|
|
1948
|
+
routes_reused: versionedSnapshotRoutes.filter((route) =>
|
|
1759
1949
|
["unchanged_route_reused", "changed_route_semantic_reused"].includes(
|
|
1760
1950
|
route.semantic_origin,
|
|
1761
1951
|
),
|
|
1762
1952
|
).length,
|
|
1763
|
-
routes_ai_generated:
|
|
1953
|
+
routes_ai_generated: versionedSnapshotRoutes.filter((route) =>
|
|
1764
1954
|
[
|
|
1765
1955
|
"new_route_ai_generated",
|
|
1766
1956
|
"changed_route_semantic_regenerated",
|
|
1767
1957
|
].includes(route.semantic_origin),
|
|
1768
1958
|
).length,
|
|
1769
1959
|
routes_deleted: deletedRouteCount,
|
|
1770
|
-
routes_needs_review:
|
|
1960
|
+
routes_needs_review: versionedSnapshotRoutes.filter(
|
|
1771
1961
|
(route) => route.semantic_origin === "changed_route_needs_review",
|
|
1772
1962
|
).length,
|
|
1773
|
-
changed_routes_semantic_reused:
|
|
1963
|
+
changed_routes_semantic_reused: versionedSnapshotRoutes.filter(
|
|
1774
1964
|
(route) => route.semantic_origin === "changed_route_semantic_reused",
|
|
1775
1965
|
).length,
|
|
1776
|
-
changed_routes_semantic_regenerated:
|
|
1966
|
+
changed_routes_semantic_regenerated: versionedSnapshotRoutes.filter(
|
|
1777
1967
|
(route) =>
|
|
1778
1968
|
route.semantic_origin === "changed_route_semantic_regenerated",
|
|
1779
1969
|
).length,
|
|
1780
1970
|
},
|
|
1781
|
-
routes:
|
|
1971
|
+
routes: versionedSnapshotRoutes,
|
|
1782
1972
|
target_object_profiles: targetObjectProfiles,
|
|
1783
1973
|
target_object_catalog: [...catalogByGroup.values()],
|
|
1784
1974
|
target_object_mappings: targetObjectMappings,
|
|
@@ -1983,6 +2173,8 @@ const classifyRoutesForSnapshot = async ({
|
|
|
1983
2173
|
previousSnapshot,
|
|
1984
2174
|
aiProviderApiKey,
|
|
1985
2175
|
hashScope,
|
|
2176
|
+
generationContract,
|
|
2177
|
+
agentSkills,
|
|
1986
2178
|
}) => {
|
|
1987
2179
|
const previousRoutes = previousRouteMap(previousSnapshot);
|
|
1988
2180
|
const plans = new Map();
|
|
@@ -2021,8 +2213,10 @@ const classifyRoutesForSnapshot = async ({
|
|
|
2021
2213
|
route: buildRouteEvidencePromptEntry({ route, hashScope }),
|
|
2022
2214
|
previousRoute,
|
|
2023
2215
|
currentHash,
|
|
2216
|
+
generationContract,
|
|
2217
|
+
agentSkills,
|
|
2024
2218
|
});
|
|
2025
|
-
if (decision.decision === "reuse_previous") {
|
|
2219
|
+
if (decision.decision === "reuse_previous" && decision.confidence >= 0.75) {
|
|
2026
2220
|
plans.set(route.operation_source_key, {
|
|
2027
2221
|
origin: "changed_route_semantic_reused",
|
|
2028
2222
|
route_input_hash: currentHash,
|
|
@@ -2034,6 +2228,21 @@ const classifyRoutesForSnapshot = async ({
|
|
|
2034
2228
|
});
|
|
2035
2229
|
continue;
|
|
2036
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
|
+
}
|
|
2037
2246
|
if (decision.decision === "regenerate") {
|
|
2038
2247
|
plans.set(route.operation_source_key, {
|
|
2039
2248
|
origin: "changed_route_semantic_regenerated",
|
|
@@ -2333,6 +2542,32 @@ const assertNoRawCodeStructure = (value, path = "snapshot") => {
|
|
|
2333
2542
|
}
|
|
2334
2543
|
};
|
|
2335
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
|
+
|
|
2336
2571
|
const semanticSnapshotUrl = (baseUrl) => {
|
|
2337
2572
|
const normalized = baseUrl.replace(/\/+$/, "");
|
|
2338
2573
|
if (normalized.endsWith("/api/v1/semantic-snapshots")) {
|
|
@@ -2360,6 +2595,8 @@ const fetchLatestSnapshot = async ({ request, env }) => {
|
|
|
2360
2595
|
if (!apiKey) {
|
|
2361
2596
|
return null;
|
|
2362
2597
|
}
|
|
2598
|
+
const allowFullRegeneration =
|
|
2599
|
+
env.CLUE_SEMANTIC_ALLOW_FULL_REGEN_WITHOUT_CACHE === "1";
|
|
2363
2600
|
let response;
|
|
2364
2601
|
try {
|
|
2365
2602
|
response = await fetch(semanticSnapshotLatestUrl(request), {
|
|
@@ -2368,19 +2605,34 @@ const fetchLatestSnapshot = async ({ request, env }) => {
|
|
|
2368
2605
|
authorization: `Bearer ${apiKey}`,
|
|
2369
2606
|
},
|
|
2370
2607
|
});
|
|
2371
|
-
} catch {
|
|
2372
|
-
|
|
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
|
+
);
|
|
2373
2617
|
}
|
|
2374
2618
|
if (response.status === 404 || response.status === 204) {
|
|
2375
2619
|
return null;
|
|
2376
2620
|
}
|
|
2377
2621
|
if (!response.ok) {
|
|
2378
|
-
|
|
2622
|
+
if (allowFullRegeneration) {
|
|
2623
|
+
return null;
|
|
2624
|
+
}
|
|
2625
|
+
throw new Error(
|
|
2626
|
+
`previous semantic snapshot lookup failed: ${response.status}`,
|
|
2627
|
+
);
|
|
2379
2628
|
}
|
|
2380
2629
|
const body = await response.json();
|
|
2381
2630
|
const candidate = body?.snapshot ?? body;
|
|
2382
2631
|
if (!candidate || !Array.isArray(candidate.routes)) {
|
|
2383
|
-
|
|
2632
|
+
if (allowFullRegeneration) {
|
|
2633
|
+
return null;
|
|
2634
|
+
}
|
|
2635
|
+
throw new Error("previous semantic snapshot lookup returned invalid body");
|
|
2384
2636
|
}
|
|
2385
2637
|
const parsed = validateSemanticSnapshotRequest(candidate);
|
|
2386
2638
|
assertNoRawCodeStructure(parsed);
|
|
@@ -2426,14 +2678,89 @@ const sendSnapshot = async ({ request, env, snapshot }) => {
|
|
|
2426
2678
|
}
|
|
2427
2679
|
};
|
|
2428
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
|
+
|
|
2429
2751
|
export const runSemanticCi = async ({
|
|
2430
2752
|
repoRoot,
|
|
2431
2753
|
request: rawRequest,
|
|
2432
2754
|
env,
|
|
2433
2755
|
previousSnapshot: providedPreviousSnapshot,
|
|
2756
|
+
agentSkills: rawAgentSkills,
|
|
2434
2757
|
}) => {
|
|
2435
2758
|
const request = validateSemanticCiRequest(rawRequest);
|
|
2436
2759
|
const hashScope = semanticSnapshotHashScope(request);
|
|
2760
|
+
const generationContract = semanticGenerationContract();
|
|
2761
|
+
const agentSkills = validateSemanticAgentSkillBundle(
|
|
2762
|
+
rawAgentSkills ?? semanticAgentSkillBundle(),
|
|
2763
|
+
);
|
|
2437
2764
|
const files = await listAllowedPythonFiles({
|
|
2438
2765
|
repoRoot,
|
|
2439
2766
|
allowedSourcePaths: request.allowed_source_paths,
|
|
@@ -2445,6 +2772,9 @@ export const runSemanticCi = async ({
|
|
|
2445
2772
|
"semantic CI discovered zero routes; check framework, backend root path, and allowed_source_paths",
|
|
2446
2773
|
);
|
|
2447
2774
|
}
|
|
2775
|
+
if (!env.CLUE_API_KEY) {
|
|
2776
|
+
throw new Error("CLUE_API_KEY is required");
|
|
2777
|
+
}
|
|
2448
2778
|
const previousSnapshot =
|
|
2449
2779
|
providedPreviousSnapshot === undefined
|
|
2450
2780
|
? await fetchLatestSnapshot({ request, env })
|
|
@@ -2452,11 +2782,25 @@ export const runSemanticCi = async ({
|
|
|
2452
2782
|
const previousRoutes = previousRouteMap(previousSnapshot);
|
|
2453
2783
|
const routeNeedsAi = routes.some((route) => {
|
|
2454
2784
|
const previousRoute = previousRoutes.get(route.operation_source_key);
|
|
2455
|
-
return (
|
|
2456
|
-
!previousRoute || previousRoute.route_input_hash !== routeInputHash(route)
|
|
2457
|
-
);
|
|
2785
|
+
return !previousRoute || previousRoute.route_input_hash !== routeInputHash(route);
|
|
2458
2786
|
});
|
|
2459
|
-
|
|
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
|
+
});
|
|
2460
2804
|
const {
|
|
2461
2805
|
plans: routePlans,
|
|
2462
2806
|
routesRequiringGeneration,
|
|
@@ -2466,8 +2810,10 @@ export const runSemanticCi = async ({
|
|
|
2466
2810
|
env,
|
|
2467
2811
|
routes,
|
|
2468
2812
|
previousSnapshot,
|
|
2469
|
-
aiProviderApiKey,
|
|
2813
|
+
aiProviderApiKey: aiProviderConfig?.apiKey ?? null,
|
|
2470
2814
|
hashScope,
|
|
2815
|
+
generationContract,
|
|
2816
|
+
agentSkills,
|
|
2471
2817
|
});
|
|
2472
2818
|
const promptRoutes = routesRequiringGeneration.map((route) =>
|
|
2473
2819
|
buildRouteEvidencePromptEntry({ route, hashScope }),
|
|
@@ -2475,24 +2821,32 @@ export const runSemanticCi = async ({
|
|
|
2475
2821
|
const aiRoutes = await generateAiRoutesPerRoute({
|
|
2476
2822
|
request,
|
|
2477
2823
|
env,
|
|
2478
|
-
apiKey:
|
|
2824
|
+
apiKey: aiProviderConfig?.apiKey ?? null,
|
|
2479
2825
|
routes: promptRoutes,
|
|
2826
|
+
generationContract,
|
|
2827
|
+
agentSkills,
|
|
2480
2828
|
});
|
|
2481
2829
|
const { selectionCandidates, routeBySelectionKey } = buildSelectorCandidates({
|
|
2482
2830
|
routes: routesRequiringGeneration,
|
|
2483
2831
|
aiRoutes,
|
|
2484
2832
|
hashScope,
|
|
2485
2833
|
});
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
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
|
+
}
|
|
2496
2850
|
const snapshot = buildSnapshot({
|
|
2497
2851
|
request,
|
|
2498
2852
|
routes,
|
|
@@ -2501,6 +2855,14 @@ export const runSemanticCi = async ({
|
|
|
2501
2855
|
routePlans,
|
|
2502
2856
|
previousSnapshot,
|
|
2503
2857
|
deletedRouteCount,
|
|
2858
|
+
generationContract,
|
|
2859
|
+
aiRuntime,
|
|
2860
|
+
});
|
|
2861
|
+
assertSemanticSnapshotAudit({
|
|
2862
|
+
routes,
|
|
2863
|
+
snapshot,
|
|
2864
|
+
generationContract,
|
|
2865
|
+
aiRuntime,
|
|
2504
2866
|
});
|
|
2505
2867
|
const upload = await sendSnapshot({ request, env, snapshot });
|
|
2506
2868
|
return {
|