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.
Files changed (30) hide show
  1. package/README.md +5 -3
  2. package/data/config.schema.json +7 -0
  3. package/data/rules/copywriting/copywriting-anthropomorphism.yaml +4 -3
  4. package/data/rules/copywriting/copywriting-antithesis-frame.yaml +21 -0
  5. package/data/rules/copywriting/copywriting-click-here-links.yaml +4 -3
  6. package/data/rules/copywriting/copywriting-conditions-first.yaml +2 -2
  7. package/data/rules/copywriting/copywriting-copula-avoidance.yaml +21 -0
  8. package/data/rules/copywriting/copywriting-error-descriptions.yaml +2 -2
  9. package/data/rules/copywriting/copywriting-filler-words.yaml +4 -3
  10. package/data/rules/copywriting/copywriting-latinisms.yaml +6 -4
  11. package/data/rules/copywriting/copywriting-nominalisations.yaml +4 -3
  12. package/data/rules/copywriting/copywriting-participle-tail.yaml +21 -0
  13. package/data/rules/copywriting/copywriting-passive-voice.yaml +4 -3
  14. package/data/rules/copywriting/copywriting-promotional-docs-voice.yaml +4 -3
  15. package/data/rules/copywriting/copywriting-requirements-language.yaml +2 -2
  16. package/data/rules/copywriting/copywriting-searchable-headings.yaml +2 -2
  17. package/data/rules/copywriting/copywriting-tee-up.yaml +21 -0
  18. package/data/rules/copywriting/copywriting-temporal-words.yaml +4 -3
  19. package/data/rules/craft/craft-atmosphere-gradient.yaml +28 -0
  20. package/data/rules/craft/craft-default-indigo.yaml +28 -0
  21. package/data/rules/motion/motion-will-change-misuse.yaml +28 -0
  22. package/data/rules/product/product-nested-modals.yaml +1 -1
  23. package/data/rules/product/product-select-for-few-options.yaml +1 -1
  24. package/dist/cli.js +186 -37
  25. package/dist/cli.js.map +1 -1
  26. package/dist/index.d.ts +4 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +185 -35
  29. package/dist/index.js.map +1 -1
  30. 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
@@ -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":"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"}
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: "review-only",
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 = "readme-creator/references/quality-checklist.md";
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", "docs-writing/rules/structure-no-filler-sections.md", DOCS, (u) => {
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", "docs-writing/rules/format-semantic-html.md", DOCS, (u) => {
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-broken-local-link", "Document links to a missing local file", "docs-writing/rules/review-verify-links.md", DOCS, (u) => {
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: "review-only",
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: 7,
3998
- path: "skills/docs-writing/rules/clarity-one-idea-per-sentence.md",
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 loadRules = (rulesDir, options = {}) => {
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 = parse(fs.readFileSync(file, "utf-8"));
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
- throw new Error(`Invalid rule ${file}: YAML did not parse (${error.message})`, { cause: error });
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
- if (seen.has(rule.id)) throw new Error(`Duplicate rule id ${rule.id}`);
4099
- seen.add(rule.id);
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, { only: options.only });
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";