mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-17 08:36:25 +03:00
update
This commit is contained in:
@@ -380,6 +380,8 @@ Look at every asset provided (attached in chat or a file path) and record what i
|
||||
- **Reference / product images**: density, palette, type feel; what the user is drawn to.
|
||||
- **Moodboards**: recurring hues, textures, era, register cues.
|
||||
|
||||
On the questionnaire path, the files themselves also feed the design context document the picker shows after the last question. When the user provided actual files (a logo, a mood board, a reference image), copy each one into `.impeccable/design-interview/assets/`, keeping its filename. Record every staged file for Step 4's cues write: it becomes an object entry in `cues.json` `context.assets`, `{ "file": "<filename>", "kind": "logo" | "moodboard" | "reference", "note": "<one-line observation>" }`, where the note is what this step read off it. An observation with no file behind it stays a plain string entry, as before. On the interview-only path, stage nothing; the observations feed the questions and the seed alone.
|
||||
|
||||
These observations exist to sharpen Step 3. **No assets: skip straight to Step 3** with generic options.
|
||||
|
||||
### Step 3: The interview
|
||||
@@ -456,7 +458,7 @@ This seed writes a minimal frontmatter with `name` and `description` only; no co
|
||||
- `motion-energy-<mode>` keys present, all agreeing: one philosophy sentence for the product, as before.
|
||||
- Keys present and disagreeing: one sentence per surface, named (*"The landing page moves on state change only; the portfolio stages entrances and drives sequences on scroll."*). The bare `motion-energy` is the leading one of the two.
|
||||
- **No `motion-energy` key at all**: the run has neither of those surfaces, so movement was never asked. Say nothing about it, and do not fill the gap from the register; this path's chat interview never asked about motion, so there is nothing to borrow. The next Scan-mode run reads the real transitions out of the code.
|
||||
- **Colors**: the four roles with their picked hex, noting the cue they came from. `color-strategy` becomes the Named Rule. When surfaces differ (`color-strategy-<mode>` keys), state each surface's strategy and which surface leads (the bare key's owner).
|
||||
- **Colors**: the four roles with their picked hex, noting the cue they came from. Name the chosen cue by its slug, and note that the unpicked cue images stay in `.impeccable/visual-cues/` for later art direction. `color-strategy` becomes the Named Rule. When surfaces differ (`color-strategy-<mode>` keys), state each surface's strategy and which surface leads (the bare key's owner).
|
||||
- **Typography**: the real pair by name, the pairing's character, and the type scale as a rule: `type-scale` names it, `type-scale-ratio` is the ratio (e.g. *"Major third: each heading step is 1.25x the last"*). Base size and exact steps stay `[resolved at implementation]`. A `font-heading-source` / `font-body-source` value means a user-provided font file; record where it lives.
|
||||
- **Layout**: `boundary-style` (how sections separate) per surface when the `-<mode>` keys differ, plus `layout-structure` (how pages are composed), which the questionnaire asks of a landing page and a portfolio only. No invented grids beyond what the answers state.
|
||||
- `layout-structure` present: one bare key and no `-<mode>` keys, so state it as a rule for the whole product rather than per surface.
|
||||
|
||||
@@ -485,7 +485,10 @@ In the same write, add a top-level `context` object carrying the chat half of th
|
||||
"voice": [{ "say": "[a concrete line the product would write; 2 to 4 pairs, wording examples, never adjectives]", "not": "[the same message written the way the product refuses to sound]" }],
|
||||
"commitments": ["[one line per commitment from PRODUCT.md Brand Commitments]"]
|
||||
},
|
||||
"assets": ["[asset name: what Step 2 read off it]"],
|
||||
"assets": [
|
||||
"[asset name: what Step 2 read off it; a plain string when no file was provided]",
|
||||
{ "file": "[filename staged in .impeccable/design-interview/assets/]", "kind": "[logo, moodboard, or reference]", "note": "[the one-line Step 2 observation for this file]" }
|
||||
],
|
||||
"color": { "assetLocks": ["[one short color fact an asset fixes, e.g. Primary locked from the logo mark; only when an asset names one]"] },
|
||||
"interview": {
|
||||
"references": [{ "name": "[interview reference, one entry per name]", "takeaway": "[one clause: what this reference lends the design]" }],
|
||||
@@ -496,7 +499,7 @@ In the same write, add a top-level `context` object carrying the chat half of th
|
||||
|
||||
Quote the user's answers, not paraphrases of them; the document labels interview fields as the questions they answered. A missing block renders as a pointer to where that truth lives (PRODUCT.md), so an old `cues.json` without `context` still produces a complete document.
|
||||
|
||||
The optionality is field by field, and the document omits the block of any field that does not arrive, so fill a field only when its PRODUCT.md section or interview answer exists. A legacy PRODUCT.md without Positioning, Platform, Operating Context, or Brand Commitments yields a context without those fields, never an invented value. `product.clarities` carries PRODUCT.md's "What must be clear first" list under a shorter key. `product.conversion` names the single action the product most wants. `product.principles` carries PRODUCT.md's Design Principles, one `{ title, detail }` entry per line. `product.surfaces` maps each mode the run might choose to what that surface is for this product, not the generic tile copy. Only include keys for surfaces that exist in the product; the document reads the map for whichever surfaces the questionnaire chose. `interview.references` and `interview.antiReference` also accept their older shapes, plain strings, which render as the bare pills and single-name callout they always did. Never write `interview.colorStrategy`, `interview.hueAnchor`, `interview.typeDirection`, or `interview.motionEnergy`: the chat interview does not ask those questions on this path, `answers.json` owns color, typography, and motion, and the document already renders its interview-direction blocks only when those keys arrive, so their absence reads as chat silence, not as a gap.
|
||||
The optionality is field by field, and the document omits the block of any field that does not arrive, so fill a field only when its PRODUCT.md section or interview answer exists. A legacy PRODUCT.md without Positioning, Platform, Operating Context, or Brand Commitments yields a context without those fields, never an invented value. `product.clarities` carries PRODUCT.md's "What must be clear first" list under a shorter key. `product.conversion` names the single action the product most wants. `product.principles` carries PRODUCT.md's Design Principles, one `{ title, detail }` entry per line. `product.surfaces` maps each mode the run might choose to what that surface is for this product, not the generic tile copy. Only include keys for surfaces that exist in the product; the document reads the map for whichever surfaces the questionnaire chose. `interview.references` and `interview.antiReference` also accept their older shapes, plain strings, which render as the bare pills and single-name callout they always did. Never write `interview.colorStrategy`, `interview.hueAnchor`, `interview.typeDirection`, or `interview.motionEnergy`: the chat interview does not ask those questions on this path, `answers.json` owns color, typography, and motion, and the document already renders its interview-direction blocks only when those keys arrive, so their absence reads as chat silence, not as a gap. `assets` mixes both shapes in one list: a file the user actually provided is staged under `.impeccable/design-interview/assets/` (seed Step 2 owns the copy) and written as the object form, which the document renders as an image (a `logo` proofed on the committed primary and neutral grounds, a `moodboard` or `reference` in a wide frame, the note under it); a words-only observation stays the plain string it always was.
|
||||
|
||||
Three of the additions are derived at write time rather than asked: `brand.principles` copies the PRODUCT.md principles list (the current Product Principles heading or the legacy Design Principles one), `brand.voice` distills Brand Personality and Brand Commitments into two to four say / not pairs, each half a concrete line of wording the product would or would not publish, never an adjective, and `color.assetLocks` records color facts the provided assets fix (one short line each, written only when Step 2 actually read such a fact off an asset). None of the three adds an interview question, and all three are omitted rather than invented when their source is missing.
|
||||
|
||||
|
||||
@@ -36,10 +36,19 @@ const answersPath = path.join(interviewDir, 'answers.json');
|
||||
const sessionPath = path.join(interviewDir, 'doc-session.json');
|
||||
const ledgerPath = path.join(interviewDir, 'doc-edits.jsonl');
|
||||
const fontsDir = path.join(interviewDir, 'fonts');
|
||||
const brandAssetsDir = path.join(interviewDir, 'assets');
|
||||
const designPath = path.resolve(process.cwd(), 'DESIGN.md');
|
||||
|
||||
const MAX_BODY_BYTES = 1024 * 1024;
|
||||
const FONT_EXTENSIONS = new Set(['.woff2', '.woff', '.ttf', '.otf']);
|
||||
const BRAND_ASSET_MIME = new Map([
|
||||
['.svg', 'image/svg+xml'],
|
||||
['.png', 'image/png'],
|
||||
['.jpg', 'image/jpeg'],
|
||||
['.jpeg', 'image/jpeg'],
|
||||
['.webp', 'image/webp'],
|
||||
['.gif', 'image/gif'],
|
||||
]);
|
||||
const ROLES = new Set(['primary', 'secondary', 'tertiary', 'neutral']);
|
||||
const REQUEST_KINDS = new Set(['font', 'freeform']);
|
||||
/* Long polls are sliced under common proxy/undici header timeouts, the same
|
||||
@@ -251,6 +260,42 @@ async function handleRequest(request, response) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Brand-asset images for the document's Brand article. The picker server
|
||||
serves the same directory while it lives; it exits on submit, and the
|
||||
article's images load after that, so the tab fetches them from here
|
||||
with the session token on the query string, the same rule as the
|
||||
sibling GET routes. Filenames only, extension-gated, one directory. */
|
||||
if (request.method === 'GET' && requestPath.startsWith('/brand-assets/')) {
|
||||
if (url.searchParams.get('token') !== token) throw httpError(403, 'Bad token');
|
||||
let assetName;
|
||||
try {
|
||||
assetName = decodeURIComponent(requestPath.slice('/brand-assets/'.length));
|
||||
} catch {
|
||||
throw httpError(400, 'Invalid path');
|
||||
}
|
||||
const extension = path.extname(assetName).toLowerCase();
|
||||
const filePath = path.resolve(brandAssetsDir, assetName);
|
||||
if (!assetName || assetName !== path.basename(assetName)
|
||||
|| !BRAND_ASSET_MIME.has(extension)
|
||||
|| path.relative(brandAssetsDir, filePath).startsWith('..')) {
|
||||
throw httpError(404, 'Not found');
|
||||
}
|
||||
let body;
|
||||
try {
|
||||
body = await readFile(filePath);
|
||||
} catch {
|
||||
throw httpError(404, 'Not found');
|
||||
}
|
||||
response.writeHead(200, {
|
||||
'Content-Type': BRAND_ASSET_MIME.get(extension),
|
||||
'Content-Length': body.length,
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Cache-Control': 'max-age=86400',
|
||||
});
|
||||
response.end(body);
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.method === 'GET' && requestPath === '/doc/state') {
|
||||
if (url.searchParams.get('token') !== token) throw httpError(403, 'Bad token');
|
||||
lastBrowserSeen = Date.now();
|
||||
|
||||
@@ -17,13 +17,18 @@ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const pickerDir = path.join(scriptDir, 'picker');
|
||||
const answersPath = path.resolve(process.cwd(), '.impeccable/design-interview/answers.json');
|
||||
const fontsDir = path.resolve(process.cwd(), '.impeccable/design-interview/fonts');
|
||||
const brandAssetsDir = path.resolve(process.cwd(), '.impeccable/design-interview/assets');
|
||||
const MAX_BODY_BYTES = 1024 * 1024;
|
||||
const FONT_EXTENSIONS = new Set(['.woff2', '.woff', '.ttf', '.otf']);
|
||||
const BRAND_ASSET_EXTENSIONS = ['.svg', '.png', '.jpg', '.jpeg', '.webp', '.gif'];
|
||||
const MIME = new Map([
|
||||
['.html', 'text/html; charset=utf-8'],
|
||||
['.css', 'text/css; charset=utf-8'],
|
||||
['.js', 'text/javascript; charset=utf-8'],
|
||||
['.jpg', 'image/jpeg'],
|
||||
['.jpeg', 'image/jpeg'],
|
||||
['.webp', 'image/webp'],
|
||||
['.gif', 'image/gif'],
|
||||
['.png', 'image/png'],
|
||||
['.svg', 'image/svg+xml'],
|
||||
['.json', 'application/json; charset=utf-8'],
|
||||
@@ -267,9 +272,28 @@ async function handleRequest(request, response) {
|
||||
sendJson(response, 404, { error: 'Not found' });
|
||||
return;
|
||||
}
|
||||
// Cue images are re-requested by the design context document after this
|
||||
// process has exited (article content only enters the live DOM after
|
||||
// submit), so they must be servable from the browser's cache.
|
||||
response.setHeader('Cache-Control', 'max-age=86400');
|
||||
await serveFile(response, options.cuesDir, cueName, ['.png']);
|
||||
return;
|
||||
}
|
||||
/* Brand-asset files the agent staged from the chat interview (logos, mood
|
||||
boards, reference images), displayed by the design context document.
|
||||
Read-only, one directory, filenames only. The /assets/ prefix is taken
|
||||
by the picker's own static files, hence the distinct name. */
|
||||
if (requestPath.startsWith('/brand-assets/')) {
|
||||
const assetName = requestPath.slice('/brand-assets/'.length);
|
||||
if (!assetName || assetName.includes('/')) {
|
||||
sendJson(response, 404, { error: 'Not found' });
|
||||
return;
|
||||
}
|
||||
response.setHeader('Cache-Control', 'max-age=86400');
|
||||
await serveFile(response, brandAssetsDir, assetName, BRAND_ASSET_EXTENSIONS);
|
||||
return;
|
||||
}
|
||||
|
||||
// Uploaded faces are read back so the specimen can render in them.
|
||||
if (requestPath.startsWith('/fonts/')) {
|
||||
const fontName = requestPath.slice('/fonts/'.length);
|
||||
|
||||
Reference in New Issue
Block a user