taste-lint 0.2.0 → 0.3.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/README.md +5 -3
- package/data/config.schema.json +7 -0
- package/data/rules/copywriting/copywriting-anthropomorphism.yaml +4 -3
- package/data/rules/copywriting/copywriting-antithesis-frame.yaml +21 -0
- package/data/rules/copywriting/copywriting-click-here-links.yaml +4 -3
- package/data/rules/copywriting/copywriting-conditions-first.yaml +2 -2
- package/data/rules/copywriting/copywriting-copula-avoidance.yaml +21 -0
- package/data/rules/copywriting/copywriting-error-descriptions.yaml +2 -2
- package/data/rules/copywriting/copywriting-filler-words.yaml +4 -3
- package/data/rules/copywriting/copywriting-latinisms.yaml +6 -4
- package/data/rules/copywriting/copywriting-nominalisations.yaml +4 -3
- package/data/rules/copywriting/copywriting-participle-tail.yaml +21 -0
- package/data/rules/copywriting/copywriting-passive-voice.yaml +4 -3
- package/data/rules/copywriting/copywriting-promotional-docs-voice.yaml +4 -3
- package/data/rules/copywriting/copywriting-requirements-language.yaml +2 -2
- package/data/rules/copywriting/copywriting-searchable-headings.yaml +2 -2
- package/data/rules/copywriting/copywriting-tee-up.yaml +21 -0
- package/data/rules/copywriting/copywriting-temporal-words.yaml +4 -3
- package/data/rules/craft/craft-atmosphere-gradient.yaml +28 -0
- package/data/rules/craft/craft-default-indigo.yaml +28 -0
- package/data/rules/motion/motion-will-change-misuse.yaml +28 -0
- package/data/rules/product/product-nested-modals.yaml +1 -1
- package/data/rules/product/product-select-for-few-options.yaml +1 -1
- package/dist/cli.js +186 -37
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +185 -35
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -431,6 +431,8 @@ interface Config {
|
|
|
431
431
|
};
|
|
432
432
|
smartQuotesAtBuild: boolean;
|
|
433
433
|
exclude: string[];
|
|
434
|
+
/** Extra rule directories from the config, absolute. */
|
|
435
|
+
rules?: string[];
|
|
434
436
|
}
|
|
435
437
|
//#endregion
|
|
436
438
|
//#region src/scan/profiles.d.ts
|
|
@@ -507,6 +509,8 @@ interface LoadOptions {
|
|
|
507
509
|
allowDraft?: boolean;
|
|
508
510
|
/** Restrict to these rule ids. */
|
|
509
511
|
only?: string[];
|
|
512
|
+
/** More rule directories with the same domain-folder layout, such as a voice pack. */
|
|
513
|
+
extraDirs?: string[];
|
|
510
514
|
}
|
|
511
515
|
declare const loadRules: (rulesDir: string, options?: LoadOptions) => Rule[];
|
|
512
516
|
//#endregion
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/analysis/repository.ts","../src/types.ts","../src/scan/profiles.ts","../src/lib/config.ts","../src/lint.ts","../src/rules/load.ts","../src/rules/taxonomy.ts","../src/extract/index.ts","../src/extract/tsx.ts","../src/map/jev.ts","../src/report/json.ts","../src/report/sarif.ts","../src/report/tty.ts","../src/lib/stats.ts","../src/eval/coverage.ts","../src/eval/metrics.ts","../src/eval/corpus.ts","../src/eval/promotion.ts","../src/eval/tune.ts"],"mappings":";UAQiB;EACf;EACA;EACA;EACA;EACA;EACA;EACA,WAAW;EACX;IAAa;MAAS;;IAAmB;MAAO;;;;UAGjC;EACf;EACA;EACA;;UAGe;EACf;IAAW;IAAgB;IAAsB;;EACjD,WAAW;EACX,UAAU;EACV;EACA,YAAY,KACV;;cAYS;WACF;mBACQ;EAEjB,YAAY;EAIZ,QAAQ,cAAc;EAStB,KAAK;EAkBL,OAAO;EAUP,SACE;IACG;IAAc,OAAO;;;;;cCtFf;KASD,iBAAiB;cAEhB;;;;;;cAaA,2BAA2B;KAC5B,mBAAmB;cAElB;KACD,eAAe;cAEd;KACD,mBAAmB;;cAElB,eAAe,OAAO;cAEtB;KACD,qBAAqB;cAEpB;KAcD,kBAAkB;cAEjB;KAcD,eAAe;cAEd;KAQD,qBAAqB;KAErB;UAEK;EACf;EACA;EACA;EACA;;UAGe;;EAEf;EACA;;EAEA;;EAEA;;EAEA;;UAGe;EACf;EACA;;UAGe;EACf;EACA;IAAa,MAAM;IAAe,OAAO;;;EAEzC,UAAU;;UAGK;EACf;EACA;;UAGe;EACf;;EAEA;;UAGe;EACf;EACA,UAAU;EACV,OAAO;;EAEP;;UAGe;EACf;EACA;EACA;EACA;;EAEA;;UAGe;EACf,SAAS;EACT;EACA;EACA;EACA,QAAQ;EACR,QAAQ;;EAER;IAAS;IAAmB;;EAC5B,MAAM;;EAEN,MAAM;EACN,aAAa;EACb,WAAW;EACX,YAAY;EACZ,UAAU;EACV,KAAK;EACL,gBAAgB;EAChB,QAAQ;EACR;;EAEA;;;;;EAKA,SAAS,MAAM,SAAS;;UAGT;EACf;EACA,SAAS;EACT;EACA;EACA;;KAEU,SAAS,eAAe;UAEnB;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA,aAAa;;UAGE;;EAEf,YAAY;EACZ;EACA;EACA,SAAS;EACT;EACA;EACA;EACA,MAAM;;EAEN;;EAEA;EACA;EACA;;UAGe;EACf,QAAQ;;EAER;EACA,MAAM;;EAEN;;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;;;;;;;EAOA;EACA,SAAS;EACT,aAAa;EACb;IAAe,OAAO;IAAkB,OAAO;;;EAE/C;;UAGe;EACf;EACA;;EAEA;;UAGe;;EAEf;EACA,SAAS;EACT;EACA;EACA,QAAQ;EACR,UAAU;EACV,MAAM;EACN;EACA,MAAM;EACN;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;EACA;EACA;;UAGe;EACf,YAAY;IAGR,QAAQ;IACR;IACA;IACA;IACA;IACA;;EAGJ,UAAU;IAGN;IACA;IACA;IACA;IACA;;EAGJ,QAAQ;IAEJ;IAAa;IAAgB;IAAgB;;;UAIlC;EACf;EACA;EACA;EACA;EACA;EACA;EACA;;UAGe;;EAEf;EACA;EACA;EACA;EACA;EACA;EACA;;KAGU;UAEK;EACf;EACA;EACA;EACA;EACA,QAAQ;EACR;;UAGe;EACf,QAAQ;IAGJ;IACA;IACA;IACA;IACA;IACA;;EAGJ,YAAY;IAAiB;IAAuB;;;UAGrC;EACf;EACA;EACA,WAAW;EACX;EACA;;UAGe;EACf,UAAU;EACV,eAAe;EACf,gBAAgB;EAChB,WAAW;EACX,QAAQ;EACR;EACA;EACA,QAAQ;EACR,UAAU;EACV,UAAU;EACV,WAAW;EACX,OAAO;EACP;EACA;EACA;IAAc;IAAkB;IAAqB;;EACrD;;KAKU,gDAKR;GACG,cAAc;;UAEJ;EACf;EACA,cAAc;EACd;IAAa,OAAO;IAAY,QAAQ;;;UAGzB;EACf;EACA,OAAO;EACP,WAAW,eAAe;;UAGX;EACf;EACA;EACA,SAAS;IAAiB;IAAe;;EACzC;IAAS;IAAsB;;;KAGrB,YACV,SAAS,kBACT,2BACG,QAAQ;UAEI;EACf,SAAS,QAAQ,4BAA4B;EAC7C,UAAU,KAAK,4BAA4B;EAC3C;;UAGe;EACf;EACA,MAAM;EACN;EACA,UAAU,QAAQ;EAClB,aAAa;EACb;EACA;EACA,QAAQ;EACR;EACA;EACA;EACA;IAAU;IAAc;IAAc;IAAa;;EACnD;;UAGe;EACf;EACA;IAAY;IAAc,MAAM;;EAChC;IAAc;IAAkB;;EAChC;IAAY,OAAO;;EACnB;EACA;;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/analysis/repository.ts","../src/types.ts","../src/scan/profiles.ts","../src/lib/config.ts","../src/lint.ts","../src/rules/load.ts","../src/rules/taxonomy.ts","../src/extract/index.ts","../src/extract/tsx.ts","../src/map/jev.ts","../src/report/json.ts","../src/report/sarif.ts","../src/report/tty.ts","../src/lib/stats.ts","../src/eval/coverage.ts","../src/eval/metrics.ts","../src/eval/corpus.ts","../src/eval/promotion.ts","../src/eval/tune.ts"],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { fromMarkdown } from "mdast-util-from-markdown";
|
|
|
4
4
|
import { gfmFromMarkdown } from "mdast-util-gfm";
|
|
5
5
|
import { gfm } from "micromark-extension-gfm";
|
|
6
6
|
import { parseSync } from "oxc-parser";
|
|
7
|
+
import os from "node:os";
|
|
7
8
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
8
9
|
import { mdxFromMarkdown } from "mdast-util-mdx";
|
|
9
10
|
import { mdxjs } from "micromark-extension-mdxjs";
|
|
@@ -204,7 +205,10 @@ const DEFAULT_EXCLUDE = [
|
|
|
204
205
|
"**/.next/**",
|
|
205
206
|
"**/.git/**",
|
|
206
207
|
"**/results/**",
|
|
207
|
-
"**/coverage/**"
|
|
208
|
+
"**/coverage/**",
|
|
209
|
+
"**/LICENSE",
|
|
210
|
+
"**/LICENSE.*",
|
|
211
|
+
"**/CHANGELOG.md"
|
|
208
212
|
];
|
|
209
213
|
const globToRegExp = (glob) => {
|
|
210
214
|
let out = "^";
|
|
@@ -413,10 +417,12 @@ function validateConfig(raw) {
|
|
|
413
417
|
"smartQuotesAtBuild",
|
|
414
418
|
"docTypes",
|
|
415
419
|
"components",
|
|
416
|
-
"tailwind"
|
|
420
|
+
"tailwind",
|
|
421
|
+
"rules"
|
|
417
422
|
], "");
|
|
418
423
|
if (value.$schema !== void 0 && typeof value.$schema !== "string") invalid("$schema", "a string");
|
|
419
424
|
if (value.exclude !== void 0) strings(value.exclude, "exclude");
|
|
425
|
+
if (value.rules !== void 0) strings(value.rules, "rules");
|
|
420
426
|
if (value.smartQuotesAtBuild !== void 0 && typeof value.smartQuotesAtBuild !== "boolean") invalid("smartQuotesAtBuild", "a boolean");
|
|
421
427
|
if (value.docTypes !== void 0) {
|
|
422
428
|
if (!Array.isArray(value.docTypes)) invalid("docTypes", "an array");
|
|
@@ -441,6 +447,22 @@ function validateConfig(raw) {
|
|
|
441
447
|
}
|
|
442
448
|
}
|
|
443
449
|
}
|
|
450
|
+
const ruleDir = (root, raw) => {
|
|
451
|
+
const expanded = raw.replace(/^~(?=$|\/)/u, os.homedir()).replaceAll(/\$\{?(\w+)\}?/gu, (_, name) => {
|
|
452
|
+
const value = process.env[name];
|
|
453
|
+
if (!value) throw new InputError("INVALID_CONFIG", `taste-lint.config.json: rules entry ${raw} names ${name}, which is not set.`, {
|
|
454
|
+
field: "rules",
|
|
455
|
+
variable: name
|
|
456
|
+
});
|
|
457
|
+
return value;
|
|
458
|
+
});
|
|
459
|
+
const dir = path.resolve(root, expanded);
|
|
460
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) throw new InputError("INVALID_CONFIG", `taste-lint.config.json: rules entry ${raw} is not a directory (${dir}).`, {
|
|
461
|
+
field: "rules",
|
|
462
|
+
path: dir
|
|
463
|
+
});
|
|
464
|
+
return dir;
|
|
465
|
+
};
|
|
444
466
|
const loadConfig = (root, defaults = []) => {
|
|
445
467
|
const absRoot = path.resolve(root);
|
|
446
468
|
try {
|
|
@@ -475,6 +497,7 @@ const loadConfig = (root, defaults = []) => {
|
|
|
475
497
|
],
|
|
476
498
|
exclude: user.exclude ?? [],
|
|
477
499
|
root: absRoot,
|
|
500
|
+
rules: (user.rules ?? []).map((raw) => ruleDir(absRoot, raw)),
|
|
478
501
|
smartQuotesAtBuild: typeof user.smartQuotesAtBuild === "boolean" ? user.smartQuotesAtBuild : detectSmartQuotesAtBuild(absRoot),
|
|
479
502
|
tailwind: { theme: user.tailwind?.theme ?? {} }
|
|
480
503
|
};
|
|
@@ -2351,11 +2374,54 @@ const smartQuotes = (prose) => prose.replaceAll(OPENING_DOUBLE, "$1“").replace
|
|
|
2351
2374
|
const ellipsis = (prose) => prose.replaceAll("...", "…");
|
|
2352
2375
|
const multiplicationSign = (prose) => prose.replaceAll(/(?<![\w.])([1-9]\d*)\s?x\s?(\d+)(?![\w.%])/gu, "$1×$2");
|
|
2353
2376
|
const nbspValueUnit = (prose) => prose.replaceAll(/(\d) (?=(?:px|pt|em|rem|ms|min|KB|MB|GB|TB|kg|km|cm|mm|fps|%)(?![\p{L}\d]))/gu, "$1\xA0");
|
|
2377
|
+
const stripUtm = (prose) => prose.replaceAll(/\?utm_source=chatgpt\.com(?![\w&=-])/gu, "").replaceAll(/([?&])utm_source=chatgpt\.com&/gu, "$1").replaceAll("&utm_source=chatgpt.com", "");
|
|
2378
|
+
const AU_SPELLING = {
|
|
2379
|
+
analyze: "analyse",
|
|
2380
|
+
analyzed: "analysed",
|
|
2381
|
+
apologize: "apologise",
|
|
2382
|
+
behavior: "behaviour",
|
|
2383
|
+
behaviors: "behaviours",
|
|
2384
|
+
canceled: "cancelled",
|
|
2385
|
+
canceling: "cancelling",
|
|
2386
|
+
catalog: "catalogue",
|
|
2387
|
+
color: "colour",
|
|
2388
|
+
colored: "coloured",
|
|
2389
|
+
colors: "colours",
|
|
2390
|
+
customize: "customise",
|
|
2391
|
+
customized: "customised",
|
|
2392
|
+
favorite: "favourite",
|
|
2393
|
+
favorites: "favourites",
|
|
2394
|
+
gray: "grey",
|
|
2395
|
+
honor: "honour",
|
|
2396
|
+
labeled: "labelled",
|
|
2397
|
+
labeling: "labelling",
|
|
2398
|
+
modeling: "modelling",
|
|
2399
|
+
neighbor: "neighbour",
|
|
2400
|
+
optimize: "optimise",
|
|
2401
|
+
optimized: "optimised",
|
|
2402
|
+
organization: "organisation",
|
|
2403
|
+
organizations: "organisations",
|
|
2404
|
+
organize: "organise",
|
|
2405
|
+
organized: "organised",
|
|
2406
|
+
prioritize: "prioritise",
|
|
2407
|
+
realize: "realise",
|
|
2408
|
+
realized: "realised",
|
|
2409
|
+
recognize: "recognise",
|
|
2410
|
+
traveling: "travelling"
|
|
2411
|
+
};
|
|
2412
|
+
const AU_SPELLING_PATTERN = String.raw`(?<!\S*/\S*)\b(?:${Object.keys(AU_SPELLING).join("|")})\b(?!\S*/)`;
|
|
2413
|
+
const auSpelling = (prose) => prose.replaceAll(new RegExp(AU_SPELLING_PATTERN, "giu"), (word) => {
|
|
2414
|
+
const au = AU_SPELLING[word.toLowerCase()];
|
|
2415
|
+
if (!au || word !== word.toLowerCase() && word !== `${word[0]}${word.slice(1).toLowerCase()}`) return word;
|
|
2416
|
+
return word[0] === word[0].toUpperCase() ? `${au[0].toUpperCase()}${au.slice(1)}` : au;
|
|
2417
|
+
});
|
|
2354
2418
|
const FIXES = {
|
|
2419
|
+
auSpelling,
|
|
2355
2420
|
ellipsis,
|
|
2356
2421
|
multiplicationSign,
|
|
2357
2422
|
nbspValueUnit,
|
|
2358
|
-
smartQuotes
|
|
2423
|
+
smartQuotes,
|
|
2424
|
+
stripUtm
|
|
2359
2425
|
};
|
|
2360
2426
|
//#endregion
|
|
2361
2427
|
//#region src/rules/validate-sections.ts
|
|
@@ -3316,7 +3382,7 @@ const codeRule = (spec, module) => {
|
|
|
3316
3382
|
//#endregion
|
|
3317
3383
|
//#region src/rules/code/classes.ts
|
|
3318
3384
|
const MODULE$1 = "src/rules/code/classes.ts";
|
|
3319
|
-
const ANIMATION = "skills/ui-animation/references";
|
|
3385
|
+
const ANIMATION$1 = "skills/ui-animation/references";
|
|
3320
3386
|
const bases = (unit) => (unit.classes ?? []).map(baseClass);
|
|
3321
3387
|
const animates = (list) => list.some((c) => c.startsWith("transition") && c !== "transition-none" || c.startsWith("duration-") || c.startsWith("animate-") && c !== "animate-none");
|
|
3322
3388
|
const durationMs = (cls) => {
|
|
@@ -3381,7 +3447,7 @@ const CLASS_RULES = [
|
|
|
3381
3447
|
id: "motion-ease-in-on-transition",
|
|
3382
3448
|
source: {
|
|
3383
3449
|
line: 27,
|
|
3384
|
-
path: `${ANIMATION}/debugging-symptoms.md`
|
|
3450
|
+
path: `${ANIMATION$1}/debugging-symptoms.md`
|
|
3385
3451
|
},
|
|
3386
3452
|
title: "Transition eases in, so it starts slow while the user is watching"
|
|
3387
3453
|
}),
|
|
@@ -3396,7 +3462,7 @@ const CLASS_RULES = [
|
|
|
3396
3462
|
id: "motion-linear-easing-on-transition",
|
|
3397
3463
|
source: {
|
|
3398
3464
|
line: 37,
|
|
3399
|
-
path: `${ANIMATION}/debugging-symptoms.md`
|
|
3465
|
+
path: `${ANIMATION$1}/debugging-symptoms.md`
|
|
3400
3466
|
},
|
|
3401
3467
|
title: "Linear easing on motion that is not a spinner or marquee"
|
|
3402
3468
|
}),
|
|
@@ -3412,7 +3478,7 @@ const CLASS_RULES = [
|
|
|
3412
3478
|
id: "motion-duration-over-300ms",
|
|
3413
3479
|
source: {
|
|
3414
3480
|
line: 29,
|
|
3415
|
-
path: `${ANIMATION}/debugging-symptoms.md`
|
|
3481
|
+
path: `${ANIMATION$1}/debugging-symptoms.md`
|
|
3416
3482
|
},
|
|
3417
3483
|
title: "Transition longer than 300ms on product UI"
|
|
3418
3484
|
}),
|
|
@@ -3423,7 +3489,7 @@ const CLASS_RULES = [
|
|
|
3423
3489
|
id: "motion-transition-all",
|
|
3424
3490
|
source: {
|
|
3425
3491
|
line: 58,
|
|
3426
|
-
path: `${ANIMATION}/review-format.md`
|
|
3492
|
+
path: `${ANIMATION$1}/review-format.md`
|
|
3427
3493
|
},
|
|
3428
3494
|
title: "transition-all animates every property, layout included"
|
|
3429
3495
|
}),
|
|
@@ -3438,7 +3504,7 @@ const CLASS_RULES = [
|
|
|
3438
3504
|
id: "motion-scale-from-zero",
|
|
3439
3505
|
source: {
|
|
3440
3506
|
line: 46,
|
|
3441
|
-
path: `${ANIMATION}/debugging-symptoms.md`
|
|
3507
|
+
path: `${ANIMATION$1}/debugging-symptoms.md`
|
|
3442
3508
|
},
|
|
3443
3509
|
title: "Entrance from scale zero"
|
|
3444
3510
|
}),
|
|
@@ -3454,7 +3520,7 @@ const CLASS_RULES = [
|
|
|
3454
3520
|
id: "motion-animate-without-reduced-motion",
|
|
3455
3521
|
source: {
|
|
3456
3522
|
line: 57,
|
|
3457
|
-
path: `${ANIMATION}/live-tuning.md`
|
|
3523
|
+
path: `${ANIMATION$1}/live-tuning.md`
|
|
3458
3524
|
},
|
|
3459
3525
|
status: "review-only",
|
|
3460
3526
|
title: "Animation has no local reduced-motion guard"
|
|
@@ -3523,7 +3589,7 @@ const document = (unit) => {
|
|
|
3523
3589
|
return unit.facts.document;
|
|
3524
3590
|
};
|
|
3525
3591
|
const nodes = (unit) => descendants(document(unit));
|
|
3526
|
-
const make$1 = (name, title, source, include, check, hint, authoring = false) => codeRule({
|
|
3592
|
+
const make$1 = (name, title, source, include, check, hint, authoring = false, status = "review-only") => codeRule({
|
|
3527
3593
|
categoryId: authoring ? "instruction-quality" : "reference-reading",
|
|
3528
3594
|
check,
|
|
3529
3595
|
hint,
|
|
@@ -3534,7 +3600,7 @@ const make$1 = (name, title, source, include, check, hint, authoring = false) =>
|
|
|
3534
3600
|
path: `skills/${source}`,
|
|
3535
3601
|
repo: "mblode/agent-skills"
|
|
3536
3602
|
},
|
|
3537
|
-
status
|
|
3603
|
+
status,
|
|
3538
3604
|
title,
|
|
3539
3605
|
unit: ["source"]
|
|
3540
3606
|
}, "src/rules/code/documents.ts");
|
|
@@ -3545,7 +3611,7 @@ const INSTRUCTIONS = [
|
|
|
3545
3611
|
"**/SKILL.md"
|
|
3546
3612
|
];
|
|
3547
3613
|
const DOCS = ["**/*.md"];
|
|
3548
|
-
const README_SOURCE = "
|
|
3614
|
+
const README_SOURCE = "ghostwriter/references/readme.md";
|
|
3549
3615
|
const DOCUMENT_RULES = [
|
|
3550
3616
|
make$1("readme-scaffold", "README still contains scaffold instructions", README_SOURCE, README, (u) => {
|
|
3551
3617
|
const n = nodes(u).find((candidate) => candidate.type === "paragraph" && /This is a \[?Next\.js.*bootstrapped with|This template provides a minimal setup to get React working in Vite/i.test(nodeText(candidate)));
|
|
@@ -3568,7 +3634,7 @@ const DOCUMENT_RULES = [
|
|
|
3568
3634
|
}
|
|
3569
3635
|
return none;
|
|
3570
3636
|
}, "Provide a complete first-run example; move intentionally partial examples to a tutorial."),
|
|
3571
|
-
make$1("document-empty-heading", "A section has no content", "
|
|
3637
|
+
make$1("document-empty-heading", "A section has no content", "ghostwriter/references/docs.md", DOCS, (u) => {
|
|
3572
3638
|
const children = document(u).children ?? [];
|
|
3573
3639
|
for (const [i, n] of children.entries()) {
|
|
3574
3640
|
if (n.type !== "heading") continue;
|
|
@@ -3577,7 +3643,7 @@ const DOCUMENT_RULES = [
|
|
|
3577
3643
|
}
|
|
3578
3644
|
return none;
|
|
3579
3645
|
}, "Add the section's content or remove its heading."),
|
|
3580
|
-
make$1("document-heading-order", "Heading skips a structural level", "
|
|
3646
|
+
make$1("document-heading-order", "Heading skips a structural level", "ghostwriter/references/docs.md", DOCS, (u) => {
|
|
3581
3647
|
let previous = 0;
|
|
3582
3648
|
for (const n of nodes(u).filter((candidate) => candidate.type === "heading")) {
|
|
3583
3649
|
const depth = n.depth ?? 1;
|
|
@@ -3586,7 +3652,11 @@ const DOCUMENT_RULES = [
|
|
|
3586
3652
|
}
|
|
3587
3653
|
return none;
|
|
3588
3654
|
}, "Nest heading levels in order so the document outline reflects its structure."),
|
|
3589
|
-
make$1("document-
|
|
3655
|
+
make$1("document-code-fence-language", "Code fence has no language tag", "ghostwriter/references/docs.md", DOCS, (u) => {
|
|
3656
|
+
const n = nodes(u).find((candidate) => candidate.type === "code" && !candidate.lang);
|
|
3657
|
+
return n ? at(n, "Fence without a language tag") : none;
|
|
3658
|
+
}, "Tag the fence: ```bash, ```ts, or ```text for output. The tag drives highlighting and tells the reader what to paste where."),
|
|
3659
|
+
make$1("document-broken-local-link", "Document links to a missing local file", "ghostwriter/references/docs.md", DOCS, (u) => {
|
|
3590
3660
|
const repo = u.facts.repository;
|
|
3591
3661
|
for (const n of nodes(u).filter((candidate) => [
|
|
3592
3662
|
"link",
|
|
@@ -3607,7 +3677,7 @@ const DOCUMENT_RULES = [
|
|
|
3607
3677
|
if (!repo.exists(resolved)) return at(n, `Missing local target: ${target}`);
|
|
3608
3678
|
}
|
|
3609
3679
|
return none;
|
|
3610
|
-
}, "Correct the link or provide the referenced file. Generated site routes need a route-aware check."),
|
|
3680
|
+
}, "Correct the link or provide the referenced file. Generated site routes need a route-aware check.", false, "active"),
|
|
3611
3681
|
make$1("instruction-missing-script", "Instruction invokes an undefined package script", "agents-md/references/quick-checklist.md", INSTRUCTIONS, (u) => {
|
|
3612
3682
|
const manifest = u.facts.repository.manifest(u.file);
|
|
3613
3683
|
if (!manifest) throw new UnresolvedError("No readable package manifest for this instruction file");
|
|
@@ -3676,6 +3746,7 @@ const IMAGE_RULES = [{
|
|
|
3676
3746
|
id: "interaction-a11y-image-alt-text",
|
|
3677
3747
|
required: ["alt"],
|
|
3678
3748
|
source: "a11y-image-alt-text",
|
|
3749
|
+
status: "active",
|
|
3679
3750
|
title: "Image is missing an alt attribute"
|
|
3680
3751
|
}, {
|
|
3681
3752
|
categoryId: "resilience",
|
|
@@ -3683,6 +3754,7 @@ const IMAGE_RULES = [{
|
|
|
3683
3754
|
id: "craft-image-dimensions-and-priority",
|
|
3684
3755
|
required: ["width", "height"],
|
|
3685
3756
|
source: "perf-image-dimensions-and-priority",
|
|
3757
|
+
status: "review-only",
|
|
3686
3758
|
title: "Image has no declared dimensions or style"
|
|
3687
3759
|
}].map((spec) => codeRule({
|
|
3688
3760
|
categoryId: spec.categoryId,
|
|
@@ -3708,11 +3780,62 @@ const IMAGE_RULES = [{
|
|
|
3708
3780
|
repo: "mblode/agent-skills",
|
|
3709
3781
|
ruleId: spec.source
|
|
3710
3782
|
},
|
|
3711
|
-
status:
|
|
3783
|
+
status: spec.status,
|
|
3712
3784
|
title: spec.title,
|
|
3713
3785
|
unit: ["source"]
|
|
3714
3786
|
}, "src/rules/code/images.ts"));
|
|
3715
3787
|
//#endregion
|
|
3788
|
+
//#region src/rules/code/motion-source.ts
|
|
3789
|
+
const ANIMATION = "skills/ui-animation/SKILL.md";
|
|
3790
|
+
const stripComments = (text) => text.replaceAll(/\/\*[\s\S]*?\*\/|(?<![:"'\w])\/\/[^\n]*/gu, (m) => m.replaceAll(/[^\n]/gu, " "));
|
|
3791
|
+
const first = (unit, re, evidence) => {
|
|
3792
|
+
const m = re.exec(stripComments(unit.text));
|
|
3793
|
+
return m ? {
|
|
3794
|
+
evidence,
|
|
3795
|
+
fired: true,
|
|
3796
|
+
offset: m.index
|
|
3797
|
+
} : none;
|
|
3798
|
+
};
|
|
3799
|
+
const MOTION_SOURCE_RULES = [codeRule({
|
|
3800
|
+
categoryId: "motion-restraint",
|
|
3801
|
+
check: (unit) => first(unit, /(?:^|[{;\s])transition(?:-property)?\s*:\s*["'`]?all\b/mu, "transition: all"),
|
|
3802
|
+
hint: "List the properties: transition: transform 200ms, opacity 200ms. `all` animates layout and silently adopts properties added later.",
|
|
3803
|
+
id: "motion-css-transition-all",
|
|
3804
|
+
scope: { include: [
|
|
3805
|
+
"**/*.css",
|
|
3806
|
+
"**/*.scss",
|
|
3807
|
+
"**/*.tsx",
|
|
3808
|
+
"**/*.jsx"
|
|
3809
|
+
] },
|
|
3810
|
+
source: {
|
|
3811
|
+
line: 67,
|
|
3812
|
+
path: ANIMATION,
|
|
3813
|
+
repo: "mblode/agent-skills"
|
|
3814
|
+
},
|
|
3815
|
+
title: "transition: all animates every property, layout included",
|
|
3816
|
+
unit: ["source"]
|
|
3817
|
+
}, "src/rules/code/motion-source.ts"), codeRule({
|
|
3818
|
+
categoryId: "motion-restraint",
|
|
3819
|
+
check: (unit) => first(unit, /\bfrom\s*["']framer-motion(?:\/[^"']*)?["']|\b(?:import|require)\s*\(\s*["']framer-motion["']/u, "imports framer-motion"),
|
|
3820
|
+
hint: "Install `motion` and import from \"motion/react\". A mixed codebase compiles but ships two copies of the library.",
|
|
3821
|
+
id: "motion-framer-motion-import",
|
|
3822
|
+
scope: { include: [
|
|
3823
|
+
"**/*.tsx",
|
|
3824
|
+
"**/*.jsx",
|
|
3825
|
+
"**/*.ts",
|
|
3826
|
+
"**/*.js",
|
|
3827
|
+
"**/*.mjs",
|
|
3828
|
+
"**/*.mts"
|
|
3829
|
+
] },
|
|
3830
|
+
source: {
|
|
3831
|
+
line: 153,
|
|
3832
|
+
path: ANIMATION,
|
|
3833
|
+
repo: "mblode/agent-skills"
|
|
3834
|
+
},
|
|
3835
|
+
title: "Imports framer-motion instead of motion/react",
|
|
3836
|
+
unit: ["source"]
|
|
3837
|
+
}, "src/rules/code/motion-source.ts")];
|
|
3838
|
+
//#endregion
|
|
3716
3839
|
//#region src/rules/code/skill-classes.ts
|
|
3717
3840
|
const make = (id, title, categoryId, source, check, hint) => codeRule({
|
|
3718
3841
|
categoryId,
|
|
@@ -3972,7 +4095,7 @@ const CODE_RULES = [
|
|
|
3972
4095
|
rule({
|
|
3973
4096
|
categoryId: "machine-prose",
|
|
3974
4097
|
check: (unit) => {
|
|
3975
|
-
const long = unit.text.split(/(?<=[.!?])\s+/u).map((s) => [s, countWords(s)]).filter(([, n]) => n > 25);
|
|
4098
|
+
const long = unit.text.split(/(?<=[.!?])\s+/u).map((s) => [s, countWords(s)]).filter(([s, n]) => n > 25 && (s.match(/\b(?:and|but)\b/giu)?.length ?? 0) >= 2);
|
|
3976
4099
|
if (long.length === 0) return none;
|
|
3977
4100
|
const [sentence, n] = long[0];
|
|
3978
4101
|
return hit(`${n} words: "${sentence.slice(0, 80)}${sentence.length > 80 ? "..." : ""}"`);
|
|
@@ -3994,18 +4117,19 @@ const CODE_RULES = [
|
|
|
3994
4117
|
notInCode: true
|
|
3995
4118
|
},
|
|
3996
4119
|
source: {
|
|
3997
|
-
line:
|
|
3998
|
-
path: "skills/
|
|
4120
|
+
line: 17,
|
|
4121
|
+
path: "skills/ghostwriter/references/docs.md",
|
|
3999
4122
|
repo: "mblode/agent-skills"
|
|
4000
4123
|
},
|
|
4001
|
-
title: "Sentence over 25 words",
|
|
4124
|
+
title: "Sentence over 25 words joins several clauses",
|
|
4002
4125
|
unit: ["paragraph", "jsx-text"]
|
|
4003
4126
|
})
|
|
4004
4127
|
],
|
|
4005
4128
|
...CLASS_RULES,
|
|
4006
4129
|
...DOCUMENT_RULES,
|
|
4007
4130
|
...IMAGE_RULES,
|
|
4008
|
-
...SKILL_CLASS_RULES
|
|
4131
|
+
...SKILL_CLASS_RULES,
|
|
4132
|
+
...MOTION_SOURCE_RULES
|
|
4009
4133
|
];
|
|
4010
4134
|
//#endregion
|
|
4011
4135
|
//#region src/rules/load.ts
|
|
@@ -4068,10 +4192,8 @@ const readTuning = (rulesDir) => {
|
|
|
4068
4192
|
}
|
|
4069
4193
|
return out;
|
|
4070
4194
|
};
|
|
4071
|
-
const
|
|
4072
|
-
const tuning = readTuning(rulesDir);
|
|
4195
|
+
const readYamlRules = (rulesDir) => {
|
|
4073
4196
|
const rules = [];
|
|
4074
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4075
4197
|
const domains = fs.readdirSync(rulesDir, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name !== "schema").map((d) => d.name).toSorted();
|
|
4076
4198
|
for (const domain of domains) {
|
|
4077
4199
|
const dir = path.join(rulesDir, domain);
|
|
@@ -4079,24 +4201,49 @@ const loadRules = (rulesDir, options = {}) => {
|
|
|
4079
4201
|
if (!name.endsWith(".yaml")) continue;
|
|
4080
4202
|
const file = path.join(dir, name);
|
|
4081
4203
|
const expectedId = name.slice(0, -5);
|
|
4082
|
-
let raw;
|
|
4083
4204
|
try {
|
|
4084
|
-
raw
|
|
4205
|
+
let raw;
|
|
4206
|
+
try {
|
|
4207
|
+
raw = parse(fs.readFileSync(file, "utf-8"));
|
|
4208
|
+
} catch (error) {
|
|
4209
|
+
throw new Error(`Invalid rule ${file}: YAML did not parse (${error.message})`, { cause: error });
|
|
4210
|
+
}
|
|
4211
|
+
const rule = validateRule(raw, file, expectedId);
|
|
4212
|
+
if (rule.domain !== domain) throw new Error(`Invalid rule ${file}: domain ${rule.domain} does not match folder ${domain}`);
|
|
4213
|
+
rules.push(rule);
|
|
4085
4214
|
} catch (error) {
|
|
4086
|
-
|
|
4215
|
+
const invalid = new InputError("INVALID_RULE", error.message, { file });
|
|
4216
|
+
invalid.cause = error;
|
|
4217
|
+
throw invalid;
|
|
4087
4218
|
}
|
|
4088
|
-
const rule = validateRule(raw, file, expectedId);
|
|
4089
|
-
if (rule.domain !== domain) throw new Error(`Invalid rule ${file}: domain ${rule.domain} does not match folder ${domain}`);
|
|
4090
|
-
rules.push(rule);
|
|
4091
4219
|
}
|
|
4092
4220
|
}
|
|
4221
|
+
return rules;
|
|
4222
|
+
};
|
|
4223
|
+
const loadRules = (rulesDir, options = {}) => {
|
|
4224
|
+
const tuning = readTuning(rulesDir);
|
|
4225
|
+
const rules = [];
|
|
4226
|
+
const seen = /* @__PURE__ */ new Map();
|
|
4227
|
+
rules.push(...readYamlRules(rulesDir));
|
|
4228
|
+
for (const dir of options.extraDirs ?? []) {
|
|
4229
|
+
const extra = readYamlRules(dir);
|
|
4230
|
+
if (extra.length === 0) throw new InputError("INVALID_CONFIG", `rules entry ${dir} holds no rules; put YAML rule files in a domain folder such as ${path.join(dir, "copywriting")}/`, {
|
|
4231
|
+
field: "rules",
|
|
4232
|
+
path: dir
|
|
4233
|
+
});
|
|
4234
|
+
rules.push(...extra);
|
|
4235
|
+
}
|
|
4093
4236
|
for (const code of CODE_RULES) rules.push({
|
|
4094
4237
|
...code,
|
|
4095
4238
|
thresholds: { ...code.thresholds }
|
|
4096
4239
|
});
|
|
4097
4240
|
for (const rule of rules) {
|
|
4098
|
-
|
|
4099
|
-
|
|
4241
|
+
const first = seen.get(rule.id);
|
|
4242
|
+
if (first !== void 0) throw new InputError("DUPLICATE_RULE", `Rule id ${rule.id} is defined in ${first} and ${rule.file}; rename one.`, {
|
|
4243
|
+
files: [first, rule.file],
|
|
4244
|
+
id: rule.id
|
|
4245
|
+
});
|
|
4246
|
+
seen.set(rule.id, rule.file);
|
|
4100
4247
|
const overlay = tuning[rule.id];
|
|
4101
4248
|
if (isCandidateRule(rule) && (rule.status === "active" || overlay?.status === "active")) throw new Error(`Candidate-only rule ${rule.id} cannot be active; add an evidence-based check before promotion.`);
|
|
4102
4249
|
if (overlay) {
|
|
@@ -4272,7 +4419,10 @@ const runLint = async (options = {}, ctx = {}) => {
|
|
|
4272
4419
|
const config = loadConfig(options.root ?? process.cwd());
|
|
4273
4420
|
const rulesDir = resolveRulesDir(options.rulesDir);
|
|
4274
4421
|
const profile = options.profile ? profileFor(options.profile) : void 0;
|
|
4275
|
-
const loaded = loadRules(rulesDir, {
|
|
4422
|
+
const loaded = loadRules(rulesDir, {
|
|
4423
|
+
extraDirs: config.rules,
|
|
4424
|
+
only: options.only
|
|
4425
|
+
});
|
|
4276
4426
|
const rules = profile && !options.only?.length ? profileRules(profile, loaded) : loaded;
|
|
4277
4427
|
if (rules.length === 0) throw new InputError("EMPTY_RULE_SELECTION", "No rules match the selected profile and ids.");
|
|
4278
4428
|
const model = options.model ?? "jev-latest";
|