Skip to content
This repository was archived by the owner on Jul 22, 2026. It is now read-only.

vicw0ng-hk/hipo

Repository files navigation

HK IPO Verify (hipo)

🗄️ Archived (2026-07-22)

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 the mise run check gate still passes offline. The infra/*.tf resource blocks and the deploy-* 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.


What it does

  1. A scheduled Cloudflare Worker keeps a catalog of recent HKEX filings fresh. You never upload a file — the app sources everything from HKEX itself.
  2. 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.)
  3. The agent works over the in-hand document and calls analysis tools only — parse → chunk → select → extract → validate — recording every step as a trace.
  4. 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.
  5. If a model times out or validation fails, the app degrades gracefully into one of three visible states rather than showing an unsupported claim.

Design

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.

Trust boundaries, everywhere

  • No claim without a citation. The schema enforces it: a DISCLOSED field 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.

One schema across three document types

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.

Cloudflare-first, AWS where it must be

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.

Implementation

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 — swappable LLM gateway

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.

Infrastructure

  • 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/destroy locally. 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 only region. 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.

Running it

Archived: the deployed stack is gone, but the code still builds and the mise run check gate still passes offline. The steps below are for exploring the archived project locally, not for redeploying.

Prerequisites

  • mise — task runner + non-Python tool versions
  • uv — Python deps/environments
  • bun — JS runtime / package manager (web + catalog)
  • terraform 1.15 — infra (applied through HCP, never locally)
  • For live model calls: copy .env.example to .env and fill in the gateway base URL + per-vendor keys (gitignored, auto-loaded by mise; a missing .env still leaves mise run check green)

Commands

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 + vitest

Run 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.

How AI was used to build this

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.md contract + 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 PreToolUse hook (.claude/hooks/guard-terraform.sh, wired in .claude/settings.json) hard-blocks any local terraform apply/destroy command 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.

Roadmap

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.

Used by

Contributors

Languages