Skip to content

Commit 0df82f0

Browse files
neo-opus-adatobiu
andauthored
feat(graph): computed GP never-empty actionable focus-as-route + release-window config leaf (#15066) (#15058)
* feat(graph): computed GP no-survivor focus-as-route fallback — never empty (#14609) Behavior 1 of the GP-guard follow-up split (parent defect merged via PR #14596): when the contradiction guard blocks EVERY computed candidate (the no-survivor state — the operator-verified zero-route class), the section now surfaces the live Current Release / Incident Focus items as the numbered '**issue-N**:' recommendation instead of rendering zero routes. parseGoldenPath then routes the incident/release work rather than leaving the agent with no route. - renderComputedGoldenPathContradictionSection (reached only when routedTopNodes is empty) renders the focus candidates as numbered routes; the blocked content stays in the diagnostic filtered line, never routed. The 'does not boost focus into routing' clause gains its documented no-survivor exception — focus becomes the route ONLY when nothing else survives, never alongside a surviving candidate. - Title-less focus candidates fall back to their reasons label. Tests: 2 new routing unit cases + the GoldenPathSynthesizer integration assertion updated to the never-empty behavior. 9/9 routing + 47/47 synthesizer specs green. Remaining behaviors (separate commits): guard-filter ledger recording + the release-window config leaf. * feat(graph): release-window config leaf — GP focus tracks the current release, not a shipped one (#14609) Behavior 3 of the GP-guard follow-up split: the 'current release focus' version was hardcoded as v13.1 in issueFocusSections.mjs, so a SHIPPED release kept scoring as current focus (the operator-verified release-gate-blind class). Now sourced from a declarative AiConfig leaf (ADR-0019 sanctioned form), read at the use site. - ai/config.template.mjs: currentReleaseVersion leaf (default 'v13.2', NEO_CURRENT_RELEASE) — advanced by publish.mjs at release (follow-on half of this behavior). - issueFocusSections.mjs: reads aiConfig.currentReleaseVersion at the use site (never a stale module-load capture), builds the title matcher regex-escaped, emits the release as the focus reason. No release-version literal remains (AC falsifier: grep 'v13.1|V13_1_PATTERN' -> zero). - Tests made release-agnostic: fixtures read aiConfig.currentReleaseVersion so they never re-stale. 47/47 GoldenPathSynthesizer + 9/9 routing specs green. config.mjs is gitignored; only the template leaf is committed. Remaining behavior: guard-filter ledger recording (behavior 2). * fix(graph): no-survivor fallback routes only ACTIONABLE focus, epic→diagnostic (#14609) Cycle-2 RA-2 (@neo-gpt review on #15058). The behavior-1 never-empty fallback rendered EVERY live Current Focus candidate as a numbered machine route — including epic umbrellas and not-code-ready items, which an agent would then try to route at. Now the fallback filters focus candidates through the SAME isActionableComputedRecom- mendation authority the computed surface uses (not a divergent label list): only actionable leaves render as numbered routes, bounded to the caller-supplied Golden Path render limit (aiConfig.goldenPathTopNodeRenderLimit, read at the synthesizer use site). Epic-only / no-actionable focus renders diagnostically (named, never a numbered route) rather than lying that an umbrella is immediate work. 3 new exact cases: epic+actionable-leaf (leaf routes, epic doesn't), epic-only (zero routes, diagnostic), noisy-set (bounded to render limit). 59 passed / 0 failed. --------- Co-authored-by: tobiu <tobiasuhlig78@gmail.com>
1 parent c718c0c commit 0df82f0

6 files changed

Lines changed: 230 additions & 80 deletions

File tree

ai/config.template.mjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ class Config extends ConfigProvider {
3939
data: {
4040
neoRootDir : leaf(neoRootDir),
4141
projectRoot: leaf(projectRoot),
42+
/**
43+
* The current in-flight release version whose milestone / epic work counts as "current
44+
* release focus" for the Golden Path emitter. Set at cut-prep, advanced by
45+
* `buildScripts/release/publish.mjs` at release — so a shipped release never lingers as
46+
* current focus (the release-gate-blind class). Consumers read it at the use site; never
47+
* a hardcoded release literal.
48+
* @type {string}
49+
*/
50+
currentReleaseVersion: leaf('v13.2', 'NEO_CURRENT_RELEASE', 'string'),
4251
/**
4352
* Universal JSONL backup/export directory for Agent OS databases.
4453
* @type {string}

ai/services/graph/GoldenPathSynthesizer.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,6 +1110,7 @@ DO NOT output markdown, \`\`\`json blocks, or any other explanations. Provide pu
11101110
markdownAppend = this.constructor.renderComputedGoldenPathContradictionSection({
11111111
capturedAt : handoffTimestamp,
11121112
contradiction: focusContradiction,
1113+
renderLimit : aiConfig.goldenPathTopNodeRenderLimit,
11131114
stats : scoringStats
11141115
});
11151116
logger.info('[GoldenPathSynthesizer] Computed route contradicted Current Focus; rendered diagnostic instead of routing content work.');

ai/services/graph/computedGoldenPathRouting.mjs

Lines changed: 64 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -161,39 +161,84 @@ export function findComputedFocusContradiction({
161161
}
162162

163163
/**
164-
* @summary Renders the computed-route contradiction diagnostic.
164+
* @summary Renders the no-survivor contradiction fallback — the never-empty focus-as-route surface.
165165
*
166-
* The section intentionally contains no numbered `**issue-N**:` entries, so
167-
* `AgentOrchestrator.parseGoldenPath()` will not treat filtered content work
168-
* as an immediate route.
166+
* Reached only in the no-survivor state: EVERY computed content/narrative candidate contradicted live
167+
* Current Release / Incident Focus, so zero computed candidates survived the guard (the caller routes
168+
* surviving computed candidates directly and never calls this while any survive). Rather than emit zero
169+
* routes, the section surfaces the live Current Focus items as the numbered `**issue-N**:` recommendation
170+
* so `AgentOrchestrator.parseGoldenPath()` routes the incident/release work instead of leaving the agent
171+
* with no route. The BLOCKED content is never routed — it appears only in the diagnostic
172+
* filtered-candidates line. This is the explicit no-survivor exception to the "does not boost focus
173+
* nodes into routing" clause: focus becomes the route only when nothing else survives, never alongside
174+
* a surviving computed candidate.
169175
*
170176
* @param {Object} options
171-
* @param {Object} options.contradiction Result from `findComputedFocusContradiction`.
177+
* @param {Object} options.contradiction Result from `findComputedFocusContradiction` (carries the focus
178+
* candidates surfaced as the route + the blocked computed candidates).
172179
* @param {Object} [options.stats={}] Candidate-count diagnostics for the current pass.
173180
* @param {Date|String} [options.capturedAt=new Date()] Current pass capture timestamp.
174181
* @returns {String} Markdown section.
175182
*/
176183
export function renderComputedGoldenPathContradictionSection({
177184
contradiction,
178-
stats = {},
179-
capturedAt = new Date()
185+
stats = {},
186+
capturedAt = new Date(),
187+
renderLimit
180188
} = {}) {
181-
const count = value => Number.isFinite(Number(value)) ? Number(value) : 0;
182-
const focusRefs = (contradiction?.focusCandidates || [])
183-
.slice(0, 3)
184-
.map(candidate => `#${candidate.number}`)
185-
.join(', ') || 'none';
189+
const count = value => Number.isFinite(Number(value)) ? Number(value) : 0;
190+
const focusItems = contradiction?.focusCandidates || [];
191+
const focusRefs = focusItems.slice(0, 3).map(candidate => `#${candidate.number}`).join(', ') || 'none';
186192
const blockedRefs = (contradiction?.blockedNodes || [])
187193
.map(item => item.node.id)
188194
.join(', ') || 'none';
189195

190-
return [
196+
// Route ONLY focus candidates that pass the SAME actionability authority the computed surface uses —
197+
// an epic umbrella / not-code-ready item is visibility, never an immediate machine route. Reusing
198+
// isActionableComputedRecommendation (not a divergent label list) keeps a single authority.
199+
const routableFocus = focusItems.filter(candidate => isActionableComputedRecommendation({
200+
id : `issue-${candidate.number}`,
201+
type : 'ISSUE',
202+
properties: {labels: candidate.labels, title: candidate.title}
203+
}));
204+
// Bound to the caller's Golden Path render limit — no hidden default: unbounded only when the caller
205+
// supplies none (the synthesizer passes aiConfig.goldenPathTopNodeRenderLimit).
206+
const routedFocus = Number.isFinite(renderLimit) ? routableFocus.slice(0, renderLimit) : routableFocus;
207+
208+
const lines = [
191209
'',
192210
'## Computed Golden Path (Strategic Recommendation)',
193211
'',
194212
`Captured at: ${formatGoldenPathTimestamp(capturedAt)}`,
195-
'',
196-
'Computed routing paused because the surviving content/narrative recommendation contradicts live Current Release / Incident Focus.',
213+
''
214+
];
215+
216+
if (routedFocus.length > 0) {
217+
// No-survivor focus-as-route fallback: render the live ACTIONABLE Current Focus items as the
218+
// numbered route so the pass is never empty (the zero-route class). Blocked computed content is
219+
// NOT routed — it stays in the diagnostic filtered line below.
220+
lines.push(
221+
'Every surviving computed content/narrative candidate contradicted live Current Release / Incident Focus, so no computed candidate survived the guard. The live actionable focus work is surfaced as the immediate route instead of an empty pass:',
222+
''
223+
);
224+
225+
routedFocus.forEach((candidate, index) => {
226+
const label = candidate.title || candidate.name ||
227+
(Array.isArray(candidate.reasons) ? candidate.reasons.join(', ') : 'Current Release / Incident Focus');
228+
229+
lines.push(`${index + 1}. **issue-${candidate.number}**: Current Release / Incident Focus (${label})`)
230+
});
231+
} else {
232+
// Epic-only / no-actionable-focus: the honest state is NO immediate route — an epic umbrella or
233+
// not-code-ready item is not immediate machine work. Render the focus diagnostically (named, never
234+
// as a numbered route) rather than lying that an umbrella is the immediate route.
235+
lines.push(
236+
'No computed candidate survived the contradiction guard, and the live Current Release / Incident Focus is visibility-only this pass (epic umbrella / not-code-ready) — there is no actionable immediate computed route. The focus context is surfaced diagnostically below, NOT as a machine route.',
237+
''
238+
);
239+
}
240+
241+
lines.push(
197242
'',
198243
`- Active incident/release focus candidates: ${focusRefs}`,
199244
`- Contradictory computed candidates filtered: ${blockedRefs}`,
@@ -203,9 +248,11 @@ export function renderComputedGoldenPathContradictionSection({
203248
`- Selected routed nodes: ${count(stats.selectedTopNodes)}`,
204249
`- Stale frontier GUIDES pruned: ${count(stats.prunedGuideEdges)}`,
205250
'',
206-
'No numbered immediate recommendation is rendered for this pass; use the Current Release / Incident Focus section for visibility and rerun after the incident/release focus clears or the computed route aligns.',
251+
'> **Routing Guard:** any numbered items above are the live ACTIONABLE Current Release / Incident Focus, surfaced as the route ONLY because zero computed candidate survived the contradiction filter; epic / visibility-only focus is never rendered as a route. Focus is never boosted into routing while a computed candidate survives — that state routes the computed candidate directly.',
207252
''
208-
].join('\n')
253+
);
254+
255+
return lines.join('\n')
209256
}
210257

211258
/**

0 commit comments

Comments
 (0)