# sub-reviewer-2 — LENS 2 / FLOOR (ii) archive VALUE-GATE vs FIFO-by-date  [CRUX]

**Run:** 2026-07-13-presence-not-age-adopt · **Lane:** 2 of 5 · **Lens:** archive value-gate vs FIFO-by-date
**Broadcast:** ab6c387e (presence-not-age, type=update) · **Directed:** 6c32df89 · **Reviewer:** adversarial, value-vs-age lens

---

## VERDICT: NEEDS-FIX — necessity = SHOULD (minimal ~10-line value-primary drain reorder; DRY-RUN preserved)

SE's `memory-archive-gate.ps1` PASS-1 planner is functionally a **FIFO-queue-with-a-value-overlay**, NOT a genuine value-gate. The **base drain ordering is OLDEST-by-position** (file position used as an age-proxy), and `value_protect` is bolted on as an **ADVISORY flag that never changes the proposed move-set**. The exact (iii) red-flag signature ("oldest-first to decide cool-down") is present in the code, its comments, and its resolve-string. DRY-RUN + `keep_floor` + em-main-final + the advisory flag together form a *functional backstop* (no automatic age-cut ever happens), which is why this is NEEDS-FIX/SHOULD and not RED-FLAG/MUST — but the floor asks the **decision-step ordering itself** to be value-first, and today it is age-first. The minimal fix promotes `value_protect` from advisory-flag to a **hard-skip in the drain-candidate loop**, making the printed PROPOSAL value-gated by construction (position demoted to a within-low-value tiebreak).

---

## Evidence (file:line + short quote)

### 1. The base drain ordering is OLDEST-first (age-proxy = file position)

- `scripts/memory-archive-gate.ps1:137` — comment: **"how many OLDEST entries to move?"**
- `scripts/memory-archive-gate.ps1:138` — comment: **"Move oldest entries one-by-one"**
- `scripts/memory-archive-gate.ps1:153-162` — the loop walks `for ($move = 1; $move -le ($entryCount - $keepFloor); $move++)` and cuts `$prefixBytes` = `lines[0..cutLine-1]` (the **TOP** of the file). It accumulates the move-set strictly **top-downward** until `$est -lt $lowMark`. There is no value term in this loop; the ONLY ranking key is position (`markers[$move]`).
- `scripts/memory-archive-gate.ps1:182` — the human-facing resolution literally reads **"move $moveCount oldest -> curate L1->L2 by hand"**.
- `scripts/memory-archive-gate.ps1:50` — `keep_floor` protects the mirror-image age axis: **"A5: never auto-drain below N newest"** (protect newest-5 = a pure recency protection). budget.json:38 `keep_floor_entries: 5`.

→ Both ends of the base mechanism are AGE-based: drain-oldest (top) + protect-newest (bottom-5). Position is the age-proxy (the convention = newest appended at bottom). This is *exactly* the (iii) pattern: "any place that SORTS/FILTERS by ... 'oldest-first' to DECIDE ... cool-down = RED FLAG."

### 2. `value_protect` is ADVISORY ONLY — it does not change the move-set

- `scripts/memory-archive-gate.ps1:53-56` — "If a planned MOVE would archive one OUT of L1-hot regardless of age, FLAG it ... **ADVISORY FLAG ONLY - em-main decides (no auto-exclude); keep_floor stays the recency axis.**"
- `scripts/memory-archive-gate.ps1:167-176` — the value scan runs **AFTER** `$moveCount` is already computed (lines 149-165). It reads the *already-chosen* moved prefix (`$cutLine = $markers[$moveCount]`) and only appends `$valHits`. It does **not** feed back into `$moveCount` or the drain loop.
- `scripts/memory-archive-gate.ps1:188-190` — the flag is a `Write-Output` only: **"-> KEEP in L1 regardless of age (do NOT age-archive); em-main decides"**. No exclusion.
- budget.json:39 confirms by design: "the patterns below are an **advisory grep-hint** for the DRY-RUN planner to FLAG protected entries, **NOT an enforced auto-exclude**".

