feat(app): mount the Rewards section on a Content-tab store row - #405
Conversation
The reward-distributor fact layer (`rewards::pane::rewards_sections`) shipped with nothing in the running app mounting it. This mounts it as a collapsed-by-default Rewards section reachable from a Content-tab store row. Placement is Content -> store row -> in-place disclosure -> Rewards section. There is no store-DETAIL screen in this window today and adding one would mean adding a screen to `window_model`, which this lane does not own, so the row grows the section beneath itself instead. Six tabs are untouched. Rendered in `content.rs`'s own hand-painted idiom, not the `Section` model: the four async states are a private `RewardsBody` enum decided before layout, the shape `wallet_coins::SectionBody` uses. The store-id lookup is an explicit, tested conversion between the pane's 64-hex `String` and the rewards wire's `[u8; 32]`, so `HostedStore` never becomes a money type. A store nothing has reported on is amber with a remedy, never the empty state: no released dig-node answers `dig.listRewardDistributors` (-32601), and "no distributor exists for this store" is a positive claim only an answered read may make. No create, mint, refill or clawback affordance ships, not even disabled. Refs #3273 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`store_rewards_gallery` writes one PNG per async state through `photograph_shell`, which reads the real framebuffer back rather than asking GDI — GDI is blind to a GL surface and returns a plausible black rectangle. Both halves of the state are planted before the first frame: the section is collapsed by default, and its reading comes from a node. A committed screenshot must never be taken after synthetic input. The READY record is built inside dig-app-core because `RewardCounters`'s fields are `pub(crate)` on purpose — an example must not be able to assemble a money record — and it is rendered by the shipping formatter, which is the part a picture is evidence about. Refs #3273 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ead helpers `pub` inside a private module does not escape it, so `store_key_of_bytes`, `forget_all` and `seed_expanded` read as dead code under `-D warnings`. The fix is a real caller rather than an allow: `seed_preview` now converts the row's store id to the wire's `[u8; 32]` and keys the reading off that, which is the same conversion a node-backed read will make — a fixture keyed by a retyped string would photograph a record about a different store than the row above it. `forget_all` is test-only and says so. Refs #3273 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`shoot-gallery.ps1` runs the new example, and the README names the file pattern and says plainly that the READY capture's figures are a fixture -- no released dig-node answers `dig.listRewardDistributors`, so a live distributor cannot be photographed, and the picture is evidence about the shipping formatter and the layout rather than about the numbers. Refs #3273 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A feature: the Content tab's store rows now reach the Rewards section. Refs #3273 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ises The predecessor lane wrote docs/gallery/README.md's row for content-store-rewards-<state>-light-960.png but was capped before the example that produces them ever ran, so the row documented four pictures that did not exist (dig_ecosystem#3273). Built examples/store_rewards_gallery.rs (gui feature, openssl-sys vendored build) and ran it against docs/gallery. Each PNG is a real DPI-aware GL framebuffer read via ViewportCommand::Screenshot at 960x900 logical / 1920x1800 physical px, with nothing clicked or dragged -- staged through CaptureStaging::rewards before the first frame, same discipline as the rest of the gallery. Verified the ready capture still shows the stated fixture figures (a prover reading built in-process, not a live distributor), matching the README row's existing "stated FIXTURE" framing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
d050b6e to
16173d1
Compare
loop-security audit — PASSHead audited: Findings by threat lens
Held but not gating
Scope auditedFull diff Read-only — no merge performed, no edits made. 🤖 Generated with Claude Code |
Adversarial gate (third leg) — CHANGES-REQUIREDHead attacked: I attacked subjects, not values, and I read the four committed captures as evidence rather than only the source. Two blocking findings, three follow-ups. The first blocking finding is the thesis defect in its wrapper form, and the second is the reason the first survived three legs. F1 — BLOCKING. A compiled-in
|
… unaskable node amber The Rewards section on a store row is read by a PAYEE -- "Capsules mirrored here" lists stores this computer mirrors for someone else -- and it was saying two things it had no right to say. Finding 1. It passed a compiled-in `0` as the daily funding rate into `rewards_sections`, which made `CadenceReading::NoFundingRateChosen` the answer for every distributor with a written entry set: "Choose a funding rate to see how often a mirror would claim." That addressed a mirror operator as the funder, was a dead control made of words (no funding affordance ships anywhere), and rendered a compiled-in zero as a claim about on-chain funding (SPEC 2.6 clause 2). `rewards::cadence` is scoped to a cadence shown beside a CHOSEN funding amount, and this surface has no such amount -- so it now renders only the three facts it can honestly support: prover status, entry set, payout total. The dropped section is proved to be the cadence one rather than assumed. Finding 5. Nothing calls `remember` on a user path, so `PaneState::Unreachable` was the only state a real person could reach -- amber, every row, every install. `RewardsBody::NotAnswerable` splits out of it: no read taken, or a node answering JSON-RPC `-32601`, is drawn in the recessed treatment with its remedy in the sentence (`content-store-rewards-not-answerable`, already shipped in all 14 catalogs), and amber is now reachable only from a read that genuinely failed on the transport. `state::neutral_note` is the treatment, not a fifth pane state. Finding 4. `store_rewards_tests`'s doc comment claimed the fact sentences were guarded in `rewards::copy`; that module's only sweep is a funding-floor phrase sweep, so SPEC 12.5 clauses 6 and 7 over the rendered sentences were asserted by nothing. The comment is corrected and the missing assertion is made here, over every record shape a person can reach. Finding 2 (part). The gallery gains the `not-answerable` state -- the one a real install actually shows -- and its window height is raised past the point where the section closes, so the payout figure is inside the frame instead of below it. Captures re-shot separately. Tests: `the_facts_shown_are_the_shipped_fact_layers_own` compared `body_of` against `rewards_sections` while passing the same constant -- green by construction, at any value, which is why it covered the cadence sentence. It is replaced by an assertion about the rendered sentences, plus new tests for the dropped section, the party the sentences address, the amber reachability set, and the -32601 classifier. No `.ftl` value is added or changed. Refs dig_ecosystem#3273 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…on is in frame
At 960x900 the `ready` frame ended after the second of four sentences.
`rewards-paid-out-total` -- the only $DIG figure this surface renders -- and the
cadence sentence that finding 1 was about appeared in no committed capture, and
a frame that cuts mid-content looks exactly like a frame that ends because the
content ended. That is why the defect survived three gate legs and a human
inspection: it was the sentence a person spots on sight, and it was off-frame.
Re-shot at 960x1240 logical (1920x2480 px), five states, every one of them
closing inside the frame: the Reward distributor panel's own bottom edge, the
"Capsules mirrored here" card border beneath it, and the whole following
"Mirror another store" card are all visible below the last sentence, which is
the element that proves nothing was cut.
What is inside each frame:
waiting no money figure, no role sentence; one recessed banner
not-answerable no money figure; the recessed "your node cannot yet be asked"
sentence -- the state a real install shows today
unreachable no money figure; the amber banner wrapping the node's own
reason
empty no money figure; the recessed "your node answered: no reward
distributor" sentence
ready one money figure, fully visible: "This distributor has paid
12.5 $DIG in total to mirrors, as of the last completed cycle
at unix time ...", plus the prover-status and mirror-count
sentences. Three sentences, no cadence sentence, and nothing
telling the reader to choose a funding rate
Still a real DPI-aware framebuffer readback through
`ViewportCommand::Screenshot`, still no synthetic input, still the pinned scale.
The banner treatments are verified by pixel, not by eye: amber (251,243,224)
appears in the `unreachable` frame and in no other.
Refs dig_ecosystem#3273
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`body_of` deciding the right body is half the claim. The other half is that the paint code has an arm for it -- and the unanswerable note goes through a different painter from the three banners (`state::neutral_note` rather than `state::banner`), which is exactly the kind of second path that can be added and never called. A section that silently painted nothing would leave a person looking at an empty card, which is worse than the wrong colour. Measured against a CLOSED section rather than against zero, so the assertion is that the section took room on screen and not merely that the affordance above it did. Refs dig_ecosystem#3273 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comment said a fifth state could not be added upstream "without this array failing to name it". An array of tuples compiles perfectly well while naming five of six variants, so nothing there would have failed -- the claim was false in the commit that wrote it. What actually fails is `store_rewards::fixture_reading`, whose match over `RewardsPreview` is exhaustive, and that is the compiler error which sends the next person to this list. Refs dig_ecosystem#3273 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adversarial-gate round: F1, F2, F4, F5 addressed at
|
| capture | money figures visible | role-bearing sentences visible |
|---|---|---|
waiting |
none | none — one recessed banner, "Asking your node about the reward distributor for this store." |
not-answerable |
none | none — recessed, "Your node cannot yet be asked which reward distributor covers this store. It answers once you run a DIG node version that lists reward distributors." |
unreachable |
none | none — amber, "Your node could not be asked about the reward distributor for this store: the node closed the connection" |
empty |
none | none — recessed, "Your node answered: no reward distributor covers this store. One appears here once a distributor for this store exists on chain and your node has seen it." |
ready |
one, fully in frame: "This distributor has paid 12.5 $DIG in total to mirrors, as of the last completed cycle at unix time ..." | none. The other two sentences are "The prover is running and reporting on schedule." and "3 mirror(s) as of the last entry write at unix time ...". Nothing instructs the reader to fund, set a rate or choose anything |
Banner treatments are verified by pixel rather than by eye: the amber panel colour (251,243,224) appears in the unreachable frame and in none of the other four. The README row keeps its statement that the ready figures are a stated fixture.
F5 — amber is no longer the universal state
RewardsBody::NotAnswerable splits out of Unreachable. The routing:
body_of(None, _)becomesNotAnswerable— nothing has reported on this store. This is the state a real install shows today, on every store row, because no shipped dig-node serves the mapping read.Some(PaneReading::Unreachable(why)) if is_method_not_found(why)becomesNotAnswerable.is_method_not_foundmatches-32601and the message spellingsmethod not found,method_not_found,unknown method; the wire's code itself never reaches the screen.Some(PaneReading::Unreachable(why))staysUnreachable, amber, wrapping the node's own reason. The only amber state.
state::neutral_note is the recessed treatment, deliberately not a fifth PaneState: no new variant a pane can opt into, no new claim, and what is amber stays a value a test can compare (RewardsBody::painted() -> Painted). amber_is_reachable_only_from_a_read_that_genuinely_failed asserts over all five states at once that exactly one paints PaneState::Unreachable and names it, so a revert fails even if every sentence stays correct.
No read is wired, and none could be from this file set — there is no RewardsClient transport implementation anywhere in dig-app (only FakeRewardsClient), and the method answers -32601 on every released node regardless. Nothing was faked: the truthful state is reached by the absence of a reading, which is exactly what a real install has.
F4 — the false coverage claim, and the assertion that was missing
The comment claimed the fact sentences were "guarded there" in rewards::copy. It now says what is actually guarded: that module's only sweep, no_rewards_copy_contains_a_forbidden_phrase, is a funding-FLOOR phrase sweep (minimum funding, requires uptime, floor, gate) with no eviction, entitlement or accrual term in it — so SPEC §12.5 clauses 6 and 7 over the rendered sentences were asserted by nothing.
rewards/ is read-only to this lane, so the missing assertion is made here. the_rendered_fact_sentences_carry_no_claim_entitlement_or_eviction sweeps the rendered sentences of six record shapes for clause 6 (eligible, eligibility, claimable, claiming, will claim, entitle, accru, owed, earned, you will be paid) and clause 7 (evict, removed from, no longer in, never admitted, not admitted, dropped from).
Worth noting: that test is red on the pre-fix code — rewards-cadence-no-mirrors-yet reads "No mirror is claiming, so there is no cadence to state", an independent clause-6 breach the F1 removal also carries away.
F3 — untouched, and not made more prominent
Neither rewards-entry-set-never-written nor rewards-entry-set-known is edited, and the F1 fix does not separate the pair: the two are mutually exclusive per record and were never rendered together. The entry-set sentence keeps its position (second in the section) and its treatment. The section is three sentences instead of four, so nothing was moved, restyled or promoted.
Verification
- Revert-proof rather than a bare green: reverting the two production lines (
FACTS_THIS_MOUNT_CAN_SUPPORT = 4;Noneback toUnreachablewith the guard arm deleted) turns 9 of 17 tests in this module red. The new tests fail for the reason they were written for. cargo test --workspaceall green locally.dig-app-core --lib2949 to 2955 passed, 4 ignored unchanged; thestore_rewardsmodule goes 11 to 17 tests.cargo fmt --all --checkclean;cargo clippy -p dig-app-core --all-targets -- -D warningsclean.- Blast radius:
as_statehad no caller outside this module;RewardsPreviewcrosses onlygui/mod.rs's re-export andshell.rs'sCaptureStagingfield, neither of which matches on it;state::neutral_notehas one caller. Grep-verified — the gitnexus index is not fresh for this worktree. - Six tabs untouched;
tab_id_all_is_still_the_six_labelsandactivity_tab_emits_zero_action_rowspass. No.ftlvalue added or changed in any of the 14 catalogs. No affordance added, disabled or otherwise.
🤖 Generated with Claude Code
Security re-audit — PR #405PASS Scope: diff Findings, verified against the diff
Standing checks re-verified on this head
Known, not re-filed
Not coveredDid not re-run the full test suite locally (background compile did not finish in time); relying on the stated 11/11 green CI including Worktree KG: dig_ecosystem#3273 finding set (compiled-zero-as-claim, wrapper-vs-element recurrence of a fixed defect class, method-not-found vs transport-failure classification, amber-as-warning-fatigue, sweep-count assertions as non-vacuity proof) confirmed closed on dig-app PR #405 head d0f3061. |
Adversarial re-gate — CHANGES-REQUIREDHead attacked: One blocking finding, two follow-ups. All five prior findings verified closed on their own terms. BLOCKING — V1: the unanswerable note blames the reader's node version for a reader this app never ships
The second clause is false at this head, and not for a future reason. Nothing in
So This is the same class as finding 1 and reaches every row of every install exactly as finding 5 did: a dead control made of words, addressed to a payee, about whether they are being paid. The honest form already ships two cards below it and is visible in the same captures:
Remedy is one clause naming the app's own gap instead of — or alongside — the node's: 14 Follow-up — V2: the funder sweep's stated coverage overstates what it checks
False. The guard itself survives: Follow-up — V3: the new clause-7 sweep passes by avoiding the shipped phrasing
Prior findings — verified closedF1 — closed, and the residual is genuinely unreachable by a reader. F1 adjacency attack — no fourth form found in the three-sentence shape. With cadence gone, the section ends on "This distributor has paid 12.5 $DIG in total to mirrors, as of the last completed cycle at unix time 1789072916." That sentence carries its own subject ( F2 — closed. All four remaining frames verified at 1920x2480 against the amended rule. F5 — closed. F4 — closed as to the false comment; the new sweep exists and runs over every reachable record. Its needle coverage is V3. F3 — not made more prominent. Dropping the cadence sentence shortened the section to three; Revert-proof — the stated reds would be for the stated causes. Both mutations are to a Not re-filed
KG: |
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
Verdict: PASS
Head reviewed: d0f3061f90bbeb42f34b96f49e1e9ea716e3d3c4 (base 9c85d140, the tip of main). Full diff read against main, not a delta — this is the first verdict on this PR (no prior reviews or threads existed). CI: 11/11 green at this head, confirmed via gh pr checks.
What I checked against the brief, one by one
1. Money honesty and subject. crates/dig-app-core/src/rewards/pane.rs::payout_sentence (pre-existing, unchanged by this PR) formats every figure through amount_with_unit, and the catalog string is explicit about whose money it is: rewards-paid-out-total = "This distributor has paid { $amount } in total to mirrors, ..." (i18n/en.ftl:34). Confirmed visually in the committed content-store-rewards-ready-light-960.png capture — the sentence reads "This distributor has paid 12.5 $DIG in total to mirrors," never framed as this operator's own earnings. No hand division by 1000 anywhere in the new files.
2. The cadence-drop fix (finding 1). store_rewards.rs's FACTS_THIS_MOUNT_CAN_SUPPORT = 3 plus .take(3) on rewards_sections's four sections confirms only prover/entry-set/payout reach the screen; store_rewards_tests.rs::the_dropped_section_is_the_cadence_one_and_nothing_here_renders_it independently re-derives the fourth section from rewards_sections and asserts it is the cadence one (not vacuous — it fails if rewards_sections ever reorders, and fails if the mount ever renders 4). no_sentence_here_addresses_the_reader_as_the_funder sweeps all 6 reachable record shapes plus banner text for "funding rate"/"choose a"/etc. and additionally asserts an exact sentence count (18), so a reversion back to 4 sections breaks it on count alone, not just content.
3. The NotAnswerable vs Unreachable split (finding 5). is_method_not_found is tested in both directions (a_method_not_found_answer_becomes_the_unanswerable_note_and_not_amber) — real spellings recognized, and a genuine transport failure ("the node closed the connection") explicitly asserted NOT to match. state.rs::neutral_note and PaneState::Waiting/Empty all route through Look::Neutral; only PaneState::Unreachable gets Look::Problem (amber). Confirmed visually: content-store-rewards-not-answerable-light-960.png renders in the recessed/neutral treatment, content-store-rewards-unreachable-light-960.png in amber, side by side with identical layout.
4. No fabricated input reaches a reader. The cadence rendering is gone from the mount entirely (see #2) — confirmed no cadence sentence in the ready capture (3 sentences only, card border visible below the last one, nothing cut off).
5. i18n. All 7 new content-store-rewards-* keys are real, distinct per-locale translations in all 14 catalogs (spot-checked de/es/fr/hi/ja/ko/ru/tr/vi/zh-CN/zh-TW — genuinely translated prose, not English echoed back). The existing anti-hardcoding guards (every_locale_carries_every_key_and_no_more, a_locale_is_not_english_in_disguise, digit_free_where_english_is_digit_free) are untouched and passed in CI. store_rewards_tests.rs::every_sentence_resolves_through_the_catalog and this_modules_own_sentences_carry_no_figure close the specific blind-spot named in the brief (hardcoded English builders beside a translated catalog) for this module's own banner sentences.
6. SPEC §12.5 clauses 6 and 7. no_sentence_here_offers_a_claim_status_or_an_eviction (this module's own 4 banner strings) and the_rendered_fact_sentences_carry_no_claim_entitlement_or_eviction (swept over all 6 reachable record shapes for the 3 rendered fact sentences) both assert absence of eligible/claimable/accru/owed/evict/never-admitted/etc. Together with rewards/pane.rs's own no_mirrors_yet_sentence_carries_no_never_admitted_vs_evicted_distinction (pre-existing, unchanged), clause 6 and 7 are covered at both layers.
7. Six tabs. window_model.rs is untouched by this PR (not in the diff at all) — no seventh tab, no new TabId variant.
8. store_id conversion. store_key/store_key_of_bytes/store_bytes tested for 0x prefix, case folding, and refusal (not truncation) of anything not exactly 32 bytes of hex (text_that_is_not_a_store_id_is_refused_rather_than_truncated), plus round-trip agreement between the wire's [u8;32] and the pane's string (a_store_id_string_and_the_wires_bytes_agree_on_one_key, a_remembered_reading_is_found_from_either_form_of_the_id).
9. No dead control. Grepped store_rewards.rs, store_rewards_tests.rs, and the content.rs diff for clawback references — zero reachable references. No create/mint/refill affordance in the new section.
10. Known, ticketed, non-blocking. Confirmed dig_ecosystem#3297's exact defect is present and reachable through this mount: rewards-entry-set-known and rewards-paid-out-total both carry raw unix-time placeables (i18n/en.ftl:33-34), visible in the ready capture ("unix time 1789069256" / "unix time 1789072916"). Nothing is false — correct subject, formatted amount, just an unreadable timestamp — so I agree this is non-blocking as already ruled.
11. The revert-proof claim. I traced the 17 tests in store_rewards_tests.rs by hand against the described revert (FACTS_THIS_MOUNT_CAN_SUPPORT = 4, routing None back to Unreachable). At minimum these fail for the stated reason, not incidentally: every_state_reaches_the_screen_as_its_own_body and an_unremembered_store_is_not_reported_as_having_no_distributor (the matches!(_, RewardsBody::NotAnswerable(_)) assertion becomes false), a_failed_read_names_the_nodes_own_reason_and_an_unasked_one_does_not (destructure panics), the_dropped_section_is_the_cadence_one_and_nothing_here_renders_it and the_three_sentences_shown_are_the_catalog_sentences_for_this_record (hardcoded length 3 vs actual 4), and no_sentence_here_addresses_the_reader_as_the_funder (hardcoded sentence-count sweep 18 vs actual 24, plus direct content match on "choose a"/"funding rate"). That is 6 of the claimed 9 confirmed by direct assertion-tracing; I could not execute the mutation locally to get the exact count — this environment's local cargo test fails during openssl-sys's vendored build (perl ./Configure errors on a missing Locale::Maketext::Simple module), a pre-existing local toolchain gap unrelated to this PR (CI's hosted runners don't hit it). Stated as not run rather than glossed over.
Screenshots
Viewed all 5 committed captures directly (docs/gallery/content-store-rewards-*-light-960.png, 1920x2480 = 960x1240 at 2x, matching store_rewards_gallery.rs's SIZE constant). ready shows exactly 3 sentences with the card border below the last one (nothing cut); not-answerable and unreachable are visually distinct (neutral lavender vs. amber); all match the code paths above.
Findings
None blocking. No inline threads opened — nothing here requires one.
What I did not run
Live mutation testing of the revert-proof (see #11) — blocked by a local Perl/OpenSSL toolchain gap in this sandbox, not by anything in the PR. Did not re-run the native confirmer or coverage jobs myself; relied on the green CI run (Test + coverage: 92.17% total line coverage, well over the 80% gate) and read its log directly to confirm the 17 store_rewards tests all pass at this head.
KG: dig_ecosystem#3273 — reward-distributor fact layer mounted; two adversarial-gate defects (funder-address cadence leak, unconditional-amber unanswerable state) verified closed by independent, non-vacuous tests and matching screenshots; #3297 (raw-epoch i18n keys) reconfirmed reachable and correctly ruled non-blocking.
🤖 Generated with Claude Code
…ig-app causes
The not-answerable sentence's second clause promised the reward-distributor
read would work "once you run a DIG node version that lists reward
distributors." False on both sides of the wire: dig-app never sends
`dig.listRewardDistributors` on any user path (`remember`'s only caller is
the gallery preview), and no dig-node build implements the method either.
A mirror operator upgrading their node changes nothing, on either side.
Reword all 14 catalogs to attribute the absence to DIG and promise no
node-version fix, matching the register already shipping in the "Mirror this
store" caption. Correct the two doc comments that also described the
sentence as carrying "its remedy," and the module-doc line that implied only
released nodes are missing the method. Re-shoot the affected gallery
capture under the amended criterion (1920x2480 px, section closes in
frame).
Also corrects two test-comment inaccuracies caught alongside this: the
funder-role sweep's comment claimed a coverage property ("every cadence
sentence contains funding rate") that `rewards-cadence-no-mirrors-yet`
disproves — the guard actually holds via the sweep count and the clause-6
list, not this one; and the clause-6/7 sweep's doc now names
`rewards-entry-set-never-written` as a sentence it passes by wording, not by
the never-admitted-vs-evicted property holding, citing dig_ecosystem#3300
for that pair-wise property.
Refs dig_ecosystem#3273 adversarial gate.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adversarial gate (leg 3) — PASS at
|
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
PASS — head 5dd37448 (narrow re-review of d0f3061f..5dd37448, i18n/comments/capture only, per remit).
Scope confirmed by measurement, not assertion: 17 files, .ftl catalogs + doc comments + one capture, zero non-comment Rust logic changed (git diff --stat shows no .rs insertions beyond comment lines; ran cargo test -p dig-app-core --lib before and after touching nothing — full suite green, 2955 passed).
- All 14 catalogs. Read every diffed
.ftlline. Each non-English locale's new sentence is a real translation carrying the same two claims asen.ftl:44("DIG does not send that question yet" / "nothing about your node changes that") — none reintroduced a node-version, release, or timeline promise (the old "It answers once you run a DIG node version…" / "wenn Sie eine DIG-Knoten-Version ausführen" / "quando você executar uma versão do nó DIG" pattern is gone from all 14, including de/es/fr/hi/id/ja/ko/pt-BR/ru/tr/vi/zh-CN/zh-TW). every_locale_carries_every_key_and_no_more,a_locale_is_not_english_in_disguise: both pass (cargo test -p dig-app-core --lib i18n::→ 11 passed, 0 failed). Anti-hardcoding:SECTION_TITLE/SHOW/HIDE/WAITING/EMPTY/NOT_ANSWERABLE/UNREACHABLEinstore_rewards.rs:84-97are all stillMsg::new("content-store-rewards-*")catalog lookups — no English literal reintroduced beside the catalog (the historical blind-spot this PR family shipped once, #3273 finding).- Confirmed the specific claim:
store_rewards::remember(line 253) has exactly one caller in the crate,seed_preview(line 487→495), and the module ispub(crate) mod store_rewards(pane/mod.rs:104) so nothing outsidedig-app-corecan reach it either.grep -rn "listRewardDistributors" /tmp/dig-node/→ zero matches, consistent with the doc's "no dig-node build, released or otherwise, implements it" claim. - Both doc-comment changes verified true at this head:
store_rewards.rs:25-49module doc ("gap is on both sides of the wire") and theNotAnswerablevariant doc (store_rewards.rs:163-168, "no promised fix") — matches (3) above and the new.ftltext; neither promises a remedy that doesn't exist. store_rewards_tests.rs:swept == 18(line 539) and"claiming"present inCLAUSE_6(line 663) — both match the comment's citation exactly.rewards-entry-set-never-written's English string ("Entry set: never written. No peer has been added to this distributor yet.") contains none ofCLAUSE_7's needles (evict,removed from,no longer in,never admitted,not admitted,dropped from) — confirmed by direct grep — so "passes by wording, not by the property holding" is accurate, and thedig_ecosystem#3300cross-reference is descriptive only (no needle added, no logic changed, as stated).- Capture: opened
content-store-rewards-not-answerable-light-960.png— renders the exact new sentence ("Your node cannot yet be asked which reward distributor covers this store. DIG does not send that question yet, and nothing about your node changes that."), matching shippeden.ftl. The other 4content-store-rewards-*gallery frames (empty,ready,unreachable,waiting) show zero diff in this delta — confirmed byte-identical tod0f3061f, consistent with the reverted font-rasterization noise.
Flake claim sanity-checked, not just accepted: cli_session::engine_proxy.rs is untouched by this delta (confirmed via git diff --stat, no engine_proxy entry) and lives in an unrelated module (cli_session vs. confirm::gui). Ran the full cargo test -p dig-app-core --lib myself: 2955 passed, 0 failed, including no_node_anywhere_is_not_connected_and_names_the_ladder. The test's dead_endpoint() helper (line 207) binds an ephemeral port then drops the listener before use — a genuine TOCTOU race under parallel test execution if another test's FakeNode binds the reclaimed port in the interim, matching a real "green under --test-threads=1, flaky otherwise" shape rather than a masked regression. Given the file is outside this diff's blast radius entirely, this could not be a regression introduced by d0f3061f..5dd37448.
No findings. No threads opened.
KG: content-store-rewards-not-answerable copy fix (dig-app#405) — i18n re-review confirmed the false node-version promise was removed from all 14 locales (not just en), store_rewards::remember's single caller is gallery-only seed_preview (module is pub(crate), unreachable from outside the crate), and the cited port-bind-then-drop pattern in engine_proxy.rs::dead_endpoint() is a plausible parallel-test-race source, unrelated to this PR's diff.
DO NOT MERGE — gate round in progress. Draft until the orchestrator lane says otherwise.
Refs #3273What this does
crates/dig-app-core/src/rewards/pane.rs::rewards_sectionsshipped fourSectionfacts (prover status, entry set, payout, cadence) that nothing in the running app
mounted. This mounts them.
Placement: Content tab → store row → in-place disclosure → Rewards section,
collapsed by default.
Why an in-place disclosure and not a store-detail screen
There is no store-detail screen in this window today. Adding one means adding a
screen to
crate::window_model, which this lane does not own (single-writer), anda seventh tab is forbidden by
tab_id_all_is_still_the_six_labels. So the storerow grows the section beneath itself. The parent object is the row directly above,
which is the relationship the placement is meant to express: a distributor cannot
exist without the locally-held store it pays for.
The idiom
content.rs:373 capsule_rowdoes not use theSectionmodel — it is hand-paintedimmediate-mode egui. So the section follows this file's own idiom:
RewardsBody { Waiting | Unreachable(String) | Empty | Facts(Vec<String>) },decided before anything is laid out — the shape
wallet_coins.rs:211 SectionBodyuses.card::interactive_card(already used atcontent.rs:145) replacescard::cardonthe capsules card, so the pane's own
livereaches the disclosure instead ofcard::card's hardcodedtrue.state::banner, so a wait is recessed and only a fault is amber.The store-id conversion is tested, not assumed
HostedStore::store_idis a lowercase 64-hexString; the rewards wire carriesstore_id: [u8; 32].store_key/store_key_of_bytesare the two halves, withcases for
0xprefix, case folding, and refusal (never truncation) of anything thatis not 32 bytes of hex. A lookup that silently never matched would look exactly like
a store with no distributor — a wrong claim about money that no green test catches.
Honesty: an unremembered store is amber, never empty
No released dig-node answers
dig.listRewardDistributors— it returns-32601. So ontoday's nodes nothing can map a store to a distributor. That is painted as
PaneState::Unreachablewith a remedy (a newer node version), and not asPaneState::Empty: "no reward distributor covers this store" is a positive claim onlyan answered read may make, and making it from an unanswerable one is the absence-as-zero
failure SPEC §12.5 clause 6 forbids.
store_rewards_tests::an_unremembered_store_is_not_reported_as_having_no_distributorpins both halves.
Ship no dead control
No create, no mint, no refill, no clawback affordance — not even a disabled one.
dig.listRewardDistributorCommitmentsis unserved, so any such control could only fail.SPEC §12.5
No claim status, no accrual, no entitlement, no
eligible/claimingword derived froma distributor existing (clause 6); nothing distinguishes never-admitted from evicted
(clause 7). Both swept by
no_sentence_here_offers_a_claim_status_or_an_eviction.Money
Every figure comes from
rewards_sections, which formats throughamount::format_asset_amount. This module renders no figure of its own, pinned bythis_modules_own_sentences_carry_no_figure.i18n
Seven new keys in all 14 locales, as real per-locale translations. The existing
i18n::testsguards (every_locale_carries_every_key_and_no_more,a_locale_is_not_english_in_disguise,brand_literals_survive_translation,digit_free_where_english_is_digit_free,no_catalog_value_carries_a_torn_run)are unchanged and not weakened.
Blast radius
rewards::*had zero consumers outsidesrc/rewards/before this PR(
grep -rn 'rewards::' --include=*.rs src/ | grep -v '^src/rewards/'— empty), sothis is the first mount. Within
content.rsthe changed functions arecapsules_cardand
capsules;capsule_rowis untouched. gitnexus was NOT-INDEXED in this worktree,so the radius was measured with ripgrep plus reading, and that is stated rather than
implied.
Not done yet in this PR
Screenshots and the gallery that produces them — a following commit on this branch.
🤖 Generated with Claude Code