SANDSCOPE / ARCHITECTURE

Decisions, and what they cost

14 architecture decision records, 13 accepted. Each one names the alternative it rejected and what the choice gives up, because a decision record that lists only advantages is marketing. Two of these were written after being measured wrong the first time.

Built for a laptop display — everything here works on a phone, but the diagrams and tables scroll sideways. Safari or Chrome on a Mac or PC shows it as intended.

Request path

EDGE — vercel dub1RUNTIME — northflank containerPROVIDERS — ordered failoverDATA — irelandBrowser consoleSSE consumerBFF route handlerrate limit · fails closedOrchestratorlanggraph · one chokepointHybrid retrievalBM25 + denseEvidence gate3 bands · default refuseSpend guardreserve worst case firstSemantic cacheexact hash, then vectorProvider chain5, time-boxed disablesNeon + pgvectorchunks · runs · spansUpstashredis · edgecache hit — no provider callINSUFFICIENT — nothing is emitted

Select any component to see what it does and the file that implements it.

The two dashed returns are the paths that never reach a model: a semantic cache hit, and an INSUFFICIENT verdict. The second emits nothing at all — no draft is rendered, which is enforced in the console rather than assumed, after it once displayed output the governance layer had refused (D-009).

Provider order

Fixed, not adaptive. A provider that rate-limits is disabled for a bounded interval rather than the process lifetime, and the clock is injected so expiry is tested instead of waited on. Spend is reserved against the worst-case surviving provider before the call — pricing the first one under-reserved by 4× the moment failover reached a costlier model (D-010).

  1. 1 groq
  2. 2 gemini
  3. 3 cerebras
  4. 4 openrouter
  5. 5 mistral

