Skip to content

feat(settings): không nói sai về kho khoá — server kiểm tiền đề, client phân loại lỗi đọc dương cả hai chiều - #95

Merged
phanlemanh merged 35 commits into
mainfrom
feat/khong-noi-sai-ve-kho-khoa
Sep 4, 2026
Merged

feat(settings): không nói sai về kho khoá — server kiểm tiền đề, client phân loại lỗi đọc dương cả hai chiều#95
phanlemanh merged 35 commits into
mainfrom
feat/khong-noi-sai-ve-kho-khoa

Conversation

@phanlemanh

Copy link
Copy Markdown
Owner

Bất biến

Không bề mặt nào — kể cả server — được nói sai chuyện gì đã xảy ra với kho khoá BYO.
«Kho hỏng» chỉ được nói khi có tín hiệu dương. «Xoá vì hỏng» chỉ được thi hành khi
server tự xác nhận.

Hồ sơ này đi ra từ bốn phát hiện HIGH của hội đồng review chong-mat-khoa-byo-giao-dien (PR #94), owner định đoạt «mở hợp đồng mới» tại Cổng 2 ngày 02/09. Hợp đồng cũ khoanh theo bề mặt; khuyết tật sống ở bất biến phía server nên lọt ra ngoài đúng luật.

Người dùng thấy gì khác

Trước Sau
Phiên hết hạn → «Không đọc được kho khoá đã lưu», kèm nút xoá sạch kho «Phiên đăng nhập đã hết» + Thử lại, không nút phá huỷ
Proxy trả 502 → cùng tấm thẻ đỏ đó «Không tới được máy chủ khoá» + lý do + Thử lại
Lượt đọc/ghi treo vô hạn — màn Cài đặt quay mãi, node kẹt giữa chừng Trần 30 giây cho cả hai chiều, phủ tới hết phần thân phản hồi
Kho lành nhưng lệnh xoá dựa trên số liệu cũ vẫn xoá Server kiểm tiền đề, trả 409 và không ghi một byte; màn hình đọc lại thay vì bịa ra một sự cố
Ba bề mặt tự quyết «lỗi đọc nghĩa là gì», cả ba sai theo ba kiểu Một bảng ánh xạ dùng chung; nút Thử lại có ở cả chín ô không-ok do cấu tạo

Thay đổi chính

  • Server (src/app/api/settings/env/route.ts): cờ replaceUnreadableStore là một lời khai về trạng thái server, do bên gọi không nhìn thấy được — nên server kiểm nó trước khi thi hành. Kho đọc ra ok hoặc absent → 409 ENV_STORE_REPLACE_REFUSED, không ghi gì.
  • Phân loại lỗi đọc (src/lib/settings/env-client.ts): bốn trạng thái ok | store-unreadable | unauthenticated | unavailable, mỗi kết luận nặng đòi tín hiệu dương — 503 đơn thuần không đủ, phải kèm mã của chính kho. Luật này nay áp cả đường ghi (409 cũng vậy).
  • Một chỗ phát tín hiệu đăng-nhập-lại (notifyUnauthorized trong src/lib/api/client.ts), hai bên gọi, có guard đếm số lần chứ không đếm số tệp.
  • Trần 30 giây dùng chung hằng số với apiClient; lượt ghi trả về lỗi hết giờ chứ không ném — một throw sẽ rơi vào catch của node và hiện thành «khoá của bạn hỏng».
  • Giọng trầm cho sự cố tạm thời ở năm ngôn ngữ. Đỏ là một lời khẳng định rằng dữ liệu đang gặp nguy; hết phiên và hết giờ không phải vậy.
  • Ba bề mặt × bốn trạng thái, 12 ô ghim: nút xoá kho tồn tại ở đúng một ô.
  • Hai mã trên dây (src/lib/settings/env-codes.ts) — hợp đồng giữa một tệp server và một tệp trình duyệt, trước đây là chuỗi viết tay hai nơi mà không bên nào typecheck được.

Ba vòng nghiệm thu

Vòng Ô đo Suite Phán quyết Hội đồng
1 13/13 7/7 REJECT 11 phát hiện
2 13/13 6/7 (nhiễu song song) REJECT 9 phát hiện
3 13/13 7/7 PASS 14 phát hiện, 12 ngoài hợp đồng

Không vòng nào REJECT vì ô đo đỏ. Cả hai lần đều do hội đồng, và ba phát hiện HIGH nói về chính bộ đo chứ không phải sản phẩm:

  • Ma trận tự dựng một NodeKeyPrompt riêng rồi tự bơm onRetry — node thật chưa bao giờ nhận prop đó, nên mọi tấm chặn trên node không có nút nào bấm được mà ma trận vẫn xanh. Sửa bằng cấu trúc: NodeKeyGateSurface là chỗ dựng duy nhất, bộ đo mount đúng vật ship.
  • Trần thời gian tắt đồng hồ ngay khi header về, nên response.json() không có trần — đúng cái treo nó sinh ra để chặn, và mọi ca cũ đều treo header nên không ca nào chạm tới.
  • Bộ đo i18n tự dựng nút, tự nạp chuỗi, rồi tự tìm lại. Từ điển thật có unchanged/retry giống hệt nhau giữa ba nhóm nên 4/6 ca rỗng nghĩa; nay dùng từ điển mẫu có giá trị duy nhất.

Một hồi quy do chính vòng sửa gây ra cũng được hội đồng bắt: thêm {seconds} vào từ điển mà chỉ dạy một trong ba bộ dịch cách truyền — người dùng hết giờ lúc lưu sẽ đọc nguyên đường khoá. Ba bộ dịch nay gộp làm một.

Kiểm chứng

  • pnpm lint:check · pnpm typecheck · pnpm test (900 xanh, 5 skip)
  • pnpm build && pnpm typecheck · pnpm gen:abi không sinh diff
  • 13 ô đo của hồ sơ, mỗi ô có cặp hai chiều: vật lành xanh, phá vật thật trong bản sao đỏ với thông điệp ghim
  • 4 guard cấu trúc: seam đăng-nhập-lại + răng của nó, một-bộ-đọc, sàn a11y (axe trong Chrome thật, 4 trang)
  • pre-merge-check.sh --base origin/main: clean, 4 luật chạy
  • conformance-l0 re-pin: 15/15 ô đo của nó xanh tại HEAD

Đã ký

Cổng 2 ngày 04/09/2026 — Phan Le Manh. Hợp đồng status: signed-off, verdict: PASS, không bypass.

Known limits (owner nhận trước khi phát hành, chi tiết trong contract.md): nút «Thử lại» trên bảng media-library mang nghĩa khác hai bề mặt kia — đo tại Cổng 2: bấm khi thẻ đang chặn gửi đi GET, GET, GET, không lượt ghi nào, AC-5 giữ được; một câu báo lỗi hiếm chưa dịch; một dòng chữ đỏ có thể sót lại dưới thẻ trung tính; và bốn khoảng trống của bộ kiểm tra nội bộ.

Mở hợp đồng mới cho hai việc: lưu khoá thất bại vẫn hiện tích xanh «Đã lưu khoá» trên node; và lượt ghi bị huỷ ở trình duyệt sau 30 giây trong khi máy chủ có thể đã ghi xong.

Ngoài phạm vi, đã hoãn có chủ ý

PUT vẫn ghi đè toàn phần và hợp nhất vẫn ở client — PUT {env:{}} không cờ vẫn xoá kho lành. Chuyển hợp nhất về server đổi hợp đồng dây và chạm mọi biểu mẫu; đó là hồ sơ kế.

🤖 Generated with Claude Code

phanlemanh and others added 30 commits September 3, 2026 12:27
Follow-up to chong-mat-khoa-byo-giao-dien: the four HIGH findings its review
council found, all triaged out of that contract because it was scoped by
surface while the defect lives in a server-side invariant.

Invariant: no surface, the server included, may misstate what happened to
the key store. Two positive-assertion claims — "store unreadable" needs a
positive signal, "wipe because unreadable" needs the server's own confirmation.

Five load-bearing decisions: server verifies the premise of
replaceUnreadableStore (409 ENV_STORE_REPLACE_REFUSED, nothing written); the
client classifies positively in both directions (ok / store-unreadable /
unauthenticated / unavailable) instead of dumping every non-2xx into
"unreadable"; the tf:unauthorized seam becomes a shared helper rather than a
second inline copy; the 30s ceiling covers both read and write; the client
re-reads on the new 409 instead of surfacing an error.

Also names what was discovered while opening the dossier and deliberately
deferred: PUT is a wholesale replace whose merge lives on the client, so an
unflagged empty PUT wipes a healthy store just the same. Narrow now, deep as
a named follow-up.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t, T3)

