Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
187 commits
Select commit Hold shift + click to select a range
abca606
[CI] Vendor the emdash CAS build and move the host pin to 0.37.1-otta.1
vedanshujain Sep 13, 2026
ffb46bc
Merge pull request #246 from UrumiAI/chore/vendor-emdash-cas-build
vedanshujain Sep 13, 2026
42706d1
[Adapters] Scaffold @otta-sh/store-emdash with the StorageAccess port…
vedanshujain Sep 13, 2026
185d618
Merge pull request #247 from UrumiAI/feat/store-emdash-scaffold
vedanshujain Sep 13, 2026
9d71d5c
[Adapters] Implement EmdashInventoryStore over one inventory document…
vedanshujain Sep 13, 2026
5212449
Merge pull request #248 from UrumiAI/feat/emdash-inventory-store
vedanshujain Sep 13, 2026
c9b9436
[Test] Prove the inventory store's races and crash seams over real st…
vedanshujain Sep 13, 2026
bee2275
Merge pull request #249 from UrumiAI/test/emdash-inventory-races
vedanshujain Sep 13, 2026
44058ea
[Test][CI] Run the store-emdash contract and races on D1 under the wo…
vedanshujain Sep 13, 2026
602c084
Merge pull request #250 from UrumiAI/test/store-emdash-d1-tier
vedanshujain Sep 13, 2026
2233006
[CI][Docs] Admit the domain and store-emdash into the plugin boundary…
vedanshujain Sep 13, 2026
d056c40
Merge pull request #251 from UrumiAI/chore/plugin-boundary-admits-domain
vedanshujain Sep 13, 2026
b461f2e
[Plugin] Route every commerce-client construction through one factory…
vedanshujain Sep 13, 2026
bcf403d
Merge pull request #252 from UrumiAI/refactor/commerce-client-factory
vedanshujain Sep 13, 2026
764434f
[Test] Extract the commerce-client contract from the HTTP client's te…
vedanshujain Sep 13, 2026
0321e50
Merge pull request #253 from UrumiAI/test/commerce-client-contract
vedanshujain Sep 13, 2026
79affa3
[Docs] ADR-0019: commerce aggregates are one storage document each, a…
vedanshujain Sep 13, 2026
6e50d7d
Merge pull request #254 from UrumiAI/docs/adr-commerce-aggregates
vedanshujain Sep 13, 2026
3575c36
[Adapters] Implement EmdashCartStore with an embedded mutation ledger…
vedanshujain Sep 13, 2026
48afa91
Merge pull request #255 from UrumiAI/feat/emdash-cart-store
vedanshujain Sep 13, 2026
cb86735
[Adapters] Implement EmdashOrderStore creation, transitions and hold-…
vedanshujain Sep 14, 2026
98b8879
Merge pull request #256 from UrumiAI/feat/emdash-order-store-core
vedanshujain Sep 14, 2026
4729d43
[Adapters] Implement EmdashOrderStore refunds, reconciliation, fulfil…
vedanshujain Sep 14, 2026
68207a0
Merge pull request #257 from UrumiAI/feat/emdash-order-store-refunds
vedanshujain Sep 14, 2026
6e0766a
[Adapters] Implement EmdashOrderStore lists, counts, search, customer…
vedanshujain Sep 14, 2026
b980515
Merge pull request #258 from UrumiAI/feat/emdash-order-store-lists
vedanshujain Sep 14, 2026
f38f045
[Domain] Narrow the order-search contract to anchored prefix matches …
vedanshujain Sep 14, 2026
2844573
[Domain] Run the order contract's prefix search on the document store…
vedanshujain Sep 14, 2026
8a5bb7a
Merge pull request #259 from UrumiAI/refactor/order-search-prefix-con…
vedanshujain Sep 14, 2026
66ac98d
[Adapters] Product commerce over the document primitives, variants em…
vedanshujain Sep 14, 2026
a63c1ce
[Adapters] Port the batch-read and rename-ledger suites, and tell a r…
vedanshujain Sep 14, 2026
1686936
[Adapters] Move the stock carry after the write that commits the rename
vedanshujain Sep 14, 2026
b1a3754
[Adapters] Prove the rename's crash seams, run the contract on D1, an…
vedanshujain Sep 14, 2026
6131b70
[Adapters] Hold a renamed-from sku until its carry lands, and give a …
vedanshujain Sep 14, 2026
8e95c34
[Adapters] Re-assert the sku claim at the commit, so a stalled writer…
vedanshujain Sep 14, 2026
b60271e
[Adapters] Derive the claim's target-creation flag once, and re-asser…
vedanshujain Sep 14, 2026
228c494
Merge pull request #260 from UrumiAI/feat/emdash-product-commerce-store
vedanshujain Sep 14, 2026
45ba06c
[Adapters] Let the fault-injection helper park and fail a guarded update
vedanshujain Sep 14, 2026
7cbe9d5
[Adapters] The coupon suites, before the adapter they specify
vedanshujain Sep 14, 2026
b9c3795
[Adapters] EmdashCouponStore: no over-redeem without a transaction
vedanshujain Sep 14, 2026
1d54e7c
[Adapters] Write down the coupon document model and what it cost
vedanshujain Sep 14, 2026
3d9bf57
[Adapters] The key document owns the bump right, not the counter's guard
vedanshujain Sep 14, 2026
036e8d3
[Adapters] Re-assert the bump right immediately before the counter write
vedanshujain Sep 14, 2026
352c1d3
Merge pull request #261 from UrumiAI/feat/emdash-coupon-store
vedanshujain Sep 14, 2026
eb6d867
[Adapters] Shipping and tax rules contracts, against the document tier
vedanshujain Sep 14, 2026
cb3233a
[Adapters] EmdashShippingRulesStore and EmdashTaxRulesStore
vedanshujain Sep 14, 2026
2aea996
[Adapters] The rules money-CAS race, the claim seams, and the D1 tier
vedanshujain Sep 14, 2026
d51fab2
[Adapters] Document the rules stores' model, seams and measured depth
vedanshujain Sep 14, 2026
c0f7ce8
[Adapters] Drop two unused test imports and apply the formatter
vedanshujain Sep 14, 2026
d071f48
[Adapters] Re-assert the child-id claim at the embed, and heal a lost…
vedanshujain Sep 14, 2026
48f5f7e
[Adapters] Say what the claim is, what the fallback costs, and prove …
vedanshujain Sep 14, 2026
036dcc7
Merge pull request #262 from UrumiAI/feat/emdash-rules-stores
vedanshujain Sep 14, 2026
99df411
[Docs] ADR-0019: say what the built adapters actually guarantee
vedanshujain Sep 14, 2026
1acc78b
[Docs] ADR-0019: scope the audit-pair claim, and strike the structura…
vedanshujain Sep 14, 2026
44849d8
Merge pull request #263 from UrumiAI/docs/adr-0019-amendment-phase-b
vedanshujain Sep 14, 2026
01b1ec9
[Adapters] Identity contracts against the document stores, harness first
vedanshujain Sep 14, 2026
dd62e14
[Adapters] Customers with addresses embedded, hash-keyed sessions, ma…
vedanshujain Sep 14, 2026
c6d918c
[Adapters] Race, seam and D1 coverage for the identity claims
vedanshujain Sep 14, 2026
f096461
[Adapters] Document the identity model, its two claims and its residuals
vedanshujain Sep 14, 2026
f2664b1
[Adapters] The email claim's lease renews, and its fence has a test
vedanshujain Sep 14, 2026
d1808bd
Merge pull request #264 from UrumiAI/feat/emdash-identity-stores
vedanshujain Sep 14, 2026
910cdfa
[Adapters] Test the entitlement, payment-event, settings and order-no…
vedanshujain Sep 14, 2026
d3b4893
[Adapters] The entitlement, payment-event, settings and order-note st…
vedanshujain Sep 14, 2026
81978b9
[Adapters] Document the four models, their two seams and their measur…
vedanshujain Sep 14, 2026
817245b
[Docs] ADR-0019 §4: rows corrected to the declared layout
vedanshujain Sep 14, 2026
82c6579
[Adapters] A settings mutation records what it LANDED, not what it de…
vedanshujain Sep 14, 2026
84c0e17
[Adapters] Restore the README's untouched lines, and close four gaps …
vedanshujain Sep 14, 2026
638c3d8
[Docs] ADR-0019: one meaning for the dagger, and three statements the…
vedanshujain Sep 14, 2026
ae90a5d
[Adapters] A settings mutation is pinned to the revision it was decid…
vedanshujain Sep 14, 2026
130ddab
[Adapters] State the settings pin, its invariant and its residual in …
vedanshujain Sep 14, 2026
7a49cf7
[Docs] ADR-0019 §7.17: the settings row, as the pin shipped
vedanshujain Sep 14, 2026
7d1fda1
[Adapters] The superseded refusal reports the revision it read, not t…
vedanshujain Sep 14, 2026
e5277e8
[Docs] ADR-0019 §7.17: the settings row admits the no-op merge
vedanshujain Sep 14, 2026
59fb2fc
Merge pull request #265 from UrumiAI/feat/emdash-misc-stores
vedanshujain Sep 14, 2026
5695060
[Adapters] Reporting rollups: the behavioural suites, before the adapter
vedanshujain Sep 14, 2026
fef99be
[Adapters] Reporting over precomputed day documents, with the recompu…
vedanshujain Sep 14, 2026
6787e7b
[Adapters] Document the rollup model, its residual direction and its …
vedanshujain Sep 14, 2026
bc8a8f4
[Docs] ADR-0019 §4: the rollups' claim collection, and the day row as…
vedanshujain Sep 14, 2026
692ebc5
[Adapters] Pin the recompute against live events, the finalize hook p…
vedanshujain Sep 14, 2026
0d96630
[Adapters] The recompute pins before it scans, and a claim it absorbe…
vedanshujain Sep 14, 2026
7a0900e
[Adapters] Document the real recompute proof, the exact window, and w…
vedanshujain Sep 14, 2026
28e0172
[Docs] ADR-0019 §4: the claim id's real shape, and where the rollup c…
vedanshujain Sep 14, 2026
602959d
[Adapters] A read-parking arm, and the case that tells the two pin or…
vedanshujain Sep 14, 2026
a80065f
[Adapters] The absorb pass is indexed and budgeted, and the pin decid…
vedanshujain Sep 14, 2026
c254335
[Adapters] README: the absorb budget, when to reconcile a day, and th…
vedanshujain Sep 14, 2026
929317f
[Adapters] A day's claims are read by the day, not by the order
vedanshujain Sep 14, 2026
a727a28
[Adapters] README: the absorb pass's real unit, and the ceiling it im…
vedanshujain Sep 14, 2026
c70dab3
[Docs] ADR-0019 §4: the rollup claim collection declares `date` as we…
vedanshujain Sep 14, 2026
3ed4c39
[Adapters] State the recompute ceiling honestly, and make one refusal…
vedanshujain Sep 14, 2026
0c3f617
Merge pull request #266 from UrumiAI/feat/emdash-reporting-rollups
vedanshujain Sep 14, 2026
eef3f71
[Plugin] Bind the client contract's storefront slice to an in-process…
vedanshujain Sep 14, 2026
fde3584
[Plugin] Implement the in-process commerce client over the plugin's o…
vedanshujain Sep 14, 2026
fa2c021
[Docs] Document the plugin's commerce transports and where commerce t…
vedanshujain Sep 14, 2026
31e701c
[Plugin] Make the package build, and keep the host out of its publish…
vedanshujain Sep 14, 2026
12e2382
[Plugin] Refuse malformed commerce input at the boundary again
vedanshujain Sep 14, 2026
40407f2
[Plugin] Make the sandbox document store opt-in, and prove more than …
vedanshujain Sep 14, 2026
dba8b2f
[Plugin] Prove identity derivation, and pin the two gaps and one asse…
vedanshujain Sep 14, 2026
14737b5
[Docs] Document the two unwired gaps and the responses that are narrower
vedanshujain Sep 14, 2026
9f443d8
[Docs] Record the hold-TTL fallback as a parity item, not a design ch…
vedanshujain Sep 14, 2026
2a8502a
[Plugin] Finish the storage-shape seam and tidy three comments
vedanshujain Sep 15, 2026
c438cbe
[Docs] Correct the changeset's storage bullet and name the third gap
vedanshujain Sep 15, 2026
96c757b
[Plugin] Drop a stale clause from the packaging guard's comment
vedanshujain Sep 15, 2026
818a431
Merge pull request #267 from UrumiAI/feat/in-process-commerce-client-…
vedanshujain Sep 15, 2026
983acb6
[Test] Hold both commerce transports to the identity, quote and input…
vedanshujain Sep 15, 2026
baac701
[Docs] Document the session hook, the two optional gates, and what co…
vedanshujain Sep 15, 2026
24a3666
[Test] Assert the post-state of every bound, and stop two cases passi…
vedanshujain Sep 15, 2026
70f4462
[Docs] Give the egress count and the empty variant key their own reasons
vedanshujain Sep 15, 2026
08190db
Merge pull request #268 from UrumiAI/feat/in-process-commerce-contrac…
vedanshujain Sep 15, 2026
50452b3
[Plugin] Run the admin Products console on the plugin's own store
vedanshujain Sep 16, 2026
bd091a0
[Test] Hold both transports to the same admin Products contract
vedanshujain Sep 16, 2026
a1ab1f3
[Plugin] Refuse the cursor and the edit body on exactly the bounds th…
vedanshujain Sep 16, 2026
43c4408
[Test] Cover the two states the admin surface reads and cannot write,…
vedanshujain Sep 16, 2026
4994213
[Docs] Add the changeset this increment owes
vedanshujain Sep 16, 2026
f48d217
[Test] Cast signed.body past the DOM BodyInit shadow in download-rout…
vedanshujain Sep 16, 2026
3092526
Merge pull request #270 from UrumiAI/fix/plugin-typecheck-dom-leak
vedanshujain Sep 16, 2026
109868c
Merge remote-tracking branch 'origin/feat/in-process-commerce' into f…
vedanshujain Sep 16, 2026
19ba99e
Merge pull request #269 from UrumiAI/feat/in-process-admin-products
vedanshujain Sep 16, 2026
d3cc281
[Plugin] Run the admin Orders console on the plugin's own store
vedanshujain Sep 16, 2026
fc1432a
[Test] Run the admin-orders contract slice on both transports
vedanshujain Sep 16, 2026
5e51d30
Merge pull request #271 from UrumiAI/feat/in-process-admin-orders
vedanshujain Sep 16, 2026
40a5fd0
[Plugin] Run the Shipping, Tax and Coupons consoles on the plugin's o…
vedanshujain Sep 16, 2026
3b7e713
[Test] Run the whole admin-rules surface against both transports
vedanshujain Sep 16, 2026
69ceeb8
Merge pull request #272 from UrumiAI/feat/in-process-admin-rules
vedanshujain Sep 16, 2026
a4e4de5
[Test] Widen the reporting contract slice to all six methods
vedanshujain Sep 16, 2026
6dbc92d
[Test] Bind the reporting slice to the in-process tier, and read its …
vedanshujain Sep 16, 2026
4591949
[Plugin] Serve reports and operational settings in-process
vedanshujain Sep 16, 2026
735c311
[Docs] Changeset for the in-process reporting + settings fold-in
vedanshujain Sep 16, 2026
9968057
[Plugin] Catch an in-process construction failure on the Reports page
vedanshujain Sep 16, 2026
9331168
[Docs] Say that `UpdateSettingsResult.reason` was ratified, not impro…
vedanshujain Sep 16, 2026
c34d607
Merge pull request #273 from UrumiAI/feat/in-process-reporting-settings
vedanshujain Sep 16, 2026
a1df28a
[Adapters] HMAC the payment adapters with WebCrypto, not node:crypto
vedanshujain Sep 16, 2026
cfdd396
[Adapters] Hold both payment packages to the sandbox-clean perimeter
vedanshujain Sep 16, 2026
af66b2b
[Adapters] Changeset for the WebCrypto HMAC port
vedanshujain Sep 16, 2026
487addf
Merge pull request #274 from UrumiAI/refactor/payments-webcrypto
vedanshujain Sep 16, 2026
02de293
[Plugin] Test first: write-only payment/email secrets and per-mode al…
vedanshujain Sep 16, 2026
36b959b
[Plugin] Payment/email secrets in write-only kv; allowedHosts widened…
vedanshujain Sep 16, 2026
764e0b0
Merge pull request #275 from UrumiAI/feat/plugin-payment-secrets
vedanshujain Sep 16, 2026
996ebde
[Plugin] The plugin settles Stripe webhooks itself, behind an edge to…
vedanshujain Sep 16, 2026
a566496
Merge pull request #276 from UrumiAI/feat/plugin-settle-route
vedanshujain Sep 16, 2026
34d9c67
[Site] RED: the Stripe webhook edge's transport contract, before it e…
vedanshujain Sep 16, 2026
2b4c62c
[Site] GREEN: POST /webhooks/stripe forwards the raw delivery to the …
vedanshujain Sep 16, 2026
967a56c
[Plugin] Harden header() to read the edge token from either a Headers…
vedanshujain Sep 17, 2026
9211f23
[Site] Align webhook test fake with EmDash's real header-flattening s…
vedanshujain Sep 17, 2026
9b0dbef
[Plugin] The header-shape fix is defence in depth, not a shipped-bug fix
vedanshujain Sep 17, 2026
48b8bd6
[Plugin] Name the two conditions that would make the Headers branch live
vedanshujain Sep 17, 2026
cacc82b
[Site] Assert the token was actually sent before asserting the gate l…
vedanshujain Sep 17, 2026
9351627
Merge pull request #277 from UrumiAI/feat/site-stripe-webhook-edge
vedanshujain Sep 17, 2026
c02a0c5
[Plugin] Cron hook: four ported sweeps plus five new sweepers
vedanshujain Sep 17, 2026
37532fb
[Plugin] Cron sweeps, revision 2: make registration fire, and stop ev…
vedanshujain Sep 17, 2026
11e00ac
Merge pull request #278 from UrumiAI/feat/plugin-cron-sweeps
vedanshujain Sep 17, 2026
f51f7de
[Plugin] Email dispatch and x402 settlement in-process
vedanshujain Sep 17, 2026
5f304d1
[Plugin][Domain][Adapters] Make the in-process x402 path reachable, a…
vedanshujain Sep 17, 2026
8dbad30
[Domain][Plugin][Adapters] One on-chain payment settles exactly one o…
vedanshujain Sep 17, 2026
7fcad4a
Merge pull request #281 from UrumiAI/feat/x402-email-settlement
vedanshujain Sep 17, 2026
5f3779b
[Site] Flip staging to in-process: declare the commerce storage layout
vedanshujain Sep 17, 2026
01490ff
[Site] Re-seed the quickstart through the site, not a commerce service
vedanshujain Sep 17, 2026
44c422b
[Docs] Seed the quickstart through the site, and a changeset for the …
vedanshujain Sep 17, 2026
9a4a88c
[Site][Plugin] Read before you publish, and allowlist what the bundle…
vedanshujain Sep 17, 2026
1895d90
[Site] Say what nothing builds, and stop a zero-stock product reading…
vedanshujain Sep 19, 2026
864a540
Merge pull request #284 from UrumiAI/feat/staging-in-process-cutover
vedanshujain Sep 19, 2026
cc5b8f1
[CI] Make the D1 tier the release gate, and document pnpm test:d1
vedanshujain Sep 19, 2026
e5a9c29
Merge pull request #285 from UrumiAI/ci/d1-tier-release-gate
vedanshujain Sep 19, 2026
336f1b6
[Service][Site][CI] Retire the service deployment and mode plumbing
vedanshujain Sep 19, 2026
080d8f3
[Plugin][Test] Address review: fix the retired-remedy copy, restore t…
vedanshujain Sep 19, 2026
ad6697f
[Plugin] Reference the filed tracking issues for the two parked test.…
vedanshujain Sep 19, 2026
6657292
[Plugin][Test] Retrofit the two Postgres-gated sandbox suites the mod…
vedanshujain Sep 19, 2026
a852756
Merge pull request #288 from UrumiAI/chore/retire-service-deployment
vedanshujain Sep 19, 2026
b319dc1
[Service][Adapters] Delete @otta-sh/service and @otta-sh/store-postgres
vedanshujain Sep 20, 2026
e6f6c7d
[Plugin][Test] Delete the HTTP transport: clients, wire tests, live-s…
vedanshujain Sep 20, 2026
a0a47b1
[Docs] Re-point the 59 changesets that named the two deleted packages
vedanshujain Sep 20, 2026
fb2c72c
[Docs][Test] Close the review findings on the service deletion
vedanshujain Sep 20, 2026
fd80ab7
[Test] Make the two new concurrency cases actually race
vedanshujain Sep 20, 2026
80333d2
Merge pull request #290 from UrumiAI/chore/delete-service-and-store-p…
vedanshujain Sep 20, 2026
c554aa3
[CI][Docs] Stop naming the packages INC-D3b deleted
vedanshujain Sep 20, 2026
ae36b68
[CI][Docs] Close the review findings on the INC-D3c reference trim
vedanshujain Sep 20, 2026
550ebb3
Merge pull request #292 from UrumiAI/chore/post-deletion-cleanup
vedanshujain Sep 20, 2026
a1d2bf6
[Docs] One deployable: ADR-0020, and drop the split from README and D…
vedanshujain Sep 20, 2026
75ee847
[Docs] One deployable: finish the README sweep, fix renumbered cross-…
vedanshujain Sep 20, 2026
22de0bb
Merge pull request #293 from UrumiAI/docs/one-deployable
vedanshujain Sep 20, 2026
266deee
[Docs] Work order 02: the memory note, with the numbers that exist an…
vedanshujain Sep 20, 2026
d07cff7
[Docs] Work order 02 memory note: correct the attributions and the nu…
vedanshujain Sep 20, 2026
23e9544
Merge pull request #294 from UrumiAI/docs/work-order-02-memory
vedanshujain Sep 20, 2026
2d6b6d4
[CI] Move the EmDash host off the vendored build and onto emdash@0.38.0
vedanshujain Sep 20, 2026
c2b64b9
[CI] Delete vendor/ and the build script, folding their record into t…
vedanshujain Sep 20, 2026
cef7a88
[Test] Repair the ADR-0006 additive gate, which had drifted in both d…
vedanshujain Sep 20, 2026
5529fdb
[CI] Fix backwards invariant/coincidence comment and dangling migrati…
vedanshujain Sep 20, 2026
d0a6ecc
Merge pull request #295 from UrumiAI/chore/unvendor-emdash
vedanshujain Sep 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
34 changes: 11 additions & 23 deletions .changeset/accordion-titles-carry-values.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

Accordion labels state the values they hide, so the collapsed screen is readable
(admin-UX INC-15). A Block Kit console cannot draw cards, and a group whose label is a
bare noun — `Identity`, `Service connection` — makes the operator open it just to find
bare noun — `Identity`, `Checkout & holds` — makes the operator open it just to find
out whether it holds anything. The labels now answer that, which is the cheapest
density win the surface allows.

Expand All @@ -13,24 +13,19 @@ density win the surface allows.
renders as its natural-key slug rather than `name (id)`: the pair would consume the
whole 60-character label budget on its own, leaving no room for the weight the group
also exists to show.
- **Settings.** `Checkout & holds — 15 min hold · low stock at 5` and `Service
connection — token set · service token not set`, and each group now renders closed.
The screen used to open `Store`, the one cosmetic field on it, pushing the two groups
that hold operational and connection state below an expanded form. This is the
- **Settings.** `Checkout & holds — 15 min hold · low stock at 5`, and each group now
renders closed. The screen used to open `Store`, the one cosmetic field on it, pushing
the group that holds operational state below an expanded form. This is the
render-time kind of closing: no `block_id` changes to force a group shut, so no
unsubmitted operator input is ever discarded.
- **A token's label states a FACT about the credential, never any part of it.** "Token
set" is derived from a boolean the render already had; neither token value is in
scope where the labels are built, and the whole-response no-echo pins cover the
labels along with everything else. Both tokens stay write-only and never render back.
- **An absent value is named, not implied.** `Identity — no SKU`, `Classification &
shipping — no tax class · no weight`, `Store — no display name`, and — when the
secondary `GET /settings` fails — `Checkout & holds — not loaded` rather than a label
settings read fails — `Checkout & holds — not loaded` rather than a label
reading `0 min hold · low stock at 0`.
- **A collapsed label reads as persisted state, so it only ever states persisted state.**
On a REJECTED operational save the form keeps the attempted value for correction, and
the label keeps stating what the service actually holds — a group reading
`99999 min hold` after the service refused 99999 would be reporting a value nothing
the label keeps stating what is actually persisted — a group reading
`99999 min hold` after the save was refused would be reporting a value nothing
stored.
- **An over-budget label loses a value, not the tail.** Right-truncation would delete the
last segment outright and leave a label that looks complete, so the truncation costs
Expand All @@ -46,16 +41,9 @@ density win the surface allows.
renders no edit forms at all) still states its kind. Nothing replaced the Title row
with a Title input: `product_commerce.title` is a CMS-owned single-writer cache
(ADR-0013) and `ProductEditWire` has no `title` member, so one would not compile.
- **A blank token submit stops claiming it saved something.** The token fields render
empty on every mount and a blank submit deliberately keeps the stored token, so the
receipt now says `Nothing entered — admin token unchanged` instead of `Admin token
saved` above a group labelled `token not set`.

