@blundergoat/goat-flow
Version:
AI coding agent harness and local dashboard for Claude Code, OpenAI Codex, Google Antigravity, and GitHub Copilot - setup audits, guardrails, structured skills, deny hooks, and persistent learning loops.
971 lines (939 loc) • 39.3 kB
HTML
<!-- ═══ Skills View ═══ -->
<div
x-show="activeView === 'skills'"
x-cloak
style="height: calc(100vh - 3.5rem); overflow-y: auto"
>
<div style="max-width: 1100px; margin: 0 auto; padding: 24px 32px 24px">
<!-- Page head: evaluator-first title + installed-skill support action. -->
<div
style="
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24px;
margin-bottom: 16px;
"
>
<div style="flex: 1">
<div
class="gf-text-primary"
style="font-size: 22px; font-weight: 600; margin-bottom: 4px"
>
Skill Evaluator
</div>
<div
class="gf-text-muted"
style="font-size: 13px; max-width: 820px; line-height: 1.5"
>
Evaluate coding-agent skill drafts and bundles from pasted markdown or
local files. Installed goat-flow skill reports stay available below as
supporting evidence.
</div>
</div>
<div style="display: flex; gap: 8px; flex-shrink: 0; margin-top: 4px">
<button
@click="reauditAllSkills()"
:disabled="skillQualityPrefetching"
class="gf-btn gf-btn-md gf-btn-secondary"
>
<span x-show="!skillQualityPrefetching">Re-audit all</span>
<span x-show="skillQualityPrefetching">Auditing…</span>
</button>
</div>
</div>
<div
class="gf-skill-evaluator-panel"
@dragover="skillEvaluatorDragOver($event)"
@dragleave="skillEvaluatorDragLeave($event)"
@drop="skillEvaluatorDrop($event)"
:class="{ 'gf-modal-card-dragover': skillEvaluatorDragActive }"
>
<div class="gf-modal-header">
<div class="gf-modal-title">
<h2>Evaluate skill</h2>
<span
class="gf-modal-scope-tag"
x-text="skillEvaluatorResult ? 'evaluated just now' : 'draft · not saved'"
></span>
</div>
<div class="gf-modal-actions">
<button
x-show="skillEvaluatorResult"
@click="skillEvaluatorReportCopied = true; copySkillEvaluatorReport()"
class="gf-btn gf-btn-md gf-btn-secondary gf-copy-report-btn"
:class="{ 'is-copied': skillEvaluatorReportCopied }"
aria-live="polite"
>
<span
x-text="skillEvaluatorReportCopied ? 'Copied' : 'Copy report'"
></span>
</button>
<button
x-show="skillEvaluatorResult"
@click="resetSkillEvaluator()"
class="gf-btn gf-btn-md gf-btn-ghost"
>
Evaluate new
</button>
</div>
</div>
<div class="gf-modal-body">
<template x-if="!skillEvaluatorResult">
<div>
<template x-if="skillEvaluatorFiles.length === 0">
<div>
<label class="gf-dropzone" style="cursor: pointer">
<input
type="file"
accept=".md,.markdown,text/markdown,text/plain"
multiple
@change="loadSkillEvaluatorFile($event)"
style="display: none"
/>
<div class="gf-dropzone-icon" aria-hidden="true">
<svg
width="28"
height="28"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.75"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="17 8 12 3 7 8" />
<line x1="12" y1="3" x2="12" y2="15" />
</svg>
</div>
<div class="gf-dropzone-primary">
Drop SKILL.md and companion markdown files
</div>
<div class="gf-dropzone-secondary">
or
<span class="gf-dropzone-browse">browse</span>
· accepts <code>.md</code> · bundles are composed before
scoring
</div>
</label>
<div class="gf-divider-or">or paste</div>
<div class="gf-paste-block">
<textarea
x-model="skillEvaluatorContent"
placeholder="Paste SKILL.md content..."
rows="7"
></textarea>
</div>
</div>
</template>
<template x-if="skillEvaluatorFiles.length > 0">
<div class="gf-dropzone has-files">
<div class="gf-files-header">
<span
class="gf-fh-label"
x-text="'Files · ' + skillEvaluatorFiles.length + ' attached'"
></span>
<label class="gf-fh-add">
<input
type="file"
accept=".md,.markdown,text/markdown,text/plain"
multiple
@change="loadSkillEvaluatorFile($event)"
style="display: none"
/>
+ Add more
</label>
</div>
<div class="gf-file-chip-list">
<template
x-for="file in skillEvaluatorFiles"
:key="file.name"
>
<span class="gf-file-chip">
<span
class="gf-pill-file"
:class="{ 'is-skill': file.name === 'SKILL.md' }"
style="min-width: 50px"
x-text="skillFileRole(file.name)"
></span>
<span x-text="file.name"></span>
<button
@click="removeSkillEvaluatorFile(file.name)"
class="gf-file-chip-x"
aria-label="Remove file"
title="Remove from bundle"
>
×
</button>
</span>
</template>
</div>
</div>
</template>
<div
x-show="skillEvaluatorDragActive"
x-cloak
class="gf-text-primary"
style="
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: var(--accent-bg);
border: 2px dashed var(--accent);
border-radius: 8px;
z-index: 10;
font-weight: 600;
font-size: 14px;
pointer-events: none;
"
>
Drop .md files to add to the bundle
</div>
<div class="gf-input-footer">
<span
class="gf-if-hint"
x-text="skillEvaluatorFiles.length > 0 ? 'Engine will compose ' + skillEvaluatorFiles.length + ' file' + (skillEvaluatorFiles.length === 1 ? '' : 's') + ' into one scored surface.' : 'Evaluation runs on this local dashboard server.'"
></span>
<div class="gf-if-actions">
<button
@click="resetSkillEvaluator()"
x-show="skillEvaluatorFiles.length > 0 || skillEvaluatorContent.trim()"
class="gf-btn gf-btn-md gf-btn-ghost"
>
Clear
</button>
<button
@click="runSkillEvaluator()"
:disabled="skillEvaluatorLoading || (skillEvaluatorFiles.length === 0 && !skillEvaluatorContent.trim())"
class="gf-btn gf-btn-md gf-btn-primary"
>
<span x-show="!skillEvaluatorLoading">Evaluate</span>
<span x-show="skillEvaluatorLoading">Scoring…</span>
</button>
</div>
</div>
<template x-if="skillEvaluatorError">
<div class="gf-modal-error" x-text="skillEvaluatorError"></div>
</template>
</div>
</template>
<template x-if="skillEvaluatorResult">
<div>
<div
class="gf-verdict-banner"
:class="{
'is-keep': skillEvaluatorResult.recommendation === 'keep-skill',
'is-reference': skillEvaluatorResult.recommendation === 'reference-playbook',
'is-retire': skillEvaluatorResult.recommendation === 'retire' || skillEvaluatorResult.recommendation === 'consider-revision',
}"
>
<span
class="gf-vb-pill"
x-text="skillEvaluatorResult.recommendation"
></span>
<div class="gf-vb-text">
<div
class="gf-vb-title"
x-text="skillEvaluatorVerdict(skillEvaluatorResult).title"
></div>
<div
class="gf-vb-desc"
x-text="skillEvaluatorVerdict(skillEvaluatorResult).desc"
></div>
</div>
</div>
<div class="gf-result-grid">
<div>
<div class="gf-skill-id" style="margin-bottom: 10px">
<h2
style="font-size: 16px"
x-text="skillEvaluatorResult.artifact.name"
></h2>
<span
class="gf-pill-type"
:class="'is-' + skillEvaluatorResult.subtype"
x-text="skillEvaluatorResult.subtype"
></span>
<span
x-show="skillEvaluatorResult.shapeMismatch"
class="gf-pill-type"
:class="'is-' + (skillEvaluatorResult.detectedShape || 'playbook')"
x-text="'shape: ' + skillEvaluatorResult.detectedShape"
></span>
</div>
<div class="gf-score-display">
<div class="gf-sd-row">
<span
class="gf-sd-grade"
:class="{
'is-a': skillLetterGrade(skillReportPct(skillEvaluatorResult)) === 'A',
'is-b': skillLetterGrade(skillReportPct(skillEvaluatorResult)) === 'B',
'is-c': skillLetterGrade(skillReportPct(skillEvaluatorResult)) === 'C',
'is-d': skillLetterGrade(skillReportPct(skillEvaluatorResult)) === 'D',
'is-f': skillLetterGrade(skillReportPct(skillEvaluatorResult)) === 'F',
}"
x-text="skillLetterGrade(skillReportPct(skillEvaluatorResult))"
></span>
<span
class="gf-sd-pct"
x-text="Math.round(skillReportPct(skillEvaluatorResult) * 100) + '%'"
></span>
</div>
<div
class="gf-sd-raw"
x-text="skillEvaluatorResult.totalScore + ' / ' + skillEvaluatorResult.profileMax"
></div>
</div>
<div class="gf-meta-strip">
<span class="gf-ms-label">Classification</span>
<span>
<span
class="gf-ms-pct"
:class="{ 'is-low': skillEvaluatorResult.classification.confidence < 0.7 }"
x-text="Math.round(skillEvaluatorResult.classification.confidence * 100) + '%'"
></span>
<span
x-text="skillEvaluatorResult.classification.detectedSubtype"
></span>
<span
x-show="skillEvaluatorResult.shapeMismatch"
class="gf-cs-alt"
x-text="'Shape: ' + skillEvaluatorResult.detectedShape + ' (' + Math.round((skillEvaluatorResult.shapeConfidence || 0) * 100) + '%)'"
></span>
</span>
</div>
<div class="gf-section-label">Structural metrics</div>
<div class="gf-metrics">
<template
x-for="m in skillEvaluatorResult.metrics"
:key="'eval-metric-' + m.metric"
>
<div class="gf-metric-row">
<div
class="gf-metric-label"
:class="{ 'is-na': m.severity === 'n/a' }"
x-text="m.label"
></div>
<div
class="gf-bar-track"
:class="{ 'is-na': m.severity === 'n/a' }"
>
<template x-if="m.severity !== 'n/a'">
<div
class="gf-bar-fill"
:class="{
'is-ok': m.severity === 'ok',
'is-warn': m.severity === 'warn',
'is-fail': m.severity === 'fail',
}"
:style="{ width: (m.maxScore > 0 ? (m.score / m.maxScore) * 100 : 0) + '%' }"
></div>
</template>
</div>
<div
class="gf-metric-score"
:class="{
'is-ok': m.severity === 'ok',
'is-warn': m.severity === 'warn',
'is-fail': m.severity === 'fail',
'is-na': m.severity === 'n/a',
}"
x-text="m.severity === 'n/a' ? 'n/a' : (m.score + '/' + m.maxScore)"
></div>
</div>
</template>
</div>
<template x-if="skillEvaluatorResult.composedFrom.length > 0">
<div style="margin-top: 16px">
<div
class="gf-section-label"
x-text="'Composed from · ' + skillEvaluatorResult.composedFrom.length + ' file' + (skillEvaluatorResult.composedFrom.length === 1 ? '' : 's')"
></div>
<div class="gf-composed-from">
<template
x-for="src in skillEvaluatorResult.composedFrom"
:key="src"
>
<div class="gf-composed-row">
<span
class="gf-pill-file"
:class="{ 'is-skill': src === 'SKILL.md' }"
x-text="skillFileRole(src)"
></span>
<span class="gf-composed-name" x-text="src"></span>
</div>
</template>
</div>
</div>
</template>
</div>
<div>
<div class="gf-tips-header">
<h3>Improvement tips</h3>
<span
class="gf-tips-count"
x-text="skillEvaluatorResult.tips.length"
></span>
</div>
<template x-if="skillEvaluatorResult.tips.length === 0">
<div
class="gf-text-muted"
style="font-size: 12px; padding: 12px 0"
>
No improvement tips - all metrics passing.
</div>
</template>
<template
x-for="g in skillEvaluatorTipGroups(skillEvaluatorResult)"
:key="'eval-tip-' + g.metric"
>
<div class="gf-tip-group">
<button
@click="toggleSkillEvaluatorTipGroup(g.metric)"
class="gf-tip-group-header"
:class="{ 'is-collapsed': skillEvaluatorTipCollapsed[g.metric] }"
>
<div class="gf-tgh-left">
<span class="gf-tgh-chev">▾</span>
<span class="gf-tgh-name" x-text="g.label"></span>
<span
class="gf-tgh-count"
x-text="'· ' + g.tips.length + ' tip' + (g.tips.length === 1 ? '' : 's')"
></span>
</div>
<span
class="gf-tgh-score"
:class="{ 'is-warn': g.severity === 'warn', 'is-fail': g.severity === 'fail' }"
x-text="g.score + '/' + g.maxScore"
></span>
</button>
<div
class="gf-tip-group-body"
x-show="!skillEvaluatorTipCollapsed[g.metric]"
>
<template
x-for="(tip, idx) in g.tips"
:key="'eval-tip-row-' + g.metric + '-' + idx"
>
<div
class="gf-tip-row"
:class="{ 'is-fail': tip.severity === 'fail' }"
>
<span class="gf-tr-marker">●</span>
<span class="gf-tr-body" x-text="tip.message"></span>
</div>
</template>
</div>
</div>
</template>
</div>
</div>
<div class="gf-modal-result-footer">
<div class="gf-mrf-meta">
Slug:
<span
class="gf-mrf-slug"
x-text="skillEvaluatorSlug(skillEvaluatorResult)"
></span>
</div>
</div>
</div>
</template>
</div>
</div>
<!-- Scope strip - counts of audited skills + warnings + scope tag + avg
grade. Mirrors the Setup page's "14 of 14 installed · audited just now"
pattern so the headline finding is visible without drilling in. -->
<template x-if="skillQualityArtifacts.length > 0">
<div class="gf-skill-scope-strip">
<div class="gf-scope-meta">
<span class="gf-scope-pass">
<span class="gf-scope-check">✓</span>
<strong
x-text="skillQualityArtifacts.length + ' skill' + (skillQualityArtifacts.length === 1 ? '' : 's') + ' audited'"
></strong>
</span>
<span class="gf-scope-sep">·</span>
<template x-if="skillsWithWarningsCount() > 0">
<span class="gf-scope-warn">
⚠
<span
x-text="skillsWithWarningsCount() + ' with warning' + (skillsWithWarningsCount() === 1 ? '' : 's')"
></span>
</span>
</template>
<template
x-if="skillsWithWarningsCount() === 0 && Object.keys(skillQualityReports).length > 0"
>
<span class="gf-scope-dim">no warnings</span>
</template>
<span class="gf-scope-sep">·</span>
<span class="gf-scope-dim" x-text="skillAuditedRelative()"></span>
<span class="gf-scope-sep">·</span>
<span class="gf-scope-dim" x-text="'scope: ' + activeRunner"></span>
</div>
<div
class="gf-scope-avg"
x-show="Object.keys(skillQualityReports).length > 0"
>
avg
<span class="gf-scope-avg-val">
<span x-text="skillLetterGrade(skillsAvgPct())"></span>
<span x-text="Math.round(skillsAvgPct() * 100) + '%'"></span>
</span>
</div>
</div>
</template>
<!-- Supporting installed-skill audit context. -->
<div
class="gf-card gf-installed-skills-grid"
style="
display: grid;
grid-template-columns: 240px 1fr;
padding: 0;
overflow: hidden;
"
>
<!-- Sidebar - one row per discovered skill, with letter grade and
percentage on the right (populated from the prefetch cache). -->
<div
class="gf-installed-skills-sidebar"
style="
border-right: 1px solid var(--border-subtle);
padding: 14px 0;
max-height: calc(100vh - 16rem);
overflow-y: auto;
"
>
<div style="padding: 0 16px 8px">
<div
style="
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
letter-spacing: 0.08em;
text-transform: uppercase;
"
>
Installed skills
<span
style="opacity: 0.5"
x-text="'· ' + skillQualityArtifacts.length"
></span>
</div>
</div>
<template x-if="skillQualityArtifacts.length === 0">
<div
class="gf-text-muted"
style="padding: 8px 16px; font-size: 12px; text-align: center"
>
No installed skills discovered.
</div>
</template>
<div class="gf-skill-sidebar-list">
<template
x-for="art in skillQualityArtifacts"
:key="'sq-side-' + art.id"
>
<button
@click="loadSkillQualityReport(art.id)"
class="gf-skill-sidebar-item"
:class="{ 'is-active': skillQualitySelectedId === art.id }"
:aria-label="'View audit report for ' + art.name"
title="View audit report"
>
<span x-text="art.name"></span>
<template x-if="skillQualityReports[art.id]">
<span
class="gf-skill-sidebar-score"
:class="{
'is-a': skillLetterGrade(skillReportPct(skillQualityReports[art.id])) === 'A',
'is-b': skillLetterGrade(skillReportPct(skillQualityReports[art.id])) === 'B',
'is-c': skillLetterGrade(skillReportPct(skillQualityReports[art.id])) === 'C',
'is-d': skillLetterGrade(skillReportPct(skillQualityReports[art.id])) === 'D',
'is-f': skillLetterGrade(skillReportPct(skillQualityReports[art.id])) === 'F',
}"
>
<span
x-text="skillLetterGrade(skillReportPct(skillQualityReports[art.id]))"
></span>
<span
x-text="Math.round(skillReportPct(skillQualityReports[art.id]) * 100) + '%'"
></span>
</span>
</template>
<template x-if="!skillQualityReports[art.id]">
<span class="gf-skill-sidebar-score is-pending">-</span>
</template>
</button>
</template>
</div>
</div>
<!-- Detail panel: per-selected-skill report breakdown. -->
<div style="padding: 22px 26px; min-width: 0">
<!-- Loading -->
<template x-if="skillQualityLoading">
<div
style="
padding: 20px 0;
display: flex;
align-items: center;
gap: 8px;
"
>
<svg
class="animate-spin"
style="width: 14px; height: 14px; color: var(--accent)"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
/>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
<span class="gf-text-muted" style="font-size: 12px"
>Scoring...</span
>
</div>
</template>
<!-- Empty state -->
<template x-if="!skillQualityLoading && !skillQualityReport">
<div
class="gf-text-muted"
style="padding: 32px 0; font-size: 12px; text-align: center"
>
<span x-show="skillQualityArtifacts.length === 0"
>Re-audit all to discover installed goat-flow skills.</span
>
<span x-show="skillQualityArtifacts.length > 0"
>Select an installed skill to view its quality metrics.</span
>
</div>
</template>
<!-- Report -->
<template x-if="!skillQualityLoading && skillQualityReport">
<div>
<!-- Detail toolbar (top, right-aligned) -->
<div class="gf-skill-detail-toolbar">
<template x-if="skillQualityReport.prompt && terminalAvailable">
<button
@click="launchPreset(skillQualityReport.prompt, activeRunner, 'Skill Quality: ' + skillQualityReport.artifact.name, { cwdPath: projectPath }); activeView = 'workspace'"
:disabled="launching || serverSessions.length >= 10"
class="gf-btn gf-btn-md gf-btn-primary"
>
Assess in Runner
<svg
width="13"
height="13"
fill="none"
stroke="currentColor"
stroke-width="2.5"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14 5l7 7m0 0l-7 7m7-7H3"
/>
</svg>
</button>
</template>
<button
@click="(skillQualityReports[skillQualityReport.artifact.id] = undefined); loadSkillQualityReport(skillQualityReport.artifact.id)"
class="gf-btn gf-btn-md gf-btn-secondary"
>
Re-audit skill
</button>
</div>
<!-- Detail head: skill identity + file path on the left, big
grade block on the right. -->
<div
style="
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
"
>
<div style="flex: 1; min-width: 0">
<div class="gf-skill-id">
<h2 x-text="skillQualityReport.artifact.name"></h2>
<span
class="gf-pill-type"
:class="'is-' + skillQualityReport.subtype"
x-text="skillQualityReport.subtype"
></span>
<span
x-show="skillQualityReport.shapeMismatch"
class="gf-pill-type"
:class="'is-' + (skillQualityReport.detectedShape || 'playbook')"
x-text="'shape: ' + skillQualityReport.detectedShape"
></span>
<span
class="gf-pill-verdict"
:class="{
'is-keep': skillQualityReport.recommendation === 'keep-skill',
'is-reference': skillQualityReport.recommendation === 'reference-playbook',
'is-review': skillQualityReport.recommendation === 'needs-human-review' || skillQualityReport.recommendation === 'consider-reclassifying',
'is-retire': skillQualityReport.recommendation === 'retire' || skillQualityReport.recommendation === 'consider-revision',
}"
x-text="skillQualityReport.recommendation"
></span>
</div>
<div class="gf-skill-file-path">
<span x-text="skillQualityReport.artifact.path"></span>
<button
@click="navigator.clipboard.writeText(skillQualityReport.artifact.path); showToast('Path copied')"
class="gf-path-icon"
title="Copy path"
aria-label="Copy path"
>
⧉
</button>
</div>
</div>
<div class="gf-grade-block">
<div class="gf-grade-row">
<span
class="gf-grade-letter"
:class="{
'is-a': skillLetterGrade(skillReportPct(skillQualityReport)) === 'A',
'is-b': skillLetterGrade(skillReportPct(skillQualityReport)) === 'B',
'is-c': skillLetterGrade(skillReportPct(skillQualityReport)) === 'C',
'is-d': skillLetterGrade(skillReportPct(skillQualityReport)) === 'D',
'is-f': skillLetterGrade(skillReportPct(skillQualityReport)) === 'F',
}"
x-text="skillLetterGrade(skillReportPct(skillQualityReport))"
></span>
<span
class="gf-grade-pct"
x-text="Math.round(skillReportPct(skillQualityReport) * 100) + '%'"
></span>
</div>
<div
class="gf-grade-raw"
x-text="skillQualityReport.totalScore + ' / ' + skillQualityReport.profileMax"
></div>
</div>
</div>
<!-- Summary banner - promotes the headline conclusion (e.g.
"Strong skill identity with adequate structural quality")
instead of leaving it buried in fit notes. -->
<div
class="gf-summary-banner"
:class="{
'is-pass': skillSummaryBanner(skillQualityReport).severity === 'pass',
'is-danger': skillSummaryBanner(skillQualityReport).severity === 'fail',
}"
>
<div
class="gf-summary-title"
x-text="skillSummaryBanner(skillQualityReport).title"
></div>
<div
class="gf-summary-desc"
x-text="skillSummaryBanner(skillQualityReport).desc"
></div>
</div>
<!-- Classification strip -->
<div class="gf-classification-strip">
<span class="gf-cs-label">Classification</span>
<span>
<span
class="gf-cs-pct"
:class="{ 'is-low': skillQualityReport.classification.confidence < 0.7 }"
x-text="Math.round(skillQualityReport.classification.confidence * 100) + '%'"
></span>
<span
x-text="skillQualityReport.classification.detectedSubtype"
></span>
</span>
<span
x-show="skillQualityReport.shapeMismatch"
class="gf-cs-alt"
x-text="'Shape: ' + skillQualityReport.detectedShape + ' (' + Math.round((skillQualityReport.shapeConfidence || 0) * 100) + '%)'"
></span>
<template
x-if="skillQualityReport.classification.alternatives.length > 0"
>
<span class="gf-cs-alt">
(also:
<span
x-text="skillQualityReport.classification.alternatives.map(a => a.subtype + '=' + a.score).join(', ')"
></span
>)
</span>
</template>
</div>
<!-- Reclassification hint (kept from prior implementation). -->
<template
x-if="skillQualityReport.recommendation === 'consider-reclassifying'"
>
<div
style="
padding: 8px 12px;
background: rgba(251, 191, 36, 0.08);
border: 1px solid rgba(251, 191, 36, 0.25);
border-left: 3px solid var(--status-waiting);
border-radius: 6px;
margin-bottom: 14px;
font-size: 12px;
color: var(--text-secondary);
"
>
<strong>Consider reclassifying:</strong> structure quality is
high but subtype confidence is below <code>70%</code>. Review
whether
<code
x-text="skillQualityReport.classification.detectedSubtype"
></code>
is the correct shape, or
<code
x-text="skillQualityReport.classification.alternatives[0]?.subtype"
></code>
fits better.
</div>
</template>
<!-- Structural metrics -->
<div class="gf-section-label">Structural metrics</div>
<div class="gf-metrics">
<template
x-for="m in skillQualityReport.metrics"
:key="'sqm-' + m.metric"
>
<div class="gf-metric-row">
<div
class="gf-metric-label"
:class="{ 'is-na': m.severity === 'n/a' }"
x-text="m.label"
></div>
<div
class="gf-bar-track"
:class="{ 'is-na': m.severity === 'n/a' }"
>
<template x-if="m.severity !== 'n/a'">
<div
class="gf-bar-fill"
:class="{
'is-ok': m.severity === 'ok',
'is-warn': m.severity === 'warn',
'is-fail': m.severity === 'fail',
}"
:style="{ width: (m.maxScore > 0 ? (m.score / m.maxScore) * 100 : 0) + '%' }"
></div>
</template>
</div>
<div
class="gf-metric-score"
:class="{
'is-ok': m.severity === 'ok',
'is-warn': m.severity === 'warn',
'is-fail': m.severity === 'fail',
'is-na': m.severity === 'n/a',
}"
x-text="m.severity === 'n/a' ? 'n/a' : (m.score + '/' + m.maxScore)"
></div>
</div>
</template>
</div>
<!-- Findings - only renders for warn/fail metrics. Each finding
is its own bordered banner with severity colour, score meta,
and the metric's detail string. -->
<template
x-if="skillQualityReport.metrics.filter(m => m.severity === 'warn' || m.severity === 'fail').length > 0"
>
<div>
<div
class="gf-section-label"
style="margin-top: 22px; margin-bottom: 8px"
x-text="'Findings · ' + skillQualityReport.metrics.filter(m => m.severity === 'warn' || m.severity === 'fail').length"
></div>
<div class="gf-findings">
<template
x-for="m in skillQualityReport.metrics.filter(m => m.severity === 'warn' || m.severity === 'fail')"
:key="'sqf-' + m.metric"
>
<div
class="gf-finding"
:class="{ 'is-fail': m.severity === 'fail' }"
>
<span
class="gf-finding-icon"
x-text="m.severity === 'fail' ? '✕' : '⚠'"
></span>
<div class="gf-finding-body">
<div class="gf-finding-title">
<span x-text="m.label"></span>
<span
class="gf-finding-meta"
x-text="'· ' + m.score + '/' + m.maxScore"
></span>
</div>
<div class="gf-finding-desc" x-text="m.detail"></div>
</div>
</div>
</template>
</div>
</div>
</template>
<!-- Composed-from list - every file the engine pulled into the
scored surface. Lives at the bottom so the score and metric
breakdown stay above the fold. The engine's fit-notes prose
was duplicating the summary banner above, so it's been
removed (the headline finding lives in the banner now). -->
<template x-if="skillQualityReport.composedFrom.length > 0">
<div style="margin-top: 22px">
<div
class="gf-section-label"
x-text="'Composed from · ' + skillQualityReport.composedFrom.length + ' file' + (skillQualityReport.composedFrom.length === 1 ? '' : 's')"
></div>
<div class="gf-composed-from">
<template
x-for="src in skillQualityReport.composedFrom"
:key="src"
>
<div class="gf-composed-row">
<span
class="gf-pill-file"
:class="{ 'is-skill': src === 'SKILL.md' }"
x-text="skillFileRole(src)"
></span>
<span class="gf-composed-name" x-text="src"></span>
</div>
</template>
</div>
</div>
</template>
</div>
</template>
</div>
</div>
<div
class="gf-footer"
style="text-align: center; font-size: 11px; padding: 14px 0"
>
Built by
<a
href="https://www.blundergoat.com"
target="_blank"
class="gf-footer-link"
>BlunderGOAT</a
>
· <span x-text="dashboardVersion ? `v${dashboardVersion}` : ''"></span>
</div>
</div>
</div>