Files
solution-erp/.claude/workflows/runs/2026-07-13-presence-not-age-adopt/sub-reviewer-3.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

14 KiB

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-154for ($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:

  • :185foreach ($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.