Nine criteria from the eleven Core cells of the morphological scan, thirteen
evals: ten new, three carried by executor key from chong-mat-khoa-byo-giao-dien
(one-reader guard, locale parity — both re-run because this round touches their
paths). Every new eval declares a red direction on the same fixture with a
pinned message; the two full matrices (9 signals → 4 states, 3 surfaces × 4
states) pin their counts as constants so a collapsed matrix cannot stay green.

The a11y floor for the two new read states gets its own wrapper and dist dir
rather than editing the previous dossier's script, which sits in that dossier's
E9 paths and would go stale.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… 3 P2), all fixed; proto for S1-D

Clean-context critic on five inputs. Both P1s were cases the design named in
prose and the evals still could not tell apart:

- put() on timeout — throw or return? Unspecified, and E4 was green either
  way. Now: throws with code "timeout" so the existing writeFailed catch keeps
  working; E4(d) mounts abi-node-shell with a hung fetch and asserts the phase
  leaves "verifying" without reaching "verified".
- E5's negative assertion checked the wrong substring under an unpinned
  locale. Now a relation against the message catalogue, en and vi pinned; red
  direction points the new state at the old key.

Three P2s: the "nine signals" lists disagreed across three artifacts (now ten,
nine at E2 plus timeout at E4); E8 counted files where it should count
occurrences; E13 gets a data-proto-component marker so the a11y floor is
measured on the shipped component, not on a look-alike.

