This project has been sunset. The live site (
hipo.vsh852.com) is offline and all cloud infrastructure — the AWS Lambda agent backend, the Cloudflare Pages SPA, the catalog Worker, and the R2/KV stores — has been torn down. The repo is preserved read-only as a portfolio archive; the code, design docs, and commit history remain intact and themise run checkgate still passes offline. Theinfra/*.tfresource blocks and thedeploy-*workflows are commented out (not deleted) so the architecture stays readable — restore them from git history to redeploy. See docs/progress.md for the teardown record.
Browse recent Hong Kong IPO filings — Application Proofs, PHIPs, and prospectuses — pick one, and watch a bounded analysis agent turn it into a validated, source-cited factsheet in real time. Every claim deep-links to the page it came from; every step and tool call streams into a live trace you can inspect; and the agent stops at a hard interrupt — it never acts on your behalf.
The product signature is trust: an inspectable agent behind a polished UI, held inside hard boundaries — validation, recoverability, evidence presentation — not autonomy.
Draft analysis from a public filing. Verify the cited source before you act. The system never executes transactions or submits orders.
- A scheduled Cloudflare Worker keeps a catalog of recent HKEX filings fresh. You never upload a file — the app sources everything from HKEX itself.
- You pick a filing. Its PDF is fetched from HKEX on first view and cached in R2, then handed to the agent. (Retrieval is plain I/O, not an agent action.)
- The agent works over the in-hand document and calls analysis tools only — parse → chunk → select → extract → validate — recording every step as a trace.
- The SPA renders four factsheet panels (overview, financials, risks, coverage), a right-rail agent trace that streams live over SSE (steps + the model's own output tokens), page-level citations, and a persistent "never auto-act" boundary banner.
- If a model times out or validation fails, the app degrades gracefully into one of three visible states rather than showing an unsupported claim.
The agent is the product, not a hidden pipeline
The core is a bounded, code-driven analysis loop (apps/api/hipo_api/agent/loop.py). It is an agent in that it works step by step over a document and records what it did at each stage — but deliberately not a free-planning agent that decides its own next action. The sequence of stages is fixed in code; that constraint is the point. The design choices that make it trustworthy:
- Fixed pipeline, whitelisted tools. The stages run in a set order —
parse → chunk → select → extract → validate— over a fixed whitelist of analysis tools and nothing external. The agent cannot reorder the pipeline, invent an action, fetch anything, or reach outside the whitelist. Retrieving the PDF happens before the loop. - Model only where judgment is needed. Every step is deterministic code
except
extract, which is the single point a model is called. Parsing, chunking, and validation are plain functions — cheaper, faster, and testable. - Bounded and terminal. A step cap (8) bounds the loop, and its only exit is a hard interrupt that surfaces the draft factsheet and stops. These invariants — stays inside the whitelist, always ends at the interrupt — are exactly what an eval harness can assert.
- Inspectable by construction. Every step is recorded as a
TraceStep(thought, tool, args digest, observation, model) and streamed to the UI, so how the factsheet was produced is auditable, not a black box.
- No claim without a citation. The schema enforces it: a
DISCLOSEDfield that carries no citation fails validation (packages/contracts/hipo_contracts/factsheet.py). The UI never renders an unsupported claim. - Three honest degradation states. Full (validated), Partial (a model failed; single-model output with a degraded badge), Rejected (schema or citation validation failed; explain and render nothing unsupported).
- Privacy discipline. Fixtures are public filings, but treated as sensitive financial PII: no raw document text in logs — only document hash, page count, job id, and field-level validation outcomes. Correlation IDs run end to end.
HKEX filings progress Application Proof → PHIP → Prospectus, disclosing more at
each stage. A single Pydantic schema (schema_version = 202401, grounded in
HKEX Guidance Letter GL56-13) spans all three; a per-field status
(disclosed / pending / redacted / omitted / not_applicable) carries
the stage-dependent completeness instead of forcing three schemas.
Prefer a free-tier Cloudflare service wherever it fits; fall back to AWS only where Cloudflare has no good free-tier alternative — decided per resource. The one documented fallback: the FastAPI + PDF-parsing agent runs on AWS Lambda because Workers Python is beta and a poor host for it.
apps/web/ React 19 + Vite + TS SPA (Cloudflare Pages) — the analyst workstation
apps/api/ FastAPI + the analysis agent + its tools (AWS Lambda)
apps/catalog/ Cron Worker — HKEX catalog refresh + filing serve (Cloudflare)
packages/contracts/ Pydantic factsheet / rejection / trace schemas (shared)
infra/ single Terraform root — AWS + Cloudflare providers (HCP Terraform)
fixtures/ public HKEX sample filings (a PHIP + an AP)
scripts/, tests/ ops/smoke scripts + the pytest suite
Frontend — apps/web
A polished, responsive, accessible SPA (Brief A is front-end-strong). Filing
list → analysis view with the four factsheet panels, a right-rail trace that
streams live via SSE, citation popovers that deep-link into the source PDF, a
dark-mode toggle, an API-key gate, and Brief-A trust components: an elapsed
timer with a slow-run cue, a retry-on-failure affordance, an evidence-coverage
card (disclosed-% / cited-% dials from a pure factsheetStats walker), and a
JSON export carrying the never-act disclaimer + provenance. Tested with Vitest +
React Testing Library + axe (accessibility).
Agent + API — apps/api
FastAPI hosts both the buffered endpoints (/health, /filing/{id},
/factsheet/{id}) and the streaming POST /analyze/{id}. The streaming route
drives run_agent on a worker thread and yields each event off a queue the
instant it fires: event: step frames for trace steps, event: thinking frames
for the model's own output tokens (streamed via the gateway's OpenAI-style chunk
deltas — surfaced honestly as the model's output, not a separate reasoning
channel), then one terminal event: result / event: rejected. Buffered API
Gateway can't stream, so this route runs on a separate uvicorn Lambda behind
a RESPONSE_STREAM Function URL — see docs/operations.md.
Catalog Worker — apps/catalog
HKEX exposes filings through two interfaces, and neither alone suffices, so the
Worker queries both on each scheduled refresh (30 1 * * 1-5, HK trading days)
and merges:
- Title-search servlet — returns prospectuses, but drops Application Proofs and PHIPs after ~1–2 years.
- Applicant JSON archive — a static list of all applicants, including the AP/PHIP links the servlet eventually drops.
A CatalogSource interface keeps the source swappable; each source failure is
isolated. The catalog is stored in KV (pruned to a 90-day window so it stays
bounded); filing PDFs cache to R2 on first view. An SSRF allowlist guards
outbound fetches.
Models are reached through one vendor-neutral gateway — a single base URL
fanning out to Claude, Gemini, and GPT, each with its own key. The client speaks
the OpenAI-compatible /v1/chat/completions path for all vendors, keeping
the multi-model code single-path (one client, no per-vendor SDKs). The gateway
provider is swappable, so its name stays out of code and config.
Models are a fallback chain, not pre-bound roles — gateway model IDs listed cheapest → most capable. The runtime defaults to the cheapest for every call and escalates only on failure, keeping cost low with a graceful-degradation path.
- One HCP Terraform workspace, one root module at infra/ — AWS + Cloudflare providers together. The current split: Pages, R2, KV, and the Cron Worker on Cloudflare; Lambda + API Gateway + CloudWatch on AWS.
- Never
terraform apply/destroylocally. HCP applies on push to the linked branch; approve in the workspace UI. A pre-tool-use hook blocks local apply/destroy as a backstop. - No static AWS keys anywhere — the AWS provider uses OIDC dynamic
credentials (
TFC_AWS_PROVIDER_AUTH/TFC_AWS_RUN_ROLE_ARN), taking onlyregion. Cloudflare auth is a sensitive workspace token. IAM is least-privilege — scoped actions and resource ARNs, never*.
Deploy, secrets, bootstrap, and verification detail live in docs/operations.md.
Archived: the deployed stack is gone, but the code still builds and the
mise run checkgate still passes offline. The steps below are for exploring the archived project locally, not for redeploying.
mise— task runner + non-Python tool versionsuv— Python deps/environmentsbun— JS runtime / package manager (web + catalog)terraform1.15 — infra (applied through HCP, never locally)- For live model calls: copy
.env.exampleto.envand fill in the gateway base URL + per-vendor keys (gitignored, auto-loaded by mise; a missing.envstill leavesmise run checkgreen)
mise run check # format + lint + typecheck + test + tf fmt/validate — the gate
mise run test # pytest
mise run models:smoke # opt-in gateway connectivity check (network-dependent)
mise run catalog:smoke # opt-in live HKEX reachability check (network-dependent)
mise run tf:validate # terraform validate (no backend init)
cd apps/web && bun install && bun run check # SPA: biome + tsc + vitest + axe
cd apps/catalog && bun install && bun run check # Worker: biome + tsc + vitestRun mise run check and confirm it is green before pushing. Development is
test-driven and trunk-based with short-lived branches — never commit directly to
master. See CLAUDE.md for the full workflow and coding standards.
This project was built with Claude Code as a pair-programmer, but under a deliberately controlled process rather than free-form prompting. The controls that shaped how I worked with it:
- Spec-driven development. Work flowed through a brainstorm → design spec → task breakdown → implement → review loop. Each phase started from a written design spec I reviewed (the committed specs and plans live under docs/superpowers/); the spec was split into small, numbered task briefs with explicit file lists and interfaces; Claude implemented one brief at a time and wrote a per-task report; and each commit range got a saved review diff (task briefs, reports, and diffs stayed in a local working directory, not committed). That kept changes small, auditable, and tied back to an intent I had signed off on.
- A
CLAUDE.mdcontract + persistent memory. Project rules — TDD, trunk-based branching, Cloudflare-first infra, the never-log-raw-text and never-auto-act boundaries — live in CLAUDE.md and are loaded into every session, so the standards held across many separate conversations. A memory file carried durable context (secrets layout, the agent framing, hosting decisions) between sessions. - A guardrail hook. A
PreToolUsehook (.claude/hooks/guard-terraform.sh, wired in .claude/settings.json) hard-blocks any localterraform apply/destroycommand before it can run — a machine-enforced backstop for the "infra is applied only by HCP" rule, not just a written one. - The quality gate as the contract.
mise run check(lint, typecheck, the full test suite,terraform fmt/validate) plus a matching GitHub Actions CI run were the definition of done for every change; TDD meant a failing test came first.
I directed the architecture and hand-verified everything that carries risk — the agent's boundary invariants (fixed pipeline + terminal interrupt), all threat-modeling and Terraform/IAM permissions, the model-routing and fallback logic, the schema and citation-validation rules, and every claim in this README.
Shipped: the Brief-A MVP end to end — catalog Worker, agent backend with live SSE streaming, and the analyst-workstation SPA. Deferred: Brief B (multi-model agreement/disagreement panel + arbiter on disputes — the schema and trace are built to accept it), Brief C (gold-dataset CI eval gate + one-command rollback), Brief D (a trust-degradation alarm). See docs/progress.md for current status and docs/initial-plan.md for the full vision.