@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.
@@ -15,7 +15,7 @@ import {
15
15
  palette,
16
16
  registerAdapters,
17
17
  resolve
18
- } from "../chunk-SXIZBKVJ.js";
18
+ } from "../chunk-JXI6UJ5I.js";
19
19
  import {
20
20
  resolveContext
21
21
  } from "../chunk-KLEXJFJ6.js";
@@ -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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
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 in this module</span>' : ""}</td>
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>${finding.other ? ` <span class="unit">and</span> <code>${escape(finding.other)}</code>` : ""}</td>
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 ` <h2>${escape(label)}: where to look</h2>
4968
- <table>
4969
- <thead>
4970
- <tr><th scope="col">File</th><th scope="col">Name</th><th scope="col">Lines</th><th scope="col">Confidence</th></tr>
4971
- </thead>
4972
- <tbody>
4985
+ return `
4986
+ <h2>${escape(label)} &middot; 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
- </tbody>
4975
- </table>
4976
- ${rest > 0 ? ` <p class="sub">The ${HTML_GROUP_LIMIT} longest are listed; ${rest} more are in <code>${SCRIPTS.json}</code>.</p>` : ""}`;
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
- :root { color-scheme: light dark; --ink: #1a1a1a; --bg: #fdfdfc; --rule: #e2e0da; --warn: #9a3412; --dim: #6b6b6b; }
4985
- @media (prefers-color-scheme: dark) {
4986
- :root { --ink: #e8e6e1; --bg: #1b1b1a; --rule: #35332e; --warn: #fb923c; --dim: #9b9892; }
4987
- }
4988
- body { margin: 0; padding: 2rem 1rem; background: var(--bg); color: var(--ink);
4989
- font: 15px/1.55 ui-sans-serif, system-ui, sans-serif; }
4990
- main { max-width: 60rem; margin: 0 auto; }
4991
- h1 { font-size: 1.25rem; margin: 0 0 0.25rem; }
4992
- p.sub { color: var(--dim); margin: 0 0 1.5rem; }
4993
- h2 { font-size: 0.95rem; margin: 2.5rem 0 0.5rem; }
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.6rem 0.75rem; border-bottom: 1px solid var(--rule);
4996
- vertical-align: top; }
4997
- thead th { font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--dim); }
4998
- td.num { font-variant-numeric: tabular-nums; white-space: nowrap; }
4999
- code { font: 0.9em ui-monospace, SFMono-Regular, Menlo, monospace; }
5000
- .unit { color: var(--dim); font-size: 0.85em; }
5001
- .moved { color: var(--warn); font-size: 0.85em; }
5002
- .caveat { color: var(--dim); font-size: 0.9em; }
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
- <h1>Static analysis: ${escape(report.module)}</h1>
5008
- <p class="sub">Passes: ${escape(report.passes.join(", "))}</p>
5009
- <table>
5010
- <thead>
5011
- <tr><th scope="col">Metric</th><th scope="col">Value</th><th scope="col">What it does not say</th></tr>
5012
- </thead>
5013
- <tbody>
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
- </tbody>
5016
- </table>
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
- <table>
5019
- <thead>
5020
- <tr><th scope="col">Setting</th><th scope="col">Value</th><th scope="col">Origin</th></tr>
5021
- </thead>
5022
- <tbody>
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
- </tbody>
5025
- </table>
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?.name ?? "(unknown)",
5500
- other: entry.secondFile?.name,
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
@@ -6,7 +6,7 @@ import {
6
6
  registerAdapters,
7
7
  resolve,
8
8
  setDefaultRunner
9
- } from "./chunk-SXIZBKVJ.js";
9
+ } from "./chunk-JXI6UJ5I.js";
10
10
  import {
11
11
  BaseAdapter
12
12
  } from "./chunk-KLEXJFJ6.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hublo/sentinel",
3
- "version": "1.5.0-alpha.11",
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",