Skip to main content

Report analysis workflow

Purpose

sdac-report-analysis is the post-ingestion Mastra workflow that turns a parsed SDAC workbook into durable, queryable analysis evidence. It runs after the ingestion service has written the report and personnel rows to the SDAC database. Its output is a persisted REPORT_ANALYSIS_PACKET that conversational agents can retrieve without re-running every validation tool.

The workflow is intentionally separate from the agent. The agent answers from persisted evidence first, then falls back to raw tools only when a result is missing, stale, still running, or explicitly refreshed by the user.

High-level flow

End-to-end tool orchestration

This is the current production path from report upload to a conversational answer. Browser/widget clients call Mastra, not the ingestion worker. Mastra owns public upload/status, persists durable upload progress, calls ingestion as an internal parser worker, then starts the same report-analysis workflow once SDAC.data_CostReports and SDAC.data_PersonnelRecords exist.

Tool call order

The report-analysis path deliberately separates entry-point tools, workflow-owned tools, and answer-retrieval tools.

OrderCallerTool or stepWhat it doesOutput/persistence
1Widget/API clientPOST /sdac/upload on MastraCreates a durable upload job and submits the workbook to the internal ingestion worker.SDAC.ingestion_UploadJobs row; jobId; eventual reportId.
2Widget/API clientGET /sdac/uploads/:jobId/status on MastraMirrors worker progress and starts report analysis after parsing succeeds.Durable status, report ID, analysis run metadata.
3WorkflowLoad report stepWaits for ingestion readiness and creates pending rows.Pending rows in SDAC.analysis_Results.
4Workflowsdac-cross-reference-replacements, deterministic modePerforms deterministic replacement/support linkage.LINKAGE_DETERMINISTIC.
5WorkflowConditional AI linkage branchRuns sdac-cross-reference-replacements with AI assist only when deterministic linkage has blocking unresolved rows; otherwise persists a skipped AI row.LINKAGE_AI.
6WorkflowSafe analysis waveRuns analyses that do not require trusted canonical linkage.One result row per safe analysis.
7WorkflowCanonical linkage mergeChooses deterministic or AI-assisted linkage output and computes blocking linkage issues.LINKAGE_CANONICAL.
8WorkflowLinkage-dependent analysis waveRuns position-dependent analyses only when canonical linkage is clean.One completed or blocked row per linkage-dependent analysis.
9WorkflowPacket compile stepBuilds one report-level packet from persisted rows.REPORT_ANALYSIS_PACKET.
10AAgentsdac-get-analysis-packetRetrieves the full packet for broad prompts.Conversationally visible packet sections.
10BAgentsdac-get-analysis-resultRetrieves one persisted analysis for focused prompts.Conversationally visible single analysis result.

For broad prompts such as "review this report", step 10A is the first answer tool. For focused prompts such as "provide fringe analysis", step 10B is the first answer tool. Raw validation tools should be a fallback path only when persisted evidence is missing, stale, still running, or explicitly refreshed.

Current parallelism

The implementation uses workflow-level parallelism around dependency units, not broad analysis buckets:

StageExecutionNotes
Load reportSequentialWaits for ingestion readiness and creates pending analysis rows.
Deterministic linkageSequentialLINKAGE_DETERMINISTIC runs first and decides whether AI help is needed.
Conditional AI linkage + safe analysesParallelLINKAGE_AI runs only when deterministic linkage has unresolved/blocking rows; otherwise it persists a skipped result. Safe analyses run in the same parallel node.
Canonical linkageSequentialMerges deterministic linkage and any AI linkage output, then determines blocking issues.
Linkage-dependent branchesParallelEvery registered linkage-dependent analysis branch runs in the same workflow parallel node after canonical linkage.
Packet compileSequentialReads persisted rows and writes the final packet row.

The maximum current analysis fan-out after deterministic linkage is the conditional AI linkage branch plus all safe analyses. Branches that require clean linkage do not run calculations when canonical linkage has blocking issues; they persist a blocked result immediately. Independent branches continue normally.

When one user-facing analysis has both deterministic and AI work, the intended model is deterministic-first with AI fallback for unresolved cases, then a reducer that persists one canonical result. Linkage follows this pattern: deterministic linkage runs first, AI-assisted linkage only runs for unresolved/blocking deterministic linkage, then canonical linkage persists the merged prerequisite result.

Dependency model

Blocking linkage issues stop position-dependent calculations because salary, fringe, replacement, duplicate, duplicate-context, and vacant analysis rely on canonical position linkage. Those branches still execute their blocking path so the database records why the analysis is unavailable. Independent branches still produce results because they do not require trusted replacement/support attribution.

Analysis registry

The workflow reads its analyzable sections from packages/domain-sdac/src/analysis/analysis-registry.ts.

