mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-13 06:36:26 +03:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e8eea2d91 | ||
|
|
aa1b4e4b02 | ||
|
|
6e6b0227b0 | ||
|
|
a075d89bdb | ||
|
|
e76b3424d2 | ||
|
|
e46e0da885 | ||
|
|
b14df98183 | ||
|
|
6886ab8c0e | ||
|
|
ae5e95101a | ||
|
|
a37b3f6b02 | ||
|
|
d086837dfc | ||
|
|
80e4dd0d58 | ||
|
|
2f609915eb |
@@ -12,6 +12,7 @@ Each of these is a check on the built result, not an intention. Run them togethe
|
||||
- **Type:** body measure 65–75ch, display max 6rem, tracking floor -0.04em, balanced headings, obvious scale and weight steps. Run the real copy at every breakpoint and fix what overflows.
|
||||
- **Motion:** one authored moment, not scattered effects and not one identical entrance on every section. Exponential ease-out from an already-visible default. Reach past transform and opacity: blur, backdrop-filter, clip-path, mask, and shadow belong to the palette when they stay smooth.
|
||||
- **States:** hover, disabled, loading, error, empty. Plus real content, working controls, responsive composition, keyboard focus.
|
||||
- **Browser surfaces:** the parts you did not draw still carry the design. Text selection, the caret, custom scrollbars, focus rings, underline offset, and the numerals in tabular data all ship with browser defaults that belong to no design system. Theme them from the palette. This is the cheapest signal that a page was built rather than assembled, and the one models skip most reliably.
|
||||
- **Copy:** the product's own language. Controls name their action; errors name the problem and the recovery.
|
||||
- **Coverage:** every brief requirement present and findable within seconds.
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { loadContext, extractPlatform } from './context.mjs';
|
||||
import { getCritiqueDir } from './lib/impeccable-paths.mjs';
|
||||
import { readLatestSnapshotAcrossTargets } from './critique-storage.mjs';
|
||||
|
||||
/** Is there code here at all, or just context files / an empty repo? */
|
||||
function hasCode(cwd) {
|
||||
@@ -34,23 +34,13 @@ function hasCode(cwd) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The most recent critique snapshot across all targets. Filenames are
|
||||
* timestamp-prefixed (`<iso>__<slug>.md`), so a lexical sort is chronological.
|
||||
* Parses the small frontmatter for score + P0/P1 counts.
|
||||
* Summarize the most recent critique snapshot across all targets.
|
||||
*/
|
||||
function latestCritique(cwd) {
|
||||
try {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return null;
|
||||
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort();
|
||||
if (!files.length) return null;
|
||||
const newest = files[files.length - 1];
|
||||
const text = fs.readFileSync(path.join(dir, newest), 'utf-8');
|
||||
const front = text.split('---')[1] || '';
|
||||
const get = (k) => {
|
||||
const m = front.match(new RegExp(`^${k}:\\s*(.+)$`, 'm'));
|
||||
return m ? m[1].trim() : null;
|
||||
};
|
||||
const latest = readLatestSnapshotAcrossTargets({ cwd });
|
||||
if (!latest) return null;
|
||||
const get = (key) => latest.meta[key] ?? null;
|
||||
const num = (v) => {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
@@ -61,7 +51,7 @@ function latestCritique(cwd) {
|
||||
p0: num(get('p0')),
|
||||
p1: num(get('p1')),
|
||||
timestamp: get('timestamp'),
|
||||
file: path.relative(cwd, path.join(dir, newest)),
|
||||
file: path.relative(cwd, latest.path),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
@@ -105,28 +105,37 @@ function parseFrontmatter(text) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all snapshot files for `slug`, sorted oldest → newest.
|
||||
* Return snapshot files matching `suffix`, sorted oldest → newest.
|
||||
*/
|
||||
function listSnapshotsForSlug(slug, cwd) {
|
||||
const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/;
|
||||
|
||||
function listSnapshots(suffix, cwd) {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
const suffix = `__${slug}.md`;
|
||||
return fs.readdirSync(dir)
|
||||
.filter((f) => f.endsWith(suffix))
|
||||
.filter((f) => SNAPSHOT_FILENAME.test(f) && f.endsWith(suffix))
|
||||
.sort()
|
||||
.map((f) => path.join(dir, f));
|
||||
}
|
||||
|
||||
function readLatestSnapshotMatching(suffix, cwd) {
|
||||
const filePath = listSnapshots(suffix, cwd).at(-1);
|
||||
if (!filePath) return null;
|
||||
const body = fs.readFileSync(filePath, 'utf-8');
|
||||
return { path: filePath, body, meta: parseFrontmatter(body) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the most recent snapshot for `slug`, or null. Polish reads this
|
||||
* to find its fix backlog when the slug matches.
|
||||
*/
|
||||
export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
if (!all.length) return null;
|
||||
const latest = all[all.length - 1];
|
||||
const body = fs.readFileSync(latest, 'utf-8');
|
||||
return { path: latest, body, meta: parseFrontmatter(body) };
|
||||
return readLatestSnapshotMatching(`__${slug}.md`, cwd);
|
||||
}
|
||||
|
||||
/** Return the most recent snapshot across all targets, or null. */
|
||||
export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) {
|
||||
return readLatestSnapshotMatching('.md', cwd);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,7 +143,7 @@ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
* Critique appends a one-line trend to its output using this.
|
||||
*/
|
||||
export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
const all = listSnapshots(`__${slug}.md`, cwd);
|
||||
const slice = all.slice(-limit);
|
||||
return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8')));
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ export { detectUrl, createBrowserDetector } from './engines/browser/detect-url.m
|
||||
export { detectText, extractStyleBlocks, extractCSSinJS } from './engines/regex/detect-text.mjs';
|
||||
export {
|
||||
walkDir,
|
||||
hasScannableExtension,
|
||||
SCANNABLE_EXTENSIONS,
|
||||
SKIP_DIRS,
|
||||
buildImportGraph,
|
||||
|
||||
@@ -26,11 +26,20 @@ const HIDDEN_SOURCE_DIRS = new Set(['.vitepress', '.vuepress', '.storybook']);
|
||||
const SCANNABLE_EXTENSIONS = new Set([
|
||||
'.html', '.htm', '.css', '.scss', '.sass', '.less',
|
||||
'.jsx', '.tsx', '.js', '.ts',
|
||||
'.vue', '.svelte', '.astro',
|
||||
'.vue', '.svelte', '.astro', '.blade.php',
|
||||
]);
|
||||
|
||||
const HTML_EXTENSIONS = new Set(['.html', '.htm']);
|
||||
|
||||
function hasScannableExtension(filename) {
|
||||
const lower = filename.toLowerCase();
|
||||
if (SCANNABLE_EXTENSIONS.has(path.extname(lower))) return true;
|
||||
for (const ext of SCANNABLE_EXTENSIONS) {
|
||||
if (ext.indexOf('.', 1) !== -1 && lower.endsWith(ext)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const IMPORT_SPECIFIER_PATTERNS = [
|
||||
/import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g,
|
||||
/@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g,
|
||||
@@ -46,7 +55,7 @@ function walkDir(dir) {
|
||||
if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue;
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) files.push(...walkDir(full));
|
||||
else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full);
|
||||
else if (hasScannableExtension(entry.name)) files.push(full);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
@@ -194,6 +203,7 @@ export {
|
||||
SKIP_DIRS,
|
||||
SCANNABLE_EXTENSIONS,
|
||||
HTML_EXTENSIONS,
|
||||
hasScannableExtension,
|
||||
walkDir,
|
||||
resolveImport,
|
||||
buildImportGraph,
|
||||
|
||||
@@ -109,6 +109,18 @@ export function validateConceptEntry(concept, { existingForms = new Map(), axes
|
||||
|| concept.tags.some(tag => typeof tag !== 'string' || !tag.trim())) {
|
||||
errors.push(`concept ${id} must have exactly three structural tags`);
|
||||
}
|
||||
// The slop this world in particular is at risk of. Optional, because 541
|
||||
// entries predate it and none of them are wrong for lacking it. A world built
|
||||
// from posters is at risk of shouting and one built from instruments is at
|
||||
// risk of dead greys; a global detector cannot know which, and the author can.
|
||||
if (concept?.avoid !== undefined) {
|
||||
if (!Array.isArray(concept.avoid)
|
||||
|| concept.avoid.length < 2
|
||||
|| concept.avoid.length > 3
|
||||
|| concept.avoid.some(item => typeof item !== 'string' || item.trim().length < 12 || item.trim().length > 160)) {
|
||||
errors.push(`concept ${id} avoid must be two or three negations of 12–160 characters`);
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(concept?.system)
|
||||
|| concept.system.length !== SYSTEM_PREFIXES.length
|
||||
|| concept.system.some(rule => typeof rule !== 'string' || rule.trim().length < 12 || rule.trim().length > 180)) {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
export function browserOpenCommand(url, {
|
||||
platform = process.platform,
|
||||
comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe',
|
||||
} = {}) {
|
||||
if (platform === 'darwin') return { command: 'open', args: [url] };
|
||||
if (platform === 'win32') return { command: comspec, args: ['/c', 'start', '', url] };
|
||||
return { command: 'xdg-open', args: [url] };
|
||||
}
|
||||
|
||||
export function openSystemBrowser(url, {
|
||||
platform = process.platform,
|
||||
comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe',
|
||||
spawnImpl = spawn,
|
||||
} = {}) {
|
||||
const { command, args } = browserOpenCommand(url, { platform, comspec });
|
||||
try {
|
||||
const child = spawnImpl(command, args, { stdio: 'ignore', detached: true });
|
||||
child.on('error', () => {});
|
||||
child.unref();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -93,15 +93,17 @@ function commandPrefixForSkillsDir(skillsDir) {
|
||||
return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/';
|
||||
}
|
||||
|
||||
function generatePinnedSkill(command, metadata, commandPrefix) {
|
||||
function generatePinnedSkill(command, metadata, commandPrefix, isCodex) {
|
||||
const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`;
|
||||
const hint = metadata[command]?.argumentHint || '[target]';
|
||||
const providerFrontmatter = isCodex
|
||||
? `metadata:\n argument-hint: "${hint}"`
|
||||
: `argument-hint: "${hint}"\nuser-invocable: true`;
|
||||
|
||||
return `---
|
||||
name: ${command}
|
||||
description: "${desc}"
|
||||
argument-hint: "${hint}"
|
||||
user-invocable: true
|
||||
${providerFrontmatter}
|
||||
---
|
||||
|
||||
${PIN_MARKER}
|
||||
@@ -128,7 +130,7 @@ function pin(command, projectRoot) {
|
||||
|
||||
for (const skillsDir of harnessDirs) {
|
||||
const commandPrefix = commandPrefixForSkillsDir(skillsDir);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$');
|
||||
// Check if skill already exists (and isn't a pin)
|
||||
const skillDir = join(skillsDir, command);
|
||||
if (existsSync(skillDir)) {
|
||||
|
||||
@@ -79,6 +79,7 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { openSystemBrowser } from './lib/open-system-browser.mjs';
|
||||
|
||||
function arg(name, fallback = null) {
|
||||
const i = process.argv.indexOf(`--${name}`);
|
||||
@@ -976,8 +977,7 @@ server.listen(portArg, '127.0.0.1', () => {
|
||||
console.log('Waiting for the user to choose in the browser (Ctrl-C aborts)...');
|
||||
}
|
||||
if (!hasFlag('no-open')) {
|
||||
const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
||||
try { spawn(opener, [url], { stdio: 'ignore', detached: true }).unref(); } catch { /* URL printed anyway */ }
|
||||
openSystemBrowser(url);
|
||||
}
|
||||
if (timeoutSec > 0) {
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -12,6 +12,7 @@ Each of these is a check on the built result, not an intention. Run them togethe
|
||||
- **Type:** body measure 65–75ch, display max 6rem, tracking floor -0.04em, balanced headings, obvious scale and weight steps. Run the real copy at every breakpoint and fix what overflows.
|
||||
- **Motion:** one authored moment, not scattered effects and not one identical entrance on every section. Exponential ease-out from an already-visible default. Reach past transform and opacity: blur, backdrop-filter, clip-path, mask, and shadow belong to the palette when they stay smooth.
|
||||
- **States:** hover, disabled, loading, error, empty. Plus real content, working controls, responsive composition, keyboard focus.
|
||||
- **Browser surfaces:** the parts you did not draw still carry the design. Text selection, the caret, custom scrollbars, focus rings, underline offset, and the numerals in tabular data all ship with browser defaults that belong to no design system. Theme them from the palette. This is the cheapest signal that a page was built rather than assembled, and the one models skip most reliably.
|
||||
- **Copy:** the product's own language. Controls name their action; errors name the problem and the recovery.
|
||||
- **Coverage:** every brief requirement present and findable within seconds.
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { loadContext, extractPlatform } from './context.mjs';
|
||||
import { getCritiqueDir } from './lib/impeccable-paths.mjs';
|
||||
import { readLatestSnapshotAcrossTargets } from './critique-storage.mjs';
|
||||
|
||||
/** Is there code here at all, or just context files / an empty repo? */
|
||||
function hasCode(cwd) {
|
||||
@@ -34,23 +34,13 @@ function hasCode(cwd) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The most recent critique snapshot across all targets. Filenames are
|
||||
* timestamp-prefixed (`<iso>__<slug>.md`), so a lexical sort is chronological.
|
||||
* Parses the small frontmatter for score + P0/P1 counts.
|
||||
* Summarize the most recent critique snapshot across all targets.
|
||||
*/
|
||||
function latestCritique(cwd) {
|
||||
try {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return null;
|
||||
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort();
|
||||
if (!files.length) return null;
|
||||
const newest = files[files.length - 1];
|
||||
const text = fs.readFileSync(path.join(dir, newest), 'utf-8');
|
||||
const front = text.split('---')[1] || '';
|
||||
const get = (k) => {
|
||||
const m = front.match(new RegExp(`^${k}:\\s*(.+)$`, 'm'));
|
||||
return m ? m[1].trim() : null;
|
||||
};
|
||||
const latest = readLatestSnapshotAcrossTargets({ cwd });
|
||||
if (!latest) return null;
|
||||
const get = (key) => latest.meta[key] ?? null;
|
||||
const num = (v) => {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
@@ -61,7 +51,7 @@ function latestCritique(cwd) {
|
||||
p0: num(get('p0')),
|
||||
p1: num(get('p1')),
|
||||
timestamp: get('timestamp'),
|
||||
file: path.relative(cwd, path.join(dir, newest)),
|
||||
file: path.relative(cwd, latest.path),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
@@ -105,28 +105,37 @@ function parseFrontmatter(text) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all snapshot files for `slug`, sorted oldest → newest.
|
||||
* Return snapshot files matching `suffix`, sorted oldest → newest.
|
||||
*/
|
||||
function listSnapshotsForSlug(slug, cwd) {
|
||||
const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/;
|
||||
|
||||
function listSnapshots(suffix, cwd) {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
const suffix = `__${slug}.md`;
|
||||
return fs.readdirSync(dir)
|
||||
.filter((f) => f.endsWith(suffix))
|
||||
.filter((f) => SNAPSHOT_FILENAME.test(f) && f.endsWith(suffix))
|
||||
.sort()
|
||||
.map((f) => path.join(dir, f));
|
||||
}
|
||||
|
||||
function readLatestSnapshotMatching(suffix, cwd) {
|
||||
const filePath = listSnapshots(suffix, cwd).at(-1);
|
||||
if (!filePath) return null;
|
||||
const body = fs.readFileSync(filePath, 'utf-8');
|
||||
return { path: filePath, body, meta: parseFrontmatter(body) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the most recent snapshot for `slug`, or null. Polish reads this
|
||||
* to find its fix backlog when the slug matches.
|
||||
*/
|
||||
export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
if (!all.length) return null;
|
||||
const latest = all[all.length - 1];
|
||||
const body = fs.readFileSync(latest, 'utf-8');
|
||||
return { path: latest, body, meta: parseFrontmatter(body) };
|
||||
return readLatestSnapshotMatching(`__${slug}.md`, cwd);
|
||||
}
|
||||
|
||||
/** Return the most recent snapshot across all targets, or null. */
|
||||
export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) {
|
||||
return readLatestSnapshotMatching('.md', cwd);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,7 +143,7 @@ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
* Critique appends a one-line trend to its output using this.
|
||||
*/
|
||||
export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
const all = listSnapshots(`__${slug}.md`, cwd);
|
||||
const slice = all.slice(-limit);
|
||||
return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8')));
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ export { detectUrl, createBrowserDetector } from './engines/browser/detect-url.m
|
||||
export { detectText, extractStyleBlocks, extractCSSinJS } from './engines/regex/detect-text.mjs';
|
||||
export {
|
||||
walkDir,
|
||||
hasScannableExtension,
|
||||
SCANNABLE_EXTENSIONS,
|
||||
SKIP_DIRS,
|
||||
buildImportGraph,
|
||||
|
||||
@@ -26,11 +26,20 @@ const HIDDEN_SOURCE_DIRS = new Set(['.vitepress', '.vuepress', '.storybook']);
|
||||
const SCANNABLE_EXTENSIONS = new Set([
|
||||
'.html', '.htm', '.css', '.scss', '.sass', '.less',
|
||||
'.jsx', '.tsx', '.js', '.ts',
|
||||
'.vue', '.svelte', '.astro',
|
||||
'.vue', '.svelte', '.astro', '.blade.php',
|
||||
]);
|
||||
|
||||
const HTML_EXTENSIONS = new Set(['.html', '.htm']);
|
||||
|
||||
function hasScannableExtension(filename) {
|
||||
const lower = filename.toLowerCase();
|
||||
if (SCANNABLE_EXTENSIONS.has(path.extname(lower))) return true;
|
||||
for (const ext of SCANNABLE_EXTENSIONS) {
|
||||
if (ext.indexOf('.', 1) !== -1 && lower.endsWith(ext)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const IMPORT_SPECIFIER_PATTERNS = [
|
||||
/import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g,
|
||||
/@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g,
|
||||
@@ -46,7 +55,7 @@ function walkDir(dir) {
|
||||
if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue;
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) files.push(...walkDir(full));
|
||||
else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full);
|
||||
else if (hasScannableExtension(entry.name)) files.push(full);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
@@ -194,6 +203,7 @@ export {
|
||||
SKIP_DIRS,
|
||||
SCANNABLE_EXTENSIONS,
|
||||
HTML_EXTENSIONS,
|
||||
hasScannableExtension,
|
||||
walkDir,
|
||||
resolveImport,
|
||||
buildImportGraph,
|
||||
|
||||
@@ -109,6 +109,18 @@ export function validateConceptEntry(concept, { existingForms = new Map(), axes
|
||||
|| concept.tags.some(tag => typeof tag !== 'string' || !tag.trim())) {
|
||||
errors.push(`concept ${id} must have exactly three structural tags`);
|
||||
}
|
||||
// The slop this world in particular is at risk of. Optional, because 541
|
||||
// entries predate it and none of them are wrong for lacking it. A world built
|
||||
// from posters is at risk of shouting and one built from instruments is at
|
||||
// risk of dead greys; a global detector cannot know which, and the author can.
|
||||
if (concept?.avoid !== undefined) {
|
||||
if (!Array.isArray(concept.avoid)
|
||||
|| concept.avoid.length < 2
|
||||
|| concept.avoid.length > 3
|
||||
|| concept.avoid.some(item => typeof item !== 'string' || item.trim().length < 12 || item.trim().length > 160)) {
|
||||
errors.push(`concept ${id} avoid must be two or three negations of 12–160 characters`);
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(concept?.system)
|
||||
|| concept.system.length !== SYSTEM_PREFIXES.length
|
||||
|| concept.system.some(rule => typeof rule !== 'string' || rule.trim().length < 12 || rule.trim().length > 180)) {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
export function browserOpenCommand(url, {
|
||||
platform = process.platform,
|
||||
comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe',
|
||||
} = {}) {
|
||||
if (platform === 'darwin') return { command: 'open', args: [url] };
|
||||
if (platform === 'win32') return { command: comspec, args: ['/c', 'start', '', url] };
|
||||
return { command: 'xdg-open', args: [url] };
|
||||
}
|
||||
|
||||
export function openSystemBrowser(url, {
|
||||
platform = process.platform,
|
||||
comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe',
|
||||
spawnImpl = spawn,
|
||||
} = {}) {
|
||||
const { command, args } = browserOpenCommand(url, { platform, comspec });
|
||||
try {
|
||||
const child = spawnImpl(command, args, { stdio: 'ignore', detached: true });
|
||||
child.on('error', () => {});
|
||||
child.unref();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -93,15 +93,17 @@ function commandPrefixForSkillsDir(skillsDir) {
|
||||
return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/';
|
||||
}
|
||||
|
||||
function generatePinnedSkill(command, metadata, commandPrefix) {
|
||||
function generatePinnedSkill(command, metadata, commandPrefix, isCodex) {
|
||||
const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`;
|
||||
const hint = metadata[command]?.argumentHint || '[target]';
|
||||
const providerFrontmatter = isCodex
|
||||
? `metadata:\n argument-hint: "${hint}"`
|
||||
: `argument-hint: "${hint}"\nuser-invocable: true`;
|
||||
|
||||
return `---
|
||||
name: ${command}
|
||||
description: "${desc}"
|
||||
argument-hint: "${hint}"
|
||||
user-invocable: true
|
||||
${providerFrontmatter}
|
||||
---
|
||||
|
||||
${PIN_MARKER}
|
||||
@@ -128,7 +130,7 @@ function pin(command, projectRoot) {
|
||||
|
||||
for (const skillsDir of harnessDirs) {
|
||||
const commandPrefix = commandPrefixForSkillsDir(skillsDir);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$');
|
||||
// Check if skill already exists (and isn't a pin)
|
||||
const skillDir = join(skillsDir, command);
|
||||
if (existsSync(skillDir)) {
|
||||
|
||||
@@ -79,6 +79,7 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { openSystemBrowser } from './lib/open-system-browser.mjs';
|
||||
|
||||
function arg(name, fallback = null) {
|
||||
const i = process.argv.indexOf(`--${name}`);
|
||||
@@ -976,8 +977,7 @@ server.listen(portArg, '127.0.0.1', () => {
|
||||
console.log('Waiting for the user to choose in the browser (Ctrl-C aborts)...');
|
||||
}
|
||||
if (!hasFlag('no-open')) {
|
||||
const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
||||
try { spawn(opener, [url], { stdio: 'ignore', detached: true }).unref(); } catch { /* URL printed anyway */ }
|
||||
openSystemBrowser(url);
|
||||
}
|
||||
if (timeoutSec > 0) {
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -12,6 +12,7 @@ Each of these is a check on the built result, not an intention. Run them togethe
|
||||
- **Type:** body measure 65–75ch, display max 6rem, tracking floor -0.04em, balanced headings, obvious scale and weight steps. Run the real copy at every breakpoint and fix what overflows.
|
||||
- **Motion:** one authored moment, not scattered effects and not one identical entrance on every section. Exponential ease-out from an already-visible default. Reach past transform and opacity: blur, backdrop-filter, clip-path, mask, and shadow belong to the palette when they stay smooth.
|
||||
- **States:** hover, disabled, loading, error, empty. Plus real content, working controls, responsive composition, keyboard focus.
|
||||
- **Browser surfaces:** the parts you did not draw still carry the design. Text selection, the caret, custom scrollbars, focus rings, underline offset, and the numerals in tabular data all ship with browser defaults that belong to no design system. Theme them from the palette. This is the cheapest signal that a page was built rather than assembled, and the one models skip most reliably.
|
||||
- **Copy:** the product's own language. Controls name their action; errors name the problem and the recovery.
|
||||
- **Coverage:** every brief requirement present and findable within seconds.
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { loadContext, extractPlatform } from './context.mjs';
|
||||
import { getCritiqueDir } from './lib/impeccable-paths.mjs';
|
||||
import { readLatestSnapshotAcrossTargets } from './critique-storage.mjs';
|
||||
|
||||
/** Is there code here at all, or just context files / an empty repo? */
|
||||
function hasCode(cwd) {
|
||||
@@ -34,23 +34,13 @@ function hasCode(cwd) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The most recent critique snapshot across all targets. Filenames are
|
||||
* timestamp-prefixed (`<iso>__<slug>.md`), so a lexical sort is chronological.
|
||||
* Parses the small frontmatter for score + P0/P1 counts.
|
||||
* Summarize the most recent critique snapshot across all targets.
|
||||
*/
|
||||
function latestCritique(cwd) {
|
||||
try {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return null;
|
||||
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort();
|
||||
if (!files.length) return null;
|
||||
const newest = files[files.length - 1];
|
||||
const text = fs.readFileSync(path.join(dir, newest), 'utf-8');
|
||||
const front = text.split('---')[1] || '';
|
||||
const get = (k) => {
|
||||
const m = front.match(new RegExp(`^${k}:\\s*(.+)$`, 'm'));
|
||||
return m ? m[1].trim() : null;
|
||||
};
|
||||
const latest = readLatestSnapshotAcrossTargets({ cwd });
|
||||
if (!latest) return null;
|
||||
const get = (key) => latest.meta[key] ?? null;
|
||||
const num = (v) => {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
@@ -61,7 +51,7 @@ function latestCritique(cwd) {
|
||||
p0: num(get('p0')),
|
||||
p1: num(get('p1')),
|
||||
timestamp: get('timestamp'),
|
||||
file: path.relative(cwd, path.join(dir, newest)),
|
||||
file: path.relative(cwd, latest.path),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
@@ -105,28 +105,37 @@ function parseFrontmatter(text) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all snapshot files for `slug`, sorted oldest → newest.
|
||||
* Return snapshot files matching `suffix`, sorted oldest → newest.
|
||||
*/
|
||||
function listSnapshotsForSlug(slug, cwd) {
|
||||
const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/;
|
||||
|
||||
function listSnapshots(suffix, cwd) {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
const suffix = `__${slug}.md`;
|
||||
return fs.readdirSync(dir)
|
||||
.filter((f) => f.endsWith(suffix))
|
||||
.filter((f) => SNAPSHOT_FILENAME.test(f) && f.endsWith(suffix))
|
||||
.sort()
|
||||
.map((f) => path.join(dir, f));
|
||||
}
|
||||
|
||||
function readLatestSnapshotMatching(suffix, cwd) {
|
||||
const filePath = listSnapshots(suffix, cwd).at(-1);
|
||||
if (!filePath) return null;
|
||||
const body = fs.readFileSync(filePath, 'utf-8');
|
||||
return { path: filePath, body, meta: parseFrontmatter(body) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the most recent snapshot for `slug`, or null. Polish reads this
|
||||
* to find its fix backlog when the slug matches.
|
||||
*/
|
||||
export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
if (!all.length) return null;
|
||||
const latest = all[all.length - 1];
|
||||
const body = fs.readFileSync(latest, 'utf-8');
|
||||
return { path: latest, body, meta: parseFrontmatter(body) };
|
||||
return readLatestSnapshotMatching(`__${slug}.md`, cwd);
|
||||
}
|
||||
|
||||
/** Return the most recent snapshot across all targets, or null. */
|
||||
export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) {
|
||||
return readLatestSnapshotMatching('.md', cwd);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,7 +143,7 @@ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
* Critique appends a one-line trend to its output using this.
|
||||
*/
|
||||
export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
const all = listSnapshots(`__${slug}.md`, cwd);
|
||||
const slice = all.slice(-limit);
|
||||
return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8')));
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ export { detectUrl, createBrowserDetector } from './engines/browser/detect-url.m
|
||||
export { detectText, extractStyleBlocks, extractCSSinJS } from './engines/regex/detect-text.mjs';
|
||||
export {
|
||||
walkDir,
|
||||
hasScannableExtension,
|
||||
SCANNABLE_EXTENSIONS,
|
||||
SKIP_DIRS,
|
||||
buildImportGraph,
|
||||
|
||||
@@ -26,11 +26,20 @@ const HIDDEN_SOURCE_DIRS = new Set(['.vitepress', '.vuepress', '.storybook']);
|
||||
const SCANNABLE_EXTENSIONS = new Set([
|
||||
'.html', '.htm', '.css', '.scss', '.sass', '.less',
|
||||
'.jsx', '.tsx', '.js', '.ts',
|
||||
'.vue', '.svelte', '.astro',
|
||||
'.vue', '.svelte', '.astro', '.blade.php',
|
||||
]);
|
||||
|
||||
const HTML_EXTENSIONS = new Set(['.html', '.htm']);
|
||||
|
||||
function hasScannableExtension(filename) {
|
||||
const lower = filename.toLowerCase();
|
||||
if (SCANNABLE_EXTENSIONS.has(path.extname(lower))) return true;
|
||||
for (const ext of SCANNABLE_EXTENSIONS) {
|
||||
if (ext.indexOf('.', 1) !== -1 && lower.endsWith(ext)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const IMPORT_SPECIFIER_PATTERNS = [
|
||||
/import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g,
|
||||
/@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g,
|
||||
@@ -46,7 +55,7 @@ function walkDir(dir) {
|
||||
if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue;
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) files.push(...walkDir(full));
|
||||
else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full);
|
||||
else if (hasScannableExtension(entry.name)) files.push(full);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
@@ -194,6 +203,7 @@ export {
|
||||
SKIP_DIRS,
|
||||
SCANNABLE_EXTENSIONS,
|
||||
HTML_EXTENSIONS,
|
||||
hasScannableExtension,
|
||||
walkDir,
|
||||
resolveImport,
|
||||
buildImportGraph,
|
||||
|
||||
@@ -109,6 +109,18 @@ export function validateConceptEntry(concept, { existingForms = new Map(), axes
|
||||
|| concept.tags.some(tag => typeof tag !== 'string' || !tag.trim())) {
|
||||
errors.push(`concept ${id} must have exactly three structural tags`);
|
||||
}
|
||||
// The slop this world in particular is at risk of. Optional, because 541
|
||||
// entries predate it and none of them are wrong for lacking it. A world built
|
||||
// from posters is at risk of shouting and one built from instruments is at
|
||||
// risk of dead greys; a global detector cannot know which, and the author can.
|
||||
if (concept?.avoid !== undefined) {
|
||||
if (!Array.isArray(concept.avoid)
|
||||
|| concept.avoid.length < 2
|
||||
|| concept.avoid.length > 3
|
||||
|| concept.avoid.some(item => typeof item !== 'string' || item.trim().length < 12 || item.trim().length > 160)) {
|
||||
errors.push(`concept ${id} avoid must be two or three negations of 12–160 characters`);
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(concept?.system)
|
||||
|| concept.system.length !== SYSTEM_PREFIXES.length
|
||||
|| concept.system.some(rule => typeof rule !== 'string' || rule.trim().length < 12 || rule.trim().length > 180)) {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
export function browserOpenCommand(url, {
|
||||
platform = process.platform,
|
||||
comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe',
|
||||
} = {}) {
|
||||
if (platform === 'darwin') return { command: 'open', args: [url] };
|
||||
if (platform === 'win32') return { command: comspec, args: ['/c', 'start', '', url] };
|
||||
return { command: 'xdg-open', args: [url] };
|
||||
}
|
||||
|
||||
export function openSystemBrowser(url, {
|
||||
platform = process.platform,
|
||||
comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe',
|
||||
spawnImpl = spawn,
|
||||
} = {}) {
|
||||
const { command, args } = browserOpenCommand(url, { platform, comspec });
|
||||
try {
|
||||
const child = spawnImpl(command, args, { stdio: 'ignore', detached: true });
|
||||
child.on('error', () => {});
|
||||
child.unref();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -93,15 +93,17 @@ function commandPrefixForSkillsDir(skillsDir) {
|
||||
return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/';
|
||||
}
|
||||
|
||||
function generatePinnedSkill(command, metadata, commandPrefix) {
|
||||
function generatePinnedSkill(command, metadata, commandPrefix, isCodex) {
|
||||
const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`;
|
||||
const hint = metadata[command]?.argumentHint || '[target]';
|
||||
const providerFrontmatter = isCodex
|
||||
? `metadata:\n argument-hint: "${hint}"`
|
||||
: `argument-hint: "${hint}"\nuser-invocable: true`;
|
||||
|
||||
return `---
|
||||
name: ${command}
|
||||
description: "${desc}"
|
||||
argument-hint: "${hint}"
|
||||
user-invocable: true
|
||||
${providerFrontmatter}
|
||||
---
|
||||
|
||||
${PIN_MARKER}
|
||||
@@ -128,7 +130,7 @@ function pin(command, projectRoot) {
|
||||
|
||||
for (const skillsDir of harnessDirs) {
|
||||
const commandPrefix = commandPrefixForSkillsDir(skillsDir);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$');
|
||||
// Check if skill already exists (and isn't a pin)
|
||||
const skillDir = join(skillsDir, command);
|
||||
if (existsSync(skillDir)) {
|
||||
|
||||
@@ -79,6 +79,7 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { openSystemBrowser } from './lib/open-system-browser.mjs';
|
||||
|
||||
function arg(name, fallback = null) {
|
||||
const i = process.argv.indexOf(`--${name}`);
|
||||
@@ -976,8 +977,7 @@ server.listen(portArg, '127.0.0.1', () => {
|
||||
console.log('Waiting for the user to choose in the browser (Ctrl-C aborts)...');
|
||||
}
|
||||
if (!hasFlag('no-open')) {
|
||||
const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
||||
try { spawn(opener, [url], { stdio: 'ignore', detached: true }).unref(); } catch { /* URL printed anyway */ }
|
||||
openSystemBrowser(url);
|
||||
}
|
||||
if (timeoutSec > 0) {
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -12,6 +12,7 @@ Each of these is a check on the built result, not an intention. Run them togethe
|
||||
- **Type:** body measure 65–75ch, display max 6rem, tracking floor -0.04em, balanced headings, obvious scale and weight steps. Run the real copy at every breakpoint and fix what overflows.
|
||||
- **Motion:** one authored moment, not scattered effects and not one identical entrance on every section. Exponential ease-out from an already-visible default. Reach past transform and opacity: blur, backdrop-filter, clip-path, mask, and shadow belong to the palette when they stay smooth.
|
||||
- **States:** hover, disabled, loading, error, empty. Plus real content, working controls, responsive composition, keyboard focus.
|
||||
- **Browser surfaces:** the parts you did not draw still carry the design. Text selection, the caret, custom scrollbars, focus rings, underline offset, and the numerals in tabular data all ship with browser defaults that belong to no design system. Theme them from the palette. This is the cheapest signal that a page was built rather than assembled, and the one models skip most reliably.
|
||||
- **Copy:** the product's own language. Controls name their action; errors name the problem and the recovery.
|
||||
- **Coverage:** every brief requirement present and findable within seconds.
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { loadContext, extractPlatform } from './context.mjs';
|
||||
import { getCritiqueDir } from './lib/impeccable-paths.mjs';
|
||||
import { readLatestSnapshotAcrossTargets } from './critique-storage.mjs';
|
||||
|
||||
/** Is there code here at all, or just context files / an empty repo? */
|
||||
function hasCode(cwd) {
|
||||
@@ -34,23 +34,13 @@ function hasCode(cwd) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The most recent critique snapshot across all targets. Filenames are
|
||||
* timestamp-prefixed (`<iso>__<slug>.md`), so a lexical sort is chronological.
|
||||
* Parses the small frontmatter for score + P0/P1 counts.
|
||||
* Summarize the most recent critique snapshot across all targets.
|
||||
*/
|
||||
function latestCritique(cwd) {
|
||||
try {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return null;
|
||||
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort();
|
||||
if (!files.length) return null;
|
||||
const newest = files[files.length - 1];
|
||||
const text = fs.readFileSync(path.join(dir, newest), 'utf-8');
|
||||
const front = text.split('---')[1] || '';
|
||||
const get = (k) => {
|
||||
const m = front.match(new RegExp(`^${k}:\\s*(.+)$`, 'm'));
|
||||
return m ? m[1].trim() : null;
|
||||
};
|
||||
const latest = readLatestSnapshotAcrossTargets({ cwd });
|
||||
if (!latest) return null;
|
||||
const get = (key) => latest.meta[key] ?? null;
|
||||
const num = (v) => {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
@@ -61,7 +51,7 @@ function latestCritique(cwd) {
|
||||
p0: num(get('p0')),
|
||||
p1: num(get('p1')),
|
||||
timestamp: get('timestamp'),
|
||||
file: path.relative(cwd, path.join(dir, newest)),
|
||||
file: path.relative(cwd, latest.path),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
@@ -105,28 +105,37 @@ function parseFrontmatter(text) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all snapshot files for `slug`, sorted oldest → newest.
|
||||
* Return snapshot files matching `suffix`, sorted oldest → newest.
|
||||
*/
|
||||
function listSnapshotsForSlug(slug, cwd) {
|
||||
const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/;
|
||||
|
||||
function listSnapshots(suffix, cwd) {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
const suffix = `__${slug}.md`;
|
||||
return fs.readdirSync(dir)
|
||||
.filter((f) => f.endsWith(suffix))
|
||||
.filter((f) => SNAPSHOT_FILENAME.test(f) && f.endsWith(suffix))
|
||||
.sort()
|
||||
.map((f) => path.join(dir, f));
|
||||
}
|
||||
|
||||
function readLatestSnapshotMatching(suffix, cwd) {
|
||||
const filePath = listSnapshots(suffix, cwd).at(-1);
|
||||
if (!filePath) return null;
|
||||
const body = fs.readFileSync(filePath, 'utf-8');
|
||||
return { path: filePath, body, meta: parseFrontmatter(body) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the most recent snapshot for `slug`, or null. Polish reads this
|
||||
* to find its fix backlog when the slug matches.
|
||||
*/
|
||||
export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
if (!all.length) return null;
|
||||
const latest = all[all.length - 1];
|
||||
const body = fs.readFileSync(latest, 'utf-8');
|
||||
return { path: latest, body, meta: parseFrontmatter(body) };
|
||||
return readLatestSnapshotMatching(`__${slug}.md`, cwd);
|
||||
}
|
||||
|
||||
/** Return the most recent snapshot across all targets, or null. */
|
||||
export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) {
|
||||
return readLatestSnapshotMatching('.md', cwd);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,7 +143,7 @@ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
* Critique appends a one-line trend to its output using this.
|
||||
*/
|
||||
export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
const all = listSnapshots(`__${slug}.md`, cwd);
|
||||
const slice = all.slice(-limit);
|
||||
return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8')));
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ export { detectUrl, createBrowserDetector } from './engines/browser/detect-url.m
|
||||
export { detectText, extractStyleBlocks, extractCSSinJS } from './engines/regex/detect-text.mjs';
|
||||
export {
|
||||
walkDir,
|
||||
hasScannableExtension,
|
||||
SCANNABLE_EXTENSIONS,
|
||||
SKIP_DIRS,
|
||||
buildImportGraph,
|
||||
|
||||
@@ -26,11 +26,20 @@ const HIDDEN_SOURCE_DIRS = new Set(['.vitepress', '.vuepress', '.storybook']);
|
||||
const SCANNABLE_EXTENSIONS = new Set([
|
||||
'.html', '.htm', '.css', '.scss', '.sass', '.less',
|
||||
'.jsx', '.tsx', '.js', '.ts',
|
||||
'.vue', '.svelte', '.astro',
|
||||
'.vue', '.svelte', '.astro', '.blade.php',
|
||||
]);
|
||||
|
||||
const HTML_EXTENSIONS = new Set(['.html', '.htm']);
|
||||
|
||||
function hasScannableExtension(filename) {
|
||||
const lower = filename.toLowerCase();
|
||||
if (SCANNABLE_EXTENSIONS.has(path.extname(lower))) return true;
|
||||
for (const ext of SCANNABLE_EXTENSIONS) {
|
||||
if (ext.indexOf('.', 1) !== -1 && lower.endsWith(ext)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const IMPORT_SPECIFIER_PATTERNS = [
|
||||
/import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g,
|
||||
/@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g,
|
||||
@@ -46,7 +55,7 @@ function walkDir(dir) {
|
||||
if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue;
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) files.push(...walkDir(full));
|
||||
else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full);
|
||||
else if (hasScannableExtension(entry.name)) files.push(full);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
@@ -194,6 +203,7 @@ export {
|
||||
SKIP_DIRS,
|
||||
SCANNABLE_EXTENSIONS,
|
||||
HTML_EXTENSIONS,
|
||||
hasScannableExtension,
|
||||
walkDir,
|
||||
resolveImport,
|
||||
buildImportGraph,
|
||||
|
||||
@@ -109,6 +109,18 @@ export function validateConceptEntry(concept, { existingForms = new Map(), axes
|
||||
|| concept.tags.some(tag => typeof tag !== 'string' || !tag.trim())) {
|
||||
errors.push(`concept ${id} must have exactly three structural tags`);
|
||||
}
|
||||
// The slop this world in particular is at risk of. Optional, because 541
|
||||
// entries predate it and none of them are wrong for lacking it. A world built
|
||||
// from posters is at risk of shouting and one built from instruments is at
|
||||
// risk of dead greys; a global detector cannot know which, and the author can.
|
||||
if (concept?.avoid !== undefined) {
|
||||
if (!Array.isArray(concept.avoid)
|
||||
|| concept.avoid.length < 2
|
||||
|| concept.avoid.length > 3
|
||||
|| concept.avoid.some(item => typeof item !== 'string' || item.trim().length < 12 || item.trim().length > 160)) {
|
||||
errors.push(`concept ${id} avoid must be two or three negations of 12–160 characters`);
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(concept?.system)
|
||||
|| concept.system.length !== SYSTEM_PREFIXES.length
|
||||
|| concept.system.some(rule => typeof rule !== 'string' || rule.trim().length < 12 || rule.trim().length > 180)) {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
export function browserOpenCommand(url, {
|
||||
platform = process.platform,
|
||||
comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe',
|
||||
} = {}) {
|
||||
if (platform === 'darwin') return { command: 'open', args: [url] };
|
||||
if (platform === 'win32') return { command: comspec, args: ['/c', 'start', '', url] };
|
||||
return { command: 'xdg-open', args: [url] };
|
||||
}
|
||||
|
||||
export function openSystemBrowser(url, {
|
||||
platform = process.platform,
|
||||
comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe',
|
||||
spawnImpl = spawn,
|
||||
} = {}) {
|
||||
const { command, args } = browserOpenCommand(url, { platform, comspec });
|
||||
try {
|
||||
const child = spawnImpl(command, args, { stdio: 'ignore', detached: true });
|
||||
child.on('error', () => {});
|
||||
child.unref();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -93,15 +93,17 @@ function commandPrefixForSkillsDir(skillsDir) {
|
||||
return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/';
|
||||
}
|
||||
|
||||
function generatePinnedSkill(command, metadata, commandPrefix) {
|
||||
function generatePinnedSkill(command, metadata, commandPrefix, isCodex) {
|
||||
const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`;
|
||||
const hint = metadata[command]?.argumentHint || '[target]';
|
||||
const providerFrontmatter = isCodex
|
||||
? `metadata:\n argument-hint: "${hint}"`
|
||||
: `argument-hint: "${hint}"\nuser-invocable: true`;
|
||||
|
||||
return `---
|
||||
name: ${command}
|
||||
description: "${desc}"
|
||||
argument-hint: "${hint}"
|
||||
user-invocable: true
|
||||
${providerFrontmatter}
|
||||
---
|
||||
|
||||
${PIN_MARKER}
|
||||
@@ -128,7 +130,7 @@ function pin(command, projectRoot) {
|
||||
|
||||
for (const skillsDir of harnessDirs) {
|
||||
const commandPrefix = commandPrefixForSkillsDir(skillsDir);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$');
|
||||
// Check if skill already exists (and isn't a pin)
|
||||
const skillDir = join(skillsDir, command);
|
||||
if (existsSync(skillDir)) {
|
||||
|
||||
@@ -79,6 +79,7 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { openSystemBrowser } from './lib/open-system-browser.mjs';
|
||||
|
||||
function arg(name, fallback = null) {
|
||||
const i = process.argv.indexOf(`--${name}`);
|
||||
@@ -976,8 +977,7 @@ server.listen(portArg, '127.0.0.1', () => {
|
||||
console.log('Waiting for the user to choose in the browser (Ctrl-C aborts)...');
|
||||
}
|
||||
if (!hasFlag('no-open')) {
|
||||
const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
||||
try { spawn(opener, [url], { stdio: 'ignore', detached: true }).unref(); } catch { /* URL printed anyway */ }
|
||||
openSystemBrowser(url);
|
||||
}
|
||||
if (timeoutSec > 0) {
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -12,6 +12,7 @@ Each of these is a check on the built result, not an intention. Run them togethe
|
||||
- **Type:** body measure 65–75ch, display max 6rem, tracking floor -0.04em, balanced headings, obvious scale and weight steps. Run the real copy at every breakpoint and fix what overflows.
|
||||
- **Motion:** one authored moment, not scattered effects and not one identical entrance on every section. Exponential ease-out from an already-visible default. Reach past transform and opacity: blur, backdrop-filter, clip-path, mask, and shadow belong to the palette when they stay smooth.
|
||||
- **States:** hover, disabled, loading, error, empty. Plus real content, working controls, responsive composition, keyboard focus.
|
||||
- **Browser surfaces:** the parts you did not draw still carry the design. Text selection, the caret, custom scrollbars, focus rings, underline offset, and the numerals in tabular data all ship with browser defaults that belong to no design system. Theme them from the palette. This is the cheapest signal that a page was built rather than assembled, and the one models skip most reliably.
|
||||
- **Copy:** the product's own language. Controls name their action; errors name the problem and the recovery.
|
||||
- **Coverage:** every brief requirement present and findable within seconds.
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { loadContext, extractPlatform } from './context.mjs';
|
||||
import { getCritiqueDir } from './lib/impeccable-paths.mjs';
|
||||
import { readLatestSnapshotAcrossTargets } from './critique-storage.mjs';
|
||||
|
||||
/** Is there code here at all, or just context files / an empty repo? */
|
||||
function hasCode(cwd) {
|
||||
@@ -34,23 +34,13 @@ function hasCode(cwd) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The most recent critique snapshot across all targets. Filenames are
|
||||
* timestamp-prefixed (`<iso>__<slug>.md`), so a lexical sort is chronological.
|
||||
* Parses the small frontmatter for score + P0/P1 counts.
|
||||
* Summarize the most recent critique snapshot across all targets.
|
||||
*/
|
||||
function latestCritique(cwd) {
|
||||
try {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return null;
|
||||
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort();
|
||||
if (!files.length) return null;
|
||||
const newest = files[files.length - 1];
|
||||
const text = fs.readFileSync(path.join(dir, newest), 'utf-8');
|
||||
const front = text.split('---')[1] || '';
|
||||
const get = (k) => {
|
||||
const m = front.match(new RegExp(`^${k}:\\s*(.+)$`, 'm'));
|
||||
return m ? m[1].trim() : null;
|
||||
};
|
||||
const latest = readLatestSnapshotAcrossTargets({ cwd });
|
||||
if (!latest) return null;
|
||||
const get = (key) => latest.meta[key] ?? null;
|
||||
const num = (v) => {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
@@ -61,7 +51,7 @@ function latestCritique(cwd) {
|
||||
p0: num(get('p0')),
|
||||
p1: num(get('p1')),
|
||||
timestamp: get('timestamp'),
|
||||
file: path.relative(cwd, path.join(dir, newest)),
|
||||
file: path.relative(cwd, latest.path),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
@@ -105,28 +105,37 @@ function parseFrontmatter(text) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all snapshot files for `slug`, sorted oldest → newest.
|
||||
* Return snapshot files matching `suffix`, sorted oldest → newest.
|
||||
*/
|
||||
function listSnapshotsForSlug(slug, cwd) {
|
||||
const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/;
|
||||
|
||||
function listSnapshots(suffix, cwd) {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
const suffix = `__${slug}.md`;
|
||||
return fs.readdirSync(dir)
|
||||
.filter((f) => f.endsWith(suffix))
|
||||
.filter((f) => SNAPSHOT_FILENAME.test(f) && f.endsWith(suffix))
|
||||
.sort()
|
||||
.map((f) => path.join(dir, f));
|
||||
}
|
||||
|
||||
function readLatestSnapshotMatching(suffix, cwd) {
|
||||
const filePath = listSnapshots(suffix, cwd).at(-1);
|
||||
if (!filePath) return null;
|
||||
const body = fs.readFileSync(filePath, 'utf-8');
|
||||
return { path: filePath, body, meta: parseFrontmatter(body) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the most recent snapshot for `slug`, or null. Polish reads this
|
||||
* to find its fix backlog when the slug matches.
|
||||
*/
|
||||
export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
if (!all.length) return null;
|
||||
const latest = all[all.length - 1];
|
||||
const body = fs.readFileSync(latest, 'utf-8');
|
||||
return { path: latest, body, meta: parseFrontmatter(body) };
|
||||
return readLatestSnapshotMatching(`__${slug}.md`, cwd);
|
||||
}
|
||||
|
||||
/** Return the most recent snapshot across all targets, or null. */
|
||||
export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) {
|
||||
return readLatestSnapshotMatching('.md', cwd);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,7 +143,7 @@ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
* Critique appends a one-line trend to its output using this.
|
||||
*/
|
||||
export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
const all = listSnapshots(`__${slug}.md`, cwd);
|
||||
const slice = all.slice(-limit);
|
||||
return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8')));
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ export { detectUrl, createBrowserDetector } from './engines/browser/detect-url.m
|
||||
export { detectText, extractStyleBlocks, extractCSSinJS } from './engines/regex/detect-text.mjs';
|
||||
export {
|
||||
walkDir,
|
||||
hasScannableExtension,
|
||||
SCANNABLE_EXTENSIONS,
|
||||
SKIP_DIRS,
|
||||
buildImportGraph,
|
||||
|
||||
@@ -26,11 +26,20 @@ const HIDDEN_SOURCE_DIRS = new Set(['.vitepress', '.vuepress', '.storybook']);
|
||||
const SCANNABLE_EXTENSIONS = new Set([
|
||||
'.html', '.htm', '.css', '.scss', '.sass', '.less',
|
||||
'.jsx', '.tsx', '.js', '.ts',
|
||||
'.vue', '.svelte', '.astro',
|
||||
'.vue', '.svelte', '.astro', '.blade.php',
|
||||
]);
|
||||
|
||||
const HTML_EXTENSIONS = new Set(['.html', '.htm']);
|
||||
|
||||
function hasScannableExtension(filename) {
|
||||
const lower = filename.toLowerCase();
|
||||
if (SCANNABLE_EXTENSIONS.has(path.extname(lower))) return true;
|
||||
for (const ext of SCANNABLE_EXTENSIONS) {
|
||||
if (ext.indexOf('.', 1) !== -1 && lower.endsWith(ext)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const IMPORT_SPECIFIER_PATTERNS = [
|
||||
/import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g,
|
||||
/@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g,
|
||||
@@ -46,7 +55,7 @@ function walkDir(dir) {
|
||||
if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue;
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) files.push(...walkDir(full));
|
||||
else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full);
|
||||
else if (hasScannableExtension(entry.name)) files.push(full);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
@@ -194,6 +203,7 @@ export {
|
||||
SKIP_DIRS,
|
||||
SCANNABLE_EXTENSIONS,
|
||||
HTML_EXTENSIONS,
|
||||
hasScannableExtension,
|
||||
walkDir,
|
||||
resolveImport,
|
||||
buildImportGraph,
|
||||
|
||||
@@ -109,6 +109,18 @@ export function validateConceptEntry(concept, { existingForms = new Map(), axes
|
||||
|| concept.tags.some(tag => typeof tag !== 'string' || !tag.trim())) {
|
||||
errors.push(`concept ${id} must have exactly three structural tags`);
|
||||
}
|
||||
// The slop this world in particular is at risk of. Optional, because 541
|
||||
// entries predate it and none of them are wrong for lacking it. A world built
|
||||
// from posters is at risk of shouting and one built from instruments is at
|
||||
// risk of dead greys; a global detector cannot know which, and the author can.
|
||||
if (concept?.avoid !== undefined) {
|
||||
if (!Array.isArray(concept.avoid)
|
||||
|| concept.avoid.length < 2
|
||||
|| concept.avoid.length > 3
|
||||
|| concept.avoid.some(item => typeof item !== 'string' || item.trim().length < 12 || item.trim().length > 160)) {
|
||||
errors.push(`concept ${id} avoid must be two or three negations of 12–160 characters`);
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(concept?.system)
|
||||
|| concept.system.length !== SYSTEM_PREFIXES.length
|
||||
|| concept.system.some(rule => typeof rule !== 'string' || rule.trim().length < 12 || rule.trim().length > 180)) {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
export function browserOpenCommand(url, {
|
||||
platform = process.platform,
|
||||
comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe',
|
||||
} = {}) {
|
||||
if (platform === 'darwin') return { command: 'open', args: [url] };
|
||||
if (platform === 'win32') return { command: comspec, args: ['/c', 'start', '', url] };
|
||||
return { command: 'xdg-open', args: [url] };
|
||||
}
|
||||
|
||||
export function openSystemBrowser(url, {
|
||||
platform = process.platform,
|
||||
comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe',
|
||||
spawnImpl = spawn,
|
||||
} = {}) {
|
||||
const { command, args } = browserOpenCommand(url, { platform, comspec });
|
||||
try {
|
||||
const child = spawnImpl(command, args, { stdio: 'ignore', detached: true });
|
||||
child.on('error', () => {});
|
||||
child.unref();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -93,15 +93,17 @@ function commandPrefixForSkillsDir(skillsDir) {
|
||||
return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/';
|
||||
}
|
||||
|
||||
function generatePinnedSkill(command, metadata, commandPrefix) {
|
||||
function generatePinnedSkill(command, metadata, commandPrefix, isCodex) {
|
||||
const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`;
|
||||
const hint = metadata[command]?.argumentHint || '[target]';
|
||||
const providerFrontmatter = isCodex
|
||||
? `metadata:\n argument-hint: "${hint}"`
|
||||
: `argument-hint: "${hint}"\nuser-invocable: true`;
|
||||
|
||||
return `---
|
||||
name: ${command}
|
||||
description: "${desc}"
|
||||
argument-hint: "${hint}"
|
||||
user-invocable: true
|
||||
${providerFrontmatter}
|
||||
---
|
||||
|
||||
${PIN_MARKER}
|
||||
@@ -128,7 +130,7 @@ function pin(command, projectRoot) {
|
||||
|
||||
for (const skillsDir of harnessDirs) {
|
||||
const commandPrefix = commandPrefixForSkillsDir(skillsDir);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$');
|
||||
// Check if skill already exists (and isn't a pin)
|
||||
const skillDir = join(skillsDir, command);
|
||||
if (existsSync(skillDir)) {
|
||||
|
||||
@@ -79,6 +79,7 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { openSystemBrowser } from './lib/open-system-browser.mjs';
|
||||
|
||||
function arg(name, fallback = null) {
|
||||
const i = process.argv.indexOf(`--${name}`);
|
||||
@@ -976,8 +977,7 @@ server.listen(portArg, '127.0.0.1', () => {
|
||||
console.log('Waiting for the user to choose in the browser (Ctrl-C aborts)...');
|
||||
}
|
||||
if (!hasFlag('no-open')) {
|
||||
const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
||||
try { spawn(opener, [url], { stdio: 'ignore', detached: true }).unref(); } catch { /* URL printed anyway */ }
|
||||
openSystemBrowser(url);
|
||||
}
|
||||
if (timeoutSec > 0) {
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -12,6 +12,7 @@ Each of these is a check on the built result, not an intention. Run them togethe
|
||||
- **Type:** body measure 65–75ch, display max 6rem, tracking floor -0.04em, balanced headings, obvious scale and weight steps. Run the real copy at every breakpoint and fix what overflows.
|
||||
- **Motion:** one authored moment, not scattered effects and not one identical entrance on every section. Exponential ease-out from an already-visible default. Reach past transform and opacity: blur, backdrop-filter, clip-path, mask, and shadow belong to the palette when they stay smooth.
|
||||
- **States:** hover, disabled, loading, error, empty. Plus real content, working controls, responsive composition, keyboard focus.
|
||||
- **Browser surfaces:** the parts you did not draw still carry the design. Text selection, the caret, custom scrollbars, focus rings, underline offset, and the numerals in tabular data all ship with browser defaults that belong to no design system. Theme them from the palette. This is the cheapest signal that a page was built rather than assembled, and the one models skip most reliably.
|
||||
- **Copy:** the product's own language. Controls name their action; errors name the problem and the recovery.
|
||||
- **Coverage:** every brief requirement present and findable within seconds.
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { loadContext, extractPlatform } from './context.mjs';
|
||||
import { getCritiqueDir } from './lib/impeccable-paths.mjs';
|
||||
import { readLatestSnapshotAcrossTargets } from './critique-storage.mjs';
|
||||
|
||||
/** Is there code here at all, or just context files / an empty repo? */
|
||||
function hasCode(cwd) {
|
||||
@@ -34,23 +34,13 @@ function hasCode(cwd) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The most recent critique snapshot across all targets. Filenames are
|
||||
* timestamp-prefixed (`<iso>__<slug>.md`), so a lexical sort is chronological.
|
||||
* Parses the small frontmatter for score + P0/P1 counts.
|
||||
* Summarize the most recent critique snapshot across all targets.
|
||||
*/
|
||||
function latestCritique(cwd) {
|
||||
try {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return null;
|
||||
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort();
|
||||
if (!files.length) return null;
|
||||
const newest = files[files.length - 1];
|
||||
const text = fs.readFileSync(path.join(dir, newest), 'utf-8');
|
||||
const front = text.split('---')[1] || '';
|
||||
const get = (k) => {
|
||||
const m = front.match(new RegExp(`^${k}:\\s*(.+)$`, 'm'));
|
||||
return m ? m[1].trim() : null;
|
||||
};
|
||||
const latest = readLatestSnapshotAcrossTargets({ cwd });
|
||||
if (!latest) return null;
|
||||
const get = (key) => latest.meta[key] ?? null;
|
||||
const num = (v) => {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
@@ -61,7 +51,7 @@ function latestCritique(cwd) {
|
||||
p0: num(get('p0')),
|
||||
p1: num(get('p1')),
|
||||
timestamp: get('timestamp'),
|
||||
file: path.relative(cwd, path.join(dir, newest)),
|
||||
file: path.relative(cwd, latest.path),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
@@ -105,28 +105,37 @@ function parseFrontmatter(text) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all snapshot files for `slug`, sorted oldest → newest.
|
||||
* Return snapshot files matching `suffix`, sorted oldest → newest.
|
||||
*/
|
||||
function listSnapshotsForSlug(slug, cwd) {
|
||||
const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/;
|
||||
|
||||
function listSnapshots(suffix, cwd) {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
const suffix = `__${slug}.md`;
|
||||
return fs.readdirSync(dir)
|
||||
.filter((f) => f.endsWith(suffix))
|
||||
.filter((f) => SNAPSHOT_FILENAME.test(f) && f.endsWith(suffix))
|
||||
.sort()
|
||||
.map((f) => path.join(dir, f));
|
||||
}
|
||||
|
||||
function readLatestSnapshotMatching(suffix, cwd) {
|
||||
const filePath = listSnapshots(suffix, cwd).at(-1);
|
||||
if (!filePath) return null;
|
||||
const body = fs.readFileSync(filePath, 'utf-8');
|
||||
return { path: filePath, body, meta: parseFrontmatter(body) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the most recent snapshot for `slug`, or null. Polish reads this
|
||||
* to find its fix backlog when the slug matches.
|
||||
*/
|
||||
export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
if (!all.length) return null;
|
||||
const latest = all[all.length - 1];
|
||||
const body = fs.readFileSync(latest, 'utf-8');
|
||||
return { path: latest, body, meta: parseFrontmatter(body) };
|
||||
return readLatestSnapshotMatching(`__${slug}.md`, cwd);
|
||||
}
|
||||
|
||||
/** Return the most recent snapshot across all targets, or null. */
|
||||
export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) {
|
||||
return readLatestSnapshotMatching('.md', cwd);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,7 +143,7 @@ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
* Critique appends a one-line trend to its output using this.
|
||||
*/
|
||||
export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
const all = listSnapshots(`__${slug}.md`, cwd);
|
||||
const slice = all.slice(-limit);
|
||||
return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8')));
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ export { detectUrl, createBrowserDetector } from './engines/browser/detect-url.m
|
||||
export { detectText, extractStyleBlocks, extractCSSinJS } from './engines/regex/detect-text.mjs';
|
||||
export {
|
||||
walkDir,
|
||||
hasScannableExtension,
|
||||
SCANNABLE_EXTENSIONS,
|
||||
SKIP_DIRS,
|
||||
buildImportGraph,
|
||||
|
||||
@@ -26,11 +26,20 @@ const HIDDEN_SOURCE_DIRS = new Set(['.vitepress', '.vuepress', '.storybook']);
|
||||
const SCANNABLE_EXTENSIONS = new Set([
|
||||
'.html', '.htm', '.css', '.scss', '.sass', '.less',
|
||||
'.jsx', '.tsx', '.js', '.ts',
|
||||
'.vue', '.svelte', '.astro',
|
||||
'.vue', '.svelte', '.astro', '.blade.php',
|
||||
]);
|
||||
|
||||
const HTML_EXTENSIONS = new Set(['.html', '.htm']);
|
||||
|
||||
function hasScannableExtension(filename) {
|
||||
const lower = filename.toLowerCase();
|
||||
if (SCANNABLE_EXTENSIONS.has(path.extname(lower))) return true;
|
||||
for (const ext of SCANNABLE_EXTENSIONS) {
|
||||
if (ext.indexOf('.', 1) !== -1 && lower.endsWith(ext)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const IMPORT_SPECIFIER_PATTERNS = [
|
||||
/import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g,
|
||||
/@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g,
|
||||
@@ -46,7 +55,7 @@ function walkDir(dir) {
|
||||
if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue;
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) files.push(...walkDir(full));
|
||||
else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full);
|
||||
else if (hasScannableExtension(entry.name)) files.push(full);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
@@ -194,6 +203,7 @@ export {
|
||||
SKIP_DIRS,
|
||||
SCANNABLE_EXTENSIONS,
|
||||
HTML_EXTENSIONS,
|
||||
hasScannableExtension,
|
||||
walkDir,
|
||||
resolveImport,
|
||||
buildImportGraph,
|
||||
|
||||
@@ -109,6 +109,18 @@ export function validateConceptEntry(concept, { existingForms = new Map(), axes
|
||||
|| concept.tags.some(tag => typeof tag !== 'string' || !tag.trim())) {
|
||||
errors.push(`concept ${id} must have exactly three structural tags`);
|
||||
}
|
||||
// The slop this world in particular is at risk of. Optional, because 541
|
||||
// entries predate it and none of them are wrong for lacking it. A world built
|
||||
// from posters is at risk of shouting and one built from instruments is at
|
||||
// risk of dead greys; a global detector cannot know which, and the author can.
|
||||
if (concept?.avoid !== undefined) {
|
||||
if (!Array.isArray(concept.avoid)
|
||||
|| concept.avoid.length < 2
|
||||
|| concept.avoid.length > 3
|
||||
|| concept.avoid.some(item => typeof item !== 'string' || item.trim().length < 12 || item.trim().length > 160)) {
|
||||
errors.push(`concept ${id} avoid must be two or three negations of 12–160 characters`);
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(concept?.system)
|
||||
|| concept.system.length !== SYSTEM_PREFIXES.length
|
||||
|| concept.system.some(rule => typeof rule !== 'string' || rule.trim().length < 12 || rule.trim().length > 180)) {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
export function browserOpenCommand(url, {
|
||||
platform = process.platform,
|
||||
comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe',
|
||||
} = {}) {
|
||||
if (platform === 'darwin') return { command: 'open', args: [url] };
|
||||
if (platform === 'win32') return { command: comspec, args: ['/c', 'start', '', url] };
|
||||
return { command: 'xdg-open', args: [url] };
|
||||
}
|
||||
|
||||
export function openSystemBrowser(url, {
|
||||
platform = process.platform,
|
||||
comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe',
|
||||
spawnImpl = spawn,
|
||||
} = {}) {
|
||||
const { command, args } = browserOpenCommand(url, { platform, comspec });
|
||||
try {
|
||||
const child = spawnImpl(command, args, { stdio: 'ignore', detached: true });
|
||||
child.on('error', () => {});
|
||||
child.unref();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -93,15 +93,17 @@ function commandPrefixForSkillsDir(skillsDir) {
|
||||
return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/';
|
||||
}
|
||||
|
||||
function generatePinnedSkill(command, metadata, commandPrefix) {
|
||||
function generatePinnedSkill(command, metadata, commandPrefix, isCodex) {
|
||||
const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`;
|
||||
const hint = metadata[command]?.argumentHint || '[target]';
|
||||
const providerFrontmatter = isCodex
|
||||
? `metadata:\n argument-hint: "${hint}"`
|
||||
: `argument-hint: "${hint}"\nuser-invocable: true`;
|
||||
|
||||
return `---
|
||||
name: ${command}
|
||||
description: "${desc}"
|
||||
argument-hint: "${hint}"
|
||||
user-invocable: true
|
||||
${providerFrontmatter}
|
||||
---
|
||||
|
||||
${PIN_MARKER}
|
||||
@@ -128,7 +130,7 @@ function pin(command, projectRoot) {
|
||||
|
||||
for (const skillsDir of harnessDirs) {
|
||||
const commandPrefix = commandPrefixForSkillsDir(skillsDir);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$');
|
||||
// Check if skill already exists (and isn't a pin)
|
||||
const skillDir = join(skillsDir, command);
|
||||
if (existsSync(skillDir)) {
|
||||
|
||||
@@ -79,6 +79,7 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { openSystemBrowser } from './lib/open-system-browser.mjs';
|
||||
|
||||
function arg(name, fallback = null) {
|
||||
const i = process.argv.indexOf(`--${name}`);
|
||||
@@ -976,8 +977,7 @@ server.listen(portArg, '127.0.0.1', () => {
|
||||
console.log('Waiting for the user to choose in the browser (Ctrl-C aborts)...');
|
||||
}
|
||||
if (!hasFlag('no-open')) {
|
||||
const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
||||
try { spawn(opener, [url], { stdio: 'ignore', detached: true }).unref(); } catch { /* URL printed anyway */ }
|
||||
openSystemBrowser(url);
|
||||
}
|
||||
if (timeoutSec > 0) {
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -12,6 +12,7 @@ Each of these is a check on the built result, not an intention. Run them togethe
|
||||
- **Type:** body measure 65–75ch, display max 6rem, tracking floor -0.04em, balanced headings, obvious scale and weight steps. Run the real copy at every breakpoint and fix what overflows.
|
||||
- **Motion:** one authored moment, not scattered effects and not one identical entrance on every section. Exponential ease-out from an already-visible default. Reach past transform and opacity: blur, backdrop-filter, clip-path, mask, and shadow belong to the palette when they stay smooth.
|
||||
- **States:** hover, disabled, loading, error, empty. Plus real content, working controls, responsive composition, keyboard focus.
|
||||
- **Browser surfaces:** the parts you did not draw still carry the design. Text selection, the caret, custom scrollbars, focus rings, underline offset, and the numerals in tabular data all ship with browser defaults that belong to no design system. Theme them from the palette. This is the cheapest signal that a page was built rather than assembled, and the one models skip most reliably.
|
||||
- **Copy:** the product's own language. Controls name their action; errors name the problem and the recovery.
|
||||
- **Coverage:** every brief requirement present and findable within seconds.
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { loadContext, extractPlatform } from './context.mjs';
|
||||
import { getCritiqueDir } from './lib/impeccable-paths.mjs';
|
||||
import { readLatestSnapshotAcrossTargets } from './critique-storage.mjs';
|
||||
|
||||
/** Is there code here at all, or just context files / an empty repo? */
|
||||
function hasCode(cwd) {
|
||||
@@ -34,23 +34,13 @@ function hasCode(cwd) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The most recent critique snapshot across all targets. Filenames are
|
||||
* timestamp-prefixed (`<iso>__<slug>.md`), so a lexical sort is chronological.
|
||||
* Parses the small frontmatter for score + P0/P1 counts.
|
||||
* Summarize the most recent critique snapshot across all targets.
|
||||
*/
|
||||
function latestCritique(cwd) {
|
||||
try {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return null;
|
||||
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort();
|
||||
if (!files.length) return null;
|
||||
const newest = files[files.length - 1];
|
||||
const text = fs.readFileSync(path.join(dir, newest), 'utf-8');
|
||||
const front = text.split('---')[1] || '';
|
||||
const get = (k) => {
|
||||
const m = front.match(new RegExp(`^${k}:\\s*(.+)$`, 'm'));
|
||||
return m ? m[1].trim() : null;
|
||||
};
|
||||
const latest = readLatestSnapshotAcrossTargets({ cwd });
|
||||
if (!latest) return null;
|
||||
const get = (key) => latest.meta[key] ?? null;
|
||||
const num = (v) => {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
@@ -61,7 +51,7 @@ function latestCritique(cwd) {
|
||||
p0: num(get('p0')),
|
||||
p1: num(get('p1')),
|
||||
timestamp: get('timestamp'),
|
||||
file: path.relative(cwd, path.join(dir, newest)),
|
||||
file: path.relative(cwd, latest.path),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
@@ -105,28 +105,37 @@ function parseFrontmatter(text) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all snapshot files for `slug`, sorted oldest → newest.
|
||||
* Return snapshot files matching `suffix`, sorted oldest → newest.
|
||||
*/
|
||||
function listSnapshotsForSlug(slug, cwd) {
|
||||
const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/;
|
||||
|
||||
function listSnapshots(suffix, cwd) {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
const suffix = `__${slug}.md`;
|
||||
return fs.readdirSync(dir)
|
||||
.filter((f) => f.endsWith(suffix))
|
||||
.filter((f) => SNAPSHOT_FILENAME.test(f) && f.endsWith(suffix))
|
||||
.sort()
|
||||
.map((f) => path.join(dir, f));
|
||||
}
|
||||
|
||||
function readLatestSnapshotMatching(suffix, cwd) {
|
||||
const filePath = listSnapshots(suffix, cwd).at(-1);
|
||||
if (!filePath) return null;
|
||||
const body = fs.readFileSync(filePath, 'utf-8');
|
||||
return { path: filePath, body, meta: parseFrontmatter(body) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the most recent snapshot for `slug`, or null. Polish reads this
|
||||
* to find its fix backlog when the slug matches.
|
||||
*/
|
||||
export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
if (!all.length) return null;
|
||||
const latest = all[all.length - 1];
|
||||
const body = fs.readFileSync(latest, 'utf-8');
|
||||
return { path: latest, body, meta: parseFrontmatter(body) };
|
||||
return readLatestSnapshotMatching(`__${slug}.md`, cwd);
|
||||
}
|
||||
|
||||
/** Return the most recent snapshot across all targets, or null. */
|
||||
export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) {
|
||||
return readLatestSnapshotMatching('.md', cwd);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,7 +143,7 @@ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
* Critique appends a one-line trend to its output using this.
|
||||
*/
|
||||
export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
const all = listSnapshots(`__${slug}.md`, cwd);
|
||||
const slice = all.slice(-limit);
|
||||
return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8')));
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ export { detectUrl, createBrowserDetector } from './engines/browser/detect-url.m
|
||||
export { detectText, extractStyleBlocks, extractCSSinJS } from './engines/regex/detect-text.mjs';
|
||||
export {
|
||||
walkDir,
|
||||
hasScannableExtension,
|
||||
SCANNABLE_EXTENSIONS,
|
||||
SKIP_DIRS,
|
||||
buildImportGraph,
|
||||
|
||||
@@ -26,11 +26,20 @@ const HIDDEN_SOURCE_DIRS = new Set(['.vitepress', '.vuepress', '.storybook']);
|
||||
const SCANNABLE_EXTENSIONS = new Set([
|
||||
'.html', '.htm', '.css', '.scss', '.sass', '.less',
|
||||
'.jsx', '.tsx', '.js', '.ts',
|
||||
'.vue', '.svelte', '.astro',
|
||||
'.vue', '.svelte', '.astro', '.blade.php',
|
||||
]);
|
||||
|
||||
const HTML_EXTENSIONS = new Set(['.html', '.htm']);
|
||||
|
||||
function hasScannableExtension(filename) {
|
||||
const lower = filename.toLowerCase();
|
||||
if (SCANNABLE_EXTENSIONS.has(path.extname(lower))) return true;
|
||||
for (const ext of SCANNABLE_EXTENSIONS) {
|
||||
if (ext.indexOf('.', 1) !== -1 && lower.endsWith(ext)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const IMPORT_SPECIFIER_PATTERNS = [
|
||||
/import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g,
|
||||
/@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g,
|
||||
@@ -46,7 +55,7 @@ function walkDir(dir) {
|
||||
if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue;
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) files.push(...walkDir(full));
|
||||
else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full);
|
||||
else if (hasScannableExtension(entry.name)) files.push(full);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
@@ -194,6 +203,7 @@ export {
|
||||
SKIP_DIRS,
|
||||
SCANNABLE_EXTENSIONS,
|
||||
HTML_EXTENSIONS,
|
||||
hasScannableExtension,
|
||||
walkDir,
|
||||
resolveImport,
|
||||
buildImportGraph,
|
||||
|
||||
@@ -109,6 +109,18 @@ export function validateConceptEntry(concept, { existingForms = new Map(), axes
|
||||
|| concept.tags.some(tag => typeof tag !== 'string' || !tag.trim())) {
|
||||
errors.push(`concept ${id} must have exactly three structural tags`);
|
||||
}
|
||||
// The slop this world in particular is at risk of. Optional, because 541
|
||||
// entries predate it and none of them are wrong for lacking it. A world built
|
||||
// from posters is at risk of shouting and one built from instruments is at
|
||||
// risk of dead greys; a global detector cannot know which, and the author can.
|
||||
if (concept?.avoid !== undefined) {
|
||||
if (!Array.isArray(concept.avoid)
|
||||
|| concept.avoid.length < 2
|
||||
|| concept.avoid.length > 3
|
||||
|| concept.avoid.some(item => typeof item !== 'string' || item.trim().length < 12 || item.trim().length > 160)) {
|
||||
errors.push(`concept ${id} avoid must be two or three negations of 12–160 characters`);
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(concept?.system)
|
||||
|| concept.system.length !== SYSTEM_PREFIXES.length
|
||||
|| concept.system.some(rule => typeof rule !== 'string' || rule.trim().length < 12 || rule.trim().length > 180)) {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
export function browserOpenCommand(url, {
|
||||
platform = process.platform,
|
||||
comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe',
|
||||
} = {}) {
|
||||
if (platform === 'darwin') return { command: 'open', args: [url] };
|
||||
if (platform === 'win32') return { command: comspec, args: ['/c', 'start', '', url] };
|
||||
return { command: 'xdg-open', args: [url] };
|
||||
}
|
||||
|
||||
export function openSystemBrowser(url, {
|
||||
platform = process.platform,
|
||||
comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe',
|
||||
spawnImpl = spawn,
|
||||
} = {}) {
|
||||
const { command, args } = browserOpenCommand(url, { platform, comspec });
|
||||
try {
|
||||
const child = spawnImpl(command, args, { stdio: 'ignore', detached: true });
|
||||
child.on('error', () => {});
|
||||
child.unref();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -93,15 +93,17 @@ function commandPrefixForSkillsDir(skillsDir) {
|
||||
return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/';
|
||||
}
|
||||
|
||||
function generatePinnedSkill(command, metadata, commandPrefix) {
|
||||
function generatePinnedSkill(command, metadata, commandPrefix, isCodex) {
|
||||
const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`;
|
||||
const hint = metadata[command]?.argumentHint || '[target]';
|
||||
const providerFrontmatter = isCodex
|
||||
? `metadata:\n argument-hint: "${hint}"`
|
||||
: `argument-hint: "${hint}"\nuser-invocable: true`;
|
||||
|
||||
return `---
|
||||
name: ${command}
|
||||
description: "${desc}"
|
||||
argument-hint: "${hint}"
|
||||
user-invocable: true
|
||||
${providerFrontmatter}
|
||||
---
|
||||
|
||||
${PIN_MARKER}
|
||||
@@ -128,7 +130,7 @@ function pin(command, projectRoot) {
|
||||
|
||||
for (const skillsDir of harnessDirs) {
|
||||
const commandPrefix = commandPrefixForSkillsDir(skillsDir);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$');
|
||||
// Check if skill already exists (and isn't a pin)
|
||||
const skillDir = join(skillsDir, command);
|
||||
if (existsSync(skillDir)) {
|
||||
|
||||
@@ -79,6 +79,7 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { openSystemBrowser } from './lib/open-system-browser.mjs';
|
||||
|
||||
function arg(name, fallback = null) {
|
||||
const i = process.argv.indexOf(`--${name}`);
|
||||
@@ -976,8 +977,7 @@ server.listen(portArg, '127.0.0.1', () => {
|
||||
console.log('Waiting for the user to choose in the browser (Ctrl-C aborts)...');
|
||||
}
|
||||
if (!hasFlag('no-open')) {
|
||||
const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
||||
try { spawn(opener, [url], { stdio: 'ignore', detached: true }).unref(); } catch { /* URL printed anyway */ }
|
||||
openSystemBrowser(url);
|
||||
}
|
||||
if (timeoutSec > 0) {
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -12,6 +12,7 @@ Each of these is a check on the built result, not an intention. Run them togethe
|
||||
- **Type:** body measure 65–75ch, display max 6rem, tracking floor -0.04em, balanced headings, obvious scale and weight steps. Run the real copy at every breakpoint and fix what overflows.
|
||||
- **Motion:** one authored moment, not scattered effects and not one identical entrance on every section. Exponential ease-out from an already-visible default. Reach past transform and opacity: blur, backdrop-filter, clip-path, mask, and shadow belong to the palette when they stay smooth.
|
||||
- **States:** hover, disabled, loading, error, empty. Plus real content, working controls, responsive composition, keyboard focus.
|
||||
- **Browser surfaces:** the parts you did not draw still carry the design. Text selection, the caret, custom scrollbars, focus rings, underline offset, and the numerals in tabular data all ship with browser defaults that belong to no design system. Theme them from the palette. This is the cheapest signal that a page was built rather than assembled, and the one models skip most reliably.
|
||||
- **Copy:** the product's own language. Controls name their action; errors name the problem and the recovery.
|
||||
- **Coverage:** every brief requirement present and findable within seconds.
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { loadContext, extractPlatform } from './context.mjs';
|
||||
import { getCritiqueDir } from './lib/impeccable-paths.mjs';
|
||||
import { readLatestSnapshotAcrossTargets } from './critique-storage.mjs';
|
||||
|
||||
/** Is there code here at all, or just context files / an empty repo? */
|
||||
function hasCode(cwd) {
|
||||
@@ -34,23 +34,13 @@ function hasCode(cwd) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The most recent critique snapshot across all targets. Filenames are
|
||||
* timestamp-prefixed (`<iso>__<slug>.md`), so a lexical sort is chronological.
|
||||
* Parses the small frontmatter for score + P0/P1 counts.
|
||||
* Summarize the most recent critique snapshot across all targets.
|
||||
*/
|
||||
function latestCritique(cwd) {
|
||||
try {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return null;
|
||||
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort();
|
||||
if (!files.length) return null;
|
||||
const newest = files[files.length - 1];
|
||||
const text = fs.readFileSync(path.join(dir, newest), 'utf-8');
|
||||
const front = text.split('---')[1] || '';
|
||||
const get = (k) => {
|
||||
const m = front.match(new RegExp(`^${k}:\\s*(.+)$`, 'm'));
|
||||
return m ? m[1].trim() : null;
|
||||
};
|
||||
const latest = readLatestSnapshotAcrossTargets({ cwd });
|
||||
if (!latest) return null;
|
||||
const get = (key) => latest.meta[key] ?? null;
|
||||
const num = (v) => {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
@@ -61,7 +51,7 @@ function latestCritique(cwd) {
|
||||
p0: num(get('p0')),
|
||||
p1: num(get('p1')),
|
||||
timestamp: get('timestamp'),
|
||||
file: path.relative(cwd, path.join(dir, newest)),
|
||||
file: path.relative(cwd, latest.path),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
@@ -105,28 +105,37 @@ function parseFrontmatter(text) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all snapshot files for `slug`, sorted oldest → newest.
|
||||
* Return snapshot files matching `suffix`, sorted oldest → newest.
|
||||
*/
|
||||
function listSnapshotsForSlug(slug, cwd) {
|
||||
const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/;
|
||||
|
||||
function listSnapshots(suffix, cwd) {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
const suffix = `__${slug}.md`;
|
||||
return fs.readdirSync(dir)
|
||||
.filter((f) => f.endsWith(suffix))
|
||||
.filter((f) => SNAPSHOT_FILENAME.test(f) && f.endsWith(suffix))
|
||||
.sort()
|
||||
.map((f) => path.join(dir, f));
|
||||
}
|
||||
|
||||
function readLatestSnapshotMatching(suffix, cwd) {
|
||||
const filePath = listSnapshots(suffix, cwd).at(-1);
|
||||
if (!filePath) return null;
|
||||
const body = fs.readFileSync(filePath, 'utf-8');
|
||||
return { path: filePath, body, meta: parseFrontmatter(body) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the most recent snapshot for `slug`, or null. Polish reads this
|
||||
* to find its fix backlog when the slug matches.
|
||||
*/
|
||||
export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
if (!all.length) return null;
|
||||
const latest = all[all.length - 1];
|
||||
const body = fs.readFileSync(latest, 'utf-8');
|
||||
return { path: latest, body, meta: parseFrontmatter(body) };
|
||||
return readLatestSnapshotMatching(`__${slug}.md`, cwd);
|
||||
}
|
||||
|
||||
/** Return the most recent snapshot across all targets, or null. */
|
||||
export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) {
|
||||
return readLatestSnapshotMatching('.md', cwd);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,7 +143,7 @@ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
* Critique appends a one-line trend to its output using this.
|
||||
*/
|
||||
export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
const all = listSnapshots(`__${slug}.md`, cwd);
|
||||
const slice = all.slice(-limit);
|
||||
return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8')));
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ export { detectUrl, createBrowserDetector } from './engines/browser/detect-url.m
|
||||
export { detectText, extractStyleBlocks, extractCSSinJS } from './engines/regex/detect-text.mjs';
|
||||
export {
|
||||
walkDir,
|
||||
hasScannableExtension,
|
||||
SCANNABLE_EXTENSIONS,
|
||||
SKIP_DIRS,
|
||||
buildImportGraph,
|
||||
|
||||
@@ -26,11 +26,20 @@ const HIDDEN_SOURCE_DIRS = new Set(['.vitepress', '.vuepress', '.storybook']);
|
||||
const SCANNABLE_EXTENSIONS = new Set([
|
||||
'.html', '.htm', '.css', '.scss', '.sass', '.less',
|
||||
'.jsx', '.tsx', '.js', '.ts',
|
||||
'.vue', '.svelte', '.astro',
|
||||
'.vue', '.svelte', '.astro', '.blade.php',
|
||||
]);
|
||||
|
||||
const HTML_EXTENSIONS = new Set(['.html', '.htm']);
|
||||
|
||||
function hasScannableExtension(filename) {
|
||||
const lower = filename.toLowerCase();
|
||||
if (SCANNABLE_EXTENSIONS.has(path.extname(lower))) return true;
|
||||
for (const ext of SCANNABLE_EXTENSIONS) {
|
||||
if (ext.indexOf('.', 1) !== -1 && lower.endsWith(ext)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const IMPORT_SPECIFIER_PATTERNS = [
|
||||
/import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g,
|
||||
/@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g,
|
||||
@@ -46,7 +55,7 @@ function walkDir(dir) {
|
||||
if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue;
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) files.push(...walkDir(full));
|
||||
else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full);
|
||||
else if (hasScannableExtension(entry.name)) files.push(full);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
@@ -194,6 +203,7 @@ export {
|
||||
SKIP_DIRS,
|
||||
SCANNABLE_EXTENSIONS,
|
||||
HTML_EXTENSIONS,
|
||||
hasScannableExtension,
|
||||
walkDir,
|
||||
resolveImport,
|
||||
buildImportGraph,
|
||||
|
||||
@@ -109,6 +109,18 @@ export function validateConceptEntry(concept, { existingForms = new Map(), axes
|
||||
|| concept.tags.some(tag => typeof tag !== 'string' || !tag.trim())) {
|
||||
errors.push(`concept ${id} must have exactly three structural tags`);
|
||||
}
|
||||
// The slop this world in particular is at risk of. Optional, because 541
|
||||
// entries predate it and none of them are wrong for lacking it. A world built
|
||||
// from posters is at risk of shouting and one built from instruments is at
|
||||
// risk of dead greys; a global detector cannot know which, and the author can.
|
||||
if (concept?.avoid !== undefined) {
|
||||
if (!Array.isArray(concept.avoid)
|
||||
|| concept.avoid.length < 2
|
||||
|| concept.avoid.length > 3
|
||||
|| concept.avoid.some(item => typeof item !== 'string' || item.trim().length < 12 || item.trim().length > 160)) {
|
||||
errors.push(`concept ${id} avoid must be two or three negations of 12–160 characters`);
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(concept?.system)
|
||||
|| concept.system.length !== SYSTEM_PREFIXES.length
|
||||
|| concept.system.some(rule => typeof rule !== 'string' || rule.trim().length < 12 || rule.trim().length > 180)) {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
export function browserOpenCommand(url, {
|
||||
platform = process.platform,
|
||||
comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe',
|
||||
} = {}) {
|
||||
if (platform === 'darwin') return { command: 'open', args: [url] };
|
||||
if (platform === 'win32') return { command: comspec, args: ['/c', 'start', '', url] };
|
||||
return { command: 'xdg-open', args: [url] };
|
||||
}
|
||||
|
||||
export function openSystemBrowser(url, {
|
||||
platform = process.platform,
|
||||
comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe',
|
||||
spawnImpl = spawn,
|
||||
} = {}) {
|
||||
const { command, args } = browserOpenCommand(url, { platform, comspec });
|
||||
try {
|
||||
const child = spawnImpl(command, args, { stdio: 'ignore', detached: true });
|
||||
child.on('error', () => {});
|
||||
child.unref();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -93,15 +93,17 @@ function commandPrefixForSkillsDir(skillsDir) {
|
||||
return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/';
|
||||
}
|
||||
|
||||
function generatePinnedSkill(command, metadata, commandPrefix) {
|
||||
function generatePinnedSkill(command, metadata, commandPrefix, isCodex) {
|
||||
const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`;
|
||||
const hint = metadata[command]?.argumentHint || '[target]';
|
||||
const providerFrontmatter = isCodex
|
||||
? `metadata:\n argument-hint: "${hint}"`
|
||||
: `argument-hint: "${hint}"\nuser-invocable: true`;
|
||||
|
||||
return `---
|
||||
name: ${command}
|
||||
description: "${desc}"
|
||||
argument-hint: "${hint}"
|
||||
user-invocable: true
|
||||
${providerFrontmatter}
|
||||
---
|
||||
|
||||
${PIN_MARKER}
|
||||
@@ -128,7 +130,7 @@ function pin(command, projectRoot) {
|
||||
|
||||
for (const skillsDir of harnessDirs) {
|
||||
const commandPrefix = commandPrefixForSkillsDir(skillsDir);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$');
|
||||
// Check if skill already exists (and isn't a pin)
|
||||
const skillDir = join(skillsDir, command);
|
||||
if (existsSync(skillDir)) {
|
||||
|
||||
@@ -79,6 +79,7 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { openSystemBrowser } from './lib/open-system-browser.mjs';
|
||||
|
||||
function arg(name, fallback = null) {
|
||||
const i = process.argv.indexOf(`--${name}`);
|
||||
@@ -976,8 +977,7 @@ server.listen(portArg, '127.0.0.1', () => {
|
||||
console.log('Waiting for the user to choose in the browser (Ctrl-C aborts)...');
|
||||
}
|
||||
if (!hasFlag('no-open')) {
|
||||
const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
||||
try { spawn(opener, [url], { stdio: 'ignore', detached: true }).unref(); } catch { /* URL printed anyway */ }
|
||||
openSystemBrowser(url);
|
||||
}
|
||||
if (timeoutSec > 0) {
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -12,6 +12,7 @@ Each of these is a check on the built result, not an intention. Run them togethe
|
||||
- **Type:** body measure 65–75ch, display max 6rem, tracking floor -0.04em, balanced headings, obvious scale and weight steps. Run the real copy at every breakpoint and fix what overflows.
|
||||
- **Motion:** one authored moment, not scattered effects and not one identical entrance on every section. Exponential ease-out from an already-visible default. Reach past transform and opacity: blur, backdrop-filter, clip-path, mask, and shadow belong to the palette when they stay smooth.
|
||||
- **States:** hover, disabled, loading, error, empty. Plus real content, working controls, responsive composition, keyboard focus.
|
||||
- **Browser surfaces:** the parts you did not draw still carry the design. Text selection, the caret, custom scrollbars, focus rings, underline offset, and the numerals in tabular data all ship with browser defaults that belong to no design system. Theme them from the palette. This is the cheapest signal that a page was built rather than assembled, and the one models skip most reliably.
|
||||
- **Copy:** the product's own language. Controls name their action; errors name the problem and the recovery.
|
||||
- **Coverage:** every brief requirement present and findable within seconds.
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { loadContext, extractPlatform } from './context.mjs';
|
||||
import { getCritiqueDir } from './lib/impeccable-paths.mjs';
|
||||
import { readLatestSnapshotAcrossTargets } from './critique-storage.mjs';
|
||||
|
||||
/** Is there code here at all, or just context files / an empty repo? */
|
||||
function hasCode(cwd) {
|
||||
@@ -34,23 +34,13 @@ function hasCode(cwd) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The most recent critique snapshot across all targets. Filenames are
|
||||
* timestamp-prefixed (`<iso>__<slug>.md`), so a lexical sort is chronological.
|
||||
* Parses the small frontmatter for score + P0/P1 counts.
|
||||
* Summarize the most recent critique snapshot across all targets.
|
||||
*/
|
||||
function latestCritique(cwd) {
|
||||
try {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return null;
|
||||
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort();
|
||||
if (!files.length) return null;
|
||||
const newest = files[files.length - 1];
|
||||
const text = fs.readFileSync(path.join(dir, newest), 'utf-8');
|
||||
const front = text.split('---')[1] || '';
|
||||
const get = (k) => {
|
||||
const m = front.match(new RegExp(`^${k}:\\s*(.+)$`, 'm'));
|
||||
return m ? m[1].trim() : null;
|
||||
};
|
||||
const latest = readLatestSnapshotAcrossTargets({ cwd });
|
||||
if (!latest) return null;
|
||||
const get = (key) => latest.meta[key] ?? null;
|
||||
const num = (v) => {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
@@ -61,7 +51,7 @@ function latestCritique(cwd) {
|
||||
p0: num(get('p0')),
|
||||
p1: num(get('p1')),
|
||||
timestamp: get('timestamp'),
|
||||
file: path.relative(cwd, path.join(dir, newest)),
|
||||
file: path.relative(cwd, latest.path),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
@@ -105,28 +105,37 @@ function parseFrontmatter(text) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all snapshot files for `slug`, sorted oldest → newest.
|
||||
* Return snapshot files matching `suffix`, sorted oldest → newest.
|
||||
*/
|
||||
function listSnapshotsForSlug(slug, cwd) {
|
||||
const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/;
|
||||
|
||||
function listSnapshots(suffix, cwd) {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
const suffix = `__${slug}.md`;
|
||||
return fs.readdirSync(dir)
|
||||
.filter((f) => f.endsWith(suffix))
|
||||
.filter((f) => SNAPSHOT_FILENAME.test(f) && f.endsWith(suffix))
|
||||
.sort()
|
||||
.map((f) => path.join(dir, f));
|
||||
}
|
||||
|
||||
function readLatestSnapshotMatching(suffix, cwd) {
|
||||
const filePath = listSnapshots(suffix, cwd).at(-1);
|
||||
if (!filePath) return null;
|
||||
const body = fs.readFileSync(filePath, 'utf-8');
|
||||
return { path: filePath, body, meta: parseFrontmatter(body) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the most recent snapshot for `slug`, or null. Polish reads this
|
||||
* to find its fix backlog when the slug matches.
|
||||
*/
|
||||
export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
if (!all.length) return null;
|
||||
const latest = all[all.length - 1];
|
||||
const body = fs.readFileSync(latest, 'utf-8');
|
||||
return { path: latest, body, meta: parseFrontmatter(body) };
|
||||
return readLatestSnapshotMatching(`__${slug}.md`, cwd);
|
||||
}
|
||||
|
||||
/** Return the most recent snapshot across all targets, or null. */
|
||||
export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) {
|
||||
return readLatestSnapshotMatching('.md', cwd);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,7 +143,7 @@ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
* Critique appends a one-line trend to its output using this.
|
||||
*/
|
||||
export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
const all = listSnapshots(`__${slug}.md`, cwd);
|
||||
const slice = all.slice(-limit);
|
||||
return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8')));
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ export { detectUrl, createBrowserDetector } from './engines/browser/detect-url.m
|
||||
export { detectText, extractStyleBlocks, extractCSSinJS } from './engines/regex/detect-text.mjs';
|
||||
export {
|
||||
walkDir,
|
||||
hasScannableExtension,
|
||||
SCANNABLE_EXTENSIONS,
|
||||
SKIP_DIRS,
|
||||
buildImportGraph,
|
||||
|
||||
@@ -26,11 +26,20 @@ const HIDDEN_SOURCE_DIRS = new Set(['.vitepress', '.vuepress', '.storybook']);
|
||||
const SCANNABLE_EXTENSIONS = new Set([
|
||||
'.html', '.htm', '.css', '.scss', '.sass', '.less',
|
||||
'.jsx', '.tsx', '.js', '.ts',
|
||||
'.vue', '.svelte', '.astro',
|
||||
'.vue', '.svelte', '.astro', '.blade.php',
|
||||
]);
|
||||
|
||||
const HTML_EXTENSIONS = new Set(['.html', '.htm']);
|
||||
|
||||
function hasScannableExtension(filename) {
|
||||
const lower = filename.toLowerCase();
|
||||
if (SCANNABLE_EXTENSIONS.has(path.extname(lower))) return true;
|
||||
for (const ext of SCANNABLE_EXTENSIONS) {
|
||||
if (ext.indexOf('.', 1) !== -1 && lower.endsWith(ext)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const IMPORT_SPECIFIER_PATTERNS = [
|
||||
/import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g,
|
||||
/@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g,
|
||||
@@ -46,7 +55,7 @@ function walkDir(dir) {
|
||||
if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue;
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) files.push(...walkDir(full));
|
||||
else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full);
|
||||
else if (hasScannableExtension(entry.name)) files.push(full);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
@@ -194,6 +203,7 @@ export {
|
||||
SKIP_DIRS,
|
||||
SCANNABLE_EXTENSIONS,
|
||||
HTML_EXTENSIONS,
|
||||
hasScannableExtension,
|
||||
walkDir,
|
||||
resolveImport,
|
||||
buildImportGraph,
|
||||
|
||||
@@ -109,6 +109,18 @@ export function validateConceptEntry(concept, { existingForms = new Map(), axes
|
||||
|| concept.tags.some(tag => typeof tag !== 'string' || !tag.trim())) {
|
||||
errors.push(`concept ${id} must have exactly three structural tags`);
|
||||
}
|
||||
// The slop this world in particular is at risk of. Optional, because 541
|
||||
// entries predate it and none of them are wrong for lacking it. A world built
|
||||
// from posters is at risk of shouting and one built from instruments is at
|
||||
// risk of dead greys; a global detector cannot know which, and the author can.
|
||||
if (concept?.avoid !== undefined) {
|
||||
if (!Array.isArray(concept.avoid)
|
||||
|| concept.avoid.length < 2
|
||||
|| concept.avoid.length > 3
|
||||
|| concept.avoid.some(item => typeof item !== 'string' || item.trim().length < 12 || item.trim().length > 160)) {
|
||||
errors.push(`concept ${id} avoid must be two or three negations of 12–160 characters`);
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(concept?.system)
|
||||
|| concept.system.length !== SYSTEM_PREFIXES.length
|
||||
|| concept.system.some(rule => typeof rule !== 'string' || rule.trim().length < 12 || rule.trim().length > 180)) {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
export function browserOpenCommand(url, {
|
||||
platform = process.platform,
|
||||
comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe',
|
||||
} = {}) {
|
||||
if (platform === 'darwin') return { command: 'open', args: [url] };
|
||||
if (platform === 'win32') return { command: comspec, args: ['/c', 'start', '', url] };
|
||||
return { command: 'xdg-open', args: [url] };
|
||||
}
|
||||
|
||||
export function openSystemBrowser(url, {
|
||||
platform = process.platform,
|
||||
comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe',
|
||||
spawnImpl = spawn,
|
||||
} = {}) {
|
||||
const { command, args } = browserOpenCommand(url, { platform, comspec });
|
||||
try {
|
||||
const child = spawnImpl(command, args, { stdio: 'ignore', detached: true });
|
||||
child.on('error', () => {});
|
||||
child.unref();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -93,15 +93,17 @@ function commandPrefixForSkillsDir(skillsDir) {
|
||||
return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/';
|
||||
}
|
||||
|
||||
function generatePinnedSkill(command, metadata, commandPrefix) {
|
||||
function generatePinnedSkill(command, metadata, commandPrefix, isCodex) {
|
||||
const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`;
|
||||
const hint = metadata[command]?.argumentHint || '[target]';
|
||||
const providerFrontmatter = isCodex
|
||||
? `metadata:\n argument-hint: "${hint}"`
|
||||
: `argument-hint: "${hint}"\nuser-invocable: true`;
|
||||
|
||||
return `---
|
||||
name: ${command}
|
||||
description: "${desc}"
|
||||
argument-hint: "${hint}"
|
||||
user-invocable: true
|
||||
${providerFrontmatter}
|
||||
---
|
||||
|
||||
${PIN_MARKER}
|
||||
@@ -128,7 +130,7 @@ function pin(command, projectRoot) {
|
||||
|
||||
for (const skillsDir of harnessDirs) {
|
||||
const commandPrefix = commandPrefixForSkillsDir(skillsDir);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$');
|
||||
// Check if skill already exists (and isn't a pin)
|
||||
const skillDir = join(skillsDir, command);
|
||||
if (existsSync(skillDir)) {
|
||||
|
||||
@@ -79,6 +79,7 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { openSystemBrowser } from './lib/open-system-browser.mjs';
|
||||
|
||||
function arg(name, fallback = null) {
|
||||
const i = process.argv.indexOf(`--${name}`);
|
||||
@@ -976,8 +977,7 @@ server.listen(portArg, '127.0.0.1', () => {
|
||||
console.log('Waiting for the user to choose in the browser (Ctrl-C aborts)...');
|
||||
}
|
||||
if (!hasFlag('no-open')) {
|
||||
const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
||||
try { spawn(opener, [url], { stdio: 'ignore', detached: true }).unref(); } catch { /* URL printed anyway */ }
|
||||
openSystemBrowser(url);
|
||||
}
|
||||
if (timeoutSec > 0) {
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -12,6 +12,7 @@ Each of these is a check on the built result, not an intention. Run them togethe
|
||||
- **Type:** body measure 65–75ch, display max 6rem, tracking floor -0.04em, balanced headings, obvious scale and weight steps. Run the real copy at every breakpoint and fix what overflows.
|
||||
- **Motion:** one authored moment, not scattered effects and not one identical entrance on every section. Exponential ease-out from an already-visible default. Reach past transform and opacity: blur, backdrop-filter, clip-path, mask, and shadow belong to the palette when they stay smooth.
|
||||
- **States:** hover, disabled, loading, error, empty. Plus real content, working controls, responsive composition, keyboard focus.
|
||||
- **Browser surfaces:** the parts you did not draw still carry the design. Text selection, the caret, custom scrollbars, focus rings, underline offset, and the numerals in tabular data all ship with browser defaults that belong to no design system. Theme them from the palette. This is the cheapest signal that a page was built rather than assembled, and the one models skip most reliably.
|
||||
- **Copy:** the product's own language. Controls name their action; errors name the problem and the recovery.
|
||||
- **Coverage:** every brief requirement present and findable within seconds.
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { loadContext, extractPlatform } from './context.mjs';
|
||||
import { getCritiqueDir } from './lib/impeccable-paths.mjs';
|
||||
import { readLatestSnapshotAcrossTargets } from './critique-storage.mjs';
|
||||
|
||||
/** Is there code here at all, or just context files / an empty repo? */
|
||||
function hasCode(cwd) {
|
||||
@@ -34,23 +34,13 @@ function hasCode(cwd) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The most recent critique snapshot across all targets. Filenames are
|
||||
* timestamp-prefixed (`<iso>__<slug>.md`), so a lexical sort is chronological.
|
||||
* Parses the small frontmatter for score + P0/P1 counts.
|
||||
* Summarize the most recent critique snapshot across all targets.
|
||||
*/
|
||||
function latestCritique(cwd) {
|
||||
try {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return null;
|
||||
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort();
|
||||
if (!files.length) return null;
|
||||
const newest = files[files.length - 1];
|
||||
const text = fs.readFileSync(path.join(dir, newest), 'utf-8');
|
||||
const front = text.split('---')[1] || '';
|
||||
const get = (k) => {
|
||||
const m = front.match(new RegExp(`^${k}:\\s*(.+)$`, 'm'));
|
||||
return m ? m[1].trim() : null;
|
||||
};
|
||||
const latest = readLatestSnapshotAcrossTargets({ cwd });
|
||||
if (!latest) return null;
|
||||
const get = (key) => latest.meta[key] ?? null;
|
||||
const num = (v) => {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
@@ -61,7 +51,7 @@ function latestCritique(cwd) {
|
||||
p0: num(get('p0')),
|
||||
p1: num(get('p1')),
|
||||
timestamp: get('timestamp'),
|
||||
file: path.relative(cwd, path.join(dir, newest)),
|
||||
file: path.relative(cwd, latest.path),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
@@ -105,28 +105,37 @@ function parseFrontmatter(text) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all snapshot files for `slug`, sorted oldest → newest.
|
||||
* Return snapshot files matching `suffix`, sorted oldest → newest.
|
||||
*/
|
||||
function listSnapshotsForSlug(slug, cwd) {
|
||||
const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/;
|
||||
|
||||
function listSnapshots(suffix, cwd) {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
const suffix = `__${slug}.md`;
|
||||
return fs.readdirSync(dir)
|
||||
.filter((f) => f.endsWith(suffix))
|
||||
.filter((f) => SNAPSHOT_FILENAME.test(f) && f.endsWith(suffix))
|
||||
.sort()
|
||||
.map((f) => path.join(dir, f));
|
||||
}
|
||||
|
||||
function readLatestSnapshotMatching(suffix, cwd) {
|
||||
const filePath = listSnapshots(suffix, cwd).at(-1);
|
||||
if (!filePath) return null;
|
||||
const body = fs.readFileSync(filePath, 'utf-8');
|
||||
return { path: filePath, body, meta: parseFrontmatter(body) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the most recent snapshot for `slug`, or null. Polish reads this
|
||||
* to find its fix backlog when the slug matches.
|
||||
*/
|
||||
export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
if (!all.length) return null;
|
||||
const latest = all[all.length - 1];
|
||||
const body = fs.readFileSync(latest, 'utf-8');
|
||||
return { path: latest, body, meta: parseFrontmatter(body) };
|
||||
return readLatestSnapshotMatching(`__${slug}.md`, cwd);
|
||||
}
|
||||
|
||||
/** Return the most recent snapshot across all targets, or null. */
|
||||
export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) {
|
||||
return readLatestSnapshotMatching('.md', cwd);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,7 +143,7 @@ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
* Critique appends a one-line trend to its output using this.
|
||||
*/
|
||||
export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
const all = listSnapshots(`__${slug}.md`, cwd);
|
||||
const slice = all.slice(-limit);
|
||||
return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8')));
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ export { detectUrl, createBrowserDetector } from './engines/browser/detect-url.m
|
||||
export { detectText, extractStyleBlocks, extractCSSinJS } from './engines/regex/detect-text.mjs';
|
||||
export {
|
||||
walkDir,
|
||||
hasScannableExtension,
|
||||
SCANNABLE_EXTENSIONS,
|
||||
SKIP_DIRS,
|
||||
buildImportGraph,
|
||||
|
||||
@@ -26,11 +26,20 @@ const HIDDEN_SOURCE_DIRS = new Set(['.vitepress', '.vuepress', '.storybook']);
|
||||
const SCANNABLE_EXTENSIONS = new Set([
|
||||
'.html', '.htm', '.css', '.scss', '.sass', '.less',
|
||||
'.jsx', '.tsx', '.js', '.ts',
|
||||
'.vue', '.svelte', '.astro',
|
||||
'.vue', '.svelte', '.astro', '.blade.php',
|
||||
]);
|
||||
|
||||
const HTML_EXTENSIONS = new Set(['.html', '.htm']);
|
||||
|
||||
function hasScannableExtension(filename) {
|
||||
const lower = filename.toLowerCase();
|
||||
if (SCANNABLE_EXTENSIONS.has(path.extname(lower))) return true;
|
||||
for (const ext of SCANNABLE_EXTENSIONS) {
|
||||
if (ext.indexOf('.', 1) !== -1 && lower.endsWith(ext)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const IMPORT_SPECIFIER_PATTERNS = [
|
||||
/import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g,
|
||||
/@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g,
|
||||
@@ -46,7 +55,7 @@ function walkDir(dir) {
|
||||
if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue;
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) files.push(...walkDir(full));
|
||||
else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full);
|
||||
else if (hasScannableExtension(entry.name)) files.push(full);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
@@ -194,6 +203,7 @@ export {
|
||||
SKIP_DIRS,
|
||||
SCANNABLE_EXTENSIONS,
|
||||
HTML_EXTENSIONS,
|
||||
hasScannableExtension,
|
||||
walkDir,
|
||||
resolveImport,
|
||||
buildImportGraph,
|
||||
|
||||
@@ -109,6 +109,18 @@ export function validateConceptEntry(concept, { existingForms = new Map(), axes
|
||||
|| concept.tags.some(tag => typeof tag !== 'string' || !tag.trim())) {
|
||||
errors.push(`concept ${id} must have exactly three structural tags`);
|
||||
}
|
||||
// The slop this world in particular is at risk of. Optional, because 541
|
||||
// entries predate it and none of them are wrong for lacking it. A world built
|
||||
// from posters is at risk of shouting and one built from instruments is at
|
||||
// risk of dead greys; a global detector cannot know which, and the author can.
|
||||
if (concept?.avoid !== undefined) {
|
||||
if (!Array.isArray(concept.avoid)
|
||||
|| concept.avoid.length < 2
|
||||
|| concept.avoid.length > 3
|
||||
|| concept.avoid.some(item => typeof item !== 'string' || item.trim().length < 12 || item.trim().length > 160)) {
|
||||
errors.push(`concept ${id} avoid must be two or three negations of 12–160 characters`);
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(concept?.system)
|
||||
|| concept.system.length !== SYSTEM_PREFIXES.length
|
||||
|| concept.system.some(rule => typeof rule !== 'string' || rule.trim().length < 12 || rule.trim().length > 180)) {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
export function browserOpenCommand(url, {
|
||||
platform = process.platform,
|
||||
comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe',
|
||||
} = {}) {
|
||||
if (platform === 'darwin') return { command: 'open', args: [url] };
|
||||
if (platform === 'win32') return { command: comspec, args: ['/c', 'start', '', url] };
|
||||
return { command: 'xdg-open', args: [url] };
|
||||
}
|
||||
|
||||
export function openSystemBrowser(url, {
|
||||
platform = process.platform,
|
||||
comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe',
|
||||
spawnImpl = spawn,
|
||||
} = {}) {
|
||||
const { command, args } = browserOpenCommand(url, { platform, comspec });
|
||||
try {
|
||||
const child = spawnImpl(command, args, { stdio: 'ignore', detached: true });
|
||||
child.on('error', () => {});
|
||||
child.unref();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -93,15 +93,17 @@ function commandPrefixForSkillsDir(skillsDir) {
|
||||
return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/';
|
||||
}
|
||||
|
||||
function generatePinnedSkill(command, metadata, commandPrefix) {
|
||||
function generatePinnedSkill(command, metadata, commandPrefix, isCodex) {
|
||||
const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`;
|
||||
const hint = metadata[command]?.argumentHint || '[target]';
|
||||
const providerFrontmatter = isCodex
|
||||
? `metadata:\n argument-hint: "${hint}"`
|
||||
: `argument-hint: "${hint}"\nuser-invocable: true`;
|
||||
|
||||
return `---
|
||||
name: ${command}
|
||||
description: "${desc}"
|
||||
argument-hint: "${hint}"
|
||||
user-invocable: true
|
||||
${providerFrontmatter}
|
||||
---
|
||||
|
||||
${PIN_MARKER}
|
||||
@@ -128,7 +130,7 @@ function pin(command, projectRoot) {
|
||||
|
||||
for (const skillsDir of harnessDirs) {
|
||||
const commandPrefix = commandPrefixForSkillsDir(skillsDir);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$');
|
||||
// Check if skill already exists (and isn't a pin)
|
||||
const skillDir = join(skillsDir, command);
|
||||
if (existsSync(skillDir)) {
|
||||
|
||||
@@ -79,6 +79,7 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { openSystemBrowser } from './lib/open-system-browser.mjs';
|
||||
|
||||
function arg(name, fallback = null) {
|
||||
const i = process.argv.indexOf(`--${name}`);
|
||||
@@ -976,8 +977,7 @@ server.listen(portArg, '127.0.0.1', () => {
|
||||
console.log('Waiting for the user to choose in the browser (Ctrl-C aborts)...');
|
||||
}
|
||||
if (!hasFlag('no-open')) {
|
||||
const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
||||
try { spawn(opener, [url], { stdio: 'ignore', detached: true }).unref(); } catch { /* URL printed anyway */ }
|
||||
openSystemBrowser(url);
|
||||
}
|
||||
if (timeoutSec > 0) {
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -12,6 +12,7 @@ Each of these is a check on the built result, not an intention. Run them togethe
|
||||
- **Type:** body measure 65–75ch, display max 6rem, tracking floor -0.04em, balanced headings, obvious scale and weight steps. Run the real copy at every breakpoint and fix what overflows.
|
||||
- **Motion:** one authored moment, not scattered effects and not one identical entrance on every section. Exponential ease-out from an already-visible default. Reach past transform and opacity: blur, backdrop-filter, clip-path, mask, and shadow belong to the palette when they stay smooth.
|
||||
- **States:** hover, disabled, loading, error, empty. Plus real content, working controls, responsive composition, keyboard focus.
|
||||
- **Browser surfaces:** the parts you did not draw still carry the design. Text selection, the caret, custom scrollbars, focus rings, underline offset, and the numerals in tabular data all ship with browser defaults that belong to no design system. Theme them from the palette. This is the cheapest signal that a page was built rather than assembled, and the one models skip most reliably.
|
||||
- **Copy:** the product's own language. Controls name their action; errors name the problem and the recovery.
|
||||
- **Coverage:** every brief requirement present and findable within seconds.
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { loadContext, extractPlatform } from './context.mjs';
|
||||
import { getCritiqueDir } from './lib/impeccable-paths.mjs';
|
||||
import { readLatestSnapshotAcrossTargets } from './critique-storage.mjs';
|
||||
|
||||
/** Is there code here at all, or just context files / an empty repo? */
|
||||
function hasCode(cwd) {
|
||||
@@ -34,23 +34,13 @@ function hasCode(cwd) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The most recent critique snapshot across all targets. Filenames are
|
||||
* timestamp-prefixed (`<iso>__<slug>.md`), so a lexical sort is chronological.
|
||||
* Parses the small frontmatter for score + P0/P1 counts.
|
||||
* Summarize the most recent critique snapshot across all targets.
|
||||
*/
|
||||
function latestCritique(cwd) {
|
||||
try {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return null;
|
||||
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort();
|
||||
if (!files.length) return null;
|
||||
const newest = files[files.length - 1];
|
||||
const text = fs.readFileSync(path.join(dir, newest), 'utf-8');
|
||||
const front = text.split('---')[1] || '';
|
||||
const get = (k) => {
|
||||
const m = front.match(new RegExp(`^${k}:\\s*(.+)$`, 'm'));
|
||||
return m ? m[1].trim() : null;
|
||||
};
|
||||
const latest = readLatestSnapshotAcrossTargets({ cwd });
|
||||
if (!latest) return null;
|
||||
const get = (key) => latest.meta[key] ?? null;
|
||||
const num = (v) => {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
@@ -61,7 +51,7 @@ function latestCritique(cwd) {
|
||||
p0: num(get('p0')),
|
||||
p1: num(get('p1')),
|
||||
timestamp: get('timestamp'),
|
||||
file: path.relative(cwd, path.join(dir, newest)),
|
||||
file: path.relative(cwd, latest.path),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
@@ -105,28 +105,37 @@ function parseFrontmatter(text) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all snapshot files for `slug`, sorted oldest → newest.
|
||||
* Return snapshot files matching `suffix`, sorted oldest → newest.
|
||||
*/
|
||||
function listSnapshotsForSlug(slug, cwd) {
|
||||
const SNAPSHOT_FILENAME = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z__.+\.md$/;
|
||||
|
||||
function listSnapshots(suffix, cwd) {
|
||||
const dir = getCritiqueDir(cwd);
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
const suffix = `__${slug}.md`;
|
||||
return fs.readdirSync(dir)
|
||||
.filter((f) => f.endsWith(suffix))
|
||||
.filter((f) => SNAPSHOT_FILENAME.test(f) && f.endsWith(suffix))
|
||||
.sort()
|
||||
.map((f) => path.join(dir, f));
|
||||
}
|
||||
|
||||
function readLatestSnapshotMatching(suffix, cwd) {
|
||||
const filePath = listSnapshots(suffix, cwd).at(-1);
|
||||
if (!filePath) return null;
|
||||
const body = fs.readFileSync(filePath, 'utf-8');
|
||||
return { path: filePath, body, meta: parseFrontmatter(body) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the most recent snapshot for `slug`, or null. Polish reads this
|
||||
* to find its fix backlog when the slug matches.
|
||||
*/
|
||||
export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
if (!all.length) return null;
|
||||
const latest = all[all.length - 1];
|
||||
const body = fs.readFileSync(latest, 'utf-8');
|
||||
return { path: latest, body, meta: parseFrontmatter(body) };
|
||||
return readLatestSnapshotMatching(`__${slug}.md`, cwd);
|
||||
}
|
||||
|
||||
/** Return the most recent snapshot across all targets, or null. */
|
||||
export function readLatestSnapshotAcrossTargets({ cwd = process.cwd() } = {}) {
|
||||
return readLatestSnapshotMatching('.md', cwd);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,7 +143,7 @@ export function readLatestSnapshot(slug, { cwd = process.cwd() } = {}) {
|
||||
* Critique appends a one-line trend to its output using this.
|
||||
*/
|
||||
export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
|
||||
const all = listSnapshotsForSlug(slug, cwd);
|
||||
const all = listSnapshots(`__${slug}.md`, cwd);
|
||||
const slice = all.slice(-limit);
|
||||
return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8')));
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ export { detectUrl, createBrowserDetector } from './engines/browser/detect-url.m
|
||||
export { detectText, extractStyleBlocks, extractCSSinJS } from './engines/regex/detect-text.mjs';
|
||||
export {
|
||||
walkDir,
|
||||
hasScannableExtension,
|
||||
SCANNABLE_EXTENSIONS,
|
||||
SKIP_DIRS,
|
||||
buildImportGraph,
|
||||
|
||||
@@ -26,11 +26,20 @@ const HIDDEN_SOURCE_DIRS = new Set(['.vitepress', '.vuepress', '.storybook']);
|
||||
const SCANNABLE_EXTENSIONS = new Set([
|
||||
'.html', '.htm', '.css', '.scss', '.sass', '.less',
|
||||
'.jsx', '.tsx', '.js', '.ts',
|
||||
'.vue', '.svelte', '.astro',
|
||||
'.vue', '.svelte', '.astro', '.blade.php',
|
||||
]);
|
||||
|
||||
const HTML_EXTENSIONS = new Set(['.html', '.htm']);
|
||||
|
||||
function hasScannableExtension(filename) {
|
||||
const lower = filename.toLowerCase();
|
||||
if (SCANNABLE_EXTENSIONS.has(path.extname(lower))) return true;
|
||||
for (const ext of SCANNABLE_EXTENSIONS) {
|
||||
if (ext.indexOf('.', 1) !== -1 && lower.endsWith(ext)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const IMPORT_SPECIFIER_PATTERNS = [
|
||||
/import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g,
|
||||
/@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g,
|
||||
@@ -46,7 +55,7 @@ function walkDir(dir) {
|
||||
if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue;
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) files.push(...walkDir(full));
|
||||
else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full);
|
||||
else if (hasScannableExtension(entry.name)) files.push(full);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
@@ -194,6 +203,7 @@ export {
|
||||
SKIP_DIRS,
|
||||
SCANNABLE_EXTENSIONS,
|
||||
HTML_EXTENSIONS,
|
||||
hasScannableExtension,
|
||||
walkDir,
|
||||
resolveImport,
|
||||
buildImportGraph,
|
||||
|
||||
@@ -109,6 +109,18 @@ export function validateConceptEntry(concept, { existingForms = new Map(), axes
|
||||
|| concept.tags.some(tag => typeof tag !== 'string' || !tag.trim())) {
|
||||
errors.push(`concept ${id} must have exactly three structural tags`);
|
||||
}
|
||||
// The slop this world in particular is at risk of. Optional, because 541
|
||||
// entries predate it and none of them are wrong for lacking it. A world built
|
||||
// from posters is at risk of shouting and one built from instruments is at
|
||||
// risk of dead greys; a global detector cannot know which, and the author can.
|
||||
if (concept?.avoid !== undefined) {
|
||||
if (!Array.isArray(concept.avoid)
|
||||
|| concept.avoid.length < 2
|
||||
|| concept.avoid.length > 3
|
||||
|| concept.avoid.some(item => typeof item !== 'string' || item.trim().length < 12 || item.trim().length > 160)) {
|
||||
errors.push(`concept ${id} avoid must be two or three negations of 12–160 characters`);
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(concept?.system)
|
||||
|| concept.system.length !== SYSTEM_PREFIXES.length
|
||||
|| concept.system.some(rule => typeof rule !== 'string' || rule.trim().length < 12 || rule.trim().length > 180)) {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
export function browserOpenCommand(url, {
|
||||
platform = process.platform,
|
||||
comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe',
|
||||
} = {}) {
|
||||
if (platform === 'darwin') return { command: 'open', args: [url] };
|
||||
if (platform === 'win32') return { command: comspec, args: ['/c', 'start', '', url] };
|
||||
return { command: 'xdg-open', args: [url] };
|
||||
}
|
||||
|
||||
export function openSystemBrowser(url, {
|
||||
platform = process.platform,
|
||||
comspec = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe',
|
||||
spawnImpl = spawn,
|
||||
} = {}) {
|
||||
const { command, args } = browserOpenCommand(url, { platform, comspec });
|
||||
try {
|
||||
const child = spawnImpl(command, args, { stdio: 'ignore', detached: true });
|
||||
child.on('error', () => {});
|
||||
child.unref();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -93,15 +93,17 @@ function commandPrefixForSkillsDir(skillsDir) {
|
||||
return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/';
|
||||
}
|
||||
|
||||
function generatePinnedSkill(command, metadata, commandPrefix) {
|
||||
function generatePinnedSkill(command, metadata, commandPrefix, isCodex) {
|
||||
const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}impeccable ${command}.`;
|
||||
const hint = metadata[command]?.argumentHint || '[target]';
|
||||
const providerFrontmatter = isCodex
|
||||
? `metadata:\n argument-hint: "${hint}"`
|
||||
: `argument-hint: "${hint}"\nuser-invocable: true`;
|
||||
|
||||
return `---
|
||||
name: ${command}
|
||||
description: "${desc}"
|
||||
argument-hint: "${hint}"
|
||||
user-invocable: true
|
||||
${providerFrontmatter}
|
||||
---
|
||||
|
||||
${PIN_MARKER}
|
||||
@@ -128,7 +130,7 @@ function pin(command, projectRoot) {
|
||||
|
||||
for (const skillsDir of harnessDirs) {
|
||||
const commandPrefix = commandPrefixForSkillsDir(skillsDir);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$');
|
||||
// Check if skill already exists (and isn't a pin)
|
||||
const skillDir = join(skillsDir, command);
|
||||
if (existsSync(skillDir)) {
|
||||
|
||||
@@ -79,6 +79,7 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { openSystemBrowser } from './lib/open-system-browser.mjs';
|
||||
|
||||
function arg(name, fallback = null) {
|
||||
const i = process.argv.indexOf(`--${name}`);
|
||||
@@ -976,8 +977,7 @@ server.listen(portArg, '127.0.0.1', () => {
|
||||
console.log('Waiting for the user to choose in the browser (Ctrl-C aborts)...');
|
||||
}
|
||||
if (!hasFlag('no-open')) {
|
||||
const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
||||
try { spawn(opener, [url], { stdio: 'ignore', detached: true }).unref(); } catch { /* URL printed anyway */ }
|
||||
openSystemBrowser(url);
|
||||
}
|
||||
if (timeoutSec > 0) {
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -12,6 +12,7 @@ Each of these is a check on the built result, not an intention. Run them togethe
|
||||
- **Type:** body measure 65–75ch, display max 6rem, tracking floor -0.04em, balanced headings, obvious scale and weight steps. Run the real copy at every breakpoint and fix what overflows.
|
||||
- **Motion:** one authored moment, not scattered effects and not one identical entrance on every section. Exponential ease-out from an already-visible default. Reach past transform and opacity: blur, backdrop-filter, clip-path, mask, and shadow belong to the palette when they stay smooth.
|
||||
- **States:** hover, disabled, loading, error, empty. Plus real content, working controls, responsive composition, keyboard focus.
|
||||
- **Browser surfaces:** the parts you did not draw still carry the design. Text selection, the caret, custom scrollbars, focus rings, underline offset, and the numerals in tabular data all ship with browser defaults that belong to no design system. Theme them from the palette. This is the cheapest signal that a page was built rather than assembled, and the one models skip most reliably.
|
||||
- **Copy:** the product's own language. Controls name their action; errors name the problem and the recovery.
|
||||
- **Coverage:** every brief requirement present and findable within seconds.
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user