|
10 | 10 | refusal — is unchanged and still binding. Decision 2's "the staged state a two-step flow |
11 | 11 | needs" is **moot**, not unaffected: the deletion removes exactly that member. Decisions 1, |
12 | 12 | 4, 5 and 6 stand as written. See "Amended 2026-08-03" at the end of this record. |
| 13 | +- Amended: 2026-08-03, second — **Decision 3 again, this time for Pricing & inventory.** The |
| 14 | + first amendment above is scoped by its own text to three named ORDERS ids, and its own |
| 15 | + argument — that enumerating only what an earlier record listed is not a record of what was |
| 16 | + lost — applies to itself. This second amendment records the Pricing & inventory drop: |
| 17 | + `products:remove-stock-review` is not ported, taking the DA-3c bound check, the |
| 18 | + `REMOVE_STOCK_INVALID_QTY` field-level refusal and the `remove-draft`/`remove-staged` render |
| 19 | + state with it. Neither had a reachable caller. The stale-watermark refusal is again |
| 20 | + unchanged and still binding. See "Amended 2026-08-03, second" at the end of this record. |
13 | 21 | - Supersedes: **one clause of [ADR-0014](./0014-second-native-descriptor-for-react-admin.md)** — |
14 | 22 | "The Block Kit screens stay in the tree and stay green until a migration increment replaces |
15 | 23 | each one" — **as to Orders and Pricing & inventory only**. That clause is the third bullet of |
@@ -363,3 +371,97 @@ member. |
363 | 371 | tests that drove the deleted ids, and each of those pinned behaviour that no longer exists |
364 | 372 | rather than behaviour that moved somewhere else. |
365 | 373 | - Reports and Coupons remain unruled; Tax, Shipping and Settings remain Block Kit permanently. |
| 374 | + |
| 375 | +## Amended 2026-08-03, second — Decision 3 again, for Pricing & inventory |
| 376 | + |
| 377 | +Everything above the first amendment is left exactly as written on 2026-08-01, and the first |
| 378 | +amendment is left exactly as written. This block records one more change to the same decision. |
| 379 | + |
| 380 | +**Why a second block rather than an edit to the first.** The amendment above is scoped by its |
| 381 | +own text to three named Orders ids, and it argues — in its own words — that "a record of what |
| 382 | +was lost that enumerates only what an earlier record happened to list is not a record of what |
| 383 | +was lost". That argument applies to the amendment itself: it enumerates the Orders drop and |
| 384 | +nothing else, so folding the Pricing & inventory drop into it silently, or leaving it out |
| 385 | +altogether, repeats exactly the mistake it was written to correct. Decision 3 still says a |
| 386 | +dropped refusal needs its own record. This is that record, for the second screen. |
| 387 | + |
| 388 | +### What happened |
| 389 | + |
| 390 | +The Pricing & inventory write-path extraction found the same shape one screen along. |
| 391 | +`products:remove-stock-review` was DA-3 state 1 → state 2 for the Block Kit screen: it parsed |
| 392 | +a quantity, staged it server-side together with the on-hand watermark, and returned a second |
| 393 | +render carrying a confirm button — because a Block Kit form cannot show a dialog over the |
| 394 | +values just typed. React can. The React screen composes its own confirm client-side and posts |
| 395 | +`products:remove-stock` directly, which is why the console's action gate has EXCLUDED the |
| 396 | +review id since the migration increment, and why **no shipped surface has ever reached that |
| 397 | +step**. Two reviews confirmed it independently: the id is unreachable, and an over-removal is |
| 398 | +refused on the path that is reachable. |
| 399 | + |
| 400 | +The reasoning of the first amendment carries over unchanged and is not restated at length: an |
| 401 | +unreachable safety check is not a safety check. It is a claim in the tree that a check is being |
| 402 | +made, which a reader has every reason to believe and which the running system does not honour. |
| 403 | + |
| 404 | +### The decision |
| 405 | + |
| 406 | +**`products:remove-stock-review` is not ported, as unreached surface.** |
| 407 | + |
| 408 | +**What goes with it — the whole of it, not only what Decision 3 happened to name:** |
| 409 | + |
| 410 | +- the **DA-3c bound check** — the requested quantity tested against the on-hand JUST re-read, |
| 411 | + so that a confirm could never name a quantity already false at the moment it was drawn. It |
| 412 | + lived only on the review step; |
| 413 | +- the **`REMOVE_STOCK_INVALID_QTY` field-level refusal** — the per-field line an unparseable or |
| 414 | + non-positive quantity produced against the staged form's own input. Decision 3 never named |
| 415 | + it, which is exactly why it is named here; |
| 416 | +- the **`remove-draft` / `remove-staged` render state** — the staged quantity plus watermark |
| 417 | + the step handed back for a second render, and the draft that carried the operator's raw text |
| 418 | + into a refusal. Both are members of a result shape that only a server-rendered second state |
| 419 | + needs; the new outcome (Decision 2, as narrowed by the first amendment) is the flag and the |
| 420 | + notice. |
| 421 | + |
| 422 | +None of the three ever ran for any surface that shipped. Dropping them removes no protection |
| 423 | +any operator has had. |
| 424 | + |
| 425 | +### What protects the reachable path instead |
| 426 | + |
| 427 | +**The removal that IS reachable is guarded in three places, and the bound is one of them.** |
| 428 | + |
| 429 | +- **The service applies a guarded decrement.** Removing more than is on hand is refused there — |
| 430 | + never a negative count, never an oversell — and the refusal comes back with the REAL on-hand, |
| 431 | + which the console surfaces to the operator as a named refusal quoting the actual count rather |
| 432 | + than as a generic failure. That is a better bound than the deleted one in the respect that |
| 433 | + matters: it is taken by the same statement that would have applied the movement, so nothing |
| 434 | + can change between the check and the write. |
| 435 | +- **The domain contract pins it.** The no-negative-stock behaviour is a contract-suite |
| 436 | + invariant, not an implementation detail of one adapter, so it holds for every store the |
| 437 | + service runs on. |
| 438 | +- **The stale-watermark refusal (DA-3a) still runs first**, on the reachable path, and is |
| 439 | + untouched: the on-hand the operator saw is re-read against live truth before anything moves, |
| 440 | + a mismatch refuses with nothing posted, and an ABSENT watermark refuses fail-closed with no |
| 441 | + re-read at all. |
| 442 | + |
| 443 | +What is genuinely gone is the *earlier* refusal — the one that could tell an operator the |
| 444 | +quantity was too large before any request left the plugin. On the reachable path that |
| 445 | +conversation now happens one round trip later, and it names the real number when it does. |
| 446 | + |
| 447 | +### The consequence to be clear-eyed about |
| 448 | + |
| 449 | +**Re-introducing a server-side staged removal later means WRITING these checks, not restoring |
| 450 | +them.** There is nothing left to restore, and a future flow will not have the same shape: the |
| 451 | +deleted bound check assumed a quantity staged between two server round trips and a watermark |
| 452 | +carried across them, and the deleted field-level refusal assumed a server-rendered form with a |
| 453 | +field to attach itself to. Anyone adding that flow owns all of it as new work. This amendment |
| 454 | +is not permission to ship a staged removal without them. |
| 455 | + |
| 456 | +### What is NOT changed |
| 457 | + |
| 458 | +- Decisions 1, 4, 5 and 6 stand as written, and Decision 2 stands as narrowed by the first |
| 459 | + amendment — the outcome is the flag and the notice, because no two-step flow remains on |
| 460 | + either screen. |
| 461 | +- **The stale-watermark refusal is Decision 3's binding requirement and is carried across |
| 462 | + verbatim on this screen**, for both of its watermarks: the stock on-hand and the edit's |
| 463 | + `expectedUpdatedAt`. A reworded one is still a failed port. |
| 464 | +- ADR-0006 Decision 1 is reaffirmed a fourth time: the sandbox suites remain the contract gate. |
| 465 | + The Pricing & inventory write path is proven in the workerd sandbox on its new module, and |
| 466 | + the retired screen's suite loses only assertions about a rendering that no longer exists. |
| 467 | +- Reports and Coupons remain unruled; Tax, Shipping and Settings remain Block Kit permanently. |
0 commit comments