qualia-framework 4.4.0 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/state.js CHANGED
@@ -1267,6 +1267,186 @@ function cmdBackfillLifetime(opts) {
1267
1267
  });
1268
1268
  }
1269
1269
 
1270
+ // ─── Backfill Milestones from JOURNEY.md ─────────────────
1271
+ // Reconstructs the milestones[] array + lifetime counters from the
1272
+ // "Milestone arc" table in .planning/JOURNEY.md. Required when a project
1273
+ // pre-dates v4 milestone bookkeeping (or had its tracking.json reset)
1274
+ // but JOURNEY.md captures the historical arc. Idempotent — only adds
1275
+ // missing milestones or overwrites entries flagged backfilled:true.
1276
+ //
1277
+ // JOURNEY.md table format expected:
1278
+ // | # | Milestone | Status | Phases | Closed |
1279
+ // | 1 | Name | CLOSED | 1–13 | YYYY-MM-DD |
1280
+ // | 5 | Name | OPEN | rolling| — |
1281
+ //
1282
+ // Phase counting handles ranges (`1–13` → 13), comma lists (`14, 15, 16.1–16.6` → 8),
1283
+ // and "rolling" / "—" / "-" → 0.
1284
+ function cmdBackfillMilestones(opts) {
1285
+ const t = readTracking();
1286
+ if (!t) return output(fail("NO_PROJECT", "No .planning/ found."));
1287
+ ensureLifetime(t);
1288
+
1289
+ const journeyPath = path.join(PLANNING, "JOURNEY.md");
1290
+ if (!fs.existsSync(journeyPath)) {
1291
+ return output(fail("NO_JOURNEY", "JOURNEY.md not found. Cannot backfill without milestone history source."));
1292
+ }
1293
+
1294
+ const content = fs.readFileSync(journeyPath, "utf8");
1295
+
1296
+ // Parse the milestone arc table. Each row must have 5 columns:
1297
+ // num | name | status | phases | closed
1298
+ const rows = [];
1299
+ const lines = content.split(/\r?\n/);
1300
+ for (const line of lines) {
1301
+ const m = line.match(
1302
+ /^\|\s*(\d+)\s*\|\s*([^|]+?)\s*\|\s*(CLOSED|OPEN|CURRENT)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|/i
1303
+ );
1304
+ if (m) {
1305
+ rows.push({
1306
+ num: parseInt(m[1], 10),
1307
+ name: m[2].trim(),
1308
+ status: m[3].trim().toUpperCase(),
1309
+ phasesStr: m[4].trim(),
1310
+ closedStr: m[5].trim(),
1311
+ });
1312
+ }
1313
+ }
1314
+
1315
+ if (rows.length === 0) {
1316
+ return output(
1317
+ fail(
1318
+ "NO_MILESTONE_TABLE",
1319
+ "No milestone arc table found in JOURNEY.md. Expected `| # | Milestone | Status | Phases | Closed |` header followed by rows."
1320
+ )
1321
+ );
1322
+ }
1323
+
1324
+ // Count phases from a phasesStr.
1325
+ // "1–13" → 13. "14, 15, 16.1–16.6" → 8. "19–25" → 7. "rolling" / "—" / "-" → 0.
1326
+ const countPhases = (s) => {
1327
+ if (!s) return 0;
1328
+ const lower = s.toLowerCase();
1329
+ if (lower === "—" || lower === "-" || lower === "rolling" || lower === "n/a") return 0;
1330
+ let count = 0;
1331
+ for (const seg of s.split(",")) {
1332
+ const trimmed = seg.trim();
1333
+ if (!trimmed || trimmed === "—" || trimmed === "-") continue;
1334
+ // Match X–Y or X-Y where X/Y can be "13" or "16.6"
1335
+ const range = trimmed.match(/^(\d+(?:\.\d+)?)\s*[–-]\s*(\d+(?:\.\d+)?)$/);
1336
+ if (range) {
1337
+ const startStr = range[1];
1338
+ const endStr = range[2];
1339
+ // Sub-phase range like "16.1–16.6" → count by sub-index difference + 1
1340
+ if (startStr.includes(".") && endStr.includes(".")) {
1341
+ const startSub = parseInt(startStr.split(".")[1], 10);
1342
+ const endSub = parseInt(endStr.split(".")[1], 10);
1343
+ count += Math.max(0, endSub - startSub + 1);
1344
+ } else {
1345
+ const start = parseInt(startStr, 10);
1346
+ const end = parseInt(endStr, 10);
1347
+ count += Math.max(0, end - start + 1);
1348
+ }
1349
+ } else {
1350
+ // Single phase like "14" or "17.1"
1351
+ count += 1;
1352
+ }
1353
+ }
1354
+ return count;
1355
+ };
1356
+
1357
+ const closed = rows.filter((r) => r.status === "CLOSED").sort((a, b) => a.num - b.num);
1358
+ const openRow = rows.find((r) => r.status === "OPEN" || r.status === "CURRENT");
1359
+
1360
+ t.milestones = Array.isArray(t.milestones) ? t.milestones : [];
1361
+ let added = 0;
1362
+ let updated = 0;
1363
+ let totalClosedPhases = 0;
1364
+ const closedSummaries = [];
1365
+
1366
+ for (const row of closed) {
1367
+ const phaseCount = countPhases(row.phasesStr);
1368
+ totalClosedPhases += phaseCount;
1369
+
1370
+ const dateMatch = row.closedStr.match(/\d{4}-\d{2}-\d{2}/);
1371
+ const closedAt = dateMatch ? `${dateMatch[0]}T00:00:00.000Z` : "";
1372
+
1373
+ const summary = {
1374
+ num: row.num,
1375
+ name: row.name,
1376
+ total_phases: phaseCount,
1377
+ phases_completed: phaseCount,
1378
+ tasks_completed: 0, // unknown for historical backfill
1379
+ shipped_url: "",
1380
+ closed_at: closedAt,
1381
+ backfilled: true,
1382
+ };
1383
+ closedSummaries.push({ num: row.num, name: row.name, phases: phaseCount });
1384
+
1385
+ const existing = t.milestones.findIndex((mm) => mm && mm.num === row.num);
1386
+ if (existing >= 0) {
1387
+ // Don't override entries that came from real /qualia-milestone close
1388
+ // (they have richer data). Only overwrite previously-backfilled entries.
1389
+ if (t.milestones[existing].backfilled) {
1390
+ t.milestones[existing] = summary;
1391
+ updated++;
1392
+ }
1393
+ } else {
1394
+ t.milestones.push(summary);
1395
+ added++;
1396
+ }
1397
+ }
1398
+
1399
+ // Stable order by milestone number.
1400
+ t.milestones.sort((a, b) => (a.num || 0) - (b.num || 0));
1401
+
1402
+ const lastClosed = closed.length > 0 ? closed[closed.length - 1].num : 0;
1403
+
1404
+ // Math.max — never reduce lifetime counters. Preserves real /qualia-milestone
1405
+ // history if a project was partly closed properly and partly backfilled.
1406
+ t.lifetime.milestones_completed = Math.max(
1407
+ t.lifetime.milestones_completed || 0,
1408
+ closed.length
1409
+ );
1410
+ t.lifetime.last_closed_milestone = Math.max(
1411
+ t.lifetime.last_closed_milestone || 0,
1412
+ lastClosed
1413
+ );
1414
+ t.lifetime.total_phases = Math.max(
1415
+ t.lifetime.total_phases || 0,
1416
+ totalClosedPhases
1417
+ );
1418
+
1419
+ // Set current milestone — prefer the OPEN row; otherwise next-after-last-closed.
1420
+ if (openRow) {
1421
+ t.milestone = openRow.num;
1422
+ t.milestone_name = openRow.name;
1423
+ } else if (lastClosed > 0) {
1424
+ t.milestone = lastClosed + 1;
1425
+ t.milestone_name = readNextMilestoneNameFromJourney(t.milestone);
1426
+ }
1427
+
1428
+ t.last_updated = new Date().toISOString();
1429
+ writeTracking(t);
1430
+
1431
+ _trace("backfill-milestones", "allow", {
1432
+ added,
1433
+ updated,
1434
+ closed_count: closed.length,
1435
+ total_phases: totalClosedPhases,
1436
+ lifetime: t.lifetime,
1437
+ });
1438
+
1439
+ output({
1440
+ ok: true,
1441
+ action: "backfill-milestones",
1442
+ added,
1443
+ updated,
1444
+ closed: closedSummaries,
1445
+ open_milestone: openRow ? { num: openRow.num, name: openRow.name } : null,
1446
+ lifetime: t.lifetime,
1447
+ });
1448
+ }
1449
+
1270
1450
  // ─── Next Report ID ──────────────────────────────────────