Also the clickable prototype for the design-pass ritual: two new read states
on three surfaces plus a divergence scene that pins the open decision (should
transient states share the destructive red?) on the artifact itself.
build/knsk-a11y joins tsconfig include only so Next leaves the file alone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… frames, divergence opened

Static-frame context, scaffold material stated up front: the baseline and
direction A use the shipped StoreUnreadableNotice as-is; direction B needs a
non-destructive tone the component does not have, so the two new states are
composed from the same primitives and tokens as the draft of a `tone` prop.

Divergence pinned on the artifact: A (one colour) vs B (two registers, machine
recommends) for the same "unreachable" case, side by side, with the reason.

Two token-level findings patched: Retry is now the one primary action and the
destructive escape is outline + text-destructive; action buttons at default
size (36px). Three deferred for Gate 1: the `tone` prop, no 44px touch size in
the DS, no declared DS skill.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nd figure briefs for Gate 1

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…gures, and fix a miscount in their brief

The brief said six signals fall through to unavailable; counting the ten it
lists gives seven. The figure was drawn from the contract and is right at
7/10, so the brief is what changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nine criteria, thirteen evals, T3. Scope sealed with seven ledger entries,
including the two deliberate deferrals: the wholesale PUT and the green tick
on a failed write both stay named and open.

Product map regenerated in this same commit — the map is a view over records
this gate just changed, and CI turns any drift red.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
env-client returns SaveOutcome; all three callers read out.ok and branch. The
approved wording said put() should throw so the existing writeFailed branch
would catch it — but that catch is abi-node-shell:253, which maps a throw to
phase "invalid" ("your key is bad, retype it"). That is precisely the trap the
previous dossier removed, and its own comment at line 152 says so.

Timeout now extends the WriteFailure union instead. The AC's intent is
unchanged; only the mechanism it named was wrong. Logged as a post-seal fix so
Gate 2 sees it in the provisional block.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Eight tasks, thirteen evals, none unowned. Three are independent (server
premise, seam extraction, tone + copy) and can run in one parallel pass; the
other five follow the dependency arrows.

Every task carries its red direction as a runnable perturbation on a copy of
the real tree, with the pinned message the failure must print — so a measure
that has never been red cannot pass for a measure that works.

One intermediate state is declared rather than discovered: Task 2's guard is
deliberately red until Task 3 adds the second caller.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The flag is a claim about SERVER state made by a caller that cannot see it.
Until now the server used it only to skip the 409 refusal and never asked
whether the claim was true — so a transient 401 or proxy 502 in the browser
could end with an empty PUT erasing a store nothing was wrong with.

