Skip to content

feat(app): mount the Rewards section on a Content-tab store row - #405

Merged
MichaelTaylor3d merged 11 commits into
mainfrom
loop/3273-rewards-mount
Sep 10, 2026
Merged

MichaelTaylor3d merged 11 commits into
mainfrom
loop/3273-rewards-mount

Conversation

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor

DO NOT MERGE — gate round in progress. Draft until the orchestrator lane says otherwise.

Refs #3273

What this does

crates/dig-app-core/src/rewards/pane.rs::rewards_sections shipped four Section
facts (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), and
a seventh tab is forbidden by tab_id_all_is_still_the_six_labels. So the store
row 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_row does not use the Section model — it is hand-painted
immediate-mode egui. So the section follows this file's own idiom:

  • Four async states as a private RewardsBody { Waiting | Unreachable(String) | Empty | Facts(Vec<String>) },
    decided before anything is laid out — the shape wallet_coins.rs:211 SectionBody uses.
  • card::interactive_card (already used at content.rs:145) replaces card::card on
    the capsules card, so the pane's own live reaches the disclosure instead of
    card::card's hardcoded true.
  • The state banners are state::banner, so a wait is recessed and only a fault is amber.

The store-id conversion is tested, not assumed

HostedStore::store_id is a lowercase 64-hex String; the rewards wire carries
store_id: [u8; 32]. store_key / store_key_of_bytes are the two halves, with
cases for 0x prefix, case folding, and refusal (never truncation) of anything that
is 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 on
today's nodes nothing can map a store to a distributor. That is painted as
PaneState::Unreachable with a remedy (a newer node version), and not as
PaneState::Empty: "no reward distributor covers this store" is a positive claim only
an 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_distributor
pins both halves.

Ship no dead control

No create, no mint, no refill, no clawback affordance — not even a disabled one.
dig.listRewardDistributorCommitments is unserved, so any such control could only fail.

SPEC §12.5

No claim status, no accrual, no entitlement, no eligible/claiming word derived from
a 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 through
amount::format_asset_amount. This module renders no figure of its own, pinned by
this_modules_own_sentences_carry_no_figure.

i18n

