Skip to content

Commit de365d8

Browse files
authored
fix(ai): diagnose empty Golden Path output (#13828) (#13829)
1 parent 054fc49 commit de365d8

2 files changed

Lines changed: 209 additions & 1 deletion

File tree

ai/services/graph/GoldenPathSynthesizer.mjs

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,68 @@ class GoldenPathSynthesizer extends Base {
354354
return !labels.some(label => COMPUTED_RECOMMENDATION_EXCLUDED_LABELS.has(label))
355355
}
356356

357+
/**
358+
* @summary Removes stale Computed Golden Path guide edges from the frontier.
359+
*
360+
* `frontier -> GUIDES` edges are a machine-consumed steering surface. Each
361+
* synthesis pass must remove recommendations that are no longer present in
362+
* the current computed result; otherwise a zero-node render can leave old
363+
* guidance active in the graph after the handoff stops rendering it.
364+
*
365+
* @param {Object} [options]
366+
* @param {Object} [options.graphService=GraphService] Graph service instance.
367+
* @param {Set<String>} [options.currentTargetIds=new Set()] Current computed target ids.
368+
* @returns {Number} Count of stale guide edges removed.
369+
*/
370+
static pruneStaleFrontierGuideEdges({
371+
graphService = GraphService,
372+
currentTargetIds = new Set()
373+
} = {}) {
374+
graphService?.db?.getAdjacentNodes?.('frontier', 'out');
375+
376+
const staleEdges = (graphService?.db?.edges?.getByIndex?.('source', 'frontier') || [])
377+
.filter(edge => edge.type === 'GUIDES' && !currentTargetIds.has(edge.target));
378+
379+
if (staleEdges.length > 0) {
380+
graphService.db.edges.remove(staleEdges.map(edge => edge.id));
381+
// Drop the exact index references returned above in case the Store map points at refreshed edge objects.
382+
graphService.db.edges.updateIndexMaps?.(null, staleEdges);
383+
}
384+
385+
return staleEdges.length
386+
}
387+
388+
/**
389+
* @summary Renders the bounded diagnostic for an empty Computed Golden Path pass.
390+
*
391+
* The handoff should distinguish "no computed recommendation survived the
392+
* filter chain" from "the handoff forgot to render the routing surface".
393+
*
394+
* @param {Object} stats Candidate-count diagnostics for the current pass.
395+
* @returns {String} Markdown section.
396+
*/
397+
static renderComputedGoldenPathEmptySection(stats = {}) {
398+
const count = value => Number.isFinite(Number(value)) ? Number(value) : 0;
399+
400+
return [
401+
'',
402+
'## Computed Golden Path (Strategic Recommendation)',
403+
'',
404+
'No actionable computed recommendations survived the current Tri-Vector filter pass.',
405+
'',
406+
`- Semantic candidates: ${count(stats.semanticCandidates)}`,
407+
`- SQLite OPEN matches: ${count(stats.sqliteOpenMatches)}`,
408+
`- Blocked candidates filtered: ${count(stats.blockedCandidates)}`,
409+
`- Non-actionable candidates filtered: ${count(stats.nonActionableCandidates)}`,
410+
`- Scored actionable candidates: ${count(stats.scoredCandidates)}`,
411+
`- Selected top nodes: ${count(stats.selectedTopNodes)}`,
412+
`- Stale frontier GUIDES pruned: ${count(stats.prunedGuideEdges)}`,
413+
'',
414+
'This is an empty-state diagnostic for the computed routing surface. Use the Current Release / Incident Focus section for visibility-only hot work while the computed candidate chain is empty.',
415+
''
416+
].join('\n')
417+
}
418+
357419
/**
358420
* @summary Scores one synced issue as a current release / incident focus candidate.
359421
*
@@ -702,6 +764,15 @@ class GoldenPathSynthesizer extends Base {
702764

703765
// Pillar 2: Structural Weight from SQLite Graph
704766
const scoredNodes = [];
767+
const scoringStats = {
768+
semanticCandidates : semanticIds.length,
769+
sqliteOpenMatches : 0,
770+
blockedCandidates : 0,
771+
nonActionableCandidates: 0,
772+
scoredCandidates : 0,
773+
selectedTopNodes : 0,
774+
prunedGuideEdges : 0
775+
};
705776
const SEMANTIC_WEIGHT = 2.0;
706777
const STRUCTURAL_WEIGHT = 1.0;
707778

@@ -722,6 +793,7 @@ class GoldenPathSynthesizer extends Base {
722793
`);
723794

724795
const results = stmt.all(...semanticIds);
796+
scoringStats.sqliteOpenMatches = results.length;
725797

726798
for (const row of results) {
727799
const issueId = row.id;
@@ -741,7 +813,10 @@ class GoldenPathSynthesizer extends Base {
741813
}
742814
}
743815

744-
if (isBlocked) continue; // Architecturally blocked issues cannot be Golden
816+
if (isBlocked) {
817+
scoringStats.blockedCandidates++;
818+
continue; // Architecturally blocked issues cannot be Golden
819+
}
745820

746821
const idx = semanticIds.indexOf(issueId);
747822
const semantic_distance = parseFloat(semanticDistances[idx]) || 0.1;
@@ -756,6 +831,7 @@ class GoldenPathSynthesizer extends Base {
756831
let priority = (semanticScore * SEMANTIC_WEIGHT) + (struct_score * STRUCTURAL_WEIGHT);
757832

758833
if (!this.constructor.isActionableComputedRecommendation(nodeData || {id: issueId})) {
834+
scoringStats.nonActionableCandidates++;
759835
logger.debug(`[GoldenPathSynthesizer] Skipping non-actionable computed recommendation: ${issueId}`);
760836
continue;
761837
}
@@ -777,6 +853,11 @@ class GoldenPathSynthesizer extends Base {
777853
// Remove mathematically rejected targets (Negative ROI), then slice
778854
const topNodes = scoredNodes.filter(n => n.score > -5000).slice(0, aiConfig.goldenPathTopNodeRenderLimit);
779855
const goldenIds = new Set(topNodes.map(item => item.node.id));
856+
scoringStats.scoredCandidates = scoredNodes.length;
857+
scoringStats.selectedTopNodes = topNodes.length;
858+
scoringStats.prunedGuideEdges = this.constructor.pruneStaleFrontierGuideEdges({
859+
currentTargetIds: goldenIds
860+
});
780861

781862
let markdownAppend = '';
782863

@@ -834,6 +915,7 @@ DO NOT output markdown, \`\`\`json blocks, or any other explanations. Provide pu
834915
logger.warn('[GoldenPathSynthesizer] Failed to generate semantic interpretation for Golden Path (LLM Offline). Proceeding with pure mathematical output.', e);
835916
}
836917
} else {
918+
markdownAppend = this.constructor.renderComputedGoldenPathEmptySection(scoringStats);
837919
logger.info('[GoldenPathSynthesizer] No actionable unblocked issues found. Golden path empty.');
838920
}
839921

test/playwright/unit/ai/services/graph/GoldenPathSynthesizer.spec.mjs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,132 @@ test.describe('Neo.ai.daemons.services.GoldenPathSynthesizer', () => {
632632
expect(handoffContent).not.toContain(notReadyId);
633633
});
634634

635+
test('synthesizeGoldenPath renders empty computed diagnostics and clears stale frontier guides (#13828)', async () => {
636+
const originalGetGraphCollection = StorageRouter.getGraphCollection;
637+
const originalGetSummaryCollection = StorageRouter.getSummaryCollection;
638+
const originalEmbedText = TextEmbeddingService.embedText;
639+
const originalFetchOpenPRs = GoldenPathSynthesizer.fetchOpenPRs;
640+
const suffix = `${process.pid}-${Date.now()}`;
641+
const staleId = `issue-stale-guide-${suffix}`;
642+
const notReadyId = `issue-empty-not-ready-${suffix}`;
643+
aiConfig.vectorDimension = 2;
644+
645+
GraphService.upsertNode({
646+
id : 'frontier',
647+
type : 'SYSTEM_TENET',
648+
properties: {name: 'Active Context Frontier'}
649+
});
650+
GraphService.upsertNode({
651+
id : staleId,
652+
type : 'ISSUE',
653+
properties: {state: 'OPEN', title: 'Stale guide edge'}
654+
});
655+
GraphService.upsertNode({
656+
id : notReadyId,
657+
type : 'ISSUE',
658+
properties: {state: 'OPEN', title: 'Not ready candidate', labels: ['not-code-ready', 'ai']}
659+
});
660+
GoldenPathSynthesizer.constructor.pruneStaleFrontierGuideEdges();
661+
GraphService.linkNodes('frontier', staleId, 'GUIDES', 3);
662+
663+
StorageRouter.getGraphCollection = async () => ({
664+
query: async () => ({
665+
ids : [[notReadyId]],
666+
distances: [[0.1]]
667+
})
668+
});
669+
StorageRouter.getSummaryCollection = async () => ({get: async () => ({documents: ['Agent OS regression focus']})});
670+
TextEmbeddingService.embedText = async () => [0.1, 0.2];
671+
GoldenPathSynthesizer.fetchOpenPRs = async () => [];
672+
673+
try {
674+
await GoldenPathSynthesizer.synthesizeGoldenPath({repoEnrichmentEnabled: false});
675+
} finally {
676+
StorageRouter.getGraphCollection = originalGetGraphCollection;
677+
StorageRouter.getSummaryCollection = originalGetSummaryCollection;
678+
TextEmbeddingService.embedText = originalEmbedText;
679+
GoldenPathSynthesizer.fetchOpenPRs = originalFetchOpenPRs;
680+
}
681+
682+
const handoffContent = fs.readFileSync(tmpHandoffFile, 'utf-8');
683+
const guideTargets = GraphService.db.edges
684+
.getByIndex('source', 'frontier')
685+
.filter(edge => edge.type === 'GUIDES')
686+
.map(edge => edge.target);
687+
688+
expect(handoffContent).toContain('## Computed Golden Path (Strategic Recommendation)');
689+
expect(handoffContent).toContain('No actionable computed recommendations survived the current Tri-Vector filter pass.');
690+
expect(handoffContent).toContain('- Semantic candidates: 1');
691+
expect(handoffContent).toContain('- SQLite OPEN matches: 1');
692+
expect(handoffContent).toContain('- Non-actionable candidates filtered: 1');
693+
expect(handoffContent).toContain('- Selected top nodes: 0');
694+
expect(handoffContent).toMatch(/- Stale frontier GUIDES pruned: [1-9]\d*/);
695+
expect(guideTargets).not.toContain(staleId);
696+
});
697+
698+
test('synthesizeGoldenPath prunes stale guides while preserving current computed guides (#13828)', async () => {
699+
const originalGetGraphCollection = StorageRouter.getGraphCollection;
700+
const originalGetSummaryCollection = StorageRouter.getSummaryCollection;
701+
const originalEmbedText = TextEmbeddingService.embedText;
702+
const originalFetchOpenPRs = GoldenPathSynthesizer.fetchOpenPRs;
703+
const OpenAiCompatible = (await import('../../../../../../ai/provider/OpenAiCompatible.mjs')).default;
704+
const originalGenerate = OpenAiCompatible.prototype.generate;
705+
const suffix = `${process.pid}-${Date.now()}`;
706+
const staleId = `issue-stale-guide-nonzero-${suffix}`;
707+
const readyId = `issue-current-guide-${suffix}`;
708+
aiConfig.vectorDimension = 2;
709+
710+
GraphService.upsertNode({
711+
id : 'frontier',
712+
type : 'SYSTEM_TENET',
713+
properties: {name: 'Active Context Frontier'}
714+
});
715+
GraphService.upsertNode({
716+
id : staleId,
717+
type : 'ISSUE',
718+
properties: {state: 'OPEN', title: 'Old computed guide'}
719+
});
720+
GraphService.upsertNode({
721+
id : readyId,
722+
type : 'ISSUE',
723+
properties: {state: 'OPEN', title: 'Current computed guide', labels: ['bug', 'ai']}
724+
});
725+
GoldenPathSynthesizer.constructor.pruneStaleFrontierGuideEdges();
726+
GraphService.linkNodes('frontier', staleId, 'GUIDES', 3);
727+
728+
StorageRouter.getGraphCollection = async () => ({
729+
query: async () => ({
730+
ids : [[readyId]],
731+
distances: [[0.1]]
732+
})
733+
});
734+
StorageRouter.getSummaryCollection = async () => ({get: async () => ({documents: ['Agent OS regression focus']})});
735+
TextEmbeddingService.embedText = async () => [0.1, 0.2];
736+
GoldenPathSynthesizer.fetchOpenPRs = async () => [];
737+
OpenAiCompatible.prototype.generate = async () => ({content: '{"strategic_brief":"stub"}'});
738+
739+
try {
740+
await GoldenPathSynthesizer.synthesizeGoldenPath({repoEnrichmentEnabled: false});
741+
} finally {
742+
StorageRouter.getGraphCollection = originalGetGraphCollection;
743+
StorageRouter.getSummaryCollection = originalGetSummaryCollection;
744+
TextEmbeddingService.embedText = originalEmbedText;
745+
GoldenPathSynthesizer.fetchOpenPRs = originalFetchOpenPRs;
746+
OpenAiCompatible.prototype.generate = originalGenerate;
747+
}
748+
749+
const handoffContent = fs.readFileSync(tmpHandoffFile, 'utf-8');
750+
const guideTargets = GraphService.db.edges
751+
.getByIndex('source', 'frontier')
752+
.filter(edge => edge.type === 'GUIDES')
753+
.map(edge => edge.target);
754+
755+
expect(handoffContent).toContain(readyId);
756+
expect(handoffContent).not.toContain('No actionable computed recommendations survived');
757+
expect(guideTargets).toContain(readyId);
758+
expect(guideTargets).not.toContain(staleId);
759+
});
760+
635761
test('synthesizeGoldenPath lists the 5 most recent open PRs with cross-family status', async () => {
636762
const originalGetGraphCollection = StorageRouter.getGraphCollection;
637763
const originalGetSummaryCollection = StorageRouter.getSummaryCollection;

0 commit comments

Comments
 (0)