1271
1451
  // Increments report_seq and returns the next QS-REPORT-NN id. Per-project
1272
1452
  // counter (lives in tracking.json). /qualia-report calls this to tag each
@@ -1342,6 +1522,9 @@ try {
1342
1522
  case "backfill-lifetime":
1343
1523
  cmdBackfillLifetime(opts);
1344
1524
  break;
1525
+ case "backfill-milestones":
1526
+ cmdBackfillMilestones(opts);
1527
+ break;
1345
1528
  case "next-report-id":
1346
1529
  cmdNextReportId(opts);
1347
1530
  break;
@@ -1349,7 +1532,7 @@ try {
1349
1532
  output(
1350
1533
  fail(
1351
1534
  "UNKNOWN_COMMAND",
1352
- `Usage: state.js <check|transition|init|fix|validate-plan|close-milestone|backfill-lifetime|next-report-id> [--options]`
1535
+ `Usage: state.js <check|transition|init|fix|validate-plan|close-milestone|backfill-lifetime|backfill-milestones|next-report-id> [--options]`
1353
1536
  )
1354
1537
  );
1355
1538
  }
@@ -200,6 +200,11 @@ Authorization: Bearer <api-key>
200
200
  external callers. Internal idempotent UPSERT on `(project_id,
201
201
  client_report_id)` retries is the one exception (see "Idempotent UPSERT
202
202
  on retry" above).
203
+ - The ERP resolves each report to a canonical internal project when possible.
204
+ Repository URL is the strongest signal, followed by repo/project slug, then
205
+ configured aliases, then the human report project name. This keeps legacy
206
+ repo/report names like `USD-Academy` or `USD-ACVADEMY` correctly linked to
207
+ ERP project names like `Underdog-Sales-Academy`.
203
208
  - **`dry_run` retention (v4.0.4+):** The ERP deletes rows where
204
209
  `dry_run = true AND submitted_at < now() - 7 days` via a daily cron at
205
210
  03:00 UTC. Production report views (list, project tree, email digests)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qualia-framework",
3
- "version": "4.4.0",
3
+ "version": "4.5.0",
4
4
  "description": "Claude Code workflow framework by Qualia Solutions. Plan, build, verify, ship.",
5
5
  "bin": {
6
6
  "qualia-framework": "./bin/cli.js"
@@ -0,0 +1,110 @@
1
+ # Design — Brand register
2
+
3
+ **When this register applies:** marketing pages, landing pages, campaign sites, portfolios, brand microsites, anything where the design IS the product.
4
+
5
+ **The bar:** distinctiveness. Memorable beats safe. If a visitor can't remember what your site looked like an hour later, you failed.
6
+
7
+ This register inherits all of `design-laws.md`. The rules below add what changes when distinctiveness, not familiarity, is the goal.
8
+
9
+ ## The brand brief (what the agent commits to first)
10
+
11
+ Before any code, the agent writes a 4-line brief and waits for user confirmation:
12
+
13
+ ```
14
+ Aesthetic direction: [editorial · brutalist · luxury · maximalist · retro-futuristic · organic · terminal-native · sci-fi · pastoral · industrial · ...]
15
+ Color strategy: [Restrained · Committed · Full palette · Drenched]
16
+ Scene sentence: [single concrete sentence — who, where, ambient light, mood]
17
+ Differentiation: [what someone will remember 24 hours later]
18
+ ```
19
+
20
+ This is mandatory. Skipping it produces generic output that ignores the brand.
21
+
22
+ ## Reflex-reject aesthetic lanes
23
+
24
+ The currently-saturated AI-design families. Reject by default unless the user explicitly asks for one and you confirmed they understand it's a saturated lane.
25
+
26
+ | Lane | Tells | Reject because |
27
+ |---|---|---|
28
+ | **Generic SaaS-cream** | Off-white bg, soft shadows, rounded cards, slate text, one-color accent | Notion clone aesthetic. Every AI tool builds this first |
29
+ | **Vercel-terminal-native** | Dark + monospace + green/blue accents + grain | The 2024-2025 v0 aesthetic. Saturated |
30
+ | **Crypto-neon** | Pure black + electric accent + glow + grid | Web3 cliché |
31
+ | **Healthcare-clinical** | White + teal + rounded sans + soft icons | Every health app |
32
+ | **Fintech-navy-gold** | Navy + gold accent + serif headings | Every bank, every wealth tool |
33
+ | **Sleek-corporate** | Inter + blue + gradient mesh + isometric illustrations | Every B2B SaaS landing 2020-2024 |
34
+
35
+ If the project lands in one of these by default, the slop test failed at second-order. Rework.
36
+
37
+ ## Brand register — additional laws
38
+
39
+ ### Typography
40
+
41
+ - Pick a **distinctive display face**. Editorial serifs (Fraunces, Reckless, Ogg, Editorial New, Mazius). Geometric grotesques (Migra, Söhne, Gambarino). Variable display fonts (Authentic Sans, Departure Mono).
42
+ - Pair the display with a **refined body**. JetBrains Mono, Geist, Inter Display (NOT Inter), Söhne, Untitled Sans, Mona Sans, Switzer.
43
+ - **Banned display fonts:** Inter, Roboto, Arial, system-ui, Space Grotesk, Montserrat, Poppins, Lato, Open Sans. These are the AI defaults — and the worst signal of "designer didn't choose this."
44
+ - **Variable fonts encouraged.** A single variable font with axis-driven weight/optical-size is more interesting than three static cuts.
45
+
46
+ ### Color
47
+
48
+ - Brand register defaults to **Committed** color strategy. One saturated color carries identity.
49
+ - **Drenched** is a strong choice for hero sections. Don't be afraid.
50
+ - Accent should be **sharp** — not a desaturated pastel-of-the-brand. The accent is the loudest element on the page.
51
+
52
+ ### Layout
53
+
54
+ - **Asymmetry > symmetry.** Real design has tension. Centered-everything is the AI default.
55
+ - **Diagonal flow, overlap, grid-breaking elements** when they serve the design.
56
+ - **Scale contrast.** Hero text at clamp(4rem, 9vw, 8rem) next to body at 1rem creates drama. Hero at 2.5rem next to body at 1rem is timid.
57
+ - **Negative space is content.** Generous whitespace > dense layouts for marketing.
58
+
59
+ ### Motion
60
+
61
+ - One **signature motion** that gives the site personality:
62
+ - Parallax scroll-triggered reveal
63
+ - Magnetic buttons
64
+ - Cursor-following element
65
+ - Letter-by-letter text reveal on hero
66
+ - Scroll-driven scrubbed scene
67
+ - Morphing shapes between sections
68
+ - Subtle elsewhere. Restraint everywhere except the signature.
69
+ - Use the Web Animations API or Framer Motion for orchestrated sequences. CSS keyframes for one-offs.
70
+
71
+ ### Backgrounds and visual details
72
+
73
+ This is where Brand earns its register. Allowed and encouraged:
74
+
75
+ - Gradient meshes (CSS or SVG)
76
+ - Noise textures (subtle, 2-5% opacity)
77
+ - Geometric patterns (SVG, not images)
78
+ - Layered transparencies
79
+ - Dramatic shadows (with brand-tinted color, not gray)
80
+ - Decorative borders (sparingly)
81
+ - Custom cursors (page-specific, not site-wide)
82
+ - Grain overlays on images
83
+ - Scroll-progress indicators (interesting ones, not generic top bars)
84
+
85
+ Don't use all of these on one page. Pick 1-2 that serve the brief.
86
+
87
+ ## Brand-specific anti-patterns
88
+
89
+ In addition to `design-laws.md` absolute bans:
90
+
91
+ - **Hero pattern: text-center + gradient bg + 2 CTAs + screenshot below.** The single most overused brand pattern of 2020-2025.
92
+ - **Three-column feature grid in section 2.** The default AI brand layout.
93
+ - **"Trusted by" logo strip in faded gray.** Lazy social proof. If the logos matter, design with them. If they don't, omit.
94
+ - **Parallax scroll on every section.** Heavy-handed. Use it for the signature, nowhere else.
95
+ - **Animated dots / pulsing orbs / blinking status indicators.** Decorative noise.
96
+ - **Generic "Get Started" / "Learn More" CTA pair.** Always specify the action.
97
+ - **Stock photography of diverse smiling people in offices.** Use illustration, abstract imagery, product screenshots, or no imagery at all.
98
+
99
+ ## Brand register success criteria
100
+
101
+ If you can answer YES to all of these, the brand register passed:
102
+
103
+ - [ ] A designer would call this **distinctive** — not "polished" but **specific**
104
+ - [ ] The aesthetic direction is identifiable in 3 seconds
105
+ - [ ] The color strategy is one of the four (and reads that way)
106
+ - [ ] The scene sentence and the page agree
107
+ - [ ] One signature motion, executed well
108
+ - [ ] No reflex-reject lane (first or second order)
109
+ - [ ] All anti-references avoided
110
+ - [ ] Implementation matches vision (effort proportional to ambition)
@@ -0,0 +1,144 @@
1
+ # Design Laws
2
+
3
+ The non-negotiable rules every Qualia frontend honors. Both registers (Brand and Product) inherit from this file. Skip nothing.
4
+
5
+ These laws exist because AI-generated UI has identifiable defaults — Inter font, purple gradients, identical card grids, gray-on-gray, modal-as-first-thought — and shipping the defaults is shipping AI slop. Each rule below has a specific failure mode it prevents.
6
+
7
+ ## 1. Color: OKLCH only
8
+
9
+ Use the OKLCH color space for every color in the codebase. RGB / hex / HSL are translated values, not source values.
10
+
11
+ ```css
12
+ /* yes */
13
+ --bg: oklch(0.16 0.012 220);
14
+ --accent: oklch(0.78 0.14 196);
15
+
16
+ /* no */
17
+ --bg: #1a1d22;
18
+ --accent: #00ced1;
19
+ ```
20
+
21
+ **Why:** OKLCH is perceptually uniform. Equal lightness numbers look equally light to the eye, regardless of hue. Hex doesn't give you that — `#777777` and `#7777ff` are nominally equal lightness but the blue reads darker.
22
+
23
+ **Reduce chroma at the extremes.** As lightness approaches 0 or 100, high chroma reads garish. Cap chroma at 0.04 below L=0.20 and above L=0.92.
24
+
25
+ **Never use `#000` or `#fff`.** Tint every neutral toward the brand hue with chroma 0.005–0.01. Pure black/white reads as untuned and generic — the dead giveaway of "designer didn't pick this color, the framework did."
26
+
27
+ ```css
28
+ /* tinted neutrals */
29
+ --text: oklch(0.94 0.006 220); /* 220 is the brand hue */
30
+ --bg: oklch(0.16 0.012 220);
31
+ --muted: oklch(0.62 0.010 220);
32
+ ```
33
+
34
+ ## 2. Color strategy: pick one, commit to it
35
+
36
+ Before picking colors, pick a strategy. Four steps on a commitment axis:
37
+
38
+ | Strategy | Rule | When |
39
+ |---|---|---|
40
+ | **Restrained** | Tinted neutrals + one accent at ≤10% surface coverage | Product default; brand minimalism |
41
+ | **Committed** | One saturated color carries 30–60% of the surface | Brand default for identity-driven pages |
42
+ | **Full palette** | 3–4 named roles, each used deliberately | Brand campaigns; product data viz |
43
+ | **Drenched** | The surface IS the color | Brand heroes; campaign pages |
44
+
45
+ The "≤10% accent" rule is **Restrained only**. Committed / Full palette / Drenched exceed it on purpose. Don't collapse every design to Restrained by reflex.
46
+
47
+ ## 3. Theme: write a scene sentence
48
+
49
+ Dark vs light is never a default. Not dark "because tools look cool dark." Not light "to be safe." Before choosing, write **one sentence of physical scene**: who uses this, where, under what ambient light, in what mood.
50
+
51
+ | Vague (no answer) | Concrete (forces an answer) |
52
+ |---|---|
53
+ | "Observability dashboard" | "SRE glancing at incident severity on a 27-inch monitor at 2am in a dim room" |
54
+ | "Customer support tool" | "Agent at a coffee-lit desk juggling 4 chats while a coworker asks a question" |
55
+ | "Marketing site" | "Founder showing the page to an investor on an iPad in a sunlit lobby" |
56
+
57
+ Run the sentence, not the category. If the sentence doesn't force an answer, add detail until it does.
58
+
59
+ ## 4. Typography
60
+
61
+ - Body line length capped at 65–75ch
62
+ - Hierarchy through scale + weight contrast (≥1.25 ratio between adjacent steps)
63
+ - Avoid flat scales — if h1 is 32px and h2 is 28px, the hierarchy is invisible
64
+ - Letter-spacing as a semantic signal:
65
+ - Display headlines: tight (-0.02em to -0.04em)
66
+ - Body: neutral (0)
67
+ - Labels and badges: open (+0.04em to +0.08em)
68
+ - Category tags: wide (+0.08em to +0.14em)
69
+
70
+ ## 5. Layout
71
+
72
+ - **Vary spacing for rhythm.** Same padding everywhere is monotony. Tight within groups, generous between sections.
73
+ - **Cards are the lazy answer.** Use them only when they're truly the best affordance. Nested cards are always wrong.
74
+ - **Container depth max 2.** Card → content. Not card → panel → pill → content.
75
+ - **Don't wrap everything in a container.** Most things don't need one.
76
+ - **Full-width with fluid padding.** No hardcoded `max-width: 1200px` or `max-w-7xl`. Use `clamp(1rem, 5vw, 4rem)` for horizontal padding.
77
+
78
+ ## 6. Motion
79
+
80
+ - Don't animate CSS layout properties (`width`, `height`, `top`, `left`, `margin`, `padding`). Animate `transform` and `opacity`.
81
+ - Ease out with exponential curves: `cubic-bezier(0.22, 1, 0.36, 1)` (out-quart) or `cubic-bezier(0.16, 1, 0.3, 1)` (out-expo). No bounce. No elastic.
82
+ - One signature motion per page > scattered micro-interactions
83
+ - Always respect `prefers-reduced-motion: reduce`
84
+
85
+ ## 7. Copy
86
+
87
+ - Every word earns its place
88
+ - No restated headings
89
+ - No intros that repeat the title
90
+ - **No em dashes.** Use commas, colons, semicolons, periods, or parentheses. Also not `--`.
91
+ - CTAs name the action. "Download invoice" not "Get Started". "Continue setup" not "Learn More".
92
+
93
+ ## 8. Absolute bans (match-and-refuse)
94
+
95
+ If you're about to write any of these, rewrite the element with different structure. No exceptions.
96
+
97
+ | Banned pattern | Why | Rewrite with |
98
+ |---|---|---|
99
+ | **Side-stripe borders** — `border-left: 4px` colored as accent on cards / list items / callouts | Decorative noise, never semantic | Full borders, background tints, leading numbers/icons, or nothing |
100
+ | **Gradient text** — `background-clip: text` with a gradient | Decorative, never meaningful | Single solid color. Emphasis via weight or size |
101
+ | **Glassmorphism by default** — blur + glass cards used everywhere | Trendy slop from 2023-2024 | Rare and purposeful, or nothing |
102
+ | **Hero-metric template** — big number, small label, three supporting stats, gradient accent | SaaS cliché | Vary layout per metric. Don't grid them |
103
+ | **Identical card grids** — same-sized cards with icon + heading + text repeated 3+ times | The default AI hero pattern | Vary card sizes, layouts, content shapes |
104
+ | **Modal as first thought** — every action goes through a modal | Lazy interaction design | Inline expansion, progressive disclosure, dedicated route |
105
+
106
+ ## 9. The AI slop test (run at two altitudes)
107
+
108
+ If someone could look at this interface and say "AI made that" without doubt, it failed.
109
+
110
+ **First-order check.** If someone could guess the theme + palette from the category alone, the first reflex won:
111
+ - Observability → dark blue
112
+ - Healthcare → white + teal
113
+ - Finance → navy + gold
114
+ - Crypto → neon on black
115
+
116
+ If yes, rework the scene sentence and color strategy until the answer isn't obvious from the domain.
117
+
118
+ **Second-order check.** If someone could guess the aesthetic family from category-plus-anti-references, the trap one tier deeper won:
119
+ - AI workflow tool that's not SaaS-cream → editorial-typographic
120
+ - Fintech that's not navy-and-gold → terminal-native dark mode
121
+ - Health app that's not white-and-teal → soft-pastel
122
+
123
+ The first reflex was avoided; the second wasn't. Rework until both answers are not obvious. The Brand register's reflex-reject aesthetic lanes catch the currently-saturated families.
124
+
125
+ ## 10. Single-icon-family rule
126
+
127
+ Pick one icon family per project and enforce it. Lucide OR Heroicons OR Phosphor OR Radix Icons. Never mixed. Mixing icon families is the visual equivalent of mixing fonts.
128
+
129
+ ## 11. Anti-references are mandatory
130
+
131
+ Every PRODUCT.md must list 3–5 sites the project should NOT look like. Anti-references are more useful than positive references because they pin down what the design is reacting against.
132
+
133
+ ```
134
+ Anti-references:
135
+ - generic SaaS-cream (Notion clones)
136
+ - terminal-nostalgia (vercel/v0 dark)
137
+ - corporate-navy fintech (most banks)
138
+ ```
139
+
140
+ ## 12. The implementation must match the vision
141
+
142
+ Maximalist designs need elaborate code. Minimalist designs need restraint, precision, and careful attention to spacing and typography. The failure mode is mismatch: elaborate animation on a "clean minimal" claim, or flat execution of an ambitious concept.
143
+
144
+ If you wrote 200 lines of CSS to render a "minimal" landing page, the page is not minimal. If your "bold and maximalist" page uses 4 colors and `text-center`, it's not bold.
@@ -0,0 +1,110 @@
1
+ # Design — Product register
2
+
3
+ **When this register applies:** app UI, admin consoles, dashboards, internal tools, settings pages, anything where the design SERVES the product.
4
+
5
+ **The bar:** earned familiarity. Fluent users of Linear, Figma, Notion, Stripe, Raycast should trust the interface on first glance. Distinctiveness is allowed but never at the cost of usability.
6
+
7
+ This register inherits all of `design-laws.md`. The rules below add what changes when familiarity, not distinctiveness, is the goal.
8
+
9
+ ## The product brief (what the agent commits to first)
10
+
11
+ Before any code, the agent writes a 4-line brief and waits for user confirmation:
12
+
13
+ ```
14
+ Reference apps: [Linear · Stripe · Notion · Figma · Raycast · Vercel · Plaid Dashboard · Pylon · ...]
15
+ Color strategy: [Restrained — almost always]
16
+ Scene sentence: [physical scene — focus on context of use, not aesthetic mood]
17
+ Density: [comfortable · standard · compact · ultra-compact]
18
+ ```
19
+
20
+ Reference apps anchor the visual language. Pick 2-3 that share the user mental model and the density.
21
+
22
+ ## Density target
23
+
24
+ Product UIs have to choose density. State it explicitly.
25
+
26
+ | Density | Examples | Use when |
27
+ |---|---|---|
28
+ | **Comfortable** | Notion, Linear (default), Mailchimp | Knowledge work, low-frequency |
29
+ | **Standard** | Stripe Dashboard, Vercel | Mainstream SaaS |
30
+ | **Compact** | Linear (compact mode), Figma left rail | Power users, workflow tools |
31
+ | **Ultra-compact** | Bloomberg Terminal, Trader UIs | Information-dense, professional users |
32
+
33
+ Higher density = smaller font sizes (down to 12px), tighter padding, more rows per viewport. Don't pretend the app is comfortable when it's compact, or vice versa. Match the user's actual workflow.
34
+
35
+ ## Product register — additional laws
36
+
37
+ ### Typography
38
+
39
+ - **One typeface family is fine** for product. Variable fonts shine here (Inter Display, Geist, Switzer, Mona Sans, Söhne, Untitled Sans).
40
+ - Banned (per laws): Inter (regular cut), Roboto, Arial, system-ui, Space Grotesk
41
+ - Body: 13-15px depending on density (comfortable=15, standard=14, compact=13, ultra-compact=12)
42
+ - Hierarchy through weight (400 body, 500 labels, 600 headings) more than size
43
+ - Tabular numerals for any numeric data: `font-feature-settings: "tnum" 1`
44
+ - Truncate long strings with ellipsis + tooltip — never let layout collapse on a long username
45
+
46
+ ### Color
47
+
48
+ - **Restrained is the default.** Tinted neutrals + one accent.
49
+ - **Semantic colors required:** success (green) / warning (amber) / error (red) / info (blue). Always paired with non-color signal (icon, label, pattern).
50
+ - Status indicators must work for color-blind users. Run the design through a deuteranopia simulator.
51
+ - Background depth: 3 surface levels max (`bg`, `surface`, `elevated`). Subtle differences (oklch L delta of 0.03-0.05).
52
+
53
+ ### Layout
54
+
55
+ - **Density first, aesthetics second.** A beautiful UI a user can't navigate quickly is a failed product UI.
56
+ - **Predictable layout.** Sidebar left, main center, optional right rail. Don't reinvent navigation patterns.
57
+ - **Sticky headers** for long-scroll views (tables, lists)
58
+ - **Keyboard-first.** Every action accessible via keyboard. Power users will memorize shortcuts; expose them.
59
+ - **Fitts's Law.** Frequently-used controls go to screen edges. Action buttons in tight clusters.
60
+
61
+ ### Components
62
+
63
+ | Pattern | Product register rule |
64
+ |---|---|
65
+ | **Buttons** | 3 variants max: primary / secondary / ghost. Destructive variant where needed. No "outline + filled + ghost + soft + subtle" proliferation. |
66
+ | **Inputs** | Always have visible labels (not placeholder-only). Validation inline, on blur. Error messages specific and actionable. |
67
+ | **Tables** | Tabular numerals. Right-align numbers. Sort indicators on hover, persistent on active. Sticky headers. |
68
+ | **Empty states** | Always include: icon, single sentence of context, primary action |
69
+ | **Loading** | Skeleton (not spinner) for known-shape content. Spinner only for unknown duration. |
70
+ | **Empty data** | Never "No results." Always "No invoices yet — try [action]." |
71
+ | **Toasts** | Auto-dismiss at 5s minimum. Always dismissible. Errors don't auto-dismiss. |
72
+ | **Modals** | Last resort (per laws). When unavoidable: trap focus, ESC to close, restore focus on close. |
73
+
74
+ ### Motion
75
+
76
+ - Restrained. Product UI motion exists to communicate state changes, not to delight.
77
+ - Hover transitions: 100-150ms ease-out
78
+ - Panel slides: 200ms ease-out
79
+ - Page transitions: 0ms (yes, zero) for navigation between similar views. Motion when it would disorient should be skipped.
80
+ - Skeleton shimmer: 1500ms loop, subtle
81
+ - No bounce. No elastic. No overshoot.
82
+
83
+ ### States (every interactive element)
84
+
85
+ - Default
86
+ - Hover (within 100ms)
87
+ - Focus (visible ring, 2px+ offset, contrasting color)
88
+ - Active/pressed (subtle scale or color shift)
89
+ - Disabled (opacity 0.5, `cursor: not-allowed`, `aria-disabled="true"`)
90
+ - Loading (per-element spinner or skeleton)
91
+ - Error (inline message + `aria-describedby`)
92
+
93
+ Every state on every interactive element. No exceptions.
94
+
95
+ ## Product register success criteria
96
+
97
+ If you can answer YES to all of these, the product register passed:
98
+
99
+ - [ ] A user fluent in the reference apps would feel oriented in 5 seconds
100
+ - [ ] Density is consistent across the app (one chosen value, applied)
101
+ - [ ] Keyboard navigation reaches every action
102
+ - [ ] Empty / loading / error states present on every async surface
103
+ - [ ] Color is never the sole information signal
104
+ - [ ] Tabular numerals on numeric columns
105
+ - [ ] No reinvented navigation pattern
106
+ - [ ] Implementation matches vision (precision and restraint, not flourish)
107
+
108
+ ## When in doubt
109
+
110
+ Ask: "Would Linear ship this?" If the answer is no, rework. Linear is not the only standard, but it is the highest commonly-cited bar for product design in 2025-2026 — fluency, restraint, density, motion all dialed in. Most product work will land within ±10% of Linear's bar; be honest about which side.