Skip to content

fix(ui): source the marketplace from the session, not from the model list's presence - #256

Merged
argszero merged 1 commit into
mainfrom
fix/market-tables-follow-the-session
Sep 15, 2026
Merged

argszero merged 1 commit into
mainfrom
fix/market-tables-follow-the-session

Conversation

@argszero

Copy link
Copy Markdown
Owner

Summary

The marketplace graded its data source on whether /api/models had arrived, not on whether the session is live. ui/js/data.js states the contract explicitly (rant 2026-08-19T15:54:06): its tables are for the guest marketplace and for the publish-form fallback — a logged-in session must never read them. Three consumers in ui/js/app.js instead wrote Live.models ? <live> : D.<guest table>, each with a comment restating the rule right above it.

Reachable window (the normal shape of a failure, not a fabricated one): the session is live and the catalogue request fails (500 / timeout / network) — loadMarketplace() sets Live.models = null and renders the honest "load failed + retry" row, while:

  • the provider filter keeps offering the guest table's providers — 10 of them, 5 of which this deployment does not have; selecting one filters to 0 rows;
  • the "recently used" chips render guest-market models, and their only consumer openChat() is guarded by if (loggedIn() && !Live.models) … return — so the chip is a button that can only answer "load failed";
  • the market nav badge shows the publish-form price mirror's row count (13), while the live catalogue has 6 rows and the guest market 7 — 13 is a third number.

Fingerprint of drift rather than design: ten lines below the provider filter, the same function gets it right — let list = Live.models ? modelsToView(Live.models) : (loggedIn() ? null : D.MARKET);, with the comment "绝不 fallback D.MARKET". One line right, one line wrong, differing only by loggedIn().

Changes

  • ui/js/app.js: one source per market table — marketRows() (returns null for a live session without a catalogue) and marketProviders(). The discriminator is written once, at the read of the table. List, provider filter, recent chips, chat and nav badge all consume them. openChat's guard line is untouched (it owns the toast); its lookup now goes through the helper. The provider dropdown's dataset.provSource distinguishes three states (live / none / mock) so it still rebuilds when the catalogue arrives.
  • ui/index.html: app.js?v=20260915-10-11.
  • src/state_gate.rs: new invariant market_tables_follow_the_session_not_the_data, three rules with independent teeth and zero exemption list: (1) every code line reading D.MARKET / D.PROVIDERS must itself branch on the session; (2) no line may mix a market table with the publish-form fallback tables (D.MODELS / D.PLANS) — the badge line did exactly that; (3) each market table has exactly one reader (the source helper), which also rejects the over-correction "just delete the mocks". A companion test the_market_source_scanners_have_teeth pins the scanner on synthetic input.
  • ui/README.md: new section documenting the convention and why the gate and the probe each cover half of it.
  • The scanner is comment-aware: it strips //-started lines, multi-line /* … */ bodies and inline /* … */ pairs — the module's own explanation mentions table names, so a naive contains would red-flag its own documentation (ledger pitfall #296/#309).

Related Issue

No issue exists for this; found during Recon. No linked issue is expected.

Tests

  • cargo test283 passed (281 before; +2 gate tests).
  • cargo fmt --check — clean.
  • cargo clippy --all-targets — only the pre-existing collapsible_match at src/protocol.rs:662.
  • jsdom probe (real index.html + the four real scripts, fetch stubbed and logged, real controls driven) — 14/14 as declared, with per-leg expectations pinned to an explicit baseline:
leg scenario expectation result
A0 live session + marketplace + /api/models → 500 precondition pass
A1 control: the list shows the honest degraded state (retry button) pass pass
A2 axis: a live session's provider filter must not offer the guest table's providers fail on the old tree / pass now red → green
A3 axis: no guest-market "recently used" chips in a live session fail on the old tree / pass now red → green
A4 axis: the nav badge must not show the static table's count fail on the old tree / pass now red → green
B1–B4 control: live catalogue arrives — providers, chips, badge, no leak pass pass
C0–C3 control: guest session (real "browse as guest") — provider list, chips, badge from the guest tables pass pass
D1 control: empty recent store ⇒ no chip, i.e. A3 is red because of its fixture pass pass
  • A/B over six trees, both instruments on each (gate + probe):
tree gate probe
un-fixed (git show HEAD:ui/js/app.js) RED · rule 1 RED {A2,A3,A4}
competitor 1 — fix only the two probe-visible faces, badge keeps D.MODELS RED · rule 2 RED {A4}
competitor 2 — a second session-guarded reader (behaviour identical) RED · rule 3 (two readers) GREEN
competitor 3 — over-correction: the helper drops the guest table RED · rule 3 (no reader) RED {C2,C3}
line mentions the session test yet still uses the guest table GREEN RED {A2}
fixed tree GREEN GREEN 14/14

The last two rows are recorded honestly: the gate is stricter than the probe on one axis (it rejects a second, behaviour-identical reader) and the probe is stricter than the gate on another (it rejects a line whose shape lies). Each instrument covers a different half.

Checklist

  • Branch naming follows the convention (fix/…)
  • Commit message uses Conventional Commits
  • Single responsibility, minimal change
  • Cache-busting stamp bumped for the changed ui/js/*.js

…list's presence

The marketplace graded its data source on whether /api/models had arrived, not on
whether the session is live. With a catalogue request that failed (or had not
arrived yet), a logged-in session rendered the guest tables of data.js: the
provider filter offered providers this deployment does not have, the "recently
used" chips rendered guest-market models (clicking one only showed "load
failed"), and the nav badge showed the publish-form price mirror's row count
(13) instead of the catalogue's (0). The list itself was already honest
(load-failed + retry), so the three leaks were per-consumer drift.

All market consumers (list, provider filter, recent chips, chat, nav badge) now
go through one source per table -- `marketRows()` / `marketProviders()` -- whose
discriminator is `loggedIn()` (null = a live session without a catalogue). The
guest path is untouched and still by design.

Gate: `state_gate::market_tables_follow_the_session_not_the_data` (three rules
with independent teeth, zero exemption list) plus a comment-aware scanner that
strips //, /* */ blocks and inline /* */ pairs.
@argszero
argszero merged commit afc9c6f into main Sep 15, 2026
1 check passed
@argszero
argszero deleted the fix/market-tables-follow-the-session branch September 15, 2026 01:25
@argszero argszero mentioned this pull request Sep 15, 2026
10 tasks
argszero added a commit that referenced this pull request Sep 15, 2026
Ships the 18 PRs merged since v0.7.24 (#242-#259). Schema 14 -> 15 (two
covering indexes, applied at startup). No config change, so no deployment-side
config.toml edit is needed.

Two themes:

Perf on the NFS dev database
- #259: stop mapping the db (PRAGMA mmap_size 64MB -> 0) and stop a real write
  per request (dao::touch_api_key gains a 60s guard). Measured on the live dev
  db: mmap=64MB 1.7-3.1s per COUNT / 250 MiB read vs mmap=0 ~10.5ms / 80 KiB;
  mmap=0 alone still leaves ~1.2s behind any write, so the pair is required.
- #242: codify the two emergency indexes in a v15 migration and gate the
  conditional joins at the plan level.
- #243: read the sharing page's earn total from one batched aggregate.

Frontend: display must equal what it filters on, and one fact, one source
- #250 one writer for the transaction cache; #251 clear every session slot at
  the identity boundary and give the wallet view a loader; #253 one shared
  writer for the wallet/dashboard month-changes; #254 boot loads only the
  destination view; #255 a model row's identity is the model, not its index;
  #256 the marketplace source follows the session, not whether data arrived;
  #257 the sidebar advertises only digits that work; #258 the admin
  total-balance card sums the gift amount its caption names.

i18n
- #249 every backend error reaches the wordlist, and the comment stripper stops
  mangling UTF-8; #252 the backend stops inventing Chinese display labels in
  response data fields.

Forms and robustness
- #244 a non-auth boot failure no longer looks like being logged out; #245 a
  credential 401 is no longer read as a session expiry; #246 wire timestamps
  reach the renderer unsliced; #247 inline cards submit from every field; #248
  a market row's availability label comes from that row.

- Cargo.toml / Cargo.lock: 0.7.24 -> 0.7.25.
- CHANGELOG.md: v0.7.25 entry.
- ui/index.html: cache-bust left as-is; the UI PRs in this release already
  advanced it past the value deployed with v0.7.24 (app.js 20260915-13,
  i18n.js 20260915-3).

cargo test 288 passed; cargo fmt --check clean; clippy unchanged.
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