taste-lint 0.0.4 → 0.0.5

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
@@ -4,7 +4,7 @@
4
4
 
5
5
  **Catch AI slop before you ship.**
6
6
 
7
- Scan your project with local checks and [Jev by TypeSafe AI](https://docs.typesafe.ai/introduction).
7
+ Scan your project with [Jev by TypeSafe AI](https://docs.typesafe.ai/introduction).
8
8
 
9
9
  <p align="center">
10
10
  <a href="https://www.npmjs.com/package/taste-lint"><img alt="npm version" src="https://img.shields.io/npm/v/taste-lint?style=flat&colorA=000000&colorB=000000" /></a>
@@ -19,7 +19,7 @@ Scan your project with local checks and [Jev by TypeSafe AI](https://docs.typesa
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 check scripts.
22
+ Requires Node 24.11 or later. Run from your project directory to install locally and add a scan script.
23
23
 
24
24
  ## Quickstart
25
25
 
@@ -32,7 +32,7 @@ npm run taste
32
32
 
33
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.
34
34
 
35
- Run `npm run check:taste` for local checks without a key. 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. Add `--agent` to init for agent instructions, or `--dry-run` to preview setup.
36
36
 
37
37
  ## What it checks
38
38
 
@@ -44,12 +44,11 @@ Rules draw on [Agent Skills](https://github.com/mblode/agent-skills) and [Taste
44
44
 
45
45
  ## Useful options
46
46
 
47
- | Option | What it does |
48
- | ------------------- | ---------------------------------------------------- |
49
- | `--dry-run` | Preview scope and estimated cost without model calls |
50
- | `--mechanical-only` | Run local checks without an API key |
51
- | `--output json` | Save findings for scripts and agents |
52
- | `--output sarif` | Export findings for code review tools |
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 |
53
52
 
54
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.
55
54
 
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.4";
19
+ var version = "0.0.5";
20
20
  //#endregion
21
21
  //#region src/lib/stats.ts
22
22
  const makePRNG = (seed) => {
@@ -1211,7 +1211,7 @@ const GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh/v4/ai";
1211
1211
  const DEFAULT_MODEL = "jev-latest";
1212
1212
  /** The gateway's id for Jev; a `--model` with a slash in it is passed through. */
1213
1213
  const GATEWAY_MODEL = "typesafe-ai/jev";
1214
- const KEY_HINT = "Set AI_GATEWAY_API_KEY to your Vercel AI Gateway key, then rerun. Create a key at https://vercel.com/docs/ai-gateway/authentication-and-byok/api-keys. Use --mechanical-only for lint/scan without a key.";
1214
+ const KEY_HINT = "Set AI_GATEWAY_API_KEY to your Vercel AI Gateway key, then rerun. Create a key at https://vercel.com/docs/ai-gateway/authentication-and-byok/api-keys.";
1215
1215
  const MAX_RETRY_AFTER_MS = 3e4;
1216
1216
  var ProviderError = class extends Error {
1217
1217
  category;
@@ -2003,13 +2003,8 @@ const planRequests = (units, rules, config) => {
2003
2003
  //#endregion
2004
2004
  //#region src/map/judge.ts
2005
2005
  const prepareJudgement = (units, rules, options) => {
2006
- const plan = planRequests(units, options.mechanicalOnly ? rules.filter((r) => r.tier === "mechanical") : rules, options.config);
2007
- if (options.mechanicalOnly) for (const unit of units) {
2008
- const skipped = plan.skipped.get(unit.id) ?? {};
2009
- for (const rule of rules) if (rule.tier !== "mechanical") skipped[rule.id] = "mechanical_only";
2010
- plan.skipped.set(unit.id, skipped);
2011
- }
2012
- let jobs = options.mechanicalOnly ? [] : plan.jobs;
2006
+ const plan = planRequests(units, rules, options.config);
2007
+ let jobs = plan.jobs;
2013
2008
  if (options.jobFilter) jobs = jobs.map((job) => options.jobFilter?.(job) ?? null).filter((job) => job !== null);
2014
2009
  const prepared = prepareRequests(jobs, options.cache, options.model);
2015
2010
  const pending = prepared.filter((p) => Object.keys(p.questions).length > 0);
@@ -4571,8 +4566,7 @@ const marker = "<!-- taste-lint -->";
4571
4566
  const agentText = `${marker}
4572
4567
  ## Taste Lint
4573
4568
 
4574
- Run the local check with the project's check:taste script after editing copy or UI.
4575
- For AI checks, preview the taste script with --dry-run, then run it with a user-supplied AI_GATEWAY_API_KEY.
4569
+ 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.
4576
4570
  Fix act findings, review advisory findings in context, and recheck the edited files.
4577
4571
  Never invent a key or treat unknown checks as passes.
4578
4572
  <!-- /taste-lint -->
@@ -4615,22 +4609,33 @@ function initProject(options) {
4615
4609
  ].some((name) => name in dependencies) ? "product" : "writing";
4616
4610
  const scripts = { ...manifest.scripts };
4617
4611
  const added = [];
4618
- for (const [name, command] of Object.entries({
4619
- "check:taste": `taste-lint scan . --profile ${profile} --mechanical-only`,
4620
- taste: `taste-lint scan . --profile ${profile}`
4621
- })) if (!(name in scripts)) {
4612
+ for (const oldProfile of ["product", "writing"]) if (scripts["check:taste"] === `taste-lint scan . --profile ${oldProfile} --mechanical-only`) {
4613
+ scripts["check:taste"] = `taste-lint scan . --profile ${oldProfile}`;
4614
+ added.push("check:taste");
4615
+ }
4616
+ for (const [name, command] of Object.entries({ taste: `taste-lint scan . --profile ${profile}` })) if (!(name in scripts)) {
4622
4617
  scripts[name] = command;
4623
4618
  added.push(name);
4624
4619
  }
4625
4620
  const agentPath = path.join(root, "AGENTS.md");
4626
4621
  const previousAgent = options.agent && fs.existsSync(agentPath) ? fs.readFileSync(agentPath, "utf-8") : "";
4622
+ const legacyAgentText = `${marker}
4623
+ ## Taste Lint
4624
+
4625
+ Run the local check with the project's check:taste script after editing copy or UI.
4626
+ For AI checks, preview the taste script with --dry-run, then run it with a user-supplied AI_GATEWAY_API_KEY.
4627
+ Fix act findings, review advisory findings in context, and recheck the edited files.
4628
+ Never invent a key or treat unknown checks as passes.
4629
+ <!-- /taste-lint -->
4630
+ `;
4631
+ const upgradeAgent = Boolean(options.agent && previousAgent.includes(legacyAgentText));
4627
4632
  const addAgent = Boolean(options.agent && !previousAgent.includes(marker));
4628
4633
  const needsDependency = !("taste-lint" in dependencies);
4629
4634
  const installArgs = pm === "npm" ? ["install", "--save-dev"] : ["add", "--dev"];
4630
4635
  installArgs.push(`taste-lint@^${version}`);
4631
4636
  const shouldInstall = needsDependency && options.install !== false;
4632
4637
  const result = {
4633
- agentAdded: addAgent,
4638
+ agentAdded: addAgent || upgradeAgent,
4634
4639
  dryRun: Boolean(options.dryRun),
4635
4640
  installCommand: shouldInstall ? [pm, ...installArgs] : null,
4636
4641
  packageManager: pm,
@@ -4656,6 +4661,7 @@ function initProject(options) {
4656
4661
  manifest.scripts = scripts;
4657
4662
  fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
4658
4663
  }
4664
+ if (upgradeAgent) fs.writeFileSync(agentPath, previousAgent.replace(legacyAgentText, agentText));
4659
4665
  if (addAgent) fs.writeFileSync(agentPath, `${previousAgent}${previousAgent ? "\n\n" : ""}${agentText}`);
4660
4666
  return result;
4661
4667
  }
@@ -4668,7 +4674,7 @@ function registerInitCommand(program) {
4668
4674
  process.stdout.write(`Scripts added: ${result.scriptsAdded.join(", ") || "none (existing scripts preserved)"}\n`);
4669
4675
  if (result.installCommand) process.stdout.write(`Install: ${result.installCommand.join(" ")}\n`);
4670
4676
  if (result.agentAdded) process.stdout.write("Agent instructions: AGENTS.md\n");
4671
- process.stdout.write(`\nRun ${result.packageManager} run check:taste for local checks.\nSet AI_GATEWAY_API_KEY, then run ${result.packageManager} run taste for AI checks.\n`);
4677
+ process.stdout.write(`\nSet AI_GATEWAY_API_KEY, then run ${result.packageManager} run taste.\n`);
4672
4678
  if (options.install === false) process.stdout.write("Installation skipped; the scripts require taste-lint to be installed.\n");
4673
4679
  });
4674
4680
  }
@@ -5190,15 +5196,14 @@ const runLint = async (options, ctx = {}) => {
5190
5196
  const judgeOptions = {
5191
5197
  cache,
5192
5198
  config,
5193
- mechanicalOnly: options.mechanicalOnly,
5194
5199
  model
5195
5200
  };
5196
5201
  const prepared = prepareJudgement(units, rules, judgeOptions);
5197
5202
  let evaluate = ctx.evaluate;
5198
5203
  let recorder;
5199
- if (!options.dryRun && !options.mechanicalOnly && prepared.pending.length > 0) {
5204
+ if (!options.dryRun && prepared.pending.length > 0) {
5200
5205
  evaluate ??= evaluateFromEnv(options.apiKey);
5201
- if (!evaluate) throw new InputError("MISSING_CREDENTIALS", `${KEY_HINT} Or run with --dry-run or --mechanical-only.`);
5206
+ if (!evaluate) throw new InputError("MISSING_CREDENTIALS", `${KEY_HINT} Use --dry-run to preview without calling Jev.`);
5202
5207
  } else evaluate = void 0;
5203
5208
  if (options.printRequests) for (const p of prepared.pending) for (const questions of chunkQuestions(p)) {
5204
5209
  const ordered = Object.fromEntries(Object.keys(questions).toSorted().map((k) => [k, questions[k]]));
@@ -5464,7 +5469,7 @@ const renderTty = (result, options = {}) => {
5464
5469
  //#region src/commands/lint.ts
5465
5470
  const quote = (value) => `'${value.replaceAll("'", "'\"'\"'")}'`;
5466
5471
  function registerLintCommand(program) {
5467
- program.command("lint").description("Lint files (or a rendered page) against the taste rules").argument("[paths...]", "Files or directories to lint, relative to --root").option("--root <path>", "Project root for globs and config", process.cwd()).option("--rules <path>", "Rules directory (default: packaged data/rules)").option("--only <ids>", "Comma-separated rule ids").option("--exclude <globs>", "Comma-separated globs to skip (added to taste-lint.config.json exclude)").option("--dry-run", "Plan and estimate cost without calling Jev").option("--print-requests", "With --dry-run and --output tty, print request JSONL to stderr").option("--writing-context <file>", "Explicit JSON facts/profile/instructions for personal writing; sent to Jev on live runs").option("--mechanical-only", "Skip every Jev-backed rule").option("--no-cache", "Ignore cached answers (new answers are still recorded)").option("--limit-units <n>", "Only consider the first n units").option("--fail-on <severity>", "Lowest severity that fails the run: major or minor", "minor").option("--fix", "Apply deterministic fixes for act-band findings").option("--output <format>", "tty, json or sarif", "tty").option("--results-dir <path>", "Where logs and cache live", defaultResultsDir()).option("--model <id>", "Jev model id", DEFAULT_MODEL).option("--progress", "Print periodic progress on stderr even when piped").option("--verbose", "Show suppressed findings and unknowns").option("--url <url>", "Lint a rendered page through style-capture").option("--selector <css>", "Root selector for --url", "body").option("--capture <file>", "Lint a saved style-capture CaptureResult JSON").action(async (paths, options) => {
5472
+ program.command("lint").description("Lint files (or a rendered page) against the taste rules").argument("[paths...]", "Files or directories to lint, relative to --root").option("--root <path>", "Project root for globs and config", process.cwd()).option("--rules <path>", "Rules directory (default: packaged data/rules)").option("--only <ids>", "Comma-separated rule ids").option("--exclude <globs>", "Comma-separated globs to skip (added to taste-lint.config.json exclude)").option("--dry-run", "Plan and estimate cost without calling Jev").option("--print-requests", "With --dry-run and --output tty, print request JSONL to stderr").option("--writing-context <file>", "Explicit JSON facts/profile/instructions for personal writing; sent to Jev on live runs").option("--no-cache", "Ignore cached answers (new answers are still recorded)").option("--limit-units <n>", "Only consider the first n units").option("--fail-on <severity>", "Lowest severity that fails the run: major or minor", "minor").option("--fix", "Apply deterministic fixes for act-band findings").option("--output <format>", "tty, json or sarif", "tty").option("--results-dir <path>", "Where logs and cache live", defaultResultsDir()).option("--model <id>", "Jev model id", DEFAULT_MODEL).option("--progress", "Print periodic progress on stderr even when piped").option("--verbose", "Show suppressed findings and unknowns").option("--url <url>", "Lint a rendered page through style-capture").option("--selector <css>", "Root selector for --url", "body").option("--capture <file>", "Lint a saved style-capture CaptureResult JSON").action(async (paths, options) => {
5468
5473
  if (!["major", "minor"].includes(options.failOn)) throw new InputError("INVALID_ARGUMENT", "--fail-on must be major or minor");
5469
5474
  if (![
5470
5475
  "tty",
@@ -5492,7 +5497,6 @@ function registerLintCommand(program) {
5492
5497
  failOn: options.failOn,
5493
5498
  fix: options.fix,
5494
5499
  limitUnits,
5495
- mechanicalOnly: options.mechanicalOnly,
5496
5500
  model: options.model,
5497
5501
  noCache: !options.cache,
5498
5502
  only: options.only?.split(",").map((s) => s.trim()).filter(Boolean),
@@ -6125,7 +6129,6 @@ const runScan = async (options, ctx = {}) => {
6125
6129
  },
6126
6130
  exclude,
6127
6131
  graph: graph?.digest,
6128
- mechanicalOnly: !!options.mechanicalOnly,
6129
6132
  model: options.model ?? "jev-latest",
6130
6133
  profile,
6131
6134
  root,
@@ -6146,7 +6149,6 @@ const runScan = async (options, ctx = {}) => {
6146
6149
  docTypes: profile.docTypes,
6147
6150
  dryRun: options.dryRun,
6148
6151
  exclude,
6149
- mechanicalOnly: options.mechanicalOnly,
6150
6152
  model: options.model,
6151
6153
  only: rules.map((r) => r.id),
6152
6154
  resultsDir: options.resultsDir,
@@ -6215,7 +6217,7 @@ const runScan = async (options, ctx = {}) => {
6215
6217
  //#endregion
6216
6218
  //#region src/commands/scan.ts
6217
6219
  const registerScanCommand = (program) => {
6218
- const scan = program.command("scan").enablePositionalOptions().description("Run a scoped scan with stable findings, baselines, and review exports").argument("[paths...]", "Targets within the root; defaults to .").option("--root <path>", "Repository root", process.cwd()).option("--profile <name>", `Scan objective: ${PROFILE_NAMES.join(", ")}`, "product").option("--rules <path>", "Rules directory").option("--only <ids>", "Comma-separated rule IDs within the selected profile").option("--exclude <globs>", "Additional comma-separated exclusions").option("--dry-run", "Preview scope and cost without provider calls").option("--mechanical-only", "Run deterministic checks only").option("--model <id>", "Evaluation model", "jev-latest").option("--results-dir <path>", "Answer cache and sanitized request records").option("--baseline <file>", "Compare a compatible completed scan").option("--new-only", "Report and gate only new findings against the baseline").option("--since <ref>", "Report changed lines against a Git commit; retain analysis context").option("--decisions <file>", "Apply accepted or dismissed review decisions").option("--dependency-cruiser <file>", "Import dependency-cruiser JSON violations").option("--samples <file>", "Export blind labeling samples, including source text, without predicted scores").option("--save <file>", "Save a scan report; only completed scans can serve as baselines").option("--output <format>", "tty, json, or sarif", "tty").option("--progress", "Print provider progress to stderr").action(async (targets, options) => {
6220
+ const scan = program.command("scan").enablePositionalOptions().description("Run a scoped scan with stable findings, baselines, and review exports").argument("[paths...]", "Targets within the root; defaults to .").option("--root <path>", "Repository root", process.cwd()).option("--profile <name>", `Scan objective: ${PROFILE_NAMES.join(", ")}`, "product").option("--rules <path>", "Rules directory").option("--only <ids>", "Comma-separated rule IDs within the selected profile").option("--exclude <globs>", "Additional comma-separated exclusions").option("--dry-run", "Preview scope and cost without provider calls").option("--model <id>", "Evaluation model", "jev-latest").option("--results-dir <path>", "Answer cache and sanitized request records").option("--baseline <file>", "Compare a compatible completed scan").option("--new-only", "Report and gate only new findings against the baseline").option("--since <ref>", "Report changed lines against a Git commit; retain analysis context").option("--decisions <file>", "Apply accepted or dismissed review decisions").option("--dependency-cruiser <file>", "Import dependency-cruiser JSON violations").option("--samples <file>", "Export blind labeling samples, including source text, without predicted scores").option("--save <file>", "Save a scan report; only completed scans can serve as baselines").option("--output <format>", "tty, json, or sarif", "tty").option("--progress", "Print provider progress to stderr").action(async (targets, options) => {
6219
6221
  if (![
6220
6222
  "tty",
6221
6223
  "json",
@@ -6545,7 +6547,6 @@ Quickstart:
6545
6547
  taste-lint scan .
6546
6548
 
6547
6549
  Preview: taste-lint scan . --dry-run
6548
- Without a key: taste-lint scan . --mechanical-only
6549
6550
  Get a key: https://vercel.com/docs/ai-gateway/authentication-and-byok/api-keys
6550
6551
  `);
6551
6552
  registerLintCommand(program);