Seven new keys in all 14 locales, as real per-locale translations. The existing
i18n::tests guards (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 outside src/rewards/ before this PR
(grep -rn 'rewards::' --include=*.rs src/ | grep -v '^src/rewards/' — empty), so
this is the first mount. Within content.rs the changed functions are capsules_card
and capsules; capsule_row is 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

MichaelTaylor3d and others added 6 commits September 10, 2026 10:21
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>
@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

loop-security audit — PASS

Head audited: 16173d1dadb29d2205d8cbf28d1ada59ac408e68

Findings by threat lens

  1. Whose money. No new false-subject defect. content-store-rewards-empty reads "Your node answered: no reward distributor covers this store" — subject is the store, not the person's earnings. rewards-paid-out-total (unchanged, from the predecessor PR) reads "This distributor has paid { $amount } in total to mirrors" — correctly attributed to the distributor, not the viewer. This PR only mounts rewards_sections/rewards::pane (crates/dig-app-core/src/rewards/pane.rs, not in this diff); it introduces no new money-bearing sentence of its own — store_rewards.rs renders zero digits itself and is tested for it (this_modules_own_sentences_carry_no_figure, store_rewards_tests.rs).

  2. Absence-as-zero. body_of() (store_rewards.rs:114) has the required three-way split: None (nothing ever asked) → Unreachable(NOT_ANSWERABLE), Answered(None)Empty (the one state entitled to say "no distributor"), Answered(Some(_))Facts. Test an_unremembered_store_is_not_reported_as_having_no_distributor asserts the unasked and empty-answered cases differ. entry_count == 0 is handled upstream by EntrySetReading::{NeverWritten, Known{0,..}, Known{n>0,..}} (three cases, cadence.rs:51-66) — not collapsed.

  3. Enumeration/never-admitted vs evicted. New copy in all 14 locales checked; none pairs a raw count with an eviction-adjacent state. The store-level empty state ("no distributor covers this store") is a different axis than mirror admission/eviction and does not combine with a mirror count anywhere in the new layout (verified against the rendered empty and ready screenshots).

  4. Entitlement/claim framing from distributor presence. store_rewards.rs module doc + test no_sentence_here_offers_a_claim_status_or_an_eviction sweep for eligible/claiming/entitle/accru/evict/"never admitted"/"removed from" — none present. The section itself is gated on the row, not on distributor existence (it's attached to every store row, collapsed by default), so its mere appearance is not an entitlement signal.

  5. Dead/live controls. No create/mint/refill/clawback affordance anywhere in the new files. Grepped store_rewards.rs, content.rs, store_rewards_gallery.rs for clawback/listRewardDistributor* — only doc-comment references explaining why those methods are unserved. No caller of rewards::clawback exists in this diff.

  6. Data layer / store_id conversion. store_key/store_bytes/store_key_of_bytes (store_rewards.rs:195-235) correctly round-trip 64-hex text ⇄ [u8;32], reject anything not exactly 32 bytes of hex (no truncation), and are covered by a_store_id_string_and_the_wires_bytes_agree_on_one_key and text_that_is_not_a_store_id_is_refused_rather_than_truncated. In the mount site (content.rs capsules()), store_id is taken per-iteration from the same store the row itself renders (let store_id = store.store_id.clone()), so there is no shared/indexed lookup that could cross-wire one row's id with another row's distributor. HostedStore gained no money field.

  7. Compiled-in-constant share. NO_FUNDING_RATE_CHOSEN = 0 short-circuits in days_between_claims (cadence.rs:61-62) to NoFundingRateChosen before any division — no share is computed from it. Not part of this diff's changed files, already gated in the predecessor PR.

  8. Screenshots. All four PNGs (1920×1800, no EXIF/metadata) inspected via Read. Fixture data only: constant-byte launcher_id/root (0x11/0x33 repeated), a synthetic store id, no wallet address, no real key material. README's ready-is-a-fixture claim (docs/gallery/README.md) is true — no live-node path exists for this pass (dig.listRewardDistributors unserved).

Held but not gating

  • dig_ecosystem#3297 (raw-epoch-integer locale strings) confirmed pre-existing and unchanged by this PR; agree it's a legibility defect, not a security one — no false claim, correct subject, correctly formatted money. Not re-filed.
  • The transmute/UB test-harness note in the brief refers to crates/dig-app-core/src/rewards/test_scan.rs, which is not touched by this diff (not in the file list) — out of scope for this audit, not re-verified.
  • Cargo.lock's hand-resolved rebase touches exactly the 3 workspace-member version strings (15.5.2→15.6.0), matching Cargo.toml's version bump — no drift found, but I did not run cargo generate-lockfile --locked to confirm zero regeneration diff elsewhere.

Scope audited

Full diff origin/main...16173d1d (31 files): store_rewards.rs + store_rewards_tests.rs, content.rs mount, shell.rs/window.rs/mod.rs wiring, 14 locale .ftl files, store_rewards_gallery.rs, Cargo.lock/Cargo.toml, docs/gallery/README.md, the 4 committed PNGs, shoot-gallery.ps1. rewards/pane.rs and rewards/cadence.rs were read for context (whose-money / clamp checks) but are unchanged by this PR.

Read-only — no merge performed, no edits made.

🤖 Generated with Claude Code

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

Adversarial gate (third leg) — CHANGES-REQUIRED

Head attacked: 16173d1dadb29d2205d8cbf28d1ada59ac408e68. Read-only, fresh context, D:/worktrees/dig-app-3273.

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 0 becomes a sentence that tells a payee to set the funder's rate, pointing at a control this app does not have.

crates/dig-app-core/src/confirm/gui/window/pane/store_rewards.rs:84

const NO_FUNDING_RATE_CHOSEN: u64 = 0;

fed at store_rewards.rs:118 into rewards_sections(record, now, NO_FUNDING_RATE_CHOSEN).

For any store whose distributor has a written entry set (last_entry_write_at: Some, entry_count > 0reading.rs:103), cadence.rs:61 returns CadenceReading::NoFundingRateChosen and the fourth sentence a person reads is i18n/en.ftl:36:

Choose a funding rate to see how often a mirror would claim.

Three separate defects in one sentence, all created by the wrapper:

  1. Wrong subject — the predecessor defect's exact shape. The mount is on the Content tab's "Capsules mirrored here" card (visible in every committed capture, content.rs:275 capsules_card). That row is a store this computer mirrors for someone else. The reader is a payee. "Choose a funding rate" addresses them as the distributor's funder. Money consequence: a mirror operator concludes the rate at which they are paid is theirs to set, and that nothing is accruing until they act. On the predecessor it was the funder's total attributed to the operator; here it is the funder's role. Same misattribution, one layer up.
  2. A dead control made of words. This PR ships no funding affordance anywhere and says so ("Ship no dead control"). A sentence that names an action with no route is worse than the disabled button the PR correctly refused: a disabled button at least shows itself unavailable. There is nothing to choose, on any screen, in any version.
  3. §2.6 clause 2 in the wrapper — the trap reappearing exactly where it did last time. The 0 is not data. It is a constant this file compiled in, rendered as a claim about the distributor's funding state. cadence.rs:40-43 is explicit that this variant means "THIS pane has nothing chosen … mirrors may exist and be claiming under whatever rate is already funded" — and the rendered English drops that qualifier and states the funding condition instead. cadence.rs:1 scopes the module to "the claim cadence a funder is shown beside a chosen funding amount". This wrapper mounts it with no amount beside it, so the sentence has no truth conditions to be true against.

The one test over the rendered facts cannot catch this. store_rewards_tests.rs:216-229 the_facts_shown_are_the_shipped_fact_layers_own compares body_of(...) against rewards_sections(..., NO_FUNDING_RATE_CHOSEN) — the same constant. It is green by construction and would stay green if the constant were 1, or 10^9. And no test in the 303-line file asserts who any figure is about; the three subject-adjacent tests are about store-id keying (which store), never which party. The brief's thesis holds unchanged in this file.

Fix (small): do not emit the cadence Section in this mount — there is no rate to speak about, and the honest section is three facts, not four. Alternatively route rate == 0 to a sentence that states the pane's own limit without instructing the reader. Do not fix it by inventing a rate.


F2 — BLOCKING (one constant). The acceptance evidence for a money surface stops above the money.

crates/dig-app-core/examples/store_rewards_gallery.rs:58

const SIZE: (f32, f32) = (960.0, 900.0);

In docs/gallery/content-store-rewards-ready-light-960.png the Reward distributor card is visibly clipped by the image's bottom edge after sentence 2 of 4:

The prover is running and reporting on schedule.
3 mirror(s) as of the last entry write at unix time 1789053304.

Sentence 3 (rewards-paid-out-total — the only $DIG figure the section shows) and sentence 4 (F1's sentence) appear in no committed capture. Four screenshots are offered as acceptance for the first paint of a money surface, and the money is not in any of them.

This is not cosmetic and it is not separate from F1: the precedent you cite — you found #3297 by looking — cannot operate on a picture that ends above the figures. F1 is a sentence any person would have flagged on sight, and it is off-frame in the only artifact a person would look at. Raise the height (or capture the section alone) so all four sentences are in frame, and re-shoot after F1.


F3 — follow-up ticket (blocks the fact layer, not this mount). §12.5 clause 7 is reconstructible from two sentences together.

i18n/en.ftl:30 and :33 are deliberately different sentences:

  • never written → "Entry set: never written. No peer has been added to this distributor yet."
  • known → "{ $entry_count } mirror(s) as of the last entry write at unix time { $last_entry_write_at }."

So 0 mirror(s) as of the last entry write at unix time T tells a person two things at once: a write has happened (T exists), and the set is now empty. Therefore every peer ever admitted has been removed. That is the never-admitted-versus-evicted split, reconstructed — and reading.rs:103-110 makes the state reachable from a real node answer (Some(T) with entry_count == 0), which is the post-eviction data your attack list item 2 names. Neither sentence leaks alone; the pair does, and the pair only exists because this PR gave the fact layer a renderer — which is why four prior adversarial rounds could not see it.

Not blocking #405 on the same reasoning you applied to #3297: this PR authors neither string, and holding the only thing that makes the pane reachable does not remove the leak from the tree. It is a normative clause violation rather than legibility, so the ticket should block the fact layer. Fix belongs at the wrapper or in rewards::copy: when the count is zero, do not also assert the write time.


F4 — follow-up ticket. The new test's doc comment asserts coverage that does not exist.

store_rewards_tests.rs:252-255 justifies scoping its clause-6/7 sweep to this module's own seven strings with: "the fact sentences themselves are rewards::copy's and are guarded there."

They are not. rewards/copy.rs:269 no_rewards_copy_contains_a_forbidden_phrase's FORBIDDEN list (copy.rs:271-284) is a funding-floor sweep only — "requires uptime", "stay online", "minimum funding", "floor", "gate". It contains no eviction, entitlement, accrual, eligible or claiming term. Clauses 6 and 7 over the four sentences that actually reach a person are asserted by nothing, and this doc comment is what will make the next reviewer believe otherwise. This is the mechanism by which F3 survived. Fix: extend copy.rs's sweep with store_rewards_tests.rs's own BANNED list over ALL_KEYS.


F5 — follow-up ticket. Amber is the only state a real operator can reach, so amber stops meaning anything.

Nothing calls store_rewards::remember on any user path — grep over src/ and examples/ returns only seed_preview (store_rewards.rs:375) and the tests. So in the shipped binary reading(store_id) is always None, store_rewards.rs:111 maps NoneRewardsBody::Unreachable(NOT_ANSWERABLE)PaneState::Unreachable, and every store row, in every install, forever, opens to an amber banner. Waiting, Empty and Facts are unreachable outside the gallery and the tests.

Two consequences:

  • content.rs:302's rule exists so warning colours keep their meaning. Amber on 100% of rows in 100% of installs is that same harm arriving by a different route. The sentence distinction (cannot-be-asked vs asked-and-failed) is correctly kept — that half of attack item 6 passes cleanly — but the treatment collapses them. The not-answerable case is a capability gap with an upgrade remedy, not a fault; it should take the recessed treatment and leave amber to a read that was taken and failed.
  • The gallery photographs four states, three of which no operator can reach, and omits the one that is universal. content-store-rewards-unreachable-light-960.png shows the failed sentence ("…: the node closed the connection"), not content-store-rewards-not-answerable. Add that capture — it is the picture of what shipping this actually looks like.

Minor, same area: { $why } is spliced raw, so that banner ends with no full stop and its punctuation depends on the node's reason string.


What survived the attack (stated so the record is not one-sided)

  • The predecessor's defect is genuinely fixed. en.ftl:34 reads "This distributor has paid { $amount } in total to mirrors" — correct subject, correct party, and the Activity mirror-claim record that misattributed it is gone (pane.rs:16-21).
  • Attack item 8 passes. No create / mint / refill / clawback affordance, not even disabled. rewards::clawback is reachable from this file only through a doc-comment mention; there is no import and no call path, so nothing here can reach the ClawbackAuthority-witnessed strings from feat(app): gate rewards-clawback-* strings on a proven viewer authority #404.
  • Attack item 4 passes. No claim status, accrual, entitlement or eligible/claiming word. The section-exists-therefore-entitled framing is avoided: the heading is the neutral "Reward distributor", and the empty sentence (en.ftl:43) is a claim about the node's answer, not about the reader's standing.
  • Empty is honest. It is reachable only from PaneReading::Answered(None) (store_rewards.rs:116); nothing unread, missing or unasked paints as empty, and an_unremembered_store_is_not_reported_as_having_no_distributor pins both halves. Attack item 2's absence-as-zero has no path here at the wrapper — the one absence-as-data defect is F1's 0, which is a rate, not a count.
  • The store-id conversions are real tests, not assumptions — refusal rather than truncation at 63 hex digits, case folding, 0x on both halves, and seed_preview keyed off the wire's own bytes so a capture cannot photograph a record about a different store than the row above it.
  • Absence-as-zero on the mirror count does not occur: cadence.rs:58 returns NoMirrorsYet before any division, and zero_entry_count_wins_over_zero_funding_rate pins the precedence.

On #3297 — your ruling stands

I agree, and for a sharper reason than legibility alone: the unreadable timestamp in rewards-paid-out-total sits in the same sentence as the money figure, so a person cannot date the amount. That is still not false — correct subject, correctly formatted amount — and #405 authors neither string. Holding the only thing that makes the pane reachable for a timestamp format would be disproportionate. Note that fixing F2 is what makes #3297 verifiable-with-evidence when it is picked up.

Merge judgement

CHANGES-REQUIRED. F1 and F2 before merge — F1 is roughly a two-line deletion, F2 is one constant plus a re-shoot, and they belong in the same push because the re-shoot is the proof of the first. F3, F4 and F5 as tickets; none of them is authored by this diff and none is worth holding the only route to this pane.

KG: a-compiled-in-zero-rendered-as-a-funding-claim-tells-a-payee-to-set-the-funders-rate · an-acceptance-capture-clipped-above-the-figures-is-not-evidence-about-money · two-honest-sentences-can-reconstruct-the-split-neither-admits · a-test-doc-comment-can-assert-coverage-that-does-not-exist · a-state-reachable-on-every-row-in-every-install-makes-its-warning-colour-meaningless

MichaelTaylor3d and others added 4 commits September 10, 2026 14:37
… 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>
@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

Adversarial-gate round: F1, F2, F4, F5 addressed at d0f3061f — 11/11 green

Still DRAFT. F3 untouched by design.

F1 — the cadence sentence is gone, not re-inputted

body_of now takes only the first three sections rewards_sections produces, so the section renders exactly:

  1. the prover-status sentence (rewards-status-*)
  2. the entry-set sentence (rewards-entry-set-*)
  3. the payout sentence (rewards-paid-out-*)

No fabricated input remains in the sense that matters: nothing a person reads is computed from a compiled-in stand-in. The daily_funding_base_units argument still has to be passed to call rewards_sections at all — it is named CADENCE_ARGUMENT_THIS_MOUNT_DISCARDS, its only consumer inside that function is the section thrown away before layout, and the doc comment records that the clean shape (a rewards_sections that does not take a funding rate) lives in read-only rewards/ and is reported upward rather than worked around here.

What now prevents a payee being addressed as a funder:

  • the_dropped_section_is_the_cadence_one_and_nothing_here_renders_it — asserts the fact layer still produces four sections AND that the fourth is still the cadence sentence, so a reorder upstream fails here instead of silently dropping the payout total.
  • no_sentence_here_addresses_the_reader_as_the_funder — sweeps the rendered sentences of six record shapes (18 sentences) plus the mount's own banner copy for funding rate, funding amount, choose a, set a rate, top up, refill, fund the, your distributor. Every cadence sentence in rewards::copy contains "funding rate", so the whole family is kept off this surface however it is reached.
  • The green-by-construction test is gone: the_facts_shown_are_the_shipped_fact_layers_own compared body_of against rewards_sections while passing it the same constant. It is replaced by the_three_sentences_shown_are_the_catalog_sentences_for_this_record, which builds the expected sentences from the catalog keys and the record's own numbers.

F2 — five captures, each closing inside the frame

Re-shot at 960x1240 logical (1920x2480 px), still a DPI-aware framebuffer readback via ViewportCommand::Screenshot, no synthetic input, pinned scale.

Closure is proved by an element AFTER the last sentence: in all five files the Reward-distributor panel's bottom edge, the "Capsules mirrored here" card border, and the entire following "Mirror another store" card are visible.

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, _) becomes NotAnswerable — 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) becomes NotAnswerable. is_method_not_found matches -32601 and the message spellings method not found, method_not_found, unknown method; the wire's code itself never reaches the screen.
  • Some(PaneReading::Unreachable(why)) stays Unreachable, 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 coderewards-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; None back to Unreachable with 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 --workspace all green locally. dig-app-core --lib 2949 to 2955 passed, 4 ignored unchanged; the store_rewards module goes 11 to 17 tests.
  • cargo fmt --all --check clean; cargo clippy -p dig-app-core --all-targets -- -D warnings clean.
  • Blast radius: as_state had no caller outside this module; RewardsPreview crosses only gui/mod.rs's re-export and shell.rs's CaptureStaging field, neither of which matches on it; state::neutral_note has one caller. Grep-verified — the gitnexus index is not fresh for this worktree.
  • Six tabs untouched; tab_id_all_is_still_the_six_labels and activity_tab_emits_zero_action_rows pass. No .ftl value added or changed in any of the 14 catalogs. No affordance added, disabled or otherwise.

🤖 Generated with Claude Code

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

Security re-audit — PR #405

PASS
Head audited: d0f3061f90bbeb42f34b96f49e1e9ea716e3d3c4

Scope: diff 16173d1d..d0f3061f (5 files: store_rewards.rs, state.rs, store_rewards_tests.rs, store_rewards_gallery.rs, docs/gallery/README.md, 5 regenerated PNGs). Fresh context, no prior verdict carried forward.

Findings, verified against the diff

  1. Compiled-in 0 reaching a reader — fixed. crates/dig-app-core/src/confirm/gui/window/pane/store_rewards.rs:194 now calls rewards_sections(record, now, CADENCE_ARGUMENT_THIS_MOUNT_DISCARDS).into_iter().take(FACTS_THIS_MOUNT_CAN_SUPPORT). rewards_sections (rewards/pane.rs:265) still returns [prover, entries, payout, cadence] in that literal order — confirmed by reading the source — so take(3) drops exactly the cadence section, never one of the three real facts. Confirmed with a direct test at store_rewards_tests.rs:461 (the_dropped_section_is_the_cadence_one_and_nothing_here_renders_it) that asserts index 3 is CADENCE_NO_FUNDING_RATE and that the shown sentences equal produced[..3]. rewards::pane::rewards_sections has exactly one non-test caller in the crate (grep-confirmed) — this mount — so there is no second path a 0 could still reach a screen through.

  2. NotAnswerable / is_method_not_found — sound, no leak. PaneReading::Unreachable carries &'static str, not an arbitrary String (rewards/pane.rs:51) — the reason space is a fixed small set of literals chosen by the (not-yet-wired) transport layer, so no raw RPC text, address, or path can flow through even after classification. is_method_not_found matches -32601/method not found/method_not_found/unknown method case-insensitively, checked before the general Unreachable arm (ordering confirmed at store_rewards.rs:184-190). Tested for both false-positive and false-negative shapes at store_rewards_tests.rs:230-253 (accepts real method-not-found spellings, rejects "the method timed out", "connection reset"). content-store-rewards-not-answerable copy (i18n/en.ftl:44) states only "your node cannot yet be asked… run a DIG node version that lists reward distributors" — no code, path or address in any locale I read. This is a UI-classification heuristic with no live transport behind it yet (see feat(keystore): DIG identity key mgmt — sign/unlock + DIGOP1 sealing + OS-store primary #3), so a misclassification today has no exploitable consequence; flagging as non-blocking correctness note for whoever wires the real client, not a live finding.

  3. No fake read reaches a user path — confirmed. RewardsClient (rewards/client.rs:53) has exactly one implementor in the crate, FakeRewardsClient, used only from its own #[cfg(test)]/doctest blocks (grep-confirmed, no non-test caller). store_rewards::remember/forget are called only from seed_preview (gallery/test fixture staging) and the module's own tests. The absence of a reading on a real install is genuine, not a disguised fake read.

  4. Test sweeps are non-vacuous. every_reachable_record() yields 6 named record shapes; sentences_for renders 3 facts each → 18, matching the sweep's own assert_eq!(swept, 18, …). Confirmed the FUNDER_ROLE list ("choose a", "funding rate", …) would have caught the pre-fix defect: rewards-cadence-no-funding-rate's shipped English is literally "Choose a funding rate to see how often a mirror would claim." — contains "choose a" and "funding rate". Not a contains-on-superstring false pass; the needle list is drawn from the actual banned phrase family, and the count assertions (swept == 18, shown.len() == 3) guard against the loop silently iterating zero times.

  5. Captures — fixture-derived, no leak. STORE_ID in the gallery is a fixed hex constant (not a derived real key), CAPTURES grew to 5 tuples with not-answerable added, and SIZE grew to (960.0, 1240.0) specifically so the card's bottom border is visible in every frame (addresses the prior below-the-fold crop). No address, seed or private material in the new PNGs' source data path.

Standing checks re-verified on this head

  • Whose money: payout_sentence/rewards_sections money-formatting code is untouched by this diff; unaffected.
  • Absence as reassuring zero: three-case entry-count split (NeverWritten / Known{0} / Known{n}) is produced by unchanged code in rewards/pane.rs; the section-count change doesn't touch it.
  • Enumeration pair (never-admitted vs evicted): unchanged by this diff; known ticketed follow-up, not re-filed.
  • Clause 6: rewards-cadence-no-mirrors-yet ("No mirror is claiming…") is still catalog text in rewards/copy.rs, but its only production producer, cadence_sentence, is called solely from inside rewards_sections's dropped 4th section for this mount — confirmed unreachable from the only non-test caller (finding 1's grep). Genuinely closed for this surface.
  • No dead/live funding or clawback control: no funding affordance code exists anywhere in this crate; rewards::clawback::{ClawbackAuthority, ProvenClawback} have no caller outside their own module/tests (grep-confirmed) — unreached from this mount.

