sheleg-design-skill 1.62.0 → 1.64.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/CHANGELOG.md CHANGED
@@ -1,3 +1,147 @@
1
+ ## 1.64.0 — a published variable names its token, the family has one tier model, and a redesign starts from a capture
2
+
3
+ A 2026-10-08 read of Figma's own guidance — *Claude Code for designers*, the MCP
4
+ server's structure, custom-rules, code-to-canvas and Code Connect pages, and rules
5
+ 5–6 of the installed plugin's `figma-generate-library` — found the Figma contract
6
+ publishing variables that Figma could not trace back to code, a tier model that
7
+ disagreed with `super-ux`'s, and two places where Figma's own skills fall back
8
+ silently. Every adopted point is paraphrased with a source line and recorded in
9
+ `KNOWLEDGE_PROVENANCE.md` (W6, W7). Read from the documentation, not confirmed by a
10
+ call on a file.
11
+
12
+ - **Code syntax and scopes on every published variable** (`FIGMA_BRIDGE.md` §1). A
13
+ variable's WEB code syntax is the pack's CSS custom property in `var(--…)` form,
14
+ wrapper included, so the reference code `get_design_context` returns names
15
+ `var(--accent-weak)` rather than a hex, and "map, don't import" becomes a lookup.
16
+ Scopes are set per token family from a table — surfaces, ink, lines, accent and
17
+ status, radius, spacing, type, families, shadow parts, primitives hidden — and
18
+ never `ALL_SCOPES`, which is what an unset scope is. The §4 re-read now checks
19
+ value, code syntax and scopes, and a semantic variable holding a raw value fails it.
20
+ - **One token-tier model for the family, with one home** — `FIGMA_BRIDGE.md`
21
+ §1 *Token tiers* (`#token-tiers`). Two tiers, primitive → semantic alias, the shape
22
+ Figma's library skill recommends. Primitives are the pack's own ramp steps (named
23
+ by their property) and each other colour literal (named by value), hidden and one
24
+ mode; semantic variables are every other custom property, 1:1, aliasing what the
25
+ CSS points them at, carrying the themes as modes. Figma adds no component tier the
26
+ pack does not declare. "Map, don't import" and "the pack is the authority; Figma
27
+ variables are an output" are kept. `super-ux` links the anchor instead of
28
+ restating a count.
29
+ - **A redesign's baseline is a capture** (`CREATIVE_DIRECTOR.md`, the Redesign row).
30
+ With Figma connected, the shipped UI is captured with `generate_figma_design` into
31
+ the surface's recorded file (App, Web or ASO — never a new file in drafts) on a page
32
+ marked `captured`, after the pack's variables are published into that file. Any
33
+ value left unbound after the capture is a raw value in the code and a finding. The
34
+ page is kept until the redesign ships.
35
+ - **`FIGMA_BRIDGE.md` §6 — where this contract overrides Figma's own guidance.** Code
36
+ Connect's "Add instructions for MCP" is generated from the pack's bans or left
37
+ unused, never hand-written. A `generate_image` placeholder is labelled as one, or
38
+ it is SLOP_MARKERS V035. A font that fails to load is declared and asserted after
39
+ the write, never replaced by a fallback family. "1:1 look and behavior" is parity
40
+ of layout and structure, never of identity; the pack is the token authority; a
41
+ correction goes into the director record and the skills, not session notes.
42
+ - **Routing words.** The description's lead now names the surfaces before
43
+ `Triggers -`, so a truncated listing keeps them, and carries «сделай дашборд»,
44
+ «админка», «анимации» and «макеты» for the umbrella's hook; 964 of 1024 characters (the house working limit is 970),
45
+ every T1 carrier and every routed trigger kept.
46
+ - **Gates.** `validate_figma_contract()` and `validate_routing_forms_lead()` in
47
+ `test/validate.py`, with four plants caught: code syntax without `var()`, a scope
48
+ row assigning `ALL_SCOPES`, the tier model restated beside Figma as three tiers, and
49
+ «админка» dropped from the description. A tier count stated beside Figma anywhere in
50
+ the bundle, README or cursor rule must equal the model's and link its home.
51
+
52
+ ## 1.63.0 — the director record is a file with a validator, and a flagship is scored on a rubric that says where it came from
53
+
54
+ The director's output was a prose block at the end of `CREATIVE_DIRECTOR.md`: nothing
55
+ held it, so a brief with no falsifier, a taste of "modern, clean", a fork judged by a
56
+ rubric written afterwards and a `Markers` line nobody ran all passed as a record. The
57
+ plan's pipeline (contracts K3, K6, K7) needs a record the next stage can check, and a
58
+ quality bar for flagship surfaces that is more than the measurable table.
59
+
60
+ - **D3 — `templates/director-record.md` and `npx sheleg-design-skill --check-record
61
+ <file> [--json]`.** A header with `surface_class` (`flagship`, `product`,
62
+ `internal`, `ad`) and sixteen `## <Field>` headings: Brief, Mode, Taste, References,
63
+ Cast, Fork, Rubric, Critique, Markers, Alignment, Quality, Signature, Surfaces,
64
+ Haptics, ADA, Open. A flagship owes all sixteen; a product screen Brief, Mode,
65
+ References, Markers, Open; an internal tool Brief, Mode, Markers, Open; an ad Brief,
66
+ Mode, Taste, References, Markers, Signature, ADA, Open. Each owed field is held to
67
+ its rule: Brief needs Surface, Job, Constraint and Falsifier; Taste needs `Take:` and
68
+ `Ban:` lines with something other than generic words (English and Russian); References
69
+ needs five real-product URLs each with `take:`, or `none found` with `Searched:`;
70
+ `Fork: yes` needs a Rubric section it names, `A:`/`B:` and a `Winner:`; Rubric three
71
+ criteria; Critique region → defect → change triples or `clean render`; Markers a
72
+ `--lint` run with its revision and counts, and no S1; Signature exactly one moment
73
+ with What/Where/Why; ADA the class profile, every G item passing, at most two J FAIL
74
+ on a flagship, no J PASS without a `Labelled set:`, safe zones on an ad. Guidance
75
+ lives in HTML comments, so a copied template fails until it is filled. `Mode:
76
+ declined` with its reason passes on its own. Exit 0 valid, 1 violations as
77
+ `<Field>: <problem>`, 2 usage. `bin/record.js`, Node, no dependencies; its rules
78
+ join `--self-test`. The prose block in `CREATIVE_DIRECTOR.md` is replaced by a
79
+ pointer to both, keeping the doctrine on `Open`.
80
+ - **K6 — `ADA_RUBRIC.md`.** R1–R25, each binary, typed G (a gate no judge outvotes),
81
+ J (a judge on a per-task checklist) or H (the human on the contact sheet), with
82
+ applicability and how to verify (tool → artifact). Profiles: flagship all 25, every
83
+ G PASS and at most two J FAIL with reasons; product the G items R1, R3–R5, R8, R9,
84
+ R12–R17, R19, R20, R22; ad R7, R8, R10, R11, R14, R18, R21, R22 plus safe zones. The
85
+ judge: a checklist per task, pairwise only against an approved reference in both
86
+ orders, three samples, disagreement `uncertain` to the human, J items NOT_ASSESSED
87
+ until a labelled set exists, never above a G-FAIL, and never the agent that
88
+ rendered the surface. Every FAIL is a triple. **The file says plainly that the
89
+ rubric is derived and Apple publishes none**, and cites Apple's award pages with
90
+ the date they were read. Linked from Act 5 and `SKILL.md`.
91
+ - **D4 — the reference sweep is mandatory** for a new design and a redesign of a brand
92
+ or flagship surface (Act 2, `APPLYING.md`, `SKILL.md`), through whichever reference
93
+ servers the session exposes — Refero, Lazyweb, Mobbin where authorized — with an
94
+ empty result recorded as `none found` and where it looked.
95
+ - **First time right**, a new section of `CREATIVE_DIRECTOR.md`: decisions before
96
+ pixels in the record; the forbidden list loaded into context and the linter run;
97
+ one key screen in one state first; feedback only external and specific; a
98
+ re-render budget of one, at most two, then `unresolved` to the person; the
99
+ generator is not the judge.
100
+ - **D5 — `MOTION_DOCTRINE.md` §12, reviewing motion:** a frame strip and a run video,
101
+ timings judged by one `DUR-*` row each, reduced motion turned on and captured again,
102
+ one moment per viewport; `npx hyperframes snapshot` / `compare` as the evidence for a
103
+ rendered file (compare is a review surface, not a gate); `review-animations` by name.
104
+ It feeds the record's Quality and Critique.
105
+ - **D6 — `FIGMA_BRIDGE.md` §3 re-drawn.** "Motion stays in code — Figma has no easing
106
+ variable type" was no longer true: the official Figma plugin (2.2.120) documents
107
+ TIMING and EASING variables, keyframes, animation styles and `get_motion_context`
108
+ snippets. Durations and curves can now be published; motion read back is held to
109
+ the doctrine (Figma's `EASE_IN`, `…_BACK` and `BOUNCY` are banned forms in UI); the
110
+ scroll clock, scrubbed motion and the reduced-motion branch stay in code. Read from
111
+ the plugin's documentation, not confirmed by a call (B-144). New §5: a frame is
112
+ critiqued — `get_screenshot`, the record's rubric, triples, `get_variable_defs` —
113
+ before anyone approves it; and one Figma file per surface (App / Web / ASO). README,
114
+ the Cursor rule and `DESIGN_SYNC_BRIDGE.md` follow.
115
+ - **D10 — the taste profile** is the record's `Taste` field and a guide in the template
116
+ (what we take, what we ban, anti-patterns, the three dials); Act 1 writes it beside
117
+ the brief. A profile of "modern / clean" is empty. A personal profile's content stays
118
+ with its owner.
119
+ - **`MOBILE_SURFACES.md`, the axes matrix:** states × viewport × theme × text size ×
120
+ locale, pairwise coverage plus the mandatory pairs dark × largest text and RTL ×
121
+ narrowest viewport, a capture record per cell, the commands that set each axis; the
122
+ native state matrix is its native case, and a web render still proves no native cell.
123
+ - **D8 — `evals/render/`:** four fixed briefs (landing hero with motion, mobile
124
+ onboarding, product dashboard, paywall) and a runner that counts linter findings by
125
+ severity, validates each director record, and scores the G items source can decide
126
+ (R8, R20, R24), reporting the rest NOT_RUN; `--write` appends a dated row to
127
+ `test/evals/RESULTS.md`, whose new section states the method. The model generation
128
+ step is NOT_RUN in CI; the harness row on the hand-written sample is recorded as
129
+ such (B-142).
130
+ - **B-140 — the kits obey their own floor.** `workbench`'s progress fill slides by a
131
+ translate instead of transitioning `width`, `paperclip`'s lane mark grows by a scale
132
+ instead of `width`/`height`, both rendered before and after with the same geometry;
133
+ `deskmate`'s gradient word carries its waiver and reason. `node bin/cli.js --lint
134
+ kits` runs in `npm test` and in CI: 0 S1.
135
+ - **Provenance:** a new table for sources with no commit to pin — Apple's award pages,
136
+ the judge papers, the Figma plugin's motion skills and the HyperFrames CLI skill,
137
+ each `derived`, dated, with SHA256 where a file was read.
138
+ - **Gates:** `test/record_test.js` (49 cases through the real CLI, every rule watched
139
+ failing on one planted defect before `bin/record.js` existed); `validate_director_record()`
140
+ and `validate_ada_rubric()` with three plants; `test/render_eval_test.js` on the sample.
141
+
142
+ The `SKILL.md` body is 4747/4750 tokens: the new load-on-demand line was paid for by
143
+ tightening the READ FIRST paragraph and the references section.
144
+
1
145
  ## 1.62.0 — a visual floor with reasons and exceptions, and a linter that reads a project against it
2
146
 
3
147
  Generated design kept shipping through reviews that had nothing to point at.
package/README.md CHANGED
@@ -176,10 +176,12 @@ skills.
176
176
  | `SHELEG_DESIGN.md` | The full reference: architecture, layer-by-layer mechanics with code, the exact morph math, the DOM↔WebGL projection bridge, a build-from-scratch recipe, and why each piece works |
177
177
  | `SURFACE_COMPOSITION.md` | Two decisions the pack layer does not make: the six depth layers of a scene, read before writing CSS for a cinematic page; and the handoff to `dataviz`, read before drawing a chart in any pack — token names are not uniform across the thirteen and an undefined custom property fails silently |
178
178
  | `SLOP_MARKERS.md` | The visual floor: 49 markers of generated design (purple gradients, emoji as icons, default faces, the side stripe, the three default looks, banned motion forms…), each with severity, reason, what to do instead and the exception under which it is not slop. Holds with or without a pack; twenty of them are checked by `--lint` |
179
+ | `ADA_RUBRIC.md` | 25 binary checks derived from the Apple Design Awards categories (Apple publishes no rubric; the file says so and cites the pages it read), each typed as a deterministic gate, a judged item or a human one, with profiles for flagship, product and ad surfaces and the rules that keep a model judge honest |
180
+ | `templates/director-record.md` | The director record: one `## <Field>` per decision — brief, taste, references, fork and rubric, critique, markers, signature moment, ADA verdict, what is left open — filled before the first render and checked by `--check-record` |
179
181
  | `MOTION_DOCTRINE.md` | Whether to animate at all, before how: the frequency table that kills motion on high-repetition paths, the easing tree and the `ease-in` ban, the duration ceiling, the forbidden forms, and the reduced-motion contract. `SKILL.md` marks it required before any animation |
180
182
  | `MOTION_PRODUCTION.md` | What changes when motion leaves the page as a file: the four things a rendered asset gives up, the two programmatic-video tools measured side by side with the licence threshold and the pre-1.0 risk stated, which one this library recommends and the three conditions that reverse it, how a pack's token layer crosses the render seam, and where the reduced-motion obligation lands once there is no user to read a media query |
181
183
  | `DESIGN_SYNC_BRIDGE.md` | The Claude Design contract: what a pack sends to claude.ai/design and in what shape, the rule for each of the four reference types, and the border motion does not cross |
182
- | `FIGMA_BRIDGE.md` | The design↔code contract: how a pack's tokens map onto Figma variable collections and modes, how to implement a design without importing raw values, and what cannot cross the border |
184
+ | `FIGMA_BRIDGE.md` | The design↔code contract: the family's token-tier model, how a pack's tokens map onto Figma variables (collections, modes, code syntax, scopes), how to implement a design without importing raw values, what cannot cross the border, and where it overrides Figma's own guidance |
183
185
  | `AI_PRODUCT_PATTERNS.md` | The surfaces a model drives: the five states of a call, streaming instead of spinners, latency, provenance and uncertainty, agent confirmations, and the bans that keep it honest |
184
186
  | `styles/*.md` | The thirty-nine style packs — palette, type, texture, motion tokens, motifs, bans, and the traps each one carries |
185
187
  | `styles/tokens/*.css` | The ready-made token layer per pack, copied verbatim instead of transcribed (`workbench` and `field-notes` each ship a light `:root` plus a `data-theme="dark"` twin) |
@@ -208,6 +210,25 @@ reason is required. `--self-test` plants a defect for every rule. It is a patter
208
210
  reader, not a renderer: the catalogue's `review` rows stay with the render
209
211
  critique.
210
212
 
213
+ ### Check a director record
214
+
215
+ ```bash
216
+ npx sheleg-design-skill --check-record docs/design/pricing/director-record.md
217
+ npx sheleg-design-skill --check-record docs/design/pricing/director-record.md --json
218
+ ```
219
+
220
+ A design's decisions live in a record before the first pixel: a header with
221
+ `surface_class` (`flagship`, `product`, `internal` or `ad`) and one `## <Field>`
222
+ per decision, from the template in the installed skill. The class decides which
223
+ fields are owed — a flagship all sixteen, a product screen Brief, Mode,
224
+ References, Markers and Open — and each owed field is held to its rule, not just
225
+ its presence: a taste profile of "modern, clean" is empty, five references on
226
+ placeholder hosts are none, `Fork: yes` with no rubric compared nothing,
227
+ `Markers` must cite a `--lint` run with its revision and counts, and a flagship's
228
+ ADA verdict fails on any G item. A refusal is a record too: `Mode: declined` with
229
+ its reason passes. Exit 0 valid, 1 with each violation as `<Field>: <problem>`,
230
+ 2 a usage error.
231
+
211
232
  ## What you get out of it
212
233
 
213
234
  - **A motion methodology, not a component dump.** Scroll-linked animation,
@@ -249,15 +270,20 @@ now that anyone can prompt their way to a prototype.
249
270
  Design files and design tokens are two encodings of one system, and the usual
250
271
  outcome is that they drift until nobody trusts either. The skill's rule is that
251
272
  **the pack is the source of truth on both sides**: publishing writes a pack's
252
- values into Figma variable collections; implementing a design maps the file's
253
- values *onto* the pack's tokens instead of inlining hexes.
273
+ values into Figma variables — hidden primitives under semantic aliases, each alias
274
+ carrying its CSS custom property as code syntax and a scope set per token family,
275
+ so the code Figma hands back names `var(--accent)` instead of a hex; implementing a
276
+ design maps the file's values *onto* the pack's tokens instead of inlining hexes. A
277
+ redesign starts by capturing the shipped UI into the surface's file, where every
278
+ value the capture cannot bind to a variable is a raw value in the code.
254
279
 
255
280
  The bridge is specific because the traps are: `workbench`'s light and dark are
256
281
  two **modes of one collection**, while `editorial-luxury`'s espresso sections
257
282
  are surfaces and not a mode at all; Figma colors are 0..1 floats, not hex; and
258
- motion never crosses — Figma has no easing variable type, so the ease, durations
259
- and stagger stay code-only, and shadows are effect styles whose parts bind to
260
- variables. A value in a file with no matching token is either a gap in the pack
283
+ motion crosses only in part — durations and curves publish as Figma's TIMING and
284
+ EASING variables, but the scroll clock, scrubbed motion and the reduced-motion branch
285
+ stay code-only, and motion read back from a file is held to the doctrine — and
286
+ shadows are effect styles whose parts bind to variables. A value in a file with no matching token is either a gap in the pack
261
287
  or drift in the file — the one thing it is never is a literal in a component.
262
288
 
263
289
  ## Claude Design, in one direction
@@ -283,18 +309,22 @@ stops an agent inventing motion to fill the silence.
283
309
  The kits are not part of the install. `--kit` fetches one on demand, which is how
284
310
  the skill stays documentation while still having real components to hand.
285
311
 
286
- ## Optional: Lazyweb MCP
312
+ ## Reference sweeps (Refero, Lazyweb, Mobbin)
287
313
 
288
314
  A style pack locks *how it looks*. It says nothing about what a good version of
289
- the screen you're about to build actually **contains**. If the
290
- [Lazyweb](https://www.lazyweb.com) MCP server is connected, the skill sweeps
291
- real-world references for the target screen (signup, onboarding, paywall,
292
- pricing, checkout, dashboard, settings) before laying anything out.
315
+ the screen you're about to build actually **contains**. For a new design or a
316
+ redesign of a brand or flagship surface the sweep is not optional: through
317
+ whichever reference servers are connected — Refero, [Lazyweb](https://www.lazyweb.com),
318
+ Mobbin where the account is authorized — the skill looks at five or more shipped
319
+ products for the target screen (signup, onboarding, paywall, pricing, checkout,
320
+ dashboard, settings) before laying anything out, and records what it takes from
321
+ each. An empty sweep is recorded as `none found` with where it looked.
293
322
 
294
323
  The division of labor keeps the result one system: references inform **layout,
295
324
  hierarchy and content order**; palette, type and motion stay the pack's. Setup
296
325
  is a Streamable HTTP MCP server plus a per-user token — keep it out of your
297
- repo. Entirely optional; without it the skill works from the pack alone.
326
+ repo. Without any reference server the sweep runs on the open web, and the
327
+ record says the servers were absent.
298
328
 
299
329
  ## Development
300
330
 
@@ -317,6 +347,9 @@ third of the contract:
317
347
  | `node --check bin/cli.js` | the installer parses |
318
348
  | `node test/installer_test.js` | both installers against throwaway HOMEs, above all the refusal to shadow an installed plugin |
319
349
  | `node test/lint_test.js` | the project linter end to end: every S1 rule blocks on its plant, every quiet case stays quiet, the ratchet and the exit contract; `--self-test` checks the catalogue against the rules |
350
+ | `node test/record_test.js` | the director-record validator end to end: a valid record for every surface class and a declined one pass; one planted defect per rule fails, each on its own field |
351
+ | `node test/render_eval_test.js` | the render eval runner (`evals/render/`) on the committed hand-written sample: every count it must report |
352
+ | `node bin/cli.js --lint kits` | the reference kits obey the floor they ship with: no S1 marker in any kit |
320
353
 
321
354
  Each gate ships a `--self-test` that plants a defect it must catch (`npm run
322
355
  selftest`), rejects an unknown argument instead of silently running the normal
package/bin/cli.js CHANGED
@@ -160,6 +160,7 @@ function parseArgs(argv) {
160
160
  ratchet: null, // --ratchet <budget.json>: per-file budgets that only fall
161
161
  includeTests: false, // --include-tests: lint test files and fixtures too
162
162
  selfTest: false, // --self-test: every lint rule against its planted defect
163
+ checkRecord: null, // --check-record <file>: validate a director record (bin/record.js)
163
164
  error: null, // set → print help and exit non-zero
164
165
  };
165
166
  const seen = [];
@@ -205,6 +206,10 @@ function parseArgs(argv) {
205
206
  const value = a === "--ratchet" ? argv[++i] : a.slice("--ratchet=".length);
206
207
  if (!value || value.startsWith("-")) opts.error = "--ratchet needs a budget file";
207
208
  else opts.ratchet = value;
209
+ } else if (a === "--check-record" || a.startsWith("--check-record=")) {
210
+ const value = a === "--check-record" ? argv[++i] : a.slice("--check-record=".length);
211
+ if (!value || value.startsWith("-")) opts.error = "--check-record needs a director-record file";
212
+ else opts.checkRecord = value;
208
213
  } else if (a === "--json") opts.json = true;
209
214
  else if (a === "--include-tests") opts.includeTests = true;
210
215
  else if (a === "--self-test") opts.selfTest = true;
@@ -221,6 +226,16 @@ function parseArgs(argv) {
221
226
  opts.error = "--self-test runs on its own";
222
227
  return opts;
223
228
  }
229
+ // The record validator reads one file and writes nothing; --json is the only
230
+ // flag it takes, and it does not run beside the linter or an install.
231
+ if (opts.checkRecord) {
232
+ const other = seen.find((x) => x !== "--check-record" && x !== "--json");
233
+ if (other) {
234
+ opts.error = `--check-record reads one record and takes only --json — ${other} does not combine with it`;
235
+ return opts;
236
+ }
237
+ return opts;
238
+ }
224
239
  if (!opts.lint && seen.some((x) => LINT_ONLY.includes(x))) {
225
240
  opts.error = `${seen.find((x) => LINT_ONLY.includes(x))} is only meaningful with --lint <dir>`;
226
241
  return opts;
@@ -328,11 +343,20 @@ ${c("bold", "Project linter")}
328
343
  also counts raw palette classes and hex outside tokens
329
344
  --include-tests Read tests and fixtures too (skipped by default)
330
345
  --self-test Every rule against its planted defect, and the
331
- catalogue against the rules
346
+ catalogue against the rules (the record rules too)
332
347
 
333
348
  Exit: 0 no S1 finding, 1 an S1 finding or a file over budget, 2 usage error.
334
349
  S2 and S3 findings print and do not change the exit code.
335
350
 
351
+ ${c("bold", "Director record")}
352
+ --check-record <file> Validate a director record (templates/director-record.md
353
+ in the installed skill): the fields its surface_class
354
+ owes are present and filled by their rules
355
+ --json The verdict as {valid, surface_class, violations[]}
356
+
357
+ Exit: 0 valid (a declined record with its reason included), 1 violations,
358
+ each listed as <Field>: <problem>, 2 usage error.
359
+
336
360
  ${c("bold", "Default")}
337
361
  Auto-detects: uses .cursor/ if present, else .claude/ if present,
338
362
  otherwise creates .cursor/skills/${SKILL_SLUG}/.
@@ -589,15 +613,28 @@ function main() {
589
613
  return;
590
614
  }
591
615
 
616
+ if (opts.checkRecord) {
617
+ const record = require("./record.js");
618
+ const io = { out: (l) => console.log(l), err: (l) => console.error(l) };
619
+ process.exitCode = record.runCheckRecord({ file: opts.checkRecord, json: opts.json }, io);
620
+ return;
621
+ }
622
+
592
623
  if (opts.selfTest || opts.lint) {
593
624
  const lint = require("./lint.js");
594
625
  const io = { out: (l) => console.log(l), err: (l) => console.error(l) };
595
- process.exitCode = opts.selfTest
596
- ? lint.selfTest(io.out)
597
- : lint.runLint(
598
- { dir: opts.lint, json: opts.json, ratchet: opts.ratchet, includeTests: opts.includeTests },
599
- io,
600
- );
626
+ if (opts.selfTest) {
627
+ // Both validators answer for themselves; either failing fails the run.
628
+ const record = require("./record.js");
629
+ const a = lint.selfTest(io.out);
630
+ const b = record.selfTest(io.out);
631
+ process.exitCode = a || b ? 1 : 0;
632
+ } else {
633
+ process.exitCode = lint.runLint(
634
+ { dir: opts.lint, json: opts.json, ratchet: opts.ratchet, includeTests: opts.includeTests },
635
+ io,
636
+ );
637
+ }
601
638
  return;
602
639
  }
603
640