docs: redesign maturity scorecard pages (#96057)

Merged via squash.

Prepared head SHA: d2c680a48e
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
This commit is contained in:
Vincent Koc 2026-06-23 17:37:58 +08:00 committed by GitHub
parent 67b26126ce
commit 9089a8ab32
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 8143 additions and 1108 deletions

View file

@ -1654,6 +1654,15 @@
}
]
},
{
"tab": "Maturity",
"groups": [
{
"group": "Maturity docs",
"pages": ["maturity/scorecard", "maturity/taxonomy"]
}
]
},
{
"tab": "Reference",
"groups": [

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -135,3 +135,753 @@ html.dark .nav-tabs-underline {
grid-template-columns: 1fr;
}
}
.maturity-hero {
display: grid;
gap: 14px;
margin: 10px 0 38px;
padding: 4px 0 26px 20px;
border-bottom: 1px solid color-mix(in oklab, rgb(var(--primary)) 22%, transparent);
border-left: 3px solid rgb(var(--primary));
}
.maturity-hero-compact {
margin-bottom: 34px;
}
.maturity-hero h2 {
max-width: 46rem;
margin: 0;
font-size: clamp(26px, 3vw, 38px);
line-height: 1.08;
letter-spacing: -0.01em;
}
.maturity-hero-title {
max-width: 46rem;
margin: 0;
font-size: clamp(26px, 3vw, 38px);
font-weight: 750;
line-height: 1.08;
letter-spacing: -0.01em;
}
.maturity-hero > p:not(.maturity-kicker):not(.maturity-jump-links) {
max-width: 58rem;
margin: 0;
font-size: 16px;
line-height: 1.65;
opacity: 0.76;
}
.maturity-kicker {
margin: 0;
color: rgb(var(--primary));
font-size: 10px;
font-weight: 750;
letter-spacing: 0.1em;
line-height: 1.3;
text-transform: uppercase;
}
.maturity-jump-links {
margin: 0;
font-size: 13px;
line-height: 1.5;
}
.maturity-jump-links a {
text-decoration: none;
}
.maturity-jump-links a:hover {
text-decoration: underline;
text-underline-offset: 3px;
}
.maturity-score-stable,
.maturity-band-stable {
color: #4ca574;
}
.maturity-score-beta,
.maturity-band-beta {
color: #849fd2;
}
.maturity-score-alpha,
.maturity-band-alpha {
color: #d39a4b;
}
.maturity-score-experimental,
.maturity-band-experimental {
color: #dc7669;
}
.maturity-score-clawesome,
.maturity-band-clawesome {
color: #46b59a;
}
.maturity-level-pill {
display: inline-flex;
align-items: center;
gap: 6px;
width: max-content;
max-width: 100%;
padding: 3px 8px;
border: 1px solid color-mix(in oklab, currentColor 32%, transparent);
border-radius: 999px;
background: color-mix(in oklab, currentColor 10%, transparent);
color: inherit;
font-size: 10px;
font-weight: 750;
line-height: 1.25;
white-space: nowrap;
}
.maturity-level-code {
font-size: 9px;
letter-spacing: 0.04em;
opacity: 0.72;
}
.maturity-level-experimental {
color: #dc7669;
}
.maturity-level-alpha {
color: #d39a4b;
}
.maturity-level-beta {
color: #849fd2;
}
.maturity-level-stable {
color: #4ca574;
}
.maturity-level-clawesome {
color: #46b59a;
}
.maturity-summary-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
margin: 14px 0 20px;
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 18%, transparent);
border-bottom: 1px solid color-mix(in oklab, rgb(var(--primary)) 18%, transparent);
}
.maturity-summary-item {
display: grid;
gap: 10px;
min-width: 0;
padding: 18px 20px 18px 0;
}
.maturity-summary-item + .maturity-summary-item {
padding-left: 20px;
border-left: 1px solid color-mix(in oklab, rgb(var(--primary)) 14%, transparent);
}
.maturity-summary-heading {
display: flex;
align-items: baseline;
gap: 9px;
}
.maturity-summary-value {
display: inline-block;
font-size: 30px;
font-weight: 750;
letter-spacing: -0.04em;
}
.maturity-summary-heading > span:not(.maturity-summary-value) {
font-size: 13px;
font-weight: 700;
}
.maturity-summary-bar {
height: 7px;
overflow: hidden;
background: color-mix(in oklab, currentColor 14%, transparent);
}
.maturity-summary-bar span {
display: block;
width: calc(var(--score) * 1%);
height: 100%;
background: currentColor;
}
.maturity-summary-meta {
display: flex;
flex-wrap: wrap;
gap: 4px 10px;
font-size: 11px;
line-height: 1.4;
}
.maturity-summary-meta span:first-child {
font-weight: 700;
}
.maturity-summary-meta span:last-child {
opacity: 0.62;
}
.maturity-band-list {
display: flex;
margin: 12px 0 30px;
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 16%, transparent);
border-bottom: 1px solid color-mix(in oklab, rgb(var(--primary)) 16%, transparent);
}
.maturity-band {
display: grid;
flex: 1 1 0;
gap: 3px;
padding: 10px 12px 11px 0;
}
.maturity-band + .maturity-band {
padding-left: 12px;
border-left: 1px solid color-mix(in oklab, rgb(var(--primary)) 12%, transparent);
}
.maturity-band-title {
font-size: 12px;
font-weight: 700;
}
.maturity-band-title + span {
color: inherit;
font-size: 11px;
opacity: 0.7;
}
.maturity-band > span:last-child {
color: inherit;
font-size: 11px;
opacity: 0.7;
}
.maturity-band span {
color: inherit;
font-size: 11px;
opacity: 0.7;
}
.maturity-band .maturity-level-pill {
font-size: 10px;
opacity: 1;
}
.maturity-band .maturity-level-pill span {
font-size: inherit;
opacity: inherit;
}
.maturity-score {
display: grid;
gap: 4px;
min-width: 0;
color: inherit;
font-size: 11px;
font-weight: 700;
}
.maturity-score-label {
display: flex;
justify-content: space-between;
gap: 6px;
line-height: 1.2;
}
.maturity-score-label > span:first-child {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.maturity-score-label > span:last-child {
flex: 0 0 auto;
}
.maturity-score-label .maturity-level-pill {
gap: 4px;
padding: 2px 6px;
font-size: 9px;
}
.maturity-score-label-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.maturity-summary-meta .maturity-level-pill {
opacity: 1;
}
.maturity-meter {
display: inline-block;
width: 100%;
height: 3px;
overflow: hidden;
background: color-mix(in oklab, currentColor 15%, transparent);
vertical-align: middle;
}
.maturity-meter > span {
display: block;
width: 0;
height: 100%;
background: currentColor;
}
.maturity-score-unscored,
.maturity-lts-none {
color: inherit;
opacity: 0.52;
}
.maturity-surface-table {
display: grid;
gap: 0;
margin: 8px 0 22px;
}
.maturity-surface-row {
display: grid;
grid-template-columns: minmax(190px, 1.55fr) repeat(3, minmax(110px, 1fr)) minmax(72px, 0.55fr);
gap: 12px;
align-items: center;
padding: 13px 0;
border-top: 1px solid color-mix(in oklab, currentColor 14%, transparent);
}
.maturity-surface-row-header {
padding: 0 0 9px;
border-top: 0;
color: inherit;
font-size: 10px;
font-weight: 750;
letter-spacing: 0.04em;
opacity: 0.56;
text-transform: uppercase;
}
.maturity-surface-name {
display: grid;
gap: 3px;
min-width: 0;
border-bottom: 0 !important;
text-decoration: none;
}
.maturity-surface-name:hover .maturity-surface-title {
color: rgb(var(--primary));
}
.maturity-surface-title {
overflow-wrap: anywhere;
font-size: 13px;
font-weight: 700;
line-height: 1.25;
}
.maturity-surface-meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
min-width: 0;
}
.maturity-surface-meta > span:not(.maturity-level-pill) {
font-size: 11px;
opacity: 0.62;
}
.maturity-surface-meta .maturity-level-pill {
opacity: 1;
}
.maturity-surface-metric {
min-width: 0;
}
.maturity-surface-metric-label {
display: none;
font-size: 10px;
opacity: 0.62;
}
.maturity-surface-support {
justify-self: start;
}
.maturity-lts {
display: inline-flex;
align-items: center;
gap: 5px;
color: inherit;
font-size: 11px;
font-weight: 700;
white-space: nowrap;
}
.maturity-lts::before {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
content: "";
}
.maturity-lts-partial {
color: #d39a4b;
}
.maturity-lts-full {
color: #4ca574;
}
.maturity-evidence-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
margin: 14px 0 24px;
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 16%, transparent);
border-bottom: 1px solid color-mix(in oklab, rgb(var(--primary)) 16%, transparent);
}
.maturity-evidence-card {
display: grid;
gap: 4px;
min-width: 0;
padding: 14px 16px 14px 0;
}
.maturity-evidence-card + .maturity-evidence-card {
padding-left: 16px;
border-left: 1px solid color-mix(in oklab, rgb(var(--primary)) 12%, transparent);
}
.maturity-evidence-title {
font-size: 13px;
font-weight: 700;
}
.maturity-evidence-card span {
font-size: 11px;
line-height: 1.4;
opacity: 0.68;
}
.maturity-readiness-summary {
margin: 0 0 12px;
font-size: 12px;
opacity: 0.65;
}
.maturity-readiness-list {
display: grid;
margin: 0;
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 14%, transparent);
}
.maturity-readiness-row {
display: grid;
grid-template-columns: minmax(0, 1.7fr) minmax(120px, 0.8fr) minmax(110px, 0.7fr);
gap: 14px;
align-items: center;
padding: 11px 0;
border-bottom: 1px solid color-mix(in oklab, rgb(var(--primary)) 10%, transparent);
font-size: 11px;
}
.maturity-readiness-row-header {
padding: 8px 0;
border-bottom-color: color-mix(in oklab, rgb(var(--primary)) 14%, transparent);
font-size: 10px;
font-weight: 750;
letter-spacing: 0.04em;
opacity: 0.56;
text-transform: uppercase;
}
.maturity-readiness-area {
display: grid;
gap: 3px;
min-width: 0;
}
.maturity-readiness-title {
overflow-wrap: anywhere;
font-weight: 700;
}
.maturity-readiness-status {
font-size: 10px;
opacity: 0.62;
}
.maturity-readiness-status-ready {
color: #4ca574;
}
.maturity-readiness-status-partially-reviewed {
color: #d39a4b;
}
.maturity-readiness-status-needs-review {
color: #dc7669;
}
.maturity-category-list {
display: grid;
width: 100%;
margin-top: 4px;
overflow: hidden;
}
.maturity-category-row {
display: grid;
grid-template-columns: minmax(180px, 1.55fr) repeat(3, minmax(100px, 1fr)) minmax(140px, 1.2fr);
gap: 12px;
align-items: center;
padding: 12px 0;
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 11%, transparent);
font-size: 11px;
}
.maturity-category-row-header {
padding: 8px 0;
border-top: 0;
color: inherit;
font-size: 10px;
font-weight: 750;
letter-spacing: 0.04em;
opacity: 0.56;
text-transform: uppercase;
}
.maturity-category-area {
display: grid;
gap: 3px;
min-width: 0;
}
.maturity-category-title {
overflow-wrap: anywhere;
font-weight: 700;
}
.maturity-category-area > span:last-child {
font-size: 10px;
opacity: 0.62;
}
.maturity-category-docs {
min-width: 0;
overflow-wrap: anywhere;
line-height: 1.4;
}
.maturity-category-docs a {
text-decoration: none;
}
.maturity-category-docs a:hover {
text-decoration: underline;
text-underline-offset: 3px;
}
.maturity-level-list {
display: grid;
margin: 12px 0 28px;
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 16%, transparent);
border-bottom: 1px solid color-mix(in oklab, rgb(var(--primary)) 16%, transparent);
}
.maturity-level-row {
display: grid;
grid-template-columns: minmax(130px, 0.32fr) minmax(0, 1fr);
gap: 4px 14px;
padding: 13px 0;
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 11%, transparent);
}
.maturity-level-row:first-child {
border-top: 0;
}
.maturity-level-title {
grid-row: span 2;
font-size: 13px;
font-weight: 700;
}
.maturity-level-title .maturity-level-pill {
opacity: 1;
}
.maturity-level-row span,
.maturity-level-promotion {
font-size: 12px;
line-height: 1.45;
opacity: 0.68;
}
.maturity-surface-link {
display: grid;
gap: 3px;
margin: 0;
padding: 11px 0;
border-bottom: 1px solid color-mix(in oklab, currentColor 14%, transparent);
text-decoration: none;
}
.maturity-surface-link:hover {
color: rgb(var(--primary));
}
.maturity-surface-link .maturity-surface-title {
font-size: 13px;
font-weight: 700;
}
.maturity-surface-link > .maturity-surface-meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
}
.maturity-surface-link > .maturity-surface-meta > span:not(.maturity-level-pill) {
font-size: 11px;
opacity: 0.68;
}
.maturity-surface-link > .maturity-surface-meta .maturity-level-pill {
opacity: 1;
}
.maturity-surface-rollup {
display: flex;
flex-wrap: wrap;
gap: 5px 14px;
margin: 0 0 14px;
padding: 9px 0;
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 13%, transparent);
border-bottom: 1px solid color-mix(in oklab, rgb(var(--primary)) 13%, transparent);
}
.maturity-surface-rollup > span {
color: inherit;
font-size: 11px;
font-weight: 700;
}
#content table .maturity-score,
#content table .maturity-lts {
white-space: nowrap;
}
@media (max-width: 960px) {
.maturity-summary-grid,
.maturity-evidence-grid {
grid-template-columns: 1fr;
}
.maturity-summary-item,
.maturity-summary-item + .maturity-summary-item,
.maturity-evidence-card,
.maturity-evidence-card + .maturity-evidence-card {
padding: 14px 0;
border-left: 0;
}
.maturity-summary-item + .maturity-summary-item,
.maturity-evidence-card + .maturity-evidence-card {
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 12%, transparent);
}
.maturity-surface-row {
grid-template-columns: minmax(160px, 1.35fr) repeat(3, minmax(98px, 1fr)) minmax(70px, 0.5fr);
gap: 8px;
}
.maturity-category-row {
grid-template-columns: minmax(160px, 1.35fr) repeat(3, minmax(86px, 1fr)) minmax(110px, 1fr);
gap: 8px;
}
}
@media (max-width: 640px) {
.maturity-hero {
padding-left: 14px;
}
.maturity-surface-row-header {
display: none;
}
.maturity-surface-row {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 9px 12px;
}
.maturity-surface-metric {
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
gap: 6px;
}
.maturity-surface-metric-label {
display: block;
}
.maturity-surface-support {
justify-self: end;
}
.maturity-readiness-row,
.maturity-category-row {
grid-template-columns: 1fr;
gap: 5px;
}
.maturity-readiness-row-header,
.maturity-category-row-header {
display: none;
}
.maturity-category-docs {
padding-top: 3px;
}
.maturity-band-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.maturity-band + .maturity-band {
padding-left: 0;
border-left: 0;
}
.maturity-level-row {
grid-template-columns: 1fr;
}
.maturity-level-title {
grid-row: auto;
}
}

