@casomoltd/tooling 0.28.0 → 0.29.0
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/README.md +80 -21
- package/bin/check-hooks.sh +84 -0
- package/bin/check-personal-data.mjs +270 -0
- package/bin/render-figures.mjs +159 -0
- package/bin/render-report.mjs +47 -46
- package/bin/spec-check.mjs +229 -0
- package/bin/utils.mjs +19 -0
- package/bin/verify-pack.mjs +1 -0
- package/package.json +13 -6
- package/styles/casomo-spec.css +230 -0
- package/styles/casomo-tokens.css +43 -0
package/bin/render-report.mjs
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Render a markdown + mermaid report to a self-contained HTML
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Render a markdown + mermaid report to a self-contained HTML page — the
|
|
5
|
+
* renderer for agent reports (docs-xray, design-xray), whose mermaid diagrams
|
|
6
|
+
* render unreliably in editor previews. The page is meant to be published as
|
|
7
|
+
* an artifact by the caller; the agents themselves cannot publish.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Two properties the output depends on, both learned the hard way.
|
|
10
|
+
*
|
|
11
|
+
* The markdown is converted HERE, in node, not in the page. A page that builds
|
|
12
|
+
* its own body at load time has no `pre.mermaid` in it when the artifact host
|
|
13
|
+
* looks for diagrams to render, so the diagrams never appear — and its first
|
|
14
|
+
* still frame, which is what a thumbnail and a skimming reader get, says
|
|
15
|
+
* "Rendering…".
|
|
16
|
+
*
|
|
17
|
+
* The page ships NO mermaid runtime. The artifact host has its own and renders
|
|
18
|
+
* every `pre.mermaid` itself; a second runtime does not add a fallback, it
|
|
19
|
+
* races for the same elements, and because mermaid scopes a diagram's fills to
|
|
20
|
+
* the svg id it generated, the loser leaves those rules matching nothing and
|
|
21
|
+
* every shape falls back to the SVG default fill, which is black.
|
|
12
22
|
*
|
|
13
23
|
* Usage:
|
|
14
24
|
* node bin/render-report.mjs <report.md> [--no-open]
|
|
@@ -23,7 +33,9 @@
|
|
|
23
33
|
* 1 Bad arguments or the input file is missing
|
|
24
34
|
*/
|
|
25
35
|
import {existsSync, readFileSync, writeFileSync} from "node:fs";
|
|
26
|
-
import {
|
|
36
|
+
import {marked} from "marked";
|
|
37
|
+
import {dirname, resolve} from "node:path";
|
|
38
|
+
import {fileURLToPath} from "node:url";
|
|
27
39
|
import {openPath} from "./utils.mjs";
|
|
28
40
|
|
|
29
41
|
const args = process.argv.slice(2);
|
|
@@ -37,6 +49,20 @@ if (!mdPath || !existsSync(mdPath)) {
|
|
|
37
49
|
|
|
38
50
|
const htmlPath = mdPath.replace(/\.md$/, "") + ".html";
|
|
39
51
|
|
|
52
|
+
/* The house stylesheets, read from `styles/` at render time and inlined.
|
|
53
|
+
*
|
|
54
|
+
* Inlining a GENERATED copy is not the thing `draft-design-spec` forbids. That
|
|
55
|
+
* rule is about authoring a second copy by hand, which drifts; here the single
|
|
56
|
+
* file on disk stays the only source, so a restyle is still one edit. What it
|
|
57
|
+
* buys is a report that is one self-contained page — nothing to publish
|
|
58
|
+
* alongside it, and nothing to resolve at the far end.
|
|
59
|
+
*
|
|
60
|
+
* Tokens first: the chrome sheet reads every colour and face from them. */
|
|
61
|
+
const styleDir = resolve(dirname(fileURLToPath(import.meta.url)), "..", "styles");
|
|
62
|
+
const css = ["casomo-tokens.css", "casomo-spec.css"]
|
|
63
|
+
.map((f) => readFileSync(resolve(styleDir, f), "utf8"))
|
|
64
|
+
.join("\n");
|
|
65
|
+
|
|
40
66
|
// A mermaid classDiagram rejects union/bracket types in a member line
|
|
41
67
|
// (`date|None`, `list[Item]`) — soften them inside class bodies so an imperfect
|
|
42
68
|
// source (e.g. a design-xray map) still renders. Scoped to classDiagram fences
|
|
@@ -52,13 +78,15 @@ const softenClassDiagrams = (text) =>
|
|
|
52
78
|
: block,
|
|
53
79
|
);
|
|
54
80
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
)
|
|
81
|
+
const body = marked
|
|
82
|
+
.parse(softenClassDiagrams(readFileSync(mdPath, "utf8")))
|
|
83
|
+
// `marked` emits a fenced mermaid block as <pre><code class="language-mermaid">.
|
|
84
|
+
// The artifact host looks for <pre class="mermaid">, so rewrite it at build
|
|
85
|
+
// time rather than in the page — see the header note on why that matters.
|
|
86
|
+
.replace(
|
|
87
|
+
/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g,
|
|
88
|
+
(_, diagram) => `<pre class="mermaid">${diagram}</pre>`,
|
|
89
|
+
);
|
|
62
90
|
|
|
63
91
|
const html = `<!doctype html>
|
|
64
92
|
<html lang="en">
|
|
@@ -67,40 +95,13 @@ const html = `<!doctype html>
|
|
|
67
95
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
68
96
|
<title>${mdPath}</title>
|
|
69
97
|
<style>
|
|
70
|
-
|
|
71
|
-
font: 15px/1.6 system-ui, sans-serif; color: #1a1a1a; }
|
|
72
|
-
h1, h2, h3 { line-height: 1.25; }
|
|
73
|
-
h2 { border-bottom: 1px solid #eee; padding-bottom: .3rem; margin-top: 2.5rem; }
|
|
74
|
-
table { border-collapse: collapse; margin: 1rem 0; font-size: 14px; }
|
|
75
|
-
th, td { border: 1px solid #ddd; padding: 6px 10px; text-align: left; vertical-align: top; }
|
|
76
|
-
th { background: #f7f7f7; }
|
|
77
|
-
code { background: #f4f4f4; padding: 1px 5px; border-radius: 3px; font-size: .9em; }
|
|
78
|
-
pre { background: #f7f7f7; padding: 1rem; border-radius: 6px; overflow-x: auto; }
|
|
79
|
-
pre.mermaid { background: #fff; }
|
|
80
|
-
a { color: #0b66c3; }
|
|
98
|
+
${css}
|
|
81
99
|
</style>
|
|
82
100
|
</head>
|
|
83
101
|
<body>
|
|
84
|
-
<div
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
</script>
|
|
88
|
-
<script type="module">
|
|
89
|
-
import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js";
|
|
90
|
-
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
|
|
91
|
-
mermaid.initialize({ startOnLoad: false, theme: "neutral" });
|
|
92
|
-
const el = document.getElementById("content");
|
|
93
|
-
el.innerHTML = marked.parse(document.getElementById("md").textContent);
|
|
94
|
-
// marked emits mermaid fences as <code class="language-mermaid">; mermaid wants
|
|
95
|
-
// <pre class="mermaid">.
|
|
96
|
-
el.querySelectorAll("code.language-mermaid").forEach((c) => {
|
|
97
|
-
const pre = document.createElement("pre");
|
|
98
|
-
pre.className = "mermaid";
|
|
99
|
-
pre.textContent = c.textContent;
|
|
100
|
-
c.closest("pre").replaceWith(pre);
|
|
101
|
-
});
|
|
102
|
-
await mermaid.run({ querySelector: ".mermaid" });
|
|
103
|
-
</script>
|
|
102
|
+
<div class="wrap" style="padding-top:32px">
|
|
103
|
+
${body}
|
|
104
|
+
</div>
|
|
104
105
|
</body>
|
|
105
106
|
</html>
|
|
106
107
|
`;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Check one HTML design spec's mechanics, so the authoring skill does not
|
|
4
|
+
* have to teach them.
|
|
5
|
+
*
|
|
6
|
+
* These rules are measurements -- a width budget, a contiguous figure
|
|
7
|
+
* sequence, a resolving anchor -- so they belong in a command. What stays in
|
|
8
|
+
* the skill is the half no tool can apply: what to draw, and whether it is
|
|
9
|
+
* worth drawing.
|
|
10
|
+
*
|
|
11
|
+
* Exits non-zero on the first failing rule set, listing every instance.
|
|
12
|
+
*
|
|
13
|
+
* node bin/spec-check.mjs <spec.html>
|
|
14
|
+
*/
|
|
15
|
+
import { readFileSync } from 'node:fs';
|
|
16
|
+
import { FIGURE_WIDTH_BUDGET } from './utils.mjs';
|
|
17
|
+
|
|
18
|
+
const file = process.argv[2];
|
|
19
|
+
if (!file) throw new Error('usage: spec-check.mjs <spec.html>');
|
|
20
|
+
const html = readFileSync(file, 'utf8');
|
|
21
|
+
|
|
22
|
+
/** A pre-rendered figure is hundreds of kilobytes of inline SVG carrying its
|
|
23
|
+
* own ids, and a figure's mermaid source sits in an HTML comment above it.
|
|
24
|
+
* Neither is the document, so an id in either must not answer one of the
|
|
25
|
+
* document's links, and neither may be counted as an anchor a reader has. */
|
|
26
|
+
const doc = html
|
|
27
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
28
|
+
.replace(/<svg[\s\S]*?<\/svg>/g, '');
|
|
29
|
+
|
|
30
|
+
const findings = [];
|
|
31
|
+
const fail = (rule, detail) => findings.push({ rule, detail });
|
|
32
|
+
const all = (re, s = html) => [...s.matchAll(re)];
|
|
33
|
+
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
34
|
+
const text = (s) => s.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim();
|
|
35
|
+
|
|
36
|
+
// --- anchors -------------------------------------------------------------
|
|
37
|
+
const ids = new Set(all(/\sid="([^"]+)"/g, doc).map((m) => m[1]));
|
|
38
|
+
for (const m of all(/href="#([^"]+)"/g, doc)) {
|
|
39
|
+
if (!ids.has(m[1])) fail('anchor', `href="#${m[1]}" resolves to nothing`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// --- contents against the headings ---------------------------------------
|
|
43
|
+
const nav = doc.match(/<nav class="toc">([\s\S]*?)<\/nav>/);
|
|
44
|
+
if (!nav) fail('toc', 'no <nav class="toc">');
|
|
45
|
+
else {
|
|
46
|
+
// Parsed from inside the nav, not filtered out of the whole document: once
|
|
47
|
+
// prose cites a section by name, a body link and a contents entry are the
|
|
48
|
+
// same markup, and a filter counts both.
|
|
49
|
+
//
|
|
50
|
+
// Both sides capture markup and are reduced to text, so a heading carrying
|
|
51
|
+
// a <code> span stays in the comparison. Matching bare text instead drops
|
|
52
|
+
// such a heading from BOTH sets, which leaves the counts equal and the
|
|
53
|
+
// mismatch never compared.
|
|
54
|
+
const labels = [...nav[1].matchAll(/<a href="#[^"]+"[^>]*>([\s\S]*?)<\/a>/g)]
|
|
55
|
+
.map((m) => text(m[1]));
|
|
56
|
+
const heads = all(/<h2[^>]*>([\s\S]*?)<\/h2>/g, doc).map((m) => text(m[1]));
|
|
57
|
+
if (labels.length !== heads.length) {
|
|
58
|
+
fail('toc', `${labels.length} entries against ${heads.length} sections`);
|
|
59
|
+
} else {
|
|
60
|
+
labels.forEach((l, i) => {
|
|
61
|
+
if (l !== heads[i]) fail('toc', `entry "${l}" against heading "${heads[i]}"`);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// --- S11: the masthead carries no loose prose ----------------------------
|
|
67
|
+
// The Overview & goals card is the document's summary. A standfirst above it
|
|
68
|
+
// says the same thing a second time, and belongs to no section, so it is the
|
|
69
|
+
// copy that goes stale unread. Everything else in the masthead -- eyebrow,
|
|
70
|
+
// title, contents, status pills -- is not a paragraph, so a <p> left after
|
|
71
|
+
// the nav and the card are removed is the defect.
|
|
72
|
+
const hero = doc.match(/<header[^>]*class="hero"[\s\S]*?<\/header>/);
|
|
73
|
+
if (hero) {
|
|
74
|
+
const loose = hero[0]
|
|
75
|
+
.replace(/<nav[\s\S]*?<\/nav>/g, '')
|
|
76
|
+
.replace(/<section[\s\S]*?<\/section>/g, '');
|
|
77
|
+
// Prose, not `<p>`: the standfirst this rule exists to catch was carried as
|
|
78
|
+
// a non-paragraph element, so keying on the tag let the exact defect past.
|
|
79
|
+
for (const m of loose.matchAll(/<(?:p|div)\b[^>]*>([\s\S]*?)<\/(?:p|div)>/g)) {
|
|
80
|
+
if (/\bclass="[^"]*\b(?:eyebrow|pillrow|pill|wrap)\b/.test(m[0])) continue;
|
|
81
|
+
const words = text(m[1]);
|
|
82
|
+
if (!words) continue;
|
|
83
|
+
fail('masthead', `prose sits in the masthead outside the Overview card: `
|
|
84
|
+
+ `"${words.slice(0, 60)}…" (S11)`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// --- no typed numbers: the stylesheet generates them -------------------
|
|
89
|
+
for (const m of all(/<h[23][^>]*>\s*(\d+(?:\.\d+)?)\s*(?:·|·)/g, doc)) {
|
|
90
|
+
fail('numbering', `a heading types "${m[1]}"; section numbers are generated`);
|
|
91
|
+
}
|
|
92
|
+
const TYPED_TOC = /<a href="#[^"]+"[^>]*>\s*(\d+(?:\.\d+)?)\s*(?:·|·)/g;
|
|
93
|
+
for (const m of all(TYPED_TOC, doc)) {
|
|
94
|
+
fail('numbering', `a contents entry types "${m[1]}"; numbers are generated`);
|
|
95
|
+
}
|
|
96
|
+
for (const m of all(/(?:§|§)\d+(?:\.\d+)?/g, doc)) {
|
|
97
|
+
fail('numbering', `prose cites "${m[0]}" by number; cite the section by name`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// --- figures: numbered, anchored, captioned, and inside the budget -------
|
|
101
|
+
const figs = all(/<div\b[^>]*\bid="fig-(\d+)"[^>]*>/g);
|
|
102
|
+
figs.forEach((m, i) => {
|
|
103
|
+
const n = Number(m[1]);
|
|
104
|
+
if (n !== i + 1) {
|
|
105
|
+
fail('figure', i === 0 ? `expected fig-1, found fig-${n}`
|
|
106
|
+
: `fig-${n} follows fig-${figs[i - 1][1]}`);
|
|
107
|
+
}
|
|
108
|
+
if (!m[0].includes('scroll-margin-top')) {
|
|
109
|
+
fail('figure', `fig-${n} has no scroll-margin-top, so host chrome can cover it`);
|
|
110
|
+
}
|
|
111
|
+
// Bounded by what comes next rather than by a character count: a
|
|
112
|
+
// pre-rendered figure is hundreds of kilobytes of inline SVG, so a window
|
|
113
|
+
// wide enough to clear one is wide enough to borrow the next figure's
|
|
114
|
+
// caption and call every figure captioned.
|
|
115
|
+
const from = m.index + m[0].length;
|
|
116
|
+
const nextFig = figs[i + 1]?.index ?? -1;
|
|
117
|
+
const nextSec = html.indexOf('</section>', from);
|
|
118
|
+
const ends = [nextFig, nextSec].filter((x) => x !== -1);
|
|
119
|
+
const between = html.slice(from, ends.length ? Math.min(...ends) : html.length);
|
|
120
|
+
if (!/<(?:div|p)\b[^>]*\bclass="[^"]*\blegend\b/.test(between)) {
|
|
121
|
+
fail('caption', `fig-${n} has no legend before the next figure or section end`);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Mermaid emits `viewBox="-8 -8 W H"` on the figure, while every arrowhead
|
|
126
|
+
// marker inside it carries its own `viewBox="0 0 10 10"`. So the figure is
|
|
127
|
+
// found by the id the renderer assigns, and the width read as the third
|
|
128
|
+
// value whatever the origin.
|
|
129
|
+
//
|
|
130
|
+
// The cardinality check is the load-bearing half. A width rule that matches
|
|
131
|
+
// nothing reports nothing, so an unrendered spec, or one figure whose render
|
|
132
|
+
// failed, would otherwise be certified clean by a measurement that never ran.
|
|
133
|
+
const svgTags = all(/<svg\b[^>]*\bid="fig-svg-\d+"[^>]*>/g).map((m) => m[0]);
|
|
134
|
+
if (svgTags.length !== figs.length) {
|
|
135
|
+
fail('figure', `${figs.length} figure(s) but ${svgTags.length} rendered svg(s); `
|
|
136
|
+
+ 'run render-figures.mjs');
|
|
137
|
+
}
|
|
138
|
+
svgTags.forEach((tag, i) => {
|
|
139
|
+
const vb = tag.match(/viewBox="[-\d.]+\s+[-\d.]+\s+([\d.]+)/);
|
|
140
|
+
if (!vb) {
|
|
141
|
+
fail('width', `fig-${i + 1} rendered without a parsable viewBox`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const w = Math.round(Number(vb[1]));
|
|
145
|
+
if (w > FIGURE_WIDTH_BUDGET) {
|
|
146
|
+
fail('width',
|
|
147
|
+
`fig-${i + 1} renders ${w}px against a ${FIGURE_WIDTH_BUDGET}px budget`);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// --- things that only fail once the host renders the page ----------------
|
|
152
|
+
if (/<pre[^>]*class="[^"]*\bmermaid\b/.test(html)) {
|
|
153
|
+
fail('runtime', 'a <pre class="mermaid"> is present; the artifact host claims it '
|
|
154
|
+
+ 'and renames the svg, so every fill falls back to black. Pre-render instead.');
|
|
155
|
+
}
|
|
156
|
+
if (html.includes('Syntax error in text')) {
|
|
157
|
+
fail('render', 'a rendered figure carries mermaid\'s syntax-error text');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// A mermaid click directive compiles to one of two things. `call fn()` and the
|
|
161
|
+
// bare-callback form become inline JS, which the host CSP drops. `href "#a"`
|
|
162
|
+
// becomes an ordinary <a> in the SVG, which works. So a call is refused, and
|
|
163
|
+
// an href has to be findable on that node in the pre-rendered output -- which
|
|
164
|
+
// also catches a directive naming a node no longer in the diagram.
|
|
165
|
+
//
|
|
166
|
+
// The node id is matched loosely and the remainder classified, so a spelling
|
|
167
|
+
// this does not know becomes a finding rather than a skip: the rule exists to
|
|
168
|
+
// ban inline JS, and a pattern that only recognises one spelling of it lets
|
|
169
|
+
// the rest through silently.
|
|
170
|
+
const svgs = all(/<svg\b[^>]*\bid="fig-svg-\d+"[\s\S]*?<\/svg>/g).map((m) => m[0]);
|
|
171
|
+
for (const m of all(/^[ \t]*click\s+(\S+)\s+(.*)$/gm)) {
|
|
172
|
+
const [, node, rest] = m;
|
|
173
|
+
const href = rest.match(/^(?:href\s+)?"([^"]+)"/);
|
|
174
|
+
if (/^call\b/.test(rest)) {
|
|
175
|
+
fail('click',
|
|
176
|
+
`click ${node} ${rest.trim()} is inline JS, which the host CSP blocks`);
|
|
177
|
+
} else if (!href) {
|
|
178
|
+
fail('click', `click ${node} ${rest.trim()} is not an href; a bare callback is `
|
|
179
|
+
+ 'inline JS, which the host CSP blocks');
|
|
180
|
+
} else if (!svgs.some((svg) => {
|
|
181
|
+
// Find the anchor by the node it wraps, then assert the attribute on that
|
|
182
|
+
// tag. Searching for `href="…"` alone matches the tail of `xlink:href`,
|
|
183
|
+
// so it would pass on the deprecated attribute and never see the one the
|
|
184
|
+
// renderer adds.
|
|
185
|
+
const tag = svg.match(new RegExp(`<a\\b[^>]*\\bdata-id="${esc(node)}"[^>]*>`));
|
|
186
|
+
return tag && new RegExp(`(^|\\s)href="${esc(href[1])}"`).test(tag[0]);
|
|
187
|
+
})) {
|
|
188
|
+
fail('click', `click ${node} href ${href[1]} did not survive pre-rendering`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Matched as a whole tag, then read attribute by attribute: requiring `rel`
|
|
193
|
+
// before `href` let a reordered link through, and a missed blocked stylesheet
|
|
194
|
+
// is the direction that fails open.
|
|
195
|
+
for (const m of all(/<link\b[^>]*>/g, doc)) {
|
|
196
|
+
if (!/\brel="[^"]*\bstylesheet\b/.test(m[0])) continue;
|
|
197
|
+
const href = m[0].match(/\bhref="([^"]+)"/);
|
|
198
|
+
if (!href || !/^https?:/.test(href[1])) continue;
|
|
199
|
+
let host = null;
|
|
200
|
+
try { host = new URL(href[1]).host; } catch { /* foreign by default */ }
|
|
201
|
+
if (host !== 'fonts.googleapis.com') {
|
|
202
|
+
fail('csp', `${href[1]} is blocked by the host CSP; inline it`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// --- open questions ------------------------------------------------------
|
|
207
|
+
const qs = all(/<tr id="q(\d+)"/g, doc).map((m) => Number(m[1]));
|
|
208
|
+
qs.forEach((n, i) => {
|
|
209
|
+
if (n !== i + 1) {
|
|
210
|
+
fail('questions', i === 0 ? `expected Q1, found Q${n}`
|
|
211
|
+
: `Q${n} follows Q${qs[i - 1]}`);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// --- report --------------------------------------------------------------
|
|
216
|
+
if (findings.length === 0) {
|
|
217
|
+
const n = figs.length;
|
|
218
|
+
const s = n === 1 ? '' : 's';
|
|
219
|
+
console.log(`spec-check: clean (${ids.size} anchors, ${n} figure${s})`);
|
|
220
|
+
process.exit(0);
|
|
221
|
+
}
|
|
222
|
+
const byRule = new Map();
|
|
223
|
+
for (const f of findings) byRule.set(f.rule, [...(byRule.get(f.rule) || []), f.detail]);
|
|
224
|
+
for (const [rule, details] of byRule) {
|
|
225
|
+
console.error(`\n${rule}:`);
|
|
226
|
+
for (const d of details) console.error(` - ${d}`);
|
|
227
|
+
}
|
|
228
|
+
console.error(`\nspec-check: ${findings.length} finding(s) in ${file}`);
|
|
229
|
+
process.exit(1);
|
package/bin/utils.mjs
CHANGED
|
@@ -7,6 +7,25 @@ import path from "node:path";
|
|
|
7
7
|
import fs from "node:fs";
|
|
8
8
|
import {pathToFileURL} from "node:url";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Usable width inside a design spec's figure card, in px.
|
|
12
|
+
*
|
|
13
|
+
* Read off `styles/casomo-spec.css`, which sets no `box-sizing`, so every
|
|
14
|
+
* width there is a CONTENT width:
|
|
15
|
+
*
|
|
16
|
+
* --measure: 62rem .wrap's content box 992
|
|
17
|
+
* .figure-card padding 16px each side -32
|
|
18
|
+
* .figure-card border 1px each side -2
|
|
19
|
+
* ---
|
|
20
|
+
* 958
|
|
21
|
+
*
|
|
22
|
+
* One producer, because the renderer reports a figure against this and the
|
|
23
|
+
* checker fails a spec over it; two copies are two answers at the boundary.
|
|
24
|
+
* It mirrors a stylesheet, so a change to `--measure` or to the card's
|
|
25
|
+
* padding is a change to this number.
|
|
26
|
+
*/
|
|
27
|
+
export const FIGURE_WIDTH_BUDGET = 958;
|
|
28
|
+
|
|
10
29
|
/**
|
|
11
30
|
* Open a file or URL with the platform opener, detached so the CLI can exit.
|
|
12
31
|
*
|
package/bin/verify-pack.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@casomoltd/tooling",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Casomo's shared dev config: ESLint, Prettier, commitlint, knip & jscpd presets and dev CLIs (git hooks, readability, screenshots)",
|
|
3
|
+
"version": "0.29.0",
|
|
4
|
+
"description": "Casomo's shared dev config: ESLint, Prettier, commitlint, knip & jscpd presets and dev CLIs (git hooks, readability, screenshots, design specs)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -16,17 +16,19 @@
|
|
|
16
16
|
"jscpd/",
|
|
17
17
|
"readability/",
|
|
18
18
|
"report/",
|
|
19
|
+
"styles/",
|
|
19
20
|
"README.md",
|
|
20
21
|
"!**/*.test.mjs"
|
|
21
22
|
],
|
|
22
23
|
"scripts": {
|
|
23
|
-
"check": "node bin/check-gates.mjs && node bin/verify-pack.mjs && npm run check:private-refs && npm run typecheck && npm run test && npm run lint:md",
|
|
24
|
+
"check": "node bin/check-gates.mjs && node bin/verify-pack.mjs && npm run check:private-refs && npm run check:personal-data && npm run typecheck && npm run test && npm run lint:md",
|
|
24
25
|
"typecheck": "tsc --noEmit -p hooks/tsconfig.json",
|
|
25
|
-
"test": "node hooks/test.mts && node bin/check-gates.test.mjs && node commitlint/index.test.mjs",
|
|
26
|
+
"test": "node hooks/test.mts && node bin/check-gates.test.mjs && node bin/check-personal-data.test.mjs && node commitlint/index.test.mjs",
|
|
26
27
|
"lint:md": "remark --frail --quiet --no-stdout --rc-path bin/skills-lint.config.mjs skills agents docs",
|
|
27
28
|
"build": "echo 'no-op: source-only repo, nothing to build'",
|
|
28
29
|
"prepare": "husky || true",
|
|
29
|
-
"check:private-refs": "node bin/check-private-refs.mjs"
|
|
30
|
+
"check:private-refs": "node bin/check-private-refs.mjs",
|
|
31
|
+
"check:personal-data": "node bin/check-personal-data.mjs"
|
|
30
32
|
},
|
|
31
33
|
"casomo": {
|
|
32
34
|
"gates": {
|
|
@@ -49,7 +51,9 @@
|
|
|
49
51
|
"bin": {
|
|
50
52
|
"build-report": "./bin/build-report.mjs",
|
|
51
53
|
"check-gates": "./bin/check-gates.mjs",
|
|
54
|
+
"check-hooks": "./bin/check-hooks.sh",
|
|
52
55
|
"check-linear": "./bin/check-linear.sh",
|
|
56
|
+
"check-personal-data": "./bin/check-personal-data.mjs",
|
|
53
57
|
"check-private-refs": "./bin/check-private-refs.mjs",
|
|
54
58
|
"check-tags": "./bin/check-tags.sh",
|
|
55
59
|
"check-version": "./bin/check-version.sh",
|
|
@@ -57,12 +61,15 @@
|
|
|
57
61
|
"pre-commit": "./bin/pre-commit.sh",
|
|
58
62
|
"pre-push": "./bin/pre-push.sh",
|
|
59
63
|
"readability": "./bin/readability.mjs",
|
|
60
|
-
"
|
|
64
|
+
"render-figures": "./bin/render-figures.mjs",
|
|
65
|
+
"screenshot": "./bin/screenshot.mjs",
|
|
66
|
+
"spec-check": "./bin/spec-check.mjs"
|
|
61
67
|
},
|
|
62
68
|
"publishConfig": {
|
|
63
69
|
"access": "public"
|
|
64
70
|
},
|
|
65
71
|
"dependencies": {
|
|
72
|
+
"marked": "^18.0.13",
|
|
66
73
|
"text-readability": "^1.1.1"
|
|
67
74
|
},
|
|
68
75
|
"peerDependencies": {
|