feat: BRD depth, house cover, flow diagrams, and two drafting modes
Generated BRDs were short, plain and slow, and the house cover never reached a deployed instance. This is the work behind all four.
Output quality
- Sections are written to depth targets with numbered sub-blocks, so a phase reads as a flow rather than a paragraph, and process phases carry a rendered flowchart (services/export/diagram.js) with decision branches labelled.
- GENERATION_MAX_TOKENS 4096 -> 8192. A cap is a ceiling, not a spend: a table that writes 300 tokens costs 300 whatever the cap says. What a cap costs is being hit, because on finish_reason "length" the attempt is discarded and re-asked at double the room. Both sections that failed with "Model did not return valid JSON" in the stored Aug 22 document failed on an unterminated string -- truncation, not formatting. services/kb/json.js now closes a truncated answer's JSON instead of losing the section.
The cover
- packages/brd-service/assets/ was untracked and the Dockerfile copied only src and public, so no deployment ever had the artwork. A missing cover is not an error -- coverImage() returns null, logs once, and every exporter falls back to a plain wordmark -- which is why this looked unimplemented rather than unshipped. Verified on page one of the pdfkit PDF, the Chrome PDF and the docx.
Speed, measured rather than assumed
- Two drafting modes are visible to everyone again instead of admin-only behind an "Advanced options" disclosure. The old control persisted its choice in localStorage and kept sending it for users who could no longer see it, so a slow mode picked once stayed picked invisibly.
- deepseek-v4-flash takes 21.6s a section against 100.8s for v4-pro, and the UI now puts that on screen before a run starts. Concurrency is not an alternative: DeepSeek meters a key by throughput, so three sections at once finished in 46.9s where six took 89.0s -- the same rate twice. Recorded next to generationConcurrency so the knob is not reached for again.
- The mode list comes from the server's whitelist via /api/health, because /generate answers an unknown model with a 400 and a browser holding a dropped name failed every generation with no cure but clearing localStorage.
Reliability
- A chat failure now carries its own retry verdict: 429/5xx/dropped connections are retried on a 2s..8s schedule that honours Retry-After, while a bad key or unknown model is reported at once. A chat error becomes "This section could not be generated" in a client's deliverable, so the trade is fourteen seconds against a hole in the document.
- Rate limiting is keyed per credential rather than per IP, since an office shares one address, and the once-a-second progress poll is exempt -- a single long generation used to spend the whole quota on its own progress bar and then 429 the next click.
Housekeeping
- restore.ps1 printed the live admin and analyst keys; it now names the .env variables to read them from.
- .gitignore covers qdrant_backup.tar.gz (333 MB of embeddings of real client records) and generated deliverables in the root; .dockerignore covers .scratch/, where reference copies of client BRDs land.
Suite: 995 passing.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com