LearnNewsExamplesServices
Frontmatter
id15105
titleGolden Path replenishes candidates after liveness filtering
stateOpen
labels
bugaitestingregressionarchitecture
assigneesneo-gpt
createdAtJul 12, 2026, 9:55 PM
updatedAtJul 12, 2026, 10:37 PM
githubUrlhttps://github.com/neomjs/neo/issues/15105
authorneo-gpt
commentsCount4
parentIssue14472
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
milestonev13.2

Golden Path replenishes candidates after liveness filtering

Open Backlog/active-chunk-5 bugaitestingregressionarchitecture
neo-gpt
neo-gpt commented on Jul 12, 2026, 9:55 PM

Context

The 2026-07-12 19:11 UTC Golden Path pass is fresh but operationally wrong. Its Current Release / Incident Focus sees 55 live candidates, while the machine-consumed route recommends:

  1. issue-14788 — a v13.3 scoping wave;
  2. discussion-13846 — the target-architecture ADR;
  3. discussion-14561 — post-v13.1 goal scoping.

That is not a stale-file failure. It is a live candidate-admission failure during the v13.2 convergence window.

The first attempted repair, PR #15104, multiplied semantic relevance by raw age. It was correctly closed unmerged: it recreated the dropped #13793 recency anti-pattern, could not admit candidates outside Chroma's raw top 20, and made unrelated comment touches a ranking input.

The prior type-scope work remains valid. #13844 / PR #13837 made ISSUE + DISCUSSION the semantic population and intersected it with OPEN graph state. This ticket addresses the next boundary that work did not specify: filters run only after the fixed raw top-20 query, and OPEN is currently treated as sufficient proof that a Discussion is still convergence-to-drive.

Structure-map gate: npm run --silent ai:structure-map -- --files --loc passed on 2026-07-12. Owning substrate is ai/services/graph/, with source lifecycle projection owned by ai/services/github-workflow/ and graph persistence owned by ai/services/ingestion/.

The Problem

Two independent defects compose into one misleading route:

  1. Raw top-K is mistaken for the candidate contract. GoldenPathSynthesizer.synthesizeGoldenPath() queries exactly 20 ISSUE/DISCUSSION vectors, then applies SQLite OPEN state, blocker, label/actionability, and routing guards. If all 20 are rejected, a legitimate rank-21 candidate is invisible. Increasing K to another fixed number only moves the cliff.
  2. Discussion OPEN state is mistaken for routing liveness. computedGoldenPathRouting.isActionableComputedRecommendation() accepts any ISSUE or DISCUSSION without exclusion labels. A graduated, resolved, superseded, or dormant Discussion can therefore remain an immediate route merely because GitHub keeps it open for archaeology.

The source pipeline exposes enough material to classify this honestly, but no typed contract joins the layers:

  • DiscussionSyncer.#renderDiscussionMarkdown() persists createdAt, updatedAt, closed, closedAt, and the body/comments.
  • IssueIngestor.ingestDiscussionStates() persists only state, closed, closedAt, and category to the Native Edge Graph.
  • audit-discussion-lifecycle.mjs contains a partial regex classifier for graduated/resolved/stale states, but production routing cannot import a diagnostic script and must not fork another regex island.
  • Raw updatedAt is observability, not authority: an unrelated comment must not reactivate a route.

The Architectural Reality

ADR 0023 governs this exact boundary:

  • the route must be earned-and-forgetting;
  • structural scent decays;
  • Current Focus remains visibility-only;
  • no non-decaying recency/release/label boost may synthesize priority;
  • any PR changing candidate-pool composition/filter must cite the ADR.

ADR 0024 defines ISSUE/DISCUSSION as the current Golden Path ranking population. ADR 0033 keeps Direction-GP additive and fail-open; this leaf does not add a direction gate. ADR 0035 keeps lifecycle facts separate from canonical ranking and requires one Golden Path result for all consumers.

The smallest correct composition is therefore:

  • source-owned Discussion lifecycle facts — pure, typed, and reused;
  • graph-persisted projection — no route-time Markdown parsing;
  • adaptive candidate admission — enough admissible candidates for the render limit or verified corpus exhaustion;
  • unchanged ranking math2 × semantic + 1 × structural, with existing Hebbian reinforcement/decay ordering live candidates.

A Discussion source disposition is one of:

  • active: an explicit recognized convergence/revalidation signal exists;
  • terminal: graduated/resolved-with-no-open-scope/superseded archaeology;
  • undetermined: neither fact is established.

undetermined is not silently promoted by age or comment activity. It is routable only while current decaying structural support demonstrates live swarm motion; otherwise it is visibility-only dormant work. Classification failure never coerces a Discussion to active.

The Fix

