Core changes: - Add @defgroup/@ingroup to 1791 C2+ contracts (555 files) for HCA 128× pre-training DSA grouping - Add §0.1 Pre-Training Frequency matrix to semantics-core - Add §VIII Attention Architecture rules (ATTN_1-4) with MLA/CSA/HCA/DSA mechanics - Add @defgroup/@ingroup to canonical syntax (§II) and all contract examples Agent prompts (5 files): - Add ZERO-STATE RATIONALE with MLA/CSA/HCA/DSA compression mechanics - Add pre-training note: @RATIONALE/@REJECTED are in-context learned tags - svelte-coder: add missing #region contract, fix Svelte rule violations - python-coder/fullstack-coder: honor function contracts from speckit plan - qa-tester: add attention compliance audit (P3 ATTN_1-4 checks) Skills (6 files): - Translate all axiom_config descriptions to English - Fix doc_dirs to index .opencode/ and .specify/ - Deduplicate 5× complexity_rules → single global_tags catalog - Reduce semantics-svelte 591→485 lines (remove duplicate code blocks) - Fix semantics-testing: 'Short IDs' → 'Short hierarchical IDs' - Fix all examples: flat IDs → hierarchical Domain.Name format - Fix Svelte examples: replace raw Tailwind + <button> with semantic tokens + /ui Speckit workflow (commands + templates): - speckit.plan: add Function-Level Contracts for C3+ with @PRE/@POST/@TEST_EDGE - speckit.plan: add Attention Compliance Gate (ATTN_1-4 before contract generation) - speckit.tasks: add function contract inlining format (constraints in task description) - speckit.specify: load semantics-core for spec density rules - spec-template: add #region contract, @SEMANTICS grouping, hierarchical IDs - ux-reference-template: add #region wrapper - plan-template: add attention gate, @defgroup/@ingroup guidance - tasks-template: add attention audit + rebuild + orphan check tasks - constitution.md: translate to English, add Principle VIII (attention-optimized contracts) Reference modules rewritten (hierarchical IDs + full contracts): - Auth.Jwt: 6 child contracts with @RATIONALE/@REJECTED/@TEST_EDGE - Api.Auth: 5 endpoints with @TEST_EDGE + molecular CoT markers - Migration.Model: @defgroup Migration with 18 @ACTION + 6 @INVARIANT Scripts: - add_defgroup_ingroup.py: zero-risk additive @ingroup migration (1791 insertions) - migrate_hierarchical.py: flat→hierarchical ID dry-run analysis (792 contracts) - merge_prompts.py: merge all prompts/skills/commands into one review file Config: - axiom_config.yaml: 749→395 lines (-47%), English, doc_dirs include prompts - Fix test_datasets.py import collision (rename → test_datasets_routes.py) - Fix test_preview.py: SupersetClient→get_superset_client, AsyncMock, logger f-string
209 lines
9.0 KiB
TypeScript
209 lines
9.0 KiB
TypeScript
// #region ValidationRunDetailModel [C:3] [TYPE Model] [SEMANTICS validation,run,detail,dashboard,collapsible,model]
|
|
// @ingroup Models
|
|
// @BRIEF Screen model for validation run detail — dashboard-by-dashboard results with screenshots, issues, and task logs.
|
|
// @INVARIANT Screenshot blob URLs are cached per (dashboardId, tabIndex) key. Revoked when cache key is overwritten.
|
|
// @ACTION toggleDashboard(id) / toggleLogsSent(key) / toggleTaskLogs(key) — Collapsible section toggles.
|
|
// @ACTION loadScreenshot(path, key) — Loads screenshot blob from storage API.
|
|
// @RELATION DEPENDS_ON -> [ApiModule]
|
|
|
|
import { api } from '$lib/api.js';
|
|
import type { TaskLogEntry } from '$lib/api.js';
|
|
import { SvelteSet } from "svelte/reactivity";
|
|
|
|
type UxState = 'idle' | 'loading' | 'populated' | 'error' | 'not_found';
|
|
|
|
interface RunData {
|
|
id?: string;
|
|
task_id?: string;
|
|
policy_id?: string;
|
|
status?: string;
|
|
started_at?: string;
|
|
finished_at?: string;
|
|
trigger_type?: string;
|
|
dashboards?: DashboardResult[];
|
|
results?: DashboardResult[];
|
|
}
|
|
|
|
interface DashboardResult {
|
|
id?: string;
|
|
dashboard_id?: string;
|
|
title?: string;
|
|
slug?: string;
|
|
status?: string;
|
|
duration_ms?: number;
|
|
path?: string;
|
|
tabs?: TabResult[];
|
|
screenshots?: ScreenshotResult[];
|
|
issues?: IssueResult[];
|
|
datasets?: unknown[];
|
|
charts?: unknown[];
|
|
logs_sent_to_llm?: string[];
|
|
dataset_health?: unknown;
|
|
_taskLogsLoaded?: boolean;
|
|
task_logs?: TaskLogEntry[];
|
|
}
|
|
|
|
interface TabResult {
|
|
label?: string;
|
|
index?: number;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
interface ScreenshotResult {
|
|
path?: string;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
interface IssueResult {
|
|
severity?: string;
|
|
message?: string;
|
|
location?: string;
|
|
tab_index?: number | string;
|
|
}
|
|
|
|
export class ValidationRunDetailModel {
|
|
// ── Atoms ─────────────────────────────────────────────────────
|
|
uxState: UxState = $state('populated');
|
|
run: RunData | null = $state(null);
|
|
errorMsg: string | null = $state(null);
|
|
expandedDashboards: Set<string> = $state(new SvelteSet());
|
|
selectedScreenshotTab: Record<string, string> = $state({});
|
|
screenshotUrls: Record<string, string> = $state({});
|
|
logsSentExpanded: Set<string> = $state(new SvelteSet());
|
|
taskLogsExpanded: Set<string> = $state(new SvelteSet());
|
|
|
|
// ── Derived ───────────────────────────────────────────────────
|
|
dashboards = $derived(this.run?.dashboards || this.run?.results || []);
|
|
passCount = $derived(this.dashboards.filter((d: DashboardResult) => d.status === 'PASS').length);
|
|
warnCount = $derived(this.dashboards.filter((d: DashboardResult) => d.status === 'WARN').length);
|
|
failCount = $derived(this.dashboards.filter((d: DashboardResult) => d.status === 'FAIL').length);
|
|
unknownCount = $derived(this.dashboards.filter((d: DashboardResult) => !d.status || d.status === 'UNKNOWN').length);
|
|
totalIssues = $derived(this.dashboards.reduce((s: number, d: DashboardResult) => s + (d.issues?.length || 0), 0));
|
|
|
|
// ── Actions ───────────────────────────────────────────────────
|
|
|
|
toggleDashboard(dashboardId: string): void {
|
|
const next = new SvelteSet(this.expandedDashboards);
|
|
if (next.has(dashboardId)) {
|
|
next.delete(dashboardId);
|
|
} else {
|
|
next.add(dashboardId);
|
|
if (!this.selectedScreenshotTab[dashboardId]) {
|
|
this.selectedScreenshotTab = { ...this.selectedScreenshotTab, [dashboardId]: '0' };
|
|
const db = this.dashboards.find((d: DashboardResult) => d.id === dashboardId || d.dashboard_id === dashboardId);
|
|
if (db?.screenshots?.length) {
|
|
const tabKey = `${dashboardId}_0`;
|
|
if (!this.screenshotUrls[tabKey]) {
|
|
const path = db.screenshots[0]?.path || String(db.screenshots[0] || '');
|
|
this.loadScreenshot(path, tabKey);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
this.expandedDashboards = next;
|
|
}
|
|
|
|
isDashboardExpanded(dashboardId: string): boolean {
|
|
return this.expandedDashboards.has(dashboardId);
|
|
}
|
|
|
|
toggleLogsSent(key: string): void {
|
|
const next = new SvelteSet(this.logsSentExpanded);
|
|
next.has(key) ? next.delete(key) : next.add(key);
|
|
this.logsSentExpanded = next;
|
|
}
|
|
|
|
toggleTaskLogs(key: string): void {
|
|
const next = new SvelteSet(this.taskLogsExpanded);
|
|
if (next.has(key)) {
|
|
next.delete(key);
|
|
} else {
|
|
next.add(key);
|
|
const dId = key.replace('tasklogs_', '');
|
|
const db = this.dashboards.find((d: DashboardResult) => d.id === dId || d.dashboard_id === dId) as DashboardResult;
|
|
if (db && !db._taskLogsLoaded && this.run?.task_id) {
|
|
db._taskLogsLoaded = true;
|
|
api.getTaskLogs(this.run.task_id as string, { limit: 200 }).then((logs: unknown) => {
|
|
db.task_logs = (logs as TaskLogEntry[]) || [];
|
|
}).catch(() => { db.task_logs = []; });
|
|
}
|
|
}
|
|
this.taskLogsExpanded = next;
|
|
}
|
|
|
|
async loadScreenshot(screenshotPath: string, cacheKey: string): Promise<void> {
|
|
if (this.screenshotUrls[cacheKey]) return;
|
|
try {
|
|
const blob = await api.getStorageFileBlob(screenshotPath);
|
|
this.screenshotUrls = { ...this.screenshotUrls, [cacheKey]: URL.createObjectURL(blob) };
|
|
} catch { /* silently fail */ }
|
|
}
|
|
|
|
openScreenshot(url: string): void {
|
|
window.open(url, '_blank');
|
|
}
|
|
|
|
// ── Static utilities ──────────────────────────────────────────
|
|
|
|
static getStatusDotClass(s: string | undefined): string {
|
|
const map: Record<string, string> = { PASS: 'text-success', WARN: 'text-warning', FAIL: 'text-destructive', UNKNOWN: 'text-text-subtle' };
|
|
return map[s ?? ''] || 'text-text-subtle';
|
|
}
|
|
|
|
static getStatusDot(s: string | undefined): string {
|
|
const dots: Record<string, string> = { PASS: '🟢', WARN: '🟡', FAIL: '🔴', UNKNOWN: '⚪' };
|
|
return dots[s ?? ''] || dots.UNKNOWN;
|
|
}
|
|
|
|
static getSeverityClass(severity: string | undefined): string {
|
|
const map: Record<string, string> = { critical: 'bg-destructive-light text-destructive', high: 'bg-warning-light text-warning', medium: 'bg-warning-light text-warning', low: 'bg-primary-light text-primary', info: 'bg-surface-muted text-text-muted', error: 'bg-destructive-light text-destructive', warning: 'bg-warning-light text-warning' };
|
|
return map[severity?.toLowerCase() ?? ''] || 'bg-surface-muted text-text-muted';
|
|
}
|
|
|
|
static formatDuration(ms: number | undefined): string {
|
|
if (!ms && ms !== 0) return '-';
|
|
if (ms < 1000) return `${ms}ms`;
|
|
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
|
|
}
|
|
|
|
static getTriggerLabel(trigger: string | undefined, tFn: (_key: string) => string): string {
|
|
if (trigger === 'manual') return tFn('validation.trigger_manual') || 'Manual';
|
|
if (trigger === 'scheduled') return tFn('validation.trigger_scheduled') || 'Scheduled';
|
|
return trigger || '-';
|
|
}
|
|
|
|
static getPathLabel(path: string | undefined, tFn: (_key: string) => string): string {
|
|
if (path === 'A') return tFn('validation.path_a') || 'Path A';
|
|
if (path === 'B') return tFn('validation.path_b') || 'Path B';
|
|
return path || '-';
|
|
}
|
|
|
|
static getPathBadgeClass(path: string | undefined): string {
|
|
if (path === 'A') return 'bg-info-light text-info';
|
|
if (path === 'B') return 'bg-success-light text-success';
|
|
return 'bg-surface-muted text-text-muted';
|
|
}
|
|
|
|
static getTabIssueSeverity(tabLabel: string, tabIdx: number, totalTabs: number, issues: IssueResult[]): { count: number; severity: string } | null {
|
|
if (!issues?.length) return null;
|
|
const byIndex = issues.filter(i => i.tab_index === tabIdx || i.tab_index === String(tabIdx));
|
|
if (byIndex.length > 0) {
|
|
const worst = byIndex.some(i => i.severity === 'FAIL') ? 'FAIL' : byIndex.some(i => i.severity === 'WARN') ? 'WARN' : 'INFO';
|
|
return { count: byIndex.length, severity: worst };
|
|
}
|
|
const lowerLabel = tabLabel?.toLowerCase();
|
|
let matching: IssueResult[] = [];
|
|
if (lowerLabel) matching = issues.filter(i => i.location?.toLowerCase().includes(lowerLabel) || lowerLabel.includes(i.location?.toLowerCase() || ''));
|
|
const unmatched = issues.filter(i => i.tab_index === -1 || i.tab_index === '-1' || i.tab_index === undefined || i.tab_index === null);
|
|
if (matching.length === 0 && tabIdx === totalTabs - 1 && unmatched.length > 0) {
|
|
const worst = unmatched.some(i => i.severity === 'FAIL') ? 'FAIL' : unmatched.some(i => i.severity === 'WARN') ? 'WARN' : 'INFO';
|
|
return { count: unmatched.length, severity: worst };
|
|
}
|
|
if (matching.length === 0) return null;
|
|
const worst = matching.some(i => i.severity === 'FAIL') ? 'FAIL' : matching.some(i => i.severity === 'WARN') ? 'WARN' : 'INFO';
|
|
return { count: matching.length, severity: worst };
|
|
}
|
|
}
|
|
// #endregion ValidationRunDetailModel
|