fix (ornith-35): watchlistRepository double-encoding bug — single JSON.stringify, 13/13 tests pass

This commit is contained in:
Investor Flow Build
2026-06-30 17:54:01 -04:00
parent 97607e0bd4
commit 1007ab4ed5
62 changed files with 11617 additions and 34 deletions
+21
View File
@@ -0,0 +1,21 @@
# ADR-0001: Local-first, multi-tenant architecture
Date: 2026-06-27
Status: Accepted (design phase)
## Context
Investor Flow must serve many beginner users (not just the operator) while honoring "store locally, run reports, avoid rate limits." Single-user local SQLite does not fit once multiple users exist.
## Decision
One shared Bun + SQLite backend (Docker Compose), local-first (not cloud). Per-user logical isolation inside one DB:
- Tier A (shared market cache): price, options, filings, 13F/13G/4 snapshots, sector map.
- Tier B (shared content): X/Reddit thread mirrors, adapter backoff state.
- Tier C (per-user, ownerId): watchlists, portfolios, journal, reports, alerts, trusted accounts, saved posts.
- Tier D (system): users, sessions.
Adapter queue dedupes: User A and B both requesting NVDA price within staleness window triggers ONE fetch; result shared.
## Consequences
- Multi-tenant + local-first coexist via shared public cache, private user rows.
- Most LLM summaries are free for second-and-later users (deterministic by prompt-hash).
- Operator runs admin tooling for user management, GDPR export, queue health.
@@ -0,0 +1,18 @@
# ADR-0002: Automaton as the issue tracker for engineering skills
Date: 2026-06-27
Status: Accepted
## Context
Matt Pocock's engineering skills (to-prd, to-issues, triage, setup-*) assume an external issue tracker (gh/glab/.scratch). This project uses Automaton tasks + phases instead.
## Decision
Adopt Automaton as the issue tracker. Mapping:
- Issues = task folders under .automaton/tasks/<name>/.
- Triage states → phases: needs-triage=new, needs-info=research (awaiting user), ready-for-agent=decomposition:approved/implement, ready-for-human=flagged in notes, wontfix=complete+tombstone.
- Publish issues = automaton_orchestrate_finalize(task, subtasks) from DECOMPOSITION.md.
- The 5 issue-tracker skills are rewritten to call Automaton; the 8 methodology skills port verbatim.
## Consequences
- Skills installed as global pi dev skills at ~/.agents/skills/, symlinked into ~/.pi/agent/skills/.
- One house flow: grill-with-docs → to-prd → to-issues → implement(tdd) → code_review:approved.
+12
View File
@@ -0,0 +1,12 @@
# ADR-0003: Cookie-based X (Twitter) data source
Date: 2026-06-27
Status: Accepted
## Decision
X research feed uses cookie-based auth (operator-supplied auth_token + ct0 in a gitignored local secrets file). One shared read-only X source, not per-user.
## Consequences
- TOS-sensitive: read-only, rate-limited, attribution preserved, no bulk scraping. Operator owns cookie freshness.
- Output → Tier B shared content (thread mirrors). Trusted-account allowlists are per-user (Tier C).
- Adapter uses bird CLI / Twitter API v2 with cookies for cashtag search ($SYMBOL) + curated timelines.
@@ -0,0 +1,12 @@
# ADR-0004: Shared market cache with multi-tenant fetch dedupe
Date: 2026-06-27
Status: Accepted
## Decision
Public market data (price, filings, ownership snapshots) is shared across all users. The adapter queue dedupes concurrent cache-misses from multiple users into a single fetch.
## Consequences
- Rare public-data fetches; private user data isolated by ownerId.
- Staleness windows per data class (live quote 1min, daily OHLCV permanent, filings immutable forever, 13F per-quarter immutable).
- Report runner reconstructs from cache only; no live calls.
@@ -0,0 +1,26 @@
# ADR-0005: Analyst Voice — Alfred-leaning single house voice
Date: 2026-06-27
Status: Accepted
## Context
LLM-generated summaries need a consistent, trustworthy voice that teaches beginners without baby talk, grounded in real investors' public conduct.
## Decision
Single house voice (not a per-user picker). 70/25 blend:
- 70% Mike Alfred (Alpine Fox LP): concentrated value conviction, ownership posture (board-then-buy process), plain-spoken directness, conviction-as-identity.
- 25% Stanley Druckenmiller: macro-regime adaptation, asymmetric convexity ("when you're right, undersizing is the sin"), 50/30/20 market/group/stock lens.
Voice rules:
1. Process over prediction (trace reasoning, never bare forecast).
2. Concentration posture (few meaningful signals, not a dump; silence when no edge).
3. Macro + company twin-lens.
4. Honest about uncertainty & invalidation.
5. P6 plain English (jargon always glossed).
6. Cite every claim to a cached source row.
7. Confident when conviction exists; silent when it doesn't.
## Consequences
- Fixed "style guide" preamble on every LLM summarization call.
- Cached by prompt-hash + source-hash so the voice doesn't drift run-to-run.
- Every summary reads like the same analyst, consistently.
+49
View File
@@ -0,0 +1,49 @@
# ADR-0006: LLM data provenance — no training, no retention, local-first
Date: 2026-06-27
Status: Accepted (design phase)
Supersedes: none
Related: ADR-0001 (local-first multi-tenant), ADR-0005 (Analyst Voice)
## Context
Investor Flow sends sensitive user data to an LLM for summaries, explainers, alerts, and commentary: SEC filings, portfolio positions, trade theses, sentiment annotations, and the user's own journal entries. This is a legal and trust issue, not just a preference.
Provider policies vary materially and can change with notice:
- OpenAI / Anthropic / Google: default may include training eligibility unless explicitly disabled via API toggle.
- OpenCode Go (paid Zen tier): Terms explicitly exclude paid-account Content from the "develop and improve our services" grant, so OpenCode itself is not training on prompts. THIS IS SAFE FOR THE OPERATOR'S OWN CODING USE. Residual gap: the underlying model vendor (e.g., Zhipu AI for GLM 5.2) may have its own unclear retention/transit policy — not addressed by OpenCode's Terms.
- Hosted SaaS model providers generally: subject to upstream-vendor ambiguity + policy-drift.
The app cannot rely on each external provider's current policy staying safe; it must enforce local-first in code so sensitive user data never leaves the operator's machine by construction.
## Decision — three hard constraints baked into the build
### Constraint 1 — Production LLM Gateway defaults to local OpenAI-compatible endpoint
- The `LLMGateway` deep module (M14) must default its `baseURL` to a **local endpoint** (e.g., `http://localhost:11434/v1` for Ollama, or a self-hosted vLLM/serverless endpoint on the operator's LAN).
- The local endpoint is the only provider configuration loaded by default from environment/secrets.
- Non-local providers MAY be supported behind the provider-agnostic interface, but their activation requires:
- An explicit operator override in a gitignored secrets file (`secrets/llm_providers.local.json`), never committed.
- A documented data-provenance review per provider (recorded as an ADR or provider-config note: what that provider's policy is as of the review date, who reviewed, when).
### Constraint 2 — Sensitive user data never routed through external LLM providers
- Sensitive user category (default): portfolio positions, trade plans, journal entries, SEC filings content, sentiment annotations, saved posts, any data tagged `ownerId` or sourced from Tier C / shared Tier A filings/threads.
- The Gateway enforces a **data-classification gate** before any non-local provider call:
- Local provider → any data allowed (nothing leaves the host).
- Non-local provider → only data explicitly classified `public-safe` (e.g., generic financial term glosses, non-user-specific educational content) is eligible; sensitive user data is blocked at the Gateway with a typed error, not sent.
- This is a code-level guarantee, not a runtime toggle — tests assert the gate refuses sensitive payloads against non-local providers.
### Constraint 3 — Developer conduct for the build itself
- When building Investor Flow via external AI assistants (OpenCode Go + GLM 5.2, Claude Code, etc.), developers do NOT paste live user-data samples into prompts.
- Use **fixtures and anonymized synthetic data** for any prompt that touches realistic shapes (filing text, portfolio rows, journal entries). The repo includes a `fixtures/` directory of synthetic, non-PII, freely-shareable sample data for this purpose.
- This keeps the OpenCode Go paid-tier-safety (which holds today) from being the only safeguard; it removes the risk vector upstream of any policy.
## Consequences
- The app's production LLM never sees user data leave the host → the entire upstream-vendor ambiguity + future-policy-drift question is eliminated by architecture, not by trust.
- Operators who want best-quality hosted models for non-sensitive features (e.g., the "beginner explainer" on public market data) can configure them via explicit override; the data gate still refuses anything sensitive.
- A fixture corpus must be maintained for realistic prompt testing; CI asserts the data-classification gate works against a fuzz set of sensitive vs payload-safe inputs.
- LLMGateway's interface stays provider-agnostic (same as ADR-0005 contract); only the *default* + *data gate* are new.
## Implementation notes (for DESIGN.md Section 3)
- `LLMGateway.classifyPayload(payload): 'public_safe' | 'sensitive'` runs before any provider dispatch.
- `LLMGateway.dispatch(feature, payload, opts): Promise<Summary>` routes: if classified sensitive AND provider is non-local → throw `SensitiveDataBlockedError`; never send.
- Provider config schema: `{ id, baseURL, isLocal: boolean }`; `isLocal` trusts only `localhost`, `127.0.0.1`, `::1`, and entries in `LOCAL_LLM_SUBNETS` env override.
- Tests: property-test the classifier over a fuzz corpus; contract-test `dispatch` rejects sensitive payloads on non-local providers.
@@ -0,0 +1,32 @@
# ADR-0007: Education, not investment advice
Date: 2026-06-28
Status: Accepted (design phase)
## Context
Investor Flow is beginner-first and reads like a mentor (70/25 Alfred/Druckenmiller). The user is a beginner. Surfacing personalized, per-portfolio analysis in a mentor voice risks crossing the line from "educational publisher" into "investment adviser" (US SEC IA definition), which would invoke registration, fiduciary, and liability obligations the product does not assume. The user explicitly stated: "this is more of an investment education approach. not financial advice. that should be a primary rule/goal."
## Decision
Investor Flow operates as an **educational research terminal**, not an investment adviser. This is the Primary Rule and takes precedence over every other UX principle when in conflict.
### Concrete teeth
- **Voice:** Analyst Voice teaches *process* and *reasoning*; never "buy/sell/hold this." Recommendations are reworded to **considerations + questions**.
- **RiskEngine `recommendedActions`:** renamed in contract and UI.
- `cut_to_cash` → `consider_reducing_position` (framed: "your drawdown framework says reduce; here's the trade-off to think through")
- `halt_new_entries` stays (it's a guardrail on the app's own journal, not an instruction about the user's brokerage)
- `trim_cluster` → `consider_rebalancing_cluster`
All carry explicit "educational, not advice" framing.
- **SizingEngine:** outputs the *math* ("if risk budget is 1% and stop is $4 → ~Y shares"), never "buy Y shares."
- **Journal:** prompts ask "what's your reasoning?" not "do this."
- **Alerts:** "something changed in the data you're watching" not "action needed." Push informs, never directs.
- **Reports:** every report closes with an explicit footer: *"Educational analysis, not investment advice. Verify the underlying data; you are responsible for your own decisions."*
- **LLM Gateway preamble (ADR-0005):** extended to forbid imperative trade instructions; require educational framing ("a disciplined investor might consider...", "the framework raises these questions...").
### Legal posture
Operates as an **educational publisher**. Output is framed as teaching reasoning about securities, not advising transactions. The Data-Classification Gate (ADR-0006) still keeps sensitive user thesis data local; this ADR governs *output posture* in addition to *input handling*.
## Consequences
- Every UI copy review and LLM prompt template reviewed against this rule.
- A "Primary Rule lint": no curated string in the app may contain an unframed imperative trade directive ("buy", "sell", "you should", "add to your portfolio").
- Does NOT weaken the product: education IS the moat (Alfred teaches his thesis publicly; Druckenmiller teaches adapt-don't-predict). The app is that teaching, interactive.
- Subject to jurisdiction; operator-owned responsibility to display disclaimers appropriate to deployment locale.
+25
View File
@@ -0,0 +1,25 @@
# ADR-0008: Default LLM provider = `ornith` (remote-local OpenAI-compatible)
Date: 2026-06-28
Status: Accepted (design phase)
## Context
The LLM Gateway (ADR-0005, ADR-0006) is provider-agnostic by design. The operator uses a remote-hosted LLM endpoint named `ornith` exposed via the standard OpenAI-compatible REST interface (`/v1/chat/completions`). It is "remote-local": remote in network location, local in trust posture (owned by the operator, not a third-party SaaS). For sensitive content (ADR-0006 Data-Classification Gate), `ornith` qualifies as the local-only provider because the operator owns it.
## Decision
- Default primary LLM provider = `ornith` (OpenAI-compatible REST endpoint).
- `llm_providers` table (Section 1 schema) seeded on first boot with:
- `id='ornith', name='ornith', base_url=<from env ORNITH_LLM_URL>, api_key=<from env ORNITH_LLM_API_KEY>, is_local=true, default_for_public=true, default_for_sensitive=true`
- A secondary fallback provider (e.g. local Ollama or another OpenAI-compatible endpoint) may be configured but is optional in v1.
- Env vars `ORNITH_LLM_URL` and `ORNITH_LLM_API_KEY` injected via Docker Compose secrets (slice 26); never written to the repo.
- `LLMGateway.dispatch` resumes honoring classifyPayload (ADR-0006): sensitive features route per `llm_providers.is_local`; `ornith` is `is_local=true`, so it serves thesis_monitor_l1 / sizing_explain / derisk_suggestion / macro_commentary as well as public features.
- Analyst Voice preamble (ADR-0005) + Primary-Rule (ADR-0007) passed to `ornith` on every dispatch regardless of feature.
## Implementation model (updated)
Ornith (35B MoE) is also the **primary implementation/coding model** for the loop-runner orchestrator, prioritized over Qwythos-9B (9B dense) for code generation quality. Qwythos-9B remains available as a local fallback. The orchestrator dispatches `pi --print --model remote/ornith:medium` for implementer ticks; reviews the diff itself; cross-checks with `remote/ornith` or `omlx/Qwythos-9B` as needed.
## Consequences
- Loop-runner implementer configures `ornith` in the FakeLLMGateway fixture path with a canned OpenAI-compatible shape, and real `LLMGateway` reads `llm_providers` + env at boot.
- Tests use FakeLLMGateway (no network) so `ornith` is never required for green tests; tests asserting prompt-hash cache + Analyst Voice preamble still pass.
- If `ornith` is unreachable at runtime, LLM features degrade gracefully (cached summary if available; "LLM source degraded" UI banner; cache-first rule preserved).
- ADR-0006's "local-only for sensitive" obligation is honored because `ornith` is operator-owned and configured `is_local=true`.