@monoes/monodesign 1.1.0 → 1.2.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.
Files changed (83) hide show
  1. package/NOTICE.md +7 -19
  2. package/cli/bin/cli.js +5 -1
  3. package/cli/bin/commands/ignores.mjs +0 -1
  4. package/cli/engine/browser/injected/index.mjs +25 -1
  5. package/cli/engine/cli/main.mjs +0 -1
  6. package/cli/engine/design-system.mjs +0 -1
  7. package/cli/engine/detect-antipatterns-browser.js +356 -1
  8. package/cli/engine/detect-antipatterns.mjs +0 -1
  9. package/cli/engine/engines/browser/detect-url.mjs +0 -1
  10. package/cli/engine/engines/browser/drivers.mjs +0 -1
  11. package/cli/engine/engines/regex/detect-text.mjs +63 -1
  12. package/cli/engine/engines/static-html/css-cascade.mjs +0 -1
  13. package/cli/engine/engines/static-html/detect-html.mjs +19 -1
  14. package/cli/engine/engines/visual/screenshot-contrast.mjs +0 -1
  15. package/cli/engine/findings.mjs +0 -1
  16. package/cli/engine/fix/css-regions.mjs +181 -0
  17. package/cli/engine/fix/fixers.mjs +373 -0
  18. package/cli/engine/fix/index.mjs +426 -0
  19. package/cli/engine/node/file-system.mjs +0 -1
  20. package/cli/engine/profile/profiler.mjs +0 -1
  21. package/cli/engine/registry/antipatterns.mjs +49 -1
  22. package/cli/engine/rules/checks.mjs +292 -1
  23. package/cli/engine/shared/color.mjs +0 -1
  24. package/cli/engine/shared/constants.mjs +0 -1
  25. package/cli/engine/shared/fonts.mjs +0 -1
  26. package/cli/engine/shared/inline-ignores.mjs +0 -1
  27. package/cli/engine/shared/page.mjs +0 -1
  28. package/cli/lib/monodesign-config.mjs +0 -1
  29. package/package.json +2 -2
  30. package/skill/SKILL.src.md +2 -2
  31. package/skill/reference/antipatterns-catalog.md +47 -2
  32. package/skill/reference/critique.md +1 -1
  33. package/skill/reference/live.md +0 -8
  34. package/skill/reference/polish.md +2 -2
  35. package/skill/scripts/context-signals.mjs +39 -6
  36. package/skill/scripts/context.mjs +0 -1
  37. package/skill/scripts/critique-storage.mjs +175 -2
  38. package/skill/scripts/detect-csp.mjs +0 -1
  39. package/skill/scripts/detect.mjs +0 -1
  40. package/skill/scripts/hook-admin.mjs +0 -1
  41. package/skill/scripts/hook-before-edit.mjs +0 -1
  42. package/skill/scripts/hook-lib.mjs +0 -1
  43. package/skill/scripts/hook.mjs +0 -1
  44. package/skill/scripts/lib/design-parser.mjs +0 -1
  45. package/skill/scripts/lib/is-generated.mjs +0 -1
  46. package/skill/scripts/lib/monodesign-paths.mjs +0 -1
  47. package/skill/scripts/lib/provider.mjs +0 -1
  48. package/skill/scripts/lib/target-args.mjs +0 -1
  49. package/skill/scripts/live/browser-script-parts.mjs +0 -1
  50. package/skill/scripts/live/completion.mjs +0 -1
  51. package/skill/scripts/live/event-validation.mjs +0 -1
  52. package/skill/scripts/live/insert-ui.mjs +0 -1
  53. package/skill/scripts/live/manual-apply.mjs +0 -1
  54. package/skill/scripts/live/manual-edit-routes.mjs +0 -1
  55. package/skill/scripts/live/manual-edits-buffer.mjs +0 -1
  56. package/skill/scripts/live/session-store.mjs +5 -2
  57. package/skill/scripts/live/svelte-component.mjs +0 -1
  58. package/skill/scripts/live/sveltekit-adapter.mjs +0 -1
  59. package/skill/scripts/live/ui-core.mjs +0 -1
  60. package/skill/scripts/live/vocabulary.mjs +0 -1
  61. package/skill/scripts/live-accept.mjs +0 -1
  62. package/skill/scripts/live-browser-dom.js +0 -1
  63. package/skill/scripts/live-browser-session.js +0 -1
  64. package/skill/scripts/live-browser.js +0 -1
  65. package/skill/scripts/live-commit-manual-edits.mjs +0 -1
  66. package/skill/scripts/live-complete.mjs +0 -1
  67. package/skill/scripts/live-copy-edit-agent.mjs +0 -1
  68. package/skill/scripts/live-discard-manual-edits.mjs +0 -1
  69. package/skill/scripts/live-inject.mjs +0 -1
  70. package/skill/scripts/live-insert.mjs +0 -1
  71. package/skill/scripts/live-manual-edit-evidence.mjs +0 -1
  72. package/skill/scripts/live-poll.mjs +0 -1
  73. package/skill/scripts/live-resume.mjs +0 -1
  74. package/skill/scripts/live-server.mjs +0 -1
  75. package/skill/scripts/live-status.mjs +0 -1
  76. package/skill/scripts/live-target.mjs +0 -1
  77. package/skill/scripts/live-wrap.mjs +0 -1
  78. package/skill/scripts/live.mjs +0 -1
  79. package/skill/scripts/modern-screenshot.umd.js +0 -1
  80. package/skill/scripts/palette.mjs +0 -1
  81. package/skill/scripts/pin.mjs +0 -1
  82. package/src/rules.ts +60 -3
  83. package/src/tokens.css +0 -1
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
3
2
  /**
4
3
  * Critique persistence helper.
5
4
  *
@@ -19,6 +18,13 @@
19
18
  * node critique-storage.mjs write <slug> <snapshot-body-file>
20
19
  * node critique-storage.mjs latest <slug>
21
20
  * node critique-storage.mjs trend <slug> [limit]
21
+ * node critique-storage.mjs recall <slug> [limit]
22
+ *
23
+ * `write` also best-effort mirrors a compact record of the snapshot into
24
+ * monomind's persistent memory (namespace `design-critique`) so design
25
+ * health survives across sessions. The mirror is fire-and-forget: if the
26
+ * monomind CLI is not installed it is skipped silently, and it can be
27
+ * disabled outright with MONODESIGN_NO_MEMORY=1.
22
28
  *
23
29
  * Note: there is intentionally no `ignore` subcommand. ignore.md is a plain
24
30
  * markdown file; the model reads it directly with its file-read tool. This
@@ -28,6 +34,7 @@
28
34
 
29
35
  import fs from 'node:fs';
30
36
  import path from 'node:path';
37
+ import { spawnSync } from 'node:child_process';
31
38
  import { fileURLToPath, pathToFileURL } from 'node:url';
32
39
  import { getCritiqueDir } from './lib/monodesign-paths.mjs';
33
40
 
@@ -179,6 +186,164 @@ export function readTrend(slug, { limit = 5, cwd = process.cwd() } = {}) {
179
186
  return slice.map((file) => parseFrontmatter(fs.readFileSync(file, 'utf-8')));
180
187
  }
181
188
 
189
+ // ---- Monomind memory mirror --------------------------------------------
190
+
191
+ const asFiniteNumber = (v) => {
192
+ const n = Number(v);
193
+ return Number.isFinite(n) ? n : null;
194
+ };
195
+
196
+ /**
197
+ * Locate a locally installed monomind CLI bin by climbing from `cwd`.
198
+ * Returns null when none is installed — the caller then tries
199
+ * `npx --no-install`, which also fails fast (and silently) when absent.
200
+ */
201
+ function resolveMonomindBin(cwd) {
202
+ const name = process.platform === 'win32' ? 'monomind.cmd' : 'monomind';
203
+ let dir = path.resolve(cwd);
204
+ for (;;) {
205
+ const bin = path.join(dir, 'node_modules', '.bin', name);
206
+ try {
207
+ fs.accessSync(bin, fs.constants.X_OK);
208
+ return bin;
209
+ } catch { /* keep climbing */ }
210
+ const parent = path.dirname(dir);
211
+ if (parent === dir) return null;
212
+ dir = parent;
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Best-effort mirror of a snapshot's structured metadata into monomind's
218
+ * persistent memory (namespace `design-critique`, key `<project>-<slug>`,
219
+ * upserted), so design health is queryable across sessions via
220
+ * `monomind memory search`. Never throws and never fails the critique flow:
221
+ * if the monomind CLI is unavailable the call is skipped silently.
222
+ * Kill-switch: MONODESIGN_NO_MEMORY=1.
223
+ */
224
+ export function mirrorToMemory({ slug, meta = {}, filePath, cwd = process.cwd(), now = new Date(), env = process.env }) {
225
+ if (!slug) return { mirrored: false, reason: 'no-slug' };
226
+ if (env.MONODESIGN_NO_MEMORY === '1') return { mirrored: false, reason: 'disabled' };
227
+ try {
228
+ const record = {
229
+ score: asFiniteNumber(meta.total_score ?? meta.score),
230
+ p0: asFiniteNumber(meta.p0_count ?? meta.p0),
231
+ p1: asFiniteNumber(meta.p1_count ?? meta.p1),
232
+ date: now.toISOString(),
233
+ slug,
234
+ path: filePath ? path.relative(cwd, filePath) : null,
235
+ };
236
+ const project = kebab(path.basename(path.resolve(cwd))) || 'project';
237
+ const key = `${project}-${slug}`;
238
+ const argv = [
239
+ 'memory', 'store',
240
+ '--key', key,
241
+ '--value', JSON.stringify(record),
242
+ '--namespace', 'design-critique',
243
+ '--upsert',
244
+ ];
245
+ const bin = resolveMonomindBin(cwd);
246
+ const opts = { cwd, env, stdio: 'ignore', timeout: 15000 };
247
+ const run = bin
248
+ ? spawnSync(bin, argv, opts)
249
+ : spawnSync('npx', ['--no-install', 'monomind', ...argv], { ...opts, shell: process.platform === 'win32' });
250
+ if (run.error || run.status !== 0) return { mirrored: false, reason: 'cli-unavailable' };
251
+ return { mirrored: true, key };
252
+ } catch {
253
+ return { mirrored: false, reason: 'error' };
254
+ }
255
+ }
256
+
257
+ // ---- Recall (latest + trend + open items, one compact block) -----------
258
+
259
+ /**
260
+ * Extract open issue titles tagged [P0] / [P1] from a snapshot body.
261
+ * Matches the critique report's "**[P?] What**: ..." lines, but tolerates
262
+ * loose formatting: any line containing `[P0]`/`[P1]` counts, with the
263
+ * title taken from the text after the tag (markdown emphasis stripped).
264
+ */
265
+ export function extractIssueLines(body, { max = 8 } = {}) {
266
+ const issues = { p0: [], p1: [] };
267
+ if (!body || typeof body !== 'string') return issues;
268
+ // Drop frontmatter so metadata lines never masquerade as issues.
269
+ const text = body.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '');
270
+ for (const line of text.split(/\r?\n/)) {
271
+ const m = line.match(/\[(P0|P1)\]\s*(.*)/i);
272
+ if (!m) continue;
273
+ let title = m[2];
274
+ // "**[P0] Title**: explanation" → keep just the title.
275
+ const boldColon = title.indexOf('**:');
276
+ if (boldColon !== -1) title = title.slice(0, boldColon);
277
+ title = title.replace(/\*+/g, '').replace(/^[-–—\s]+/, '').replace(/[:\s]+$/, '').trim();
278
+ if (!title) continue;
279
+ const bucket = m[1].toUpperCase() === 'P0' ? issues.p0 : issues.p1;
280
+ if (bucket.length < max && !bucket.includes(title)) bucket.push(title);
281
+ }
282
+ return issues;
283
+ }
284
+
285
+ /**
286
+ * Direction of the last (up to) 3 finite scores: improving | declining |
287
+ * flat, or null when fewer than 2 scores exist.
288
+ */
289
+ export function trendDirection(scores) {
290
+ const s = (scores || []).map(asFiniteNumber).filter((n) => n !== null).slice(-3);
291
+ if (s.length < 2) return null;
292
+ if (s[s.length - 1] > s[0]) return 'improving';
293
+ if (s[s.length - 1] < s[0]) return 'declining';
294
+ return 'flat';
295
+ }
296
+
297
+ /**
298
+ * Combined latest + trend + open-issue view for `slug`, or null when no
299
+ * snapshot exists. This backs the `recall` subcommand that polish/critique
300
+ * consume instead of separate latest+trend calls.
301
+ */
302
+ export function readRecall(slug, { limit = 5, cwd = process.cwd() } = {}) {
303
+ const latest = readLatestSnapshot(slug, { cwd });
304
+ if (!latest) return null;
305
+ const trend = readTrend(slug, { limit, cwd });
306
+ const scores = trend.map((t) => asFiniteNumber(t.total_score ?? t.score));
307
+ return {
308
+ slug,
309
+ latest,
310
+ trend,
311
+ scores,
312
+ direction: trendDirection(scores),
313
+ issues: extractIssueLines(latest.body),
314
+ };
315
+ }
316
+
317
+ /** Render a recall result as a compact markdown block. */
318
+ export function formatRecall(recall, { cwd = process.cwd() } = {}) {
319
+ const { slug, latest, trend, scores, direction, issues } = recall;
320
+ const meta = latest.meta;
321
+ const score = asFiniteNumber(meta.total_score ?? meta.score);
322
+ const p0 = asFiniteNumber(meta.p0_count ?? meta.p0);
323
+ const p1 = asFiniteNumber(meta.p1_count ?? meta.p1);
324
+ const lines = [`## Design health: \`${slug}\``];
325
+ lines.push(
326
+ `- Latest score: ${score !== null ? `${score}/40` : 'n/a'}`
327
+ + ` (P0: ${p0 ?? '?'}, P1: ${p1 ?? '?'})`
328
+ + (meta.timestamp ? ` — ${meta.timestamp}` : ''),
329
+ );
330
+ const arrows = scores.map((n) => (n === null ? '?' : n)).join(' → ');
331
+ lines.push(`- Trend (last ${trend.length}): ${arrows}${direction ? ` (${direction})` : ''}`);
332
+ if (issues.p0.length) {
333
+ lines.push('- Open P0:');
334
+ for (const t of issues.p0) lines.push(` - ${t}`);
335
+ }
336
+ if (issues.p1.length) {
337
+ lines.push('- Open P1:');
338
+ for (const t of issues.p1) lines.push(` - ${t}`);
339
+ }
340
+ if (!issues.p0.length && !issues.p1.length) {
341
+ lines.push('- No open P0/P1 lines found in the latest snapshot.');
342
+ }
343
+ lines.push(`- Snapshot: ${path.relative(cwd, latest.path)}`);
344
+ return lines.join('\n');
345
+ }
346
+
182
347
  // ---- CLI ---------------------------------------------------------------
183
348
 
184
349
  function main(argv) {
@@ -203,6 +368,8 @@ function main(argv) {
203
368
  try { meta = JSON.parse(metaArg); } catch { /* ignore */ }
204
369
  }
205
370
  const out = writeSnapshot({ slug, meta, body: raw });
371
+ // Best-effort cross-session mirror; must never break the critique flow.
372
+ mirrorToMemory({ slug, meta, filePath: out });
206
373
  process.stdout.write(`${out}\n`);
207
374
  return;
208
375
  }
@@ -217,8 +384,14 @@ function main(argv) {
217
384
  process.stdout.write(JSON.stringify(rows, null, 2) + '\n');
218
385
  return;
219
386
  }
387
+ case 'recall': {
388
+ const recall = readRecall(args[0], { limit: args[1] ? Number(args[1]) : 5 });
389
+ if (!recall) { process.exit(2); }
390
+ process.stdout.write(`${formatRecall(recall)}\n`);
391
+ return;
392
+ }
220
393
  default:
221
- process.stderr.write('usage: critique-storage.mjs <slug|write|latest|trend> [args]\n');
394
+ process.stderr.write('usage: critique-storage.mjs <slug|write|latest|trend|recall> [args]\n');
222
395
  process.exit(1);
223
396
  }