One coherent PR:

  1. Add a pure shared source classifier at ai/services/github-workflow/shared/discussionRoutingDisposition.mjs. It owns marker recognition and returns the typed disposition plus machine-readable evidence/reason. DiscussionSyncer and the lifecycle diagnostic both consume it; no copied regex set remains.
  2. Persist the source projection in synced Discussion frontmatter, then carry it through IssueIngestor.ingestDiscussionStates() into Discussion graph properties. Route-time code reads typed graph facts, not Markdown.
  3. Add a pure graph-side routability composition that combines the source disposition with current decaying structural support. OPEN remains necessary; it is no longer sufficient for Discussions.
  4. Replace fixed raw-top-20-as-contract with bounded adaptive admission: widen/retrieve until the render limit has enough candidates that survive state, blocker, actionability, and Discussion-liveness filters, or the eligible ISSUE/DISCUSSION corpus is exhausted. Final scoring remains one merged pool with the existing formula.
  5. Emit Discussion-liveness rejections through the existing #15076 type-gate rejection ledger seam (new stage/bucket, no third ledger). Candidate-pool exhaustion stays in the synthesis stats. #15057 remains the owner of accumulated filter-disposition analysis.
  6. Record and execute the July-12 hindcast through the real sync → ingest → graph → Golden Path chain.

Intake revalidation fold

The implementation contract is narrowed by the independent intake falsifiers as follows:

  • Marker authority: only the trusted current Discussion root body can establish a source disposition. Comments and replies are diagnostic-only and never classify; truncation cannot alter routing. Instructional, historical, quoted, and fenced-code marker mentions are ignored. Tests include external-comment spoofing, truncated-comment absence, quoted markers, and fenced examples.
  • Decaying support: the graph layer exports the canonical frozen PROTECTED_EDGE_TYPES registry and owns one RLS-safe inbound-support primitive. It requires the root node, inbound source node, and edge to be visible; excludes BLOCKS; and separates total positive inbound weight from the positive decaying subset. An undetermined Discussion is live only when decayingWeight > 0 after current decay/pruning. Protected facts alone remain dormant.
  • Gate separation: actionability remains the existing type/label gate. Discussion liveness is a subsequent pure gate with buckets terminal and undetermined-no-decaying-support; a candidate is recorded only at the first gate that rejects it.
  • One physical, multi-stage rejection ledger: type-gate-rejection.jsonl remains the sole file/config surface. It gains discussion-liveness-gate alongside actionability-type-gate. Read/summarize/query default to the actionability stage so #15057's existing view is unchanged; an explicit stage selects liveness. Missing or unknown stages do not enter either stage-specific view.
  • Adaptive whole-prefix admission: query widths widen 20 → 40 → 80 → … with the same ISSUE/DISCUSSION filter. Every pass rebuilds the full positional id/distance prefix and all admission gates; no delta stability is assumed. Only the final successful attempt publishes stats or ledger rows. Stop when enough fully admitted candidates satisfy the render limit, or when returnedCount < requestedWidth proves exhaustion. Equality is not proof. A later query/mapping failure discards earlier partial attempts and uses the existing fail-loud surface.
  • Graph-proxy fact correction: the current graph collection is a plain CollectionProxy; StorageRouter.getGraphCollection() does not inject the 3× ANN/topology reranker used by memory, summary, and temporal-summary collections. The present defect therefore is the literal fixed raw top-20 graph query described above. A defensive _degraded check remains valid for compatible/future proxy envelopes, but current graph query failures throw. Tests cover first-pass and later-pass failure/degradation without false exhaustion.
  • Stats and math: record final-prefix semanticQueryPasses, requested width, Discussion-liveness rejection count, exhaustion boolean, and stop reason. Preserve exact distances—including zero—and keep 2 × semantic + 1 × structural unchanged.

Required falsifiers include rank 21 surviving 20 rejected neighbors; short-corpus exhaustion; later-pass failure with no partial healthy route or ledger writes; protected-only versus decaying support; root/source/edge RLS isolation; actionability/liveness stage isolation; and the real sync → ingest → graph → fresh-handoff hindcast.

Pre-Flight (structural fast-path): the new shared helper matches the pure, no-I/O sibling pattern of conversationTrust.mjs and contentPath.mjs; no novel directory or Architecture Overview map change is introduced.

Contract Ledger

Target surface Source of Authority Proposed behavior Fallback / failure behavior Docs Evidence
Synced Discussion lifecycle projection DiscussionSyncer.#renderDiscussionMarkdown() + shared pure classifier Emit routingDisposition: active | terminal | undetermined plus recognized evidence/reason; raw timestamps remain observability only Unknown/malformed marker set → undetermined; never active by error Anchor & Echo JSDoc on classifier and frontmatter field Table-driven marker corpus, including graduated-open archaeology
Discussion graph properties IssueIngestor.ingestDiscussionStates() Preserve the typed source disposition and evidence through SQLite + Chroma metadata Missing legacy field → undetermined; no body reparse in the graph layer Ingestor JSDoc + schema comment Real DiscussionSyncer → IssueIngestor → graph round-trip
Discussion route eligibility computedGoldenPathRouting OPEN + active is eligible; terminal is visibility-only; undetermined requires current decaying structural support Classifier/metadata degradation cannot fabricate active convergence Routing contract JSDoc Dormant, evergreen/revalidated, comment-touch, terminal fixtures
Candidate admission GoldenPathSynthesizer.synthesizeGoldenPath() Adaptively replenish after all admission filters until render limit or corpus exhaustion Query/graph failure keeps existing fail-loud outcome; no partial “healthy” route Synthesizer JSDoc + stats names 20 rejected semantic neighbors + legitimate rank-21 candidate
Final priority ADR 0023 + existing synthesizer constants Preserve 2 × semantic + 1 × structural; one merged ISSUE/DISCUSSION pool No recency, release, milestone, Current Focus, or ID boost Existing ADR citations Equal-live-candidate reinforcement/decay ordering test
Rejection evidence Existing typeGateRejectionLedgerStore from #15076 Record Discussion-liveness rejection under a distinct stage/bucket in the existing sibling ledger Write failure remains fail-open observability; no new ledger or config leaf Store/recorder JSDoc Hermetic separation + query test; feeds #15057's accumulated disposition window
Lifecycle audit ai/scripts/diagnostics/audit-discussion-lifecycle.mjs Import the shared classifier; audit-specific age reporting stays diagnostic only No production import from ai/scripts/ Script JSDoc Existing audit suite plus shared-classifier parity test

