docs: close scenario agentic workflow contracts

This commit is contained in:
2026-08-10 20:06:27 +03:00
parent e52c5777ba
commit 1145a1922c
105 changed files with 1452 additions and 2692 deletions

View File

@@ -33,6 +33,57 @@ FORBIDDEN_IDENTITY = [
COMPILED_OUTPUT_SCHEMA = "dashboard-test-scenario.schema.json"
COMPILED_OUTPUT_DIRS = {"fixtures"}
# Rejected architectural alternatives that have previously resurfaced in prose
# after the data model changed. These are deliberately narrow, high-signal
# phrases; a hit is a review task, not a substitute for architectural review.
REJECTED_DECISION_PATTERNS = {
"major revision analytics key": r"same major scenario revision",
"triage classification fingerprint": r"step\s*\+\s*error_code\s*\+\s*classification",
"runner plan pack source": r"RunnerPlan.*loaded from the saved scenario pack",
"materialized runner plan source": r"runner\.plan\.json.*(real materialized contract|runner consumes)",
"ordinal step identity": r"step_key\s*=.*ordinal",
"derived step uuid identity": r"logical_step_id.*derived from scenario_key\s*\+\s*step_key",
"ambiguous dependency step id": r"target_step_id",
"resolved recurrence suppressed": r"(resolved|known-issue|accepted).*not re-alerted",
"save silently activates revision": r"(?:save|saved|saving).*current_revision\s+(?:advanced|advances)",
"current pointer advances on edit": r"current_revision\s+pointer\s+advances\s+on\s+edit",
}
# Final closure invariants are presence checks for the canonical, normative
# clauses. They deliberately complement (not replace) schema/OpenAPI parsing.
REQUIRED_CONTRACT_FRAGMENTS = {
"038-dashboard-scenario-model/data-model.md": (
"AuthoringValidation", "RunPreflight", "ActionRegistry(version)", "PROD permits read-only execution only",
),
"036-agent-test-stabilization/data-model.md": (
"owner_type", "scenario_run", "load_run", "DelegatedAuthorityPolicy", "InvestigationSignal",
),
"036-agent-test-stabilization/contracts/agent-runs.openapi.yaml": (
"/action-approval-gates/{gateId}/decision", "ScenarioExecutionApprovalRequest", "/investigation-cases/{caseId}/actions", "AgentActionRequest", "DelegatedAuthorityPolicy",
),
"042-dashboard-scenario-registry/data-model.md": (
"042 does not derive health", "MVP is archive-only", "intersection of scenario permission", "Revision save and activation are separate operations", "may_activate_current_revision",
),
"043-dashboard-scenario-editor/contracts/openapi.yaml": (
"/metadata", "If-Match", "agent_action_id", "/migration-proposals/{proposal_id}/resolve",
),
"044-dashboard-scenario-execution/data-model.md": (
"pending_approval", "Idempotency uses canonical execution-request hash", "BrowserExecutor", "A PROD mutation is allowed only", "checkpoint_type", "manual_run_only=true", "InvestigationSignal", "AnalyticsContextKey",
),
"044-dashboard-scenario-execution/contracts/openapi.yaml": (
"ScenarioExecutionResult", "RunComparison", "ScenarioRunEvent", "requested_target_reference",
),
"046-dashboard-scenario-automation/data-model.md": (
"revision_id (required when pinned)", "canonical_execution_request_hash", "Concurrency bucket", "AUTOMATION_INELIGIBLE_HUMAN_STEP", "candidate", "atomic activation",
),
"047-dashboard-scenario-analytics/data-model.md": (
"InvestigationQueueItem", "InvestigationCase", "compatibility_family", "FailureEpisode", "product_health", "does not start AgentRun", "Closure policy", "AnalyticsContextKey", "InvestigationSignal",
),
"036-agent-test-stabilization/contracts/investigation-cases.md": (
"InvestigationQueueItem", "InvestigationCase", "AgentAction", "MUST NOT use a modal", "DelegatedAuthorityPolicy", "InvestigationSignal", "Process Boundary Matrix",
),
}
# Mutable vs derived: a `revision_hash`-style field present in an OpenAPI/JSON Schema path is a drift flag.
SKIP_DIRS = {"__pycache__"}
MERGEABLE_REVIEW = {".md", ".yaml", ".yml", ".json", ".html"}
@@ -163,6 +214,88 @@ def check_revision_vs_content_hash(paths: list[Path]) -> list[str]:
return out
def check_openapi_path_parameters(paths: list[Path]) -> list[str]:
"""Validate OpenAPI path-template variables against operation parameters.
YAML parsing proves only syntax. OpenAPI requires every `{name}` in a path
template to be represented by an `in: path`, `required: true` parameter on
either the path item or every operation that exposes the path.
"""
out = []
methods = {"get", "post", "put", "patch", "delete", "head", "options", "trace"}
for p in paths:
if p.name != "openapi.yaml":
continue
try:
document = yaml.safe_load(p.read_text(encoding="utf-8")) or {}
except Exception: # check_yaml reports the parse failure with detail.
continue
for path, path_item in (document.get("paths") or {}).items():
if not isinstance(path_item, dict):
continue
placeholders = set(re.findall(r"\{([^}/]+)\}", path))
shared = path_item.get("parameters") or []
for method, operation in path_item.items():
if method.lower() not in methods or not isinstance(operation, dict):
continue
declared: dict[str, dict] = {}
for parameter in [*shared, *(operation.get("parameters") or [])]:
if not isinstance(parameter, dict) or "$ref" in parameter:
continue
if parameter.get("in") == "path" and isinstance(parameter.get("name"), str):
declared[parameter["name"]] = parameter
missing = sorted(
name for name in placeholders
if name not in declared or declared[name].get("required") is not True
)
extra = sorted(name for name in declared if name not in placeholders)
op_name = operation.get("operationId", method.upper())
if missing:
out.append(
f"[OPENAPI] {p.relative_to(SPECS_DIR)}: {op_name} {path} "
f"missing required path parameter(s): {', '.join(missing)}"
)
if extra:
out.append(
f"[OPENAPI] {p.relative_to(SPECS_DIR)}: {op_name} {path} "
f"declares path parameter(s) absent from template: {', '.join(extra)}"
)
return out
def check_rejected_decision_drift(paths: list[Path]) -> list[str]:
"""Catch known superseded design phrases in normative prose/contracts."""
out = []
for p in paths:
if p.suffix not in {".md", ".yaml", ".yml", ".json"}:
continue
for line_number, line in enumerate(p.read_text(encoding="utf-8", errors="ignore").splitlines(), 1):
for decision, pattern in REJECTED_DECISION_PATTERNS.items():
if re.search(pattern, line, flags=re.IGNORECASE):
out.append(
f"[DECISION] {p.relative_to(SPECS_DIR)}:{line_number}: "
f"superseded '{decision}'"
)
return out
def check_final_closure_invariants(paths: list[Path]) -> list[str]:
"""Keep the final cross-spec execution decisions from silently eroding."""
out = []
available = {p.relative_to(SPECS_DIR).as_posix(): p for p in paths}
for relative_path, fragments in REQUIRED_CONTRACT_FRAGMENTS.items():
p = available.get(relative_path)
if p is None:
continue
content = p.read_text(encoding="utf-8", errors="ignore")
for fragment in fragments:
if fragment not in content:
out.append(
f"[INVARIANT] {relative_path}: missing final-closure clause '{fragment}'"
)
return out
def gate(spec_ids: list[str]) -> int:
total = 0
for token in spec_ids:
@@ -179,6 +312,9 @@ def gate(spec_ids: list[str]) -> int:
findings += check_json(paths)
findings += check_forbidden_identity(paths)
findings += check_revision_vs_content_hash(paths)
findings += check_openapi_path_parameters(paths)
findings += check_rejected_decision_drift(paths)
findings += check_final_closure_invariants(paths)
findings += check_fixtures_vs_schema(d.name)
if not findings:
print(" PASS — machine contracts consistent")

View File

@@ -21,6 +21,47 @@ paths:
content: { application/json: { schema: { $ref: '#/components/schemas/AgentRunSnapshot' } } }
'403': { $ref: '#/components/responses/Forbidden' }
'422': { $ref: '#/components/responses/Invalid' }
/investigation-cases/{caseId}/agent-runs:
post:
operationId: createInvestigationAgentRun
summary: Start an explicitly opened case's recoverable agent workstream
security: [{ bearerAuth: [] }]
parameters:
- name: caseId
in: path
required: true
schema: { type: string, format: uuid }
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/CreateAgentRunRequest' }
responses:
'201': { description: Created for an existing InvestigationCase }
'403': { $ref: '#/components/responses/Forbidden' }
'409': { description: Case is terminal or request idempotency conflicts }
/investigation-cases/{caseId}/actions:
post:
operationId: executeInvestigationAction
summary: Evaluate delegated authority and execute or gate one canonical AgentAction
security: [{ bearerAuth: [] }, { serviceAndUserAuth: [] }]
parameters:
- name: caseId
in: path
required: true
schema: { type: string, format: uuid }
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/AgentActionRequest' }
responses:
'202':
description: Delegated action accepted or ActionApprovalGate required
content: { application/json: { schema: { $ref: '#/components/schemas/AgentActionDecision' } } }
'403': { $ref: '#/components/responses/Forbidden' }
'409': { description: Case terminal, policy changed, or side-effect key replay conflict }
'422': { $ref: '#/components/responses/Invalid' }
/agent/runs/{runId}:
get:
operationId: getAgentRun
@@ -141,6 +182,54 @@ paths:
'200': { description: Side effect committed and gate consumed }
'403': { $ref: '#/components/responses/Forbidden' }
'409': { description: Hash, actor, status, expiry, or replay conflict }
/action-approval-gates/{gateId}:
get:
operationId: getActionApprovalGate
summary: Get a generic ActionApprovalGate subject to owner-object authorization
security: [{ bearerAuth: [] }]
parameters:
- name: gateId
in: path
required: true
schema: { type: string, format: uuid }
responses:
'200': { description: Generic approval gate, content: { application/json: { schema: { $ref: '#/components/schemas/ApprovalGateView' } } } }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { description: Not found }
/action-approval-gates/{gateId}/decision:
post:
operationId: decideActionApprovalGate
summary: Decide a generic ActionApprovalGate for any supported owner type
security: [{ bearerAuth: [] }]
parameters:
- name: gateId
in: path
required: true
schema: { type: string, format: uuid }
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/ApprovalDecision' }
responses:
'200': { description: Immutable decision recorded, content: { application/json: { schema: { $ref: '#/components/schemas/ApprovalGateView' } } } }
'409': { description: Gate already decided or expired }
/action-approval-gates/{gateId}/consume:
post:
operationId: consumeActionApprovalGate
summary: Internal one-time consume for a generic ActionApprovalGate
security: [{ serviceAndUserAuth: [] }]
parameters:
- name: gateId
in: path
required: true
schema: { type: string, format: uuid }
requestBody:
required: true
content:
application/json:
schema: { type: object, required: [operationPayload], properties: { operationPayload: { type: object } } }
responses: { '200': { description: Side effect committed and gate consumed }, '409': { description: Hash, status, expiry, or replay conflict } }
components:
securitySchemes:
bearerAuth: { type: http, scheme: bearer, bearerFormat: JWT }
@@ -164,20 +253,21 @@ components:
additionalProperties: false
required: [objectType, objectId, envId, route, contextVersion, intent]
properties:
objectType: { const: dashboard }
objectId: { type: string, pattern: '^[0-9]{1,20}$' }
objectType: { type: string, enum: [dashboard, scenario, scenario_run, load_run, queue_item] }
objectId: { type: string, minLength: 1, maxLength: 128 }
objectName: { type: [string, 'null'], maxLength: 256 }
envId: { type: string, minLength: 1, maxLength: 128 }
route: { type: string, pattern: '^/dashboards/', maxLength: 512 }
route: { type: string, pattern: '^/', maxLength: 512 }
contextVersion: { const: 2 }
intent: { const: build_dashboard_test_scenario }
intent: { type: string, enum: [build_dashboard_test_scenario, investigate_failure, investigate_staleness, revalidate_scenario, analyze_load, remediate_scenario, manage_automation] }
CreateAgentRunRequest:
type: object
required: [conversationId, context, idempotencyKey]
properties:
conversationId: { type: string, minLength: 1 }
investigationCaseId: { type: string, format: uuid, nullable: true }
context: { $ref: '#/components/schemas/UIContextV2' }
trigger: { type: string, enum: [manual, deploy_to_preprod, release_create, release_approve, release_publish, post_publish, scheduled, etl_completed], default: manual }
trigger: { type: string, enum: [manual, investigation_case, deploy_to_preprod, release_create, release_approve, release_publish, post_publish, scheduled, etl_completed], default: manual }
idempotencyKey: { type: string, minLength: 16, maxLength: 128 }
AppendEventRequest:
type: object
@@ -187,7 +277,7 @@ components:
eventType:
type: string
enum: [run_started, progress, evidence_captured, drafts_updated, approval_requested, approval_resolved, terminal]
stage: { type: string, enum: [context, inspect, scenario, parameters, generate, validate, save] }
stage: { type: string, enum: [context, inspect, scenario, parameters, generate, validate, save, evidence, hypothesis, plan, action, verify, resolve] }
status: { type: string, enum: [pending, active, completed, blocked, failed, skipped] }
payload: { type: object, additionalProperties: true }
payloadHash: { type: string, pattern: '^[a-f0-9]{64}$' }
@@ -214,6 +304,11 @@ components:
maskSelectors: { type: array, items: { type: string } }
browserVersion: { type: [string, 'null'] }
ApprovalRequest:
oneOf:
- $ref: '#/components/schemas/RepositoryApprovalRequest'
- $ref: '#/components/schemas/ScenarioExecutionApprovalRequest'
- $ref: '#/components/schemas/DelegatedActionApprovalRequest'
RepositoryApprovalRequest:
type: object
required: [operation, artifactIds, targetPaths]
properties:
@@ -221,6 +316,70 @@ components:
artifactIds: { type: array, minItems: 1, uniqueItems: true, items: { type: string, format: uuid } }
targetPaths: { type: array, minItems: 1, uniqueItems: true, items: { type: string } }
warnings: { type: array, items: { type: object } }
ScenarioExecutionApprovalRequest:
type: object
required: [operation, scenarioRunId, executionRequestHash]
properties:
operation: { const: scenario_execution }
scenarioRunId: { type: string, format: uuid }
executionRequestHash: { type: string }
targetSnapshot: { type: object }
riskSummary: { type: object }
DelegatedActionApprovalRequest:
type: object
required: [operation, ownerType, ownerId, actionRequestHash, riskClass, target]
properties:
operation: { type: string, enum: [load_execution, scenario_revision_write, activate_current_revision, automation_policy_write, controlled_test_data_mutation, prod_mutation] }
ownerType: { type: string, enum: [agent_run, scenario_run, verification_run, load_run] }
ownerId: { type: string, format: uuid }
actionRequestHash: { type: string, pattern: '^[a-f0-9]{64}$' }
riskClass: { type: string }
target: { type: object, additionalProperties: true }
preconditionEvidenceRefs: { type: array, items: { type: string, format: uuid } }
reconciliationPlan: { type: object, additionalProperties: true }
DelegatedAuthorityPolicy:
type: object
required: [policyId, version, actionClass, autonomous, cleanupRequired, approvalRequired]
properties:
policyId: { type: string, format: uuid }
version: { type: integer, minimum: 1 }
scope: { type: object, additionalProperties: true }
actionClass: { type: string }
autonomous: { type: boolean }
allowedEnvironmentClasses: { type: array, items: { type: string } }
allowedFixtureIds: { type: array, items: { type: string } }
maxRequestVolume: { type: integer, minimum: 1 }
maxConcurrency: { type: integer, minimum: 1 }
cleanupRequired: { type: boolean }
mayActivateCurrentRevision: { type: boolean }
requiredPermission: { type: [string, 'null'] }
approvalRequired: { type: boolean }
AgentActionRequest:
type: object
required: [intent, actionClass, canonicalInputs, target, expectedEffect]
properties:
intent: { type: string }
actionClass: { type: string, enum: [read, diagnostic_run, controlled_test_data_mutation, draft_write, scenario_revision_write, activate_current_revision, baseline_approval, automation_policy_write, prod_mutation] }
canonicalInputs: { type: object, additionalProperties: true }
target: { type: object, additionalProperties: true }
expectedEffect: { type: object, additionalProperties: true }
sideEffectKey: { type: [string, 'null'] }
preconditionEvidenceRefs: { type: array, items: { type: string, format: uuid } }
reconciliationPlan: { type: object, additionalProperties: true }
AgentActionDecision:
type: object
required: [agentActionId, policyDecision]
properties:
agentActionId: { type: string, format: uuid }
policyDecision: { type: string, enum: [delegated, approval_required, denied] }
policy: { $ref: '#/components/schemas/DelegatedAuthorityPolicy' }
approvalGateId: { type: [string, 'null'], format: uuid }
ApprovalDecision:
type: object
required: [decision]
properties:
decision: { type: string, enum: [confirm, deny] }
reason: { type: [string, 'null'], maxLength: 2000 }
AgentRunEvent:
allOf:
- $ref: '#/components/schemas/AppendEventRequest'
@@ -246,9 +405,11 @@ components:
captureMeta: { type: object, description: 'Present when kind=screenshot_evidence' }
ApprovalGateView:
type: object
required: [gateId, operation, status, targetPaths, reasonRequired, expiresAt]
required: [gateId, ownerType, ownerId, operation, status, targetPaths, reasonRequired, expiresAt]
properties:
gateId: { type: string, format: uuid }
ownerType: { type: string, enum: [agent_run, scenario_run, verification_run, load_run] }
ownerId: { type: string, format: uuid }
operation: { type: string }
status: { type: string, enum: [pending, confirmed, denied, consumed, expired] }
targetPaths: { type: array, items: { type: string } }

View File

@@ -0,0 +1,63 @@
#region AgentInvestigation.Cases [C:5] [TYPE ADR] [SEMANTICS agent,investigation,case,queue,policy,actions]
@BRIEF Shared agentic investigation contract consumed by 036047.
@RELATION DEPENDS_ON -> [AgentTestStabilization.DataModel]
@RATIONALE Failures, staleness and load findings need an evidence-led workstream, not a collection of isolated forms. Agent reasoning may plan and execute permitted work, while deterministic systems remain the authority for execution, validation and access control.
@REJECTED Opening an agent chat for every failure — rejected because transient and duplicate failures create noise. Events enter a queue; an analyst explicitly opens the agentic case.
@REJECTED Replacing deterministic runners, validators, schedulers or policy checks with LLM decisions — rejected because execution truth, safety and reproducibility must remain independently verifiable.
## Investigation Queue
Deterministic producers emit the idempotent `InvestigationSignal`; only 047 consumes it to create or update an `InvestigationQueueItem` from a failed/inconclusive/blocked ScenarioRun, scenario staleness signal, baseline immutability violation, load circuit-breaker/consistency finding, or repeated automation failure.
Fields: `id`, `source_type`, `source_id`, `scenario_id?`, `run_id?`, `logical_step_id?`, `severity`, `fingerprint?`, `active_episode_id?`, `evidence_summary`, `target_snapshot`, `execution_principal_fingerprint?`, `suggested_next_action`, `state`, `count`, `first_seen_at`, `last_seen_at`, `case_id?`.
State: `new | acknowledged | case_opened | suppressed | resolved`. A matching occurrence updates one queue item only inside the active failure episode; a matching occurrence after the episode is resolved creates a new queue item. Creating a queue item never starts an AgentRun.
## InvestigationCase and AgentThread
An analyst opens a queue item into one durable `InvestigationCase`. Fields: `id`, `queue_item_id`, `status`, `source_snapshot`, `evidence_snapshot`, `owner_actor_id`, `agent_thread_id`, `opened_at`, `resolved_at?`, `final_disposition?`, `resolution_summary?`.
Status: `open | investigating | awaiting_approval | awaiting_external_change | verifying | resolved | accepted | reopened`. A case owns a chat thread and may launch many 036 `AgentRun` instances; an AgentRun is a recoverable tool-execution session, never the long-lived business case itself.
`TriageRecord` is the compact audited projection of the current case disposition for Registry, Run Monitor and analytics. It never changes historical run truth.
## Delegated AgentAction
Every tool use is an `AgentAction` with `intent`, canonical inputs, risk class, policy decision, target/environment, affected entity keys, `side_effect_key?`, precondition evidence, postcondition evidence, cleanup/reconciliation plan, actor/delegator, agent and tool versions, and linked approval gate if one is required.
Risk classes:
- `read` and `diagnostic_run`: agent executes autonomously within ACL and capacity policy.
- `controlled_test_data_mutation`: agent executes autonomously only in an authorized fixture scope with a lease, exact record keys, reconciliation plan and postcondition evidence.
- `draft_write` and `scenario_revision_write`: agent may create WorkingDrafts and save immutable executable revisions after deterministic validation and delegated policy allow it.
- `activate_current_revision`, `baseline_approval`, `automation_policy_write`, and any `prod_mutation`: require the applicable ActionApprovalGate unless an explicitly stronger delegated policy permits the exact operation.
No operation bypasses object ACL, environment policy, ActionRegistry mutation contract, capacity allocation, canonical validation, immutable revision creation, or ActionApprovalGate consumption. Failed cleanup moves the case to `awaiting_external_change`; it cannot be resolved silently.
## DelegatedAuthorityPolicy
`DelegatedAuthorityPolicy` is server-owned and versioned. Fields: `policy_id`, `scope {scenario_id?, dashboard_id?, team_id?, environment_ids?}`, `action_class`, `autonomous`, `allowed_environment_classes`, `allowed_fixture_ids?`, `allowed_record_key_patterns?`, `max_request_volume?`, `max_concurrency?`, `cleanup_required`, `may_activate_current_revision`, `required_permission`, `approval_required`, `effective_from`, `effective_to?`, `version`.
Policy evaluation is deterministic and snapshots `policy_id + version + decision` on every AgentAction. `autonomous=true` never grants more authority than ACL or the action's mutation contract. `approval_required=true` always creates a payload-bound ActionApprovalGate. No client or agent may supply a policy decision.
## InvestigationSignal
Producers publish one idempotent `InvestigationSignal { source_type, source_id, scenario_id?, run_id?, logical_step_id?, severity, canonical_fingerprint?, evidence_refs, target_snapshot?, execution_principal_fingerprint?, occurred_at }` through the outbox. Its dedup key is `(source_type, source_id, canonical_fingerprint?)`; 047 maps it to the active Queue item/FailureEpisode. Producers include 037 comparison/immutability, 040 breaker/consistency, 041 impact/deprecation, 042 staleness, 044 terminal run and 046 automation attention events.
## Process Boundary Matrix
| Process class | Agent role | Deterministic owner | Approval mode |
|---|---|---|---|
| reasoning, evidence search, hypothesis, plan | leads | case/event storage | delegated read policy |
| query/compare/validate/index/health/fingerprint | consumes output | 037/038/040/041/044/046/047 engines | never agent-decided |
| diagnostic run and fixture experiment | proposes/executes | runner, capacity, mutation contract | delegated only in exact policy scope |
| revision save | may execute | 038 validation + 042 immutable revision | delegated policy |
| revision activation / automation adoption | may propose | 042/046 lifecycle and eligibility | policy or ActionApprovalGate |
| baseline publish, automation policy, PROD mutation | may propose | bound action consumer | ActionApprovalGate unless exact stronger policy |
| HumanCheckpoint and final case disposition | explains evidence | 044 checkpoint / 047 case CAS | authenticated analyst only |
## UX Invariant
Investigation work opens a persistent case workspace with chat, evidence, tool timeline and inline action cards. It MUST NOT use a modal or a confirmation dialog as the primary workflow. An ActionApprovalGate and a RunHumanCheckpoint are inline cards with typed decisions; they remain distinct domain controls.
#endregion AgentInvestigation.Cases

View File

@@ -15,7 +15,7 @@
- applyMetadata(event): validates run id and monotonic sequence before mutation.
- recover(runId): loads AgentRunSnapshot and atomically replaces projection.
- previewArtifact(id): obtains safe preview; does not mutate repository.
- requestSave(ids): asks backend/agent for a bound gate; does not write.
- requestAction(action): asks backend for delegated-action policy; it either records an autonomous AgentAction or returns a bound inline gate.
- decideGate(decision, reason): validates reason locally, then submits authoritative decision.
- reset(): clears only run projection when starting a new conversation/context.

View File

@@ -13,9 +13,9 @@
| envId | string | Required, max 128 chars |
| route | string | Must begin /dashboards/, max 512 chars |
| contextVersion | 1 or 2 | v1 ordinary chat; v2 scenario extension |
| intent | literal/null | build_dashboard_test_scenario only in v2 |
| intent | enum/null | build_dashboard_test_scenario, investigate_failure, investigate_staleness, revalidate_scenario, analyze_load, remediate_scenario, manage_automation |
Compatibility: v1 payloads without intent remain valid. A scenario intent with v1 is invalid; an unknown intent is invalid.
Compatibility: v1 payloads without intent remain valid. A typed agentic intent with v1 is invalid; an unknown intent is invalid.
## AgentRun
@@ -24,8 +24,8 @@ Compatibility: v1 payloads without intent remain valid. A scenario intent with v
| id | UUID | Stable public agent_run_id |
| conversation_id | UUID/string | Existing conversation correlation |
| user_id | string | Owner; immutable |
| intent | enum | Initial value: dashboard scenario build |
| trigger | enum | manual, deploy_to_preprod, release_create, release_approve, release_publish, post_publish, scheduled, etl_completed |
| intent | enum | Typed workstream; starts with dashboard scenario build and also supports investigation/revalidation/remediation intents |
| trigger | enum | manual, investigation_case, deploy_to_preprod, release_create, release_approve, release_publish, post_publish, scheduled, etl_completed |
| dashboard_id, environment_id | string | Immutable context correlation |
| context_snapshot | JSON | Validated UIContext v2 |
| status | enum | See FSM |
@@ -34,6 +34,10 @@ Compatibility: v1 payloads without intent remain valid. A scenario intent with v
| error_code, error_detail | nullable string | Sanitized terminal/degraded information |
| created_at, updated_at, finished_at | timestamp | UTC |
`intent` is a typed workstream: `build_dashboard_test_scenario | investigate_failure | investigate_staleness | revalidate_scenario | analyze_load | remediate_scenario | manage_automation`. An AgentRun may be linked to an InvestigationCase (047) and is one recoverable tool-execution session inside that case; it is never the case's lifecycle owner. The shared queue/case/action contract is `contracts/investigation-cases.md`.
`DelegatedAuthorityPolicy` is the server-owned versioned authority source for AgentAction evaluation; every action records its policy snapshot/decision. `InvestigationSignal` is the outbox envelope joining deterministic producers to 047 Queue. Neither an agent nor a client may decide authorization, emit a synthetic policy decision, or create a Queue item by prose.
### AgentRun FSM
~~~text
@@ -97,13 +101,14 @@ When `kind` is `screenshot_evidence`, the artifact carries a `capture_meta` bloc
Masked derivative for LLM submission is stored as a separate DraftArtifact of kind `other` with `name` containing the original artifact id and `_masked` suffix. The original unmasked capture is never transmitted externally.
## ApprovalGate
## ActionApprovalGate
| Field | Rule |
|---|---|
| id | UUID |
| run_id | Parent run |
| operation | repository_write or baseline_approval |
| owner_type | agent_run, scenario_run, verification_run, or load_run |
| owner_id | UUID of the owning run/intent |
| operation | repository_write, baseline_approval, scenario_execution, load_execution, scenario_revision_write, automation_policy_write, controlled_test_data_mutation, or prod_mutation |
| request_hash | SHA-256 of canonical operation, targets, artifact hashes, and baseline payload |
| target_paths | Normalized relative paths |
| risk_level | guarded or dangerous |
@@ -121,7 +126,7 @@ PENDING → CONFIRMED → CONSUMED
→ EXPIRED
~~~
Execution requires CONFIRMED status, matching actor, non-expired gate, unchanged request_hash, and a fresh RBAC check. Consumption is atomic with the durable side effect.
Execution requires CONFIRMED status, non-expired gate, unchanged request_hash, and a fresh object-level authorization check. Consumption is atomic with the durable side effect. A gate is rendered as an inline card in its case/chat or work surface, never as a blocking modal. `ApprovalGate` is a legacy name only; all new contracts use `ActionApprovalGate`. `/agent/runs/{runId}/approval-gates` remains an adapter route that fixes `owner_type=agent_run`.
## Snapshot DTO

View File

@@ -5,7 +5,7 @@
## Summary
Extend the existing Gradio/LangGraph agent with a durable backend-owned AgentRun lifecycle. Dashboard scenario intent is carried in UIContext v2 without changing the positional Gradio contract. Structured progress and draft metadata drive the UI, while repository writes and baseline approvals use one-shot, payload-bound HITL gates.
Extend the existing Gradio/LangGraph agent with a durable backend-owned AgentRun lifecycle for scenario and investigation workstreams. UIContext v2 carries typed intents without changing the positional Gradio contract. Structured progress and AgentAction metadata drive persistent workspaces; non-delegated repository/baseline actions use payload-bound inline gates.
## Technical Context

View File

@@ -1,5 +1,5 @@
#region AgentTestStabilization.Spec [C:3] [TYPE ADR] [SEMANTICS spec,requirements,agent,test-scenarios,stabilization]
@BRIEF Stabilize the existing Gradio/LangGraph agent runtime so it can safely support long-running dashboard test scenario generation.
@BRIEF Stabilize the durable agent runtime for scenario creation and evidence-led investigation, revalidation, and remediation workstreams.
@RELATION DEPENDS_ON -> [Doc.Adr.ADR0001]
@RELATION DEPENDS_ON -> [Doc.Adr.ADR0003]
@RELATION DEPENDS_ON -> [Doc.Adr.ADR0005]
@@ -13,7 +13,7 @@
**Feature Branch**: `036-agent-test-stabilization`
**Created**: 2026-07-07 | **Status**: Ready for Implementation
**Input**: "Stabilize the existing Gradio LangGraph agent runtime so it can support long running dashboard test scenario generation with stable dashboard UIContext, explicit scenario intent, structured progress events, draft artifact preview, recoverable agent run identifiers, and HITL confirmation for repository writes or baseline approvals."
**Input**: "Provide a durable agent runtime for dashboard scenario creation and agentic investigations: stable UI context, explicit workstream intent, structured progress/events, recoverable identifiers, tool-action provenance, and policy-bound approvals."
## User Scenarios
@@ -58,13 +58,13 @@
### Story 4 — HITL Write and Approval Gates (P2)
**Why P2**: Generated files and approved baselines affect future regression truth and must never be committed by the agent without human approval.
**Why P2**: Risky actions need payload-bound authorization, while delegated low-risk actions remain autonomous and audited.
**Independent Test**: Trigger a repository-write or baseline-approval action and verify `confirm_required` appears with target path, operation type, risks, and denial path.
**Acceptance**:
1. **Given** the agent proposes saving generated artifacts **When** save is requested **Then** a confirmation card lists target paths, file count, risk level, and warnings before the write.
2. **Given** the agent proposes approving a baseline candidate **When** approval is requested **Then** confirmation requires a user-visible reason and shows provenance.
1. **Given** the agent proposes an action requiring approval **When** it is requested **Then** an inline card lists exact targets, risk, precondition evidence, and rollback/reconciliation before consumption.
2. **Given** the agent proposes baseline approval, production mutation, or automation policy change **When** approval is requested **Then** the inline card requires a user-visible reason and shows provenance.
3. **Given** the user denies a write or approval **When** denial is submitted **Then** no artifact is written or approved, and the chat records an explicit cancellation outcome.
---
@@ -83,11 +83,15 @@
- **AGSTAB-FR-002**: Every long-running agent operation for dashboard testing MUST expose a stable `agent_run_id` correlated with conversation id, user id, dashboard context, and current progress stage.
- **AGSTAB-FR-003**: The agent runtime MUST emit structured progress metadata for scenario-oriented stages; UI MUST NOT infer these stages by parsing prose.
- **AGSTAB-FR-004**: Draft artifacts MUST be previewable before repository writes and must include type, name, intended path, validation status, warnings, and producing run id.
- **AGSTAB-FR-005**: Repository writes and baseline approvals MUST require HITL confirmation; approval must capture a user-supplied reason.
- **AGSTAB-FR-005**: Repository writes, baseline approvals, production mutations, and automation policy changes MUST obey the bound ActionApprovalGate policy; any required decision captures a user-supplied reason in an inline card.
- **AGSTAB-FR-006**: Permission-denied operations MUST emit `permission_denied` metadata and never present a confirm button for unauthorized actions.
- **AGSTAB-FR-007**: The existing Gradio/LangGraph chat, streaming, confirmation, context, and guardrail flows from 033/035 MUST remain compatible.
- **AGSTAB-FR-008**: A live smoke test MUST verify context → stream → tool event → draft artifact preview → confirmation/denial recovery.
- **AGSTAB-FR-009**: Screenshot evidence intended for external LLM/VLM analysis MUST be masked (DOM selectors from dashboard configuration) before transmission; the original unmasked capture MUST remain as a separate artifact for audit; text-based redaction alone is insufficient for image payloads.
- **AGSTAB-FR-010**: The runtime MUST support typed investigation/revalidation/remediation intents linked to the shared InvestigationCase contract. Events enter Investigation Queue and MUST NOT automatically start an agent chat or tool run.
- **AGSTAB-FR-011**: Every agent tool call MUST persist AgentAction provenance and pass deterministic ACL, environment, capacity, ActionRegistry, and approval-policy checks before execution.
- **AGSTAB-FR-012**: Delegated policy MAY authorize the agent to execute read, diagnostic, authorized fixture mutation, draft, and executable-revision writes autonomously. It MUST NOT bypass deterministic validators, immutable revision creation, or a required ActionApprovalGate.
- **AGSTAB-FR-013**: Persistent case workspaces and inline action cards are the primary agent UX; modal/dialog workflows MUST NOT be required to complete work.
### Key Entities
@@ -98,7 +102,8 @@
- **DraftArtifactRef**: Previewable reference to generated content that is not yet persisted as an approved repository artifact.
- **ScreenshotEvidence**: A `DraftArtifact` of kind `screenshot_evidence` carrying capture metadata (viewport, filters_hash, tab identifier, readiness policy, masking config), a content-addressable digest, and an opaque preview URL. Never contains raw storage paths.
- **CaptureMeta**: Structured metadata attached to screenshot artifacts: viewport dimensions, device scale factor, capture method, tab/filter context hash, readiness strategy, browser version, and applied masking selectors.
- **ApprovalGate**: HITL confirmation envelope for repository writes and baseline approvals.
- **ActionApprovalGate**: Payload-bound authorization envelope for actions that policy does not delegate; rendered inline in its owning work surface.
- **InvestigationCase / AgentAction**: Shared 036047 case and delegated-tool-action contracts defined in `contracts/investigation-cases.md`.
## Success Criteria

View File

@@ -1,18 +1,18 @@
#region AgentTestStabilization.UxReference [C:3] [TYPE ADR] [SEMANTICS ux,reference,agent,dashboard-testing]
@BRIEF UX reference for stabilized agent execution before dashboard test scenario generation.
@BRIEF UX reference for durable agent workspaces for scenario creation and investigations.
**Feature Branch**: `036-agent-test-stabilization`
**Created**: 2026-07-07 | **Status**: Ready for Implementation
## 1. User Persona & Context
* **Who is the user?**: QA engineer, dashboard owner, or analyst preparing an agent-generated dashboard test scenario.
* **What is their goal?**: Start a reliable, recoverable agent run from a dashboard and preview generated outputs before any durable write.
* **Who is the user?**: BI analyst working on scenario creation, an opened investigation, revalidation, or remediation.
* **What is their goal?**: Work in a reliable, recoverable agent thread with evidence, tool actions, and durable outputs.
* **Context**: Browser-based Svelte `/agent` workspace launched from a dashboard page with Superset environment selected.
## 2. Happy Path Narrative
The user clicks "Создать сценарий тестирования" on a dashboard. `/agent` opens with the dashboard context already visible: dashboard name, id, environment, and intent. The agent starts a recoverable run, streams structured progress stages, produces draft artifacts, and shows a confirmation card before saving anything. If the user denies, the run ends cleanly with no side effects.
The analyst starts a scenario workspace or opens an Investigation Queue item. The persistent workspace shows the source context, evidence and agent thread. The agent starts recoverable tool runs, records actions, and may save a validated revision under delegated policy. High-risk actions render a bound inline approval card; denial records a cancellation without side effects.
## 3. Interface Mockups
@@ -46,17 +46,17 @@ The user clicks "Создать сценарий тестирования" on a
└─────────────────────────────────────────────────────────────────────────────┘
```
### Confirmation Gate
### Inline ActionApprovalGate
```text
┌──────────────────────── Confirmation required ──────────────────────────────┐
Save generated artifacts
Publish baseline candidate
│ Target: tests/generated/dashboards/fi_0080/ │
Files: 3
│ Risk: repository write
Warnings: 1 baseline candidate remains draft
Evidence: comparison SR-1845, lineage snapshot
│ Risk: baseline approval
Reconciliation: revert candidate if verification fails
│ │
│ [Confirm save] [Deny]
│ [Approve] [Deny] [Open diff]
└─────────────────────────────────────────────────────────────────────────────┘
```

View File

@@ -118,6 +118,8 @@ Contains status, actual, expected, policy, diff, stale_dimensions, warnings, sou
Status enum: pass, fail, inconclusive, missing_baseline, stale_baseline, stale_visual_baseline, immutability_violation, permission_denied, source_error.
A non-pass result is deterministic evidence, not an agent conclusion. It emits an idempotent 036 InvestigationSignal with comparison, release, filter, target and principal provenance; 047 creates/updates the Queue item. The agent may inspect and explain the evidence, create a baseline candidate, or run diagnostic comparisons; normalization, comparison and immutable baseline state remain engine-owned.
`immutability_violation` is CRITICAL severity — it means `source_response_hash` changed for a closed-period entry, indicating retroactive data modification. It blocks release publication and triggers investigation.
## VisualBaseline

View File

@@ -59,7 +59,7 @@
**Why P2**: The agent may discover candidate reference values but must not silently define the truth.
**Independent Test**: Run discovery for a metric without baseline and verify a draft candidate is produced; approving it requires HITL from feature 036.
**Independent Test**: Run discovery for a metric without baseline and verify a draft candidate is produced; approving it follows the bound inline ActionApprovalGate policy from feature 036.
**Acceptance**:
1. **Given** no approved baseline exists for metric+filters **When** discovery runs **Then** a draft baseline candidate is created with provenance and source values.
@@ -87,6 +87,8 @@
- **AGBASE-FR-006**: Each baseline entry MUST record a `source_response_hash` (SHA-256 of the Superset API response at the time the expected value was captured) and `captured_at` (ISO-8601 timestamp). These enable immutability violation detection independent of metric value comparison.
- **AGBASE-FR-007**: For closed-period entries (`immutability.enabled=true`), the system MUST detect immutability violations: when `source_response_hash` changes for the same filters, the status MUST be `immutability_violation` (CRITICAL severity), NOT a stale warning. Automated baseline updates for closed-period entries are forbidden.
- **AGBASE-FR-008**: Comparison output MUST include source, actual value, expected value, diff, status, tolerance rule, and warnings. Status enum MUST include `immutability_violation` as a distinct, critical category separate from `stale_baseline` and `stale_visual_baseline`.
- **AGBASE-FR-009**: Non-pass comparison outcomes MUST emit an idempotent 036 InvestigationSignal with immutable evidence provenance; 047 owns Queue creation/update. They MUST NOT automatically begin an agent conversation, alter a baseline, or reclassify comparison truth.
- **AGBASE-FR-010**: An opened InvestigationCase MAY use baseline-engine tools for read-only inspection, diagnostic comparison and candidate creation. Any baseline publication remains governed by deterministic catalog validation and its ActionApprovalGate policy.
- **AGBASE-FR-009**: Direct SQL execution, generated SQL assertions, and SQL-based baseline provenance are explicitly out of scope.
- **AGBASE-FR-010**: The baseline catalog MUST support visual baselines for screenshot comparison, with the same release-pinning and immutability rules as metric baselines.
- **AGBASE-FR-011**: The system MUST compute a `StructureDiff` between two releases' `DashboardQueryModel` snapshots. The diff MUST classify changes by target (chart, filter, column), kind (scope_change, column_reorder, chart_removed, etc.), severity (critical, warning, info), and affected artifacts. StructureDiff is orthogonal to metric comparison.

View File

@@ -50,7 +50,7 @@ The PDF records historic outcomes such as “Успешно пройдено”,
1. Missing required capability → human_checkpoint when safe manual verification exists, otherwise unsupported.
2. Missing parameter, selector, relationship, or test data → needs_context.
3. Mutating cases B05B09/C01/C03 require explicit safe environment/test-data context.
3. Mutating cases B05B09/C01/C03 require a 038 mutation_contract: safe fixture, bounded record keys, allowed non-PROD environment, cleanup/reconciliation and a side-effect key. They are manual/needs_context without it; no mutating browser step is automated in PROD.
4. T01T03 must satisfy the 037 no-direct-SQL invariant.
5. C02 must not simulate elapsed time against production records without an approved test fixture.
6. Each mapping records selected template, rationale, and resulting step ids.

View File

@@ -8,6 +8,7 @@
"schema_version",
"compiler_version",
"template_version",
"action_registry_version",
"scenario_key",
"content_hash",
"dashboard_context",
@@ -33,6 +34,10 @@
"template_version": {
"type": "string"
},
"action_registry_version": {
"type": "string",
"description": "Version-pinned 038 ActionRegistry used to validate every {tool, action}."
},
"scenario_key": {
"type": "string",
"pattern": "^[a-z0-9][A-Za-z0-9_-]{2,127}$",
@@ -198,7 +203,6 @@
"type",
"required",
"source",
"status",
"affected_logical_step_ids"
],
"properties": {
@@ -239,14 +243,6 @@
"null"
]
},
"value": {},
"status": {
"enum": [
"unresolved",
"resolved",
"invalid"
]
},
"affected_logical_step_ids": {
"type": "array",
"uniqueItems": true,
@@ -445,10 +441,26 @@
"enum": [
"read",
"browser_interaction",
"test_data_mutation",
"external_mutation",
"dangerous_mutation",
"draft_write",
"human"
]
},
"mutation_contract": {
"type": ["object", "null"],
"additionalProperties": false,
"required": ["safe_test_fixture_id", "mutation_scope", "allowed_environment", "affected_record_keys", "cleanup_policy", "side_effect_key"],
"properties": {
"safe_test_fixture_id": { "type": "string" },
"mutation_scope": { "type": "string", "enum": ["controlled_test_data", "external_system"] },
"allowed_environment": { "type": "array", "minItems": 1, "items": { "type": "string" } },
"affected_record_keys": { "type": "array", "minItems": 1, "items": { "type": "string" } },
"cleanup_policy": { "type": "string", "enum": ["rollback", "reconcile", "none"] },
"side_effect_key": { "type": "string" }
}
},
"capture_spec": {
"oneOf": [
{

View File

@@ -55,7 +55,7 @@
# @POST Same canonical inputs/compiler version yield byte-identical graph and stable keys/order; emits scenario_key + content_hash (NO scenario_id/revision_id).
# @SIDE_EFFECT None.
# @DATA_CONTRACT CompileInput (scenario_key basis + objective + query model + checklist + parameters + capabilities + baselines) + CompileProvenance -> DashboardTestScenario (scenario_key + content_hash)
# @INVARIANT Steps consume only context/parameter/baseline/earlier-step refs; logical_step_id is stable UUID derived from scenario_key + step_key.
# @INVARIANT Steps consume only context/parameter/baseline/earlier-step refs; initial graph creation mints logical_step_id and edits/migrations carry it forward.
# @TEST_INVARIANT Deterministic_Graph -> VERIFIED_BY: repeated_compile, shuffled_input_order.
# @TEST_EDGE missing_selector -> NEEDS_SELECTOR step and save blocker.
# @TEST_EDGE missing_baseline -> NEEDS_BASELINE; no embedded numeric truth.

View File

@@ -138,7 +138,8 @@ paths:
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
baseline makes the pack preview_only. Unbound required ParameterDefinitions
are run-preflight inputs and do not block saving. Save-eligible packs are
registered as 036 drafts. Idempotent per content_hash.
security:
- BearerAuth: [scenario.draft]
@@ -182,6 +183,7 @@ paths:
operationId: captureScenarioScreenshot
tags: [scenario]
deprecated: true
parameters: [{ name: scenarioId, in: path, required: true, schema: { type: string } }]
summary: MOVED TO 044 — runtime capture is owned by ScenarioExecution
description: |
Deprecated. 038 defines ScreenshotCaptureSpec only; runtime capture,
@@ -199,6 +201,7 @@ paths:
operationId: analyzeScenarioScreenshot
tags: [scenario]
deprecated: true
parameters: [{ name: scenarioId, in: path, required: true, schema: { type: string } }]
summary: MOVED TO 044 — runtime VLM is owned by ScenarioExecution
description: |
Deprecated. 038 defines VlmAnalysisSpec only; runtime VLM submission
@@ -213,6 +216,7 @@ paths:
operationId: disposeVlmFindings
tags: [scenario]
deprecated: true
parameters: [{ name: scenarioId, in: path, required: true, schema: { type: string } }]
summary: MOVED TO 044 — human checkpoint disposition is owned by ScenarioExecution
description: |
Deprecated. 038 no longer owns human disposition; it is a 044

View File

@@ -25,7 +25,7 @@
## Stale Revision Handling
- ✅ CHOSEN: 409 reject + recompile guidance modal
- ✅ CHOSEN: 409 reject + persistent recompile guidance panel
- ❌ Rejected: Silent auto-merge — changes business intent without review
## Draft Pack States

View File

@@ -26,7 +26,7 @@ ready, needs_context, needs_selector, needs_baseline, manual, unsupported, block
| AUTH_01 | 401 | ✅ | Redirect to login; preserve intent | Login → redirect back |
| AUTH_02 | 403 | ✅ | Full-page explanation; no approval gate | Navigate to dashboard |
| NF_01 | 404 scenarioId | ✅ | Full-page not found + link to list | Navigate to scenario list |
| CONF_01 | 409 stale base revision | ✅ | Modal "Scenario changed. Recompile?" | Recompile or snapshot diff |
| CONF_01 | 409 stale base revision | ✅ | Persistent conflict panel: "Scenario changed. Recompile?" | Recompile or snapshot diff |
| CONF_02 | 409 duplicate pack | ✅ | Return existing DraftPack (idempotent) | Transparent; log event |
| 422 | Unprocessable compile/resolve | ✅ | Step/field-mapped error detail | Correct input + re-submit |
| 429 | Rate limited | ✅ | Toast + countdown | Wait Retry-After |
@@ -34,7 +34,7 @@ ready, needs_context, needs_selector, needs_baseline, manual, unsupported, block
| STALE | Stale baseline | ✅ | Assertion step warning badge | Baseline discovery via 037; mark pending |
| PARTIAL | Partial graph load | ✅ | Failed steps show placeholder | Per-step retry; reload all |
| DUP_01 | Duplicate draft-pack submit | ✅ | Button disabled + spinner | Normal completion |
| DUP_02 | Navigation interruption | ✅ | beforeunload + confirm dialog | Stay or discard |
| DUP_02 | Navigation interruption | ✅ | Persistent unsaved-work panel | Stay or discard |
| LARGE | >100 steps | ✅ | Virtualized lanes | Pagination/refinement |
| EMPTY | No applicable cases | ✅ | Empty state + guidance | Accept partial coverage / manual checkpoints |
| MALFORMED | Malformed VLM response | ✅ | Toast with error ID; step inconclusive | Re-run analysis |
@@ -49,7 +49,7 @@ ready, needs_context, needs_selector, needs_baseline, manual, unsupported, block
| Validate submitted | Validation pending; then grouped findings | Findings are deterministic; grouping per step/case aids recovery |
| Resolve changes | Affected controls pending; unrelated ids unchanged | Immutable revisions; partial progress only on affected targets |
| Draft-pack generate | generate/validate progress; then manifest | Registered 036 artifacts; preview_only/save_eligible explicit |
| 409 stale revision | Modal with recompile guidance | Never silent merge; user must recompile to see latest graph |
| 409 stale revision | Persistent panel with recompile guidance | Never silent merge; user must recompile to see latest graph |
| VLM finding disposition | Audit event + finding status update | Typed, auditable human decision |
## Recovery Paths
@@ -76,7 +76,7 @@ ready, needs_context, needs_selector, needs_baseline, manual, unsupported, block
| Technical cases never show SQL | T01T03 dashboard | Load preview | Superset API or human checkpoint shown |
| Parameter resolution updates affected steps only | Resolve one parameter | Apply change | Unrelated step ids/order unchanged; new revision hash |
| Preview-only pack explains blockers | Invalid/unresolved graph | Generate pack | All save blockers listed; preview_only state explicit |
| 409 stale revision recovery | Stale base revision | Resolve | Modal with recompile guidance; no silent merge |
| 409 stale revision recovery | Stale base revision | Resolve | Persistent panel with recompile guidance; no silent merge |
| VLM finding disposition | Unresolved finding | Confirm/dismiss/inconclusive | Typed status; audit event; graph unchanged |
#endregion DashboardScenarioModel.GraphUx

View File

@@ -29,9 +29,11 @@ Compiler input MUST NOT require an AgentRun. Provenance is passed separately:
- source_type: agent_run | editor | migration | api
- source_id: nullable
## ScenarioParameter
## ParameterDefinition
Fields: name, label, type, required, default, source, validation, value, status, affected_step_ids. Supported types: string, integer, decimal, boolean, date, datetime, enum, string_list, baseline_choice, selector_hint.
Fields: name, label, type, required, default (optional), source, validation, affected_logical_step_ids. Supported types: string, integer, decimal, boolean, date, datetime, enum, string_list, baseline_choice, selector_hint.
This immutable definition contains no resolved `value` or runtime `status`. Those belong exclusively to 044 `ParameterBinding`, so a value such as `test_date=2026-08-10` never changes a scenario `content_hash`.
## ScenarioStep — with logical identity
@@ -44,18 +46,25 @@ Fields: name, label, type, required, default, source, validation, value, status,
| phase | setup/interact/observe/assert/evidence/report |
| title/description | Bounded display text |
| tool | browser, superset_api, xlsx, assertion, screenshot, report, artifact, human |
| action | Must be allowed for tool by registry |
| action | Must be allowed by the version-pinned `ActionRegistry` for its tool; never free-form runtime dispatch |
| inputs | Typed refs only |
| outputs | Unique ScenarioRef declarations |
| expected | Structural expectation or baseline ref, never raw numeric truth |
| depends_on | Existing logical_step_ids; DAG |
| automation_status | ready, needs_context, needs_selector, needs_baseline, manual, unsupported, blocked |
| checklist_case_ids | Known catalog ids |
| risk | read, browser_interaction, draft_write, human |
| risk | read, browser_interaction, test_data_mutation, external_mutation, dangerous_mutation, draft_write, human |
| mutation_contract | Mandatory for every mutating action: safe_test_fixture_id, scope, allowed environment, affected keys, cleanup/reconciliation, side-effect key |
| capture_spec | Required when tool=screenshot; null otherwise |
| vlm_analysis_spec | Required when tool=assertion and input is screenshot; null otherwise |
`logical_step_id` is assigned deterministically by the compiler (stable UUID derivation from scenario_key + step_key) and persists across reorder/revision. Analytics (047) and comparison (045) key on it.
The compiler assigns a UUID when it creates an initial graph. An editor/migration MUST carry an existing `logical_step_id` forward for the same logical step; it MUST mint a new UUID only for a genuinely new step. `step_key` and `position` are never identity inputs. Analytics (047) and comparison (045) key on it.
## ActionRegistry and mutation safety
`ActionRegistry(version)` is the canonical 038 catalog of every allowed `{tool, action}`. Each entry declares typed inputs/outputs, allowed risk, timeout, idempotency, retry safety, and whether it mutates state. 044 BrowserExecutor resolves actions only from this pinned registry.
Mutating actions require `mutation_contract`. MVP policy: `dangerous_mutation` is never automated; PROD permits read-only execution only; `test_data_mutation` is permitted only in an explicitly listed non-PROD environment against a named safe fixture, with bounded affected record keys and rollback/reconciliation. Missing safety context maps the checklist case to `needs_context` or HumanCheckpoint, never automatic dispatch.
### ScreenshotCaptureSpec
@@ -103,15 +112,19 @@ ChecklistCase holds id, section, goal, reusable expected semantics, required/opt
CapabilityMapping holds case_id, classification, matched/missing capabilities, selected template, rationale, and resulting step keys.
## ScenarioValidationResult
## AuthoringValidation and RunPreflight
Fields: valid, errors, warnings, blockers, coverage, topological_order, unresolved_parameters, unresolved_selectors, unresolved_baselines, graph_hash. Finding contains code, severity, message, json_pointer, step_key/case_id, and recovery options.
`AuthoringValidation` determines whether an immutable ScenarioRevision/DraftPack may be saved. Fields: valid, errors, warnings, blockers, coverage, topological_order, unresolved_selectors, unresolved_baselines, parameter_definitions_valid, graph_hash. Required ParameterDefinitions without a default are valid authoring inputs and appear only as `unbound_parameter_names` warnings.
`RunPreflight` is owned by 044: it resolves ParameterBinding, target, baselines, RLS/principal and runtime policy for one launch. An unbound required parameter makes only that launch `run_ineligible`; it never changes `content_hash` or blocks saving a reusable scenario.
An agent may construct coverage, resolve missing context with the analyst, and create a WorkingDraft or executable revision under delegated policy. It never bypasses `AuthoringValidation`, `ActionRegistry(version)`, mutation contracts, canonicalization, or immutable content hashing; those are deterministic authorities.
## ArtifactPlan and DraftPack (authoring-only)
ArtifactPlan entries declare artifact_key, kind, relative_path_template, template_id, input_refs, required, and generation blockers.
DraftPack is the **authoring** output (generated files previewed before Save): contains scenario_key, content_hash, template version, manifest, authoring DraftArtifact refs, validation summary, and warnings. Status preview_only or save_eligible. Any validation error, NEEDS_SELECTOR, forbidden action, or unresolved required parameter makes it preview_only.
DraftPack is the **authoring** output (generated files previewed before Save): contains scenario_key, content_hash, template version, manifest, authoring DraftArtifact refs, validation summary, and warnings. Status preview_only or save_eligible. Validation error, NEEDS_SELECTOR, forbidden action, or unresolved baseline makes it preview_only; unbound required runtime parameters do not.
**Runtime evidence is NOT a DraftPack.** During execution (044), screenshots/report/xlsx are `Artifact(owner_type=scenario_run, ...)`, never authoring DraftArtifacts (reconciliation step 7). DraftPack is consumed by 042 CreateScenario to register the revision.
@@ -119,9 +132,9 @@ DraftPack is the **authoring** output (generated files previewed before Save): c
- **scenario_key** = dashboard key + normalized objective slug (semantic; may repeat across clones);
- **content_hash** = SHA-256 of canonical executable graph (steps/params/refs/expected), timestamps and display-only excluded;
- **logical_step_id** = stable UUID derived from scenario_key + step_key; immutable;
- **logical_step_id** = immutable UUID, minted on initial graph creation and carried forward by edits/migrations;
- **step_content_hash** = SHA-256 of a step's executable content;
- **step_key** = phase + case id + action slug + ordinal (readable, NOT an identity).
- **step_key** = phase + case id + action slug (readable, NOT an identity; no ordinal).
`scenario_id` (UUID) and `revision_id` (UUID) are assigned by 042 at persistence, never by the compiler.

View File

@@ -2,6 +2,7 @@
"schema_version": 1,
"compiler_version": "038.1.0",
"template_version": "v1",
"action_registry_version": "v1",
"dashboard_context": {
"environment_id": "env-test",
"dashboard_id": 80,
@@ -106,4 +107,4 @@
"risk_summary": {},
"scenario_key": "fi-0080_cycle_fixture",
"content_hash": "0f3213b7af7ad4fc0b403bc741f1ac44d19b86bea783f22c432458254661e323"
}
}

View File

@@ -2,6 +2,7 @@
"schema_version": 1,
"compiler_version": "038.1.0",
"template_version": "v1",
"action_registry_version": "v1",
"dashboard_context": {
"environment_id": "env-prod-01",
"dashboard_id": 80,
@@ -32,8 +33,6 @@
"required": true,
"source": "user",
"validation": null,
"value": "2026-07-01",
"status": "resolved",
"affected_logical_step_ids": [
"b4219430-b9b7-a57d-ead9-20c05061e61d"
]
@@ -45,8 +44,6 @@
"required": true,
"source": "user",
"validation": null,
"value": "ACME Corp",
"status": "resolved",
"affected_logical_step_ids": [
"b4219430-b9b7-a57d-ead9-20c05061e61d"
]
@@ -475,4 +472,4 @@
},
"scenario_key": "fi-0080_duplicate_output",
"content_hash": "a0871c087584b0263a8ffa002543db4cc872dfb48426d9fd370c55f63fa35b0a"
}
}

View File

@@ -2,6 +2,7 @@
"schema_version": 1,
"compiler_version": "038.1.0",
"template_version": "v1",
"action_registry_version": "v1",
"dashboard_context": {
"environment_id": "env-prod-01",
"dashboard_id": 80,
@@ -32,8 +33,6 @@
"required": true,
"source": "user",
"validation": null,
"value": "2026-07-01",
"status": "resolved",
"affected_logical_step_ids": [
"a2c76cb9-3305-8d42-d189-933ea909fb71"
]
@@ -45,8 +44,6 @@
"required": true,
"source": "user",
"validation": null,
"value": "ACME Corp",
"status": "resolved",
"affected_logical_step_ids": [
"a2c76cb9-3305-8d42-d189-933ea909fb71"
]
@@ -476,4 +473,4 @@
},
"scenario_key": "fi-0080_missing_ref",
"content_hash": "2a9ee48b1e14f9175573d13ca05f10d634e6ffd7f21b39c37f284fa652b76d4e"
}
}

View File

@@ -2,6 +2,7 @@
"schema_version": 1,
"compiler_version": "038.1.0",
"template_version": "v1",
"action_registry_version": "v1",
"dashboard_context": {
"environment_id": "env-prod-01",
"dashboard_id": 80,
@@ -32,8 +33,6 @@
"required": true,
"source": "user",
"validation": null,
"value": "2026-07-01",
"status": "resolved",
"affected_logical_step_ids": [
"d57c118b-2cec-78fd-521b-40f417b9cb9f"
]
@@ -45,8 +44,6 @@
"required": true,
"source": "user",
"validation": null,
"value": "ACME Corp",
"status": "resolved",
"affected_logical_step_ids": [
"d57c118b-2cec-78fd-521b-40f417b9cb9f"
]
@@ -450,4 +447,4 @@
},
"scenario_key": "fi-0080_raw_baseline",
"content_hash": "c5c4184173556d836aa86963f8093584d7771e664cb83eb1f6762bacc1cd5ee5"
}
}

View File

@@ -2,6 +2,7 @@
"schema_version": 1,
"compiler_version": "038.1.0",
"template_version": "v1",
"action_registry_version": "v1",
"dashboard_context": {
"environment_id": "env-prod-01",
"dashboard_id": 80,
@@ -32,8 +33,6 @@
"required": true,
"source": "user",
"validation": null,
"value": "2026-07-01",
"status": "resolved",
"affected_logical_step_ids": [
"79512b6c-4f55-95aa-c266-42029595a544"
]
@@ -45,8 +44,6 @@
"required": true,
"source": "user",
"validation": null,
"value": "ACME Corp",
"status": "resolved",
"affected_logical_step_ids": [
"79512b6c-4f55-95aa-c266-42029595a544"
]
@@ -470,4 +467,4 @@
},
"scenario_key": "fi-0080_sql_injection",
"content_hash": "b03813705ae494c58256a03c9a0d7f38fadb7e49df14c40ff63917b7d8720eb9"
}
}

View File

@@ -2,6 +2,7 @@
"schema_version": 1,
"compiler_version": "038.1.0",
"template_version": "v1",
"action_registry_version": "v1",
"dashboard_context": {
"environment_id": "env-prod-01",
"dashboard_id": 80,
@@ -32,8 +33,6 @@
"required": true,
"source": "user",
"validation": null,
"value": "2026-07-01",
"status": "resolved",
"affected_logical_step_ids": [
"13dbf34f-0257-3a27-6da0-91da450a368f"
]
@@ -45,8 +44,6 @@
"required": true,
"source": "user",
"validation": null,
"value": "ACME Corp",
"status": "resolved",
"affected_logical_step_ids": [
"13dbf34f-0257-3a27-6da0-91da450a368f"
]
@@ -448,4 +445,4 @@
},
"scenario_key": "fi-0080_verify-filters-metric-xlsx",
"content_hash": "2edafc0ddad7a93bdaa93f3eb670317b90b55cd9f6af8bbfe2edaf5eed75ed27"
}
}

View File

@@ -1,687 +1 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>038 — Dashboard Scenario Preview (Interactive Prototype)</title>
<style>
/* ═══════════════════════════════════════════════════════════════════
Design System Alignment — Tailwind utility shim
Colors/radius/shadow/spacing: ONLY from frontend/tailwind.config.js
Class recipes: verbatim from frontend/src/lib/ui/*.svelte
═══════════════════════════════════════════════════════════════════ */
/* ── Semantic action palette (tailwind.config.js colors.*) ── */
.bg-primary { background-color: #2563eb; }
.bg-primary-hover { background-color: #1d4ed8; }
.bg-primary-ring { background-color: #3b82f6; }
.bg-primary-light { background-color: #eff6ff; }
.text-primary { color: #2563eb; }
.ring-primary-ring { --tw-ring-color: #3b82f6; }
.hover\:bg-primary-hover:hover { background-color: #1d4ed8; }
.focus-visible\:ring-primary-ring:focus-visible { --tw-ring-color: #3b82f6; }
.bg-secondary { background-color: #f3f4f6; }
.bg-secondary-hover { background-color: #e5e7eb; }
.text-secondary-text { color: #111827; }
.hover\:bg-secondary-hover:hover { background-color: #e5e7eb; }
.bg-destructive { background-color: #dc2626; }
.bg-destructive-hover { background-color: #b91c1c; }
.bg-destructive-light { background-color: #fef2f2; }
.text-destructive { color: #dc2626; }
.border-destructive { border-color: #dc2626; }
.hover\:bg-destructive-hover:hover { background-color: #b91c1c; }
.bg-success { background-color: #22c55e; }
.bg-success-light { background-color: #f0fdf4; }
.text-success { color: #22c55e; }
.bg-warning { background-color: #f59e0b; }
.bg-warning-light { background-color: #fffbeb; }
.text-warning { color: #f59e0b; }
.border-warning { border-color: #f59e0b; }
.bg-info { background-color: #0ea5e9; }
.bg-info-light { background-color: #f0f9ff; }
.text-info { color: #0ea5e9; }
.bg-transparent { background-color: transparent; }
.bg-ghost-hover { background-color: #f3f4f6; }
.text-ghost-text { color: #374151; }
.hover\:bg-ghost-hover:hover { background-color: #f3f4f6; }
/* ── Surface hierarchy (colors.surface.*) ── */
.bg-surface-page { background-color: #f8fafc; }
.bg-surface-card { background-color: #ffffff; }
.bg-surface-muted { background-color: #f1f5f9; }
.bg-surface-overlay { background-color: rgba(15, 23, 42, 0.5); }
/* ── Border hierarchy (colors.border.*) ── */
.border-border { border-color: #e2e8f0; }
.border-border-strong { border-color: #cbd5e1; }
/* ── Text hierarchy (colors.text.*) ── */
.text-text { color: #0f172a; }
.text-text-muted { color: #64748b; }
.text-text-subtle { color: #94a3b8; }
.text-text-inverse { color: #ffffff; }
/* ── Base resets ── */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; background-color: #f8fafc; color: #0f172a; }
/* ── Tailwind utilities used by components (shim) ── */
.inline-flex { display: inline-flex; }
.flex { display: flex; }
.flex-col { flex-direction: column; }
.flex-wrap { flex-wrap: wrap; }
.items-center { align-items: center; }
.items-start { align-items: flex-start; }
.justify-center { justify-content: center; }
.justify-between { justify-content: space-between; }
.w-full { width: 100%; }
.w-16 { width: 4rem; }
.w-4 { width: 1rem; }
.h-16 { height: 4rem; }
.h-10 { height: 2.5rem; }
.h-8 { height: 2rem; }
.h-4 { height: 1rem; }
.h-24 { height: 6rem; }
.rounded { border-radius: 0.25rem; }
.rounded-md { border-radius: 0.375rem; }
.rounded-lg { border-radius: 0.5rem; }
.rounded-full { border-radius: 9999px; }
.border { border-width: 1px; }
.border-b { border-bottom-width: 1px; }
.border-t { border-top-width: 1px; }
.shadow-sm { box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); }
.px-2 { padding-left: 0.5rem; padding-right: 0.5rem; }
.px-2\.5 { padding-left: 0.625rem; padding-right: 0.625rem; }
.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
.px-4 { padding-left: 1rem; padding-right: 1rem; }
.p-3 { padding: 0.75rem; }
.p-4 { padding: 1rem; }
.p-6 { padding: 1.5rem; }
.py-0\.5 { padding-top: 0.125rem; padding-bottom: 0.125rem; }
.py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; }
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
.py-12 { padding-top: 3rem; padding-bottom: 3rem; }
.mb-1 { margin-bottom: 0.25rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-4 { margin-bottom: 1rem; }
.mb-8 { margin-bottom: 2rem; }
.mt-1 { margin-top: 0.25rem; }
.mt-4 { margin-top: 1rem; }
.mt-6 { margin-top: 1.5rem; }
.mt-8 { margin-top: 2rem; }
.gap-1\.5 { gap: 0.375rem; }
.gap-4 { gap: 1rem; }
.space-y-1 > * + * { margin-top: 0.25rem; }
.space-y-1\.5 > * + * { margin-top: 0.375rem; }
.space-y-3 > * + * { margin-top: 0.75rem; }
.text-xs { font-size: 0.75rem; line-height: 1rem; }
.text-sm { font-size: 0.875rem; line-height: 1.25rem; }
.text-lg { font-size: 1.125rem; line-height: 1.75rem; }
.text-3xl { font-size: 1.875rem; line-height: 2.25rem; }
.text-white { color: #ffffff; }
.font-medium { font-weight: 500; }
.font-semibold { font-weight: 600; }
.font-bold { font-weight: 700; }
.tracking-tight { letter-spacing: -0.025em; }
.leading-none { line-height: 1; }
.text-center { text-align: center; }
.max-w-md { max-width: 28rem; }
.transition-colors { transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
.disabled\:pointer-events-none:disabled { pointer-events: none; }
.disabled\:opacity-50:disabled { opacity: 0.5; }
.disabled\:cursor-not-allowed:disabled { cursor: not-allowed; }
.focus-visible\:outline-none:focus-visible { outline: 2px solid transparent; outline-offset: 2px; }
.focus-visible\:ring-2:focus-visible { --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); }
.focus-visible\:ring-offset-2:focus-visible { --tw-ring-offset-width: 2px; --tw-ring-offset-color: #ffffff; }
.focus-visible\:ring-primary-ring:focus-visible { --tw-ring-color: #3b82f6; }
.focus-visible\:ring-secondary-ring:focus-visible { --tw-ring-color: #6b7280; }
.focus-visible\:ring-destructive-ring:focus-visible { --tw-ring-color: #ef4444; }
.focus-visible\:ring-ghost-ring:focus-visible { --tw-ring-color: #6b7280; }
.ring-offset-white { --tw-ring-offset-color: #ffffff; }
.animate-pulse { animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
.animate-spin { animation: spin 1s linear infinite; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
@keyframes spin { to { transform: rotate(360deg); } }
.placeholder\:text-text-subtle::placeholder { color: #94a3b8; }
.text-current { color: currentColor; }
.-ml-1 { margin-left: -0.25rem; }
.mr-2 { margin-right: 0.5rem; }
.opacity-25 { opacity: 0.25; }
.opacity-75 { opacity: 0.75; }
.overflow-x-auto { overflow-x: auto; }
.overflow-y-auto { overflow-y: auto; }
.flex-1 { flex: 1 1 0%; }
.min-w-\[150px\] { min-width: 150px; }
.relative { position: relative; }
/* ── Prototype chrome (NOT app UI — switcher only) ── */
.proto-bar { position: sticky; top: 0; z-index: 50; background: #ffffff; border-bottom: 1px solid #e2e8f0; padding: 10px 16px; display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.proto-bar h1 { font-size: 14px; font-weight: 600; margin-right: auto; }
.proto-bar label { font-size: 12px; color: #64748b; display: inline-flex; align-items: center; gap: 6px; }
.proto-bar select, .proto-bar button { font: inherit; font-size: 13px; padding: 6px 10px; border: 1px solid #e2e8f0; border-radius: 6px; background: #ffffff; cursor: pointer; min-height: 36px; }
.proto-bar select:focus-visible, .proto-bar button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
.stage { max-width: 1080px; margin: 24px auto; padding: 0 16px 48px; }
.hidden { display: none !important; }
.lane-node { font-size: 12px; }
[data-viewport="mobile"] .lanes-wrap { flex-direction: column; }
@media (prefers-reduced-motion: reduce) {
.animate-pulse, .animate-spin { animation: none !important; }
.transition-colors { transition-duration: 0.01ms !important; }
}
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th { text-align: left; padding: 8px 10px; border-bottom: 1px solid #e2e8f0; font-size: 11px; text-transform: uppercase; letter-spacing: 0.03em; color: #64748b; }
td { text-align: left; padding: 8px 10px; border-bottom: 1px solid #e2e8f0; }
code { font-family: "JetBrains Mono", "Fira Code", monospace; font-size: 0.75rem; background: #f1f5f9; padding: 1px 4px; border-radius: 4px; }
</style>
</head>
<body>
<div class="proto-bar" role="toolbar" aria-label="Prototype state switcher">
<h1>038 — Dashboard Scenario Preview</h1>
<label>Screen
<select id="screen" aria-label="Select screen">
<option value="scenario">Scenario Preview</option>
</select>
</label>
<label>State
<select id="state" aria-label="Select state">
<option value="idle">idle (no scenario)</option>
<option value="loading">loading (compiling…)</option>
<option value="loaded">loaded (18 steps)</option>
<option value="empty">empty (no applicable cases)</option>
<option value="error">error (server 5xx)</option>
<option value="blocked">blocked (NEEDS_SELECTOR)</option>
<option value="stale409">stale revision (409)</option>
<option value="preview_only">draft pack preview_only</option>
<option value="save_eligible">draft pack save_eligible</option>
<option value="vlm">VLM findings + disposition</option>
<option value="net">network (offline/timeout/retry)</option>
<option value="val">validation (422 fields)</option>
<option value="auth">auth (401/403)</option>
<option value="notfound">not found (404)</option>
<option value="ratelimit">rate limited (429)</option>
<option value="duplicate">duplicate submit (DUP)</option>
<option value="large">large graph (100+ steps)</option>
<option value="malformed">malformed VLM response</option>
</select>
</label>
<label>Viewport
<select id="viewport" aria-label="Select viewport">
<option value="desktop">Desktop 1280px</option>
<option value="mobile">Mobile 375px</option>
</select>
</label>
</div>
<main class="stage" id="stage" data-viewport="desktop">
<!-- idle — EmptyState recipe: flex flex-col items-center justify-center py-12 px-4 text-center -->
<section id="state-idle" class="scenario-state">
<div class="flex flex-col items-center justify-center py-12 px-4 text-center rounded-lg border border-border bg-surface-card text-text shadow-sm">
<svg class="w-16 h-16 text-text-subtle mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<h3 class="text-lg font-semibold text-text mb-1">No scenario yet</h3>
<p class="text-sm text-text-muted max-w-md">Compile a scenario from a dashboard goal to preview its test flow.</p>
<div class="mt-6 inline-flex gap-4">
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-primary text-white hover:bg-primary-hover focus-visible:ring-primary-ring h-10 px-4 py-2 text-sm">Compile from dashboard goal</button>
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-secondary text-secondary-text hover:bg-secondary-hover focus-visible:ring-secondary-ring h-10 px-4 py-2 text-sm">Open checklist coverage</button>
</div>
</div>
</section>
<!-- loading — Skeleton recipe: animate-pulse bg-surface-muted rounded -->
<section id="state-loading" class="scenario-state hidden">
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6" role="status" aria-live="polite">
<div class="flex items-center justify-between mb-8">
<div class="space-y-1">
<h1 class="text-3xl font-bold tracking-tight text-text">Scenario Preview</h1>
</div>
</div>
<p class="text-sm text-text-muted mb-4">Compiling scenario from dashboard goal, checklist catalog v1, and baseline version 2026-07-01…</p>
<div class="animate-pulse bg-surface-muted rounded w-full h-4 mb-4"></div>
<div class="animate-pulse bg-surface-muted rounded-lg w-full h-24 mb-4"></div>
<div class="animate-pulse bg-surface-muted rounded-lg w-full h-24"></div>
</div>
</section>
<!-- loaded — Card recipe + PageHeader recipe + Badge recipe + table -->
<section id="state-loaded" class="scenario-state hidden">
<div class="flex items-center justify-between mb-8">
<div class="space-y-1">
<h1 class="text-3xl font-bold tracking-tight text-text">Scenario Preview</h1>
<p class="text-sm text-text-muted">Проверка фильтров, метрики и XLSX выгрузки</p>
</div>
</div>
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6 mb-4" role="status" aria-live="polite">
<div class="flex items-center gap-4 flex-wrap">
<span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-primary-light text-primary">18 steps</span></span>
<span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-info-light text-info">browser · superset_api · xlsx</span></span>
<span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-warning-light text-warning">2 warnings</span></span>
<span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-surface-muted text-text-muted">revision a1b2c3d4…</span></span>
</div>
</div>
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6 mb-4">
<h3 class="text-lg font-semibold leading-none tracking-tight mb-4">Phase graph</h3>
<div class="flex gap-4 overflow-x-auto lanes-wrap">
<div class="flex-1 min-w-[150px]">
<div class="text-xs font-semibold text-text-muted mb-2">setup</div>
<div class="rounded-md border border-border bg-surface-muted p-3 mb-2 lane-node">open_dashboard<br><span class="text-xs text-info font-medium">browser</span><br><span class="text-xs text-text-muted">ready</span></div>
</div>
<div class="flex-1 min-w-[150px]">
<div class="text-xs font-semibold text-text-muted mb-2">interact</div>
<div class="rounded-md border border-border bg-surface-muted p-3 mb-2 lane-node">apply_filters<br><span class="text-xs text-info font-medium">browser</span><br><span class="text-xs text-text-muted">ready</span></div>
<div class="rounded-md border border-border bg-surface-muted p-3 mb-2 lane-node">execute_metric<br><span class="text-xs text-info font-medium">superset_api</span><br><span class="text-xs text-text-muted">ready</span></div>
</div>
<div class="flex-1 min-w-[150px]">
<div class="text-xs font-semibold text-text-muted mb-2">observe</div>
<div class="rounded-md border border-border bg-surface-muted p-3 mb-2 lane-node">parse_xlsx_metric<br><span class="text-xs text-info font-medium">xlsx</span><br><span class="text-xs text-text-muted">ready</span></div>
</div>
<div class="flex-1 min-w-[150px]">
<div class="text-xs font-semibold text-text-muted mb-2">assert</div>
<div class="rounded-md border border-border bg-surface-muted p-3 mb-2 lane-node">compare_to_baseline<br><span class="text-xs text-info font-medium">assertion</span><br><span class="text-xs text-text-muted">ready</span></div>
</div>
<div class="flex-1 min-w-[150px]">
<div class="text-xs font-semibold text-text-muted mb-2">report</div>
<div class="rounded-md border border-border bg-surface-muted p-3 mb-2 lane-node">generate_report<br><span class="text-xs text-info font-medium">report</span><br><span class="text-xs text-text-muted">ready</span></div>
</div>
</div>
</div>
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6">
<h3 class="text-lg font-semibold leading-none tracking-tight mb-4">Step table (accessible fallback)</h3>
<table>
<thead><tr><th>#</th><th>Step</th><th>Tool</th><th>Expected result</th><th>Status</th></tr></thead>
<tbody>
<tr><td>1</td><td>Открыть дашборд</td><td>browser</td><td>dashboard_loaded</td><td><span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-success-light text-success">ready</span></span></td></tr>
<tr><td>2</td><td>Применить фильтры</td><td>browser</td><td>filter_state.normalized</td><td><span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-success-light text-success">ready</span></span></td></tr>
<tr><td>3</td><td>Выполнить chart query</td><td>superset_api</td><td>metric value returned</td><td><span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-success-light text-success">ready</span></span></td></tr>
<tr><td>4</td><td>Скачать XLSX</td><td>browser</td><td>xlsx.file</td><td><span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-success-light text-success">ready</span></span></td></tr>
<tr><td>5</td><td>Сравнить с baseline</td><td>assertion</td><td>pass/fail/inconclusive</td><td><span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-success-light text-success">ready</span></span></td></tr>
</tbody>
</table>
</div>
</section>
<!-- empty — EmptyState recipe -->
<section id="state-empty" class="scenario-state hidden">
<div class="flex flex-col items-center justify-center py-12 px-4 text-center rounded-lg border border-border bg-surface-card text-text shadow-sm">
<svg class="w-16 h-16 text-text-subtle mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
</svg>
<h3 class="text-lg font-semibold text-text mb-1">Coverage is empty</h3>
<p class="text-sm text-text-muted max-w-md">No checklist cases are applicable for this dashboard's capabilities. You can accept partial coverage or add manual checkpoint instructions.</p>
<div class="mt-6 inline-flex gap-4">
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-primary text-white hover:bg-primary-hover focus-visible:ring-primary-ring h-10 px-4 py-2 text-sm">Add manual checkpoint</button>
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-secondary text-secondary-text hover:bg-secondary-hover focus-visible:ring-secondary-ring h-10 px-4 py-2 text-sm">Accept partial coverage</button>
</div>
</div>
</section>
<!-- error — destructive alert + retry -->
<section id="state-error" class="scenario-state hidden">
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6" role="alert" aria-live="assertive">
<div class="flex items-start gap-4">
<span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-destructive-light text-destructive">ERROR</span></span>
<div>
<p class="text-sm text-text">Something went wrong compiling the scenario. Our team has been notified.</p>
<p class="text-xs text-text-muted mt-1">Error ID: 7f3a-22d1</p>
</div>
</div>
<div class="mt-6 inline-flex gap-4">
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-primary text-white hover:bg-primary-hover focus-visible:ring-primary-ring h-10 px-4 py-2 text-sm">Try again</button>
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-secondary text-secondary-text hover:bg-secondary-hover focus-visible:ring-secondary-ring h-10 px-4 py-2 text-sm">Contact support</button>
</div>
</div>
</section>
<!-- blocked — destructive badges + recovery buttons -->
<section id="state-blocked" class="scenario-state hidden">
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6 mb-4" role="alert" aria-live="assertive">
<div class="flex items-start gap-4">
<span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-destructive-light text-destructive">2 BLOCKERS</span></span>
<p class="text-sm text-text">These blockers prevent saving this scenario as an executable draft.</p>
</div>
</div>
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6 mb-4">
<h3 class="text-lg font-semibold leading-none tracking-tight mb-4">Blockers</h3>
<table>
<thead><tr><th>Step</th><th>Issue</th><th>Recovery</th></tr></thead>
<tbody>
<tr>
<td>apply_filters</td>
<td><span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-warning-light text-warning">NEEDS_SELECTOR</span></span> filter input selector unknown</td>
<td>
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-secondary text-secondary-text hover:bg-secondary-hover focus-visible:ring-secondary-ring h-8 px-3 text-xs">Provide hint</button>
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-secondary text-secondary-text hover:bg-secondary-hover focus-visible:ring-secondary-ring h-8 px-3 text-xs">Convert to checkpoint</button>
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-transparent text-ghost-text hover:bg-ghost-hover focus-visible:ring-ghost-ring h-8 px-3 text-xs">Remove step</button>
</td>
</tr>
<tr>
<td>compare_to_baseline</td>
<td><span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-warning-light text-warning">NEEDS_BASELINE</span></span> baseline stale (2026-06-15)</td>
<td>
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-secondary text-secondary-text hover:bg-secondary-hover focus-visible:ring-secondary-ring h-8 px-3 text-xs">Run 037 discovery</button>
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-secondary text-secondary-text hover:bg-secondary-hover focus-visible:ring-secondary-ring h-8 px-3 text-xs">Mark pending</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6">
<h3 class="text-lg font-semibold leading-none tracking-tight mb-4">Coverage (19 cases)</h3>
<table>
<thead><tr><th>Case</th><th>Classification</th><th>Rationale</th></tr></thead>
<tbody>
<tr><td>B01</td><td><span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-success-light text-success">automated</span></span></td><td>browser + superset_api + assertion</td></tr>
<tr><td>C04C06</td><td><span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-surface-muted text-text-muted">unsupported</span></span></td><td>xlsx_export capability absent</td></tr>
<tr><td>T01T03</td><td><span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-info-light text-info">human_checkpoint</span></span></td><td>no dataset fields exposed; no SQL</td></tr>
</tbody>
</table>
</div>
</section>
<!-- stale409 -->
<section id="state-stale409" class="scenario-state hidden">
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6" role="alert" aria-live="assertive">
<div class="flex items-start gap-4">
<span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-warning-light text-warning">409 STALE REVISION</span></span>
<div>
<p class="text-sm text-text">Scenario changed since your base revision <code>b2c3…</code>. Current revision: <code>d4e5…</code>. Stale edits are rejected — never auto-merged.</p>
</div>
</div>
<div class="mt-6 inline-flex gap-4">
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-primary text-white hover:bg-primary-hover focus-visible:ring-primary-ring h-10 px-4 py-2 text-sm">Recompile from latest</button>
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-secondary text-secondary-text hover:bg-secondary-hover focus-visible:ring-secondary-ring h-10 px-4 py-2 text-sm">Snapshot diff</button>
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-transparent text-ghost-text hover:bg-ghost-hover focus-visible:ring-ghost-ring h-10 px-4 py-2 text-sm">Discard my changes</button>
</div>
</div>
</section>
<!-- preview_only -->
<section id="state-preview_only" class="scenario-state hidden">
<div class="flex items-center justify-between mb-8">
<div class="space-y-1">
<h1 class="text-3xl font-bold tracking-tight text-text">Draft Pack</h1>
<p class="text-sm text-text-muted">Preview mode — resolve blockers to become save_eligible</p>
</div>
</div>
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6 mb-4" role="status" aria-live="polite">
<div class="flex items-center gap-4">
<span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-warning-light text-warning">preview_only</span></span>
<p class="text-sm text-text">Generated via versioned templates. Save is disabled until all blockers resolve.</p>
</div>
</div>
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6">
<h3 class="text-lg font-semibold leading-none tracking-tight mb-4">Manifest</h3>
<table>
<thead><tr><th>Artifact</th><th>Template</th><th>Status</th></tr></thead>
<tbody>
<tr><td>scenario.yaml</td><td>v1</td><td><span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-success-light text-success">ready</span></span></td></tr>
<tr><td>runner.plan.json</td><td>v1</td><td><span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-success-light text-success">ready</span></span></td></tr>
<tr><td>report_template.md</td><td>v1</td><td><span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-success-light text-success">ready</span></span></td></tr>
<tr><td>browser_steps.ts</td><td>v1</td><td><span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-destructive-light text-destructive">blocked: NEEDS_SELECTOR</span></span></td></tr>
</tbody>
</table>
<div class="mt-6 inline-flex gap-4">
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-primary text-white hover:bg-primary-hover focus-visible:ring-primary-ring h-10 px-4 py-2 text-sm" disabled>Save draft</button>
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-secondary text-secondary-text hover:bg-secondary-hover focus-visible:ring-secondary-ring h-10 px-4 py-2 text-sm">Review blockers</button>
</div>
</div>
</section>
<!-- save_eligible -->
<section id="state-save_eligible" class="scenario-state hidden">
<div class="flex items-center justify-between mb-8">
<div class="space-y-1">
<h1 class="text-3xl font-bold tracking-tight text-text">Draft Pack</h1>
<p class="text-sm text-text-muted">Save-eligible — registered via 036 AgentRuns.Artifacts.Register</p>
</div>
</div>
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6 mb-4" role="status" aria-live="polite">
<div class="flex items-center gap-4">
<span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-success-light text-success">save_eligible</span></span>
<p class="text-sm text-text">CONF_02: re-posting the same content_hash returns this existing DraftPack (idempotent).</p>
</div>
</div>
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6">
<h3 class="text-lg font-semibold leading-none tracking-tight mb-4">Manifest</h3>
<table>
<thead><tr><th>Artifact</th><th>Template</th><th>Status</th></tr></thead>
<tbody>
<tr><td>scenario.yaml</td><td>v1</td><td><span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-success-light text-success">ready</span></span></td></tr>
<tr><td>runner.plan.json</td><td>v1</td><td><span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-success-light text-success">ready</span></span></td></tr>
<tr><td>report_template.md</td><td>v1</td><td><span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-success-light text-success">ready</span></span></td></tr>
<tr><td>evidence_manifest.json</td><td>v1</td><td><span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-success-light text-success">ready</span></span></td></tr>
</tbody>
</table>
<div class="mt-6 inline-flex gap-4">
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-primary text-white hover:bg-primary-hover focus-visible:ring-primary-ring h-10 px-4 py-2 text-sm">Save draft</button>
</div>
</div>
</section>
<!-- vlm -->
<section id="state-vlm" class="scenario-state hidden">
<div class="flex items-center justify-between mb-8">
<div class="space-y-1">
<h1 class="text-3xl font-bold tracking-tight text-text">VLM Analysis</h1>
<p class="text-sm text-text-muted">Typed findings — advisory, never alter metric baseline truth</p>
</div>
</div>
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6 mb-4">
<h3 class="text-lg font-semibold leading-none tracking-tight mb-4">Findings</h3>
<table>
<thead><tr><th>Finding</th><th>Severity</th><th>Code</th><th>Confidence</th><th>Region</th><th>Disposition</th></tr></thead>
<tbody>
<tr>
<td>f-001</td>
<td><span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-warning-light text-warning">warning</span></span></td>
<td>TRUNCATED_TABLE</td><td>0.82</td><td>#table-3</td>
<td>
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-secondary text-secondary-text hover:bg-secondary-hover focus-visible:ring-secondary-ring h-8 px-3 text-xs">Confirm</button>
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-secondary text-secondary-text hover:bg-secondary-hover focus-visible:ring-secondary-ring h-8 px-3 text-xs">Dismiss</button>
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-transparent text-ghost-text hover:bg-ghost-hover focus-visible:ring-ghost-ring h-8 px-3 text-xs">Inconclusive</button>
</td>
</tr>
<tr>
<td>f-002</td>
<td><span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-info-light text-info">info</span></span></td>
<td>EMPTY_CHART</td><td>0.65</td><td>#chart-1</td>
<td>
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-secondary text-secondary-text hover:bg-secondary-hover focus-visible:ring-secondary-ring h-8 px-3 text-xs">Confirm</button>
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-secondary text-secondary-text hover:bg-secondary-hover focus-visible:ring-secondary-ring h-8 px-3 text-xs">Dismiss</button>
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-transparent text-ghost-text hover:bg-ghost-hover focus-visible:ring-ghost-ring h-8 px-3 text-xs">Inconclusive</button>
</td>
</tr>
</tbody>
</table>
<p class="text-xs text-text-muted mt-4">Provenance: model <code>vlm-2</code>, prompt v1 <code>sha256:9f2a…</code>, analyzed 2026-07-31T10:00:00Z. Stale prompts block analysis (422 STALE_PROMPT).</p>
</div>
</section>
<!-- net: offline / timeout / retry exhausted -->
<section id="state-net" class="scenario-state hidden">
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6 mb-4">
<h3 class="text-lg font-semibold leading-none tracking-tight mb-4">Network states</h3>
<div class="space-y-3">
<div class="rounded-md border border-destructive bg-destructive-light p-4" role="alert" aria-live="assertive">
<p class="text-sm text-text"><span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-destructive-light text-destructive">NET_01 OFFLINE</span></span> You are offline. Compile/validate actions are disabled. Auto-retry on reconnect.</p>
</div>
<div class="rounded-md border border-border bg-surface-muted p-4">
<p class="text-sm text-text"><span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-warning-light text-warning">NET_02 TIMEOUT</span></span> Request timed out after 30s. Retry 2 of 3 — exponential backoff.</p>
<div class="mt-4 inline-flex gap-4">
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-primary text-white hover:bg-primary-hover focus-visible:ring-primary-ring h-8 px-3 text-xs">Retry now</button>
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-secondary text-secondary-text hover:bg-secondary-hover focus-visible:ring-secondary-ring h-8 px-3 text-xs">Cancel</button>
</div>
</div>
<div class="rounded-md border border-destructive bg-destructive-light p-4" role="alert" aria-live="assertive">
<p class="text-sm text-text"><span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-destructive-light text-destructive">NET_03 EXHAUSTED</span></span> Could not reach server. Check your connection. Error ID: net-88a1.</p>
<div class="mt-4 inline-flex gap-4">
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-primary text-white hover:bg-primary-hover focus-visible:ring-primary-ring h-8 px-3 text-xs">Manual retry</button>
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-secondary text-secondary-text hover:bg-secondary-hover focus-visible:ring-secondary-ring h-8 px-3 text-xs">Contact support</button>
</div>
</div>
</div>
</div>
</section>
<!-- val: field + cross-field + 422 -->
<section id="state-val" class="scenario-state hidden">
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6">
<h3 class="text-lg font-semibold leading-none tracking-tight mb-4">Validation</h3>
<div class="flex flex-col gap-1.5 w-full mb-4">
<label class="text-sm font-medium text-text">objective.goal</label>
<input class="flex h-10 w-full rounded-md border border-destructive bg-surface-card px-3 py-2 text-sm text-text ring-offset-white placeholder:text-text-subtle focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50" placeholder="e.g. Verify filters and metric" value="">
<span class="text-xs text-destructive">goal is required (VAL_01)</span>
</div>
<div class="flex flex-col gap-1.5 w-full mb-4">
<label class="text-sm font-medium text-text">selected_case_ids</label>
<input class="flex h-10 w-full rounded-md border border-destructive bg-surface-card px-3 py-2 text-sm text-text ring-offset-white placeholder:text-text-subtle focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50" placeholder="B01, C04, C05" value="B01, B99">
<span class="text-xs text-destructive">case "B99" does not exist in catalog v1 (VAL_01)</span>
</div>
<div class="rounded-md border border-warning bg-warning-light p-4" role="alert" aria-live="polite">
<p class="text-sm text-text"><span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-warning-light text-warning">VAL_02 CROSS-FIELD</span></span> resolve changes conflict with base revision — review the summary banner before re-submitting.</p>
</div>
</div>
</section>
<!-- auth -->
<section id="state-auth" class="scenario-state hidden">
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6 mb-4">
<div class="flex items-start gap-4">
<span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-info-light text-info">AUTH_01 · 401</span></span>
<p class="text-sm text-text">Session expired. Redirecting to login… <em>(intended destination preserved)</em></p>
</div>
</div>
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6" role="alert" aria-live="assertive">
<div class="flex items-start gap-4">
<span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-destructive-light text-destructive">AUTH_02 · 403</span></span>
<div>
<p class="text-sm text-text">You don't have permission to compile scenarios. Requires role <code>scenario.compile</code>.</p>
<p class="text-xs text-text-muted mt-1">Contact admin@example.com to request access.</p>
</div>
</div>
</div>
</section>
<!-- notfound -->
<section id="state-notfound" class="scenario-state hidden">
<div class="flex flex-col items-center justify-center py-12 px-4 text-center rounded-lg border border-border bg-surface-card text-text shadow-sm">
<svg class="w-16 h-16 text-text-subtle mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
</svg>
<h3 class="text-lg font-semibold text-text mb-1">Scenario not found</h3>
<p class="text-sm text-text-muted max-w-md">It may have been deleted or the id is wrong: <code>scn-404-unknown</code></p>
<div class="mt-6">
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-primary text-white hover:bg-primary-hover focus-visible:ring-primary-ring h-10 px-4 py-2 text-sm">Navigate to scenario list</button>
</div>
</div>
</section>
<!-- ratelimit -->
<section id="state-ratelimit" class="scenario-state hidden">
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6" role="alert" aria-live="assertive">
<div class="flex items-start gap-4">
<span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-warning-light text-warning">429 RATE LIMITED</span></span>
<div>
<p class="text-sm text-text">Too many requests. Retry in <b>30s</b>. Action disabled during countdown.</p>
<p class="text-xs text-text-muted mt-1">Header: <code>Retry-After: 30</code></p>
</div>
</div>
<div class="mt-6">
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-primary text-white hover:bg-primary-hover focus-visible:ring-primary-ring h-10 px-4 py-2 text-sm" disabled>Compile (waiting 30s…)</button>
</div>
</div>
</section>
<!-- duplicate: DUP_01/02 -->
<section id="state-duplicate" class="scenario-state hidden">
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6 mb-4">
<h3 class="text-lg font-semibold leading-none tracking-tight mb-4">Duplicate & interruption</h3>
<div class="space-y-3">
<div class="rounded-md border border-border bg-surface-muted p-4">
<p class="text-sm text-text"><span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-info-light text-info">DUP_01</span></span> Draft-pack submit in progress — repeated clicks ignored.</p>
<div class="mt-4">
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-primary text-white hover:bg-primary-hover focus-visible:ring-primary-ring h-10 px-4 py-2 text-sm" disabled>
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-current" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
Generating draft…
</button>
</div>
</div>
<div class="rounded-md border border-warning bg-warning-light p-4" role="alert" aria-live="assertive">
<p class="text-sm text-text"><span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-warning-light text-warning">DUP_02</span></span> You have unsaved resolution changes. Discard and leave?</p>
<div class="mt-4 inline-flex gap-4">
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-primary text-white hover:bg-primary-hover focus-visible:ring-primary-ring h-8 px-3 text-xs">Stay on page</button>
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-destructive text-white hover:bg-destructive-hover focus-visible:ring-destructive-ring h-8 px-3 text-xs">Discard changes</button>
</div>
</div>
</div>
</div>
</section>
<!-- large -->
<section id="state-large" class="scenario-state hidden">
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6">
<div class="flex items-start gap-4 mb-4">
<span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-info-light text-info">LARGE · 142 STEPS</span></span>
<p class="text-sm text-text">Showing 100 of 142 — refine to see the rest. Virtualized lanes; step table remains the semantic fallback.</p>
</div>
<div class="flex gap-4 overflow-x-auto lanes-wrap">
<div class="flex-1 min-w-[150px]">
<div class="text-xs font-semibold text-text-muted mb-2">setup</div>
<div class="rounded-md border border-border bg-surface-muted p-3 mb-2 lane-node">open_dashboard<br><span class="text-xs text-info font-medium">browser</span><br><span class="text-xs text-text-muted">ready</span></div>
</div>
<div class="flex-1 min-w-[150px]">
<div class="text-xs font-semibold text-text-muted mb-2">interact</div>
<div class="rounded-md border border-border bg-surface-muted p-3 mb-2 lane-node">apply_filters<br><span class="text-xs text-info font-medium">browser</span><br><span class="text-xs text-text-muted">ready</span></div>
</div>
<div class="flex-1 min-w-[150px]">
<div class="text-xs font-semibold text-text-muted mb-2">observe</div>
<div class="rounded-md border border-border bg-surface-muted p-3 mb-2 lane-node">parse_xlsx<br><span class="text-xs text-info font-medium">xlsx</span><br><span class="text-xs text-text-muted">ready</span></div>
</div>
</div>
</div>
</section>
<!-- malformed -->
<section id="state-malformed" class="scenario-state hidden">
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6" role="alert" aria-live="assertive">
<div class="flex items-start gap-4">
<span class="inline-flex items-center gap-1.5"><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-destructive-light text-destructive">MALFORMED RESPONSE</span></span>
<div>
<p class="text-sm text-text">Unexpected VLM response. Step marked inconclusive.</p>
<p class="text-xs text-text-muted mt-1">Error ID: 9f3c-77b2</p>
</div>
</div>
<div class="mt-6 inline-flex gap-4">
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-primary text-white hover:bg-primary-hover focus-visible:ring-primary-ring h-10 px-4 py-2 text-sm">Re-run analysis</button>
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 rounded-md bg-secondary text-secondary-text hover:bg-secondary-hover focus-visible:ring-secondary-ring h-10 px-4 py-2 text-sm">Contact support (note error ID)</button>
</div>
</div>
</section>
</main>
<script>
const stateSel = document.getElementById('state');
const viewportSel = document.getElementById('viewport');
const stage = document.getElementById('stage');
function apply() {
const s = stateSel.value, v = viewportSel.value;
document.querySelectorAll('.scenario-state').forEach(el => el.classList.add('hidden'));
document.getElementById('state-' + s).classList.remove('hidden');
stage.dataset.viewport = v;
}
stateSel.addEventListener('change', apply);
viewportSel.addEventListener('change', apply);
apply();
// A11Y: state transitions announced via aria-live regions (role=status/alert) present in each section.
// RESP: viewport toggle switches [data-viewport], collapsing .lanes-wrap to a single column below 768px.
</script>
</body>
</html>
<!doctype html><html lang="ru"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Модель сценария</title><link rel="stylesheet" href="../../prototype-ui.css"></head><body><div class="shell"><header class="topbar"><div class="brand">Superset Tools · BI testing</div><nav class="nav"><a class="active" href="#registry">Сценарии</a><a href="#runs">Запуски</a><a href="#automation">Автоматизация</a><a href="#health">Качество</a></nav><span class="badge info">Authoring validation</span></header><main><div class="page-head"><div><div class="eyebrow">Сценарий до сохранения</div><h1>Проверяемая модель сценария</h1><p class="sub">Аналитик видит бизнес-цель и безопасный граф, а не generated code.</p></div><button class="btn primary" onclick="setProtoState('eligible')">Проверить для сохранения</button></div><div class="main-aside"><section class="card"><h2>Граф</h2><div class="step"><div class="step-no">1</div><div><strong>Открыть FI-0080</strong><br><span class="sub">browser / open_dashboard · read</span></div><span class="badge ok">Registered</span></div><div class="step"><div class="step-no">2</div><div><strong>Применить filters</strong><br><span class="sub">browser / apply_native_filter · read</span></div><span class="badge ok">Registered</span></div><div class="step"><div class="step-no">3</div><div><strong>Скачать XLSX</strong><br><span class="sub">browser / download · read</span></div><span class="badge ok">Registered</span></div><div class="step"><div class="step-no">4</div><div><strong>Сравнить baseline</strong><br><span class="sub">assertion / compare_to_baseline</span></div><span class="badge ok">Registered</span></div></section><aside class="grid"><section class="card"><h2>ParameterDefinitions</h2><p><strong>test_date</strong> · date · required</p><p><strong>counterparty</strong> · string · required</p><div class="notice info">Нет runtime values — и это нормально. Сценарий может быть сохранён; bindings появятся только в ручном/автоматическом запуске.</div></section><section class="card"><h2>Action safety</h2><p><span class="badge ok">ActionRegistry v1</span></p><p class="sub">Каждая пара tool/action имеет typed inputs, outputs, risk, timeout и retry policy.</p></section><section id="validation" class="notice info"><strong>Authoring valid</strong><br>Можно собрать DraftPack и сохранить revision.</section></aside></div></main><div class="statebar"><span class="sub">State: <b id="proto-state"></b></span><button class="btn" onclick="setProtoState('eligible')">Save eligible</button><button class="btn" onclick="setProtoState('blocked')">Blocked</button></div></div><script src="../../prototype-ui.js"></script><script>protoState('eligible',s=>{let v=document.getElementById('validation');v.className='notice '+(s==='blocked'?'danger':'info');v.innerHTML=s==='blocked'?'<strong>Authoring blocked</strong><br>Неизвестный selector или baseline — исправьте модель.':'<strong>Authoring valid</strong><br>Можно собрать DraftPack и сохранить revision.'})</script></body></html>

View File

@@ -34,8 +34,8 @@ python3 -c "import yaml; d=yaml.safe_load(open('specs/038-dashboard-scenario-mod
2. Map a full-capability dashboard; every case has one classification.
3. Map a no-XLSX/no-dataset-field dashboard; C04C06 and T01T03 retain rationale with no SQL.
4. Compile the same canonical inputs repeatedly and with shuffled input maps; scenario_key/content_hash must match.
5. Validate invalid fixtures: cycle, missing ref, duplicate output, raw metric expected, stale baseline, unknown selector/tool, unresolved required parameter.
6. Resolve one parameter; unrelated step keys/order must remain unchanged and content_hash must change.
5. Validate invalid fixtures: cycle, missing ref, duplicate output, raw metric expected, stale baseline, unknown selector/tool, invalid ParameterDefinition.
6. Verify an unbound required ParameterDefinition remains save-eligible; bind it in 044 RunPreflight and verify ScenarioRevision/content_hash remain unchanged.
7. Compile a valid graph to draft pack (authoring) through registered templates.
8. Attempt executable-code, shell, SQL, custom path, and unknown-template injection; all must block before draft registration.
9. Prototype: every `@UX_STATE` in `contracts/ux/scenario-graph-ux.md` reachable via `prototype/index.html` state switcher (see `prototype/manifest.md`).

View File

@@ -82,7 +82,7 @@ Errors block draft-pack compilation. Warnings may allow preview but are repeated
## 8. Resolution Semantics
Supplying parameters produces a new immutable scenario revision with parent_revision_hash. Unrelated steps retain ids and serialization. Manual conversion and selector hints are explicit resolution operations with audit reason.
Editing ParameterDefinitions/defaults may produce a new executable revision with `parent_revision_id`; supplying runtime ParameterBindings occurs only at 044 launch and never changes content identity. Unrelated steps retain logical_step_id and serialization. Manual conversion and selector hints are explicit resolution operations with audit reason.
## 9. LLM Verification Tooling — Module Reuse

View File

@@ -73,7 +73,7 @@
**Acceptance**:
1. **Given** a scenario needs test data **When** graph is generated **Then** required parameters include name, type, validation rule, default/source, and affected steps.
2. **Given** an action cannot be automated reliably **When** graph is generated **Then** a human checkpoint step describes the manual action and expected evidence.
3. **Given** a user later supplies a parameter **When** graph is resolved **Then** dependent steps reference the resolved value without changing unrelated graph structure.
3. **Given** a reusable scenario has a required ParameterDefinition without a default **When** it is saved **Then** it remains save-eligible; a later 044 launch binds the value without changing the graph or content_hash.
---
@@ -103,7 +103,7 @@
| E9 | 429 rate limit on compile/validate | throttling | Retry-After honored; UI countdown | User waits; L2 UX test |
| E10 | 5xx backend failure on compile | server-error | Error section + retry; partial graph not persisted | User retries; L2 UX test |
| E11 | Malformed VLM response / empty findings | integration | Findings array empty; step inconclusive with reason; stale prompt blocked (422 STALE_PROMPT) | Re-run analysis; L1 VLM test |
| E12 | Missing parameter value on pack compile | data-quality | Pack becomes `preview_only` with all save blockers listed | User resolves parameters; L1 pack test |
| E12 | Missing runtime parameter at launch | data-quality | Saved pack remains save_eligible; 044 RunPreflight rejects only that launch | Supply a typed binding; L1 preflight test |
| E13 | Unsafe path / executable code / SQL injection into pack | security | Template/path/code validation blocks before draft registration | L1 security test; injected-code fixture |
| E14 | Duplicate submit of draft-pack | idempotency | Idempotency key / revision hash prevents double registration | L1 API test; 409 on changed revision |
@@ -115,7 +115,7 @@
- **AGSCN-FR-002**: Every scenario step MUST declare tool category, action, inputs, outputs, expected result, dependencies, and automation status.
- **AGSCN-FR-003**: Supported tool categories MUST include browser automation, Superset API execution, XLSX parsing, assertion, screenshot/evidence, report generation, artifact generation, and human checkpoint.
- **AGSCN-FR-004**: Assertions against reference values MUST use baseline references or baseline candidate references; raw expected numbers MUST NOT be embedded directly in executable steps.
- **AGSCN-FR-005**: Scenario validation MUST detect missing refs, cycles, duplicate outputs, missing baselines, stale baselines, unknown selectors, unsupported tools, and unresolved required parameters.
- **AGSCN-FR-005**: Authoring validation MUST detect missing refs, cycles, duplicate outputs, missing/stale baselines, unknown selectors, unsupported tools, and invalid ParameterDefinitions. Required runtime bindings are enforced only by 044 RunPreflight.
- **AGSCN-FR-006**: Checklist mapping MUST use normalized checklist cases derived from the research PDF and capability tags, not hardcoded one-size-fits-all scripts. Mapping accepts the target release_version for baseline lookup.
- **AGSCN-FR-007**: The model MUST allow manual/human checkpoint steps where automation is unsafe, unavailable, or underspecified.
- **AGSCN-FR-008**: Scenario output MUST be deterministic for the same dashboard query model, checklist template, baseline catalog, and user parameters.
@@ -123,6 +123,8 @@
- **AGSCN-FR-010**: Screenshot steps MUST carry a capture SPECIFICATION: target (tab/viewport), viewport dimensions, readiness strategy, masking selectors, and max wait. **Execution of capture is owned by 044** (ScenarioExecution CaptureService delegating to `Plugin.Service.ScreenshotService`), with artifacts `owner_type=scenario_run`; 038 defines the spec, not the runtime path.
- **AGSCN-FR-011**: Visual-analysis steps MUST carry a typed `VlmAnalysisSpec` (profile/provider/model/prompt template/hash/confidence). **Runtime VLM submission and `VlmFinding` production are owned by 044**, reusing `Plugin.Service.LLMClient` resolved through `Services.LlmProvider.LLMProviderService` (multimodal-required, encrypted-key handling, JSON mode) and redacting raw responses via `Plugin.Service.RedactionService`. A stub/default submit returning empty findings without a real provider call is incomplete.
- **AGSCN-FR-012**: Human checkpoint steps MAY reference specific VLM finding ids. Resolution options (confirm, **false_positive**, inconclusive) MUST be typed and auditable — this is a 044 `HumanCheckpoint`, **distinct from** the 036 authorization `ActionApprovalGate`. Disposition changes finding status, not graph structure.
- **AGSCN-FR-013**: The agent MAY plan scenario coverage, resolve ambiguity, and create a validated WorkingDraft or executable revision when delegated policy permits. Every resulting graph MUST pass the deterministic 038 validator and retain canonical immutable provenance.
- **AGSCN-FR-014**: Graph authoring and revision actions MUST be available in the persistent scenario workspace or agent thread; modal/dialog interaction MUST NOT be required for authoring, review, conflict recovery, or approval.
### Key Entities

View File

@@ -24,7 +24,7 @@
| AGSCN-FR-011 | preview (vlm) | N/A — DTO only | analyzeScenarioScreenshot | ScenarioGraph.Vlm.Analyze | T040T042 | N/A — UI in 039 | Test.Scenario.Vlm |
| AGSCN-FR-012 | preview (disposition) | N/A — DTO only | disposeVlmFindings | ScenarioGraph.Human.Disposition | T043T044 | N/A — UI in 039 | Test.Scenario.Disposition |
| Draft pack (pack compiler) | preview (preview_only / save_eligible) | N/A — DTO only | compileScenarioDraftPack | ScenarioGraph.PackCompiler.Generate | T026T030 | N/A — UI in 039 | Test.Scenario.Pack |
| Edge E6 (409 stale) | preview (stale409 modal) | N/A — DTO only | resolveDashboardScenario | ScenarioGraph.Resolver.Resolve | T023 | N/A — UI in 039 | Test.Scenario.Resolver.Edge |
| Edge E6 (409 stale) | preview (stale409 conflict panel) | N/A — DTO only | resolveDashboardScenario | ScenarioGraph.Resolver.Resolve | T023 | N/A — UI in 039 | Test.Scenario.Resolver.Edge |
| Edge E9 (429) | preview (rate limited) | N/A — DTO only | any scenario op | ScenarioGraph.Api | T031T032 | N/A — UI in 039 | Test.Api.Scenarios.Edge |
| Edge E11 (malformed VLM) | preview (vlm inconclusive) | N/A — DTO only | analyzeScenarioScreenshot | ScenarioGraph.Vlm.Analyze | T040 | N/A — UI in 039 | Test.Scenario.Vlm.Edge |
| Edge E13 (injection) | preview (pack blocked) | N/A — DTO only | compileScenarioDraftPack | ScenarioGraph.PackCompiler.Generate | T026, T034 | N/A — UI in 039 | Test.Scenario.Pack.Security |

View File

@@ -85,7 +85,7 @@ $ scenario compile --objective "verify filters, metric, XLSX export" --case-ids
| AUTH_01 | 401 Unauthorized | Expired token | ✅ | Redirect to login; preserve intent | Login → redirect back | L1 |
| AUTH_02 | 403 Forbidden | Wrong role | ✅ | Full-page explanation; no approval gate | Navigate to dashboard | L1+L2 |
| NF_01 | 404 scenarioId | Deleted/unknown scenario | ✅ | Full-page not found + link to list | Navigate to scenario list | L1+L2 |
| CONF_01 | 409 Stale base revision | Version mismatch on resolve | ✅ | Modal: "Scenario changed. Recompile?" | Recompile or snapshot diff | L1+L2 |
| CONF_01 | 409 Stale base revision | Version mismatch on resolve | ✅ | Persistent conflict panel: "Scenario changed. Recompile?" | Recompile or snapshot diff | L1+L2 |
| CONF_02 | 409 Duplicate pack registration | Same revision hash re-posted | ✅ | Return existing DraftPack (idempotent) | Transparent; log event | L1 |
| 422 | 422 Unprocessable (compile/resolve) | Invalid canonical inputs | ✅ | Step/field-mapped error detail | Correct input + re-submit | L1+L2 |
| 429 | 429 Rate Limited + Retry-After | Too many compile/validate | ✅ | Toast + countdown on action | Wait Retry-After; disable during countdown | L1+L2 |
@@ -93,7 +93,7 @@ $ scenario compile --objective "verify filters, metric, XLSX export" --case-ids
| STALE | Stale baseline fingerprint | Baseline updated in 037 | ✅ | Assertion step warning badge | Run baseline discovery; mark pending | L1+L2 |
| PARTIAL | Partial graph load | Some steps failed to compile | ✅ | Failed steps show placeholder | Per-step retry; "Reload all" | L1+L2 |
| DUP_01 | Duplicate submit (draft-pack) | Rapid double-click | ✅ | Button disabled + spinner | Normal completion | L2 |
| DUP_02 | Navigation interruption (dirty resolution) | Route change with unsaved resolution | ✅ | `beforeunload` + confirm dialog | Stay or discard | L2 |
| DUP_02 | Navigation interruption (dirty resolution) | Route change with unsaved resolution | ✅ | Persistent unsaved-work panel | Stay or discard | L2 |
| LARGE | Large dataset (>100 steps) | Big scenario graph | ✅ | Virtualized lanes; "Showing 100 of 200" | Pagination/refinement | L2 |
| EMPTY | Empty result (no cases applicable) | Dashboard has no mapped cases | ✅ | Empty state + guidance | Accept partial coverage / manual checkpoints | L1+L2 |
| MALFORMED | Malformed VLM response | Backend/LLM bug | ✅ | Toast with error ID; step inconclusive | Re-run analysis; note error ID | L1 |
@@ -119,7 +119,7 @@ $ scenario compile --objective "verify filters, metric, XLSX export" --case-ids
### Scenario D: Stale Revision on Resolve (409)
* **System Response**: Modal "Scenario changed since your base revision. Recompile to see the latest graph."
* **System Response**: Persistent panel: "Scenario changed since your base revision. Recompile to see the latest graph."
* **Recovery**: User recompiles; stale edits are rejected, never auto-merged.
## 6. Tone & Voice

View File

@@ -17,7 +17,7 @@
- [x] CHK005 ADR relations include frontend architecture, RBAC, and upstream specs 036-038.
- [x] CHK006 Decision memory rejects Playwright/SQL/XLSX primary dropdown UX.
- [x] CHK007 Svelte 5 runes/model-first requirement is explicit.
- [x] CHK008 HITL confirmation and RBAC are explicit for durable actions.
- [x] CHK008 Delegated-action policy, inline gates and RBAC are explicit for durable actions.
## Readiness for Plan

View File

@@ -7,7 +7,7 @@
2. Gradio emits agent_run_started; run panel appears.
3. Agent invokes 037 query-model inspection and emits inspect progress.
4. Agent submits bounded intent to 038 compile; workspace receives ScenarioResponse.
5. User resolves parameters via 038 resolve with base revision hash.
5. User may edit ParameterDefinitions/defaults through the constrained authoring flow; runtime values are bound only by 044/045 launch preflight.
6. User requests draft pack; 038 registers 036 drafts.
7. User previews/downloads through 036 URLs.
8. Save or baseline approval creates 036 gate; decision/consume is authoritative.

View File

@@ -1,7 +1,7 @@
#region DashboardScenarioUi.WorkspaceUx [C:4] [TYPE ADR] [SEMANTICS ux,dashboard-testing,workspace,scenario,agent]
@BRIEF Layout, state, feedback, recovery, and browser test contract for the AGENT-DRIVEN scenario workspace (manual ad-hoc flow only).
@RELATION DEPENDS_ON -> [DashboardScenarioUi.DataModel]
@RATIONALE The agent workspace is the interactive environment where the analyst creates, refines, and approves a test scenario through structured dialog. Pipeline-triggered verification runs use separate views (see release-verification-ux.md).
@RATIONALE The agent workspace is the persistent interactive environment where the analyst creates, refines and remediates a test scenario through chat plus structured panels. Pipeline-triggered verification runs use separate views (see release-verification-ux.md).
@REJECTED Using the agent workspace for pipeline-triggered verification — pipeline views are read-only projections of VerificationRun data and must not require an AgentRun or conversation.
## Desktop Layout
@@ -11,7 +11,7 @@
- Main two-column area: scenario/steps/coverage (wide) and parameters/baselines (narrow).
- Draft area: file tree left, preview right.
- Evidence area: screenshot viewer + finding list + finding detail card for VLM review.
- Confirmation: existing blocking ConfirmationCard bound to current 036 gate.
- Policy-gated action: existing inline ActionApprovalCard bound to current 036 gate.
At widths below large breakpoint, panels stack in workflow order. Step table and file tree scroll within bounded containers; page retains one primary vertical scroll.
@@ -31,7 +31,7 @@ At widths below large breakpoint, panels stack in workflow order. Step table and
1. Three dashboards generate correct isolated context.
2. 18-step fixture at 1366×768 does not overlap/collapse actions.
3. Keyboard completes parameter form and confirmation.
3. Keyboard completes parameter form and inline policy-gated action when present.
4. Missing/stale baseline states and no-direct-SQL copy.
5. Invalid artifact blocks save; download keeps repository unchanged.
6. Reload recovers run/scenario/drafts.

View File

@@ -31,7 +31,7 @@ Owned by `DashboardTesting.WorkspaceModel`, composed under `AgentChat.Model`. ON
- workspaceState from intent, AgentRun status/stage, scenario, draft pack, and domain error;
- canApplyParameters when changed drafts are valid;
- canGenerateDraft when scenario exists and required parameters/selectors are resolved;
- canGenerateDraft when scenario exists and authoring blockers (for example selectors/baselines) are resolved; runtime ParameterBindings are collected only in 045 launch preflight;
- canRequestSave when draftPack.status=save_eligible, artifacts valid, and no blockers;
- baselineApprovalReady when candidate selected and non-blank reason;
- progress stages from AgentRunModel only.
@@ -41,13 +41,13 @@ Owned by `DashboardTesting.WorkspaceModel`, composed under `AgentChat.Model`. ON
1. Domain status never derives from assistant text.
2. Graph/validation are server-authoritative immutable revisions.
3. Parameter edits cannot modify undeclared fields.
4. Save and approve controls delegate to 036 pending gate.
4. Durable actions delegate to 036 delegated-action policy; only actions requiring authorization render a pending ActionApprovalGate.
5. Download/preview never set persisted state.
6. Direct SQL is absent from actions and explanatory alternatives.
7. Switching run/context resets all scenario-local selections and drafts.
8. Evidence and findings are bound to the owning AgentRun and never cross run boundaries.
9. Finding disposition is recorded once per finding; replay of same disposition is idempotent.
10. WorkspaceModel is ONLY instantiated for `intent=build_dashboard_test_scenario` with `trigger=manual`; it MUST NOT activate for pipeline-triggered runs.
10. WorkspaceModel is instantiated for analyst-opened agent intents (creation, investigation, revalidation, remediation, load analysis) and MUST NOT activate for pipeline-triggered runs.
---

View File

@@ -57,7 +57,7 @@ frontend/src/
2. Dashboard entry/context/RBAC path.
3. Workspace model and progress/scenario/coverage views.
4. Parameter and baseline resolution.
5. Draft preview and reused 036 confirmation flow.
5. Draft preview and reused 036 delegated-action/inline-gate flow.
6. Responsive/a11y/E2E/regression gates.
## No New Backend Domain Logic

View File

@@ -1,195 +1 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>039 Dashboard Test Scenario UI — Interactive Prototype</title>
<style>
.bg-surface-page{background:#f8fafc}.bg-surface-card{background:#fff}.bg-surface-muted{background:#f1f5f9}
.bg-primary{background:#2563eb}.bg-primary-light{background:#eff6ff}
.bg-destructive{background:#dc2626}.bg-destructive-light{background:#fef2f2}
.bg-success{background:#16a34a}.bg-success-light{background:#f0fdf4}
.bg-warning{background:#d97706}.bg-warning-light{background:#fffbeb}
.bg-info-light{background:#f0f9ff}
.border-border{border-color:#e2e8f0}.border-border-strong{border-color:#cbd5e1}
.text-text{color:#0f172a}.text-text-muted{color:#64748b}.text-white{color:#fff}
.text-primary{color:#2563eb}.text-destructive{color:#dc2626}.text-warning{color:#b45309}.text-info{color:#0369a1}.text-success{color:#16a34a}
.rounded-md{border-radius:.375rem}.rounded-lg{border-radius:.5rem}.rounded-full{border-radius:9999px}
.border{border-width:1px}.divide-y>*+*{border-top-width:1px}.divide-border>*+*{border-color:#e2e8f0}
.shadow-sm{box-shadow:0 1px 2px 0 rgb(0 0 0/.05)}
.p-1{padding:.25rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}
.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}
.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}
.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}
.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}
.flex{display:flex}.inline-flex{display:inline-flex}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-between{justify-content:space-between}.w-full{width:100%}
.h-2{height:.5rem}.h-8{height:2rem}.h-10{height:2.5rem}.grid{display:grid}.grid-cols-2{grid-template-columns:repeat(2,1fr)}
.text-xs{font-size:.75rem;line-height:1rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-base{font-size:1rem;line-height:1.5rem}.text-2xl{font-size:1.5rem;line-height:2rem}
.font-medium{font-weight:500}.font-bold{font-weight:700}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.text-left{text-align:left}
.block{display:block}.max-h-\[300px\]{max-height:300px}.overflow-auto{overflow:auto}
.disabled\:opacity-50:disabled{opacity:.5}
body{margin:0;font-family:ui-sans-serif,system-ui,sans-serif;background:#f8fafc}
.chrome{position:sticky;top:0;z-index:50;background:#0f172a;color:#e2e8f0;padding:10px 16px;display:flex;gap:12px;align-items:center;flex-wrap:wrap;font-size:13px}
.chrome button{background:#334155;color:#e2e8f0;border:0;border-radius:6px;padding:6px 10px;cursor:pointer}
.chrome button.active{background:#2563eb;color:#fff}.chrome label{color:#94a3b8}
.proto-screen{display:none;padding:24px}.proto-screen.active{display:block}
.state-pill{background:#334155;border-radius:9999px;padding:2px 10px;font-size:12px}
</style>
</head>
<body>
<nav class="chrome">
<strong style="color:#fff">039 Prototype</strong>
<span id="curState" class="state-pill"></span>
<label>Экран:</label>
<button data-screen="workspace" class="active">Workspace</button>
<button data-screen="artifacts">Artifacts</button>
<button data-screen="evidence">Evidence/VLM</button>
<button data-screen="pipeline">Pipeline Views</button>
<label>Состояние:</label>
<span id="stateButtons"></span>
</nav>
<div class="proto-screen active" id="workspace">
<header class="flex items-center justify-between mb-4">
<h1 class="text-2xl font-bold tracking-tight text-text">Сценарий тестирования дашборда</h1>
<span class="text-xs text-text-muted">run: run-a1f3 · connected</span>
</header>
<ol class="flex flex-wrap items-center gap-2 mb-4" aria-label="progress">
<li><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-primary text-white">Контекст</span></li>
<li class="text-text-muted"></li>
<li><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-primary text-white">Анализ</span></li>
<li class="text-text-muted"></li>
<li><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-primary text-white">Сценарий</span></li>
<li class="text-text-muted"></li>
<li><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-info-light text-info">Параметры</span></li>
<li class="text-text-muted"></li>
<li><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-surface-muted text-text-muted">Генерация</span></li>
<li class="text-text-muted"></li>
<li><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-surface-muted text-text-muted">Валидация</span></li>
<li class="text-text-muted"></li>
<li><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-surface-muted text-text-muted">Сохранение</span></li>
</ol>
<div class="grid grid-cols-2 gap-4">
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6">
<h2 class="text-base font-medium mb-1">Операционный отчёт</h2>
<p class="text-sm text-text-muted mb-2">Проверить, что выручка и заказы корректны при фильтрах.</p>
<p class="text-xs text-info mb-3">Проверки метрик выполняются через Superset-native API; прямой SQL не используется.</p>
<table class="w-full text-sm text-left divide-y divide-border border border-border rounded-lg">
<thead><tr class="text-text-muted text-xs"><th class="p-3">Шаг</th><th class="p-3">Инструмент</th><th class="p-3">Ожидание</th><th class="p-3">Статус</th></tr></thead>
<tbody class="divide-y divide-border">
<tr><td class="p-3">Метрика выручка</td><td class="p-3 text-xs">superset_api</td><td class="p-3 text-xs">значение &gt; 0</td><td class="p-3"><span class="rounded-full px-2 py-0.5 text-xs font-medium bg-success-light text-success">automated</span></td></tr>
<tr><td class="p-3">Снимок региона</td><td class="p-3 text-xs">playwright</td><td class="p-3 text-xs">изображение</td><td class="p-3"><span class="rounded-full px-2 py-0.5 text-xs font-medium bg-warning-light text-warning">needs_context</span></td></tr>
<tr><td class="p-3">Проверка вручную</td><td class="p-3 text-xs"></td><td class="p-3 text-xs">чекпоинт</td><td class="p-3"><span class="rounded-full px-2 py-0.5 text-xs font-medium bg-surface-muted text-text-muted">manual</span></td></tr>
</tbody>
</table>
</div>
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6 space-y-4">
<div>
<h3 class="text-sm font-medium mb-2">Параметры</h3>
<label class="block text-sm mb-2">Тестовая дата <span class="text-destructive">*</span>
<input id="paramDate" class="mt-1 block w-full border border-border-strong rounded-md p-2 text-sm" value="2026-08-04" /></label>
<label class="block text-sm">Контрагент
<select class="mt-1 block w-full border border-border-strong rounded-md p-2 text-sm"><option>ACME</option><option>Globex</option></select></label>
</div>
<div>
<h3 class="text-sm font-medium mb-2">Baseline</h3>
<div class="flex items-center justify-between text-sm mb-1"><span>approved</span><span class="rounded-full px-2 py-0.5 text-xs font-medium bg-success-light text-success">2</span></div>
<div class="flex items-center justify-between text-sm mb-1"><span>stale</span><span class="rounded-full px-2 py-0.5 text-xs font-medium bg-warning-light text-warning">1</span></div>
<div class="flex items-center justify-between text-sm mb-1"><span>missing</span><span class="rounded-full px-2 py-0.5 text-xs font-medium bg-surface-muted text-text-muted">1</span></div>
</div>
<button class="w-full rounded-md bg-primary text-white h-10 px-4 text-sm">Применить параметры</button>
</div>
</div>
</div>
<div class="proto-screen" id="artifacts">
<h1 class="text-2xl font-bold tracking-tight text-text mb-4">Сгенерированные артефакты</h1>
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6">
<div class="flex items-center justify-between mb-3">
<span class="rounded-full px-2 py-0.5 text-xs font-medium bg-warning-light text-warning" id="saveBadge">preview only</span>
</div>
<ul id="warnings" class="mb-3 space-y-1">
<li class="rounded-md bg-warning-light text-warning p-2 text-sm">⚠ unresolved marker: scenarios/sc-1/runners/check.py:42</li>
</ul>
<div class="grid grid-cols-2 gap-4">
<ul class="text-sm divide-y divide-border border border-border rounded-lg">
<li class="px-3 py-2">▸ evidence/</li>
<li class="px-3 py-2 bg-primary-light text-primary">· scenarios/sc-1/scenario.json</li>
<li class="px-3 py-2">· scenarios/sc-1/runners/check.py</li>
<li class="px-3 py-2">· reports/report.html</li>
</ul>
<div class="rounded-md bg-surface-muted p-3 max-h-[300px] overflow-auto">
<pre class="text-xs font-mono whitespace-pre-wrap">{"scenario_key": "sc-1", "objective": {"goal": "..."}}</pre>
</div>
</div>
<button class="mt-4 bg-surface-muted text-text rounded-md h-10 px-4 text-sm">Скачать черновик</button>
</div>
</div>
<div class="proto-screen" id="evidence">
<h1 class="text-2xl font-bold tracking-tight text-text mb-4">Evidence / VLM</h1>
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6">
<div class="flex gap-2 mb-3">
<span class="rounded-full px-2.5 py-1 text-xs font-medium bg-primary-light text-primary">1366×768</span>
<span class="rounded-full px-2.5 py-1 text-xs font-medium bg-surface-muted text-text-muted">1440×900</span>
</div>
<div class="rounded-md bg-surface-muted p-3 mb-3 flex items-center justify-center min-h-[180px]">
<div class="text-center"><div class="h-20 w-40 bg-info-light rounded-md mx-auto mb-2"></div><span class="text-text-muted text-sm">screenshot placeholder</span></div>
</div>
<p class="text-xs text-text-muted mb-2">Capture: viewport 1366×768 · filters h1 · 2026-08-04</p>
<h4 class="text-sm font-medium mb-2">VLM findings</h4>
<div class="rounded-lg border border-border p-4">
<div class="flex items-center justify-between mb-2">
<span class="rounded-full px-2 py-0.5 text-xs font-medium bg-destructive-light text-destructive">high</span>
<code class="text-xs text-text-muted">VLM-12</code>
</div>
<p class="text-sm mb-1">chart label clipped</p>
<div class="mb-2"><div class="h-2 w-full bg-surface-muted rounded-full"><div class="h-2 bg-primary rounded-full" style="width:85%"></div></div>
<span class="text-xs text-text-muted">confidence 85%</span></div>
<p class="text-xs text-text-muted mb-2">Region: {"x":1,"y":2,"w":30,"h":40}</p>
<div class="flex gap-2" id="vlmControls">
<button class="bg-success text-white rounded-md h-8 px-3 text-xs">Confirm</button>
<button class="bg-surface-muted text-text rounded-md h-8 px-3 text-xs">Dismiss</button>
<button class="bg-warning text-white rounded-md h-8 px-3 text-xs">Inconclusive</button>
</div>
<p class="text-xs text-text-muted mt-2 hidden" id="vlmDone">Disposition: <span id="vlmDisposition"></span></p>
</div>
</div>
</div>
<div class="proto-screen" id="pipeline">
<h1 class="text-2xl font-bold tracking-tight text-text mb-4">Pipeline Verification Views</h1>
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6 mb-4">
<div class="flex items-center justify-between mb-3">
<h2 class="text-base font-medium">Verification</h2>
<span class="inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-xs font-medium bg-warning-light text-warning">⚠ warn</span>
</div>
<table class="w-full text-sm text-left divide-y divide-border border border-border rounded-lg">
<thead><tr class="text-text-muted text-xs"><th class="p-3">Trigger</th><th class="p-3">Env</th><th class="p-3">Status</th><th class="p-3">Summary</th></tr></thead>
<tbody class="divide-y divide-border">
<tr><td class="p-3">deploy_to_preprod</td><td class="p-3">preprod</td><td class="p-3"><span class="rounded-full px-2 py-0.5 text-xs font-medium bg-warning-light text-warning">warn</span></td><td class="p-3 text-xs">structure_diff: 2 changes</td></tr>
<tr><td class="p-3">release_create</td><td class="p-3">preprod</td><td class="p-3"><span class="rounded-full px-2 py-0.5 text-xs font-medium bg-success-light text-success">pass</span></td><td class="p-3 text-xs">metrics 12/0/1</td></tr>
</tbody>
</table>
</div>
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6">
<h3 class="text-sm font-medium mb-2">StructureDiff</h3>
<div class="flex items-center justify-between text-sm mb-1"><span>column_removed · amount</span><span class="rounded-full px-2 py-0.5 text-xs font-medium bg-destructive-light text-destructive">critical</span></div>
<div class="flex items-center justify-between text-sm"><span>type_changed · revenue</span><span class="rounded-full px-2 py-0.5 text-xs font-medium bg-warning-light text-warning">warning</span></div>
</div>
</div>
<script>
const screens=["workspace","artifacts","evidence","pipeline"];
const states={workspace:["context","inspect","scenario","parameters","generate","validate","save","permission_denied","api_error"],artifacts:["preview_only","save_eligible","blocked"],evidence:["pending","disposed"],pipeline:["warn","pass","immutability"]};
let current={screen:"workspace",state:"parameters"};
function renderScreen(s){document.querySelectorAll(".proto-screen").forEach(x=>x.classList.remove("active"));document.getElementById(s).classList.add("active");document.querySelectorAll(".chrome button[data-screen]").forEach(b=>b.classList.toggle("active",b.dataset.screen===s));renderStateButtons(s);}
function renderStateButtons(s){const w=document.getElementById("stateButtons");w.innerHTML="";(states[s]||[]).forEach(st=>{const b=document.createElement("button");b.textContent=st;b.className=st===current.state?"active":"";b.onclick=()=>{current.state=st;applyState(s,st);renderStateButtons(s);};w.appendChild(b);});document.getElementById("curState").textContent=s+" / "+current.state;}
function applyState(s,st){if(s==="artifacts"){document.getElementById("saveBadge").textContent=st==="save_eligible"?"save eligible":"preview only";document.getElementById("saveBadge").className="rounded-full px-2 py-0.5 text-xs font-medium "+(st==="save_eligible"?"bg-success-light text-success":"bg-warning-light text-warning");document.getElementById("warnings").style.display=st==="save_eligible"?"none":"block";}
if(s==="evidence"&&st==="disposed"){document.getElementById("vlmControls").style.display="none";document.getElementById("vlmDone").classList.remove("hidden");document.getElementById("vlmDisposition").textContent="confirmed";}
if(s==="evidence"&&st==="pending"){document.getElementById("vlmControls").style.display="flex";document.getElementById("vlmDone").classList.add("hidden");}}
document.querySelectorAll(".chrome button[data-screen]").forEach(b=>b.onclick=()=>{current.screen=b.dataset.screen;current.state=(states[b.dataset.screen]||[])[0];renderScreen(current.screen);renderStateButtons(current.screen);applyState(current.screen,current.state);});
renderScreen("workspace");applyState("workspace","parameters");
</script>
</body>
</html>
<!doctype html><html lang="ru"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Создание сценария</title><link rel="stylesheet" href="../../prototype-ui.css"></head><body><div class="shell"><header class="topbar"><div class="brand">Superset Tools · BI testing</div><nav class="nav"><a class="active" href="#registry">Сценарии</a><a href="#runs">Запуски</a><a href="#automation">Автоматизация</a><a href="#health">Качество</a></nav><span class="badge info">Новый сценарий</span></header><main><div class="page-head"><div><button class="btn">В Registry</button><h1 style="margin-top:14px">Создать проверку dashboard</h1><p class="sub">Формулируйте business goal; система соберёт проверяемый graph.</p></div></div><div class="main-aside"><section class="card"><h2>1. Контекст</h2><p><strong>Dashboard:</strong> FI-0080 · PREPROD</p><h2 style="margin-top:22px">2. Цель проверки</h2><textarea style="width:100%;min-height:96px">Проверить, что XLSX export отражает dashboard и table filters.</textarea><h2 style="margin-top:22px">3. Уточнения от автора</h2><div class="notice info"><strong>Какой filter использовать для smoke?</strong><br>Это authoring question, а не runtime human step.<br><button class="btn" style="margin-top:8px">Контрагент = ACME</button> <button class="btn">Выбрать другой</button></div><div class="actions"><button class="btn">Сохранить черновик</button><button class="btn primary" onclick="setProtoState('graph')">Собрать graph</button></div></section><aside class="grid"><section class="card"><h2>Создание ≠ запуск</h2><p class="sub">Здесь задаются ParameterDefinitions и business rules. Дата/контрагент конкретного запуска запрашиваются позже в Run configuration.</p></section><section id="next" class="card"><h2>Дальше</h2><p>Проверить graph → увидеть DraftPack → сохранить в Scenario Registry.</p></section></aside></div></main><div class="statebar"><span class="sub">State: <b id="proto-state"></b></span><button class="btn" onclick="setProtoState('context')">Context</button><button class="btn" onclick="setProtoState('graph')">Graph ready</button></div></div><script src="../../prototype-ui.js"></script><script>protoState('context',s=>document.getElementById('next').innerHTML=s==='graph'?'<span class="badge ok">Graph готов</span><h2 style="margin-top:10px">Проверьте сценарий</h2><p class="sub">Все actions зарегистрированы; authoring validation может сделать DraftPack save-eligible.</p><button class="btn primary">Открыть graph</button>':'<h2>Дальше</h2><p>Проверить graph → увидеть DraftPack → сохранить в Scenario Registry.</p>')</script></body></html>

View File

@@ -56,13 +56,13 @@ Transitions are driven by structured AgentRun events and authoritative ScenarioR
- File tree shows intended relative paths, validation, warnings, and unresolved markers.
- Preview/download use 036 opaque URLs and do not mutate repository.
- Save is disabled for preview_only or invalid drafts.
- Repository save and baseline approval reuse the 036 confirmation card, including exact paths/hash/warnings and mandatory baseline reason.
- Non-delegated repository actions and baseline approval reuse the 036 inline ActionApprovalGate card, including exact paths/hash/warnings and mandatory baseline reason. Delegated scenario-revision save is recorded as AgentAction instead.
## 8. Responsive and Accessibility
- Target: no collapse at 1366px for 15+ steps; below large breakpoint use stacked panels.
- Keyboard: entry, phase/step navigation, parameter form, file tree, preview, confirmation.
- Focus moves to first invalid parameter/finding and returns after modal/card action.
- Focus moves to first invalid parameter/finding and returns to the persistent workspace after an inline card action.
- Status uses text/icon and ARIA, never color alone.
## 9. Data Source Decision (amended 2026-08-07)

View File

@@ -1,5 +1,5 @@
#region DashboardScenarioUi.Spec [C:3] [TYPE ADR] [SEMANTICS spec,requirements,ux,scenario,dashboard-testing]
@BRIEF User-facing dashboard test scenario generation experience driven by agent analysis, scenario preview, parameter collection, artifact preview, and HITL save/approval.
@BRIEF Persistent agent workspace for scenario generation, scenario revision saves, evidence-led remediation, and policy-bound approvals.
@RELATION DEPENDS_ON -> [Doc.Adr.ADR0001]
@RELATION DEPENDS_ON -> [Doc.Adr.ADR0005]
@RELATION DEPENDS_ON -> [Doc.Adr.ADR0006]
@@ -14,7 +14,7 @@
**Feature Branch**: `039-dashboard-scenario-ui`
**Created**: 2026-07-07 | **Status**: Ready for Implementation
**Input**: "Provide the user-facing dashboard test scenario generation experience. From a dashboard page the user starts Create test scenario, the agent analyzes the dashboard, proposes a unique scenario graph, collects missing business parameters, previews generated artifacts and baseline impacts, and requires HITL confirmation before saving files or approving baselines."
**Input**: "Provide the user-facing persistent agent workspace. From a dashboard page the analyst creates or remediates a scenario; the agent analyzes context, proposes a graph, collects authoring context, previews artifacts/evidence, saves delegated revisions, and uses inline approval only where policy requires it."
## User Scenarios
@@ -70,16 +70,16 @@
---
### Story 5 — Save or Approve With HITL (P2)
### Story 5 — Save or Approve Through Delegated Policy (P2)
**Why P2**: Saving test packs and approving baselines have durable effects and must require explicit user approval.
**Why P2**: Durable actions need attributable policy decisions; the agent may save validated scenario revisions while high-risk baseline/publish actions remain gated.
**Independent Test**: Trigger save and baseline approval actions and verify confirmation cards include target, risk, warnings, and approval reason where required.
**Independent Test**: Save a validated revision under delegated policy and trigger baseline approval; verify immutable provenance and inline gate information where required.
**Acceptance**:
1. **Given** the user requests save **When** generated artifacts target repository paths **Then** confirmation shows target paths, files, warnings, and risk before saving.
2. **Given** baseline approval is requested **When** confirmation appears **Then** the user must provide a reason and can inspect provenance/diff.
3. **Given** the user denies either action **When** denial is submitted **Then** no save or approval occurs and the run records cancellation.
1. **Given** a validated scenario revision is ready **When** delegated policy permits **Then** the agent saves it with immutable revision, delegator, action and evidence provenance.
2. **Given** baseline approval or another non-delegated action is requested **When** policy requires it **Then** an inline card shows target, risk, provenance/diff and a required reason.
3. **Given** a gate is denied **When** denial is submitted **Then** no gated side effect occurs and the run records cancellation.
---
@@ -92,9 +92,9 @@
## Requirements
### Functional — Agent Workspace (dialog-driven, manual ad-hoc checks)
### Functional — Agent Workspace (persistent chat and work surfaces)
These requirements cover the interactive agent workspace where the analyst creates, refines, and approves a test scenario through structured dialog with the agent.
These requirements cover the persistent agent workspace where the analyst creates, refines, investigates, and remediates scenarios through chat plus structured work surfaces.
- **AGUI-FR-001**: Dashboard pages MUST expose a single business-level action labeled "Создать сценарий тестирования"; UI MUST NOT present low-level artifact/tool choices as the primary entry point.
- **AGUI-FR-002**: `/agent` MUST show Dashboard Test Scenario Agent mode when opened with scenario intent from 036.
@@ -103,14 +103,14 @@ These requirements cover the interactive agent workspace where the analyst creat
- **AGUI-FR-005**: Parameter collection MUST support typed required parameters and update dependent scenario readiness without restarting the full flow.
- **AGUI-FR-006**: Baseline UI MUST show approved baseline matches, stale warnings, missing baselines, and draft candidate approval paths.
- **AGUI-FR-007**: Generated artifact preview MUST show file tree, file content preview, validation status, unresolved markers, and warnings before save.
- **AGUI-FR-008**: Saving generated files and approving baselines MUST go through HITL confirmation from 036; baseline approval requires a reason.
- **AGUI-FR-008**: The agent MAY save a validated executable scenario revision under delegated policy. Baseline approval and every non-delegated risky action MUST use a 036 inline ActionApprovalGate; baseline approval requires a reason.
- **AGUI-FR-009**: The UI MUST explicitly communicate that metric validation uses Superset-native execution and does not execute direct SQL.
- **AGUI-FR-010**: All new UI state MUST follow Svelte 5 runes/model-first conventions and remain accessible by keyboard for confirmations, parameter forms, and previews.
- **AGUI-FR-010**: All new UI state MUST follow Svelte 5 runes/model-first conventions and remain accessible by keyboard for inline action cards, parameter forms, and previews; modal/dialog interaction MUST NOT be required to complete a workflow.
- **AGUI-FR-011**: Screenshot evidence artifacts MUST render in a dedicated EvidencePanel showing the captured image, capture metadata (viewport, filters_hash, timestamp), and any linked VLM findings with severity, region, and confidence.
- **AGUI-FR-012**: VLM findings MUST be reviewable with typed disposition controls: confirm (accepts finding as valid), dismiss (marks as false positive), or inconclusive (defers to human checkpoint). Disposition changes MUST be auditable and MUST NOT alter the scenario graph.
- **AGUI-FR-013**: The artifact preview file tree MUST include an `evidence/` branch listing screenshot artifacts and their associated VLM finding files.
These requirements are satisfied through the **DashboardTesting.AgentWorkspace** components. The agent is the interaction surface; structured panels (Parameters, ArtifactPreview, EvidencePanel) provide data entry and review within the agent workspace. The user converses with the agent to create and refine the scenario, fills parameters through forms, and confirms durable actions through 036 gates.
These requirements are satisfied through the **DashboardTesting.AgentWorkspace** components. The agent is the interaction surface; structured panels (Parameters, ArtifactPreview, EvidencePanel, ActionTimeline) provide data entry and review within the workspace. The agent may perform delegated durable actions, while policy-gated actions remain inline cards in the same thread.
### Functional — Pipeline Verification Views (automated, no agent interaction)
@@ -132,7 +132,7 @@ Interaction: диалог с агентом dashboard page
State: WorkspaceModel + AgentRunModel State: VerificationRun[] + StructureDiff
Trigger: manual (аналитик) Trigger: deploy_to_preprod, release_create,
Data: 038 ScenarioResponse, 036 drafts scheduled, etl_completed (автоматически)
Save: через 036 HITL gate Save: validate/approve/publish (pipeline gates)
Save: delegated policy / inline gate Save: validate/approve/publish (pipeline gates)
```
### LLM Verification Tooling Reuse (039)
@@ -151,12 +151,12 @@ The EvidencePanel and VlmFindingReviewCard components consume **DTOs only** —
### Key Entities — Agent Workspace
- **ScenarioEntryAction**: Dashboard-page action that launches `/agent` with dashboard scenario intent. Only valid in manual (ad-hoc) flow.
- **ScenarioWorkspaceState**: Frontend state machine for agent-driven scenario creation: context, progress, scenario preview, parameter collection, artifact preview, and HITL confirmation states.
- **ScenarioWorkspaceState**: Frontend state machine for agent-driven creation and remediation: context, progress, scenario preview, parameter collection, artifact preview, evidence and inline action states.
- **ScenarioPreviewCard**: UI representation of `DashboardTestScenario` from 038.
- **ParameterPanel**: Form for scenario business parameters and validation feedback. Text-input based, with typed validation.
- **BaselineImpactPanel**: UI section showing approved, stale, missing, and candidate baseline statuses within the agent workspace.
- **ArtifactPreviewPanel**: UI file tree/content preview for draft generated artifacts. Download is side-effect-free.
- **ScenarioConfirmationCard**: HITL card for save and baseline approval actions, reusing 036 gate.
- **ScenarioActionCard**: Inline 036 policy/gate card for baseline approval and other non-delegated actions.
- **EvidencePanel**: Panel for viewing captured screenshots, reviewing VLM findings, recording dispositions. Composes screenshot viewer, finding list, finding detail, and disposition controls.
- **VlmFindingReviewCard**: Card per VLM finding showing severity badge, confidence bar, region highlight, model/prompt provenance, and confirm/dismiss/inconclusive controls.
@@ -174,7 +174,7 @@ These entities are independent of the agent workspace. They consume `Verificatio
- **SC-002**: Scenario preview renders at least 15-step fixture graphs with phases, tools, warnings, and blockers without layout collapse at 1366px width.
- **SC-003**: Parameter updates resolve dependent scenario readiness within 200ms in model tests.
- **SC-004**: Artifact preview blocks save when unresolved markers are present in fixture data.
- **SC-005**: 100% of save and baseline approval flows require confirmation; baseline approval cannot submit without reason.
- **SC-005**: 100% of durable actions have immutable delegated-policy provenance; baseline approval cannot submit without a reason and any required gate.
- **SC-006**: UX copy never presents direct SQL as a validation path for this feature.
- **SC-007**: Pipeline verification views render StructureDiff, metric results, and VerificationRun data without initializing an AgentRun or WorkspaceModel.
- **SC-008**: VerificationStatusBadge appears on dashboard, release, and PREPROD pages within 200ms of data load.

View File

@@ -23,6 +23,8 @@
| enabled | bool | schedule control |
| created_by | str | audit owner |
An agent may propose or save a validated LoadProfile and launch policy-permitted diagnostic LoadRuns from an opened InvestigationCase. Server caps, variation validation, capacity allocation, circuit breakers and PROD gates remain deterministic; the agent never adapts an active worker ramp by prose.
## LoadVariation
Expanded immutable coordinate stored with a run:

View File

@@ -1,221 +1 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>040 Dashboard Load Testing — Interactive Prototype</title>
<style>
/* Tailwind utility shim — values from frontend/tailwind.config.js */
.bg-surface-page { background-color: #f8fafc; }
.bg-surface-card { background-color: #ffffff; }
.bg-surface-muted { background-color: #f1f5f9; }
.bg-primary { background-color: #2563eb; }
.bg-primary-light { background-color: #eff6ff; }
.bg-destructive { background-color: #dc2626; }
.bg-destructive-light { background-color: #fef2f2; }
.bg-success-light { background-color: #f0fdf4; }
.bg-warning-light { background-color: #fffbeb; }
.bg-info-light { background-color: #f0f9ff; }
.border-border { border-color: #e2e8f0; }
.border-border-strong { border-color: #cbd5e1; }
.text-text { color: #0f172a; }
.text-text-muted { color: #64748b; }
.text-white { color: #fff; }
.text-primary { color: #2563eb; }
.text-destructive { color: #dc2626; }
.text-warning { color: #b45309; }
.text-info { color: #0369a1; }
.text-success { color: #16a34a; }
.rounded-md { border-radius: .375rem; }
.rounded-lg { border-radius: .5rem; }
.rounded-full { border-radius: 9999px; }
.border { border-width: 1px; }
.border-t { border-top-width: 1px; }
.divide-y > * + * { border-top-width: 1px; }
.divide-border > * + * { border-color: #e2e8f0; }
.shadow-sm { box-shadow: 0 1px 2px 0 rgb(0 0 0 / .05); }
.p-2 { padding: .5rem; } .p-3 { padding: .75rem; } .p-4 { padding: 1rem; } .p-6 { padding: 1.5rem; }
.px-2\.5 { padding-left:.625rem; padding-right:.625rem; } .px-4 { padding-left:1rem; padding-right:1rem; }
.py-0\.5 { padding-top:.125rem; padding-bottom:.125rem; } .py-1 { padding-top:.25rem; padding-bottom:.25rem; }
.py-2 { padding-top:.5rem; padding-bottom:.5rem; } .py-3 { padding-top:.75rem; padding-bottom:.75rem; }
.mb-1 { margin-bottom:.25rem; } .mb-2 { margin-bottom:.5rem; } .mb-3 { margin-bottom:.75rem; }
.mb-4 { margin-bottom:1rem; } .mb-6 { margin-bottom:1.5rem; } .mb-8 { margin-bottom:2rem; }
.mt-2 { margin-top:.5rem; } .mt-4 { margin-top:1rem; }
.gap-2 { gap:.5rem; } .gap-3 { gap:.75rem; } .gap-4 { gap:1rem; }
.flex { display:flex; } .inline-flex { display:inline-flex; } .flex-col { flex-direction:column; }
.items-center { align-items:center; } .items-end { align-items:flex-end; } .justify-between { justify-content:space-between; }
.w-full { width:100%; } .h-8 { height:2rem; } .h-10 { height:2.5rem; }
.grid { display:grid; } .grid-cols-1 { grid-template-columns:1fr; } .lg\:grid-cols-3 { grid-template-columns:repeat(3,minmax(0,1fr)); }
.text-xs { font-size:.75rem; line-height:1rem; } .text-sm { font-size:.875rem; line-height:1.25rem; }
.text-base { font-size:1rem; line-height:1.5rem; } .text-3xl { font-size:1.875rem; line-height:2.25rem; }
.font-medium { font-weight:500; } .font-bold { font-weight:700; } .font-mono { font-family:ui-monospace,SFMono-Regular,Menlo,monospace; }
.tracking-tight { letter-spacing:-.025em; } .text-left { text-align:left; } .text-right { text-align:right; }
.block { display:block; }
.disabled\:opacity-50:disabled { opacity:.5; } .animate-spin { animation:spin 1s linear infinite; }
@keyframes spin { to { transform:rotate(360deg); } }
.ml-auto { margin-left:auto; }
body { margin:0; font-family:ui-sans-serif,system-ui,sans-serif; background:#f8fafc; }
.chrome { position:sticky; top:0; z-index:50; background:#0f172a; color:#e2e8f0; padding:10px 16px; display:flex; gap:12px; align-items:center; flex-wrap:wrap; font-size:13px; }
.chrome button { background:#334155; color:#e2e8f0; border:0; border-radius:6px; padding:6px 10px; cursor:pointer; }
.chrome button.active { background:#2563eb; color:#fff; }
.chrome label { color:#94a3b8; }
#viewport { width:100%; max-width:1280px; margin:0 auto; transition:width .2s; }
.proto-screen { display:none; padding:24px; } .proto-screen.active { display:block; }
.state-pill { background:#334155; border-radius:9999px; padding:2px 10px; font-size:12px; }
</style>
</head>
<body>
<nav class="chrome">
<strong style="color:#fff">040 Prototype</strong>
<span id="curState" class="state-pill"></span>
<label>Экран:</label>
<button data-screen="editor" class="active">Профиль</button>
<button data-screen="monitor">Монитор</button>
<button data-screen="results">Результаты</button>
<label>Состояние:</label>
<span id="stateButtons"></span>
</nav>
<div id="viewport"><main>
<!-- Screen 1: Load Profile Editor -->
<section id="editor" class="proto-screen active">
<div class="mb-8"><h1 class="text-3xl font-bold tracking-tight text-text">Нагрузочный профиль</h1>
<p class="text-text-muted text-sm">Дашборд: Операционный отчёт · env: prod</p></div>
<div class="grid lg:grid-cols-3 gap-4">
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6">
<h2 class="text-base font-medium mb-3">Параметры</h2>
<label class="block text-sm mb-2">Concurrency <input class="mt-1 block w-full border border-border-strong rounded-md p-2 text-sm" value="5" /></label>
<label class="block text-sm mb-2">Iterations <input class="mt-1 block w-full border border-border-strong rounded-md p-2 text-sm" value="10" /></label>
<div id="capClamp" class="hidden rounded-md bg-warning-light text-warning p-2 text-xs mb-2">Снижено до лимита среды (5)</div>
<div class="flex items-center gap-2 mb-2">
<input type="checkbox" checked /> Фильтры
<input type="checkbox" /> Viewport
<input type="checkbox" /> Роль
</div>
<div class="space-y-1 mb-3">
<div class="flex items-center justify-between text-sm"><span>Фильтр «region»</span>
<span id="needsContext" class="hidden rounded-full px-2 py-0.5 text-xs font-medium bg-warning-light text-warning">NEEDS_CONTEXT</span></div>
<div class="flex items-center justify-between text-sm"><span>Фильтр «status»</span><span class="text-success text-xs">✓ valid</span></div>
</div>
<label class="block text-sm mb-2">Error-rate % <input class="mt-1 block w-full border border-border-strong rounded-md p-2 text-sm" value="25" /></label>
<label class="block text-sm mb-3">p99 × <input class="mt-1 block w-full border border-border-strong rounded-md p-2 text-sm" value="3.0" /></label>
<button id="validateBtn" class="inline-flex items-center justify-center rounded-md bg-primary text-white hover:bg-primary h-10 px-4 text-sm w-full">Validate &amp; Preview</button>
<button id="startBtn" disabled class="mt-2 inline-flex items-center justify-center rounded-md bg-primary text-white h-10 px-4 text-sm w-full disabled:opacity-50">Start load run</button>
</div>
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6">
<h2 class="text-base font-medium mb-3">Матрица</h2>
<p id="matrixPreview" class="text-sm text-text-muted"></p>
</div>
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6">
<h2 class="text-base font-medium mb-3">Blast-radius</h2>
<p id="blastRadius" class="text-sm text-text-muted">Загрузка…</p>
</div>
</div>
</section>
<!-- Screen 2: Load Run Monitor -->
<section id="monitor" class="proto-screen">
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6">
<header class="flex items-center justify-between mb-4">
<h2 class="text-base font-medium">Load Run</h2>
<div class="flex items-center gap-2">
<span id="phaseBadge" class="rounded-full px-2.5 py-1 text-xs font-medium bg-info-light text-info">queued</span>
<span id="breakerBadge" class="rounded-full px-2.5 py-1 text-xs font-medium bg-success-light text-success">breaker: green</span>
<button id="stopBtn" class="rounded-md bg-destructive text-white h-10 px-4 text-sm">Stop</button>
</div>
</header>
<div class="flex items-center justify-between text-sm text-text-muted mb-3">
<span id="runId">run: run-f9a2</span><span id="inFlight">in-flight: 0</span>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-3">
<div class="rounded-md bg-surface-muted p-3"><span class="text-xs text-text-muted block mb-1">region=north</span><span id="tile1">0/20</span></div>
<div class="rounded-md bg-surface-muted p-3"><span class="text-xs text-text-muted block mb-1">region=south</span><span id="tile2">0/20</span></div>
<div class="rounded-md bg-surface-muted p-3"><span class="text-xs text-text-muted block mb-1">viewport 1366</span><span id="tile3">0/20</span></div>
</div>
<div id="findingsStrip" class="mt-4 space-y-1">
<div class="hidden rounded-md bg-warning-light text-warning p-2 text-sm" id="breakerReason">Прервано: error-rate 34% &gt; 25% — показать частичные результаты</div>
</div>
<div id="drainNote" class="hidden mt-2 rounded-md bg-info-light text-info p-2 text-sm">Завершение in-flight, новые не отправляются</div>
</div>
</section>
<!-- Screen 3: Results & Comparison -->
<section id="results" class="proto-screen">
<div class="mb-8"><h1 class="text-3xl font-bold tracking-tight text-text">Результаты</h1>
<p class="text-text-muted text-sm">run-f9a2 · база: run-f8a1</p></div>
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6">
<h2 class="text-base font-medium mb-3">Латентность (upstream), ms</h2>
<table class="w-full text-sm text-left divide-y divide-border border border-border rounded-lg">
<thead><tr class="text-text-muted text-xs"><th class="p-3">Чарт</th><th class="p-3">p50</th><th class="p-3">p90</th><th class="p-3">p95</th><th class="p-3">p99</th><th class="p-3">Кэш</th><th class="p-3 text-right">Δ p99 vs база</th></tr></thead>
<tbody class="divide-y divide-border">
<tr><td class="p-3">Выручка</td><td class="p-3">240</td><td class="p-3">520</td><td class="p-3">670</td><td class="p-3">920</td><td class="p-3 text-xs text-text-muted">miss→hit</td><td class="p-3 text-right text-success">-180</td></tr>
<tr><td class="p-3">Заказы</td><td class="p-3">310</td><td class="p-3">610</td><td class="p-3">780</td><td class="p-3">1150</td><td class="p-3 text-xs text-text-muted">hit</td><td class="p-3 text-right text-destructive">+40</td></tr>
</tbody>
</table>
<div id="consistency" class="hidden mt-4 rounded-md bg-warning-light text-warning p-3 text-sm">
<strong>Consistency violation</strong> · классифицировано как flakiness, не baseline drift
</div>
<p class="mt-3 text-xs text-success">Baseline catalog не изменён (SC-003)</p>
</div>
</section>
</main></div>
<script>
const screens = ["editor","monitor","results"];
const states = {
editor: ["idle","validating","matrix_ready","gate_required","needs_context","cap_clamped","permission_denied","start_error"],
monitor: ["queued","ramping","steady","draining","circuit_breaker_abort","stopped_by_user"],
results: ["ready","consistency"],
};
let current = { screen: "editor", state: "idle" };
function renderScreen(screen){
document.querySelectorAll(".proto-screen").forEach(s=>s.classList.remove("active"));
document.getElementById(screen).classList.add("active");
document.querySelectorAll(".chrome button[data-screen]").forEach(b=>b.classList.toggle("active", b.dataset.screen===screen));
renderStateButtons(screen);
}
function renderStateButtons(screen){
const w=document.getElementById("stateButtons"); w.innerHTML="";
(states[screen]||[]).forEach(st=>{
const b=document.createElement("button"); b.textContent=st; b.className=st===current.state?"active":"";
b.onclick=()=>{ current.state=st; applyState(screen,st); renderStateButtons(screen); }; w.appendChild(b);
});
document.getElementById("curState").textContent=screen+" / "+current.state;
}
function applyState(screen, st){
if(screen==="editor"){
const cap=document.getElementById("capClamp"), nc=document.getElementById("needsContext"), start=document.getElementById("startBtn");
cap.classList.toggle("hidden", st!=="cap_clamped");
nc.classList.toggle("hidden", st!=="needs_context");
const mp=document.getElementById("matrixPreview");
if(st==="matrix_ready"||st==="gate_required"||st==="needs_context"||st==="cap_clamped"){ mp.textContent="120 комбинаций × 8 чартов = 960 запросов"; start.disabled=false; }
else if(st==="validating"){ mp.textContent="валидация…"; start.disabled=true; }
else { mp.textContent="—"; start.disabled=true; }
const br=document.getElementById("blastRadius");
if(st==="matrix_ready"||st==="gate_required") br.innerHTML="3 других дашборда используют те же датасеты <span class='text-warning'>⚠ PROD</span>";
else if(st==="cap_clamped") br.textContent="3 других дашборда (кэш будет прогрет)";
}
if(screen==="monitor"){
const badge=document.getElementById("phaseBadge"), bkr=document.getElementById("breakerBadge");
badge.textContent=st; badge.className="rounded-full px-2.5 py-1 text-xs font-medium "+
(st==="circuit_breaker_abort"?"bg-destructive-light text-destructive":st==="draining"?"bg-warning-light text-warning":st==="queued"?"bg-surface-muted text-text-muted":"bg-info-light text-info");
bkr.textContent = st==="circuit_breaker_abort" ? "breaker: tripped" : "breaker: green";
bkr.className = "rounded-full px-2.5 py-1 text-xs font-medium "+(st==="circuit_breaker_abort"?"bg-destructive-light text-destructive":"bg-success-light text-success");
document.getElementById("breakerReason").classList.toggle("hidden", st!=="circuit_breaker_abort");
document.getElementById("drainNote").classList.toggle("hidden", st!=="draining");
document.getElementById("inFlight").textContent = st==="ramping"?"in-flight: 5":st==="steady"?"in-flight: 10":st==="draining"?"in-flight: 3 (drain)":"in-flight: 0";
if(st!=="queued"&&st!=="circuit_breaker_abort"&&st!=="stopped_by_user"){ document.getElementById("tile1").textContent="14/20"; document.getElementById("tile2").textContent="15/20"; document.getElementById("tile3").textContent="11/20"; }
else { document.getElementById("tile1").textContent="0/20"; document.getElementById("tile2").textContent="0/20"; document.getElementById("tile3").textContent="0/20"; }
}
if(screen==="results") document.getElementById("consistency").classList.toggle("hidden", st!=="consistency");
}
document.querySelectorAll(".chrome button[data-screen]").forEach(b=>b.onclick=()=>{ current.screen=b.dataset.screen; current.state=(states[b.dataset.screen]||[])[0]; renderScreen(current.screen); renderStateButtons(current.screen); applyState(current.screen,current.state); });
renderScreen("editor"); applyState("editor","idle");
</script>
</body>
</html>
<!doctype html><html lang="ru"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Нагрузочная проверка</title><link rel="stylesheet" href="../../prototype-ui.css"></head><body><div class="shell"><header class="topbar"><div class="brand">Superset Tools · BI testing</div><nav class="nav"><a href="#registry">Сценарии</a><a class="active" href="#runs">Запуски</a><a href="#automation">Автоматизация</a><a href="#health">Качество</a></nav><span class="badge warn">Controlled environment</span></header><main><div class="page-head"><div><div class="eyebrow">Load testing</div><h1>Нагрузочная проверка dashboard</h1><p class="sub">Проверяйте производительность в выделенной среде, не производственные данные.</p></div></div><div class="main-aside"><section class="card"><h2>Профиль</h2><label class="sub">Dashboard</label><select style="width:100%;margin:5px 0 12px"><option>FI-0080</option></select><label class="sub">Environment</label><select style="width:100%;margin:5px 0 12px"><option>PREPROD load-sandbox</option><option>PROD — запрещено policy</option></select><div class="cols-2 grid"><div><label class="sub">Virtual users</label><input style="width:100%;margin-top:5px" value="20"></div><div><label class="sub">Duration</label><input style="width:100%;margin-top:5px" value="5 min"></div></div><div class="notice warn"><strong>Safety preflight</strong><br>Read-only profile · capacity allocated · no browser/data mutation.</div><div class="actions"><button class="btn">Предпросмотр матрицы</button><button class="btn primary" onclick="setProtoState('running')">Запустить нагрузку</button></div></section><aside class="grid"><section class="card"><h2>Capacity</h2><p><strong>PREPROD</strong> · reserved 20 VU</p><p class="sub">Scenario, Verification и Load workloads используют общий capacity manager.</p></section><section id="monitor" class="card"><h2>Run status</h2><p><span class="badge">Не запущен</span></p><p class="sub">Результат покажет p50/p95/p99, errors и regression against baseline.</p></section></aside></div></main><div class="statebar"><span class="sub">State: <b id="proto-state"></b></span><button class="btn" onclick="setProtoState('idle')">Idle</button><button class="btn" onclick="setProtoState('running')">Running</button><button class="btn" onclick="setProtoState('result')">Result</button></div></div><script src="../../prototype-ui.js"></script><script>protoState('idle',s=>document.getElementById('monitor').innerHTML=s==='running'?'<h2>Run status</h2><p><span class="badge info">Running · 12/20 VU</span></p><p class="sub">p95: 1.8 sec · errors: 0</p>':s==='result'?'<h2>Run result</h2><p><span class="badge ok">PASS</span></p><p class="sub">p95: 1.9 sec · baseline: 1.7 sec</p>':'<h2>Run status</h2><p><span class="badge">Не запущен</span></p><p class="sub">Результат покажет p50/p95/p99, errors и regression against baseline.</p>')</script></body></html>

View File

@@ -125,6 +125,8 @@
- **LOAD-FR-013**: Scheduled load runs MUST evaluate overlap against deployment/maintenance windows of blast-radius-dependent dashboards and block or warning-gate conflicts per policy.
- **LOAD-FR-014**: RBAC MUST distinguish `dashboard:loadtest:execute` (PREPROD/staging) from `dashboard:loadtest:prod` (PROD-classified); unauthorized actors see `permission_denied`, never a confirm control (036 gate semantics).
- **LOAD-FR-015**: Comparison of two runs of the same profile MUST show per-chart latency deltas and consistency-finding deltas; comparison MUST NOT require both runs to be complete (partial-current vs baseline-run allowed).
- **LOAD-FR-020**: An opened InvestigationCase MAY ask the agent to construct/profile/compare a load experiment and autonomously run policy-permitted diagnostics. The agent MUST NOT bypass caps, mutate an active ramp, suppress a circuit breaker, or replace a required ActionApprovalGate.
- **LOAD-FR-021**: Circuit-breaker aborts and consistency findings MUST emit idempotent 036 InvestigationSignals with blast-radius and run provenance; 047 creates/updates Queue items and MUST NOT automatically start agent work.
### Key Entities

View File

@@ -93,6 +93,8 @@ Unique: `(environment_id, dataset_uuid, chart_uuid, dashboard_uuid)`.
Mirror of backend schemas, generated/hand-authored 1:1; cross-stack `@RELATION` edges declared in contracts/modules.md.
An agent may explain an impact record, assemble a revalidation/fan-out plan, and create scenario migration drafts from an opened InvestigationCase. Index construction, schema-diff severity, impact projection and snapshot pinning remain deterministic. Agent actions never rewrite lineage history or apply a recreate/migration candidate without the applicable policy decision.
## Invariants
1. Edge rows are disposable: full rebuild = delete+insert within one transaction; snapshot fingerprint flips atomically.

View File

@@ -1,350 +1 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>041 Dataset Lineage &amp; Blast Radius — Interactive Prototype</title>
<style>
/* ═══ Tailwind utility shim — values verbatim from frontend/tailwind.config.js ═══ */
.bg-surface-page { background-color: #f8fafc; }
.bg-surface-card { background-color: #ffffff; }
.bg-surface-muted { background-color: #f1f5f9; }
.bg-primary { background-color: #2563eb; }
.bg-primary-light { background-color: #eff6ff; }
.bg-destructive-light { background-color: #fef2f2; }
.bg-success-light { background-color: #f0fdf4; }
.bg-warning-light { background-color: #fffbeb; }
.bg-info-light { background-color: #f0f9ff; }
.border-border { border-color: #e2e8f0; }
.border-border-strong { border-color: #cbd5e1; }
.text-text { color: #0f172a; }
.text-text-muted { color: #64748b; }
.text-white { color: #ffffff; }
.text-primary { color: #2563eb; }
.text-destructive { color: #dc2626; }
.text-success { color: #16a34a; }
.text-warning { color: #b45309; }
.text-info { color: #0369a1; }
.hover\:bg-primary-hover:hover { background-color: #1d4ed8; }
.focus-visible\:ring-primary-ring:focus-visible { outline: none; box-shadow: 0 0 0 2px #ffffff, 0 0 0 4px #3b82f6; }
.rounded-md { border-radius: 0.375rem; }
.rounded-lg { border-radius: 0.5rem; }
.rounded-full { border-radius: 9999px; }
.border { border-width: 1px; }
.border-b { border-bottom-width: 1px; }
.divide-y > * + * { border-top-width: 1px; }
.divide-border > * + * { border-color: #e2e8f0; }
.shadow-sm { box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); }
.p-2 { padding: 0.5rem; }
.p-3 { padding: 0.75rem; }
.p-4 { padding: 1rem; }
.p-6 { padding: 1.5rem; }
.px-2 { padding-left: 0.5rem; padding-right: 0.5rem; }
.px-2\.5 { padding-left: 0.625rem; padding-right: 0.625rem; }
.px-4 { padding-left: 1rem; padding-right: 1rem; }
.py-0\.5 { padding-top: 0.125rem; padding-bottom: 0.125rem; }
.py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; }
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-3 { margin-bottom: 0.75rem; }
.mb-4 { margin-bottom: 1rem; }
.mb-6 { margin-bottom: 1.5rem; }
.mb-8 { margin-bottom: 2rem; }
.mt-1 { margin-top: 0.25rem; }
.mt-2 { margin-top: 0.5rem; }
.mr-2 { margin-right: 0.5rem; }
.gap-1 { gap: 0.25rem; }
.gap-1\.5 { gap: 0.375rem; }
.gap-2 { gap: 0.5rem; }
.gap-4 { gap: 1rem; }
.flex { display: flex; }
.inline-flex { display: inline-flex; }
.flex-col { flex-direction: column; }
.items-center { align-items: center; }
.items-start { align-items: flex-start; }
.items-end { align-items: flex-end; }
.justify-between { justify-content: space-between; }
.space-y-2 > * + * { margin-top: 0.5rem; }
.w-full { width: 100%; }
.h-10 { height: 2.5rem; }
.h-12 { height: 3rem; }
.min-w-\[160px\] { min-width: 160px; }
.text-xs { font-size: 0.75rem; line-height: 1rem; }
.text-sm { font-size: 0.875rem; line-height: 1.25rem; }
.text-base { font-size: 1rem; line-height: 1.5rem; }
.text-3xl { font-size: 1.875rem; line-height: 2.25rem; }
.font-medium { font-weight: 500; }
.font-bold { font-weight: 700; }
.font-mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.tracking-tight { letter-spacing: -0.025em; }
.underline { text-decoration: underline; }
.text-left { text-align: left; }
.text-right { text-align: right; }
.block { display: block; }
.grid { display: grid; }
.grid-cols-1 { grid-template-columns: 1fr; }
.transition-colors { transition: color .15s, background-color .15s, border-color .15s; }
.disabled\:opacity-50:disabled { opacity: 0.5; }
.disabled\:pointer-events-none:disabled { pointer-events: none; }
.animate-pulse { animation: pulse 2s cubic-bezier(.4,0,.6,1) infinite; }
@keyframes pulse { 50% { opacity: .5; } }
.animate-spin { animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.ml-auto { margin-left: auto; }
.block { display: block; }
@media (min-width: 1024px) { .lg\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0,1fr)); } }
.lg\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0,1fr)); }
/* ═══ Prototype chrome (NOT app UI) ═══ */
body { margin: 0; font-family: ui-sans-serif, system-ui, sans-serif; background: #f8fafc; }
.chrome { position: sticky; top: 0; z-index: 50; background: #0f172a; color: #e2e8f0; padding: 10px 16px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; font-size: 13px; }
.chrome button { background: #334155; color: #e2e8f0; border: 0; border-radius: 6px; padding: 6px 10px; cursor: pointer; }
.chrome button.active { background: #2563eb; color: #fff; }
.chrome label { color: #94a3b8; }
#viewport { width: 100%; margin: 0 auto; transition: width .2s; }
.proto-screen { display: none; padding: 24px; }
.proto-screen.active { display: block; }
.state-pill { background:#334155; border-radius:9999px; padding:2px 10px; font-size:12px; }
</style>
</head>
<body>
<!-- ═══ State Switcher (prototype chrome) ═══ -->
<nav class="chrome">
<strong style="color:#fff">041 Prototype</strong>
<span id="curState" class="state-pill"></span>
<label>Экран:</label>
<button data-screen="lineage" class="active">Панель зависимости</button>
<button data-screen="impact">Кросс-влияние</button>
<button data-screen="deprecation">Менеджер депрекации</button>
<button data-screen="fleet">Сводный отчёт</button>
<label>Состояние:</label>
<span id="stateButtons"></span>
<label>Вьюпорт:</label>
<button id="viewportToggle">1280px</button>
</nav>
<div id="viewport">
<main>
<!-- ═══ Screen 1: Dataset Lineage Panel ═══ -->
<section id="lineage" class="proto-screen active">
<div class="mb-8">
<h1 class="text-3xl font-bold tracking-tight text-text">Датасет: sales_orders</h1>
<p class="text-text-muted text-sm">Окружение: prod | <span id="lpFreshness">Индекс обновлён 5 мин назад</span></p>
</div>
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6 mb-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-base font-medium">Зависимые дашборды</h2>
<span id="lpImpactBadge" class="rounded-full px-2.5 py-1 text-xs font-medium bg-success-light text-success">impact: info</span>
</div>
<!-- loading -->
<div id="lp-loading" class="space-y-2">
<div class="h-10 bg-surface-muted rounded-md animate-pulse w-full"></div>
<div class="h-10 bg-surface-muted rounded-md animate-pulse w-full"></div>
<div class="h-10 bg-surface-muted rounded-md animate-pulse w-full"></div>
</div>
<!-- ready / stale / refresh_failed -->
<div id="lp-body">
<div class="flex items-center gap-2 mb-3">
<span id="lpSeverityBadge" class="rounded-full px-2.5 py-1 text-xs font-medium bg-success-light text-success">info</span>
<span class="text-sm text-text-muted">«Используется в 4 дашбордах, 11 чартах»</span>
<button id="lpRetry" class="ml-auto hidden text-primary underline text-sm">Повторить обновление</button>
</div>
<table class="w-full text-sm text-left divide-y divide-border border border-border rounded-lg">
<thead><tr class="text-text-muted text-xs"><th class="p-3">Дашборд</th><th class="p-3">Чарты</th><th class="p-3">Колонки</th><th class="p-3">Релиз / baseline</th><th class="p-3 text-right">Влияние</th></tr></thead>
<tbody class="divide-y divide-border">
<tr><td class="p-3 text-primary underline">Операционный отчёт</td><td class="p-3">5</td><td class="p-3 font-mono text-xs">amount, status</td><td class="p-3">v1.4 · 3 baseline</td><td class="p-3 text-right"><span class="rounded-full px-2.5 py-0.5 text-xs font-medium bg-destructive-light text-destructive">critical</span></td></tr>
<tr><td class="p-3 text-primary underline">Финансовая сводка</td><td class="p-3">3</td><td class="p-3 font-mono text-xs">revenue, cnt</td><td class="p-3">v1.2 · 2 baseline</td><td class="p-3 text-right"><span class="rounded-full px-2.5 py-0.5 text-xs font-medium bg-warning-light text-warning">warning</span></td></tr>
<tr><td class="p-3 text-primary underline">Воронка продаж</td><td class="p-3">2</td><td class="p-3 font-mono text-xs">status (conservative)</td><td class="p-3"></td><td class="p-3 text-right"><span class="rounded-full px-2.5 py-0.5 text-xs font-medium bg-info-light text-info">info</span></td></tr>
<tr><td class="p-3 text-primary underline">Региональная аналитика</td><td class="p-3">1</td><td class="p-3 font-mono text-xs">amount</td><td class="p-3">v1.1</td><td class="p-3 text-right"><span class="rounded-full px-2.5 py-0.5 text-xs font-medium bg-info-light text-info">info</span></td></tr>
</tbody>
</table>
<!-- deprecation action (gated) -->
<div class="mt-4 flex items-end gap-2">
<button class="inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 rounded-md bg-primary text-white hover:bg-primary-hover focus-visible:ring-primary-ring h-10 px-4 py-2 text-sm">Отметить устаревшим</button>
<button class="inline-flex items-center justify-center font-medium transition-colors rounded-md bg-surface-muted text-text h-10 px-4 py-2 text-sm">Запустить fan-out</button>
</div>
</div>
</div>
</section>
<!-- ═══ Screen 2: Cross-Dashboard Impact View ═══ -->
<section id="impact" class="proto-screen">
<div class="mb-8">
<h1 class="text-3xl font-bold tracking-tight text-text">Влияние изменения: sales_orders</h1>
<p class="text-text-muted text-sm">kind: column_removed | severity: critical | obs pair: b→a</p>
</div>
<div class="grid lg:grid-cols-2 gap-4">
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6">
<div class="flex items-center justify-between mb-3">
<span class="text-base font-medium">Операционный отчёт</span>
<span class="rounded-full px-2.5 py-1 text-xs font-medium bg-destructive-light text-destructive">critical</span>
</div>
<p class="text-sm text-text-muted mb-2">затронуты 2 чарта, 3 baseline-записи release 1.4</p>
<ul class="text-sm divide-y divide-border">
<li class="py-2">Колонка <code class="font-mono">amount</code> удалена — 2 чарта</li>
<li class="py-2">Baseline 1.4: 3 записи → stale</li>
<li class="py-2">Scenario-шаги: 4 (038 refs)</li>
</ul>
</div>
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6">
<div class="flex items-center justify-between mb-3">
<span class="text-base font-medium">Финансовая сводка</span>
<span class="rounded-full px-2.5 py-1 text-xs font-medium bg-warning-light text-warning">warning</span>
</div>
<p class="text-sm text-text-muted mb-2">затронут 1 чарт (conservative), 2 baseline-записи</p>
</div>
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6">
<div class="flex items-center justify-between mb-3">
<span class="text-base font-medium">Воронка продаж</span>
<span class="rounded-full px-2.5 py-1 text-xs font-medium bg-info-light text-info">info</span>
</div>
<p class="text-sm text-text-muted mb-2">аддитивная колонка — baselines не помечаются stale</p>
</div>
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6">
<div class="flex items-center justify-between mb-3">
<span class="text-base font-medium">Закрытый период</span>
<span class="rounded-full px-2.5 py-1 text-xs font-medium bg-destructive-light text-destructive">immutability_violation</span>
</div>
<p class="text-sm text-text-muted">ретроактивная дивергенция source_response_hash — не stale, а violation</p>
</div>
</div>
</section>
<!-- ═══ Screen 3: Deprecation Manager ═══ -->
<section id="deprecation" class="proto-screen">
<div class="mb-8">
<h1 class="text-3xl font-bold tracking-tight text-text">Депрекация датасетов</h1>
<p class="text-text-muted text-sm">Менеджер жизненного цикла: successor + grace window + миграция</p>
</div>
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6 space-y-4">
<!-- none -->
<div id="dep-none">
<p class="text-sm text-text-muted mb-4">Нет активных депрекаций.</p>
<form class="space-y-2 max-w-sm">
<label class="block text-sm">Грейс-окно (дни) <input class="mt-1 block w-full border border-border-strong rounded-md p-2 text-sm" value="14" /></label>
<label class="block text-sm">Преемник <input class="mt-1 block w-full border border-border-strong rounded-md p-2 text-sm" value="sales_orders_v2" /></label>
<button class="inline-flex items-center justify-center font-medium transition-colors rounded-md bg-primary text-white hover:bg-primary-hover h-10 px-4 py-2 text-sm">Пометить deprecated</button>
</form>
</div>
<!-- noticed / warning / expired_blocked -->
<div id="dep-record" class="hidden">
<div class="flex items-center justify-between mb-2">
<span class="font-medium">sales_orders → sales_orders_v2</span>
<span id="depChip" class="rounded-full px-2.5 py-1 text-xs font-medium bg-info-light text-info">noticed</span>
</div>
<p class="text-sm text-text-muted mb-2">Грейс-окно: <span id="depCountdown">14 дней</span> | миграция: 0 из 4</p>
<div class="h-2 w-full bg-surface-muted rounded-full mb-3"><div id="depProgress" class="h-2 bg-primary rounded-full" style="width:0%"></div></div>
<ul class="divide-y divide-border text-sm">
<li class="py-2 flex items-center justify-between">Операционный отчёт <span class="rounded-full px-2 py-0.5 text-xs font-medium bg-surface-muted text-text-muted">ожидает</span></li>
<li class="py-2 flex items-center justify-between">Финансовая сводка <span class="rounded-full px-2 py-0.5 text-xs font-medium bg-success-light text-success">мигрирован</span></li>
</ul>
</div>
<div id="dep-expired" class="hidden rounded-md bg-destructive-light text-destructive p-4 text-sm">
Датасет выведен из эксплуатации 3 дня назад. Преемник: <strong>sales_orders_v2</strong>. 2 дашборда ещё не перенесены.
</div>
</div>
</section>
<!-- ═══ Screen 4: Fleet Report ═══ -->
<section id="fleet" class="proto-screen">
<div class="mb-8">
<h1 class="text-3xl font-bold tracking-tight text-text">Сводный отчёт (fan-out)</h1>
<p class="text-text-muted text-sm">trigger: dataset_updated | план: fan-9f3c | impact: sales_orders</p>
</div>
<div class="rounded-lg border border-border bg-surface-card text-text shadow-sm p-6">
<ul class="divide-y divide-border text-sm">
<li class="py-3 flex items-center justify-between"><span>Операционный отчёт</span><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-success-light text-success">pass</span></li>
<li class="py-3 flex items-center justify-between"><span>Финансовая сводка</span><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-warning-light text-warning">warn · baseline re-approval</span></li>
<li class="py-3 flex items-center justify-between"><span>Воронка продаж</span><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-info-light text-info">inspection_only · нет baseline</span></li>
<li class="py-3 flex items-center justify-between"><span>Региональная аналитика</span><span class="rounded-full px-2.5 py-1 text-xs font-medium bg-success-light text-success">pass</span></li>
</ul>
<div class="mt-4 flex items-center gap-2">
<span class="rounded-full px-2.5 py-1 text-xs font-medium bg-surface-muted text-text-muted">2 pass</span>
<span class="rounded-full px-2.5 py-1 text-xs font-medium bg-warning-light text-warning">1 warn</span>
<span class="rounded-full px-2.5 py-1 text-xs font-medium bg-info-light text-info">1 inspection</span>
<button class="ml-auto inline-flex items-center justify-center font-medium transition-colors rounded-md bg-surface-muted text-text h-10 px-4 py-2 text-sm">Экспорт</button>
</div>
</div>
</section>
</main>
</div>
<script>
// ═══ Prototype state model ═══
const screens = ["lineage", "impact", "deprecation", "fleet"];
const states = {
lineage: ["loading", "ready", "stale_index", "refresh_failed"],
impact: ["loaded"],
deprecation: ["none", "noticed", "warning", "expired_blocked"],
fleet: ["loaded"],
};
let current = { screen: "lineage", state: "ready" };
function renderScreen(screen) {
document.querySelectorAll(".proto-screen").forEach(s => s.classList.remove("active"));
document.getElementById(screen).classList.add("active");
document.querySelectorAll(".chrome button[data-screen]").forEach(b =>
b.classList.toggle("active", b.dataset.screen === screen));
renderStateButtons(screen);
}
function renderStateButtons(screen) {
const wrap = document.getElementById("stateButtons");
wrap.innerHTML = "";
(states[screen] || ["loaded"]).forEach(st => {
const b = document.createElement("button");
b.textContent = st;
b.className = st === current.state ? "active" : "";
b.onclick = () => { current.state = st; applyState(screen, st); renderStateButtons(screen); };
wrap.appendChild(b);
});
document.getElementById("curState").textContent = screen + " / " + current.state;
}
function applyState(screen, st) {
const fresh = document.getElementById("lpFreshness");
const sev = document.getElementById("lpSeverityBadge");
const retry = document.getElementById("lpRetry");
if (screen === "lineage") {
document.getElementById("lp-loading").style.display = st === "loading" ? "block" : "none";
document.getElementById("lp-body").style.display = st === "loading" ? "none" : "block";
fresh.textContent = st === "stale_index" ? "данные могут быть устаревшими — последняя ошибка: timeout" : "Индекс обновлён 5 мин назад";
sev.textContent = st === "stale_index" || st === "refresh_failed" ? "stale_index" : "info";
sev.className = "rounded-full px-2.5 py-1 text-xs font-medium " +
(st === "stale_index" || st === "refresh_failed" ? "bg-warning-light text-warning" : "bg-success-light text-success");
retry.classList.toggle("hidden", st !== "refresh_failed");
} else if (screen === "deprecation") {
document.getElementById("dep-none").style.display = st === "none" ? "block" : "none";
document.getElementById("dep-record").classList.toggle("hidden", st === "none" || st === "expired_blocked");
document.getElementById("dep-expired").classList.toggle("hidden", st !== "expired_blocked");
if (st !== "none") {
const chip = document.getElementById("depChip");
chip.textContent = st;
chip.className = "rounded-full px-2.5 py-1 text-xs font-medium " +
(st === "expired_blocked" ? "bg-destructive-light text-destructive" : st === "warning" ? "bg-warning-light text-warning" : "bg-info-light text-info");
document.getElementById("depCountdown").textContent = st === "warning" ? "5 дней" : st === "expired_blocked" ? "истекло" : "14 дней";
document.getElementById("depProgress").style.width = st === "warning" ? "75%" : st === "expired_blocked" ? "100%" : "25%";
}
}
}
document.querySelectorAll(".chrome button[data-screen]").forEach(b =>
b.onclick = () => { current.screen = b.dataset.screen; current.state = (states[b.dataset.screen]||[])[1] || "loaded"; renderScreen(current.screen); renderStateButtons(current.screen); applyState(current.screen, current.state); });
let mobile = false;
document.getElementById("viewportToggle").onclick = () => {
mobile = !mobile;
document.getElementById("viewport").style.maxWidth = mobile ? "375px" : "1280px";
document.getElementById("viewport").style.margin = mobile ? "0 auto" : "0 auto";
document.getElementById("viewportToggle").textContent = mobile ? "375px" : "1280px";
};
renderScreen("lineage");
applyState("lineage", "ready");
</script>
</body>
</html>
<!doctype html><html lang="ru"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Влияние изменений данных</title><link rel="stylesheet" href="../../prototype-ui.css"></head><body><div class="shell"><header class="topbar"><div class="brand">Superset Tools · BI testing</div><nav class="nav"><a class="active" href="#registry">Сценарии</a><a href="#runs">Запуски</a><a href="#automation">Автоматизация</a><a href="#health">Качество</a></nav><span class="badge info">Lineage</span></header><main><div class="page-head"><div><div class="eyebrow">Dataset impact</div><h1>Влияние изменения: sales_orders</h1><p class="sub">Перед изменением датасета поймите, какие dashboardы и сценарии потребуют ревалидации.</p></div><button class="btn primary" onclick="setProtoState('stale')">Смоделировать изменение</button></div><div class="main-aside"><section class="card"><h2>Цепочка зависимости</h2><div class="flow"><span>sales_orders</span><b></b><span>Chart: Sales by region</span><b></b><span>Dashboard: FI-0080</span><b></b><span>Scenario: XLSX reconciliation</span></div><h2 style="margin-top:24px">Затронутые проверки</h2><table class="table"><thead><tr><th>Scenario</th><th>Причина</th><th>Риск</th><th></th></tr></thead><tbody><tr><td><strong>XLSX reconciliation</strong></td><td>dataset lineage changed</td><td><span class="badge warn">Warning</span></td><td><button class="btn">Открыть</button></td></tr><tr><td><strong>Фильтры и метрики</strong></td><td>metric definition changed</td><td><span class="badge danger">Critical</span></td><td><button class="btn">Открыть</button></td></tr></tbody></table></section><aside class="grid"><section id="status" class="card"><h2>Index status</h2><p><span class="badge ok">Актуален</span></p><p class="sub">Последнее обновление: 4 минуты назад.</p></section><section class="card"><h2>Что сделать</h2><p class="sub">Lineage не меняет сценарий автоматически. Он создаёт deduplicated staleness signal; аналитик запускает revalidate и принимает новую revision.</p><button class="btn">Открыть Registry</button></section></aside></div></main><div class="statebar"><span class="sub">State: <b id="proto-state"></b></span><button class="btn" onclick="setProtoState('ready')">Ready</button><button class="btn" onclick="setProtoState('stale')">Signals</button></div></div><script src="../../prototype-ui.js"></script><script>protoState('ready',s=>document.getElementById('status').innerHTML=s==='stale'?'<h2>Index status</h2><p><span class="badge warn">2 signals created</span></p><p class="sub">Registry aggregates active signals. READY невозможен, пока остаётся critical signal.</p>':'<h2>Index status</h2><p><span class="badge ok">Актуален</span></p><p class="sub">Последнее обновление: 4 минуты назад.</p>')</script></body></html>

View File

@@ -63,7 +63,7 @@ Resolves design decisions Q1Q3 and audit gaps E2/E5 after code verification a
- **`schema_hash` = physical identity** (`column_name + type + nullable`). A pure relabel (`verbose_name` change only, `column_name` unchanged) does NOT bump the hash and produces no diff at all — it is invisible to impact projection and scenario churn. This is the agreed Q1.
- **`schema_payload` carries full info** including per-column `verbose_name` — used for rename disambiguation and label display resolution.
- Superset itself resolves columns/metrics by `metric_name` OR `verbose_name` via its `verbose_map` (`superset/connectors/sqla/models.py`) — confirms the label-aware matching design.
- **Scenario refs (`context.query_model.{col}`, 038) carry only the physical `column_name`** as the authoritative key. Labels are resolved as a render-time display layer from the observation/lineage index — never stored in the ref, never in `revision_hash`. Optional denormalized `label` in a ref is allowed but excluded from `revision_hash`. (Q2)
- **Scenario refs (`context.query_model.{col}`, 038) carry only the physical `column_name`** as the authoritative key. Labels are resolved as a render-time display layer from the observation/lineage index — never stored in the ref or executable `content_hash`. Optional denormalized `label` in a ref is allowed but excluded from `content_hash`. (Q2)
### R9b — Dataset Metrics as a Required Consumption Vector

View File

@@ -147,6 +147,7 @@
- **LIN-FR-014**: RBAC MUST gate mutations: `dataset:lineage:refresh` (index rebuild), `dataset:deprecation:manage` (mark/migrate), `dataset:fanout:trigger` (PROD-classified fan-out requires 036 approval-gate semantics with reason).
- **LIN-FR-015**: The blast-radius graph is two-level (dataset ← chart ← dashboard) by construction; fan-out MUST order by impact severity only. Chained SQL-source lineage and cycle detection are out of scope.
- **LIN-FR-017**: Impact projection MUST respect `projection_confidence`: a schema change marks exact-confidence charts affected only when the changed column/metric is in their consumed set; conservative-confidence charts (or expressions) are marked affected on any schema change to their dataset, with the conservative basis shown to the user.
- **LIN-FR-021**: Impact, deprecation, fan-out and recreated-dataset findings MUST emit idempotent 036 InvestigationSignals. An analyst-opened case MAY use an agent to explain blast radius, build a revalidation plan and save validated scenario revisions; deterministic impact records remain immutable evidence and never auto-start agent work.
### Key Entities

View File

@@ -15,7 +15,7 @@
|----------|-------------:|----------------|---------|
| spec.md | 26768 | 2026-08-07 | `05376064c101e49212debbcd49b42cb0be0983132db22f7a8480de25b64a6a08` |
| plan.md | 8578 | 2026-08-07 | `3addce567c9d8352dc9f292a8fec059b73a950acd5467a2210539dc5f400a792` |
| tasks.md | 13825 | 2026-08-07 | `f0827e4cc31da8473cb7881cbbd24e8ccfc6b67eace16e9773e7dd3ec4a58e8e` |
| tasks.md | 14046 | 2026-08-10 | `8855963a5de3a2b67743b4a92f7ab012edeb9376a12327cfdbbc20033af816a0` |
| traceability.md | 4595 | 2026-08-07 | `2d999a60e1489db4f1bc4dc61a696b7fb1017244cde5068a3ad6b0e159e3912f` |
| contracts/modules.md | 9765 | 2026-08-04 | `8d9fa9d99aad966cf0a8c94f4aef360f572f13c0589d69290d46edc4e2a236f2` |
| data-model.md | 5637 | 2026-08-07 | `4ed4dfd36b8216a56ee92a3b19c20f91ad15febadeeb356cbd4646bafb26848b` |

View File

@@ -12,7 +12,8 @@
## Revisions (SCREG-FR-003)
- [ ] CHK005 Edit creates immutable revision with parent link; current_revision advances
- [ ] CHK005 Edit creates immutable candidate revision with parent link; save does not advance current_revision
- [ ] CHK005a Activation atomically advances current_revision only after eligibility and policy/gate checks
- [ ] CHK006 Runs pin scenario_id (UUID) + revision_id (UUID) + content_hash snapshot
- [ ] CHK007 Revision diff (added/changed/removed) available

View File

@@ -13,8 +13,8 @@
# @ingroup ScenarioRegistry
# @BRIEF List/search/filter persisted scenarios by name, tag, dashboard, status, owner.
# @PRE caller has scenario:view; filters validated.
# @POST returns paged ScenarioRegistryEntry[] with derived health; stable order.
# @SIDE_EFFECT read-only; derives health from run history.
# @POST returns paged ScenarioRegistryEntry[] with 047-provided overall_attention/unknown health; stable order.
# @SIDE_EFFECT read-only; never derives health locally.
# @TEST_EDGE empty->empty state; filter unknown dashboard->empty; LARGE->paged.
def list_scenarios(db, filters, page, page_size): ...
# #endregion ScenarioRegistry.List
@@ -31,15 +31,25 @@ def get_scenario(db, scenario_id): ...
# #region ScenarioRegistry.RevisionChain [C:4] [TYPE Function] [SEMANTICS scenario,registry,revision,chain]
# @ingroup ScenarioRegistry
# @BRIEF Append an immutable revision and advance current_revision on edit.
# @PRE base revision hash matches; edit is HITL-approved.
# @POST new ScenarioRevision row with parent link; current_revision advanced; no mutation of prior rows.
# @BRIEF Append an immutable candidate revision on validated save.
# @PRE base revision id/digest matches; deterministic validation and delegated policy permit save.
# @POST new ScenarioRevision candidate row with parent link; current_revision unchanged; no mutation of prior rows.
# @SIDE_EFFECT DB write; audit log.
# @INVARIANT prior revisions are immutable and never mutated.
# @TEST_EDGE stale base->409; edit->new revision with parent.
def create_revision(db, scenario_id, base_hash, graph, change_summary): ...
def create_revision(db, scenario_id, base_revision_id, validated_graph_handle, agent_action_id): ...
# #endregion ScenarioRegistry.RevisionChain
# #region ScenarioRegistry.ActivateRevision [C:4] [TYPE Function] [SEMANTICS scenario,registry,revision,activation]
# @ingroup ScenarioRegistry
# @BRIEF Atomically promote an eligible candidate revision to current.
# @PRE materialized/validated/automation eligibility holds; actor or AgentAction passes DelegatedAuthorityPolicy, or bound ActionApprovalGate is consumed.
# @POST exactly one revision is current and current_revision points to it; audit/policy provenance written.
# @INVARIANT save does not activate; current-policy automation resolves only this pointer.
# @TEST_EDGE incompatible family->409; policy gate->202 no promotion; competing promotion->409.
def activate_current_revision(db, scenario_id, revision_id, actor, agent_action_id=None): ...
# #endregion ScenarioRegistry.ActivateRevision
# #region ScenarioRegistry.Diff [C:3] [TYPE Function] [SEMANTICS scenario,registry,diff,revision]
# @ingroup ScenarioRegistry
# @BRIEF Compute added/changed/removed between two revisions.
@@ -87,8 +97,8 @@ def apply_staleness(db, signals): ...
# #region ScenarioRegistry.Health [C:3] [TYPE Function] [SEMANTICS scenario,registry,health,flakiness]
# @ingroup ScenarioRegistry
# @BRIEF Derive scenario health from run history.
# @BRIEF Read the 047-derived contextual ScenarioHealth badge projection.
# @POST returns pass/warn/fail with success rate and flakiness ratio.
def derive_health(db, scenario_id): ...
def get_health_badge(analytics_client, scenario_id): ...
# #endregion ScenarioRegistry.Health
#endregion ScenarioRegistry.Modules

View File

@@ -40,6 +40,7 @@ paths:
- { name: q, in: query, schema: { type: string } }
- { name: dashboard_id, in: query, schema: { type: integer } }
- { name: status, in: query, schema: { type: string, enum: [DRAFT,READY,STALE,NEEDS_REVALIDATION,BLOCKED,DISABLED,DEPRECATED,ARCHIVED] } }
- { name: tag, in: query, schema: { type: string } }
- { name: owner, in: query, schema: { type: string } }
- { name: page, in: query, schema: { type: integer, default: 1 } }
- { name: page_size, in: query, schema: { type: integer, default: 25 } }
@@ -83,12 +84,14 @@ paths:
operationId: scenarioRegistry.clone
summary: Clone a scenario
security: [{ bearerAuth: [] }]
parameters: [{ name: scenario_id, in: path, required: true, schema: { type: string } }]
responses: { "200": { description: New scenario id } }
/api/dashboard-testing/scenarios/{scenario_id}/archive:
post:
operationId: scenarioRegistry.archive
summary: Archive a scenario (terminal non-deleting state)
security: [{ bearerAuth: [] }]
parameters: [{ name: scenario_id, in: path, required: true, schema: { type: string } }]
requestBody: { required: true, content: { application/json: { schema: { type: object, required: [reason], properties: { reason: { type: string } } } } } }
responses: { "200": { description: Archived }, "403": { description: Permission denied } }
/api/dashboard-testing/scenarios/{scenario_id}/restore:
@@ -96,12 +99,14 @@ paths:
operationId: scenarioRegistry.restore
summary: Restore an archived scenario
security: [{ bearerAuth: [] }]
parameters: [{ name: scenario_id, in: path, required: true, schema: { type: string } }]
responses: { "200": { description: Restored } }
/api/dashboard-testing/scenarios/{scenario_id}/staleness:
get:
operationId: scenarioRegistry.staleness
summary: List staleness signals for a scenario
security: [{ bearerAuth: [] }]
parameters: [{ name: scenario_id, in: path, required: true, schema: { type: string } }]
responses: { "200": { description: Staleness signals } }
/api/dashboard-testing/scenarios/{scenario_id}/transition:
post:
@@ -130,6 +135,28 @@ paths:
- { name: scenario_id, in: path, required: true, schema: { type: string } }
- { name: revision_id, in: path, required: true, schema: { type: string } }
responses: { "200": { description: RevisionMaterialization } }
/api/dashboard-testing/scenarios/{scenario_id}/revisions/{revision_id}/activate:
post:
operationId: scenarioRegistry.activateCurrentRevision
summary: Promote an eligible candidate revision to the current revision
description: Save and activation are separate. The server evaluates delegated authority and may return an inline approval gate.
security: [{ bearerAuth: [] }]
parameters:
- { name: scenario_id, in: path, required: true, schema: { type: string } }
- { name: revision_id, in: path, required: true, schema: { type: string } }
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
agent_action_id: { type: string, nullable: true }
reason: { type: string }
responses:
"200": { description: Candidate atomically promoted to current }
"202": { description: ActionApprovalGate created; no activation yet }
"409": { description: Candidate is not activation-eligible or current pointer changed }
components:
securitySchemes:
bearerAuth: { type: http, scheme: bearer }
@@ -166,7 +193,8 @@ components:
change_summary: { type: object }
created_by: { type: string }
created_at: { type: string, format: date-time }
is_current: { type: boolean }
activation_status: { type: string, enum: [candidate, current] }
is_current: { type: boolean, readOnly: true }
ScenarioDetail:
type: object
properties:

View File

@@ -14,10 +14,12 @@ Indexes: dashboard_id, owner_id, lifecycle_status, tags (GIN), name (trigram for
## ScenarioRevision — revision identity (revision for #7)
Fields: revision_id (UUID — unique immutable identity), scenario_id, content_hash (SHA-256 of the **executable** canonical graph only, per 038 rules — timestamps/display-only excluded), parent_revision_id (nullable), graph_snapshot (DashboardTestScenario JSON), execution_template_hash (hash of revision-derived execution template only; no environment/parameters/baselines), template_version, schema_version, compatibility_family, change_summary (added/changed/removed), created_by, created_at, is_current (bool).
Fields: revision_id (UUID — unique immutable identity), scenario_id, content_hash (SHA-256 of the **executable** canonical graph only, per 038 rules — timestamps/display-only excluded), parent_revision_id (nullable), graph_snapshot (DashboardTestScenario JSON), execution_template_hash (hash of revision-derived execution template only; no environment/parameters/baselines), template_version, schema_version, compatibility_family, change_summary (added/changed/removed), created_by, created_at, activation_status (`candidate|current`), activated_by?, activated_at?, activation_agent_action_id?. `is_current` is a read-model convenience derived only from `activation_status=current`.
**Metadata vs executable split (#8)**: entity-metadata edits (name/description/tags) update `ScenarioRegistryEntry` fields and DO NOT create a new executable `ScenarioRevision` (content_hash unchanged). Only executable-graph edits create a new revision. `revision_id` (UUID) is the unique identity; `content_hash` detects content change.
**Clone provenance**: cloning creates a new `scenario_id` and a new initial revision with `parent_revision_id=null`; `ForkProvenance { source_scenario_id, source_revision_id }` preserves its origin. Cross-scenario revisions MUST NOT be joined by `parent_revision_id`.
## CreateScenario — server-owned handle + outbox saga (#1/#5/#6)
`CreateScenario` never accepts an arbitrary client graph, draft pack, or owner. The authoring path creates server-owned immutable handles:
@@ -28,6 +30,14 @@ The authenticated principal supplies ownership; the client supplies only `{compi
Git/filesystem materialization is NOT part of the DB transaction. An idempotent worker consumes the outbox event, writes reference artifacts (`scenario.yaml`, reference `runner.plan.json`) keyed by content hash, and updates `RevisionMaterialization` from `pending → materialized | failed`. A failed materialization is retryable without duplicating Registry rows; the DB revision remains authoritative.
Agent-created registry mutations use the same server-owned handles and transaction. `created_by` records the agent identity and delegated analyst; `agent_run_id?` and `investigation_case_id?` preserve provenance. A delegated agent may save a validated immutable revision but cannot silently bypass lifecycle transitions, object ACL, materialization, or a required ActionApprovalGate.
## Revision save and activation are separate operations
The initial revision created with a new scenario is `current`. Every later executable save creates an immutable `candidate`; saving never changes `ScenarioRegistryEntry.current_revision`. `ActivateCurrentRevision` is a separate atomic operation: it makes exactly one candidate current, updates the entry pointer, and records actor/agent/policy/gate provenance. A candidate is activation-eligible only if validation and materialization succeeded, it is `automation_eligible`, contains no human step for automation adoption, passes required verification, does not increase risk outside policy, and is compatible with the current revision (`compatibility_family` unchanged unless an explicit analyst-approved migration policy permits it).
The server evaluates the versioned 036 `DelegatedAuthorityPolicy`. An agent may activate only when its recorded policy snapshot explicitly has `may_activate_current_revision=true`; otherwise the operation creates/consumes an `ActionApprovalGate`. Schedules with `revision_policy=current` resolve the pointer only after this atomic activation; pinned schedules may use only an explicitly selected eligible revision.
## RevisionMaterialization and OutboxEvent
- `RevisionMaterialization`: revision_id, status (pending|materialized|failed), artifact_manifest_hash, attempt_count, last_error, materialized_at.
@@ -51,15 +61,19 @@ Every transition writes an audit record: scenario_id, from, to, actor_id, reason
## ScenarioStalenessSignal
Fields: id, scenario_id, kind (dashboard_release_diff | lineage_blast_radius | reference_removed), severity (info/warning/critical), reason, affected_refs (chart/filter/metric/selector), detected_at, resolved_at (nullable), source (037 StructureDiff | 041 lineage).
Fields: id, scenario_id, source_type, source_fingerprint, kind (dashboard_release_diff | lineage_blast_radius | reference_removed), severity (info/warning/critical), reason, affected_ref, detected_at, resolved_at (nullable), source (037 StructureDiff | 041 lineage). Unique identity is `(scenario_id, source_type, source_fingerprint, affected_ref)`; repeated events upsert rather than duplicate a signal. Lifecycle derives from the aggregate of active signals: READY is permitted only when no active critical/blocking signal remains.
## HealthDerivation
## Health projection
Derived per scenario from run history: last N runs (default 30), success rate, flakiness ratio, infrastructure-failure ratio. Health = pass if success rate >= threshold, warn if flaky, fail if blocking failures.
042 does not derive health. It consumes only 047 `ScenarioHealth.overall_attention` as the registry badge; if analytics is unavailable the badge is `unknown`. The algorithm and historical windows belong exclusively to 047.
## RBAC Scopes
The Registry is also the portfolio entry surface for Investigation Queue. Staleness and health signals may link to queue items; opening one opens its persistent agent case rather than a modal or an automatic chat.
- scenario:view, scenario:create, scenario:edit, scenario:archive, scenario:run (separate from edit), scenario:delete (admin, empty-history only).
## Object-level authorization
- scenario:view, scenario:create, scenario:edit, scenario:archive, scenario:run (separate from edit). MVP is archive-only: there is no hard-delete route, even for administrators.
Every registry/run/evidence decision additionally requires the intersection of scenario permission, dashboard ACL, environment ACL, artifact/evidence ACL, and the caller's effective Superset/RLS access. A registry permission alone never grants screenshot, XLSX, VLM, or result access.
## Storage Notes

View File

@@ -4,7 +4,7 @@
## Summary
Persist user-created dashboard test scenarios as a first-class registry projection with list/search/detail, immutable revision chains, a lifecycle state machine (archive-not-delete), staleness detection via 037 StructureDiff + 041 lineage, and health derivation. This is the queryable source of truth shared by the Scenario Editor (043) and Scenario Execution Engine (044), and fixes the currently-missing `GET /scenarios/{id}` route.
Persist user-created dashboard test scenarios as a first-class registry projection with list/search/detail, immutable candidate/current revision chains, a lifecycle state machine (archive-not-delete), staleness detection via 037 StructureDiff + 041 lineage, and a 047-consumed health badge. This is the queryable source of truth shared by the Scenario Editor (043) and Scenario Execution Engine (044), and fixes the currently-missing `GET /scenarios/{id}` route.
## Technical Context

View File

@@ -1,196 +1,8 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>042 — Scenario Registry Prototype</title>
<style>
:root{
--bg:#0f1115; --panel:#171a21; --panel2:#1d2129; --border:#2a2f3a;
--text:#e6e8ee; --muted:#9aa3b2; --accent:#4f8cff; --green:#2ecc71;
--warn:#f39c12; --red:#e74c3c; --muted2:#6b7280;
}
*{box-sizing:border-box}
body{margin:0;font-family:system-ui,Segoe UI,Roboto,sans-serif;background:var(--bg);color:var(--text);line-height:1.45}
.wrap{max-width:1180px;margin:0 auto;padding:20px}
h1{font-size:20px;margin:0 0 4px}
.sub{color:var(--muted);font-size:13px;margin-bottom:20px}
.toolbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:16px}
input,select,button{background:var(--panel2);border:1px solid var(--border);color:var(--text);border-radius:8px;padding:8px 12px;font-size:13px}
input{flex:1;min-width:180px}
button{cursor:pointer}
button.primary{background:var(--accent);border-color:var(--accent);color:#fff}
button.ghost{background:transparent}
table{width:100%;border-collapse:collapse}
th,td{text-align:left;padding:10px 12px;border-bottom:1px solid var(--border);font-size:13px}
th{color:var(--muted);font-weight:500;font-size:12px;text-transform:uppercase;letter-spacing:.4px}
tr:hover{background:var(--panel)}
.badge{display:inline-block;padding:2px 9px;border-radius:20px;font-size:11px;font-weight:600;white-space:nowrap}
.b-ready{background:rgba(46,204,113,.15);color:var(--green)}
.b-warn{background:rgba(243,156,18,.15);color:var(--warn)}
.b-fail{background:rgba(231,76,60,.15);color:var(--red)}
.b-stale{background:rgba(120,130,150,.18);color:var(--muted2)}
.b-draft{background:rgba(79,140,255,.15);color:var(--accent)}
.b-needs{background:rgba(243,156,18,.15);color:var(--warn)}
.health{font-weight:600}
.h-pass{color:var(--green)} .h-warn{color:var(--warn)} .h-fail{color:var(--red)}
/* detail */
.header{display:flex;justify-content:space-between;align-items:flex-start;gap:16px;flex-wrap:wrap;margin-bottom:16px}
.meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px;margin-bottom:16px}
.meta .kv{background:var(--panel);border:1px solid var(--border);border-radius:8px;padding:10px}
.meta .k{font-size:11px;color:var(--muted);text-transform:uppercase}
.meta .v{font-size:15px;font-weight:600}
.tabs{display:flex;gap:2px;border-bottom:1px solid var(--border);margin-bottom:16px}
.tab{padding:8px 14px;cursor:pointer;color:var(--muted);border-bottom:2px solid transparent;font-size:13px}
.tab.active{color:var(--text);border-bottom-color:var(--accent)}
.card{background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:14px}
.bar{height:8px;background:var(--panel2);border-radius:4px;overflow:hidden;margin-top:6px}
.bar>div{height:100%;background:var(--accent)}
.graph{display:flex;flex-direction:column;gap:2px}
.step{display:flex;gap:8px;align-items:center;padding:6px 8px;border-radius:6px;font-size:13px}
.step .arrow{color:var(--muted2)}
.st-ok{color:var(--green)} .st-run{color:var(--accent)} .st-pend{color:var(--muted2)}
.banner{padding:10px 14px;border-radius:8px;margin-bottom:14px;font-size:13px;display:flex;gap:10px;align-items:center;justify-content:space-between}
.banner.stale{background:rgba(243,156,18,.12);border:1px solid rgba(243,156,18,.4)}
.banner.block{background:rgba(231,76,60,.12);border:1px solid rgba(231,76,60,.4)}
.empty{text-align:center;padding:60px 20px;color:var(--muted)}
.state-switcher{position:fixed;right:14px;top:14px;background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:10px;font-size:12px;width:230px;z-index:50}
.state-switcher h4{margin:0 0 8px;color:var(--muted);font-size:11px;text-transform:uppercase}
.state-switcher button{display:block;width:100%;margin:3px 0;text-align:left}
@media(max-width:768px){ .toolbar{flex-direction:column} .state-switcher{position:static;width:auto;margin-bottom:12px} }
</style>
</head>
<body>
<div class="wrap">
<h1>Test scenarios</h1>
<div class="sub">042 — Scenario Registry &amp; Lifecycle · <span id="stateLabel">State: loaded</span></div>
<!-- Registry List -->
<section id="listView">
<div class="toolbar">
<input id="search" placeholder="Search scenarios...">
<select id="fDash"><option>Dashboard: All</option><option>Revenue BI</option><option>FI-0080</option></select>
<select id="fStatus"><option>Status: All</option><option>Ready</option><option>Needs review</option><option>Stale</option><option>Draft</option></select>
<button class="primary" onclick="setState('empty_create')">+ Create scenario</button>
</div>
<table>
<thead><tr><th>Scenario</th><th>Dashboard</th><th>Status</th><th>Last run</th><th>Health</th></tr></thead>
<tbody>
<tr onclick="setState('detail')"><td><strong>Revenue filters regression</strong></td><td>Revenue BI</td><td><span class="badge b-ready">Ready</span></td><td>2h ago</td><td><span class="health h-pass">✓ Pass</span></td></tr>
<tr onclick="setState('detail')"><td><strong>XLSX reconciliation</strong></td><td>FI-0080</td><td><span class="badge b-needs">Needs review</span></td><td>yesterday</td><td><span class="health h-warn">⚠ Warn</span></td></tr>
<tr onclick="setState('detail_stale')"><td><strong>Closed period validation</strong></td><td>AR Aging</td><td><span class="badge b-stale">Stale</span></td><td>3d ago</td><td><span class="health h-fail">✕ Fail</span></td></tr>
<tr onclick="setState('detail')"><td><strong>Mobile rendering</strong></td><td>Sales</td><td><span class="badge b-draft">Draft</span></td><td>Never</td><td></td></tr>
</tbody>
</table>
</section>
<!-- Empty state -->
<section id="emptyView" class="empty" hidden>
<div style="font-size:40px;margin-bottom:8px">📋</div>
<h3>No scenarios yet</h3>
<p>Create a test scenario from a dashboard to see it here.</p>
<button class="primary" onclick="setState('list')">+ Create scenario</button>
</section>
<!-- Detail -->
<section id="detailView" hidden>
<div class="header">
<div>
<h1>XLSX reconciliation <span class="muted" style="font-size:14px;color:var(--muted)">— FI-0080</span></h1>
<div class="sub" style="margin-bottom:0">
<span class="badge b-ready">Ready</span> · Revision r17 · Dashboard FI-0080 · Coverage 17/19 ·
Automation 15 automated / 2 manual · Last run <strong class="h-pass">PASS</strong> · 37 min ago
</div>
</div>
<div style="display:flex;gap:8px">
<button class="primary" onclick="setState('run_monitor')">▶ Run</button>
<button onclick="setState('detail')">Edit</button>
<button class="ghost"></button>
</div>
</div>
<div id="staleBanner" class="banner stale" hidden>
<span>Scenario references <strong>chart 43</strong> which changed in release r18 (StructureDiff: filter_scope_change). Marked <strong>NEEDS_REVALIDATION</strong>.</span>
<div><button onclick="this.parentElement.parentElement.hidden=true">Revalidate</button><button class="ghost" onclick="this.parentElement.parentElement.hidden=true">Dismiss</button></div>
</div>
<div class="tabs">
<div class="tab active">Overview</div>
<div class="tab">Steps</div>
<div class="tab">Parameters</div>
<div class="tab">Baselines</div>
<div class="tab">Runs</div>
<div class="tab">Revisions</div>
<div class="tab">Artifacts</div>
</div>
<div class="card">
<div class="meta">
<div class="kv"><div class="k">Status</div><div class="v">Ready</div></div>
<div class="kv"><div class="k">Revision</div><div class="v">r17</div></div>
<div class="kv"><div class="k">Coverage</div><div class="v">17/19</div></div>
<div class="kv"><div class="k">Automation</div><div class="v">15 auto / 2 manual</div></div>
<div class="kv"><div class="k">Last run</div><div class="v h-pass">PASS · 37m</div></div>
<div class="kv"><div class="k">Baseline set</div><div class="v">v31</div></div>
</div>
<div style="font-size:13px;color:var(--muted);margin-bottom:6px">Steps by phase</div>
<div class="graph">
<div class="step"><span class="st-ok"></span> 01 Open dashboard</div>
<div class="step"><span class="st-ok"></span> 02 Apply filters <span class="arrow"></span></div>
<div class="step" style="padding-left:22px"><span class="st-ok"></span> 03 Query chart API</div>
<div class="step" style="padding-left:22px"><span class="st-ok"></span> 04 Download XLSX</div>
<div class="step"><span class="st-run"></span> 05 Compare values</div>
<div class="step"><span class="st-pend"></span> 06 Screenshot</div>
<div class="step"><span class="st-pend"></span> 07 Visual validation</div>
<div class="step"><span class="st-pend"></span> 08 Human review</div>
</div>
</div>
</section>
<!-- Run Monitor stub (owned by 045) -->
<section id="runMonitorView" hidden>
<div class="card">
<h2 style="font-size:16px">Scenario run #SR-1842 <span class="badge b-ready">RUNNING</span></h2>
<div class="sub">FI-0080 · revision r17 · PREPROD · Elapsed 02:31 · Progress 11 / 18</div>
<div class="bar"><div style="width:61%"></div></div>
<div style="margin-top:12px">
<div class="step"><span class="st-ok"></span> Context 0.2s</div>
<div class="step"><span class="st-ok"></span> Open dashboard 1.7s</div>
<div class="step"><span class="st-ok"></span> Query Superset 0.6s</div>
<div class="step"><span class="st-run"></span> Capture screenshot</div>
<div class="step"><span class="st-pend"></span> VLM analysis</div>
<div class="step"><span class="st-pend"></span> Human review</div>
</div>
<p class="sub" style="margin-top:12px">Full live monitor is spec 045.</p>
</div>
</section>
<!-- State switcher -->
<div class="state-switcher">
<h4>States</h4>
<button onclick="setState('list')">loaded (list)</button>
<button onclick="setState('detail')">detail</button>
<button onclick="setState('detail_stale')">detail + stale banner</button>
<button onclick="setState('empty_create')">empty</button>
<button onclick="setState('run_monitor')">run monitor (→045)</button>
</div>
</div>
<script>
function setState(s){
document.getElementById('listView').hidden = true;
document.getElementById('detailView').hidden = true;
document.getElementById('emptyView').hidden = true;
document.getElementById('runMonitorView').hidden = true;
var lbl = document.getElementById('stateLabel');
document.getElementById('staleBanner').hidden = true;
if(s==='list'){ document.getElementById('listView').hidden=false; lbl.textContent='State: loaded (list)'; }
if(s==='empty_create'){ document.getElementById('emptyView').hidden=false; lbl.textContent='State: empty'; }
if(s==='detail'){ document.getElementById('detailView').hidden=false; lbl.textContent='State: detail'; }
if(s==='detail_stale'){ document.getElementById('detailView').hidden=false; document.getElementById('staleBanner').hidden=false; lbl.textContent='State: detail + stale'; }
if(s==='run_monitor'){ document.getElementById('runMonitorView').hidden=false; lbl.textContent='State: run monitor (→045)'; }
}
setState('list');
</script>
</body>
</html>
<!doctype html><html lang="ru"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Регистр сценариев — BI testing</title><link rel="stylesheet" href="../../prototype-ui.css"></head><body><div class="shell">
<header class="topbar"><div class="brand">Superset Tools · BI testing</div><nav class="nav" aria-label="Разделы"><a class="active" href="#registry">Сценарии</a><a href="#runs">Запуски</a><a href="#automation">Автоматизация</a><a href="#health">Качество</a><a href="#investigation-queue">Расследования <span class="badge warn">2</span></a></nav><span class="badge info">Аналитик · Анна</span></header>
<main><div class="page-head"><div><div class="eyebrow">Рабочий стол</div><h1>Регистр сценариев</h1><p class="sub">Сценарии проверок для dashboardов: состояние, последняя проверка и следующий шаг.</p></div><div class="actions"><button class="btn" onclick="setProtoState('empty')">Пустой список</button><button class="btn primary" onclick="setProtoState('create')">Создать сценарий</button></div></div>
<section id="list"><div class="card"><div class="toolbar"><input type="search" aria-label="Поиск сценариев" placeholder="Найти по названию или dashboard"><select aria-label="Статус"><option>Все статусы</option><option>Готов</option><option>Нужна проверка</option></select><select aria-label="Тег"><option>Все теги</option><option>Smoke</option><option>XLSX</option></select><button class="btn">Фильтровать</button></div></div>
<div class="card" style="margin-top:16px"><table class="table"><thead><tr><th>Сценарий</th><th>Dashboard</th><th>Статус</th><th>Последний запуск</th><th>Качество</th><th></th></tr></thead><tbody><tr data-open="detail"><td><strong>XLSX reconciliation</strong><br><span class="sub">Smoke · 8 шагов</span></td><td>FI-0080</td><td><span class="badge ok">Готов</span></td><td>Сегодня, 10:42 · PASS</td><td><span class="badge warn">Требует внимания</span></td><td><button class="btn" onclick="setProtoState('detail')">Открыть</button></td></tr><tr><td><strong>Комментарии по строкам</strong><br><span class="sub">Ручной запуск · 6 шагов</span></td><td>FI-0080</td><td><span class="badge info">Только вручную</span></td><td>Вчера · PASS</td><td><span class="badge ok">Стабильно</span></td><td><button class="btn" onclick="setProtoState('detail')">Открыть</button></td></tr><tr><td><strong>Фильтры и метрики</strong><br><span class="sub">Smoke · 5 шагов</span></td><td>Sales overview</td><td><span class="badge warn">Нужна ревалидация</span></td><td></td><td><span class="badge">Нет данных</span></td><td><button class="btn">Открыть</button></td></tr></tbody></table></div></section>
<section id="detail" class="hidden"><div class="page-head"><div><button class="btn" onclick="setProtoState('list')">К списку</button><h1 style="margin-top:14px">XLSX reconciliation <span class="badge ok">Готов</span></h1><p class="sub">FI-0080 · revision r18 · обновлено Анной сегодня в 10:18</p></div><div class="actions"><button class="btn" onclick="location.hash='editor'">Редактировать</button><button class="btn primary" onclick="location.hash='run-config'">Запустить</button></div></div><div class="main-aside"><div class="grid"><div class="card"><h2>Что проверяет сценарий</h2><p>Экспорт XLSX соответствует активным dashboard и table filters.</p><div class="flow"><span>Открыть dashboard</span><b></b><span>Применить фильтры</span><b></b><span>Скачать XLSX</span><b></b><span>Сравнить baseline</span></div></div><div class="card"><h2>Последняя проверка</h2><p><strong>PASS</strong> · PREPROD · 1 мин 42 сек</p><button class="btn">Открыть результат</button></div></div><aside class="card"><h2>Автоматизация</h2><p><span class="badge ok">Ежедневно 09:00</span></p><p class="sub">Сценарий полностью автоматизируемый.</p><button class="btn">Настроить</button></aside></div></section>
<section id="empty" class="hidden"><div class="card" style="text-align:center;padding:56px"><h2>Пока нет сценариев</h2><p class="sub">Создайте первую проверку из dashboard или опишите бизнес-цель.</p><button class="btn primary" onclick="setProtoState('create')">Создать сценарий</button></div></section><section id="create" class="hidden"><div class="card"><h2>Новый сценарий</h2><p class="sub">Сначала выберите dashboard и цель проверки. Параметры запуска не нужны для сохранения сценария.</p><div class="actions"><button class="btn" onclick="setProtoState('list')">Отмена</button><button class="btn primary" onclick="location.hash='authoring'">Продолжить</button></div></div></section></main>
<div class="statebar" aria-label="Состояния прототипа"><span class="sub">State: <b id="proto-state"></b></span><button class="btn" onclick="setProtoState('list')">List</button><button class="btn" onclick="setProtoState('detail')">Detail</button><button class="btn" onclick="setProtoState('empty')">Empty</button></div></div><script src="../../prototype-ui.js"></script><script>protoState('list',s=>{for(const id of['list','detail','empty','create'])document.getElementById(id).classList.toggle('hidden',id!==s)});wireOpen()</script></body></html>

View File

@@ -4,8 +4,8 @@
## Prototype Metadata
- **Feature**: 042 Scenario Registry & Lifecycle
- **Source contracts**: ux_reference.md, contracts/modules.md
- **Screens represented**: 2 (Registry List, Scenario Detail) + run-monitor stub → 045
- **Total states**: 5 (list, detail, detail+stale, empty, run-monitor stub)
- **Screens represented**: 2 (Registry List, Scenario Detail) + persistent Queue entry + run-monitor stub → 045
- **Total states**: 5 (list, detail, detail+stale, empty, run-monitor stub); Queue is a navigation handoff to 047, not a dialog state
- **Accessibility**: keyboard nav, focus-visible, aria-live (via script), ≥44px targets, prefers-reduced-motion
- **Responsive breakpoints**: 375px, 1280px
@@ -31,4 +31,5 @@
| Detail | US3 Revisions | revision shown (r17) |
| Detail+stale | US4 Staleness | banner + revalidate |
| Detail | US5 Lifecycle | status badge (Ready) |
| Registry List | Operations | persistent Investigation Queue link → 047, no auto-chat |
#endregion ScenarioRegistry.PrototypeManifest

View File

@@ -20,7 +20,7 @@
**Alternatives**: store "current graph" only (rejected: unreproducible runs).
**Impact**: Revisions are append-only rows; `current_revision` pointer advances on edit.
**Impact**: Revisions are append-only rows. Later saves produce candidates; a separate policy-controlled atomic activation advances the `current_revision` pointer.
## R3. Staleness source — release diff + lineage, not blind rescan

View File

@@ -52,7 +52,7 @@
**Independent Test**: Resolve a persisted scenario and verify a new revision is created with parent link and that a run references the exact revision hash.
**Acceptance**:
1. **Given** a scenario is edited **When** a new revision is saved **Then** `scenario_id` (UUID), `revision_id` (UUID), and `parent_revision_id` form a linked chain, and `current_revision` advances.
1. **Given** a scenario is edited **When** a new revision is saved **Then** `scenario_id` (UUID), `revision_id` (UUID), and `parent_revision_id` form a linked chain, and a `candidate` revision is created without advancing `current_revision`.
2. **Given** a run starts **When** it references a scenario **Then** it pins `scenario_id` + `revision_id` + `content_hash` so later edits never change what was executed.
3. **Given** revisions exist **When** the user requests a diff **Then** the change set between two revisions is returned (added/changed/removed).
@@ -103,18 +103,22 @@
- **SCREG-FR-001**: The system MUST persist scenarios as a first-class entity in a `scenario_registry` projection (scenario_id, revision, dashboard, environment compatibility, owner, tags, lifecycle status, validation status, last run, health).
- **SCREG-FR-002**: The system MUST expose `GET /dashboard-testing/scenarios` (list/search/filter by name, tag, dashboard, status, owner) and `GET /dashboard-testing/scenarios/{id}` (detail).
- **SCREG-FR-003**: Every executable-graph edit MUST create a new immutable revision (`revision_id` UUID + `content_hash` + `parent_revision_id`); `current_revision` points to the active revision; runs pin an explicit revision snapshot. Entity-metadata edits (name/description/tags) MUST NOT create an executable revision (content_hash unchanged).
- **SCREG-FR-003**: Every executable-graph edit MUST create a new immutable `candidate` revision (`revision_id` UUID + `content_hash` + `parent_revision_id`); runs pin an explicit revision snapshot. Entity-metadata edits (name/description/tags) MUST NOT create an executable revision (content_hash unchanged).
- **SCREG-FR-003a**: `ActivateCurrentRevision` MUST be a separate atomic operation. It may advance `current_revision` only after deterministic eligibility checks and the recorded 036 delegated-authority decision or ActionApprovalGate; saving a revision never silently changes an automation target.
- **SCREG-FR-004**: Scenario detail MUST be loadable by id independent of any agent session (not event-driven).
- **SCREG-FR-005**: Lifecycle states MUST include DRAFT, READY, STALE, NEEDS_REVALIDATION, BLOCKED, DISABLED, DEPRECATED, ARCHIVED; only valid transitions apply and are audit-logged.
- **SCREG-FR-006**: The system MUST support lifecycle operations clone, rename, archive, restore, and RBAC-scoped delete (archive-only when run history exists).
- **SCREG-FR-007**: Staleness MUST be computed from dashboard release diffs and 041 lineage blast radius; affected scenarios transition to NEEDS_REVALIDATION/BLOCKED with a reason.
- **SCREG-FR-008**: RBAC MUST distinguish scenario:view, scenario:create, scenario:edit, scenario:archive; editing a scenario does NOT grant running it.
- **SCREG-FR-009**: Hard delete MUST be forbidden for scenarios with run history; terminal lifecycle is archive to preserve audit reproducibility.
- **SCREG-FR-010**: Registry health and staleness findings MUST emit an idempotent 036 InvestigationSignal, from which 047 may link an Investigation Queue item. An analyst opens a persistent agent case explicitly; signal production MUST NOT auto-start an agent conversation or tool action.
- **SCREG-FR-011**: A delegated agent MAY create/save validated immutable revisions and portfolio operations through the same server-owned handle, ACL, lifecycle and outbox contracts as an analyst. Risky lifecycle/publish actions remain subject to their ActionApprovalGate policy.
- **SCREG-FR-012**: Registry workflow, stale recovery and lifecycle actions MUST use persistent pages, inline panels or agent cases; modal/dialog interaction MUST NOT be required.
### Key Entities
- **ScenarioRegistryEntry**: Queryable projection of a scenario: name, description, dashboard, environment compatibility, owner, tags, current revision, lifecycle status, validation status, last run, last successful run, health, last modified, baseline compatibility.
- **ScenarioRevision**: Immutable snapshot of a `DashboardTestScenario` graph (`revision_id` UUID + `content_hash` + `parent_revision_id`); `current_revision` is the active one. `scenario_id` is a UUID assigned at Save; `scenario_key` is the semantic slug.
- **ScenarioRevision**: Immutable snapshot of a `DashboardTestScenario` graph (`revision_id` UUID + `content_hash` + `parent_revision_id`); later saves are `candidate`, and only explicit activation makes one revision `current`. `scenario_id` is a UUID assigned at Save; `scenario_key` is the semantic slug.
- **ScenarioLifecycleState**: DRAFT / READY / STALE / NEEDS_REVALIDATION / BLOCKED / DISABLED / DEPRECATED / ARCHIVED with a valid-transition map.
- **ScenarioStalenessSignal**: Cause of staleness (dashboard release diff, lineage blast-radius change, removed reference) with severity and affected scenario ids.

View File

@@ -9,12 +9,12 @@
- **Context**: Browser, dashboard-testing workspace, after having saved a scenario from the agent.
## 2. Happy Path
An analyst opens the Scenario Registry, searches "Revenue", sees "Revenue filters regression — Revenue BI · Ready · PASS 2h ago", opens the detail, reviews the graph/revisions, and clicks "Run" (handed to 045). Staleness is surfaced as a banner with a revalidate action.
An analyst opens the Scenario Registry, searches "Revenue", sees "Revenue filters regression — Revenue BI · Ready · PASS 2h ago", opens the detail, reviews the graph/revisions, and clicks "Run" (handed to 045). Staleness is surfaced as a banner with a revalidate action. The same primary workspace exposes a persistent Investigation Queue count/link; opening it goes to 047, and never opens an automatic chat.
## 3. Screens & States
### Screen: Scenario Registry List
- **Layout**: Toolbar (search, dashboard/status/owner filters, [+ Create scenario]) + table rows (name, dashboard, status, last run, health).
- **Layout**: Toolbar (search, dashboard/status/owner filters, persistent `Investigation Queue` count/link, [+ Create scenario]) + table rows (name, dashboard, status, last run, health).
- **Key Elements**: Search input; filter selects; status/health badges; row click → detail.
- **@UX_STATE**: idle, loading, loaded, empty, error, filtered, LARGE.
- **@UX_RECOVERY**: error → retry; empty → Create CTA.
@@ -24,8 +24,11 @@ An analyst opens the Scenario Registry, searches "Revenue", sees "Revenue filter
- **@UX_STATE**: idle, loading, loaded, stale(409), not_found, error, blocked.
- **@UX_RECOVERY**: stale → reload/discard; not_found → back to list; blocked → revalidate/archive.
### Persistent Investigation Queue Entry
- **Behavior**: Queue count is a navigation link, not an interrupting dialog. A row or detail evidence link opens the corresponding 047 Queue item; `Investigate with agent` is shown only there after analyst intent.
## 4. Error Experience
- **409 concurrent edit** → modal "Reload or discard".
- **409 concurrent edit** → persistent conflict panel with Reload, Compare and Discard.
- **404 not found** → back to list.
- **403 archive** → permission_denied, no confirm control.
- **429** → Retry-After countdown.

View File

@@ -12,7 +12,7 @@
- [ ] CHK004 Business fields + parameters editable manually
- [ ] CHK005 Durable edit → new immutable revision + change summary
- [ ] CHK006 Every save requires HITL confirmation; no silent agent edit
- [ ] CHK006 Every save is policy-authorized, immutable and attributed; non-delegated actions expose an inline ActionApprovalGate
## Constrained Assertions (FR-003)
@@ -27,7 +27,7 @@
## Agent Edit (FR-006)
- [ ] CHK011 "Edit with agent" proposes revision with diff
- [ ] CHK012 Proposal validated before display; never auto-saved
- [ ] CHK012 Proposal validated before display; any agent save is delegated, provenance-bearing and policy checked
## RBAC / UX (FR-007/008)
@@ -36,4 +36,4 @@
## Success Criteria
- [ ] CHK015 SC-001..006 verified (read-only default, revision, rejection, HITL, hybrid C)
- [ ] CHK015 SC-001..006 verified (read-only default, revision, rejection, delegated policy, hybrid C)

View File

@@ -4,7 +4,7 @@
@RELATION DEPENDS_ON -> [ScenarioRegistry.Modules]
@RELATION DEPENDS_ON -> [ScenarioGraph.Validator]
@RELATION DEPENDS_ON -> [ScenarioGraph.Resolver]
@RATIONALE Editing is validated, typed, and revision-bound; never silent, never unsafe.
@RATIONALE Editing is validated, typed, revision-bound and fully attributed; delegated agents may save only through deterministic policy.
@REJECTED Free-form graph mutation; agent-only editing; unconstrained assertion text.
# #region ScenarioEditor.Load [C:3] [TYPE Function] [SEMANTICS scenario,editor,load,readonly]
@@ -28,21 +28,21 @@ def apply_ops(db, scenario_id, base_revision_id, ops): ...
# #region ScenarioEditor.SaveRevision [C:4] [TYPE Function] [SEMANTICS scenario,editor,save,revision,hitl]
# @ingroup ScenarioEditor
# @BRIEF Save a server-stored WorkingDraft by id + digest as a new immutable revision after HITL.
# @PRE draft_id+digest match; draft re-validated/canonicalized/hashed server-side; user confirmed; base revision matches.
# @POST new revision via 042; current_revision advanced; 409 on stale base or digest mismatch.
# @BRIEF Save a server-stored WorkingDraft by id + digest as a new immutable revision after policy evaluation.
# @PRE draft_id+digest match; draft re-validated/canonicalized/hashed server-side; actor/delegated AgentAction authorized; base revision matches.
# @POST new candidate revision via 042; current_revision unchanged; 409 on stale base or digest mismatch.
# @SIDE_EFFECT DB write; 042 revision; audit.
# @INVARIANT prior revisions immutable; never saved without confirmation; no arbitrary-draft bypass.
def save_revision(db, draft_id, digest, change_summary, actor): ...
# @INVARIANT prior revisions immutable; never saved without policy authorization; no arbitrary-draft bypass.
def save_revision(db, draft_id, digest, actor, agent_action_id=None): ...
# #endregion ScenarioEditor.SaveRevision
# #region ScenarioEditor.Revalidate [C:4] [TYPE Function] [SEMANTICS scenario,editor,revalidate,migration]
# @ingroup ScenarioEditor
# @BRIEF Stale-scenario migration: revalidate against current dashboard, produce proposed revision with diff.
# @PRE scenario stale (NEEDS_REVALIDATION); current dashboard query model available.
# @POST proposes r18 with automatic mappings + manual conflicts + diff; nothing persisted until approved.
# @POST proposes r18 with automatic mappings + manual conflicts + diff; nothing persists until a policy-authorized save.
# @SIDE_EFFECT reads 037/041; read-only proposal.
# @INVARIANT proposal passes 038 validation; never auto-saved.
# @INVARIANT proposal passes 038 validation; any agent save is delegated, provenance-bearing and policy-checked.
def revalidate(db, scenario_id, base_revision_id): ...
# #endregion ScenarioEditor.Revalidate
@@ -50,9 +50,9 @@ def revalidate(db, scenario_id, base_revision_id): ...
# @ingroup ScenarioEditor
# @BRIEF Generate an agent-assisted edit proposal as a full revision with diff.
# @PRE request is bounded; proposal validated before display.
# @POST returns proposed graph + diff + validation; nothing persisted until human confirms.
# @POST returns proposed graph + diff + validation; a delegated agent may convert it into a saved immutable revision through SaveRevision.
# @SIDE_EFFECT LLM call (agent tool); read-only draft.
# @INVARIANT proposal passes 038 validation; never auto-saved.
# @INVARIANT proposal passes 038 validation; never bypasses policy, immutable revision creation, or a required gate.
def agent_propose(db, scenario_id, base_hash, request): ...
# #endregion ScenarioEditor.AgentProposal

View File

@@ -4,11 +4,31 @@ info:
version: 0.1.0
description: View and edit persisted scenarios with a hybrid edit model (043).
paths:
/api/dashboard-testing/scenarios/{scenario_id}/metadata:
patch:
operationId: editor.updateMetadata
summary: Update registry metadata without creating an executable revision
security: [{ bearerAuth: [] }]
parameters:
- { name: scenario_id, in: path, required: true, schema: { type: string } }
- { name: If-Match, in: header, required: true, schema: { type: string }, description: "ScenarioRegistryEntry.metadata_version" }
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name: { type: string }
description: { type: string }
tags: { type: array, items: { type: string } }
responses: { "200": { description: "ScenarioRegistryEntry with advanced metadata_version" }, "409": { description: Stale metadata version } }
/api/dashboard-testing/scenarios/{scenario_id}/edits/apply:
post:
operationId: editor.apply
summary: Apply typed edit operations; persists a WorkingDraft server-side
security: [{ bearerAuth: [] }]
parameters: [{ name: scenario_id, in: path, required: true, schema: { type: string } }]
requestBody:
required: true
content:
@@ -22,7 +42,6 @@ paths:
type: array
items:
oneOf:
- { $ref: "#/components/schemas/SetFieldOp" }
- { $ref: "#/components/schemas/SetParameterOp" }
- { $ref: "#/components/schemas/SetAssertionOp" }
- { $ref: "#/components/schemas/AddStepOp" }
@@ -35,21 +54,23 @@ paths:
/api/dashboard-testing/scenarios/{scenario_id}/edits/save:
post:
operationId: editor.save
summary: Save a server-stored WorkingDraft by id + digest (HITL); re-validates/canonicalizes/hashes server-side
summary: Save a server-stored WorkingDraft by id + digest after deterministic delegated-action policy evaluation
security: [{ bearerAuth: [] }]
parameters: [{ name: scenario_id, in: path, required: true, schema: { type: string } }]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [draft_id, digest, change_summary]
required: [draft_id, digest]
properties:
draft_id: { type: string }
digest: { type: string }
change_summary: { type: object }
agent_action_id: { type: string, nullable: true, description: "Server-owned delegated AgentAction identity; audit/provenance is derived server-side" }
responses:
"200": { description: New revision }
"200": { description: New immutable candidate revision with policy/provenance result; activation is a separate 042 operation }
"202": { description: Draft awaiting inline ActionApprovalGate }
"409": { description: STALE_REVISION / digest mismatch }
"422": { description: Draft invalid after re-validation (no arbitrary-draft bypass) }
/api/dashboard-testing/scenarios/{scenario_id}/edits/agent-propose:
@@ -57,6 +78,7 @@ paths:
operationId: editor.agentPropose
summary: Generate an agent-assisted edit proposal with diff
security: [{ bearerAuth: [] }]
parameters: [{ name: scenario_id, in: path, required: true, schema: { type: string } }]
requestBody:
required: true
content:
@@ -68,25 +90,52 @@ paths:
base_revision_id: { type: string }
request: { type: string }
responses:
"200": { description: Proposed graph + diff + validation (not persisted) }
"200": { description: "Server-stored EditProposal { proposal_id, base_revision_id, graph, diff, validation }" }
/api/dashboard-testing/scenarios/{scenario_id}/edits/proposals/{proposal_id}/accept:
post:
operationId: editor.acceptProposal
summary: Accept a validated proposal into a server-stored WorkingDraft
security: [{ bearerAuth: [] }]
parameters:
- { name: scenario_id, in: path, required: true, schema: { type: string } }
- { name: proposal_id, in: path, required: true, schema: { type: string } }
responses: { "200": { description: "WorkingDraft { draft_id, digest, validation }" }, "409": { description: Proposal/base revision stale } }
/api/dashboard-testing/scenarios/{scenario_id}/revalidate:
post:
operationId: editor.revalidate
summary: Produce a migration proposal with automatic mappings and manual conflicts
security: [{ bearerAuth: [] }]
parameters: [{ name: scenario_id, in: path, required: true, schema: { type: string } }]
responses: { "200": { description: "MigrationProposal { proposal_id, mappings, conflicts, diff }" } }
/api/dashboard-testing/scenarios/{scenario_id}/migration-proposals/{proposal_id}/resolve:
post:
operationId: editor.resolveMigrationProposal
summary: Resolve explicit migration conflicts before proposal acceptance
security: [{ bearerAuth: [] }]
parameters:
- { name: scenario_id, in: path, required: true, schema: { type: string } }
- { name: proposal_id, in: path, required: true, schema: { type: string } }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [resolutions]
properties:
resolutions: { type: array, items: { type: object, required: [conflict_id, target_ref], properties: { conflict_id: { type: string }, target_ref: { type: string } } } }
responses: { "200": { description: Updated server-stored MigrationProposal }, "409": { description: Proposal stale or conflict already resolved } }
components:
securitySchemes:
bearerAuth: { type: http, scheme: bearer }
schemas:
SetFieldOp:
type: object
required: [op, field, value]
properties:
op: { type: string, enum: [set_field] }
field: { type: string, enum: [name, description, tags] }
value: { oneOf: [{ type: string }, { type: array, items: { type: string } }] }
SetParameterOp:
type: object
required: [op, param_name, value]
properties:
op: { type: string, enum: [set_parameter] }
op: { type: string, enum: [set_parameter_definition] }
param_name: { type: string }
value: { type: string }
value: { description: "JSON value validated server-side against ParameterDefinition.type" }
SetAssertionOp:
type: object
required: [op, logical_step_id, comparison, baseline_ref]
@@ -111,9 +160,9 @@ components:
logical_step_id: { type: string }
SetDependencyOp:
type: object
required: [op, logical_step_id, action, target_step_id]
required: [op, logical_step_id, action, target_logical_step_id]
properties:
op: { type: string, enum: [set_dependency] }
logical_step_id: { type: string }
action: { type: string, enum: [add, remove] }
target_step_id: { type: string }
target_logical_step_id: { type: string }

View File

@@ -6,7 +6,7 @@
Business fields/parameters manual, assertions constrained, dependencies visual, generated executable read-only, complex changes "Edit with agent". Compatible with 038 safety and mandatory human participation.
## Decision 2 — Read-only default
Editor loads read-only; Edit mode is explicit. No revision created until a durable edit is confirmed.
Editor loads read-only; Edit mode is explicit. No revision is created until a durable edit passes delegated-action policy.
## Decision 3 — Constrained assertion editor
Only registered operators (037 comparison) + baseline refs; free-form SQL/raw baseline/path forbidden.
@@ -15,5 +15,5 @@ Only registered operators (037 comparison) + baseline refs; free-form SQL/raw ba
Dependency edits revalidate for cycles/duplicate outputs (038) on every change.
## Decision 5 — Agent edits never silent
"Edit with agent" proposes a full revision + diff; human confirms/rejects; nothing auto-saved.
"Edit with agent" proposes a full revision + diff; a delegated agent may save it after validation, otherwise an inline ActionApprovalGate decides it.
#endregion ScenarioEditor.Ux.Decisions

View File

@@ -11,37 +11,40 @@ Fields: session_id, scenario_id, base_revision_id, owner_id, opened_at, dirty (b
## EditOperation
Typed discriminant union:
- op=set_field {field: name|description|tags, value}
- op=set_parameter {param_name, value}
- op=set_assertion {step_id, comparison: enum, threshold, baseline_ref}
- op=add_step {template, after_step_id?}
- op=set_parameter_definition {param_name, default?, validation?, source?}
- op=set_assertion {logical_step_id, comparison: enum, threshold, baseline_ref}
- op=add_step {template, after_logical_step_id?}
- op=remove_step {logical_step_id}
- op=set_dependency {logical_step_id, add|remove, target_step_id}
- op=set_dependency {logical_step_id, add|remove, target_logical_step_id}
Invariant: op payloads only reference registered templates/operators/baseline refs; `extra="forbid"` on all edit ops (no SQL/code/path smuggling). Each durable op carries a `logical_step_id` (immutable, from #8) for stable analytics.
Invariant: op payloads only reference registered templates/operators/baseline refs; `extra="forbid"` on all edit ops (no SQL/code/path smuggling). Metadata uses `PATCH /metadata` with `If-Match: metadata_version` and never enters WorkingDraft. Every WorkingDraft operation is executable and creates a revision only on policy-authorized save. Parameter operations edit 038 `ParameterDefinition`, never a runtime value. Each durable step op carries a `logical_step_id` (immutable) for stable analytics.
## WorkingDraft — server-stored, no client-draft bypass (#7)
`apply` persists a WorkingDraft server-side and returns `{draft_id, digest}`. `save(draft_id, digest)` reloads it server-side, re-validates, canonicalizes, re-hashes, and compares the base revision. The client NEVER returns the full graph; `save` cannot accept an arbitrary `draft` object. A WorkingDraft is bound to a base revision and expires on conflict/staleness.
Fields: draft_id, scenario_id, base_revision_id, ops[], applied_graph, digest, created_by, created_at, status (open|saved|expired).
Fields: draft_id, scenario_id, base_revision_id, ops[], applied_graph, digest, created_by, delegated_by?, agent_run_id?, investigation_case_id?, created_at, status (open|saved|awaiting_approval|expired).
## ConstrainedAssertionEdit
Fields: step_id, operator (enum from 037 comparison: exact, absolute, relative, range, row_set), threshold (typed), baseline_ref (approved baseline or candidate), evidence_required (bool). Free-form expected value forbidden.
Fields: logical_step_id, operator (enum from 037 comparison: exact, absolute, relative, range, row_set), threshold (typed), baseline_ref (approved baseline or candidate), evidence_required (bool). Free-form expected value forbidden.
## VisualDependencyEdit
Fields: step_id, target_step_id, action (add|remove). Validated against 038 DAG rules (cycles, duplicate producers).
Fields: logical_step_id, target_logical_step_id, action (add|remove). Validated against 038 DAG rules (cycles, duplicate producers).
## EditRevisionResult
Fields: new_revision_id, parent_revision_id, content_hash, change_summary {added, changed, removed}, validation (ScenarioValidationResult), diff_payload. Returned by save; never mutates prior revisions.
Fields: new_revision_id, parent_revision_id, content_hash, activation_status=`candidate`, server_derived_change_summary {added, changed, removed}, validation (AuthoringValidation), diff_payload, policy_decision, agent_action_id?. Returned by save; never mutates prior revisions or advances `current_revision`. The caller submits only `{draft_id, digest}` plus its authenticated/delegated action identity; it never supplies audit/provenance summary. Promotion is the distinct 042 `ActivateCurrentRevision` operation, with its own deterministic eligibility and delegated-authority/gate decision.
## EditProposal and ScenarioMigration
`EditProposal { proposal_id, scenario_id, base_revision_id, proposed_graph, diff, validation, expires_at, agent_run_id?, investigation_case_id? }` is server-stored. `acceptProposal(proposal_id)` validates its base revision and produces a WorkingDraft, which then uses normal `save(draft_id,digest)`. A delegated agent may invoke that save after deterministic validation; if policy requires approval, the draft moves to `awaiting_approval` and an inline ActionApprovalGate is linked. `revalidate` creates a migration proposal with mappings/conflicts; conflicts must be resolved before accept. There is no client-side graph handoff.
## Conflict & Safety
- Save requires `base_revision_id` match → 409 STALE_REVISION on mismatch.
- Every edit operation validated by 038 validator before revision creation.
- Agent proposals carry a full proposed graph which passes validation before display.
- Agent proposals carry a full proposed graph which passes validation before display; an agent-saved revision retains AgentAction/InvestigationCase provenance. A save is not permission to activate it or change an automation target.
#endregion ScenarioEditor.DataModel

View File

@@ -4,7 +4,7 @@
## Summary
A first-class editor for viewing and editing persisted scenarios (hybrid edit model C): manual business fields/parameters, constrained assertion editing, visual DAG dependency editing, read-only generated executable, and agent-assisted complex edits. Every durable edit produces a new immutable revision via 042, gated by human confirmation.
A first-class editor for viewing and editing persisted scenarios (hybrid edit model C): manual business fields/parameters, constrained assertion editing, visual DAG dependency editing, read-only generated executable, and agent-assisted complex edits. Every durable edit produces a new immutable revision via 042 under delegated-action policy.
## Technical Context
@@ -14,7 +14,7 @@ A first-class editor for viewing and editing persisted scenarios (hybrid edit mo
**Testing**: vitest (L1 model + L2 UX with @testing-library/svelte), pytest (edit op validation)
**Frontend Architecture**: `ScenarioEditorModel.svelte.ts`, model-first, runes-only
**Performance Goals**: edit-op apply < 100ms; revision save < 200ms; visual DAG revalidation < 100ms
**Constraints**: hybrid edit model; no SQL/raw-baseline injection; mandatory HITL on save; RBAC scenario:edit
**Constraints**: hybrid edit model; no SQL/raw-baseline injection; deterministic delegated-action policy; RBAC scenario:edit
**Scale**: up to 100-step graphs, dozens of edit ops per session
## Constitution Check
@@ -39,7 +39,7 @@ specs/043-dashboard-scenario-editor/
backend/src/services/dashboard_testing/editor/ (ops.py, save.py, agent.py, assert_validate.py)
frontend/src/lib/models/ScenarioEditorModel.svelte.ts
frontend/src/lib/components/scenario-editor/ (StepCard, VisualDagCanvas, ConstrainedAssertionEditor, EditRevisionDiff, AgentProposalDialog)
frontend/src/lib/components/scenario-editor/ (StepCard, VisualDagCanvas, ConstrainedAssertionEditor, EditRevisionDiff, AgentActionPanel)
frontend/src/routes/dashboard-testing/scenarios/[id]/edit/+page.svelte
```
@@ -47,7 +47,7 @@ frontend/src/routes/dashboard-testing/scenarios/[id]/edit/+page.svelte
1. EditOperation DTOs + validation (tests first).
2. Load read-only editor + ApplyOps.
3. SaveRevision (HITL + 042 revision chain).
3. SaveRevision (delegated policy + 042 revision chain).
4. Constrained assertion editor.
5. Visual DAG dependency editor.
6. Agent-assisted edit proposal.

File diff suppressed because one or more lines are too long

View File

@@ -15,19 +15,19 @@
|-----------|-----------------|------------|----------|
| view_only | view_only | ✅ | Edit toggle |
| editing | editing | ✅ | — |
| saving | saving | ✅ | HITL confirm |
| saving | saving | ✅ | delegated save or inline gate |
| saving_conflict (409) | saving_conflict | ✅ | reload/discard |
| validation_error | validation_error | ✅ | fix + resubmit |
| agent_proposing | agent_proposing | ✅ | — |
| proposal_diff | proposal_diff | ✅ | confirm/reject |
| proposal_diff | proposal_diff | ✅ | inspect policy/diff/action |
## Screen ↔ Story Traceability
| Story | Prototype Feature | Acceptance Verified |
|-------|-------------------|---------------------|
| US1 View | read-only badge, graph | read-only default |
| US2 Manual | Edit toggle, dirty, save | revision + HITL |
| US2 Manual | Edit toggle, dirty, save | revision + delegated policy |
| US3 Assertion | constrained editor + raw blocked | SQL injection rejected |
| US4 Deps | dep select + cycle error | cycle rejected |
| US5 Agent | agent_proposing + proposal_diff | proposal + diff, confirm/reject |
| US5 Agent | agent_proposing + proposal_diff | proposal + diff + delegated save/gate |
#endregion ScenarioEditor.PrototypeManifest

View File

@@ -22,6 +22,6 @@ cd frontend && npm run lint
- [ ] Read-only view renders persisted scenario
- [ ] SQL/raw-baseline injection rejected in assertion editor
- [ ] Cycle/duplicate rejected in DAG editor
- [ ] Save produces new revision with HITL confirmation
- [ ] Agent proposal shows diff; nothing auto-saved
- [ ] Save produces new immutable revision after delegated-policy evaluation or an inline ActionApprovalGate
- [ ] Agent proposal shows diff and provenance; delegated agent save remains validator/policy checked
- [ ] ruff clean; prototype states covered

View File

@@ -4,7 +4,7 @@
@RELATION DEPENDS_ON -> [Doc.Adr.ADR0006]
@RELATION DEPENDS_ON -> [ScenarioRegistry.Spec]
@RELATION DEPENDS_ON -> [DashboardScenarioModel.Spec]
@RATIONALE 038 defines the DTOs but no lifecycle editing UX. A scenario must be viewable and editable outside the agent chat, with every durable edit producing a new immutable revision and human approval (the mandatory human-in-the-loop for scenario authoring/correction).
@RATIONALE 038 defines the DTOs but no lifecycle editing UX. A scenario must be viewable and editable outside the agent chat, with every durable edit producing a new immutable revision and delegated policy determining whether an inline approval is required.
@REJECTED Agent-only editing (no visual surface) — rejected because users need to review and adjust a scenario without re-prompting the agent each time.
@REJECTED Unconstrained free-form DAG/assertion editor — rejected because it could inject SQL/raw baselines/unsafe paths, violating 038 safety invariants; assertions use constrained editors and generated executable stays read-only.
@@ -13,7 +13,7 @@
**Feature Branch**: `043-dashboard-scenario-editor`
**Created**: 2026-08-07 | **Status**: Draft
**Input**: "Provide a first-class Scenario Editor for viewing and editing persisted scenarios: business fields and parameters editable manually, assertions via constrained editors, dependencies via visual DAG editing, generated executable read-only, and complex changes delegated to 'Edit with agent'. Every durable edit produces a new immutable revision."
**Input**: "Provide a first-class Scenario Editor for viewing and editing persisted scenarios: business metadata and parameter definitions editable manually, assertions via constrained editors, dependencies via visual DAG editing, generated executable read-only, and complex changes delegated to 'Edit with agent'. Metadata changes use a registry metadata version; executable changes create immutable revisions."
## User Scenarios
@@ -34,12 +34,12 @@
**Why P1**: Name, description, tags, and parameter values are safe to edit directly.
**Independent Test**: Modify a scenario name and a parameter default and verify a new revision is produced with a change summary.
**Independent Test**: Modify a scenario name and verify `metadata_version` advances without a revision; modify a parameter default and verify a new executable revision is produced.
**Acceptance**:
1. **Given** the user edits business fields (name/description/tags) **When** saved **Then** a new revision is created with `change_summary`.
2. **Given** the user edits a parameter default/value **When** validated **Then** type validation and dependent-step readiness update.
3. **Given** a durable edit is requested **When** save is confirmed **Then** a HITL confirmation is shown; the agent does not edit silently.
1. **Given** the user edits business fields (name/description/tags) **When** saved **Then** only `ScenarioRegistryEntry.metadata_version` advances; no executable revision is created.
2. **Given** the user edits a parameter definition default **When** validated **Then** type validation and dependent-step readiness update, and a new executable revision is created.
3. **Given** a durable edit is requested **When** it passes policy evaluation **Then** it is saved as an immutable revision or rendered as an inline ActionApprovalGate; an agent-saved revision retains delegated provenance.
---
@@ -71,11 +71,11 @@
**Why P3**: Some changes (new checklist case, complex assertion) are easier described in natural language.
**Independent Test**: Request "add XLSX comparison" via Edit-with-agent and verify the agent proposes a new revision for human approval.
**Independent Test**: Request "add XLSX comparison" via Edit-with-agent and verify the agent creates a validated WorkingDraft and saves a revision when delegated policy permits.
**Acceptance**:
1. **Given** a complex change request **When** "Edit with agent" runs **Then** the agent proposes a new scenario revision.
2. **Given** the agent proposes a revision **When** shown **Then** a diff is displayed and the user confirms or rejects; nothing is saved silently.
1. **Given** a complex change request **When** "Edit with agent" runs **Then** the agent creates a server-stored `EditProposal`.
2. **Given** the proposal is accepted **When** its base revision is still current **Then** it becomes a `WorkingDraft`; a policy-authorized analyst or agent save creates the revision. A stale proposal never saves; a non-delegated action waits at an inline gate.
---
@@ -88,7 +88,7 @@
**Acceptance**:
1. **Given** a scenario is stale **When** "Revalidate" runs **Then** affected refs (chart/filter/metric) are listed and mapped against the current dashboard (038 validator + 037/041).
2. **Given** some mappings are ambiguous **When** conflicts exist **Then** they are surfaced for manual resolution, not auto-accepted.
3. **Given** a proposal is generated **When** shown **Then** a diff (r17→r18) is displayed and the user approves or rejects; nothing is saved silently.
3. **Given** a proposal is generated **When** shown **Then** a diff (r17→r18) is displayed; the agent may save it after validation if delegated policy allows, otherwise the linked inline gate decides it.
---
@@ -96,7 +96,7 @@
| # | Scenario | Expected Behavior | Recovery |
|---|----------|-------------------|----------|
| E1 | Concurrent edit (409) | Version conflict modal | Reload / compare / discard |
| E1 | Concurrent edit (409) | Persistent conflict panel with reload/compare/discard | Reload / compare / discard |
| E2 | Invalid assertion/SQL/raw baseline | Rejected with validator findings | Correct via constrained editor |
| E3 | Cycle introduced by dependency edit | Rejected with cycle path | Revert edge |
| E4 | Agent proposes unsafe revision | Blocked by validator before save | Describe differently / manual edit |
@@ -107,11 +107,11 @@
### Functional
- **SCEDIT-FR-001**: The editor MUST render a persisted scenario as a structured document with steps, dependency graph, parameters, baselines, assertions, and read-only generated executable.
- **SCEDIT-FR-002**: Business fields (name, description, tags) and parameter values MUST be editable manually and produce a new immutable revision on save.
- **SCEDIT-FR-002**: Metadata fields (name, description, tags) MUST be editable manually through `metadata_version` without creating an executable revision; ParameterDefinition/default and graph edits MUST create one.
- **SCEDIT-FR-003**: Assertion editing MUST use a constrained editor (registered operators + baseline references); free-form SQL, shell, paths, and raw numeric baseline literals MUST be forbidden.
- **SCEDIT-FR-004**: Dependency editing MUST be visual (graph), with 038 cycle/duplicate-output validation on every change.
- **SCEDIT-FR-005**: Every durable edit MUST require a HITL confirmation and produce a new revision; the agent MUST NOT edit scenarios silently.
- **SCEDIT-FR-006**: "Edit with agent" MUST propose a revision with a diff for human approval before any save.
- **SCEDIT-FR-005**: Every durable executable graph edit MUST produce a new immutable revision; metadata uses metadata_version concurrency. Deterministic policy decides whether the actor/agent may save immediately or must obtain an inline ActionApprovalGate.
- **SCEDIT-FR-006**: "Edit with agent" MUST create a server-stored validated proposal/draft with a diff. A delegated agent MAY save the revision; no agent edit may bypass 038 validation, 042 revision provenance, or a required gate.
- **SCEDIT-FR-009**: Every save MUST use a server-stored WorkingDraft (draft_id + digest); the client MUST NOT return the full graph to save (no arbitrary-draft bypass). Save re-validates/canonicalizes/hashes server-side.
- **SCEDIT-FR-010**: A stale scenario MUST be revalidatable: affected refs mapped against the current dashboard, conflicts surfaced manually, a proposed revision + diff shown for approval (Scenario Migration workflow).
- **SCEDIT-FR-007**: RBAC MUST enforce scenario:edit separately from scenario:run.
@@ -130,7 +130,7 @@
- **SC-002**: Manual field/parameter edits produce a new immutable revision with change summary in model tests.
- **SC-003**: 100% of attempted SQL/raw-baseline/path injections via the assertion editor are rejected.
- **SC-004**: Dependency edits that introduce a cycle or duplicate output are rejected with a precise error.
- **SC-005**: No durable edit is persisted without HITL confirmation; agent edits are always diff-proposed.
- **SC-005**: Every durable edit is immutable, validated and fully attributed; agent edits expose their diff/provenance and are either policy-authorized or ActionApprovalGate-bound.
- **SC-006**: Edit model C (hybrid) is enforced: manual fields/params, constrained assertions, visual deps, read-only generated executable.
## Clarifications
@@ -138,7 +138,7 @@
### Session 2026-08-07
- Q: Which edit model? → A: **C (hybrid)** — business fields/params manual, assertions constrained, dependencies visual, generated executable read-only, complex changes "Edit with agent".
- Q: Does the agent edit silently? → A: No. Every durable edit needs human confirmation and produces a new revision (mandatory human participation).
- Q: May the agent save a revision? → A: Yes, after deterministic validation when delegated policy permits. It always creates an immutable revision with agent/delegator/case provenance; policy-gated actions use an inline ActionApprovalGate.
- Q: Does this replace 039 workspace? → A: No. 039 is the create flow in agent chat; 043 is the post-save edit surface over the registry.
## Implementation Status & MVP Debt (audit 2026-08-07)

View File

@@ -23,8 +23,8 @@
- [ ] T007 [US2] Implement `apply_ops` + `validate_assertion` in `backend/src/services/dashboard_testing/editor/`
@INVARIANT: no SQL/code/path/raw-baseline via ops
@TEST_EDGE: SQL in assertion->rejected; cycle->rejected; raw baseline->rejected
- [ ] T008 [US2] Implement `save_revision` (HITL + 042) in `backend/src/services/dashboard_testing/editor/save.py`
@POST: new revision; 409 stale base; never without confirmation
- [ ] T008 [US2] Implement `save_revision` (delegated policy + 042) in `backend/src/services/dashboard_testing/editor/save.py`
@POST: new revision or inline ActionApprovalGate; 409 stale base; never without policy authorization
- [ ] T009 [US2] L2 UX test for field/parameter edit → revision in `frontend/src/routes/dashboard-testing/scenarios/[id]/edit/__tests__/edit.ux.test.ts`
## Phase 4 — US3 Constrained Assertion Editor
@@ -41,8 +41,8 @@
## Phase 6 — US5 Agent-Assisted Edit
- [ ] T014 [US5] Implement `agent_propose` in `backend/src/services/dashboard_testing/editor/agent.py`
@INVARIANT: proposal passes validation; never auto-saved
- [ ] T015 [US5] Build `AgentProposalDialog.svelte` + `EditRevisionDiff.svelte`; add `POST /scenarios/{id}/edits/agent-propose`
@INVARIANT: proposal passes validation; agent save is fully attributed and policy checked
- [ ] T015 [US5] Build persistent `AgentActionPanel.svelte` + `EditRevisionDiff.svelte`; add `POST /scenarios/{id}/edits/agent-propose`
## Phase 6b — WorkingDraft + Revalidate (P0 #7 / #9)

View File

@@ -9,7 +9,7 @@
- **Context**: Browser, scenario detail → Edit.
## 2. Happy Path
The analyst opens a scenario in the editor, edits the description and a parameter, changes an assertion operator via the constrained editor, re-parents a dependency in the DAG, clicks Save, confirms the HITL dialog, and a new revision r18 is created with a change summary.
The analyst edits directly in the persistent editor or asks the agent for a complex change. The agent creates a validated draft, can save an immutable revision under delegated policy, then runs verification; a non-delegated action appears as an inline approval card with diff/provenance.
## 3. Screens & States
@@ -19,10 +19,10 @@ The analyst opens a scenario in the editor, edits the description and a paramete
- **ConstrainedAssertionEditor**: operator select + threshold + baseline ref; no free-text expected value.
- **VisualDagCanvas**: drag dependency edges; live cycle/duplicate validation.
- **@UX_STATE**: view_only, editing, saving, saving_conflict(409), saving_error, agent_proposing, proposal_diff, validation_error.
- **@UX_RECOVERY**: 409 → reload/discard; validation_error → show findings; agent proposal → confirm/reject.
- **@UX_RECOVERY**: 409 → persistent reload/compare/discard panel; validation_error → show findings; agent proposal → inspect policy/diff/action timeline.
## 4. Error Experience
- **409 concurrent edit** → modal "Reload or discard".
- **409 concurrent edit** → persistent conflict panel with Reload, Compare and Discard.
- **422 invalid edit** → inline findings (e.g., "SQL not allowed in assertion").
- **403 edit denied** → permission_denied, no confirm control.

View File

@@ -22,21 +22,21 @@ def derive_runner_plan(db, scenario_id, revision_id): ...
# #region ScenarioExecution.Start [C:5] [TYPE Function] [SEMANTICS scenario,execution,start,run]
# @ingroup ScenarioExecution
# @BRIEF Create and start a ScenarioRun pinned to an immutable revision.
# @PRE caller has scenario:run; PROD requires ActionApprovalGate; params validated; Idempotency-Key unique.
# @POST ScenarioRun queued->running; RunnerPlan derived; step order established; idempotency key recorded.
# @PRE caller has scenario:run; PROD creates ActionApprovalGate; RunPreflight validates bindings/target; Idempotency-Key supplied.
# @POST same idempotency key + canonical request returns existing ScenarioRun; differing request returns IDEMPOTENCY_KEY_REUSED; otherwise run queued/pending_approval and RunnerPlan derived.
# @SIDE_EFFECT DB write; enqueue; audit.
# @INVARIANT revision snapshot immutable; PROD gated; no duplicate run for same Idempotency-Key.
# @TEST_EDGE prod_without_approval->blocked; stale_revision->blocked; duplicate_idempotency->409.
# @INVARIANT revision snapshot immutable; PROD gated; idempotency identity is key + canonical execution-request hash.
# @TEST_EDGE prod_without_approval->pending_approval; stale_revision->blocked; same_idempotency_replay->existing run; changed_request->409.
async def start_run(db, scenario_id, revision_id, params, env, actor, idempotency_key): ...
# #endregion ScenarioExecution.Start
# #region ScenarioExecution.Dispatch [C:5] [TYPE Function] [SEMANTICS scenario,execution,dispatch,step,executor]
# @ingroup ScenarioExecution
# @BRIEF Dispatch a step by tool to its typed executor; record a ScenarioStepRun.
# @PRE step dependencies satisfied; tool registered in executor registry.
# @PRE step dependencies satisfied; {tool, action} exists in the revision-pinned 038 ActionRegistry; mutation policy preflight passed.
# @POST returns step outcome; output refs bound; ScenarioStepRun persisted.
# @SIDE_EFFECT executor side effects (browser/superset/xlsx/evidence); DB write.
# @INVARIANT human not dispatched here; deterministic per tool.
# @INVARIANT human not dispatched here; deterministic per registered {tool, action}; unregistered action is rejected before executor I/O.
# @TEST_EDGE unknown_tool->rejected; step_fail->dependents blocked; ref_binding->dependent reads producer output.
async def dispatch_step(run, step): ...
# #endregion ScenarioExecution.Dispatch
@@ -100,9 +100,10 @@ def claim_step(run_id, logical_step_id, worker_id): ...
# #region ScenarioExecution.ExecutorRegistry [C:3] [TYPE Module] [SEMANTICS scenario,execution,registry,executor]
# @ingroup ScenarioExecution
# @BRIEF tool -> executor mapping; human excluded; each executor declares idempotency/retry-safety.
# @BRIEF tool -> executor mapping; BrowserExecutor resolves only 038 ActionRegistry actions; human excluded; each executor declares idempotency/retry-safety.
# @REJECTED human as executor (HumanCheckpoint lifecycle control instead).
EXECUTOR_MAP = {browser, superset_api, xlsx, assertion, screenshot, report, artifact}
EXECUTOR_MAP = {browser: BrowserExecutor(ActionRegistry), superset_api, xlsx, assertion, screenshot, report, artifact}
# @INVARIANT each executor declares {idempotent, retry_safe, side_effect_key, external_request_id}.
# @INVARIANT mutation actions require immutable mutation_contract; PROD mutation is rejected; mutation retries default false.
# #endregion ScenarioExecution.ExecutorRegistry
#endregion ScenarioExecution.Modules

View File

@@ -22,15 +22,14 @@ paths:
scenario_id: { type: string }
revision_id: { type: string }
environment_id: { type: string }
params: { type: object }
release: { type: string, nullable: true }
params: { type: object, description: "Launch values; server validates against 038 ParameterDefinition and persists immutable ParameterBinding[]" }
requested_target_reference: { type: object, description: "User-selected release/target; server verifies it matches the captured TargetSnapshot" }
baseline_set: { type: string }
execution_toggles: { type: object, description: "Optional evidence only (diagnostic screenshots, verbose logs, optional VLM). Mandatory steps cannot be disabled." }
triggered_by: { type: string, enum: [manual, deploy, release, etl, schedule, api], default: manual }
responses:
"201": { description: ScenarioRun created, content: { application/json: { schema: { $ref: "#/components/schemas/ScenarioRun" } } } }
"403": { description: PROD ActionApprovalGate required / permission denied }
"409": { description: Stale revision or duplicate Idempotency-Key }
"201": { description: ScenarioRun created (queued or pending_approval), content: { application/json: { schema: { $ref: "#/components/schemas/ScenarioRun" } } } }
"403": { description: Permission denied; PROD approval creates pending_approval run rather than returning 403 }
"409": { description: Stale revision or IDEMPOTENCY_KEY_REUSED with a different canonical request }
/api/scenarios/{scenario_id}/runs:
get:
operationId: scenarioRun.history
@@ -49,7 +48,7 @@ paths:
summary: Final result with aggregation + provenance
security: [{ bearerAuth: [] }]
parameters: [{ name: run_id, in: path, required: true, schema: { type: string } }]
responses: { "200": { description: ScenarioExecutionResult } }
responses: { "200": { description: ScenarioExecutionResult, content: { application/json: { schema: { $ref: "#/components/schemas/ScenarioExecutionResult" } } } } }
/api/scenario-runs/compare:
get:
operationId: scenarioRun.compare
@@ -58,7 +57,7 @@ paths:
parameters:
- { name: a, in: query, required: true, schema: { type: string } }
- { name: b, in: query, required: true, schema: { type: string } }
responses: { "200": { description: RunComparison } }
responses: { "200": { description: RunComparison, content: { application/json: { schema: { $ref: "#/components/schemas/RunComparison" } } } } }
/api/scenario-runs/{run_id}/steps/{logical_step_id}/retry:
post:
operationId: scenarioRun.retryStep
@@ -87,36 +86,50 @@ paths:
- { name: Last-Event-ID, in: header, schema: { type: integer }, description: "sequence to replay from" }
responses:
"200":
description: text/event-stream — ScenarioRunEvent { id, sequence, event_type, run_id, step_id?, attempt?, occurred_at, payload }; heartbeat events; terminal close event on run end.
content: { text/event-stream: { schema: { $ref: "#/components/schemas/ScenarioRunEvent" } } }
/api/scenario-runs/{run_id}/cancel:
post:
operationId: scenarioRun.cancel
summary: Cancel a run (bounded drain)
security: [{ bearerAuth: [] }]
parameters: [{ name: run_id, in: path, required: true, schema: { type: string } }]
responses: { "200": { description: Cancelled } }
/api/scenario-runs/{run_id}/resume:
post:
operationId: scenarioRun.resume
summary: Resume a paused run from resume token
security: [{ bearerAuth: [] }]
responses: { "200": { description: Resumed } }
/api/scenario-runs/{run_id}/human/decision:
post:
operationId: scenarioRun.humanDecision
summary: Resolve a HumanCheckpoint (observation disposition)
security: [{ bearerAuth: [] }]
parameters: [{ name: run_id, in: path, required: true, schema: { type: string } }]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [checkpoint_id, disposition]
required: [resume_token, resume_reason]
properties:
resume_token: { type: string }
resume_reason: { type: string, enum: [worker_recovered, infrastructure_pause_resolved] }
responses: { "200": { description: Infrastructure pause resumed }, "409": { description: Cannot resume HumanCheckpoint or stale token } }
/api/scenario-runs/{run_id}/human/decision:
post:
operationId: scenarioRun.humanDecision
summary: Resolve a HumanCheckpoint (observation disposition)
security: [{ bearerAuth: [] }]
parameters: [{ name: run_id, in: path, required: true, schema: { type: string } }]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [checkpoint_id, decision_version, disposition]
properties:
checkpoint_id: { type: string }
disposition: { type: string, enum: [confirm, false_positive, inconclusive] }
decision_version: { type: integer }
disposition: { type: string, enum: [confirm, false_positive, pass, fail, inconclusive] }
comment: { type: string }
responses: { "200": { description: Step outcome recorded; run resumed } }
responses: { "200": { description: Checkpoint consumed; step outcome recorded; runner resumed internally }, "409": { description: Checkpoint already decided, expired, or version conflict } }
components:
securitySchemes:
bearerAuth: { type: http, scheme: bearer }
@@ -128,8 +141,11 @@ components:
scenario_id: { type: string }
scenario_revision_id: { type: string }
environment_id: { type: string }
status: { type: string, enum: [queued, running, waiting_human, blocked, cancel_requested, cancelled, passed, failed, inconclusive] }
params_snapshot: { type: object }
status: { type: string, enum: [pending_approval, queued, running, waiting_human, blocked, cancel_requested, cancelled, passed, failed, inconclusive] }
parameter_bindings: { type: array, items: { $ref: "#/components/schemas/ParameterBinding" } }
target_snapshot: { $ref: "#/components/schemas/TargetSnapshot" }
execution_principal_fingerprint: { type: string }
analytics_context_key: { type: string, description: "Server-derived immutable analytics grouping key" }
step_runs: { type: array, items: { $ref: "#/components/schemas/ScenarioStepRun" } }
ScenarioStepRun:
type: object
@@ -142,3 +158,65 @@ components:
outputs: { type: object }
artifact_refs: { type: array, items: { type: string } }
error_code: { type: string, nullable: true }
ParameterBinding:
type: object
required: [parameter_name, resolved_value, source, resolved_at]
properties:
parameter_name: { type: string }
resolved_value: {}
source: { type: string, enum: [launch_input, default, schedule, trigger] }
resolved_at: { type: string, format: date-time }
TargetSnapshot:
type: object
required: [environment_id, dashboard_fingerprint, dataset_lineage_fingerprint, captured_at]
properties:
environment_id: { type: string }
dashboard_release_id: { type: [string, "null"] }
dashboard_fingerprint: { type: string }
dataset_lineage_fingerprint: { type: string }
captured_at: { type: string, format: date-time }
ScenarioExecutionResult:
type: object
required: [run_id, status, step_counts, provenance, failures]
properties:
run_id: { type: string }
status: { type: string }
step_counts: { type: object, additionalProperties: { type: integer } }
failures: { type: array, items: { type: object } }
provenance: { type: object }
analytics_context_key: { type: string }
RunComparison:
type: object
required: [run_a, run_b, step_deltas, compatibility]
properties:
run_a: { type: string }
run_b: { type: string }
compatibility: { type: object }
step_deltas: { type: array, items: { type: object } }
ScenarioRunEvent:
oneOf:
- { $ref: "#/components/schemas/RunStartedEvent" }
- { $ref: "#/components/schemas/StepStartedEvent" }
- { $ref: "#/components/schemas/StepCompletedEvent" }
- { $ref: "#/components/schemas/CheckpointCreatedEvent" }
- { $ref: "#/components/schemas/RunCompletedEvent" }
RunStartedEvent:
type: object
required: [id, sequence, event_type, run_id, occurred_at]
properties: { id: { type: string }, sequence: { type: integer }, event_type: { const: run_started }, run_id: { type: string }, occurred_at: { type: string, format: date-time }, payload: { type: object } }
StepStartedEvent:
type: object
required: [id, sequence, event_type, run_id, logical_step_id, occurred_at]
properties: { id: { type: string }, sequence: { type: integer }, event_type: { const: step_started }, run_id: { type: string }, logical_step_id: { type: string }, occurred_at: { type: string, format: date-time }, payload: { type: object } }
StepCompletedEvent:
type: object
required: [id, sequence, event_type, run_id, logical_step_id, occurred_at]
properties: { id: { type: string }, sequence: { type: integer }, event_type: { const: step_completed }, run_id: { type: string }, logical_step_id: { type: string }, occurred_at: { type: string, format: date-time }, payload: { type: object } }
CheckpointCreatedEvent:
type: object
required: [id, sequence, event_type, run_id, occurred_at]
properties: { id: { type: string }, sequence: { type: integer }, event_type: { const: checkpoint_created }, run_id: { type: string }, occurred_at: { type: string, format: date-time }, payload: { type: object } }
RunCompletedEvent:
type: object
required: [id, sequence, event_type, run_id, occurred_at]
properties: { id: { type: string }, sequence: { type: integer }, event_type: { const: run_completed }, run_id: { type: string }, occurred_at: { type: string, format: date-time }, payload: { type: object } }

View File

@@ -4,12 +4,28 @@
@RATIONALE A typed run/step state model is required for reload during a test, recovery, resume, and reproducibility. Without it, human-checkpoint resume and page reload are impossible.
@REJECTED In-memory-only run state — rejected because runs must survive disconnect and be recoverable by run id.
## ScenarioRun
## ScenarioRun and PROD approval lifecycle
Fields: id, scenario_id, scenario_revision_id (revision_id UUID), scenario_content_hash, dashboard_id, environment_id, status (enum), phase (preflight|setup|executing|waiting_human|draining|terminal), params_snapshot (JSON), baselines_pinned (version), release (nullable), execution_toggles (optional evidence only), triggered_by (manual|deploy|release|etl|schedule|api), agent_run_id? (provenance), verification_run_id? (aggregation), idempotency_key (from client Idempotency-Key, unique), started_at, finished_at, resume_token, error_code, runner_version.
`ScenarioRun` is created before dispatch. Fields: id, scenario_id, scenario_revision_id (revision_id UUID), scenario_content_hash, dashboard_id, environment_id, status (`pending_approval|queued|running|waiting_human|blocked|cancel_requested|cancelled|passed|failed|inconclusive`), phase (preflight|setup|executing|waiting_human|draining|terminal), parameter_bindings (immutable JSON), baselines_pinned (version), target_snapshot, execution_principal_fingerprint, execution_toggles (optional evidence only), trigger_source (server-owned), agent_run_id? (provenance), verification_run_id? (aggregation), idempotency_key (unique), started_at, finished_at, resume_token, error_code, runner_version.
If the selected revision contains a `human` step, it is derived as `manual_run_only=true`: it may start only from the authenticated analyst manual-run route. Scheduler, deploy/ETL/API trigger and any background runner are ineligible; no HumanCheckpoint may be skipped or defaulted.
For a PROD request, the service atomically creates `ScenarioRun(status=pending_approval)` and `ActionApprovalGate(owner_type=scenario_run, owner_id=run_id, operation=scenario_execution)`. Approval transitions only `pending_approval → queued`; denial/expiry transitions to `blocked`. Schedulers and external triggers therefore receive a durable run/intent, never an unusable 403. `trigger_source` is set only by the trusted entry point (manual route, scheduler, deploy connector, or API key), never by a bearer client field. Idempotency uses canonical execution-request hash: same key + same hash returns the existing run; same key + different hash returns `409 IDEMPOTENCY_KEY_REUSED`.
## ParameterBinding, ExecutionPrincipal, and TargetSnapshot
`ParameterBinding`: parameter_name, resolved_value, source, resolved_at, validation_fingerprint. It is derived at start from the 038 `ParameterDefinition` and launch input; it is never embedded in ScenarioRevision or its `content_hash`.
`ExecutionPrincipal`: auth_mode, actor_id?, service_identity?, impersonated_user?, effective_roles_hash, rls_context_hash. Its immutable fingerprint is stored on the run and used for every Superset request.
`TargetSnapshot`: environment_id, dashboard_release_id, dashboard_fingerprint, dataset_lineage_fingerprint, captured_at. It is mandatory even when no release was selected, so `environment_id=preprod` is never treated as an immutable target.
`AnalyticsContextKey` is a server-derived SHA-256 over `environment_class + compatibility_family + baseline_family + dashboard_release_id + dashboard_fingerprint + dataset_lineage_fingerprint + execution_principal_fingerprint`. It is captured on the run and every step result; analytics never groups runs merely by environment or revision id.
Statuses: queued, running, waiting_human, blocked, cancel_requested, cancelled, passed, failed, inconclusive.
Terminal `failed`, `inconclusive` and `blocked` outcomes emit a 036 `InvestigationSignal` with this immutable execution snapshot. 047 deterministically creates/updates the Queue/Episode from that signal. Signal delivery never changes run status and never auto-starts an agent run; an analyst opens the case explicitly from 045/047.
## ScenarioStepRun
Fields: id, run_id FK, logical_step_id (immutable UUID, from #8), step_position (mutable), step_content_hash (mutable), attempt, status (queued|running|waiting_human|passed|failed|inconclusive|blocked|skipped), started_at, finished_at, inputs_snapshot (JSON, **no secrets**), outputs (JSON), artifact_refs (JSON), error_code, progress, timeout_ms, side_effect_key (nullable).
@@ -22,7 +38,7 @@ The materialized `runner.plan.json` in git is a **reference artifact**, never th
## ScenarioExecutionContext (#11)
Fields: run_id, browser_session (ref, not raw cookies), page/context ref, auth_context ref, current_dashboard, current_filters, artifact_namespace (owner_type=scenario_run), environment client. Browser secrets/cookies live in a separate secure context, NEVER in `inputs_snapshot` JSON (keeps reproducibility snapshot secret-free).
Fields: run_id, browser_session (ref, not raw cookies), page/context ref, auth_context ref, current_dashboard, current_filters, artifact_namespace (owner_type=scenario_run), environment client. Browser secrets/cookies live in a separate secure context, NEVER in `inputs_snapshot` JSON (keeps reproducibility snapshot secret-free). Browser workers resume only by deterministic replay from the last browser-safe checkpoint (`dashboard_open`, `filters_applied`, etc.); replay records `reconstruction_replay=true` and never reclassifies already completed logical steps as rerun. API/XLSX/pure assertion steps may resume directly only when their executor declares retry-safe/idempotent.
## Artifact ownership (#4)
@@ -30,8 +46,10 @@ Artifacts use a **generic owner**: `Artifact { id, owner_type: agent_run|scenari
## Decision gates (#3)
- `ActionApprovalGate` — authorization approval for PROD execution, baseline approval, repository mutation. Generalized 036 gate mechanism, owner = any run type.
- `HumanCheckpoint`test-result disposition: confirm | false_positive | inconclusive. A distinct entity with own lifecycle; NOT a 036 ApprovalGate decision. Used for `human` steps.
- `ActionApprovalGate` — authorization approval for PROD execution, baseline approval, repository mutation. Generalized 036 gate with `owner_type` + `owner_id`.
- `HumanCheckpoint``checkpoint_id, run_id, logical_step_id, checkpoint_type, decision_policy, status, created_at, expires_at, eligible_role?, eligible_actor_ids?, assigned_to?, evidence_refs, decision_version, decided_by?, decided_at?, disposition?, comment?`. Status is `pending|decided|expired|cancelled`; decision is CAS on `decision_version`, stale/concurrent decision returns 409. `finding_review` maps confirm→failed, false_positive→passed, inconclusive→inconclusive; `manual_assertion` maps pass→passed, fail→failed, inconclusive→inconclusive. It is NOT a 036 ApprovalGate decision.
A HumanCheckpoint is never delegated to the agent: it is a manual-run-only analyst decision inside a currently executing run. The agent may explain the evidence in its workspace but cannot consume the checkpoint or convert it into an automated result.
## Worker semantics — at-least-once execution (#6)
@@ -51,12 +69,12 @@ Retry of a failed step invalidates its **downstream closure** (descendants depen
## Lifecycle
queued → running → waiting_human | blocked → passed | failed | inconclusive; cancel_requested → cancelled. Human step → HumanCheckpoint resume from resume_token. Cancel drains in-flight within a bounded window.
pending_approval → queued → running → waiting_human | blocked → passed | failed | inconclusive; cancel_requested → cancelled. Human decision atomically consumes the checkpoint and resumes internally. Public `/resume` is reserved for a recoverable infrastructure pause and requires a typed resume token/reason; it cannot consume a HumanCheckpoint. Cancel drains in-flight within a bounded window.
## ScenarioExecutorRegistry
## ScenarioExecutorRegistry and BrowserExecutor
Mapping `tool -> executor`:
- browser -> ScreenshotService/Playwright (036 infra)
- browser -> `BrowserExecutor` → version-pinned 038 `ActionRegistry` → Playwright/session infrastructure
- superset_api -> 037 metric_executor_async / SupersetClient.ChartData.Execute
- xlsx -> xlsx parser + 037 normalization
- assertion -> 037 comparison.py (+ vlm via LLMClient)
@@ -65,8 +83,18 @@ Mapping `tool -> executor`:
- artifact -> generic artifact register (owner_type=scenario_run)
- human -> EXCLUDED (runner-lifecycle HumanCheckpoint control, not an executor)
`BrowserExecutor` implements only registered actions (for example `open_dashboard`, `apply_native_filter`, `apply_table_filter`, `click`, `fill`, `select_rows`, `edit_row`, `bulk_edit`, `refresh`, `download`, `navigate_tab`, `wait_for_state`). ScreenshotService is evidence infrastructure only, not the browser action executor. It validates the registry-declared inputs/outputs/risk/timeout/idempotency before dispatch.
## Mutation authorization and execution policy
Authorization is evaluated per run and step as `environment_class + scenario risk profile + action mutation profile`. Read-only PROD steps require the ScenarioExecution approval policy. Every mutation requires the immutable 038 mutation contract, scoped target keys, precondition evidence, side-effect identity, cleanup/reconciliation outcome and `retry_safe=false` unless the registry proves an idempotent compensating action. A PROD mutation is allowed only by an explicit policy for the exact operation and a consumed ActionApprovalGate; non-PROD test-data mutation may be delegated inside an authorized fixture lease.
## Immutable Execution Snapshot
A ScenarioRun pins `scenario_revision_id` + `scenario_content_hash` at start. Results carry provenance: scenario_revision_id, runner_version, template_version, baseline_revision, environment, parameter snapshot, query fingerprints. Later edits never alter completed runs.
A ScenarioRun pins `scenario_revision_id` + `scenario_content_hash` at start. Results carry provenance: scenario_revision_id, runner_version, template_version, baseline_revision, target_snapshot, parameter_bindings, execution_principal_fingerprint, query fingerprints. Later edits never alter completed runs.
## ExecutionCapacityManager
All `AgentRun`, `VerificationRun`, `LoadRun`, and `ScenarioRun` claims pass through one environment-scoped capacity manager: `environment, workload_class, priority, quota, reserved_capacity`. A 046 scenario policy is a consumer of this global allocator, not an independent PROD/PREPROD concurrency limit.
#endregion ScenarioExecution.DataModel

View File

@@ -4,7 +4,7 @@
## Summary
Implement a deterministic ScenarioRun/ScenarioStepRun execution engine that walks the validated DashboardTestScenario DAG, dispatches each step by tool to a typed executor (reusing 037/038/036/040 services), and manages the full run lifecycle including human-checkpoint pause/resume, cancel, retry, and timeout. `runner.plan.json` becomes a real materialized contract. Agent is not in the hot path.
Implement a deterministic ScenarioRun/ScenarioStepRun execution engine that walks the validated DashboardTestScenario DAG, dispatches each registered `{tool, action}` to a typed executor (reusing 037/038/036/040 services), and manages the full run lifecycle including human-checkpoint pause/resume, cancel, retry, and timeout. RunnerPlan is derived at launch; `runner.plan.json` is a diagnostic/reference artifact. Agent is not in the hot path.
## Technical Context
@@ -49,7 +49,7 @@ backend/src/services/dashboard_testing/execution/
## Delivery Phases
1. ScenarioRun/ScenarioStepRun models + migration + fixtures.
2. RunnerPlan Load/Validate (promote stub to real contract).
2. RunnerPlan Derive/Validate from revision + bindings + target.
3. Executor registry + per-tool executors (reuse 037/038/036).
4. Deterministic DAG dispatch + ref binding + failure propagation.
5. Human suspend/resume (036 gate) + recoverable run.

View File

@@ -1,102 +1,4 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>044 — Scenario Execution Engine Prototype</title>
<style>
:root{--bg:#0f1115;--panel:#171a21;--panel2:#1d2129;--border:#2a2f3a;--text:#e6e8ee;--muted:#9aa3b2;--accent:#4f8cff;--green:#2ecc71;--warn:#f39c12;--red:#e74c3c;--violet:#a78bfa}
*{box-sizing:border-box}
body{margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background:var(--bg);color:var(--text);line-height:1.5;font-size:13px}
.wrap{max-width:900px;margin:0 auto;padding:20px}
h1{font-size:18px;margin:0 0 4px}.sub{color:var(--muted);margin-bottom:14px}
.card{background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:16px;margin-bottom:12px}
.row{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:6px 8px;border-bottom:1px solid var(--border);font-size:12.5px}
.row:last-child{border-bottom:none}
.ok{color:var(--green)} .run{color:var(--accent)} .pend{color:var(--muted2,#6b7280)} .fail{color:var(--red)} .human{color:var(--violet)}
.badge{display:inline-block;padding:2px 9px;border-radius:20px;font-size:11px;font-weight:600}
.b-run{background:rgba(79,140,255,.15);color:var(--accent)}
.b-wait{background:rgba(167,139,250,.15);color:var(--violet)}
.b-ok{background:rgba(46,204,113,.15);color:var(--green)}
.b-cancel{background:rgba(231,76,60,.15);color:var(--red)}
.bar{height:8px;background:var(--panel2);border-radius:4px;overflow:hidden}
.bar>div{height:100%;background:var(--accent);transition:width .3s}
.hbox{border:1px solid rgba(167,139,250,.4);background:rgba(167,139,250,.08);border-radius:10px;padding:14px;margin-top:10px}
.btnrow{display:flex;gap:8px;margin-top:12px}
button{background:var(--panel2);border:1px solid var(--border);color:var(--text);border-radius:8px;padding:8px 14px;font-size:12.5px;cursor:pointer}
button.primary{background:var(--accent);border-color:var(--accent);color:#fff}
button.violet{background:var(--violet);border-color:var(--violet);color:#fff}
button:disabled{opacity:.45;cursor:not-allowed}
.state-switcher{position:fixed;right:14px;top:14px;background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:10px;font-size:12px;width:240px;z-index:50}
.state-switcher h4{margin:0 0 8px;color:var(--muted);font-size:11px;text-transform:uppercase}
.state-switcher button{display:block;width:100%;margin:3px 0;text-align:left}
@media(max-width:900px){.state-switcher{position:static;width:auto;margin-bottom:12px}}
</style>
</head>
<body>
<div class="wrap">
<h1>Scenario run <span style="color:var(--muted)">#SR-1842</span> <span class="badge b-run" id="runBadge">RUNNING</span></h1>
<div class="sub">FI-0080 · revision r17 · PREPROD · Elapsed <span id="elapsed">02:31</span> · <span id="prog">11 / 18</span></div>
<div class="card">
<div class="bar"><div id="pbar" style="width:61%"></div></div>
<div style="margin-top:12px" id="steps">
<div class="row"><span><span class="ok"></span> Context</span><span class="ok">0.2s</span></div>
<div class="row"><span><span class="ok"></span> Open dashboard</span><span class="ok">1.7s</span></div>
<div class="row"><span><span class="ok"></span> Apply filters</span><span class="ok">0.8s</span></div>
<div class="row"><span><span class="ok"></span> Query Superset</span><span class="ok">0.6s</span></div>
<div class="row"><span><span class="ok"></span> Download XLSX</span><span class="ok">2.4s</span></div>
<div class="row"><span><span class="run"></span> Capture screenshot</span><span class="run">running</span></div>
<div class="row"><span><span class="pend"></span> VLM analysis</span><span class="pend">queued</span></div>
<div class="row"><span><span class="pend"></span> Human review</span><span class="pend">queued</span></div>
<div class="row"><span><span class="pend"></span> Final report</span><span class="pend">queued</span></div>
</div>
</div>
<div class="card">
<div class="row"><span>Dispatch (tool → executor)</span><span class="muted">browser→ScreenshotService · superset_api→037 · xlsx→parser · assertion→037 comparison</span></div>
<div class="row"><span>Human</span><span class="human">NOT an executor — lifecycle suspend/resume control</span></div>
<div class="row"><span>Snapshot</span><span class="muted">revision r17 (immutable) · baseline v31 · runner v0.1</span></div>
</div>
<div id="humanBox" class="hbox" hidden>
<div class="sub" style="margin:0 0 8px;color:var(--violet)"><strong>WAITING FOR HUMAN</strong> — Step 08 · Verify visual anomaly</div>
<div style="font-size:12px;color:var(--muted)">[VLM screenshot] · Finding: possible clipping of total value · Confidence 0.84</div>
<div class="btnrow">
<button class="primary" onclick="resume('confirm')">Confirm issue</button>
<button onclick="resume('false_positive')">False positive</button>
<button onclick="resume('inconclusive')">Inconclusive</button>
</div>
</div>
<div class="btnrow">
<button id="cancelBtn" onclick="setState('cancelled')">⏹ Stop run</button>
<button class="violet" id="humanBtn" onclick="setState('waiting_human')">⏸ Simulate human checkpoint</button>
</div>
<div class="state-switcher">
<h4>States</h4>
<button onclick="setState('running')">running</button>
<button onclick="setState('waiting_human')">waiting_human</button>
<button onclick="setState('resumed')">resumed</button>
<button onclick="setState('cancelled')">cancelled</button>
<button onclick="setState('failed')">failed (step)</button>
<button onclick="setState('passed')">passed</button>
</div>
</div>
<script>
function setState(s){
var badge=document.getElementById('runBadge');var hb=document.getElementById('humanBox');
hb.hidden=true;
if(s==='running'){badge.className='badge b-run';badge.textContent='RUNNING';document.getElementById('elapsed').textContent='02:31';document.getElementById('prog').textContent='11 / 18';}
if(s==='waiting_human'){badge.className='badge b-wait';badge.textContent='WAITING_HUMAN';hb.hidden=false;document.getElementById('elapsed').textContent='02:38';document.getElementById('prog').textContent='12 / 18';}
if(s==='resumed'){badge.className='badge b-run';badge.textContent='RUNNING (resumed)';hb.hidden=true;document.getElementById('elapsed').textContent='02:45';document.getElementById('prog').textContent='14 / 18';}
if(s==='cancelled'){badge.className='badge b-cancel';badge.textContent='CANCELLED';hb.hidden=true;document.getElementById('elapsed').textContent='03:02';document.getElementById('prog').textContent='12 / 18';document.getElementById('cancelBtn').disabled=true;}
if(s==='failed'){badge.className='badge b-cancel';badge.textContent='FAILED';hb.hidden=true;document.getElementById('prog').textContent='13 / 18';}
if(s==='passed'){badge.className='badge b-ok';badge.textContent='PASSED';hb.hidden=true;document.getElementById('prog').textContent='18 / 18';}
}
function resume(d){document.getElementById('humanBox').hidden=true;setState('resumed');}
setState('running');
</script>
</body>
</html>
<!doctype html><html lang="ru"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Ручной запуск сценария</title><link rel="stylesheet" href="../../prototype-ui.css"></head><body><div class="shell"><header class="topbar"><div class="brand">Superset Tools · BI testing</div><nav class="nav" aria-label="Разделы"><a href="#registry">Сценарии</a><a class="active" href="#runs">Запуски</a><a href="#automation">Автоматизация</a><a href="#health">Качество</a></nav><span class="badge info">Ручной запуск</span></header>
<main><div class="page-head"><div><div class="eyebrow">Scenario run SR-1842 · revision r18</div><h1>XLSX reconciliation <span id="run-status" class="badge info">Выполняется</span></h1><p class="sub">PREPROD · Target: rc-17 · Анна · параметры сохранены в snapshot</p></div><div class="actions"><button class="btn danger" onclick="setProtoState('cancelled')">Остановить запуск</button></div></div><div id="approval" class="notice info hidden"><strong>Ожидается approval для PROD.</strong> Run создан как pending approval; steps ещё не запущены.</div>
<div class="main-aside"><section class="card"><h2>Ход проверки <span class="sub">4 из 6</span></h2><div class="step"><div class="step-no">1</div><div><strong>Открыть dashboard</strong><br><span class="sub">BrowserExecutor · 6 сек</span></div><span class="badge ok">Готово</span></div><div class="step"><div class="step-no">2</div><div><strong>Применить filters</strong><br><span class="sub">Контрагент = ACME · дата = 10.08.2026</span></div><span class="badge ok">Готово</span></div><div class="step"><div class="step-no">3</div><div><strong>Скачать XLSX</strong><br><span class="sub">XlsxExecutor · file: fi-0080.xlsx</span></div><span class="badge ok">Готово</span></div><div class="step active"><div class="step-no">4</div><div><strong>Сравнить с baseline</strong><br><span class="sub">AssertionExecutor · 2 различия обнаружены</span></div><span class="badge warn">Проверка</span></div><div class="step manual" id="human-step"><div class="step-no">5</div><div><strong>Ручная проверка evidence</strong><br><span class="sub">Manual assertion · доступна только в ручном запуске</span></div><span class="badge warn">Ожидает вас</span></div><div class="step"><div class="step-no">6</div><div><strong>Сформировать отчёт</strong><br><span class="sub">Начнётся после решения</span></div><span class="badge">Ожидает</span></div></section>
<aside class="grid"><section class="card"><h2>Evidence</h2><div class="evidence">Screenshot · filters applied<br><small>XLSX: 124 rows · baseline: 124 rows</small></div><button class="btn" style="margin-top:10px">Открыть dashboard</button></section><section id="checkpoint" class="card"><span class="badge warn">Требуется ваша проверка</span><h2 style="margin-top:10px">Комментарии соответствуют строкам?</h2><p>Сверьте screenshot и XLSX: комментарий должен относиться к строке <strong>ACME-184</strong>.</p><p class="sub">Тип: Manual assertion. Решение будет частью immutable audit trail.</p><div class="actions"><button class="btn primary" onclick="decide('pass')">✓ Соответствует</button><button class="btn danger" onclick="decide('fail')">Не соответствует</button><button class="btn" onclick="decide('inconclusive')">Неясно</button></div></section><section class="card"><h2>Почему запуск ручной?</h2><p class="sub">Сценарий содержит manual step. Его нельзя запланировать или запустить по trigger/API: пропуск проверки дал бы ложный PASS.</p></section></aside></div></main><div class="statebar"><span class="sub">State: <b id="proto-state"></b></span><button class="btn" onclick="setProtoState('running')">Running</button><button class="btn" onclick="setProtoState('waiting')">Waiting</button><button class="btn" onclick="setProtoState('passed')">Passed</button><button class="btn" onclick="setProtoState('cancelled')">Cancelled</button></div></div><script src="../../prototype-ui.js"></script><script>function decide(v){document.getElementById('checkpoint').innerHTML='<span class="badge ok">Решение сохранено</span><h2>'+({pass:'Соответствует',fail:'Не соответствует',inconclusive:'Недостаточно данных'}[v])+'</h2><p class="sub">Runner продолжает только зависимые шаги. Решение, evidence и автор зафиксированы.</p>';setProtoState(v==='pass'?'passed':'running')}protoState('waiting',s=>{let b=document.getElementById('run-status'),c=document.getElementById('checkpoint'),a=document.getElementById('approval');b.className='badge '+(s==='passed'?'ok':s==='cancelled'?'danger':'info');b.textContent=s==='passed'?'PASS':s==='cancelled'?'Остановлен':s==='waiting'?'Ожидает проверки':'Выполняется';c.classList.toggle('hidden',s!=='waiting');a.classList.toggle('hidden',s!=='approval')})</script></body></html>

View File

@@ -32,13 +32,13 @@
## R4. RunnerPlan ownership
**Decision**: Promote `runner.plan.json` from stub to a real materialized contract (Load/Validate). The compiler produces it; the runner consumes it.
**Decision**: Derive `RunnerPlan` at run start from ScenarioRevision + ParameterBinding + TargetSnapshot + baselines + runtime policy. `runner.plan.json` is a diagnostic/reference materialization only.
**Rationale**: Currently an execution plan is generated with no engine. Owning the contract closes that gap.
**Alternatives**: runner reads raw graph each time (rejected: no materialized env/baseline binding).
**Alternatives**: consuming a saved `runner.plan.json` (rejected: it can diverge from revision/bindings/target); a raw unbound graph (rejected: no preflight binding).
**Impact**: `RunnerPlan.Load/Validate`, pack artifact becomes real.
**Impact**: `RunnerPlan.Derive/Validate`; reference artifact may be regenerated but is never execution truth.
## R5. Executor reuse

View File

@@ -29,7 +29,7 @@
**Acceptance**:
1. **Given** a saved scenario with params **When** "Run" is submitted **Then** a ScenarioRun is created pinned to `scenario_id` + `revision_id` + `content_hash`, environment, and parameter snapshot.
2. **Given** the runner starts **When** execution begins **Then** the run derives RunnerPlan, preflights, and transitions through queued → running.
3. **Given** a PROD environment **When** start is requested **Then** an ActionApprovalGate (036 mechanism, generalized owner) is required before any step executes.
3. **Given** a PROD environment **When** start is requested **Then** the service creates `ScenarioRun(pending_approval)` plus an ActionApprovalGate before any step executes; approval queues it and denial/expiry blocks it.
---
@@ -54,7 +54,7 @@
**Acceptance**:
1. **Given** a `human` step is reached **When** the runner executes **Then** the run sets status `waiting_human`, persists the ScenarioStepRun as paused, creates a **HumanCheckpoint** (confirm/false_positive/inconclusive — NOT a 036 ApprovalGate) with evidence context, and stops advancing the DAG.
2. **Given** a human decision (confirm/false-positive/inconclusive) **When** submitted **Then** the step records its outcome and the runner resumes dependents from the resume token.
2. **Given** a human decision (confirm/false-positive/inconclusive) **When** submitted with its checkpoint version **Then** an atomic CAS consumes the checkpoint, records the outcome, and internally resumes dependents.
3. **Given** a page reload mid-wait **When** reopened **Then** the run status, waiting step, and gate are recoverable by `scenario_run_id`.
---
@@ -88,11 +88,11 @@
| # | Scenario | Category | Expected Behavior | Recovery |
|---|----------|----------|-------------------|----------|
| E1 | PROD run without approval | auth/gate | 403 / approval required, no dispatch | Approve via 036 gate |
| E1 | PROD run without approval | auth/gate | Durable `pending_approval` run + gate, no dispatch | Approve via ActionApprovalGate |
| E2 | Step timeout | execution | Step failed/inconclusive with reason | Retry / continue |
| E3 | Retry exhausted | execution | Step failed; dependents blocked | Triage (047) |
| E4 | Superset 5xx/403/422 | integration | Typed error taxonomy preserved | Retry / continue |
| E5 | Runner crash mid-run | resilience | Recoverable by run_id; resume from last persisted step | Resume |
| E5 | Runner crash mid-run | resilience | API/XLSX steps resume only if retry-safe; browser state replays from last browser-safe checkpoint | Recover / replay |
| E6 | Cancel mid-step | concurrency | In-flight completes or times out in drain window | — |
| E7 | Duplicate output ref | data-integrity | 038 validator rejects before run | Fix graph |
| E8 | Stale scenario run (registry) | data-quality | Warning-gated or blocked per policy | Revalidate |
@@ -104,19 +104,23 @@
- **SCEX-FR-001**: A ScenarioRun MUST be a first-class entity pinned to `scenario_id` (UUID) + `revision_id` (UUID) + `content_hash`, environment, parameter snapshot, and optional `agent_run_id`/`verification_run_id` provenance.
- **SCEX-FR-002**: The runner MUST walk the DAG in topological order and dispatch each step by `step.tool` to a typed executor (browser, superset_api, xlsx, assertion, screenshot, report, artifact).
- **SCEX-FR-003**: Execution MUST be deterministic and MUST NOT require an LLM call per step; the agent is not in the hot path.
- **SCEX-FR-004**: A `human` step MUST suspend the run (status `waiting_human`), persist step state, create a **HumanCheckpoint** (confirm/false_positive/inconclusive), and resume dependents after disposition without rerunning completed steps. HumanCheckpoint is distinct from the 036 authorization ActionApprovalGate.
- **SCEX-FR-005**: The run lifecycle MUST include queued, running, waiting_human, blocked, cancel_requested, cancelled, passed, failed, inconclusive; cancel drains in-flight within a bounded window.
- **SCEX-FR-004**: A `human` step MUST suspend the run (status `waiting_human`), persist full HumanCheckpoint state (eligibility, evidence, expiry, CAS decision version), and internally resume dependents after its one-time disposition. HumanCheckpoint is distinct from ActionApprovalGate.
- **SCEX-FR-004a**: A revision containing a `human` step MUST be `manual_run_only`. Scheduled, trigger, deploy, ETL and API execution are rejected before run creation; a HumanCheckpoint is never skipped to obtain an automatic PASS.
- **SCEX-FR-005**: The lifecycle MUST include pending_approval, queued, running, waiting_human, blocked, cancel_requested, cancelled, passed, failed, inconclusive; cancel drains in-flight within a bounded window.
- **SCEX-FR-006**: Steps MUST support attempt counts, retry policy, per-step timeout, input/output ref binding, artifact_refs, and error_code.
- **SCEX-FR-007**: A ScenarioRun MUST be recoverable by `scenario_run_id` after disconnect; results MUST carry full provenance for reproducibility.
- **SCEX-FR-007**: A ScenarioRun MUST be recoverable by `scenario_run_id`; browser recovery MUST reconstruct state from a declared browser-safe checkpoint, not continue a dead Playwright context. Results MUST carry target and execution-principal provenance.
- **SCEX-FR-008**: PROD-classified environments MUST require an ActionApprovalGate before execution.
- **SCEX-FR-009**: Executors MUST reuse 037 (metric_executor/comparison), 038 (capture), 036 (evidence/artifacts/HITL), and existing browser/xlsx infra; a second Playwright/LLM/SQL stack is forbidden.
- **SCEX-FR-010**: `human` is a runner-lifecycle control, not a dispatched executor; the executor registry covers the other seven tools.
- **SCEX-FR-011**: Failed, inconclusive and blocked runs MUST emit idempotent 036 InvestigationSignals carrying immutable run/evidence provenance; 047 creates/updates the Queue/Episode. They MUST NOT automatically start a chat, an AgentRun, or a remediation action.
- **SCEX-FR-012**: An opened InvestigationCase MAY use the agent to construct diagnostic runs and controlled experiments under delegated policy. The agent is never in the DAG hot path and cannot bypass executor contracts, runner lifecycle, capacity, mutation policy, or a required ActionApprovalGate.
- **SCEX-FR-013**: A HumanCheckpoint remains a manual-run-only analyst decision. The agent may summarize evidence but MUST NOT consume the checkpoint, choose its disposition, or turn it into scheduled automation.
### Key Entities
- **ScenarioRun**: Recoverable execution instance of a pinned scenario revision; owns status, phase, params, provenance, steps.
- **ScenarioStepRun**: One step execution attempt/record: status, attempt, timing, inputs/outputs, artifact_refs, error_code, progress.
- **RunnerPlan**: Materialized, validated execution plan (params resolved, baselines pinned, env targets set) loaded from the saved scenario pack.
- **RunnerPlan**: Deterministically derived at run start from ScenarioRevision + ParameterBinding + TargetSnapshot + baseline/runtime policy. `runner.plan.json` is diagnostic/reference materialization only.
- **ScenarioExecutorRegistry**: Mapping of `step.tool` → typed executor; `human` excluded (lifecycle control).
- **ScenarioExecutionResult**: Aggregated run result with pass/fail/inconclusive per step and provenance.
@@ -133,7 +137,7 @@
### Session 2026-08-07
- Q: Is the agent the runner? → A: No. Deterministic backend runner; the agent participates only at human checkpoints, NEEDS_CONTEXT handling, and result interpretation.
- Q: Is the agent the runner? → A: No. The backend runner is deterministic. The agent acts before/after runs through case-owned diagnostic/remediation actions, never in the executor hot path.
- Q: Is human an executor? → A: No. It is a runner-lifecycle suspend/resume control; the executor registry covers browser/superset_api/xlsx/assertion/screenshot/report/artifact.
- Q: How does this differ from VerificationRun/AgentRun? → A: ScenarioRun executes the user-created DashboardTestScenario DAG; AgentRun is the creation run; VerificationRun is release-pipeline category verification. Three distinct run concepts.

View File

@@ -13,8 +13,8 @@
## Phase 2 — RunnerPlan (promote stub to contract)
- [ ] T004 [US2] Write failing RunnerPlan Load/Validate tests in `backend/tests/services/dashboard_testing/execution/test_runner_plan.py`
- [ ] T005 [US2] Implement `load_runner_plan` in `backend/src/services/dashboard_testing/execution/runner_plan.py`
- [ ] T004 [US2] Write failing RunnerPlan derivation/validation tests in `backend/tests/services/dashboard_testing/execution/test_runner_plan.py`
- [ ] T005 [US2] Implement `derive_runner_plan` in `backend/src/services/dashboard_testing/execution/runner_plan.py`
@POST: env targets, resolved params, pinned baselines, topological order, executor mapping
@TEST_EDGE: missing plan->reject; unsafe plan->reject
@@ -24,7 +24,7 @@
@TEST_EDGE: prod_without_approval->blocked; stale_revision->blocked
- [ ] T007 [US1] Implement `ScenarioExecutorRegistry` + per-tool executors in `execution/executor_registry.py` and `execution/executors/` (reuse 037/038/036)
- [ ] T008 [US1] Implement `start_run` in `execution/runner.py`
@POST: run pinned to immutable revision; queued->running; RunnerPlan loaded
@POST: run pinned to immutable revision; queued->running; RunnerPlan deterministically derived
## Phase 4 — US2 Deterministic Dispatch

View File

@@ -10,9 +10,11 @@ paths:
summary: Global Run Operations Center — all runs with filters
security: [{ bearerAuth: [] }]
parameters:
- { name: status, in: query, schema: { type: string, enum: [queued, running, waiting_human, failed, passed, cancelled] } }
- { name: status, in: query, schema: { type: string, enum: [pending_approval, queued, running, waiting_human, blocked, cancel_requested, failed, passed, cancelled, inconclusive] } }
- { name: environment_id, in: query, schema: { type: string } }
- { name: scenario_id, in: query, schema: { type: string } }
- { name: dashboard_id, in: query, schema: { type: string } }
- { name: owner, in: query, schema: { type: string } }
- { name: trigger, in: query, schema: { type: string } }
- { name: waiting_for_me, in: query, schema: { type: boolean } }
- { name: page, in: query, schema: { type: integer, default: 1 } }
@@ -29,13 +31,13 @@ paths:
type: array
items: { $ref: "#/components/schemas/ScenarioRunRow" }
total: { type: integer }
/api/scenario-runs/{run_id}/run-configuration:
/api/scenarios/{scenario_id}/run-configuration:
get:
operationId: runCenter.configuration
summary: Pre-populated run configuration for a scenario (env/revision/release/baseline/params/toggles)
security: [{ bearerAuth: [] }]
parameters:
- { name: scenario_id, in: query, required: true, schema: { type: string } }
- { name: scenario_id, in: path, required: true, schema: { type: string } }
responses:
"200":
description: RunConfiguration template

View File

@@ -6,7 +6,7 @@
## RunConfiguration
Fields: scenario_id, revision_id, environment_id, release (optional), baseline_set (version), parameters (map), execution_toggles (optional evidence only — diagnostic screenshots, verbose logs, optional VLM; mandatory graph steps NOT toggleable), prod_gate (ActionApprovalGate ref when PROD). Validated before launch; matches 044 start contract.
Fields: scenario_id, revision_id, environment_id, target_preview, baseline_set (version), parameters (map), required_checks (read-only graph steps), execution_toggles (optional evidence only — diagnostic screenshots, verbose logs, optional VLM; mandatory graph steps such as XLSX are NOT toggleable), prod_gate (ActionApprovalGate ref when PROD). This is pre-run data and is loaded into a persistent launch panel from `GET /scenarios/{scenario_id}/run-configuration`; validated before launch and matches 044 start contract.
## RunCenterModel
@@ -28,4 +28,6 @@ Two runs pinned to the same scenario (optionally same revision). Per-step latenc
Human checkpoint evidence (screenshot, VLM finding) bound to a gate; disposition (confirm/false-positive/inconclusive) is auditable and never alters the graph.
Failure/blocked/inconclusive results show a linked Investigation Queue item when one exists. `Investigate with agent` opens the persistent 047 case workspace; it is not a modal, does not change the run, and never consumes a HumanCheckpoint.
#endregion ScenarioRunMonitor.DataModel

View File

@@ -4,7 +4,7 @@
## Summary
A standalone live Scenario Run Monitor and Results surface: run configuration dialog, live step timeline bound to 044 SSE events, human-checkpoint actions inside the monitor, final result with provenance, and run history/comparison (reusing 040 comparison UX). Consumes 044 run/step API; no new backend entities beyond frontend DTOs.
A standalone live Scenario Run Monitor and Results surface: persistent run configuration panel, live step timeline bound to 044 SSE events, human-checkpoint actions inside the monitor, final result/provenance, run history/comparison and Investigation Queue entry. Consumes 044 run/step API plus 047 queue/case DTOs.
## Technical Context
@@ -36,13 +36,13 @@ specs/045-dashboard-run-monitor/
└── prototype/index.html + manifest.md
frontend/src/lib/models/RunMonitorModel.svelte.ts
frontend/src/lib/components/scenario-run/ (RunConfigurationDialog, RunTimeline, StepInspector, HumanCheckpointPanel, ScenarioResultView, RunComparison, RunHistoryList)
frontend/src/lib/components/scenario-run/ (RunConfigurationPanel, RunTimeline, StepInspector, HumanCheckpointPanel, ScenarioResultView, RunComparison, RunHistoryList, InvestigationEntry)
frontend/src/routes/dashboard-testing/scenarios/[id]/runs/[runId]/+page.svelte
```
## Delivery Phases
1. RunConfiguration dialog + DTOs.
1. Persistent RunConfiguration panel + DTOs.
2. RunMonitorModel + SSE binding (L1 tests first).
3. Live timeline + step inspector.
4. Human checkpoint panel + disposition.

View File

@@ -1,127 +1,4 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>045 — Scenario Run Monitor & Results Prototype</title>
<style>
:root{--bg:#0f1115;--panel:#171a21;--panel2:#1d2129;--border:#2a2f3a;--text:#e6e8ee;--muted:#9aa3b2;--accent:#4f8cff;--green:#2ecc71;--warn:#f39c12;--red:#e74c3c;--violet:#a78bfa}
*{box-sizing:border-box}
body{margin:0;font-family:system-ui,Segoe UI,Roboto,sans-serif;background:var(--bg);color:var(--text);line-height:1.45}
.wrap{max-width:1100px;margin:0 auto;padding:20px}
h1{font-size:19px;margin:0 0 4px}.sub{color:var(--muted);font-size:13px;margin-bottom:14px}
.badge{display:inline-block;padding:2px 9px;border-radius:20px;font-size:11px;font-weight:600}
.b-run{background:rgba(79,140,255,.15);color:var(--accent)}
.b-wait{background:rgba(167,139,250,.15);color:var(--violet)}
.b-ok{background:rgba(46,204,113,.15);color:var(--green)}
.b-fail{background:rgba(231,76,60,.15);color:var(--red)}
.card{background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:16px;margin-bottom:12px}
.row{display:flex;justify-content:space-between;gap:10px;padding:6px 8px;border-bottom:1px solid var(--border);font-size:13px}
.row:last-child{border-bottom:none}
.ok{color:var(--green)}.run{color:var(--accent)}.pend{color:var(--muted2,#6b7280)}.fail{color:var(--red)}
.bar{height:8px;background:var(--panel2);border-radius:4px;overflow:hidden}
.bar>div{height:100%;background:var(--accent)}
.hbox{border:1px solid rgba(167,139,250,.4);background:rgba(167,139,250,.08);border-radius:10px;padding:14px;margin-top:10px}
button{background:var(--panel2);border:1px solid var(--border);color:var(--text);border-radius:8px;padding:8px 14px;font-size:13px;cursor:pointer}
button.primary{background:var(--accent);border-color:var(--accent);color:#fff}
button.violet{background:var(--violet);border-color:var(--violet);color:#fff}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px}
.kv{background:var(--panel2);border:1px solid var(--border);border-radius:8px;padding:8px}
.kv .k{font-size:11px;color:var(--muted);text-transform:uppercase}.kv .v{font-size:14px;font-weight:600}
.tabs{display:flex;gap:2px;border-bottom:1px solid var(--border);margin:12px 0}
.tab{padding:8px 14px;cursor:pointer;color:var(--muted);border-bottom:2px solid transparent;font-size:13px}
.tab.active{color:var(--text);border-bottom-color:var(--accent)}
.state-switcher{position:fixed;right:14px;top:14px;background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:10px;font-size:12px;width:250px;z-index:50}
.state-switcher h4{margin:0 0 8px;color:var(--muted);font-size:11px;text-transform:uppercase}
.state-switcher button{display:block;width:100%;margin:3px 0;text-align:left}
@media(max-width:900px){.state-switcher{position:static;width:auto;margin-bottom:12px}}
</style>
</head>
<body>
<div class="wrap">
<h1>Scenario run <span style="color:var(--muted)">#SR-1842</span> <span class="badge b-run" id="badge">RUNNING</span></h1>
<div class="sub">FI-0080 · revision r17 · PREPROD · Elapsed <span id="elapsed">02:31</span> · <span id="prog">11 / 18</span></div>
<div class="card">
<div class="bar"><div id="pbar" style="width:61%"></div></div>
<div style="margin-top:12px" id="timeline">
<div class="row"><span><span class="ok"></span> Context</span><span class="ok">0.2s</span></div>
<div class="row"><span><span class="ok"></span> Open dashboard</span><span class="ok">1.7s</span></div>
<div class="row"><span><span class="ok"></span> Query Superset</span><span class="ok">0.6s</span></div>
<div class="row"><span><span class="run"></span> Capture screenshot</span><span class="run">running</span></div>
<div class="row"><span><span class="pend"></span> VLM analysis</span><span class="pend">queued</span></div>
<div class="row"><span><span class="pend"></span> Human review</span><span class="pend">queued</span></div>
</div>
</div>
<div id="humanBox" class="hbox" hidden>
<div class="sub" style="margin:0 0 8px;color:var(--violet)"><strong>WAITING FOR HUMAN</strong> — Step 13 · Verify visual anomaly</div>
<div style="font-size:12px;color:var(--muted)">[VLM screenshot] · Finding: possible clipping of total value · Confidence 0.84</div>
<div style="margin-top:10px;display:flex;gap:8px">
<button class="primary" onclick="setState('running')">Confirm issue</button>
<button class="violet" onclick="setState('running')">False positive</button>
<button onclick="setState('running')">Inconclusive</button>
</div>
</div>
<div class="tabs"><div class="tab active">Monitor</div><div class="tab" onclick="setState('result')">Result</div><div class="tab" onclick="setState('history')">History</div><div class="tab" onclick="setState('compare')">Compare</div></div>
<div id="resultView" class="card" hidden>
<h2 style="font-size:15px">Run SR-1842 <span class="badge b-fail">FAILED</span></h2>
<div class="grid" style="margin:10px 0">
<div class="kv"><div class="k">17 checks</div><div class="v">14 pass / 2 fail / 1 inc</div></div>
<div class="kv"><div class="k">Duration</div><div class="v">04:32</div></div>
<div class="kv"><div class="k">Revision</div><div class="v">r17</div></div>
<div class="kv"><div class="k">Baseline</div><div class="v">v31</div></div>
</div>
<div class="row"><span>Revenue metric</span><span class="fail">expected 1,234,567 · actual 1,232,901 · delta -0.13%</span></div>
<div class="row"><span>XLSX rows</span><span class="fail">expected 134 · actual 132</span></div>
<div class="sub" style="margin:8px 0 0">Provenance: revision r17 · runner v0.1 · template v1 · baseline v31 · PREPROD · params snapshot · query fingerprints</div>
</div>
<div id="historyView" class="card" hidden>
<h2 style="font-size:15px">Runs</h2>
<div class="row"><span>#1842</span><span class="fail">FAIL</span><span>PREPROD · r17 · 4m32s · 17:01</span></div>
<div class="row"><span>#1831</span><span class="ok">PASS</span><span>PREPROD · r17 · 4m10s · 15:42</span></div>
<div class="row"><span>#1792</span><span class="ok">PASS</span><span>PREPROD · r16 · 3m55s · yesterday</span></div>
</div>
<div id="compareView" class="card" hidden>
<h2 style="font-size:15px">Compare #1831 vs #1842</h2>
<div class="row"><span>Revenue</span><span class="fail">1,234,567 → 1,232,901 · -0.13% (changed)</span></div>
<div class="row"><span>XLSX rows</span><span class="fail">134 → 132 · -2 (changed)</span></div>
<div class="row"><span>Duration</span><span class="warn" style="color:var(--warn)">4m10s → 4m32s · +8.8%</span></div>
<div class="sub" style="margin:8px 0 0">Same revision r17 — no revision-diff warning.</div>
</div>
<div class="state-switcher">
<h4>States</h4>
<button onclick="setState('config')">run config</button>
<button onclick="setState('running')">running (live)</button>
<button onclick="setState('waiting_human')">waiting_human</button>
<button onclick="setState('result')">result</button>
<button onclick="setState('history')">history</button>
<button onclick="setState('compare')">compare</button>
<button onclick="setState('disconnected')">disconnected/reconnect</button>
</div>
</div>
<script>
function setState(s){
var b=document.getElementById('badge');var hb=document.getElementById('humanBox');
document.getElementById('resultView').hidden=true;
document.getElementById('historyView').hidden=true;
document.getElementById('compareView').hidden=true;
hb.hidden=true;
if(s==='config'){b.className='badge b-run';b.textContent='CONFIG';document.getElementById('elapsed').textContent='—';document.getElementById('prog').textContent='ready';}
if(s==='running'){b.className='badge b-run';b.textContent='RUNNING';document.getElementById('elapsed').textContent='02:31';document.getElementById('prog').textContent='11 / 18';}
if(s==='waiting_human'){b.className='badge b-wait';b.textContent='WAITING_HUMAN';hb.hidden=false;document.getElementById('elapsed').textContent='02:38';document.getElementById('prog').textContent='12 / 18';}
if(s==='result'){b.className='badge b-fail';b.textContent='FAILED';document.getElementById('resultView').hidden=false;document.getElementById('elapsed').textContent='04:32';document.getElementById('prog').textContent='17 / 18';}
if(s==='history'){b.className='badge b-ok';b.textContent='PASS';document.getElementById('historyView').hidden=false;}
if(s==='compare'){b.className='badge b-fail';b.textContent='FAIL';document.getElementById('compareView').hidden=false;}
if(s==='disconnected'){b.className='badge b-run';b.textContent='RECONNECT';document.getElementById('prog').textContent='recovering by run_id...';}
}
setState('running');
</script>
</body>
</html>
<!doctype html><html lang="ru"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Запуски сценариев</title><link rel="stylesheet" href="../../prototype-ui.css"></head><body><div class="shell"><header class="topbar"><div class="brand">Superset Tools · BI testing</div><nav class="nav" aria-label="Разделы"><a href="#registry">Сценарии</a><a class="active" href="#runs">Запуски</a><a href="#automation">Автоматизация</a><a href="#health">Качество</a><a href="#investigation-queue">Расследования <span class="badge warn">2</span></a></nav><span class="badge info">3 активных</span></header><main><div class="page-head"><div><div class="eyebrow">Operations Center</div><h1>Запуски сценариев</h1><p class="sub">Ручные и автоматические проверки, evidence и результаты.</p></div><button class="btn" onclick="setProtoState('waiting')">Требуют моей проверки: 1</button></div>
<section id="list"><div class="card"><div class="toolbar"><select><option>Все статусы</option><option>Выполняется</option><option>Ожидает проверки</option><option>Ошибка</option></select><select><option>Все environments</option><option>PREPROD</option><option>PROD</option></select><input type="search" placeholder="Сценарий или dashboard"><button class="btn">Применить</button></div></div><div class="card" style="margin-top:16px"><table class="table"><thead><tr><th>Запуск</th><th>Сценарий</th><th>Environment</th><th>Источник</th><th>Статус</th><th></th></tr></thead><tbody><tr><td>SR-1842<br><span class="sub">сегодня, 10:42</span></td><td><strong>Комментарии по строкам</strong><br><span class="sub">FI-0080</span></td><td>PREPROD</td><td>Ручной</td><td><span class="badge warn">Ожидает вас</span></td><td><button class="btn primary" onclick="setProtoState('checkpoint')">Открыть</button></td></tr><tr><td>SR-1841</td><td><strong>XLSX reconciliation</strong></td><td>PREPROD</td><td>Schedule</td><td><span class="badge info">Выполняется</span></td><td><button class="btn">Открыть</button></td></tr><tr><td>SR-1839</td><td><strong>Фильтры и метрики</strong></td><td>PREPROD</td><td>Deploy</td><td><span class="badge danger">FAIL</span></td><td><button class="btn" onclick="setProtoState('result')">Результат</button></td></tr></tbody></table></div></section>
<section id="checkpoint" class="hidden"><div class="page-head"><div><button class="btn" onclick="setProtoState('list')">К запускам</button><h1 style="margin-top:14px">SR-1842 <span class="badge warn">Ожидает вашей проверки</span></h1><p class="sub">Ручной ScenarioRun · шаг 5 из 6 · checkpoint создан 2 минуты назад</p></div></div><div class="main-aside"><div class="card"><h2>Проверка: комментарий виден у нужной строки</h2><p>Система уже сохранила evidence. Проверьте, что «Проверено аналитиком» принадлежит строке ACME-184.</p><div class="evidence">Screenshot · выбранная строка: ACME-184<br>Submitted comment: «Проверено аналитиком»</div><div class="actions"><button class="btn primary" onclick="finish('pass')">✓ Соответствует</button><button class="btn danger" onclick="finish('fail')">Не соответствует</button><button class="btn" onclick="finish('inconclusive')">Неясно</button></div></div><aside class="card"><h2>Граница automation</h2><p class="sub">Это manual assertion. Сценарий нельзя добавить в schedule, trigger rule или API запуск.</p><button class="btn">Открыть сценарий</button></aside></div></section>
<section id="result" class="hidden"><div class="page-head"><div><button class="btn" onclick="setProtoState('list')">К запускам</button><h1 style="margin-top:14px">SR-1839 <span class="badge danger">FAIL</span></h1><p class="sub">Фильтры и метрики · PREPROD · deploy trigger</p></div><button class="btn primary" onclick="location.hash='triage'">Разобрать причину</button></div><div class="cols-3 grid"><div class="card"><div class="sub">Шаги</div><div class="kpi">4 / 5</div><span class="sub">пройдено</span></div><div class="card"><div class="sub">Отклонение</div><div class="kpi">+18</div><span class="sub">строк в XLSX</span></div><div class="card"><div class="sub">Target</div><div class="kpi">rc-17</div><span class="sub">dashboard fingerprint pinned</span></div></div><div class="card" style="margin-top:16px"><h2>Failure evidence</h2><p>Шаг «Сравнить row set» · expected 124, actual 142.</p><button class="btn">Открыть XLSX</button><button class="btn" style="margin-left:8px">Сравнить с SR-1828</button></div></section></main><div class="statebar"><span class="sub">State: <b id="proto-state"></b></span><button class="btn" onclick="setProtoState('list')">List</button><button class="btn" onclick="setProtoState('waiting')">Waiting</button><button class="btn" onclick="setProtoState('result')">Result</button></div></div><script src="../../prototype-ui.js"></script><script>function finish(out){alert('Решение «'+out+'» сохранено. Runner продолжает зависимые шаги.');setProtoState('list')}protoState('list',s=>{document.getElementById('list').classList.toggle('hidden',s==='checkpoint'||s==='result');document.getElementById('checkpoint').classList.toggle('hidden',s!=='checkpoint');document.getElementById('result').classList.toggle('hidden',s!=='result')})</script></body></html>

View File

@@ -4,7 +4,7 @@
## Prototype Metadata
- **Feature**: 045 Scenario Run Monitor & Results UX
- **Source contracts**: ux_reference.md, contracts/modules.md
- **Screens represented**: 4 (Config, Live Monitor, Result, History/Compare)
- **Screens represented**: 4 (Config, Live Monitor, Result, History/Compare) + Investigation Queue handoff → 047
- **Total states**: 7 (config, running, waiting_human, result, history, compare, disconnected/reconnect)
- **Accessibility**: keyboard nav, focus-visible, aria-live, ≥44px, prefers-reduced-motion
- **Responsive**: 375px, 900px
@@ -30,4 +30,5 @@
| US3 Human | WAITING FOR HUMAN panel | resume from monitor |
| US4 Result | result tab + provenance | counts/failures/provenance |
| US5 Compare | compare tab + history | deltas + revision check |
| Failed result | Queue handoff | analyst explicitly opens 047 case; no auto-chat |
#endregion ScenarioRunMonitor.PrototypeManifest

View File

@@ -12,10 +12,11 @@ npm run lint
```
## Exit Gates
- [ ] Run configuration dialog opens with env/revision/baseline/params/toggles; PROD gate appears
- [ ] Persistent run configuration panel opens with env/revision/baseline/params/toggles; inline PROD gate appears
- [ ] Live timeline renders from events only; step inspector shows inputs/outputs/evidence
- [ ] Reconnect recovers run by scenario_run_id
- [ ] WAITING FOR HUMAN panel resolves and resumes
- [ ] Final result shows counts + failures + full provenance
- [ ] Failed/blocked/inconclusive result exposes Investigation Queue entry without auto-starting agent work
- [ ] Comparison surfaces per-step deltas + revision-diff warning
- [ ] Scenario runs labeled distinctly from 037/040

View File

@@ -14,7 +14,7 @@
**Feature Branch**: `045-dashboard-run-monitor`
**Created**: 2026-08-07 | **Status**: Draft
**Input**: "Provide a live Scenario Run Monitor and Results UX: run configuration dialog, live step timeline with per-step status/duration/logs/evidence, human checkpoint actions inside the monitor, final result with provenance, run history and comparison. Recoverable server-side run; human actions pause and resume."
**Input**: "Provide a live Scenario Run Monitor and Results UX: persistent run configuration panel, live step timeline with per-step status/duration/logs/evidence, human checkpoint actions inside the monitor, final result/provenance, comparison, and an entry to agent-led investigation."
## User Scenarios
@@ -25,7 +25,7 @@
**Independent Test**: Open the run configuration for a scenario and verify it pre-populates environment, revision, baseline set, parameters, and per-category toggles; launching starts a run.
**Acceptance**:
1. **Given** a scenario detail **When** "Run" is clicked **Then** a configuration dialog shows Environment, Revision (current), Release, Baseline set, Parameters, and Execution toggles (screenshots, VLM, XLSX).
1. **Given** a scenario detail **When** "Run" is clicked **Then** a persistent configuration panel shows Environment, Revision (current), target/release information, Baseline set, and Parameters. It separates mandatory graph checks (for example XLSX comparison) from optional diagnostic enrichment (screenshots, verbose logs, VLM commentary), which alone may be toggled.
2. **Given** the user picks PROD **When** launch is requested **Then** a 036 approval gate appears (concurrency ceiling, volume, blast-radius dependents, reason).
3. **Given** the user confirms **When** submitted **Then** a ScenarioRun starts and the monitor opens.
@@ -101,7 +101,7 @@
| # | Scenario | Expected Behavior | Recovery |
|---|----------|-------------------|----------|
| E1 | PROD gate denied | No dispatch; denial recorded | Adjust / retry |
| E2 | Run failed | Failure detail + provenance | Triage (047) |
| E2 | Run failed | Failure detail + provenance + Investigation Queue item | Open 047 case with agent |
| E3 | Disconnect mid-run | Recover by run_id | Reopen monitor |
| E4 | Run terminated mid-step | In-flight completes/times out | View partial results |
| E5 | Compare different revisions | Revision diff surfaced | Note before compare |
@@ -121,6 +121,8 @@
- **RUNMON-FR-008**: All UI MUST follow Svelte 5 runes/model-first conventions and be keyboard-accessible.
- **RUNMON-FR-009**: A Global Run Operations Center MUST list all runs (active/queued/waiting-human/failed/recent) with filters and a "Waiting for me" view for pending human checkpoints.
- **RUNMON-FR-010**: Run Configuration MUST match the 044 start contract (environment, revision, release, baseline_set, parameters, execution_toggles for optional evidence only); mandatory graph steps MUST NOT be toggleable off.
- **RUNMON-FR-011**: Failed, blocked and inconclusive results MUST expose their Investigation Queue item and an explicit "Investigate with agent" transition into the persistent 047 case workspace. Opening it never mutates run truth or auto-executes tools.
- **RUNMON-FR-012**: Run configuration, gate decisions, conflict recovery and investigation entry MUST use persistent pages/panels or inline cards; modal/dialog interaction MUST NOT be required.
### Key Entities

View File

@@ -13,7 +13,7 @@
## Phase 2 — US1 Configure and Launch
- [ ] T003 [US1] L1 model test for `RunMonitorModel` launch in `frontend/src/lib/models/__tests__/RunMonitorModel.test.ts`
- [ ] T004 [US1] Build `RunConfigurationDialog.svelte` (env, revision, baseline, params, toggles, PROD gate)
- [ ] T004 [US1] Build persistent `RunConfigurationPanel.svelte` (env, revision, baseline, params, toggles, inline PROD gate)
- [ ] T005 [US1] L2 UX test for PROD gate in `frontend/src/routes/dashboard-testing/scenarios/[id]/runs/[runId]/__tests__/run.ux.test.ts`
## Phase 3 — US2 Live Monitor
@@ -48,7 +48,7 @@
## Phase 6c — Run Configuration binding (P0 #12/#17)
- [ ] T014f [P] Bind `RunConfigurationDialog.svelte` to 044 start contract (release/baseline_set/toggles); mandatory steps not toggleable
- [ ] T014f [P] Bind `RunConfigurationPanel.svelte` to 044 start contract (release/baseline_set/toggles); mandatory steps not toggleable
- [ ] T014g [P] L2 UX test for mandatory-step toggle protection
## Phase 7 — Polish

View File

@@ -9,11 +9,11 @@
- **Context**: Browser; from scenario detail "Run" or automation trigger.
## 2. Happy Path
Analyst opens Run, picks PREPROD + r17 + v31 baseline + params, toggles screenshots/VLM/XLSX, confirms. The monitor streams progress 11/18; at a human checkpoint a WAITING FOR HUMAN panel shows a VLM finding; analyst marks False positive; run resumes and completes PASS. The result view shows 14/1/0/1 and full provenance; analyst compares with the previous run and sees a -0.13% revenue delta.
Analyst opens the persistent launch panel, picks PREPROD + r17 + v31 baseline + params, and selects optional diagnostics. The monitor streams progress; a manual-only run may show an inline HumanCheckpoint. A failed/blocked/inconclusive result links to Investigation Queue, where the analyst may explicitly open an agent case. The result and comparison retain full provenance.
## 3. Screens & States
### Screen: Run Configuration Dialog
### Screen: Run Configuration Panel
- **Layout**: Environment select, Revision (current), Release, Baseline set, Parameters, Execution toggles.
- **@UX_STATE**: idle, validating, prod_gate, launching, error.
- **@UX_RECOVERY**: prod gate → approve/deny; 403 → no confirm.
@@ -24,7 +24,7 @@ Analyst opens Run, picks PREPROD + r17 + v31 baseline + params, toggles screensh
- **@UX_RECOVERY**: disconnect → reconnect by run_id (server-side).
### Screen: Final Result
- **Layout**: Summary card (counts, duration, revision, env, baseline) + failures (expected/actual/delta) + evidence + provenance.
- **Layout**: Summary card (counts, duration, revision, env, baseline) + failures (expected/actual/delta) + evidence + provenance + persistent Investigation Queue entry when qualifying evidence exists.
- **@UX_STATE**: loaded, failed, provenance_expanded.
### Screen: Runs History + Compare
@@ -33,7 +33,8 @@ Analyst opens Run, picks PREPROD + r17 + v31 baseline + params, toggles screensh
## 4. Error Experience
- PROD gate denied → no dispatch; record denial.
- Run failed → failure detail + triage link (047).
- Run failed → failure detail + Investigation Queue entry (047); “Investigate with agent” opens a persistent case.
- Queue is a navigation destination from the result and Operations Center; it does not become a modal or start case work before analyst selection.
- Compare different revisions → warning surfaced.
## 5. Tone & Voice

View File

@@ -7,7 +7,7 @@ paths:
/api/scenario-schedules:
post:
operationId: automation.schedule.create
summary: Create/enable a cron schedule binding scenario+revision+env
summary: Create/enable a cron schedule binding a scenario to an eligible current/pinned revision and environment
security: [{ bearerAuth: [] }]
requestBody:
required: true
@@ -15,7 +15,7 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/ScenarioSchedule"
responses: { "201": { description: Schedule created } }
responses: { "201": { description: Schedule created }, "422": { description: AUTOMATION_INELIGIBLE_HUMAN_STEP, candidate/not-activated revision, or preflight failure } }
get:
operationId: automation.schedule.list
summary: List schedules
@@ -26,16 +26,18 @@ paths:
operationId: automation.schedule.update
summary: Update a schedule (cron, revision policy, enabled)
security: [{ bearerAuth: [] }]
parameters: [{ name: schedule_id, in: path, required: true, schema: { type: string } }]
responses: { "200": { description: Updated } }
delete:
operationId: automation.schedule.delete
summary: Delete a schedule
security: [{ bearerAuth: [] }]
parameters: [{ name: schedule_id, in: path, required: true, schema: { type: string } }]
responses: { "204": { description: Deleted } }
/api/scenario-trigger-rules:
post:
operationId: automation.trigger.create
summary: Create a trigger rule (deploy/release/ETL)
summary: Create a trigger rule using an eligible current/pinned revision (deploy/release/ETL)
security: [{ bearerAuth: [] }]
requestBody:
required: true
@@ -43,7 +45,7 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/ScenarioTriggerRule"
responses: { "201": { description: Rule created } }
responses: { "201": { description: Rule created }, "422": { description: AUTOMATION_INELIGIBLE_HUMAN_STEP, candidate/not-activated revision, or preflight failure } }
get:
operationId: automation.trigger.list
summary: List trigger rules
@@ -54,17 +56,48 @@ paths:
operationId: automation.trigger.update
summary: Update a trigger rule
security: [{ bearerAuth: [] }]
parameters: [{ name: rule_id, in: path, required: true, schema: { type: string } }]
responses: { "200": { description: Updated } }
delete:
operationId: automation.trigger.delete
summary: Delete a trigger rule
security: [{ bearerAuth: [] }]
parameters: [{ name: rule_id, in: path, required: true, schema: { type: string } }]
responses: { "204": { description: Deleted } }
/api/automation-policies:
post:
operationId: automation.policy.create
summary: Create an automation policy
security: [{ bearerAuth: [] }]
requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/AutomationPolicy" } } } }
responses: { "201": { description: Policy created } }
get:
operationId: automation.policy.list
summary: List automation policies
security: [{ bearerAuth: [] }]
responses: { "200": { description: "AutomationPolicy[]" } }
/api/automation-policies/{policy_id}:
patch:
operationId: automation.policy.update
summary: Update or enable/disable an automation policy
security: [{ bearerAuth: [] }]
parameters: [{ name: policy_id, in: path, required: true, schema: { type: string } }]
requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/AutomationPolicy" } } } }
responses: { "200": { description: Updated } }
delete:
operationId: automation.policy.delete
summary: Delete an unused automation policy
security: [{ bearerAuth: [] }]
parameters: [{ name: policy_id, in: path, required: true, schema: { type: string } }]
responses: { "204": { description: Deleted } }
/api/scenarios/{scenario_id}/trigger:
post:
operationId: automation.triggerApi
summary: External API run trigger (distinct from creating a trigger rule)
security: [{ bearerAuth: [] }]
parameters:
- { name: scenario_id, in: path, required: true, schema: { type: string } }
- { name: Idempotency-Key, in: header, required: true, schema: { type: string } }
requestBody:
required: true
content:
@@ -75,9 +108,13 @@ paths:
properties:
environment_id: { type: string }
revision_id: { type: string, nullable: true }
params: { type: object }
baseline_bindings: { type: object }
target_reference: { type: object }
execution_toggles: { type: object, description: "Optional diagnostics only" }
responses:
"202": { description: Run started }
"403": { description: PROD ActionApprovalGate required }
"202": { description: "ScenarioRun accepted: queued or pending_approval; response contains run_id, status, approval_gate_id?" }
"403": { description: Permission denied; an authorized PROD request creates pending_approval rather than 403 }
/api/automation/metrics:
get:
operationId: automation.metrics
@@ -109,6 +146,10 @@ components:
environment_id: { type: string }
cron_expr: { type: string }
revision_policy: { type: string, enum: [current, pinned], default: current }
revision_id: { type: string, nullable: true, description: "Required when revision_policy=pinned; must be an activated, automation-eligible revision" }
timezone: { type: string, description: "IANA timezone" }
policy_id: { type: string }
missed_execution_policy: { type: string, enum: [skip, run_latest, queue_all], default: skip }
enabled: { type: boolean, default: true }
ScenarioTriggerRule:
type: object
@@ -117,12 +158,29 @@ components:
scenario_id: { type: string }
environment_id: { type: string }
trigger: { type: string, enum: [deploy_to_preprod, release_created, etl_completed, api] }
revision_policy: { type: string, enum: [current, pinned], default: current }
revision_id: { type: string, nullable: true, description: "Required when pinned; must be an activated, automation-eligible revision" }
policy_id: { type: string }
enabled: { type: boolean, default: true }
AutomationPolicy:
type: object
required: [name, enabled, workload_class, max_concurrent_per_env]
properties:
id: { type: string, nullable: true }
name: { type: string }
enabled: { type: boolean }
workload_class: { type: string, enum: [scenario_smoke, scenario_regression] }
max_concurrent_per_env: { type: integer, minimum: 1 }
dedup_window_seconds: { type: integer, minimum: 0 }
overlap_rule: { type: string, enum: [block, warn] }
retention_days: { type: integer, minimum: 1 }
prod_gate_required: { type: boolean }
on_repeated_failure: { type: string, enum: [alert, disable] }
NotificationEvent:
type: object
properties:
id: { type: string }
type: { type: string, enum: [completed, failed, blocked, human_action_required, scenario_stale, repeated_flaky_failure] }
type: { type: string, enum: [completed, failed, blocked, scenario_stale, repeated_flaky_failure] }
scenario_id: { type: string }
run_id: { type: string, nullable: true }
severity: { type: string }

View File

@@ -6,23 +6,27 @@
## ScenarioSchedule
Fields: id, scenario_id, revision_policy (current|pinned), environment_id, cron_expr, timezone, enabled, policy_id, created_by, created_at. Reuses existing APScheduler.
Fields: id, scenario_id, revision_policy (current|pinned), revision_id (required when pinned), environment_id, cron_expr, timezone, missed_execution_policy, enabled, policy_id, created_by, created_at. Reuses existing APScheduler.
Schedule/trigger creation and every `current`-policy dispatch preflight require `automation_eligible=true` **and** an activated 042 `current` revision. A newly saved `candidate` is never silently adopted by a `current` schedule; it becomes eligible only through 042 atomic activation. A revision containing any 044 human step is `manual_run_only`; the service rejects the rule/dispatch with `AUTOMATION_INELIGIBLE_HUMAN_STEP`, rather than skipping the step or emitting a false PASS.
## Scheduler semantics (#22)
Explicit APScheduler configuration: timezone (per schedule, IANA), DST policy (cron still fires on wall-clock), `misfire_grace_time` (default 300s), `coalesce` (true — missed occurrences collapsed to one), `max_instances` (1 — no overlapping instances of the same schedule). Missed-execution policy (scheduler down then returns): `run_latest | skip | queue_all` declared per schedule; default skip. Scheduler-restart replays non-coalesced due schedules per policy.
Explicit APScheduler configuration: timezone (per schedule, IANA), DST policy (cron still fires on wall-clock), `misfire_grace_time` (default 300s), and `max_instances` (1 — no overlapping instances of the same schedule). `coalesce` is derived, never independently configured: `skip` ignores missed occurrences, `run_latest` sets `coalesce=true`, and `queue_all` sets `coalesce=false`. Scheduler restart applies that derived policy.
## ScenarioTriggerRule
Fields: id, scenario_id, environment_id, trigger (deploy_to_preprod | release_created | etl_completed | api), revision_policy, enabled, policy_id. Mapped onto the 037 trigger framework (release_create/scheduled hooks).
Fields: id, scenario_id, environment_id, trigger (deploy_to_preprod | release_created | etl_completed | api), revision_policy, revision_id (required when pinned), enabled, policy_id. Mapped onto the 037 trigger framework (release_create/scheduled hooks).
## NotificationEvent
Fields: id, type (completed|failed|blocked|human_action_required|scenario_stale|repeated_flaky_failure), scenario_id, run_id?, severity, payload, emitted_at. Channel delivery left to infrastructure; domain event contracts exist.
Fields: id, type (completed|failed|blocked|scenario_stale|repeated_flaky_failure), scenario_id, run_id?, severity, payload, emitted_at, investigation_signal_id?, investigation_queue_item_id?. Channel delivery is infrastructure-owned; qualifying attention events emit the canonical 036 `InvestigationSignal`; 047 creates/updates the Queue item from it rather than auto-starting agent work. `human_action_required` is not an automation event because human-step revisions are manual-run-only.
## AutomationPolicy
Fields: id, name, max_concurrent_per_env, dedup_window_seconds, overlap_rule (block|warn), retention_days, prod_gate_required (bool), on_repeated_failure (alert|disable). Applies to schedule/trigger runs.
Fields: id, name, enabled, workload_class, max_concurrent_per_env, dedup_window_seconds, overlap_rule (block|warn), retention_days, prod_gate_required (bool), on_repeated_failure (alert|disable). Applies to schedule/trigger runs through the global 044 ExecutionCapacityManager; it cannot reserve capacity owned by another workload class.
An agent may create, modify, pause or resume schedules/trigger rules under delegated policy and may investigate a repeated failure after an analyst opens its queue item. Scheduler dispatch, eligibility checks, deduplication, capacity, retention and trigger provenance stay deterministic. A policy-gated automation mutation is represented by an inline ActionApprovalGate, never a modal.
## Retention tiers (#23)
@@ -30,6 +34,6 @@ Layered retention independent of the analytics minimum history window: run metad
## RunBinding
A triggered run pins scenario_id + revision_id + environment_id + triggered_by (from 044). Overlap/concurrency evaluated against AutomationPolicy.
A triggered run pins scenario_id + revision_id + environment_id + target snapshot + execution principal + server-owned trigger source (from 044). Concurrency bucket (for capacity) may be environment+workload class; dedup identity is `canonical_execution_request_hash`, or for an event trigger `(source_type, source_event_id, scenario_id)`. External API triggers require an `Idempotency-Key`; same key/hash returns the same run, while a changed canonical request returns 409.
#endregion ScenarioAutomation.DataModel

View File

@@ -1,96 +1,2 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>046 — Scenario Automation & Operations Prototype</title>
<style>
:root{--bg:#0f1115;--panel:#171a21;--panel2:#1d2129;--border:#2a2f3a;--text:#e6e8ee;--muted:#9aa3b2;--accent:#4f8cff;--green:#2ecc71;--warn:#f39c12;--red:#e74c3c}
*{box-sizing:border-box}
body{margin:0;font-family:system-ui,Segoe UI,Roboto,sans-serif;background:var(--bg);color:var(--text);line-height:1.5}
.wrap{max-width:900px;margin:0 auto;padding:20px}
h1{font-size:19px;margin:0 0 4px}.sub{color:var(--muted);font-size:13px;margin-bottom:14px}
.card{background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:16px;margin-bottom:12px}
.card h2{font-size:14px;margin:0 0 12px}
.opt{display:flex;align-items:center;gap:10px;padding:8px 10px;border:1px solid var(--border);border-radius:8px;margin-bottom:8px;background:var(--panel2)}
.opt input{accent-color:var(--accent);width:16px;height:16px}
.opt label{flex:1}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px;margin-bottom:12px}
.kv{background:var(--panel2);border:1px solid var(--border);border-radius:8px;padding:8px}
.kv .k{font-size:11px;color:var(--muted);text-transform:uppercase}.kv .v{font-size:13px}
button{background:var(--panel2);border:1px solid var(--border);color:var(--text);border-radius:8px;padding:8px 14px;font-size:13px;cursor:pointer}
button.primary{background:var(--accent);border-color:var(--accent);color:#fff}
.banner{padding:9px 12px;border-radius:8px;margin-bottom:10px;font-size:13px}
.b-over{background:rgba(243,156,18,.12);border:1px solid rgba(243,156,18,.4)}
.b-prod{background:rgba(231,76,60,.12);border:1px solid rgba(231,76,60,.4)}
.row{display:flex;justify-content:space-between;gap:10px;padding:6px 8px;border-bottom:1px solid var(--border);font-size:13px}
.state-switcher{position:fixed;right:14px;top:14px;background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:10px;font-size:12px;width:240px;z-index:50}
.state-switcher h4{margin:0 0 8px;color:var(--muted);font-size:11px;text-transform:uppercase}
.state-switcher button{display:block;width:100%;margin:3px 0;text-align:left}
@media(max-width:900px){.state-switcher{position:static;width:auto;margin-bottom:12px}}
</style>
</head>
<body>
<div class="wrap">
<h1>Automation — XLSX reconciliation</h1>
<div class="sub">046 — Scenario Automation &amp; Operations · revision r17 · <span id="stateLabel">State: idle</span></div>
<div id="overBanner" class="banner b-over" hidden>Scheduled run overlaps a PREPROD deployment window — blocked per policy (or warning-gated).</div>
<div id="prodBanner" class="banner b-prod" hidden>PROD scheduled run requires approval gate (036) before dispatch.</div>
<div class="card">
<h2>Triggers</h2>
<div class="opt"><input type="checkbox" checked><label>Every PREPROD deployment</label></div>
<div class="opt"><input type="checkbox" checked><label>Daily at 07:00</label></div>
<div class="opt"><input type="checkbox"><label>On release created</label></div>
<div class="opt"><input type="checkbox"><label>After ETL completed</label></div>
</div>
<div class="card">
<h2>Policy</h2>
<div class="grid">
<div class="kv"><div class="k">Max concurrent / env</div><div class="v">5</div></div>
<div class="kv"><div class="k">Dedup window</div><div class="v">10 min</div></div>
<div class="kv"><div class="k">Overlap rule</div><div class="v">block</div></div>
<div class="kv"><div class="k">Retention</div><div class="v">90 days</div></div>
</div>
<div class="opt"><input type="checkbox"><label>Require PROD approval gate</label></div>
<div class="opt"><input type="checkbox" checked><label>Notify on: completed, failed, blocked, human-action, stale, repeated-flaky</label></div>
</div>
<button class="primary" onclick="setState('saved')">Save automation</button>
<div id="metrics" class="card" style="margin-top:12px" hidden>
<h2>Operational metrics</h2>
<div class="row"><span>Scheduled runs (30d)</span><span>183</span></div>
<div class="row"><span>Success rate</span><span class="green" style="color:var(--green)">94.2%</span></div>
<div class="row"><span>Trigger distribution</span><span>deploy 61% · schedule 28% · release 11%</span></div>
<div class="row"><span>Repeated flaky alert</span><span class="warn" style="color:var(--warn)">"Wait for dashboard loaded" — 7 failures / 183 runs</span></div>
</div>
<div class="state-switcher">
<h4>States</h4>
<button onclick="setState('idle')">idle</button>
<button onclick="setState('saved')">saved</button>
<button onclick="setState('overlap_warning')">overlap warning</button>
<button onclick="setState('prod_gate')">PROD gate</button>
<button onclick="setState('metrics')">metrics</button>
</div>
</div>
<script>
function setState(s){
var lbl=document.getElementById('stateLabel');
document.getElementById('overBanner').hidden=true;
document.getElementById('prodBanner').hidden=true;
document.getElementById('metrics').hidden=true;
if(s==='idle'){lbl.textContent='State: idle';}
if(s==='saved'){lbl.textContent='State: saved';}
if(s==='overlap_warning'){lbl.textContent='State: overlap_warning';document.getElementById('overBanner').hidden=false;}
if(s==='prod_gate'){lbl.textContent='State: PROD gate';document.getElementById('prodBanner').hidden=false;}
if(s==='metrics'){lbl.textContent='State: metrics';document.getElementById('metrics').hidden=false;}
}
setState('idle');
</script>
</body>
</html>
<!doctype html><html lang="ru"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Автоматизация сценариев</title><link rel="stylesheet" href="../../prototype-ui.css"></head><body><div class="shell"><header class="topbar"><div class="brand">Superset Tools · BI testing</div><nav class="nav" aria-label="Разделы"><a href="#registry">Сценарии</a><a href="#runs">Запуски</a><a class="active" href="#automation">Автоматизация</a><a href="#health">Качество</a></nav><button class="btn primary" onclick="setProtoState('new')">Новое правило</button></header><main><div class="page-head"><div><div class="eyebrow">Automation management</div><h1>Расписания и правила запуска</h1><p class="sub">Аналитик сам настраивает автоматические проверки. В список попадают только полностью автоматизируемые revisions.</p></div></div><div id="list" class="grid"><section class="card"><h2>Расписания</h2><table class="table"><thead><tr><th>Сценарий</th><th>Когда</th><th>Revision</th><th>Environment</th><th>Статус</th></tr></thead><tbody><tr><td><strong>XLSX reconciliation</strong></td><td>Каждый день · 09:00 Europe/Simferopol</td><td>Current</td><td>PREPROD</td><td><span class="badge ok">Включено</span></td></tr><tr><td><strong>Фильтры и метрики</strong></td><td>Пн–Пт · 08:30</td><td>r12 pinned</td><td>PREPROD</td><td><span class="badge ok">Включено</span></td></tr></tbody></table></section><section class="card"><h2>Trigger rules</h2><table class="table"><thead><tr><th>Сценарий</th><th>Событие</th><th>Policy</th><th></th></tr></thead><tbody><tr><td>XLSX reconciliation</td><td>Deploy to PREPROD</td><td>Smoke / max 2</td><td><button class="btn">Изменить</button></td></tr><tr><td>Фильтры и метрики</td><td>ETL completed</td><td>Regression / max 1</td><td><button class="btn">Изменить</button></td></tr></tbody></table></section></div>
<section id="new" class="hidden"><div class="page-head"><div><button class="btn" onclick="setProtoState('list')">К правилам</button><h1 style="margin-top:14px">Новое расписание</h1><p class="sub">Сначала проверим, может ли выбранная revision выполняться без человека.</p></div></div><div class="main-aside"><section class="card"><h2>Что запускать</h2><label class="sub">Сценарий</label><select style="width:100%;margin:5px 0 12px" onchange="checkScenario(this.value)"><option value="xlsx">XLSX reconciliation · fully automated</option><option value="comments">Комментарии по строкам · manual-run-only</option></select><label class="sub">Revision policy</label><select style="width:100%;margin:5px 0 12px"><option>Current revision</option><option>Pin r18</option></select><label class="sub">Расписание</label><input style="width:100%;margin:5px 0 12px" value="0 9 * * *"><label class="sub">Environment</label><select style="width:100%;margin:5px 0 12px"><option>PREPROD</option><option>PROD</option></select><div class="actions"><button class="btn" onclick="setProtoState('list')">Отмена</button><button id="save" class="btn primary" onclick="setProtoState('saved')">Сохранить правило</button></div></section><aside class="card"><h2>Preflight automation</h2><div id="eligibility" class="notice info"><strong>Можно автоматизировать</strong><br>Нет HumanSteps; registry action contracts и target policy валидны.</div><p class="sub">Если future current revision добавит HumanStep, запуск не создаётся: правило получает статус «Нужна revision без ручной проверки».</p></aside></div></section><section id="saved" class="hidden"><div class="card"><span class="badge ok">Правило включено</span><h2 style="margin-top:10px">XLSX reconciliation будет запускаться ежедневно в 09:00</h2><p class="sub">Dedup использует canonical execution request, а capacity контролируется отдельно для PREPROD.</p><button class="btn primary" onclick="setProtoState('list')">К правилам</button></div></section></main><div class="statebar"><span class="sub">State: <b id="proto-state"></b></span><button class="btn" onclick="setProtoState('list')">List</button><button class="btn" onclick="setProtoState('new')">New</button><button class="btn" onclick="setProtoState('saved')">Saved</button></div></div><script src="../../prototype-ui.js"></script><script>function checkScenario(v){let e=document.getElementById('eligibility'),b=document.getElementById('save');if(v==='comments'){e.className='notice danger';e.innerHTML='<strong>Нельзя автоматизировать</strong><br>Revision содержит RunHumanCheckpoint. Создайте полностью автоматизируемую revision.';b.disabled=true}else{e.className='notice info';e.innerHTML='<strong>Можно автоматизировать</strong><br>Нет HumanSteps; registry action contracts и target policy валидны.';b.disabled=false}}protoState('list',s=>['list','new','saved'].forEach(id=>document.getElementById(id).classList.toggle('hidden',id!==s))</script></body></html>

View File

@@ -25,7 +25,7 @@
**Independent Test**: Configure a schedule and a deploy/release trigger for a scenario and verify runs are created on the trigger with the correct environment and revision.
**Acceptance**:
1. **Given** a scenario **When** a schedule is configured **Then** runs are created per cron/daily schedule with the pinned revision and environment.
1. **Given** a scenario **When** a schedule is configured **Then** runs are created per cron/daily schedule with the pinned eligible revision or the atomically activated current revision and environment.
2. **Given** a PREPROD deploy or release-created event fires **When** matched **Then** a scenario run is triggered automatically (reuse 037 trigger framework).
3. **Given** an ETL completion event **When** matched **Then** a scenario run is triggered.
@@ -40,7 +40,7 @@
**Acceptance**:
1. **Given** a run completes **When** the run ends **Then** a "Scenario completed" notification domain event is emitted.
2. **Given** a run fails or blocks **When** the run ends **Then** a "Scenario failed"/"Scenario blocked" event is emitted.
3. **Given** a human checkpoint is reached **When** a run pauses **Then** a "Human action required" event is emitted.
3. **Given** a scenario revision contains a human step **When** automation is configured or dispatch preflight runs **Then** it is rejected as manual-run-only; no scheduled run and no false-PASS path exists.
4. **Given** a scenario becomes stale **When** detected **Then** a "Scenario became stale" event is emitted.
---
@@ -92,8 +92,8 @@
### Functional
- **SCAUTO-FR-001**: Scenario runs MUST be triggerable by manual, PREPROD deploy, release-created, ETL-completed, scheduled, and API triggers, reusing the 037 trigger framework.
- **SCAUTO-FR-002**: Scheduled runs MUST pin the scenario revision and environment at trigger time.
- **SCAUTO-FR-003**: The system MUST emit domain notification events: completed, failed, blocked, human-action-required, scenario-stale, repeated-flaky-failure.
- **SCAUTO-FR-002**: Scheduled runs MUST pin the scenario revision and environment at trigger time. A newly saved candidate MUST NOT be adopted until 042 activation; a pinned rule MUST reject an ineligible revision.
- **SCAUTO-FR-003**: The system MUST emit domain notification events: completed, failed, blocked, scenario-stale, repeated-flaky-failure. `human-action-required` is excluded because HumanCheckpoint scenarios are manual-run-only.
- **SCAUTO-FR-004**: Concurrency and deduplication policies MUST prevent redundant parallel runs on the same environment+revision.
- **SCAUTO-FR-005**: Schedule/deployment-window overlap MUST be blocked or warning-gated per policy.
- **SCAUTO-FR-006**: Retention MUST prune old runs per policy while preserving provenance and referenced artifacts.
@@ -102,8 +102,11 @@
- **SCAUTO-FR-009**: Scenario automation MUST NOT write into the 037 baseline catalog or release verification pipeline; scenario runs remain distinct.
- **SCAUTO-FR-010**: Schedules, trigger rules, and policies MUST be fully manageable (CRUD + enable/disable) via an Automation Management UI.
- **SCAUTO-FR-011**: An external API run trigger MUST exist as `POST /scenarios/{id}/trigger`, distinct from configuring a trigger rule; PROD requires an ActionApprovalGate.
- **SCAUTO-FR-012**: Scheduler semantics MUST be explicit (timezone, DST, misfire_grace_time, coalesce, max_instances, missed-execution policy, scheduler-restart handling).
- **SCAUTO-FR-013**: Retention MUST use layered tiers; the analytics minimum history window (047) MUST be guaranteed independent of run retention.
- **SCAUTO-FR-012**: Failed, blocked, stale and repeated-failure automation events MUST emit idempotent 036 InvestigationSignals with trigger/run provenance; 047 creates/updates the Queue item. They MUST NOT automatically start an agent conversation or action.
- **SCAUTO-FR-013**: An analyst-opened case MAY let the agent create, edit, pause or resume schedules and trigger rules when delegated policy permits. Scheduler dispatch, eligibility, deduplication, capacity and trigger provenance MUST remain deterministic; non-delegated mutations require an inline ActionApprovalGate.
- **SCAUTO-FR-014**: Automation management and policy decisions MUST be completed in persistent pages, agent cases or inline cards; modal/dialog interaction MUST NOT be required.
- **SCAUTO-FR-015**: Scheduler semantics MUST be explicit (timezone, DST, misfire_grace_time, coalesce, max_instances, missed-execution policy, scheduler-restart handling).
- **SCAUTO-FR-016**: Retention MUST use layered tiers; the analytics minimum history window (047) MUST be guaranteed independent of run retention.
### Key Entities

View File

@@ -1,25 +1,25 @@
# Requirements Checklist: Failure Triage & Quality Analytics (047)
# Requirements Checklist: Investigation Queue & Scenario Analytics (047)
**Purpose**: Verify SCAN-FR-001..006 completeness. | **Created**: 2026-08-07
**Purpose**: Verify SCAN-FR-001..011 completeness. | **Created**: 2026-08-07
## Triage (FR-001/005/006)
## Queue and Case (FR-001..003/009/011)
- [ ] CHK001 Failed runs support triage split: investigation_status (New/Investigating/Resolved), classification, resolution (Fixed/Accepted risk/Duplicate/Won't fix)
- [ ] CHK002 Classification (product/data/baseline/env/flaky/scenario-bug/infra)
- [ ] CHK003 Triage persisted + auditable; never alters graph/result/baseline
- [ ] CHK004 RBAC scenario-result:view vs :triage
- [ ] CHK001 Qualifying events create/update a deduplicated queue item and never auto-start agent work
- [ ] CHK002 Analyst explicitly opens Queue item into Case with evidence/chat/actions
- [ ] CHK003 Case disposition projects triage through CAS/audit; never alters graph/result/baseline
- [ ] CHK004 Object ACL plus scenario-result:view vs :triage enforced
## Flakiness & Health (FR-002/003)
## Flakiness & Health (FR-004..006)
- [ ] CHK005 Per-step flaky detection with ratio
- [ ] CHK006 Scenario health: 30d success rate, flaky ratio, infra ratio, most unstable step
- [ ] CHK005 Per-step flaky detection requires post-failure pass and two transitions; excluded outcomes are not in denominator
- [ ] CHK006 Contextual health: product/test/infra/overall plus confidence
- [ ] CHK007 Health feeds 042 registry badge on threshold cross
## Trends & Recurring (FR-004/005)
## Trends & Recurring (FR-007/008/010)
- [ ] CHK008 Success-rate trend + failure-classification distribution
- [ ] CHK009 Recurring failures grouped (count, first/last occurrence)
- [ ] CHK010 Known-issue/Accepted recurrences flagged but not re-alerted as new
- [ ] CHK010 Matching recurrence after resolved episode opens a new alertable FailureEpisode
## Success Criteria

View File

@@ -1,21 +1,32 @@
#region ScenarioAnalytics.Modules [C:4] [TYPE ADR] [SEMANTICS scenario,analytics,contracts,modules,triage,flakiness]
@BRIEF Module contracts for Failure Triage & Quality Analytics (047).
@defgroup ScenarioAnalytics Triage, classification, flakiness, health, trends for scenario runs.
@BRIEF Module contracts for Investigation Queue/Case and deterministic scenario analytics (047).
@defgroup ScenarioAnalytics Queue/case, compact disposition projection, flakiness, health, trends for scenario runs.
@RELATION DEPENDS_ON -> [ScenarioExecution.Modules]
@RELATION DEPENDS_ON -> [ScenarioRegistry.Modules]
@RATIONALE Triage/analytics make red runs actionable and distinguish product/data regressions from flaky/infra failures; never mutate graph/result/baseline.
@RATIONALE Queue/case makes red runs actionable through an analyst-controlled agent workstream, while deterministic analytics distinguish product/data regressions from flaky/infra failures; never mutate graph/result/baseline.
@REJECTED Ending at pass/fail; triage altering result truth.
# #region Analytics.Triage [C:4] [TYPE Function] [SEMANTICS scenario,analytics,triage,status]
# #region Analytics.OpenCase [C:4] [TYPE Function] [SEMANTICS scenario,analytics,queue,case,agent]
# @ingroup ScenarioAnalytics
# @BRIEF Set triage (investigation_status + classification + resolution) on a run/step; persisted and audited.
# @PRE caller has scenario-result:triage; run exists.
# @POST TriageRecord saved/updated; auditable; RunResult unchanged (immutable truth).
# @BRIEF Explicitly open a queued signal into an InvestigationCase and agent thread.
# @PRE caller has source-object/evidence access; queue item is active.
# @POST one durable case returned/created; no RunResult or baseline changed.
# @SIDE_EFFECT DB write; audit; agent thread provision only.
# @INVARIANT a queue event alone never starts the case or tool action.
# @TEST_EDGE duplicate-open->existing case; denied->403; suppressed->409.
def open_case(db, queue_item_id, actor): ...
# #endregion Analytics.OpenCase
# #region Analytics.Disposition [C:4] [TYPE Function] [SEMANTICS scenario,analytics,case,triage,status]
# @ingroup ScenarioAnalytics
# @BRIEF Persist an analyst-confirmed case disposition as compact TriageRecord projection.
# @PRE caller has scenario-result:triage; case decision version matches.
# @POST TriageRecord updated/audited; RunResult unchanged (immutable truth).
# @SIDE_EFFECT DB write; audit.
# @INVARIANT triage is orthogonal metadata; does not change run result or baselines.
# @INVARIANT case disposition is orthogonal metadata; does not change run result or baselines.
# @TEST_EDGE concurrent->409; denied->403; audit recorded; run stays FAILED.
def set_triage(db, run_id, logical_step_id, investigation_status, classification, resolution, comment, actor): ...
# #endregion Analytics.Triage
def set_disposition(db, case_id, decision_version, disposition, actor): ...
# #endregion Analytics.Disposition
# #region Analytics.Flakiness [C:4] [TYPE Function] [SEMANTICS scenario,analytics,flakiness,detect]
# @ingroup ScenarioAnalytics
@@ -41,7 +52,7 @@ def trends(db, scenario_id): ...
# #region Analytics.Recurring [C:4] [TYPE Function] [SEMANTICS scenario,analytics,recurring,group]
# @ingroup ScenarioAnalytics
# @BRIEF Group recurring failures by fingerprint; flag known-issue/accepted without re-alerting.
# @BRIEF Group recurring failures by fingerprint; deduplicate only an active FailureEpisode and alert a recurrence after resolution.
# @POST returns RecurringFailureGroup[] with counts and first/last occurrence.
def recurring(db, scenario_id): ...
# #endregion Analytics.Recurring

View File

@@ -1,71 +1,224 @@
openapi: 3.1.0
info:
title: Scenario Analytics & Triage API
version: 0.2.0
description: Failure triage, flakiness, health, trends, recurring failures for scenarios (047). Health/trends aggregate scenario history.
title: Investigation Queue & Scenario Analytics API
version: 0.3.0
description: Analyst-opened agentic investigation cases over deterministic scenario health, trends and recurring failures.
paths:
/api/scenario-runs/{run_id}/triage:
/api/internal/investigation-signals:
post:
operationId: analytics.triage
summary: Set triage on a run/step (investigation_status + classification + resolution)
security: [{ bearerAuth: [] }]
parameters: [{ name: run_id, in: path, required: true, schema: { type: string } }]
operationId: investigations.ingestSignal
summary: Idempotently ingest a deterministic producer signal into the Investigation Queue
description: Internal producer route. Ingestion creates/updates Queue/Episode only and never starts an agent chat or AgentRun.
security: [{ serviceAndUser: [] }]
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/TriageRequest"
schema: { $ref: "#/components/schemas/InvestigationSignal" }
responses:
"200": { description: Triage saved (audited) }
"403": { description: Requires scenario-result:triage }
"409": { description: Conflict }
"202": { description: Signal accepted; Queue item created or updated deterministically, content: { application/json: { schema: { $ref: "#/components/schemas/InvestigationQueueItem" } } } }
"409": { description: Signal identity was reused with non-canonical content }
/api/investigation-queue:
get:
operationId: investigations.listQueue
summary: List deduplicated attention items; listing never starts an agent run
security: [{ bearerAuth: [] }]
parameters:
- { name: state, in: query, schema: { type: string, enum: [new, acknowledged, case_opened, suppressed, resolved] } }
- { name: scenario_id, in: query, schema: { type: string } }
- { name: severity, in: query, schema: { type: string, enum: [info, warning, critical] } }
responses:
"200":
description: InvestigationQueueItem[]
content: { application/json: { schema: { type: array, items: { $ref: "#/components/schemas/InvestigationQueueItem" } } } }
/api/investigation-queue/{queue_item_id}/open-case:
post:
operationId: investigations.openCase
summary: Explicitly open a persistent InvestigationCase and its agent thread
security: [{ bearerAuth: [] }]
parameters: [{ name: queue_item_id, in: path, required: true, schema: { type: string, format: uuid } }]
responses:
"201":
description: Case opened
content: { application/json: { schema: { $ref: "#/components/schemas/InvestigationCase" } } }
"200":
description: Existing case for the active queue item
content: { application/json: { schema: { $ref: "#/components/schemas/InvestigationCase" } } }
"403": { description: Requires source-object and evidence access }
"409": { description: Queue item is suppressed/resolved or has incompatible case state }
/api/investigation-cases/{case_id}:
get:
operationId: investigations.getCase
summary: Get persistent case, evidence, compact triage and action timeline
security: [{ bearerAuth: [] }]
parameters: [{ name: case_id, in: path, required: true, schema: { type: string, format: uuid } }]
responses:
"200": { description: InvestigationCase, content: { application/json: { schema: { $ref: "#/components/schemas/InvestigationCase" } } } }
"403": { description: Requires source-object and evidence access }
"404": { description: Not found }
/api/investigation-cases/{case_id}/disposition:
post:
operationId: investigations.setDisposition
summary: Record an analyst-confirmed case disposition and update compact triage projection
security: [{ bearerAuth: [] }]
parameters: [{ name: case_id, in: path, required: true, schema: { type: string, format: uuid } }]
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/CaseDispositionRequest" }
responses:
"200": { description: Updated case and audited TriageRecord projection, content: { application/json: { schema: { $ref: "#/components/schemas/InvestigationCase" } } } }
"403": { description: Requires scenario-result:triage and object access }
"409": { description: Stale decision_version or terminal-case conflict }
"422": { description: Invalid state/disposition combination }
/api/scenarios/{scenario_id}/health:
get:
operationId: analytics.health
summary: Scenario health (30d success, flaky, infra) — feeds 042 badge
summary: Contextual deterministic health feeding 042 badge
security: [{ bearerAuth: [] }]
parameters: [{ name: scenario_id, in: path, required: true, schema: { type: string } }]
parameters:
- { name: scenario_id, in: path, required: true, schema: { type: string } }
- { name: environment_class, in: query, schema: { type: string } }
- { name: context_key, in: query, schema: { type: string } }
responses:
"200": { description: ScenarioHealth }
"200": { description: ScenarioHealth, content: { application/json: { schema: { $ref: "#/components/schemas/ScenarioHealth" } } } }
/api/scenarios/{scenario_id}/trends:
get:
operationId: analytics.trends
summary: Success-rate trend + failure-classification distribution
security: [{ bearerAuth: [] }]
parameters: [{ name: scenario_id, in: path, required: true, schema: { type: string } }]
responses: { "200": { description: Trends } }
/api/scenarios/{scenario_id}/recurring-failures:
get:
operationId: analytics.recurring
summary: Recurring failure groups (immutable fingerprint)
summary: Deterministic success and classified/disposition trend series
security: [{ bearerAuth: [] }]
parameters: [{ name: scenario_id, in: path, required: true, schema: { type: string } }]
responses:
"200":
description: RecurringFailureGroup[]
content:
application/json:
schema: { type: array, items: { $ref: "#/components/schemas/RecurringFailureGroup" } }
"200": { description: Trends, content: { application/json: { schema: { $ref: "#/components/schemas/Trends" } } } }
/api/scenarios/{scenario_id}/recurring-failures:
get:
operationId: analytics.recurring
summary: Compatibility-scoped recurring groups and episodes
security: [{ bearerAuth: [] }]
parameters: [{ name: scenario_id, in: path, required: true, schema: { type: string } }]
responses:
"200": { description: "RecurringFailureGroup[]", content: { application/json: { schema: { type: array, items: { $ref: "#/components/schemas/RecurringFailureGroup" } } } } }
components:
securitySchemes:
bearerAuth: { type: http, scheme: bearer }
serviceAndUser: { type: http, scheme: bearer, description: "Trusted producer service with originating user/provenance context" }
schemas:
TriageRequest:
InvestigationQueueItem:
type: object
required: [investigation_status]
required: [id, source_type, severity, state, count, first_seen_at, last_seen_at]
properties:
step_id: { type: string, nullable: true }
id: { type: string, format: uuid }
source_type: { type: string, enum: [scenario_run, staleness_signal, baseline_immutability, load_finding, automation_failure] }
scenario_id: { type: [string, 'null'] }
run_id: { type: [string, 'null'], format: uuid }
logical_step_id: { type: [string, 'null'], format: uuid }
severity: { type: string, enum: [info, warning, critical] }
fingerprint: { type: [string, 'null'] }
active_episode_id: { type: [string, 'null'], format: uuid }
evidence_summary: { type: object, additionalProperties: true }
target_snapshot: { type: object, additionalProperties: true }
suggested_next_action: { type: [string, 'null'] }
state: { type: string, enum: [new, acknowledged, case_opened, suppressed, resolved] }
count: { type: integer, minimum: 1 }
first_seen_at: { type: string, format: date-time }
last_seen_at: { type: string, format: date-time }
case_id: { type: [string, 'null'], format: uuid }
InvestigationSignal:
type: object
required: [source_type, source_id, severity, evidence_refs, occurred_at]
properties:
source_type: { type: string, enum: [scenario_run, staleness_signal, baseline_immutability, load_finding, automation_failure] }
source_id: { type: string }
scenario_id: { type: [string, 'null'] }
run_id: { type: [string, 'null'], format: uuid }
logical_step_id: { type: [string, 'null'], format: uuid }
severity: { type: string, enum: [info, warning, critical] }
canonical_fingerprint: { type: [string, 'null'] }
evidence_refs: { type: array, items: { type: string, format: uuid } }
target_snapshot: { type: [object, 'null'], additionalProperties: true }
execution_principal_fingerprint: { type: [string, 'null'] }
occurred_at: { type: string, format: date-time }
InvestigationCase:
type: object
required: [id, queue_item_id, status, source_snapshot, evidence_snapshot, owner_actor_id, agent_thread_id, opened_at]
properties:
id: { type: string, format: uuid }
queue_item_id: { type: string, format: uuid }
status: { type: string, enum: [open, investigating, awaiting_approval, awaiting_external_change, verifying, resolved, accepted, reopened] }
source_snapshot: { type: object, additionalProperties: true }
evidence_snapshot: { type: object, additionalProperties: true }
owner_actor_id: { type: string }
agent_thread_id: { type: string }
opened_at: { type: string, format: date-time }
resolved_at: { type: [string, 'null'], format: date-time }
final_disposition: { type: [string, 'null'], enum: [fixed, accepted_risk, duplicate, wont_fix, false_positive] }
resolution_summary: { type: [string, 'null'] }
triage: { $ref: "#/components/schemas/TriageRecord" }
actions: { type: array, items: { $ref: "#/components/schemas/AgentAction" } }
CaseDispositionRequest:
type: object
required: [decision_version, investigation_status]
properties:
decision_version: { type: integer, minimum: 1 }
investigation_status: { type: string, enum: [new, investigating, resolved] }
classification: { type: string, enum: [product_regression, data_regression, baseline_stale, environment_failure, false_positive, scenario_bug, infrastructure_failure] }
resolution: { type: string, enum: [fixed, accepted_risk, duplicate, wont_fix] }
comment: { type: string }
classification: { type: string, enum: [product_regression, data_regression, baseline_stale, scenario_bug, infrastructure_failure, not_confirmed] }
resolution: { type: string, enum: [fixed, accepted_risk, duplicate, wont_fix, false_positive] }
comment: { type: string, maxLength: 4000 }
verification_evidence_refs: { type: array, items: { type: string, format: uuid }, description: "Required to transition a case to resolved" }
acceptance_rationale: { type: string, maxLength: 4000, description: "Required for accepted-risk/wont-fix terminal acceptance" }
TriageRecord:
type: object
properties:
investigation_status: { type: string }
classification: { type: [string, 'null'] }
resolution: { type: [string, 'null'] }
comment: { type: [string, 'null'] }
decision_version: { type: integer }
actor_id: { type: string }
AgentAction:
type: object
required: [id, intent, risk_class, policy_decision, status]
properties:
id: { type: string, format: uuid }
intent: { type: string }
risk_class: { type: string, enum: [read, diagnostic_run, controlled_test_data_mutation, draft_write, scenario_revision_write, activate_current_revision, baseline_approval, automation_policy_write, prod_mutation] }
policy_decision: { type: string, enum: [delegated, approval_required, denied] }
status: { type: string, enum: [planned, running, completed, failed, awaiting_reconciliation] }
evidence_refs: { type: array, items: { type: string, format: uuid } }
approval_gate_id: { type: [string, 'null'], format: uuid }
ScenarioHealth:
type: object
required: [scenario_id, overall_attention, generated_at, confidence]
properties:
scenario_id: { type: string }
environment_class: { type: string }
context_key: { type: string, description: "044 AnalyticsContextKey" }
product_health: { type: string, enum: [healthy, attention, unknown] }
scenario_test_health: { type: string, enum: [healthy, attention, unknown] }
infrastructure_health: { type: string, enum: [healthy, attention, unknown] }
overall_attention: { type: string, enum: [healthy, attention, unknown] }
success_rate: { type: number }
flaky_ratio: { type: number }
infra_failure_ratio: { type: number }
inconclusive_ratio: { type: number }
generated_at: { type: string, format: date-time }
confidence: { type: string, enum: [insufficient_history, low, high] }
Trends:
type: object
required: [generated_at, success_rate_series, disposition_distribution]
properties:
generated_at: { type: string, format: date-time }
success_rate_series: { type: array, items: { type: object } }
disposition_distribution: { type: object, additionalProperties: { type: integer } }
RecurringFailureGroup:
type: object
required: [id, fingerprint, compatibility_family, count, first_occurred_at, last_occurred_at, episodes]
properties:
id: { type: string }
fingerprint: { type: string, description: "immutable: logical_step_id + error_code + normalized_error_signature + assertion_kind + affected_ref" }
id: { type: string, format: uuid }
fingerprint: { type: string, description: "logical_step_id + error_code + normalized_error_signature + assertion_kind + affected_ref" }
compatibility_family: { type: string }
count: { type: integer }
first_occurred_at: { type: string, format: date-time }
last_occurred_at: { type: string, format: date-time }
investigation_status: { type: string, nullable: true }
episodes: { type: array, items: { type: object, properties: { id: { type: string, format: uuid }, opened_at: { type: string, format: date-time }, resolved_at: { type: [string, 'null'], format: date-time } } } }

View File

@@ -1,16 +1,19 @@
#region ScenarioAnalytics.Ux.Decisions [C:3] [TYPE ADR] [SEMANTICS scenario,analytics,ux,decisions]
@BRIEF Final UX decisions for Failure Triage & Quality Analytics (047).
@BRIEF Final UX decisions for Investigation Queue & Scenario Analytics (047).
@RELATION DEPENDS_ON -> [ScenarioAnalytics.Spec]
## Decision 1 — Triage orthogonality
Triage panel sets status + classification over a failed run/step; never alters graph, result truth, or baselines.
## Decision 1 — Analyst-opened case, orthogonal disposition
Qualifying evidence enters Investigation Queue; it never auto-starts agent work. The analyst opens a persistent case with chat/evidence/actions, and its compact triage projection never alters graph, result truth or baselines.
## Decision 2 — Health feeds registry
Scenario health (success rate, flaky ratio, infra ratio, most unstable step) derived and feeds the 042 registry health badge.
## Decision 3 — Recurring no re-alert
Recurring failures grouped; Known-issue/Accepted groups flagged but not re-alerted as new.
Recurring failures are grouped; only active episodes are deduplicated, while recurrence after resolution is newly alertable.
## Decision 4 — RBAC view vs triage
scenario-result:view grants read; scenario-result:triage grants status changes.
## Decision 4 — Object ACL and disposition
scenario-result:view grants only object-authorized read; scenario-result:triage permits case disposition. Agent actions separately obey delegated policy and tool ACL.
## Decision 5 — No modal workflow
Queue, case, conflict recovery and approvals are persistent work surfaces or inline cards; no modal/dialog is required to complete investigation.
#endregion ScenarioAnalytics.Ux.Decisions

View File

@@ -1,36 +1,50 @@
#region ScenarioAnalytics.DataModel [C:4] [TYPE ADR] [SEMANTICS data-model,scenario,triage,flakiness,health]
@BRIEF Triage record, flakiness signal, scenario health, and recurring-failure group models for 047.
@RELATION DEPENDS_ON -> [ScenarioAnalytics.Research]
@RATIONALE Typed triage and derived health make red runs actionable and distinguish product/data regressions from flaky or infra failures; triage never mutates graph/result truth.
@REJECTED Ending at pass/fail without triage; triage altering graph/result.
#region ScenarioAnalytics.DataModel [C:5] [TYPE ADR] [SEMANTICS data-model,scenario,investigation,queue,agent,analytics,health]
@BRIEF Investigation Queue/Case, compact triage projection, and deterministic analytics models for 047.
@RELATION DEPENDS_ON -> [AgentInvestigation.Cases]
@RATIONALE Failed runs need an evidence-led agentic workstream, while historical execution truth and calculated analytics must remain independently reproducible.
@REJECTED Treating triage as a standalone form — rejected because analysts investigate through an agent thread with evidence and tools, not isolated classification fields.
@REJECTED Letting agent judgment rewrite RunResult, health calculations, baseline truth or recurring identity — rejected because those are deterministic historical facts.
## TriageRecord — orthogonal, split from RunResult (#27)
## InvestigationQueueItem — attention, not automatic chat
`RunResult` is **immutable truth** (a historical run stays FAILED). Triage is orthogonal operational metadata:
- investigation_status: new | investigating | resolved
- classification: product_regression | data_regression | baseline_stale | environment_failure | false_positive | scenario_bug | infrastructure_failure
- resolution: fixed | accepted_risk | duplicate | wont_fix
Queue entries are generated only by consuming the canonical 036 `InvestigationSignal` from failed/inconclusive/blocked ScenarioRuns, staleness signals, baseline immutability violations, load circuit-breaker/consistency findings and repeated automation failures. Fields: `id`, `source_type`, `source_id`, `scenario_id?`, `run_id?`, `logical_step_id?`, `severity`, `fingerprint?`, `active_episode_id?`, `evidence_summary`, `target_snapshot`, `execution_principal_fingerprint?`, `suggested_next_action`, `state`, `count`, `first_seen_at`, `last_seen_at`, `case_id?`. The signal idempotency identity is preserved, so a producer cannot manufacture duplicate Queue work by retry.
Fields: id, run_id, logical_step_id (nullable for run-level), investigation_status, classification, resolution, comment, actor_id, created_at, updated_at. Editable; each change audited. Never alters the run result, scenario graph, or baselines.
State: `new | acknowledged | case_opened | suppressed | resolved`. A matching occurrence updates one item only inside an active FailureEpisode; a matching occurrence after its resolution creates a new item. Queue creation does not start AgentRun, tool calls or a chat.
## FlakinessSignal — strict rules (#25)
## InvestigationCase and AgentAction — agentic workstream
Window: last N eligible runs (default 30). Eligibility per step: same environment class, same logical_step_id, same major scenario revision, same baseline family. Infra failures excluded. A step is **flaky** iff: pass AND fail both observed in the window AND failure ratio within (X, Y) (default 5%50%) AND infra failures excluded. A monotonically increasing failure run (e.g., 3×PASS then 4×FAIL) is NOT flaky — it is a product/data regression.
An analyst explicitly opens a queue item into `InvestigationCase { id, queue_item_id, status, source_snapshot, evidence_snapshot, owner_actor_id, agent_thread_id, opened_at, resolved_at?, final_disposition?, resolution_summary? }`.
Fields: scenario_id, logical_step_id, window, eligible_runs, total_runs, failures, flaky_runs, ratio, is_flaky, excludes_infra (bool).
Status: `open | investigating | awaiting_approval | awaiting_external_change | verifying | resolved | accepted | reopened`.
## ScenarioHealth
The case owns chat, hypotheses, tool timeline and linked AgentRuns. Every tool call is the shared 036 `AgentAction` record: canonical inputs, risk/policy decision, target/key scope, side-effect identity, pre/postcondition evidence, cleanup/reconciliation, approvals and actor/agent/tool provenance. The agent may autonomously read, diagnose, run policy-permitted diagnostics, mutate authorized fixture data, and save validated scenario revisions. It never bypasses ACL, deterministic validation, ActionRegistry mutation contracts, capacity, immutable revision creation, or a required ActionApprovalGate. Failed cleanup prevents case resolution.
Fields: scenario_id, window, success_rate, flaky_ratio, infra_failure_ratio, most_unstable_step (logical_step_id + failure count), health (pass/warn/fail). Derived; feeds 042 registry badge. Analytics minimum history window guaranteed independent of 046 retention (#23).
**Closure policy**: `resolved` requires non-empty verification evidence proving the stated acceptance condition and no unresolved cleanup/reconciliation action. `accepted` requires an analyst-confirmed accepted-risk/won't-fix/duplicate rationale; it is not a claim that the product passed. `awaiting_external_change` is used while an external remediation or cleanup is pending. A new matching active signal after a terminal disposition reopens the case (or opens a new case if the episode is new); it never silently remains resolved.
## RecurringFailureGroup — immutable fingerprint (#26)
## TriageRecord — compact audited case projection
Grouped by an **immutable** fingerprint built only from raw evidence that never changes after the failure: `logical_step_id + error_code + normalized_error_signature + assertion_kind + affected_ref`. Triage classification/investigation are **separate attributes** of the group and never part of the fingerprint (so a later classification change does not change group identity).
`RunResult` is immutable historical truth. `TriageRecord` projects the current case disposition onto `run_id + logical_step_id?` for Registry, Run Monitor and analytics: `investigation_status (new|investigating|resolved)`, `classification (product_regression|data_regression|baseline_stale|scenario_bug|infrastructure_failure|not_confirmed)`, `resolution (fixed|accepted_risk|duplicate|wont_fix|false_positive)`, `comment`, `case_id`, `decision_version`, `actor_id`, timestamps.
Fields: id, scenario_id, fingerprint, count, first_occurred_at, last_occurred_at, investigation_status (nullable), classification (nullable, separate).
The projection is versioned/CAS and append-only audited. It is never a free-standing modal form, never changes a RunResult, scenario graph or baseline, and is derived/updated only by an authenticated case decision.
## FlakinessSignal — deterministic rules
Window: last N eligible runs (default 30). Eligibility per step: same environment class, same `logical_step_id`, same `compatibility_family`, same baseline family, and comparable target/principal context. Infrastructure outcomes, cancelled and inconclusive runs are excluded from the pass/fail denominator. A step is flaky only when pass and fail are both observed, a pass occurs after the first failure, at least two pass/fail state transitions occur, and failure ratio is within `(X,Y)` (default 5%50%). A one-way PASS→FAIL change is a regression signal, never flaky.
Fields: scenario_id, logical_step_id, compatibility_family, context_key, window, eligible_runs, total_runs, failures, flaky_runs, ratio, is_flaky, excluded_outcomes.
## ScenarioHealth — deterministic, contextual
`AnalyticsContextKey` is exactly the server-derived 044 SHA-256 over `environment_class + compatibility_family + baseline_family + dashboard_release_id + dashboard_fingerprint + dataset_lineage_fingerprint + execution_principal_fingerprint`. Fields: scenario_id, environment_class, context_key (=AnalyticsContextKey), window, product_health, scenario_test_health, infrastructure_health, overall_attention, success_rate, flaky_ratio, infra_failure_ratio, inconclusive_ratio, most_unstable_step, generated_at, confidence.
Product health consumes product/data regressions; scenario-test health consumes scenario bugs, stale baselines and flaky signals; infrastructure health consumes typed infrastructure outcomes. Untriaged failures raise `overall_attention=attention` with cause `unknown`, but do not fabricate a classification. 042 displays only `overall_attention`, or `unknown` if analytics is unavailable.
## RecurringFailureGroup and FailureEpisode — immutable identity
Group identity is `(scenario_id, compatibility_family, logical_step_id, error_code, normalized_error_signature, assertion_kind, affected_ref)`. Triage data is excluded. `RecurringFailureGroup` has counts and first/last occurrence; `FailureEpisode { id, group_id, opened_at, resolved_at?, resolution? }` is active iff `resolved_at is null`. A matching occurrence in an active episode increments it and suppresses a duplicate queue item; a matching occurrence after resolution opens a new alertable episode and a new queue item.
## Boundary
Triage and analytics are orthogonal operational metadata: they never alter the scenario graph, the run result truth, or 037 baselines.
047 owns queue, case, triage projection and analytics. It consumes 044 execution, 042 staleness, 037 baseline and 040 load evidence; it may request actions through 036 but never replaces their deterministic execution or ownership.
#endregion ScenarioAnalytics.DataModel

View File

@@ -1,20 +1,20 @@
# Implementation Plan: Failure Triage & Quality Analytics
# Implementation Plan: Investigation Queue & Scenario Analytics
**Branch**: `047-dashboard-scenario-analytics` | **Date**: 2026-08-07 | **Spec**: spec.md | **Status**: Draft
## Summary
Failure triage and quality analytics for scenario runs: typed triage status + classification (auditable, orthogonal to result truth), flakiness detection, scenario health feeding 042, trends, and recurring-failure grouping — distinguishing product/data regressions from flaky or infra failures.
Investigation Queue and analyst-opened agentic cases over scenario evidence, backed by compact audited triage projection, deterministic flakiness detection, health feeding 042, trends, and recurring-failure grouping.
## Technical Context
**Language/Version**: Python 3.13+ (backend), TypeScript + Svelte 5 runes (frontend)
**Primary Dependencies**: FastAPI, SQLAlchemy; 044 run/step results, 042 registry health
**Storage**: new table `triage_records`; derived analytics (flakiness, health, trends) computed, not stored as truth
**Testing**: pytest (triage, flakiness, health, trends), vitest (L1/L2 for health/triage UI)
**Frontend Architecture**: model-first `.svelte.ts` for health/triage views
**Storage**: queue/case/action/triage projection tables; derived analytics (flakiness, health, trends) computed, not stored as truth
**Testing**: pytest (queue/case, flakiness, health, trends), vitest (L1/L2 for queue/case UI)
**Frontend Architecture**: model-first `.svelte.ts` for persistent queue/case/health views
**Performance Goals**: health/flakiness derivation < 500ms for fixture histories; trends bounded
**Constraints**: triage never alters graph/result/baseline; RBAC view vs triage; derived health feeds 042
**Constraints**: case/triage never alters graph/result/baseline; analyst explicitly opens agent work; RBAC/object ACL; derived health feeds 042
**Scale**: hundreds of runs per scenario; windows up to 90d
## Constitution Check
@@ -36,17 +36,17 @@ specs/047-dashboard-scenario-analytics/
├── contracts/modules.md, contracts/openapi.yaml, contracts/ux/
└── prototype/index.html + manifest.md
backend/src/models/scenario_triage.py
backend/src/services/dashboard_testing/analytics/ (triage.py, flakiness.py, health.py, trends.py, recurring.py)
backend/src/models/scenario_investigation.py
backend/src/services/dashboard_testing/analytics/ (investigation.py, flakiness.py, health.py, trends.py, recurring.py)
backend/src/api/routes/dashboard_testing/scenario_analytics.py
frontend/src/lib/models/ScenarioHealthModel.svelte.ts
frontend/src/lib/components/scenario-analytics/ (TriagePanel, FlakinessBadge, ScenarioHealthCard, TrendsChart, RecurringFailuresList)
frontend/src/lib/models/InvestigationQueueModel.svelte.ts, InvestigationCaseModel.svelte.ts
frontend/src/lib/components/scenario-analytics/ (QueueList, CaseWorkspace, AgentActionTimeline, ScenarioHealthCard, TrendsChart, RecurringFailuresList)
```
## Delivery Phases
1. TriageRecord model + migration + fixtures.
2. Triage + classification + RBAC.
1. Queue/Case/AgentAction/TriageRecord models + migration + fixtures.
2. Queue projection, explicit case open, disposition CAS + RBAC/object ACL.
3. Flakiness detection.
4. Health derivation 042 badge.
5. Trends + recurring failures.
@@ -60,9 +60,9 @@ traceability.md maps Story → model → operationId → contract → task → t
## Cross-Spec Boundary
- Consumes 044 run/step results; feeds 042 health badge.
- Triage/analytics never write to 037 baselines.
- UI consumed within 045 result view and 042 registry.
- Case/analytics never rewrite 037 baselines or historical run truth.
- Queue is surfaced in 042 Registry and 045 Results; the persistent case workspace is 047.
## Complexity Tracking
No exception planned. Triage/flakiness/health are bounded C3-C4; derivation service decomposed.
No exception planned. Queue/case orchestration is C4/C5; deterministic derivations stay decomposed.

View File

@@ -1,92 +1,7 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>047 — Failure Triage & Quality Analytics Prototype</title>
<style>
:root{--bg:#0f1115;--panel:#171a21;--panel2:#1d2129;--border:#2a2f3a;--text:#e6e8ee;--muted:#9aa3b2;--accent:#4f8cff;--green:#2ecc71;--warn:#f39c12;--red:#e74c3c}
*{box-sizing:border-box}
body{margin:0;font-family:system-ui,Segoe UI,Roboto,sans-serif;background:var(--bg);color:var(--text);line-height:1.5}
.wrap{max-width:960px;margin:0 auto;padding:20px}
h1{font-size:19px;margin:0 0 4px}.sub{color:var(--muted);font-size:13px;margin-bottom:14px}
.card{background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:16px;margin-bottom:12px}
.card h2{font-size:14px;margin:0 0 12px}
.badge{display:inline-block;padding:2px 9px;border-radius:20px;font-size:11px;font-weight:600}
.b-pass{background:rgba(46,204,113,.15);color:var(--green)}
.b-warn{background:rgba(243,156,18,.15);color:var(--warn)}
.b-fail{background:rgba(231,76,60,.15);color:var(--red)}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-bottom:12px}
.kv{background:var(--panel2);border:1px solid var(--border);border-radius:8px;padding:10px}
.kv .k{font-size:11px;color:var(--muted);text-transform:uppercase}.kv .v{font-size:18px;font-weight:700}
.field{display:flex;flex-direction:column;gap:4px;margin-bottom:10px}
.field label{font-size:11px;color:var(--muted);text-transform:uppercase}
.field select,.field textarea{background:var(--panel2);border:1px solid var(--border);color:var(--text);border-radius:6px;padding:8px 10px;font-size:13px}
.row{display:flex;justify-content:space-between;gap:10px;padding:6px 8px;border-bottom:1px solid var(--border);font-size:13px}
.bar{height:10px;background:var(--panel2);border-radius:5px;overflow:hidden;margin:4px 0}
.bar>div{height:100%;border-radius:5px}
.gr{background:var(--green)}.yw{background:var(--warn)}.rd{background:var(--red)}
button{background:var(--panel2);border:1px solid var(--border);color:var(--text);border-radius:8px;padding:8px 14px;font-size:13px;cursor:pointer}
button.primary{background:var(--accent);border-color:var(--accent);color:#fff}
.state-switcher{position:fixed;right:14px;top:14px;background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:10px;font-size:12px;width:240px;z-index:50}
.state-switcher h4{margin:0 0 8px;color:var(--muted);font-size:11px;text-transform:uppercase}
.state-switcher button{display:block;width:100%;margin:3px 0;text-align:left}
@media(max-width:900px){.state-switcher{position:static;width:auto;margin-bottom:12px}}
</style>
</head>
<body>
<div class="wrap">
<h1>Triage — Run SR-1842 <span class="badge b-fail">FAILED</span></h1>
<div class="sub">047 — Failure Triage &amp; Quality Analytics · <span id="stateLabel">State: idle</span></div>
<div class="card">
<h2>Triage failure</h2>
<div class="field"><label>Status</label>
<select><option>New</option><option selected>Investigating</option><option>Resolved</option></select>
</div>
<div class="field"><label>Classification</label>
<select><option>Product regression</option><option>Data regression</option><option>Baseline stale</option><option>Environment failure</option><option>False positive</option><option selected>Scenario bug</option><option>Infrastructure failure</option></select>
</div>
<div class="field"><label>Comment</label><textarea rows="2" placeholder="Intermittent; see 'Wait for dashboard loaded' step">Intermittent failure, unrelated to data.</textarea></div>
<button class="primary" onclick="setState('saved')">Save triage</button>
</div>
<div class="card">
<h2>Scenario health <span class="badge b-warn">WARN</span></h2>
<div class="grid">
<div class="kv"><div class="k">30d success rate</div><div class="v">94.2%</div></div>
<div class="kv"><div class="k">Flaky runs</div><div class="v">4.1%</div></div>
<div class="kv"><div class="k">Infra failures</div><div class="v">1.7%</div></div>
</div>
<div class="row"><span>Most unstable step: "Wait for dashboard loaded"</span><span class="warn" style="color:var(--warn)">7 failures / 183 runs</span></div>
<div class="bar"><div class="gr" style="width:94.2%"></div></div>
<div class="sub" style="margin:4px 0 0">Feeds 042 registry health badge.</div>
</div>
<div class="card">
<h2>Recurring failures</h2>
<div class="row"><span><strong>Wait for dashboard loaded</strong> · scenario_bug</span><span>7 · Resolved · not re-alerted</span></div>
<div class="row"><span><strong>Revenue metric compare</strong> · product_regression</span><span>3 · Investigating</span></div>
</div>
<div class="state-switcher">
<h4>States</h4>
<button onclick="setState('idle')">idle (triage)</button>
<button onclick="setState('saved')">saved</button>
<button onclick="setState('conflict')">conflict (409)</button>
<button onclick="setState('empty')">empty (no history)</button>
</div>
</div>
<script>
function setState(s){
var lbl=document.getElementById('stateLabel');
if(s==='idle'){lbl.textContent='State: idle (triage)';}
if(s==='saved'){lbl.textContent='State: saved (audited)';}
if(s==='conflict'){lbl.textContent='State: conflict (409) — reload triage';}
if(s==='empty'){lbl.textContent='State: empty (no run history) — health unknown';}
}
setState('idle');
</script>
</body>
</html>
<!doctype html>
<html lang="ru"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Расследования</title><link rel="stylesheet" href="../../prototype-ui.css"></head>
<body><div class="shell"><header class="topbar"><div class="brand">Superset Tools · BI testing</div><nav class="nav" aria-label="Разделы"><a href="#registry">Сценарии</a><a href="#runs">Запуски</a><a href="#automation">Автоматизация</a><a class="active" href="#quality">Расследования</a></nav><span class="badge warn">2 требуют внимания</span></header>
<main>
<section id="queue"><div class="page-head"><div><div class="eyebrow">Investigation Queue</div><h1>Очередь расследований</h1><p class="sub">Сигналы не запускают агента сами. Аналитик открывает только нужный case.</p></div></div><section class="card"><table class="table"><thead><tr><th>Серьёзность</th><th>Источник и evidence</th><th>Повторяемость</th><th>Следующее действие</th><th></th></tr></thead><tbody><tr><td><span class="badge danger">Critical</span></td><td><strong>XLSX reconciliation · SR-1839</strong><br><span class="sub">ROW_SET_CHANGED · target rc-17 · RLS analyst</span></td><td>Новая episode #2<br><span class="sub">18 строк</span></td><td>Проверить ETL-1421</td><td><button class="btn primary" onclick="setProtoState('case')">Разобрать с агентом</button></td></tr><tr><td><span class="badge warn">Warning</span></td><td><strong>Фильтры и метрики</strong><br><span class="sub">browser timeout · PREPROD</span></td><td>3 occurrences</td><td>Сравнить с прошлым run</td><td><button class="btn" onclick="setProtoState('case')">Открыть case</button></td></tr></tbody></table></section><section class="cols-3 grid" style="margin-top:16px"><div class="card"><div class="sub">Здоровье продукта</div><div class="kpi">Attention</div><span class="badge danger">2 regression runs</span></div><div class="card"><div class="sub">Здоровье теста</div><div class="kpi">Stable</div><span class="badge ok">flaky 0%</span></div><div class="card"><div class="sub">Инфраструктура</div><div class="kpi">Healthy</div><span class="badge ok">0 infra failures</span></div></section></section>
<section id="case" class="hidden"><div class="page-head"><div><button class="btn" onclick="setProtoState('queue')">К очереди</button><div class="eyebrow" style="margin-top:14px">Case IC-204 · opened from SR-1839</div><h1>Расхождение строк после ETL</h1><p class="sub">Run truth остаётся FAILED. Ниже — расследование и управляемые действия.</p></div><span class="badge info">Investigating</span></div><div class="main-aside"><section class="card"><h2>Agent chat</h2><div class="evidence"><strong>Agent</strong><br>18 дополнительных строк появились после ETL-1421. Сначала сопоставлю lineage, предыдущий XLSX и release rc-17.</div><div class="evidence" style="margin-top:10px"><strong>Tool timeline</strong><br>✓ Сравнил SR-1839 и SR-1812<br>✓ Получил lineage snapshot<br>• Проверяю ETL event и affected datasets</div><div class="actions"><button class="btn" onclick="showAction('read')">Показать evidence</button><button class="btn primary" onclick="showAction('run')">Запустить diagnostic run</button></div><div id="action-result" class="notice info hidden" style="margin-top:12px"></div></section><aside class="grid"><section class="card"><h2>Immutable evidence</h2><div class="evidence">XLSX diff · 124 → 142 rows<br>logical step: compare-row-set<br>target: rc-17 · principal: analyst/RLS-3</div><button class="btn" style="margin-top:10px">Открыть comparison</button></section><section class="card"><h2>Policy</h2><p class="sub">Read и diagnostic run агент выполняет сам. Изменение baseline или automation появится здесь как inline approval card.</p></section><section class="card"><h2>Закрытие case</h2><select style="width:100%;margin-bottom:10px"><option>Data regression</option><option>Scenario bug</option><option>Baseline stale</option></select><button class="btn" onclick="showAction('resolve')">Сохранить disposition</button></section></aside></div></section>
</main><div class="statebar"><span class="sub">State: <b id="proto-state"></b></span><button class="btn" onclick="setProtoState('queue')">Queue</button><button class="btn" onclick="setProtoState('case')">Case</button></div></div><script src="../../prototype-ui.js"></script><script>function showAction(v){let e=document.getElementById('action-result');e.classList.remove('hidden');e.innerHTML=v==='run'?'<strong>Diagnostic run SR-1845 started.</strong><br>Действие delegated: read-only scenario run, quota проверена.':v==='resolve'?'<strong>Disposition сохранён.</strong><br>Case audit и compact triage projection обновлены; historical run не изменён.':'<strong>Evidence готов.</strong><br>Сравнение, lineage и target snapshot закреплены за case.'}protoState('queue',s=>['queue','case'].forEach(id=>document.getElementById(id).classList.toggle('hidden',id!==s)))</script></body></html>

View File

@@ -4,7 +4,7 @@
## Prototype Metadata
- **Feature**: 047 Failure Triage & Quality Analytics
- **Source contracts**: ux_reference.md, contracts/modules.md
- **Screens represented**: 1 (Triage + Health + Recurring)
- **Screens represented**: 2 (Investigation Queue and persistent agent-led Case; health summary embedded in Queue)
- **Total states**: 4 (idle, saved, conflict, empty)
- **Accessibility**: keyboard nav, focus-visible, aria-live, ≥44px, prefers-reduced-motion
- **Responsive**: 375px, 900px
@@ -22,7 +22,7 @@
| Story | Prototype Feature | Acceptance Verified |
|-------|-------------------|---------------------|
| US1 Triage | status + classification + comment + save | persisted + audited |
| US1 Investigation | queue item → explicit case, evidence, agent tools, disposition | case/triage projection persisted + audited |
| US2 Flakiness/Health | health card + unstable step | flaky detection + health |
| US3 Recurring | recurring list + known-issue | grouping + no re-alert |
| US3 Recurring | recurring list + episode state | grouping + alert after resolution |
#endregion ScenarioAnalytics.PrototypeManifest

View File

@@ -1,7 +1,7 @@
# Quickstart: Failure Triage & Quality Analytics (047)
# Quickstart: Investigation Queue & Scenario Analytics (047)
## Prereqs
- 044 run/step results, 042 registry, DB migrated (triage_records)
- 044 run/step results, 042 registry, 036 AgentAction contract, DB migrated (investigation tables)
## Commands
@@ -17,9 +17,9 @@ npm run lint
```
## Exit Gates
- [ ] Failed runs triage with status + classification, persisted + auditable
- [ ] Qualifying events queue without auto-starting agent work; analyst-opened cases retain evidence/actions/disposition audit
- [ ] Flaky steps detected; health derived and feeds 042 badge
- [ ] Trends + recurring failures render; known-issue not re-alerted
- [ ] RBAC view vs triage enforced
- [ ] Triage never alters graph/result/baseline
- [ ] Trends + recurring failures render; recurrence after resolution opens a new alertable episode
- [ ] Object ACL + RBAC view vs disposition enforced
- [ ] Case/triage never alters graph/result/baseline
- [ ] ruff clean; prototype states covered

View File

@@ -1,16 +1,16 @@
# Failure Triage & Quality Analytics — Phase 0/1 Research (047)
# Investigation Queue & Scenario Analytics — Phase 0/1 Research (047)
**Branch**: `047-dashboard-scenario-analytics` | **Date**: 2026-08-07 | **Spec**: spec.md
## R1. Triage as orthogonal metadata
## R1. Case disposition as orthogonal projection
**Decision**: TriageRecord is orthogonal operational metadata over 044 run/step results. It never alters the graph, result truth, or baselines.
**Decision**: An analyst-opened InvestigationCase is the primary workstream; TriageRecord is its compact orthogonal projection over 044 run/step results. Neither alters graph, result truth or baselines.
**Rationale**: Triage is analyst judgment; changing result truth on triage would corrupt reproducibility.
**Alternatives**: triage mutates result (rejected); no triage (rejected: graveyard).
**Impact**: TriageRecord table; RBAC view vs triage.
**Impact**: InvestigationQueueItem, InvestigationCase, AgentAction and TriageRecord projection; object ACL plus RBAC view vs disposition.
## R2. Flakiness + health derived, not stored-truth
@@ -22,14 +22,14 @@
## R3. Recurring-failure grouping
**Decision**: Group identical failures by fingerprint (step+error_code+classification) with counts; known-issue/accepted groups flagged but not re-alerted as new.
**Decision**: Group identical failures by immutable fingerprint (`logical_step_id + error_code + normalized_error_signature + assertion_kind + affected_ref`) with counts. Classification is triage metadata, not fingerprint input. A new matching occurrence after an episode is resolved opens a new episode and alerts.
**Impact**: recurring group + alert-suppression.
**Impact**: recurring group + FailureEpisode + active-episode deduplication.
## Contracts & API
- `contracts/modules.md``Analytics.Triage`, `Analytics.Classify`, `Analytics.Flakiness`, `Analytics.Health`, `Analytics.Trends`, `Analytics.Recurring`.
- OpenAPI: `POST /runs/{id}/triage`, `GET /scenarios/{id}/health`, `GET /scenarios/{id}/trends`, `GET /scenarios/{id}/recurring`.
- OpenAPI: queue list/open-case/disposition, `GET /scenarios/{id}/health`, `GET /scenarios/{id}/trends`, `GET /scenarios/{id}/recurring-failures`.
## Constitution Check

View File

@@ -1,32 +1,33 @@
#region ScenarioAnalytics.Spec [C:3] [TYPE ADR] [SEMANTICS spec,requirements,scenario,triage,flakiness,analytics,health]
@BRIEF Failure triage and quality analytics for scenario runs: failure statuses, classification, flakiness detection, scenario health, and trends — so red runs become actionable instead of a graveyard.
@BRIEF Investigation Queue and agent-led case workspaces over deterministic scenario analytics, so failures become evidence-led remediation work rather than isolated forms.
@RELATION DEPENDS_ON -> [Doc.Adr.ADR0001]
@RELATION DEPENDS_ON -> [Doc.Adr.ADR0005]
@RELATION DEPENDS_ON -> [ScenarioExecution.Spec]
@RELATION DEPENDS_ON -> [ScenarioRegistry.Spec]
@RELATION DEPENDS_ON -> [ScenarioRunMonitor.Spec]
@RATIONALE 036 was born from test stabilization, but flakiness/health is not extended to ScenarioRun. Without triage and analytics, operators cannot tell "dashboard is broken" from "our test is broken", and failures accumulate with no investigation state.
@REJECTED Ending at pass/fail/inconclusive — rejected because operational workflow (triage, classification, investigation) starts where specs currently stop.
@RATIONALE 036 supplies durable agent work and 044 supplies immutable run evidence, but neither provides an analyst-controlled queue and long-lived investigation case. Health and failure identity remain deterministic inputs to that work.
@REJECTED Ending at pass/fail/inconclusive — rejected because operational workflow begins there.
@REJECTED Opening an agent chat for every failure — rejected because queue deduplication and analyst intent are needed to avoid noise.
## Navigation (DSA Indexer keywords)
@SEMANTICS: spec, requirements, feature, scenario, triage, flakiness, analytics, health, trend
**Feature Branch**: `047-dashboard-scenario-analytics`
**Created**: 2026-08-07 | **Status**: Draft
**Input**: "Provide failure triage and quality analytics for scenario runs: typed failure statuses and classification, flakiness detection with scenario health, trends, recurring failures, and investigation state — distinguishing product/data regressions from flaky or infrastructure failures."
**Input**: "Provide an Investigation Queue and agentic case workspace for failed/stale/load/automation evidence, backed by deterministic flakiness, health, trends and recurring-failure analytics."
## User Scenarios
### Story 1 — Triage a Failed Run (P1)
### Story 1 — Open an Agent-Led Investigation (P1)
**Why P1**: A failed test must be actionable, not just red.
**Why P1**: A failed test must enter a deliberate evidence-led investigation, not a classification form.
**Independent Test**: Open a failed run and verify the analyst can set a triage status and classification, persisted and auditable.
**Independent Test**: Feed a failed run into the queue, open it explicitly, and verify the case creates an agent thread with immutable evidence and an audited disposition projection.
**Acceptance**:
1. **Given** a failed run **When** triage is opened **Then** the analyst can set investigation_status (New, Investigating, Resolved), classification (product/data/baseline/env/false_positive/scenario-bug/infra), and resolution (Fixed, Accepted risk, Duplicate, Won't fix).
2. **Given** a triage is saved **When** persisted **Then** it is auditable and updates the run/step record.
3. **Given** a run is already triaged **When** reopened **Then** the existing triage is shown.
1. **Given** a failed/inconclusive/blocked run **When** its deterministic evidence is produced **Then** an Investigation Queue item is created or updated; no agent chat or tool run starts automatically.
2. **Given** an analyst selects “Investigate with agent” **When** the item opens **Then** a durable case has chat, source/evidence snapshots, tool timeline and linked AgentRuns.
3. **Given** the case reaches a disposition **When** it is persisted **Then** a versioned, audited TriageRecord projection updates the run/step view without changing historical run truth.
---
@@ -34,12 +35,12 @@
**Why P1**: Distinguish a broken dashboard from a broken test.
**Independent Test**: Feed a run history with repeated step failures and verify flaky classification and scenario health are computed.
**Independent Test**: Feed a run history with repeated step failures and verify flakiness and contextual health are calculated deterministically before any agent decision.
**Acceptance**:
1. **Given** a step fails intermittently **When** analytics compute **Then** it is classified flaky with a flakiness ratio.
2. **Given** run history **When** health is derived **Then** 30d success rate, flaky ratio, infra-failure ratio, and most-unstable-step are shown.
3. **Given** a scenario health changes **When** thresholds are crossed **Then** the registry health badge updates (042).
3. **Given** a scenario health changes **When** thresholds are crossed **Then** the registry health badge updates (042), while an untriaged failure remains attention with unknown cause.
---
@@ -52,7 +53,7 @@
**Acceptance**:
1. **Given** run history **When** trends are rendered **Then** success-rate trend and failure-classification distribution are shown.
2. **Given** the same step fails repeatedly **When** aggregated **Then** recurring failures are grouped with a count and first/last occurrence.
3. **Given** a failure is resolved (Fixed/Accepted risk) **When** recurring **Then** the recurrence is flagged but not re-alerted as new.
3. **Given** a failure recurs after its FailureEpisode was resolved **When** aggregated **Then** a new alertable episode opens; only duplicates inside an active episode are suppressed.
---
@@ -61,43 +62,52 @@
| # | Scenario | Expected Behavior | Recovery |
|---|----------|-------------------|----------|
| E1 | No run history | Health unknown, no flaky signal | — |
| E2 | Triage concurrent edit | 409 conflict | Reload |
| E3 | RBAC triage denied | 403 permission_denied | Contact admin |
| E2 | Case disposition concurrent edit | 409 conflict panel | Reload |
| E3 | Case/agent access denied | 403 permission_denied | Contact admin |
| E4 | Infra failure spike | Classified infra, not product regression | Investigate infra |
## Requirements
### Functional
- **SCAN-FR-001**: Failed runs MUST support triage split into `investigation_status` (new/investigating/resolved), `classification`, and `resolution` (fixed/accepted_risk/duplicate/wont_fix), persisted and auditable; the historical RunResult stays immutable truth.
- **SCAN-FR-002**: The system MUST detect flakiness per step with strict rules (eligible window = same environment class, logical step, major revision, baseline family; flaky iff pass AND fail observed AND failure ratio in (X,Y) AND infra failures excluded) and compute scenario health (30d success rate, flaky ratio, infra-failure ratio, most unstable step).
- **SCAN-FR-003**: Health MUST feed the 042 registry health badge.
- **SCAN-FR-004**: The system MUST render trends (success-rate over time, failure-classification distribution) and group recurring failures by an **immutable fingerprint** (`logical_step_id + error_code + normalized_error_signature + assertion_kind + affected_ref`), never including triage classification.
- **SCAN-FR-005**: Known-issue/Accepted (resolved/accepted_risk) recurring failures MUST be flagged but not re-alerted as new.
- **SCAN-FR-006**: RBAC MUST distinguish scenario-result:view from scenario-result:triage.
- **SCAN-FR-007**: Health/trends/recurring MUST aggregate scenario history (`/scenarios/{scenario_id}/health|trends|recurring-failures`), not a single run.
- **SCAN-FR-001**: Failed/inconclusive/blocked runs, staleness, baseline immutability, load and repeated automation findings MUST emit the canonical 036 `InvestigationSignal`; 047 MUST create/update a deduplicated Investigation Queue item from that idempotent envelope. The queue MUST NOT auto-start an agent chat, AgentRun or tool action.
- **SCAN-FR-002**: An analyst MUST be able to open a queue item into a durable InvestigationCase with chat, immutable evidence snapshot, hypotheses, AgentAction timeline, linked AgentRuns, approvals, verification and final disposition. The agent may execute only delegated/policy-authorized actions.
- **SCAN-FR-003**: Triage MUST be a CAS/audited compact projection of a case disposition; the historical RunResult stays immutable truth.
- **SCAN-FR-004**: The system MUST detect flakiness per step using same environment class, logical_step_id, compatibility_family, baseline family and comparable context. It requires a post-failure pass and two result transitions; infra/cancelled/inconclusive outcomes are excluded from the pass/fail denominator.
- **SCAN-FR-005**: Health MUST be contextual and computed separately for product, scenario-test and infrastructure health plus overall attention.
- **SCAN-FR-006**: Health MUST feed the 042 registry health badge.
- **SCAN-FR-007**: The system MUST render trends and group recurring failures by immutable compatibility-scoped fingerprint, never including triage classification.
- **SCAN-FR-008**: A matching failure inside an active FailureEpisode is deduplicated; a matching failure after resolution MUST open a new alertable FailureEpisode and Queue item.
- **SCAN-FR-009**: Object-level result/evidence access and agent-action authority MUST enforce ACL separately from `scenario-result:view` and `scenario-result:triage`.
- **SCAN-FR-010**: Health/trends/recurring MUST aggregate scenario history, not a single run.
- **SCAN-FR-011**: Investigation, approval, conflict recovery and closure MUST use persistent case workspaces and inline cards; modal/dialog interaction MUST NOT be required.
- **SCAN-FR-012**: A case may become `resolved` only with verification evidence and completed reconciliation. `accepted` requires an analyst-recorded accepted-risk/won't-fix/duplicate rationale. A new matching active signal MUST reopen the work (or create a new case for a new FailureEpisode).
- **SCAN-FR-013**: Health and flakiness eligibility MUST use the exact 044 `AnalyticsContextKey`; grouping by environment or revision alone is forbidden.
### Key Entities
- **TriageRecord**: Failure status + classification + comment + actor + timestamp for a run/step.
- **InvestigationQueueItem**: Deduplicated attention item with source/evidence context; opening it is explicit.
- **InvestigationCase**: Agent-led durable workstream with chat, tools, approvals, verification and closure.
- **TriageRecord**: Compact audited projection of case disposition for a run/step.
- **FlakinessSignal**: Per-step intermittent-failure metric (ratio, window).
- **ScenarioHealth**: 30d success rate, flaky ratio, infra ratio, most unstable step.
- **RecurringFailureGroup**: Grouped identical failures with count, first/last occurrence, current triage.
## Success Criteria
- **SC-001**: 100% of failed fixture runs can be triaged with status + classification, persisted and auditable.
- **SC-001**: 100% of qualifying fixture events create/update a deduplicated queue item without starting agent work; an opened case has immutable evidence and an audited disposition projection.
- **SC-002**: Flaky steps are detected and health derived from run history with deterministic output.
- **SC-003**: Health badge in 042 updates when thresholds cross.
- **SC-004**: Trends and recurring failures render; known-issue recurrences are not re-alerted.
- **SC-004**: Trends and recurring failures render; recurrence after a resolved episode creates a new alertable episode.
- **SC-005**: RBAC enforces view vs triage.
## Clarifications
### Session 2026-08-07
- Q: New data? → A: Reuses 044 run/step results; adds TriageRecord + derived analytics; feeds 042 health.
- Q: Does triage change graph/result? → A: No. Triage is orthogonal operational metadata, never alters scenario graph or run result truth.
- Q: New data? → A: Reuses 044/037/040/041 evidence; adds Queue, Case, AgentAction projection and deterministic analytics; feeds 042 health.
- Q: Does a case change graph/result? → A: No. A case may create a separately immutable revision or policy-bound action, but it never rewrites historical run truth.
- Q: Does every failure open chat? → A: No. It enters a deduplicated Investigation Queue; the analyst explicitly opens the case.
## Implementation Status & MVP Debt (audit 2026-08-07)

View File

@@ -1,5 +1,5 @@
#region ScenarioAnalytics.Tasks [C:3] [TYPE ADR] [SEMANTICS tasks,scenario,analytics,implementation]
@BRIEF Ordered TDD backlog for Failure Triage & Quality Analytics (047). Tests FIRST.
@BRIEF Ordered TDD backlog for Investigation Queue/Case and deterministic quality analytics (047). Tests FIRST.
**Prerequisites**: plan.md, spec.md; contracts/modules.md, traceability.md.
@@ -7,21 +7,21 @@
## Phase 1 — Setup
- [ ] T001 Create `TriageRecord` model + migration in `backend/src/models/scenario_triage.py`
- [ ] T001 Create `InvestigationQueueItem`, `InvestigationCase`, `AgentAction` projection, `TriageRecord`, and migration in `backend/src/models/scenario_investigation.py`
- [ ] T002 [P] Create canonical fixtures in `specs/047-dashboard-scenario-analytics/fixtures/`
## Phase 2 — US1 Triage
## Phase 2 — US1 Queue and Agentic Case
- [ ] T003 [US1] Write failing triage tests in `backend/tests/services/dashboard_testing/analytics/test_triage.py`
@TEST_EDGE: concurrent->409; denied->403; audit recorded; RunResult stays FAILED
- [ ] T004 [US1] Implement `set_triage` (investigation_status/classification/resolution split) in `analytics/triage.py`
@INVARIANT: triage orthogonal; RunResult immutable; never alters graph/baseline
- [ ] T005 [US1] Add `POST /scenario-runs/{id}/triage` + RBAC scenario-result:triage tests
- [ ] T003 [US1] Write failing queue/case tests in `backend/tests/services/dashboard_testing/analytics/test_investigation.py`
@TEST_EDGE: event queues but does not start agent; duplicate active episode updates count; explicit open is idempotent; disposition CAS->409
- [ ] T004 [US1] Implement queue projection, explicit `open_case`, AgentAction linkage and compact `set_disposition` in `analytics/investigation.py`
@INVARIANT: case/triage orthogonal; RunResult immutable; never alters graph/baseline
- [ ] T005 [US1] Add queue/case/disposition API + object/RBAC tests
## Phase 3 — US2 Flakiness + Health
- [ ] T006 [US2] Write failing flakiness/health tests (strict rules, logical_step_id) in `analytics/test_flakiness.py`
@TEST_EDGE: 3xPASS+4xFAIL -> product regression (NOT flaky); intermittent pass/fail -> flaky; infra excluded
- [ ] T006 [US2] Write failing contextual flakiness/health tests in `analytics/test_flakiness.py`
@TEST_EDGE: one-way PASSFAIL -> regression (NOT flaky); post-failure PASS + two transitions -> flaky; infra/cancelled/inconclusive excluded
- [ ] T007 [US2] Implement `detect_flakiness` + `derive_health` in `analytics/flakiness.py`, `analytics/health.py`
@POST: deterministic signals; feeds 042 badge on threshold cross
- [ ] T008 [US2] Add `GET /scenarios/{scenario_id}/health`
@@ -31,12 +31,12 @@
- [ ] T009 [US3] Write failing trend/recurring tests in `analytics/test_trends.py`
@TEST_EDGE: classification change does NOT change fingerprint (immutable group identity)
- [ ] T010 [US3] Implement `trends` + `recurring` in `analytics/trends.py`, `analytics/recurring.py`
@POST: immutable fingerprint (logical_step_id+error_code+normalized_signature+assertion_kind+affected_ref); resolved/accepted_risk not re-alerted
@POST: compatibility-scoped immutable fingerprint; a matching occurrence after a resolved episode opens a new alertable episode and queue item
## Phase 5 — Frontend + Polish
- [ ] T011 [P] Build `ScenarioHealthModel.svelte.ts` + components (TriagePanel, FlakinessBadge, ScenarioHealthCard, TrendsChart, RecurringFailuresList) in `frontend/src/lib/components/scenario-analytics/`
- [ ] T012 [P] L1/L2 model + UX tests for health/triage
- [ ] T011 [P] Build `InvestigationQueueModel.svelte.ts`, `InvestigationCaseModel.svelte.ts` and components (QueueList, CaseWorkspace, AgentActionTimeline, ScenarioHealthCard, TrendsChart, RecurringFailuresList) in `frontend/src/lib/components/scenario-analytics/`
- [ ] T012 [P] L1/L2 model + UX tests for queue/case/health; no modal is required for workflow completion
- [ ] T013 Run quickstart, scoped/full backend + frontend tests, ruff; ATTN_1-4; semantic rebuild
- [ ] T014 **Prototype validation**: verify @UX_STATE reachable; responsive

Some files were not shown because too many files have changed in this diff Show More