224
397
  }
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * Scan a project tree for Content-Security-Policy signals and classify the
4
3
  * shape so the agent knows which patch template to propose.
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
3
2
 
4
3
  import fs from 'node:fs';
5
4
  import path from 'node:path';
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
3
2
  /**
4
3
  * The Monodesign hooks command manages the design hook runtime
5
4
  * via the `hook` key and shared detector ignores via the `detector` key in
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
3
2
  /**
4
3
  * Monodesign design hook — Cursor preToolUse write gate.
5
4
  *
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * Shared library for the Monodesign design hook.
4
3
  *
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
3
2
  /**
4
3
  * Monodesign design hook — PostToolUse entry point.
5
4
  *
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  // Parse a DESIGN.md (Stitch-spec format) into a structured JSON model that
3
2
  // the live-mode design-system panel can render. Deterministic, dependency-free.
4
3
  //
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * Decide whether a given file is "generated" (regenerated by a build step,
4
3
  * unsafe to write variants into) or "source" (safe to edit, changes persist).
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  import fs from 'node:fs';
3
2
  import path from 'node:path';
4
3
  import { resolveProjectRoot } from '../context.mjs';
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  // Source scripts default to slash commands. The provider build replaces only
3
2
  // this exact declaration, avoiding heuristic rewrites across executable code.
4
3
  export const MONODESIGN_COMMAND_PREFIX = '/'; // @monodesign-provider-command-prefix
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  class TargetArgError extends Error {
3
2
  constructor(message, code) {
4
3
  super(message);
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  import fs from 'node:fs';
3
2
  import path from 'node:path';
4
3
 
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  export function completionTypeForAcceptResult(eventType, acceptResult) {
3
2
  if (eventType === 'discard') return acceptResult?.handled === true ? 'discarded' : 'error';
4
3
  if (acceptResult?.handled === true && acceptResult?.carbonize === true) return 'agent_done';
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * Shared event validation for the live helper server.
4
3
  * Extracted for unit testing (insert mode rules).
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * Pure helpers for live-mode insert UI (browser + tests).
4
3
  * Kept separate from live-browser.js so insert logic is unit-testable.
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  import { randomUUID } from 'node:crypto';
3
2
  import fs from 'node:fs';
4
3
  import path from 'node:path';
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  import { validateEvent } from './event-validation.mjs';
3
2
  import {
4
3
  countByPage as countPendingByPage,
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * Shared helpers for the pending-manual-edits buffer on disk.
4
3
  *
@@ -1,9 +1,12 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  import fs from 'node:fs';
3
2
  import path from 'node:path';
4
3
  import { getLegacyLiveSessionsDir, getLiveSessionsDir } from '../lib/monodesign-paths.mjs';
5
4
 
6
- const COMPLETED_PHASES = new Set(['completed', 'discarded']);
5
+ // steer_done is terminal too: a steer session has no pending work after the
6
+ // agent's reply, and leaving it "active" makes live-status suggest recovery
7
+ // for finished steers forever (the browser's trailing steer checkpoint would
8
+ // also re-stamp phase "steer"; terminal phases ignore late checkpoints).
9
+ const COMPLETED_PHASES = new Set(['completed', 'discarded', 'steer_done']);
7
10
 
8
11
  export function createLiveSessionStore({ cwd = process.cwd(), sessionId } = {}) {
9
12
  const rootDir = getLiveSessionsDir(cwd);
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * Svelte live-mode component injection helpers.
4
3
  *
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * SvelteKit live-mode adapter.
4
3
  *
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * Framework-neutral Monodesign live chrome contract.
4
3
  *
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * Canonical design-command vocabulary for Live Mode: each command's value, human
4
3
  * label, and SVG icon. Icons stack above the chip label; strokes use currentColor
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * CLI helper: deterministic accept/discard of variant sessions.
4
3
  *
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * Browser-side DOM helpers for Monodesign live mode.
4
3
  *
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * Browser-side durable session helpers for Monodesign live mode.
4
3
  *
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * Monodesign Live Variant Mode - Browser Script
4
3
  *
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
3
2
  /**
4
3
  * CLI helper: apply pending live copy edits as one AI-owned batch.
5
4
  *
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
3
2
  /**
4
3
  * Canonical durable completion acknowledgement for Monodesign live sessions.
5
4
  */
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
3
2
  /**
4
3
  * Applies staged live copy-edit batches by waking a local AI coding agent.
5
4
  *
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
3
2
  /**
4
3
  * CLI helper: discard pending manual edits from the buffer without applying.
5
4
  *
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * CLI helper: insert/remove the live variant mode script tag in the project's
4
3
  * main HTML entry point.
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * CLI helper: find an anchor element in source and splice an insert-variant
4
3
  * wrapper before or after it (no original variant — net-new content).
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
3
2
  /**
4
3
  * Collect evidence for pending live copy edits.
5
4
  *
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * CLI client for the live variant mode poll/reply protocol.
4
3
  *
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
3
2
  /**
4
3
  * Recover the next agent action from the durable live-session journal.
5
4
  */
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
3
2
  /**
4
3
  * Live variant mode server (self-contained, zero dependencies).
5
4
  *
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
3
2
  /**
4
3
  * Print durable recovery status for Monodesign live sessions.
5
4
  */
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  import path from 'node:path';
3
2
  import { resolveProjectRoot } from './context.mjs';
