Skip to content

Add agent analysis sections, custom templates, and a unified async report pipeline #4280

Description

@haJ1t

Add agent analysis sections, custom templates, and a unified async report pipeline

Design spec: docs/superpowers/specs/2026-07-17-reporting-agent-analysis-design.md (commit dcad85be3).

Current state

Reporting already ships four tabs, four tables, and a working BullMQ scheduler. This issue finishes that domain rather than starting a new one. What exists must not be rebuilt:

  • report_templates, report_template_versions, scheduled_reports, report_runs (20260619190359-create-reporting-domain.js), 3 seeded system templates
  • Templates / Scheduled / Archive tabs + the 6-step ConfigureReportWizard
  • BullMQ repeatable report_scheduler_tick (*/15 * * * *) + timezone-correct computeNextRun() (scheduleCalculator.ts)
  • DELETE /api/reporting/scheduled-reports/:id (scheduledReport.route.ts:20softDeleteQuery, scheduledReport.utils.ts:44-48) — already implemented, has no frontend caller
  • AI summarisation (aiSummarizer.ts:388-460) producing executive summary, keyFindings, recommendations, riskHighlights, per-section prose — off-pattern and unpersisted

What is missing: persisted/versioned/auditable analysis, any template write path, and a report id to attach analysis to.

Note the *.agent.ts + registry layer is orphaned — bootstrapAgentNetwork() (Servers/index.ts:85) registers 9 agents across two registries, but nothing reads AgentDefinition.systemPrompt/.tools and getAgentToolFilter has zero callers. A report.agent.ts would do nothing at runtime. Analysis follows the shipped evidence-analyzer pattern instead: backend calls the tenant's own llm_keys row, outbound only. No inbound agent API, no sk-mcp-*.

Tasks

Phase 1 — Pipeline (everything else depends on a run id existing)

  • Create Servers/database/migrations/YYYYMMDDHHMMSS-create-report-run-analyses.jsreport_run_analyses, unique (report_run_id, section_key, organization_id), JSONB payload, analysis_model, analysis_version, analyzed_at, analyzed_by, audit_metadata, full down()
  • Create Servers/database/migrations/YYYYMMDDHHMMSS-fix-report-runs-fields.js — add report_runs.file_id FK → files (missing at 20260619190359-...js:80), add scheduled_reports.llm_key_id column (read at reportRunOrchestrator.ts:22, no column exists), add trigger_type enum (manual | scheduled)
  • Servers/controllers/reporting.ctrl.tsgenerateReportsV2 stops returning a blob; creates a report_runs row with trigger_type='manual', enqueues, returns run id
  • Servers/services/automations/automationProducer.tsscheduleReportGeneration(), explicit tz, no queue.obliterate()
  • Servers/jobs/producer.ts — call it at the end of addAllJobs()
  • Servers/services/automations/automationWorker.ts — dispatch arm for the new job
  • Servers/services/reporting/reportRunOrchestrator.ts — per-org Redis lock (SET key ts PX <ttl> NX, TTL < tick interval, released in finally; copy aiDetection/scheduledScanProcessor.ts:36-52)
  • Retire the legacy scheduled_report automation trigger (automationWorker.ts:250-492) which bypasses scheduled_reports/report_runs entirely — preserve its working email send as the reference for Phase 4
  • Clients/src/domain/interfaces/i.reporting.tsReportTemplate, ReportTemplateVersion, ScheduledReport, ReportRun, ReportRunAnalysis, SectionsConfig, AiBlocksConfig, ScheduleConfig, DeliveryConfig (every payload fn in reporting.repository.ts returns any today except downloadReportRun)
  • Clients/src/application/hooks/useReportRunStatus.ts — polling
  • Rewrite Clients/src/presentation/components/Reporting/GenerateReport/ — async + real progress (DownloadReportFrom/index.tsx:32 is literally // Simulated progress)

Phase 2 — Analyzers

  • Create Servers/advisor/reportAnalyzers/schema.ts — zod .strict(), .describe() every field, nullable abstain_reason
  • Create Servers/advisor/reportAnalyzers/prompts.tsANALYZER_VERSION = "report-analyzer-v1" + prompt builders
  • Create 6 pure analyzers under Servers/advisor/reportAnalyzers/executiveSummary, keyFindings, recommendedActions, riskAnalysis, complianceGap, vendorRisk. Pure: data + llmKey in, result out, zero DB writes, never touch req/res
  • Create Servers/advisor/reportAnalyzers/index.tsrunAnalyzers, Promise.all, only template-enabled blocks
  • Widen createModel() to an exhaustive switch on the provider union (evidenceAnalyzer/analyzer.service.ts:37; :130 only discriminates Anthropic, :137-141 collapses OpenAI/OpenRouter/Custom) and use openai.chat(modelId) when a custom baseURL is set
  • complianceGap consumes getControlScoresQuery / getWeakestControlsQuery / getFrameworkScoreByTypeQuery (readiness.utils.ts:185-299) — must pass a non-null projectId; all three default to AND project_id IS NULL (:194, 231, 269) and no stored row can ever have a null project_id (readiness.ctrl.ts:83-88), so a project-less call returns [] and renders as "no gaps" forever
  • complianceGap treats /api/evidence-ai/gaps as an independent input, not a join — it covers only eu_ai_act/iso_42001 (evidenceAi.utils.ts:161), is not project-scoped, uses a different key space, and ?framework_type=iso_27001 returns EU rows mislabeled (:170-172)
  • Create Servers/utils/reportRunAnalysis.utils.ts — org-scoped ON CONFLICT DO UPDATE version bump (follow readiness.utils.ts:31-120, not the racy check-then-write in evidenceAi.utils.ts:12-107)
  • Widen AiBlocksConfig (i.reportTemplate.ts:14-18) to 6 independent blocks; reportTemplateResolver.ts stops OR-ing them into aiEnhanced. Column is unconstrained JSONB — no migration needed
  • Render analysis blocks in Servers/templates/reports/report-pdf.ejs and Servers/services/reporting/docxGenerator.ts — miss the second and analysis silently vanishes from docx
  • Delete Servers/services/reporting/aiSummarizer.ts once ported — one AI system in reporting, not two
  • Create Clients/src/presentation/components/Reporting/ReportAnalysisPanel/index.tsx — presentational, {analysis, isLoading, isAnalyzing, hasLLMKey, onTrigger}, caller owns hooks. Copy types from the analyzer, not by hand (EvidenceAnalysisPanel's rationales/document_signals have zero producers → :480 is unreachable dead UI)

Phase 3 — Templates

  • Create GET /api/reporting/sections — catalog derived from VALID_SECTION_KEYS (services/reporting/index.ts:21-35), excluding the all wildcard; frontend stops hardcoding REPORT_SECTION_GROUPS (constants.ts:22-53)
  • Servers/controllers/reportTemplate.ctrl.ts (38 lines today, list+get only) — add POST/PATCH/DELETE, authorize(["Admin","Editor"]), is_system_template write guard
  • Servers/utils/reportTemplate.utils.ts — write queries + append-only version bump; fix org-scoping at :24-39 (template versions are unscoped, harmless only while all templates are system templates)
  • Validate templateVersionId belongs to templateId and the caller's org in createScheduledReportQuery — accepted raw from req.body today (reportTemplate.ctrl.ts:37)
  • Create Clients/src/presentation/pages/Reporting/TemplateBuilder/ — mirrors the existing ConfigureReportWizard Stepper
  • TemplatesTab.tsx — create/edit/delete

Phase 4 — Truthfulness

  • Servers/services/reporting/reportDeliveryService.ts — wire sendEmailLink/attachFile to the MJML service, link to /api/reporting/runs/:id/download. Today :42-44 is a TODO, :46/:49 write status:"success" unconditionally, and the catch at :51-53 is unreachable (try block is pure object assignment) so status:"failed" is already dead code
  • Validate recipients — unvalidated free text today
  • Fix scheduled-report invisibility: project_id = null at reportDeliveryService.ts:29 vs bare JOIN projects at reporting.utils.ts:93-94. Note deliverReport only uploads when delivery.saveToStorage is true (:16)
  • Add PATCH /api/reporting/scheduled-reports/:id — no update endpoint exists. Do not add DELETE — it already exists (scheduledReport.route.ts:20); wire it into reporting.repository.ts, which has no caller for it
  • Add refetchInterval to useReportRuns — running/pending runs stay stale forever
  • Paginate listRunsQuery — hard LIMIT 200 at reportRun.utils.ts:38
  • Gate the wizard's AI blocks on useLLMKeyStatus().hasKeys — a keyless user can schedule an AI report today. Consume hasKeys directly (useLLMKeyStatus.ts:38)
  • Unhardcode format: "pdf"ConfigureReportWizard.tsx:103, the file's only format occurrence

Docs

  • Update docs/technical/domains/reporting.md + bump Last Updated; correct :185 (describes report-docx.ejs as live — it is dead)
  • Correct docs/technical/infrastructure/pdf-generation.md:195 — same stale report-docx.ejs claim
  • Add the new job to the schedule table in docs/technical/infrastructure/automations.md (~L103)
  • npm run generate:swagger && npm run generate:endpoints — CI api-docs-drift fails otherwise

API Endpoints

Route Method Purpose
/api/reporting/v2/generate-report POST Changed — creates a run, enqueues, returns run id (no longer a blob)
/api/reporting/runs/:id GET Run status for polling
/api/reporting/runs/:id/analyses GET Persisted analysis sections for a run
/api/reporting/sections GET Section catalog (single source of truth)
/api/reporting/templates POST Create custom template (Admin, Editor)
/api/reporting/templates/:id PATCH Update custom template (Admin, Editor)
/api/reporting/templates/:id DELETE Delete custom template (Admin, Editor)
/api/reporting/scheduled-reports/:id PATCH Update schedule (Admin, Editor) — new

Acceptance

cd Servers && npm run build && npm run test and cd Clients && npm run build && npm run test pass; a report generated with all six AI blocks enabled produces one report_run_analyses row per enabled section_key with a non-null analysis_model, renders those sections in both PDF and DOCX, and a run whose email delivery fails records status: "failed" — never "success"; an analyzer throwing leaves the report generated with that section abstained; a keyless org receives 400 on generate and cannot enable AI blocks in the wizard.

Risks

  • The async cutover changes an existing endpoint's contract and rewrites the Generate modal. Phase 1 lands green before anything else starts.
  • Worker process required. Servers/index.ts only calls addAllJobs(); it never constructs a Worker. Without npm run worker, generation jobs queue and never run — after this change that means reports never generate at all, where today manual ones still work.
  • Six analyzers is real LLM spend per report. Template gating is the control; system template defaults must not enable all six.

Branch

hp-apr-16-add-tasks-agent


Claims in the linked spec were adversarially verified against the codebase. Four were wrong and are corrected there — notably that DELETE /scheduled-reports/:id already ships, and that the section taxonomy lists do not actually disagree.

Metadata

Metadata

Assignees

Labels

ai-featureAI-powered featurebackendBackend related tasks/issuesfrontendFrontend related tasks/issuesphase-0Phase 0 — Immediate Priority AI Features

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions