@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.
- package/NOTICE.md +7 -19
- package/cli/bin/cli.js +5 -1
- package/cli/bin/commands/ignores.mjs +0 -1
- package/cli/engine/browser/injected/index.mjs +25 -1
- package/cli/engine/cli/main.mjs +0 -1
- package/cli/engine/design-system.mjs +0 -1
- package/cli/engine/detect-antipatterns-browser.js +356 -1
- package/cli/engine/detect-antipatterns.mjs +0 -1
- package/cli/engine/engines/browser/detect-url.mjs +0 -1
- package/cli/engine/engines/browser/drivers.mjs +0 -1
- package/cli/engine/engines/regex/detect-text.mjs +63 -1
- package/cli/engine/engines/static-html/css-cascade.mjs +0 -1
- package/cli/engine/engines/static-html/detect-html.mjs +19 -1
- package/cli/engine/engines/visual/screenshot-contrast.mjs +0 -1
- package/cli/engine/findings.mjs +0 -1
- package/cli/engine/fix/css-regions.mjs +181 -0
- package/cli/engine/fix/fixers.mjs +373 -0
- package/cli/engine/fix/index.mjs +426 -0
- package/cli/engine/node/file-system.mjs +0 -1
- package/cli/engine/profile/profiler.mjs +0 -1
- package/cli/engine/registry/antipatterns.mjs +49 -1
- package/cli/engine/rules/checks.mjs +292 -1
- package/cli/engine/shared/color.mjs +0 -1
- package/cli/engine/shared/constants.mjs +0 -1
- package/cli/engine/shared/fonts.mjs +0 -1
- package/cli/engine/shared/inline-ignores.mjs +0 -1
- package/cli/engine/shared/page.mjs +0 -1
- package/cli/lib/monodesign-config.mjs +0 -1
- package/package.json +2 -2
- package/skill/SKILL.src.md +2 -2
- package/skill/reference/antipatterns-catalog.md +47 -2
- package/skill/reference/critique.md +1 -1
- package/skill/reference/live.md +0 -8
- package/skill/reference/polish.md +2 -2
- package/skill/scripts/context-signals.mjs +39 -6
- package/skill/scripts/context.mjs +0 -1
- package/skill/scripts/critique-storage.mjs +175 -2
- package/skill/scripts/detect-csp.mjs +0 -1
- package/skill/scripts/detect.mjs +0 -1
- package/skill/scripts/hook-admin.mjs +0 -1
- package/skill/scripts/hook-before-edit.mjs +0 -1
- package/skill/scripts/hook-lib.mjs +0 -1
- package/skill/scripts/hook.mjs +0 -1
- package/skill/scripts/lib/design-parser.mjs +0 -1
- package/skill/scripts/lib/is-generated.mjs +0 -1
- package/skill/scripts/lib/monodesign-paths.mjs +0 -1
- package/skill/scripts/lib/provider.mjs +0 -1
- package/skill/scripts/lib/target-args.mjs +0 -1
- package/skill/scripts/live/browser-script-parts.mjs +0 -1
- package/skill/scripts/live/completion.mjs +0 -1
- package/skill/scripts/live/event-validation.mjs +0 -1
- package/skill/scripts/live/insert-ui.mjs +0 -1
- package/skill/scripts/live/manual-apply.mjs +0 -1
- package/skill/scripts/live/manual-edit-routes.mjs +0 -1
- package/skill/scripts/live/manual-edits-buffer.mjs +0 -1
- package/skill/scripts/live/session-store.mjs +5 -2
- package/skill/scripts/live/svelte-component.mjs +0 -1
- package/skill/scripts/live/sveltekit-adapter.mjs +0 -1
- package/skill/scripts/live/ui-core.mjs +0 -1
- package/skill/scripts/live/vocabulary.mjs +0 -1
- package/skill/scripts/live-accept.mjs +0 -1
- package/skill/scripts/live-browser-dom.js +0 -1
- package/skill/scripts/live-browser-session.js +0 -1
- package/skill/scripts/live-browser.js +0 -1
- package/skill/scripts/live-commit-manual-edits.mjs +0 -1
- package/skill/scripts/live-complete.mjs +0 -1
- package/skill/scripts/live-copy-edit-agent.mjs +0 -1
- package/skill/scripts/live-discard-manual-edits.mjs +0 -1
- package/skill/scripts/live-inject.mjs +0 -1
- package/skill/scripts/live-insert.mjs +0 -1
- package/skill/scripts/live-manual-edit-evidence.mjs +0 -1
- package/skill/scripts/live-poll.mjs +0 -1
- package/skill/scripts/live-resume.mjs +0 -1
- package/skill/scripts/live-server.mjs +0 -1
- package/skill/scripts/live-status.mjs +0 -1
- package/skill/scripts/live-target.mjs +0 -1
- package/skill/scripts/live-wrap.mjs +0 -1
- package/skill/scripts/live.mjs +0 -1
- package/skill/scripts/modern-screenshot.umd.js +0 -1
- package/skill/scripts/palette.mjs +0 -1
- package/skill/scripts/pin.mjs +0 -1
- package/src/rules.ts +60 -3
- 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
|
}
|
package/skill/scripts/detect.mjs
CHANGED
package/skill/scripts/hook.mjs
CHANGED
|
@@ -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
|
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,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
|
-
|
|
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);
|
package/skill/scripts/live.mjs
CHANGED
|
@@ -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;
|
package/skill/scripts/pin.mjs
CHANGED
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
|
-
|
|
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-
|
|
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
|
-
]
|
|
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];
|