→ So the PROPOSAL (the thing printed and handed to em-main) is generated by pure FIFO-by-age; value is a *post-hoc annotation* on that FIFO set, not the gate that selects it.

### 3. The advisory value-detector has coverage holes that the age-base can drain through

`value_protect.patterns` (budget.json:40) = `["gotcha #","anti-pattern","recurring","lost-update","race","bai hoc","lesson","guard","root-cause","silent-fail"]` — a 9-token substring grep. Two concrete leak paths where a high-value OLD entry is proposed for drain **without a flag** (so em-main gets no signal):

- **Spine sections carry no token.** In a reviewer/lead `MEMORY.md` the TOP-of-file spine is `## 🎯 Role baseline`, `## 📋 6-category checklist`, `## 🧠 ... review essentials`. These are the highest-value keep-forever sections but contain **none** of the 9 tokens. Being at the TOP (oldest position) they are the FIRST drain candidates, and they are **UNFLAGGED** → the FIFO proposal targets the spine and the advisory backstop is silent. (Verify: the injected reviewer MEMORY.md "🎯 Role baseline" body has no `gotcha #`/`race`/`lesson`/etc.)
- **Paraphrased anti-patterns.** An entry describing a lost-update as "hai lượt lưu liên tiếp đè nhau" without the literal `race`/`lost-update`/`lesson` token is high-value but UNFLAGGED → drainable by age. This is the precise "silent age-creep" the broadcast warns of at ab6c387e:44 ("'sắp theo ngày' ... lặng-lẽ đưa một tiêu-chí đã-bị-cấm quay trở lại đúng khâu ra-quyết-định").

→ The functional backstop (em-main honoring flags) can therefore **silently fail** for exactly the class floor (ii) protects. The value-gate must be the PRIMARY filter, not an opt-in-by-human-attention overlay.

### Broadcast self-check (iii) applied — the drop-the-date-column test

