Files
solution-erp/.claude/workflows/runs/2026-07-13-presence-not-age-adopt/sub-reviewer-2.md
pqhuy1987 81b59f0389
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m23s
[CLAUDE] Workflow: adopt presence-not-age selector guardrail + rec-3 hmw.js STOP-HARD (S115)
Adopt AI_INFRA 2026-07-13 broadcast ab6c387e (presence-not-age selector,
type=update) + directed 6c32df89 rec-3, via /fable-clone 5-lane reviewer
ensemble (wf_b621aac4-f0b) -> spec -> /fable-real deep-pass (PASS-WITH-FIXES,
M1+M2 applied) -> HMW execute (em-main solo; governance single-writer D9).

D2 (hmw.js): unknown-role fail-soft-WARN -> up-front STOP-HARD throw (before
  parallel; preserves null/'' role-less inherit-lead path). node --check + stub
  7/7. Doc-sync 5 live lines (ultra-on/README/harness-11-engine/runbook).
D3 (memory-archive-gate.ps1): value_protect advisory-flag -> pre-selection
  HARD-SKIP (value-primary; heading-only spans; non-contiguous byte accum) +
  value-floor WARN. Fault-inject ALL PASS + real regression A7 242/242. DRY-RUN.
D1 (reinject-ledger.md): presence re-verify stamp + 3 honest-notes + BUILD-GAP.

Re-verify: reinject (i) + MFE age-band (iii) already COMPLIANT; only the
archive-gate age-trace needed hardening. D3 = defense-in-depth codify, NOT
leak-closure (9-token grep unchanged; em-main value-scan stays the guarantee).

adap-report + email AI_INFRA (8b9dc5165d5a); inbox STAGE-2 processed. No new
User-Mark (codify-only). #53 garble x1 (lane-4) recovered from disk, 0 loss.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 12:25:24 +07:00

15 KiB

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:50keep_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):

# 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.