Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
150 commits
Select commit Hold shift + click to select a range
21da04a
chore(deps): add better-auth 1.7.5 and the drizzle adapter
claude Sep 22, 2026
cb5f334
chore: merge claude/chore-app-skeleton-3
claude Sep 22, 2026
1cf8f2f
feat(identity): company roles and authorize() (test-first), organizat…
claude Sep 22, 2026
e4d28d2
feat(db): auth schema (Better Auth) and app.requests with tenant policy
claude Sep 23, 2026
c67654e
feat(db): migration for auth tables and app.requests
claude Sep 23, 2026
0364a58
wip(identity,tenancy): withTenant, auth wiring and integration tests
claude Sep 23, 2026
0958d25
feat(identity,tenancy): invite-only Better Auth, withTenant, forced R…
claude Sep 23, 2026
4992b90
feat(app): auth route, login/sign-up/invite pages, demo seed
claude Sep 23, 2026
799551e
chore: merge claude/chore-app-skeleton-3
claude Sep 23, 2026
6a3490a
docs: data model with classification, architecture map and changelog …
claude Sep 23, 2026
5dfea7d
chore: merge claude/chore-app-skeleton-3
claude Sep 23, 2026
fce4d6e
chore(deps): add pg-boss 12.33.6
claude Sep 23, 2026
10623ec
feat(db,jobs): documents and audit_events tables, intake columns, pg-…
claude Sep 23, 2026
e0afd29
feat(db): migration 0003 intake
claude Sep 23, 2026
40d3c8f
feat(intake): upload validation, fingerprint, atomic submit, upload a…
claude Sep 23, 2026
81ee674
test(intake): atomic submit with injected failure, duplicates, append…
claude Sep 23, 2026
a8a2808
fix(identity): address the fresh security review of PR #31
claude Sep 23, 2026
20aeb0f
chore: merge claude/feat-identity-tenancy-4
claude Sep 23, 2026
c75a955
test(intake): upload/download routes – tenant 404, anonymous 401, typ…
claude Sep 23, 2026
bff8c68
feat(app): requests list, upload form, request detail with downloads;…
claude Sep 23, 2026
02715e0
fix(config): explicit APP_ENV; only local may use the committed auth …
claude Sep 23, 2026
c0da064
chore: merge claude/feat-identity-tenancy-4
claude Sep 23, 2026
fef74ea
chore(ai-service): scaffold uv project with pinned deps and PDF fixtu…
claude Sep 22, 2026
15c1c9d
test(ai-service): add failing grounding verifier tests (red)
claude Sep 22, 2026
77f8371
feat(ai-service): implement deterministic grounding verifier
claude Sep 22, 2026
e18d8b7
test(ai-service): add failing parser tests for EML, PDF and type dete…
claude Sep 22, 2026
50fe234
feat(ai-service): parse EML and PDF into segments with stable locators
claude Sep 22, 2026
75e139a
test(ai-service): add failing tests for prompt rendering and the Vert…
claude Sep 22, 2026
12d7ac6
feat(ai-service): add versioned prompt and fail-closed Gemini model c…
claude Sep 22, 2026
7b98b18
test(ai-service): add failing pipeline, API and contract tests (red)
claude Sep 22, 2026
5f23ee6
feat(ai-service): expose POST /v1/extract with bearer auth, JSON logs…
claude Sep 22, 2026
b4519ad
fix(ai-service): never echo rejected settings values in startup errors
claude Sep 22, 2026
450af28
build(ai-service): add Dockerfile (python:3.13-slim, uv, non-root, uv…
claude Sep 22, 2026
3e22e5a
docs(ai-service): document run, env vars, contract, verified and unve…
claude Sep 22, 2026
7de6d3d
test(ai-service): pin the verbatim-injection-quote limitation; clarif…
claude Sep 22, 2026
78b0e52
feat(extraction): TS types generated from the AI-service contract, dr…
claude Sep 23, 2026
66ab13c
wip(intake): review fixes – pg-boss into src/db, body bounds, OOXML c…
claude Sep 23, 2026
2a0b891
fix(intake): address the fresh security review of PR #32
claude Sep 23, 2026
a162aa9
test: rate-limit assertion matches Better Auth's expanded IPv6 key
claude Sep 23, 2026
20942a3
chore: merge claude/feat-intake-upload-5
claude Sep 23, 2026
d0b7594
feat(requests): status machine (test-first)
claude Sep 23, 2026
d805a9a
refactor(db): pg-boss pool factory in its own file, covered by the no…
claude Sep 23, 2026
8c19a21
chore: merge claude/feat-intake-upload-5
claude Sep 23, 2026
0e81da1
chore: merge claude/feat-ai-service-6
claude Sep 23, 2026
15cb889
feat(extraction): AI-service client with timeout and error classifica…
claude Sep 23, 2026
91ffe9b
feat(db): extraction runs, segments, fields; request processing state
claude Sep 23, 2026
1ffa247
feat(db): migration 0006 processing
claude Sep 23, 2026
81dc7ea
feat(db): force RLS on extraction tables
claude Sep 23, 2026
6c648ea
feat(jobs): processing handler, drain(), dead-letter → ERROR, reprocess
claude Sep 23, 2026
346ff1c
feat(jobs): worker loop, compose ai profile, processing integration t…
claude Sep 23, 2026
afb34e6
test(ai): ISO date normalisation, word-boundary text and ambiguous da…
claude Sep 23, 2026
633a779
fix(ai): return ISO dates and trimmed text, match text on word bounda…
claude Sep 23, 2026
ef330f1
test(ai): dev flag together with VERTEX_PROJECT must fail closed (red)
claude Sep 23, 2026
4a63df4
fix(ai): refuse to start when the Gemini dev flag and VERTEX_PROJECT …
claude Sep 23, 2026
a1437bc
test(ai): auth and length checks before the body is read, 500 with re…
claude Sep 23, 2026
e2bea12
fix(ai): check token and Content-Length before reading the body; 500 …
claude Sep 23, 2026
16971c0
test(ai): PDF page cap and fail-closed layout pipeline at startup (red)
claude Sep 23, 2026
5d2a686
fix(ai): cap PDF pages and build the layout pipeline at startup
claude Sep 23, 2026
8cbddf8
docs(ai): document early request checks, value normalisation, page ca…
claude Sep 23, 2026
3b6e72b
chore(extraction): regenerate AI service contract types
claude Sep 23, 2026
0ca5ea9
test(ai): request guard must also apply behind a proxy root path (red)
claude Sep 23, 2026
a749ef4
fix(ai): apply the request guard to the route path, not the raw path
claude Sep 23, 2026
22f6ddb
docs(ai): mark the qpdf stderr level as not established; guard behind…
claude Sep 23, 2026
e10e5ee
style(ai): ruff format
claude Sep 23, 2026
e213a03
chore: merge claude/feat-ai-service-6
claude Sep 23, 2026
293bc6d
fix(jobs,extraction): address the fresh review of PR #34 (code)
claude Sep 23, 2026
8712386
feat(db): migration 0008 evidence and request FKs on extracted_fields
claude Sep 23, 2026
36a9b61
test,docs: crash recovery via supervise, company-mismatch skip, hones…
claude Sep 23, 2026
1c0a4fe
feat(db,jobs): field corrections, rejection reason, export queue defi…
claude Sep 23, 2026
1212e9a
feat(db): migration 0009 review
claude Sep 23, 2026
16605d2
feat(db): force RLS and append-only grants on field corrections
claude Sep 23, 2026
c2bab79
fix(jobs): repair queue definitions file
claude Sep 23, 2026
6d5fc5d
feat(review): review view with source, corrections, approve/reject (s…
claude Sep 23, 2026
dd04907
feat(app): review screen – fields with status and source, corrections…
claude Sep 23, 2026
fa136ca
chore(deps): add @playwright/test 1.63.0
claude Sep 23, 2026
d095e52
chore: drop e2e fixture template (the spec builds its mail)
claude Sep 23, 2026
0e75f20
test(e2e): review smoke flow with AI stub (wip)
claude Sep 23, 2026
1f131e1
test(e2e): wait for navigation; CI installs Chromium for verify:full
claude Sep 23, 2026
1f33b07
docs(review): data model, architecture status, changelog; shared stat…
claude Sep 23, 2026
fd10871
docs: correct verify:full note
claude Sep 23, 2026
f76b18c
fix(review): corrected values never shown as found; fixed message cod…
claude Sep 23, 2026
918570a
feat(export): ERP export contract (OpenAPI 3.1, Idempotency-Key)
claude Sep 23, 2026
7413dac
feat(export): generated ERP contract types + drift test
claude Sep 23, 2026
5d80084
feat(export): ERP mock (idempotent receiver, fault injection) and RES…
claude Sep 23, 2026
0789759
chore: checkpoint before migration generation
claude Sep 23, 2026
866f818
feat(export): request_exports migration (generated)
claude Sep 23, 2026
d21d3eb
feat(export): force RLS, key = request id, no deletes for app_rw
claude Sep 23, 2026
8f1eb5a
feat(export): ERP config (fail-closed token, placeholder refused outs…
claude Sep 23, 2026
2187b0b
feat(export): exactly-once export handler, drainExports, reprocess ex…
claude Sep 23, 2026
9c2b62e
fix(jobs): failure bookkeeping never aborts drain(); regression test
claude Sep 23, 2026
bbd42d4
feat(export): worker drains export jobs; flag-gated mock route with b…
claude Sep 23, 2026
9e905a7
feat(export): request page shows ERP reference and export retries; E2…
claude Sep 23, 2026
23d8f72
docs(export): API, operations, data model, architecture status, chang…
claude Sep 23, 2026
943cc5f
fix(export): review findings – approval refused over ERP limits, unre…
claude Sep 23, 2026
34a6c4e
feat(tenancy): RLS guard spec – allow-list (empty) and violation rules
claude Sep 23, 2026
386b7e6
test(tenancy): guard – every app table has company_id, forced RLS and…
claude Sep 23, 2026
c43ee48
test(tenancy): guard also refuses materialized/foreign tables and def…
claude Sep 23, 2026
f5bae36
feat(identity): last-admin rule (pure, test-first)
claude Sep 23, 2026
10c1d84
feat(identity): user management service (list, role, deactivate/react…
claude Sep 23, 2026
468f061
feat(identity): /users page with server actions; integration tests in…
claude Sep 23, 2026
631ac6a
fix(identity): disable Better Auth organization endpoints except set-…
claude Sep 23, 2026
f8692a1
fix(identity): security review – invitations audited without e-mail, …
claude Sep 23, 2026
7c6321c
feat(ai): normalisers for quantities, units, e-mail, phone and calend…
claude Sep 23, 2026
c9b4989
chore: checkpoint before migration generation
claude Sep 23, 2026
fd1b3a8
feat(extraction): extracted_fields.item_index for line items (migrati…
claude Sep 23, 2026
97b5da9
feat(ai): schema v2 with e-mail, phone, additional requirements and v…
claude Sep 23, 2026
516cf71
test(ai): end-to-end multi-item request with replayed model response;…
claude Sep 23, 2026
f1067ae
style(ai): ruff format verifier
claude Sep 23, 2026
c9632cc
fix(ai): keep phone value exactly as written, only trimmed
claude Sep 23, 2026
46dcf51
Merge branch 'claude/feat-ai-ts-22' into claude/feat-ai-full-fields-22
claude Sep 23, 2026
eb2646e
wip(extraction): TS side of schema v2 – contract types, zod, merge, p…
claude Sep 23, 2026
c16afde
feat(extraction): schema v2 on the TS side – six header fields, line …
claude Sep 23, 2026
56d92dc
docs(extraction): schema v2 in architecture, data model, changelog
claude Sep 23, 2026
eba33ca
fix(ai): units count only after a number or as a whole cell; phone nu…
claude Sep 23, 2026
7df4b9f
test(ai): pin the line-item injection limitation; README states it fo…
claude Sep 23, 2026
3e51788
fix(export): ERP limits checked only on exported fields at approval (…
claude Sep 23, 2026
5ff0bb6
build(ai): pin openpyxl, python-docx, python-oxmsg and olefile as dir…
claude Sep 23, 2026
24f9ee7
feat(evals): generator for the synthetic PDF inputs of the eval cases…
claude Sep 23, 2026
854b87c
feat(evals): 15 synthetic eval cases with expected results and hand-w…
claude Sep 23, 2026
43514cf
wip(ai): XLSX/DOCX/MSG parsers, OCR cap in the verifier, in-test docu…
claude Sep 23, 2026
3408c57
feat(evals): eval runner with replay/live model, metrics per key fiel…
claude Sep 23, 2026
91a7e32
style(evals): ruff format
claude Sep 23, 2026
1b4144d
fix(evals): lint and type fixes in the eval package
claude Sep 23, 2026
d0d1c5c
chore(ai): checkpoint - OCR for text-less PDF pages, dispatcher wired…
claude Sep 23, 2026
be5caf8
chore(ai): checkpoint before ruff format - log allow-list for attachm…
claude Sep 23, 2026
4967cc9
chore(ai): checkpoint - lint and type fixes, OcrMode.FULL_PAGE instea…
claude Sep 23, 2026
967ef59
test(evals): metric and gate unit tests, replay gate, degraded runs, …
claude Sep 23, 2026
d04daf9
test(evals): tighten injection and degraded-replay assertions; typing
claude Sep 23, 2026
4000068
ci: path-targeted AI eval gate in replay mode for changes under servi…
claude Sep 23, 2026
fbbb8d4
feat(ai): XLSX, DOCX, MSG and scanned-PDF parsing with additive contr…
claude Sep 23, 2026
909c6ee
docs(evals): README Evals section, operations runbook, changelog (#24)
claude Sep 23, 2026
d3c9e3e
docs(evals): state denominator sizes and the replay gate's blind spot…
claude Sep 23, 2026
fcb827e
docs(ai): formats, locators, limits, OCR measurements; Dockerfile opt…
claude Sep 23, 2026
44b8357
fix(ai): tighter OOXML limits (64 MiB, 100:1) and texts that still sp…
claude Sep 23, 2026
30616f7
chore(ai): regenerate contract (mediaType description)
claude Sep 23, 2026
e2c4672
Merge remote-tracking branch 'origin/claude/feat-ai-full-fields-22' i…
claude Sep 23, 2026
77f305e
feat(extraction): worker sends XLSX, DOCX and MSG to the AI service; …
claude Sep 23, 2026
6c0ad22
Merge remote-tracking branch 'origin/claude/feat-ai-formats-23' into …
claude Sep 23, 2026
43b967d
chore(evals): pnpm evals; verify:full runs the replay gate; docs
claude Sep 23, 2026
b9d8d4c
fix(evals): finite threshold and baseline values required, every base…
claude Sep 23, 2026
0463c37
fix(ai): reject OLE stream bombs in .msg before any stream is read; n…
claude Sep 23, 2026
b4f53b2
chore(ai): type the olefile directory walk; sort test imports
claude Sep 23, 2026
bd7f0a3
fix(ai): cap single OOXML parts at 16 MiB and count every visited DOC…
claude Sep 23, 2026
4ae2bb6
fix(ai): OCR the first MAX_OCR_PAGES pages without text and skip the …
claude Sep 23, 2026
23b13d0
fix(ai): one shared parse budget per extracted document
claude Sep 23, 2026
460abe4
docs(ai): regenerate contract (budget_exceeded, ocr_pages_skipped); R…
claude Sep 23, 2026
9480e43
test(ai): pin the additive contract values budget_exceeded and ocr_pa…
claude Sep 23, 2026
f986c2a
fix(ai): bound OLE header sector counts before olefile builds its FAT
claude Sep 23, 2026
7df4d87
feat(review): failed Outlook attachments and skipped OCR pages are st…
claude Sep 23, 2026
caba13b
Merge remote-tracking branch 'origin/claude/feat-ai-formats-23' into …
claude Sep 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .dependency-cruiser.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,14 @@ module.exports = {
severity: "error",
comment: "Feature modules never open connections or run migrations: createDatabase()/runMigrations() belong to the composition roots (src/app/_server, src/*.ts entrypoints). Types and table definitions (src/db/schema) are fine.",
from: { path: "^src/features/" },
to: { path: "^src/db/(index|client|migrate)\\.ts$", dependencyTypesNot: ["type-only"] },
to: { path: "^src/db/(index|client|migrate|job-queue-client)\\.ts$", dependencyTypesNot: ["type-only"] },
},
{
name: "pg-boss-client-only-in-db",
severity: "error",
comment: "pg-boss opens its own pool: construct it only in src/db (job-queue.ts); features use injected JobSender types.",
from: { path: "^src/", pathNot: "^src/db/" },
to: { path: "(^|/)node_modules/pg-boss(/|$)", dependencyTypesNot: ["type-only"] },
},
{
name: "not-to-unresolvable",
Expand Down
47 changes: 47 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,53 @@ S3_FORCE_PATH_STYLE=true
# Host port of the local S3 gateway.
S3_PORT=8333

# --- Environment -------------------------------------------------------------------------------
# local | showcase | production. Only `local` accepts the committed local-default auth secret.
APP_ENV=local

# --- Authentication (Better Auth) ---------------------------------------------------------------
# Signs sessions and cookies; at least 32 characters. Generate a real one: `openssl rand -base64 32`.
BETTER_AUTH_SECRET=local-dev-only-secret-change-me-0123456789
# Public base URL of the web app (cookies, redirects, trusted origin).
BETTER_AUTH_URL=http://localhost:3000
# Client IP for the login rate limit. Set to what YOUR reverse proxy writes and list the proxy
# addresses; without trusted proxies only a single-value header is trusted. If the web container is
# reachable without a proxy, clients can forge this header – put a proxy in front (see operations.md).
AUTH_IP_HEADERS=x-forwarded-for
AUTH_TRUSTED_PROXIES=

# Demo seed only (`pnpm seed:demo`): password of the synthetic demo accounts, local use only.
SEED_PASSWORD=demo-password-local-only

# --- Upload limits (intake) ---------------------------------------------------------------------
# Maximum size per file in bytes (default 20 MiB) and files per request (default 10).
UPLOAD_MAX_FILE_BYTES=20971520
UPLOAD_MAX_FILES=10
# Cap of one whole upload request (all files + form overhead); requests without Content-Length are refused.
UPLOAD_MAX_REQUEST_BYTES=41943040

# --- AI service (services/ai) --------------------------------------------------------------------
# Called by the worker only. The token must equal the service's AI_SERVICE_TOKEN (at least 24 chars).
AI_SERVICE_URL=http://127.0.0.1:8000
AI_SERVICE_TOKEN=local-dev-only-ai-token-0123456789
# Per-document timeout; a timeout is retried with backoff.
AI_SERVICE_TIMEOUT_MS=120000

# --- ERP export (ADR-0001 D9) ----------------------------------------------------------------------
# Base URL of the ERP REST API; the pilot uses the mock inside the web app. The worker exports, the
# web app serves the mock – both need the same token (at least 24 chars; the local default is
# refused outside APP_ENV=local).
# Host value for `pnpm worker` outside Docker; compose sets http://web:3000/api/erp-mock itself – do not
# copy this line into a .env used by compose.
ERP_BASE_URL=http://127.0.0.1:3000/api/erp-mock
ERP_TOKEN=local-dev-only-erp-token-0123456789
# Per-call timeout (max 20000); a timeout is retried with backoff under the same idempotency key.
ERP_TIMEOUT_MS=10000
# The mock route exists only with "true". Fault injection for demos, consumed in order per process:
# 503 (before storing), lost (stored, answer 503), timeout (hangs) – e.g. ERP_MOCK_FAULTS=503,lost
ERP_MOCK_ENABLED=true
ERP_MOCK_FAULTS=

# --- Web ---------------------------------------------------------------------------------------
# Host port of the web container.
WEB_PORT=3000
15 changes: 13 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,15 @@ jobs:
id: profile
if: steps.diff.outputs.code == 'true' && hashFiles('package.json') != ''
run: echo "stage=$(sed -n 's/^[[:space:]]*stage:[[:space:]]*\([a-z]*\).*/\1/p' project-profile.yml | head -1)" >> "$GITHUB_OUTPUT"
- name: Install Playwright Chromium (E2E smoke)
if: steps.diff.outputs.code == 'true' && hashFiles('package.json') != '' && (steps.profile.outputs.stage == 'production' || contains(github.event.pull_request.labels.*.name, 'verify-full'))
run: pnpm exec playwright install --with-deps chromium
- run: pnpm verify:full
if: steps.diff.outputs.code == 'true' && hashFiles('package.json') != '' && (steps.profile.outputs.stage == 'production' || contains(github.event.pull_request.labels.*.name, 'verify-full'))

# --- Python AI service: path-targeted (services/ai, contracts) ---------------------------
# The AI eval gate (ADR-0001 D8) joins this block with Epic 2 (#17): it runs on every change
# under services/ai/ and needs Vertex credentials via Workload Identity Federation.
# The AI eval gate (ADR-0001 D8, #24) runs in replay mode on every change under services/ai/:
# recorded model responses, no credentials. Live evals (--live, Vertex) never run in CI.
- name: Foundation phase (AI service)
if: steps.diff.outputs.ai == 'true' && hashFiles('services/ai/pyproject.toml') == ''
run: echo "No services/ai/pyproject.toml – AI service not created yet."
Expand All @@ -123,6 +126,14 @@ jobs:
uv run ruff format --check .
uv run pyright
uv run pytest -q
# Eval gate: fails when a key field drops by more than EVAL_GATE_THRESHOLD points against
# services/ai/evals/baseline.json, on any injection violation or case error.
- name: AI eval gate (replay)
if: steps.diff.outputs.ai == 'true' && hashFiles('services/ai/pyproject.toml') != ''
working-directory: services/ai
env:
EVAL_GATE_THRESHOLD: "5"
run: uv run python -m requestflow_ai.evals --replay --report "$RUNNER_TEMP/eval-report.json"

# Image + compose smoke (ADR-0001 D11): only when the Dockerfile or the compose file changes.
compose-smoke:
Expand Down
9 changes: 8 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ next-env.d.ts
# Python
__pycache__/
*.py[cod]
.venv/
.venv
.pytest_cache/
.ruff_cache/
.mypy_cache/
Expand All @@ -38,3 +38,10 @@ Thumbs.db

# Agent worktrees (Claude Code)
.claude/worktrees/

# Local cloud credentials (never committed)
.secrets/

# Playwright
/test-results/
/playwright-report/
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ as a real customer engagement for a mid-sized machine-building company. **All da
- Setup: `pnpm install && uv sync --project services/ai` · Start: `docker compose up` (runs the `setup` deploy step: migrations as `app_owner` + bucket) · Local services only: `docker compose up -d postgres storage`, then `pnpm setup:deploy`
- `verify:changed` – inner loop: lint (ESLint incl. style rules), typecheck, focused tests of the touched files. `pnpm verify:changed -- <path>` · AI service: `uv run --project services/ai pytest <path>`
- `verify` – canonical PR proof: lint, types, unit tests **and integration tests against real Postgres + S3 storage** (they prove tenant isolation and exactly-once export on every PR), architecture check (dependency-cruiser), build, `pnpm audit`, plus ruff/pyright/pytest for `services/ai`. `pnpm verify` (needs `docker compose up -d postgres storage`). A PR is not `ready-for-review` while verify fails, cannot run, or the exception is not justified in the PR.
- `verify:full` – Playwright smoke flow + full AI eval run (until the smoke flow exists it equals `verify`). `pnpm verify:full` – before a release, after risky refactors or with PR label `verify-full`.
- `verify:full` – `verify` + Playwright smoke flow (upload → worker with an AI stub → review → approve; needs `playwright install chromium` or `PW_CHROMIUM_PATH`); plus the AI eval gate in replay mode (`pnpm evals`). `pnpm verify:full` – before a release, after risky refactors or with PR label `verify-full`.
- **AI eval gate** (ADR-0001 D8): additionally runs path-targeted in CI on every change under `services/ai/` (prompts, parsing, extraction, model config) – a regression on a key field fails the PR.
- Test and verify output is trimmed automatically (`scripts/quiet-run.sh` via the hook `filter-test-output.sh`): exit code unchanged, full log path printed; prefix `FLUORY_FULL_OUTPUT=1` once when the cause is unclear.
- **Docs guard:** `scripts/doku-check.sh` – runs in CI and in `/finish-work`.
Expand Down
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,42 @@ This file records what changes **in the product** – process and session state
## [Unreleased]

### Added
- AI eval set and gate: 15 synthetic cases (tables, scans, missing values, prompt injection) measure
extraction quality per field; every change to the AI service is checked against a committed
baseline and fails when a field gets worse by more than 5 points or an injected value is accepted.
- More document formats: Outlook `.msg` (with attachments, parsed recursively), Excel `.xlsx` and Word
`.docx` are extracted with exact source positions; scanned PDFs can be read with OCR – such values are
at most "uncertain". A broken attachment no longer fails the whole request.
- Extraction schema v2: e-mail, phone and additional requirements as header fields, plus line items
(description, quantity, unit, material, dimensions), each with its own status and source quote; German
number formats, units and dates are normalised, a bare calendar week ("KW 42") stays at most uncertain.
- User management for admins (`/users`): see the company's users with role and status, change roles,
deactivate (sign-in blocked, sessions ended) and reactivate; invitations and every change are audited.
The last active admin of a company cannot be demoted or deactivated; clerks have no access.
- Guard for tenant isolation: the build fails when a table with company data lacks enforced
row-level security or its company policy.
- Export: approved requests are sent to the ERP (a simulated ERP in the pilot, contract
`contracts/erp-export.openapi.yaml`) exactly once – retries after errors or lost answers never create a
second record; the request page shows the ERP reference, running retries, and a visible error if the
export finally fails (reprocess possible). The ERP mock is off unless `ERP_MOCK_ENABLED=true`.
- Review (`/requests/:id`): staff see each extracted field with its status (found, uncertain,
missing, not verified) beside the source passage, correct values (every correction is kept with
who and when), approve the request – which queues it for export – or reject it with a reason.
- `pnpm verify:full` runs a browser smoke flow (upload → processing → review → correction → approval).
- Background processing: the worker sends each document to the AI service, stores the extracted
fields with their evidence and moves the request to review; failures retry with backoff and end
in a visible error with attempts and cause; failed requests can be reprocessed.
- AI service (`services/ai`): `POST /v1/extract` turns an e-mail or PDF into segments with stable
locators and extracts company, contact person and requested delivery date with evidence; a
deterministic verifier marks every value whose quote is not in the cited segment as `unverified`.
- Upload of a quote request (`/requests`): .eml, .msg, .pdf, .xlsx, .docx up to a configured size;
originals stored privately, download only for the own company. Request, documents, audit entry and
the processing job are created in one step; exact duplicates are flagged and linked.
- Invite-only login (e-mail + password): admins invite staff into their own company and hand over
an invitation link; sign-up without a valid invitation link creates no account. Roles `admin` and `clerk` per company.
- Tenant isolation: every company-owned table has forced row-level security; data access runs
inside `withTenant()`.
- Login rate limit (stored in the database) and `pnpm seed:demo` with two synthetic companies.
- Runnable local stack: `docker compose up` starts PostgreSQL 17, SeaweedFS (S3), a one-shot `setup`
step (migrations + private bucket), the web app and a no-op worker.
- `GET /api/health` reports database and storage status (200 / 503, no connection details).
Expand Down
27 changes: 27 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ services:
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID:-local-access-key}
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:-local-secret-key}
S3_FORCE_PATH_STYLE: "true"
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:-local-dev-only-secret-change-me-0123456789}
BETTER_AUTH_URL: ${BETTER_AUTH_URL:-http://localhost:3000}
APP_ENV: ${APP_ENV:-local}
AI_SERVICE_URL: ${AI_SERVICE_URL:-http://ai:8080}
AI_SERVICE_TOKEN: ${AI_SERVICE_TOKEN:-local-dev-only-ai-token-0123456789}
# ERP port (ADR-0001 D9): the worker exports to the mock inside `web`; the token is shared.
ERP_BASE_URL: ${ERP_BASE_URL:-http://web:3000/api/erp-mock}
ERP_TOKEN: ${ERP_TOKEN:-local-dev-only-erp-token-0123456789}
ERP_MOCK_ENABLED: ${ERP_MOCK_ENABLED:-true}
ERP_MOCK_FAULTS: ${ERP_MOCK_FAULTS:-}
depends_on:
postgres:
condition: service_healthy
Expand Down Expand Up @@ -85,6 +95,23 @@ services:
setup:
condition: service_completed_successfully

# AI service (services/ai). Opt-in profile: it refuses to start without Vertex AI credentials
# (fail-closed, ADR-0001 D8). Without it, processing jobs retry and end in ERROR with a visible cause.
# docker compose --profile ai up
ai:
build: ./services/ai
profiles: ["ai"]
environment:
AI_SERVICE_TOKEN: ${AI_SERVICE_TOKEN:-local-dev-only-ai-token-0123456789}
VERTEX_PROJECT: ${VERTEX_PROJECT:-}
VERTEX_LOCATION: ${VERTEX_LOCATION:-eu}
VERTEX_MODEL: ${VERTEX_MODEL:-gemini-3.5-flash}
GOOGLE_APPLICATION_CREDENTIALS: /secrets/adc.json
volumes:
- ${GOOGLE_ADC_FILE:-./.secrets/adc.json}:/secrets/adc.json:ro
ports:
- "${AI_PORT:-8000}:8080"

volumes:
pgdata:
seaweed:
Loading
Loading