The archive-gate has no literal date column; **file position IS the age-column** (drain top-first, protect bottom-5). Run the broadcast's test: remove position-as-age from the selector input. Today the drain has *nothing else* to rank by — behavior collapses (it can't choose a move-set). Per the rubric ab6c387e:68, position is being used for **age-RANK** (which entry to cool), NOT merely for PRESENCE/last-seen → **"date used for age-RANK = fix."** This is a genuine (iii) hit on the *base ordering*. (Contrast lane-1's age-band, which is print-only and passes the same test.)

---

## Self-refutation (I must beat BOTH directions before settling)

### Direction A — "I'm UNDER-fixing; this should be COMPLIANT / quick re-verify only"

**The case for COMPLIANT:** The script is `FLAG-ONLY, DRY-RUN` (header :5-8, :86) and **never cuts anything** — "auto-WRITE of rules = top hazard" (:7). The real *selector* per the broadcast is em-main, who reads the `value_protect` flags and decides by value; the script is a byte-budget smoke-detector, not the selector. SE **already adopted** the 2026-06-20 principle this broadcast references — budget.json:39 explicitly cites "mark RC-...10-29-11 (time/age=false-proxy) applied to the memory layer" and :67 states "age is a FLAG, never a cut ... drops only on status-change ... NOT by age." honest-note-3 (ab6c387e:94) says floor = FUNCTION not FORM; the FUNCTION (high-value kept regardless of age) is achieved when em-main honors the flags. Broadcast pre-classifies this: "already-presence-based → quick re-verify only" (honest-note-2). Forcing a code reorder on a DRY-RUN planner is FORM-policing → over-engineering a type=UPDATE.

**Why it fails (rebuttal):** The stated *policy* (budget.json:67 "never by age") is presence-based, but the *mechanism that implements it* (the PASS-1 drain loop) is age-first with value as advisory — a policy/mechanism gap. Floor (ii)+(iii) do not merely ask "is high-value ever auto-cut?" (answer: no, DRY-RUN); they ask the **decision-step ordering itself** to not be oldest-first (ab6c387e:66,75). SE's ordering IS oldest-first by its own comments (:137-138,182). Relying on em-main to correct a mechanically-age-ranked proposal is precisely the "easy default that silently re-admits the banned criterion" the broadcast names (:44). And §3 shows the advisory backstop has real holes (spine + paraphrase) → the FUNCTION can silently fail, so "function is met" is not safely true. COMPLIANT overstates it.

### Direction B — "I'm OVER-fixing; this should be RED-FLAG / MUST / rip out FIFO"

**The case for RED-FLAG/MUST:** (iii) is a mandatory function-floor ("sàn chức-năng bắt-buộc", ab6c387e:46) and calls oldest-first-to-cool a **"cờ đỏ"** (:66). The code + comments + resolve-string all carry that signature verbatim. So compliance is mandatory and the base FIFO must be redesigned now.

**Why it fails (rebuttal):** RED-FLAG/MUST would overstate a **DRY-RUN planner that never cuts**, that **already has a value axis** (partial compliance, not naked FIFO), and whose final decision is a value-aware human. The broadcast itself scopes this as **proactive-prevention, ONE occurrence already fixed, not a spreading incident** (honest-note-1, :90) and type=UPDATE "re-verify CHỈ phần bộ chọn." There is no live harm to block. Ripping out position-ordering entirely is also wrong: after value-filtering, you still need *some* order among equally-low-value candidates, and position-as-tiebreak among already-low-value entries is defensible ("date used only to determine PRESENCE = OK", :68). So the honest necessity is SHOULD, and the fix is a minimal value-primary reorder, not a rewrite.

### Settle

Between A and B: the base ordering **is** a real (iii) hit (not COMPLIANT), but DRY-RUN + existing value_protect + em-main form a functional (if leaky) backstop with no automatic harm (not RED-FLAG/MUST). → **NEEDS-FIX, necessity SHOULD**, minimal value-primary reorder that keeps DRY-RUN/keep_floor/em-main-final intact.

---

## Concrete change (minimal — value becomes the PRIMARY drain filter)

**File:** `scripts/memory-archive-gate.ps1`, PASS-1 drain block (currently :141-176). Keep DRY-RUN, `keep_floor`, `strike` gating, and the printed-proposal contract; change only WHICH entries enter the move-set: **hard-skip `value_protect`-matching entries**, so position only orders the LOW-VALUE remainder.

Pseudocode (lead may reshape — FORM self-determined, honest-note-3):

```powershell
# NEW (before the drain loop): flag each entry span as value-protected
# entry k spans lines [ markers[k] .. markers[k+1]-1 ]  (last entry -> end of file)
$protected = @{}   # k -> $true if the entry body carries any $valPatterns token
for ($k = 0; $k -lt $entryCount; $k++) {
    $start = $markers[$k]
    $end   = if ($k -lt $entryCount-1) { $markers[$k+1]-1 } else { $lines.Count-1 }
    for ($li = $start; $li -le $end; $li++) {
        foreach ($vp in $valPatterns) { if ($lines[$li] -like "*$vp*") { $protected[$k] = $true; break } }
        if ($protected[$k]) { break }
    }
}

# CHANGED drain loop: walk oldest-position-first BUT SKIP protected entries.
# Only LOW-VALUE entries (and never the newest keep_floor) can enter the move-set.
# Accumulate moved bytes over the SKIP-filtered candidates until est < lowMark.
# If the low-value candidates are exhausted and still >= lowMark  ->  $warnValueFloor = $true
#   ("over-cap but every drainable entry is value-protected -> SPLIT/condense by hand")
#   -- mirrors the existing keep_floor WARN at :180; do NOT propose a protected entry.
```

Resolution-string change (:182): drop the word "oldest" → e.g. **"move $moveCount lowest-value (oldest-first tiebreak) -> curate by hand"**, so the human-facing plan no longer reads as an age-queue. Add a `WARN value-floor hit` branch alongside the existing `WARN keep-floor hit` (:180).

**Net effect:** the PROPOSAL itself is value-gated (a recurring-bug/anti-pattern/root-cause entry is never *proposed* for cool-down regardless of position); position survives only as a tiebreak among equally-low-value entries. This converts "FIFO-queue-with-value-overlay" → "value-gate with position tiebreak," removing the (iii) red-flag signature from the decision step while preserving DRY-RUN/keep_floor/em-main-final. ~10-15 LOC, no new file, no hub-org mirroring.

**Companion doc-stamps (honest-notes are mandatory, ab6c387e:88-94):**
1. budget.json — update `value_protect._note` (:39) + `age_band` caveat (:67) to state the selector is value-PRIMARY (hard-skip), position only a within-low-value tiebreak; and that the grep is a **lower-bound** signal, so em-main must value-scan the whole proposed set, not only flagged lines.
2. Keep the 3 broadcast honest-notes once (may be a single cross-lane block synthesized by lead): basis = ONE occurrence proactively fixed / SPECIFIC-APPLICATION not new rule / FUNCTION not FORM.

**Residual honesty (do NOT overclaim the fix):** the value-signal is still a 9-token substring grep; the hard-skip narrows but does not eliminate the paraphrase leak (§3). The fix makes the DEFAULT value-first (structural), but em-main-value-scan remains the true guarantee (procedural). State this in the note — do not claim "now fully value-gated."

---

## Measurable acceptance criteria (fault-injection, not happy-path)

Use `-RepoRoot <temp-tree>` (the script's own param :29) to build synthetic MEMORY.md fixtures — proves teeth, per `feedback_faultinjection_proves_teeth`.

1. **High-value-at-top is NOT proposed.** Fixture: over-cap MEMORY.md with a `## ... gotcha #99 root-cause ...` entry at the TOP (oldest position) + several low-value entries below (outside keep_floor). Run gate. **ASSERT:** the top high-value entry is NOT in the proposed move-set; a low-value entry is. (Baseline today: it IS in the move prefix, only advisory-flagged.)
2. **Permutation-invariance (broadcast drop-date test).** Two fixtures with the SAME entries but POSITIONS permuted, value-markers preserved. **ASSERT:** both runs propose the same value-CLASS to drain (the low-value entries), not "whatever sits at top." (Baseline: proposal changes with position → age-ranked.)
3. **Value-floor WARN fires.** Fixture: over-cap where EVERY non-keep_floor entry carries a value marker. **ASSERT:** output prints a `value-floor hit ... condense by hand` WARN and proposes moving ZERO protected entries (moveCount over protected = 0). (Baseline: proposes draining a protected entry with only an advisory note.)
4. **DRY-RUN + keep_floor preserved.** **ASSERT:** no MEMORY.md/archive file mutated by any run; newest-`keep_floor` entries never proposed; over-cap still exits 0 (FLAG not error), A7 exit-2-on-pointer-fail unchanged (:295).
5. **A7 pass-2 byte-unchanged behavior.** **ASSERT:** `git diff` touches only the PASS-1 block + resolve-string + budget.json note; PASS-2 integrity gate (:209-295) untouched.

---

## One-line finding

SE `memory-archive-gate.ps1` drains OLDEST-by-position (age-proxy) as the base ordering (:137-138,153-162,182) with `value_protect` only an advisory post-hoc flag (:53-56,188-190) — a FIFO-queue-with-value-overlay carrying the exact (iii) red-flag signature; DRY-RUN + em-main + advisory-flag are a leaky backstop (spine + paraphrase entries drain unflagged), so NEEDS-FIX/SHOULD via a ~10-line hard-skip that makes the PROPOSAL value-primary.
