# sub-reviewer-3 — LENS 3: FLOOR (iii) SELF-CHECK (drop-date/drop-order test)

**Run:** 2026-07-13-presence-not-age-adopt · **Lane:** reviewer 3 of 5 · **Broadcast:** `ab6c387e` (presence-not-age selector guardrail, type=update) + directed `6c32df89`
**FloorPoint owned:** (iii) self-check — apply the broadcast's own drop-date-column test to ALL 3 SE selectors (reinject / archive-gate / mfe age-band).

---

## SETTLED VERDICT — floorPoint (iii): **RED-FLAG** · necessity **SHOULD**

One of the three SE selectors trips the broadcast's own self-check. Enumeration:

| Selector | Drop date/order input → behavior change? | date/order used for… | Verdict |
|---|---|---|---|
| REINJECT (`reinject-ledger.md` + CG-1) | SELECTION: no · CG-1 debounce: yes | floor-rot = presence-gap; CG-1 = per-item **last-seen** debounce | **COMPLIANT** |
| ARCHIVE-GATE (`memory-archive-gate.ps1`) | **YES** | **age-RANK** (entry-ORDER = oldest-first drain) | **RED-FLAG** |
| MFE age-band (`mfe-eval.ps1`) | prints a different count only | advisory **FLAG** print (never cut/select) | **COMPLIANT** |

The RED-FLAG is driven entirely by the **archive-gate planner**. It is NOT a live incident (DRY-RUN + advisory value-flag + documented design-intent), hence **SHOULD** not MUST — but per the broadcast's own mechanical test it **is** the exact "sorts/filters by oldest-first to decide cool-down" pattern (iii) tells me to surface and redesign. Reporting it (not rationalizing it away) is the anti-Smart-Friend call.

---

## SELECTOR 1 — REINJECT — COMPLIANT (with one honest-note)

**Selection trigger is pure presence.** `reinject-ledger.md:3` — reinject fires for a *"mục-sàn floor-rot (đã-từng-ở-hot-mem nay RỚT)"*; `:12` — *"item ĐÃ-từng-tồn-tại ∩ CÒN-giá-trị ∩ RỚT-khỏi-L1"*. That is verbatim the broadcast (i) condition "should be present but is missing." No age term.

**Drop-date test on the SELECTION:** the floor-rot classifier tests (a) was-in-L1, (b) still-valuable, (c) dropped-from-L1. None is a date. Drop every date/session column → the *what-to-reinject* decision is unchanged. ✓ presence-based.

**The one date-touch = CG-1 debounce, and it is the OK kind.** `:9` — *"Mỗi item-id được reinject TỐI-ĐA 1 lần trong N phiên (N = 3)."* Enforcing this needs the `reinjected` session column (`:21`). Drop it → the debounce can't count → behavior changes. BUT this is a **per-item last-acted marker** ("when did I last reinject item X?") used as a loop-breaker — it does NOT rank candidates against each other by age, and it never cuts "the oldest." Per the broadcast carve-out (`ab6c387e:68`: *"date used only to determine PRESENCE (last-seen) … = OK"*) this is compliant. It is a debounce keyed to the last-reinject-event, not an age-rank selector.

**Extra safety:** ledger is a CONVENTION (`:38` — em-main hand-appends, git-tracked; no OS-hook auto-write) and currently EMPTY (`:34`). No automated age-based selection exists here at all.

**1-line re-verify note to keep in docs (broadcast §5):** "reinject SELECTS by floor-rot presence-gap; CG-1's N=3 session-distance is a per-item last-seen debounce (loop-breaker), not an age-rank — re-verified, no change."

---

## SELECTOR 2 — ARCHIVE-GATE — **RED-FLAG** (the finding)

### Evidence — the planner selects by entry-ORDER (age-proxy), oldest-first

`memory-archive-gate.ps1`:
- `:137` — *"A4 hysteresis + A5 keep-floor : how many **OLDEST** entries to move?"*
- `:138` — *"Move **oldest** entries one-by-one"*
- `:153-154` — `for ($move = 1; $move -le ($entryCount - $keepFloor); $move++) { $cutLine = $markers[$move]  # first KEPT entry starts here }` → the moved set is always `markers[1..moveCount]` = the entries at the **TOP** of the file.
- `:182` — resolution string: *"PROPOSE archive … move `$moveCount` **oldest** -> curate L1->L2 by hand"*
- `:164-165` — keep_floor protects the LAST `keepFloor` markers (bottom); the drain targets the top.

There is **no literal date column** — but **entry-ORDER is the age-proxy** (MEMORY.md convention = append-newest-to-end, `memory-budget.json:24` l2_verbatim *"curated L1 entries APPEND to end only"*; `keep_floor` note *"protect NEWEST-n"* assumes newest=bottom). So "position from top" ≡ "age rank."

### Drop-order test → FAILS as age-RANK

