Skip to content

Align backend/ with docs/Backend.md (Mandate Next.js service)#5

Merged
trenchsheikh merged 1 commit into
mainfrom
cursor/backend-restructure-0987
Apr 30, 2026
Merged

Align backend/ with docs/Backend.md (Mandate Next.js service)#5
trenchsheikh merged 1 commit into
mainfrom
cursor/backend-restructure-0987

Conversation

@trenchsheikh

Copy link
Copy Markdown
Owner

Why

backend/docs/ is a complete, opinionated brief for a Next.js Route Handler service called Mandate: six diligence desks fanning out in parallel, an SSE event stream into the Next.js frontend, fixture fallbacks, and an amendment-PR flow. The matching frontend in front-end/ already imports the contract types and event names from that brief.

But backend/ itself was a tiny Python FastAPI stub with / and /health — completely out of step with the docs. The user asked us to walk through backend/docs/, follow the structure, and make sure it's "all working clean."

This PR rebuilds backend/ to match docs/Backend.md exactly, end-to-end, and verifies it works.

What changed

Folder layout now mirrors docs/Backend.md §1:

backend/
├── MANDATE.md
├── app/
│   ├── layout.tsx
│   ├── page.tsx
│   └── api/
│       ├── run/route.ts
│       ├── memo/[runId]/route.ts
│       ├── amend/route.ts
│       └── health/route.ts
├── agents/
│   ├── orchestrator.ts
│   ├── parse-prompt.ts
│   ├── synthesise.ts
│   ├── memo.ts
│   ├── amend.ts
│   └── desks/{company,founder,investor,round,mandate,wire-safety}.ts
├── lib/
│   ├── contract.ts          # cross-team seam (matches Backend.md §2)
│   ├── types.ts
│   ├── mandate-loader.ts    # parses MANDATE.md frontmatter
│   ├── mandate-evaluator.ts # pure rule evaluation (no LLM in hot path)
│   ├── ledger.ts            # mock wire ledger
│   ├── cache.ts             # in-memory + fixture fallback
│   ├── util.ts
│   └── sources/{specter,companies-house,opensanctions,whois,pdf-parse,llm}.ts
├── fixtures/
│   ├── specter/   companies-house/   opensanctions/   whois/   pdfs/
│   └── fund-state.json
└── scripts/smoke.ts

Behaviour highlights

  • lib/contract.ts is the single source of truth for RunEvent, DeskFinding, Verdict, MemoData, and AmendmentDraft. The shapes match the spec verbatim.
  • The orchestrator fans out six desks with Promise.allSettled, a 30s per-desk timeout, and degraded findings on tier-2 failure (per §14).
  • The mandate desk uses a pure rule evaluator; no LLM in the hot path.
  • The wire-safety desk catches the demo BEC: lookalike domain (edit distance 2 vs verified acme.co), domain age 6 days, DKIM fail, and account-holder-name mismatch with the SPA entity.
  • Every external source has a fixture fallback, and they all default to fixtures because no API keys are present in the demo env. DEMO_FORCE_FIXTURES=true is the on-stage panic button (per §13).
  • The mock ledger queues a wire on proceed and holds it on hold (per §11).
  • agents/amend.ts produces a real diff against MANDATE.md's wire_safety block and captures the offending domain.

Removed

  • backend/app/__init__.py, backend/app/main.py, backend/requirements.txt — the prior FastAPI stub. MANDATE.md is now at backend root where the loader looks for it (per §5.1).

Verification

backend/.env.example, backend/README.md, and root AGENTS.md are updated to reflect the new dev workflow.

Step Result
npm install clean (44 packages)
npm run typecheck (tsc --noEmit) clean
npm run build (next build) clean, no warnings, all 4 routes registered
GET /api/health {"status":"ok",...}
Smoke: clean Acme deal 6/6 desks PASS → verdict proceed, memo generated
Smoke: BEC Acme deal 5/6 PASS, wire-safety BLOCK with 4 signals → verdict hold, memo generated, amendment diff captures acrne.co

The clean smoke transcript is attached:

backend_smoke_clean_and_bec.txt

> running clean scenario
  [clean-acme] verdict proceed (PROCEED — 6/6 desks pass)