View file

@ -175,10 +175,6 @@ function markdownEscape(value: RenderScalar): string {
return String(value ?? "").replaceAll("|", "\\|");
}
function yamlCode(value: RenderScalar): string {
return `\`${markdownEscape(value)}\``;
}
function markdownSlug(value: string): string {
return value
.trim()
@ -256,31 +252,231 @@ function docsLink(docPath: string, docsRouteIndex: DocsRouteIndex): string | und
return `[${markdownEscape(title)}](/${markdownEscape(publicHref)})`;
}
function markdownTable(rows: RenderScalar[][]): string[] {
if (rows.length === 0) {
return [];
function scorePercent(value?: QaMaturityScoreObject): number | undefined {
if (!value || typeof value !== "object" || !Number.isFinite(value.score)) {
return undefined;
}
const columnCount = Math.max(...rows.map((row) => row.length));
const normalizedRows = rows.map((row) =>
Array.from({ length: columnCount }, (_, index) => String(row[index] ?? "")),
);
const widths = Array.from({ length: columnCount }, (_, index) =>
Math.max(3, ...normalizedRows.map((row) => row[index]?.length ?? 0)),
);
const formatRow = (row: string[]) =>
`| ${row.map((cell, index) => cell.padEnd(widths[index] ?? 3)).join(" | ")} |`;
return Math.max(0, Math.min(100, Math.round(value.score)));
}
function scoreClass(value?: QaMaturityScoreObject): string {
const score = scorePercent(value);
if (score === undefined) {
return "maturity-score-unscored";
}
if (score >= 95) {
return "maturity-score-clawesome";
}
if (score >= 80) {
return "maturity-score-stable";
}
if (score >= 70) {
return "maturity-score-beta";
}
if (score >= 50) {
return "maturity-score-alpha";
}
return "maturity-score-experimental";
}
function scoreLabel(value?: QaMaturityScoreObject): string {
if (!value || typeof value !== "object") {
return "Unscored";
}
const label = maturityDisplayLabel(value.label ?? "Unscored");
return `${label}${scorePercent(value) === undefined ? "" : ` - ${scorePercent(value)}%`}`;
}
function scoreMeter(value?: QaMaturityScoreObject): string {
const score = scorePercent(value);
if (score === undefined) {
return '<span className="maturity-score maturity-score-unscored"><span className="maturity-score-label"><span>Unscored</span><span>-</span></span></span>';
}
return `<span className="maturity-score ${scoreClass(value)}"><span className="maturity-score-label">${maturityLabelPill(value?.label ?? "Unscored")}<span>${score}%</span></span><span className="maturity-meter" aria-hidden="true"><span style={{ width: "${score}%" }} /></span></span>`;
}
function scoreSummary(
title: string,
value: QaMaturityScoreObject | undefined,
description: string,
): string[] {
const score = scorePercent(value);
const displayScore = score === undefined ? "-" : `${score}%`;
const cssScore = score === undefined ? "0" : String(score);
return [
formatRow(normalizedRows[0] ?? []),
formatRow(widths.map((width) => "-".repeat(width))),
...normalizedRows.slice(1).map(formatRow),
`<div className="maturity-summary-item ${scoreClass(value)}">`,
' <div className="maturity-summary-heading">',
` <span className="maturity-summary-value">${displayScore}</span>`,
` <span>${markdownEscape(title)}</span>`,
" </div>",
` <div className="maturity-summary-bar" style={{ "--score": "${cssScore}" }}><span /></div>`,
' <div className="maturity-summary-meta">',
` ${maturityLabelPill(value?.label ?? "Unscored")}`,
` <span>${markdownEscape(description)}</span>`,
" </div>",
"</div>",
];
}
function scoreText(value?: QaMaturityScoreObject): string {
if (!value || typeof value !== "object") {
return "`Unscored`";
function maturityLtsBadge(lts?: QaMaturityScoreSurfaceLts): string {
if (!lts || typeof lts !== "object") {
return '<span className="maturity-lts maturity-lts-none">Unscored</span>';
}
return `\`${markdownEscape(value.label ?? "")} (${markdownEscape(value.score ?? "")}%)\``;
const supportedCategories = lts.supported_categories ?? 0;
const status = lts.status ?? "unknown";
const label = status === "full" ? "Full" : status === "partial" ? "Partial" : "None";
const detail = status === "none" ? "" : ` - ${supportedCategories}`;
return `<span className="maturity-lts maturity-lts-${status}">${label}${detail}</span>`;
}
function maturityLevelClass(code: RenderScalar): string {
const level = String(code ?? "")
.trim()
.toUpperCase();
if (level === "M5") {
return "maturity-level-clawesome";
}
if (level === "M4") {
return "maturity-level-stable";
}
if (level === "M3") {
return "maturity-level-beta";
}
if (level === "M2") {
return "maturity-level-alpha";
}
return "maturity-level-experimental";
}
function maturityLabelCode(label: RenderScalar): string | undefined {
switch (
String(label ?? "")
.trim()
.toLowerCase()
) {
case "planned":
return "M0";
case "experimental":
return "M1";
case "alpha":
return "M2";
case "beta":
return "M3";
case "stable":
return "M4";
case "lovable":
case "clawesome":
return "M5";
default:
return undefined;
}
}
function maturityDisplayLabel(label: RenderScalar): string {
return String(label ?? "")
.trim()
.toLowerCase() === "lovable"
? "Clawesome"
: String(label ?? "");
}
function maturityLabelPill(label: RenderScalar): string {
const code = maturityLabelCode(label);
if (!code) {
return `<span className="maturity-score-label-text">${markdownEscape(maturityDisplayLabel(label))}</span>`;
}
return `<span className="maturity-level-pill ${maturityLevelClass(code)}">${markdownEscape(maturityDisplayLabel(label))}</span>`;
}
function maturityBandClass(label: RenderScalar): string {
const code = maturityLabelCode(label);
return code
? maturityLevelClass(code).replace("maturity-level-", "maturity-band-")
: "maturity-band-experimental";
}
function maturityLevelPill(code: RenderScalar, label: RenderScalar): string {
return `<span className="maturity-level-pill ${maturityLevelClass(code)}"><span className="maturity-level-code">${markdownEscape(code)}</span><span>${markdownEscape(maturityDisplayLabel(label))}</span></span>`;
}
function maturityLevelPillFromText(value: string): string {
const match = value.trim().match(/^(M\d+)\s+(.+)$/i);
if (!match) {
return `<span className="maturity-level-pill maturity-level-experimental">${markdownEscape(value)}</span>`;
}
return maturityLevelPill(match[1], match[2]);
}
function indentMarkdown(lines: string[], spaces = 4): string[] {
const prefix = " ".repeat(spaces);
return lines.map((line) => (line ? `${prefix}${line}` : ""));
}
function renderSurfaceRows({
coverage,
levels,
scoreSurfaces,
surfaces,
}: {
coverage: DerivedCoverageScores;
levels: Map<string, QaMaturityTaxonomyLevel>;
scoreSurfaces: Map<string, QaMaturityScoreSurface>;
surfaces: QaMaturityTaxonomySurface[];
}): string[] {
const rows = [
'<div className="maturity-surface-table">',
' <div className="maturity-surface-row maturity-surface-row-header"><span>Surface</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Support</span></div>',
];
for (const surface of surfaces) {
const scoreSurface = scoreSurfaces.get(surface.id);
rows.push(
' <div className="maturity-surface-row">',
` <a className="maturity-surface-name" href="/maturity/taxonomy#${markdownSlug(surface.name)}"><span className="maturity-surface-title">${markdownEscape(surface.name)}</span><span className="maturity-surface-meta">${maturityLevelPillFromText(levelText(surface, levels))}<span>${surface.categories.length} areas</span></span></a>`,
` <div className="maturity-surface-metric"><span className="maturity-surface-metric-label">Coverage</span>${scoreMeter(coverage.surfaces.get(surface.id))}</div>`,
` <div className="maturity-surface-metric"><span className="maturity-surface-metric-label">Quality</span>${scoreMeter(scoreSurface?.scores?.quality)}</div>`,
` <div className="maturity-surface-metric"><span className="maturity-surface-metric-label">Completeness</span>${scoreMeter(scoreSurface?.scores?.completeness)}</div>`,
` <div className="maturity-surface-support">${maturityLtsBadge(scoreSurface?.lts)}</div>`,
" </div>",
);
}
rows.push("</div>");
return rows;
}
function renderSurfaceTabs({
coverage,
levels,
scoreSurfaces,
surfaces,
}: {
coverage: DerivedCoverageScores;
levels: Map<string, QaMaturityTaxonomyLevel>;
scoreSurfaces: Map<string, QaMaturityScoreSurface>;
surfaces: QaMaturityTaxonomySurface[];
}): string[] {
const families = qaMaturityFamilyOrder(surfaces);
const tabs = [
"<Tabs>",
' <Tab title="All surfaces">',
...indentMarkdown(renderSurfaceRows({ coverage, levels, scoreSurfaces, surfaces })),
" </Tab>",
];
for (const family of families) {
tabs.push(
` <Tab title="${markdownEscape(familyTitle(family))}">`,
...indentMarkdown(
renderSurfaceRows({
coverage,
levels,
scoreSurfaces,
surfaces: surfaces.filter((surface) => surface.family === family),
}),
),
" </Tab>",
);
}
tabs.push("</Tabs>");
return tabs;
}
function levelText(
@ -296,31 +492,67 @@ function levelText(
return [level?.code, level?.label ?? levelId].filter(Boolean).join(" ");
}
function ltsText(lts?: QaMaturityScoreSurfaceLts): string {
if (!lts || typeof lts !== "object") {
return "unscored";
function maturityLevelRank(
surface: QaMaturityTaxonomySurface,
taxonomyLevels: Map<string, QaMaturityTaxonomyLevel>,
): number {
const match = levelText(surface, taxonomyLevels).match(/M(\d+)/i);
return match ? Number(match[1]) : -1;
}
function ltsRank(lts?: QaMaturityScoreSurfaceLts): number {
if (lts?.status === "full") {
return 0;
}
const supportedCategories = lts.supported_categories ?? 0;
if (lts.status === "full") {
return `full (${supportedCategories})`;
if (lts?.status === "partial") {
return 1;
}
if (lts.status === "partial") {
return `partial (${supportedCategories})`;
}
if (lts.status === "none") {
return "none";
}
return lts.status ?? "unknown";
return 2;
}
function sortedMaturitySurfaces(
surfaces: QaMaturityTaxonomySurface[],
scoreSurfaces: Map<string, QaMaturityScoreSurface>,
taxonomyLevels: Map<string, QaMaturityTaxonomyLevel>,
): QaMaturityTaxonomySurface[] {
return surfaces.toSorted((left, right) => {
const leftScore = scoreSurfaces.get(left.id);
const rightScore = scoreSurfaces.get(right.id);
const levelOrder =
maturityLevelRank(right, taxonomyLevels) - maturityLevelRank(left, taxonomyLevels);
if (levelOrder !== 0) {
return levelOrder;
}
const completenessOrder =
(rightScore?.scores.completeness.score ?? -1) - (leftScore?.scores.completeness.score ?? -1);
if (completenessOrder !== 0) {
return completenessOrder;
}
const qualityOrder =
(rightScore?.scores.quality.score ?? -1) - (leftScore?.scores.quality.score ?? -1);
if (qualityOrder !== 0) {
return qualityOrder;
}
const ltsOrder = ltsRank(leftScore?.lts) - ltsRank(rightScore?.lts);
if (ltsOrder !== 0) {
return ltsOrder;
}
return left.name.localeCompare(right.name);
});
}
function renderScoreBands(): string[] {
return [
"## Score bands",
"",
...markdownTable([
["Label", "Score range"],
...QA_MATURITY_SCORE_LABEL_BANDS.map(([label, low, high]) => [label, `${low}-${high}%`]),
]),
'<div className="maturity-band-list">',
...[...QA_MATURITY_SCORE_LABEL_BANDS]
.reverse()
.map(
([label, low, high]) =>
` <div className="maturity-band ${maturityBandClass(label)}"><span className="maturity-band-title">${maturityLabelPill(label)}</span><span>${low}-${high}%</span></div>`,
),
"</div>",
"",
];
}
@ -643,41 +875,72 @@ function renderEvidenceSection(
"",
];
const summaryRows: RenderScalar[][] = [
["Check set", "Completed", "Checks run", "Results", "Areas reviewed", "Capabilities reviewed"],
];
lines.push('<div className="maturity-evidence-grid">');
for (const item of scorecardSummaries) {
const scorecard = item.scorecard;
summaryRows.push([
markdownEscape(checkSetTitle(item.profile)),
markdownEscape(item.generatedAt),
item.entryCount,
markdownEscape(resultCountsText(item.statuses)),
markdownEscape(countText(scorecard?.categories)),
markdownEscape(countText(scorecard?.features)),
]);
lines.push(
' <div className="maturity-evidence-card">',
` <span className="maturity-evidence-title">${markdownEscape(checkSetTitle(item.profile))}</span>`,
` <span>${markdownEscape(item.generatedAt)}</span>`,
` <span>${item.entryCount} checks - ${markdownEscape(resultCountsText(item.statuses))}</span>`,
` <span>${markdownEscape(countText(scorecard?.categories))} areas - ${markdownEscape(countText(scorecard?.features))} capabilities</span>`,
" </div>",
);
}
lines.push(...markdownTable(summaryRows), "");
lines.push("</div>", "");
const categoryRows = scorecardSummaries.flatMap((item) =>
(item.scorecard?.categoryReports ?? []).map((category) => ({ item, category })),
);
if (categoryRows.length > 0) {
const readinessRows: RenderScalar[][] = [
["Check set", "Surface", "Area", "Status", "Capabilities reviewed", "Follow-up"],
];
for (const { item, category } of categoryRows) {
const features = countText(category.features);
readinessRows.push([
markdownEscape(checkSetTitle(item.profile)),
markdownEscape(surfaceNames.get(category.surfaceId) ?? familyTitle(category.surfaceId)),
markdownEscape(category.name),
markdownEscape(readinessStatusText(category.status)),
markdownEscape(features),
markdownEscape(followUpText(category.missingCoverageIds)),
]);
const grouped = new Map<string, Array<(typeof categoryRows)[number]>>();
for (const row of categoryRows) {
const existing = grouped.get(row.category.surfaceId) ?? [];
existing.push(row);
grouped.set(row.category.surfaceId, existing);
}
lines.push("### Readiness by area", "", ...markdownTable(readinessRows), "");
lines.push(
"### Readiness by area",
"",
"Open a surface to inspect the evidence state of each category. The list stays collapsed so the page remains useful at a glance.",
"",
"<AccordionGroup>",
);
for (const [surfaceId, rows] of grouped) {
const surfaceName = surfaceNames.get(surfaceId) ?? familyTitle(surfaceId);
const statusCounts = rows.reduce(
(counts, row) => {
counts[readinessStatusText(row.category.status)] =
(counts[readinessStatusText(row.category.status)] ?? 0) + 1;
return counts;
},
{} as Record<string, number>,
);
const summary = Object.entries(statusCounts)
.map(([status, count]) => `${count} ${status.toLowerCase()}`)
.join(" / ");
lines.push(
` <Accordion title="${markdownEscape(surfaceName)} - ${rows.length} areas">`,
` <p className="maturity-readiness-summary">${markdownEscape(summary)}</p>`,
' <div className="maturity-readiness-list">',
' <div className="maturity-readiness-row maturity-readiness-row-header"><span>Area</span><span>Capabilities</span><span>Follow-up</span></div>',
);
for (const { item, category } of rows) {
const status = readinessStatusText(category.status);
lines.push(
' <div className="maturity-readiness-row">',
' <div className="maturity-readiness-area">',
` <span className="maturity-readiness-title">${markdownEscape(category.name)}</span>`,
` <span className="maturity-readiness-status maturity-readiness-status-${markdownSlug(status)}">${markdownEscape(status)} - ${markdownEscape(checkSetTitle(item.profile))}</span>`,
" </div>",
` <span>${markdownEscape(countText(category.features))}</span>`,
` <span>${markdownEscape(followUpText(category.missingCoverageIds))}</span>`,
" </div>",
);
}
lines.push(" </div>", " </Accordion>", "");
}
lines.push("</AccordionGroup>", "");
}
return lines;
}
@ -690,9 +953,16 @@ function renderMaturityScorecard({
}: RenderMaturityScorecardInputs): string {
const levels = qaMaturityTaxonomyLevelMap(taxonomy);
const scoreSurfaces = surfaceScoreMap(scores);
const surfaces = activeQaMaturityTaxonomySurfaces(taxonomy);
const surfaces = sortedMaturitySurfaces(
activeQaMaturityTaxonomySurfaces(taxonomy),
scoreSurfaces,
levels,
);
const surfaceNames = surfaceNameMap(surfaces);
const updatedDate = latestScoreRunDate(scores);
const surfaceAverage = coverage.rollups.surface_average;
const qualityAverage = scores.rollups.surface_average.quality;
const completenessAverage = scores.rollups.surface_average.completeness;
const lines = [
...frontmatter(
"Maturity scorecard",
@ -700,65 +970,44 @@ function renderMaturityScorecard({
),
"# Maturity scorecard",
"",
"These scores summarize release readiness across OpenClaw product areas, integrations, and supported workflows.",
'<div className="maturity-hero">',
' <p className="maturity-kicker">release readiness - generated from taxonomy + QA evidence</p>',
' <p className="maturity-hero-title">A practical view of what is ready, what is proven, and what still needs work.</p>',
` <p>${scores.counts.active_surfaces} surfaces - ${scores.counts.category_scores} capability areas - deterministic coverage plus human-reviewed quality and completeness.</p>`,
' <p className="maturity-jump-links"><a href="#surface-explorer">Browse surfaces</a> / <a href="#qa-evidence-summary">Inspect QA evidence</a> / <a href="/maturity/taxonomy">Read the taxonomy</a></p>',
"</div>",
"",
`The current scorecard covers ${scores.counts.active_surfaces} surfaces and ${scores.counts.category_scores} capability areas.`,
"## What this page is for",
"",
"## Overall scores",
"Use this page to answer one question: which OpenClaw surfaces are credible choices for a release, and what evidence supports that judgment? Coverage comes from deterministic QA evidence; quality and completeness are maintained as reviewed maturity scores.",
"",
...markdownTable([
["Basis", "Coverage", "Quality", "Completeness"],
[
"Surface average",
scoreText(coverage.rollups.surface_average),
scoreText(scores.rollups.surface_average.quality),
scoreText(scores.rollups.surface_average.completeness),
],
[
"Category average",
scoreText(coverage.rollups.category_average),
scoreText(scores.rollups.category_average.quality),
scoreText(scores.rollups.category_average.completeness),
],
]),
"## At a glance",
"",
"- Coverage is derived from QA profile evidence.",
"- Quality measures reliability and operational confidence.",
"- Completeness measures how much of the expected user workflow is available.",
'<div className="maturity-summary-grid">',
...indentMarkdown(scoreSummary("Coverage", surfaceAverage, "QA profile evidence"), 2),
...indentMarkdown(
scoreSummary("Quality", qualityAverage, "Reliability and operator confidence"),
2,
),
...indentMarkdown(
scoreSummary("Completeness", completenessAverage, "Expected workflow coverage"),
2,
),
"</div>",
"",
'Coverage is deliberately evidence-led: an area does not become "ready" just because the implementation exists.',
"",
...renderScoreBands(),
];
const surfaceRows: RenderScalar[][] = [
[
"Surface",
"Family",
"Level",
"Coverage",
"Quality",
"Completeness",
"Long-term support",
"Areas",
],
];
for (const surface of surfaces) {
const scoreSurface = scoreSurfaces.get(surface.id);
const surfaceName = surface.name;
surfaceRows.push([
`[${markdownEscape(surfaceName)}](/maturity/taxonomy#${markdownSlug(surfaceName)})`,
markdownEscape(familyTitle(surface.family)),
markdownEscape(levelText(surface, levels)),
scoreText(coverage.surfaces.get(surface.id)),
scoreText(scoreSurface?.scores?.quality),
scoreText(scoreSurface?.scores?.completeness),
markdownEscape(ltsText(scoreSurface?.lts)),
surface.categories.length,
]);
}
lines.push(
"## Surface scorecard",
"## Surface explorer",
"",
...markdownTable(surfaceRows),
'<a id="surface-explorer" />',
"",
"Surfaces are ordered by maturity level, completeness, and quality. LTS support is shown alongside each row so release-ready options are easy to compare.",
"",
...renderSurfaceTabs({ coverage, levels, scoreSurfaces, surfaces }),
"",
...renderEvidenceSection(evidenceSummaries, surfaceNames),
);
@ -776,7 +1025,11 @@ function renderTaxonomy({
}: RenderInputs & { docsRouteIndex: DocsRouteIndex }): string {
const levels = qaMaturityTaxonomyLevelMap(taxonomy);
const scoreSurfaces = surfaceScoreMap(scores);
const surfaces = activeQaMaturityTaxonomySurfaces(taxonomy);
const surfaces = sortedMaturitySurfaces(
activeQaMaturityTaxonomySurfaces(taxonomy),
scoreSurfaces,
levels,
);
const lines = [
...frontmatter(
"Maturity taxonomy",
@ -784,50 +1037,64 @@ function renderTaxonomy({
),
"# Maturity taxonomy",
"",
"This page explains the product areas and capability groups behind the maturity scorecard.",
'<div className="maturity-hero maturity-hero-compact">',
' <p className="maturity-kicker">the model behind the scorecard</p>',
' <p className="maturity-hero-title">Surfaces &gt; categories &gt; capabilities &gt; evidence.</p>',
` <p>${surfaces.length} surfaces grouped into ${qaMaturityFamilyOrder(surfaces).length} families, with every category tied back to canonical docs and QA coverage IDs.</p>`,
' <p className="maturity-jump-links"><a href="#product-areas">Browse product areas</a> / <a href="#taxonomy-details">Open detailed taxonomy</a> / <a href="/maturity/scorecard">View scores</a></p>',
"</div>",
"",
"## How to read this page",
"",
"A surface is a product area such as Gateway runtime, Discord, or the macOS app. Each surface contains categories, and each category contains the capability-level checks that QA scenarios cover. Use the scorecard for release-level judgment; use this page to inspect the model underneath it.",
"",
"## Maturity levels",
"",
...markdownTable([
["Level", "Label", "Meaning", "Promotion bar"],
...taxonomy.levels.map((level) => [
yamlCode(level.code ?? level.id),
markdownEscape(level.label ?? level.id),
markdownEscape(level.meaning ?? ""),
markdownEscape(level.promotion_bar ?? ""),
]),
]),
'<div className="maturity-level-list">',
...taxonomy.levels.map(
(level) =>
` <div className="maturity-level-row"><span className="maturity-level-title">${maturityLevelPill(level.code ?? level.id, level.label ?? level.id)}</span><span>${markdownEscape(level.meaning ?? "")}</span><span className="maturity-level-promotion">Promotion: ${markdownEscape(level.promotion_bar ?? "")}</span></div>`,
),
"</div>",
"",
"## Product areas",
"",
'<a id="product-areas" />',
"",
];
for (const family of qaMaturityFamilyOrder(surfaces)) {
lines.push(`### ${familyTitle(family)} surfaces`, "");
const families = qaMaturityFamilyOrder(surfaces);
lines.push("<Tabs>");
for (const family of families) {
lines.push(` <Tab title="${markdownEscape(familyTitle(family))}">`, "");
for (const surface of surfaces.filter((candidate) => candidate.family === family)) {
const surfaceName = surface.name;
lines.push(`- [${markdownEscape(surfaceName)}](#${markdownSlug(surfaceName)})`);
const scoreSurface = scoreSurfaces.get(surface.id);
lines.push(
...indentMarkdown(
[
`<a className="maturity-surface-link" href="#${markdownSlug(surface.name)}">`,
` <span className="maturity-surface-title">${markdownEscape(surface.name)}</span>`,
` <span className="maturity-surface-meta">${maturityLevelPillFromText(levelText(surface, levels))}<span>${surface.categories.length} areas - ${scorePercent(scoreSurface?.scores?.completeness) ?? "-"}% complete</span></span>`,
"</a>",
],
4,
),
);
lines.push("");
}
lines.push("");
lines.push(" </Tab>");
}
lines.push("</Tabs>", "", "## Details", "", '<a id="taxonomy-details" />', "");
lines.push("## Details", "");
for (const family of qaMaturityFamilyOrder(surfaces)) {
lines.push(`### ${familyTitle(family)}`, "");
for (const family of families) {
lines.push(`### ${familyTitle(family)}`, "", "<AccordionGroup>");
for (const surface of surfaces.filter((candidate) => candidate.family === family)) {
const surfaceName = surface.name;
const scoreSurface = scoreSurfaces.get(surface.id);
const categoryScores = categoryScoreMap(scoreSurface);
const categoryRows: RenderScalar[][] = [
[
"Area",
"Capabilities",
"Docs",
"Coverage",
"Quality",
"Completeness",
"Long-term support",
],
const categoryLines = [
'<div className="maturity-category-list">',
' <div className="maturity-category-row maturity-category-row-header"><span>Area</span><span>Coverage</span><span>Quality</span><span>Completeness</span><span>Docs</span></div>',
];
for (const category of surface.categories) {
const docs = (category.docs ?? [])
@ -838,26 +1105,39 @@ function renderTaxonomy({
const coverageScore = coverage.categories.get(
qaMaturityCoverageCategoryKey(surface.id, category.name),
);
categoryRows.push([
markdownEscape(category.name),
category.features.length,
docs,
scoreText(coverageScore),
scoreText(scoreCategory?.quality),
scoreText(scoreCategory?.completeness),
markdownEscape(scoreCategory?.lts?.supported ? "Yes" : "No"),
]);
categoryLines.push(
' <div className="maturity-category-row">',
' <div className="maturity-category-area">',
` <span className="maturity-category-title">${markdownEscape(category.name)}</span>`,
` <span>${category.features.length} capabilities${scoreCategory?.lts?.supported ? " / LTS-supported" : ""}</span>`,
" </div>",
` <div>${scoreMeter(coverageScore)}</div>`,
` <div>${scoreMeter(scoreCategory?.quality)}</div>`,
` <div>${scoreMeter(scoreCategory?.completeness)}</div>`,
` <div className="maturity-category-docs">${docs || "No linked docs"}</div>`,
" </div>",
);
}
categoryLines.push("</div>");
lines.push(
`#### ${surfaceName}`,
` <Accordion title="${markdownEscape(surfaceName)} - ${markdownEscape(levelText(surface, levels))} - ${surface.categories.length} areas">`,
` <a id="${markdownSlug(surfaceName)}" />`,
"",
`- Level: ${markdownEscape(levelText(surface, levels))}`,
`- Rationale: ${surface.rationale ?? ""}`,
` ${markdownEscape(surface.rationale ?? "")}`,
"",
...markdownTable(categoryRows),
...indentMarkdown(
[
`<div className="maturity-surface-rollup"><span>Coverage ${scoreLabel(coverage.surfaces.get(surface.id))}</span><span>Quality ${scoreLabel(scoreSurface?.scores?.quality)}</span><span>Completeness ${scoreLabel(scoreSurface?.scores?.completeness)}</span><span>${maturityLtsBadge(scoreSurface?.lts)}</span></div>`,
"",
...categoryLines,
],
4,
),
" </Accordion>",
);
lines.push("");
}
lines.push("</AccordionGroup>", "");
}
return `${lines.join("\n").trimEnd()}\n`;
}