Skip to content

Action surface: self-verifying proof footer + tier-gated bounce reporting; gemini-3.5-flash; DEBATE off - #41

Merged
ejmockler merged 1 commit into
mainfrom
recover-proof-footer
Jun 15, 2026
Merged

Action surface: self-verifying proof footer + tier-gated bounce reporting; gemini-3.5-flash; DEBATE off#41
ejmockler merged 1 commit into
mainfrom
recover-proof-footer

Conversation

@ejmockler

@ejmockler ejmockler commented Jun 15, 2026

Copy link
Copy Markdown
Member

Recovers + melds the pre-spectrum-sweep WIP (set aside on 2026-06-14, preserved in a stash) onto current main (b226693, post-spectrum #40). Clean meld: 0 file conflicts (main never touched these 13 files since the stash base), convex tsc 0, svelte-check 0 errors, 4,802 tests pass — DEBATE:false broke nothing (debate tests are flag-aware).

What it adds

  • Self-verifying proof footer on the /s/[slug] action surface — emits commons.email/v/{credentialHash} only when the hash resolves, backed by a new auth-scoped users.getActiveCredentialHash (returns only the caller's own hash — no userId→hash→district enumeration oracle).
  • Tier-gated bounce reporting on the action cards (DecisionMakerLandscapeCard/DistrictOfficialCard/PowerLandscape/RoleGroup) — affordance only for address-verified (tier 2+) users.
  • /s/[slug] refactor — server + page, net smaller.
  • gemini-3.5-flash — pinned (the prior gemini-3-flash-preview ran away under MAX_TOKENS on subject-line calls), thinkingLevel: low; the gemini-provider test updated to match (this is the source of the "2 gemini WIP-collateral" failures that shadowed every prior PR this session — now resolved).
  • features.ts: DEBATE: false — intentional, per your direction to bring the WIP in including the flag.
  • template-browser preview + emailService + subject-line tweaks.

Evidence

0 conflicts on apply · convex tsc 0 · svelte-check 0 errors · vitest 4,802 passed (the prior 2 gemini failures fixed; regenerated convex/_generated/api.d.ts from the merged function set).

Follow-up flagged (not in this PR — a decision)

DEBATE off makes the strategy corpus's "Adversarial Quality (Debate Markets)" a dormant/flag-gated capability (like CONGRESSIONAL), not a live one. Whether that's a launch-gate or a deprecation is a call — the docs may want a caveat once decided.

Summary by CodeRabbit

  • New Features

    • Added bounce reporting functionality for email delivery tracking.
    • Updated proof footer display to show abbreviated credential identifiers.
  • Improvements

    • Enhanced credential verification integration throughout the platform.
    • Optimized AI model performance with configuration updates.
  • Chores

    • Disabled DEBATE feature flag.
    • Refactored backend delivery record handling.

…ng; gemini-3.5-flash; DEBATE off

- /s/[slug] action surface emits a commons.email/v/{credentialHash} verify URL in the proof footer only when the hash resolves (new auth-scoped users.getActiveCredentialHash — returns only the caller's own hash, no userId->hash->district enumeration oracle)
- action cards (DecisionMakerLandscapeCard/DistrictOfficialCard/PowerLandscape/RoleGroup): gate the bounce-report affordance to address-verified (tier 2+) users
- /s/[slug] server+page refactor (net smaller)
- gemini-client: pin gemini-3.5-flash (3-flash-preview ran away under MAX_TOKENS on subject-line calls) + thinkingLevel low; gemini-provider test updated to match
- features.ts: DEBATE off
- template-browser preview + emailService + subject-line tweaks
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying communique-site with  Cloudflare Pages  Cloudflare Pages

Latest commit: f50f1d4
Status:🚫  Build failed.

View logs

@github-actions

Copy link
Copy Markdown

Code Coverage

Package Line Rate Branch Rate Health
packages.sdk-typescript.src 74% 64%
src 0% 0%
src.lib 100% 100%
src.lib.components.action 0% 0%
src.lib.components.activation 0% 0%
src.lib.components.auth 0% 0%
src.lib.components.auth.address-steps 0% 0%
src.lib.components.auth.parts 0% 0%
src.lib.components.automation 0% 0%
src.lib.components.billing 0% 0%
src.lib.components.bubble 0% 0%
src.lib.components.crypto 0% 0%
src.lib.components.debate 0% 0%
src.lib.components.error 0% 0%
src.lib.components.events 0% 0%
src.lib.components.fundraising 0% 0%
src.lib.components.geographic 0% 0%
src.lib.components.identity 0% 0%
src.lib.components.layout 0% 0%
src.lib.components.layout.header 0% 0%
src.lib.components.modals 0% 0%
src.lib.components.networks 0% 0%
src.lib.components.onboarding 0% 0%
src.lib.components.org 2% 5%
src.lib.components.org.os 17% 16%
src.lib.components.org.studio 23% 18%
src.lib.components.profile 0% 0%
src.lib.components.scorecard 0% 0%
src.lib.components.segments 0% 0%
src.lib.components.setup 0% 0%
src.lib.components.sms 0% 0%
src.lib.components.submission 0% 0%
src.lib.components.template 0% 0%
src.lib.components.template-browser 0% 0%
src.lib.components.template-browser.parts 0% 0%
src.lib.components.template-browser.spectrum 0% 0%
src.lib.components.template.creator 0% 0%
src.lib.components.template.parts 0% 0%
src.lib.components.thoughts 0% 0%
src.lib.components.ui 0% 0%
src.lib.components.verify 0% 0%
src.lib.components.visualization 0% 0%
src.lib.components.wallet 0% 0%
src.lib.components.wallet.debate 0% 0%
src.lib.config 53% 53%
src.lib.constants 25% 100%
src.lib.core 3% 5%
src.lib.core.agents 91% 82%
src.lib.core.agents.agents 49% 40%
src.lib.core.agents.prompts 10% 0%
src.lib.core.agents.providers 29% 29%
src.lib.core.agents.types 100% 100%
src.lib.core.agents.utils 63% 55%
src.lib.core.analytics 27% 11%
src.lib.core.api 0% 0%
src.lib.core.auth 42% 40%
src.lib.core.blockchain 26% 25%
src.lib.core.bubble 6% 0%
src.lib.core.census 100% 100%
src.lib.core.crypto 86% 57%
src.lib.core.email 98% 100%
src.lib.core.encoding 100% 100%
src.lib.core.gas 0% 0%
src.lib.core.identity 60% 58%
src.lib.core.legislative 100% 100%
src.lib.core.locale 0% 0%
src.lib.core.location 47% 50%
src.lib.core.location.resolvers 100% 90%
src.lib.core.near 0% 0%
src.lib.core.org 91% 92%
src.lib.core.privacy 100% 100%
src.lib.core.proof 2% 6%
src.lib.core.search 0% 0%
src.lib.core.security 60% 60%
src.lib.core.server 57% 70%
src.lib.core.server.moderation 20% 25%
src.lib.core.shadow-atlas 49% 43%
src.lib.core.thoughts 0% 0%
src.lib.core.tools 2% 0%
src.lib.core.topic 98% 84%
src.lib.core.wallet 6% 7%
src.lib.core.zkp 41% 48%
src.lib.data 98% 87%
src.lib.design 0% 0%
src.lib.server 81% 69%
src.lib.server.agents 0% 0%
src.lib.server.api-v1 68% 75%
src.lib.server.auth 96% 98%
src.lib.server.billing 45% 27%
src.lib.server.calls 0% 0%
src.lib.server.delegation 100% 100%
src.lib.server.email 83% 58%
src.lib.server.events 90% 67%
src.lib.server.exa 85% 74%
src.lib.server.firecrawl 14% 0%
src.lib.server.geographic 100% 100%
src.lib.server.ground 62% 58%
src.lib.server.identity 98% 89%
src.lib.server.internal 91% 79%
src.lib.server.legislation 0% 0%
src.lib.server.legislation.ingest 100% 100%
src.lib.server.legislation.receipts 0% 0%
src.lib.server.legislation.scorecard 100% 100%
src.lib.server.platform-sync 100% 91%
src.lib.server.reducto 0% 0%
src.lib.server.sms 55% 39%
src.lib.server.smt 77% 62%
src.lib.server.tee 96% 91%
src.lib.server.workflows 0% 0%
src.lib.services 15% 3%
src.lib.services.ai 100% 86%
src.lib.stores 23% 19%
src.lib.types 12% 13%
src.lib.types.analytics 48% 0%
src.lib.utils 14% 10%
src.routes 0% 0%
src.routes..well-known.jwks.json 0% 0%
src.routes.about.integrity 0% 0%
src.routes.accountability.[id] 0% 0%
src.routes.api.(dev).dev-login 0% 0%
src.routes.api.admin.backfill-embeddings 0% 0%
src.routes.api.admin.reconcile-registrations 0% 0%
src.routes.api.agents.generate-subject 0% 0%
src.routes.api.agents.message-jobs.[jobId] 0% 0%
src.routes.api.agents.stream-decision-makers 0% 0%
src.routes.api.agents.stream-message 77% 71%
src.routes.api.agents.stream-subject 78% 64%
src.routes.api.agents.traces.[traceId] 0% 0%
src.routes.api.analytics.increment 0% 0%
src.routes.api.auth.passkey 100% 83%
src.routes.api.auth.passkey.authenticate 0% 0%
src.routes.api.auth.passkey.current 0% 0%
src.routes.api.auth.passkey.register 0% 0%
src.routes.api.automation.process 0% 0%
src.routes.api.billing.checkout 0% 0%
src.routes.api.billing.checkout-individual 0% 0%
src.routes.api.billing.portal 0% 0%
src.routes.api.blast.[blastId].dispatch-claim 0% 0%
src.routes.api.blast.[blastId].unsubscribe-tokens 0% 0%
src.routes.api.c.[slug].stats 0% 0%
src.routes.api.c.[slug].verify-district 0% 0%
src.routes.api.campaigns.[id].debate 0% 0%
src.routes.api.d.[campaignId].checkout 0% 0%
src.routes.api.d.[campaignId].stats 0% 0%
src.routes.api.debates.[debateId].ai-resolution 0% 0%
src.routes.api.debates.[debateId].appeal 0% 0%
src.routes.api.debates.[debateId].arguments 0% 0%
src.routes.api.debates.[debateId].claim 0% 0%
src.routes.api.debates.[debateId].commit 0% 0%
src.routes.api.debates.[debateId].cosign 0% 0%
src.routes.api.debates.[debateId].governance-resolve 0% 0%
src.routes.api.debates.[debateId].position-proof 0% 0%
src.routes.api.debates.[debateId].resolve 0% 0%
src.routes.api.debates.[debateId].reveal 0% 0%
src.routes.api.debates.[debateId].settle 0% 0%
src.routes.api.debates.[debateId].stream 0% 0%
src.routes.api.debates.by-template.[templateId] 0% 0%
src.routes.api.debates.create 0% 0%
src.routes.api.delegation 0% 0%
src.routes.api.delegation.[id] 0% 0%
src.routes.api.delegation.parse-policy 0% 0%
src.routes.api.delegation.review.[reviewId] 0% 0%
src.routes.api.deliveries.record 82% 78%
src.routes.api.dm.[id].scorecard 0% 0%
src.routes.api.dm.scorecard.compare 0% 0%
src.routes.api.e.[id].checkin 0% 0%
src.routes.api.e.[id].rsvp 0% 0%
src.routes.api.e.[id].stats 0% 0%
src.routes.api.email.confirm.[token] 0% 0%
src.routes.api.emails.report-bounce 0% 0%
src.routes.api.embed.scorecard.[id] 100% 61%
src.routes.api.embeddings.generate 0% 0%
src.routes.api.geographic.infer-scope 92% 100%
src.routes.api.geographic.resolve 0% 0%
src.routes.api.ground.bundle 0% 0%
src.routes.api.ground.restore-state 0% 0%
src.routes.api.ground.state 0% 0%
src.routes.api.ground.wrapper 0% 0%
src.routes.api.health 0% 0%
src.routes.api.identity.delete-blob 0% 100%
src.routes.api.identity.retrieve-blob 0% 100%
src.routes.api.identity.store-blob 0% 100%
src.routes.api.identity.verify-address 73% 60%
src.routes.api.identity.verify-mdl 0% 0%
src.routes.api.identity.verify-mdl.start 76% 69%
src.routes.api.identity.verify-mdl.verify 0% 0%
src.routes.api.internal.alert 0% 0%
src.routes.api.internal.anchor-incidents 0% 0%
src.routes.api.internal.anchor-proof 0% 0%
src.routes.api.internal.dev-login 95% 63%
src.routes.api.internal.emit-revocation 87% 87%
src.routes.api.internal.health.empty-tree-root 88% 82%
src.routes.api.internal.identity.mdl-readiness 92% 73%
src.routes.api.internal.metrics.client-event 88% 79%
src.routes.api.internal.revocation-root 0% 0%
src.routes.api.location.ip-lookup 0% 0%
src.routes.api.location.resolve 0% 0%
src.routes.api.location.resolve-address 96% 75%
src.routes.api.location.search 0% 0%
src.routes.api.moderation.check 92% 92%
src.routes.api.moderation.personalization 0% 0%
src.routes.api.org 0% 0%
src.routes.api.org.[slug] 0% 0%
src.routes.api.org.[slug].alerts 0% 0%
src.routes.api.org.[slug].alerts.[id] 0% 0%
src.routes.api.org.[slug].bills.[billId].watch 0% 0%
src.routes.api.org.[slug].bills.browse 0% 0%
src.routes.api.org.[slug].bills.search 0% 0%
src.routes.api.org.[slug].bills.watching 0% 0%
src.routes.api.org.[slug].branding 0% 0%
src.routes.api.org.[slug].calls 0% 0%
src.routes.api.org.[slug].campaigns 0% 0%
src.routes.api.org.[slug].campaigns.[campaignId].receipts 0% 0%
src.routes.api.org.[slug].campaigns.[campaignId].responses 0% 0%
src.routes.api.org.[slug].campaigns.[campaignId].stream 0% 0%
src.routes.api.org.[slug].campaigns.targeting 0% 0%
src.routes.api.org.[slug].decision-makers.[dmId].activity 0% 0%
src.routes.api.org.[slug].decision-makers.[dmId].follow 0% 0%
src.routes.api.org.[slug].decision-makers.feed 0% 0%
src.routes.api.org.[slug].decision-makers.following 0% 0%
src.routes.api.org.[slug].dm.receipts 0% 0%
src.routes.api.org.[slug].dm.receipts.export.csv 0% 0%
src.routes.api.org.[slug].endorsements 0% 0%
src.routes.api.org.[slug].events 0% 0%
src.routes.api.org.[slug].events.[id] 0% 0%
src.routes.api.org.[slug].fundraising 0% 0%
src.routes.api.org.[slug].fundraising.[id] 0% 0%
src.routes.api.org.[slug].fundraising.[id].donors 0% 0%
src.routes.api.org.[slug].invites 0% 0%
src.routes.api.org.[slug].issue-domains 0% 0%
src.routes.api.org.[slug].issue-domains.rescore 0% 0%
src.routes.api.org.[slug].members 0% 0%
src.routes.api.org.[slug].networks 0% 0%
src.routes.api.org.[slug].networks.[networkId] 0% 0%
src.routes.api.org.[slug].networks.[networkId].accept 0% 0%
src.routes.api.org.[slug].networks.[networkId].decline 0% 0%
src.routes.api.org.[slug].networks.[networkId].invite 0% 0%
src.routes.api.org.[slug].networks.[networkId].leave 0% 0%
src.routes.api.org.[slug].networks.[networkId].members.[orgId] 0% 0%
src.routes.api.org.[slug].networks.[networkId].report 0% 0%
src.routes.api.org.[slug].profile 0% 0%
src.routes.api.org.[slug].representatives 0% 0%
src.routes.api.org.[slug].scorecards 0% 0%
src.routes.api.org.[slug].scorecards.export 85% 84%
src.routes.api.org.[slug].segments 0% 0%
src.routes.api.org.[slug].ses-token 0% 0%
src.routes.api.org.[slug].settings.alert-preferences 0% 0%
src.routes.api.org.[slug].sms 0% 0%
src.routes.api.org.[slug].sms.[id] 0% 0%
src.routes.api.org.[slug].sms.[id].messages 0% 0%
src.routes.api.org.[slug].sms.audience-count 0% 0%
src.routes.api.org.[slug].workflows 89% 88%
src.routes.api.org.[slug].workflows.[id] 100% 90%
src.routes.api.org.[slug].workflows.[id].executions 100% 50%
src.routes.api.org.check-slug 0% 0%
src.routes.api.positions.batch-register 0% 0%
src.routes.api.positions.confirm-send 0% 0%
src.routes.api.positions.count.[templateId] 0% 0%
src.routes.api.positions.engagement-by-district.[templateId] 0% 0%
src.routes.api.positions.register 0% 0%
src.routes.api.proofs.revocation-witness 0% 0%
src.routes.api.shadow-atlas.bubble 0% 0%
src.routes.api.shadow-atlas.community-field 0% 0%
src.routes.api.shadow-atlas.engagement 0% 0%
src.routes.api.shadow-atlas.register 0% 0%
src.routes.api.submissions.[id].retry 0% 0%
src.routes.api.submissions.[id].status 0% 0%
src.routes.api.submissions.create 62% 54%
src.routes.api.tee.public-key 0% 0%
src.routes.api.templates 0% 0%
src.routes.api.templates.check-slug 0% 0%
src.routes.api.templates.search 0% 0%
src.routes.api.user.profile 0% 0%
src.routes.api.user.templates 0% 0%
src.routes.api.v1 100% 100%
src.routes.api.v1.activity 0% 0%
src.routes.api.v1.calls 0% 0%
src.routes.api.v1.campaigns 18% 9%
src.routes.api.v1.campaigns.[id] 0% 0%
src.routes.api.v1.campaigns.[id].actions 0% 0%
src.routes.api.v1.docs 67% 50%
src.routes.api.v1.donations 0% 0%
src.routes.api.v1.donations.[id] 0% 0%
src.routes.api.v1.events 0% 0%
src.routes.api.v1.events.[id] 0% 0%
src.routes.api.v1.keys 0% 0%
src.routes.api.v1.keys.[id] 0% 0%
src.routes.api.v1.networks 0% 0%
src.routes.api.v1.networks.[id] 0% 0%
src.routes.api.v1.networks.[id].stats 0% 0%
src.routes.api.v1.orgs 0% 0%
src.routes.api.v1.representatives 0% 0%
src.routes.api.v1.sms 0% 0%
src.routes.api.v1.stream 0% 0%
src.routes.api.v1.supporters 30% 18%
src.routes.api.v1.supporters.[id] 0% 0%
src.routes.api.v1.tags 0% 0%
src.routes.api.v1.tags.[id] 0% 0%
src.routes.api.v1.usage 0% 0%
src.routes.api.v1.webhooks 0% 0%
src.routes.api.v1.webhooks.[id] 0% 0%
src.routes.api.v1.webhooks.[id].rotate-secret 0% 0%
src.routes.api.v1.webhooks.[id].test-fire 0% 0%
src.routes.api.v1.workflows 100% 88%
src.routes.api.v1.workflows.[id] 100% 58%
src.routes.api.waitlist 0% 0%
src.routes.api.wallet 0% 0%
src.routes.api.wallet.balance 0% 0%
src.routes.api.wallet.connect 0% 0%
src.routes.api.wallet.disconnect 0% 0%
src.routes.api.wallet.near.sponsor 0% 0%
src.routes.api.wallet.nonce 0% 0%
src.routes.api.wallet.sponsor-userop 99% 77%
src.routes.api.wallet.status 0% 0%
src.routes.auth.coinbase 0% 0%
src.routes.auth.coinbase.callback 0% 0%
src.routes.auth.discord 0% 100%
src.routes.auth.discord.callback 0% 100%
src.routes.auth.facebook 0% 0%
src.routes.auth.facebook.callback 0% 100%
src.routes.auth.google 0% 0%
src.routes.auth.google.callback 0% 100%
src.routes.auth.linkedin 0% 0%
src.routes.auth.linkedin.callback 0% 100%
src.routes.auth.logout 0% 0%
src.routes.auth.prepare 0% 0%
src.routes.auth.twitter 0% 100%
src.routes.auth.twitter.callback 0% 100%
src.routes.browse 0% 0%
src.routes.c.[slug] 0% 0%
src.routes.d.[campaignId] 0% 0%
src.routes.deliberation 0% 0%
src.routes.developers 0% 0%
src.routes.directory 0% 0%
src.routes.dm.[id] 0% 0%
src.routes.dm.[id].scorecard 0% 0%
src.routes.e.[id] 0% 0%
src.routes.embed 0% 100%
src.routes.embed.campaign.[slug] 0% 0%
src.routes.governance 0% 0%
src.routes.help.verification 0% 0%
src.routes.migrate 0% 0%
src.routes.n.[slug] 0% 0%
src.routes.og.campaign.[id] 0% 0%
src.routes.og.integrity 0% 100%
src.routes.og.org 0% 100%
src.routes.og.org-for.[segment] 0% 0%
src.routes.org 0% 0%
src.routes.org.[slug] 0% 0%
src.routes.org.[slug].calls 0% 0%
src.routes.org.[slug].campaigns 0% 0%
src.routes.org.[slug].campaigns.[id] 0% 0%
src.routes.org.[slug].campaigns.[id].report 0% 0%
src.routes.org.[slug].campaigns.[id].report.email-html 0% 0%
src.routes.org.[slug].campaigns.new 0% 0%
src.routes.org.[slug].emails 0% 0%
src.routes.org.[slug].emails.[blastId] 0% 0%
src.routes.org.[slug].emails.[blastId].receipts 0% 0%
src.routes.org.[slug].emails.compose 0% 0%
src.routes.org.[slug].events 0% 0%
src.routes.org.[slug].events.[id] 0% 0%
src.routes.org.[slug].events.[id].attendees.csv 0% 0%
src.routes.org.[slug].events.[id].calendar.ics 0% 0%
src.routes.org.[slug].events.new 0% 0%
src.routes.org.[slug].fundraising 0% 0%
src.routes.org.[slug].fundraising.[id] 0% 0%
src.routes.org.[slug].fundraising.new 0% 0%
src.routes.org.[slug].legislation 0% 0%
src.routes.org.[slug].networks 0% 0%
src.routes.org.[slug].networks.[networkId] 0% 0%
src.routes.org.[slug].networks.new 0% 0%
src.routes.org.[slug].representatives 0% 0%
src.routes.org.[slug].representatives.[repId] 0% 0%
src.routes.org.[slug].results 0% 100%
src.routes.org.[slug].scorecards 0% 0%
src.routes.org.[slug].settings 0% 0%
src.routes.org.[slug].settings.webhooks 0% 0%
src.routes.org.[slug].sms 0% 0%
src.routes.org.[slug].sms.[id] 0% 0%
src.routes.org.[slug].sms.new 0% 0%
src.routes.org.[slug].studio 0% 0%
src.routes.org.[slug].supporters 0% 0%
src.routes.org.[slug].supporters.[id] 0% 0%
src.routes.org.[slug].supporters.import 0% 0%
src.routes.org.[slug].supporters.import.action-network 0% 100%
src.routes.org.[slug].supporters.import.platform-api 0% 0%
src.routes.org.[slug].workflows 0% 0%
src.routes.org.[slug].workflows.[id] 0% 0%
src.routes.org.[slug].workflows.new 0% 0%
src.routes.org.for 0% 100%
src.routes.org.for.agency-rulemaking 0% 0%
src.routes.org.for.local-government 0% 0%
src.routes.org.for.state-legislature 0% 0%
src.routes.org.invite.[token] 0% 0%
src.routes.org.new 0% 0%
src.routes.profile 5% 8%
src.routes.profile.receipts 0% 0%
src.routes.profile.security 5% 6%
src.routes.record 100% 100%
src.routes.record.vol-1.issue-1 0% 0%
src.routes.s.[slug] 0% 0%
src.routes.s.[slug].debate.[debateId] 0% 0%
src.routes.s.[slug].og-image 0% 0%
src.routes.settings.delegation 0% 0%
src.routes.spec 0% 0%
src.routes.template-modal.[slug] 0% 0%
src.routes.unsubscribe 0% 0%
src.routes.unsubscribe.[supporterId].[orgId].[token] 0% 0%
src.routes.v.[hash] 0% 0%
src.routes.verify.[hash] 0% 0%
src.routes.verify.receipt.[id] 0% 0%
Summary 17% (8352 / 49513) 15% (5625 / 38285)

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e42b049c-5b31-4fda-a482-b46061cb3a86

📥 Commits

Reviewing files that changed from the base of the PR and between b226693 and f50f1d4.

⛔ Files ignored due to path filters (1)
  • convex/_generated/api.d.ts is excluded by !**/_generated/**
📒 Files selected for processing (14)
  • convex/users.ts
  • src/lib/components/action/DecisionMakerLandscapeCard.svelte
  • src/lib/components/action/DistrictOfficialCard.svelte
  • src/lib/components/action/PowerLandscape.svelte
  • src/lib/components/action/RoleGroup.svelte
  • src/lib/components/template-browser/TemplatePreview.svelte
  • src/lib/components/template-browser/parts/PreviewContent.svelte
  • src/lib/config/features.ts
  • src/lib/core/agents/agents/subject-line.ts
  • src/lib/core/agents/gemini-client.ts
  • src/lib/services/emailService.ts
  • src/routes/s/[slug]/+page.server.ts
  • src/routes/s/[slug]/+page.svelte
  • tests/unit/agents/gemini-provider.test.ts

📝 Walkthrough

Walkthrough

The PR introduces a getActiveCredentialHash Convex query and threads the credential hash through the server load, proof-footer components, and email service, replacing prior truncated user-ID fallbacks. Bounce reporting is refactored from a page-level aggregated list into per-card props propagated through PowerLandscape and RoleGroup, gated to trust tier ≥ 2. The Gemini model is upgraded and thinking level lowered; the DEBATE feature flag is disabled.

Changes

Credential Hash Feature

Layer / File(s) Summary
Convex getActiveCredentialHash query
convex/users.ts
Auth-scoped query validates userId ownership, selects the active credential row, and returns the hash or null.
Server load: fetch and expose credentialHash
src/routes/s/[slug]/+page.server.ts
Removes delivery-record loading and computePseudonymousId; adds Batch 2 call to getActiveCredentialHash; extends returned user payload with credentialHash; drops deliveredRecipients.
Proof footer in template components and email service
src/lib/components/template-browser/TemplatePreview.svelte, src/lib/components/template-browser/parts/PreviewContent.svelte, src/lib/services/emailService.ts
Adds credentialHash to user prop types; replaces proof-hash derivation from user ID with user.credentialHash; adds showProofFooter guard; shortens displayed hash; removes /v/[idPrefix] email fallback.
Page-level proof footer emission
src/routes/s/[slug]/+page.svelte
Emits commons.email/v/{credentialHash} in buildProofFooter only when data.user.credentialHash is present.

Bounce Reporting Refactor

Layer / File(s) Summary
Bounce flag UI in card components
src/lib/components/action/DistrictOfficialCard.svelte, src/lib/components/action/DecisionMakerLandscapeCard.svelte
Both cards add optional bounce-reporting props, compute canFlagBounce, render a "didn't arrive?" button or "Reported" label, add group class, and add .bounce-flag CSS for hover/focus reveal on fine-pointer devices.
Bounce props propagation through RoleGroup and PowerLandscape
src/lib/components/action/RoleGroup.svelte, src/lib/components/action/PowerLandscape.svelte
RoleGroup derives reported/reporting per member email and forwards to card components; PowerLandscape adds four bounce props with defaults and wires them into every RoleGroup instance.
Page-level bounce state, capability gate, and UI removal
src/routes/s/[slug]/+page.svelte
Adds canReportBounce derived from trust_tier >= 2; removes deliveredRecipients from PowerLandscapeData and old aggregated bounce UI; passes bounce state and handleReportBounce into PowerLandscape.

Gemini Config and Feature Flag Updates

Layer / File(s) Summary
Gemini model upgrade and thinkingLevel reduction
src/lib/core/agents/gemini-client.ts, src/lib/core/agents/agents/subject-line.ts, tests/unit/agents/gemini-provider.test.ts
Pins GEMINI_CONFIG to gemini-3.5-flash; changes subject-line agent interact calls to thinkingLevel: 'low'; updates unit test assertion to match.
DEBATE feature flag disabled
src/lib/config/features.ts
Flips FEATURES.DEBATE from true to false.

Sequence Diagram(s)

sequenceDiagram
  participant Browser as Browser (+page.svelte)
  participant PageServer as +page.server.ts
  participant Convex as convex/users.ts (getActiveCredentialHash)
  participant PowerLandscape as PowerLandscape
  participant RoleGroup as RoleGroup
  participant Card as DistrictOfficialCard / DecisionMakerLandscapeCard

  PageServer->>Convex: getActiveCredentialHash({ userId })
  Convex-->>PageServer: credentialHash | null
  PageServer-->>Browser: user { credentialHash, trust_tier, ... }
  Browser->>Browser: canReportBounce = trust_tier >= 2
  Browser->>PowerLandscape: canReportBounce, reportedBounces, reportingBounce, onReportBounce
  PowerLandscape->>RoleGroup: same bounce props
  RoleGroup->>Card: canReportBounce, reported, reporting, onReportBounce
  Card-->>Browser: onReportBounce(email) on button click
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 A hash from the vault, no more truncated tail,
The credential flows true from Convex to trail.
Each card now whispers, "did the email arrive?"
While Gemini leaps to a swifter 3.5 dive.
DEBATE takes a nap, low-thinking takes flight —
The rabbit hops forward, and everything's right! ✨

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch recover-proof-footer

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ejmockler
ejmockler merged commit 5dac729 into main Jun 15, 2026
3 of 5 checks passed

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🪓 Brutalist Review

Both critics agree that the session-scoped bounce reporting is the headline problem: the "didn't arrive?" affordance is gated behind contacted state that evaporates on every reload, making the feature unreachable for its most common use-case (a user returns hours later after seeing a bounce). The credential-hash fix itself is correct — replacing 404-guaranteed truncated user IDs is the right call — but both critics flag that failure is now silent (.catch(() => null) is indistinguishable from "no credential"). Claude focuses on CSS scope escape and component duplication accumulating as tech debt; Codex emphasizes the operational risk of leaving maxOutputTokens: 65536 unchanged when that same ceiling caused the original runaway incident and could do so again under a prompt or schema regression. The Codex claim of a duplicate class attribute in DistrictOfficialCard.svelte was wrong — the actual file is clean — and was dropped.

Inline comments: 7 (1 🟠 high · 6 🟡 medium)

Per-CLI breakdown

✅ Claude (default, 146448ms)

7 findings across security, performance, correctness, and maintainability. Headlined the session-scoped bounce reporting regression and the :global(.group:hover) CSS scope escape. Strong focus on the duplication accumulating across the two card components and the credential hash now living in page source. All claims verified against actual files.

✅ Codex (default, 126850ms)

6 findings after dropping 1 false positive (fabricated duplicate class attribute in DistrictOfficialCard that doesn't exist in the actual file). Valid catches: maxOutputTokens ceiling unchanged, silent credential hash failure, proof-link policy duplication, and DEBATE as a source-code kill switch. Corroborates Claude on the session-scoped bounce UX regression from the server-load angle.

❌ agy (default, 900010ms)

Timed out after 900s. No findings contributed.

Out-of-diff findings (6)

correctness

  • 🟠 high src/lib/components/action/DecisionMakerLandscapeCard.svelteClaude [unanchored]: Bounce affordance gated on session-only contacted state — invisible after any reload
  • 🔵 low src/lib/components/action/DecisionMakerLandscapeCard.svelteClaude [sub-threshold]: :global(.group:hover) .bounce-flag matches any .group ancestor, not just the immediate card wrapper
  • 🔵 low src/routes/s/[slug]/+page.svelteClaude [sub-threshold]: canReportBounce is stale: trust_tier from server load doesn't update reactively mid-session
  • 🔵 low src/routes/s/[slug]/+page.server.tsClaude [sub-threshold]: userId cast as Id<'users'> without non-empty validation before the Convex call

perf

  • 🔵 low src/routes/s/[slug]/+page.server.tsClaude [sub-threshold]: getActiveCredentialHash placed in Batch 2 but depends only on locals — adds unnecessary serial latency

testing

  • 🔵 low tests/unit/agents/gemini-provider.test.tsCodex [sub-threshold]: Updated tests only assert the new constant values — the regression they guard against (runaway output) is untested

Brutalist orchestrator schemaVersion=1 · context_id=6b42a854-7662-4e68-b7c3-d5d54539fa51

})(),

// Batch 2: Queries depending on Batch 1 results.
// Delivery records are intentionally NOT loaded here: a mailto handoff is not

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🪓 Brutalist — 1 critic, rollup: 🟠 high

[Codex 🟠 high] correctness — Deliberate removal of delivery persistence makes bounce reporting unreachable after reload

The PR removes deliveredRecipients from the server load and the corresponding client-side restoration of contactedRecipients. The justification — a mailto handoff is not a confirmed send — is correct in principle, but the operational consequence is that bounce data will be systematically undercounted. Bounce discovery is inherently async: the user sends, receives a bounce reply, and returns to report it. Every return visit starts with contactedRecipients = new Set(), so the per-recipient 'didn't arrive?' button is invisible. The old aggregate bounce list below the landscape was a lower-fidelity UX, but it was reachable across sessions. The new per-recipient affordance is better UX when reachable, but unreachable is worse than imperfect. Recommend either restoring some form of delivery state (even session-storage) or adding a dedicated out-of-band bounce-report path that doesn't depend on transient contacted state.

Suggested change
// Delivery records are intentionally NOT loaded here: a mailto handoff is not
// Minimal sessionStorage approach in page.svelte $effect:
// onMount(() => {
// const stored = sessionStorage.getItem(`contacted:${templateId}`);
// if (stored) contactedRecipients = new Set(JSON.parse(stored));
// });
// after handleWriteTo: sessionStorage.setItem(`contacted:${templateId}`, JSON.stringify([...contactedRecipients]));


// A bounce only means something for a direct email route; CWC/form delivery
// has no email to bounce. Tier gate is enforced by the parent (server too).
const canFlagBounce = $derived(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🪓 Brutalist — 1 critic, rollup: 🟡 medium

[Claude 🟡 medium] maintainability — Bounce logic duplicated verbatim in DecisionMakerLandscapeCard and DistrictOfficialCard

~80 lines of identical code now exist in both card components: the four-prop signature (canReportBounce, reported, reporting, onReportBounce), the canFlagBounce derived expression, the reportBounce() function, the reported/unreported template block, and the entire .bounce-flag CSS block with its @media (hover: hover) reveal rules and :disabled override. This is the same duplication pattern that already made the existing showRoleBadge, departing, and contactedRecipients logic hard to maintain across these two files — every feature addition has to be applied twice. The next change in this area will either introduce a regression in one component or force the shared-component refactor. Extracting a <BounceFlag> Svelte component would reduce this to a single pass-through prop each.

Suggested change
const canFlagBounce = $derived(
// Extract to src/lib/components/action/BounceFlag.svelte:
// <script lang="ts">
// let { reported, reporting, onReportBounce }: {
// reported: boolean; reporting: boolean;
// onReportBounce: () => void;
// } = $props();
// </script>
// then in each card: <BounceFlag {reported} {reporting} onReportBounce={() => reportBounce(e)} />

export const FEATURES = {
/** Deliberation surfaces, argument submission, LMSR market, resolution/appeal */
DEBATE: true,
DEBATE: false,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🪓 Brutalist — 1 critic, rollup: 🟡 medium

[Codex 🟡 medium] maintainability — DEBATE: false is a hard source-code kill switch with no env override — rollback requires a deploy

Flipping DEBATE to false disables deliberation surfaces, argument submission, LMSR market, and resolution/appeal globally in every environment that picks up this build. There is no environment-variable override shown, so reverting the kill requires another full code deploy. The surrounding infrastructure (DebateRow types, debate route branches, load-path debate queries) still exists and still runs through the page load function — it just doesn't render. If the intent is a temporary gate, an env override like VITE_ENABLE_DEBATE !== 'false' would allow toggling without a redeploy. If it is a permanent removal, the dead code should be cleaned up to avoid maintaining it.

model: 'gemini-3.5-flash',
defaults: {
temperature: 0.3,
maxOutputTokens: 65536,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🪓 Brutalist — 1 critic, rollup: 🟡 medium

[Codex 🟡 medium] perf — maxOutputTokens: 65536 unchanged — the 200s runaway ceiling remains in place for all agent calls

The comment above this config documents exactly the failure mode: the prior model 'generated to the 65k output cap (~64k tokens, ~200s, MAX_TOKENS truncation) on every subject-line call.' The fix switches model and lowers thinkingLevel — both good mitigations — but leaves the system-wide output cap at 65 536 tokens. A subject-line agent producing a JSON object with five fields has no legitimate need for that headroom. If the model, prompt, or schema enforcement regresses (including on a future Gemini rollout behind the pinned version), the full 200-second blast radius is still available. The call site in subject-line.ts should pass a per-call maxOutputTokens of 1 000–2 000 to cap the damage, leaving the default ceiling for agents that genuinely need it.

footer += `\n${proofLine}`;
// Only emit the verify URL when it resolves: the active credential
// hash is the record /v/[hash] looks up. A truncated user id 404s.
if (user?.credentialHash) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🪓 Brutalist — 1 critic, rollup: 🟡 medium

[Codex 🟡 medium] maintainability — Proof-link policy ('only credentialHash may produce /v/[hash]') duplicated across three rendering paths

The rule 'only emit commons.email/v/[hash] when a resolved credentialHash exists' is now enforced independently in emailService.ts (line 318), PreviewContent.svelte (line 76), and +page.svelte (line 71). The previous truncated-user-id bug was precisely this kind of drift — each rendering path had its own implementation of the same policy and they diverged. A shared helper (e.g., getProofUrl(user): string | null) would make the invariant auditable in one place and prevent the next silent divergence.

userId
? serverQuery(api.users.getActiveCredentialHash, { userId: userId as Id<'users'> })
.then((r) => r?.credentialHash ?? null)
.catch(() => null)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🪓 Brutalist — 1 critic, rollup: 🟡 medium

[Codex 🟡 medium] correctness — Credential hash lookup swallows all errors silently — outage is indistinguishable from 'no credential'

Every error path from getActiveCredentialHash — Convex outage, deployment mismatch, auth token problem, schema regression — collapses into null, which the downstream renderers interpret as 'user has no active credential; render no proof link.' The user still sends the email, but the verification attestation silently disappears. There is no monitoring signal distinguishing 'zero users have credentials' (valid) from 'the credential lookup is broken' (incident). At minimum, log the error before returning null so the failure is observable: catch((err) => { console.error('credential hash lookup failed', err); return null; }).

Suggested change
.catch(() => null)
.catch((err) => {
console.error('[page.server] getActiveCredentialHash failed:', err);
return null;
})

trust_tier: locals.user.trust_tier,
is_verified: locals.user.is_verified
is_verified: locals.user.is_verified,
credentialHash

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🪓 Brutalist — 1 critic, rollup: 🟡 medium

[Claude 🟡 medium] security — Full 64-char credentialHash serialized into SvelteKit page payload / /__data.json

The complete credential hash is now placed in the server return payload and therefore written into the page HTML source and queryable unauthenticated at /__data.json. The doc comment in convex/users.ts argues it's 'public by design' — it already appears in email footers and resolveCredentialHash is unauthenticated. That's defensible, but the old code never put this value in the page source; it put 8 chars of a user ID. Any unauthenticated observer of the rendered page (CDN edge cache, browser history, shared device, network inspector) now has the full token needed to hit /v/[hash] and confirm 'this user has an active credential in this district.' Verify that resolveCredentialHash returns nothing beyond what is already public, and confirm you are comfortable with the full hash appearing in CDN caches.

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