Now: flag set AND the store reads ok or absent -> 409
ENV_STORE_REPLACE_REFUSED, nothing written. The legitimate path (store really
unreadable) is untouched. `absent` refuses too: "replacing nothing with
nothing is harmless" is true about the bytes and false about the premise.

The race falls out rather than needing a rule — a store repaired between the
browser's read and the user's click is simply `ok` when the write arrives.

Assertions are ordered damage-first on purpose: with the branch removed the
suite says "case ok: keys must survive the refusal: expected 0 to be 2", not
"expected 200 to be 409". A red run should name what was lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…opy the seam

env-client needs the same 401 seam the shared API client already has. Copying
the dispatch would put the event name and the cancelable contract in two
places — the same "each surface rolled its own" mechanism that produced the
key-store defect this line of dossiers is closing.

So the dispatch becomes one exported function, and a structural guard holds it
there: exactly one dispatch site, exactly two callers. It counts OCCURRENCES,
not files, because a half-done extraction leaves the helper and the inline
block in the same file and a file-counting guard sails past that. Its teeth
exercise that case specifically.

DEFAULT_TIMEOUT_MS is exported alongside it for the same reason: task 4 needs
the same ceiling, and two ceilings that drift are two behaviours the user
experiences as one.

The guard is RED at this commit, on purpose: env-client does not call the
helper until the next task. The plan declares this intermediate state so it
reads as a step rather than a regression.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The old gate asserted `ok` positively and let the COMPLEMENT fall into
"unreadable". That reads as caution and behaves as its opposite: a proxy 502,
an expired session and a dropped connection all inherited the heaviest name
the reader had — and the settings screen offers to WIPE THE KEY STORE under
that name.

Four states now, each heavy conclusion needing its own positive signal:
store-unreadable only from 503 AND the store's own code; unauthenticated only
from 401; everything else lands on the neutral `unavailable`. Nine signals,
count pinned. Collapsing the gate back turns exactly seven of them red, each
naming its signal.

403 stays out of `unauthenticated` on purpose: it is authenticated-and-refused,
and asking the shell to re-auth for it is its own small lie.

Two knock-on changes the compiler forced:

- SaveOutcome's refusal now carries the whole read instead of a flattened
  reason. Flattening is how a surface ends up saying "your key store is
  corrupt" because a session expired. A declared bridging shim keeps the three
  surfaces compiling unchanged; task 6 teaches them the four states.
- The previous dossier's suites moved with it. Its reader suite now asserts the
  invariant it actually protects (a failed read is never mistaken for a good
  one) and leaves the precise mapping to the new matrix. Its pinned case count
  went 10 -> 16 — that pin is what noticed the shared fixture had grown.

And one case is superseded outright: "replaceUnreadableStore is ignored when
the store is healthy" is now "is REFUSED". The old expectation only looked
harmless because that case sends a non-empty map; the screen that sets the flag
sends an empty one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A hung read leaves the settings screen spinning forever; a hung WRITE leaves a
node stuck mid-verification. The original finding named the write, so a ceiling
on the read alone would have closed the half nobody complained about.

put() RETURNS the timeout, it does not throw. Three callers read out.ok and
branch; a throw lands in abi-node-shell's catch as phase "invalid" and tells
the user their key is bad because the network stalled. Both red directions are
exercised: removing the AbortController hangs three cases, and swapping the
return for a throw reddens exactly the write case.

The ceiling is imported from the shared API client rather than written again —
two ceilings that drift apart are two behaviours the user experiences as one.

One case exists because this suite corrected its own premise: with everything
hung, saveEnvKeys reports read-failed, not write-failed, because it reads
before it writes. The first draft asserted the wrong half.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Widening WriteFailure in the previous commit left writeFailureText with a
switch that no longer covers its own union. tsc caught it: a user past the
30s ceiling would have been handed an empty reason inside an otherwise
complete sentence.

The switch has no fallback case on purpose. A defensive one would have
compiled and shipped the empty string instead of failing the build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ales

Colour carries a claim. The store-is-broken card is red because a broken key
store puts the user's data at risk; an expired session and a 30-second timeout
put nothing at risk. Painting them the same red is the same misstatement this
dossier removes from the words, made in the visual channel instead.

