@dailephd/my-dev-kit-lab 0.9.2 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -1
- package/README.md +7 -5
- package/dist/src/agents/adapters/codexAdapter.js +4 -1
- package/dist/src/cli/help.js +38 -2
- package/dist/src/cli/runLabCli.js +9 -1
- package/dist/src/commands/buildGalleryCommand.js +16 -3
- package/dist/src/commands/runComparePromptsCommand.js +109 -0
- package/dist/src/commands/runExperimentRunCommand.js +47 -2
- package/dist/src/core/processTree.js +41 -17
- package/dist/src/core/runMeasuredCommand.js +14 -8
- package/dist/src/experiments/plugins/agentSuccessRate/campaignCheckpointStore.js +23 -1
- package/dist/src/experiments/plugins/agentSuccessRate/campaignExecution.js +7 -2
- package/dist/src/experiments/plugins/agentSuccessRate/campaignIdentity.js +29 -4
- package/dist/src/experiments/plugins/agentSuccessRate/campaignResumableExecution.js +1 -1
- package/dist/src/experiments/plugins/agentSuccessRate/config.js +47 -2
- package/dist/src/experiments/plugins/agentSuccessRate/plugin.js +4 -2
- package/dist/src/experiments/plugins/agentSuccessRate/promptContract.js +121 -0
- package/dist/src/experiments/plugins/agentSuccessRate/realAgentExecution.js +32 -2
- package/dist/src/experiments/plugins/agentSuccessRate/realAgentPrompt.js +105 -11
- package/dist/src/experiments/plugins/agentSuccessRate/strictDiffResponse.js +312 -0
- package/dist/src/gallery/galleryPathSafety.js +111 -0
- package/dist/src/gallery/readGallerySources.js +542 -0
- package/dist/src/gallery/writeGalleryManifest.js +57 -3
- package/dist/src/report/experiments/buildPluginExperimentReport.js +4 -16
- package/dist/src/report/experiments/htmlSafety.js +32 -0
- package/dist/src/report/experiments/metricEvidence.js +389 -0
- package/dist/src/report/experiments/metricGlossary.js +227 -0
- package/dist/src/report/experiments/metricGlossaryCatalog.js +199 -0
- package/dist/src/report/experiments/promptTemplateComparisonEvidence.js +377 -0
- package/dist/src/report/experiments/promptTemplateComparisonModel.js +436 -0
- package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +158 -64
- package/dist/src/report/experiments/renderPluginExperimentReportText.js +22 -677
- package/dist/src/report/experiments/renderPromptTemplateComparison.js +192 -0
- package/dist/src/report/experiments/renderSpecializedTextSections.js +633 -0
- package/dist/src/report/experiments/reportSectionRegistry.js +79 -0
- package/dist/src/report/experiments/specializedReportSections.js +142 -0
- package/dist/src/report/experiments/writePluginExperimentReports.js +4 -2
- package/dist/src/securityValidation/attackScenarios/scenarios/secretLeakageScenario.js +146 -133
- package/dist/src/securityValidation/attackScenarios/scenarios/subprocessInjectionScenario.js +54 -36
- package/dist/src/securityValidation/commandRunner.js +13 -6
- package/docs/METRICS.md +11 -1
- package/package.json +1 -1
|
@@ -1,13 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
import { renderRetrievalQueryStrategyComparisonHtml } from "./renderRetrievalQueryStrategyComparisonHtml.js";
|
|
7
|
-
import { renderContextPackGenerationHtml } from "./renderContextPackGenerationHtml.js";
|
|
8
|
-
import { renderAgentSuccessRateHtml } from "./renderAgentSuccessRateHtml.js";
|
|
9
|
-
export function renderPluginExperimentReportHtml(report) {
|
|
1
|
+
import { getSpecializedReportSectionRegistry } from "./specializedReportSections.js";
|
|
2
|
+
import { escapeHtml, safeInternalHref, sectionAnchorId } from "./htmlSafety.js";
|
|
3
|
+
import { buildMetricEvidenceCaveats, buildMetricEvidenceViews } from "./metricEvidence.js";
|
|
4
|
+
import { listMetricDescriptors } from "./metricGlossary.js";
|
|
5
|
+
export function renderPluginExperimentReportHtml(report, evidence) {
|
|
10
6
|
const redactedTarget = report.target.privacyProjection === "external-local-redacted";
|
|
7
|
+
const views = buildMetricEvidenceViews(report, evidence);
|
|
8
|
+
const caveats = buildMetricEvidenceCaveats(report, views);
|
|
9
|
+
const glossary = glossaryDescriptorsFor(views);
|
|
10
|
+
const family = renderSpecializedSections(report);
|
|
11
|
+
const nav = [
|
|
12
|
+
{ anchor: "section-plugin-target", label: "Plugin And Target" },
|
|
13
|
+
{ anchor: "section-interpretation", label: "Interpretation" },
|
|
14
|
+
{ anchor: "section-variants", label: "Variants" },
|
|
15
|
+
{ anchor: "section-cases", label: "Cases" },
|
|
16
|
+
{ anchor: "section-metrics", label: "Metrics" },
|
|
17
|
+
{ anchor: "section-evidence-caveats", label: "Evidence Caveats" },
|
|
18
|
+
...family.nav,
|
|
19
|
+
{ anchor: "section-findings", label: "Findings" },
|
|
20
|
+
{ anchor: "section-artifacts", label: "Artifacts" },
|
|
21
|
+
...(glossary.length > 0 ? [{ anchor: "section-metric-glossary", label: "Metric Glossary" }] : []),
|
|
22
|
+
];
|
|
11
23
|
return `<!DOCTYPE html>
|
|
12
24
|
<html lang="en">
|
|
13
25
|
<head>
|
|
@@ -22,11 +34,19 @@ export function renderPluginExperimentReportHtml(report) {
|
|
|
22
34
|
h2 { margin: 0 0 12px; font-size: 22px; }
|
|
23
35
|
h3 { margin: 14px 0 8px; font-size: 17px; }
|
|
24
36
|
p, li { line-height: 1.45; }
|
|
25
|
-
|
|
26
|
-
|
|
37
|
+
a { color: #0b4f9c; }
|
|
38
|
+
a:focus-visible, .table-wrap:focus-visible { outline: 3px solid #1a5fb4; outline-offset: 2px; }
|
|
39
|
+
.skip-link { position: absolute; left: -9999px; top: 0; background: #fff; padding: 8px 12px; z-index: 10; }
|
|
40
|
+
.skip-link:focus { left: 8px; top: 8px; }
|
|
41
|
+
section, .hero, .report-nav { background: #fff; border: 1px solid #d7e0ea; border-radius: 8px; padding: 18px; margin-bottom: 16px; }
|
|
42
|
+
.report-nav h2 { font-size: 17px; }
|
|
43
|
+
.report-nav ul { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px 16px; }
|
|
44
|
+
.table-wrap { overflow-x: auto; margin: 8px 0 16px; }
|
|
45
|
+
table { width: 100%; border-collapse: collapse; background: #fff; }
|
|
46
|
+
caption { text-align: left; font-weight: 700; padding: 4px 0; }
|
|
27
47
|
th, td { border: 1px solid #d9e1ea; padding: 8px 10px; text-align: left; vertical-align: top; }
|
|
28
48
|
th { background: #edf2f7; }
|
|
29
|
-
code { font-family: Consolas, "Courier New", monospace; }
|
|
49
|
+
code { font-family: Consolas, "Courier New", monospace; overflow-wrap: anywhere; }
|
|
30
50
|
.muted { color: #5b6b7b; }
|
|
31
51
|
.grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
32
52
|
.card { border: 1px solid #d9e1ea; border-radius: 6px; padding: 12px; background: #fbfcfe; }
|
|
@@ -34,11 +54,21 @@ export function renderPluginExperimentReportHtml(report) {
|
|
|
34
54
|
.status-completed { background: #dcf4e4; color: #0c6b30; }
|
|
35
55
|
.status-partial, .status-skipped { background: #fff0ce; color: #785400; }
|
|
36
56
|
.status-failed { background: #fde2e2; color: #8a1f1f; }
|
|
57
|
+
.availability-available { background: #dcf4e4; color: #0c6b30; }
|
|
58
|
+
.availability-unavailable { background: #fde2e2; color: #8a1f1f; border: 1px solid #8a1f1f; }
|
|
59
|
+
.availability-not-applicable { background: #e8eef7; color: #26405e; border: 1px dashed #26405e; }
|
|
60
|
+
.caveat-warning { border-left: 4px solid #a06a00; }
|
|
61
|
+
.caveat-info { border-left: 4px solid #3c6ea8; }
|
|
62
|
+
.caveats { list-style: none; margin: 0; padding: 0; }
|
|
63
|
+
.caveats li { padding: 6px 10px; margin-bottom: 8px; background: #fbfcfe; }
|
|
64
|
+
dt { font-weight: 700; margin-top: 12px; }
|
|
65
|
+
dd { margin: 4px 0 0 16px; }
|
|
37
66
|
@media (max-width: 860px) { main { padding: 14px; } .grid { grid-template-columns: 1fr; } table { font-size: 13px; } }
|
|
38
67
|
</style>
|
|
39
68
|
</head>
|
|
40
69
|
<body>
|
|
41
|
-
<main>
|
|
70
|
+
<a class="skip-link" href="#main-content">Skip to report content</a>
|
|
71
|
+
<main id="main-content">
|
|
42
72
|
<header class="hero">
|
|
43
73
|
<h1>${escapeHtml(report.plugin.name)}</h1>
|
|
44
74
|
<p>${escapeHtml(report.plugin.description)}</p>
|
|
@@ -46,9 +76,14 @@ export function renderPluginExperimentReportHtml(report) {
|
|
|
46
76
|
<p class="muted">Generated ${escapeHtml(report.metadata.generatedAt)} for run <code>${escapeHtml(report.metadata.runId)}</code></p>
|
|
47
77
|
</header>
|
|
48
78
|
|
|
49
|
-
<
|
|
50
|
-
<h2>
|
|
51
|
-
|
|
79
|
+
<nav class="report-nav" aria-labelledby="report-nav-title">
|
|
80
|
+
<h2 id="report-nav-title">Report sections</h2>
|
|
81
|
+
<ul>${nav.map(renderNavLink).join("")}</ul>
|
|
82
|
+
</nav>
|
|
83
|
+
|
|
84
|
+
<section id="section-plugin-target" aria-labelledby="section-plugin-target-title">
|
|
85
|
+
<h2 id="section-plugin-target-title">Plugin And Target</h2>
|
|
86
|
+
${table("Plugin and target", ["Field", "Value"], [
|
|
52
87
|
["Plugin ID", report.plugin.id],
|
|
53
88
|
["Plugin schema", report.plugin.schemaVersion],
|
|
54
89
|
["Mode", report.target.mode],
|
|
@@ -62,15 +97,15 @@ export function renderPluginExperimentReportHtml(report) {
|
|
|
62
97
|
])}
|
|
63
98
|
</section>
|
|
64
99
|
|
|
65
|
-
<section>
|
|
66
|
-
<h2>Interpretation</h2>
|
|
100
|
+
<section id="section-interpretation" aria-labelledby="section-interpretation-title">
|
|
101
|
+
<h2 id="section-interpretation-title">Interpretation</h2>
|
|
67
102
|
<p>${escapeHtml(report.interpretation.summary)}</p>
|
|
68
103
|
<p><strong>Recommended next step:</strong> ${escapeHtml(report.interpretation.recommendedNextStep)}</p>
|
|
69
104
|
</section>
|
|
70
105
|
|
|
71
|
-
<section>
|
|
72
|
-
<h2>Variants</h2>
|
|
73
|
-
${table(["Variant", "Completed", "Partial", "Failed", "Skipped"], report.variants.map((variant) => [
|
|
106
|
+
<section id="section-variants" aria-labelledby="section-variants-title">
|
|
107
|
+
<h2 id="section-variants-title">Variants</h2>
|
|
108
|
+
${table("Variant outcome counts", ["Variant", "Completed", "Partial", "Failed", "Skipped"], report.variants.map((variant) => [
|
|
74
109
|
`${variant.name} (${variant.id})`,
|
|
75
110
|
String(variant.completedOutcomes),
|
|
76
111
|
String(variant.partialOutcomes),
|
|
@@ -79,9 +114,9 @@ export function renderPluginExperimentReportHtml(report) {
|
|
|
79
114
|
]))}
|
|
80
115
|
</section>
|
|
81
116
|
|
|
82
|
-
<section>
|
|
83
|
-
<h2>Cases</h2>
|
|
84
|
-
${table(["Case", "Status", "Completed", "Partial", "Failed", "Skipped"], report.cases.map((experimentCase) => [
|
|
117
|
+
<section id="section-cases" aria-labelledby="section-cases-title">
|
|
118
|
+
<h2 id="section-cases-title">Cases</h2>
|
|
119
|
+
${table("Case outcome counts", ["Case", "Status", "Completed", "Partial", "Failed", "Skipped"], report.cases.map((experimentCase) => [
|
|
85
120
|
`${experimentCase.name} (${experimentCase.id})`,
|
|
86
121
|
experimentCase.status,
|
|
87
122
|
String(experimentCase.completedOutcomes),
|
|
@@ -91,36 +126,22 @@ export function renderPluginExperimentReportHtml(report) {
|
|
|
91
126
|
]))}
|
|
92
127
|
</section>
|
|
93
128
|
|
|
94
|
-
<section>
|
|
95
|
-
<h2>Metrics</h2>
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
String(metric.value ?? ""),
|
|
99
|
-
metric.unit ?? "",
|
|
100
|
-
metric.variantId ?? "",
|
|
101
|
-
metric.caseId ?? "",
|
|
102
|
-
]))}
|
|
129
|
+
<section id="section-metrics" aria-labelledby="section-metrics-title">
|
|
130
|
+
<h2 id="section-metrics-title">Metrics</h2>
|
|
131
|
+
<p class="muted">Availability, provenance and reliability are taken from recorded evidence only. <em>Unavailable</em> and <em>not applicable</em> are never zero; <em>unknown</em> means the report does not record that information.</p>
|
|
132
|
+
${renderMetricsTable(views)}
|
|
103
133
|
</section>
|
|
104
134
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
${renderIncrementalChangeStalenessHtml(report.incrementalChangeStaleness)}
|
|
110
|
-
|
|
111
|
-
${renderContextWindowScalingHtml(report.contextWindowScaling)}
|
|
112
|
-
|
|
113
|
-
${renderRetrievalPrecisionRecallHtml(report.retrievalPrecisionRecall ?? null)}
|
|
114
|
-
|
|
115
|
-
${renderRetrievalQueryStrategyComparisonHtml(report.retrievalQueryStrategyComparison ?? null)}
|
|
116
|
-
|
|
117
|
-
${renderContextPackGenerationHtml(report.contextPackGeneration ?? null)}
|
|
135
|
+
<section id="section-evidence-caveats" aria-labelledby="section-evidence-caveats-title">
|
|
136
|
+
<h2 id="section-evidence-caveats-title">Evidence Caveats</h2>
|
|
137
|
+
${renderCaveats(caveats)}
|
|
138
|
+
</section>
|
|
118
139
|
|
|
119
|
-
${
|
|
140
|
+
${family.html}
|
|
120
141
|
|
|
121
|
-
<section>
|
|
122
|
-
<h2>Warnings, Skips, And Failures</h2>
|
|
123
|
-
${report.findings.length ? table(["Severity", "Code", "Message", "Variant", "Case"], report.findings.map((finding) => [
|
|
142
|
+
<section id="section-findings" aria-labelledby="section-findings-title">
|
|
143
|
+
<h2 id="section-findings-title">Warnings, Skips, And Failures</h2>
|
|
144
|
+
${report.findings.length ? table("Warnings, skips, and failures", ["Severity", "Code", "Message", "Variant", "Case"], report.findings.map((finding) => [
|
|
124
145
|
finding.severity,
|
|
125
146
|
finding.code,
|
|
126
147
|
finding.message,
|
|
@@ -129,9 +150,9 @@ export function renderPluginExperimentReportHtml(report) {
|
|
|
129
150
|
])) : "<p>No warnings, skips, or failures.</p>"}
|
|
130
151
|
</section>
|
|
131
152
|
|
|
132
|
-
<section>
|
|
133
|
-
<h2>Artifacts</h2>
|
|
134
|
-
${table(["Artifact", "Kind", "Path", "Variant", "Case"], report.artifacts.map((artifact) => [
|
|
153
|
+
<section id="section-artifacts" aria-labelledby="section-artifacts-title">
|
|
154
|
+
<h2 id="section-artifacts-title">Artifacts</h2>
|
|
155
|
+
${table("Artifacts", ["Artifact", "Kind", "Path", "Variant", "Case"], report.artifacts.map((artifact) => [
|
|
135
156
|
`${artifact.label} (${artifact.id})`,
|
|
136
157
|
artifact.kind,
|
|
137
158
|
artifact.path ?? "",
|
|
@@ -139,20 +160,93 @@ export function renderPluginExperimentReportHtml(report) {
|
|
|
139
160
|
artifact.caseId ?? "",
|
|
140
161
|
]))}
|
|
141
162
|
</section>
|
|
163
|
+
${renderGlossary(glossary)}
|
|
142
164
|
</main>
|
|
143
165
|
</body>
|
|
144
166
|
</html>`;
|
|
145
167
|
}
|
|
146
|
-
function
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
168
|
+
function renderNavLink(entry) {
|
|
169
|
+
const href = safeInternalHref(`#${entry.anchor}`);
|
|
170
|
+
return href === null ? "" : `<li><a href="${escapeHtml(href)}">${escapeHtml(entry.label)}</a></li>`;
|
|
171
|
+
}
|
|
172
|
+
// Specialized family sections come from the shared registry, in its deterministic order. Each non-empty
|
|
173
|
+
// section is wrapped in an anchored container so the compact navigation can target it.
|
|
174
|
+
function renderSpecializedSections(report) {
|
|
175
|
+
const nav = [];
|
|
176
|
+
const parts = [];
|
|
177
|
+
const used = new Set();
|
|
178
|
+
for (const descriptor of getSpecializedReportSectionRegistry().descriptors) {
|
|
179
|
+
const rendered = descriptor.renderHtml(report[descriptor.projectionKey] ?? null);
|
|
180
|
+
if (rendered.trim() === "")
|
|
181
|
+
continue;
|
|
182
|
+
let anchor = sectionAnchorId(descriptor.id);
|
|
183
|
+
for (let suffix = 2; used.has(anchor); suffix += 1)
|
|
184
|
+
anchor = `${sectionAnchorId(descriptor.id)}-${suffix}`;
|
|
185
|
+
used.add(anchor);
|
|
186
|
+
nav.push({ anchor, label: descriptor.text.title ?? descriptor.id });
|
|
187
|
+
parts.push(`<div id="${anchor}" class="family-section">${rendered}</div>`);
|
|
188
|
+
}
|
|
189
|
+
return { html: parts.join("\n\n "), nav };
|
|
190
|
+
}
|
|
191
|
+
function glossaryDescriptorsFor(views) {
|
|
192
|
+
const anchors = new Set(views.map((view) => view.glossaryAnchor).filter((anchor) => anchor !== null));
|
|
193
|
+
return listMetricDescriptors().filter((descriptor) => anchors.has(descriptor.anchor));
|
|
194
|
+
}
|
|
195
|
+
function renderMetricsTable(views) {
|
|
196
|
+
const headers = ["Metric", "Value", "Availability", "Unit", "Provenance", "Reliability", "Evidence note", "Variant", "Case"];
|
|
197
|
+
const rows = views.map((view) => {
|
|
198
|
+
const href = safeInternalHref(view.glossaryAnchor === null ? null : `#${view.glossaryAnchor}`);
|
|
199
|
+
const name = href === null
|
|
200
|
+
? `${escapeHtml(view.displayName)} <code>${escapeHtml(view.metricId)}</code> <span class="muted">(no glossary entry)</span>`
|
|
201
|
+
: `<a href="${escapeHtml(href)}">${escapeHtml(view.displayName)}</a> <code>${escapeHtml(view.metricId)}</code>`;
|
|
202
|
+
const value = view.availability === "available" ? escapeHtml(view.value === null ? "" : String(view.value)) : escapeHtml(view.availability);
|
|
203
|
+
const note = view.availability === "available" ? "" : view.reason ?? "no reason recorded";
|
|
204
|
+
return [
|
|
205
|
+
name,
|
|
206
|
+
value,
|
|
207
|
+
`<span class="badge availability-${escapeHtml(view.availability)}">${escapeHtml(view.availability.replace("-", " "))}</span>`,
|
|
208
|
+
escapeHtml(view.unit ?? ""),
|
|
209
|
+
escapeHtml(view.provenance),
|
|
210
|
+
escapeHtml(view.reliability),
|
|
211
|
+
escapeHtml(note),
|
|
212
|
+
escapeHtml(view.variantId ?? ""),
|
|
213
|
+
escapeHtml(view.caseId ?? ""),
|
|
214
|
+
];
|
|
215
|
+
});
|
|
216
|
+
return wrapTable("Metrics with availability, provenance and reliability", headers, rows);
|
|
217
|
+
}
|
|
218
|
+
function renderCaveats(caveats) {
|
|
219
|
+
if (caveats.length === 0)
|
|
220
|
+
return "<p>No metric evidence caveats were derived from the recorded evidence.</p>";
|
|
221
|
+
return `<ul class="caveats">${caveats
|
|
222
|
+
.map((caveat) => `<li class="caveat-${escapeHtml(caveat.severity)}"><span class="badge">${escapeHtml(caveat.severity)}</span> ${escapeHtml(caveat.text)}</li>`)
|
|
223
|
+
.join("")}</ul>`;
|
|
224
|
+
}
|
|
225
|
+
function renderGlossary(descriptors) {
|
|
226
|
+
if (descriptors.length === 0)
|
|
227
|
+
return "";
|
|
228
|
+
return `
|
|
229
|
+
<section id="section-metric-glossary" aria-labelledby="section-metric-glossary-title">
|
|
230
|
+
<h2 id="section-metric-glossary-title">Metric Glossary</h2>
|
|
231
|
+
<p class="muted">Definitions are condensed from docs/METRICS.md, the authoritative metric reference. They are descriptive and are not combined into a single figure.</p>
|
|
232
|
+
<dl>${descriptors
|
|
233
|
+
.map((descriptor) => {
|
|
234
|
+
const caveats = descriptor.caveats.length ? `<br /><strong>Caveats:</strong> ${descriptor.caveats.map((c) => escapeHtml(c)).join(" ")}` : "";
|
|
235
|
+
return `
|
|
236
|
+
<dt id="${escapeHtml(descriptor.anchor)}">${escapeHtml(descriptor.displayName)} <code>${escapeHtml(descriptor.metricId)}</code></dt>
|
|
237
|
+
<dd>${escapeHtml(descriptor.definition)}<br /><strong>Unit:</strong> ${escapeHtml(descriptor.unit ?? "not specified")}<br /><strong>Interpretation:</strong> ${escapeHtml(descriptor.interpretation)}${caveats}<br /><span class="muted">Plugin <code>${escapeHtml(descriptor.pluginId)}</code> · docs/METRICS.md: ${escapeHtml(descriptor.docsSection)}</span></dd>`;
|
|
238
|
+
})
|
|
239
|
+
.join("")}
|
|
240
|
+
</dl>
|
|
241
|
+
</section>
|
|
242
|
+
`;
|
|
243
|
+
}
|
|
244
|
+
function table(caption, headers, rows) {
|
|
245
|
+
return wrapTable(caption, headers, rows.map((row) => row.map((cell) => escapeHtml(cell))));
|
|
150
246
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
.
|
|
155
|
-
.
|
|
156
|
-
.replaceAll('"', """)
|
|
157
|
-
.replaceAll("'", "'");
|
|
247
|
+
/** Cells are already safe HTML. */
|
|
248
|
+
function wrapTable(caption, headers, rows) {
|
|
249
|
+
return `<div class="table-wrap" role="region" aria-label="${escapeHtml(caption)}" tabindex="0"><table><caption>${escapeHtml(caption)}</caption><thead><tr>${headers
|
|
250
|
+
.map((header) => `<th scope="col">${escapeHtml(header)}</th>`)
|
|
251
|
+
.join("")}</tr></thead><tbody>${rows.map((row) => `<tr>${row.map((cell) => `<td>${cell}</td>`).join("")}</tr>`).join("")}</tbody></table></div>`;
|
|
158
252
|
}
|