92 lines
2.9 KiB
Markdown
92 lines
2.9 KiB
Markdown
# 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. |