feat(dna): designlang dna and dna-corpus commands

`dna <url>` places a site in the design space and prints where it landed;
`dna-corpus <urls...>` builds the reference frame it ranks against, so teams
can measure against their own products or competitors rather than the
shipped corpus.

`grade` gains one line naming the nearest design system beside the letter.
A grade with no reference frame is half a claim. It is wrapped so DNA can
never be the reason a grade fails.

One unreachable site during a corpus build is reported and skipped rather
than discarding every other extraction in the run.
This commit is contained in:
Manav Arya Singh
2026-08-24 12:29:27 +04:00
parent b86b490bbd
commit e2e15e9859
2 changed files with 161 additions and 0 deletions
+112
View File
@@ -1359,6 +1359,20 @@ program
const gradeColor = s.grade === 'A' ? chalk.green : s.grade === 'B' ? chalk.cyan : s.grade === 'C' ? chalk.yellow : chalk.red;
console.log('');
console.log(` ${gradeColor.bold(`Grade ${s.grade}`)} ${chalk.gray('·')} ${chalk.bold(`${s.overall}/100`)} ${chalk.gray('·')} ${chalk.gray(url)}`);
// A letter on its own has no reference frame. When a DNA corpus is
// available, say where this design actually sits among real systems.
try {
const { analyze, loadCorpus, DEFAULT_CORPUS } = await import('../src/dna/index.js');
const corpus = loadCorpus(DEFAULT_CORPUS);
if (corpus) {
const { neighbours } = analyze(design, corpus);
if (neighbours.length) {
console.log(chalk.gray(` Nearest of ${corpus.size} systems: ${neighbours[0].url} (distance ${neighbours[0].distance.toFixed(2)}) · designlang dna ${url}`));
}
}
} catch {
// DNA is a bonus line on the grade card, never a reason it fails.
}
console.log('');
for (const f of written) console.log(` ${chalk.green('✓')} ${chalk.gray(f)}`);
@@ -2394,6 +2408,104 @@ program
process.exit(failed.length ? 1 : 0);
});
// ── DNA command — where a design sits in the measured space ─
program
.command('dna <url>')
.description('Place a design in the measured design space — nearest systems, per-axis percentiles, outliers')
.option('-o, --out <dir>', 'output directory', './design-extract-output')
.option('-n, --name <name>', 'output file prefix (default: derived from URL)')
.option('--corpus <file>', 'corpus to rank against (default: the one shipped with designlang)')
.action(async (url, opts) => {
if (!url.startsWith('http')) url = `https://${url}`;
validateUrl(url);
const { analyze, writeDnaOutputs, loadCorpus, DEFAULT_CORPUS } = await import('../src/dna/index.js');
const spinner = ora('Reading design DNA...').start();
try {
const corpus = loadCorpus(opts.corpus ? resolve(opts.corpus) : DEFAULT_CORPUS);
const design = await extractDesignLanguage(url);
const analysis = analyze(design, corpus);
const outDir = resolve(opts.out);
const written = writeDnaOutputs({
analysis,
outDir,
prefix: opts.name || nameFromUrl(url),
});
spinner.stop();
const v = analysis.vector;
console.log('');
console.log(` ${chalk.bold('Design DNA')} ${chalk.gray('·')} ${chalk.gray(url)}`);
console.log(` ${chalk.gray(`${v.order.length} features, ${Math.round(v.coverage * 100)}% measurable`)}`);
console.log('');
if (!corpus) {
console.log(chalk.yellow(' No corpus found — the vector was written, but nothing to rank it against.'));
console.log(chalk.gray(' Build one: designlang dna-corpus <urls...>'));
} else {
console.log(chalk.dim(` vs ${corpus.size} systems (${corpus.name})`));
console.log('');
for (const [axis, p] of Object.entries(analysis.percentiles.axes)) {
const line = p === null ? chalk.gray('not measured') : `${String(Math.round(p * 100)).padStart(3)}th percentile`;
console.log(` ${axis.padEnd(8)}${line}`);
}
if (analysis.neighbours.length) {
console.log('');
console.log(chalk.bold(' Nearest'));
for (const n of analysis.neighbours.slice(0, 3)) {
console.log(` ${n.distance.toFixed(2)} ${chalk.gray(n.url)}`);
}
}
}
console.log('');
for (const f of written) console.log(` ${chalk.green('\u2713')} ${chalk.gray(f)}`);
console.log('');
} catch (err) {
spinner.fail('DNA failed');
console.error(chalk.red(`\n ${err.message}\n`));
process.exit(1);
}
});
// ── DNA corpus builder ─────────────────────────────────────
program
.command('dna-corpus <urls...>')
.description('Extract several sites and write the corpus that `designlang dna` ranks against')
.option('-o, --out <file>', 'corpus file to write (default: the one shipped with designlang)')
.option('--name <name>', 'corpus name recorded in the file', 'default')
.action(async (urls, opts) => {
const { buildAndSaveCorpus, DEFAULT_CORPUS } = await import('../src/dna/index.js');
const file = opts.out ? resolve(opts.out) : DEFAULT_CORPUS;
const designs = [];
for (const raw of urls) {
const url = raw.startsWith('http') ? raw : `https://${raw}`;
validateUrl(url);
const spinner = ora(`Extracting ${url}...`).start();
try {
designs.push(await extractDesignLanguage(url));
spinner.succeed(chalk.gray(url));
} catch (err) {
// One unreachable site should not throw away the rest of the corpus.
spinner.fail(`${chalk.gray(url)} ${chalk.red(err.message)}`);
}
}
if (!designs.length) {
console.error(chalk.red('\n Nothing extracted — corpus not written.\n'));
process.exit(1);
}
const corpus = buildAndSaveCorpus(designs, file, { name: opts.name });
console.log('');
console.log(` ${chalk.green('\u2713')} ${chalk.gray(file)}`);
console.log(` ${chalk.gray(`${corpus.size} systems, vector v${corpus.version}`)}`);
console.log('');
});
// ── MCP server command ─────────────────────────────────────
program
.command('mcp')
+49
View File
@@ -0,0 +1,49 @@
---
description: Place a design in the measured design space — nearest systems, per-axis percentiles, outliers
---
# /dna
Extract a site's design language and locate it in designlang's measured design
space: a 30-feature vector covering colour, type, space, shape and motion.
Unlike `/grade`, which returns an absolute letter, `/dna` is comparative — every
number is a rank against a corpus of real design systems.
## Usage
```bash
designlang dna <url>
designlang dna <url> --corpus ./my-corpus.json
```
Build your own reference frame — your products, your competitors, whatever you
want to be measured against:
```bash
designlang dna-corpus acme.com acme.com/pricing competitor.com
```
## What you get
- `*-dna.json` — the vector, the raw measurements behind it, neighbours, percentiles
- `*-dna.md` — a readable report: where it sits per axis, its nearest design
systems, and the features that make it look the way it does
## Reading the output
- **Distance** is the mean absolute difference across the features both designs
have — `0.14` means the average feature is 14% of its range apart.
- **Percentiles** are ranks within the corpus, not judgements. A design far from
the middle is unusual, which may be exactly the intent.
- **Coverage** tells you how much of the vector was measurable. A comparison
built from a third of the features is a weaker claim, and the report says so.
## Instructions
1. Run `designlang dna <url>` for the URL the user names.
2. Read the emitted `*-dna.md`.
3. Summarise: the nearest systems and what separates them, the axes where the
design is an outlier, and — if the user is trying to hit a target look — which
axes to move and in which direction.
4. If no corpus exists yet, say so and offer to build one with `dna-corpus`.