openapi: "3.1.0" info: title: Dashboard Scenario Compiler API version: "1.0.0" description: > OpenAPI 3.1 contract for feature 038 — deterministic ScenarioGraph compile, validate, resolve, draft-pack, capture, VLM, and disposition operations. Generated from api-ux.md, data-model.md, and contracts/modules.md. servers: - url: /api/dashboard-testing description: superset-tools API gateway tags: - name: scenario description: ScenarioGraph compile, validate, resolve, draft-pack - name: evidence description: Screenshot capture, VLM analysis, human disposition paths: /scenarios/compile: post: operationId: compileDashboardScenario tags: [scenario] summary: Deterministically compile a dashboard goal into a ScenarioGraph description: | Combines bounded agent intent (objective, selected case ids) with the dashboard query model, checklist catalog, baseline summary, capability model, and parameters to produce a byte-stable DashboardTestScenario. Requires [scenario.compile] role. security: - BearerAuth: [scenario.compile] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CompileRequest" examples: valid: $ref: "#/components/examples/CompileRequestValid" responses: "200": description: Deterministic scenario plus validation summary content: application/json: schema: $ref: "#/components/schemas/ScenarioResponse" examples: compiled: $ref: "#/components/examples/ScenarioResponseCompiled" "401": $ref: "#/components/responses/UnauthorizedError" "403": $ref: "#/components/responses/ForbiddenError" "422": $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/RateLimitError" "500": $ref: "#/components/responses/InternalError" /scenarios/validate: post: operationId: validateDashboardScenario tags: [scenario] summary: Validate a candidate ScenarioGraph and return all findings description: | Pure validation of a candidate graph: schema, DAG, refs, parameters, baselines, tools, safety (no SQL / no raw metric truth / no path traversal), and checklist coverage. Returns deterministic findings. security: - BearerAuth: [scenario.compile] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ScenarioGraphInput" responses: "200": description: Complete validation findings content: application/json: schema: $ref: "#/components/schemas/ValidationResult" "401": $ref: "#/components/responses/UnauthorizedError" "422": $ref: "#/components/responses/ValidationError" "500": $ref: "#/components/responses/InternalError" /scenarios/{scenarioId}/resolve: post: operationId: resolveDashboardScenario tags: [scenario] summary: Apply typed parameter/selector/manual resolutions as an immutable revision description: | Applies declared resolutions to an unresolved graph and emits a new immutable revision linked via parent_revision_hash. Stale base revisions are rejected (409) — never silently merged. security: - BearerAuth: [scenario.resolve] parameters: - name: scenarioId in: path required: true schema: { type: string } requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ResolveRequest" responses: "200": description: New immutable revision content: application/json: schema: $ref: "#/components/schemas/ScenarioResponse" "401": $ref: "#/components/responses/UnauthorizedError" "403": $ref: "#/components/responses/ForbiddenError" "409": $ref: "#/components/responses/StaleRevisionError" "422": $ref: "#/components/responses/ValidationError" "500": $ref: "#/components/responses/InternalError" /scenarios/{scenarioId}/draft-pack: post: operationId: compileScenarioDraftPack tags: [scenario] summary: Generate a preview_only or save_eligible draft pack from versioned templates description: | Compiles a valid graph through registered versioned templates. Any validation error, NEEDS_SELECTOR, forbidden action, or unresolved required parameter makes the pack preview_only. Save-eligible packs are registered as 036 drafts. Idempotent per revision_hash. security: - BearerAuth: [scenario.draft] parameters: - name: scenarioId in: path required: true schema: { type: string } requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/DraftPackRequest" responses: "201": description: Draft pack registered via feature 036 content: application/json: schema: $ref: "#/components/schemas/DraftPack" "200": description: Idempotent replay — existing DraftPack for the same revision hash content: application/json: schema: $ref: "#/components/schemas/DraftPack" "401": $ref: "#/components/responses/UnauthorizedError" "403": $ref: "#/components/responses/ForbiddenError" "409": $ref: "#/components/responses/StaleRevisionError" "422": $ref: "#/components/responses/ValidationError" "500": $ref: "#/components/responses/InternalError" /scenarios/{scenarioId}/capture: post: operationId: captureScenarioScreenshot tags: [evidence] summary: Execute a screenshot capture step through the 036 Evidence bridge description: | Executes a validated ScreenshotCaptureSpec, registers the ScreenshotEvidence DraftArtifact (masked variant when mask_selectors present), and emits evidence_captured. Step output refs link artifact ids, not paths. security: - BearerAuth: [scenario.execute] parameters: - name: scenarioId in: path required: true schema: { type: string } requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CaptureRequest" responses: "200": description: Captured artifact refs content: application/json: schema: $ref: "#/components/schemas/CaptureResponse" "401": $ref: "#/components/responses/UnauthorizedError" "403": $ref: "#/components/responses/ForbiddenError" "422": $ref: "#/components/responses/ValidationError" "500": $ref: "#/components/responses/InternalError" /scenarios/{scenarioId}/vlm: post: operationId: analyzeScenarioScreenshot tags: [evidence] summary: Submit a masked screenshot to a VLM provider and return typed findings description: | Runs a registered VLM analysis profile against a captured screenshot. Returns typed VlmFinding[] with model/prompt provenance; raw response is stored under redaction as a separate DraftArtifact. Findings are advisory and never alter metric baseline truth. Stale prompt templates are rejected. security: - BearerAuth: [scenario.execute] parameters: - name: scenarioId in: path required: true schema: { type: string } requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/VlmRequest" responses: "200": description: Typed VLM findings content: application/json: schema: $ref: "#/components/schemas/VlmResponse" "401": $ref: "#/components/responses/UnauthorizedError" "422": $ref: "#/components/responses/StalePromptError" "500": $ref: "#/components/responses/InternalError" /scenarios/{scenarioId}/disposition: post: operationId: disposeVlmFindings tags: [evidence] summary: Record a typed human disposition on VLM findings description: | Sets confirm/dismiss/inconclusive on one or more unresolved findings. Confirm requires a non-blank comment. Each finding is disposed exactly once (409 on double disposition). Graph structure never changes. security: - BearerAuth: [scenario.execute] parameters: - name: scenarioId in: path required: true schema: { type: string } requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/DispositionRequest" responses: "200": description: Updated scenario step content: application/json: schema: $ref: "#/components/schemas/DispositionResponse" "401": $ref: "#/components/responses/UnauthorizedError" "409": $ref: "#/components/responses/DoubleDispositionError" "422": $ref: "#/components/responses/ValidationError" "500": $ref: "#/components/responses/InternalError" components: securitySchemes: BearerAuth: type: http scheme: bearer bearerFormat: JWT description: | superset-tools JWT. Roles encoded in `roles` claim. Required scopes: scenario.compile, scenario.resolve, scenario.draft, scenario.execute (per ADR-0005 RBAC). schemas: ErrorEnvelope: type: object required: [error] properties: error: type: object required: [code, detail] properties: code: { type: string, example: "VALIDATION_ERROR" } detail: { type: string } fields: type: object additionalProperties: { type: string } description: Per-field validation errors (422 only) retry_after: type: integer description: Seconds until retry is allowed (429 only) SuccessEnvelope: type: object required: [data] properties: data: {} meta: type: object properties: revision_hash: { type: string } CompileRequest: type: object additionalProperties: false required: [agent_run_id, objective, query_model, checklist_catalog_version, baseline_version, capabilities, parameters] properties: agent_run_id: { type: string, format: uuid } objective: type: object required: [goal, selected_case_ids] properties: goal: { type: string, maxLength: 2000 } selected_case_ids: { type: array, items: { type: string } } rationale: { type: [string, "null"], maxLength: 4000 } query_model: { type: object } checklist_catalog_version: { const: 1 } baseline_version: { type: string } capabilities: { type: object } parameters: { type: object } ScenarioGraphInput: type: object description: Candidate graph for validation. Forbids SQL, code bodies, raw metric truth, paths. additionalProperties: false required: [schema_version, scenario_id, dashboard_context, objective, phases, steps] properties: schema_version: { type: integer } scenario_id: { type: string } dashboard_context: { type: object } objective: { type: object } phases: { type: array, items: { type: string } } steps: { type: array, items: { type: object } } ScenarioResponse: type: object required: [scenario, validation] properties: scenario: { type: object } validation: { $ref: "#/components/schemas/ValidationResult" } ValidationResult: type: object required: [valid, errors, warnings, blockers, coverage, topological_order, graph_hash] properties: valid: { type: boolean } errors: { type: array, items: { type: object } } warnings: { type: array, items: { type: object } } blockers: { type: array, items: { type: object } } coverage: { type: array, items: { type: object } } topological_order: { type: array, items: { type: string } } unresolved_parameters: { type: array, items: { type: string } } unresolved_selectors: { type: array, items: { type: string } } unresolved_baselines: { type: array, items: { type: string } } graph_hash: { type: string } ResolveRequest: type: object additionalProperties: false required: [base_revision_hash, changes] properties: base_revision_hash: { type: string, pattern: "^[a-f0-9]{64}$" } changes: type: array items: type: object required: [kind, target, value] properties: kind: { enum: [parameter, selector, manual_conversion, remove_step] } target: { type: string } value: {} reason: { type: [string, "null"] } DraftPackRequest: type: object additionalProperties: false required: [agent_run_id, revision_hash] properties: agent_run_id: { type: string, format: uuid } revision_hash: { type: string, pattern: "^[a-f0-9]{64}$" } DraftPack: type: object required: [scenario_revision_hash, template_version, status, manifest, artifacts, validation_summary, warnings] properties: scenario_revision_hash: { type: string } template_version: { type: string } status: { enum: [preview_only, save_eligible] } manifest: { type: object } artifacts: { type: array, items: { type: object } } validation_summary: { $ref: "#/components/schemas/ValidationResult" } warnings: { type: array, items: { type: object } } CaptureRequest: type: object additionalProperties: false required: [agent_run_id, step_id, capture_spec] properties: agent_run_id: { type: string, format: uuid } step_id: { type: string } capture_spec: { $ref: "#/components/schemas/CaptureSpec" } CaptureSpec: type: object required: [target, viewport, readiness, method] properties: target: { type: string, enum: [tab, viewport] } tab_identifier: { type: [string, "null"] } viewport: type: object required: [width, height] properties: width: { type: integer } height: { type: integer } readiness: { type: string, enum: [canvas_stabilized, network_idle, fixed_wait] } readiness_timeout_ms: { type: integer, default: 15000 } mask_selectors: { type: array, items: { type: string } } method: { type: string, enum: [cdp, full_page, region] } CaptureResponse: type: object required: [artifact_refs] properties: artifact_refs: type: array items: type: object required: [artifact_id, kind, masked] properties: artifact_id: { type: string, format: uuid } kind: { type: string } masked: { type: boolean } VlmRequest: type: object additionalProperties: false required: [agent_run_id, step_id, artifact_id, analysis] properties: agent_run_id: { type: string, format: uuid } step_id: { type: string } artifact_id: { type: string, format: uuid } analysis: type: object required: [profile_id, provider_id, model_id, prompt_template_id, prompt_version, prompt_template_hash] properties: profile_id: { type: string } provider_id: { type: string } model_id: { type: string } prompt_template_id: { type: string } prompt_version: { type: string } prompt_template_hash: { type: string, pattern: "^[a-f0-9]{64}$" } confidence_threshold: { type: number, minimum: 0, maximum: 1, default: 0.7 } VlmResponse: type: object required: [findings] properties: findings: type: array items: { $ref: "#/components/schemas/VlmFinding" } VlmFinding: type: object required: [finding_id, source_artifact_id, severity, code, confidence, description, disposition, model_provenance] properties: finding_id: { type: string } source_artifact_id: { type: string, format: uuid } severity: { type: string, enum: [info, warning, error] } code: { type: string } region: type: object properties: selector: { type: string } bounds: { type: object } confidence: { type: number, minimum: 0, maximum: 1 } description: { type: string, maxLength: 1000 } disposition: { type: string, enum: [unresolved, confirmed, dismissed, inconclusive] } disposition_comment: { type: [string, "null"], maxLength: 2000 } model_provenance: type: object required: [model_id, prompt_version, prompt_template_hash, analyzed_at] properties: model_id: { type: string } prompt_version: { type: string } prompt_template_hash: { type: string, pattern: "^[a-f0-9]{64}$" } analyzed_at: { type: string, format: date-time } DispositionRequest: type: object additionalProperties: false required: [agent_run_id, step_id, dispositions] properties: agent_run_id: { type: string, format: uuid } step_id: { type: string } dispositions: type: array items: type: object required: [finding_id, decision] properties: finding_id: { type: string } decision: { enum: [confirm, dismiss, inconclusive] } comment: { type: [string, "null"], maxLength: 2000 } DispositionResponse: type: object required: [step] properties: step: { type: object } responses: UnauthorizedError: description: Missing or invalid authentication content: application/json: schema: { $ref: "#/components/schemas/ErrorEnvelope" } example: error: { code: "UNAUTHORIZED", detail: "Authentication required" } ForbiddenError: description: Insufficient permissions content: application/json: schema: { $ref: "#/components/schemas/ErrorEnvelope" } example: error: { code: "FORBIDDEN", detail: "Requires role: scenario.compile" } ValidationError: description: Request validation failed content: application/json: schema: { $ref: "#/components/schemas/ErrorEnvelope" } example: error: { code: "VALIDATION_ERROR", detail: "Request validation failed", fields: { objective: "goal is required" } } StaleRevisionError: description: Base revision is stale or already resolved content: application/json: schema: { $ref: "#/components/schemas/ErrorEnvelope" } example: error: { code: "STALE_REVISION", detail: "Base revision b2c3… no longer current. Recompile from latest." } StalePromptError: description: VLM prompt template is stale content: application/json: schema: { $ref: "#/components/schemas/ErrorEnvelope" } example: error: { code: "STALE_PROMPT", detail: "Prompt template v1 hash mismatch. Update to current template." } DoubleDispositionError: description: Finding already disposed content: application/json: schema: { $ref: "#/components/schemas/ErrorEnvelope" } example: error: { code: "DOUBLE_DISPOSITION", detail: "Finding f-001 already confirmed" } RateLimitError: description: Too many requests headers: Retry-After: schema: { type: integer } description: Seconds until next request is allowed content: application/json: schema: { $ref: "#/components/schemas/ErrorEnvelope" } example: error: { code: "RATE_LIMITED", detail: "Too many requests. Retry after 30 seconds.", retry_after: 30 } InternalError: description: Unexpected server error content: application/json: schema: { $ref: "#/components/schemas/ErrorEnvelope" } example: error: { code: "INTERNAL_ERROR", detail: "An unexpected error occurred" } examples: CompileRequestValid: summary: Compile filters/metric/XLSX scenario value: agent_run_id: "550e8400-e29b-41d4-a716-446655440000" objective: goal: "Verify filters, metric, XLSX export, and baseline" selected_case_ids: ["B01", "C04", "C05"] rationale: "Release candidate covers filter and export regressions" query_model: { dashboard_key: "fi-0080" } checklist_catalog_version: 1 baseline_version: "2026-07-01" capabilities: { native_filters: true, text_filter: true, table_filter: true, pagination: true, row_edit: true, bulk_edit: true, persistence_refresh: true, time_rollover: false, xlsx_export: true, cross_dashboard: false, superset_metric: true, dataset_field_read: true, screenshot: true, repository_write: true } parameters: { test_date: { type: "date" }, counterparty: { type: "string" } } ScenarioResponseCompiled: summary: Compiled 18-step scenario value: scenario: scenario_id: "fi-0080_verify-filters-metric-xlsx" revision_hash: "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3" phases: ["setup", "interact", "observe", "assert", "evidence", "report"] steps: [{ id: "phase-1-B01-open_dashboard", tool: "browser", action: "open_dashboard" }] validation: valid: true errors: [] warnings: [{ code: "STALE_BASELINE", detail: "baseline 2026-06-15 superseded by 2026-07-01", step_id: "phase-4-C05-compare_to_baseline" }] blockers: [] coverage: [{ case_id: "B01", classification: "automated" }, { case_id: "C04", classification: "automated" }, { case_id: "C05", classification: "automated" }] topological_order: ["phase-1-B01-open_dashboard", "phase-2-B01-apply_filters"] graph_hash: "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3"