> running BEC scenario
  [bec-acme] verdict hold (HOLD — 1 blocking finding(s); 0 flag(s); 5 pass(es))
  blockingReason: Lookalike domain acrne.co vs verified acme.co (edit distance 2 ≤ 2) — wire_safety §6.2; Domain acrne.co age 6d < min 30d — wire_safety §6.1; DKIM fail on inbound wire-instruction email — wire_safety §6.3; Account holder "AC Robotics LLC" ...
  amendment branch: amend/wire-<runId>
  amendment diff:
    @@ MANDATE.md
       wire_safety:
         domain_age_min_days: 30
         domain_edit_distance_block: 2
    +    blocked_domains_seen_in_attacks:
    +      # added from run <runId>
    +      - acrne.co

ALL CHECKS PASSED

How to run locally

cd backend
npm install
npm run dev          # http://localhost:3001
npm run smoke        # in another shell — exercises both scenarios

No API keys are required; fixtures cover every source. Set OPENAI_API_KEY (or CURSOR_API_KEY) to enable the editorial-summary sentence on the IC memo and the LLM-generated amendment rationale.

Notes / future work

  • Live-API code paths in lib/sources/* throw live-not-implemented on purpose — they're stubs ready to be filled in once Specter / Companies House / OpenSanctions / WHOIS keys exist. The shape of the cache/fixture layer doesn't change when keys are added.
  • parse-prompt.ts is deterministic regex-based for now (Backend.md §12 calls for an LLM). Plumbing exists in lib/sources/llm.ts to swap it in without touching callers.
  • Optional: next/font warnings, lint/eslint config, Dockerfile, and a CI workflow are not part of this PR — they were not blocking the brief.

Slack Thread

Open in Web Open in Cursor 

The backend folder previously held a tiny FastAPI 'health' stub that bore no
relation to the brief in backend/docs/. This commit replaces it with the
Next.js Route Handler service that the docs (and the matching front-end app)
were already designed around.

Highlights:

- Folder layout mirrors backend/docs/Backend.md §1 exactly:
    app/api/run, app/api/memo/[runId], app/api/amend, app/api/health
    agents/orchestrator.ts + agents/desks/{company,founder,investor,round,
    mandate,wire-safety}.ts + agents/{synthesise,memo,amend,parse-prompt}.ts
    lib/{contract,types,mandate-loader,mandate-evaluator,cache,ledger,util}.ts
    lib/sources/{specter,companies-house,opensanctions,whois,pdf-parse,llm}.ts
    fixtures/{specter,companies-house,opensanctions,whois,pdfs}/ + fund-state.json
    MANDATE.md (parsed by the loader)
    scripts/smoke.ts (end-to-end driver)

- lib/contract.ts is the cross-team seam (RunEvent/DeskFinding/Verdict/
  MemoData/AmendmentDraft) and matches the schema in Backend.md §2.

- Orchestrator fans out six desks with Promise.allSettled, a 30s per-desk
  timeout, and degraded findings on tier-2 failures (Backend.md §14).

- Mandate desk uses a pure rule evaluator (lib/mandate-evaluator.ts) — no
  LLM in the hot path.

- Wire-safety desk catches the demo BEC case: lookalike domain (edit
  distance 2 vs verified acme.co), domain age 6 days, DKIM fail, and
  account-holder-name mismatch. Verdict is hold; an amendment draft is
  produced with the lookalike domain captured.

- Every external source defaults to fixtures so the demo runs cleanly with
  no API keys; DEMO_FORCE_FIXTURES=true forces the fixture path on stage.

- Verified clean: tsc --noEmit clean, next build clean (no warnings),
  smoke run hits both clean and BEC scenarios end-to-end via SSE and
  reaches ALL CHECKS PASSED.

backend/.env.example, backend/README.md and root AGENTS.md updated to
reflect the new structure and dev workflow.

Co-authored-by: Mr T <trenchsheikh@users.noreply.github.com>
@trenchsheikh trenchsheikh marked this pull request as ready for review April 30, 2026 19:03
@trenchsheikh trenchsheikh merged commit 12e49f2 into main Apr 30, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants