Package garden is Gnotchi's pet realm, v4: garden/v3 plus a permanent face.
WHY THIS EXISTS, in one paragraph, because a realm cannot be edited and the reason has to travel with the code. garden/v2 accepts `plant:0:<ts>` and `harvest:0:<ts>`, enforces the grow timer, moves the acorns -- and exposes no way to read a plot back. Its whole garden is write-only on chain. The app therefore hides the garden from any player who connects a wallet (app/js/home.js gates the garden card on `!gnome.chainProof`), and since harvesting is the only acorn income, a connected player's acorns can only ever go down. The live site now apologises for this in six places (app/js/config.js's WALLET_TRADEOFF_SENTENCE). There is no fix that is not a new realm. GardenState below is that fix; everything else here is either the same correction applied to eight more unreadable fields, or deliberately unchanged from v2.
Full design, including every v2 limitation with line numbers, the migration argument, and the list of things deliberately left out: realms/garden/v3/DESIGN.md, which this realm inherits whole, and realms/garden/v4/DESIGN.md, which records only what v4 changes and why. Read both before changing anything here.
v4 IS v3 PLUS A PERMANENT FACE, AND THREE SMALL REPAIRS. garden/v3 was deployed to onyx at block 336496 on 2026-10-11 and nobody adopted on it -- TotalGnomes() read 0 on 2026-10-12 -- so replacing it costs the deploy gas and nothing else: no migration, no gnome to move. What changes: every gnome carries two more PERMANENT appearance ids, eyes and nose, each a small integer validated against a compiled-in range and written exactly once, at Adopt. That is SPRITES.md's "Phase 1 -- faces" (ten eyes, eight noses: sixteen looks become 1,280), which v3's DESIGN.md section 7.3 had argued away by applying a correct argument against OPEN-ENDED appearance to a closed list nobody had asked to be open. The realm records what it is given and validates it; whether a face is CHOSEN by the player or ROLLED by the app is deliberately not decided here, because a realm cannot be edited and that decision should not be frozen in one (DESIGN.md v4 section 3). The repairs, each cheap only while nobody has adopted: the acorn softlock AUDIT-DEPLOYED.md calls G9 is made unreachable by an acorn reserve in SaveDay; a reserved buyer typed in upper case no longer creates a listing nobody can buy; and the test-only fee override is gone from production code. Everything else -- the SetRule ratchet, the garden-settles-to-seller rule, the IsUserCall gates on Adopt and Buy, the sale, the wire format, every read -- is v3's, unchanged, with v3's reasoning kept in place below.
GNOMES ARE TRADEABLE HERE, AND THE SALE COUNTER IS THE ONLY DOOR. TRADING-RULES.md records the owner's ruling of 2026-10-10 -- "gnomes AND items are tradeable, and a cut on every trade of both is a must" -- and MARKETPLACE-V1.md records the structural fact that decides the shape: a royalty implemented as a transfer hook sees transfers, not payments, so the only way to make the cut unavoidable is for the contract holding the asset to refuse every transfer path except its own sale function. That is what this realm does. There is no Approve, no TransferFrom, no gift, no burn, no withdrawal; a registered extension panics on every ledger movement unless Buy below is moving exactly that token right now. See the SALE section and DESIGN.md section 9. The fee and its recipient are the three SALE CONSTANTS at the very top of this file, decided by the owner on 2026-10-11 for the onyx testnet; init still gates them and refuses a value that could not work.
DEPLOY STATUS IS DELIBERATELY NOT RECORDED IN THIS FILE, and neither is a claim about whether anyone has decided to deploy it. This comment ships on chain exactly as written and cannot be edited afterwards, so any status asserted here is frozen at authoring time and becomes a lie the moment it changes. garden/v2/gnomod.toml said "NOT deployed" in just such a line, was deployed hours later, and now carries that false claim on chain permanently. Live status lives in the repo root's STATUS.md and DECISIONS.md, which can still be corrected; the authority above both is the chain itself (`vm/qfile` on this path).
============================================================================ THE ONE RULE THAT REPLACES v2's MISTAKE ============================================================================
v2's bug class is not "a getter was forgotten". It is that a field was added to persisted state without a reader, on a contract that can never grow one. So:
Example
1EVERY PERSISTED FIELD HAS AN EXPORTED READER, AT DEPLOY TIME. THERE IS NO SECOND CHANCE.
Anyone adding a field to `gnome` below must add it to a reader in the same change. TestEveryStoredFieldIsReadable pins the current field list against the current readers, value by value, so the failure is at least loud and local. Gno has no reflection to assert over, so that test cannot *detect* a new field -- the rule is still a discipline and this comment is not pretending otherwise.
The corollary, which is why this file has no "extensible" blob getter: EXTENSIBILITY IS A FICTION ON AN IMMUTABLE CONTRACT. A versioned self-describing string cannot carry a field that did not exist at deploy time any more than a positional tuple can. This surface is designed for completeness now, not for growth later, because there is no later.
============================================================================ SECURITY: NO recover(), EVER. Carried over from v1/v2/hollowmere/arena verbatim in intent. ============================================================================
No code in this realm, now or later, may recover() around a call into ledger or token (grc721's PrivateLedger/Token). The sale-gate guarantee below is real, but it is NOT that the ledger write never happens -- it does, before the panic -- it is that an *uncaught* panic aborts the whole enclosing transaction, so that write (and everything else the transaction did) is discarded and never committed on chain. p/nt/grc721/v0/types.gno says this outright about extension hooks like saleGateExtension below: "Hooks run after the ledger write so they observe the post-movement state; only a caller that recovers from the panic keeps that write." A recover() anywhere between a ledger call and the transaction boundary -- even one added for an unrelated reason, even several call-frames away -- would silently convert every refused transfer into a permanent, successful one.
This is not limited to code textually inside this file: a pure `p/` package this realm calls runs in garden's own call frame too, so a helper as innocuous as `Try(func(){ ledger.TransferFrom(...) })` imported from some future `p/` dependency would break the same guarantee just as surely as a recover() written here directly.
The same rule applies to SaveDay's, ChooseType's and Buy's validate-then-commit pattern below: none recovers from its own internal panics, by design. Every check runs against a local value copy and nothing is written back until the whole call has validated; a panic before the commit leaves persisted state completely untouched, with no partial effect to undo. Buy extends the rule to money: the ledger write, the index writes and the two coin sends all happen after the last check that can fail, so a sale either happens whole or not at all -- there is no reachable state in which the gnome moved and the coins did not, or the reverse.