@clue-ai/cli 0.0.16 → 0.0.18
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 +26 -5
- package/src/public-schema.cjs +81 -0
- package/src/semantic-ci.mjs +560 -45
- package/src/setup-ai-contract.mjs +114 -0
- package/src/setup-check.mjs +174 -2
- package/src/setup-doctor.mjs +442 -0
- package/src/setup-help.mjs +23 -2
- package/src/setup-prepare.mjs +27 -0
- package/src/setup-tool.mjs +31 -7
package/src/semantic-ci.mjs
CHANGED
|
@@ -80,12 +80,25 @@ 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
|
+
]);
|
|
99
|
+
|
|
100
|
+
const shouldPurposeRecheckAllRoutes = (env) =>
|
|
101
|
+
env.CLUE_SEMANTIC_PURPOSE_RECHECK_ALL === "1";
|
|
89
102
|
|
|
90
103
|
const semanticSnapshotHashScope = (request) =>
|
|
91
104
|
[
|
|
@@ -176,6 +189,92 @@ const fallbackSemantics = (route, reason, hashScope) => ({
|
|
|
176
189
|
confidence_reason: reason,
|
|
177
190
|
});
|
|
178
191
|
|
|
192
|
+
const redactAiInputSource = (value) =>
|
|
193
|
+
String(value ?? "")
|
|
194
|
+
.slice(0, 6000)
|
|
195
|
+
.replace(
|
|
196
|
+
/\b[A-Z][A-Z0-9_]*(?:API_KEY|SECRET|TOKEN|PASSWORD|PRIVATE_KEY)\b(?:\s*=\s*["']?[^"'\s,;}]+)?/g,
|
|
197
|
+
"[secret]",
|
|
198
|
+
)
|
|
199
|
+
.replace(/\bBearer\s+(?!\[secret\])[A-Za-z0-9._~+/-]+=*/gi, "Bearer [secret]")
|
|
200
|
+
.replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, "[secret]");
|
|
201
|
+
|
|
202
|
+
const lastCallSegment = (target) =>
|
|
203
|
+
String(target ?? "")
|
|
204
|
+
.split(".")
|
|
205
|
+
.map((part) => part.trim())
|
|
206
|
+
.filter(Boolean)
|
|
207
|
+
.at(-1) ?? "";
|
|
208
|
+
|
|
209
|
+
const callActionHints = (route) =>
|
|
210
|
+
[
|
|
211
|
+
...new Set(
|
|
212
|
+
safeArray(route.ai_context?.code_structure?.call_targets)
|
|
213
|
+
.map(lastCallSegment)
|
|
214
|
+
.map((segment) => normalizeSnakeSegment(segment))
|
|
215
|
+
.filter((segment) => segment && EFFECT_ACTION_TOKENS.has(segment)),
|
|
216
|
+
),
|
|
217
|
+
].slice(0, 12);
|
|
218
|
+
|
|
219
|
+
const buildEvidencePacketSummary = (route) => {
|
|
220
|
+
const callTargets = safeArray(route.ai_context?.code_structure?.call_targets);
|
|
221
|
+
const dependencyHints = safeArray(route.ai_context?.code_structure?.dependency_hints);
|
|
222
|
+
const actionHints = callActionHints(route);
|
|
223
|
+
return {
|
|
224
|
+
contract_summary: `${route.method ?? "UNKNOWN"} ${route.path_template ?? "/"} operation source contract.`,
|
|
225
|
+
behavior_summary:
|
|
226
|
+
callTargets.length > 0
|
|
227
|
+
? `Handler has ${callTargets.length} bounded call target(s) that may describe domain behavior.`
|
|
228
|
+
: "No bounded domain call targets were detected in the route handler.",
|
|
229
|
+
data_effect_hints: actionHints,
|
|
230
|
+
side_effect_hints: callTargets
|
|
231
|
+
.map(lastCallSegment)
|
|
232
|
+
.map((segment) => normalizeSnakeSegment(segment))
|
|
233
|
+
.filter((segment) =>
|
|
234
|
+
["send", "publish", "emit", "schedule", "enqueue", "notify"].includes(
|
|
235
|
+
segment,
|
|
236
|
+
),
|
|
237
|
+
)
|
|
238
|
+
.slice(0, 12),
|
|
239
|
+
validation_hints: safeArray(
|
|
240
|
+
route.ai_context?.code_structure?.parameter_annotations,
|
|
241
|
+
)
|
|
242
|
+
.map((hint) => sanitizeText(String(hint), route))
|
|
243
|
+
.slice(0, 12),
|
|
244
|
+
auth_hints: dependencyHints
|
|
245
|
+
.map((hint) => sanitizeText(String(hint), route))
|
|
246
|
+
.slice(0, 12),
|
|
247
|
+
missing_context:
|
|
248
|
+
callTargets.length === 0
|
|
249
|
+
? ["No local dependency call evidence was detected."]
|
|
250
|
+
: [],
|
|
251
|
+
};
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const buildRouteEvidencePacket = (route, hashScope) => ({
|
|
255
|
+
contract: {
|
|
256
|
+
operation_source_key: route.operation_source_key,
|
|
257
|
+
method: route.method,
|
|
258
|
+
path_template: route.path_template,
|
|
259
|
+
router_prefix: route.ai_context?.router_prefix ?? null,
|
|
260
|
+
include_prefix: route.ai_context?.include_prefix ?? null,
|
|
261
|
+
},
|
|
262
|
+
code_context: {
|
|
263
|
+
handler_name: route.ai_context?.handler_name ?? null,
|
|
264
|
+
handler_source_excerpt: redactAiInputSource(route.ai_context?.source_snippet),
|
|
265
|
+
code_structure: route.ai_context?.code_structure ?? {},
|
|
266
|
+
},
|
|
267
|
+
stored_summary_preview: buildEvidencePacketSummary(route),
|
|
268
|
+
evidence_summaries: buildSourceEvidenceRefs(route, hashScope).map(
|
|
269
|
+
(entry, index) => ({
|
|
270
|
+
evidence_index: index,
|
|
271
|
+
kind: entry.kind,
|
|
272
|
+
summary: entry.summary,
|
|
273
|
+
evidence_strength: entry.evidence_strength,
|
|
274
|
+
}),
|
|
275
|
+
),
|
|
276
|
+
});
|
|
277
|
+
|
|
179
278
|
const buildAiPrompt = ({ routes, generationContract, agentSkills }) =>
|
|
180
279
|
JSON.stringify({
|
|
181
280
|
...semanticAgentPromptEnvelope({
|
|
@@ -187,8 +286,8 @@ const buildAiPrompt = ({ routes, generationContract, agentSkills }) =>
|
|
|
187
286
|
rules: [
|
|
188
287
|
"Do not create operation_source_key values; only copy the provided keys.",
|
|
189
288
|
"Do not create ids, refs, hashes, source fingerprints, or field paths.",
|
|
190
|
-
"
|
|
191
|
-
"Use the provided
|
|
289
|
+
"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.",
|
|
290
|
+
"Use the provided route_evidence_packet to infer purpose-level business semantics and operation effects.",
|
|
192
291
|
"Return operation_effect_candidates only when source evidence supports a schema-valid target object and effect action.",
|
|
193
292
|
"Keep multiple effects separate when one route produces multiple meaningful effects.",
|
|
194
293
|
"Do not encode actual outcomes such as completed, failed, blocked, validation_error, or abandoned into operation_effect_key or expected_domain_effect.",
|
|
@@ -228,6 +327,7 @@ const buildAiPrompt = ({ routes, generationContract, agentSkills }) =>
|
|
|
228
327
|
method: route.method,
|
|
229
328
|
path_template: route.path_template,
|
|
230
329
|
evidence_summaries: route.evidence_summaries,
|
|
330
|
+
route_evidence_packet: route.route_evidence_packet,
|
|
231
331
|
})),
|
|
232
332
|
});
|
|
233
333
|
|
|
@@ -243,24 +343,30 @@ const buildAiReuseDecisionPrompt = ({
|
|
|
243
343
|
bundle: agentSkills,
|
|
244
344
|
roleId: SEMANTIC_AGENT_ROLE_IDS.reuseJudge,
|
|
245
345
|
}),
|
|
246
|
-
task: "Decide whether previous privacy-safe route semantics still apply after a route source change. Return JSON only.",
|
|
346
|
+
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
347
|
generation_contract: generationContract,
|
|
248
348
|
rules: [
|
|
249
349
|
"Only decide for the provided operation_source_key.",
|
|
250
|
-
"
|
|
251
|
-
"
|
|
252
|
-
"
|
|
253
|
-
"
|
|
350
|
+
"First decide purpose_change_state as same_purpose, purpose_added, purpose_changed, purpose_removed, or insufficient_evidence.",
|
|
351
|
+
"Use same_purpose when implementation details or wording changed but the customer-visible purpose and operation effects are unchanged.",
|
|
352
|
+
"Use purpose_added when the previous purpose still applies and the current route adds a new customer-visible purpose or operation effect.",
|
|
353
|
+
"Use purpose_changed when a previous purpose or primary operation effect is replaced with a different business object or effect.",
|
|
354
|
+
"Use purpose_removed when a previous purpose or operation effect is no longer present.",
|
|
355
|
+
"Use insufficient_evidence when evidence is sparse, conflicting, or cannot prove a purpose-level change.",
|
|
356
|
+
"Return legacy decision as reuse_previous for same_purpose, regenerate for purpose_added/purpose_changed/purpose_removed, and needs_review for insufficient_evidence.",
|
|
357
|
+
"Never recommend changing active semantics for wording drift alone.",
|
|
254
358
|
"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
359
|
],
|
|
256
360
|
output_shape: {
|
|
257
361
|
decisions: [
|
|
258
362
|
{
|
|
259
363
|
operation_source_key: "route.POST./reports",
|
|
364
|
+
purpose_change_state: "same_purpose",
|
|
260
365
|
decision: "reuse_previous",
|
|
261
366
|
confidence: 0.82,
|
|
262
367
|
reason:
|
|
263
368
|
"The source changed but the privacy-safe evidence still supports the previous reports.create meaning.",
|
|
369
|
+
missing_context: [],
|
|
264
370
|
},
|
|
265
371
|
],
|
|
266
372
|
},
|
|
@@ -273,6 +379,7 @@ const buildAiReuseDecisionPrompt = ({
|
|
|
273
379
|
previous_semantics: previousRoute.semantics,
|
|
274
380
|
previous_operation_effects: previousRoute.operation_effects ?? [],
|
|
275
381
|
current_evidence_summaries: route.evidence_summaries,
|
|
382
|
+
current_route_evidence_packet: route.route_evidence_packet,
|
|
276
383
|
},
|
|
277
384
|
});
|
|
278
385
|
|
|
@@ -280,8 +387,10 @@ const normalizeReuseDecision = ({ parsed, operationSourceKey }) => {
|
|
|
280
387
|
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.decisions)) {
|
|
281
388
|
return {
|
|
282
389
|
decision: "needs_review",
|
|
390
|
+
purpose_change_state: "insufficient_evidence",
|
|
283
391
|
confidence: 0,
|
|
284
392
|
reason: "AI reuse decision did not return the required decisions array.",
|
|
393
|
+
missing_context: ["AI reuse decision did not return a usable response."],
|
|
285
394
|
};
|
|
286
395
|
}
|
|
287
396
|
const unknownDecision = parsed.decisions.find(
|
|
@@ -295,8 +404,10 @@ const normalizeReuseDecision = ({ parsed, operationSourceKey }) => {
|
|
|
295
404
|
if (unknownDecision) {
|
|
296
405
|
return {
|
|
297
406
|
decision: "needs_review",
|
|
407
|
+
purpose_change_state: "insufficient_evidence",
|
|
298
408
|
confidence: 0,
|
|
299
409
|
reason: "AI reuse decision returned an unexpected route key.",
|
|
410
|
+
missing_context: ["AI reuse decision returned an unexpected route key."],
|
|
300
411
|
};
|
|
301
412
|
}
|
|
302
413
|
const decision = safeArray(parsed?.decisions).find(
|
|
@@ -305,8 +416,10 @@ const normalizeReuseDecision = ({ parsed, operationSourceKey }) => {
|
|
|
305
416
|
if (!decision || typeof decision !== "object") {
|
|
306
417
|
return {
|
|
307
418
|
decision: "needs_review",
|
|
419
|
+
purpose_change_state: "insufficient_evidence",
|
|
308
420
|
confidence: 0,
|
|
309
421
|
reason: "AI reuse decision omitted this changed route.",
|
|
422
|
+
missing_context: ["AI reuse decision omitted this changed route."],
|
|
310
423
|
};
|
|
311
424
|
}
|
|
312
425
|
const normalizedDecision = [
|
|
@@ -316,8 +429,27 @@ const normalizeReuseDecision = ({ parsed, operationSourceKey }) => {
|
|
|
316
429
|
].includes(decision.decision)
|
|
317
430
|
? decision.decision
|
|
318
431
|
: "needs_review";
|
|
432
|
+
const explicitPurposeState = PURPOSE_CHANGE_STATES.has(
|
|
433
|
+
decision.purpose_change_state,
|
|
434
|
+
)
|
|
435
|
+
? decision.purpose_change_state
|
|
436
|
+
: null;
|
|
437
|
+
const purposeChangeState =
|
|
438
|
+
explicitPurposeState ??
|
|
439
|
+
{
|
|
440
|
+
reuse_previous: "same_purpose",
|
|
441
|
+
regenerate: "purpose_changed",
|
|
442
|
+
needs_review: "insufficient_evidence",
|
|
443
|
+
}[normalizedDecision];
|
|
444
|
+
const decisionFromPurpose =
|
|
445
|
+
purposeChangeState === "same_purpose"
|
|
446
|
+
? "reuse_previous"
|
|
447
|
+
: purposeChangeState === "insufficient_evidence"
|
|
448
|
+
? "needs_review"
|
|
449
|
+
: "regenerate";
|
|
319
450
|
return {
|
|
320
|
-
decision:
|
|
451
|
+
decision: decisionFromPurpose,
|
|
452
|
+
purpose_change_state: purposeChangeState,
|
|
321
453
|
confidence: Number.isFinite(Number(decision.confidence))
|
|
322
454
|
? Math.max(0, Math.min(1, Number(decision.confidence)))
|
|
323
455
|
: 0,
|
|
@@ -325,6 +457,9 @@ const normalizeReuseDecision = ({ parsed, operationSourceKey }) => {
|
|
|
325
457
|
typeof decision.reason === "string" && decision.reason.trim()
|
|
326
458
|
? decision.reason.trim()
|
|
327
459
|
: "AI reuse decision did not include a reason.",
|
|
460
|
+
missing_context: stringArray(decision.missing_context, {
|
|
461
|
+
ai_context: {},
|
|
462
|
+
}),
|
|
328
463
|
};
|
|
329
464
|
};
|
|
330
465
|
|
|
@@ -367,10 +502,12 @@ const callAiReuseDecisionProvider = async ({
|
|
|
367
502
|
: null;
|
|
368
503
|
return {
|
|
369
504
|
decision: "needs_review",
|
|
505
|
+
purpose_change_state: "insufficient_evidence",
|
|
370
506
|
confidence: 0,
|
|
371
507
|
reason: providerStatusMatch?.groups?.status
|
|
372
508
|
? `AI reuse decision failed: provider_${providerStatusMatch.groups.status}.`
|
|
373
509
|
: "AI reuse decision returned malformed JSON.",
|
|
510
|
+
missing_context: ["AI reuse decision failed or returned malformed JSON."],
|
|
374
511
|
};
|
|
375
512
|
}
|
|
376
513
|
};
|
|
@@ -1373,6 +1510,7 @@ const buildOperationAssignmentCollections = ({
|
|
|
1373
1510
|
routeEvidenceRefs,
|
|
1374
1511
|
catalogByGroup,
|
|
1375
1512
|
aiSelectionDecisions,
|
|
1513
|
+
existingEffectKeys = [],
|
|
1376
1514
|
}) => {
|
|
1377
1515
|
const operationEffects = [];
|
|
1378
1516
|
const unresolvedOperationEffects = [];
|
|
@@ -1380,7 +1518,8 @@ const buildOperationAssignmentCollections = ({
|
|
|
1380
1518
|
const mappings = [];
|
|
1381
1519
|
const aiInferenceEvidence = [];
|
|
1382
1520
|
const semanticMeaningCandidates = [];
|
|
1383
|
-
const
|
|
1521
|
+
const existingEffectKeySet = new Set(existingEffectKeys);
|
|
1522
|
+
const emittedEffectKeys = new Set(existingEffectKeys);
|
|
1384
1523
|
|
|
1385
1524
|
const candidates = normalizeAiOperationEffectCandidates(aiRoute);
|
|
1386
1525
|
for (
|
|
@@ -1569,6 +1708,9 @@ const buildOperationAssignmentCollections = ({
|
|
|
1569
1708
|
continue;
|
|
1570
1709
|
}
|
|
1571
1710
|
if (emittedEffectKeys.has(operationEffectKey)) {
|
|
1711
|
+
if (existingEffectKeySet.has(operationEffectKey)) {
|
|
1712
|
+
continue;
|
|
1713
|
+
}
|
|
1572
1714
|
unresolvedOperationEffects.push(
|
|
1573
1715
|
unresolvedEffect({
|
|
1574
1716
|
route,
|
|
@@ -1787,7 +1929,9 @@ const buildSnapshot = ({
|
|
|
1787
1929
|
for (const [operationSourceKey, plan] of routePlans.entries()) {
|
|
1788
1930
|
if (
|
|
1789
1931
|
plan.origin === "unchanged_route_reused" ||
|
|
1790
|
-
plan.origin === "changed_route_semantic_reused"
|
|
1932
|
+
plan.origin === "changed_route_semantic_reused" ||
|
|
1933
|
+
plan.active_semantic_source === "previous_kept_pending_review" ||
|
|
1934
|
+
plan.purpose_change_state === "purpose_added"
|
|
1791
1935
|
) {
|
|
1792
1936
|
reusedRouteKeys.push(operationSourceKey);
|
|
1793
1937
|
}
|
|
@@ -1813,6 +1957,10 @@ const buildSnapshot = ({
|
|
|
1813
1957
|
};
|
|
1814
1958
|
const plan = routePlans.get(route.operation_source_key) ?? {
|
|
1815
1959
|
origin: "new_route_ai_generated",
|
|
1960
|
+
purpose_change_state: "new_route",
|
|
1961
|
+
active_semantic_source: "new_confirmed",
|
|
1962
|
+
stability_confidence: 1,
|
|
1963
|
+
stability_missing_context: [],
|
|
1816
1964
|
route_input_hash: routeInputHash(route),
|
|
1817
1965
|
};
|
|
1818
1966
|
const routeEvidenceRefs = buildSourceEvidenceRefs(
|
|
@@ -1821,15 +1969,17 @@ const buildSnapshot = ({
|
|
|
1821
1969
|
);
|
|
1822
1970
|
sourceEvidenceRefs.push(...routeEvidenceRefs);
|
|
1823
1971
|
if (
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1972
|
+
plan.previous_route &&
|
|
1973
|
+
(plan.active_semantic_source === "previous_reused" ||
|
|
1974
|
+
plan.active_semantic_source === "previous_kept_pending_review") &&
|
|
1975
|
+
plan.purpose_change_state !== "purpose_added"
|
|
1827
1976
|
) {
|
|
1828
1977
|
return rebasePreviousRoute({
|
|
1829
1978
|
previousRoute: plan.previous_route,
|
|
1830
1979
|
currentRoute: route,
|
|
1831
1980
|
semanticSnapshotVersion: provisionalSemanticSnapshotVersion,
|
|
1832
1981
|
plan,
|
|
1982
|
+
routeEvidenceRefs,
|
|
1833
1983
|
});
|
|
1834
1984
|
}
|
|
1835
1985
|
const aiRoute = aiRoutes.get(route.operation_source_key);
|
|
@@ -1869,6 +2019,12 @@ const buildSnapshot = ({
|
|
|
1869
2019
|
routeEvidenceRefs,
|
|
1870
2020
|
catalogByGroup,
|
|
1871
2021
|
aiSelectionDecisions,
|
|
2022
|
+
existingEffectKeys:
|
|
2023
|
+
plan.purpose_change_state === "purpose_added"
|
|
2024
|
+
? safeArray(plan.previous_route?.operation_effects).map(
|
|
2025
|
+
(effect) => effect.operation_effect_key,
|
|
2026
|
+
)
|
|
2027
|
+
: [],
|
|
1872
2028
|
});
|
|
1873
2029
|
targetObjectProfiles.push(...assignmentCollections.profiles);
|
|
1874
2030
|
targetObjectMappings.push(...assignmentCollections.mappings);
|
|
@@ -1876,29 +2032,67 @@ const buildSnapshot = ({
|
|
|
1876
2032
|
semanticMeaningCandidates.push(
|
|
1877
2033
|
...assignmentCollections.semanticMeaningCandidates,
|
|
1878
2034
|
);
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
2035
|
+
if (plan.purpose_change_state === "purpose_added" && plan.previous_route) {
|
|
2036
|
+
const rebasedPrevious = rebasePreviousRoute({
|
|
2037
|
+
previousRoute: plan.previous_route,
|
|
2038
|
+
currentRoute: route,
|
|
2039
|
+
semanticSnapshotVersion: provisionalSemanticSnapshotVersion,
|
|
2040
|
+
plan,
|
|
2041
|
+
routeEvidenceRefs,
|
|
2042
|
+
});
|
|
2043
|
+
const previousEffectKeys = new Set(
|
|
2044
|
+
safeArray(rebasedPrevious.operation_effects).map(
|
|
2045
|
+
(effect) => effect.operation_effect_key,
|
|
2046
|
+
),
|
|
2047
|
+
);
|
|
2048
|
+
const appendedEffects = assignmentCollections.operationEffects.filter(
|
|
2049
|
+
(effect) => !previousEffectKeys.has(effect.operation_effect_key),
|
|
2050
|
+
);
|
|
2051
|
+
const mergedRoute = {
|
|
2052
|
+
...rebasedPrevious,
|
|
2053
|
+
operation_effects: [
|
|
2054
|
+
...safeArray(rebasedPrevious.operation_effects),
|
|
2055
|
+
...appendedEffects,
|
|
2056
|
+
],
|
|
2057
|
+
unresolved_operation_effects: [
|
|
2058
|
+
...safeArray(rebasedPrevious.unresolved_operation_effects),
|
|
2059
|
+
...assignmentCollections.unresolvedOperationEffects,
|
|
2060
|
+
],
|
|
2061
|
+
semantic_change_reason: plan.semantic_change_reason,
|
|
2062
|
+
};
|
|
2063
|
+
return {
|
|
2064
|
+
...mergedRoute,
|
|
2065
|
+
route_semantic_hash: routeSemanticHash(mergedRoute),
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
return withStabilityMetadata({
|
|
2070
|
+
currentRoute: route,
|
|
2071
|
+
plan,
|
|
2072
|
+
route: {
|
|
1896
2073
|
...baseRoute,
|
|
1897
2074
|
operation_effects: assignmentCollections.operationEffects,
|
|
1898
2075
|
unresolved_operation_effects:
|
|
1899
2076
|
assignmentCollections.unresolvedOperationEffects,
|
|
1900
|
-
|
|
1901
|
-
|
|
2077
|
+
source_evidence_refs: routeEvidenceRefs.map((entry) => entry.id),
|
|
2078
|
+
route_input_hash: plan.route_input_hash,
|
|
2079
|
+
previous_route_input_hash: plan.previous_route_input_hash,
|
|
2080
|
+
previous_route_semantic_hash: plan.previous_route_semantic_hash,
|
|
2081
|
+
semantic_origin:
|
|
2082
|
+
plan.origin === "changed_route_needs_review" && aiRoute?.semantics
|
|
2083
|
+
? "changed_route_needs_review"
|
|
2084
|
+
: plan.origin,
|
|
2085
|
+
semantic_change_reason: plan.semantic_change_reason,
|
|
2086
|
+
previous_semantic_snapshot_version:
|
|
2087
|
+
plan.previous_route?.semantic_snapshot_version,
|
|
2088
|
+
route_semantic_hash: routeSemanticHash({
|
|
2089
|
+
...baseRoute,
|
|
2090
|
+
operation_effects: assignmentCollections.operationEffects,
|
|
2091
|
+
unresolved_operation_effects:
|
|
2092
|
+
assignmentCollections.unresolvedOperationEffects,
|
|
2093
|
+
}),
|
|
2094
|
+
},
|
|
2095
|
+
});
|
|
1902
2096
|
});
|
|
1903
2097
|
addUniqueBy(
|
|
1904
2098
|
targetObjectProfiles,
|
|
@@ -1942,7 +2136,7 @@ const buildSnapshot = ({
|
|
|
1942
2136
|
idempotency_key: sha256(
|
|
1943
2137
|
`${request.project_key}:${request.repository.repository_id}:${request.repository.merge_commit}:${request.repository.workflow_run_id ?? generatedAt}`,
|
|
1944
2138
|
),
|
|
1945
|
-
schema_version:
|
|
2139
|
+
schema_version: SEMANTIC_SNAPSHOT_SCHEMA_VERSION,
|
|
1946
2140
|
semantic_snapshot_version: semanticSnapshotVersion,
|
|
1947
2141
|
generation_contract: generationContract,
|
|
1948
2142
|
ai_runtime: aiRuntime,
|
|
@@ -1984,6 +2178,24 @@ const buildSnapshot = ({
|
|
|
1984
2178
|
(route) =>
|
|
1985
2179
|
route.semantic_origin === "changed_route_semantic_regenerated",
|
|
1986
2180
|
).length,
|
|
2181
|
+
same_purpose_routes: versionedSnapshotRoutes.filter(
|
|
2182
|
+
(route) => route.purpose_change_state === "same_purpose",
|
|
2183
|
+
).length,
|
|
2184
|
+
purpose_added_routes: versionedSnapshotRoutes.filter(
|
|
2185
|
+
(route) => route.purpose_change_state === "purpose_added",
|
|
2186
|
+
).length,
|
|
2187
|
+
purpose_changed_routes: versionedSnapshotRoutes.filter(
|
|
2188
|
+
(route) => route.purpose_change_state === "purpose_changed",
|
|
2189
|
+
).length,
|
|
2190
|
+
purpose_removed_routes: versionedSnapshotRoutes.filter(
|
|
2191
|
+
(route) => route.purpose_change_state === "purpose_removed",
|
|
2192
|
+
).length,
|
|
2193
|
+
insufficient_evidence_routes: versionedSnapshotRoutes.filter(
|
|
2194
|
+
(route) => route.purpose_change_state === "insufficient_evidence",
|
|
2195
|
+
).length,
|
|
2196
|
+
new_route_purpose_routes: versionedSnapshotRoutes.filter(
|
|
2197
|
+
(route) => route.purpose_change_state === "new_route",
|
|
2198
|
+
).length,
|
|
1987
2199
|
},
|
|
1988
2200
|
routes: versionedSnapshotRoutes,
|
|
1989
2201
|
target_object_profiles: targetObjectProfiles,
|
|
@@ -2228,10 +2440,87 @@ const hasReusablePreviousRouteSemantics = (route) =>
|
|
|
2228
2440
|
Number(route?.semantics?.route_confidence ?? 0) > 0 &&
|
|
2229
2441
|
route?.semantics?.route_summary !== "unknown";
|
|
2230
2442
|
|
|
2443
|
+
const normalizeRoutePathForPolicy = (path) => {
|
|
2444
|
+
const trimmed = String(path ?? "").trim();
|
|
2445
|
+
const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
2446
|
+
return withSlash.replace(/\/{2,}/g, "/").replace(/\/$/g, "") || "/";
|
|
2447
|
+
};
|
|
2448
|
+
|
|
2449
|
+
const isReservedClueBrowserTokenPath = (path) =>
|
|
2450
|
+
/^\/(?:api\/(?:v[0-9]+\/)?)?clue\/browser-tokens?$/i.test(
|
|
2451
|
+
normalizeRoutePathForPolicy(path),
|
|
2452
|
+
);
|
|
2453
|
+
|
|
2454
|
+
const routeSourceForPolicy = (route) =>
|
|
2455
|
+
[
|
|
2456
|
+
route?.ai_context?.route_decorator,
|
|
2457
|
+
route?.ai_context?.source_snippet,
|
|
2458
|
+
safeArray(route?.ai_context?.code_structure?.call_targets).join("\n"),
|
|
2459
|
+
]
|
|
2460
|
+
.filter((entry) => typeof entry === "string")
|
|
2461
|
+
.join("\n");
|
|
2462
|
+
|
|
2463
|
+
const classifyClueInfrastructureRoute = (route) => {
|
|
2464
|
+
const source = routeSourceForPolicy(route);
|
|
2465
|
+
const reservedPath = isReservedClueBrowserTokenPath(route.path_template);
|
|
2466
|
+
const proxiesToClueBrowserTokenApi =
|
|
2467
|
+
/\/(?:api\/v[0-9]+\/)?ingest\/browser-tokens\b/i.test(source);
|
|
2468
|
+
const usesServerSideClueApiKey = /\bCLUE_API_KEY\b|x-clue-api-key/i.test(
|
|
2469
|
+
source,
|
|
2470
|
+
);
|
|
2471
|
+
if (
|
|
2472
|
+
route.method === "POST" &&
|
|
2473
|
+
reservedPath &&
|
|
2474
|
+
proxiesToClueBrowserTokenApi &&
|
|
2475
|
+
usesServerSideClueApiKey
|
|
2476
|
+
) {
|
|
2477
|
+
return {
|
|
2478
|
+
excluded: true,
|
|
2479
|
+
reason:
|
|
2480
|
+
"Clue SDK browser-token proxy route detected by reserved Clue path, Clue browser-token API proxy call, and server-side Clue API key usage.",
|
|
2481
|
+
evidence: {
|
|
2482
|
+
reserved_clue_path: true,
|
|
2483
|
+
proxies_to_clue_browser_token_api: true,
|
|
2484
|
+
uses_server_side_clue_api_key: true,
|
|
2485
|
+
},
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
return {
|
|
2489
|
+
excluded: false,
|
|
2490
|
+
reason: null,
|
|
2491
|
+
evidence: {
|
|
2492
|
+
reserved_clue_path: reservedPath,
|
|
2493
|
+
proxies_to_clue_browser_token_api: proxiesToClueBrowserTokenApi,
|
|
2494
|
+
uses_server_side_clue_api_key: usesServerSideClueApiKey,
|
|
2495
|
+
},
|
|
2496
|
+
};
|
|
2497
|
+
};
|
|
2498
|
+
|
|
2499
|
+
const splitSemanticProductRoutes = (routes) => {
|
|
2500
|
+
const productRoutes = [];
|
|
2501
|
+
const excludedInfrastructureRoutes = [];
|
|
2502
|
+
for (const route of routes) {
|
|
2503
|
+
const classification = classifyClueInfrastructureRoute(route);
|
|
2504
|
+
if (classification.excluded) {
|
|
2505
|
+
excludedInfrastructureRoutes.push({
|
|
2506
|
+
operation_source_key: route.operation_source_key,
|
|
2507
|
+
method: route.method,
|
|
2508
|
+
path_template: route.path_template,
|
|
2509
|
+
reason: classification.reason,
|
|
2510
|
+
evidence: classification.evidence,
|
|
2511
|
+
});
|
|
2512
|
+
continue;
|
|
2513
|
+
}
|
|
2514
|
+
productRoutes.push(route);
|
|
2515
|
+
}
|
|
2516
|
+
return { productRoutes, excludedInfrastructureRoutes };
|
|
2517
|
+
};
|
|
2518
|
+
|
|
2231
2519
|
const buildRouteEvidencePromptEntry = ({ route, hashScope }) => ({
|
|
2232
2520
|
operation_source_key: route.operation_source_key,
|
|
2233
2521
|
method: route.method,
|
|
2234
2522
|
path_template: route.path_template,
|
|
2523
|
+
route_evidence_packet: buildRouteEvidencePacket(route, hashScope),
|
|
2235
2524
|
evidence_summaries: buildSourceEvidenceRefs(route, hashScope).map(
|
|
2236
2525
|
(entry, index) => ({
|
|
2237
2526
|
evidence_index: index,
|
|
@@ -2251,6 +2540,7 @@ const classifyRoutesForSnapshot = async ({
|
|
|
2251
2540
|
hashScope,
|
|
2252
2541
|
generationContract,
|
|
2253
2542
|
agentSkills,
|
|
2543
|
+
purposeRecheckAllRoutes = false,
|
|
2254
2544
|
}) => {
|
|
2255
2545
|
const previousRoutes = previousRouteMap(previousSnapshot);
|
|
2256
2546
|
const plans = new Map();
|
|
@@ -2262,6 +2552,10 @@ const classifyRoutesForSnapshot = async ({
|
|
|
2262
2552
|
if (!previousRoute) {
|
|
2263
2553
|
plans.set(route.operation_source_key, {
|
|
2264
2554
|
origin: "new_route_ai_generated",
|
|
2555
|
+
purpose_change_state: "new_route",
|
|
2556
|
+
active_semantic_source: "new_confirmed",
|
|
2557
|
+
stability_confidence: 1,
|
|
2558
|
+
stability_missing_context: [],
|
|
2265
2559
|
route_input_hash: currentHash,
|
|
2266
2560
|
});
|
|
2267
2561
|
routesRequiringGeneration.push(route);
|
|
@@ -2272,8 +2566,86 @@ const classifyRoutesForSnapshot = async ({
|
|
|
2272
2566
|
previousRoute.route_input_hash === currentHash &&
|
|
2273
2567
|
hasReusablePreviousRouteSemantics(previousRoute)
|
|
2274
2568
|
) {
|
|
2569
|
+
if (purposeRecheckAllRoutes) {
|
|
2570
|
+
const decision = await callAiReuseDecisionProvider({
|
|
2571
|
+
request,
|
|
2572
|
+
env,
|
|
2573
|
+
apiKey: aiProviderApiKey,
|
|
2574
|
+
route: buildRouteEvidencePromptEntry({ route, hashScope }),
|
|
2575
|
+
previousRoute,
|
|
2576
|
+
currentHash,
|
|
2577
|
+
generationContract,
|
|
2578
|
+
agentSkills,
|
|
2579
|
+
});
|
|
2580
|
+
if (
|
|
2581
|
+
decision.purpose_change_state === "same_purpose" &&
|
|
2582
|
+
decision.confidence >= PURPOSE_STABILITY_CONFIDENCE_THRESHOLD
|
|
2583
|
+
) {
|
|
2584
|
+
plans.set(route.operation_source_key, {
|
|
2585
|
+
origin: "unchanged_route_reused",
|
|
2586
|
+
purpose_change_state: "same_purpose",
|
|
2587
|
+
active_semantic_source: "previous_reused",
|
|
2588
|
+
stability_confidence: decision.confidence,
|
|
2589
|
+
stability_missing_context: decision.missing_context,
|
|
2590
|
+
route_input_hash: currentHash,
|
|
2591
|
+
previous_route: previousRoute,
|
|
2592
|
+
previous_route_input_hash: previousRoute.route_input_hash,
|
|
2593
|
+
previous_route_semantic_hash:
|
|
2594
|
+
previousRoute.route_semantic_hash ??
|
|
2595
|
+
routeSemanticHash(previousRoute),
|
|
2596
|
+
semantic_change_reason: sanitizeText(
|
|
2597
|
+
`Periodic full purpose check confirmed previous semantics still apply: ${decision.reason}`,
|
|
2598
|
+
route,
|
|
2599
|
+
),
|
|
2600
|
+
});
|
|
2601
|
+
continue;
|
|
2602
|
+
}
|
|
2603
|
+
if (
|
|
2604
|
+
decision.decision === "regenerate" &&
|
|
2605
|
+
decision.confidence >= PURPOSE_STABILITY_CONFIDENCE_THRESHOLD
|
|
2606
|
+
) {
|
|
2607
|
+
plans.set(route.operation_source_key, {
|
|
2608
|
+
origin: "changed_route_semantic_regenerated",
|
|
2609
|
+
purpose_change_state: decision.purpose_change_state,
|
|
2610
|
+
active_semantic_source: "new_confirmed",
|
|
2611
|
+
stability_confidence: decision.confidence,
|
|
2612
|
+
stability_missing_context: decision.missing_context,
|
|
2613
|
+
route_input_hash: currentHash,
|
|
2614
|
+
previous_route: previousRoute,
|
|
2615
|
+
previous_route_input_hash: previousRoute.route_input_hash,
|
|
2616
|
+
previous_route_semantic_hash:
|
|
2617
|
+
previousRoute.route_semantic_hash ??
|
|
2618
|
+
routeSemanticHash(previousRoute),
|
|
2619
|
+
semantic_change_reason: sanitizeText(decision.reason, route),
|
|
2620
|
+
});
|
|
2621
|
+
routesRequiringGeneration.push(route);
|
|
2622
|
+
continue;
|
|
2623
|
+
}
|
|
2624
|
+
plans.set(route.operation_source_key, {
|
|
2625
|
+
origin: "changed_route_needs_review",
|
|
2626
|
+
purpose_change_state: "insufficient_evidence",
|
|
2627
|
+
active_semantic_source: "previous_kept_pending_review",
|
|
2628
|
+
stability_confidence: decision.confidence,
|
|
2629
|
+
stability_missing_context: decision.missing_context,
|
|
2630
|
+
route_input_hash: currentHash,
|
|
2631
|
+
previous_route: previousRoute,
|
|
2632
|
+
previous_route_input_hash: previousRoute.route_input_hash,
|
|
2633
|
+
previous_route_semantic_hash:
|
|
2634
|
+
previousRoute.route_semantic_hash ??
|
|
2635
|
+
routeSemanticHash(previousRoute),
|
|
2636
|
+
semantic_change_reason: sanitizeText(
|
|
2637
|
+
`Periodic full purpose check could not prove a purpose-level change: ${decision.reason}`,
|
|
2638
|
+
route,
|
|
2639
|
+
),
|
|
2640
|
+
});
|
|
2641
|
+
continue;
|
|
2642
|
+
}
|
|
2275
2643
|
plans.set(route.operation_source_key, {
|
|
2276
2644
|
origin: "unchanged_route_reused",
|
|
2645
|
+
purpose_change_state: "same_purpose",
|
|
2646
|
+
active_semantic_source: "previous_reused",
|
|
2647
|
+
stability_confidence: 1,
|
|
2648
|
+
stability_missing_context: [],
|
|
2277
2649
|
route_input_hash: currentHash,
|
|
2278
2650
|
previous_route: previousRoute,
|
|
2279
2651
|
previous_route_input_hash: previousRoute.route_input_hash,
|
|
@@ -2288,6 +2660,12 @@ const classifyRoutesForSnapshot = async ({
|
|
|
2288
2660
|
if (previousRoute.route_input_hash === currentHash) {
|
|
2289
2661
|
plans.set(route.operation_source_key, {
|
|
2290
2662
|
origin: "changed_route_semantic_regenerated",
|
|
2663
|
+
purpose_change_state: "new_route",
|
|
2664
|
+
active_semantic_source: "new_confirmed",
|
|
2665
|
+
stability_confidence: 0,
|
|
2666
|
+
stability_missing_context: [
|
|
2667
|
+
"Previous route semantic confidence was too low for stable reuse.",
|
|
2668
|
+
],
|
|
2291
2669
|
route_input_hash: currentHash,
|
|
2292
2670
|
previous_route: previousRoute,
|
|
2293
2671
|
previous_route_input_hash: previousRoute.route_input_hash,
|
|
@@ -2310,9 +2688,16 @@ const classifyRoutesForSnapshot = async ({
|
|
|
2310
2688
|
generationContract,
|
|
2311
2689
|
agentSkills,
|
|
2312
2690
|
});
|
|
2313
|
-
if (
|
|
2691
|
+
if (
|
|
2692
|
+
decision.purpose_change_state === "same_purpose" &&
|
|
2693
|
+
decision.confidence >= PURPOSE_STABILITY_CONFIDENCE_THRESHOLD
|
|
2694
|
+
) {
|
|
2314
2695
|
plans.set(route.operation_source_key, {
|
|
2315
2696
|
origin: "changed_route_semantic_reused",
|
|
2697
|
+
purpose_change_state: "same_purpose",
|
|
2698
|
+
active_semantic_source: "previous_reused",
|
|
2699
|
+
stability_confidence: decision.confidence,
|
|
2700
|
+
stability_missing_context: decision.missing_context,
|
|
2316
2701
|
route_input_hash: currentHash,
|
|
2317
2702
|
previous_route: previousRoute,
|
|
2318
2703
|
previous_route_input_hash: previousRoute.route_input_hash,
|
|
@@ -2322,9 +2707,16 @@ const classifyRoutesForSnapshot = async ({
|
|
|
2322
2707
|
});
|
|
2323
2708
|
continue;
|
|
2324
2709
|
}
|
|
2325
|
-
if (
|
|
2710
|
+
if (
|
|
2711
|
+
decision.purpose_change_state === "same_purpose" ||
|
|
2712
|
+
decision.purpose_change_state === "insufficient_evidence"
|
|
2713
|
+
) {
|
|
2326
2714
|
plans.set(route.operation_source_key, {
|
|
2327
2715
|
origin: "changed_route_needs_review",
|
|
2716
|
+
purpose_change_state: "insufficient_evidence",
|
|
2717
|
+
active_semantic_source: "previous_kept_pending_review",
|
|
2718
|
+
stability_confidence: decision.confidence,
|
|
2719
|
+
stability_missing_context: decision.missing_context,
|
|
2328
2720
|
route_input_hash: currentHash,
|
|
2329
2721
|
previous_route: previousRoute,
|
|
2330
2722
|
previous_route_input_hash: previousRoute.route_input_hash,
|
|
@@ -2337,9 +2729,16 @@ const classifyRoutesForSnapshot = async ({
|
|
|
2337
2729
|
});
|
|
2338
2730
|
continue;
|
|
2339
2731
|
}
|
|
2340
|
-
if (
|
|
2732
|
+
if (
|
|
2733
|
+
decision.decision === "regenerate" &&
|
|
2734
|
+
decision.confidence >= PURPOSE_STABILITY_CONFIDENCE_THRESHOLD
|
|
2735
|
+
) {
|
|
2341
2736
|
plans.set(route.operation_source_key, {
|
|
2342
2737
|
origin: "changed_route_semantic_regenerated",
|
|
2738
|
+
purpose_change_state: decision.purpose_change_state,
|
|
2739
|
+
active_semantic_source: "new_confirmed",
|
|
2740
|
+
stability_confidence: decision.confidence,
|
|
2741
|
+
stability_missing_context: decision.missing_context,
|
|
2343
2742
|
route_input_hash: currentHash,
|
|
2344
2743
|
previous_route: previousRoute,
|
|
2345
2744
|
previous_route_input_hash: previousRoute.route_input_hash,
|
|
@@ -2352,6 +2751,10 @@ const classifyRoutesForSnapshot = async ({
|
|
|
2352
2751
|
}
|
|
2353
2752
|
plans.set(route.operation_source_key, {
|
|
2354
2753
|
origin: "changed_route_needs_review",
|
|
2754
|
+
purpose_change_state: "insufficient_evidence",
|
|
2755
|
+
active_semantic_source: "previous_kept_pending_review",
|
|
2756
|
+
stability_confidence: decision.confidence,
|
|
2757
|
+
stability_missing_context: decision.missing_context,
|
|
2355
2758
|
route_input_hash: currentHash,
|
|
2356
2759
|
previous_route: previousRoute,
|
|
2357
2760
|
previous_route_input_hash: previousRoute.route_input_hash,
|
|
@@ -2380,7 +2783,14 @@ const rebasePreviousRoute = ({
|
|
|
2380
2783
|
currentRoute,
|
|
2381
2784
|
semanticSnapshotVersion,
|
|
2382
2785
|
plan,
|
|
2786
|
+
routeEvidenceRefs = [],
|
|
2383
2787
|
}) => {
|
|
2788
|
+
const sourceEvidenceRefs = [
|
|
2789
|
+
...new Set([
|
|
2790
|
+
...safeArray(previousRoute.source_evidence_refs),
|
|
2791
|
+
...routeEvidenceRefs.map((entry) => entry.id),
|
|
2792
|
+
]),
|
|
2793
|
+
];
|
|
2384
2794
|
const route = {
|
|
2385
2795
|
...previousRoute,
|
|
2386
2796
|
operation_source_key: currentRoute.operation_source_key,
|
|
@@ -2395,6 +2805,11 @@ const rebasePreviousRoute = ({
|
|
|
2395
2805
|
previous_route_semantic_hash: plan.previous_route_semantic_hash,
|
|
2396
2806
|
semantic_origin: plan.origin,
|
|
2397
2807
|
semantic_change_reason: plan.semantic_change_reason,
|
|
2808
|
+
purpose_change_state: plan.purpose_change_state,
|
|
2809
|
+
active_semantic_source: plan.active_semantic_source,
|
|
2810
|
+
semantic_stability: buildSemanticStability(plan),
|
|
2811
|
+
evidence_packet_summary: buildEvidencePacketSummary(currentRoute),
|
|
2812
|
+
source_evidence_refs: sourceEvidenceRefs,
|
|
2398
2813
|
};
|
|
2399
2814
|
return {
|
|
2400
2815
|
...route,
|
|
@@ -2402,6 +2817,42 @@ const rebasePreviousRoute = ({
|
|
|
2402
2817
|
};
|
|
2403
2818
|
};
|
|
2404
2819
|
|
|
2820
|
+
const buildSemanticStability = (plan) => ({
|
|
2821
|
+
purpose_change_state: plan.purpose_change_state ?? "new_route",
|
|
2822
|
+
confidence:
|
|
2823
|
+
typeof plan.stability_confidence === "number"
|
|
2824
|
+
? Math.max(0, Math.min(1, plan.stability_confidence))
|
|
2825
|
+
: plan.purpose_change_state === "new_route"
|
|
2826
|
+
? 1
|
|
2827
|
+
: 0,
|
|
2828
|
+
reason:
|
|
2829
|
+
plan.semantic_change_reason ??
|
|
2830
|
+
(plan.purpose_change_state === "new_route"
|
|
2831
|
+
? "No previous route semantic existed, so a new active semantic was generated."
|
|
2832
|
+
: "Semantic purpose stability was evaluated for this route."),
|
|
2833
|
+
...(plan.previous_route_input_hash
|
|
2834
|
+
? { previous_route_input_hash: plan.previous_route_input_hash }
|
|
2835
|
+
: {}),
|
|
2836
|
+
...(plan.previous_route_semantic_hash
|
|
2837
|
+
? { previous_route_semantic_hash: plan.previous_route_semantic_hash }
|
|
2838
|
+
: {}),
|
|
2839
|
+
...(plan.previous_route?.semantic_snapshot_version
|
|
2840
|
+
? {
|
|
2841
|
+
previous_semantic_snapshot_version:
|
|
2842
|
+
plan.previous_route.semantic_snapshot_version,
|
|
2843
|
+
}
|
|
2844
|
+
: {}),
|
|
2845
|
+
missing_context: safeArray(plan.stability_missing_context),
|
|
2846
|
+
});
|
|
2847
|
+
|
|
2848
|
+
const withStabilityMetadata = ({ route, currentRoute, plan }) => ({
|
|
2849
|
+
...route,
|
|
2850
|
+
purpose_change_state: plan.purpose_change_state,
|
|
2851
|
+
active_semantic_source: plan.active_semantic_source,
|
|
2852
|
+
semantic_stability: buildSemanticStability(plan),
|
|
2853
|
+
evidence_packet_summary: buildEvidencePacketSummary(currentRoute),
|
|
2854
|
+
});
|
|
2855
|
+
|
|
2405
2856
|
const fieldPathMatchesKeys = ({
|
|
2406
2857
|
fieldPath,
|
|
2407
2858
|
routeKeys,
|
|
@@ -2813,6 +3264,10 @@ const assertSemanticSnapshotAudit = ({
|
|
|
2813
3264
|
const routeByKey = new Map(
|
|
2814
3265
|
routes.map((route) => [route.operation_source_key, route]),
|
|
2815
3266
|
);
|
|
3267
|
+
const previousActiveSources = new Set([
|
|
3268
|
+
"previous_reused",
|
|
3269
|
+
"previous_kept_pending_review",
|
|
3270
|
+
]);
|
|
2816
3271
|
const allowedOrigins = new Set([
|
|
2817
3272
|
"new_route_ai_generated",
|
|
2818
3273
|
"unchanged_route_reused",
|
|
@@ -2843,6 +3298,56 @@ const assertSemanticSnapshotAudit = ({
|
|
|
2843
3298
|
`semantic snapshot audit found stale route_semantic_hash: ${route.operation_source_key}`,
|
|
2844
3299
|
);
|
|
2845
3300
|
}
|
|
3301
|
+
if (auditedSnapshot.schema_version >= 3) {
|
|
3302
|
+
if (route.semantic_stability.purpose_change_state !== route.purpose_change_state) {
|
|
3303
|
+
throw new Error(
|
|
3304
|
+
`semantic snapshot audit found mismatched purpose stability metadata: ${route.operation_source_key}`,
|
|
3305
|
+
);
|
|
3306
|
+
}
|
|
3307
|
+
if (
|
|
3308
|
+
previousActiveSources.has(route.active_semantic_source) &&
|
|
3309
|
+
(!route.previous_route_input_hash ||
|
|
3310
|
+
!route.previous_route_semantic_hash ||
|
|
3311
|
+
!route.previous_semantic_snapshot_version)
|
|
3312
|
+
) {
|
|
3313
|
+
throw new Error(
|
|
3314
|
+
`semantic snapshot audit found incomplete previous active semantic metadata: ${route.operation_source_key}`,
|
|
3315
|
+
);
|
|
3316
|
+
}
|
|
3317
|
+
if (
|
|
3318
|
+
previousActiveSources.has(route.active_semantic_source) &&
|
|
3319
|
+
route.route_semantic_hash !== route.previous_route_semantic_hash
|
|
3320
|
+
) {
|
|
3321
|
+
throw new Error(
|
|
3322
|
+
`semantic snapshot audit found changed active semantics for previous-backed route: ${route.operation_source_key}`,
|
|
3323
|
+
);
|
|
3324
|
+
}
|
|
3325
|
+
if (
|
|
3326
|
+
route.active_semantic_source === "previous_kept_pending_review" &&
|
|
3327
|
+
(route.purpose_change_state !== "insufficient_evidence" ||
|
|
3328
|
+
route.semantic_origin !== "changed_route_needs_review")
|
|
3329
|
+
) {
|
|
3330
|
+
throw new Error(
|
|
3331
|
+
`semantic snapshot audit found inconsistent pending-review semantics: ${route.operation_source_key}`,
|
|
3332
|
+
);
|
|
3333
|
+
}
|
|
3334
|
+
if (
|
|
3335
|
+
route.active_semantic_source === "previous_reused" &&
|
|
3336
|
+
route.purpose_change_state !== "same_purpose"
|
|
3337
|
+
) {
|
|
3338
|
+
throw new Error(
|
|
3339
|
+
`semantic snapshot audit found inconsistent reused semantics: ${route.operation_source_key}`,
|
|
3340
|
+
);
|
|
3341
|
+
}
|
|
3342
|
+
if (
|
|
3343
|
+
route.purpose_change_state === "purpose_added" &&
|
|
3344
|
+
(!route.previous_route_input_hash || !route.previous_route_semantic_hash)
|
|
3345
|
+
) {
|
|
3346
|
+
throw new Error(
|
|
3347
|
+
`semantic snapshot audit found missing previous metadata for added purpose: ${route.operation_source_key}`,
|
|
3348
|
+
);
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
2846
3351
|
if (
|
|
2847
3352
|
(route.semantic_origin === "unchanged_route_reused" ||
|
|
2848
3353
|
route.semantic_origin === "changed_route_semantic_reused") &&
|
|
@@ -2883,10 +3388,12 @@ export const runSemanticCi = async ({
|
|
|
2883
3388
|
allowedSourcePaths: request.allowed_source_paths,
|
|
2884
3389
|
excludedSourcePaths: request.excluded_source_paths,
|
|
2885
3390
|
});
|
|
2886
|
-
const
|
|
3391
|
+
const discoveredRoutes = await analyzeFastApiRoutes({ repoRoot, files });
|
|
3392
|
+
const { productRoutes: routes, excludedInfrastructureRoutes } =
|
|
3393
|
+
splitSemanticProductRoutes(discoveredRoutes);
|
|
2887
3394
|
if (routes.length === 0) {
|
|
2888
3395
|
throw new Error(
|
|
2889
|
-
"semantic CI discovered zero routes; check framework, backend root path, and
|
|
3396
|
+
"semantic CI discovered zero routes after infrastructure exclusions; check framework, backend root path, allowed_source_paths, and infrastructure route exclusions",
|
|
2890
3397
|
);
|
|
2891
3398
|
}
|
|
2892
3399
|
if (!env.CLUE_API_KEY) {
|
|
@@ -2897,12 +3404,14 @@ export const runSemanticCi = async ({
|
|
|
2897
3404
|
? await fetchLatestSnapshot({ request, env })
|
|
2898
3405
|
: validatePreviousSnapshotForReuse(providedPreviousSnapshot);
|
|
2899
3406
|
const previousRoutes = previousRouteMap(previousSnapshot);
|
|
3407
|
+
const purposeRecheckAllRoutes = shouldPurposeRecheckAllRoutes(env);
|
|
2900
3408
|
const routeNeedsAi = routes.some((route) => {
|
|
2901
3409
|
const previousRoute = previousRoutes.get(route.operation_source_key);
|
|
2902
3410
|
return (
|
|
2903
3411
|
!previousRoute ||
|
|
2904
3412
|
previousRoute.route_input_hash !== routeInputHash(route) ||
|
|
2905
|
-
!hasReusablePreviousRouteSemantics(previousRoute)
|
|
3413
|
+
!hasReusablePreviousRouteSemantics(previousRoute) ||
|
|
3414
|
+
purposeRecheckAllRoutes
|
|
2906
3415
|
);
|
|
2907
3416
|
});
|
|
2908
3417
|
if (routeNeedsAi && !env.CLUE_AI_PROVIDER_API_KEY) {
|
|
@@ -2935,6 +3444,7 @@ export const runSemanticCi = async ({
|
|
|
2935
3444
|
hashScope,
|
|
2936
3445
|
generationContract,
|
|
2937
3446
|
agentSkills,
|
|
3447
|
+
purposeRecheckAllRoutes,
|
|
2938
3448
|
});
|
|
2939
3449
|
const promptRoutes = routesRequiringGeneration.map((route) =>
|
|
2940
3450
|
buildRouteEvidencePromptEntry({ route, hashScope }),
|
|
@@ -2989,6 +3499,7 @@ export const runSemanticCi = async ({
|
|
|
2989
3499
|
return {
|
|
2990
3500
|
accepted: upload.accepted === true,
|
|
2991
3501
|
route_count: snapshot.routes.length,
|
|
3502
|
+
excluded_infrastructure_routes: excludedInfrastructureRoutes,
|
|
2992
3503
|
upload,
|
|
2993
3504
|
};
|
|
2994
3505
|
};
|
|
@@ -2999,10 +3510,12 @@ export const runSemanticInventory = async ({ repoRoot, request }) => {
|
|
|
2999
3510
|
allowedSourcePaths: request.allowed_source_paths,
|
|
3000
3511
|
excludedSourcePaths: request.excluded_source_paths,
|
|
3001
3512
|
});
|
|
3002
|
-
const
|
|
3513
|
+
const discoveredRoutes = await analyzeFastApiRoutes({ repoRoot, files });
|
|
3514
|
+
const { productRoutes: routes, excludedInfrastructureRoutes } =
|
|
3515
|
+
splitSemanticProductRoutes(discoveredRoutes);
|
|
3003
3516
|
if (routes.length === 0) {
|
|
3004
3517
|
throw new Error(
|
|
3005
|
-
"semantic inventory discovered zero routes; check framework, backend root path, and
|
|
3518
|
+
"semantic inventory discovered zero routes after infrastructure exclusions; check framework, backend root path, allowed_source_paths, and infrastructure route exclusions",
|
|
3006
3519
|
);
|
|
3007
3520
|
}
|
|
3008
3521
|
return {
|
|
@@ -3010,7 +3523,9 @@ export const runSemanticInventory = async ({ repoRoot, request }) => {
|
|
|
3010
3523
|
service_key: request.service_key,
|
|
3011
3524
|
allowed_source_paths: request.allowed_source_paths,
|
|
3012
3525
|
excluded_source_paths: request.excluded_source_paths,
|
|
3526
|
+
discovered_route_count: discoveredRoutes.length,
|
|
3013
3527
|
route_count: routes.length,
|
|
3528
|
+
excluded_infrastructure_routes: excludedInfrastructureRoutes,
|
|
3014
3529
|
operation_source_keys: routes.map((route) => route.operation_source_key),
|
|
3015
3530
|
routes: routes.map((route) => ({
|
|
3016
3531
|
operation_source_key: route.operation_source_key,
|