So the card gains a tone rather than a second card: one component, one set of
a11y properties, one place the heading level and the contrast decision live.
The default stays destructive, so the existing caller renders byte-identically
and the previous dossier's pinned evals keep passing.

The copy for both new states lands in all five locales at once. A hardcoded
string would have passed every render assertion in this repo and reached four
of five locales untranslated, which is why each case asserts en renders its own
value AND that the value differs from vi. The first half alone is satisfied by
a component that hardcodes English.

Three red directions run: hardcoding the title reddens two cases naming the
key, dropping the marker reddens the marker case, and giving the quiet tone the
destructive frame reddens the tone case.

The marker exists because the a11y floor is measured on the prototype. Without
one identity shared by prototype and shipping surface, that measurement proves
nothing about the screen a user sees.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The defect was never "a surface shows the wrong card". It was that three
surfaces each decided independently what a failed read means, and the heaviest
available name won every time. An expired session and a proxy 502 both arrived
as "your key store is broken" — and on the settings screen that card carries a
button offering to erase the store.

So the state-to-card table lives in ONE component. Giving each surface its own
mapping would have rebuilt the exact mechanism this dossier closes, one layer
up: three tables, drifting apart on whatever state anyone adds next. The retry
button lives inside that component too, which makes "every non-ok cell offers a
way to try again" true by construction rather than by discipline.

Twelve cells, pinned. Each asserts the right card AND the absence of the
destructive button, because counting the button in all twelve is what turns
"only settings, only when the store is really broken" into a measured fact. The
PUT count rides along in every cell: a surface can show a perfect card and
still have sent the request that overwrites the store.

Two suites from the previous dossier went red, and they were right to. They
asserted the store-unreadable card for all eight wire shapes, which is the
behaviour being corrected; they now read a shared shape-to-state table that
lives beside the fixtures. Their real invariants are untouched and one is
stronger: the way forward must still be a live control, and now the retry is
checked the same way.

The blocked-store copy stopped being threaded through two prop bags. Three
states times four strings passed by hand is the drift vector; everything that
genuinely differs per surface stays a prop.

legacyReadDetail is gone. It was declared as a bridge for exactly this step,
and a lossy shim that outlives its step becomes the lossy path.

Three red directions run: dropping the store-unreadable guard puts the wipe
button in two cells that must not have it, borrowing the old title reddens the
four relation cases, and a dead retry reddens three behaviour cases.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The race: the screen reads, the store is unreadable, the user confirms the
destructive replace, and in between the store becomes readable again. The
server checks the premise and refuses, writing nothing. Reporting that as
"could not replace the key store" invents a fault that does not exist, while
the user's keys sit intact behind an error card.

The honest response to a refused premise is to re-check the premise. So the
refusal gets its own arm of SaveOutcome rather than riding as a write failure:
nothing failed.

Two different 409s now, and telling them apart is the point. One says the store
is broken, the other says it is fine and our request was stale. The negative
case asserts the CALL SEQUENCE, not the card, because the card alone does not
discriminate: a screen that treats every 409 as a refusal re-reads, gets the
503 again, and paints the identical card. That gap was found by running the red
direction and watching the case stay green — the assertion was rewritten, not
the perturbation.

Widening SaveOutcome reddened two call sites that assumed not-ok implies a
write detail. Both are narrowed rather than cast, so "a merge-save can never
be refused this way" stays a compiler-checked claim.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…l component

Four pages, two states by two themes, scanned by axe in a real Chrome.

A separate file from the previous dossier's wrapper, deliberately. That script
sits inside paths someone already signed off on; editing it to add two states
would move an accepted measurement and leave the next reader unable to tell
which evidence belonged to which sign-off. One extra file keeps both records
readable.

It carries one assertion its ancestor does not: the page must be drawn by the
SHIPPING component. The prototype held a hand-copied quiet card until the
component grew a tone, and a floor measured on a hand-copy proves nothing about
the settings screen the floor exists to protect. So the prototype now mounts
the real component, and the guard reads the identity stamp back off the
rendered DOM.

