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.
- package/data/corpus/copywriting.jsonl +4 -4
- package/data/rule-drafts/interaction/interaction-layout-shift.yaml +3 -3
- package/data/rules/copywriting/copywriting-antithesis-contrast.yaml +43 -0
- package/data/rules/copywriting/copywriting-antithesis-frame.yaml +4 -2
- package/data/rules/copywriting/copywriting-artificial-balance.yaml +4 -1
- package/data/rules/copywriting/copywriting-boilerplate-offer.yaml +1 -1
- package/data/rules/copywriting/copywriting-canned-phrasing.yaml +1 -1
- package/data/rules/copywriting/copywriting-claim-without-evidence.yaml +10 -1
- package/data/rules/copywriting/copywriting-copula-avoidance.yaml +1 -1
- package/data/rules/copywriting/copywriting-dead-link.yaml +28 -0
- package/data/rules/copywriting/copywriting-em-dash-in-ui.yaml +23 -0
- package/data/rules/copywriting/copywriting-excessive-hedging.yaml +1 -1
- package/data/rules/copywriting/copywriting-external-arrow-internal-link.yaml +26 -0
- package/data/rules/copywriting/copywriting-fabricated-specificity.yaml +6 -3
- package/data/rules/copywriting/copywriting-generic-framing.yaml +12 -6
- package/data/rules/copywriting/copywriting-generic-language.yaml +12 -4
- package/data/rules/copywriting/copywriting-generic-validation.yaml +1 -1
- package/data/rules/copywriting/copywriting-machine-prose.yaml +4 -1
- package/data/rules/copywriting/copywriting-mechanism-over-outcome.yaml +4 -1
- package/data/rules/copywriting/copywriting-meta-commentary.yaml +1 -1
- package/data/rules/copywriting/copywriting-missing-reasoning-link.yaml +4 -1
- package/data/rules/copywriting/copywriting-over-explanation.yaml +4 -1
- package/data/rules/copywriting/copywriting-participle-tail.yaml +1 -1
- package/data/rules/copywriting/copywriting-prompt-restatement.yaml +5 -2
- package/data/rules/copywriting/copywriting-punctuation-tics.yaml +4 -1
- package/data/rules/copywriting/copywriting-reader-first.yaml +4 -1
- package/data/rules/copywriting/copywriting-repetitive-transitions.yaml +1 -1
- package/data/rules/copywriting/copywriting-semantic-links.yaml +2 -2
- package/data/rules/copywriting/copywriting-slogan-headings.yaml +27 -0
- package/data/rules/copywriting/copywriting-structural-symmetry.yaml +4 -1
- package/data/rules/copywriting/copywriting-tee-up.yaml +4 -2
- package/data/rules/copywriting/copywriting-unexplained-action.yaml +5 -2
- package/data/rules/copywriting/copywriting-unnatural-polish.yaml +8 -5
- package/data/rules/copywriting/copywriting-unverifiable-proof.yaml +1 -1
- package/data/rules/copywriting/copywriting-vague-error.yaml +2 -0
- package/data/rules/craft/craft-atmosphere-gradient.yaml +1 -1
- package/data/rules/craft/craft-decoration-no-role.yaml +2 -1
- package/data/rules/craft/craft-decorative-ordinals.yaml +26 -0
- package/data/rules/craft/craft-faux-product-chrome.yaml +4 -1
- package/data/rules/schema/rule.schema.json +7 -1
- package/dist/cli.js +565 -97
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +34 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +275 -17
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- 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 {
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/analysis/repository.ts","../src/types.ts","../src/scan/profiles.ts","../src/lib/config.ts","../src/lint.ts","../src/rules/load.ts","../src/rules/taxonomy.ts","../src/extract/index.ts","../src/extract/tsx.ts","../src/map/jev.ts","../src/report/json.ts","../src/report/sarif.ts","../src/report/tty.ts","../src/lib/stats.ts","../src/eval/coverage.ts","../src/eval/metrics.ts","../src/eval/corpus.ts","../src/eval/promotion.ts","../src/eval/tune.ts"],"mappings":";UAQiB;EACf;EACA;EACA;EACA;EACA;EACA;EACA,WAAW;EACX;IAAa;MAAS;;IAAmB;MAAO;;;;UAGjC;EACf;EACA;EACA;;UAGe;EACf;IAAW;IAAgB;IAAsB;;EACjD,WAAW;EACX,UAAU;EACV;EACA,YAAY,KACV;;cAYS;WACF;mBACQ;EAEjB,YAAY;EAIZ,QAAQ,cAAc;EAStB,KAAK;EAkBL,OAAO;EAUP,SACE;IACG;IAAc,OAAO;;;;;cCtFf;KASD,iBAAiB;cAEhB;;;;;;cAaA,2BAA2B;KAC5B,mBAAmB;cAElB;KACD,eAAe;cAEd;KACD,mBAAmB;;cAElB,eAAe,OAAO;cAEtB;KACD,qBAAqB;cAEpB;KAcD,kBAAkB;cAEjB;KAcD,eAAe;cAEd;
|
|
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 (
|
|
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();
|