Analysis typeToolBranch kindBlocks on linkage issues
FILE_METADATAsdac-validate-file-metadatasafeNo
REPORT_TOTALSsdac-validate-report-totalssafeNo
SOURCE_CODEsdac-validate-source-codessafeNo
FUNCTION_CODEsdac-validate-function-codessafeNo
FEDERAL_FUNDING_BACKOUTsdac-validate-federal-funding-backoutsafeNo
INDIRECT_COST_FUNCTIONsdac-validate-indirect-cost-functionssafeNo
COMMENT_QUALITYsdac-analyze-comment-qualitysafeNo
JOB_TITLE_CLASSIFICATIONsdac-classify-job-titlessafeNo
JUSTIFICATION_INITIALsdac-validate-justificationscomparisonNo
SALARYsdac-validate-salarieslinkage-dependentYes
FRINGEsdac-validate-fringelinkage-dependentYes
REPLACEMENTsdac-validate-replacementslinkage-dependentYes
DUPLICATEsdac-detect-duplicateslinkage-dependentYes
DUPLICATE_CONTEXTsdac-analyze-duplicates-contextlinkage-dependentYes
VACANTsdac-detect-vacant-positionslinkage-dependentYes

Internal linkage rows are also persisted:

  • LINKAGE_DETERMINISTIC
  • LINKAGE_AI
  • LINKAGE_CANONICAL

They are prerequisites for packet construction, but they are not normally shown to users as standalone analysis sections.

Upload Persistence

Mastra upload progress is persisted in SDAC.ingestion_UploadJobs.

This table is separate from analysis evidence because upload parsing and analysis can progress independently. A job starts as queued, moves through processing, then ends as success, duplicate, or error. Important fields include:

ColumnPurpose
JobIdPublic Mastra upload job ID returned to the widget.
ReportIdParsed SDAC report ID once ingestion has written SDAC.data_CostReports.
WorkerJobIdInternal ingestion worker job ID, usually the same ID supplied by Mastra.
Status, Stage, MessageDurable progress state shown to the widget.
WorkerStatusUrlInternal worker status endpoint that Mastra polls.
AnalysisTriggered, AnalysisRunId, AnalysisStatusWhether Mastra has started sdac-report-analysis for the parsed report.
ResultJson, ErrorJsonLast mirrored worker payload or failure details.
LastHeartbeatAtUtcUsed to report stalled uploads only when no progress has been observed.

The ingestion worker still exposes /sdac/upload and /sdac/uploads/:jobId/status, but those are internal service-to-service endpoints. Mastra calls them with trigger_analysis=false; analysis is started by Mastra after parsing succeeds.

Analysis Persistence

All workflow evidence is persisted in SDAC.analysis_Results.

Each report has one row per AnalysisType, enforced by UQ_analysis_Results_ReportType on (ReportId, AnalysisType). The workflow writes:

  • one row for each internal linkage branch
  • one row for each registered analysis
  • one final REPORT_ANALYSIS_PACKET row

The packet row uses:

AnalysisType = REPORT_ANALYSIS_PACKET
ToolId = sdac-report-analysis-workflow

Important columns:

ColumnPurpose
Statuspending, running, completed, error, stale, or blocked.
VisibilityStatusvisible or disabled; conversational retrieval hides disabled rows.
AnalysisVersionVersion string for the algorithm/prompt set that produced the row.
InputHashStable hash of report, version, and dependencies used to detect freshness.
DependencyJsonJSON dependency metadata, such as canonical linkage hash.
BlockedReasonHuman-readable reason when an analysis cannot run because linkage is blocking.
ResultJsonFull normalized tool or packet output.
Passed, ErrorCount, WarningCount, RecordsCheckedDenormalized summary fields for fast retrieval and dashboard display.

See SDAC Database Schema for the full column list.

Agent Retrieval Audit

Conversational retrieval is audited separately from workflow execution in SDAC.analysis_AgentRetrievalAudit.

The audit table records actual SDAC chat stream events. A row is written when the agent emits a tool-call event and is completed when the matching tool-result event arrives. This gives reviewers an observable trail of which persisted analysis tools the agent actually called, which analysis types came back, and which requested analysis types were missing.

Audited calls include:

ToolAudit purpose
sdac-get-analysis-packetConfirms broad review prompts loaded the persisted report packet and records packet coverage.
sdac-get-analysis-resultConfirms focused prompts loaded the requested persisted analysis type.
sdac-search-analysis-evidenceConfirms evidence search queries and records returned or missing analysis types.
sdac-start-report-analysisConfirms the agent requested workflow start/status, but does not count as evidence retrieval.
Raw sdac-* analysis toolsCaptures fallback validation calls when persisted evidence is unavailable, stale, still running, or explicitly refreshed.

Coverage statuses are intentionally compact:

Coverage statusMeaning
completeThe retrieval returned all requested visible analysis types, or a broad packet retrieval returned analysis sections.
partialSome requested analysis types were returned and some were missing.
singleA focused single-analysis result or raw analysis tool completed.
noneThe tool returned no analysis evidence.
not_applicableThe tool was workflow/status oriented rather than evidence retrieval.
unknownThe tool has started but has not completed yet, or the output shape could not be summarized.

Use this query when validating whether the Coordinator Release Agent saw the expected analysis for a conversation:

