@calibrate-ds/dna
v0.2.34
Published
Design-system conformance: extract a codebase's design identity (.dna/identity.yaml, with per-rule provenance), then keep source AND rendered output conformant to it. Framework-agnostic: resolvers → normalized IR → generic rules.
Maintainers
Readme
@calibrate-ds/dna
Design-system conformance for codebases that have a design identity — or need one
extracted. dna reads what your codebase already believes (tokens, type scale, spacing
grid, observed colours), records it as .dna/identity.yaml with per-rule provenance
({ value, from, at } — which side held authority, and when), and then keeps both your
source and your rendered output conformant to it.
Requirements
Node.js >= 18.17.
Installation
npm install -g @calibrate-ds/dnaOr run it one-off with no install, via npx — note the full package name (dna
alone resolves an unrelated package on npm):
npx @calibrate-ds/dna checkJSX and TypeScript files are read with the optional typescript package (5.x). A
global or npx dna uses the project's own typescript — the one that resolves from
the project root, as it would for the project's own build — and only when the project has
none, a copy installed next to dna. Nothing extra to install in a TypeScript project.
dna doctor names the copy in use (typescript 5.6.3 (project)). With no usable copy
anywhere, JS/TS files are not read and dna check, dna start, dna doctor, dna inventory
and dna similar each say so with the fix: typescript not found in this project or next to dna — run `npm i -D
typescript@5` in the project (or install dna locally: `npm i -D @calibrate-ds/dna`). A
project whose own typescript lacks the compiler API dna reads (createSourceFile,
ScriptTarget — TypeScript 7's module shape, for example; dna checks for the API, not the
version number) gets dna's copy when dna has one, and every one of those commands — and the
dna_check, dna_inventory, dna_similar and dna_doctor MCP tools — says so:
project typescript 7.0.0 is not supported (requires ^5); using dna's own typescript 5.9.3.
As a library. The package root (import … from '@calibrate-ds/dna') exports the
commands (runLint, runStart, runDoctor, runInventory, runSimilar,
runCheckManifest, …); each looks TypeScript up for the root it is given, as the CLI
does, and runInventory / runSimilar / runCheckManifest return Promises. The
lower-level readers (RESOLVERS, resolverFor) have no root of their own: call
await loadDefaultTypescript(root) once first, or they treat JS/TS as unavailable
(RESOLVERS has only postcss). getTypescript(root) returns the lookup itself
({ ts, version, origin, file, reason, note }) and withProjectTypescript(root, fn) runs
fn with it.
Once installed globally, the dna command is available directly:
dna check # → "No identity found — nothing to check. Run `dna start` …"
dna start # extract the identity + a starter config from this codebase
dna check # now it checks — and it is clean on the code it extracted fromStarting from a Figma design system
If your design tokens live in Figma (via the CalibrateDS plugin or otherwise), dna
doesn't import a token file directly — it only ever extracts from real, checkable
source in your own codebase, the same as everything else it does. The reliable path:
export the plugin's .tokens.css format (real CSS custom properties, including
var() aliases), commit it into your repo, and either add it to tokenSources in
ds-lint.config.json or just run dna start with it present under an includePaths
directory — dna reads it through the same postcss-based extractor it uses for any
other CSS, with the same tier inference, alias resolution, and fabrication guards.
Verified directly against a real, 200+-variable plugin export: colors, aliases, and
tiers all extract correctly with zero manual cleanup. Length-valued semantic tokens
(--font-size-*, --spacing-*, --border-radius-*/--radius-* aliasing a raw scale
via var(), e.g. --font-size-body-md: var(--scale400)) are classified by what they
actually RESOLVE to, not by reference syntax — they route into scales.type,
scales.spacingObserved, and scales.radius respectively, the sections that actually enforce
type/spacing/radius membership, rather than landing in palette or a generic
declaredScale bucket. A plain (non-semantically-named) length custom property still
falls back to scales.declaredScale, used by off-grid-spacing's scale mode. All four
axes — palette, type, spacing, radius — are enforceable from a .tokens.css import with
zero manual cleanup. The CSV/JSON/JS export formats aren't a supported input today;
.tokens.css is the one that goes through dna's real, hardened parser rather than a
bespoke importer.
Commands
dna --help, dna -h and dna help [command] print usage; --help / -h on any command prints that command's usage and exits 0 before anything runs (no writes, no spawned processes). It wins anywhere in the arguments, including where a value would go (dna allow color --help). An unrecognised --flag is named on stderr (dna <cmd>: unknown flag --x — ignored) and otherwise changes nothing; a single-dash token such as -4px is a positional value (dna resolve -4px), never a flag.
dna --version (or dna -v) prints the installed version and exits 0, with no writes. After a command, --version does the same and the command does not run (dna start --version prints the version and writes nothing) — the same rule as --help, which wins if both are given. -v means the version only as the first argument; after a command it is a positional value like any other single-dash token. dna help --version prints the version as well.
A --config path with nothing at it is named with the path dna looked for, resolved from the project root: ✗ config file not found: cfg.json (looked for /home/me/web/cfg.json), exit 1 — the same line with or without --cwd (an absolute --config is already that path, and is named once). dna doctor's rules line says the same: config: cfg.json (missing — named by --config, not found at /home/me/web/cfg.json), with the fix create /home/me/web/cfg.json, or pass an existing --config path. A --config value that exists but is not a file — an empty --config=, or a directory — is the same named failure as a missing one (✗ config file not found: (empty path), exit 1), not a stack trace. dna doctor reports it on its rules line with a fix for that case: <dir> is a directory — give --config the path of a ds-lint config file, or drop --config to read ds-lint.config.json, or --config was given no value — write --config=<path to a ds-lint config file>, …; only a path with nothing at it is told to create <path>. A directory at the default path, with no --config given, is described as what it is (✗ ds-lint.config.json is a directory, not a config file — rename or remove the directory, and on doctor's rules line ds-lint.config.json is a directory, not a config file with the fix ds-lint.config.json is a directory — rename or remove it so dna can read a ds-lint config file at that path): no --config is mentioned, since none was given.
A ds-lint config that is not valid JSON (a trailing comma, a merge-conflict marker, an empty file) is a named failure too: ✗ ds-lint.config.json is not valid JSON (line 3, column 1: Expected double-quoted property name) — nothing was checked, exit 1, no stack trace, from dna check, dna start, dna inventory, dna similar, dna resolve and dna allow (each line ends with what was not done), and as a structured error from the MCP tools (dna_check, dna_similar, dna_inventory, dna_resolve and dna_start_preview return status: "error" with that message; dna_doctor puts it on its rules check; the server keeps running). A config that is JSON but not an object (null, a list, a number) gets its own line: ✗ ds-lint.config.json is not a JSON object (its top level is null) — nothing was checked. dna doctor's rules line names the file, the position and the reason. An invisible or control character in the parser's reason is shown escaped (\ufeff, \u0000). Comments and trailing commas are not accepted. A leading UTF-8 byte-order mark (some Windows editors write one) is ignored, in this file and in check-rendered.config.json and manifest.config.json; when dna start adds inventory.dirs to a config that has one, it keeps the BOM and the file's own formatting.
A flag that takes no value — --audit, --all-findings, --fix, --all, --force, --json, --yes, --no-exec-config — may also be written --flag=1 / --flag=true (on) or --flag=0 / --flag=false (off). When a flag is given more than once the last spelling wins. Any other value is named on stderr (dna check: flag --all-findings takes no value — "--all-findings=yes" ignored …) and the flag is not applied; it is never dropped without a word.
A flag that takes a value — --base, --only, --config (every command), --fresh (figma diff), --cwd (every project command, and mcp), --as and --why (allow) — is written --flag=value or --flag value; the two are the same run. A value flag with no value is named on stderr and the command exits 1 without running: dna check: --base needs a value — --base is the last argument …. That covers a flag that is last, one followed by another --flag (--base --only=lint), and an empty value (--base=, --base ''), so a CI job whose diff base went missing fails instead of quietly diffing against HEAD. The one empty value that is passed on is --config= / --config '', which keeps its own named failure (config file not found: (empty path)). A flag's value is never a command's positional (dna resolve --config cfg.json 8px resolves 8px; dna allow color --config cfg.json '#123456' --as navy --why x adopts #123456), and only a -- token is a flag, so dna resolve -4px and --why "-x" are values. A value that itself starts with -- is written with = (--base=--x); the free-text --as / --why also take it after a space unless it is one of that command's own flags. When a value flag is given more than once, the first value wins, except in allow, where the last wins. dna check --only takes a comma-separated list of the steps lint, cn, render and manifest; a name that is not one of them (--only=lnt), or an empty item (--only=lint,,cn), is named on stderr with the valid steps and exits 1 without running anything, and the MCP dna_check only input answers the same way (status: "error"). --help still wins wherever it appears.
--cwd=<dir> (or --cwd <dir>) runs a project command — check, doctor, start, inventory, similar, resolve, allow, hook install, agent-block, gen-manifest, figma diff, shapes — exactly as if it were started in <dir>: dna check --cwd=web --config=ds-lint.config.json is cd web && dna check --config=ds-lint.config.json, the same output, exit code and files written. dna moves into <dir> before anything runs, so everything that would follow the current directory follows it: a relative --config or --fresh path (read from <dir>, not from the shell's directory), .dna/ and ledgerDir, the git calls, the TypeScript lookup, a theme config that reads a file by a relative path, and a relative CHROME_PATH or PATH entry. Nothing is written outside <dir> that cd <dir> && dna … would not write. A <dir> that does not exist, is a file or cannot be entered (EACCES) is one named error, exit 1, with nothing run or written (dna check: --cwd "web" (/home/me/web): no such directory — nothing was run …), naming the path as given and as resolved; --cwd with no value is named like any value flag. dna mcp --cwd is the MCP server's own flag and is unchanged (see MCP server).
dna start --force over an existing identity heads its output identity re-extracted (✓, or ⚠ when something was skipped), followed by the diff of what changed or identity unchanged; identity created is the first run, or a --force with no identity on disk.
| Command | What it does |
|---|---|
| dna <command> --cwd=<dir> | Run any project command (every command below but mcp) exactly as if it were started in <dir>; a relative --config is read from <dir>. A missing directory or a file is a named error, exit 1, nothing run. See above. |
| dna start | Extract an identity → .dna/identity.yaml (palette with tiers inferred from the resolution graph, type scale, observed-in-usage values annotated for review — the extractor surfaces, never silently drops). An observed colour is named by hue (green, grey-dark) and never takes a key the palette already holds — a declared green makes the observed one green-2. An observed value the palette already holds (declared, or adopted via dna allow/any non-code provenance, compared after colour normalisation) is not re-listed; the summary line counts them (N observed value(s) already covered by palette entries) and dna_start_preview lists them as observedCovered. The type scale does the same for a size observed in CSS usage that an adopted type entry already holds (N observed value(s) already covered by type entries, listed as typeObservedCovered), and so do spacingObserved and the radius scale for a px an adopted spacing or radius entry already holds (already covered by spacing entries / already covered by radius entries, listed as spacingObservedCovered / radiusObservedCovered); a dna allow spacing 12 adoption therefore no longer leaves a spacing-12 beside its step-12 (or beside its gutter, when it was adopted --as gutter). Only adopted (non-code) entries cover a scale value — a Tailwind-theme spacing step does not, and declaredScale (real custom-property declarations) is never filtered. An observed size or radius whose generated name (step-20, radius-8) an adopted entry already uses for a different value takes the next free suffix (step-20-2, radius-8-2), exactly as colours do, instead of being left out of the file. A declared name an adopted entry also holds is the one case where the adopted entry keeps the name and the declared one is not written (a recorded decision survives --force, and dna does not rename a real source identifier) — reported on every run that meets it, never silent: a ⚠ declared name shadowed by an adopted entry: palette.<name> — declared in source as <value> (the value read: <context>, <file>:<line>), but the identity's "<name>" is an adopted entry (from: …) holding <other value> line per name — ending the declared value is NOT in the identity, or, when the adopted value is another declaration of the same property, naming that declaration (a same-value shadow says so instead), N declared name(s) shadowed by an adopted entry — see warnings on the summary line, and declaredShadowed in dna_start_preview ({ rule, axis, name, declaredValue, adoptedValue, adoptedFrom, sameValue, declaredAt?, adoptedMatches? }; palette and type, the two axes with a declared source). Delete or rename the adopted entry to have the declared one listed. Outside .dna/ it changes nothing without asking (#329): the starter ds-lint.config.json for a bare project, inventory.dirs added to an existing config, the agent-instruction block in CLAUDE.md/AGENTS.md and the two .gitignore lines are each a proposed change — see What dna start asks before writing. --yes (or -y) makes them all without asking. |
| dna check | The gate, diff-default: lint (tokens/literals/grid/scale) · cn-merge guard · rendered conformance (headless Chrome, needs puppeteer-core) · structural manifests. --audit for the whole tree, --only=lint,cn,render,manifest, --fix to preview — and --fix --yes to write — the rewrite of every finding with exactly one exact token answer (see dna check --fix), --all-findings to print every lint finding (by default the first 1,000 errors and 1,000 warnings, then a line saying how many more — see A run with more than 1,000 findings), --json to write the result as one JSON document to stdout (see dna check --json), --allow-empty to exit 0 instead of 2 when nothing was checked (see Exit codes), --config=<path> to read a ds-lint config other than ds-lint.config.json — the default file being absent is fine (built-in defaults apply, noted with one ℹ no ds-lint.config.json — built-in defaults line); an explicit --config path that does not exist fails loudly instead (exit 1, names the path) rather than silently falling back to defaults. --config is honoured the same way by every other command that reads the ds-lint config — doctor, hook install, similar, inventory, start, resolve, and allow — not just check. Every step that builds the token model — lint, cn and render, each on its own under --only= too — names a tailwind.config.* it did not read (⚠ config-load-error: … / ⚠ config-not-read: …) and prints ⚠ instead of ✓: see Theme config that fails to load. An ignore with no reason that suppresses a finding turns the lint headline ⚠ too, with a count (— 1 ignore has no reason), the exit code unchanged: see Ignoring findings. An identity entry whose from is not code and whose value the code declares otherwise (a pinned value) gets one ⚠ N pinned identity entr… differ… from the code line, every run, the exit code unchanged. When the identity holds unreviewed values, the run ends with one ℹ N identity values are unreviewed (…) — run \dna doctor` to see themline; it never changes the exit code or any step's verdict. |
|dna resolve | Reverse-lookup a raw value → identity token. Given a colour (#2E7D32, #2e7d32, or the equivalent rgb(...)— all resolve identically; alsohsl(), hwb(), oklch(), oklab(), lch(), lab(), color() — see [Colour forms](#colour-forms)) or a length (16px/1rem), prints the matching identity token name(s) labeled by axis; a value on multiple scales (type/spacing/radius) reports **all** of them, and a colour held by several palette entries (card, popoverandpaperall#ffffff, stored as hex, an opaque 8-digit hex, hsl(), or rgb()/rgba()with commas or spaces) reports **every** name, sorted by name. A size held by several tokens is the same:dna resolve 16pxwith--font-size-bodyand--font-size-leadboth 16px lists both undertype, one line each, sorted by name (code unit, as the colour names are), with the N tokens hold this valuenote; likewise several radius tokens, several named spacing steps (two adopted steps, or two design-token-file tokens, at one size) and a font family held by twofontsentries. A size held by one token per axis prints as it always did.prefer-token-classandbuiltin-type-size name the same holders (use one of your type tokens ("font-size-body", "font-size-lead"), at most five, the rest counted). An opaque colour is one colour in every spelling: #336699, #336699ff, rgb(51,102,153), rgb(51 102 153)andrgb(51 102 153 / 100%)all resolve to the same entries. A translucent one is not the opaque colour:rgba(51,102,153,0.5)and#33669980 name only entries holding that translucent value, and a fractional channel (rgb(51.4, 102, 153)) is not rounded to a hex. When the identity has a fonts block, one font family name (dna resolve "Alpha Sans", quoted or not, any case) resolves on the fontaxis to the entry holding it; a generic family is answered as always legal. The proactive counterpart todna check— author in the token vocabulary first pass instead of writing a literal and getting it flagged after the fact. Tri-state and honest: a well-formed value that isn't in your identity prints "not a token" (no green tick), no identity prints thedna start prompt, and malformed input is a clean error. Relative colour syntax (rgb(from #123456 r g b)) is answered as a computed colour with no value, exit 0, and a malformed one (rgb(from nonsense garbage)) is a clean error, exit 1 — see [Colour forms](#colour-forms). When a tailwind.config.among the token sources was not read, the answer is followed by a⚠ config-load-error: …/⚠ config-not-read: …line — the lookup then covers only what.dna/identity.yamlrecords, and a value only that config declares comes back "not a token" (exit code unchanged;configLoadon the result). A colour or length that is not a token is followed by what it is near — the palette tokens within ΔE2000 2.0, the spacing and type steps within 2px, orno token within ΔE 2.0— see [Near matches](#near-matches-nearmatch). Detector only — reads the identity, writes nothing. |
|dna allow --why "" | Adopt a deliberate arbitrary value (type/radius/color/spacing, or a font family: dna allow font "" --why "…", see [font-family-allowlist](#font-families-the-fonts-block-and-font-family-allowlist)) directly into identity.yaml's enforced section — immediate effect on the next dna check, no dna startre-run, no hand-editing YAML. It is the decision that clears an observed value's REVIEW note: the value leavesdna doctor's unreviewed-values at once, and the next dna start --forcewrites the adopted entry in place of the observed one (see [Unreviewed identity values](#unreviewed-identity-values) for the other decisions). A Tailwind colour family is kept withdna allow family --why "…" (white, black, slate, blue, …): it adds the family to the ds-lint config's rules["color-family-allowlist"].allowedFamilies— a file outside.dna/, so it asks first in a terminal, --yes/-yaccepts, and with no terminal nothing is written and the change is printed with its command — and, with that change, writes the family'sfamilies.allowedline inidentity.yaml with the reason in place of REVIEW, the decision ledger line and the seed line (see [dna allow family, step by step](#unreviewed-identity-values)). --why takes the real reason: an empty one, and the placeholder a finding prints (--why "…", or ...), are refused with nothing written. --as names it on **every** axis (required forradius/color; typeandspacingdefault tostep-). A name follows the same rule as a token name (letters, digits, _, -, non-ASCII letters — my_token, 9lives, Upper— see [Custom property names](#custom-property-names)), so any keydna startwrites can be passed to--as; it may not start with -(that is a mistyped flag), and a name the identity file could not read back is refused with nothing written. Onspacingthe name is the entry's key inscales.spacingScaleAdopted (dna allow spacing 14 --as gutterwritesgutter, and dna resolve 14px/prefer-token-classthen saygutter; an un-named step- stays a px-only step, since that label is generated, not chosen). A name that already holds a *different* value is refused on every axis (exit 1, nothing written) — pick another name. Relative colour syntax (dna allow color "rgb(from #123456 r g b)") is refused by name, exit 1: it is computed, never a value, and would match nothing (see [Colour forms](#colour-forms)). A colour is compared with the palette after whitespace normalisation, so rgba(#aabbcc,0.5)is already legal oncergba(#aabbcc, 0.5)is adopted. If an **adopted** entry (or one of the three seeded radius idioms,999px/9999px/50%) already holds the value nothing is written, and when you passed --asthe output says the name was not applied and which entry holds the value. A value that is on the scale *without* an adoption — a type step or radius step extracted from source, a radius idiomdna startobserved in source (a value of 100px or more) or someone typed intoradiusIdioms, a declared or observed colour, a scales.spacingScale value the theme declares or someone typed in — is **recorded as adopted** (✓ recorded as adopted — … was already in scales.type (as "body"), but without an adoption …): an extracted value is removed by dna start --forceonce source stops declaring or using it, and the adoption is what keeps it. Onspacingthe array line is left as it is and thespacingScaleAdoptedentry is written; on the other axes a new entry is written under your--as name, or, when that name is the extracted entry's own (dna allow type 16on astep-16, dna allow color "#111" --as inkon a declared--ink), that entry is **re-attributed in place** rather than written twice: frombecomes"dna allow", attoday,reasonyour--why; its tier/resolvesand the hand-written part of its trailing comment are kept. The output says so, and when the entry carried areasonor afromother thancode(recorded by hand, or stamped by a seed rule) it names what was replaced —! palette.ink: recorded provenance { from: code, at: 2025-01-01, reason: "body text" } replaced by this adoption → { from: "dna allow", at: …, reason: "keep ink" }— and the same three fields are kept in that adoption's.dna/decisions.jsonlline underreplaced. When the name is one **source declares** (a custom property, a theme fontSizekey), an in-place adoption means the entry no longer follows the source declaration — an adopted entry keeps its recorded value — and every laterdna start --forcereports it as a declared name shadowed by an adopted entry. Use a different--asname to keep both the declared token and a durable copy. The entry is written exactly where and howdna startrenders it (a block's adopted entries are sorted by name;spacingScale/spacingScaleAdoptedsit directly underspacing:), so a dna start --forcestraight afterwards reports "identity unchanged" — or a real diff, when the value you adopted is in use and its observed duplicate is dropped. Stamps provenance and appends to.dna/decisions.jsonl (and, when [provenanceSeed](#provenanceseed--importing-prior-authorship) is configured, one line to that seed file: the output names it as seed line appended → ). Adoptions survive dna start --forceon **all four axes**, value *and* provenance verbatim, whether or not a fresh scan still finds the value in source — the survival rule is by PROVENANCE SOURCE (anyfromother thancode, so dna allow entries and hand-recorded ones alike — a hand-recorded one that still holds a seed rule's exact stamp is the seed's, see [from, seed rules and hand fixes](#from-seed-rules-and-hand-fixes)), not by axis. A value the fresh scan no longer sees is kept and annotated adopted; not seen by the static scan (may be used from data or dynamically) — delete only if obsolete, never silently dropped and never silently relabelled from: code. (That note deliberately does not claim the value is unused: a static scan cannot see a colour held as data in a TS file and rendered dynamically.) |
| dna inventory [--json]| The component vocabulary, parsed fresh from code: name · props · use-when · path, and each entry'skind (component, or re-exportwith itssource). Every exported capitalised binding under inventory.dirsis listed or named as skipped — see [The component library](#the-component-library-what-dna-inventory-and-dna-similar-read). Read it at session start — the component failure mode is ignorance, not defiance. |
|dna similar | Two advisory duplication checks over the same scorer (role-normalized shape + class shape, asymmetric containment), diff-default (--auditfor the whole tree). **(1) Rebuild of an existing component** — changed JSX scored against the component library — the same files and componentsdna inventorylists ([The component library](#the-component-library-what-dna-inventory-and-dna-similar-read)) — ("you rebuilt by hand"). **(2) Clone of a net-new shape** — near-identical subtrees clustered across the scanned files themselves, reporting a shape repeated ≥2× with *no* backing component ("extract one and reuse it") — the case rebuild-matching is structurally blind to, since neither copy has a library entry to match against. **Both are advisory (exit 0), never gating** — see MAINTAINERS.md non-negotiable #2. See [similarconfig](#similar-config) below to tune it. |
|dna shapes [--json] [--allow-empty] | Derive a component set from committed design-frame dumps (distribution-ranked, rule of three). With no dump configured, or a dump that is missing, it is skipped and exits 2 (--json: { "status": "skipped", "reason", "message", "exitCode" }); see [Exit codes](#exit-codes). |
| dna gen-manifest | Structural screen manifests from design frames (Sec-children, y-order). |
|dna figma diff --fresh= [screen]| **Advisory, optional, hermetic** structural change detector. Diffs a freshly-produced dump against the committed baseline dump and reports theSec-*section delta per screen — sections **added**, **removed**, and **reordered** (same set, new y-order — a distinct category). See below. |
|dna hook install [--force] [--strict] [--config=]| Pre-commit hook runningdna check --only=lint,cn— the static gate only;render(headless Chrome) andmanifestare too slow/environment-dependent for every commit and belong in CI or an explicitdna check --only=render,manifestinstead. Prefers the project-local./node_modules/.bin/dnaso the hook runs the version yourpackage.jsonpins, falling back todnaonPATHfor global-only installs. With dna installed in neither place the hook printsdna: not installed (run npm install) — commit not checkedand lets the commit through (it used to exit 127 and block every commit);--strict writes the form that blocks instead (dna: not installed (run npm install) — commit blocked (hook installed with --strict)). The hook runs DNA_HOOK=1 dna check …: under that variable dna checkappends nothing todecisions.jsonl(the committed ledger, which a hook never stages, so every commit used to leave it modified) and does not writestate.json(sodna check --auditstill records the decisions, and the hook run says how many it left unrecorded);log.jsonlis still written (it is gitignored). A hook run judges what the commit holds: the staged content (the index git commits, whichgit commit -aandgit commit -- fill too) against HEAD, never the working tree, so an unstaged edit neither blocks nor passes a commit, and an untracked or git-ignored file is never read; its headline saysstaged vs HEAD. On a repository's first commit (no HEAD yet) it says no baseon one line and judges every staged value as new (before 0.2.34 a first commit failed withcannot resolve base ref 'HEAD'). dna checkby hand,--base, --fix(it rewrites the working tree) and the MCPdna_checkstill read the working tree. A hook written by dna 0.2.30 or earlier does not setDNA_HOOK=1, so it still judges the working tree until it is rewritten (see below). A hook run also warns, and never blocks, when package.jsonpins@calibrate-ds/dna(dependencies or devDependencies; an exact version,^x.y.zor~x.y.z) to a version the running dna does not satisfy. The hook file is the one git runs (git rev-parse --git-path hooks/pre-commit), so a linked git worktreeinstalls into the shared hooks directory instead of crashing. Run from the top of the repository: from a directory below it nothing is written (exit 1) and the top is named —✗ is inside the git repository at , not at its top — run dna from ; for a dna project of its own below the top (a monorepo package) it writes that package's own marked block — # >>> dna hook install () >>>…# <<< dna hook install () <<<, holding (cd '' && unset GIT_DIR GIT_WORK_TREE && <dna's check line>) || exit 1— into the pre-commit file git runs at the top, only with--forceor a yes at the prompt in a terminal (otherwise it prints the block, writes nothing and exits 1); what else is in that file is kept, a re-run changes only that package's block, and a top-level install then keeps its own(.)block instead of replacing the file, and dna's whole-file hook at the top becomes that(.)block in the package's write, so an older dna refuses the file instead of replacing it;dna check --base=anddna check --auditrun there are named for CI (see [The pre-commit hook andcore.hooksPath](#the-pre-commit-hook-and-corehookspath)). With git's own .git/hooks: refuses to overwrite an existing .git/hooks/pre-commitit didn't write itself — pass--forceto replace a foreign hook: dna first copies it, byte for byte, topre-commit.dna-backup-in the same directory and prints that path (a copy is made only past every refusal below, and a name that exists is never overwritten; a hook dna wrote itself is replaced without a copy). Hooks written by dna 0.2.30 and earlier are still dna's own:dna doctorcalls them outdated, anddna hook install(the whole-file hook) ordna hook install --force(a block or line in a hooks file) rewrites them to the current form. When.git/hooks/pre-commitis a symlink, dna never writes through it to a file outside the project or one git ignores,--forceor not: it names the link and its target and says to replace the link with a regular file or to install in the repository that owns the target; a link to a file inside the project that git does not ignore is written through, the link kept (see [When.git/hooks/pre-commitis a symlink](#when-githookspre-commit-is-a-symlink)). Re-running over its own prior install proceeds without--force: a hook that would be byte-identical is reported already up to dateand not rewritten; one that differs is replaced, and the output says the previous dna-installed hook was replaced (so a hand-edit made directly to it isn't silently lost — this includes replacing a hook that was installed with a *different*--config). With core.hooksPathset (a hook manager such as husky, or a hand-set directory) git never runs.git/hooks, and the pre-commit file there is not dna's: without --forcenothing is written, and dna prints the directory, the reason and the one line to add;--forceappends that line as a marked block and never changes what was already there — see [The pre-commit hook andcore.hooksPath](#the-pre-commit-hook-and-corehookspath). Pass --config=to have the installed hook's owncheckcall carry that same--configon every commit, so a project reading its ds-lint config from a non-default location stays covered by the pre-commit gate too; omitted, the hook body is byte-identical to before this flag existed.dna startprints a one-line reminder to run this when the project is a git repo and no dna-managed hook exists yet. |
|dna agent-block| Insert or refresh the dna agent-instruction block (the same onedna startproposes, and writes with a yes or--yes— see [Whatdna startasks before writing](#what-dna-start-asks-before-writing)) inCLAUDE.md/AGENTS.md, without re-extracting an identity. It is the command a non-interactive dna startprints for that change, and it writes without asking: running it is the yes. Inserts the block if the delimiters are absent (creatingCLAUDE.mdif neither file exists), replaces only the delimited region if present, and writes nothing when the computed content is already byte-identical — run twice and the second run reports "already up to date". The file is written atomically (a temporary file beside it, then a rename). ACLAUDE.md/AGENTS.md that is a link to a regular file inside the project (CLAUDE.md -> AGENTS.md) is written through its real target: the block goes into that file and the link stays. A link that leads outside the project, a dangling link and a path below a directory link that leads outside are refused with a named message and exit 1, never written through. dna startrefuses them the same way: it names the file, exits 1, reports thesetupentry asoutcome: "refused", and keeps the identity and everything else it wrote. Works with or without an identity present, since the block's own wording never depends on one. dna checkwarns (non-failing) when an identity exists but the block is missing from both files, naming this command as the fix. |
|dna doctor [--json] [--config=]| One-screen integration status: is the harness actually wired in, not just installed? Each line is one of four statuses with the one fix command:ok(present and right — nothing to do, never carries afix), warn(present but not right — action recommended: a stale identity, a pre-commit hook whose body is outdated, or a local/PATH version mismatch),missing(not there at all), orunknown(can't be determined, e.g. no git — never a guessed state). Includes: (1) identity present + age/staleness (stale →warn, names dna start --force; the count is the commits touching checked source after the identity's last commit — checked source being the scope dna checkreads: the ds-lint config'sincludePathsminus itsexcludePaths(the one passed with--configincluded) — or touching the files the identity is read from wherever they sit: every filetokenSourcesmatches (stylesheets, design token files, the theme config — a file deleted since included) and the ds-lint config the command loaded (the defaultds-lint.config.json, or the one passed with --config); a commit touching several of them counts once, and when a counted commit touched one of those files outside checked source the line says N commit(s) have touched checked source or the files the identity is read from (token sources, theme config, ds-lint config) since, naming only the kinds that moved (with nothing but checked source moved it reads have touched checked source since, as it always did); a tokenSourcespattern git cannot be asked about — not a string, one dna cannot read (a NUL character, an unclosed[, a ` at the end, an absolute path), empty or outside the project — is not counted and is named, and so is an includePaths or tokenSources entry that matches only because the file system ignores case (tokenSources "Styles/tokens.css" (matches styles/tokens.css only because this file system ignores case), includePaths "Src" (…), advice write it as "styles/tokens.css"): ⚠ not counted for staleness: tokenSources "<pattern>" (<reason>) … in dna check, the same words on the identity line in dna doctor (warn, never ok; the advice is to list the source by a path inside the project, or to take the entry out if its commits need not mark the identity stale — dna then no longer reads its tokens either; no setting accepts the line, which stays while the entry does — or, for an entry dna cannot read, to write it as a string file or glob relative to the project root), and stalenessNotCounted: [{ pattern, reason }] in dna_check and --json/dna_doctor (a case entry also carries write, its spelling on disk, and an include entry key: "includePaths"); a user's GIT_LITERAL_PATHSPECS (or GIT_GLOB_/GIT_NOGLOB_/GIT_ICASE_PATHSPECS) does not change what is counted; dna check, dna doctor and dna_doctor give the same count (a rename counts when either of its paths is in scope; a merge counts only when a file in scope differs from all its parents — a conflict resolution, or an edit made in the merge — and a merge that only brings in a branch's commits counts nothing, those commits being counted themselves; if git does not answer within its time limit, prints more than its output limit or fails (a repository git refuses to read, a corrupt object, git missing beside a .git), the count is not made and says so, with git's reason: ⚠ identity staleness not checked — … in dna check (stalenessNotChecked in dna_check, with stalenessNotCheckedFix, the fix dna doctor gives), an unknown identity line in dna doctor, never ✓; for a failure the line also says what to fix — for a repository another user owns, git's git config --global --add safe.directory '<dir>' — while no repository at all, or one with no commit yet, still gets no staleness line — a branch ref git cannot read is not "no commit yet": it is not checked, with the fix); a ds-lint config that cannot be read leaves the line unknown, staleness not checked — and a depth-1 clone, where that commit cannot be told apart from the clone boundary, gets no staleness count at all, while a deeper shallow clone counts only the commits after its boundary, so its line says at least N commit(s) and at least N day(s), names the shallow clone, and dna_check's staleness carries historyTruncated: true — unless the clone holds the identity's commit and every commit after it, when the count is exact and printed as in a full clone) and duplicate keys (a key written twice in one block of identity.yaml → warn, naming each key with its line numbers and values and which one is in effect — the first; --json/dna_doctor carry them as duplicateKeys) and text the identity parser does not read (a line under no block it reads, a field it does not carry, text after an entry's closing brace … → warn, each with its line number and text, the first 8 then … and N more; unreadText in --json/dna_doctor), (2) .dna/identity.yaml tracked in git (not ignored), (3) agent-instruction block present in CLAUDE.md/AGENTS.md, (4) pre-commit hook present, dna-marked, and whether its body matches what dna hook install currently ships (outdated body → warn, names dna hook install, no --force needed to replace dna's own hook; a .git/hooks/pre-commit that is a symlink to a file outside the project or one git ignores is named with its target, and the fix is to replace the link, never --force) — in the hooks directory git runs: with core.hooksPath set the line names that directory and says a .git/hooks/pre-commit is not run (naming a dna hook left there), is missing until dna's line or block is in the pre-commit file git runs and ok naming that file once it is, and is warn when the line is there but would not run (the file is not executable, has Windows line endings, is not a shell script, or a column-0 line before it may end the script) or the block is outdated; an empty or unusable core.hooksPath is missing — git runs no hooks (see The pre-commit hook and core.hooksPath); git that cannot be asked → unknown, (5) dna mcp registered in a project-level .mcp.json (per-user client configs report "unknown — not checked", never guessed), (6) a CI step invoking dna check — a heuristic scan of .github/workflows/*.yml, .gitlab-ci.yml, pipelines/*.yml, each folder in path order, so with several files that run it the line names the first in that order on every machine; "not detected" is not "absent" (an indirect invocation, e.g. via an npm script, isn't seen), (7) which dna would actually run — ./node_modules/.bin/dna vs. dna on PATH (mismatch → warn, names running via the local install or aligning the global; with no local install, a dna on PATH older than the dna running the check → warn too, naming both versions, saying the pre-commit hook falls back to the PATH one, and giving the fix — install dna in the project, or update the global; if the project's package.json declares @calibrate-ds/dna, the declared range is named as information; an equal or newer PATH dna stays ok; a PATH dna whose version cannot be read, or is not a plain x.y.z, is unknown and says it was not compared), then a typescript line: which copy reads JS/TS files — typescript 5.6.3 (project) (the project's own) or (dna install) (the one next to dna, when the project has none) with its file, ok; the project's own copy lacks the compiler API dna reads (TypeScript 7's module shape, for example) and dna's was used → warn, naming both versions; none usable while a JS/TS file is in scope (the scope dna check lints) → missing, with the same reason dna check and dna start print (with no JS/TS file in scope it is not needed: ok, typescript not found; not needed: no JS/TS files in scope) (typescript: { version, origin, file, note? } or { version: null, origin: null, reason } in --json/dna_doctor), (8) the rules line — which ds-lint config it actually read (--config, same flag and same loadConfig dna check --config uses; omitted reads the default ds-lint.config.json, missing default → "config: none found — built-in defaults"; a named --config path that doesn't exist → missing, not unknown, naming the path) plus which rules are active/off and why, and — only when it applies — (9) theme-config: a tailwind.config.* among your tokenSources that could not be loaded (warn, naming the file and the error; --json/dna_doctor carry themeConfig: { file, reason, detail }), and/or further ones that matched and were not read because dna reads one (warn, naming each; themeConfigsNotRead: [{ file, reason: "config-not-read", detail }]). The line is absent when the one config loads or there is none, so every other project's doctor output is unchanged. Doctor imports the config (its code runs, as under dna check) through the same loader dna check and dna start use, which writes nothing, so the line says what dna check would. With no identity yet, the warn says a dna start would extract one without the config rather than talking about identity.yaml. (10) tokenScopes — a token-scopes line, present when the config sets tokenScopes or, without the key, when a custom property in the token sources is declared with more than one value: then warn with the count, the first 8 names, both values of each and the exact "tokenScopes": { "prefer": [...] } snippet that would record the other value plus dna start --force as the fix (redefinedTokens and tokenScopesHint in --json/dna_doctor); when the dark-mode rule decided a value the line names the variant (darkModeVariant), and an identity that still records the dark-mode values is named stale (tokenScopesStale). With the key: ok with each prefer entry and how many custom-property declarations it matches, warn naming each entry that matches none (tokenScopesUnmatched), each part of the key that is malformed (tokenScopesProblems) and each palette entry whose recorded value the key no longer chooses — the identity is older than the key, and dna start --force re-reads it (tokenScopesStale). (11) unreviewed-values — present only when the identity holds unreviewed values: always warn, one row per value — under the reason it is unreviewed, most used first — with where it is used now, the count dna start recorded when that differs, and the dna allow command that keeps it (unreviewedValues in --json/dna_doctor). (11a) review-notes — present only when the # … REVIEW notes in identity.yaml disagree with the rule that list follows (an identity written by an older dna keeps its notes until dna start --force): warn, with the count, the entries that carry REVIEW the rule would not give and those that lack the REVIEW it would give (the first 8 names of each), and dna start --force as the fix; they are the entries on which --force reports a REVIEW change (REVIEW note added / removed, or REVIEW → decided; entries of another from in the spacingObserved and fontSizeObserved blocks are not compared), and after it the line is gone (reviewNotesStale: { total, extra, missing } in --json/dna_doctor; dna check is unchanged). (11b) decisions — present only when a recorded decision drifted: always warn, one row per decision under its kind — a stale or inert ghost-tokens allow, an allow with no decision in the ledger, an ignore that suppresses nothing, an ignore naming an old rule (learned engine), an ignore with no reason, a ledger entry whose ignore is gone — each with its file:line, config key or ledger line, and its fix (decisions in --json/dna_doctor). (12) identity-health — present only when the recorded identity has a health note or the config sets identityHealth: warn with each note (type scale shape, small text, alpha clusters) and the id that acknowledges it, ok when every note is acknowledged with a reason (identityHealth in --json/dna_doctor). (13) unscanned-styles — present only when a style-bearing file is neither checked nor a source for the identity or the config sets unscannedStyles: warn with one row per file — or per directory, for files of a type dna reads — saying what was counted, under the reason it is not checked, ok when every one is acknowledged with a reason (unscannedStyles in --json/dna_doctor). (14) scan-paths — present only when an includePaths or excludePaths entry cannot work: always warn, naming each entry that is ignored (empty, or not a string), each that is outside the project, each include entry under an exclude entry, an exclude entry that is the project root, each that does not exist (an entry holding a glob character is told that entries are not patterns) and each include entry inside node_modules, and also present when a tokenSources entry is one dna cannot read — not a string, a NUL character, an unclosed [, an absolute path — each named once (tokenSources 42 is not a string — not read, tokenSources "styles/[ab.css" is not a pattern dna reads (unclosed "[") — not read, tokenSources "/styles/*.css" is not a pattern dna reads (an absolute path) — not read; patterns are relative to the project root: write it as "styles/*.css"), and when an includePaths, excludePaths or tokenSources entry matches only because the file system ignores case (macOS), named with its spelling on disk (tokenSources "Styles/tokens.css" matches styles/tokens.css only because this file system ignores case — git does not, so a commit to it does not mark the identity stale; write it as "styles/tokens.css"), and when a tokenSources entry the config lists matches no file (tokenSources "styles/tokns.css" matches no file — no token is read through it; not the built-in entries) (scanPathProblems in --json/dna_doctor). (15) contrast — present only when the identity has a pairs block: warn while a pair is below its WCAG threshold, is not checked or the block is malformed, ok when every pair passes (contrast in --json/dna_doctor). (16) inventory-dirs — present only when an inventory.dirs entry reads nothing: always warn, naming each entry that does not exist, is not a directory or is not a path (inventoryDirProblems in --json/dna_doctor). (17) token-sources — present only when a .json/.tokens file is among the tokenSources or the config sets tokenAxes: ok with the design token files read and their tokens per axis, warn naming each token file not read (not-dtcg, parse-error, unreadable), how many tokens were not recorded by reason, and each tokenAxes entry that does nothing (tokenSources in --json/dna_doctor). (18) provenance-seed — present only when the configured provenanceSeed cannot be used: warn when the file does not exist (no seed rule is applied; dna start goes on and says so), missing when it is a directory, cannot be read, runs through a file, or the key holds no path (dna start and dna allow stop on it and write nothing); the line names the path as configured (provenanceSeed: { path, problem: "missing" | "not-a-file" | "unreadable" | "blocked" | "not-a-path", detail? } in --json/dna_doctor). A project with no provenanceSeed key, or one whose seed file reads, has no such line. When ds-lint.config.json is a directory at the default path (no --config), the rules line says so without mentioning --config. Read-only, advisory: never fixes anything, never writes, exit 0 always. No git repo → lines 2 and 4 report "unknown — not a git repository" rather than a guessed state. Run below the top of a repository, they report unknown naming the top (<dir> is inside the git repository at <top>, not at its top — run dna from <top>); for a dna project of its own there (a package of a monorepo), line 4 checks that package's own marked block in the pre-commit file git runs at the top (ok, warn when outdated or git would not run it, missing with dna hook install --force run in the package as the fix), while line 2 and the identity's staleness are asked of git from the package, as at the top. A git that does not answer is named as such — never "not a git repository". Exposed as the dna_doctor MCP tool with the same structure, plus an optional config input mirroring this --config. |
Exit codes
dna check exits with one of three codes (core/exit-contract.mjs). dna check --json's exitCode and the MCP dna_check tool's exitCode are the same code for the same inputs.
| Exit | Meaning | Examples |
| --- | --- | --- |
| 0 | Checked and clean: no error-level finding (warnings allowed). Also a diff that found nothing to check in what changed, and a run in which some steps checked while others had nothing to check. | ✓ ds-lint clean — 3 file(s), 41 declaration(s) — audit (whole repo) — examined 3 · skipped 0; a commit that changes only README.md; a commit that changes only a .vue, .mdx or .html file, a .css that does not parse, or a .tsx with no TypeScript (each named on a ⚠ line); --only=render in a diff with no render code changed; --only=lint,cn with no cn.module (cn is named on an ℹ line). |
| 1 | An error-level finding, or a real failure. | A finding at error; a config that cannot be read; a bad flag or --only value; a render or figma module --only asks for that is not installed; a log or ledger write that failed; --fix with changes not written; a malformed Figma dump. |
| 2 | dna was expected to read something and could not: no step of the run checked anything, and a step that had to check something checked nothing. | --audit where lint examined 0 files or 0 declarations (a JS/TS-only project with no TypeScript; a stylesheet with no declaration; a Vue project, whose .vue files no reader reads). A diff (no --audit) is never 2 for what changed. A run in which every step --only names checked nothing: --only=render with no check-rendered.config.json, or a render run that verified 0 elements; --only=manifest with no manifest config; --only=cn with no cn.module, or with a cn.module whose file is missing. |
When several steps run, the codes combine by precedence: 1 outranks 2, and 2 outranks 0, and 2 is decided for the run as a whole: it is given only when no step checked anything. An error finding beside a step that checked nothing exits 1. A step that checked something beside one that checked nothing exits by the first; the second is named on a closing ℹ line. A step that --only did not name (cn, render or manifest in a plain dna check) is never named and never makes a run exit 2: when it is not configured, it prints its own nothing to check (…) line, as before.
A var() reference counts as a checked declaration: a stylesheet that only uses tokens is clean, never "nothing to check".
A run that exits 2 ends with one line saying which step checked nothing and why:
✗ dna check — nothing was checked: lint checked 0 declarations (examined 1 · skipped 0) — exit 2 (pass --allow-empty to exit 0 when nothing is checked)A run in which something was not checked but the exit code is not 2 ends with an ℹ line instead:
ℹ dna check — lint: no changed file holds anything dna reads; cn was asked for with --only and checked nothing (cn-not-configured) — this does not change the exit codeA diff whose changed files dna is meant to read were none of them examined names each one, grouped by why, and keeps the exit code:
⚠ 3 changed file(s) not checked: app/A.vue, app/B.mdx (no reader for .vue/.mdx) · app/bad.css (parse error) — the exit code is unchanged--allow-emptyturns 2 into 0, which is the behaviour before 0.2.31. The line is still printed, asℹ … — exit 0 (--allow-empty). The MCPdna_checktool takes it as theallowEmptyinput.--jsonanddna_checklist what was not checked innothingChecked([{ step, reason, requested, examined?, skipped?, unexamined? }]; lint's reason isno-files-examined,no-declarations,nothing-changedorchanged-files-unexamined, the last withunexamined: [{ file, reason }]; render'sno-changes), with or without the flag.- The pre-commit hook never blocks a commit for "nothing checked". It runs a diff, and a diff is never exit 2 for what changed, whichever hook runs it, a hook written by 0.2.30 or earlier included, and with no
cn.module. A commit that changes only files dna does not read passes with anℹline; one that changes files dna is meant to read and cannot (a.vue,.mdxor.htmlfile, a.cssthat does not parse, a.tsxwith no TypeScript) passes with the⚠line naming them. Findings still block: a commit with an error finding fails as before. A hook run (DNA_HOOK=1, whichdna hook installwrites since this version) also behaves as if--allow-emptywere given. Rewriting an older hook withdna hook install(ordna hook install --forcefor a block in a hooks file) is still a good idea, for #499's fixes, but not needed for this. - No identity yet is exit 0. A first
dna checkbeforedna startprints the seed message (No identity found — nothing to check. Run dna start …) and exits 0, by design: it writes nothing and has nothing to compare against. - The other commands follow the same contract when they check nothing. Each prints a closing
✗ dna <command> checked nothing — exit 2 …line, and each takes--allow-empty:dna figma diff: no manifest config, no screens, no fresh dump for a screen, or no screen with both dumps. A malformed dump stays 1.dna shapes: noshapes.dump, or the dump is missing.dna shapes --jsonthen prints{ "status": "skipped", "reason": "no-dump-configured" | "dump-not-found", "message", "exitCode" }.dna similaranddna inventory: no TypeScript, or noinventory.dirs.dna inventory --jsonthen prints one JSON document,{ "status": "no-op", "reason": "no-inventory-dirs" | "typescript-unavailable", "count": 0, "components": [], "skipped": null, "message" }(the same keys as its normal output), and the closing line goes to stderr.
dna allow familywith no terminal exits 1. Without--yes, and with no terminal to ask in, it changes nothing, prints the change and the command that applies it, and exits 1.
Every lint summary line carries the coverage counts, examined N · skipped K, also when nothing was skipped (examined N counts the files that includePaths matched and a reader read, in every mode). The nothing to check (…) line, the clean line and the findings headline all carry them.
similar config
dna similar is tuned with a similar config namespace (core/similar.mjs L43-46,
274, 318):
"similar": { "threshold": 0.8, "minEvidence": 8, "exclude": [], "enabled": true, "minCoverage": 0 }threshold(default0.8) — the minimum containment score (how much of a candidate subtree a library component's feature bag explains) for a rebuild match.minEvidence(default8) — the minimum feature-bag size a subtree/component needs to be compared at all; tiny bags match everything, so anything smaller is skipped by both the rebuild and clone passes.exclude(default[]) — library component names too structurally generic to match on (e.g. a plain icon+text link that matches every icon+text row). Excluded from the rebuild pass, and a clone cluster that resembles an excluded component is treated as already backed, so it isn't clone-reported either.enabled(defaulttrue) — setfalseto turn the rule off without losing the rest of the config.minCoverage(default0, disabled — issue #216) — an ADDITIONAL, opt-in floor on top of the rebuild pass's existing, unconditional "size comparability" guard (a candidate must already be ≥50% of the matched component's feature-bag size before its score is even computed — unaffected by this knob). Coverage = the candidate subtree's feature-bag size ÷ the matched library component's feature-bag size (smaller ÷ larger, the same multiset sizes the built-in 50% guard already compares). A rebuild match is reported only when this coverage is ≥minCoverage, in addition tothresholdand the built-in guard — raise it above the default to require more of a component's shape to be present before flagging a small fragment as a rebuild (the case of a small subtree matching a large library component at 80%+ containment while only covering a modest fraction of its size).minCoverage: 0means "at least 0% coverage", which every match already satisfies, so the default changes nothing — output is byte-identical to before this knob existed. Values at or below0.5change nothing either way: the built-in size guard already excludes any candidate below 50% of the component's bag, so the useful range is0.5to1. A match dropped for coverage is never silently dropped: it's counted and printed once per run (N match(es) below similar.minCoverage (<value>) — not reported; raise or lower the knob), plus the dropped pairs (file:line, matched library component, coverage), and carried on the result object (droppedForCoverage: { count, matches }) and thedna_similarMCP payload. Only affects the REBUILD pass (pass 1) — the clone/all-pairs pass (pass 2) clusters candidate subtrees against each other, not against the library, so a per-component coverage floor has no meaning there and pass 2 is untouched by this knob. A value that isn't a number in[0, 1]is reported as a named config warning (similar. minCoverage must be a number in [0, 1] — got …; ignored, using the default (0, disabled)) and the knob is ignored — never an invented fallback.
Since #163, JSX behind a conditional (&&/||/??, both ternary branches) counts as a
structural child like any other, so a component built mostly from conditional sections
is no longer scored as small/generic on that basis alone — an exclude entry added to
work around that specific false-positive class may now be redundant; worth rechecking
before carrying it forward.
The component library: what dna inventory and dna similar read
"inventory": { "dirs": [...] } in the ds-lint config names the folders that hold your
components. dna inventory lists what they export and dna similar matches against it,
and both read them through one definition (core/library.mjs), so they cannot
disagree about which files or which components count:
- Files. Every
.js/.jsx/.ts/.tsxfile under each entry, up to 3 folders down (src/components/Button/Button.tsxis one folder down), skipping hidden folders,node_modules,distandbuild. Each folder is read in path order (the entries in the orderdirslists them), so which of two same-named components is listed first, the order of the⚠ skipped:lines and the spelling of a file reached through a link are the same on every machine. A file reached by two entries is read once, and one within 3 folders of any entry is read. A file more than 3 folders down from every entry is not read and is named:⚠ skipped: <file> — more than 3 folders below inventory.dirs entry "<dir>" — not read(too-deep; an entry such as["src"]on a deep app prints one such line per file). A symbolic link to a folder is followed; one that leads back into a folder being read (symlink-cycle) or to nothing (broken-symlink) is named. Each file is parsed in the grammar of its extension (.tsas TypeScript,.tsxas TSX,.js/.jsxas JavaScript with JSX), so<T>(x: T) => xin a.tsfile is not a parse error.dna checkanddna similarpick the grammar per extension the same way (.ts,.mtsand.ctsas TypeScript, so JSX in a.tsfile is a parse error there too). - Entries. Every exported binding whose name starts with a capital letter is either
listed or named as skipped. Each listed entry has a
kind:component— a function declaration, an arrow or function expression, a call (forwardRef(...),memo(...),styled(...)), a class with arendermethod, and theexport default X/ `export
