@clue-ai/cli 0.0.4 → 0.0.5
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 +43 -0
- package/bin/clue-cli.mjs +844 -68
- package/package.json +1 -1
- package/src/contracts.mjs +5 -0
- package/src/init-tool.mjs +154 -55
- package/src/lifecycle-guard.mjs +141 -0
- package/src/lifecycle-init.mjs +210 -167
- package/src/path-policy.mjs +2 -0
- package/src/public-schema.cjs +26 -0
- package/src/semantic-ci.mjs +722 -32
- package/src/setup-check.mjs +435 -0
- package/src/setup-detect.mjs +198 -0
- package/src/setup-prepare.mjs +170 -0
- package/src/setup-tool.mjs +231 -166
package/src/semantic-ci.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash, createHmac } from "node:crypto";
|
|
|
2
2
|
import {
|
|
3
3
|
validateSemanticCiRequest,
|
|
4
4
|
validateSemanticSnapshotRequest,
|
|
5
|
+
validateSemanticSnapshotResponse,
|
|
5
6
|
} from "./contracts.mjs";
|
|
6
7
|
import { analyzeFastApiRoutes } from "./fastapi-analyzer.mjs";
|
|
7
8
|
import { listAllowedPythonFiles } from "./path-policy.mjs";
|
|
@@ -73,10 +74,43 @@ const semanticSnapshotHashScope = (request) =>
|
|
|
73
74
|
"clue_tools_semantic_snapshot",
|
|
74
75
|
request.project_key,
|
|
75
76
|
request.repository.repository_id,
|
|
76
|
-
request.
|
|
77
|
-
request.repository.workflow_run_id ?? "no_workflow_run",
|
|
77
|
+
request.service.service_key,
|
|
78
78
|
].join(":");
|
|
79
79
|
|
|
80
|
+
const canonicalJson = (value) => {
|
|
81
|
+
if (Array.isArray(value)) {
|
|
82
|
+
return `[${value.map((entry) => canonicalJson(entry)).join(",")}]`;
|
|
83
|
+
}
|
|
84
|
+
if (value && typeof value === "object") {
|
|
85
|
+
return `{${Object.keys(value)
|
|
86
|
+
.sort()
|
|
87
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
|
|
88
|
+
.join(",")}}`;
|
|
89
|
+
}
|
|
90
|
+
return JSON.stringify(value);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const routeInputHash = (route) =>
|
|
94
|
+
sha256(
|
|
95
|
+
canonicalJson({
|
|
96
|
+
operation_source_key: route.operation_source_key,
|
|
97
|
+
method: route.method,
|
|
98
|
+
path_template: route.path_template,
|
|
99
|
+
source: route.source,
|
|
100
|
+
code_structure: route.ai_context?.code_structure ?? null,
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const routeSemanticHash = (route) =>
|
|
105
|
+
sha256(
|
|
106
|
+
canonicalJson({
|
|
107
|
+
semantics: route.semantics,
|
|
108
|
+
layer_evidence: route.layer_evidence,
|
|
109
|
+
operation_effects: route.operation_effects ?? [],
|
|
110
|
+
unresolved_operation_effects: route.unresolved_operation_effects ?? [],
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
|
|
80
114
|
const opaqueRouteHash = (route, purpose, hashScope) =>
|
|
81
115
|
hmacSha256(
|
|
82
116
|
hashScope,
|
|
@@ -165,6 +199,134 @@ const buildAiPrompt = (routes) =>
|
|
|
165
199
|
})),
|
|
166
200
|
});
|
|
167
201
|
|
|
202
|
+
const buildAiReuseDecisionPrompt = ({ route, previousRoute, currentHash }) =>
|
|
203
|
+
JSON.stringify({
|
|
204
|
+
task: "Decide whether previous privacy-safe route semantics still apply after a route source change. Return JSON only.",
|
|
205
|
+
rules: [
|
|
206
|
+
"Only decide for the provided operation_source_key.",
|
|
207
|
+
"Return decision as reuse_previous, regenerate, or needs_review.",
|
|
208
|
+
"Choose reuse_previous only when the customer-visible route meaning, operation effects, and value interpretation remain the same.",
|
|
209
|
+
"Choose regenerate when the route appears to create, update, delete, read, send, schedule, call, or validate a different business object or effect.",
|
|
210
|
+
"Choose needs_review when evidence is insufficient or conflicting.",
|
|
211
|
+
"Do not include raw source code, raw SQL, file paths, function names, class names, prompts, completions, secrets, ids, or hashes except the provided hashes.",
|
|
212
|
+
],
|
|
213
|
+
output_shape: {
|
|
214
|
+
decisions: [
|
|
215
|
+
{
|
|
216
|
+
operation_source_key: "route.POST./reports",
|
|
217
|
+
decision: "reuse_previous",
|
|
218
|
+
confidence: 0.82,
|
|
219
|
+
reason:
|
|
220
|
+
"The source changed but the privacy-safe evidence still supports the previous reports.create meaning.",
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
route: {
|
|
225
|
+
operation_source_key: route.operation_source_key,
|
|
226
|
+
method: route.method,
|
|
227
|
+
path_template: route.path_template,
|
|
228
|
+
current_route_input_hash: currentHash,
|
|
229
|
+
previous_route_input_hash: previousRoute.route_input_hash ?? null,
|
|
230
|
+
previous_semantics: previousRoute.semantics,
|
|
231
|
+
previous_operation_effects: previousRoute.operation_effects ?? [],
|
|
232
|
+
current_evidence_summaries: route.evidence_summaries,
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const normalizeReuseDecision = ({ parsed, operationSourceKey }) => {
|
|
237
|
+
const decision = safeArray(parsed?.decisions).find(
|
|
238
|
+
(entry) => entry?.operation_source_key === operationSourceKey,
|
|
239
|
+
);
|
|
240
|
+
if (!decision || typeof decision !== "object") {
|
|
241
|
+
return {
|
|
242
|
+
decision: "needs_review",
|
|
243
|
+
confidence: 0,
|
|
244
|
+
reason: "AI reuse decision omitted this changed route.",
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
const normalizedDecision = ["reuse_previous", "regenerate", "needs_review"].includes(
|
|
248
|
+
decision.decision,
|
|
249
|
+
)
|
|
250
|
+
? decision.decision
|
|
251
|
+
: "needs_review";
|
|
252
|
+
return {
|
|
253
|
+
decision: normalizedDecision,
|
|
254
|
+
confidence: Number.isFinite(Number(decision.confidence))
|
|
255
|
+
? Math.max(0, Math.min(1, Number(decision.confidence)))
|
|
256
|
+
: 0,
|
|
257
|
+
reason:
|
|
258
|
+
typeof decision.reason === "string" && decision.reason.trim()
|
|
259
|
+
? decision.reason.trim()
|
|
260
|
+
: "AI reuse decision did not include a reason.",
|
|
261
|
+
};
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const callAiReuseDecisionProvider = async ({
|
|
265
|
+
request,
|
|
266
|
+
apiKey,
|
|
267
|
+
route,
|
|
268
|
+
previousRoute,
|
|
269
|
+
currentHash,
|
|
270
|
+
}) => {
|
|
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
|
+
try {
|
|
317
|
+
return normalizeReuseDecision({
|
|
318
|
+
parsed: JSON.parse(content),
|
|
319
|
+
operationSourceKey: route.operation_source_key,
|
|
320
|
+
});
|
|
321
|
+
} catch {
|
|
322
|
+
return {
|
|
323
|
+
decision: "needs_review",
|
|
324
|
+
confidence: 0,
|
|
325
|
+
reason: "AI reuse decision returned malformed JSON.",
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
|
|
168
330
|
const requireAiProviderApiKey = (env) => {
|
|
169
331
|
const apiKey = env.AI_PROVIDER_API_KEY;
|
|
170
332
|
if (!apiKey) {
|
|
@@ -233,6 +395,24 @@ const unavailableAiRoute = (operationSourceKey, confidenceReason) => ({
|
|
|
233
395
|
confidence_reason: confidenceReason,
|
|
234
396
|
});
|
|
235
397
|
|
|
398
|
+
const assertSnapshotRouteCoverage = ({ routes, snapshotRoutes }) => {
|
|
399
|
+
const expectedKeys = routes.map((route) => route.operation_source_key);
|
|
400
|
+
const actualKeys = snapshotRoutes.map((route) => route.operation_source_key);
|
|
401
|
+
const expected = new Set(expectedKeys);
|
|
402
|
+
const actual = new Set(actualKeys);
|
|
403
|
+
const missing = expectedKeys.filter((key) => !actual.has(key));
|
|
404
|
+
const extra = actualKeys.filter((key) => !expected.has(key));
|
|
405
|
+
if (
|
|
406
|
+
expectedKeys.length !== actualKeys.length ||
|
|
407
|
+
missing.length > 0 ||
|
|
408
|
+
extra.length > 0
|
|
409
|
+
) {
|
|
410
|
+
throw new Error(
|
|
411
|
+
`semantic snapshot route coverage mismatch: missing=${missing.join(",") || "none"} extra=${extra.join(",") || "none"}`,
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
|
|
236
416
|
const generateAiRoutesPerRoute = async ({ request, apiKey, routes }) => {
|
|
237
417
|
const aiRoutes = new Map();
|
|
238
418
|
for (const route of routes) {
|
|
@@ -1450,7 +1630,15 @@ const buildOperationAssignmentCollections = ({
|
|
|
1450
1630
|
};
|
|
1451
1631
|
};
|
|
1452
1632
|
|
|
1453
|
-
const buildSnapshot = ({
|
|
1633
|
+
const buildSnapshot = ({
|
|
1634
|
+
request,
|
|
1635
|
+
routes,
|
|
1636
|
+
aiRoutes,
|
|
1637
|
+
aiSelectionDecisions,
|
|
1638
|
+
routePlans = new Map(),
|
|
1639
|
+
previousSnapshot,
|
|
1640
|
+
deletedRouteCount = 0,
|
|
1641
|
+
}) => {
|
|
1454
1642
|
const generatedAt = new Date().toISOString();
|
|
1455
1643
|
const hashScope = semanticSnapshotHashScope(request);
|
|
1456
1644
|
const semanticSnapshotVersion = buildSemanticSnapshotVersion({
|
|
@@ -1463,23 +1651,69 @@ const buildSnapshot = ({ request, routes, aiRoutes, aiSelectionDecisions }) => {
|
|
|
1463
1651
|
const aiInferenceEvidence = [];
|
|
1464
1652
|
const semanticMeaningCandidates = [];
|
|
1465
1653
|
const catalogByGroup = new Map();
|
|
1654
|
+
let failedRouteCount = 0;
|
|
1655
|
+
const reusedRouteKeys = [];
|
|
1656
|
+
for (const [operationSourceKey, plan] of routePlans.entries()) {
|
|
1657
|
+
if (
|
|
1658
|
+
plan.origin === "unchanged_route_reused" ||
|
|
1659
|
+
plan.origin === "changed_route_semantic_reused"
|
|
1660
|
+
) {
|
|
1661
|
+
reusedRouteKeys.push(operationSourceKey);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
const previousContext = reusablePreviousContext({
|
|
1665
|
+
previousSnapshot,
|
|
1666
|
+
reusedRouteKeys,
|
|
1667
|
+
});
|
|
1668
|
+
sourceEvidenceRefs.push(...previousContext.sourceEvidenceRefs);
|
|
1669
|
+
targetObjectProfiles.push(...previousContext.profiles);
|
|
1670
|
+
targetObjectMappings.push(...previousContext.mappings);
|
|
1671
|
+
aiInferenceEvidence.push(...previousContext.aiInferenceEvidence);
|
|
1672
|
+
semanticMeaningCandidates.push(...previousContext.semanticMeaningCandidates);
|
|
1673
|
+
for (const entry of previousContext.catalogs) {
|
|
1674
|
+
const groupKey = `${entry.concept.toLowerCase()}::${entry.business_role.toLowerCase()}`;
|
|
1675
|
+
catalogByGroup.set(groupKey, { ...entry });
|
|
1676
|
+
}
|
|
1466
1677
|
|
|
1467
1678
|
const snapshotRoutes = routes.map((route) => {
|
|
1468
1679
|
const routeWithVersion = {
|
|
1469
1680
|
...route,
|
|
1470
1681
|
semantic_snapshot_version: semanticSnapshotVersion,
|
|
1471
1682
|
};
|
|
1683
|
+
const plan = routePlans.get(route.operation_source_key) ?? {
|
|
1684
|
+
origin: "new_route_ai_generated",
|
|
1685
|
+
route_input_hash: routeInputHash(route),
|
|
1686
|
+
};
|
|
1472
1687
|
const routeEvidenceRefs = buildSourceEvidenceRefs(
|
|
1473
1688
|
routeWithVersion,
|
|
1474
1689
|
hashScope,
|
|
1475
1690
|
);
|
|
1476
1691
|
sourceEvidenceRefs.push(...routeEvidenceRefs);
|
|
1692
|
+
if (
|
|
1693
|
+
(plan.origin === "unchanged_route_reused" ||
|
|
1694
|
+
plan.origin === "changed_route_semantic_reused") &&
|
|
1695
|
+
plan.previous_route
|
|
1696
|
+
) {
|
|
1697
|
+
return rebasePreviousRoute({
|
|
1698
|
+
previousRoute: plan.previous_route,
|
|
1699
|
+
currentRoute: route,
|
|
1700
|
+
semanticSnapshotVersion,
|
|
1701
|
+
plan,
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1477
1704
|
const aiRoute = aiRoutes.get(route.operation_source_key);
|
|
1478
|
-
|
|
1705
|
+
if (!aiRoute?.semantics || plan.origin === "changed_route_needs_review") {
|
|
1706
|
+
failedRouteCount += 1;
|
|
1707
|
+
}
|
|
1708
|
+
const unavailableReason =
|
|
1709
|
+
plan.origin === "changed_route_needs_review"
|
|
1710
|
+
? `changed_route_needs_review: ${plan.semantic_change_reason}`
|
|
1711
|
+
: (aiRoute?.confidence_reason ??
|
|
1712
|
+
"AI semantics were unavailable for this route.");
|
|
1713
|
+
const baseRoute = !aiRoute?.semantics || plan.origin === "changed_route_needs_review"
|
|
1479
1714
|
? fallbackSemantics(
|
|
1480
1715
|
routeWithVersion,
|
|
1481
|
-
|
|
1482
|
-
"AI semantics were unavailable for this route.",
|
|
1716
|
+
unavailableReason,
|
|
1483
1717
|
hashScope,
|
|
1484
1718
|
)
|
|
1485
1719
|
: {
|
|
@@ -1520,10 +1754,49 @@ const buildSnapshot = ({ request, routes, aiRoutes, aiSelectionDecisions }) => {
|
|
|
1520
1754
|
unresolved_operation_effects:
|
|
1521
1755
|
assignmentCollections.unresolvedOperationEffects,
|
|
1522
1756
|
source_evidence_refs: routeEvidenceRefs.map((entry) => entry.id),
|
|
1757
|
+
route_input_hash: plan.route_input_hash,
|
|
1758
|
+
previous_route_input_hash: plan.previous_route_input_hash,
|
|
1759
|
+
previous_route_semantic_hash: plan.previous_route_semantic_hash,
|
|
1760
|
+
semantic_origin:
|
|
1761
|
+
plan.origin === "changed_route_needs_review" && aiRoute?.semantics
|
|
1762
|
+
? "changed_route_needs_review"
|
|
1763
|
+
: plan.origin,
|
|
1764
|
+
semantic_change_reason: plan.semantic_change_reason,
|
|
1765
|
+
previous_semantic_snapshot_version:
|
|
1766
|
+
plan.previous_route?.semantic_snapshot_version,
|
|
1767
|
+
route_semantic_hash: routeSemanticHash({
|
|
1768
|
+
...baseRoute,
|
|
1769
|
+
operation_effects: assignmentCollections.operationEffects,
|
|
1770
|
+
unresolved_operation_effects:
|
|
1771
|
+
assignmentCollections.unresolvedOperationEffects,
|
|
1772
|
+
}),
|
|
1523
1773
|
};
|
|
1524
1774
|
});
|
|
1775
|
+
addUniqueBy(
|
|
1776
|
+
targetObjectProfiles,
|
|
1777
|
+
previousContext.profiles,
|
|
1778
|
+
(entry) => entry.id,
|
|
1779
|
+
);
|
|
1780
|
+
addUniqueBy(
|
|
1781
|
+
targetObjectMappings,
|
|
1782
|
+
previousContext.mappings,
|
|
1783
|
+
(entry) => entry.id,
|
|
1784
|
+
);
|
|
1785
|
+
addUniqueBy(
|
|
1786
|
+
aiInferenceEvidence,
|
|
1787
|
+
previousContext.aiInferenceEvidence,
|
|
1788
|
+
(entry) => entry.id,
|
|
1789
|
+
);
|
|
1790
|
+
addUniqueBy(
|
|
1791
|
+
semanticMeaningCandidates,
|
|
1792
|
+
previousContext.semanticMeaningCandidates,
|
|
1793
|
+
(entry) => entry.id,
|
|
1794
|
+
);
|
|
1525
1795
|
|
|
1526
|
-
|
|
1796
|
+
const uniqueSourceEvidenceRefs = [];
|
|
1797
|
+
addUniqueBy(uniqueSourceEvidenceRefs, sourceEvidenceRefs, (entry) => entry.id);
|
|
1798
|
+
|
|
1799
|
+
const snapshot = validateSemanticSnapshotRequest({
|
|
1527
1800
|
project_key: request.project_key,
|
|
1528
1801
|
idempotency_key: sha256(
|
|
1529
1802
|
`${request.project_key}:${request.repository.repository_id}:${request.repository.merge_commit}:${request.repository.workflow_run_id ?? generatedAt}`,
|
|
@@ -1541,16 +1814,41 @@ const buildSnapshot = ({ request, routes, aiRoutes, aiSelectionDecisions }) => {
|
|
|
1541
1814
|
uncertain_routes: snapshotRoutes.filter(
|
|
1542
1815
|
(route) => route.semantics.route_confidence < 0.5,
|
|
1543
1816
|
).length,
|
|
1544
|
-
failed_routes:
|
|
1817
|
+
failed_routes: failedRouteCount,
|
|
1818
|
+
routes_reused: snapshotRoutes.filter((route) =>
|
|
1819
|
+
[
|
|
1820
|
+
"unchanged_route_reused",
|
|
1821
|
+
"changed_route_semantic_reused",
|
|
1822
|
+
].includes(route.semantic_origin),
|
|
1823
|
+
).length,
|
|
1824
|
+
routes_ai_generated: snapshotRoutes.filter((route) =>
|
|
1825
|
+
[
|
|
1826
|
+
"new_route_ai_generated",
|
|
1827
|
+
"changed_route_semantic_regenerated",
|
|
1828
|
+
].includes(route.semantic_origin),
|
|
1829
|
+
).length,
|
|
1830
|
+
routes_deleted: deletedRouteCount,
|
|
1831
|
+
routes_needs_review: snapshotRoutes.filter(
|
|
1832
|
+
(route) => route.semantic_origin === "changed_route_needs_review",
|
|
1833
|
+
).length,
|
|
1834
|
+
changed_routes_semantic_reused: snapshotRoutes.filter(
|
|
1835
|
+
(route) => route.semantic_origin === "changed_route_semantic_reused",
|
|
1836
|
+
).length,
|
|
1837
|
+
changed_routes_semantic_regenerated: snapshotRoutes.filter(
|
|
1838
|
+
(route) =>
|
|
1839
|
+
route.semantic_origin === "changed_route_semantic_regenerated",
|
|
1840
|
+
).length,
|
|
1545
1841
|
},
|
|
1546
1842
|
routes: snapshotRoutes,
|
|
1547
1843
|
target_object_profiles: targetObjectProfiles,
|
|
1548
1844
|
target_object_catalog: [...catalogByGroup.values()],
|
|
1549
1845
|
target_object_mappings: targetObjectMappings,
|
|
1550
|
-
source_evidence_refs:
|
|
1846
|
+
source_evidence_refs: uniqueSourceEvidenceRefs,
|
|
1551
1847
|
ai_inference_evidence: aiInferenceEvidence,
|
|
1552
1848
|
semantic_meaning_candidates: semanticMeaningCandidates,
|
|
1553
1849
|
});
|
|
1850
|
+
assertSnapshotRouteCoverage({ routes, snapshotRoutes: snapshot.routes });
|
|
1851
|
+
return snapshot;
|
|
1554
1852
|
};
|
|
1555
1853
|
|
|
1556
1854
|
const sanitizeText = (value, route) => {
|
|
@@ -1717,6 +2015,289 @@ const sanitizeLayerEvidence = (value, route, hashScope) => {
|
|
|
1717
2015
|
};
|
|
1718
2016
|
};
|
|
1719
2017
|
|
|
2018
|
+
const previousRouteMap = (previousSnapshot) =>
|
|
2019
|
+
new Map(
|
|
2020
|
+
safeArray(previousSnapshot?.routes).map((route) => [
|
|
2021
|
+
route.operation_source_key,
|
|
2022
|
+
route,
|
|
2023
|
+
]),
|
|
2024
|
+
);
|
|
2025
|
+
|
|
2026
|
+
const buildRouteEvidencePromptEntry = ({ route, hashScope }) => ({
|
|
2027
|
+
operation_source_key: route.operation_source_key,
|
|
2028
|
+
method: route.method,
|
|
2029
|
+
path_template: route.path_template,
|
|
2030
|
+
evidence_summaries: buildSourceEvidenceRefs(route, hashScope).map(
|
|
2031
|
+
(entry, index) => ({
|
|
2032
|
+
evidence_index: index,
|
|
2033
|
+
kind: entry.kind,
|
|
2034
|
+
summary: entry.summary,
|
|
2035
|
+
evidence_strength: entry.evidence_strength,
|
|
2036
|
+
}),
|
|
2037
|
+
),
|
|
2038
|
+
});
|
|
2039
|
+
|
|
2040
|
+
const classifyRoutesForSnapshot = async ({
|
|
2041
|
+
request,
|
|
2042
|
+
routes,
|
|
2043
|
+
previousSnapshot,
|
|
2044
|
+
aiProviderApiKey,
|
|
2045
|
+
hashScope,
|
|
2046
|
+
}) => {
|
|
2047
|
+
const previousRoutes = previousRouteMap(previousSnapshot);
|
|
2048
|
+
const plans = new Map();
|
|
2049
|
+
const routesRequiringGeneration = [];
|
|
2050
|
+
|
|
2051
|
+
for (const route of routes) {
|
|
2052
|
+
const currentHash = routeInputHash(route);
|
|
2053
|
+
const previousRoute = previousRoutes.get(route.operation_source_key);
|
|
2054
|
+
if (!previousRoute) {
|
|
2055
|
+
plans.set(route.operation_source_key, {
|
|
2056
|
+
origin: "new_route_ai_generated",
|
|
2057
|
+
route_input_hash: currentHash,
|
|
2058
|
+
});
|
|
2059
|
+
routesRequiringGeneration.push(route);
|
|
2060
|
+
continue;
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
if (previousRoute.route_input_hash === currentHash) {
|
|
2064
|
+
plans.set(route.operation_source_key, {
|
|
2065
|
+
origin: "unchanged_route_reused",
|
|
2066
|
+
route_input_hash: currentHash,
|
|
2067
|
+
previous_route: previousRoute,
|
|
2068
|
+
previous_route_input_hash: previousRoute.route_input_hash,
|
|
2069
|
+
previous_route_semantic_hash:
|
|
2070
|
+
previousRoute.route_semantic_hash ?? routeSemanticHash(previousRoute),
|
|
2071
|
+
semantic_change_reason:
|
|
2072
|
+
"Route input hash matched the previous snapshot, so previous semantics were reused exactly.",
|
|
2073
|
+
});
|
|
2074
|
+
continue;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
const decision = await callAiReuseDecisionProvider({
|
|
2078
|
+
request,
|
|
2079
|
+
apiKey: aiProviderApiKey,
|
|
2080
|
+
route: buildRouteEvidencePromptEntry({ route, hashScope }),
|
|
2081
|
+
previousRoute,
|
|
2082
|
+
currentHash,
|
|
2083
|
+
});
|
|
2084
|
+
if (decision.decision === "reuse_previous") {
|
|
2085
|
+
plans.set(route.operation_source_key, {
|
|
2086
|
+
origin: "changed_route_semantic_reused",
|
|
2087
|
+
route_input_hash: currentHash,
|
|
2088
|
+
previous_route: previousRoute,
|
|
2089
|
+
previous_route_input_hash: previousRoute.route_input_hash,
|
|
2090
|
+
previous_route_semantic_hash:
|
|
2091
|
+
previousRoute.route_semantic_hash ?? routeSemanticHash(previousRoute),
|
|
2092
|
+
semantic_change_reason: sanitizeText(decision.reason, route),
|
|
2093
|
+
});
|
|
2094
|
+
continue;
|
|
2095
|
+
}
|
|
2096
|
+
if (decision.decision === "regenerate") {
|
|
2097
|
+
plans.set(route.operation_source_key, {
|
|
2098
|
+
origin: "changed_route_semantic_regenerated",
|
|
2099
|
+
route_input_hash: currentHash,
|
|
2100
|
+
previous_route: previousRoute,
|
|
2101
|
+
previous_route_input_hash: previousRoute.route_input_hash,
|
|
2102
|
+
previous_route_semantic_hash:
|
|
2103
|
+
previousRoute.route_semantic_hash ?? routeSemanticHash(previousRoute),
|
|
2104
|
+
semantic_change_reason: sanitizeText(decision.reason, route),
|
|
2105
|
+
});
|
|
2106
|
+
routesRequiringGeneration.push(route);
|
|
2107
|
+
continue;
|
|
2108
|
+
}
|
|
2109
|
+
plans.set(route.operation_source_key, {
|
|
2110
|
+
origin: "changed_route_needs_review",
|
|
2111
|
+
route_input_hash: currentHash,
|
|
2112
|
+
previous_route: previousRoute,
|
|
2113
|
+
previous_route_input_hash: previousRoute.route_input_hash,
|
|
2114
|
+
previous_route_semantic_hash:
|
|
2115
|
+
previousRoute.route_semantic_hash ?? routeSemanticHash(previousRoute),
|
|
2116
|
+
semantic_change_reason: sanitizeText(decision.reason, route),
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
const currentRouteKeys = new Set(
|
|
2121
|
+
routes.map((route) => route.operation_source_key),
|
|
2122
|
+
);
|
|
2123
|
+
const deletedRouteCount = safeArray(previousSnapshot?.routes).filter(
|
|
2124
|
+
(route) => !currentRouteKeys.has(route.operation_source_key),
|
|
2125
|
+
).length;
|
|
2126
|
+
|
|
2127
|
+
return {
|
|
2128
|
+
plans,
|
|
2129
|
+
routesRequiringGeneration,
|
|
2130
|
+
deletedRouteCount,
|
|
2131
|
+
};
|
|
2132
|
+
};
|
|
2133
|
+
|
|
2134
|
+
const rebasePreviousRoute = ({
|
|
2135
|
+
previousRoute,
|
|
2136
|
+
currentRoute,
|
|
2137
|
+
semanticSnapshotVersion,
|
|
2138
|
+
plan,
|
|
2139
|
+
}) => {
|
|
2140
|
+
const route = {
|
|
2141
|
+
...previousRoute,
|
|
2142
|
+
operation_source_key: currentRoute.operation_source_key,
|
|
2143
|
+
semantic_snapshot_version: semanticSnapshotVersion,
|
|
2144
|
+
previous_semantic_snapshot_version:
|
|
2145
|
+
previousRoute.semantic_snapshot_version ??
|
|
2146
|
+
plan.previous_semantic_snapshot_version,
|
|
2147
|
+
method: currentRoute.method,
|
|
2148
|
+
path_template: currentRoute.path_template,
|
|
2149
|
+
route_input_hash: plan.route_input_hash,
|
|
2150
|
+
previous_route_input_hash: plan.previous_route_input_hash,
|
|
2151
|
+
previous_route_semantic_hash: plan.previous_route_semantic_hash,
|
|
2152
|
+
semantic_origin: plan.origin,
|
|
2153
|
+
semantic_change_reason: plan.semantic_change_reason,
|
|
2154
|
+
};
|
|
2155
|
+
return {
|
|
2156
|
+
...route,
|
|
2157
|
+
route_semantic_hash: routeSemanticHash(route),
|
|
2158
|
+
};
|
|
2159
|
+
};
|
|
2160
|
+
|
|
2161
|
+
const fieldPathMatchesKeys = ({ fieldPath, routeKeys, profileIds, mappingIds, catalogKeys }) => {
|
|
2162
|
+
for (const routeKey of routeKeys) {
|
|
2163
|
+
if (fieldPath.startsWith(`routes[${routeKey}]`)) {
|
|
2164
|
+
return true;
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
for (const profileId of profileIds) {
|
|
2168
|
+
if (fieldPath.startsWith(`target_object_profiles[${profileId}]`)) {
|
|
2169
|
+
return true;
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
for (const mappingId of mappingIds) {
|
|
2173
|
+
if (fieldPath.startsWith(`target_object_mappings[${mappingId}]`)) {
|
|
2174
|
+
return true;
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
for (const catalogKey of catalogKeys) {
|
|
2178
|
+
if (fieldPath.startsWith(`target_object_catalog[${catalogKey}]`)) {
|
|
2179
|
+
return true;
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
return false;
|
|
2183
|
+
};
|
|
2184
|
+
|
|
2185
|
+
const reusablePreviousContext = ({ previousSnapshot, reusedRouteKeys }) => {
|
|
2186
|
+
const routeKeys = new Set(reusedRouteKeys);
|
|
2187
|
+
const profiles = safeArray(previousSnapshot?.target_object_profiles).filter(
|
|
2188
|
+
(profile) => routeKeys.has(profile.operation_source_key),
|
|
2189
|
+
);
|
|
2190
|
+
const mappings = safeArray(previousSnapshot?.target_object_mappings).filter(
|
|
2191
|
+
(mapping) => routeKeys.has(mapping.operation_source_key),
|
|
2192
|
+
);
|
|
2193
|
+
const profileIds = new Set(profiles.map((profile) => profile.id));
|
|
2194
|
+
const mappingIds = new Set(mappings.map((mapping) => mapping.id));
|
|
2195
|
+
const catalogKeys = new Set([
|
|
2196
|
+
...mappings.map((mapping) => mapping.target_object_key),
|
|
2197
|
+
...safeArray(previousSnapshot?.routes)
|
|
2198
|
+
.filter((route) => routeKeys.has(route.operation_source_key))
|
|
2199
|
+
.flatMap((route) =>
|
|
2200
|
+
safeArray(route.operation_effects).map((effect) => effect.target_object_key),
|
|
2201
|
+
),
|
|
2202
|
+
]);
|
|
2203
|
+
const catalogs = safeArray(previousSnapshot?.target_object_catalog).filter(
|
|
2204
|
+
(entry) => catalogKeys.has(entry.target_object_key),
|
|
2205
|
+
);
|
|
2206
|
+
const matches = (fieldPath) =>
|
|
2207
|
+
fieldPathMatchesKeys({
|
|
2208
|
+
fieldPath,
|
|
2209
|
+
routeKeys,
|
|
2210
|
+
profileIds,
|
|
2211
|
+
mappingIds,
|
|
2212
|
+
catalogKeys,
|
|
2213
|
+
});
|
|
2214
|
+
const aiInferenceEvidence = safeArray(
|
|
2215
|
+
previousSnapshot?.ai_inference_evidence,
|
|
2216
|
+
).filter(
|
|
2217
|
+
(entry) =>
|
|
2218
|
+
safeArray(entry.output_field_paths).length > 0 &&
|
|
2219
|
+
safeArray(entry.output_field_paths).every(matches),
|
|
2220
|
+
);
|
|
2221
|
+
const aiInferenceIds = new Set(aiInferenceEvidence.map((entry) => entry.id));
|
|
2222
|
+
const semanticMeaningCandidates = safeArray(
|
|
2223
|
+
previousSnapshot?.semantic_meaning_candidates,
|
|
2224
|
+
).filter(
|
|
2225
|
+
(entry) =>
|
|
2226
|
+
matches(entry.field_path) &&
|
|
2227
|
+
aiInferenceIds.has(entry.ai_candidate.ai_inference_evidence_ref) &&
|
|
2228
|
+
aiInferenceIds.has(entry.selection_ai_inference_evidence_ref),
|
|
2229
|
+
);
|
|
2230
|
+
const sourceEvidenceIds = new Set();
|
|
2231
|
+
for (const route of safeArray(previousSnapshot?.routes).filter((entry) =>
|
|
2232
|
+
routeKeys.has(entry.operation_source_key),
|
|
2233
|
+
)) {
|
|
2234
|
+
for (const ref of safeArray(route.source_evidence_refs)) {
|
|
2235
|
+
sourceEvidenceIds.add(ref);
|
|
2236
|
+
}
|
|
2237
|
+
for (const effect of safeArray(route.operation_effects)) {
|
|
2238
|
+
for (const ref of safeArray(effect.source_evidence_refs)) {
|
|
2239
|
+
sourceEvidenceIds.add(ref);
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
for (const effect of safeArray(route.unresolved_operation_effects)) {
|
|
2243
|
+
for (const ref of safeArray(effect.source_evidence_refs)) {
|
|
2244
|
+
sourceEvidenceIds.add(ref);
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
for (const profile of profiles) {
|
|
2249
|
+
for (const ref of [
|
|
2250
|
+
...safeArray(profile.evidence_refs),
|
|
2251
|
+
...safeArray(profile.source_evidence_refs),
|
|
2252
|
+
]) {
|
|
2253
|
+
sourceEvidenceIds.add(ref);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
for (const mapping of mappings) {
|
|
2257
|
+
sourceEvidenceIds.add(mapping.grouping_evidence_ref);
|
|
2258
|
+
}
|
|
2259
|
+
for (const catalog of catalogs) {
|
|
2260
|
+
for (const ref of safeArray(catalog.grouping_evidence_refs)) {
|
|
2261
|
+
sourceEvidenceIds.add(ref);
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
for (const evidence of aiInferenceEvidence) {
|
|
2265
|
+
for (const ref of safeArray(evidence.input_source_evidence_refs)) {
|
|
2266
|
+
sourceEvidenceIds.add(ref);
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
for (const candidate of semanticMeaningCandidates) {
|
|
2270
|
+
for (const ref of [
|
|
2271
|
+
...safeArray(candidate.deterministic_candidate?.source_evidence_refs),
|
|
2272
|
+
...safeArray(candidate.ai_candidate?.source_evidence_refs),
|
|
2273
|
+
]) {
|
|
2274
|
+
sourceEvidenceIds.add(ref);
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
const sourceEvidenceRefs = safeArray(previousSnapshot?.source_evidence_refs).filter(
|
|
2278
|
+
(entry) => sourceEvidenceIds.has(entry.id),
|
|
2279
|
+
);
|
|
2280
|
+
return {
|
|
2281
|
+
profiles,
|
|
2282
|
+
mappings,
|
|
2283
|
+
catalogs,
|
|
2284
|
+
sourceEvidenceRefs,
|
|
2285
|
+
aiInferenceEvidence,
|
|
2286
|
+
semanticMeaningCandidates,
|
|
2287
|
+
};
|
|
2288
|
+
};
|
|
2289
|
+
|
|
2290
|
+
const addUniqueBy = (target, entries, keyFn) => {
|
|
2291
|
+
const seen = new Set(target.map(keyFn));
|
|
2292
|
+
for (const entry of entries) {
|
|
2293
|
+
const key = keyFn(entry);
|
|
2294
|
+
if (!seen.has(key)) {
|
|
2295
|
+
target.push(entry);
|
|
2296
|
+
seen.add(key);
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
};
|
|
2300
|
+
|
|
1720
2301
|
const FORBIDDEN_CODE_STRUCTURE_PATTERNS = [
|
|
1721
2302
|
{ label: "file path", pattern: /\b[A-Za-z0-9_./-]+\.py\b/i },
|
|
1722
2303
|
{
|
|
@@ -1814,6 +2395,56 @@ const semanticSnapshotUrl = (baseUrl) => {
|
|
|
1814
2395
|
return `${normalized}/api/v1/semantic-snapshots`;
|
|
1815
2396
|
};
|
|
1816
2397
|
|
|
2398
|
+
const semanticSnapshotLatestUrl = (request) => {
|
|
2399
|
+
const url = new URL(`${semanticSnapshotUrl(request.clue_api_base_url)}/latest`);
|
|
2400
|
+
url.searchParams.set("project_key", request.project_key);
|
|
2401
|
+
url.searchParams.set("environment", request.environment);
|
|
2402
|
+
url.searchParams.set("repository_id", request.repository.repository_id);
|
|
2403
|
+
url.searchParams.set("service_key", request.service.service_key);
|
|
2404
|
+
return url.toString();
|
|
2405
|
+
};
|
|
2406
|
+
|
|
2407
|
+
const fetchLatestSnapshot = async ({ request, env }) => {
|
|
2408
|
+
const apiKey = env.CLUE_API_KEY;
|
|
2409
|
+
if (!apiKey) {
|
|
2410
|
+
return null;
|
|
2411
|
+
}
|
|
2412
|
+
let response;
|
|
2413
|
+
try {
|
|
2414
|
+
response = await fetch(semanticSnapshotLatestUrl(request), {
|
|
2415
|
+
method: "GET",
|
|
2416
|
+
headers: {
|
|
2417
|
+
authorization: `Bearer ${apiKey}`,
|
|
2418
|
+
},
|
|
2419
|
+
});
|
|
2420
|
+
} catch {
|
|
2421
|
+
return null;
|
|
2422
|
+
}
|
|
2423
|
+
if (response.status === 404 || response.status === 204) {
|
|
2424
|
+
return null;
|
|
2425
|
+
}
|
|
2426
|
+
if (!response.ok) {
|
|
2427
|
+
return null;
|
|
2428
|
+
}
|
|
2429
|
+
const body = await response.json();
|
|
2430
|
+
const candidate = body?.snapshot ?? body;
|
|
2431
|
+
if (!candidate || !Array.isArray(candidate.routes)) {
|
|
2432
|
+
return null;
|
|
2433
|
+
}
|
|
2434
|
+
const parsed = validateSemanticSnapshotRequest(candidate);
|
|
2435
|
+
assertNoRawCodeStructure(parsed);
|
|
2436
|
+
return parsed;
|
|
2437
|
+
};
|
|
2438
|
+
|
|
2439
|
+
const validatePreviousSnapshotForReuse = (snapshot) => {
|
|
2440
|
+
if (!snapshot) {
|
|
2441
|
+
return null;
|
|
2442
|
+
}
|
|
2443
|
+
const parsed = validateSemanticSnapshotRequest(snapshot);
|
|
2444
|
+
assertNoRawCodeStructure(parsed);
|
|
2445
|
+
return parsed;
|
|
2446
|
+
};
|
|
2447
|
+
|
|
1817
2448
|
const sendSnapshot = async ({ request, env, snapshot }) => {
|
|
1818
2449
|
const apiKey = env.CLUE_API_KEY;
|
|
1819
2450
|
if (!apiKey) {
|
|
@@ -1832,10 +2463,24 @@ const sendSnapshot = async ({ request, env, snapshot }) => {
|
|
|
1832
2463
|
if (!response.ok) {
|
|
1833
2464
|
throw new Error(`Clue semantic snapshot upload failed: ${response.status}`);
|
|
1834
2465
|
}
|
|
1835
|
-
|
|
2466
|
+
const body = await response.json();
|
|
2467
|
+
try {
|
|
2468
|
+
return validateSemanticSnapshotResponse(body);
|
|
2469
|
+
} catch (error) {
|
|
2470
|
+
throw new Error(
|
|
2471
|
+
`Clue semantic snapshot upload response is invalid: ${
|
|
2472
|
+
error instanceof Error ? error.message : String(error)
|
|
2473
|
+
}`,
|
|
2474
|
+
);
|
|
2475
|
+
}
|
|
1836
2476
|
};
|
|
1837
2477
|
|
|
1838
|
-
export const runSemanticCi = async ({
|
|
2478
|
+
export const runSemanticCi = async ({
|
|
2479
|
+
repoRoot,
|
|
2480
|
+
request: rawRequest,
|
|
2481
|
+
env,
|
|
2482
|
+
previousSnapshot: providedPreviousSnapshot,
|
|
2483
|
+
}) => {
|
|
1839
2484
|
const request = validateSemanticCiRequest(rawRequest);
|
|
1840
2485
|
const hashScope = semanticSnapshotHashScope(request);
|
|
1841
2486
|
const files = await listAllowedPythonFiles({
|
|
@@ -1844,41 +2489,59 @@ export const runSemanticCi = async ({ repoRoot, request: rawRequest, env }) => {
|
|
|
1844
2489
|
excludedSourcePaths: request.excluded_source_paths,
|
|
1845
2490
|
});
|
|
1846
2491
|
const routes = await analyzeFastApiRoutes({ repoRoot, files });
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
)
|
|
1859
|
-
|
|
1860
|
-
|
|
2492
|
+
if (routes.length === 0) {
|
|
2493
|
+
throw new Error(
|
|
2494
|
+
"semantic CI discovered zero routes; check framework, backend root path, and allowed_source_paths",
|
|
2495
|
+
);
|
|
2496
|
+
}
|
|
2497
|
+
const previousSnapshot =
|
|
2498
|
+
providedPreviousSnapshot === undefined
|
|
2499
|
+
? await fetchLatestSnapshot({ request, env })
|
|
2500
|
+
: validatePreviousSnapshotForReuse(providedPreviousSnapshot);
|
|
2501
|
+
const previousRoutes = previousRouteMap(previousSnapshot);
|
|
2502
|
+
const routeNeedsAi = routes.some((route) => {
|
|
2503
|
+
const previousRoute = previousRoutes.get(route.operation_source_key);
|
|
2504
|
+
return !previousRoute || previousRoute.route_input_hash !== routeInputHash(route);
|
|
2505
|
+
});
|
|
2506
|
+
const aiProviderApiKey = routeNeedsAi ? requireAiProviderApiKey(env) : null;
|
|
2507
|
+
const { plans: routePlans, routesRequiringGeneration, deletedRouteCount } =
|
|
2508
|
+
await classifyRoutesForSnapshot({
|
|
2509
|
+
request,
|
|
2510
|
+
routes,
|
|
2511
|
+
previousSnapshot,
|
|
2512
|
+
aiProviderApiKey,
|
|
2513
|
+
hashScope,
|
|
2514
|
+
});
|
|
2515
|
+
const promptRoutes = routesRequiringGeneration.map((route) =>
|
|
2516
|
+
buildRouteEvidencePromptEntry({ route, hashScope }),
|
|
2517
|
+
);
|
|
1861
2518
|
const aiRoutes = await generateAiRoutesPerRoute({
|
|
1862
2519
|
request,
|
|
1863
2520
|
apiKey: aiProviderApiKey,
|
|
1864
2521
|
routes: promptRoutes,
|
|
1865
2522
|
});
|
|
1866
2523
|
const { selectionCandidates, routeBySelectionKey } = buildSelectorCandidates({
|
|
1867
|
-
routes,
|
|
2524
|
+
routes: routesRequiringGeneration,
|
|
1868
2525
|
aiRoutes,
|
|
1869
2526
|
hashScope,
|
|
1870
2527
|
});
|
|
1871
|
-
const aiSelectionDecisions =
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
2528
|
+
const aiSelectionDecisions =
|
|
2529
|
+
selectionCandidates.length === 0
|
|
2530
|
+
? new Map()
|
|
2531
|
+
: await callAiSelectionProvider({
|
|
2532
|
+
request,
|
|
2533
|
+
apiKey: aiProviderApiKey,
|
|
2534
|
+
selectionCandidates,
|
|
2535
|
+
routeBySelectionKey,
|
|
2536
|
+
});
|
|
1877
2537
|
const snapshot = buildSnapshot({
|
|
1878
2538
|
request,
|
|
1879
2539
|
routes,
|
|
1880
2540
|
aiRoutes,
|
|
1881
2541
|
aiSelectionDecisions,
|
|
2542
|
+
routePlans,
|
|
2543
|
+
previousSnapshot,
|
|
2544
|
+
deletedRouteCount,
|
|
1882
2545
|
});
|
|
1883
2546
|
const upload = await sendSnapshot({ request, env, snapshot });
|
|
1884
2547
|
return {
|
|
@@ -1887,3 +2550,30 @@ export const runSemanticCi = async ({ repoRoot, request: rawRequest, env }) => {
|
|
|
1887
2550
|
upload,
|
|
1888
2551
|
};
|
|
1889
2552
|
};
|
|
2553
|
+
|
|
2554
|
+
export const runSemanticInventory = async ({ repoRoot, request }) => {
|
|
2555
|
+
const files = await listAllowedPythonFiles({
|
|
2556
|
+
repoRoot,
|
|
2557
|
+
allowedSourcePaths: request.allowed_source_paths,
|
|
2558
|
+
excludedSourcePaths: request.excluded_source_paths,
|
|
2559
|
+
});
|
|
2560
|
+
const routes = await analyzeFastApiRoutes({ repoRoot, files });
|
|
2561
|
+
if (routes.length === 0) {
|
|
2562
|
+
throw new Error(
|
|
2563
|
+
"semantic inventory discovered zero routes; check framework, backend root path, and allowed_source_paths",
|
|
2564
|
+
);
|
|
2565
|
+
}
|
|
2566
|
+
return {
|
|
2567
|
+
framework: request.framework,
|
|
2568
|
+
service_key: request.service_key,
|
|
2569
|
+
allowed_source_paths: request.allowed_source_paths,
|
|
2570
|
+
excluded_source_paths: request.excluded_source_paths,
|
|
2571
|
+
route_count: routes.length,
|
|
2572
|
+
operation_source_keys: routes.map((route) => route.operation_source_key),
|
|
2573
|
+
routes: routes.map((route) => ({
|
|
2574
|
+
operation_source_key: route.operation_source_key,
|
|
2575
|
+
method: route.method,
|
|
2576
|
+
path_template: route.path_template,
|
|
2577
|
+
})),
|
|
2578
|
+
};
|
|
2579
|
+
};
|