Decision Record impact

Aligned with ADR 0023, ADR 0024, ADR 0033, and ADR 0035. No amendment. The ticket adds no node/edge class, protected edge, ranking formula, second scorer, or lifecycle authority. It repairs candidate admission and projects source-owned lifecycle facts into the existing graph/ranking boundary.

Decision Record

Not needed. Existing accepted/graduated architecture already decides the relevant invariants; this is one executable repair leaf under Epic #14472.

Acceptance Criteria

  • A single pure Discussion routing-disposition classifier is consumed by sync, ingestion-facing projection, and the lifecycle diagnostic; no second marker/regex authority exists.
  • Synced frontmatter → IssueIngestor → SQLite/Chroma round-trip proves every routing-liveness field read by Golden Path actually exists.
  • A legitimate candidate ranked beyond 20 rejected semantic neighbors is admitted and rendered; widening stops when the render limit is satisfied or the eligible corpus is exhausted.
  • Dormant OPEN Discussion is excluded from immediate routing.
  • Equally old explicit evergreen/revalidated Discussion remains eligible.
  • Unrelated comment/raw updatedAt touch cannot reactivate a Discussion.
  • Graduated/resolved/superseded-but-open archaeology, including the discussion-10289 shape, is excluded.
  • Hebbian reinforcement/decay still reorders equally live candidates; 2 × semantic + 1 × structural is byte-for-byte unchanged.
  • Current Focus causes no admission or score boost; no release/milestone literal or issue ID enters production ranking logic.
  • The existing #15076 type-gate rejection ledger distinguishes Discussion-liveness rejection from label/actionability rejection; synthesis stats separately prove verified corpus exhaustion. No new ledger/config leaf is introduced.
  • The recorded 2026-07-12 hindcast routes at least one legitimate current option, excludes the dormant/archaeology cohort, remains non-empty, and rejects CLOSED/non-actionable nodes.
  • Evidence lands at both layers: focused unit tests for pure contracts and a local L3 sync → ingest → graph → fresh-handoff run with the selected nodes recorded.

Out of Scope

Recency multipliers · raw-age cutoffs · milestone/release boosts · hardcoded v13.2 identifiers · Current Focus promotion · Direction-GP weighting changes · Bird Views · hook rendering · a general lifecycle service · reopening #13844 · a new rejection ledger · #15057's accumulated keep/narrow/rank-with-discount decision · Fable review.

Avoided Traps

  • Reopen #13844: rejected. That leaf correctly delivered type scope + OPEN intersection; retroactively expanding it hides the new policy boundary.
  • Recency relevance (#13793 / PR #15104): rejected. It cannot recover rank-21 candidates, fights decay, and is comment-touch gameable.
  • A larger fixed K: rejected. It moves rather than removes the admission cliff.
  • Current Focus/release boost: rejected. Visibility is not earned-scent routing.
  • Route-time Markdown parsing: rejected. Lifecycle is projected once at the source and consumed as typed graph facts.
  • Second lifecycle regex island: rejected. The diagnostic and production projection share one pure classifier.

Related

Parent: #14472

Predecessors / evidence: #13750 · #13844 · #13837 · #13801

Diagnostics: #15057 · #15076

Anti-precedents: #13793 · #15104

Composes with: #14565 · #15100

Architecture: ADR 0023 · ADR 0024 · ADR 0033 · ADR 0035

Origin Session ID: 837ad74b-c2d2-413d-9aab-b7165a93a82a

Retrieval Hint: query_raw_memories("July 12 Golden Path candidate admission Discussion liveness top-20 #15104 #13844")

Retrieval Hint: query_summaries("Golden Path fresh but wrong v13.3 discussion archaeology route July 12")

Live duplicate sweep: latest 30 open GitHub issues + latest 30 all-state A2A messages re-checked at 2026-07-12T19:55:34.890Z; no equivalent filed or in-flight. Semantic KB sweep found predecessors #13750/#13844 and adjacent ledger #15057/#15076, but no ticket owning adaptive post-filter replenishment plus typed Discussion routing liveness. Exact active+archive corpus sweep found no equivalent.