SELECT
StartedAtUtc,
CompletedAtUtc,
AgentId,
ToolName,
RetrievalKind,
Status,
CoverageStatus,
RequestedAnalysisJson,
ReturnedAnalysisJson,
MissingAnalysisJson,
ResultSummary
FROM SDAC.analysis_AgentRetrievalAudit
WHERE ReportId = @ReportId
AND ConversationId = @ConversationId
ORDER BY TurnNumber, StartedAtUtc;

For a broad review prompt, look for a completed sdac-get-analysis-packet row with CoverageStatus = 'complete' and ReturnedAnalysisJson listing the visible packet analysis types. For a focused prompt, look for sdac-get-analysis-result or sdac-search-analysis-evidence rows that include the requested analysis type. If the table only shows raw tool fallbacks, the agent did not use the persisted packet path for that turn.

This audit proves what analysis the agent retrieved. It does not prove that the final prose used every retrieved finding correctly. Final assistant messages still live in SDAC.fact_ConversationHistory, so response-quality review should compare fact_ConversationHistory against analysis_AgentRetrievalAudit and the persisted rows in analysis_Results or analysis_EvidenceFindings.

Feature flags

Analysis visibility is controlled by database feature flags in Core.FeatureFlags. The report analysis still runs and persists all rows when a flag is off; only conversational retrieval hides disabled sections.

All SDAC analysis visibility flags use:

FlagScope = sdac

Managed flag keys:

Flag keyControls
sdac.analysis.visible.source_codeSOURCE_CODE
sdac.analysis.visible.function_codeFUNCTION_CODE
sdac.analysis.visible.comment_qualityCOMMENT_QUALITY
sdac.analysis.visible.job_title_classificationJOB_TITLE_CLASSIFICATION
sdac.analysis.visible.salarySALARY
sdac.analysis.visible.fringeFRINGE
sdac.analysis.visible.replacementREPLACEMENT
sdac.analysis.visible.duplicateDUPLICATE
sdac.analysis.visible.duplicate_contextDUPLICATE_CONTEXT
sdac.analysis.visible.vacantVACANT

These flags are declared in src/mastra/config/project-profile-settings.manifest.js and reconciled by the profile settings deployment scripts.

Agent retrieval

The Coordinator Release Agent has three report-analysis tools:

ToolUse
sdac-get-analysis-packetRetrieves the full persisted packet, optionally filtered to requested sections.
sdac-get-analysis-resultRetrieves one persisted analysis result, such as FRINGE or SALARY.
sdac-start-report-analysisStarts or reuses the workflow for on-demand analysis.

For broad prompts such as "review this report", the agent should call sdac-get-analysis-packet first. For focused prompts such as "provide the fringe benefit analysis", the agent should call sdac-get-analysis-result first. Raw validation tools should only run when persisted evidence is unavailable, stale, still running, or explicitly refreshed.

Progress surfaces

The workflow records step progress through the report-analysis runtime and tracker. Current progress can be read from persisted packet/status state and from the upload status route:

GET /sdac/uploads/{jobId}/status
GET /sdac/report-analysis/{reportId}/status

Widget browser code polls the same data through /api/mastra/sdac/uploads/{jobId}/status and /api/mastra/sdac/report-analysis/{reportId}/status.

Chat also proactively starts report analysis when a report context is available and can answer from completed packet lines while the rest of the workflow continues.

Known gaps

  • The workflow now schedules all registered analysis branches in one parallel node after canonical linkage. The next refinement is splitting hybrid analyses into explicit deterministic and AI sub-branches when the tool supports that separation.
  • Live testing has covered a fresh uploaded workbook and a focused fringe agent prompt, but still needs a larger fixture corpus and prompt matrix.
  • Feature flag hiding is covered by unit/tool tests, but should be live-tested by toggling a DB flag and querying the agent.
  • AI-assisted linkage depends on the replacement-linker agent being available in the runtime context. If that agent is unavailable, linkage can complete without useful AI contribution.
  • Some agent answers summarize warning counts without identifying the exact row or position until a follow-up. Row-level prompt tests should become part of acceptance.
  • The status route should be verified in the deployed route table before UI progress polling depends on it.

Source files

  • Workflow: packages/domain-sdac/src/workflows/report-analysis.workflow.ts
  • Runtime/status helpers: packages/domain-sdac/src/workflows/report-analysis.runtime.ts
  • Progress tracker: packages/domain-sdac/src/workflows/report-analysis.tracker.ts
  • Registry: packages/domain-sdac/src/analysis/analysis-registry.ts
  • Packet builder: packages/domain-sdac/src/analysis/analysis-packet.ts
  • Upload API: packages/domain-sdac/src/api/sdac-upload.ts
  • Upload job persistence: packages/domain-sdac/src/api/sdac-upload-jobs.ts
  • Ingestion worker client: packages/domain-sdac/src/api/sdac-ingestion-worker.ts
  • Persistence helpers: packages/domain-sdac/src/tools/analysis-store.ts
  • Database table: db/sqlproj/Mastra.Platform.SDACDb/model/Tables/SDAC.analysis_Results.sql
  • Upload job table: db/sqlproj/Mastra.Platform.SDACDb/model/Tables/SDAC.ingestion_UploadJobs.sql