Remove the assumption that top=oldest (shuffle entry order / drop the position input): the planner proposes a **different** move-set. The order is consumed as a **rank** (drain from position 1 upward until under low-watermark), not as presence. This is precisely broadcast (iii)'s red flag: *"any place that SORTS/FILTERS by … oldest-first to DECIDE cool-down = RED FLAG."*

### Worse for SE specifically: "oldest = top" is INVERTED here

SE MEMORY.md files are **structured** (evergreen header sections first, chronological tail last), not flat chronological logs. Verified against `.claude/agent-memory/reviewer/MEMORY.md` via the gate's own marker regex `^(#{2,3}\s|---\s*$)`:

- marker 1 = `:7` `## 📁 Area memory` · marker 3 = `:25` `## 🎯 Role baseline` · marker 5 = `:31` `## 🚨 Recurring bug patterns` · marker 7 = `:44` `## 📋 6-category checklist` · `:55` `## ⚠️ Anti-patterns + Smart Friend guard` · `:63` `## 🧠 review essentials`
- the chronological `## 📅 Recent activity` only starts at `:75`.

So "move the oldest (top) entries first" would propose archiving **Area memory → Role baseline → Recurring bug patterns** *first* — the highest-value evergreen content — while `keep_floor` protects the recent chronological tail at the bottom. The age-proxy is not merely age-based, it is **anti-correlated with value** for SE's file shape.

(Aside, secondary: `---` separators are counted as markers too, so `entryCount` double-counts each section = the moveCount arithmetic is loose. Not my floor; noted for the code-quality lane.)

### Why it is RED-FLAG and not COMPLIANT — the value-gate exists but is advisory-only

`value_protect` (`:167-191`) scans the moved prefix for high-value markers (`memory-budget.json:40` patterns: gotcha# / anti-pattern / recurring / lost-update / race / lesson / guard / root-cause / silent-fail) and prints *"KEEP in L1 regardless of age (do NOT age-archive)"* (`:190`). This is the value-axis the broadcast (ii) wants — BUT:

- it is **ADVISORY ONLY**: `:55-56` *"ADVISORY FLAG ONLY - em-main decides (no auto-exclude); keep_floor stays the recency axis"*; `memory-budget.json:39` *"the patterns below are an advisory grep-hint … NOT an enforced auto-exclude."*
- the move-set is **still computed oldest-first**; value_protect **annotates** it, it does not **re-select**. The automated selector's ranking axis remains age-proxy; value only enters as a human-read flag.

So the *function* "cut low-value not FIFO-by-date" is carried by **em-main (human) + a print-flag**, not by the selector. The selector itself still ranks by age. That is what trips (iii).

### Concrete fix (redesign the spot to value-gated presence) — pseudocode

Promote value_protect from *post-hoc annotation* to *pre-selection exclusion*, so age-order only ever tie-breaks the LOW-VALUE pool:

```
# BEFORE draining: partition entries into protected vs movable
protectedIdx = { k : entry[k] matches any value_protect pattern }        # value axis (age-blind)
movablePool  = entries NOT in protectedIdx AND NOT in keep_floor(newest N)

# size-drain ONLY the movable (low-value) pool:
for entry in movablePool (order = size-management tie-break, not an age claim):
    move entry; if bytes-after < lowMark: break

if bytes still > cap AND movablePool exhausted:
    emit "cannot auto-drain by size: remaining over-cap is value-protected/keep-floor
          -> condense high-value entries BY HAND (do NOT age-archive)"
```

Key change: a value-protected entry is **never in the move-set**, regardless of its file position. Then dropping/shuffling entry-order changes only *which low-value entries drain for byte-size* — it can never touch protected content → the drop-order self-check PASSES.

### Measurable acceptance criteria

1. **Selection-exclusion, not annotation:** re-run `memory-archive-gate.ps1` on an over-cap sub where the over-cap is caused by value-marked content → planner outputs "cannot auto-drain by size (movable pool exhausted) → condense by hand", and the proposed move-set contains **zero** value_protect-matched lines (today it lists them as "oldest" then flags them).
2. **Order-invariance of KEEP set:** in a test MEMORY.md, take one high-value entry and (a) prepend it vs (b) append it. Both runs must **keep** it. (Today: prepended → lands in moved-prefix, only FLAGGED; appended → lands in keep_floor, protected. Order-dependent treatment of the SAME entry = the bug the fix removes.)
3. **No regression on pure-size drain:** a sub over-cap purely from many LOW-value episodic entries still drains to below low-watermark, never below keep_floor.

---

## SELECTOR 3 — MFE AGE-BAND — COMPLIANT (model of a correct FLAG)

`mfe-eval.ps1:183-189`:
- `:185` — `foreach ($d in $markDates) { if (($Today - $d).TotalDays -gt 30) { $oldN++ } }`
- `:186-187` — *"age-band (FLAG only - age=false-proxy) … >30d old but still Active=`$oldN` -> KEPT (status-driven, age-blind)"*

**Drop-date test:** empty `$markDates` → the printed count changes (goes to 0 / "date=0"). But the load-bearing outputs are untouched:
- denominator `:164` `$denomCount = marks + AS + guards + recurring` — the marks in it are gated by STATUS (`:95` `if ($status -match 'Active-High' -or $status -match 'Active\b')`), date-independent;
- Coverage-FIT `:169-178`, Goodhart anchor `:191-200` — no date input;
- there is **no cut/sort/select** anywhere downstream of `$oldN`. It is a `Write-Host` count and nothing reads it.

Date is used **purely as an advisory FLAG**, never to rank or cut. Matches `memory-budget.json:67` honest_caveats.age_band *"age is a FLAG, never a cut … An item drops only on status-change … NOT by age."* This is the correct presence/status-driven pattern the broadcast endorses.

**1-line re-verify note:** "MFE age-band is FLAG-only; dropping the date changes one diagnostic count, not the denominator/FIT/Goodhart or any selection — re-verified, no change."

---

## Other date/sort sites reviewed and CLEARED (out of the refine-step selector floor)

- `memory-selfimprove-audit.ps1:292` `Sort-Object LastWriteTime -Descending` → picks `$runDirs[0]` = latest run-trace to spot-check **HCV harvest-completeness** (`:279-285`, sub-md + synthesis present?). Not a memory reinject/archive selector; and "pick the most-recent run to verify it got harvested" is a **last-seen/presence** use, not an age-rank-then-cut. Cleared.
- `memory-archive-gate.ps1:106` / `mfe-eval.ps1:216` `Sort-Object Name` → alphabetical sub ordering, not date. Cleared.
- `backup-sql.ps1:49` (SQL .bak retention) and `applied-eval-nokey.ps1` ("NO AGE DIMENSION YET" scaffold note) → unrelated to the memory refine-step. Cleared.

No literal date-sort exists in any of the three refine-step selectors; the ONLY age-mechanism that reaches a keep-vs-cut decision is the archive-gate's entry-ORDER.

---

## SELF-REFUTATION (strongest case AGAINST my RED-FLAG)

The broadcast is **type=update**, **proactive-prevention** (ONE occurrence elsewhere, already fixed — `ab6c387e:80-90` + §5 note 1), and explicitly **"floor = FUNCTION not FORM"** (§5 note 3) with **"already-presence-based → quick re-verify only"** (§5 note 2). Under that lens the archive-gate arguably ALREADY meets the FUNCTION-floor:
1. it is **DRY-RUN** (`:6-8`, `:293-295`) — it NEVER actually cuts anything by age; the binding archival act is em-main (human);
2. the human is **explicitly handed the value flags** (`:190`), so the real keep-vs-cut decision *is* value-gated;
3. `memory-budget.json:39` already documents the governing principle as *"archival cuts LOW-VALUE, NOT FIFO-by-date … mark RC-…10-29-11 applied to the memory layer"* — i.e., design-intent is already presence/value;
4. the byte-drain's job is **size** management (get under cap); ordering the drain oldest-first *while flagging* high-value is a defensible size heuristic, not a claim "old=archive."

So one could settle archive-gate as **COMPLIANT-with-a-note**, and call my RED-FLAG an over-escalation of a documented, human-gated, dry-run advisory.

**Rebuttal (why RED-FLAG stands, but only at SHOULD):** The broadcast's (iii) test is deliberately **mechanical**: *"any place that SORTS/FILTERS by … oldest-first to DECIDE cool-down = RED FLAG → redesign to presence."* The planner **literally** sorts oldest-first (entry-order) to produce the archive proposal — that is the flagged pattern **by definition**, independent of dry-run. "DRY-RUN + advisory + human override" mitigates **impact**, but does not make the **selector** presence-based; the selector still ranks by age-proxy, and for SE's structured files that proxy is even anti-correlated with value. Anti-Smart-Friend honesty requires me to **report that it trips** rather than rationalize it as fine — and the broadcast itself prescribes the resolution I gave ("redesign that spot to presence/value"). Hence: **RED-FLAG** (it trips — surface it) at **SHOULD** necessity (impact contained by dry-run/human/value-flag, and this is a proactive update — so harden, don't hard-block). I do not lower the bar to "PASS with a shrug," nor over-raise to a MUST-block on a dry-run advisory.

---

## Docs to keep (broadcast §5 three honest-notes — for the synthesized spec)
1. Basis = **ONE** occurrence already fixed elsewhere (proactive-prevention, NOT a spreading SE incident).
2. **SPECIFIC-APPLICATION**, not a new rule — SE reinject + age-band were already presence/flag-based (quick re-verify, no change); only the archive-gate needs a hardening.
3. Floor = **FUNCTION not FORM** — the fix is "value-gate the selection," implementable in SE's own script shape; no obligation to copy hub structure/filenames.