Decision records

  1. ADR-0001 Split the experience layer from the agent runtimeAccepted

    The product needs a cinematic web experience and a stateful multi-agent runtime. Every credible implementation of the agent side — LangGraph, BM25, the evaluation harness, the local embedder — exists in Python. Every credible implementation of the experience side — React Server Components, streaming, the motion layer — exists in the JavaScript ecosystem. A single runtime forces one of the two to be reimplemented.

    consequencesPositive. Each side uses its native ecosystem. The BFF holds provider secrets so the browser never does. Either side can be replaced independently. The pattern matches how large consumer platforms actually deploy — a JS edge in front of polyglot services.

  2. ADR-0002 The product name is build-time configuration, not sourceAccepted

    The Product Owner requires the ability to change the product name late without a refactor. A name hardcoded across components, metadata, manifests and asset filenames turns a rename into a risky find-and-replace across the tree.

    consequencesPositive. A rename is one command and a validated data change. The invariant is enforced by a gate rather than by discipline.

  3. ADR-0003 Host the agent runtime on Hugging Face Spaces (free CPU)Unknown

    NFR-002 requires zero infrastructure cost. Candidates evaluated: Google Cloud Run (generous always-free tier, ~1–3 s cold start, requires a card on file), Render free tier (no card, but sleeps after 15 minutes with a ~50 s cold start), Hugging Face Spaces free CPU (no card, 2 vCPU / 16 GB, sleeps only after prolonged inactivity), and Fly.io (no longer meaningfully free).

    consequencesPositive. No payment instrument required. 2 vCPU / 16 GB is materially more capacity than most free tiers. The 48-hour idle window is far more forgiving than Render's 15 minutes, and a 6-hourly warm-ping makes sleep effectively unreachable.

  4. ADR-0004 No local neural embedding model in the containerAccepted

    Dense retrieval needs embeddings. The default choice, `sentence-transformers`, pulls `torch` — 2–3 GB of image weight. Under ADR-0003 that inflates build time severely and risks the free tier's limits, for a corpus small enough that the quality difference is marginal.

    consequencesPositive. The image stays small. Retrieval works with no API key at all, which means tests run offline and deterministically. The fallback is a genuine degradation path rather than a hypothetical one.

  5. ADR-0005 Vectors from different embedding models are never comparedAccepted

    ADR-0004 introduces two embedding sources. Storing both in one `vector` column and comparing them would produce a cosine similarity that is arithmetically valid and semantically meaningless. This class of bug is silent: retrieval keeps returning results, they are simply the wrong ones, and no error is ever raised.

    consequencesPositive. Cross-space comparison becomes structurally impossible rather than merely discouraged. Adding a third embedding model later requires no migration. Degradation is explicit and visible to the user.

  6. ADR-0006 Human-approval nodes are terminal by constructionAccepted

    In prior work on a multi-agent system, a human-approval node had an outgoing edge into a registration node. The graph therefore proceeded to register the item regardless of what the reviewer actually answered. The approval step existed, rendered, collected a decision, and controlled nothing. It read as governance and functioned as a formality — the worst possible failure mode for a control, because it consumes the reviewer's attention while providing no protection.

    consequencesPositive. Auto-proceed is impossible by graph topology, not by careful coding. The approval record is a first-class row with identity and timestamp, satisfying Sofia's audit requirement. Continuation runs are independently traceable.

  7. ADR-0007 Rate limiting and the spend guard fail closedAccepted

    The service is publicly reachable and calls paid-capable model providers. The common default — allow the request when the rate-limit store is unreachable — is chosen so that an infrastructure blip does not degrade user experience. Here that default converts a Redis outage into an unbounded-cost incident, and the endpoint into free LLM capacity for whoever finds it.

    consequencesPositive. The worst outcome of a dependency failure is unavailability, not unbounded spend. Cost is bounded before it is incurred rather than reconciled after.

  8. ADR-0008 Similarity thresholds belong to the embedder, not to the cacheAccepted

    The semantic cache was first written with a module-level `SIMILARITY_THRESHOLD = 0.86`, chosen because 0.86 is the sort of number that appears in semantic-cache examples.

    consequencesPositive. Adding an embedder forces the question "what is the boundary in *your* space" to be answered with a measurement. The regression test makes a silent drift in the embedder visible as a failure rather than as slightly wrong answers.

  9. ADR-0009 Models are trained offline and served without their training frameworkAccepted

    Sprint 3 introduces trained models. ADR-0004 excluded `torch` from the runtime image because it is 2–3 GB, which breaks build times on the free tier and violates NFR-002. That reasoning still holds and is not being reversed.

    consequencesPositive. The runtime image stays small and ADR-0003's constraints hold. The split is the standard production pattern rather than a workaround, so it survives scrutiny. Training frameworks never become a deployment dependency, and a framework can be swapped without touching the serving path.

  10. ADR-0010 Training labels are true by construction, never model-generatedAccepted

    FR-028 needs a labelled dataset: questions paired with whether the corpus can answer them. The existing set is 22 questions written by hand, which is enough to falsify a threshold and nowhere near enough to fit a model.

    consequencesPositive. Ground truth is auditable — for any example, the reason for its label is mechanical and inspectable. Evaluation measures correctness rather than agreement with a model. The entity-perturbation mechanism produces exactly the failure class that defeated the heuristic gate in Sprint 2, so the hard cases are represented in proportion rather than by luck.

  11. ADR-0011 No approximate-nearest-neighbour index until the corpus reaches ~5,000 chunksAccepted

    The schema defines an HNSW index on `chunk_embedding`. It was added because a vector column with an HNSW index is what a pgvector schema looks like, which is not a reason.

    consequencesPositive. The system uses the simplest thing that works, and there is now a number attached to the word "works" and a threshold at which to revisit. Retrieval stays exact, so ADR-0005's degradation path has no approximation interacting with it.

  12. ADR-0012 The agent runtime runs on Northflank, not Hugging Face SpacesAccepted

    ADR-0003 placed the agent runtime on Hugging Face Spaces using the Docker SDK, on the stated grounds that it was free. That was true when the decision was written and is no longer true: Hugging Face now requires a PRO subscription for Docker and Gradio Spaces, and only Static Spaces remain free. A Static Space cannot run FastAPI, so the decision did not merely become expensive — it became impossible at $0.

    consequences- No SLA, and Northflank documents the Sandbox as not for production. For a demonstration surface that is acceptable and is stated on the surface itself rather than hidden. - A payment method is required on file, on every Northflank plan, even the free one. It is a verification measure and nothing is charged inside the allowance, but it is not a card-free option and should not be described as one. - Two free services, one addon. The architecture fits, with nothing spare. - The image has still never been built locally — no Docker on the development machine. Northflank's first build is the first real build.

  13. ADR-0013 The trained classifier stays out of the live evidence gateAccepted

    Sprint 3 trained an evidence-sufficiency classifier: gradient boosting over 12 retrieval features, 715 labelled examples, cross-validated AUC 0.808 against a 0.609 baseline, exported to ONNX and served without scikit-learn (ADR-0009). It has its own unit tests, its own ONNX-parity test, and a panel on `/reliability`.

    consequencesPositive. The published 4.7% false-answer rate continues to describe what the service actually does. The reliability surface stops implying that a model serves traffic when it does not. The measurement is committed and re-runnable, so this decision can be overturned by evidence rather than by opinion.

  14. ADR-0014 One Vercel project, connected to gitAccepted

    Two Vercel projects existed for one application, and the arrangement hid a staleness failure for eight days.

    consequencesPositive. The public site cannot silently go stale; a push is a deploy. One project to look at rather than two, one of which was a decoy. No manual step to forget.

derived at 2026-09-05T13:25:00.865Z from d66bfb5