A Settings render also stops re-reading kv for what it already has: seven sequential
`ctx.kv` gets become five, of which the last three run concurrently. Two were re-reads
of tokens the handler had fetched at the top of the request, and both booleans the
labels need are derivable from the tokens already in hand. A token save updates what its
own re-render is computed from, so a first-ever save reports the token it just persisted
as set rather than as missing.
A Settings render also stops re-reading kv for what it already has, collapsing the
sequential `ctx.kv` gets the handler had already made at the top of the request and
running what remains concurrently.

No service, wire, or schema change.
No wire or schema change.
2 changes: 1 addition & 1 deletion .changeset/admin-failed-load-clears.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ manual page reload. It now answers a failure in one of three ways:
- **stale** (a first page failed under rows) — the rows, the count and
`Load more` are cleared in state; the filter bar and the filter summary stay,
because the operator's typed filters are input rather than answer. The card
carries the service's own words plus a sentence saying the rows went and why,
carries the failure's own words plus a sentence saying the rows went and why,
and focus moves to Retry, which was inside a row that no longer exists;
- **partial** (a page behind a successful one failed) — every accumulated row
and the count stand, and the card renders where `Load more` was, titled for
Expand Down
22 changes: 7 additions & 15 deletions .changeset/admin-orders-console.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
---
"@otta-sh/domain": minor
"@otta-sh/store-postgres": minor
"@otta-sh/service": minor
"@otta-sh/plugin": minor
---

