mirror of
https://github.com/Manavarya09/design-extract.git
synced 2026-09-19 02:41:14 +08:00
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:
@@ -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')
|
||||
|
||||
@@ -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`.
|
||||
Reference in New Issue
Block a user