maintainer
Maintaining
How cancri is put together, and the invariants a maintainer protects. Every load-bearing decision is an smADR; this page is the map, not a substitute for them.
Three runtime classes
cancri splits cleanly into three execution classes (ADR-0002), each with a different trust and lifetime profile.
| class | runs as | responsibility |
|---|---|---|
| client | browser · Firebase Hosting | vanilla-TS terminal, single rAF loop. Subscribes to normalised ticks only — no source internals, no keys. |
| request/response | Cloud Functions (2nd gen) | Gemini normalise/confirm + logo. Vertex via IAM, no API key. |
| always-on / scheduled | Cloud Run service + Job | The feed-engine (sole RTDB writer) and the self-heal capture Job. |
The data layer
Built from scratch behind a source-agnostic adapter interface that delivers normalised ticks
(ADR-0006). The one shared seam is packages/data-contracts — nothing source-specific may
cross it (ADR-0009).
primary L&S (ls-tc.de)
Real-time, cent-accurate, an undocumented Lightstreamer-6 protocol. Isolated in
packages/ls-protocol as the break surface — a versioned config that the self-heal targets.
fallback Yahoo
Delayed ~15 min, always marked freshness: delayed. Doubles as the runtime
sanity oracle — the cross-check that primary prices are plausible.
The feed-engine is a single-process singleton (ADR-0003) and the
sole writer to the RTDB tick bus (ADR-0004 / ADR-0005). The client reads
/quotes/{isin} and /feed/status read-only.
Degradation FSM — honesty over blackout
When the primary feed is lost, the dashboard does not blank. A finite-state machine drives a visible, honest transition:
LIVE ──(primary lost)──▶ RECONNECT ──(give up after n/5)──▶ DEGRADED
▲ │
└──────────────────(primary recovers)─────────────────────┘
on transition: header flips LIVE→DELAYED · master dot recolours + switches to the
slower off-beat pulse · degraded banner slides in · every row → delayed · latency climbs.
the dashboard stays warm the whole time.
Self-heal governance
The primary source is undocumented, so it will drift. A scheduled probe watches L&S for liveness and price sanity. On a sustained break, the self-heal Job (ADR-0010):
Captures ground truth
A real browser (Playwright) captures raw protocol frames alongside the simultaneously-rendered price — the ground truth.
Diffs against the known protocol
Deterministic replay searches for the fix that makes the captured frames decode to the captured price.
Opens a reviewable PR
With frame/price fixtures attached. No auto-merge — a human merges. Fixtures accrete append-only as protocol documentation and a regression corpus.
Never wire up auto-merge for self-heal PRs. The whole governance model is "the machine
proposes, a human disposes." The ls-replay-regression check + ≥1 human review are the gate;
branch-protect main to require both.
Secrets & isolation
- All source access and Gemini calls run server-side inside Firebase. The client never holds a key.
- Gemini runs via Vertex AI + IAM (ADR-0008) — there is no Gemini API key to leak.
- Per-user isolation is enforced by the datastore's security rules (
config/firestore.rules,config/database.rules.json), tested on the emulator — not just by the UI. - L&S handshake config lives in Secret Manager, never in the repo.
- Keep
.envgitignored; document placeholders in.env.example.
CI & gates
| workflow | trigger | what it guards |
|---|---|---|
ci.yml | push to main · PRs | typecheck (all) + web build + unit tests; rules/persistence/tick-bus on the emulators. |
adr-validate.yml | changes under docs/decisions/ | smADR schema validity. |
ls-replay-regression.yml | selfheal/* PRs | the self-heal merge gate — deterministic replay + bounded-surface check. |
security-review.yml | same-repo PRs | OAuth-authenticated review of the diff (deterministic FP-filtering). |
issue-triage.yml | new issues / PRs | auto-labels; flags ADR-breaking requests; opens PRs only on a maintainer's @claude implement. |
deploy.yml | push to main gated | keyless WIF deploy — Firebase surfaces + Cloud Run feed-engine. See Deploy. |
pages.yml | push touching site/ | publishes this docs site to GitHub Pages. |
All GitHub Actions are SHA-pinned (supply-chain-conscious); pnpm dependency build
scripts are explicitly approved via allowBuilds in pnpm-workspace.yaml. Claude tooling
authenticates with CLAUDE_CODE_OAUTH_TOKEN, never ANTHROPIC_API_KEY — see
SETUP.md.
Design tokens
The design/ handover is the source of truth for everything visual (ADR-0011). UI tokens —
colour + motion — are generated from design/cancri.handover.json by
tools/tokens:
pnpm tokens # writes apps/web/src/generated/{tokens.css,tokens.ts}
Never hand-edit the generated files — change the handover and re-run. This docs site mirrors the
same tokens in site/assets/cancri.css so its optics stay in lockstep with the terminal.
Architecture decision record
Eleven accepted smADRs pin the load-bearing decisions. Read them at
docs/decisions/.
| # | decision |
|---|---|
| 0001 | Pin region europe-west1 |
| 0002 | Three runtime classes (execution model) |
| 0003 | Feed-engine single-process singleton |
| 0004 | Datastore split — Firestore (the book) / RTDB (the wire) |
| 0005 | Realtime transport — RTDB tick bus |
| 0006 | Tick schema & SourceAdapter contract |
| 0007 | ISIN resolution — LLM proposes, resolver disposes |
| 0008 | Gemini via Vertex + IAM callable |
| 0009 | ls-protocol break-surface isolation |
| 0010 | Self-heal governance — PR + deterministic gate |
| 0011 | Frontend — vanilla TS + Vite, single rAF |
Maintainer runbook
- A self-heal PR landed. Review the captured frame/price fixtures, confirm the replay-regression check is green, and merge by hand. Never auto-merge.
- The dashboard is stuck DELAYED. Expected when primary is down — the FSM degraded honestly. Check the feed-engine logs and the L&S liveness probe; recovery flips it back to LIVE automatically.
- Onboarding mis-parses. Confidence < 0.7 rows are flagged for the user, not silently trusted — by design. Tune the prompt/resolver in
functions/, and add a fixture. - A protocol capture is needed. Run the self-heal Job during trading hours to fill
packages/ls-protocol/protocol.config.v1— exactly what the replay gate verifies. - Shipping the docs. Edit under
site/, push tomain;pages.ymlpublishes it.