Known, not re-filed

#3297, #3289, #3290, #3294 — status unchanged by this diff, confirmed still present as tracked, not re-opened here.

Not covered

Did not re-run the full test suite locally (background compile did not finish in time); relying on the stated 11/11 green CI including Test + coverage and both Native confirmer jobs. Did not re-audit code paths outside the 5 changed files (payout/prover/entry-set formatting, clawback internals) since this diff does not touch them.

Worktree D:/worktrees/dig-app-3273 used read-only; nothing edited, committed, or pushed.

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.

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

Adversarial re-gate — CHANGES-REQUIRED

Head attacked: d0f3061f90bbeb42f34b96f49e1e9ea716e3d3c4. Read-only, fresh attack; prior verdict on 16173d1d not carried forward.

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

crates/dig-app-core/i18n/en.ftl:44 (content-store-rewards-not-answerable, plus the 13 sibling locales):

"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."

The second clause is false at this head, and not for a future reason. Nothing in dig-app ever takes this read:

  • remember (window/pane/store_rewards.rs:236) has exactly one caller, seed_preview (:487), which is gallery-only and documented as such.
  • The paint path is content.rs:361 to disclosure to reading(store_id) (store_rewards.rs:243), which consults a process map nothing on a user path ever writes.
  • dig.listRewardDistributors appears in this repo only in comments and test fixtures — no call site.