Expand All @@ -19,22 +17,16 @@ Add a WooCommerce-style admin Orders console — VIEW + STATUS-TRANSITION only
suite pin the spec (empty, single/multi state, date boundary, search,
pagination no-overlap/no-gap, identical-`created_at` tie-break, limit
boundary).
- `@otta-sh/store-postgres`: implements `listOrders` as a single
`orders → order_totals` SELECT with a grouped keyset predicate, dialect-identical
on better-sqlite3 and Postgres. Adds forward-only migration `0009` (a
`orders(created_at, id)` index for the keyset order).
- `@otta-sh/service`: adds the internal-token-guarded `GET /admin/orders` (filters +
an OPAQUE base64url keyset cursor that embeds the active filter so it survives
paging; a malformed/tampered cursor fails CLOSED to 400 and the decoded limit is
re-clamped) and `GET /admin/orders/:id` (full order + `allowedTransitions` from
the domain state machine; 404 when absent). `serializeOrder` gains `createdAt` +
`customerId` additively.
- `@otta-sh/plugin`: adds the Orders admin page (list with a status/date/search
filter form, keyset "Load more", open-order → detail with line items, totals,
and legal transition buttons — destructive cancel/refund guarded by a confirm
dialog). A new `AdminOrdersClient` reaches the service only via `ctx.http` +
`allowedHosts` with the write-only kv admin token; the plugin defines its own
local wire types and never imports `@otta-sh/domain` (now enforced by the
dialog). Paging rides an OPAQUE base64url keyset cursor that embeds the active
filter so it survives a "Load more"; a malformed or tampered cursor fails
CLOSED and the decoded limit is re-clamped. The detail read carries the full
order plus `allowedTransitions` derived from the domain state machine, and an
order summary now carries `createdAt` + `customerId`. The console reads
through a plugin-owned admin orders client; the plugin defines its own local
wire types and never imports `@otta-sh/domain` (now enforced by the
dependency-cruiser sandbox-clean rule). The staging trusted descriptor
registers the new page.

