taste-lint 0.3.0 → 0.4.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 (48) hide show
  1. package/data/corpus/copywriting.jsonl +4 -4
  2. package/data/rule-drafts/interaction/interaction-layout-shift.yaml +3 -3
  3. package/data/rules/copywriting/copywriting-antithesis-contrast.yaml +43 -0
  4. package/data/rules/copywriting/copywriting-antithesis-frame.yaml +4 -2
  5. package/data/rules/copywriting/copywriting-artificial-balance.yaml +4 -1
  6. package/data/rules/copywriting/copywriting-boilerplate-offer.yaml +1 -1
  7. package/data/rules/copywriting/copywriting-canned-phrasing.yaml +1 -1
  8. package/data/rules/copywriting/copywriting-claim-without-evidence.yaml +10 -1
  9. package/data/rules/copywriting/copywriting-copula-avoidance.yaml +1 -1
  10. package/data/rules/copywriting/copywriting-dead-link.yaml +28 -0
  11. package/data/rules/copywriting/copywriting-em-dash-in-ui.yaml +23 -0
  12. package/data/rules/copywriting/copywriting-excessive-hedging.yaml +1 -1
  13. package/data/rules/copywriting/copywriting-external-arrow-internal-link.yaml +26 -0
  14. package/data/rules/copywriting/copywriting-fabricated-specificity.yaml +6 -3
  15. package/data/rules/copywriting/copywriting-generic-framing.yaml +12 -6
  16. package/data/rules/copywriting/copywriting-generic-language.yaml +12 -4
  17. package/data/rules/copywriting/copywriting-generic-validation.yaml +1 -1
  18. package/data/rules/copywriting/copywriting-machine-prose.yaml +4 -1
  19. package/data/rules/copywriting/copywriting-mechanism-over-outcome.yaml +4 -1
  20. package/data/rules/copywriting/copywriting-meta-commentary.yaml +1 -1
  21. package/data/rules/copywriting/copywriting-missing-reasoning-link.yaml +4 -1
  22. package/data/rules/copywriting/copywriting-over-explanation.yaml +4 -1
  23. package/data/rules/copywriting/copywriting-participle-tail.yaml +1 -1
  24. package/data/rules/copywriting/copywriting-prompt-restatement.yaml +5 -2
  25. package/data/rules/copywriting/copywriting-punctuation-tics.yaml +4 -1
  26. package/data/rules/copywriting/copywriting-reader-first.yaml +4 -1
  27. package/data/rules/copywriting/copywriting-repetitive-transitions.yaml +1 -1
  28. package/data/rules/copywriting/copywriting-semantic-links.yaml +2 -2
  29. package/data/rules/copywriting/copywriting-slogan-headings.yaml +27 -0
  30. package/data/rules/copywriting/copywriting-structural-symmetry.yaml +4 -1
  31. package/data/rules/copywriting/copywriting-tee-up.yaml +4 -2
  32. package/data/rules/copywriting/copywriting-unexplained-action.yaml +5 -2
  33. package/data/rules/copywriting/copywriting-unnatural-polish.yaml +8 -5
  34. package/data/rules/copywriting/copywriting-unverifiable-proof.yaml +1 -1
  35. package/data/rules/copywriting/copywriting-vague-error.yaml +2 -0
  36. package/data/rules/craft/craft-atmosphere-gradient.yaml +1 -1
  37. package/data/rules/craft/craft-decoration-no-role.yaml +2 -1
  38. package/data/rules/craft/craft-decorative-ordinals.yaml +26 -0
  39. package/data/rules/craft/craft-faux-product-chrome.yaml +4 -1
  40. package/data/rules/schema/rule.schema.json +7 -1
  41. package/dist/cli.js +565 -97
  42. package/dist/cli.js.map +1 -1
  43. package/dist/index.d.ts +34 -18
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +275 -17
  46. package/dist/index.js.map +1 -1
  47. package/package.json +2 -2
  48. package/data/rules/copywriting/copywriting-idea-repetition.yaml +0 -34
