taste-lint 0.0.6 → 0.0.7

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 CHANGED
@@ -19,20 +19,20 @@ Scan your project with [Jev by TypeSafe AI](https://docs.typesafe.ai/introductio
19
19
  npx taste-lint@latest init
20
20
  ```
21
21
 
22
- Requires Node 24.11 or later. Run from your project directory to install locally and add a scan script.
22
+ Requires Node 24.11 or later. Run it from your project directory. Init installs locally and adds a scan script.
23
23
 
24
24
  ## Quickstart
25
25
 
26
- Create a [Vercel AI Gateway key](https://vercel.com/docs/ai-gateway/authentication-and-byok/api-keys). From your project directory, set your key and run:
26
+ Create a [Vercel AI Gateway key](https://vercel.com/docs/ai-gateway/authentication-and-byok/api-keys). Then:
27
27
 
28
28
  ```bash
29
29
  export AI_GATEWAY_API_KEY="your-vercel-ai-gateway-key"
30
30
  npm run taste
31
31
  ```
32
32
 
33
- No account or config for taste-lint. AI checks send selected text and rule context to Vercel AI Gateway, billed to your account. Answers are cached for repeat runs.
33
+ No taste-lint account or config. AI checks send selected text and rule context to Vercel AI Gateway, billed to your account. Repeat runs reuse cached answers.
34
34
 
35
- Use your package manager in place of npm. Add `--agent` to init for agent instructions, or `--dry-run` to preview setup.
35
+ Use your package manager in place of npm. Pass `--agent` to init for agent instructions, or `--dry-run` to preview setup.
36
36
 
37
37
  ## What it checks
38
38
 
@@ -40,17 +40,13 @@ Use your package manager in place of npm. Add `--agent` to init for agent instru
40
40
  - **Writing:** Markdown, MDX, and READMEs with `--profile writing`.
41
41
  - **Agent instructions:** AGENTS.md and skills with `--profile instructions`.
42
42
 
43
- Rules draw on [Agent Skills](https://github.com/mblode/agent-skills) and [Taste Training](https://blode.co/taste-training). Local checks handle measurable rules. Jev judges meaning and returns probabilities. Uncalibrated AI rules stay advisory; active findings can fail a run.
43
+ Rules come from [Agent Skills](https://github.com/mblode/agent-skills) and [Taste Training](https://blode.co/taste-training). Local checks handle measurable rules. Jev judges meaning and returns probabilities. Uncalibrated AI rules stay advisory. Active findings can fail a run.
44
44
 
45
- ## Useful options
45
+ ## Docs
46
46
 
47
- | Option | What it does |
48
- | ---------------- | ---------------------------------------------------- |
49
- | `--dry-run` | Preview scope and estimated cost without model calls |
50
- | `--output json` | Save findings for scripts and agents |
51
- | `--output sarif` | Export findings for code review tools |
47
+ [taste-lint.blode.md](https://taste-lint.blode.md)
52
48
 
53
- Run `taste-lint scan --help` for all options. See the [scan guide](https://github.com/mblode/taste-lint/blob/main/docs/SCANS.md), [usage reference](https://github.com/mblode/taste-lint/blob/main/docs/USAGE.md), and [changelog](https://github.com/mblode/taste-lint/blob/main/CHANGELOG.md) for more.
49
+ `taste-lint scan --help` lists every option. `--dry-run` previews scope and cost. `--output json` and `--output sarif` are for scripts and code review.
54
50
 
55
51
  ## License
56
52
 
package/dist/cli.js CHANGED
@@ -16,7 +16,7 @@ import { mdxjs } from "micromark-extension-mdxjs";
16
16
  import { styleText } from "node:util";
17
17
  import os from "node:os";
18
18
  //#region package.json
19
- var version = "0.0.6";
19
+ var version = "0.0.7";
20
20
  //#endregion
21
21
  //#region src/lib/stats.ts
22
22
  const makePRNG = (seed) => {
@@ -444,6 +444,7 @@ const loadCorpus = (corpusDir, rules, options = {}) => {
444
444
  if (r.context !== void 0) {
445
445
  if (!isRecord$1(r.context)) throw new Error(`Invalid corpus line ${where}: context must be an object`);
446
446
  const { docType, role } = r.context;
447
+ if (r.context.fontScale !== void 0 && (!isRecord$1(r.context.fontScale) || Object.values(r.context.fontScale).some((value) => typeof value !== "string"))) throw new Error(`Invalid corpus line ${where}: fontScale must map tokens to strings`);
447
448
  if (docType !== void 0 && !DOC_TYPES.includes(docType) || role !== void 0 && !ROLES.includes(role)) throw new Error(`Invalid corpus line ${where}: unknown context docType or role`);
448
449
  }
449
450
  if (r.neighbours !== void 0) {
@@ -784,6 +785,51 @@ const resolveLeading = (out, token, cls) => {
784
785
  out.unresolved.push(cls);
785
786
  };
786
787
  const splitClasses = (value) => value.split(/\s+/u).filter(Boolean);
788
+ /** Only explicit pixel font tokens establish a scale for this comparison.
789
+ * Relative lengths need a rendered root size; other families need their own
790
+ * theme evidence. Neither is inferred from Tailwind's default typography.
791
+ */
792
+ const compareScale = (classes, theme = {}) => {
793
+ const matches = [];
794
+ const unresolved = [];
795
+ for (const className of classes) {
796
+ const { base, variants } = stripVariants(className);
797
+ const candidate = base.match(/^(text|p[xytrblse]?|m[xytrblse]?|gap(?:-[xy])?|space-[xy]|rounded(?:-[a-z]+)?|leading|w|h|size|inset|top|bottom|left|right)-\[(-?(?:\d+(?:\.\d+)?|\.\d+))px\]$/u);
798
+ if (!candidate || Math.abs(Number(candidate[2])) <= 2) continue;
799
+ if (candidate[1] !== "text") {
800
+ unresolved.push(`${className}: ${candidate[1]} scale family is unsupported`);
801
+ continue;
802
+ }
803
+ const entries = Object.entries(theme);
804
+ if (!entries.length) {
805
+ unresolved.push(`${className}: no explicitly declared font scale`);
806
+ continue;
807
+ }
808
+ const value = Number(candidate[2]);
809
+ const resolved = entries.flatMap(([name, raw]) => {
810
+ if (!/^[\w-]+$/u.test(name) || !/^(?:\d+(?:\.\d+)?|\.\d+)px$/u.test(raw)) return [];
811
+ const stepPx = Number(raw.slice(0, -2));
812
+ return Number.isFinite(stepPx) && stepPx > 0 ? [{
813
+ difference: Math.abs(value - stepPx),
814
+ name,
815
+ stepPx
816
+ }] : [];
817
+ });
818
+ const nearest = resolved.toSorted((a, b) => a.difference - b.difference || a.name.localeCompare(b.name))[0];
819
+ const tolerance = Number.EPSILON * Math.max(1, Math.abs(value), nearest?.stepPx ?? 0);
820
+ if (nearest && nearest.difference <= 1 + tolerance) matches.push({
821
+ className,
822
+ difference: Math.min(nearest.difference, 1),
823
+ stepPx: nearest.stepPx,
824
+ token: [...variants, `text-${nearest.name}`].join(":")
825
+ });
826
+ else if (resolved.length < entries.length) unresolved.push(`${className}: declared font scale contains unresolved values`);
827
+ }
828
+ return {
829
+ matches,
830
+ unresolved
831
+ };
832
+ };
787
833
  //#endregion
788
834
  //#region src/lib/errors.ts
789
835
  var InputError = class extends Error {
@@ -2573,6 +2619,29 @@ const shadcn = (spec) => rule$1({
2573
2619
  unit: ["class-list"]
2574
2620
  });
2575
2621
  const CLASS_RULES = [
2622
+ rule$1({
2623
+ categoryId: "look-constraints",
2624
+ check: (unit) => {
2625
+ const compared = compareScale(unit.classes ?? [], unit.context.fontScale);
2626
+ if (!compared.matches.length) {
2627
+ if (compared.unresolved.length) throw new UnresolvedError(compared.unresolved.join("; "));
2628
+ return none;
2629
+ }
2630
+ if (unit.context.dynamic) throw new UnresolvedError("Dynamic classes may override the near-scale candidate");
2631
+ return hit(compared.matches.map(({ className, token, stepPx, difference }) => `${className} is ${difference}px from ${token} (${stepPx}px)`).join("; "));
2632
+ },
2633
+ hint: "Review the matching font token before changing this value. If the difference is intentional or recurring, name the existing size in the theme instead of changing its appearance. This comparison does not establish design intent.",
2634
+ id: "craft-near-duplicate-scale",
2635
+ source: {
2636
+ line: 10,
2637
+ path: "skills/ui-design/rules/slop-near-duplicate-scale.md",
2638
+ repo: "mblode/agent-skills",
2639
+ ruleId: "slop-near-duplicate-scale"
2640
+ },
2641
+ status: "review-only",
2642
+ title: "Arbitrary font size within 1px of a declared theme step",
2643
+ unit: ["class-list"]
2644
+ }),
2576
2645
  motion({
2577
2646
  categoryId: "easing-and-duration",
2578
2647
  check: (unit) => {
@@ -4475,6 +4544,7 @@ const extractTsx = (file, source, options) => {
4475
4544
  docType,
4476
4545
  dynamic: classInfo.dynamic || void 0,
4477
4546
  element: name,
4547
+ fontScale: config?.tailwind.theme,
4478
4548
  interpolated: classInfo.interpolated || void 0,
4479
4549
  role
4480
4550
  },
@@ -4673,6 +4743,7 @@ const agentText = `${marker}
4673
4743
  Taste Lint uses Jev to judge copy and UI. Preview the taste script with --dry-run, then run it with a user-supplied AI_GATEWAY_API_KEY.
4674
4744
  Fix act findings, review advisory findings in context, and recheck the edited files.
4675
4745
  Never invent a key or treat unknown checks as passes.
4746
+ Docs: https://taste-lint.blode.md
4676
4747
  <!-- /taste-lint -->
4677
4748
  `;
4678
4749
  function object$1(value) {
@@ -5486,7 +5557,7 @@ const renderSarif = (result, rules, version) => {
5486
5557
  ruleIndex: index.get(f.ruleId)
5487
5558
  })),
5488
5559
  tool: { driver: {
5489
- informationUri: "https://github.com/mblode/taste-lint",
5560
+ informationUri: "https://taste-lint.blode.md",
5490
5561
  name: "taste-lint",
5491
5562
  rules: ruleList.map((r) => ({
5492
5563
  fullDescription: { text: r.question?.instructions ?? r.fix.hint },
@@ -6652,6 +6723,7 @@ Quickstart:
6652
6723
 
6653
6724
  Preview: taste-lint scan . --dry-run
6654
6725
  Get a key: https://vercel.com/docs/ai-gateway/authentication-and-byok/api-keys
6726
+ Docs: https://taste-lint.blode.md
6655
6727
  `);
6656
6728
  registerLintCommand(program);
6657
6729
  registerInitCommand(program);