4
3
  import { parseTargetPath } from './lib/target-args.mjs';
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * CLI helper: find an element in source and wrap it in a variant container.
4
3
  *
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  /**
3
2
  * CLI entry point: prepare everything needed to enter the live variant poll loop.
4
3
  *
@@ -1,4 +1,3 @@
1
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
2
1
  (function(y,v){typeof exports=="object"&&typeof module!="undefined"?v(exports):typeof define=="function"&&define.amd?define(["exports"],v):(y=typeof globalThis!="undefined"?globalThis:y||self,v(y.modernScreenshot={}))})(this,function(y){"use strict";var rr=Object.defineProperty,nr=Object.defineProperties;var or=Object.getOwnPropertyDescriptors;var Z=Object.getOwnPropertySymbols;var xe=Object.prototype.hasOwnProperty,Me=Object.prototype.propertyIsEnumerable;var Oe=Math.pow,Le=(y,v,N)=>v in y?rr(y,v,{enumerable:!0,configurable:!0,writable:!0,value:N}):y[v]=N,D=(y,v)=>{for(var N in v||(v={}))xe.call(v,N)&&Le(y,N,v[N]);if(Z)for(var N of Z(v))Me.call(v,N)&&Le(y,N,v[N]);return y},M=(y,v)=>nr(y,or(v));var je=(y,v)=>{var N={};for(var R in y)xe.call(y,R)&&v.indexOf(R)<0&&(N[R]=y[R]);if(y!=null&&Z)for(var R of Z(y))v.indexOf(R)<0&&Me.call(y,R)&&(N[R]=y[R]);return N};var C=(y,v,N)=>new Promise((R,O)=>{var X=P=>{try{q(N.next(P))}catch(W){O(W)}},j=P=>{try{q(N.throw(P))}catch(W){O(W)}},q=P=>P.done?R(P.value):Promise.resolve(P.value).then(X,j);q((N=N.apply(y,v)).next())});var Be;function v(e,t){return e[13]=1,e[14]=t>>8,e[15]=t&255,e[16]=t>>8,e[17]=t&255,e}const N=112,R=72,O=89,X=115;let j;function q(){const e=new Int32Array(256);for(let t=0;t<256;t++){let r=t;for(let n=0;n<8;n++)r=r&1?3988292384^r>>>1:r>>>1;e[t]=r}return e}function P(e){let t=-1;j||(j=q());for(let r=0;r<e.length;r++)t=j[(t^e[r])&255]^t>>>8;return t^-1}function W(e){const t=e.length-1;for(let r=t;r>=4;r--)if(e[r-4]===9&&e[r-3]===N&&e[r-2]===R&&e[r-1]===O&&e[r]===X)return r-3;return 0}function ae(e,t,r=!1){const n=new Uint8Array(13);t*=39.3701,n[0]=N,n[1]=R,n[2]=O,n[3]=X,n[4]=t>>>24,n[5]=t>>>16,n[6]=t>>>8,n[7]=t&255,n[8]=n[4],n[9]=n[5],n[10]=n[6],n[11]=n[7],n[12]=1;const i=P(n),a=new Uint8Array(4);if(a[0]=i>>>24,a[1]=i>>>16,a[2]=i>>>8,a[3]=i&255,r){const s=W(e);return e.set(n,s),e.set(a,s+13),e}else{const s=new Uint8Array(4);s[0]=0,s[1]=0,s[2]=0,s[3]=9;const o=new Uint8Array(54);return o.set(e,0),o.set(s,33),o.set(n,37),o.set(a,50),o}}const qe="AAlwSFlz",We="AAAJcEhZ",He="AAAACXBI";function Ve(e){let t=e.indexOf(qe);return t===-1&&(t=e.indexOf(We)),t===-1&&(t=e.indexOf(He)),t}const se="[modern-screenshot]",U=typeof window!="undefined",ze=U&&"Worker"in window,ie=U&&"atob"in window,Xe=U&&"btoa"in window,ee=U?(Be=window.navigator)==null?void 0:Be.userAgent:"",le=ee.includes("Chrome"),G=ee.includes("AppleWebKit")&&!le,te=ee.includes("Firefox"),Ge=e=>e&&"__CONTEXT__"in e,Ye=e=>e.constructor.name==="CSSFontFaceRule",Je=e=>e.constructor.name==="CSSImportRule",Ke=e=>e.constructor.name==="CSSLayerBlockRule",I=e=>e.nodeType===1,H=e=>typeof e.className=="object",ce=e=>e.tagName==="image",Qe=e=>e.tagName==="use",V=e=>I(e)&&typeof e.style!="undefined"&&!H(e),Ze=e=>e.nodeType===8,et=e=>e.nodeType===3,$=e=>e.tagName==="IMG",Y=e=>e.tagName==="VIDEO",tt=e=>e.tagName==="CANVAS",rt=e=>e.tagName==="TEXTAREA",nt=e=>e.tagName==="INPUT",ot=e=>e.tagName==="STYLE",at=e=>e.tagName==="SCRIPT",st=e=>e.tagName==="SELECT",it=e=>e.tagName==="SLOT",lt=e=>e.tagName==="IFRAME",ct=(...e)=>console.warn(se,...e);function ut(e){var r;const t=(r=e==null?void 0:e.createElement)==null?void 0:r.call(e,"canvas");return t&&(t.height=t.width=1),!!t&&"toDataURL"in t&&!!t.toDataURL("image/webp").includes("image/webp")}const re=e=>e.startsWith("data:");function ue(e,t){if(e.match(/^[a-z]+:\/\//i))return e;if(U&&e.match(/^\/\//))return window.location.protocol+e;if(e.match(/^[a-z]+:/i)||!U)return e;const r=J().implementation.createHTMLDocument(),n=r.createElement("base"),i=r.createElement("a");return r.head.appendChild(n),r.body.appendChild(i),t&&(n.href=t),i.href=e,i.href}function J(e){var t;return(t=e&&I(e)?e==null?void 0:e.ownerDocument:e)!=null?t:window.document}const K="http://www.w3.org/2000/svg";function fe(e,t,r){const n=J(r).createElementNS(K,"svg");return n.setAttributeNS(null,"width",e.toString()),n.setAttributeNS(null,"height",t.toString()),n.setAttributeNS(null,"viewBox",`0 0 ${e} ${t}`),n}function de(e,t){let r=new XMLSerializer().serializeToString(e);return t&&(r=r.replace(/[\u0000-\u0008\v\f\u000E-\u001F\uD800-\uDFFF\uFFFE\uFFFF]/gu,"")),`data:image/svg+xml;charset=utf-8,${encodeURIComponent(r)}`}function ft(e,t="image/png",r=1){return C(this,null,function*(){try{return yield new Promise((n,i)=>{e.toBlob(a=>{a?n(a):i(new Error("Blob is null"))},t,r)})}catch(n){if(ie)return dt(e.toDataURL(t,r));throw n}})}function dt(e){var o,c;const[t,r]=e.split(","),n=(c=(o=t.match(/data:(.+);/))==null?void 0:o[1])!=null?c:void 0,i=window.atob(r),a=i.length,s=new Uint8Array(a);for(let u=0;u<a;u+=1)s[u]=i.charCodeAt(u);return new Blob([s],{type:n})}function ge(e,t){return new Promise((r,n)=>{const i=new FileReader;i.onload=()=>r(i.result),i.onerror=()=>n(i.error),i.onabort=()=>n(new Error(`Failed read blob to ${t}`)),t==="dataUrl"?i.readAsDataURL(e):t==="arrayBuffer"&&i.readAsArrayBuffer(e)})}const gt=e=>ge(e,"dataUrl"),mt=e=>ge(e,"arrayBuffer");function _(e,t){const r=J(t).createElement("img");return r.decoding="sync",r.loading="eager",r.src=e,r}function L(e,t){return new Promise(r=>{const{timeout:n,ownerDocument:i,onError:a,onWarn:s}=t!=null?t:{},o=typeof e=="string"?_(e,J(i)):e;let c=null,u=null;function l(){r(o),c&&clearTimeout(c),u==null||u()}if(n&&(c=setTimeout(l,n)),Y(o)){const d=o.currentSrc||o.src;if(!d)return o.poster?L(o.poster,t).then(r):l();if(o.readyState>=2)return l();const m=l,f=h=>{s==null||s("Failed video load",d,h),a==null||a(h),l()};u=()=>{o.removeEventListener("loadeddata",m),o.removeEventListener("error",f)},o.addEventListener("loadeddata",m,{once:!0}),o.addEventListener("error",f,{once:!0})}else{const d=ce(o)?o.href.baseVal:o.currentSrc||o.src;if(!d)return l();const m=()=>C(this,null,function*(){if($(o)&&"decode"in o)try{yield o.decode()}catch(h){s==null||s("Failed to decode image, trying to render anyway",o.dataset.originalSrc||d,h)}l()}),f=h=>{s==null||s("Failed image load",o.dataset.originalSrc||d,h),l()};if($(o)&&o.complete)return m();u=()=>{o.removeEventListener("load",m),o.removeEventListener("error",f)},o.addEventListener("load",m,{once:!0}),o.addEventListener("error",f,{once:!0})}})}function me(e,t){return C(this,null,function*(){V(e)&&($(e)||Y(e)?yield L(e,t):yield Promise.all(["img","video"].flatMap(r=>Array.from(e.querySelectorAll(r)).map(n=>L(n,t)))))})}const he=function(){let t=0;const r=()=>`0000${(Math.random()*Oe(36,4)<<0).toString(36)}`.slice(-4);return()=>(t+=1,`u${r()}${t}`)}();function we(e){return e==null?void 0:e.split(",").map(t=>t.trim().replace(/"|'/g,"").toLowerCase()).filter(Boolean)}let pe=0;function ht(e){const t=`${se}[#${pe}]`;return pe++,{time:r=>e&&console.time(`${t} ${r}`),timeEnd:r=>e&&console.timeEnd(`${t} ${r}`),warn:(...r)=>e&&ct(...r)}}function wt(e){return{cache:e?"no-cache":"force-cache"}}function k(e,t){return C(this,null,function*(){return Ge(e)?e:ye(e,M(D({},t),{autoDestruct:!0}))})}function ye(e,t){return C(this,null,function*(){var f,h,g,p,E;const{scale:r=1,workerUrl:n,workerNumber:i=1}=t||{},a=!!(t!=null&&t.debug),s=(f=t==null?void 0:t.features)!=null?f:!0,o=(h=e.ownerDocument)!=null?h:U?window.document:void 0,c=(p=(g=e.ownerDocument)==null?void 0:g.defaultView)!=null?p:U?window:void 0,u=new Map,l=M(D({width:0,height:0,quality:1,type:"image/png",scale:r,backgroundColor:null,style:null,filter:null,maximumCanvasSize:0,timeout:3e4,progress:null,debug:a,fetch:D({requestInit:wt((E=t==null?void 0:t.fetch)==null?void 0:E.bypassingCache),placeholderImage:"data:image/png;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",bypassingCache:!1},t==null?void 0:t.fetch),fetchFn:null,font:{},drawImageInterval:100,workerUrl:null,workerNumber:i,onCloneEachNode:null,onCloneNode:null,onEmbedNode:null,onCreateForeignObjectSvg:null,includeStyleProperties:null,autoDestruct:!1},t),{__CONTEXT__:!0,log:ht(a),node:e,ownerDocument:o,ownerWindow:c,dpi:r===1?null:96*r,svgStyleElement:be(o),svgDefsElement:o==null?void 0:o.createElementNS(K,"defs"),svgStyles:new Map,defaultComputedStyles:new Map,workers:[...Array.from({length:ze&&n&&i?i:0})].map(()=>{try{const b=new Worker(n);return b.onmessage=w=>C(this,null,function*(){var A,F,B,$e;const{url:S,result:T}=w.data;T?(F=(A=u.get(S))==null?void 0:A.resolve)==null||F.call(A,T):($e=(B=u.get(S))==null?void 0:B.reject)==null||$e.call(B,new Error(`Error receiving message from worker: ${S}`))}),b.onmessageerror=w=>{var T,A;const{url:S}=w.data;(A=(T=u.get(S))==null?void 0:T.reject)==null||A.call(T,new Error(`Error receiving message from worker: ${S}`))},b}catch(b){return l.log.warn("Failed to new Worker",b),null}}).filter(Boolean),fontFamilies:new Map,fontCssTexts:new Map,acceptOfImage:`${[ut(o)&&"image/webp","image/svg+xml","image/*","*/*"].filter(Boolean).join(",")};q=0.8`,requests:u,drawImageCount:0,tasks:[],features:s,isEnable:b=>{var w,S;return b==="restoreScrollPosition"?typeof s=="boolean"?!1:(w=s[b])!=null?w:!1:typeof s=="boolean"?s:(S=s[b])!=null?S:!0},shadowRoots:[]});l.log.time("wait until load"),yield me(e,{timeout:l.timeout,onWarn:l.log.warn}),l.log.timeEnd("wait until load");const{width:d,height:m}=pt(e,l);return l.width=d,l.height=m,l})}function be(e){if(!e)return;const t=e.createElement("style"),r=t.ownerDocument.createTextNode(`