So reading() returns None on every install, on every store row, forever — independent of node version. A mirror operator reads a specific, actionable cause and remedy ("upgrade your node") for an absence whose actual cause is that no reader has been written. Upgrading changes nothing and the section keeps saying the same sentence, so the failure is silent to the person who acted on it.

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:

"DIG cannot ask the node to mirror a store yet, so the control above does nothing."

Remedy is one clause naming the app's own gap instead of — or alongside — the node's: 14 .ftl files, mechanical, no code change. Blocking rather than follow-up because it is the only state a real install can reach, and it instructs a person to spend effort on a false cause.

Follow-up — V2: the funder sweep's stated coverage overstates what it checks

window/pane/store_rewards_tests.rs:510-511:

"Every cadence sentence in rewards::copy contains 'funding rate', so this list also keeps the whole cadence family off this surface however it is reached."

False. rewards-cadence-no-mirrors-yet (i18n/en.ftl:35, "No mirror is claiming, so there is no cadence to state.") contains none of the eight FUNDER_ROLE needles. For a distributor with a written entry set of zero, a surfaced cadence section would pass this sweep's needles outright.

The guard itself survives: assert_eq!(swept, 18) at :537 catches a fourth sentence for every record shape, and "claiming" is in CLAUSE_6 of the_rendered_fact_sentences_carry_no_claim_entitlement_or_eviction (:653). So the protection is real and the comment is wrong — which is the exact class finding 4 was. Correct the comment, not the code.

