API & Reports¶
API endpoints (high-level)¶
Storage (Phase 36) + access control (Phase 37)¶
- Analyses and comparisons are persisted in a SQLite file (default
data/autopsy.dbunder the API content root, overridable withStorage:DatabasePath). - When
Auth:Enabledis true, each row storesStoredArtifactAccess(owner user id,accessScope,sharedGroupIds,allowLinkAccess). Schema migration adds columns with safe defaults for existing deployments. Storage:ArtifactTtlHours(optional): when greater than0, rows get anexpires_utcand are purged on read and at startup;0or omitted means no TTL in typical config (seeappsettings.json).Storage:MaxArtifactRows(optional): when greater than0, oldest rows are deleted after inserts to cap total artifact rows (analyses + comparisons).- Payloads are full JSON snapshots of
PlanAnalysisResult/PlanComparisonResultV2(same shape as API responses)—not recomputed from raw plan text on read. - Phase 49 — artifact schema versioning: New writes stamp
artifactSchemaVersion(integer;0= legacy implicit / unversioned rows) and optionalartifactPersistedUtc. On read, the API runsPersistedArtifactNormalizerto upgrade legacy JSON to the current DTO shape where safe (suggestion family/backfills, compare carried-suggestionalsoKnownAsaliases for older deep-link ids, etc.). Unsupported future payload versions return 409; unreadable JSON returns 422 (row is not deleted). The SPA still applies a thin display normalization for missing suggestion fields as a safety net.
POST /api/analyze¶
-
When
Auth:Enabledis true andRequireIdentityForWritesis true, requests without a resolved identity return 401 (authentication_required). Successful writes persistartifactAccessderived from config + identity. -
body (either shape):
- Preferred (Phase 35):
{ planText: string, queryText?: string | null, explainMetadata?: … }—planTextis raw pasted text. The server runsPlanInputNormalizer: trims, accepts JSON starting with{/[, or strips aQUERY PLAN/psqltable wrapper, joins lines ending in+, drops(N rows)footers, then parses JSON. On failure: 400{ error: "plan_parse_failed", message, hint }. - Legacy:
{ plan: <json element>, queryText?, explainMetadata? }— omit or leaveplanTextempty;planmust be the JSON body. NoplanInputNormalizationis set (null). - response:
PlanAnalysisResult(includesqueryTextwhen provided;explainMetadataechoed when the client sent non-empty metadata after server normalization). WhenplanTextwas used:planInputNormalization:{ kind: "rawJson" | "queryPlanTable", detail? }. summary.plannerCosts: string enumpresent|notDetected|mixed|unknown— inferred from whether plan nodes carryStartup Cost/Total Cost/Plan Rows/Plan Width(not from declared EXPLAIN options).summary.hasBuffers: true if any plan node has at least one buffer counter (shared, local, or temp hit/read/dirtied/written fields). Populated from PostgreSQL’s flat per-node JSON keys and/or nestedBuffers, with optional aggregation fromWorkerswhen the parent omits totals.- Each analyzed node’s
nodeobject may includeworkers: an array of typed per-worker stats (camelCase fields mirroringPlanWorkerStats) when the plan JSON contained aWorkersarray. indexOverview,indexInsights,optimizationSuggestionsas in prior phases.
GET /api/config (Phase 37 + 38)¶
- Returns deployment flags for the SPA:
authEnabled,authMode,authIdentityKind(none|proxy|legacy_bearer|jwt|api_key),authHelp(short non-secret summary of the active mode),requireIdentityForWrites,defaultAccessScope,rateLimitingEnabled,storage.databasePath. Used for share-link copy, the Sharing panel, and optionalVITE_AUTH_*hints.
Auth identity (Phase 38)¶
JwtBearer:POST /api/analyze/POST /api/compareacceptAuthorization: Bearer <JWT>; ownerownerUserIdis the validatedsub(or configured subject claim), not the raw JWT string. Invalid Bearer JWT → 401invalid_tokenwhen a token is sent.ApiKey: send the key inX-Api-Key(orAuth:ApiKey:HeaderName); owner id comes from theapi_key_principalSQLite table (keys stored as SHA-256 hashes). Unknown/disabled key → 401.BearerSubject(legacy): entire bearer string remains the stored owner id (no JWT parsing). PreferJwtBearerorApiKeyfor production.
User preferences (Phase 40)¶
GET /api/me/preferences/{key}— whenAuth:Enabledis false, 404preferences_unavailable(SPA uses localStorage only). When auth is on, requires a resolved identity (401 without credentials); returns JSON{ "value": … }wherevalueisnullor any stored JSON subtree. Keys are opaque strings (for exampleanalyze_workspace_v1for Analyze layout state,compare_workspace_v1for Compare layout state). Values are versioned JSON blobs (visibility flags, section/column orders, optional preset id); the SPA coerces invalid stored orders back to safe defaults (Phase 42).PUT /api/me/preferences/{key}— same gates; JSON body{ "value": … }. Values persist in SQLite tableuser_preferencein the same database file as artifacts (Storage:DatabasePath).
GET /api/analyses/{analysisId}¶
- returns the stored
PlanAnalysisResult(includingartifactAccess,artifactSchemaVersion,artifactPersistedUtcwhen present); 404{ error: "analysis_not_found", analysisId }if missing or expired. - 422
{ error: "artifact_corrupt", message, analysisId }if the row exists but JSON cannot be deserialized (corrupt or irreconcilable shape). - 409
{ error: "artifact_version_unsupported", message, analysisId, schemaVersion, maxSupported }if the storedartifactSchemaVersionis newer than this server supports (or is explicitly rejected). - When
Auth:Enabledis true, 403{ error: "access_denied", analysisId }if the caller is not allowed to read (owner / group / public / link policy perArtifactAccessEvaluator).
GET /api/analyses¶
- lists analysis ids (debug-oriented; ordering not a strict contract).
POST /api/compare¶
- body (per side: text or JSON, not both required on a side if the other form is provided):
planAText/planBText(optional): same normalization as AnalyzeplanText.planA/planB: raw JSON elements (legacy).queryTextA/queryTextB,explainMetadataA/explainMetadataB(optional): passed intoAnalyzeAsyncfor that side before the comparison engine runs. EmbeddedplanA/planBon the result carry query text, metadata, andplanInputNormalizationwhen applicable.- 400
{ error: "plan_parse_failed", side, message, hint }if normalization fails for that side. - 400
{ error: "plan_required", side, message }if a side has neither text nor JSON. - query:
?diagnostics=1for matcher diagnostics. - response:
PlanComparisonResultV2(unchanged high-level shape;planA/planBare full analyze snapshots).
PUT /api/analyses/{analysisId}/sharing / PUT /api/comparisons/{comparisonId}/sharing (Phase 37)¶
- Body:
{ "accessScope": "private"|"link"|"group"|"public", "sharedGroupIds": ["…"], "allowLinkAccess": bool }. - Requires
Auth:Enabled; 401 without identity; 403 if not owner; 400 if auth disabled. - Success: 200
{ ok: true, artifactId, accessScope, sharedGroupIds, allowLinkAccess }.
GET /api/comparisons/{comparisonId}¶
- returns the stored comparison JSON (including
artifactAccess,artifactSchemaVersion,artifactPersistedUtcwhen present); 404{ error: "comparison_not_found", comparisonId }if missing/expired. - 422 / 409 — same
artifact_corrupt/artifact_version_unsupportedcontract as analyses (withcomparisonIdinstead ofanalysisId). - When
Auth:Enabledis true, 403{ error: "access_denied", comparisonId }when denied.
Compatibility matrix (persisted artifacts)¶
| Situation | HTTP | error |
Notes |
|---|---|---|---|
| Missing / expired row | 404 | analysis_not_found / comparison_not_found |
|
| Auth denies read | 403 | access_denied |
|
| JSON parse / null payload | 422 | artifact_corrupt |
Row retained for recovery |
| Schema version too new | 409 | artifact_version_unsupported |
Includes schemaVersion, maxSupported |
| Legacy unversioned or older known shape | 200 | — | Server normalizes to current response shape when possible |
Reports¶
POST /api/report/markdown/POST /api/report/html/POST /api/report/json— body is{ analysis: <PlanAnalysisResult> }(snapshot from a prior response orGET /api/analyses/{id}) or legacy{ plan: … }for server-side analyze-from-JSON. Phase 116:ArtifactPersistenceJson.ApplyToHttpSerializerOptionscopiesArtifactPersistenceJson.OptionsontoConfigureHttpJsonOptionsso analyze/compare report POST bodies use the same converters as SQLite persistence (and duplicate custom converters are not re-registered if the helper runs more than once). The SPA maps opaque4xxexport failures throughformatApiErrorResponsefor clearer user-facing lines.- Phase 117 / 118: Malformed JSON or unreadable bodies on
/api/report/*and/api/compare/report/*return 400 JSON (not an empty body):request_body_invalid(could not parse/read the body) viaReportExportBadRequestExceptionHandler(IExceptionHandler);detailmay include the raw server message in Development only. Phase 118: When the body parses but an analyze report request has neitheranalysisnorplan,export_request_incompleteis returned fromReportExportValidation.TryAnalyzeReportRequest(explicitResults.BadRequest, not exception-message parsing). Compare semantic errors (comparison_invalid,plan_required, etc.) remainResults.BadRequestfromCompareExecution. Unhandled exceptions use the default ProblemDetails pipeline (AddProblemDetails+UseExceptionHandler()), not a bespoke catch-all 500 JSON handler. The SPA showsmessagewithout echoingerrorcodes forrequest_body_invalid,export_request_incomplete, and 413payload_too_large/request_too_largewhen a JSONmessageis present. - Phase 119: For 500 responses with RFC 7807-style JSON (
title, optionaldetail) and no top-levelmessage,formatApiErrorResponsebuilds a single calm line and avoids treating stack-likedetailas user-facing text. - Phase 120 — compact 400 JSON shapes (report routes;
messageis the user-facing line the SPA prefers):
{ "error": "request_body_invalid", "message": "…" }
{ "error": "export_request_incomplete", "message": "…" }
When the body is empty or unreadable, clients may still receive 400 with no JSON; formatApiErrorResponse maps that to a single reload/re-paste hint (Phase 120). Phase 121: empty 401 responses map to a single sign-in / deployment hint; empty 413 uses the same calm tone as 400 (size limits, not alarmist wording).
- POST /api/compare/report/markdown / POST /api/compare/report/html / POST /api/compare/report/json — body is either the same compare request shape as POST /api/compare (including planAText / planBText and per-side metadata) or (Phase 114) { comparison: <PlanComparisonResultV2> } only, so exports can render from a loaded snapshot without pasting plan text again (reopen-from-link parity). ?diagnostics=1 does not recompute matcher diagnostics when a snapshot is supplied—diagnostics are whatever the snapshot contains. Phase 115: Markdown/HTML exports include a compact server ## Reading thread / Change at a glance lead (from comparisonStory.overview, else a ranked new/worsened finding-diff line) before ComparisonId—the SPA may prepend a short Selection context (pair bridge + primary pair id) without duplicating that lead. HTML includes Top worsened/improved lines with Rewrite outcome when a pair verdict exists, plus Plan capture (per side), change briefing (with Story beats when the story has beats), bottleneck posture, narrative, an Index changes section (plan overview + insight diffs, aligned with the app — Phase 94), and a Next steps after this change block when compare-scoped suggestions exist (Phase 88 tightens markdown/HTML alignment).
Markdown report notes¶
- Uses human-readable node labels in key places.
- Includes a
Source Querysection whenqueryTextis present in the analysis. - Adds Plan capture & EXPLAIN context with detected planner-cost presence and any declared
explainMetadata(options line + optional recorded command). - Compare markdown adds a Plan capture & EXPLAIN context (per side) section summarizing Plan A and Plan B (query present/absent, input normalization kind, planner costs, declared options, recorded command). The SPA lists Next steps after this change inside a
sectionlandmark (role="region",aria-labelledbytheh2) for clearer navigation (Phase 89). - Analyze markdown/HTML include an Optimization suggestions section when the engine produced items; Phase 86 HTML aligns with markdown on suggestion id, Why, and short Validate lines where present. Compare markdown adds Next steps after this change when
compareOptimizationSuggestionsis non-empty (family + optional Why line); compare HTML mirrors that block when suggestions exist (Phase 87 aligned the markdown heading with HTML). POST /api/report/jsonandPOST /api/compare/report/jsonreturn the same persisted shapes asGETartifacts (camelCase,ArtifactPersistenceJsonoptions): analyze JSON includesplanStory.inspectFirstStepswhen the builder produced steps; compare JSON includescompareOptimizationSuggestionsand per-pairrewriteVerdictOneLinerwhen present (Phase 87 tests lock the wiring).