Expand Down
17 changes: 8 additions & 9 deletions .changeset/admin-orders-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
Re-lay the admin Orders console onto the design spec's §11 — the REFERENCE screen
the other six pattern-match on. One flat full-width stack becomes a collapsed
filter panel over the data (list) and five blocks plus four task-named panels
(detail). Presentation only: no port, wire-format or money-handling change, and
the service is untouched.
(detail). Presentation only: no port change and no money-handling change.

**The list (§11.1).** `header` + one 101-char `context` + a **collapsed** 4-field
`filterPanel` accordion + the table + the drill-in picker — nothing else above the
Expand Down Expand Up @@ -65,9 +64,9 @@ instead of silently bouncing the operator to the list (DA-3b).
**Status moves are one `actions` block with per-state ids derived from
`ORDER_STATES`** (DA-6) — the old one-block-per-button split existed only because
every button shared the literal id `orders:transition` and they collided as React
keys. `customActions` is derived from the same constant and a service-offered state
outside it renders **no button**, because `admin-route.ts` falls through an
unregistered id to `{blocks: []}` — a blank console.
keys. `customActions` is derived from the same constant and an offered state outside it
renders **no button**, because `admin-route.ts` falls through an unregistered id
to `{blocks: []}` — a blank console.

Also: `formatTotal`'s catch branch renders `—` instead of raw minor units (a wrong
number dressed as a formatted total, M-1) and the totals block says so when it
Expand Down Expand Up @@ -102,7 +101,7 @@ unreadable payload rather than as licence to skip the comparison.

Copy and layout follow-ups in the same pass: the DA-3a refusal restores its causal
clause (*"someone else refunded this order since you started"*); the fail-closed
banner stops claiming the service is unreachable when a console bug lands on the
banner stops blaming an unreachable back end when a console bug lands on the
same path (E-7/X-42); both destructive group labels carry their consequence (D-6a);
`Remaining` becomes `Remaining refundable` and a total that disagrees with its
capture is reconciled in one line (M-11/M-11a), with the degenerate `$0.00 of $0.00`
Expand All @@ -128,6 +127,6 @@ clauses on each of the four refusal paths; a **positive** watermark assertion (t
deliberate identical refunds derive **different** idempotency keys, so both apply —
the property the whole no-nonce design rests on, and the one nothing asserted); a
`shipped`-order assertion that `Mark refunded` really is offered, against a fixture
whose `allowedTransitions` is the domain state machine copied verbatim; and a
service-side assertion that `GET /admin/orders/:id` on a shipped order returns
`["delivered", "refunded"]`, which is the wire shape the watermark exists for.
whose `allowedTransitions` is the domain state machine copied verbatim; and an
assertion that reading a shipped order offers exactly
`["delivered", "refunded"]`, which is the shape the watermark exists for.
2 changes: 1 addition & 1 deletion .changeset/admin-price-save-guards.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ but it now has four states:
storefront immediately, and `Discard` appears beside `Save`;
- **in flight** — only the button that was clicked reads `Saving…`, and it stays
that way until the re-read that follows the write lands, so no save button is
ever re-armed against a watermark the service has already superseded;
ever re-armed against a watermark the store has already superseded;
- **saved** — a receipt renders inside the section, under the button, naming the
two amounts and saying that orders already placed keep the price they were
charged. It persists; nothing dismisses it.
Expand Down
24 changes: 6 additions & 18 deletions .changeset/admin-products-console-list.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
---
"@otta-sh/domain": minor
"@otta-sh/store-postgres": minor
"@otta-sh/service": minor
"@otta-sh/plugin": minor
---

