# Experiment 60B.42 — Active selector terminal-status guard **Date:** 2026-08-14 **Branch:** `feature/active-selector-terminal-guard-v0.37` ## Purpose Implement the narrow selector-only fix established by 60B.41 so `selectActiveUnknownCandidate(...)` never scores or returns terminal-status unknown nodes. ## 60B.41 diagnosis 60B.41 confirmed that `selectActiveUnknownCandidate(graph, resolvedNodeIds)` filtered candidates using only: - `kind === "unknown"` - `!resolvedNodeIds.includes(node.id)` It applied **no status filter at all**. Because `scoreUnknownCandidate(...)` also ignores node status, nodes with: - `status = known` - `status = resolved` - `status = contradicted` could enter scoring whenever their IDs were absent from `resolvedNodeIds`. That meant the active-selector fallback path could still select terminal nodes, including the exact known-decision stale-target risk seen in the 60B.37 lifecycle. ## Exact selector filter change Changed only the candidate filter inside `selectActiveUnknownCandidate(...)` in `lib/graph/utils.js`. Before: ```js (n) => n.kind === "unknown" && !resolvedNodeIds.includes(n.id) ``` After: ```js (n) => n.kind === "unknown" && !["known", "resolved", "contradicted"].includes(n.status) && !resolvedNodeIds.includes(n.id) ``` No scoring weights, ordering rules, prerequisite logic, or other eligibility predicates were changed. ## Terminal-state tests Added focused tests in `tests/graph/apply-proposal.test.js` under: - `60B.42 — active selector terminal-status guard` Covered cases: 1. known node excluded when a genuine unresolved node exists 2. known-only graph returns `null` 3. resolved node excluded even when absent from supplied `resolvedNodeIds` 4. contradicted node excluded even when absent from supplied `resolvedNodeIds` 5. unresolved ranking remains unchanged when both candidates are genuinely unresolved ## Unresolved-ranking preservation The selector still chooses the same higher-priority unresolved candidate when both candidates remain valid (`status = unknown`). This confirms the change acts only as a pre-scoring terminal-state gate and does not alter ranking semantics. ## 60B.11 / pricing preservation The same focused run preserved: - 60B.11 preferred-target behaviour - prerequisite-first behaviour - pricing regression selecting `n_commercial_value` instead of downstream `n_pricing` ## Validation Command run: ```bash npx vitest run tests/graph/apply-proposal.test.js -t "60B.42|60B.11|replaces downstream pricing" ``` Result: - PASS — `16 passed | 71 skipped` ## Remaining boundary This experiment does **not** solve the broader duplicated eligibility problem. Other post-mutation eligibility checks still exist elsewhere and remain unchanged in this task. This selector guard closes one specific fallback risk, but the broader shared-eligibility cleanup still remains to be handled separately before declaring the 60B.37 stale-question lifecycle fully fixed.