Follow-up — V3: the new clause-7 sweep passes by avoiding the shipped phrasing

the_rendered_fact_sentences_carry_no_claim_entitlement_or_eviction (:646) checks evict / removed from / no longer in / never admitted / not admitted / dropped from. The clause-7-adjacent sentence this mount actually renders is rewards-entry-set-never-written (i18n/en.ftl:30): "Entry set: never written. No peer has been added to this distributor yet." — matched by none of them. The sweep is green because the needle list sidesteps the shipped copy, not because the copy is clean. The substance is already ticketed (finding 3, en.ftl:30/:33); this note is only that the test gives less assurance than its name implies.

Prior findings — verified closed

F1 — closed, and the residual is genuinely unreachable by a reader. rewards_sections builds [prover, entries, payout, cadence] (rewards/pane.rs:267) with every heading Some, so .take(FACTS_THIS_MOUNT_CAN_SUPPORT) plus filter_map at store_rewards.rs:184 cannot pull the fourth section under any record. CADENCE_ARGUMENT_THIS_MOUNT_DISCARDS flows only into the discarded section. A future reorder inside read-only rewards/ is the one way it could surface, and that is exactly what the_dropped_section_is_the_cadence_one_and_nothing_here_renders_it (:461) pins — non-vacuously: it asserts produced.len() == 4, produced[3] == CADENCE_NO_FUNDING_RATE.text(), and shown == produced[..3], so a reorder fails here rather than silently dropping the payout total. no_sentence_here_addresses_the_reader_as_the_funder (:509) is non-vacuous too — "funding rate" is present in 3 of the 4 shipped cadence strings (en.ftl:5,36,37,38) — subject to V2. No superstring hazard: a contains needle that over-matches only makes the sweep stricter.

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 (This distributor) and its own beneficiary class (to mirrors, plural), so standing last does not convert it into a personal figure; the removed sentence was the one that introduced a per-mirror frame, so subtraction reduced rather than created the inference. Adjacency to "3 mirror(s) as of the last entry write" invites a division, but that invitation was stronger with the cadence sentence present, and neither sentence claims the reader is one of the three.

