@clue-ai/cli 0.0.15 → 0.0.17
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 +9 -0
- package/bin/clue-cli.mjs +11 -0
- package/package.json +1 -1
- package/src/contracts.mjs +1 -1
- package/src/lifecycle-init.mjs +17 -3
- package/src/path-policy.mjs +8 -1
- package/src/public-schema.cjs +81 -0
- package/src/semantic-ci.mjs +423 -33
- package/src/setup-ai-contract.mjs +91 -0
- package/src/setup-check.mjs +68 -1
- package/src/setup-doctor.mjs +435 -0
- package/src/setup-help.mjs +19 -1
- package/src/setup-prepare.mjs +8 -0
- package/src/setup-tool.mjs +19 -5
package/src/semantic-ci.mjs
CHANGED
|
@@ -80,12 +80,22 @@ const ACTUAL_OUTCOME_ACTIONS = new Set([
|
|
|
80
80
|
]);
|
|
81
81
|
|
|
82
82
|
const snakeSegmentPattern = /^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/;
|
|
83
|
-
const SEMANTIC_SNAPSHOT_SCHEMA_VERSION =
|
|
84
|
-
const SEMANTIC_ANALYZER_VERSION = "fastapi-route-analyzer-
|
|
85
|
-
const SEMANTIC_ROUTE_PROMPT_CONTRACT_VERSION = "route-semantics-
|
|
86
|
-
const SEMANTIC_REUSE_PROMPT_CONTRACT_VERSION = "
|
|
83
|
+
const SEMANTIC_SNAPSHOT_SCHEMA_VERSION = 3;
|
|
84
|
+
const SEMANTIC_ANALYZER_VERSION = "fastapi-route-analyzer-v2";
|
|
85
|
+
const SEMANTIC_ROUTE_PROMPT_CONTRACT_VERSION = "route-semantics-v2";
|
|
86
|
+
const SEMANTIC_REUSE_PROMPT_CONTRACT_VERSION = "purpose-stability-v1";
|
|
87
87
|
const SEMANTIC_SELECTOR_PROMPT_CONTRACT_VERSION = "semantic-selector-v1";
|
|
88
88
|
const SEMANTIC_PRIVACY_SANITIZER_VERSION = "privacy-sanitizer-v1";
|
|
89
|
+
const PURPOSE_STABILITY_CONFIDENCE_THRESHOLD = 0.75;
|
|
90
|
+
|
|
91
|
+
const PURPOSE_CHANGE_STATES = new Set([
|
|
92
|
+
"same_purpose",
|
|
93
|
+
"purpose_added",
|
|
94
|
+
"purpose_changed",
|
|
95
|
+
"purpose_removed",
|
|
96
|
+
"insufficient_evidence",
|
|
97
|
+
"new_route",
|
|
98
|
+
]);
|
|
89
99
|
|
|
90
100
|
const semanticSnapshotHashScope = (request) =>
|
|
91
101
|
[
|
|
@@ -176,6 +186,92 @@ const fallbackSemantics = (route, reason, hashScope) => ({
|
|
|
176
186
|
confidence_reason: reason,
|
|
177
187
|
});
|
|
178
188
|
|
|
189
|
+
const redactAiInputSource = (value) =>
|
|
190
|
+
String(value ?? "")
|
|
191
|
+
.slice(0, 6000)
|
|
192
|
+
.replace(
|
|
193
|
+
/\b[A-Z][A-Z0-9_]*(?:API_KEY|SECRET|TOKEN|PASSWORD|PRIVATE_KEY)\b(?:\s*=\s*["']?[^"'\s,;}]+)?/g,
|
|
194
|
+
"[secret]",
|
|
195
|
+
)
|
|
196
|
+
.replace(/\bBearer\s+(?!\[secret\])[A-Za-z0-9._~+/-]+=*/gi, "Bearer [secret]")
|
|
197
|
+
.replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, "[secret]");
|
|
198
|
+
|
|
199
|
+
const lastCallSegment = (target) =>
|
|
200
|
+
String(target ?? "")
|
|
201
|
+
.split(".")
|
|
202
|
+
.map((part) => part.trim())
|
|
203
|
+
.filter(Boolean)
|
|
204
|
+
.at(-1) ?? "";
|
|
205
|
+
|
|
206
|
+
const callActionHints = (route) =>
|
|
207
|
+
[
|
|
208
|
+
...new Set(
|
|
209
|
+
safeArray(route.ai_context?.code_structure?.call_targets)
|
|
210
|
+
.map(lastCallSegment)
|
|
211
|
+
.map((segment) => normalizeSnakeSegment(segment))
|
|
212
|
+
.filter((segment) => segment && EFFECT_ACTION_TOKENS.has(segment)),
|
|
213
|
+
),
|
|
214
|
+
].slice(0, 12);
|
|
215
|
+
|
|
216
|
+
const buildEvidencePacketSummary = (route) => {
|
|
217
|
+
const callTargets = safeArray(route.ai_context?.code_structure?.call_targets);
|
|
218
|
+
const dependencyHints = safeArray(route.ai_context?.code_structure?.dependency_hints);
|
|
219
|
+
const actionHints = callActionHints(route);
|
|
220
|
+
return {
|
|
221
|
+
contract_summary: `${route.method ?? "UNKNOWN"} ${route.path_template ?? "/"} operation source contract.`,
|
|
222
|
+
behavior_summary:
|
|
223
|
+
callTargets.length > 0
|
|
224
|
+
? `Handler has ${callTargets.length} bounded call target(s) that may describe domain behavior.`
|
|
225
|
+
: "No bounded domain call targets were detected in the route handler.",
|
|
226
|
+
data_effect_hints: actionHints,
|
|
227
|
+
side_effect_hints: callTargets
|
|
228
|
+
.map(lastCallSegment)
|
|
229
|
+
.map((segment) => normalizeSnakeSegment(segment))
|
|
230
|
+
.filter((segment) =>
|
|
231
|
+
["send", "publish", "emit", "schedule", "enqueue", "notify"].includes(
|
|
232
|
+
segment,
|
|
233
|
+
),
|
|
234
|
+
)
|
|
235
|
+
.slice(0, 12),
|
|
236
|
+
validation_hints: safeArray(
|
|
237
|
+
route.ai_context?.code_structure?.parameter_annotations,
|
|
238
|
+
)
|
|
239
|
+
.map((hint) => sanitizeText(String(hint), route))
|
|
240
|
+
.slice(0, 12),
|
|
241
|
+
auth_hints: dependencyHints
|
|
242
|
+
.map((hint) => sanitizeText(String(hint), route))
|
|
243
|
+
.slice(0, 12),
|
|
244
|
+
missing_context:
|
|
245
|
+
callTargets.length === 0
|
|
246
|
+
? ["No local dependency call evidence was detected."]
|
|
247
|
+
: [],
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const buildRouteEvidencePacket = (route, hashScope) => ({
|
|
252
|
+
contract: {
|
|
253
|
+
operation_source_key: route.operation_source_key,
|
|
254
|
+
method: route.method,
|
|
255
|
+
path_template: route.path_template,
|
|
256
|
+
router_prefix: route.ai_context?.router_prefix ?? null,
|
|
257
|
+
include_prefix: route.ai_context?.include_prefix ?? null,
|
|
258
|
+
},
|
|
259
|
+
code_context: {
|
|
260
|
+
handler_name: route.ai_context?.handler_name ?? null,
|
|
261
|
+
handler_source_excerpt: redactAiInputSource(route.ai_context?.source_snippet),
|
|
262
|
+
code_structure: route.ai_context?.code_structure ?? {},
|
|
263
|
+
},
|
|
264
|
+
stored_summary_preview: buildEvidencePacketSummary(route),
|
|
265
|
+
evidence_summaries: buildSourceEvidenceRefs(route, hashScope).map(
|
|
266
|
+
(entry, index) => ({
|
|
267
|
+
evidence_index: index,
|
|
268
|
+
kind: entry.kind,
|
|
269
|
+
summary: entry.summary,
|
|
270
|
+
evidence_strength: entry.evidence_strength,
|
|
271
|
+
}),
|
|
272
|
+
),
|
|
273
|
+
});
|
|
274
|
+
|
|
179
275
|
const buildAiPrompt = ({ routes, generationContract, agentSkills }) =>
|
|
180
276
|
JSON.stringify({
|
|
181
277
|
...semanticAgentPromptEnvelope({
|
|
@@ -187,8 +283,8 @@ const buildAiPrompt = ({ routes, generationContract, agentSkills }) =>
|
|
|
187
283
|
rules: [
|
|
188
284
|
"Do not create operation_source_key values; only copy the provided keys.",
|
|
189
285
|
"Do not create ids, refs, hashes, source fingerprints, or field paths.",
|
|
190
|
-
"
|
|
191
|
-
"Use the provided
|
|
286
|
+
"You may use handler_source_excerpt and bounded dependency evidence to understand route purpose, but never copy raw source code, raw SQL, file paths, function names, class names, prompts, or completions into the output.",
|
|
287
|
+
"Use the provided route_evidence_packet to infer purpose-level business semantics and operation effects.",
|
|
192
288
|
"Return operation_effect_candidates only when source evidence supports a schema-valid target object and effect action.",
|
|
193
289
|
"Keep multiple effects separate when one route produces multiple meaningful effects.",
|
|
194
290
|
"Do not encode actual outcomes such as completed, failed, blocked, validation_error, or abandoned into operation_effect_key or expected_domain_effect.",
|
|
@@ -228,6 +324,7 @@ const buildAiPrompt = ({ routes, generationContract, agentSkills }) =>
|
|
|
228
324
|
method: route.method,
|
|
229
325
|
path_template: route.path_template,
|
|
230
326
|
evidence_summaries: route.evidence_summaries,
|
|
327
|
+
route_evidence_packet: route.route_evidence_packet,
|
|
231
328
|
})),
|
|
232
329
|
});
|
|
233
330
|
|
|
@@ -243,24 +340,30 @@ const buildAiReuseDecisionPrompt = ({
|
|
|
243
340
|
bundle: agentSkills,
|
|
244
341
|
roleId: SEMANTIC_AGENT_ROLE_IDS.reuseJudge,
|
|
245
342
|
}),
|
|
246
|
-
task: "Decide whether previous privacy-safe route semantics still apply after a route source change. Return JSON only.",
|
|
343
|
+
task: "Decide whether previous privacy-safe route semantics still apply at the customer-visible purpose level after a route source change. Return JSON only.",
|
|
247
344
|
generation_contract: generationContract,
|
|
248
345
|
rules: [
|
|
249
346
|
"Only decide for the provided operation_source_key.",
|
|
250
|
-
"
|
|
251
|
-
"
|
|
252
|
-
"
|
|
253
|
-
"
|
|
347
|
+
"First decide purpose_change_state as same_purpose, purpose_added, purpose_changed, purpose_removed, or insufficient_evidence.",
|
|
348
|
+
"Use same_purpose when implementation details or wording changed but the customer-visible purpose and operation effects are unchanged.",
|
|
349
|
+
"Use purpose_added when the previous purpose still applies and the current route adds a new customer-visible purpose or operation effect.",
|
|
350
|
+
"Use purpose_changed when a previous purpose or primary operation effect is replaced with a different business object or effect.",
|
|
351
|
+
"Use purpose_removed when a previous purpose or operation effect is no longer present.",
|
|
352
|
+
"Use insufficient_evidence when evidence is sparse, conflicting, or cannot prove a purpose-level change.",
|
|
353
|
+
"Return legacy decision as reuse_previous for same_purpose, regenerate for purpose_added/purpose_changed/purpose_removed, and needs_review for insufficient_evidence.",
|
|
354
|
+
"Never recommend changing active semantics for wording drift alone.",
|
|
254
355
|
"Do not include raw source code, raw SQL, file paths, function names, class names, prompts, completions, secrets, ids, or hashes except the provided hashes.",
|
|
255
356
|
],
|
|
256
357
|
output_shape: {
|
|
257
358
|
decisions: [
|
|
258
359
|
{
|
|
259
360
|
operation_source_key: "route.POST./reports",
|
|
361
|
+
purpose_change_state: "same_purpose",
|
|
260
362
|
decision: "reuse_previous",
|
|
261
363
|
confidence: 0.82,
|
|
262
364
|
reason:
|
|
263
365
|
"The source changed but the privacy-safe evidence still supports the previous reports.create meaning.",
|
|
366
|
+
missing_context: [],
|
|
264
367
|
},
|
|
265
368
|
],
|
|
266
369
|
},
|
|
@@ -273,6 +376,7 @@ const buildAiReuseDecisionPrompt = ({
|
|
|
273
376
|
previous_semantics: previousRoute.semantics,
|
|
274
377
|
previous_operation_effects: previousRoute.operation_effects ?? [],
|
|
275
378
|
current_evidence_summaries: route.evidence_summaries,
|
|
379
|
+
current_route_evidence_packet: route.route_evidence_packet,
|
|
276
380
|
},
|
|
277
381
|
});
|
|
278
382
|
|
|
@@ -280,8 +384,10 @@ const normalizeReuseDecision = ({ parsed, operationSourceKey }) => {
|
|
|
280
384
|
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.decisions)) {
|
|
281
385
|
return {
|
|
282
386
|
decision: "needs_review",
|
|
387
|
+
purpose_change_state: "insufficient_evidence",
|
|
283
388
|
confidence: 0,
|
|
284
389
|
reason: "AI reuse decision did not return the required decisions array.",
|
|
390
|
+
missing_context: ["AI reuse decision did not return a usable response."],
|
|
285
391
|
};
|
|
286
392
|
}
|
|
287
393
|
const unknownDecision = parsed.decisions.find(
|
|
@@ -295,8 +401,10 @@ const normalizeReuseDecision = ({ parsed, operationSourceKey }) => {
|
|
|
295
401
|
if (unknownDecision) {
|
|
296
402
|
return {
|
|
297
403
|
decision: "needs_review",
|
|
404
|
+
purpose_change_state: "insufficient_evidence",
|
|
298
405
|
confidence: 0,
|
|
299
406
|
reason: "AI reuse decision returned an unexpected route key.",
|
|
407
|
+
missing_context: ["AI reuse decision returned an unexpected route key."],
|
|
300
408
|
};
|
|
301
409
|
}
|
|
302
410
|
const decision = safeArray(parsed?.decisions).find(
|
|
@@ -305,8 +413,10 @@ const normalizeReuseDecision = ({ parsed, operationSourceKey }) => {
|
|
|
305
413
|
if (!decision || typeof decision !== "object") {
|
|
306
414
|
return {
|
|
307
415
|
decision: "needs_review",
|
|
416
|
+
purpose_change_state: "insufficient_evidence",
|
|
308
417
|
confidence: 0,
|
|
309
418
|
reason: "AI reuse decision omitted this changed route.",
|
|
419
|
+
missing_context: ["AI reuse decision omitted this changed route."],
|
|
310
420
|
};
|
|
311
421
|
}
|
|
312
422
|
const normalizedDecision = [
|
|
@@ -316,8 +426,27 @@ const normalizeReuseDecision = ({ parsed, operationSourceKey }) => {
|
|
|
316
426
|
].includes(decision.decision)
|
|
317
427
|
? decision.decision
|
|
318
428
|
: "needs_review";
|
|
429
|
+
const explicitPurposeState = PURPOSE_CHANGE_STATES.has(
|
|
430
|
+
decision.purpose_change_state,
|
|
431
|
+
)
|
|
432
|
+
? decision.purpose_change_state
|
|
433
|
+
: null;
|
|
434
|
+
const purposeChangeState =
|
|
435
|
+
explicitPurposeState ??
|
|
436
|
+
{
|
|
437
|
+
reuse_previous: "same_purpose",
|
|
438
|
+
regenerate: "purpose_changed",
|
|
439
|
+
needs_review: "insufficient_evidence",
|
|
440
|
+
}[normalizedDecision];
|
|
441
|
+
const decisionFromPurpose =
|
|
442
|
+
purposeChangeState === "same_purpose"
|
|
443
|
+
? "reuse_previous"
|
|
444
|
+
: purposeChangeState === "insufficient_evidence"
|
|
445
|
+
? "needs_review"
|
|
446
|
+
: "regenerate";
|
|
319
447
|
return {
|
|
320
|
-
decision:
|
|
448
|
+
decision: decisionFromPurpose,
|
|
449
|
+
purpose_change_state: purposeChangeState,
|
|
321
450
|
confidence: Number.isFinite(Number(decision.confidence))
|
|
322
451
|
? Math.max(0, Math.min(1, Number(decision.confidence)))
|
|
323
452
|
: 0,
|
|
@@ -325,6 +454,9 @@ const normalizeReuseDecision = ({ parsed, operationSourceKey }) => {
|
|
|
325
454
|
typeof decision.reason === "string" && decision.reason.trim()
|
|
326
455
|
? decision.reason.trim()
|
|
327
456
|
: "AI reuse decision did not include a reason.",
|
|
457
|
+
missing_context: stringArray(decision.missing_context, {
|
|
458
|
+
ai_context: {},
|
|
459
|
+
}),
|
|
328
460
|
};
|
|
329
461
|
};
|
|
330
462
|
|
|
@@ -367,10 +499,12 @@ const callAiReuseDecisionProvider = async ({
|
|
|
367
499
|
: null;
|
|
368
500
|
return {
|
|
369
501
|
decision: "needs_review",
|
|
502
|
+
purpose_change_state: "insufficient_evidence",
|
|
370
503
|
confidence: 0,
|
|
371
504
|
reason: providerStatusMatch?.groups?.status
|
|
372
505
|
? `AI reuse decision failed: provider_${providerStatusMatch.groups.status}.`
|
|
373
506
|
: "AI reuse decision returned malformed JSON.",
|
|
507
|
+
missing_context: ["AI reuse decision failed or returned malformed JSON."],
|
|
374
508
|
};
|
|
375
509
|
}
|
|
376
510
|
};
|
|
@@ -1373,6 +1507,7 @@ const buildOperationAssignmentCollections = ({
|
|
|
1373
1507
|
routeEvidenceRefs,
|
|
1374
1508
|
catalogByGroup,
|
|
1375
1509
|
aiSelectionDecisions,
|
|
1510
|
+
existingEffectKeys = [],
|
|
1376
1511
|
}) => {
|
|
1377
1512
|
const operationEffects = [];
|
|
1378
1513
|
const unresolvedOperationEffects = [];
|
|
@@ -1380,7 +1515,8 @@ const buildOperationAssignmentCollections = ({
|
|
|
1380
1515
|
const mappings = [];
|
|
1381
1516
|
const aiInferenceEvidence = [];
|
|
1382
1517
|
const semanticMeaningCandidates = [];
|
|
1383
|
-
const
|
|
1518
|
+
const existingEffectKeySet = new Set(existingEffectKeys);
|
|
1519
|
+
const emittedEffectKeys = new Set(existingEffectKeys);
|
|
1384
1520
|
|
|
1385
1521
|
const candidates = normalizeAiOperationEffectCandidates(aiRoute);
|
|
1386
1522
|
for (
|
|
@@ -1569,6 +1705,9 @@ const buildOperationAssignmentCollections = ({
|
|
|
1569
1705
|
continue;
|
|
1570
1706
|
}
|
|
1571
1707
|
if (emittedEffectKeys.has(operationEffectKey)) {
|
|
1708
|
+
if (existingEffectKeySet.has(operationEffectKey)) {
|
|
1709
|
+
continue;
|
|
1710
|
+
}
|
|
1572
1711
|
unresolvedOperationEffects.push(
|
|
1573
1712
|
unresolvedEffect({
|
|
1574
1713
|
route,
|
|
@@ -1787,7 +1926,9 @@ const buildSnapshot = ({
|
|
|
1787
1926
|
for (const [operationSourceKey, plan] of routePlans.entries()) {
|
|
1788
1927
|
if (
|
|
1789
1928
|
plan.origin === "unchanged_route_reused" ||
|
|
1790
|
-
plan.origin === "changed_route_semantic_reused"
|
|
1929
|
+
plan.origin === "changed_route_semantic_reused" ||
|
|
1930
|
+
plan.active_semantic_source === "previous_kept_pending_review" ||
|
|
1931
|
+
plan.purpose_change_state === "purpose_added"
|
|
1791
1932
|
) {
|
|
1792
1933
|
reusedRouteKeys.push(operationSourceKey);
|
|
1793
1934
|
}
|
|
@@ -1813,6 +1954,10 @@ const buildSnapshot = ({
|
|
|
1813
1954
|
};
|
|
1814
1955
|
const plan = routePlans.get(route.operation_source_key) ?? {
|
|
1815
1956
|
origin: "new_route_ai_generated",
|
|
1957
|
+
purpose_change_state: "new_route",
|
|
1958
|
+
active_semantic_source: "new_confirmed",
|
|
1959
|
+
stability_confidence: 1,
|
|
1960
|
+
stability_missing_context: [],
|
|
1816
1961
|
route_input_hash: routeInputHash(route),
|
|
1817
1962
|
};
|
|
1818
1963
|
const routeEvidenceRefs = buildSourceEvidenceRefs(
|
|
@@ -1821,15 +1966,17 @@ const buildSnapshot = ({
|
|
|
1821
1966
|
);
|
|
1822
1967
|
sourceEvidenceRefs.push(...routeEvidenceRefs);
|
|
1823
1968
|
if (
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1969
|
+
plan.previous_route &&
|
|
1970
|
+
(plan.active_semantic_source === "previous_reused" ||
|
|
1971
|
+
plan.active_semantic_source === "previous_kept_pending_review") &&
|
|
1972
|
+
plan.purpose_change_state !== "purpose_added"
|
|
1827
1973
|
) {
|
|
1828
1974
|
return rebasePreviousRoute({
|
|
1829
1975
|
previousRoute: plan.previous_route,
|
|
1830
1976
|
currentRoute: route,
|
|
1831
1977
|
semanticSnapshotVersion: provisionalSemanticSnapshotVersion,
|
|
1832
1978
|
plan,
|
|
1979
|
+
routeEvidenceRefs,
|
|
1833
1980
|
});
|
|
1834
1981
|
}
|
|
1835
1982
|
const aiRoute = aiRoutes.get(route.operation_source_key);
|
|
@@ -1869,6 +2016,12 @@ const buildSnapshot = ({
|
|
|
1869
2016
|
routeEvidenceRefs,
|
|
1870
2017
|
catalogByGroup,
|
|
1871
2018
|
aiSelectionDecisions,
|
|
2019
|
+
existingEffectKeys:
|
|
2020
|
+
plan.purpose_change_state === "purpose_added"
|
|
2021
|
+
? safeArray(plan.previous_route?.operation_effects).map(
|
|
2022
|
+
(effect) => effect.operation_effect_key,
|
|
2023
|
+
)
|
|
2024
|
+
: [],
|
|
1872
2025
|
});
|
|
1873
2026
|
targetObjectProfiles.push(...assignmentCollections.profiles);
|
|
1874
2027
|
targetObjectMappings.push(...assignmentCollections.mappings);
|
|
@@ -1876,7 +2029,44 @@ const buildSnapshot = ({
|
|
|
1876
2029
|
semanticMeaningCandidates.push(
|
|
1877
2030
|
...assignmentCollections.semanticMeaningCandidates,
|
|
1878
2031
|
);
|
|
1879
|
-
|
|
2032
|
+
if (plan.purpose_change_state === "purpose_added" && plan.previous_route) {
|
|
2033
|
+
const rebasedPrevious = rebasePreviousRoute({
|
|
2034
|
+
previousRoute: plan.previous_route,
|
|
2035
|
+
currentRoute: route,
|
|
2036
|
+
semanticSnapshotVersion: provisionalSemanticSnapshotVersion,
|
|
2037
|
+
plan,
|
|
2038
|
+
routeEvidenceRefs,
|
|
2039
|
+
});
|
|
2040
|
+
const previousEffectKeys = new Set(
|
|
2041
|
+
safeArray(rebasedPrevious.operation_effects).map(
|
|
2042
|
+
(effect) => effect.operation_effect_key,
|
|
2043
|
+
),
|
|
2044
|
+
);
|
|
2045
|
+
const appendedEffects = assignmentCollections.operationEffects.filter(
|
|
2046
|
+
(effect) => !previousEffectKeys.has(effect.operation_effect_key),
|
|
2047
|
+
);
|
|
2048
|
+
const mergedRoute = {
|
|
2049
|
+
...rebasedPrevious,
|
|
2050
|
+
operation_effects: [
|
|
2051
|
+
...safeArray(rebasedPrevious.operation_effects),
|
|
2052
|
+
...appendedEffects,
|
|
2053
|
+
],
|
|
2054
|
+
unresolved_operation_effects: [
|
|
2055
|
+
...safeArray(rebasedPrevious.unresolved_operation_effects),
|
|
2056
|
+
...assignmentCollections.unresolvedOperationEffects,
|
|
2057
|
+
],
|
|
2058
|
+
semantic_change_reason: plan.semantic_change_reason,
|
|
2059
|
+
};
|
|
2060
|
+
return {
|
|
2061
|
+
...mergedRoute,
|
|
2062
|
+
route_semantic_hash: routeSemanticHash(mergedRoute),
|
|
2063
|
+
};
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
return withStabilityMetadata({
|
|
2067
|
+
currentRoute: route,
|
|
2068
|
+
plan,
|
|
2069
|
+
route: {
|
|
1880
2070
|
...baseRoute,
|
|
1881
2071
|
operation_effects: assignmentCollections.operationEffects,
|
|
1882
2072
|
unresolved_operation_effects:
|
|
@@ -1898,7 +2088,8 @@ const buildSnapshot = ({
|
|
|
1898
2088
|
unresolved_operation_effects:
|
|
1899
2089
|
assignmentCollections.unresolvedOperationEffects,
|
|
1900
2090
|
}),
|
|
1901
|
-
|
|
2091
|
+
},
|
|
2092
|
+
});
|
|
1902
2093
|
});
|
|
1903
2094
|
addUniqueBy(
|
|
1904
2095
|
targetObjectProfiles,
|
|
@@ -1942,7 +2133,7 @@ const buildSnapshot = ({
|
|
|
1942
2133
|
idempotency_key: sha256(
|
|
1943
2134
|
`${request.project_key}:${request.repository.repository_id}:${request.repository.merge_commit}:${request.repository.workflow_run_id ?? generatedAt}`,
|
|
1944
2135
|
),
|
|
1945
|
-
schema_version:
|
|
2136
|
+
schema_version: SEMANTIC_SNAPSHOT_SCHEMA_VERSION,
|
|
1946
2137
|
semantic_snapshot_version: semanticSnapshotVersion,
|
|
1947
2138
|
generation_contract: generationContract,
|
|
1948
2139
|
ai_runtime: aiRuntime,
|
|
@@ -1984,6 +2175,24 @@ const buildSnapshot = ({
|
|
|
1984
2175
|
(route) =>
|
|
1985
2176
|
route.semantic_origin === "changed_route_semantic_regenerated",
|
|
1986
2177
|
).length,
|
|
2178
|
+
same_purpose_routes: versionedSnapshotRoutes.filter(
|
|
2179
|
+
(route) => route.purpose_change_state === "same_purpose",
|
|
2180
|
+
).length,
|
|
2181
|
+
purpose_added_routes: versionedSnapshotRoutes.filter(
|
|
2182
|
+
(route) => route.purpose_change_state === "purpose_added",
|
|
2183
|
+
).length,
|
|
2184
|
+
purpose_changed_routes: versionedSnapshotRoutes.filter(
|
|
2185
|
+
(route) => route.purpose_change_state === "purpose_changed",
|
|
2186
|
+
).length,
|
|
2187
|
+
purpose_removed_routes: versionedSnapshotRoutes.filter(
|
|
2188
|
+
(route) => route.purpose_change_state === "purpose_removed",
|
|
2189
|
+
).length,
|
|
2190
|
+
insufficient_evidence_routes: versionedSnapshotRoutes.filter(
|
|
2191
|
+
(route) => route.purpose_change_state === "insufficient_evidence",
|
|
2192
|
+
).length,
|
|
2193
|
+
new_route_purpose_routes: versionedSnapshotRoutes.filter(
|
|
2194
|
+
(route) => route.purpose_change_state === "new_route",
|
|
2195
|
+
).length,
|
|
1987
2196
|
},
|
|
1988
2197
|
routes: versionedSnapshotRoutes,
|
|
1989
2198
|
target_object_profiles: targetObjectProfiles,
|
|
@@ -2228,10 +2437,87 @@ const hasReusablePreviousRouteSemantics = (route) =>
|
|
|
2228
2437
|
Number(route?.semantics?.route_confidence ?? 0) > 0 &&
|
|
2229
2438
|
route?.semantics?.route_summary !== "unknown";
|
|
2230
2439
|
|
|
2440
|
+
const normalizeRoutePathForPolicy = (path) => {
|
|
2441
|
+
const trimmed = String(path ?? "").trim();
|
|
2442
|
+
const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
2443
|
+
return withSlash.replace(/\/{2,}/g, "/").replace(/\/$/g, "") || "/";
|
|
2444
|
+
};
|
|
2445
|
+
|
|
2446
|
+
const isReservedClueBrowserTokenPath = (path) =>
|
|
2447
|
+
/^\/(?:api\/(?:v[0-9]+\/)?)?clue\/browser-tokens?$/i.test(
|
|
2448
|
+
normalizeRoutePathForPolicy(path),
|
|
2449
|
+
);
|
|
2450
|
+
|
|
2451
|
+
const routeSourceForPolicy = (route) =>
|
|
2452
|
+
[
|
|
2453
|
+
route?.ai_context?.route_decorator,
|
|
2454
|
+
route?.ai_context?.source_snippet,
|
|
2455
|
+
safeArray(route?.ai_context?.code_structure?.call_targets).join("\n"),
|
|
2456
|
+
]
|
|
2457
|
+
.filter((entry) => typeof entry === "string")
|
|
2458
|
+
.join("\n");
|
|
2459
|
+
|
|
2460
|
+
const classifyClueInfrastructureRoute = (route) => {
|
|
2461
|
+
const source = routeSourceForPolicy(route);
|
|
2462
|
+
const reservedPath = isReservedClueBrowserTokenPath(route.path_template);
|
|
2463
|
+
const proxiesToClueBrowserTokenApi =
|
|
2464
|
+
/\/(?:api\/v[0-9]+\/)?ingest\/browser-tokens\b/i.test(source);
|
|
2465
|
+
const usesServerSideClueApiKey = /\bCLUE_API_KEY\b|x-clue-api-key/i.test(
|
|
2466
|
+
source,
|
|
2467
|
+
);
|
|
2468
|
+
if (
|
|
2469
|
+
route.method === "POST" &&
|
|
2470
|
+
reservedPath &&
|
|
2471
|
+
proxiesToClueBrowserTokenApi &&
|
|
2472
|
+
usesServerSideClueApiKey
|
|
2473
|
+
) {
|
|
2474
|
+
return {
|
|
2475
|
+
excluded: true,
|
|
2476
|
+
reason:
|
|
2477
|
+
"Clue SDK browser-token proxy route detected by reserved Clue path, Clue browser-token API proxy call, and server-side Clue API key usage.",
|
|
2478
|
+
evidence: {
|
|
2479
|
+
reserved_clue_path: true,
|
|
2480
|
+
proxies_to_clue_browser_token_api: true,
|
|
2481
|
+
uses_server_side_clue_api_key: true,
|
|
2482
|
+
},
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
return {
|
|
2486
|
+
excluded: false,
|
|
2487
|
+
reason: null,
|
|
2488
|
+
evidence: {
|
|
2489
|
+
reserved_clue_path: reservedPath,
|
|
2490
|
+
proxies_to_clue_browser_token_api: proxiesToClueBrowserTokenApi,
|
|
2491
|
+
uses_server_side_clue_api_key: usesServerSideClueApiKey,
|
|
2492
|
+
},
|
|
2493
|
+
};
|
|
2494
|
+
};
|
|
2495
|
+
|
|
2496
|
+
const splitSemanticProductRoutes = (routes) => {
|
|
2497
|
+
const productRoutes = [];
|
|
2498
|
+
const excludedInfrastructureRoutes = [];
|
|
2499
|
+
for (const route of routes) {
|
|
2500
|
+
const classification = classifyClueInfrastructureRoute(route);
|
|
2501
|
+
if (classification.excluded) {
|
|
2502
|
+
excludedInfrastructureRoutes.push({
|
|
2503
|
+
operation_source_key: route.operation_source_key,
|
|
2504
|
+
method: route.method,
|
|
2505
|
+
path_template: route.path_template,
|
|
2506
|
+
reason: classification.reason,
|
|
2507
|
+
evidence: classification.evidence,
|
|
2508
|
+
});
|
|
2509
|
+
continue;
|
|
2510
|
+
}
|
|
2511
|
+
productRoutes.push(route);
|
|
2512
|
+
}
|
|
2513
|
+
return { productRoutes, excludedInfrastructureRoutes };
|
|
2514
|
+
};
|
|
2515
|
+
|
|
2231
2516
|
const buildRouteEvidencePromptEntry = ({ route, hashScope }) => ({
|
|
2232
2517
|
operation_source_key: route.operation_source_key,
|
|
2233
2518
|
method: route.method,
|
|
2234
2519
|
path_template: route.path_template,
|
|
2520
|
+
route_evidence_packet: buildRouteEvidencePacket(route, hashScope),
|
|
2235
2521
|
evidence_summaries: buildSourceEvidenceRefs(route, hashScope).map(
|
|
2236
2522
|
(entry, index) => ({
|
|
2237
2523
|
evidence_index: index,
|
|
@@ -2262,6 +2548,10 @@ const classifyRoutesForSnapshot = async ({
|
|
|
2262
2548
|
if (!previousRoute) {
|
|
2263
2549
|
plans.set(route.operation_source_key, {
|
|
2264
2550
|
origin: "new_route_ai_generated",
|
|
2551
|
+
purpose_change_state: "new_route",
|
|
2552
|
+
active_semantic_source: "new_confirmed",
|
|
2553
|
+
stability_confidence: 1,
|
|
2554
|
+
stability_missing_context: [],
|
|
2265
2555
|
route_input_hash: currentHash,
|
|
2266
2556
|
});
|
|
2267
2557
|
routesRequiringGeneration.push(route);
|
|
@@ -2274,6 +2564,10 @@ const classifyRoutesForSnapshot = async ({
|
|
|
2274
2564
|
) {
|
|
2275
2565
|
plans.set(route.operation_source_key, {
|
|
2276
2566
|
origin: "unchanged_route_reused",
|
|
2567
|
+
purpose_change_state: "same_purpose",
|
|
2568
|
+
active_semantic_source: "previous_reused",
|
|
2569
|
+
stability_confidence: 1,
|
|
2570
|
+
stability_missing_context: [],
|
|
2277
2571
|
route_input_hash: currentHash,
|
|
2278
2572
|
previous_route: previousRoute,
|
|
2279
2573
|
previous_route_input_hash: previousRoute.route_input_hash,
|
|
@@ -2288,6 +2582,12 @@ const classifyRoutesForSnapshot = async ({
|
|
|
2288
2582
|
if (previousRoute.route_input_hash === currentHash) {
|
|
2289
2583
|
plans.set(route.operation_source_key, {
|
|
2290
2584
|
origin: "changed_route_semantic_regenerated",
|
|
2585
|
+
purpose_change_state: "new_route",
|
|
2586
|
+
active_semantic_source: "new_confirmed",
|
|
2587
|
+
stability_confidence: 0,
|
|
2588
|
+
stability_missing_context: [
|
|
2589
|
+
"Previous route semantic confidence was too low for stable reuse.",
|
|
2590
|
+
],
|
|
2291
2591
|
route_input_hash: currentHash,
|
|
2292
2592
|
previous_route: previousRoute,
|
|
2293
2593
|
previous_route_input_hash: previousRoute.route_input_hash,
|
|
@@ -2310,9 +2610,16 @@ const classifyRoutesForSnapshot = async ({
|
|
|
2310
2610
|
generationContract,
|
|
2311
2611
|
agentSkills,
|
|
2312
2612
|
});
|
|
2313
|
-
if (
|
|
2613
|
+
if (
|
|
2614
|
+
decision.purpose_change_state === "same_purpose" &&
|
|
2615
|
+
decision.confidence >= PURPOSE_STABILITY_CONFIDENCE_THRESHOLD
|
|
2616
|
+
) {
|
|
2314
2617
|
plans.set(route.operation_source_key, {
|
|
2315
2618
|
origin: "changed_route_semantic_reused",
|
|
2619
|
+
purpose_change_state: "same_purpose",
|
|
2620
|
+
active_semantic_source: "previous_reused",
|
|
2621
|
+
stability_confidence: decision.confidence,
|
|
2622
|
+
stability_missing_context: decision.missing_context,
|
|
2316
2623
|
route_input_hash: currentHash,
|
|
2317
2624
|
previous_route: previousRoute,
|
|
2318
2625
|
previous_route_input_hash: previousRoute.route_input_hash,
|
|
@@ -2322,9 +2629,16 @@ const classifyRoutesForSnapshot = async ({
|
|
|
2322
2629
|
});
|
|
2323
2630
|
continue;
|
|
2324
2631
|
}
|
|
2325
|
-
if (
|
|
2632
|
+
if (
|
|
2633
|
+
decision.purpose_change_state === "same_purpose" ||
|
|
2634
|
+
decision.purpose_change_state === "insufficient_evidence"
|
|
2635
|
+
) {
|
|
2326
2636
|
plans.set(route.operation_source_key, {
|
|
2327
2637
|
origin: "changed_route_needs_review",
|
|
2638
|
+
purpose_change_state: "insufficient_evidence",
|
|
2639
|
+
active_semantic_source: "previous_kept_pending_review",
|
|
2640
|
+
stability_confidence: decision.confidence,
|
|
2641
|
+
stability_missing_context: decision.missing_context,
|
|
2328
2642
|
route_input_hash: currentHash,
|
|
2329
2643
|
previous_route: previousRoute,
|
|
2330
2644
|
previous_route_input_hash: previousRoute.route_input_hash,
|
|
@@ -2337,9 +2651,16 @@ const classifyRoutesForSnapshot = async ({
|
|
|
2337
2651
|
});
|
|
2338
2652
|
continue;
|
|
2339
2653
|
}
|
|
2340
|
-
if (
|
|
2654
|
+
if (
|
|
2655
|
+
decision.decision === "regenerate" &&
|
|
2656
|
+
decision.confidence >= PURPOSE_STABILITY_CONFIDENCE_THRESHOLD
|
|
2657
|
+
) {
|
|
2341
2658
|
plans.set(route.operation_source_key, {
|
|
2342
2659
|
origin: "changed_route_semantic_regenerated",
|
|
2660
|
+
purpose_change_state: decision.purpose_change_state,
|
|
2661
|
+
active_semantic_source: "new_confirmed",
|
|
2662
|
+
stability_confidence: decision.confidence,
|
|
2663
|
+
stability_missing_context: decision.missing_context,
|
|
2343
2664
|
route_input_hash: currentHash,
|
|
2344
2665
|
previous_route: previousRoute,
|
|
2345
2666
|
previous_route_input_hash: previousRoute.route_input_hash,
|
|
@@ -2352,6 +2673,10 @@ const classifyRoutesForSnapshot = async ({
|
|
|
2352
2673
|
}
|
|
2353
2674
|
plans.set(route.operation_source_key, {
|
|
2354
2675
|
origin: "changed_route_needs_review",
|
|
2676
|
+
purpose_change_state: "insufficient_evidence",
|
|
2677
|
+
active_semantic_source: "previous_kept_pending_review",
|
|
2678
|
+
stability_confidence: decision.confidence,
|
|
2679
|
+
stability_missing_context: decision.missing_context,
|
|
2355
2680
|
route_input_hash: currentHash,
|
|
2356
2681
|
previous_route: previousRoute,
|
|
2357
2682
|
previous_route_input_hash: previousRoute.route_input_hash,
|
|
@@ -2380,7 +2705,14 @@ const rebasePreviousRoute = ({
|
|
|
2380
2705
|
currentRoute,
|
|
2381
2706
|
semanticSnapshotVersion,
|
|
2382
2707
|
plan,
|
|
2708
|
+
routeEvidenceRefs = [],
|
|
2383
2709
|
}) => {
|
|
2710
|
+
const sourceEvidenceRefs = [
|
|
2711
|
+
...new Set([
|
|
2712
|
+
...safeArray(previousRoute.source_evidence_refs),
|
|
2713
|
+
...routeEvidenceRefs.map((entry) => entry.id),
|
|
2714
|
+
]),
|
|
2715
|
+
];
|
|
2384
2716
|
const route = {
|
|
2385
2717
|
...previousRoute,
|
|
2386
2718
|
operation_source_key: currentRoute.operation_source_key,
|
|
@@ -2395,6 +2727,11 @@ const rebasePreviousRoute = ({
|
|
|
2395
2727
|
previous_route_semantic_hash: plan.previous_route_semantic_hash,
|
|
2396
2728
|
semantic_origin: plan.origin,
|
|
2397
2729
|
semantic_change_reason: plan.semantic_change_reason,
|
|
2730
|
+
purpose_change_state: plan.purpose_change_state,
|
|
2731
|
+
active_semantic_source: plan.active_semantic_source,
|
|
2732
|
+
semantic_stability: buildSemanticStability(plan),
|
|
2733
|
+
evidence_packet_summary: buildEvidencePacketSummary(currentRoute),
|
|
2734
|
+
source_evidence_refs: sourceEvidenceRefs,
|
|
2398
2735
|
};
|
|
2399
2736
|
return {
|
|
2400
2737
|
...route,
|
|
@@ -2402,6 +2739,42 @@ const rebasePreviousRoute = ({
|
|
|
2402
2739
|
};
|
|
2403
2740
|
};
|
|
2404
2741
|
|
|
2742
|
+
const buildSemanticStability = (plan) => ({
|
|
2743
|
+
purpose_change_state: plan.purpose_change_state ?? "new_route",
|
|
2744
|
+
confidence:
|
|
2745
|
+
typeof plan.stability_confidence === "number"
|
|
2746
|
+
? Math.max(0, Math.min(1, plan.stability_confidence))
|
|
2747
|
+
: plan.purpose_change_state === "new_route"
|
|
2748
|
+
? 1
|
|
2749
|
+
: 0,
|
|
2750
|
+
reason:
|
|
2751
|
+
plan.semantic_change_reason ??
|
|
2752
|
+
(plan.purpose_change_state === "new_route"
|
|
2753
|
+
? "No previous route semantic existed, so a new active semantic was generated."
|
|
2754
|
+
: "Semantic purpose stability was evaluated for this route."),
|
|
2755
|
+
...(plan.previous_route_input_hash
|
|
2756
|
+
? { previous_route_input_hash: plan.previous_route_input_hash }
|
|
2757
|
+
: {}),
|
|
2758
|
+
...(plan.previous_route_semantic_hash
|
|
2759
|
+
? { previous_route_semantic_hash: plan.previous_route_semantic_hash }
|
|
2760
|
+
: {}),
|
|
2761
|
+
...(plan.previous_route?.semantic_snapshot_version
|
|
2762
|
+
? {
|
|
2763
|
+
previous_semantic_snapshot_version:
|
|
2764
|
+
plan.previous_route.semantic_snapshot_version,
|
|
2765
|
+
}
|
|
2766
|
+
: {}),
|
|
2767
|
+
missing_context: safeArray(plan.stability_missing_context),
|
|
2768
|
+
});
|
|
2769
|
+
|
|
2770
|
+
const withStabilityMetadata = ({ route, currentRoute, plan }) => ({
|
|
2771
|
+
...route,
|
|
2772
|
+
purpose_change_state: plan.purpose_change_state,
|
|
2773
|
+
active_semantic_source: plan.active_semantic_source,
|
|
2774
|
+
semantic_stability: buildSemanticStability(plan),
|
|
2775
|
+
evidence_packet_summary: buildEvidencePacketSummary(currentRoute),
|
|
2776
|
+
});
|
|
2777
|
+
|
|
2405
2778
|
const fieldPathMatchesKeys = ({
|
|
2406
2779
|
fieldPath,
|
|
2407
2780
|
routeKeys,
|
|
@@ -2452,7 +2825,7 @@ const reusablePreviousContext = ({ previousSnapshot, reusedRouteKeys }) => {
|
|
|
2452
2825
|
),
|
|
2453
2826
|
),
|
|
2454
2827
|
]);
|
|
2455
|
-
const
|
|
2828
|
+
const catalogCandidates = safeArray(previousSnapshot?.target_object_catalog).filter(
|
|
2456
2829
|
(entry) => catalogKeys.has(entry.target_object_key),
|
|
2457
2830
|
);
|
|
2458
2831
|
const matches = (fieldPath) =>
|
|
@@ -2508,11 +2881,6 @@ const reusablePreviousContext = ({ previousSnapshot, reusedRouteKeys }) => {
|
|
|
2508
2881
|
for (const mapping of mappings) {
|
|
2509
2882
|
sourceEvidenceIds.add(mapping.grouping_evidence_ref);
|
|
2510
2883
|
}
|
|
2511
|
-
for (const catalog of catalogs) {
|
|
2512
|
-
for (const ref of safeArray(catalog.grouping_evidence_refs)) {
|
|
2513
|
-
sourceEvidenceIds.add(ref);
|
|
2514
|
-
}
|
|
2515
|
-
}
|
|
2516
2884
|
for (const evidence of aiInferenceEvidence) {
|
|
2517
2885
|
for (const ref of safeArray(evidence.input_source_evidence_refs)) {
|
|
2518
2886
|
sourceEvidenceIds.add(ref);
|
|
@@ -2526,6 +2894,21 @@ const reusablePreviousContext = ({ previousSnapshot, reusedRouteKeys }) => {
|
|
|
2526
2894
|
sourceEvidenceIds.add(ref);
|
|
2527
2895
|
}
|
|
2528
2896
|
}
|
|
2897
|
+
const catalogs = catalogCandidates
|
|
2898
|
+
.map((entry) => ({
|
|
2899
|
+
...entry,
|
|
2900
|
+
raw_target_object_profile_refs: safeArray(
|
|
2901
|
+
entry.raw_target_object_profile_refs,
|
|
2902
|
+
).filter((ref) => profileIds.has(ref)),
|
|
2903
|
+
grouping_evidence_refs: safeArray(entry.grouping_evidence_refs).filter(
|
|
2904
|
+
(ref) => sourceEvidenceIds.has(ref),
|
|
2905
|
+
),
|
|
2906
|
+
}))
|
|
2907
|
+
.filter(
|
|
2908
|
+
(entry) =>
|
|
2909
|
+
entry.raw_target_object_profile_refs.length > 0 &&
|
|
2910
|
+
entry.grouping_evidence_refs.length > 0,
|
|
2911
|
+
);
|
|
2529
2912
|
const sourceEvidenceRefs = safeArray(
|
|
2530
2913
|
previousSnapshot?.source_evidence_refs,
|
|
2531
2914
|
).filter((entry) => sourceEvidenceIds.has(entry.id));
|
|
@@ -2873,10 +3256,12 @@ export const runSemanticCi = async ({
|
|
|
2873
3256
|
allowedSourcePaths: request.allowed_source_paths,
|
|
2874
3257
|
excludedSourcePaths: request.excluded_source_paths,
|
|
2875
3258
|
});
|
|
2876
|
-
const
|
|
3259
|
+
const discoveredRoutes = await analyzeFastApiRoutes({ repoRoot, files });
|
|
3260
|
+
const { productRoutes: routes, excludedInfrastructureRoutes } =
|
|
3261
|
+
splitSemanticProductRoutes(discoveredRoutes);
|
|
2877
3262
|
if (routes.length === 0) {
|
|
2878
3263
|
throw new Error(
|
|
2879
|
-
"semantic CI discovered zero routes; check framework, backend root path, and
|
|
3264
|
+
"semantic CI discovered zero routes after infrastructure exclusions; check framework, backend root path, allowed_source_paths, and infrastructure route exclusions",
|
|
2880
3265
|
);
|
|
2881
3266
|
}
|
|
2882
3267
|
if (!env.CLUE_API_KEY) {
|
|
@@ -2979,6 +3364,7 @@ export const runSemanticCi = async ({
|
|
|
2979
3364
|
return {
|
|
2980
3365
|
accepted: upload.accepted === true,
|
|
2981
3366
|
route_count: snapshot.routes.length,
|
|
3367
|
+
excluded_infrastructure_routes: excludedInfrastructureRoutes,
|
|
2982
3368
|
upload,
|
|
2983
3369
|
};
|
|
2984
3370
|
};
|
|
@@ -2989,10 +3375,12 @@ export const runSemanticInventory = async ({ repoRoot, request }) => {
|
|
|
2989
3375
|
allowedSourcePaths: request.allowed_source_paths,
|
|
2990
3376
|
excludedSourcePaths: request.excluded_source_paths,
|
|
2991
3377
|
});
|
|
2992
|
-
const
|
|
3378
|
+
const discoveredRoutes = await analyzeFastApiRoutes({ repoRoot, files });
|
|
3379
|
+
const { productRoutes: routes, excludedInfrastructureRoutes } =
|
|
3380
|
+
splitSemanticProductRoutes(discoveredRoutes);
|
|
2993
3381
|
if (routes.length === 0) {
|
|
2994
3382
|
throw new Error(
|
|
2995
|
-
"semantic inventory discovered zero routes; check framework, backend root path, and
|
|
3383
|
+
"semantic inventory discovered zero routes after infrastructure exclusions; check framework, backend root path, allowed_source_paths, and infrastructure route exclusions",
|
|
2996
3384
|
);
|
|
2997
3385
|
}
|
|
2998
3386
|
return {
|
|
@@ -3000,7 +3388,9 @@ export const runSemanticInventory = async ({ repoRoot, request }) => {
|
|
|
3000
3388
|
service_key: request.service_key,
|
|
3001
3389
|
allowed_source_paths: request.allowed_source_paths,
|
|
3002
3390
|
excluded_source_paths: request.excluded_source_paths,
|
|
3391
|
+
discovered_route_count: discoveredRoutes.length,
|
|
3003
3392
|
route_count: routes.length,
|
|
3393
|
+
excluded_infrastructure_routes: excludedInfrastructureRoutes,
|
|
3004
3394
|
operation_source_keys: routes.map((route) => route.operation_source_key),
|
|
3005
3395
|
routes: routes.map((route) => ({
|
|
3006
3396
|
operation_source_key: route.operation_source_key,
|