From 9b0e9571d11875f8ee280bcd8df7e5b2c9c60623 Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Thu, 30 Apr 2026 07:06:36 +0000 Subject: [PATCH] docs: mark Phase 6 complete in resreq-ast-cse-impl-plan --- src/docs/resreq-ast-cse-impl-plan.md | 119 +++++++++++++++------------ 1 file changed, 66 insertions(+), 53 deletions(-) diff --git a/src/docs/resreq-ast-cse-impl-plan.md b/src/docs/resreq-ast-cse-impl-plan.md index 6b927cd..d130d95 100644 --- a/src/docs/resreq-ast-cse-impl-plan.md +++ b/src/docs/resreq-ast-cse-impl-plan.md @@ -14,12 +14,13 @@ Phase 1: TDD test skeletons (parallel with Phase 3) ✓ DONE Phase 2: Header definitions + ABI audit ✓ DONE Phase 3: PEG grammar (parallel with Phase 1) ✓ DONE 143 PASS / 3 FAIL (stubs) Phase 4a: Core interning + sel_optimize_level ✓ DONE 145 PASS / 1 FAIL (Phase 4b stub) -Phase 4b: Type validation (gated on P0-1) ← CURRENT -Phase 4c/4d: Canonicalization + const-fold -Phase 4e: Memoized eval -Phase 4f: GC + recompaction -Phase 5: Build wiring -Phase 6: Scheduler integration (after all unit tests pass) +Phase 4b: Type validation (gated on P0-1) ✓ DONE 203 PASS (2026-04-29) +Phase 4c: AND/OR associativity flattening ✓ DONE 157 PASS (2026-04-29) +Phase 4d: Const-fold transform ✓ DONE (2026-04-29) +Phase 4e: Memoized eval ✓ DONE 73 PASS (2026-04-29) +Phase 4f: GC + recompaction ✓ DONE 73 PASS (2026-04-29) +Phase 5: Build wiring ✓ DONE +Phase 6: Scheduler integration ✓ DONE 10 PASS (2026-04-30) ``` --- @@ -113,67 +114,76 @@ Develop concurrently with Phase 3 (grammar is the test oracle). --- -## Phase 4b — Parse-Time Type Validation +## Phase 4b — Parse-Time Type Validation ✓ DONE (2026-04-29) **Priority: HIGH — gate: P0-1 `makeString()` fix must be merged first** | # | Task | Notes | |---|------|-------| -| 4b-1 | Wire `parse_select()` into `parse_resreq_primary` **before** `kill_resreq_data` | Use-after-free if called after `kill_resreq_data` frees `data.selectString` | -| 4b-2 | Implement EQ/NE type validation: reject BOOL×STR; coerce SEL_NUM to SEL_STR when comparing with string resource (`snprintf "%.17g"`), NULL-check `makeString` result, symmetric coercion | Free orphaned SEL_NUM node in unoptimized mode before overwriting pointer | -| 4b-3 | Implement `>/<`/`>=`/`<=` type validation: reject non-numeric operands | `log_fatal` (not `assert`) for belt-and-suspenders at eval time too | -| 4b-4 | Implement `res_idx` validity guard and unknown-resource error; add migration diagnostic for `true`/`false` identifiers (parse as SEL_RESNAME with no match) | | -| 4b-5 | Update `kill_primary_resreq` for conditional teardown: CSE-enabled → null root only; CSE-disabled → `free_select_tree()` then `FREEUP` | NULL store before `FREEUP` to prevent GC mark dangling-pointer race | -| 4b-6 | **LC_NUMERIC enforcement**: wrap PEG parser call in `parse_select` with `newlocale(LC_NUMERIC_MASK, "C")` / `uselocale` / `freelocale`; `log_fatal` on failure in scheduler, `log_error`+return in API server | `_XOPEN_SOURCE 700` required before system headers | -| 4b-7 | **strtod locale coverage check**: `grep -n 'strtod\|atof\|strtof' build/lib/xce_select.c` — if packcc inserts `strtod` in scanner, use `%0` capture + explicit `strtod` inside PEG action | Document finding (matches or no-matches) as a confirmed safe/unsafe result | +| 4b-1 | ~~Wire `parse_select()` into `parse_resreq_primary` **before** `kill_resreq_data`~~ **DONE** | Use-after-free if called after `kill_resreq_data` frees `data.selectString` | +| 4b-2 | ~~Implement EQ/NE type validation: reject BOOL×STR; coerce SEL_NUM to SEL_STR when comparing with string resource (`snprintf "%.17g"`), NULL-check `makeString` result, symmetric coercion~~ **DONE** | Free orphaned SEL_NUM node in unoptimized mode before overwriting pointer | +| 4b-3 | ~~Implement `>/<`/`>=`/`<=` type validation: reject non-numeric operands~~ **DONE** | `log_fatal` (not `assert`) for belt-and-suspenders at eval time too | +| 4b-4 | ~~Implement `res_idx` validity guard and unknown-resource error; add migration diagnostic for `true`/`false` identifiers (parse as SEL_RESNAME with no match)~~ **DONE** | | +| 4b-5 | ~~Update `kill_primary_resreq` for conditional teardown: CSE-enabled → null root only; CSE-disabled → `free_select_tree()` then `FREEUP`~~ **DONE** | NULL store before `FREEUP` to prevent GC mark dangling-pointer race | +| 4b-6 | ~~**LC_NUMERIC enforcement**: wrap PEG parser call in `parse_select` with `newlocale(LC_NUMERIC_MASK, "C")` / `uselocale` / `freelocale`; `log_fatal` on failure in scheduler, `log_error`+return in API server~~ **DONE** | `_XOPEN_SOURCE 700` required before system headers | +| 4b-7 | ~~**strtod locale coverage check**~~ **DONE — SAFE**: packcc emits single `strtod` in `pcc_action_value_node_1` (line 1593 of generated file), inside the `uselocale` scope — no locale vulnerability | | +| 4b-R1 | ~~**[Review fix — HIGH]** Add `lock(auxil->resmgr, RTTI_LOCK_READ)` / `unlock(auxil->resmgr)` around `key_find` in `resolve_resname()` (`xce_select.peg:38`) — missing read lock caused data race in multi-threaded scheduler~~ **DONE** | | +| 4b-R2 | **[Review check — F2]** STRING×NUM coercion error paths reviewed: `makeSelectNode` releases the xstring on OOM failure (level-0 path calls `dereference(op1)` before returning NULL); outer `makeSelectNode(op_type)` failure correctly calls `SEL_DISCARD(l)` + `SEL_DISCARD(r)` — **no leak, false positive** | | --- -## Phase 4c — Commutativity + Associativity Flattening +## Phase 4c — Commutativity + Associativity Flattening ✓ DONE (2026-04-29) | # | Task | Notes | |---|------|-------| -| 4c-1 | Verify commutativity sort in `makeSelectNode` step 1 covers AND/OR/EQ/NE; non-commutative GT/LT/GE/LE/NOT unchanged | Already integrated in §5.4 design | -| 4c-2 | Implement post-parse **flatten pass** for `&&`/`||` chains: `do { changed = flatten_pass(root); } while (changed)` | Repeat-until-stable needed for depth-3+ explicit parenthesization | -| 4c-3 | In unoptimized mode: `flatten_pass` must `free()` each orphaned node — not its children (re-parented children must not be freed) | Children freed would cause double-free when `free_select_tree` walks from root | +| 4c-1 | ~~Verify commutativity sort in `makeSelectNode` step 1 covers AND/OR/EQ/NE; non-commutative GT/LT/GE/LE/NOT unchanged~~ **DONE** — `test_cse_gt_not_commutative` confirms GT is not commutative; AND/OR/EQ/NE sort verified. | Already integrated in §5.4 design | +| 4c-2 | ~~Implement post-parse **flatten pass** for `&&`/`||` chains: `do { changed = flatten_pass(root); } while (changed)`~~ **DONE** — `sel_flatten_pass` added to `xce_select.c`; do-while loop in `parse_select()` in `resreq.c`; 7 tests: noop, single-level, deep 3+, OR, mixed-ops, CSE mode. 157 PASS. | Repeat-until-stable needed for depth-3+ explicit parenthesization | +| 4c-3 | ~~In unoptimized mode: `flatten_pass` must `free()` each orphaned node — not its children (re-parented children must not be freed)~~ **DONE** — level==0 path frees orphaned intermediate node (`free(new_right); free(node)`) after re-parenting A/B/C; SEL_NOT branch frees `node` only. | Children freed would cause double-free when `free_select_tree` walks from root | + +**Senior review fixes applied (2026-04-29):** +- **[MEDIUM — OOM safety]** In AND/OR branch: update `node->u.binop.left/right` in-place (level==0) immediately after recursive calls, before any `makeSelectNode`. This ensures `return node` after OOM returns a valid tree with live (not dangling) children. +- **[LOW — dead code]** Removed dead `if (node->type == SEL_STR ...)` / `if (new_right->type == SEL_STR ...)` guards in AND/OR and SEL_NOT branches — those nodes are never SEL_STR in those arms. --- -## Phase 4d — Const-Fold Transform +## Phase 4d — Const-Fold Transform ✓ DONE (2026-04-29) | # | Task | Notes | |---|------|-------| -| 4d-1 | `resname == any` → `SEL_TRUE`; `resname != any` → `SEL_FALSE` — check **both** child slots for SEL_ANY pointer (commutativity sort is ASLR-nondeterministic across runs) | | -| 4d-2 | Identity laws: `SEL_TRUE && X` → `X`; `SEL_FALSE \|\| X` → `X` | | -| 4d-3 | Annihilation laws: `SEL_FALSE && X` → `SEL_FALSE`; `SEL_TRUE \|\| X` → `SEL_TRUE` | | -| 4d-4 | Complement laws: `!SEL_TRUE` → `SEL_FALSE`; `!SEL_FALSE` → `SEL_TRUE` | Single-pass — const-fold shrinks trees, does not create new flattening opportunities | +| 4d-1 | ~~`resname == any` → `SEL_TRUE`; `resname != any` → `SEL_FALSE` — check **both** child slots for SEL_ANY pointer (commutativity sort is ASLR-nondeterministic across runs)~~ **DONE** | | +| 4d-2 | ~~Identity laws: `SEL_TRUE && X` → `X`; `SEL_FALSE \|\| X` → `X`~~ **DONE** | | +| 4d-3 | ~~Annihilation laws: `SEL_FALSE && X` → `SEL_FALSE`; `SEL_TRUE \|\| X` → `SEL_TRUE`~~ **DONE** | | +| 4d-4 | ~~Complement laws: `!SEL_TRUE` → `SEL_FALSE`; `!SEL_FALSE` → `SEL_TRUE`~~ **DONE** | Single-pass — const-fold shrinks trees, does not create new flattening opportunities | --- -## Phase 4e — Evaluation with Memoization +## Phase 4e — Evaluation with Memoization ✓ DONE (2026-04-29) | # | Task | Notes | |---|------|-------| -| 4e-1 | Implement `eval_get_bool`/`eval_set_bool` 2-bit bitmap accessors with `node_id >= bool_bits_cap` guards using `log_fatal` | `_Static_assert(EVAL_NONE == 0)` for memset-reset correctness | -| 4e-2 | Implement `eval_select_inner` — recursive eval with **explicit** error propagation (`if (lv < 0) return -1`), NOT C `&&`/`||` (C's `-1` is truthy, would swallow errors) | SEL_ANY reaching eval → `log_fatal("SEL_ANY reached eval — const-fold not applied")` | -| 4e-3 | Leaf cache writes in `eval_select_inner`: SEL_RESNAME writes to `ctx->num_vals[node_id]` or `ctx->str_vals[node_id]`; comparison ops call `eval_select(child, ctx)` then read cached arrays | **Critical**: `get_leaf_value()` must NOT be called directly — bypasses cache writes, silently reads 0.0/NULL | -| 4e-4 | Implement memoized `eval_select` wrapper — bitmap cache check before calling `eval_select_inner`; cache EVAL_ERROR too (node-structural errors repeat per host) | | -| 4e-5 | Implement `eval_select_simple` for unoptimized path — plain recursive tree-walk, no `select_eval_ctx_t`, thread-safe (no shared state) | | -| 4e-6 | Add `eval_select` guard: `log_fatal` if `sel_get_optimize_level() <= 0` | | -| 4e-7 | Implement `select_eval_ctx_init` / `select_eval_ctx_reset` / `select_eval_ctx_fini` | `node_count` snapshot at init; `reset` uses snapshot, never re-reads `sel_node_next_id` | -| 4e-8 | Pre-session capacity check: `memset` extended bool_bits bytes after realloc (realloc does not zero extension); partial-OOM contract — all three reallocs (bool_bits, num_vals, str_vals) must succeed atomically or `log_fatal` | Mismatched capacities allow out-of-bounds write to num_vals with no inline bounds check | +| 4e-1 | ~~Implement `eval_get_bool`/`eval_set_bool` 2-bit bitmap accessors with `node_id >= bool_bits_cap` guards using `log_fatal`~~ **DONE** | `_Static_assert(EVAL_NONE == 0)` for memset-reset correctness | +| 4e-2 | ~~Implement `eval_select_inner` — recursive eval with **explicit** error propagation (`if (lv < 0) return -1`), NOT C `&&`/`||` (C's `-1` is truthy, would swallow errors)~~ **DONE** | SEL_ANY reaching eval → `log_fatal("SEL_ANY reached eval — const-fold not applied")` | +| 4e-3 | ~~Leaf cache writes in `eval_select_inner`: SEL_RESNAME writes to `ctx->num_vals[node_id]` or `ctx->str_vals[node_id]`; comparison ops call `eval_select(child, ctx)` then read cached arrays~~ **DONE** | **Critical**: `get_leaf_value()` must NOT be called directly — bypasses cache writes, silently reads 0.0/NULL | +| 4e-4 | ~~Implement memoized `eval_select` wrapper — bitmap cache check before calling `eval_select_inner`; cache EVAL_ERROR too (node-structural errors repeat per host)~~ **DONE** | | +| 4e-5 | ~~Implement `eval_select_simple` for unoptimized path — plain recursive tree-walk, no `select_eval_ctx_t`, thread-safe (no shared state)~~ **DONE** | | +| 4e-6 | ~~Add `eval_select` guard: `log_fatal` if `sel_get_optimize_level() <= 0`~~ **DONE** | | +| 4e-7 | ~~Implement `select_eval_ctx_init` / `select_eval_ctx_reset` / `select_eval_ctx_fini`~~ **DONE** | `node_count` snapshot at init; `reset` uses snapshot, never re-reads `sel_node_next_id` | +| 4e-8 | ~~Pre-session capacity check: `memset` extended bool_bits bytes after realloc (realloc does not zero extension); partial-OOM contract — all three reallocs (bool_bits, num_vals, str_vals) must succeed atomically or `log_fatal`~~ **DONE** | Mismatched capacities allow out-of-bounds write to num_vals with no inline bounds check | --- -## Phase 4f — Mark-and-Sweep GC + Recompaction +## Phase 4f — Mark-and-Sweep GC + Recompaction ✓ DONE (2026-04-29) | # | Task | Notes | |---|------|-------| -| 4f-1 | Implement `gc_mark_tree` — recursive DFS, early return on `node == NULL \|\| node->gc_mark`; handles leaf / SEL_NOT / binary node types | Recursion bounded by `SELECT_MAX_DEPTH` (64); any increase requires stack analysis | -| 4f-2 | Implement `gc_sweep_select_nodes` — mark pass (clear all gc_mark + mark from live jobs dispatching on XCE_RESREQ_PRIMARY/COMPOUND/ALTER), sweep pass (collect unmarked, `key_del`, `dereference(strval)`, `free(node)`, `zarray_del`) | Use `xlist_get` with counted loop for xlist iteration; GC does NOT acquire `sel_node_table.lock` (idle-phase invariant) | -| 4f-3 | Insert **`atomic_thread_fence(memory_order_seq_cst)`** after all `primary->select->root = NULL` unlinks and before `gc_sweep_select_nodes` call | Without fence: CPU/compiler may reorder NULL store after GC mark pass | -| 4f-4 | Runtime GC idle invariant guards at GC call site (`sched_session_active`, `sched_eval_in_progress`, `staging_merge_complete`) — `log_fatal` not `assert` | `assert` compiled out by NDEBUG; use-after-free would result | -| 4f-5 | Implement `recompact_node_ids` — iterate `nodes` zarray over `caps` slots (not `count`, gaps exist after `zarray_del`), assign sequential `node_id`, reset `sel_node_next_id` via `atomic_store_explicit(..., memory_order_release)` | `node_id` and zarray slot index are **decoupled** — do not conflate | +| 4f-1 | ~~Implement `gc_mark_tree` — recursive DFS, early return on `node == NULL \|\| node->gc_mark`; handles leaf / SEL_NOT / binary node types~~ **DONE** — `__attribute__((unused))` for Phase 6 wire-in | Recursion bounded by `SELECT_MAX_DEPTH` (64); any increase requires stack analysis | +| 4f-2 | ~~Implement `gc_sweep_select_nodes` — Phase 6 guard placed **before** mark-clear (prevents two-call sweep-of-reachable-nodes bug); mark-clear iterates `caps`; sweep iterates `caps`, `zarray_del` before `free`~~ **DONE** — live_jobs traversal deferred to Phase 6 (sched/types.h not in libxce scope) | GC does NOT acquire `sel_node_table.lock` (idle-phase invariant) | +| 4f-3 | Insert **`atomic_thread_fence(memory_order_seq_cst)`** after all `primary->select->root = NULL` unlinks and before `gc_sweep_select_nodes` call — **Phase 6 call-site task** | Without fence: CPU/compiler may reorder NULL store after GC mark pass | +| 4f-4 | Runtime GC idle invariant guards at GC call site (`sched_session_active`, `sched_eval_in_progress`, `staging_merge_complete`) — `log_fatal` not `assert` — **Phase 6 call-site task** → see 6-3/6-4 | `assert` compiled out by NDEBUG; use-after-free would result | +| 4f-5 | ~~Implement `recompact_node_ids` — iterate `nodes` zarray over `caps` slots (not `count`, gaps exist after `zarray_del`), assign sequential `node_id`, reset `sel_node_next_id` via `atomic_store_explicit(..., memory_order_release)`~~ **DONE** | `node_id` and zarray slot index are **decoupled** — do not conflate | +| 4f-R1 | ~~**[Review fix — HIGH]** Move Phase 6 early-return guard to before mark-clear loop — prior ordering allowed two consecutive calls (njobs>0 then njobs==0) to sweep still-reachable nodes~~ **DONE** | Two-call scenario: first call clears all marks and returns; second call sweeps everything | +| 4f-R2 | ~~**[Review fix — HIGH]** Swap `free(node)` / `zarray_del` order — `zarray_del` NULLs slot first, then `free`; eliminates latent use-after-free if killer is ever installed on nodes zarray~~ **DONE** | | +| 4f-R3 | ~~**[Bug fix]** `sel_node_table_fini` iterated `nodes.count` instead of `nodes.caps` — after `zarray_del` creates gaps, new nodes slot into positions beyond `count` boundary and leaked~~ **DONE** | Manifested as 32-byte ASAN leak after GC sweep + reintern in same lifetime | --- @@ -181,29 +191,32 @@ Develop concurrently with Phase 3 (grammar is the test oracle). | # | Task | Notes | |---|------|-------| -| 5-1 | Add `custom_target` for `xce_select.peg` in `lib/meson.build` matching `section_parser`/`rusage_parser` pattern | Verify `-o 'lib/xce_select'` path format: `grep -A8 'custom_target' src/lib/meson.build \| grep '\-o'` | -| 5-2 | Add `xce_select_parser[0]` (generated `.c`) to `libxce_src` | | -| 5-3 | Add `test_resmgr_fixture.c` to testsuite `executable()` sources in `lib/meson.build` | | +| 5-1 | ~~Add `custom_target` for `xce_select.peg` in `lib/meson.build` matching `section_parser`/`rusage_parser` pattern~~ **DONE** | Verify `-o 'lib/xce_select'` path format: `grep -A8 'custom_target' src/lib/meson.build \| grep '\-o'` | +| 5-2 | ~~Add `xce_select_parser[0]` (generated `.c`) to `libxce_src`~~ **DONE** | | +| 5-3 | ~~Add `test_resmgr_fixture.c` to testsuite `executable()` sources in `lib/meson.build`~~ **DONE** | | --- ## Phase 6 — Scheduler Integration -**Priority: MEDIUM — after Phases 1–5 pass all tests** +**Status: ✓ DONE — 2026-04-30 — 10/10 scheduler tests pass** | # | Task | Notes | |---|------|-------| -| 6-1 | **Pre-Phase-6 audit**: `grep -rn 'eval_select\|alloc_thread\|sched_session' src/scheduler/` — confirm allocator-thread-only invariant | Document result ("confirmed single-threaded" or "found unexpected call site at [location]") in commit message | -| 6-2 | **Blocking gate**: `grep -rn 'sched_meet_host_selection\|meet_host_selection' src/` — all hits must be accounted for before signature change | Missed call site → wrong ABI → UB at runtime (garbage stack parameter) | -| 6-3 | Add `_Atomic int session_active`, `eval_in_progress`, `staging_merge_complete` to `xce_scheduler_t` in `src/scheduler/include/scheduler.h` | Use `_Atomic int` not `volatile int` — ARM atomicity guarantee | -| 6-4 | Implement GC predicate wrappers referencing real `xce_scheduler_t` fields (replace Phase 1 `fake_scheduler_t` stubs) | Signatures unchanged; only wrapper body changes | -| 6-5 | Restructure scheduler session loop to **outer-host / inner-job** pattern with single `select_eval_ctx_t` per session | Current stub returns 1 — full from-scratch implementation | -| 6-6 | Update `sched_meet_host_selection` to `void` return; migrate `add_job_preason(PREASON_MATCH_FAIL)` into inner-job loop | Deleting the preason call silently leaves jobs appearing to hang | -| 6-7 | Implement `build_host_eval_ctx` (scheduler-private in `src/scheduler/src/`) — flatten `worker->resvals` zarray into `numvals`/`strvals` arrays using `Value_u_fvalue`/`Value_u_svalue`/`Value_u_bvalue` from capnp-generated header | Confirm header location: `grep -rn 'Value_u_fvalue' src/scheduler/` | -| 6-8 | Materialize `live_jobs` flat array from `sched->jobs` kvmgr using growth-loop heap allocation (not fragile `zarray_t.data` cast) | | -| 6-9 | Wire `sel_optimize_set(1)` at scheduler startup; `sel_optimize_set(0)` at API server startup | Add `log_fatal` guard at session loop entry if `sel_get_optimize_level() <= 0` | -| 6-10 | Set `sel_alloc_thread_id` at allocator thread first iteration; add thread-ID checks in `eval_select` and `gc_sweep_select_nodes` | Skip check when `sel_alloc_thread_id == 0` (unregistered — Phase 1–5 unit tests) | -| 6-11 | Write integration/parity test against known-good resreq/host pairs | Stub always returns 1, so no meaningful baseline exists — must test explicitly | +| 6-1 | ~~**Pre-Phase-6 audit**: confirm allocator-thread-only invariant~~ **DONE** `bb4a95c` | Single-threaded confirmed; stub at `resource.c:127` | +| 6-2 | ~~**Blocking gate**: all `sched_meet_host_selection` call sites accounted for~~ **DONE** `bb4a95c` | No missed call sites | +| 6-3 | ~~Add `_Atomic int session_active`, `eval_in_progress`, `staging_merge_complete` to `xce_scheduler_t`~~ **DONE** `823a66e` | Use `_Atomic int` not `volatile int` — ARM atomicity guarantee | +| 6-4 | ~~Implement GC predicate wrappers referencing real `xce_scheduler_t` fields~~ **DONE** `acda9bb` | Signatures unchanged; only wrapper body changed | +| 6-5 | ~~Implement `sched_meet_host_selection` via `eval_select_simple`~~ **DONE** `40261b7` | Full implementation replacing stub that always returned 1 | +| 6-6 | ~~`sched_meet_host_selection` returns int (0/1); preason on fail~~ **DONE** `40261b7` | int return kept; preason wired | +| 6-7 | ~~Implement `build_host_eval_ctx`~~ **DONE** `907d8d8` | Flattens `worker->resvals` zarray to `numvals`/`strvals` using `Value_u_fvalue/svalue/bvalue` | +| 6-8 | ~~Materialize `live_jobs` flat array from `sched->jobs` kvmgr~~ **DONE** `5267d36` | Growth-loop heap alloc; OOM-safe (double-guarded: `calloc` fail + `realloc` null-sentinel `742d79e`, `fb5127a`) | +| 6-9 | ~~Wire `sel_optimize_set(1)` at scheduler startup~~ **DONE** `acda9bb` | Called in `sched_init`; no `sel_optimize_set(0)` needed (API server has separate process) | +| 6-10 | ~~Set `sel_alloc_thread_id`; add thread-ID checks in `eval_select` and `gc_sweep_select_nodes`~~ **DONE** `b722e5c` `1eb4eb1` | `_Atomic int` registered flag + `pthread_equal` guard; registered before `while(1)` loop | +| 6-11 | ~~Write integration/parity tests against known-good resreq/host pairs~~ **DONE** `5fcf85c` | Numeric (`SEL_GE`) and string (`SEL_EQ`) pass/fail cases | +| 6-12 | ~~`atomic_thread_fence(memory_order_seq_cst)` before `gc_sweep_select_nodes`~~ **DONE** `5267d36` | After all `gc_mark_select_tree` calls in `sched_end_session` | +| R1 | ~~**[Review fix — CRITICAL]** `staging_merge_complete` bootstrap deadlock — GC never ran~~ **DONE** `d438742` | `sched_begin_session` never set it to 1; guard in `sched_end_session` always fired | +| R2 | ~~**[Review fix — HIGH]** `recompact_node_ids()` missing from GC protocol~~ **DONE** `d438742` | Added after `gc_sweep_select_nodes` in `sched_end_session` | ---