F2 — closed. All four remaining frames verified at 1920x2480 against the amended rule. waiting, empty, not-answerable: zero money figures, no role-bearing sentence, whole Reward distributor card in frame, closure proved by the entire following "Mirror another store" card including its trailing two-line note. unreachable: same, with the wrapped reason "...for this store: the node closed the connection" whole and not clipped. Amber claim verified — (251,243,224) appears in unreachable only; waiting, empty, not-answerable and ready all show the recessed lavender treatment.

F5 — closed. body_of (store_rewards.rs:174-190) routes None and Unreachable(why) if is_method_not_found(why) to NotAnswerable, and the arm order puts the method-not-found guard before the general failure arm. Amber is now unreachable on a real install rather than mandatory on every row. Attacking the matcher in both directions: the dangerous direction (a transport fault swallowed as "too old") needs a transport that injects -32601 or method not found text into an unrelated failure reason; the other direction (a real -32601 spelled not implemented / unsupported method / no such method, which falls through to amber) is a live gap in the needle list. Neither is reader-reachable at this head, because nothing produces PaneReading::Unreachable on a user path — see V1. Widen the spellings when the reader lands; not a finding now.

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; entry-set-never-written and entry-set-known remain mutually exclusive per record and are still never rendered together.

Revert-proof — the stated reds would be for the stated causes. Both mutations are to a const and a match arm, not to any assertion text, so this is not a sed-style proof that can mutate what it checks. FACTS_THIS_MOUNT_CAN_SUPPORT = 4 fails the pin test at shown.len(), the catalog test at length, and the funder sweep at its needle or at swept == 18; routing None back to Unreachable fails the unasked-store, method-not-found and amber-reachability tests on their own subjects. One nuance worth recording: for zero-entry records the funder sweep would go red via the count assert rather than the role needle, because the cadence sentence those records produce carries no needle — the V2 hole showing up inside the revert-proof itself.

Not re-filed

#3297, #3289, #3290, #3294, and the two residuals the lane surfaced (rewards_sections' unusable funding-rate parameter; cargo build -p dig-app-core --no-default-features never having compiled).

KG: a-note-that-blames-the-node-version-for-an-absence-the-app-itself-causes

@MichaelTaylor3d MichaelTaylor3d left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

Adversarial gate (leg 3) — PASS at 5dd3744858b92936fe8ba32349a6d660feda1d65

Re-verified only the d0f3061f..5dd37448 delta (17 files: 14 .ftl, two comment-only .rs, one re-shot capture). Confirmed zero non-comment changed lines in store_rewards.rs / store_rewards_tests.rs; the mechanics cleared at d0f3061f were not re-opened.

1. Blocking finding (false remedy in content-store-rewards-not-answerable) — CLOSED

crates/dig-app-core/i18n/en.ftl:44:

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.

Clause by clause at this head:

  • "cannot yet be asked" — true under both readings. crates/dig-app-core/src/rewards/client.rs:53 is a trait seam with no wired transport, and dig.listRewardDistributors has 0 matches in the dig-node checkout (modules/apps/dig-node @ b19d6ef4, v0.255.0 — in fact no reward RPC surface at all).
  • "DIG does not send that question yet" — matches the code. store_rewards.rs:253 remember has exactly one non-test caller, store_rewards.rs:496, inside seed_preview (:487), gallery-only. No production call site.
  • "nothing about your node changes that" — closes the false action without opening a new one: no version, release, timeline or user step named on either side of the wire. The residual "yet" is honest futurity, not an instruction.
  • Not over-corrected: the note is scoped to the unanswerable state only; the answered states (ready, empty) still carry what a mirror operator does know, and the surrounding card still shows pinned/capsule/size facts. Saying nothing about pay here is the truth of this state, not a withholding.

All 14 catalogs verified for meaning, not just key presence: every locale renders the same three facts (cannot-be-asked / DIG-does-not-send / nothing-on-your-node-changes-it). No locale reintroduces a node-version promise or a timelinede/es/fr/pt-BR/ru/tr/vi/id/hi use a plain temporal "not yet"; ja/ko/zh-CN/zh-TW render the third clause as "changing anything on the node side does not change this", which is stronger and still true. None names a release, build number or date.

Two doc comments verified true:

  • store_rewards.rs:28-32 — the both-sides framing is accurate (see the remember/seed_preview trace above). The retained v0.256.0 serves dig.getRewardProverStatus only clause is unchanged from the cleared head and is not refutable from a v0.255.0 checkout; noted, not a finding.
  • store_rewards.rs:166-168NotAnswerable's doc no longer claims "its remedy"; "the true cause named and no promised fix" matches the shipped sentence.

Does the rewording carry the class forward a fifth time? No. Each of the four clauses is checkable against code at this head, and every actionable claim was removed rather than relocated.

2. Follow-up — false test comment — CLOSED

store_rewards_tests.rs:510-513 now states the true guard. Verified against the code it describes: rewards-cadence-no-mirrors-yet (i18n/en.ftl:35) carries no FUNDER_ROLE needle, and the cadence family is in fact held off by assert_eq!(swept, 18, …) (:~577) plus "claiming" in CLAUSE_6 (:~660) — that sentence contains "claiming". The comment claims exactly what the assertions enforce, no more.

3. Follow-up — clause-7 sweep green by needle avoidance — CLOSED (disclosure accurate)

store_rewards_tests.rs:647-654 names rewards-entry-set-never-written as passing by wording rather than by the property, states that the sweep only ever checks one sentence for a clause-7 word, and attributes the substantive fix to dig_ecosystem#3300. All three are true of the test body below it, and the disclosure does not overstate coverage. No needle added, no logic changed — correct, since the copy lives in read-only rewards/.

4. Capture — CLOSED

docs/gallery/content-store-rewards-not-answerable-light-960.png, 1920x2480. Reads the new wording verbatim inside the frame; the Reward distributor section closes in-frame with the following "Mirror another store" card and the status footer fully visible. No money figure or role-bearing sentence is clipped.
The other four rewards frames are byte-identical to d0f3061f (blob hashes 2caf1aca empty / ce10fc34 ready / 0945dbca unreachable / 6d5d9da7 waiting); their sentences are untouched by this delta, so they still match shipped copy. Reverting the noise-only ready re-shot was the right call.

Verdict: PASS. No change requested; mergeable on this head.

🤖 Generated with Claude Code

@MichaelTaylor3d MichaelTaylor3d left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

  1. All 14 catalogs. Read every diffed .ftl line. Each non-English locale's new sentence is a real translation carrying the same two claims as en.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).
  2. 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/UNREACHABLE in store_rewards.rs:84-97 are all still Msg::new("content-store-rewards-*") catalog lookups — no English literal reintroduced beside the catalog (the historical blind-spot this PR family shipped once, #3273 finding).
  3. Confirmed the specific claim: store_rewards::remember (line 253) has exactly one caller in the crate, seed_preview (line 487→495), and the module is pub(crate) mod store_rewards (pane/mod.rs:104) so nothing outside dig-app-core can 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.
  4. Both doc-comment changes verified true at this head: store_rewards.rs:25-49 module doc ("gap is on both sides of the wire") and the NotAnswerable variant doc (store_rewards.rs:163-168, "no promised fix") — matches (3) above and the new .ftl text; neither promises a remedy that doesn't exist.
  5. store_rewards_tests.rs: swept == 18 (line 539) and "claiming" present in CLAUSE_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 of CLAUSE_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 the dig_ecosystem#3300 cross-reference is descriptive only (no needle added, no logic changed, as stated).
  6. 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 shipped en.ftl. The other 4 content-store-rewards-* gallery frames (empty, ready, unreachable, waiting) show zero diff in this delta — confirmed byte-identical to d0f3061f, 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.

@MichaelTaylor3d
MichaelTaylor3d marked this pull request as ready for review September 10, 2026 23:56
@MichaelTaylor3d
MichaelTaylor3d merged commit cb49f4d into main Sep 10, 2026
11 checks passed
@MichaelTaylor3d
MichaelTaylor3d deleted the loop/3273-rewards-mount branch September 10, 2026 23:56
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