Expand Down Expand Up @@ -29,23 +27,13 @@ VIEW-ONLY product list + read-only detail (admin-UX Increment 2, "product enumer
`InMemoryInventoryStore` fakes and the contract suites pin both specs (empty,
filters, pagination no-overlap/no-gap, identical-`created_at` tie-break, limit
boundary, tombstone exclusion).
- `@otta-sh/store-postgres`: implements `listProducts` as a single
`product_commerce` SELECT (no join) with a keyset predicate dialect-identical on
better-sqlite3 and Postgres; the substring title search escapes SQL LIKE
metacharacters (`%`, `_`, `\`) so a literal search (e.g. "50% off") never
misfires as a wildcard. Implements `InventoryStore.getOnHand` as a bare
single-row `SELECT on_hand`.
- `@otta-sh/service`: adds the internal-token-guarded `GET /admin/products` (filters
+ an OPAQUE base64url keyset cursor embedding the active filter, mirroring
`GET /admin/orders`'s cursor discipline — a malformed/tampered cursor fails
CLOSED to 400 and the decoded limit is re-clamped) and
`GET /admin/products/:id` (the full product detail plus the single-sku `onHand`
read; 404 for an unknown OR soft-deleted product — there is no admin surface for
browsing/restoring a tombstone yet).
- `@otta-sh/plugin`: adds the Products admin page (list with an active/kind/search
filter form, keyset "Load more", columns title/SKU/price/status/kind — stock
deliberately OMITTED from the list; open-product → read-only detail showing the
full product fields incl. stock). A new `AdminProductsClient` reaches the
service only via `ctx.http` + `allowedHosts` with the write-only kv admin token;
the plugin defines its own local wire types and never imports `@otta-sh/domain`
full product fields incl. stock, via the single-sku `onHand` read). The list
cursor is OPAQUE and embeds the active filter, mirroring the Orders console's
cursor discipline — a malformed or tampered cursor fails CLOSED and the decoded
limit is re-clamped. An unknown OR soft-deleted product reads as not-found;
there is no admin surface for browsing or restoring a tombstone yet. The plugin
defines its own local wire types and never imports `@otta-sh/domain`
(sandbox-clean). The staging trusted descriptor registers the new page.
56 changes: 20 additions & 36 deletions .changeset/admin-products-onhand-projection.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
---
"@otta-sh/domain": minor
"@otta-sh/service": minor
"@otta-sh/plugin": minor
"@otta-sh/store-postgres": patch
---

Carry stock on the admin Products list projection, and the product title on the
low-stock report (admin-UX INC-03). The Pricing & inventory screen already
fetched a row per product but had to send the operator to the detail leaf to
learn whether anything was in stock; the low-stock report listed bare SKUs.

`ProductSummary` (and the `GET /admin/products` wire) gains `onHand: number |
null`, and `LowStockRow` (and `GET /reports/low-stock`) gains `title: string |
null`. Both are REQUIRED fields on exported interfaces, hence `minor` for the
packages that export them; `store-postgres` changes adapter behaviour only and
stays `patch` — the same split as `title-single-writer`.
`ProductSummary` gains `onHand: number | null` and `LowStockRow` gains
`title: string | null`. Both are REQUIRED fields on exported interfaces, hence
`minor` for the packages that export them.

**`null` is not `0`.** `onHand: null` means there is no `inventory` record for
the sku — "unknown" — while `0` means a known sku that is out of stock. Nothing
Expand All @@ -26,34 +22,22 @@ two; the divergence is now documented on both sides of the port boundary.
its own field on the row — substituting it would make "named SKU-42"
indistinguishable from "name unknown".

**Shape, chosen from measurements, not estimates.** Postgres 16, 5,000 products
/ 3,997 inventory rows (~20% deliberately carrying no inventory record).
**Shape, chosen from measurements, not estimates.** Carrying stock on the list
projection itself was measured against the alternative of leaving each caller to
issue a per-row `getOnHand`: the N+1 cost several times the single joined read at
a 5,000-product catalog, in parallel and worse in sequence, on loopback and
before any real network. The projection is also unconditional rather than gated
on a "low stock only" filter — the gated variant measured *slower*, because it
must walk far more rows to fill a page.

*Products list, page size 25* — a single unconditional `LEFT JOIN` costs p50
0.43 → 0.58 ms and p95 0.61 → 0.91 ms, where an N+1 of per-row `getOnHand` reads
cost 2.60 ms p50 in parallel and 6.36 ms sequential: 6x and 15x the baseline, on
loopback, before any real network. The join is therefore unconditional rather
than gated on a "low stock only" filter — the gated variant measured *slower*
(1.15 ms), because it must walk ~9x the rows to fill a page. **No index and no
migration**: the join's inner side is already `inventory`'s primary key, and a
covering index cut buffers 28% without moving wall-clock at all.
The low-stock report's title half is the more expensive one, disclosed as such:
its cost is linear in CATALOG size rather than in the number of low-stock rows.
At a 5,000-product catalog that is comfortably inside the report's budget. Named
follow-up if low-stock latency ever matters: **bound the low-stock report** — it
currently returns every row at or below the threshold, unpaginated.

*Low-stock report* — the title join is the more expensive half, disclosed as
such: p50 2.915 → 4.858 ms (+67%), p95 6.89 → 7.48 ms. The planner picks a Hash
Right Join whose build side is a **Seq Scan over `product_commerce`**, so this
query's cost is linear in CATALOG size, not in the number of low-stock rows. At
5,000 products that is 121 shared buffers and ~4.0 ms of execution, comfortably
inside the report's budget. The partial unique index
`product_commerce_live_sku_unique` remains available to the planner and should
flip it to a nested-loop index lookup once the catalog grows enough for the seq
scan to lose. No index was added, per the user's ruling on §5.1. Named follow-up
if low-stock latency ever matters: **bound the low-stock report** — it currently
returns every row at or below the threshold, unpaginated — before reaching for
an index.

`lowStock`'s title join carries `AND product_commerce.deleted_at IS NULL` on its
ON clause. That predicate is load-bearing, not defensive: sku uniqueness on
`product_commerce` is a PARTIAL unique index over live rows, so a soft-deleted
product may legally hold a sku a live row also holds — without the predicate
such a sku would emit a DUPLICATE low-stock row and could be titled by the dead
product. Pinned by a contract case and an HTTP case on both dialects.
A soft-deleted product must not title a low-stock row or emit a second one.
Sku uniqueness is scoped to LIVE products, so a deleted product may legally hold
a sku a live product also holds; the report excludes deleted products from the
title lookup for that reason, and the exclusion is pinned by its own contract
case rather than left to the adapter.
4 changes: 2 additions & 2 deletions .changeset/admin-products-stock-column.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,15 @@ bare SKUs, and the SKU→title mapping lived in the operator's head.
reading aid, identity travels in the option's value, and nothing parses a label
back into fields.

No service, wire, or schema change: this is the console rendering `onHand`, which the
No port, wire, or schema change: this is the console rendering `onHand`, which the
admin products list projection already carries.

Three consequences worth carrying forward, none of them blocking here:

- The filter panel is now AT `MAX_FILTER_FIELDS` (4). The next filter added to this
screen makes `filterPanel` throw, so the increments that revisit filters have to
cut a field or raise the cap deliberately.
- Each list and detail render now makes one extra, uncached `GET /settings`. It is
- Each list and detail render now makes one extra, uncached settings read. It is
deliberate and cheap: it runs in parallel with the reads beside it, so it costs no
added latency, and it cannot fail either screen.
- The back button drops every filter on this screen, the low-stock toggle included.
Expand Down
Loading
Loading