Both red directions bite, and the second is the one worth having: restoring the
hand-copy leaves axe reporting PASS on every page, because the copy is
perfectly accessible, while the guard fails on all four. That is the difference
between measuring accessibility and measuring the thing users are shown.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Hai quyet dinh load-bearing cua S3 vao so: bang anh xa trang-thai->the o MOT
component chung thay vi ba ban, va viec sua hai bo do cua ho so truoc thay vi
giu chung xanh.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
13 o do, 7 lenh suite, diffBase = merge-base voi origin/main.
runBaseline tat: round 1 cua ho so moi khong co moc mang sang de so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Read each eval's expected block against the suite that serves it before
dispatching verification, and three cases had been declared but not written.
Cheaper to find here than in a rejected round.

E3 named a second caller: the API client must reach the shell through the same
helper. Only the settings reader was covered, so the structural claim rested on
a guard alone.

E4 named the node under a stalled write. The point of the write ceiling is that
a node never sits spinning and never calls a working key invalid, and neither
was asserted at the node. It also named the shared ceiling constant, which is
now checked as a value rather than trusted as an import.

E6 named the double click and all three surfaces. The double click found a real
defect: a retry set `loading`, which swapped the whole card for a spinner and
took away the sentence that makes the card safe to look at, at the exact moment
the user acted on it. The card now stays and its button disables.

The JSX in the timeout suite is written with createElement so the file keeps
the `.ts` path its eval names. Renaming the file to suit the test would have
been changing the contract to fit the measurement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every eval and every suite command passed. The REJECT came entirely from
review, and three of the four HIGH findings were about the measurements rather
than the product: my own tests were the false green.

THE BUG THE TESTS HID. abi-node-shell never passed onRetry, so on a real node
every blocked card had no control on it at all — no retry, and the link to
Settings only appears for a genuinely broken store. A user whose session
expired was stuck. Invisible because the matrix built its own <NodeKeyPrompt>
and handed it a stub onRetry: it measured a wiring the shipping surface did not
have. The fix is structural, not a prop: NodeKeyGateSurface is now the one
place the prompt is mounted, exported so the evals mount what ships, and the
gate owns `retry` rather than trusting a mount site to remember it. Removing
onRetry there now reddens three matrix cells.

THE CEILING DID NOT COVER THE BODY. Clearing the timer in a `finally` after
fetch disarms it the moment HEADERS arrive, so `response.json()` was unbounded
— a proxy that answers and then stalls hung forever, which is exactly the
failure the ceiling was added to stop. Every timeout case passed because they
all stall the headers. The body is now read inside the ceiling, an abort during
that read is reported as a timeout rather than a malformed payload, and a new
case stalls the body specifically.

THE TAXONOMY STOPPED AT THE MODULE BOUNDARY. The reader classified 401; the
writer did not. An expired session mid-save read "Could not save (the server
answered 401)" with no sign-in prompt, and on a node it landed in the
write-failed path — which tells the user the key WAS stored. SaveOutcome gains
an unauthenticated arm and all three surfaces render it as the quiet card.

MEASUREMENTS THAT ASSERTED ABOUT THEMSELVES. The i18n suite built its own
button from a string it had looked up, then found that string — no product code
took part, so pointing `unavailable` at the store-broken group left all six
cases green. It now renders the shipping card and runs both locales; both red
directions the eval declared now bite. The seam's cancel case read a jsdom flag
instead of the helper's return value, so `dispatchEvent(...); return false`
would have shipped a duplicate toast to every shell — it now asserts the return
value in both directions. The node-ceiling case was three `not.toBe` with no
positive, and every way the harness could fail to reach save() satisfied all
three; it now asserts the phase it must land on.

Two smaller ones, both real: the wire codes were literals on both sides of a
server/browser contract that neither side could typecheck, now one neutral
module; and the ceiling's number was spelled into ten catalogue strings, now
interpolated from the constant. The dead i18n subscription was deleted rather
than renamed to `_tStore` — an underscore silences the linter without doing the
cleanup it was reporting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…phat hien)

Workflow bao runLogWriteFailed nen 22 dong run-log, evidence-report va
review-findings duoc ghi tay — gioi han da khai tu ho so truoc.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round fix sau REJECT: khong co anchor mang sang, chay lai day du.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round 2 passed all thirteen evals again and was rejected on review again. The
HIGH was a regression from round 1's own fix: adding {seconds} to cause.timeout
taught only one of the three cause renderers to pass it, so a write that hit the
ceiling rendered the raw key path — "Could not save
(Settings.storeUnreadable.cause.timeout)" — in five languages, on the one code
path this dossier introduced. Three switches over one catalogue is what let
them drift, so they are now one `causeText`; the next field added to that copy
can only be missed in one place.

