patdown 0.4.0 → 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 +46 -11
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +45 -20
- 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 +25 -0
- package/dist/patdown-judge.d.ts.map +1 -1
- package/dist/patdown-judge.js +23 -0
- 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 +2 -1
- package/dist/patdown-lint.d.ts.map +1 -1
- package/dist/patdown-lint.js +57 -21
- package/dist/patdown-output.d.ts +22 -4
- package/dist/patdown-output.d.ts.map +1 -1
- package/dist/patdown-output.js +79 -14
- 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 +5 -5
|
@@ -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
|
@@ -11,6 +11,18 @@ export type PatdownTimedJudgment = {
|
|
|
11
11
|
readonly judgment: PatdownJudgment;
|
|
12
12
|
readonly elapsedMs: number;
|
|
13
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
|
+
};
|
|
14
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 & {
|
|
15
27
|
readonly _tag: "PatdownJudgeFailed";
|
|
16
28
|
} & Readonly<A>;
|
|
@@ -21,6 +33,11 @@ export declare class PatdownJudgeFailed extends PatdownJudgeFailed_base<{
|
|
|
21
33
|
}
|
|
22
34
|
declare const PatdownJudge_base: Context.ServiceClass<PatdownJudge, "@patdown/cli/PatdownJudge", {
|
|
23
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>;
|
|
24
41
|
}>;
|
|
25
42
|
/** Swappable judge service. Providers supply their own transport dependencies internally. */
|
|
26
43
|
export declare class PatdownJudge extends PatdownJudge_base {
|
|
@@ -31,5 +48,13 @@ export declare const patdownYesThreshold = 0.85;
|
|
|
31
48
|
export declare function patdownJudgmentIsYes(judgment: PatdownJudgment, yesThreshold?: PatdownYesThreshold): boolean;
|
|
32
49
|
/** Validates custom judge responses at the service boundary before applying policy or printing. */
|
|
33
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>;
|
|
34
59
|
export {};
|
|
35
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,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,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;;
|
|
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
|
@@ -30,3 +30,26 @@ export function askPatdownJudge(question, inputText) {
|
|
|
30
30
|
};
|
|
31
31
|
});
|
|
32
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
|
+
};
|
|
54
|
+
});
|
|
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,9 +1,10 @@
|
|
|
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): Effect.Effect<{
|
|
7
|
+
export declare function runPatdownLint(document: PatdownRulesDocument, verbose?: boolean, yesThreshold?: PatdownYesThreshold, selection?: PatdownLintFileSelection | null): Effect.Effect<{
|
|
7
8
|
readonly failed: boolean;
|
|
8
9
|
readonly elapsedMs: number;
|
|
9
10
|
}, PatdownJudgeFailed | PatdownYesThresholdInvalid, FileSystem.FileSystem | PatdownJudge | Path.Path | PatdownOutput>;
|
|
@@ -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,EAAS,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;
|
|
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
2
|
import { Clock, Effect, FileSystem, Path } from 'effect';
|
|
3
|
-
import {
|
|
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.',
|
|
@@ -53,39 +46,82 @@ function lintPatdownRuleFile(rule, filePath, options) {
|
|
|
53
46
|
})));
|
|
54
47
|
const timed = yield* askPatdownJudge(patdownViolationInstructions(rule), patdownFileState(relativePath, contents));
|
|
55
48
|
const failed = patdownJudgmentIsYes(timed.judgment, options.yesThreshold);
|
|
56
|
-
|
|
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
82
|
violationProbability: timed.judgment.yesProbability,
|
|
61
83
|
yesThreshold: options.yesThreshold,
|
|
62
|
-
elapsedMs
|
|
63
|
-
}
|
|
84
|
+
elapsedMs,
|
|
85
|
+
};
|
|
86
|
+
yield* output.writeLintResult(evidence === undefined ? lintResult : { ...lintResult, evidence }, options.verbose);
|
|
64
87
|
return failed;
|
|
65
88
|
});
|
|
66
89
|
}
|
|
67
|
-
function lintPatdownRule(rule,
|
|
90
|
+
function lintPatdownRule(rule, options) {
|
|
68
91
|
return Effect.gen(function* () {
|
|
69
92
|
const output = yield* PatdownOutput;
|
|
70
|
-
const files =
|
|
93
|
+
const files = selectPatdownRuleFiles(options.cwd, options.selection, rule.patdownRuleGlobs, options.selection === null
|
|
94
|
+
? yield* globPatdownRuleFiles(options.cwd, rule.patdownRuleGlobs)
|
|
95
|
+
: []);
|
|
71
96
|
const yesThreshold = rule.patdownRuleYesThreshold === undefined
|
|
72
|
-
? defaultYesThreshold
|
|
97
|
+
? options.defaultYesThreshold
|
|
73
98
|
: yield* decodePatdownRuleYesThreshold(rule.patdownRuleYesThreshold, rule.patdownRuleTitle);
|
|
74
99
|
if (files.length === 0) {
|
|
75
|
-
|
|
100
|
+
if (options.selection === null) {
|
|
101
|
+
yield* output.writeNoFilesMatched(rule.patdownRuleTitle);
|
|
102
|
+
}
|
|
76
103
|
return false;
|
|
77
104
|
}
|
|
78
|
-
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 });
|
|
79
110
|
return failures.some((failed) => failed);
|
|
80
111
|
});
|
|
81
112
|
}
|
|
82
113
|
/** Lint files matched by each rule's globs. A yes judgment means a violation. */
|
|
83
|
-
export function runPatdownLint(document, verbose = false, yesThreshold = defaultPatdownYesThreshold) {
|
|
114
|
+
export function runPatdownLint(document, verbose = false, yesThreshold = defaultPatdownYesThreshold, selection = null) {
|
|
84
115
|
return Effect.gen(function* () {
|
|
85
116
|
const path = yield* Path.Path;
|
|
86
117
|
const cwd = path.resolve('.');
|
|
87
118
|
const startedAt = yield* Clock.currentTimeMillis;
|
|
88
|
-
const failures = yield* Effect.forEach(document.patdownRules, (rule) => lintPatdownRule(rule,
|
|
119
|
+
const failures = yield* Effect.forEach(document.patdownRules, (rule) => lintPatdownRule(rule, {
|
|
120
|
+
cwd,
|
|
121
|
+
verbose,
|
|
122
|
+
defaultYesThreshold: yesThreshold,
|
|
123
|
+
selection,
|
|
124
|
+
}), { concurrency: 1 });
|
|
89
125
|
const finishedAt = yield* Clock.currentTimeMillis;
|
|
90
126
|
return {
|
|
91
127
|
failed: failures.some((failed) => failed),
|
package/dist/patdown-output.d.ts
CHANGED
|
@@ -2,27 +2,45 @@ import type { PatdownRulesDocument } from '@patdown/rules';
|
|
|
2
2
|
import { type PatdownYesThreshold } from '@patdown/rules';
|
|
3
3
|
import { Context, Effect, Layer } from 'effect';
|
|
4
4
|
import { type PatdownJudgment } from '#src/patdown-judge';
|
|
5
|
+
/** Optional 1-based span from a FAIL-only evidence Choice. */
|
|
6
|
+
export type PatdownLintEvidenceSpan = {
|
|
7
|
+
readonly startLine: number;
|
|
8
|
+
readonly endLine: number;
|
|
9
|
+
readonly confidence: number;
|
|
10
|
+
};
|
|
5
11
|
/** One file/rule result; probability estimates a violation, not correctness of the verdict. */
|
|
6
12
|
export type PatdownLintResult = {
|
|
7
13
|
readonly violated: boolean;
|
|
8
14
|
readonly ruleTitle: string;
|
|
15
|
+
readonly ruleBody: string;
|
|
16
|
+
readonly ruleGlobs: ReadonlyArray<string>;
|
|
9
17
|
readonly filePath: string;
|
|
10
18
|
readonly violationProbability: number;
|
|
11
19
|
readonly yesThreshold: PatdownYesThreshold;
|
|
12
20
|
readonly elapsedMs: number;
|
|
21
|
+
readonly evidence?: PatdownLintEvidenceSpan;
|
|
13
22
|
};
|
|
14
|
-
|
|
23
|
+
/** Writers used by the PatdownOutput service. */
|
|
24
|
+
export type PatdownOutputWriters = {
|
|
15
25
|
readonly writeLintFailed: (elapsedMs?: number) => Effect.Effect<void>;
|
|
16
26
|
readonly writeLintOk: (elapsedMs?: number) => Effect.Effect<void>;
|
|
17
27
|
readonly writeLintResult: (result: PatdownLintResult, verbose: boolean) => Effect.Effect<void>;
|
|
18
28
|
readonly writeNoFilesMatched: (ruleTitle: string) => Effect.Effect<void>;
|
|
19
29
|
readonly writeAnswer: (judgment: PatdownJudgment, verbose: boolean, yesThreshold?: PatdownYesThreshold, elapsedMs?: number) => Effect.Effect<void>;
|
|
20
30
|
readonly writeRulesDocument: (document: PatdownRulesDocument) => Effect.Effect<void>;
|
|
21
|
-
}
|
|
31
|
+
};
|
|
32
|
+
declare const PatdownOutput_base: Context.ServiceClass<PatdownOutput, "@patdown/cli/PatdownOutput", PatdownOutputWriters>;
|
|
22
33
|
/** Swappable output service. Probability details are opt-in. */
|
|
23
34
|
export declare class PatdownOutput extends PatdownOutput_base {
|
|
24
35
|
}
|
|
25
|
-
/**
|
|
26
|
-
export declare const
|
|
36
|
+
/** One-line writers shared with GitHub Actions job logs. */
|
|
37
|
+
export declare const patdownStreamingHumanOutput: PatdownOutputWriters;
|
|
38
|
+
/**
|
|
39
|
+
* Local human output. Quiet mode streams one PASS/FAIL line per judgment. Verbose mode groups
|
|
40
|
+
* judgments into per-rule console blocks.
|
|
41
|
+
*/
|
|
42
|
+
export declare const PatdownOutputLive: Layer.Layer<PatdownOutput>;
|
|
43
|
+
/** @deprecated Use patdownStreamingHumanOutput for one-line logs. */
|
|
44
|
+
export declare const patdownHumanOutput: PatdownOutputWriters;
|
|
27
45
|
export {};
|
|
28
46
|
//# sourceMappingURL=patdown-output.d.ts.map
|