patdown 0.3.1 → 0.5.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/README.md +50 -8
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +51 -26
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/patdown-console-rule-blocks.d.ts +13 -0
- package/dist/patdown-console-rule-blocks.d.ts.map +1 -0
- package/dist/patdown-console-rule-blocks.js +80 -0
- package/dist/patdown-evidence-regions.d.ts +44 -0
- package/dist/patdown-evidence-regions.d.ts.map +1 -0
- package/dist/patdown-evidence-regions.js +124 -0
- package/dist/patdown-github-actions-env.d.ts +6 -0
- package/dist/patdown-github-actions-env.d.ts.map +1 -0
- package/dist/patdown-github-actions-env.js +17 -0
- package/dist/patdown-github-actions-output.d.ts +8 -0
- package/dist/patdown-github-actions-output.d.ts.map +1 -0
- package/dist/patdown-github-actions-output.js +52 -0
- package/dist/patdown-github-actions-summary.d.ts +19 -0
- package/dist/patdown-github-actions-summary.d.ts.map +1 -0
- package/dist/patdown-github-actions-summary.js +171 -0
- package/dist/patdown-glob.d.ts +9 -0
- package/dist/patdown-glob.d.ts.map +1 -0
- package/dist/patdown-glob.js +21 -0
- package/dist/patdown-judge.d.ts +31 -1
- package/dist/patdown-judge.d.ts.map +1 -1
- package/dist/patdown-judge.js +31 -2
- package/dist/patdown-lint-files.d.ts +14 -0
- package/dist/patdown-lint-files.d.ts.map +1 -0
- package/dist/patdown-lint-files.js +75 -0
- package/dist/patdown-lint.d.ts +5 -1
- package/dist/patdown-lint.d.ts.map +1 -1
- package/dist/patdown-lint.js +67 -25
- package/dist/patdown-output.d.ts +26 -7
- package/dist/patdown-output.d.ts.map +1 -1
- package/dist/patdown-output.js +92 -19
- package/dist/patdown-probability-bar.d.ts +3 -0
- package/dist/patdown-probability-bar.d.ts.map +1 -0
- package/dist/patdown-probability-bar.js +29 -0
- package/dist/run-patdown-cli.d.ts +3 -1
- package/dist/run-patdown-cli.d.ts.map +1 -1
- package/dist/run-patdown-cli.js +16 -3
- package/dist/typesafe-judge.d.ts +1 -0
- package/dist/typesafe-judge.d.ts.map +1 -1
- package/dist/typesafe-judge.js +15 -1
- package/package.json +3 -3
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Console, Effect, FileSystem, Layer, Ref } from 'effect';
|
|
2
|
+
import { readPatdownGitHubStepSummaryPath } from '#src/patdown-github-actions-env';
|
|
3
|
+
import { formatPatdownGitHubActionsAnnotations, formatPatdownGitHubActionsSummary, } from '#src/patdown-github-actions-summary';
|
|
4
|
+
import { PatdownOutput, patdownStreamingHumanOutput, } from '#src/patdown-output';
|
|
5
|
+
/**
|
|
6
|
+
* Human stdout plus GitHub annotations and a step-summary heatmap. Intended when GITHUB_ACTIONS and
|
|
7
|
+
* GITHUB_STEP_SUMMARY are set.
|
|
8
|
+
*/
|
|
9
|
+
export const PatdownGitHubActionsOutputLive = Layer.effect(PatdownOutput, Effect.gen(function* () {
|
|
10
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
11
|
+
const state = yield* Ref.make({ results: [] });
|
|
12
|
+
const appendSummary = (markdown) => Effect.gen(function* () {
|
|
13
|
+
const summaryPath = yield* readPatdownGitHubStepSummaryPath;
|
|
14
|
+
if (summaryPath === null)
|
|
15
|
+
return;
|
|
16
|
+
yield* fileSystem.writeFileString(summaryPath, markdown, { flag: 'a' }).pipe(Effect.catch(() => Effect.sync(() => {
|
|
17
|
+
process.stderr.write('patdown: failed to write GitHub step summary\n');
|
|
18
|
+
})));
|
|
19
|
+
});
|
|
20
|
+
const publish = (failed, elapsedMs, results) => Effect.gen(function* () {
|
|
21
|
+
for (const line of formatPatdownGitHubActionsAnnotations(results)) {
|
|
22
|
+
yield* Console.error(line);
|
|
23
|
+
}
|
|
24
|
+
yield* appendSummary(formatPatdownGitHubActionsSummary({
|
|
25
|
+
failed,
|
|
26
|
+
elapsedMs: elapsedMs ?? 0,
|
|
27
|
+
results,
|
|
28
|
+
}));
|
|
29
|
+
});
|
|
30
|
+
const writers = {
|
|
31
|
+
writeAnswer: patdownStreamingHumanOutput.writeAnswer,
|
|
32
|
+
writeRulesDocument: patdownStreamingHumanOutput.writeRulesDocument,
|
|
33
|
+
writeNoFilesMatched: patdownStreamingHumanOutput.writeNoFilesMatched,
|
|
34
|
+
writeLintResult: (result, verbose) => Effect.gen(function* () {
|
|
35
|
+
yield* patdownStreamingHumanOutput.writeLintResult(result, verbose);
|
|
36
|
+
yield* Ref.update(state, (current) => ({
|
|
37
|
+
results: [...current.results, result],
|
|
38
|
+
}));
|
|
39
|
+
}),
|
|
40
|
+
writeLintOk: (elapsedMs) => Effect.gen(function* () {
|
|
41
|
+
const current = yield* Ref.get(state);
|
|
42
|
+
yield* patdownStreamingHumanOutput.writeLintOk(elapsedMs);
|
|
43
|
+
yield* publish(false, elapsedMs, current.results);
|
|
44
|
+
}),
|
|
45
|
+
writeLintFailed: (elapsedMs) => Effect.gen(function* () {
|
|
46
|
+
const current = yield* Ref.get(state);
|
|
47
|
+
yield* patdownStreamingHumanOutput.writeLintFailed(elapsedMs);
|
|
48
|
+
yield* publish(true, elapsedMs, current.results);
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
return writers;
|
|
52
|
+
}));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PatdownLintResult } from '#src/patdown-output';
|
|
2
|
+
/** GitHub only shows about ten workflow-command annotations per step. */
|
|
3
|
+
export declare const patdownGitHubActionsAnnotationLimit = 10;
|
|
4
|
+
/** How far below the cutoff still counts as a near miss in the summary. */
|
|
5
|
+
export declare const patdownGitHubActionsNearMissWindow = 0.2;
|
|
6
|
+
export type PatdownGitHubActionsSummaryInput = {
|
|
7
|
+
readonly failed: boolean;
|
|
8
|
+
readonly elapsedMs: number;
|
|
9
|
+
readonly results: ReadonlyArray<PatdownLintResult>;
|
|
10
|
+
};
|
|
11
|
+
/** Rule definition text for annotations and failure details. */
|
|
12
|
+
export declare function formatPatdownRuleGuidance(result: PatdownLintResult): string;
|
|
13
|
+
/** Near misses are below the cutoff but close enough to show in the heatmap. */
|
|
14
|
+
export declare function patdownLintResultIsNearMiss(result: PatdownLintResult): boolean;
|
|
15
|
+
/** Workflow commands for the PR Files tab. Failures only; capped. */
|
|
16
|
+
export declare function formatPatdownGitHubActionsAnnotations(results: ReadonlyArray<PatdownLintResult>): ReadonlyArray<string>;
|
|
17
|
+
/** Markdown for $GITHUB_STEP_SUMMARY. Hottest files first. */
|
|
18
|
+
export declare function formatPatdownGitHubActionsSummary(input: PatdownGitHubActionsSummaryInput): string;
|
|
19
|
+
//# sourceMappingURL=patdown-github-actions-summary.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patdown-github-actions-summary.d.ts","sourceRoot":"","sources":["../src/patdown-github-actions-summary.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAG5D,yEAAyE;AACzE,eAAO,MAAM,mCAAmC,KAAK,CAAA;AAErD,2EAA2E;AAC3E,eAAO,MAAM,kCAAkC,MAAM,CAAA;AAErD,MAAM,MAAM,gCAAgC,GAAG;IAC9C,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAA;CAClD,CAAA;AA+BD,gEAAgE;AAChE,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAI3E;AAED,gFAAgF;AAChF,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAM9E;AAgCD,qEAAqE;AACrE,wBAAgB,qCAAqC,CACpD,OAAO,EAAE,aAAa,CAAC,iBAAiB,CAAC,GACvC,aAAa,CAAC,MAAM,CAAC,CA+BvB;AAsGD,8DAA8D;AAC9D,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,gCAAgC,GAAG,MAAM,CAiCjG"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { patdownEvidenceMinConfidence } from '#src/patdown-evidence-regions';
|
|
2
|
+
import { formatPatdownProbabilityBar } from '#src/patdown-probability-bar';
|
|
3
|
+
/** GitHub only shows about ten workflow-command annotations per step. */
|
|
4
|
+
export const patdownGitHubActionsAnnotationLimit = 10;
|
|
5
|
+
/** How far below the cutoff still counts as a near miss in the summary. */
|
|
6
|
+
export const patdownGitHubActionsNearMissWindow = 0.2;
|
|
7
|
+
function escapePatdownGitHubActionsData(value) {
|
|
8
|
+
return value.replace(/%/gu, '%25').replace(/\r/gu, '%0D').replace(/\n/gu, '%0A');
|
|
9
|
+
}
|
|
10
|
+
function escapePatdownGitHubActionsProperty(value) {
|
|
11
|
+
return escapePatdownGitHubActionsData(value).replace(/:/gu, '%3A').replace(/,/gu, '%2C');
|
|
12
|
+
}
|
|
13
|
+
function formatPatdownElapsedLabel(elapsedMs) {
|
|
14
|
+
if (elapsedMs >= 10_000)
|
|
15
|
+
return `${(elapsedMs / 1000).toFixed(1)}s`;
|
|
16
|
+
return `${String(elapsedMs)}ms`;
|
|
17
|
+
}
|
|
18
|
+
function formatPatdownProbabilityCell(result) {
|
|
19
|
+
const bar = formatPatdownProbabilityBar(result.violationProbability);
|
|
20
|
+
const score = result.violationProbability.toFixed(2);
|
|
21
|
+
if (result.violated)
|
|
22
|
+
return `${bar} **${score}** ❌`;
|
|
23
|
+
return `${bar} ${score}`;
|
|
24
|
+
}
|
|
25
|
+
/** Rule definition text for annotations and failure details. */
|
|
26
|
+
export function formatPatdownRuleGuidance(result) {
|
|
27
|
+
const globs = result.ruleGlobs.length === 0 ? '*' : result.ruleGlobs.join(' ');
|
|
28
|
+
return [`# ${result.ruleTitle}`, `globs: ${globs}`, '', result.ruleBody].join('\n');
|
|
29
|
+
}
|
|
30
|
+
/** Near misses are below the cutoff but close enough to show in the heatmap. */
|
|
31
|
+
export function patdownLintResultIsNearMiss(result) {
|
|
32
|
+
if (result.violated)
|
|
33
|
+
return false;
|
|
34
|
+
const floor = Math.max(0, result.yesThreshold - patdownGitHubActionsNearMissWindow);
|
|
35
|
+
return result.violationProbability >= floor;
|
|
36
|
+
}
|
|
37
|
+
function patdownLintResultHeat(result) {
|
|
38
|
+
if (result.violated)
|
|
39
|
+
return 2 + result.violationProbability;
|
|
40
|
+
if (patdownLintResultIsNearMiss(result))
|
|
41
|
+
return 1 + result.violationProbability;
|
|
42
|
+
return result.violationProbability;
|
|
43
|
+
}
|
|
44
|
+
function comparePatdownLintResultsHottestFirst(left, right) {
|
|
45
|
+
if (left.violated !== right.violated)
|
|
46
|
+
return left.violated ? -1 : 1;
|
|
47
|
+
const leftNear = patdownLintResultIsNearMiss(left);
|
|
48
|
+
const rightNear = patdownLintResultIsNearMiss(right);
|
|
49
|
+
if (leftNear !== rightNear)
|
|
50
|
+
return leftNear ? -1 : 1;
|
|
51
|
+
if (right.violationProbability !== left.violationProbability) {
|
|
52
|
+
return right.violationProbability - left.violationProbability;
|
|
53
|
+
}
|
|
54
|
+
const byFile = left.filePath.localeCompare(right.filePath);
|
|
55
|
+
if (byFile !== 0)
|
|
56
|
+
return byFile;
|
|
57
|
+
return left.ruleTitle.localeCompare(right.ruleTitle);
|
|
58
|
+
}
|
|
59
|
+
/** Workflow commands for the PR Files tab. Failures only; capped. */
|
|
60
|
+
export function formatPatdownGitHubActionsAnnotations(results) {
|
|
61
|
+
const failures = results
|
|
62
|
+
.filter((result) => result.violated)
|
|
63
|
+
.toSorted(comparePatdownLintResultsHottestFirst)
|
|
64
|
+
.slice(0, patdownGitHubActionsAnnotationLimit);
|
|
65
|
+
return failures.map((result) => {
|
|
66
|
+
const bar = formatPatdownProbabilityBar(result.violationProbability);
|
|
67
|
+
const evidence = result.evidence !== undefined && result.evidence.confidence >= patdownEvidenceMinConfidence
|
|
68
|
+
? result.evidence
|
|
69
|
+
: undefined;
|
|
70
|
+
const span = evidence === undefined
|
|
71
|
+
? ''
|
|
72
|
+
: ` lines ${String(evidence.startLine)}-${String(evidence.endLine)}`;
|
|
73
|
+
const headline = `${bar} P(yes) ${String(result.violationProbability)} exceeds cutoff >${String(result.yesThreshold)}${span}`;
|
|
74
|
+
const message = `${headline}\n\n${formatPatdownRuleGuidance(result)}`;
|
|
75
|
+
const file = escapePatdownGitHubActionsProperty(result.filePath);
|
|
76
|
+
const title = escapePatdownGitHubActionsProperty(`patdown: ${result.ruleTitle}`);
|
|
77
|
+
const line = evidence === undefined
|
|
78
|
+
? 'line=1'
|
|
79
|
+
: `line=${String(evidence.startLine)},endLine=${String(evidence.endLine)}`;
|
|
80
|
+
return `::error file=${file},${line},title=${title}::${escapePatdownGitHubActionsData(message)}`;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function collectPatdownSummaryAxes(results) {
|
|
84
|
+
const files = [...new Set(results.map((result) => result.filePath))].toSorted();
|
|
85
|
+
const rules = [...new Set(results.map((result) => result.ruleTitle))].toSorted();
|
|
86
|
+
const byFileRule = new Map();
|
|
87
|
+
for (const result of results) {
|
|
88
|
+
byFileRule.set(`${result.filePath}\0${result.ruleTitle}`, result);
|
|
89
|
+
}
|
|
90
|
+
return { files, rules, byFileRule };
|
|
91
|
+
}
|
|
92
|
+
function hottestPatdownFileScore(results, filePath) {
|
|
93
|
+
let hottest = 0;
|
|
94
|
+
for (const result of results) {
|
|
95
|
+
if (result.filePath !== filePath)
|
|
96
|
+
continue;
|
|
97
|
+
hottest = Math.max(hottest, patdownLintResultHeat(result));
|
|
98
|
+
}
|
|
99
|
+
return hottest;
|
|
100
|
+
}
|
|
101
|
+
function patdownFileRowFailed(byFileRule, filePath, rules) {
|
|
102
|
+
for (const ruleTitle of rules) {
|
|
103
|
+
const result = byFileRule.get(`${filePath}\0${ruleTitle}`);
|
|
104
|
+
if (result?.violated === true)
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
function formatPatdownHeatmapTable(results) {
|
|
110
|
+
const { files, rules, byFileRule } = collectPatdownSummaryAxes(results);
|
|
111
|
+
if (files.length === 0 || rules.length === 0)
|
|
112
|
+
return '_No judgments._';
|
|
113
|
+
const header = `| file | status | ${rules.map((rule) => rule.replace(/\|/gu, '\\|')).join(' | ')} |`;
|
|
114
|
+
const divider = `|---|---|${rules.map(() => '---').join('|')}|`;
|
|
115
|
+
const fileOrder = [...files].toSorted((left, right) => {
|
|
116
|
+
const leftHot = hottestPatdownFileScore(results, left);
|
|
117
|
+
const rightHot = hottestPatdownFileScore(results, right);
|
|
118
|
+
if (rightHot !== leftHot)
|
|
119
|
+
return rightHot - leftHot;
|
|
120
|
+
return left.localeCompare(right);
|
|
121
|
+
});
|
|
122
|
+
const rows = fileOrder.map((filePath) => {
|
|
123
|
+
const failed = patdownFileRowFailed(byFileRule, filePath, rules);
|
|
124
|
+
const cells = rules.map((ruleTitle) => {
|
|
125
|
+
const result = byFileRule.get(`${filePath}\0${ruleTitle}`);
|
|
126
|
+
if (result === undefined)
|
|
127
|
+
return '—';
|
|
128
|
+
return formatPatdownProbabilityCell(result);
|
|
129
|
+
});
|
|
130
|
+
return `| \`${filePath.replace(/\|/gu, '\\|')}\` | ${failed ? '❌' : '✅'} | ${cells.join(' | ')} |`;
|
|
131
|
+
});
|
|
132
|
+
return [header, divider, ...rows].join('\n');
|
|
133
|
+
}
|
|
134
|
+
function formatPatdownFailureDetails(results) {
|
|
135
|
+
const failures = results
|
|
136
|
+
.filter((result) => result.violated)
|
|
137
|
+
.toSorted(comparePatdownLintResultsHottestFirst);
|
|
138
|
+
if (failures.length === 0)
|
|
139
|
+
return '';
|
|
140
|
+
const lines = ['## Failures', ''];
|
|
141
|
+
for (const result of failures) {
|
|
142
|
+
const bar = formatPatdownProbabilityBar(result.violationProbability);
|
|
143
|
+
lines.push(`### \`${result.filePath}\` · ${result.ruleTitle}`, '', `${bar} estimated P(yes) **${String(result.violationProbability)}** exceeds cutoff \`>${String(result.yesThreshold)}\` · ${formatPatdownElapsedLabel(result.elapsedMs)}`, '', formatPatdownRuleGuidance(result), '');
|
|
144
|
+
}
|
|
145
|
+
return lines.join('\n');
|
|
146
|
+
}
|
|
147
|
+
/** Markdown for $GITHUB_STEP_SUMMARY. Hottest files first. */
|
|
148
|
+
export function formatPatdownGitHubActionsSummary(input) {
|
|
149
|
+
const failedCount = input.results.filter((result) => result.violated).length;
|
|
150
|
+
const passedCount = input.results.length - failedCount;
|
|
151
|
+
const status = input.failed || failedCount > 0 ? 'failed' : 'passed';
|
|
152
|
+
const lines = [
|
|
153
|
+
`# patdown ${status}`,
|
|
154
|
+
'',
|
|
155
|
+
`${String(passedCount)} passed · ${String(failedCount)} failed · ${formatPatdownElapsedLabel(input.elapsedMs)}`,
|
|
156
|
+
'',
|
|
157
|
+
'## Heatmap',
|
|
158
|
+
'',
|
|
159
|
+
formatPatdownHeatmapTable(input.results),
|
|
160
|
+
'',
|
|
161
|
+
];
|
|
162
|
+
const failureDetails = formatPatdownFailureDetails(input.results);
|
|
163
|
+
if (failureDetails.length > 0) {
|
|
164
|
+
lines.push(failureDetails);
|
|
165
|
+
}
|
|
166
|
+
const omitted = failedCount - patdownGitHubActionsAnnotationLimit;
|
|
167
|
+
if (omitted > 0) {
|
|
168
|
+
lines.push('', `_${String(omitted)} more failure(s) are in this summary only; GitHub caps workflow annotations per step._`, '');
|
|
169
|
+
}
|
|
170
|
+
return lines.join('\n');
|
|
171
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Directories skipped for every rule glob and every --files path. */
|
|
2
|
+
export declare const patdownGlobExcludes: readonly ['**/.git/**', '**/.turbo/**', '**/coverage/**', '**/dist/**', '**/node_modules/**'];
|
|
3
|
+
/** Empty rule globs mean the whole tree. */
|
|
4
|
+
export declare function patdownGlobPatterns(globs: ReadonlyArray<string>): ReadonlyArray<string>;
|
|
5
|
+
/** True when a cwd-relative path is under a skipped directory. */
|
|
6
|
+
export declare function patdownPathIsExcluded(relativePath: string): boolean;
|
|
7
|
+
/** True when a cwd-relative path matches any rule glob after the empty-glob default. */
|
|
8
|
+
export declare function patdownPathMatchesRuleGlobs(relativePath: string, globs: ReadonlyArray<string>): boolean;
|
|
9
|
+
//# sourceMappingURL=patdown-glob.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patdown-glob.d.ts","sourceRoot":"","sources":["../src/patdown-glob.ts"],"names":[],"mappings":"AAEA,sEAAsE;AACtE,eAAO,MAAM,mBAAmB,YAC/B,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,oBAAoB,CACX,CAAA;AAEV,4CAA4C;AAC5C,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,aAAa,CAAC,MAAM,CAAC,CAEvF;AAED,kEAAkE;AAClE,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAEnE;AAED,wFAAwF;AACxF,wBAAgB,2BAA2B,CAC1C,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,GAC1B,OAAO,CAET"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { matchesGlob } from 'node:path';
|
|
2
|
+
/** Directories skipped for every rule glob and every --files path. */
|
|
3
|
+
export const patdownGlobExcludes = [
|
|
4
|
+
'**/.git/**',
|
|
5
|
+
'**/.turbo/**',
|
|
6
|
+
'**/coverage/**',
|
|
7
|
+
'**/dist/**',
|
|
8
|
+
'**/node_modules/**',
|
|
9
|
+
];
|
|
10
|
+
/** Empty rule globs mean the whole tree. */
|
|
11
|
+
export function patdownGlobPatterns(globs) {
|
|
12
|
+
return globs.length === 0 ? ['**/*'] : globs;
|
|
13
|
+
}
|
|
14
|
+
/** True when a cwd-relative path is under a skipped directory. */
|
|
15
|
+
export function patdownPathIsExcluded(relativePath) {
|
|
16
|
+
return patdownGlobExcludes.some((pattern) => matchesGlob(relativePath, pattern));
|
|
17
|
+
}
|
|
18
|
+
/** True when a cwd-relative path matches any rule glob after the empty-glob default. */
|
|
19
|
+
export function patdownPathMatchesRuleGlobs(relativePath, globs) {
|
|
20
|
+
return patdownGlobPatterns(globs).some((pattern) => matchesGlob(relativePath, pattern));
|
|
21
|
+
}
|
package/dist/patdown-judge.d.ts
CHANGED
|
@@ -6,6 +6,23 @@ export declare const PatdownJudgmentSchema: Schema.Struct<{
|
|
|
6
6
|
}>;
|
|
7
7
|
/** A judge estimates the probability that the question is true of the supplied text. */
|
|
8
8
|
export type PatdownJudgment = typeof PatdownJudgmentSchema.Type;
|
|
9
|
+
/** A validated judgment plus how long the judge call took. */
|
|
10
|
+
export type PatdownTimedJudgment = {
|
|
11
|
+
readonly judgment: PatdownJudgment;
|
|
12
|
+
readonly elapsedMs: number;
|
|
13
|
+
};
|
|
14
|
+
/** Region picked by a FAIL-only evidence Choice. Line numbers come from our slice map. */
|
|
15
|
+
export type PatdownEvidenceChoice = {
|
|
16
|
+
readonly regionId: string;
|
|
17
|
+
readonly confidence: number;
|
|
18
|
+
};
|
|
19
|
+
/** Resolved line span after mapping a Choice label onto a known region. */
|
|
20
|
+
export type PatdownEvidenceLocation = {
|
|
21
|
+
readonly startLine: number;
|
|
22
|
+
readonly endLine: number;
|
|
23
|
+
readonly regionId: string;
|
|
24
|
+
readonly confidence: number;
|
|
25
|
+
};
|
|
9
26
|
declare const PatdownJudgeFailed_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
10
27
|
readonly _tag: "PatdownJudgeFailed";
|
|
11
28
|
} & Readonly<A>;
|
|
@@ -16,6 +33,11 @@ export declare class PatdownJudgeFailed extends PatdownJudgeFailed_base<{
|
|
|
16
33
|
}
|
|
17
34
|
declare const PatdownJudge_base: Context.ServiceClass<PatdownJudge, "@patdown/cli/PatdownJudge", {
|
|
18
35
|
readonly ask: (question: string, inputText: string) => Effect.Effect<PatdownJudgment, PatdownJudgeFailed>;
|
|
36
|
+
/**
|
|
37
|
+
* Optional FAIL-only locator. Returns a line span when the provider can choose among candidate
|
|
38
|
+
* regions. Missing method means file-level annotations only.
|
|
39
|
+
*/
|
|
40
|
+
readonly locateEvidence?: (question: string, inputText: string, criteria: Readonly<Record<string, string>>) => Effect.Effect<PatdownEvidenceChoice | null, PatdownJudgeFailed>;
|
|
19
41
|
}>;
|
|
20
42
|
/** Swappable judge service. Providers supply their own transport dependencies internally. */
|
|
21
43
|
export declare class PatdownJudge extends PatdownJudge_base {
|
|
@@ -25,6 +47,14 @@ export declare const patdownYesThreshold = 0.85;
|
|
|
25
47
|
/** Applies a cutoff to a validated probability. Equality is not yes. */
|
|
26
48
|
export declare function patdownJudgmentIsYes(judgment: PatdownJudgment, yesThreshold?: PatdownYesThreshold): boolean;
|
|
27
49
|
/** Validates custom judge responses at the service boundary before applying policy or printing. */
|
|
28
|
-
export declare function askPatdownJudge(question: string, inputText: string): Effect.Effect<
|
|
50
|
+
export declare function askPatdownJudge(question: string, inputText: string): Effect.Effect<PatdownTimedJudgment, PatdownJudgeFailed, PatdownJudge>;
|
|
51
|
+
/**
|
|
52
|
+
* Asks the optional evidence locator. Returns null when the judge has no locator, chooses noMatch,
|
|
53
|
+
* or returns an unknown region id.
|
|
54
|
+
*/
|
|
55
|
+
export declare function locatePatdownEvidence(question: string, inputText: string, criteria: Readonly<Record<string, string>>, regionsById: ReadonlyMap<string, {
|
|
56
|
+
readonly startLine: number;
|
|
57
|
+
readonly endLine: number;
|
|
58
|
+
}>): Effect.Effect<PatdownEvidenceLocation | null, PatdownJudgeFailed, PatdownJudge>;
|
|
29
59
|
export {};
|
|
30
60
|
//# sourceMappingURL=patdown-judge.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"patdown-judge.d.ts","sourceRoot":"","sources":["../src/patdown-judge.ts"],"names":[],"mappings":"AAAA,OAAO,EAGN,KAAK,mBAAmB,EACxB,MAAM,gBAAgB,CAAA;AACvB,OAAO,
|
|
1
|
+
{"version":3,"file":"patdown-judge.d.ts","sourceRoot":"","sources":["../src/patdown-judge.ts"],"names":[],"mappings":"AAAA,OAAO,EAGN,KAAK,mBAAmB,EACxB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAS,OAAO,EAAQ,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAE7D,0FAA0F;AAC1F,eAAO,MAAM,qBAAqB;;EAEhC,CAAA;AAEF,wFAAwF;AACxF,MAAM,MAAM,eAAe,GAAG,OAAO,qBAAqB,CAAC,IAAI,CAAA;AAE/D,8DAA8D;AAC9D,MAAM,MAAM,oBAAoB,GAAG;IAClC,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAA;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC1B,CAAA;AAED,0FAA0F;AAC1F,MAAM,MAAM,qBAAqB,GAAG;IACnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAC3B,CAAA;AAED,2EAA2E;AAC3E,MAAM,MAAM,uBAAuB,GAAG;IACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAC3B,CAAA;;;;AAED,2EAA2E;AAC3E,qBAAa,kBAAmB,SAAQ,wBAAuC;IAC9E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACxB,CAAC;CAAG;;kBAMW,CACb,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,KACb,MAAM,CAAC,MAAM,CAAC,eAAe,EAAE,kBAAkB,CAAC;IACvD;;;OAGG;8BACuB,CACzB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,KACtC,MAAM,CAAC,MAAM,CAAC,qBAAqB,GAAG,IAAI,EAAE,kBAAkB,CAAC;;AAhBtE,6FAA6F;AAC7F,qBAAa,YAAa,SAAQ,iBAiBF;CAAG;AAEnC,uFAAuF;AACvF,eAAO,MAAM,mBAAmB,OAA6B,CAAA;AAE7D,wEAAwE;AACxE,wBAAgB,oBAAoB,CACnC,QAAQ,EAAE,eAAe,EACzB,YAAY,GAAE,mBAAgD,GAC5D,OAAO,CAET;AAED,mGAAmG;AACnG,wBAAgB,eAAe,CAC9B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GACf,MAAM,CAAC,MAAM,CAAC,oBAAoB,EAAE,kBAAkB,EAAE,YAAY,CAAC,CAmBvE;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACpC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAC1C,WAAW,EAAE,WAAW,CAAC,MAAM,EAAE;IAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,GACxF,MAAM,CAAC,MAAM,CAAC,uBAAuB,GAAG,IAAI,EAAE,kBAAkB,EAAE,YAAY,CAAC,CAqBjF"}
|
package/dist/patdown-judge.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defaultPatdownYesThreshold, patdownJudgmentIsYes as comparePatdownYesProbability, } from '@patdown/rules';
|
|
2
|
-
import { Context, Data, Effect, Schema } from 'effect';
|
|
2
|
+
import { Clock, Context, Data, Effect, Schema } from 'effect';
|
|
3
3
|
/** Provider-neutral estimate. This is P(yes), not confidence in whichever answer wins. */
|
|
4
4
|
export const PatdownJudgmentSchema = Schema.Struct({
|
|
5
5
|
yesProbability: Schema.Finite.check(Schema.isBetween({ minimum: 0, maximum: 1 })),
|
|
@@ -20,7 +20,36 @@ export function patdownJudgmentIsYes(judgment, yesThreshold = defaultPatdownYesT
|
|
|
20
20
|
export function askPatdownJudge(question, inputText) {
|
|
21
21
|
return Effect.gen(function* () {
|
|
22
22
|
const judge = yield* PatdownJudge;
|
|
23
|
+
const startedAt = yield* Clock.currentTimeMillis;
|
|
23
24
|
const answer = yield* judge.ask(question, inputText);
|
|
24
|
-
|
|
25
|
+
const finishedAt = yield* Clock.currentTimeMillis;
|
|
26
|
+
const judgment = yield* Schema.decodeEffect(PatdownJudgmentSchema)(answer).pipe(Effect.mapError(() => new PatdownJudgeFailed({ message: 'patdown: judge returned an invalid yes probability' })));
|
|
27
|
+
return {
|
|
28
|
+
judgment,
|
|
29
|
+
elapsedMs: Math.max(0, finishedAt - startedAt),
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Asks the optional evidence locator. Returns null when the judge has no locator, chooses noMatch,
|
|
35
|
+
* or returns an unknown region id.
|
|
36
|
+
*/
|
|
37
|
+
export function locatePatdownEvidence(question, inputText, criteria, regionsById) {
|
|
38
|
+
return Effect.gen(function* () {
|
|
39
|
+
const judge = yield* PatdownJudge;
|
|
40
|
+
if (judge.locateEvidence === undefined)
|
|
41
|
+
return null;
|
|
42
|
+
const chosen = yield* judge.locateEvidence(question, inputText, criteria);
|
|
43
|
+
if (chosen === null)
|
|
44
|
+
return null;
|
|
45
|
+
const region = regionsById.get(chosen.regionId);
|
|
46
|
+
if (region === undefined)
|
|
47
|
+
return null;
|
|
48
|
+
return {
|
|
49
|
+
startLine: region.startLine,
|
|
50
|
+
endLine: region.endLine,
|
|
51
|
+
regionId: chosen.regionId,
|
|
52
|
+
confidence: chosen.confidence,
|
|
53
|
+
};
|
|
25
54
|
});
|
|
26
55
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Effect, FileSystem, Option } from 'effect';
|
|
2
|
+
import { PatdownJudgeFailed } from '#src/patdown-judge';
|
|
3
|
+
/** Explicit path list from --files and/or --files-from, already cwd-relative and deduped. */
|
|
4
|
+
export type PatdownLintFileSelection = {
|
|
5
|
+
readonly relativePaths: ReadonlyArray<string>;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Builds the optional lint path list. --files and --files-from may be combined. Missing
|
|
9
|
+
* --files-from paths fail. Empty selections stay empty and still succeed.
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolvePatdownLintFileSelection(cwd: string, files: ReadonlyArray<string>, filesFrom: Option.Option<string>): Effect.Effect<PatdownLintFileSelection | null, PatdownJudgeFailed, FileSystem.FileSystem>;
|
|
12
|
+
/** Intersects an explicit path list with one rule's globs. Absolute paths are for reading. */
|
|
13
|
+
export declare function selectPatdownRuleFiles(cwd: string, selection: PatdownLintFileSelection | null, globs: ReadonlyArray<string>, globbedAbsolutePaths: ReadonlyArray<string>): ReadonlyArray<string>;
|
|
14
|
+
//# sourceMappingURL=patdown-lint-files.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patdown-lint-files.d.ts","sourceRoot":"","sources":["../src/patdown-lint-files.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAGnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAEvD,6FAA6F;AAC7F,MAAM,MAAM,wBAAwB,GAAG;IACtC,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;CAC7C,CAAA;AAqCD;;;GAGG;AACH,wBAAgB,+BAA+B,CAC9C,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,EAC5B,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,GAC9B,MAAM,CAAC,MAAM,CAAC,wBAAwB,GAAG,IAAI,EAAE,kBAAkB,EAAE,UAAU,CAAC,UAAU,CAAC,CAqC3F;AAED,8FAA8F;AAC9F,wBAAgB,sBAAsB,CACrC,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,wBAAwB,GAAG,IAAI,EAC1C,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,EAC5B,oBAAoB,EAAE,aAAa,CAAC,MAAM,CAAC,GACzC,aAAa,CAAC,MAAM,CAAC,CAcvB"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
2
|
+
import { Effect, FileSystem, Option } from 'effect';
|
|
3
|
+
import { patdownPathIsExcluded, patdownPathMatchesRuleGlobs } from '#src/patdown-glob';
|
|
4
|
+
import { PatdownJudgeFailed } from '#src/patdown-judge';
|
|
5
|
+
function normalizePatdownRelativePath(cwd, rawPath) {
|
|
6
|
+
const trimmed = rawPath.trim();
|
|
7
|
+
if (trimmed.length === 0)
|
|
8
|
+
return null;
|
|
9
|
+
if (trimmed.startsWith('#'))
|
|
10
|
+
return null;
|
|
11
|
+
const absolutePath = isAbsolute(trimmed) ? resolve(trimmed) : resolve(cwd, trimmed);
|
|
12
|
+
const relativePath = relative(cwd, absolutePath).split(sep).join('/');
|
|
13
|
+
if (relativePath === '')
|
|
14
|
+
return null;
|
|
15
|
+
if (relativePath === '..' || relativePath.startsWith('../')) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
if (patdownPathIsExcluded(relativePath))
|
|
19
|
+
return null;
|
|
20
|
+
return relativePath;
|
|
21
|
+
}
|
|
22
|
+
function parsePatdownFilesFromText(cwd, text) {
|
|
23
|
+
const paths = [];
|
|
24
|
+
for (const line of text.split(/\r?\n/u)) {
|
|
25
|
+
const relativePath = normalizePatdownRelativePath(cwd, line);
|
|
26
|
+
if (relativePath === null)
|
|
27
|
+
continue;
|
|
28
|
+
paths.push(relativePath);
|
|
29
|
+
}
|
|
30
|
+
return [...new Set(paths)].toSorted();
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Builds the optional lint path list. --files and --files-from may be combined. Missing
|
|
34
|
+
* --files-from paths fail. Empty selections stay empty and still succeed.
|
|
35
|
+
*/
|
|
36
|
+
export function resolvePatdownLintFileSelection(cwd, files, filesFrom) {
|
|
37
|
+
return Effect.gen(function* () {
|
|
38
|
+
if (files.length === 0 && Option.isNone(filesFrom))
|
|
39
|
+
return null;
|
|
40
|
+
const paths = [];
|
|
41
|
+
for (const file of files) {
|
|
42
|
+
const relativePath = normalizePatdownRelativePath(cwd, file);
|
|
43
|
+
if (relativePath === null)
|
|
44
|
+
continue;
|
|
45
|
+
paths.push(relativePath);
|
|
46
|
+
}
|
|
47
|
+
if (Option.isSome(filesFrom)) {
|
|
48
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
49
|
+
const listPath = isAbsolute(filesFrom.value)
|
|
50
|
+
? resolve(filesFrom.value)
|
|
51
|
+
: resolve(cwd, filesFrom.value);
|
|
52
|
+
const text = yield* fileSystem.readFileString(listPath).pipe(Effect.mapError(() => new PatdownJudgeFailed({
|
|
53
|
+
message: `patdown: failed to read --files-from ${relative(cwd, listPath).split(sep).join('/') || filesFrom.value}`,
|
|
54
|
+
})));
|
|
55
|
+
paths.push(...parsePatdownFilesFromText(cwd, text));
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
relativePaths: [...new Set(paths)].toSorted(),
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** Intersects an explicit path list with one rule's globs. Absolute paths are for reading. */
|
|
63
|
+
export function selectPatdownRuleFiles(cwd, selection, globs, globbedAbsolutePaths) {
|
|
64
|
+
if (selection === null)
|
|
65
|
+
return globbedAbsolutePaths;
|
|
66
|
+
const absolutePaths = [];
|
|
67
|
+
for (const relativePath of selection.relativePaths) {
|
|
68
|
+
if (!patdownPathMatchesRuleGlobs(relativePath, globs))
|
|
69
|
+
continue;
|
|
70
|
+
if (patdownPathIsExcluded(relativePath))
|
|
71
|
+
continue;
|
|
72
|
+
absolutePaths.push(resolve(cwd, relativePath));
|
|
73
|
+
}
|
|
74
|
+
return absolutePaths.toSorted();
|
|
75
|
+
}
|
package/dist/patdown-lint.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { PatdownYesThresholdInvalid, type PatdownRulesDocument, type PatdownYesThreshold } from '@patdown/rules';
|
|
2
2
|
import { Effect, FileSystem, Path } from 'effect';
|
|
3
3
|
import { PatdownJudge, PatdownJudgeFailed } from '#src/patdown-judge';
|
|
4
|
+
import { type PatdownLintFileSelection } from '#src/patdown-lint-files';
|
|
4
5
|
import { PatdownOutput } from '#src/patdown-output';
|
|
5
6
|
/** Lint files matched by each rule's globs. A yes judgment means a violation. */
|
|
6
|
-
export declare function runPatdownLint(document: PatdownRulesDocument, verbose?: boolean, yesThreshold?: PatdownYesThreshold
|
|
7
|
+
export declare function runPatdownLint(document: PatdownRulesDocument, verbose?: boolean, yesThreshold?: PatdownYesThreshold, selection?: PatdownLintFileSelection | null): Effect.Effect<{
|
|
8
|
+
readonly failed: boolean;
|
|
9
|
+
readonly elapsedMs: number;
|
|
10
|
+
}, PatdownJudgeFailed | PatdownYesThresholdInvalid, FileSystem.FileSystem | PatdownJudge | Path.Path | PatdownOutput>;
|
|
7
11
|
//# sourceMappingURL=patdown-lint.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"patdown-lint.d.ts","sourceRoot":"","sources":["../src/patdown-lint.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,0BAA0B,EAE1B,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,MAAM,gBAAgB,CAAA;AACvB,OAAO,
|
|
1
|
+
{"version":3,"file":"patdown-lint.d.ts","sourceRoot":"","sources":["../src/patdown-lint.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,0BAA0B,EAE1B,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAS,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AASxD,OAAO,EACN,YAAY,EACZ,kBAAkB,EAIlB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAA0B,KAAK,wBAAwB,EAAE,MAAM,yBAAyB,CAAA;AAC/F,OAAO,EAAE,aAAa,EAAgC,MAAM,qBAAqB,CAAA;AA+LjF,iFAAiF;AACjF,wBAAgB,cAAc,CAC7B,QAAQ,EAAE,oBAAoB,EAC9B,OAAO,GAAE,OAAe,EACxB,YAAY,GAAE,mBAAgD,EAC9D,SAAS,GAAE,wBAAwB,GAAG,IAAW,GAC/C,MAAM,CAAC,MAAM,CACf;IAAE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,EACxD,kBAAkB,GAAG,0BAA0B,EAC/C,UAAU,CAAC,UAAU,GAAG,YAAY,GAAG,IAAI,CAAC,IAAI,GAAG,aAAa,CAChE,CAyBA"}
|
package/dist/patdown-lint.js
CHANGED
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
import { defaultPatdownYesThreshold, PatdownYesThresholdInvalid, } from '@patdown/rules';
|
|
2
|
-
import { Effect, FileSystem, Path } from 'effect';
|
|
3
|
-
import {
|
|
2
|
+
import { Clock, Effect, FileSystem, Path } from 'effect';
|
|
3
|
+
import { formatPatdownEvidenceChoiceState, patdownEvidenceChoiceCriteria, patdownEvidenceChoiceInstructions, splitPatdownEvidenceCandidates, } from '#src/patdown-evidence-regions';
|
|
4
|
+
import { patdownGlobExcludes, patdownGlobPatterns } from '#src/patdown-glob';
|
|
5
|
+
import { PatdownJudge, PatdownJudgeFailed, askPatdownJudge, locatePatdownEvidence, patdownJudgmentIsYes, } from '#src/patdown-judge';
|
|
6
|
+
import { selectPatdownRuleFiles } from '#src/patdown-lint-files';
|
|
4
7
|
import { PatdownOutput } from '#src/patdown-output';
|
|
5
8
|
import { decodePatdownRuleYesThreshold } from '#src/patdown-yes-threshold-config';
|
|
6
|
-
const patdownGlobExcludes = [
|
|
7
|
-
'**/.git/**',
|
|
8
|
-
'**/.turbo/**',
|
|
9
|
-
'**/coverage/**',
|
|
10
|
-
'**/dist/**',
|
|
11
|
-
'**/node_modules/**',
|
|
12
|
-
];
|
|
13
|
-
function patdownGlobPatterns(globs) {
|
|
14
|
-
return globs.length === 0 ? ['**/*'] : globs;
|
|
15
|
-
}
|
|
16
9
|
function patdownViolationInstructions(rule) {
|
|
17
10
|
return [
|
|
18
11
|
'Does this file violate the following patdown rule? Answer yes only if there is a clear violation.',
|
|
@@ -51,39 +44,88 @@ function lintPatdownRuleFile(rule, filePath, options) {
|
|
|
51
44
|
const contents = yield* fileSystem.readFileString(filePath).pipe(Effect.mapError(() => new PatdownJudgeFailed({
|
|
52
45
|
message: `patdown: failed to read ${relativePath}`,
|
|
53
46
|
})));
|
|
54
|
-
const
|
|
55
|
-
const failed = patdownJudgmentIsYes(
|
|
56
|
-
|
|
47
|
+
const timed = yield* askPatdownJudge(patdownViolationInstructions(rule), patdownFileState(relativePath, contents));
|
|
48
|
+
const failed = patdownJudgmentIsYes(timed.judgment, options.yesThreshold);
|
|
49
|
+
let evidence;
|
|
50
|
+
let elapsedMs = timed.elapsedMs;
|
|
51
|
+
if (failed) {
|
|
52
|
+
const evidenceStartedAt = yield* Clock.currentTimeMillis;
|
|
53
|
+
const candidates = splitPatdownEvidenceCandidates(contents);
|
|
54
|
+
const candidatesById = new Map(candidates.map((candidate) => [
|
|
55
|
+
candidate.id,
|
|
56
|
+
{ startLine: candidate.startLine, endLine: candidate.endLine },
|
|
57
|
+
]));
|
|
58
|
+
const located = yield* locatePatdownEvidence(patdownEvidenceChoiceInstructions(), formatPatdownEvidenceChoiceState({
|
|
59
|
+
relativePath,
|
|
60
|
+
ruleTitle: rule.patdownRuleTitle,
|
|
61
|
+
ruleBody: rule.patdownRuleBody,
|
|
62
|
+
violationProbability: timed.judgment.yesProbability,
|
|
63
|
+
contents,
|
|
64
|
+
candidates,
|
|
65
|
+
}), patdownEvidenceChoiceCriteria(candidates), candidatesById).pipe(Effect.catchTag('PatdownJudgeFailed', () => Effect.succeed(null)));
|
|
66
|
+
const evidenceFinishedAt = yield* Clock.currentTimeMillis;
|
|
67
|
+
elapsedMs += Math.max(0, evidenceFinishedAt - evidenceStartedAt);
|
|
68
|
+
if (located !== null) {
|
|
69
|
+
evidence = {
|
|
70
|
+
startLine: located.startLine,
|
|
71
|
+
endLine: located.endLine,
|
|
72
|
+
confidence: located.confidence,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const lintResult = {
|
|
57
77
|
violated: failed,
|
|
58
78
|
ruleTitle: rule.patdownRuleTitle,
|
|
79
|
+
ruleBody: rule.patdownRuleBody,
|
|
80
|
+
ruleGlobs: rule.patdownRuleGlobs,
|
|
59
81
|
filePath: relativePath,
|
|
60
|
-
violationProbability:
|
|
82
|
+
violationProbability: timed.judgment.yesProbability,
|
|
61
83
|
yesThreshold: options.yesThreshold,
|
|
62
|
-
|
|
84
|
+
elapsedMs,
|
|
85
|
+
};
|
|
86
|
+
yield* output.writeLintResult(evidence === undefined ? lintResult : { ...lintResult, evidence }, options.verbose);
|
|
63
87
|
return failed;
|
|
64
88
|
});
|
|
65
89
|
}
|
|
66
|
-
function lintPatdownRule(rule,
|
|
90
|
+
function lintPatdownRule(rule, options) {
|
|
67
91
|
return Effect.gen(function* () {
|
|
68
92
|
const output = yield* PatdownOutput;
|
|
69
|
-
const files =
|
|
93
|
+
const files = selectPatdownRuleFiles(options.cwd, options.selection, rule.patdownRuleGlobs, options.selection === null
|
|
94
|
+
? yield* globPatdownRuleFiles(options.cwd, rule.patdownRuleGlobs)
|
|
95
|
+
: []);
|
|
70
96
|
const yesThreshold = rule.patdownRuleYesThreshold === undefined
|
|
71
|
-
? defaultYesThreshold
|
|
97
|
+
? options.defaultYesThreshold
|
|
72
98
|
: yield* decodePatdownRuleYesThreshold(rule.patdownRuleYesThreshold, rule.patdownRuleTitle);
|
|
73
99
|
if (files.length === 0) {
|
|
74
|
-
|
|
100
|
+
if (options.selection === null) {
|
|
101
|
+
yield* output.writeNoFilesMatched(rule.patdownRuleTitle);
|
|
102
|
+
}
|
|
75
103
|
return false;
|
|
76
104
|
}
|
|
77
|
-
const failures = yield* Effect.forEach(files, (filePath) => lintPatdownRuleFile(rule, filePath, {
|
|
105
|
+
const failures = yield* Effect.forEach(files, (filePath) => lintPatdownRuleFile(rule, filePath, {
|
|
106
|
+
cwd: options.cwd,
|
|
107
|
+
verbose: options.verbose,
|
|
108
|
+
yesThreshold,
|
|
109
|
+
}), { concurrency: 1 });
|
|
78
110
|
return failures.some((failed) => failed);
|
|
79
111
|
});
|
|
80
112
|
}
|
|
81
113
|
/** Lint files matched by each rule's globs. A yes judgment means a violation. */
|
|
82
|
-
export function runPatdownLint(document, verbose = false, yesThreshold = defaultPatdownYesThreshold) {
|
|
114
|
+
export function runPatdownLint(document, verbose = false, yesThreshold = defaultPatdownYesThreshold, selection = null) {
|
|
83
115
|
return Effect.gen(function* () {
|
|
84
116
|
const path = yield* Path.Path;
|
|
85
117
|
const cwd = path.resolve('.');
|
|
86
|
-
const
|
|
87
|
-
|
|
118
|
+
const startedAt = yield* Clock.currentTimeMillis;
|
|
119
|
+
const failures = yield* Effect.forEach(document.patdownRules, (rule) => lintPatdownRule(rule, {
|
|
120
|
+
cwd,
|
|
121
|
+
verbose,
|
|
122
|
+
defaultYesThreshold: yesThreshold,
|
|
123
|
+
selection,
|
|
124
|
+
}), { concurrency: 1 });
|
|
125
|
+
const finishedAt = yield* Clock.currentTimeMillis;
|
|
126
|
+
return {
|
|
127
|
+
failed: failures.some((failed) => failed),
|
|
128
|
+
elapsedMs: Math.max(0, finishedAt - startedAt),
|
|
129
|
+
};
|
|
88
130
|
});
|
|
89
131
|
}
|