3
2
  .______background-clip--text {
4
3
  background-clip: text;
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
3
2
  /**
4
3
  * Brand-seed picker. Returns one OKLCH seed color + the mood it most
5
4
  * naturally evokes, and teaches the model how to compose a full palette
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
3
2
  /**
4
3
  * Pin/unpin sub-commands as standalone skill shortcuts.
5
4
  *
package/src/rules.ts CHANGED
@@ -1,20 +1,58 @@
1
1
  /**
2
2
  * Shared design rules — the invariants every monodesign generation must satisfy.
3
3
  * These mirror the "Absolute bans" and "Shared design laws" in the skill.
4
+ *
5
+ * Where a ban has a corresponding mechanical detector rule in the engine
6
+ * registry (cli/engine/registry/antipatterns.mjs), its id and description are
7
+ * pulled from that registry at load time instead of being hand-copied here —
8
+ * one source of truth, no drift between "what generation avoids" and "what
9
+ * detection flags". Bans with no `engineRuleId` are composition/judgment
10
+ * calls the detector cannot mechanically check (holistic layout patterns);
11
+ * they exist here for generation-time guidance only.
4
12
  */
5
13
 
6
- export const ABSOLUTE_BANS = [
14
+ // Typed as plain string so tsc emits a true dynamic import instead of trying
15
+ // to type-resolve the untyped .mjs engine module (matches antipatterns.ts).
16
+ const REGISTRY_SPECIFIER: string = '../cli/engine/registry/antipatterns.mjs';
17
+
18
+ interface EngineRuleEntry {
19
+ id: string;
20
+ name: string;
21
+ description: string;
22
+ }
23
+
24
+ let registryPromise: Promise<EngineRuleEntry[] | null> | undefined;
25
+
26
+ async function loadRegistry(): Promise<EngineRuleEntry[] | null> {
27
+ registryPromise ??= import(REGISTRY_SPECIFIER).then(
28
+ (mod) => (mod as { ANTIPATTERNS: EngineRuleEntry[] }).ANTIPATTERNS,
29
+ () => null,
30
+ );
31
+ return registryPromise;
32
+ }
33
+
34
+ export interface AbsoluteBan {
35
+ id: string;
36
+ rule: string;
37
+ description: string;
38
+ /** Engine registry rule id this ban is mechanically detected by, if any. */
39
+ engineRuleId?: string;
40
+ }
41
+
42
+ const ABSOLUTE_BANS_STATIC: AbsoluteBan[] = [
7
43
  {
8
- id: 'side-stripe',
44
+ id: 'side-tab',
9
45
  rule: 'No side-stripe borders',
10
46
  description:
11
47
  'border-left or border-right > 1px as a colored accent on cards/list items/callouts is never intentional. Rewrite with full borders, background tints, leading numbers/icons, or nothing.',
48
+ engineRuleId: 'side-tab',
12
49
  },
13
50
  {
14
51
  id: 'gradient-text',
15
52
  rule: 'No gradient text',
16
53
  description:
17
54
  'background-clip:text with a gradient is decorative and never meaningful. Use a single solid color; emphasis via weight or size.',
55
+ engineRuleId: 'gradient-text',
18
56
  },
19
57
  {
20
58
  id: 'glassmorphism-default',
@@ -40,7 +78,26 @@ export const ABSOLUTE_BANS = [
40
78
  description:
41
79
  'Modals are usually laziness. Exhaust inline / progressive disclosure alternatives first.',
42
80
  },
43
- ] as const;
81
+ ];
82
+
83
+ /** Static ban list — id/rule/description as authored here (may drift from the engine registry's wording for bans that also have a detector rule). */
84
+ export const ABSOLUTE_BANS = ABSOLUTE_BANS_STATIC;
85
+
86
+ /**
87
+ * Same ban list, with descriptions for detector-backed bans (`engineRuleId`
88
+ * set) replaced by the live text from the engine registry. Falls back to the
89
+ * static description when the registry can't be loaded (e.g. package not
90
+ * fully installed) or the referenced rule id is missing.
91
+ */
92
+ export async function getAbsoluteBans(): Promise<AbsoluteBan[]> {
93
+ const registry = await loadRegistry();
94
+ if (!registry) return ABSOLUTE_BANS_STATIC;
95
+ const byId = new Map(registry.map((r) => [r.id, r]));
96
+ return ABSOLUTE_BANS_STATIC.map((ban) => {
97
+ const engineRule = ban.engineRuleId ? byId.get(ban.engineRuleId) : undefined;
98
+ return engineRule ? { ...ban, description: engineRule.description } : ban;
99
+ });
100
+ }
44
101
 
45
102
  export const COLOR_STRATEGIES = ['restrained', 'committed', 'full-palette', 'drenched'] as const;
46
103
  export type ColorStrategy = (typeof COLOR_STRATEGIES)[number];
package/src/tokens.css CHANGED
@@ -1,6 +1,5 @@
1
1
  /* monodesign — design token system
2
2
  * OKLCH color space, 8px grid, expo easing
3
- * Apache 2.0 — pattern adapted from impeccable (Apache-2.0), rebranded for monomind
4
3
  */
5
4
 
6
5
  :root {