@hublo/sentinel 1.5.0-alpha.11 → 1.5.0-alpha.13
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/dist/bin/sentinel.js
CHANGED
|
@@ -4865,6 +4865,14 @@ function helpFor(key) {
|
|
|
4865
4865
|
|
|
4866
4866
|
// src/roles/static-analysis/report.ts
|
|
4867
4867
|
var SCRIPTS = { json: "static-analysis:json", html: "static-analysis:html" };
|
|
4868
|
+
function secondSide(finding) {
|
|
4869
|
+
if (!finding.other) return "";
|
|
4870
|
+
const file = (at) => at.split(":")[0] ?? at;
|
|
4871
|
+
const sameFile = file(finding.other) === file(finding.path);
|
|
4872
|
+
const label = sameFile ? "and line" : "and";
|
|
4873
|
+
const shownSide = sameFile ? finding.other.split(":")[1] ?? finding.other : finding.other;
|
|
4874
|
+
return ` <span class="unit">${label}</span> <code>${escape(shownSide)}</code>`;
|
|
4875
|
+
}
|
|
4868
4876
|
var HTML_GROUP_LIMIT = 50;
|
|
4869
4877
|
function shown(row) {
|
|
4870
4878
|
return row.unit === "%" && typeof row.value === "number" ? row.value.toFixed(2) : String(row.value);
|
|
@@ -4926,6 +4934,9 @@ function renderJson(report) {
|
|
|
4926
4934
|
return `${JSON.stringify({ ...report, rows }, null, 2)}
|
|
4927
4935
|
`;
|
|
4928
4936
|
}
|
|
4937
|
+
function prose(text) {
|
|
4938
|
+
return escape(text).replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
4939
|
+
}
|
|
4929
4940
|
function escape(text) {
|
|
4930
4941
|
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
4931
4942
|
}
|
|
@@ -4935,14 +4946,21 @@ function renderHtml(report) {
|
|
|
4935
4946
|
return ` <tr>
|
|
4936
4947
|
<th scope="row">${escape(help?.label ?? row.metric)}</th>
|
|
4937
4948
|
<td class="num">${escape(shown(row))} <span class="unit">${escape(row.unit)}</span></td>
|
|
4938
|
-
<td class="caveat">${escape(help?.caveat ?? "")}</td>
|
|
4939
4949
|
</tr>`;
|
|
4940
4950
|
}).join("\n");
|
|
4951
|
+
const reserves = report.rows.map((row) => ({ row, help: helpFor(row.metric) })).filter((entry) => entry.help).map(
|
|
4952
|
+
({ row, help }) => ` <details>
|
|
4953
|
+
<summary>${escape(help?.label ?? row.metric)}<span class="what">${prose(help?.what ?? "")}</span></summary>
|
|
4954
|
+
<p class="read"><b>How to read it.</b> ${prose(help?.read ?? "")}</p>
|
|
4955
|
+
<p><b>What it does not say.</b> ${prose(help?.caveat ?? "")}</p>
|
|
4956
|
+
<p class="source">Source: ${prose(help?.source ?? "")}</p>
|
|
4957
|
+
</details>`
|
|
4958
|
+
).join("\n");
|
|
4941
4959
|
const settings = report.settings.map(
|
|
4942
4960
|
(setting) => ` <tr>
|
|
4943
4961
|
<th scope="row"><code>${escape(setting.key)}</code></th>
|
|
4944
4962
|
<td class="num">${escape(setting.value)}</td>
|
|
4945
|
-
<td>${setting.moved ? '<span class="moved">overridden
|
|
4963
|
+
<td>${setting.moved ? '<span class="moved">overridden here</span>' : ""}</td>
|
|
4946
4964
|
</tr>`
|
|
4947
4965
|
).join("\n");
|
|
4948
4966
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -4958,22 +4976,25 @@ function renderHtml(report) {
|
|
|
4958
4976
|
const rest = sorted.length - shown2.length;
|
|
4959
4977
|
const rows2 = shown2.map(
|
|
4960
4978
|
(finding) => ` <tr>
|
|
4961
|
-
<td><code>${escape(finding.path)}</code>${
|
|
4979
|
+
<td><code>${escape(finding.path)}</code>${secondSide(finding)}</td>
|
|
4962
4980
|
<td>${finding.name ? `<code>${escape(finding.name)}</code>` : ""}</td>
|
|
4963
4981
|
<td class="num">${finding.lines ?? ""}</td>
|
|
4964
4982
|
<td class="num">${finding.confidence ?? ""}</td>
|
|
4965
4983
|
</tr>`
|
|
4966
4984
|
).join("\n");
|
|
4967
|
-
return `
|
|
4968
|
-
<
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4985
|
+
return `
|
|
4986
|
+
<h2>${escape(label)} · where to look</h2>
|
|
4987
|
+
<section>
|
|
4988
|
+
<table class="places">
|
|
4989
|
+
<thead>
|
|
4990
|
+
<tr><th scope="col">File</th><th scope="col">Name</th><th scope="col">Lines</th><th scope="col">Confidence</th></tr>
|
|
4991
|
+
</thead>
|
|
4992
|
+
<tbody>
|
|
4973
4993
|
${rows2}
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
${rest > 0 ? `
|
|
4994
|
+
</tbody>
|
|
4995
|
+
</table>
|
|
4996
|
+
${rest > 0 ? ` <p class="sub">The ${HTML_GROUP_LIMIT} longest are listed; ${rest} more are in <code>${SCRIPTS.json}</code>.</p>` : ""}
|
|
4997
|
+
</section>`;
|
|
4977
4998
|
}).join("\n");
|
|
4978
4999
|
return `<!doctype html>
|
|
4979
5000
|
<html lang="en">
|
|
@@ -4981,49 +5002,165 @@ ${rest > 0 ? ` <p class="sub">The ${HTML_GROUP_LIMIT} longest are listed; ${res
|
|
|
4981
5002
|
<meta charset="utf-8">
|
|
4982
5003
|
<title>Static analysis: ${escape(report.module)}</title>
|
|
4983
5004
|
<style>
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
5005
|
+
/*
|
|
5006
|
+
* The Hublo palette, token by token, read from "libs/front/theme/src/branding/definitions/hublo"
|
|
5007
|
+
* rather than approximated: "commonColors.ts" holds the values and "colorTokens.ts" the semantic
|
|
5008
|
+
* names used in the comments below.
|
|
5009
|
+
*
|
|
5010
|
+
* \u26D4 One theme, and that is a decision rather than an omission: the charter has no dark palette,
|
|
5011
|
+
* zero occurrences of "prefers-color-scheme" in the whole theme package, so a dark variant here
|
|
5012
|
+
* would be charter this page invented. Every colour is painted explicitly instead, so the page
|
|
5013
|
+
* holds on whatever ground opens it.
|
|
5014
|
+
*/
|
|
5015
|
+
:root {
|
|
5016
|
+
color-scheme: light;
|
|
5017
|
+
--page: #f5f8fd; /* neutral5, background.page-header */
|
|
5018
|
+
--surface: #ffffff; /* neutral0, background.surface */
|
|
5019
|
+
--ink: #131415; /* neutral35, text.primary */
|
|
5020
|
+
--ink-soft: #5e6972; /* neutral30, text.secondary */
|
|
5021
|
+
--label: #72808e; /* neutral25, text.label-default */
|
|
5022
|
+
--rule: #ced7e0; /* neutral15, border.page-header */
|
|
5023
|
+
--brand: #3a4392; /* blue15, text.brand */
|
|
5024
|
+
--brand-bg: #e5ecff; /* blue0, background.brand */
|
|
5025
|
+
--moved: #c83c3c; /* red15, text.destructive */
|
|
5026
|
+
}
|
|
5027
|
+
/*
|
|
5028
|
+
* The brand faces first, then Inter, then the system. They are loaded at runtime from a branding
|
|
5029
|
+
* artefact in the applications, so they cannot be embedded here without an external request, and
|
|
5030
|
+
* an external request is the one thing this page must not make. A viewer who has them sees them.
|
|
5031
|
+
*/
|
|
5032
|
+
body {
|
|
5033
|
+
margin: 0;
|
|
5034
|
+
padding: 2.5rem 1rem 4rem;
|
|
5035
|
+
background: var(--page);
|
|
5036
|
+
color: var(--ink);
|
|
5037
|
+
font: 15px/1.6 'Brand Sans', Inter, ui-sans-serif, system-ui, sans-serif;
|
|
5038
|
+
}
|
|
5039
|
+
main { max-width: 64rem; margin: 0 auto; }
|
|
5040
|
+
header { border-left: 3px solid var(--brand); padding-left: 0.9rem; margin-bottom: 2rem; }
|
|
5041
|
+
h1 { font-size: 1.3rem; font-weight: 600; margin: 0 0 0.2rem; letter-spacing: -0.01em; }
|
|
5042
|
+
h2 {
|
|
5043
|
+
font-size: 0.74rem; text-transform: uppercase; letter-spacing: 0.08em;
|
|
5044
|
+
color: var(--label); margin: 2.5rem 0 0.6rem; font-weight: 600;
|
|
5045
|
+
}
|
|
5046
|
+
p.sub { color: var(--ink-soft); margin: 0; font-size: 0.9rem; }
|
|
5047
|
+
section {
|
|
5048
|
+
background: var(--surface); border: 1px solid var(--rule); border-radius: 10px;
|
|
5049
|
+
padding: 0.25rem 1.1rem; overflow-x: auto;
|
|
5050
|
+
}
|
|
4994
5051
|
table { width: 100%; border-collapse: collapse; }
|
|
4995
|
-
th, td { text-align: left; padding: 0.
|
|
4996
|
-
|
|
4997
|
-
thead th { font-size: 0.
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5052
|
+
th, td { text-align: left; padding: 0.7rem 0.6rem; border-bottom: 1px solid var(--rule); vertical-align: top; }
|
|
5053
|
+
tr:last-child th, tr:last-child td { border-bottom: 0; }
|
|
5054
|
+
thead th { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.07em; color: var(--label); font-weight: 600; }
|
|
5055
|
+
tbody th { font-weight: 500; white-space: nowrap; }
|
|
5056
|
+
td.num { font-variant-numeric: tabular-nums; white-space: nowrap; font-weight: 500; }
|
|
5057
|
+
/*
|
|
5058
|
+
* Which column absorbs the slack, per table. One global rule on the shared class got this
|
|
5059
|
+
* wrong in all three at once: the figures read well and the settings table clipped its origin
|
|
5060
|
+
* column while the findings table crushed its file paths into three lines.
|
|
5061
|
+
*/
|
|
5062
|
+
table.figures td.num { width: 100%; }
|
|
5063
|
+
/*
|
|
5064
|
+
* The settings table does NOT force its value column wide. One of its values is the test glob,
|
|
5065
|
+
* long enough to push the table past the card, and the origin column then slid out of view and
|
|
5066
|
+
* was clipped: "overridden in this mo". Long values wrap instead, and the origin stays readable.
|
|
5067
|
+
*/
|
|
5068
|
+
table.settings td.num { white-space: normal; overflow-wrap: anywhere; }
|
|
5069
|
+
table.settings td:last-child { text-align: right; white-space: nowrap; }
|
|
5070
|
+
table.places td:first-child { width: 100%; }
|
|
5071
|
+
table.places td.num { text-align: right; }
|
|
5072
|
+
td.read { color: var(--ink-soft); font-size: 0.88rem; }
|
|
5073
|
+
tbody th { color: var(--ink); }
|
|
5074
|
+
code { font: 0.86em ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--ink-soft); }
|
|
5075
|
+
.unit { color: var(--label); font-size: 0.82em; font-weight: 400; }
|
|
5076
|
+
.moved {
|
|
5077
|
+
color: var(--moved); font-size: 0.78rem; font-weight: 500;
|
|
5078
|
+
white-space: nowrap;
|
|
5079
|
+
}
|
|
5080
|
+
/* The collapsed reserves. Closed by default: the figures are what the page opens on. */
|
|
5081
|
+
details { border-bottom: 1px solid var(--rule); }
|
|
5082
|
+
details:last-of-type { border-bottom: 0; }
|
|
5083
|
+
summary {
|
|
5084
|
+
cursor: pointer; padding: 0.7rem 0.6rem; font-size: 0.9rem; font-weight: 500;
|
|
5085
|
+
display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: baseline;
|
|
5086
|
+
}
|
|
5087
|
+
/*
|
|
5088
|
+
* The chevron is drawn, not inherited: "display: flex" on a summary removes the native marker,
|
|
5089
|
+
* and a collapsed block with no affordance is a block nobody opens.
|
|
5090
|
+
*/
|
|
5091
|
+
summary { list-style: none; }
|
|
5092
|
+
summary::-webkit-details-marker { display: none; }
|
|
5093
|
+
summary::before {
|
|
5094
|
+
content: ''; flex: none; width: 0.38rem; height: 0.38rem; margin: 0.3rem 0.1rem 0 0;
|
|
5095
|
+
border-right: 1.5px solid var(--label); border-bottom: 1.5px solid var(--label);
|
|
5096
|
+
transform: rotate(-45deg); transition: transform 0.15s ease;
|
|
5097
|
+
}
|
|
5098
|
+
details[open] summary::before { transform: rotate(45deg); border-color: var(--brand); }
|
|
5099
|
+
@media (prefers-reduced-motion: reduce) { summary::before { transition: none; } }
|
|
5100
|
+
summary:hover { color: var(--brand); }
|
|
5101
|
+
/* One line, truncated: the summary is a hint, the body is the text. Opening it shows all. */
|
|
5102
|
+
summary .what {
|
|
5103
|
+
color: var(--label); font-size: 0.82rem; font-weight: 400;
|
|
5104
|
+
flex: 1 1 18rem; min-width: 0;
|
|
5105
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
5106
|
+
}
|
|
5107
|
+
details[open] summary .what { white-space: normal; overflow: visible; }
|
|
5108
|
+
details[open] summary { color: var(--brand); }
|
|
5109
|
+
details p.read { color: var(--ink-soft); }
|
|
5110
|
+
details p b { color: var(--ink); font-weight: 600; }
|
|
5111
|
+
details p { margin: 0 0 0.7rem; padding: 0 0.6rem; color: var(--ink-soft); font-size: 0.88rem; max-width: 52rem; }
|
|
5112
|
+
details p.source { color: var(--label); font-size: 0.8rem; }
|
|
5113
|
+
footer { color: var(--label); font-size: 0.78rem; margin-top: 2.5rem; }
|
|
5114
|
+
@media (max-width: 40rem) {
|
|
5115
|
+
section { padding: 0.25rem 0.6rem; }
|
|
5116
|
+
summary .what { display: none; }
|
|
5117
|
+
}
|
|
5003
5118
|
</style>
|
|
5004
5119
|
</head>
|
|
5005
5120
|
<body>
|
|
5006
5121
|
<main>
|
|
5007
|
-
<
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5122
|
+
<header>
|
|
5123
|
+
<h1>${escape(report.module)}</h1>
|
|
5124
|
+
<p class="sub">Static analysis, ${escape(report.passes.join(" / "))}</p>
|
|
5125
|
+
</header>
|
|
5126
|
+
|
|
5127
|
+
<h2>Measured</h2>
|
|
5128
|
+
<section>
|
|
5129
|
+
<table class="figures">
|
|
5130
|
+
<thead>
|
|
5131
|
+
<tr><th scope="col">Metric</th><th scope="col">Value</th></tr>
|
|
5132
|
+
</thead>
|
|
5133
|
+
<tbody>
|
|
5014
5134
|
${rows}
|
|
5015
|
-
|
|
5016
|
-
|
|
5135
|
+
</tbody>
|
|
5136
|
+
</table>
|
|
5137
|
+
</section>
|
|
5138
|
+
${report.notes.length > 0 ? `
|
|
5139
|
+
<h2>Not measured</h2>
|
|
5140
|
+
<section>
|
|
5141
|
+
<table><tbody>
|
|
5142
|
+
${report.notes.map((note) => ` <tr><td class="read">${escape(note)}</td></tr>`).join("\n")}
|
|
5143
|
+
</tbody></table>
|
|
5144
|
+
</section>
|
|
5145
|
+
` : ""}
|
|
5146
|
+
<h2>What these numbers do not say</h2>
|
|
5147
|
+
<section>
|
|
5148
|
+
${reserves}
|
|
5149
|
+
</section>
|
|
5150
|
+
|
|
5017
5151
|
<h2>Measured with</h2>
|
|
5018
|
-
<
|
|
5019
|
-
<
|
|
5020
|
-
<
|
|
5021
|
-
|
|
5022
|
-
|
|
5152
|
+
<section>
|
|
5153
|
+
<table class="settings">
|
|
5154
|
+
<thead>
|
|
5155
|
+
<tr><th scope="col">Setting</th><th scope="col">Value</th><th scope="col">Origin</th></tr>
|
|
5156
|
+
</thead>
|
|
5157
|
+
<tbody>
|
|
5023
5158
|
${settings}
|
|
5024
|
-
|
|
5025
|
-
|
|
5159
|
+
</tbody>
|
|
5160
|
+
</table>
|
|
5161
|
+
</section>
|
|
5026
5162
|
${findings}
|
|
5163
|
+
<footer>sentinel static-analysis</footer>
|
|
5027
5164
|
</main>
|
|
5028
5165
|
</body>
|
|
5029
5166
|
</html>
|
|
@@ -5494,11 +5631,12 @@ function safeParse(text) {
|
|
|
5494
5631
|
}
|
|
5495
5632
|
}
|
|
5496
5633
|
function placesToLook(product, tests, dead) {
|
|
5634
|
+
const at = (side) => `${side?.name ?? "(unknown)"}${side?.start === void 0 ? "" : `:${side.start}`}`;
|
|
5497
5635
|
const clones = (outcome, metric) => outcome.kind === "figures" ? outcome.duplicates.map((entry) => ({
|
|
5498
5636
|
metric,
|
|
5499
|
-
path: entry.firstFile
|
|
5500
|
-
other: entry.secondFile
|
|
5501
|
-
lines: entry.lines
|
|
5637
|
+
path: at(entry.firstFile),
|
|
5638
|
+
other: at(entry.secondFile),
|
|
5639
|
+
...entry.lines === void 0 ? {} : { lines: entry.lines }
|
|
5502
5640
|
})) : [];
|
|
5503
5641
|
const deadMetric = {
|
|
5504
5642
|
"unused-file": "unusedFiles",
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hublo/sentinel",
|
|
3
|
-
"version": "1.5.0-alpha.
|
|
3
|
+
"version": "1.5.0-alpha.13",
|
|
4
4
|
"description": "One CLI that guards code health across Hublo repos: shared lint/typescript/build/test presets, static & dynamic analysis, and architecture checks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|