package/dist/index.d.ts CHANGED
@@ -68,7 +68,7 @@ declare const DOC_TYPES: readonly ["tutorial", "howto", "reference", "explanatio
68
68
  type DocType = (typeof DOC_TYPES)[number];
69
69
  declare const ROLES: readonly ["heading", "body", "list-item", "cell", "button", "link", "label", "placeholder", "aria", "literal-copy", "caption", "unknown"];
70
70
  type Role = (typeof ROLES)[number];
71
- declare const CONTEXT_KEYS: readonly ["section", "headingAbove", "docType", "element", "role", "neighbours"];
71
+ declare const CONTEXT_KEYS: readonly ["section", "headingAbove", "docType", "element", "role", "neighbours", "earlier", "brief"];
72
72
  type ContextKey = (typeof CONTEXT_KEYS)[number];
73
73
  type Band = "act" | "review" | "silent";
74
74
  interface RuleSource {
@@ -116,6 +116,8 @@ interface Preconditions {
116
116
  role?: Role[];
117
117
  /** Skip Markdown units when the project curls quotes at build time. */
118
118
  smartQuotesAtBuild?: false;
119
+ /** Skip units shorter than this many words: a label or a greeting is not prose. */
120
+ minWords?: number;
119
121
  }
120
122
  interface ReviewProcedure {
121
123
  applicability: string;
@@ -186,6 +188,10 @@ interface UnitContext {
186
188
  fontScale?: Record<string, string>;
187
189
  section?: string;
188
190
  headingAbove?: string;
191
+ /** Sentences said earlier in the file that share content words with this unit. */
192
+ earlier?: string;
193
+ /** What the product is and does, from `lint --brief`: the facts copy may state. */
194
+ brief?: string;
189
195
  docType: DocType;
190
196
  element?: string;
191
197
  attr?: string;
@@ -408,6 +414,8 @@ interface CorpusItem {
408
414
  labelSource: "manifest" | "manifest-weak" | "hand" | "sweep" | "ai";
409
415
  labelModel?: string;
410
416
  labelPromptHash?: string;
417
+ /** Why a hand labeller decided as they did; feeds rubric edits. */
418
+ note?: string;
411
419
  source: {
412
420
  repo: string;
413
421
  path: string;
@@ -440,7 +448,7 @@ declare const PROFILE_NAMES: readonly ["product", "writing", "instructions", "al
440
448
  type ProfileName = (typeof PROFILE_NAMES)[number];
441
449
  //#endregion
442
450
  //#region src/lib/config.d.ts
443
- declare const loadConfig: (root: string, defaults?: Config["docTypes"]) => Config;
451
+ export declare const loadConfig: (root: string, defaults?: Config["docTypes"]) => Config;
444
452
  //#endregion
445
453
  //#region src/lint.d.ts
446
454
  /**
@@ -456,6 +464,8 @@ interface LintOptions {
456
464
  profile?: ProfileName;
457
465
  /** Report only findings on lines changed since this Git revision. */
458
466
  since?: string;
467
+ /** File stating what the product is and does; rules that compare copy against it read it. */
468
+ brief?: string;
459
469
  /** Rules directory. Default: the packaged `data/rules`. */
460
470
  rulesDir?: string;
461
471
  /** Rule ids to run; wins over `profile`. */
@@ -492,7 +502,7 @@ interface LintContext {
492
502
  /** Called at most every request with counts and elapsed time. */
493
503
  onProgress?: (progress: Progress) => void;
494
504
  /** Called once with every unit, the Jev answers and the mechanical negatives, before findings are banded. */
495
- onEvidence?: (units: Unit[], answers: Map<string, Record<string, number>>, negatives: Map<string, Set<string>>, rules: Rule[]) => void;
505
+ onEvidence?: (units: Unit[], answers: Map<string, Record<string, number>>, negatives: Map<string, Set<string>>, rules: Rule[], sources: Map<string, string[]>) => void;
496
506
  }
497
507
  /** What `runLint` resolves with: a report where the fields renderers treat as optional are always present. */
498
508
  type LintRun = LintResult & Required<Pick<LintResult, "summary" | "ruleFindings" | "ruleScorecard" | "scope">> & {
@@ -500,10 +510,10 @@ type LintRun = LintResult & Required<Pick<LintResult, "summary" | "ruleFindings"
500
510
  units: number;
501
511
  manifest: Record<string, unknown>;
502
512
  };
503
- declare const runLint: (options?: LintOptions, ctx?: LintContext) => Promise<LintRun>;
513
+ export declare const runLint: (options?: LintOptions, ctx?: LintContext) => Promise<LintRun>;
504
514
  //#endregion
505
515
  //#region src/rules/load.d.ts
506
- declare const resolveRulesDir: (explicit?: string) => string;
516
+ export declare const resolveRulesDir: (explicit?: string) => string;
507
517
  interface LoadOptions {
508
518
  /** Include rules with status draft (default false). */
509
519
  allowDraft?: boolean;
@@ -512,7 +522,7 @@ interface LoadOptions {
512
522
  /** More rule directories with the same domain-folder layout, such as a voice pack. */
513
523
  extraDirs?: string[];
514
524
  }
515
- declare const loadRules: (rulesDir: string, options?: LoadOptions) => Rule[];
525
+ export declare const loadRules: (rulesDir: string, options?: LoadOptions) => Rule[];
516
526
  //#endregion
517
527
  //#region src/rules/taxonomy.d.ts
518
528
  interface Category {
@@ -520,21 +530,21 @@ interface Category {
520
530
  domain: Domain;
521
531
  label: string;
522
532
  }
523
- declare const CATEGORIES: readonly Category[];
533
+ export declare const CATEGORIES: readonly Category[];
524
534
  //#endregion
525
535
  //#region src/extract/index.d.ts
526
- declare const extractFile: (config: Config, relativeFile: string) => Unit[];
536
+ export declare const extractFile: (config: Config, relativeFile: string) => Unit[];
527
537
  //#endregion
528
538
  //#region src/extract/tsx.d.ts
529
539
  interface TsxExtractOptions {
530
540
  config: Config;
531
541
  docType: DocType;
532
542
  }
533
- declare const extractTsx: (file: string, source: string, options: TsxExtractOptions) => Unit[];
543
+ export declare const extractTsx: (file: string, source: string, options: TsxExtractOptions) => Unit[];
534
544
  //#endregion
535
545
  //#region src/map/jev.d.ts
536
546
  type Transport = "typesafe" | "gateway";
537
- declare class ProviderError extends Error {
547
+ export declare class ProviderError extends Error {
538
548
  readonly category: "provider_error" | "invalid_response" | "timeout" | "auth";
539
549
  readonly status?: number | undefined;
540
550
  constructor(category: "provider_error" | "invalid_response" | "timeout" | "auth", status?: number | undefined);
@@ -549,16 +559,16 @@ interface FetchEvaluateOptions {
549
559
  /** Sleep hook so tests can run without waiting. */
550
560
  sleep?: (ms: number) => Promise<void>;
551
561
  }
552
- declare const makeFetchEvaluate: (options: FetchEvaluateOptions) => Evaluate;
562
+ export declare const makeFetchEvaluate: (options: FetchEvaluateOptions) => Evaluate;
553
563
  //#endregion
554
564
  //#region src/report/json.d.ts
555
- declare const renderJson: (result: LintResult, manifest: Record<string, unknown>) => string;
565
+ export declare const renderJson: (result: LintResult, manifest: Record<string, unknown>) => string;
556
566
  //#endregion
557
567
  //#region src/report/sarif.d.ts
558
- declare const renderSarif: (result: Pick<LintResult, "findings" | "ruleFindings">, rules: Rule[], version: string) => string;
568
+ export declare const renderSarif: (result: Pick<LintResult, "findings" | "ruleFindings">, rules: Rule[], version: string) => string;
559
569
  //#endregion
560
570
  //#region src/report/tty.d.ts
561
- declare const renderTty: (result: LintResult, options?: {
571
+ export declare const renderTty: (result: LintResult, options?: {
562
572
  verbose?: boolean;
563
573
  }) => string;
564
574
  //#endregion
@@ -619,6 +629,10 @@ interface EvalOptions {
619
629
  only?: string[];
620
630
  split?: "dev" | "holdout" | "all";
621
631
  includeWeak?: boolean;
632
+ /** Only items from these label sources; `hand` measures agreement with you. */
633
+ sources?: CorpusItem["labelSource"][];
634
+ /** List each item where the judge and the label disagree at the review threshold. */
635
+ disagreements?: boolean;
622
636
  dryRun?: boolean;
623
637
  noCache?: boolean;
624
638
  resultsDir?: string;
@@ -664,10 +678,10 @@ interface EvalResult {
664
678
  errors: number;
665
679
  };
666
680
  }
667
- declare const runEval: (options: EvalOptions, ctx?: EvalContext) => Promise<EvalResult>;
681
+ export declare const runEval: (options: EvalOptions, ctx?: EvalContext) => Promise<EvalResult>;
668
682
  //#endregion
669
683
  //#region src/eval/corpus.d.ts
670
- declare const splitFor: (id: string) => "dev" | "holdout";
684
+ export declare const splitFor: (id: string) => "dev" | "holdout";
671
685
  //#endregion
672
686
  //#region src/eval/promotion.d.ts
673
687
  interface PromotionEvidence {
@@ -688,6 +702,8 @@ interface TuneOptions {
688
702
  model?: string;
689
703
  apiKey?: string;
690
704
  minItems?: number;
705
+ /** Only items from these label sources, e.g. `hand`. */
706
+ sources?: CorpusItem["labelSource"][];
691
707
  }
692
708
  interface TuneContext {
693
709
  evaluate?: Evaluate;
@@ -702,11 +718,11 @@ interface TuneDecision {
702
718
  recall: number;
703
719
  action: "keep" | "raise" | "lower" | "demote" | "insufficient";
704
720
  }
705
- declare const runTune: (options: TuneOptions, ctx?: TuneContext) => Promise<{
721
+ export declare const runTune: (options: TuneOptions, ctx?: TuneContext) => Promise<{
706
722
  report: string;
707
723
  exitCode: number;
708
724
  decisions: TuneDecision[];
709
725
  }>;
710
726
  //#endregion
711
- export { type Band, CATEGORIES, type CONTEXT_KEYS, type Config, type ContextKey, type CorpusItem, type Coverage, type CriterionSide, type DOC_TYPES, type DOMAINS, type DocType, type Domain, type EntryValue, type Evaluate, type Finding, type Fix, type LintContext, type LintOptions, type LintResult, type LintRun, type Mechanical, type MechanicalHit, type NeighbourSummary, type Preconditions, type Progress, ProviderError, type Question, type ROLES, type RULE_STATUSES, type RecorderHandle, type ResolvedTypography, type ReviewProcedure, type Role, type Rule, type RuleSource, type RuleStatus, type RunStatus, type RunSummary, type SEVERITIES, type SEVERITY_RANK, type STRUCTURAL_KINDS, type ScanScope, type Scorecard, type Severity, type SystemOneNoul, type SystemOneRequest, type SystemOneResponse, type TIERS, type Thresholds, type Tier, type Tuning, type TuningEntry, type UNIT_KINDS, type Unit, type UnitContext, type UnitKind, type Unknown, type Usage, extractFile, extractTsx, loadConfig, loadRules, makeFetchEvaluate, renderJson, renderSarif, renderTty, resolveRulesDir, runEval, runLint, runTune, splitFor };
727
+ export type { Band, CONTEXT_KEYS, Config, ContextKey, CorpusItem, Coverage, CriterionSide, DOC_TYPES, DOMAINS, DocType, Domain, EntryValue, Evaluate, Finding, Fix, LintContext, LintOptions, LintResult, LintRun, Mechanical, MechanicalHit, NeighbourSummary, Preconditions, Progress, Question, ROLES, RULE_STATUSES, RecorderHandle, ResolvedTypography, ReviewProcedure, Role, Rule, RuleSource, RuleStatus, RunStatus, RunSummary, SEVERITIES, SEVERITY_RANK, STRUCTURAL_KINDS, ScanScope, Scorecard, Severity, SystemOneNoul, SystemOneRequest, SystemOneResponse, TIERS, Thresholds, Tier, Tuning, TuningEntry, UNIT_KINDS, Unit, UnitContext, UnitKind, Unknown, Usage };
712
728
  //# sourceMappingURL=index.d.ts.map
@@ -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
@@ -183,7 +183,9 @@ const CONTEXT_KEYS = [
183
183
  "docType",
184
184
  "element",
185
185
  "role",
186
- "neighbours"
186
+ "neighbours",
187
+ "earlier",
188
+ "brief"
187
189
  ];
188
190
  //#endregion
189
191
  //#region src/lib/errors.ts
@@ -399,17 +401,17 @@ const invalid = (key, expected) => {
399
401
  field: key
400
402
  });
401
403
  };
404
+ const object = (value, key) => {
405
+ if (!value || typeof value !== "object" || Array.isArray(value)) return invalid(key, "an object");
406
+ return value;
407
+ };
408
+ const keys = (value, allowed, prefix) => {
409
+ for (const key of Object.keys(value)) if (!allowed.includes(key)) invalid(`${prefix}${key}`, `one of ${allowed.join(", ")}`);
410
+ };
411
+ const strings = (value, key) => {
412
+ if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) invalid(key, "an array of strings");
413
+ };
402
414
  function validateConfig(raw) {
403
- const object = (value, key) => {
404
- if (!value || typeof value !== "object" || Array.isArray(value)) return invalid(key, "an object");
405
- return value;
406
- };
407
- const keys = (value, allowed, prefix) => {
408
- for (const key of Object.keys(value)) if (!allowed.includes(key)) invalid(`${prefix}${key}`, `one of ${allowed.join(", ")}`);
409
- };
410
- const strings = (value, key) => {
411
- if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) invalid(key, "an array of strings");
412
- };
413
415
  const value = object(raw, "config");
414
416
  keys(value, [
415
417
  "$schema",
@@ -509,6 +511,55 @@ const docTypeFor = (config, relativeFile) => {
509
511
  return "unknown";
510
512
  };
511
513
  //#endregion
514
+ //#region src/extract/earlier.ts
515
+ const NOT_PROSE = /* @__PURE__ */ new Set([
516
+ "heading",
517
+ "button",
518
+ "link",
519
+ "label",
520
+ "placeholder",
521
+ "aria"
522
+ ]);
523
+ const STOP = new Set("about above after again also always another because been before being below between both came come could does doing done down each even every from have having here into just like made make many more most much must never only other over same should since some such than that their them then there these they this those through very want were what when where which while will with would your yours you're we're it's that's there's isn't don't can't won't".split(" "));
524
+ const SENTENCE = /[^.!?\n]+[.!?]*/gu;
525
+ const WORD$1 = /[\p{L}\p{N}$][\p{L}\p{N}$'’.-]*[\p{L}\p{N}]|[\p{L}\p{N}]/gu;
526
+ const MIN_SHARED = 3;
527
+ const MAX_EVIDENCE = 8;
528
+ const tokens = (sentence) => {
529
+ const out = /* @__PURE__ */ new Set();
530
+ for (const [raw] of sentence.toLowerCase().matchAll(WORD$1)) {
531
+ const word = raw.replaceAll("’", "'");
532
+ if (/\d/u.test(word)) out.add(word);
533
+ else if (word.length >= 4 && !STOP.has(word)) out.add(word.length > 4 ? word.replace(/(?:'s|s)$/u, "") : word);
534
+ }
535
+ return out;
536
+ };
537
+ const isProse = (unit) => (unit.kind === "paragraph" || unit.kind === "jsx-text") && !unit.inCode && !NOT_PROSE.has(unit.context.role);
538
+ const attachEarlier = (units) => {
539
+ const prose = units.filter(isProse).toSorted((a, b) => a.sourceStart - b.sourceStart);
540
+ const split = prose.map((unit) => [...unit.text.matchAll(SENTENCE)].map(([s]) => s.trim()).filter(Boolean).map((text) => ({
541
+ text,
542
+ words: tokens(text)
543
+ })));
544
+ const spread = /* @__PURE__ */ new Map();
545
+ for (const sentences of split) for (const word of new Set(sentences.flatMap((s) => [...s.words]))) spread.set(word, (spread.get(word) ?? 0) + 1);
546
+ const subject = (word) => prose.length >= 6 && (spread.get(word) ?? 0) * 3 > prose.length;
547
+ const seen = [];
548
+ const index = /* @__PURE__ */ new Map();
549
+ for (const [u, unit] of prose.entries()) {
550
+ const matched = /* @__PURE__ */ new Map();
551
+ for (const sentence of split[u] ?? []) {
552
+ const words = [...sentence.words].filter((w) => !subject(w));
553
+ const shared = /* @__PURE__ */ new Map();
554
+ for (const word of words) for (const id of index.get(word) ?? []) shared.set(id, (shared.get(id) ?? 0) + 1);
555
+ for (const [id, n] of shared) if (n >= MIN_SHARED || n >= 2 && n * 2 >= words.length) matched.set(id, Math.max(matched.get(id) ?? 0, n));
556
+ const id = seen.push(sentence.text) - 1;
557
+ for (const word of words) index.set(word, [...index.get(word) ?? [], id]);
558
+ }
559
+ if (matched.size > 0) unit.context.earlier = [...matched].toSorted((a, b) => b[1] - a[1] || a[0] - b[0]).slice(0, MAX_EVIDENCE).toSorted((a, b) => a[0] - b[0]).map(([id]) => seen[id]).join("\n");
560
+ }
561
+ };
562
+ //#endregion
512
563
  //#region src/lib/frontmatter.ts
513
564
  const stripBom = (text) => text.codePointAt(0) === 65279 ? ` ${text.slice(1)}` : text;
514
565
  const scalar = (value) => {
@@ -1780,6 +1831,7 @@ const extractSource = (config, file, source, repository) => {
1780
1831
  config,
1781
1832
  docType
1782
1833
  }) : [];
1834
+ attachEarlier(units);
1783
1835
  const whole = makeUnit(file, new LineIndex(source), {
1784
1836
  context: {
1785
1837
  docType,
@@ -2133,6 +2185,8 @@ const buildState = (unit, rules) => {
2133
2185
  if (keys.has("docType")) lines.push(`DOC TYPE: ${unit.context.docType}`);
2134
2186
  if (keys.has("element") && unit.context.element) lines.push(`ELEMENT: ${unit.context.element}${unit.context.attr ? ` (${unit.context.attr})` : ""}`);
2135
2187
  if (keys.has("role")) lines.push(`ROLE: ${unit.context.role}`);
2188
+ if (keys.has("brief") && unit.context.brief) lines.push(`BRIEF: ${clip("BRIEF: ", unit.context.brief)}`);
2189
+ if (keys.has("earlier") && unit.context.earlier) lines.push(`EARLIER: ${clip("EARLIER: ", unit.context.earlier)}`);
2136
2190
  if (keys.has("section") && unit.context.section) lines.push(`SECTION: ${clip("SECTION: ", unit.context.section)}`);
2137
2191
  return {
2138
2192
  state: lines.join("\n"),
@@ -2547,7 +2601,8 @@ const preconditions = (file, v) => {
2547
2601
  "notInCode",
2548
2602
  "docType",
2549
2603
  "role",
2550
- "smartQuotesAtBuild"
2604
+ "smartQuotesAtBuild",
2605
+ "minWords"
2551
2606
  ]);
2552
2607
  const out = {};
2553
2608
  if (raw.notInCode !== void 0) {
@@ -2560,6 +2615,10 @@ const preconditions = (file, v) => {
2560
2615
  if (raw.smartQuotesAtBuild !== false) fail(file, "preconditions.smartQuotesAtBuild may only be false");
2561
2616
  out.smartQuotesAtBuild = false;
2562
2617
  }
2618
+ if (raw.minWords !== void 0) {
2619
+ if (!Number.isInteger(raw.minWords) || raw.minWords < 1) fail(file, "preconditions.minWords must be a positive integer");
2620
+ out.minWords = raw.minWords;
2621
+ }
2563
2622
  return out;
2564
2623
  };
2565
2624
  const INCLUDE_BY_KIND = {
@@ -2655,6 +2714,7 @@ const missingEvidence = (unit, rule) => {
2655
2714
  };
2656
2715
  //#endregion
2657
2716
  //#region src/map/plan.ts
2717
+ const WORD = /[\p{L}\p{N}]+(?:['’-][\p{L}\p{N}]+)*/gu;
2658
2718
  const inScope = (rule, unit) => unit.kind === "element" || matchesAny(unit.file, rule.scope.include) && !matchesAny(unit.file, rule.scope.exclude);
2659
2719
  const passesPreconditions = (rule, unit, config) => {
2660
2720
  const p = rule.preconditions;
@@ -2662,6 +2722,7 @@ const passesPreconditions = (rule, unit, config) => {
2662
2722
  if (p.notInCode && unit.inCode) return false;
2663
2723
  if (p.docType && !p.docType.includes(unit.context.docType)) return false;
2664
2724
  if (p.role && !p.role.includes(unit.context.role)) return false;
2725
+ if (p.minWords && (unit.text.match(WORD)?.length ?? 0) < p.minWords) return false;
2665
2726
  if (p.smartQuotesAtBuild === false && config.smartQuotesAtBuild && (unit.kind === "paragraph" || unit.kind === "heading") && /\.(md|mdx)$/.test(unit.file)) return false;
2666
2727
  return true;
2667
2728
  };
@@ -3836,6 +3897,110 @@ const MOTION_SOURCE_RULES = [codeRule({
3836
3897
  unit: ["source"]
3837
3898
  }, "src/rules/code/motion-source.ts")];
3838
3899
  //#endregion
3900
+ //#region src/rules/code/prose.ts
3901
+ const PROSE_ROLES = [
3902
+ "body",
3903
+ "list-item",
3904
+ "cell",
3905
+ "caption",
3906
+ "literal-copy",
3907
+ "unknown"
3908
+ ];
3909
+ const echoesEarlier = (unit) => unit.context.earlier ? hit(`shares content words with ${unit.context.earlier.split("\n").length} earlier sentence(s)`) : none;
3910
+ const hasBrief = (unit) => unit.context.brief ? hit("checked against the brief") : none;
3911
+ const PROSE_RULES = [codeRule({
3912
+ categoryId: "machine-prose",
3913
+ check: echoesEarlier,
3914
+ hint: "Cut the restatement or replace it with the evidence it was standing in for.",
3915
+ id: "copywriting-idea-repetition",
3916
+ preconditions: {
3917
+ minWords: 5,
3918
+ notInCode: true,
3919
+ role: PROSE_ROLES
3920
+ },
3921
+ question: {
3922
+ context: ["docType", "earlier"],
3923
+ criteria: {
3924
+ false: {
3925
+ examples: [
3926
+ "EARLIER: Northwind keeps both versions side by side. TEXT: Open the conflict and pick the lines you want from each version.",
3927
+ "EARLIER: Relay unreachable. Check your connection and try again. TEXT: Disk full. Free up space on this laptop and Northwind will resume.",
3928
+ "EARLIER: Speed matters to users. TEXT: A 400ms dropdown loses a click one time in five in our logs."
3929
+ ],
3930
+ what: "TEXT adds a fact, example, step or consequence, or is a parallel entry for a different case."
3931
+ },
3932
+ true: {
3933
+ examples: ["EARLIER: When two people edit the same file, Northwind keeps both versions side by side. TEXT: Conflicting edits are never overwritten: you get both versions, side by side.", "EARLIER: It costs $8 per seat per month, with a 14-day free trial. TEXT: Just $8 per seat per month, and your first 14 days are free."],
3934
+ what: "TEXT says again what an EARLIER sentence already said, with nothing new."
3935
+ }
3936
+ },
3937
+ instructions: "EARLIER lists sentences said before TEXT in the same document that share words with it. Does TEXT restate one of them without adding a new fact, example, step or consequence? Return false for parallel entries that share a structure but cover different cases (one error message, FAQ answer or step each), for a deliberate one-sentence summary closing a long section, and for a fact repeated where the reader acts on it (the price on the plan's button)."
3938
+ },
3939
+ source: {
3940
+ line: 1,
3941
+ path: "https://typesafe-parallel-judgment-lab.every-4573.chatgpt.site/downloads/typesafe-lab-source.zip (scripts/run-ai-checker.mjs)",
3942
+ repo: "every.to",
3943
+ ruleId: "idea_repetition"
3944
+ },
3945
+ thresholds: {
3946
+ act: .75,
3947
+ review: .4
3948
+ },
3949
+ title: "Repeats the same idea without adding new evidence",
3950
+ unit: ["paragraph", "jsx-text"]
3951
+ }, "src/rules/code/prose.ts"), codeRule({
3952
+ categoryId: "evidence-over-claims",
3953
+ check: hasBrief,
3954
+ hint: "Cut the setting, mode, policy or comparison, or add it to the brief if the product really does it.",
3955
+ id: "copywriting-invented-capability",
3956
+ preconditions: { notInCode: true },
3957
+ question: {
3958
+ context: [
3959
+ "docType",
3960
+ "element",
3961
+ "brief"
3962
+ ],
3963
+ criteria: {
3964
+ false: {
3965
+ examples: [
3966
+ "BRIEF: keeps both versions when edits conflict. TEXT: Keep both versions",
3967
+ "BRIEF: 14-day free trial, no card required. TEXT: Start your free trial",
3968
+ "BRIEF: a desktop sync app. TEXT: Email address",
3969
+ "BRIEF: 14-day free trial, no card required; $8 per seat per month. TEXT: 14 days. No card. $8 per seat after that.",
3970
+ "BRIEF: keeps both versions when edits conflict. TEXT: Nobody’s work gets thrown away, even when two people edit the same file offline."
3971
+ ],
3972
+ what: "Every claim in TEXT rewords, summarises or directly follows from the brief, or TEXT is ordinary interface chrome or an instruction to the reader."
3973
+ },
3974
+ true: {
3975
+ examples: [
3976
+ "BRIEF: local network first, falls back to an encrypted relay. TEXT: Relay only",
3977
+ "BRIEF: 14-day free trial, no card required. TEXT: Cancel anytime during the trial",
3978
+ "BRIEF: keeps both versions side by side. TEXT: Most sync tools pick a winner and silently overwrite the other."
3979
+ ],
3980
+ what: "TEXT offers or asserts a capability, setting, mode, role, policy, integration or comparison the brief never states."
3981
+ }
3982
+ },
3983
+ instructions: "BRIEF is everything known about the product. Does TEXT offer or assert a capability, setting, mode, role, policy, integration, page, or claim about other products that BRIEF neither states nor directly implies? Return false for rewording, shortening or drawing a direct consequence from BRIEF, for ordinary interface chrome every app has (Save, Cancel, Email, Password, Settings, Sign in), and for instructions to the reader."
3984
+ },
3985
+ source: {
3986
+ line: 10,
3987
+ path: "skills/ui-design/rules/slop-invented-behaviour.md",
3988
+ repo: "mblode/agent-skills",
3989
+ ruleId: "slop-invented-behaviour"
3990
+ },
3991
+ thresholds: {
3992
+ act: .75,
3993
+ review: .4
3994
+ },
3995
+ title: "States a capability the brief never gave",
3996
+ unit: [
3997
+ "paragraph",
3998
+ "heading",
3999
+ "jsx-text",
4000
+ "attr-string"
4001
+ ]
4002
+ }, "src/rules/code/prose.ts")];
4003
+ //#endregion
3839
4004
  //#region src/rules/code/skill-classes.ts
3840
4005
  const make = (id, title, categoryId, source, check, hint) => codeRule({
3841
4006
  categoryId,
@@ -3880,6 +4045,67 @@ const SKILL_CLASS_RULES = [
3880
4045
  }, "Prefer transform/opacity for frequent motion; measure deliberate container resizing before changing it.")
3881
4046
  ];
3882
4047
  //#endregion
4048
+ //#region src/rules/code/slop-source.ts
4049
+ const EYEBROW = /<(?:p|span|div)\b[^>]*className="[^"]*\b(?:uppercase|text-xs|text-\[(?:9|10|11|12)px\])[^"]*"[^>]*>(?:\s*<span[^>]*\/>)?[^<{]{2,80}<\/(?:p|span|div)>\s*<h[1-3]\b/gu;
4050
+ const FACT = /\b\d+[- ]days?\b|\bno (?:credit )?card\b|\bcancel any ?time\b|\$\d+(?:\.\d\d)?(?= ?(?:\/|per|a) ?(?:seat|user|member|month))/giu;
4051
+ const MAX_EYEBROWS = 2;
4052
+ const MAX_REPEATS = 3;
4053
+ const eyebrows = (unit) => {
4054
+ const found = [...unit.text.matchAll(EYEBROW)];
4055
+ if (found.length <= MAX_EYEBROWS) return none;
4056
+ return {
4057
+ ...hit(`${found.length} headings carry an eyebrow label`),
4058
+ offset: found[MAX_EYEBROWS]?.index
4059
+ };
4060
+ };
4061
+ const factPadding = (unit) => {
4062
+ const counts = /* @__PURE__ */ new Map();
4063
+ for (const m of unit.text.matchAll(FACT)) {
4064
+ const key = m[0].toLowerCase().replaceAll(/[- ]/gu, " ");
4065
+ const seen = counts.get(key) ?? {
4066
+ first: m.index,
4067
+ n: 0
4068
+ };
4069
+ counts.set(key, {
4070
+ ...seen,
4071
+ n: seen.n + 1
4072
+ });
4073
+ }
4074
+ const [top] = [...counts].toSorted((a, b) => b[1].n - a[1].n);
4075
+ if (!top || top[1].n <= MAX_REPEATS) return none;
4076
+ return {
4077
+ ...hit(`"${top[0]}" appears ${top[1].n} times`),
4078
+ offset: top[1].first
4079
+ };
4080
+ };
4081
+ const source = (ruleId) => ({
4082
+ line: 10,
4083
+ path: `skills/ui-design/rules/${ruleId}.md`,
4084
+ repo: "mblode/agent-skills",
4085
+ ruleId
4086
+ });
4087
+ const SLOP_SOURCE_RULES = [codeRule({
4088
+ categoryId: "visual-hierarchy",
4089
+ check: eyebrows,
4090
+ hint: "Keep an eyebrow on at most two sections, where it names something the heading cannot. Delete the rest.",
4091
+ id: "craft-eyebrow-overuse",
4092
+ scope: { include: ["**/*.tsx", "**/*.jsx"] },
4093
+ source: source("slop-eyebrow-overuse"),
4094
+ status: "review-only",
4095
+ title: "An eyebrow over nearly every heading",
4096
+ unit: ["source"]
4097
+ }, "src/rules/code/slop-source.ts"), codeRule({
4098
+ categoryId: "evidence-over-claims",
4099
+ check: factPadding,
4100
+ hint: "Say each fact once, where the reader needs it: the price on the plan, the trial on the button. Cut the sections that exist only to repeat them.",
4101
+ id: "copywriting-fact-padding",
4102
+ scope: { include: ["**/*.tsx", "**/*.jsx"] },
4103
+ source: source("slop-fact-padding"),
4104
+ status: "review-only",
4105
+ title: "The same few facts restated to fill the page",
4106
+ unit: ["source"]
4107
+ }, "src/rules/code/slop-source.ts")];
4108
+ //#endregion
3883
4109
  //#region src/rules/code/typography.ts
3884
4110
  const MODULE = "src/rules/code/typography.ts";
3885
4111
  const AUDIT = "skills/typography-audit/rules";
@@ -4129,7 +4355,9 @@ const CODE_RULES = [
4129
4355
  ...DOCUMENT_RULES,
4130
4356
  ...IMAGE_RULES,
4131
4357
  ...SKILL_CLASS_RULES,
4132
- ...MOTION_SOURCE_RULES
4358
+ ...MOTION_SOURCE_RULES,
4359
+ ...SLOP_SOURCE_RULES,
4360
+ ...PROSE_RULES
4133
4361
  ];
4134
4362
  //#endregion
4135
4363
  //#region src/rules/load.ts
@@ -4445,6 +4673,10 @@ const runLint = async (options = {}, ctx = {}) => {
4445
4673
  sources.set(file, source.split("\n"));
4446
4674
  }
4447
4675
  if (options.extraUnits) units.push(...options.extraUnits);
4676
+ if (options.brief) {
4677
+ const brief = fs.readFileSync(path.resolve(options.brief), "utf-8").trim();
4678
+ for (const unit of units) unit.context.brief = brief;
4679
+ }
4448
4680
  const scope = {
4449
4681
  byDocType: {},
4450
4682
  diagnostics: scan.messages,
@@ -4507,7 +4739,7 @@ const runLint = async (options = {}, ctx = {}) => {
4507
4739
  onProgress: ctx.onProgress,
4508
4740
  recorder
4509
4741
  });
4510
- ctx.onEvidence?.(units, judgement.answers, judgement.negatives, rules);
4742
+ ctx.onEvidence?.(units, judgement.answers, judgement.negatives, rules, sources);
4511
4743
  const status = options.dryRun ? "dry-run" : judgement.usage.errors > 0 || parseFailures.size > 0 || units.length === 0 ? "incomplete" : "complete";
4512
4744
  const jf = jevFindings(judgement.jobs, judgement.answers);
4513
4745
  let findings = [...judgement.mechanical, ...jf.findings];
@@ -4896,6 +5128,7 @@ const loadCorpus = (corpusDir, rules, options = {}) => {
4896
5128
  ].includes(labelSource)) throw new Error(`Invalid corpus line ${where}: labelSource`);
4897
5129
  if (labelSource === "ai" && (typeof r.labelModel !== "string" || !r.labelModel.trim() || typeof r.labelPromptHash !== "string" || !/^[a-f0-9]{64}$/.test(r.labelPromptHash))) throw new Error(`Invalid corpus line ${where}: AI labels require model and prompt hash`);
4898
5130
  if (!isRecord(r.source) || typeof r.source.repo !== "string" || typeof r.source.path !== "string" || r.source.id !== void 0 && (typeof r.source.id !== "string" || !r.source.id.trim())) throw new Error(`Invalid corpus line ${where}: source needs repo and path`);
5131
+ if (r.note !== void 0 && typeof r.note !== "string") throw new TypeError(`Invalid corpus line ${where}: note must be text`);
4899
5132
  if (r.split !== void 0 && r.split !== "dev" && r.split !== "holdout") throw new Error(`Invalid corpus line ${where}: split must be dev or holdout`);
4900
5133
  if (r.classes !== void 0 && (!Array.isArray(r.classes) || r.classes.some((c) => typeof c !== "string"))) throw new Error(`Invalid corpus line ${where}: classes must be strings`);
4901
5134
  if (r.context !== void 0) {
@@ -4922,6 +5155,7 @@ const loadCorpus = (corpusDir, rules, options = {}) => {
4922
5155
  labelSource,
4923
5156
  labels,
4924
5157
  neighbours: r.neighbours,
5158
+ note: r.note,
4925
5159
  source: r.source,
4926
5160
  split: r.split ?? splitFor(JSON.stringify([r.source.repo, r.source.path])),
4927
5161
  text: r.text
@@ -5025,6 +5259,24 @@ const pairedOutcomes = (items, rule, probabilities) => {
5025
5259
  return contrast;
5026
5260
  };
5027
5261
  //#endregion
5262
+ //#region src/eval/label.ts
5263
+ const renderDisagreements = (evals, items, rules) => {
5264
+ const byId = new Map(items.map((item) => [item.id, item]));
5265
+ const out = [];
5266
+ for (const e of evals) {
5267
+ const rule = rules.find((r) => r.id === e.ruleId);
5268
+ if (!rule) continue;
5269
+ const wrong = e.pairs.filter((p) => p.probability >= rule.thresholds.review !== p.label);
5270
+ if (!wrong.length) continue;
5271
+ out.push(`${e.ruleId}: ${wrong.length} of ${e.pairs.length} disagree (${path.relative(process.cwd(), rule.file)})`);
5272
+ for (const p of wrong) {
5273
+ const item = byId.get(p.id);
5274
+ out.push(` ${p.label ? "labelled violation, judge missed" : "labelled fine, judge flagged"} p=${p.probability.toFixed(2)} [${item?.labelSource ?? "?"}/${item?.split ?? "?"}] ${p.id}`, ` ${(item?.text ?? "").replaceAll("\n", " ").slice(0, 160)}`, ...item?.note ? [` note: ${item.note}`] : []);
5275
+ }
5276
+ }
5277
+ return out.length ? `\nDisagreements (review threshold):\n${out.join("\n")}\n` : "\nNo disagreements at the review threshold.\n";
5278
+ };
5279
+ //#endregion
5028
5280
  //#region src/eval/metrics.ts
5029
5281
  const corpusConfig = () => ({
5030
5282
  components: {
@@ -5170,7 +5422,11 @@ const runEval = async (options, ctx = {}) => {
5170
5422
  knownRuleIds
5171
5423
  });
5172
5424
  if (options.split && options.split !== "all") items = items.filter((i) => i.split === options.split);
5173
- if (!items.length) throw new Error("No corpus items in the selected split.");
5425
+ if (options.sources) {
5426
+ const sources = new Set(options.sources);
5427
+ items = items.filter((i) => sources.has(i.labelSource));
5428
+ }
5429
+ if (!items.length) throw new Error("No corpus items in the selected split and label sources.");
5174
5430
  const model = options.model ?? "jev-latest";
5175
5431
  const resultsDir = options.resultsDir ?? defaultResultsDir();
5176
5432
  const cache = new AnswerCache(path.join(resultsDir, "cache"), !options.noCache);
@@ -5187,6 +5443,7 @@ const runEval = async (options, ctx = {}) => {
5187
5443
  model,
5188
5444
  recorder
5189
5445
  });
5446
+ if (unknowns.some((u) => u.reason === "auth")) throw new ProviderError("auth", 401);
5190
5447
  const labelSources = {};
5191
5448
  for (const item of items) labelSources[item.labelSource] = (labelSources[item.labelSource] ?? 0) + 1;
5192
5449
  const evals = evaluateRules(items, rules, probabilities, unknowns, skipped);
@@ -5206,7 +5463,7 @@ const runEval = async (options, ctx = {}) => {
5206
5463
  exitCode,
5207
5464
  labelSources,
5208
5465
  referenceCoverage: corpusCoverage(items, rules),
5209
- report: (options.check ? `Regression check: ${exitCode === 0 ? "PASS" : exitCode === 1 ? "FAIL (reference disagreement)" : "INCOMPLETE (missing evaluations)"}. Uses the review threshold.\n` : "") + (labelSources.ai ? `AI-labeled reference: ${labelSources.ai} items. Metrics measure agreement with AI labels, not human judgments.\n` : "") + renderEval(evals, usage, Boolean(options.dryRun), pendingRequests),
5466
+ report: (options.check ? `Regression check: ${exitCode === 0 ? "PASS" : exitCode === 1 ? "FAIL (reference disagreement)" : "INCOMPLETE (missing evaluations)"}. Uses the review threshold.\n` : "") + (labelSources.ai ? `AI-labeled reference: ${labelSources.ai} items. Metrics measure agreement with AI labels, not human judgments.\n` : "") + renderEval(evals, usage, Boolean(options.dryRun), pendingRequests) + (options.disagreements && !options.dryRun ? renderDisagreements(evals, items, rules) : ""),
5210
5467
  rules: evals,
5211
5468
  usage
5212
5469
  };
@@ -5231,6 +5488,7 @@ const promotionEvidence = (items, ruleId, pairs, threshold, floor, minItems) =>
5231
5488
  };
5232
5489
  //#endregion
5233
5490
  //#region src/eval/tune.ts
5491
+ const fromSources = (items, sources) => sources ? items.filter((item) => sources.includes(item.labelSource)) : items;
5234
5492
  const THRESHOLDS = Array.from({ length: 10 }, (_, i) => Math.round((.5 + i * .05) * 100) / 100);
5235
5493
  const decideThreshold = (pairs, currentAct, floor, minItems, review = 0) => {
5236
5494
  const n = pairs.length;
@@ -5284,7 +5542,7 @@ const runTune = async (options, ctx = {}) => {
5284
5542
  only: options.only
5285
5543
  }).filter((r) => r.tier !== "mechanical");
5286
5544
  const knownRuleIds = new Set(loadRules(rulesDir, { allowDraft: true }).map((r) => r.id));
5287
- const allItems = loadCorpus(resolveCorpusDir(options.corpusDir), rules, { knownRuleIds });
5545
+ const allItems = fromSources(loadCorpus(resolveCorpusDir(options.corpusDir), rules, { knownRuleIds }), options.sources);
5288
5546
  const items = allItems.filter((item) => item.split === "dev");
5289
5547
  const model = options.model ?? "jev-latest";
5290
5548
  const resultsDir = options.resultsDir ?? defaultResultsDir();