THE POSITIVE-SIGNAL RULE STOPPED AT THE READ. A 409 without the refusal code
concluded "your key store is corrupt" from the status alone — including a 409
from a proxy or an auth gateway that has never heard of this store — and that
conclusion is what renders the red card and the erase-the-store button. The
read half was deliberately hardened to require both the status and the code;
the write half was not. It is now, and a code-less 409 is reported as the write
failure it is.

RETRY WAS ONLY SAFE ON ONE SURFACE. The card documents `retrying` as "one click
stays one read", and only the settings dialog passed it. On a node the button
stayed enabled for the whole read, so N clicks fired N concurrent reads and the
last to arrive won; the media-library panel cleared `blocked` before saving, so
its card unmounted and its own flag never applied. The gate now owns the
in-flight state and the panel keeps the card up.

THREE MEASUREMENTS PROVED THINGS ABOUT THEMSELVES. The refusal suite still
built its own NodeKeyPrompt with a stub onRetry — eight of its sixteen cells
asserted "every blocked card offers a retry" about a prop the harness had just
supplied — and the shell's own comment names that exact trap. "All three
surfaces carry a live retry" mounted no surface at all; it now mounts all
three, and removing onRetry or stubbing out the re-read each redden it. The
i18n cases compared strings, and `unchanged` and `retry` are word-for-word
identical across all three groups, so four of six were satisfied by any group;
a probe catalogue with unique values makes the relation visible, and pointing
retry at the wrong group now reddens two cases that could not fail before.

THE A11Y FLOOR MEASURED A HAND-BUILT CARD. The prototype chose `tone="quiet"`
itself, bypassing the table that decides tone in production — and tone is
colour, which is the thing axe measures. The blocked scenes now render
ReadStateNotice, the shell stamps the tone it was given, and the guard reads it
back: mapping a transient failure to destructive now reddens two pages.

The `gen:abi` suite command failed in the round-2 run and is clean on this tree
across repeated runs; it is parallel-load noise of the class already recorded,
not a regression.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hat hien)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
13/13 o do xanh, 7/7 lenh suite xanh, 0 BLOCKED, khong bypass.

CHUA KY. Cong 2 thuoc ve nguoi: hang T3, va khoi «Ngoai hop dong» co 12 muc
cong HAI muc TRONG hop dong dung vao AC-5 (nut Thu lai cua media-library
panel goi save() — doc roi GHI — trong khi the mang cau «Chua co gi bi thay
doi»). Workflow bao runLogWriteFailed nen bang chung ghi tay nhu hai vong truoc.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
phanlemanh and others added 4 commits September 3, 2026 20:18
The + trang bang chung, chua co truong nao thuoc ve nguoi.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bo sinh args cua vong nay GIAI cac tham chieu config: truoc khi trao cho
workflow, nen bao cang ghi duong dan tep test thay vi khoa executor —
recheck-evidence chan merge voi 9 vi pham L2 SUBSTANCE.

13 dong verifier lay lai tu chinh evals.yaml, va moi khoa da doi chieu: no
giai ra DUNG lenh nam trong run-log vong 3. Khong dong nao thuoc ve nguoi bi
dung toi.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ho so nay oi vi PR cham src/app/api/settings/env/route.ts, roi vao glob
src/app/api/** ma E12 khai qua rong — dung dieu muc 2 cua chinh bao cao do
da du doan ngay 02/09.

Chay lai TOAN BO 15 o do cua chinh ho so tai HEAD chu khong phai lan may 4
suite: ma duoi paths cua E12 doi that, nen chi o do cua no moi tra loi duoc
cau «bang chung nay con dung khong». E12 xanh 11/11, y nhu lan ky.

Glob VAN chua thu lai — bao cao do noi ro do la viec cua mot ho so rieng,
khong phai mot luot sua len trong luc go CI. Khong dung chu ky nguoi.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Chu ky o Cong 2 tao mot hang muc da ky ma so cai chua co dong — guard
roadmap freshness do dung do va chan CI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@phanlemanh
phanlemanh merged commit ca3972d into main Sep 4, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant