compliance: flag-gate AI portfolio + cloud sync + Stripe; de-risk prompts; harden reviewer

Implements docs/read-markets-compliance-changes.md as flag-gated changes
(no deletions) so paused features stay in the tree for future re-enable.
All four flags default False so a fresh deploy is compliance-safe.

- New env flags: PORTFOLIO_AI_ENABLED, PORTFOLIO_SYNC_ENABLED,
  TICKER_UNIVERSE_AGGREGATE_ENABLED, SUBSCRIPTIONS_ENABLED.
- Gates: /api/analyze, /api/portfolio/sync*, /api/stripe/*, /pricing,
  ticker_universe writes, portfolio_analysis.analyse(). is_paid_active()
  returns True for any auth'd user when subscriptions are paused.
- Prompts (PROMPT_VERSION 10): universal _COMPLIANCE_RIDER prepended to
  every system prompt; watch list removed; price-target / close-above-below
  / trigger / forward-state-as-description rules added; SPECULATIVE
  pivoted to regime-only scenarios; daily + weekly digests tightened.
- Reviewer: deterministic regex/lexicon pre-check fail-closed under the
  Haiku call; portfolio rider gated by PORTFOLIO_AI_ENABLED; base prompt
  sharpened for forward-state and MAR forward-opinion patterns;
  ReviewerVerdict audit table; generate_with_review retry helper.
- Migration 0026: purge portfolio_sync + ticker_universe; create
  reviewer_verdicts.
- Copy: MAR cite fixed to Art 3(1)(35) + Art 20 + Del Reg 2016/958;
  portfolio reframed as browser-only viewer in disclaimer / privacy /
  terms / about / pricing / landing (en + it). TODO(legal) marker for
  lawyer sign-off on disclaimer.
- Tests: 13 lexicon + 6 reviewer compliance regressions; conftest enables
  all flags so existing 402 tests still cover their code paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Giorgio Gilestro 2026-05-29 19:57:12 +02:00
parent ee8384f1ba
commit 47dce1a1a4
38 changed files with 1188 additions and 279 deletions

View file

@ -37,24 +37,9 @@
It contains your user id only and is signed so we can detect
tampering. Cookie is marked Secure and HttpOnly.
</li>
<li>
<strong>Anonymous ticker universe</strong>: when you upload a
portfolio CSV we record which Yahoo tickers appear, with
<em>no link</em> to your account. The same row would exist whether
any specific user holds the ticker or not &mdash; once a ticker is in
the universe, the row carries no signal as to whose import added it.
</li>
<li>
<strong>If you opt in to encrypted cloud sync</strong>: an opaque
blob of bytes per user. The blob is your portfolio, encrypted in
your browser with a PIN you choose, then wrapped a second time on
the server with a key only the server holds. We can&rsquo;t decrypt
the blob to plaintext without your PIN, and we can&rsquo;t recover
your PIN if you forget it. By enabling cloud sync you give your
consent (UK-GDPR Art. 6(1)(a)) to this processing; you can
withdraw consent at any time by disabling sync in Settings, which
also removes the server-side blob.
</li>
{# Cloud sync + server-side per-ticker aggregate union are flag-gated off.
See docs/read-markets-compliance-changes.md and app/config.py
(PORTFOLIO_SYNC_ENABLED, TICKER_UNIVERSE_AGGREGATE_ENABLED). #}
<li>
<strong>Anonymised cost ledger</strong> of AI calls (model, tokens,
cost). No portfolio or personal data is attached to ledger rows.
@ -76,10 +61,12 @@
<h2 class="public-section__head">What we don&rsquo;t collect</h2>
<ul>
<li>
<strong>Your portfolio holdings as plaintext on the server.</strong>
Parsed pies are returned to your browser and kept in
<code>localStorage</code>. The server&rsquo;s view is the anonymous
ticker universe described above.
<strong>Your portfolio holdings, in any form, on the server.</strong>
The portfolio feature is a browser-only composition viewer:
uploaded CSVs are parsed and returned to your browser, kept in
<code>localStorage</code>, and never sent back to or stored on
the server. The server records no per-ticker aggregate of what
anyone holds.
</li>
<li>
<strong>Third-party analytics or ad cookies.</strong> No Google
@ -104,24 +91,14 @@
<ul>
<li>
<strong>Performance of a contract</strong> (Art. 6(1)(b)) &mdash; for
operating your account, the sign-in flow, paid features, and the
mechanics of encrypted cloud sync.
operating your account, the sign-in flow, and any paid features.
</li>
<li>
<strong>Legitimate interests</strong> (Art. 6(1)(f)) &mdash; for the
anonymous ticker universe, the anonymised cost ledger, job-run
telemetry, and reverse-proxy access logs. Our interest is the
secure, abuse-resistant, cost-controlled operation of a free
public service, balanced against the minimal and de-identified
nature of the data.
</li>
<li>
<strong>Consent</strong> (Art. 6(1)(a)) &mdash; where you opt in to
encrypted cloud sync (and the related caching of a derived
encryption key in your browser&rsquo;s <code>sessionStorage</code>).
You can withdraw consent at any time by disabling sync in
Settings; the cached key is cleared and the server-side blob is
removed.
anonymised cost ledger, job-run telemetry, and reverse-proxy access
logs. Our interest is the secure, abuse-resistant, cost-controlled
operation of a free public service, balanced against the minimal
and de-identified nature of the data.
</li>
</ul>
</section>
@ -131,9 +108,10 @@
<p>
The Service does not make decisions about you that produce legal or
similarly significant effects in an automated way (UK-GDPR Art. 22).
The AI portfolio analysis is editorial commentary on the holdings
you upload; it does not approve, reject or rank you, and you remain
the sole decision-maker about anything in your account.
The strategic log and indicator summaries are general editorial
commentary on public market data, not personalised assessments of
you, and you remain the sole decision-maker about anything in your
account.
</p>
</section>
@ -151,13 +129,9 @@
browser.
</li>
<li>
<strong>Local portfolio + cached sync key</strong> &mdash; parsed pies
live in <code>localStorage</code> on your device. If you enable
cloud sync, the derived encryption key is cached in
<code>sessionStorage</code> so you don&rsquo;t have to re-enter
your PIN on every navigation. This caching is performed only with
your consent (given when you enable sync); it is cleared when you
close the tab or disable sync.
<strong>Local portfolio</strong> &mdash; parsed pies live in
<code>localStorage</code> on your device. They are not sent to
or stored on the server.
</li>
</ul>
</section>
@ -177,14 +151,15 @@
currently inside the UK; if that changes we will update this notice.
</li>
<li>
<strong>AI provider calls</strong> for the strategic log, indicator
summaries, and (paid) portfolio analysis. Where the provider sits
outside the UK, we rely on the UK International Data Transfer
Agreement (IDTA) / the UK Addendum to the EU Standard Contractual
Clauses where no adequacy decision applies. Each outbound request
carries an explicit no-training opt-out header
<strong>AI provider calls</strong> for the strategic log and
indicator summaries. Where the provider sits outside the UK, we
rely on the UK International Data Transfer Agreement (IDTA) / the
UK Addendum to the EU Standard Contractual Clauses where no
adequacy decision applies. Each outbound request carries an
explicit no-training opt-out header
(<code>X-OR-Allow-Training: false</code> on OpenRouter); see the
Third parties section below for the caveats.
Third parties section below for the caveats. None of these
outbound requests contain user holdings or other portfolio data.
</li>
</ul>
</section>
@ -200,15 +175,6 @@
<strong>Session cookies</strong>: expire automatically; you can
sign out at any time to revoke.
</li>
<li>
<strong>Ticker universe</strong>: rows untouched for 60 days are
evicted by a nightly job. Active tickers remain.
</li>
<li>
<strong>Encrypted portfolio blob</strong>: kept until you disable
cloud sync (one click in Settings) or delete your account. We hold
one row per user; new uploads overwrite the previous blob.
</li>
<li>
<strong>Account</strong>: held until you ask us to delete it.
Email <a href="mailto:{{ OPERATOR_EMAIL }}">{{ OPERATOR_EMAIL }}</a>.
@ -230,22 +196,18 @@
</li>
<li>
<strong>AI provider(s)</strong>: DeepSeek (primary) with OpenRouter
as a fallback. They see the prompt for the strategic log, the
indicator summaries, and the portfolio analysis call &mdash; which
contains your holdings only when you press
&ldquo;Generate AI analysis&rdquo; on a paid plan, and only for the
duration of that single call. The portfolio analysis output is not
persisted on the server.
as a fallback. They see the prompt for the strategic log and the
indicator summaries. These prompts contain public market data and
headlines &mdash; never any user holdings or portfolio data.
<br>
<strong>No-training opt-out.</strong> Every OpenRouter request
carries the <code>X-OR-Allow-Training: false</code> header, which
signals to OpenRouter and any compatible upstream that the prompt
must not be used to train or improve models. DeepSeek does not
currently expose a per-request opt-out; if you do not want your
holdings to leave our server at all, do not use the AI portfolio
analysis feature. We do not control retention or training policies
on the provider side beyond the headers we set &mdash; the provider&rsquo;s
own published data policy is the binding statement on that point.
currently expose a per-request opt-out. We do not control
retention or training policies on the provider side beyond the
headers we set &mdash; the provider&rsquo;s own published data policy is
the binding statement on that point.
</li>
<li>
<strong>Market-data sources</strong>: Yahoo Finance and a small set
@ -263,7 +225,7 @@
<li>Have inaccurate data corrected (Art. 16, rectification).</li>
<li>Have your account and associated data deleted (Art. 17, erasure).</li>
<li>Export the data you can recognise (Art. 20, portability): your
email, any active encrypted blob, your referral linkage.</li>
email and your referral linkage.</li>
<li>Restrict processing (Art. 18).</li>
<li>Object specifically to processing carried out on the basis of
legitimate interests (Art. 21), including any direct marketing.</li>