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
package/README.md
CHANGED
|
@@ -19,22 +19,29 @@ Node.js **>=22.22.2** and pnpm **11.8.0**. CI runs checks and built-CLI smoke te
|
|
|
19
19
|
|
|
20
20
|
## Commands
|
|
21
21
|
|
|
22
|
+
From this repo, prefer the workspace script. It builds `patdown` through Turborepo (`dependsOn: build`) and then runs the CLI from your current directory:
|
|
23
|
+
|
|
22
24
|
```
|
|
23
25
|
pnpm -w patdown
|
|
24
26
|
pnpm -w patdown -- --rules ./rules.md
|
|
27
|
+
pnpm -w patdown -- --rules ./packs/typescript --verbose
|
|
25
28
|
pnpm -w patdown -- rules
|
|
26
29
|
pnpm -w patdown -- ask "Is this markdown heading title case?" --input-text "# Hello World"
|
|
27
30
|
pnpm -w patdown -- ask "Is this urgent?" --input-text "ASAP" --verbose
|
|
28
31
|
pnpm -w patdown -- --yes-threshold 0.9
|
|
32
|
+
pnpm -w patdown -- --files src/cli.ts --files README.md
|
|
33
|
+
pnpm -w patdown -- --files-from changed.txt --verbose
|
|
29
34
|
```
|
|
30
35
|
|
|
36
|
+
Do not run `pnpm build` by hand just to exercise the CLI, and do not use bare `npx patdown` against an unpublished branch (that installs the last registry version). After a release, consumers use `npx patdown` / `pnpm add -D patdown` as usual.
|
|
37
|
+
|
|
31
38
|
Default command lints from the current directory. `rules` prints what it loaded. `ask` answers a yes/no question, no files involved. It prints `yes` or `no`; `--verbose` also shows a P(yes) shade bar, the cutoff, and how long the judge call took. The old `--noul` and `--state` flags have been replaced by a positional question and `--input-text`.
|
|
32
39
|
|
|
33
|
-
To judge piped output, use `--stdin
|
|
40
|
+
To judge piped output, use `--stdin`:
|
|
34
41
|
|
|
35
42
|
```sh
|
|
36
|
-
git diff --cached |
|
|
37
|
-
printf 'ASAP: production is down\n' |
|
|
43
|
+
git diff --cached | pnpm -w patdown -- ask "Does this diff introduce debugging statements?" --stdin
|
|
44
|
+
printf 'ASAP: production is down\n' | pnpm -w patdown -- ask "Is this urgent?" --stdin
|
|
38
45
|
```
|
|
39
46
|
|
|
40
47
|
These send the piped content to the configured judge. Do not pipe secrets. `--stdin` reads UTF-8 text through EOF, preserving newlines; it cannot be combined with `--input-text`. Without either option, the input is an empty string. `ask` reports yes/no without treating yes as a failing exit status.
|
|
@@ -94,6 +101,22 @@ await Effect.runPromise(runPatdownCli(PatdownRuleSourceLive))
|
|
|
94
101
|
|
|
95
102
|
See [the adapter guide](docs/rule-source-adapters.md) for the interface, a multi-file parser, resolution rules, resource lifetimes, and local development setup. Adapters execute trusted local code. Install the CLI from npm as `patdown`; adapters import `@patdown/rules` and may import `patdown` for types.
|
|
96
103
|
|
|
104
|
+
## Packs
|
|
105
|
+
|
|
106
|
+
Optional rule bundles live under [`packs/`](packs/README.md). The CLI only runs rules; packs are content you can take all of, some of, or skip:
|
|
107
|
+
|
|
108
|
+
- [`packs/typescript`](packs/typescript/) — type-safety crimes and type laundering
|
|
109
|
+
- [`packs/effect`](packs/effect/) — Effect v4 services, schemas, errors, time, HTTP, tests
|
|
110
|
+
- [`packs/anti-slop`](packs/anti-slop/) — fuzzy counterpart to [Dillon Mulroy's anti-slop](https://github.com/dmmulroy/anti-slop)
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
# whole pack, or a single rule file from a pack
|
|
114
|
+
patdown --rules ./packs/effect --files-from changed.txt --verbose
|
|
115
|
+
patdown --rules ./packs/typescript/do-not-launder-types-with-casts.md
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`--rules` accepts a markdown file or a directory of rule files (every `*.md` except `README.md`). Prefer stacked, backtick-wrapped `globs:` lines; commas inside one `globs:` value are separators.
|
|
119
|
+
|
|
97
120
|
## Rules
|
|
98
121
|
|
|
99
122
|
One `# heading` per rule. Optional `globs:` and `yes-threshold:` lines sit immediately under the heading, in either order. Commas or spaces, extra `globs:` lines stack. A second `yes-threshold:` line is an error. Text above the first heading is ignored. Headings inside fenced code are ignored. Only `#` headings start rules; `##` and deeper headings stay in the rule body, so sections like `## Not allowed` and `## Exceptions` are fine.
|
|
@@ -119,12 +142,22 @@ FAIL README.md: No title case
|
|
|
119
142
|
patdown: failed
|
|
120
143
|
```
|
|
121
144
|
|
|
122
|
-
Exit 1 on a violation, a missing rules file, a read error, an invalid cutoff, or a judge error. Add `--verbose` to
|
|
145
|
+
Exit 1 on a violation, a missing rules file, a read error, an invalid cutoff, or a judge error. Add `--verbose` to group local lint output into per-rule console blocks (file, shade bar, P(yes), elapsed). Quiet mode stays one `PASS`/`FAIL` line per judgment. Lint totals elapsed time across every rule/file pair.
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
┌ Do not launder types with casts ───────────────────────── 1✗ / 1
|
|
149
|
+
│
|
|
150
|
+
├─ apps/foo
|
|
151
|
+
│
|
|
152
|
+
│ ✗ ▓▓▓▓▓▓▓▓▒░ 0.86 312ms service.ts
|
|
153
|
+
└
|
|
123
154
|
|
|
124
|
-
````
|
|
125
|
-
FAIL service.ts: explicit-actor (▓▓▓▓▓▓▓▓▒░ estimated P(yes): 0.86; cutoff: >0.85; elapsed: 312ms)
|
|
126
155
|
patdown: failed (elapsed: 1840ms)
|
|
127
|
-
```
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`--files` (repeatable) and `--files-from` restrict lint to an explicit path list intersected with each rule's globs. That is how CI should run over a pull request. See [GitHub Actions](docs/github-actions.md).
|
|
159
|
+
|
|
160
|
+
Patdown counts estimated P(yes) strictly above the cutoff as yes; for lint, yes means violation. Default cutoff is 0.85. Override it with `--yes-threshold`, package.json `patdown.yesThreshold`, or a per-rule `yes-threshold:` line. The flag wins over package.json; a per-rule value wins for that rule only. `1` is rejected because nothing can exceed it. This cutoff belongs to patdown, not the provider.
|
|
128
161
|
|
|
129
162
|
See [judge providers](docs/judge-providers.md) for custom layers and the planned Effect Decision integration.
|
|
130
163
|
|
|
@@ -155,8 +188,8 @@ const JsonOutputLive = Layer.succeed(PatdownOutput, {
|
|
|
155
188
|
writeLintResult: (result, _verbose) => Console.log(JSON.stringify(result)),
|
|
156
189
|
writeRulesDocument: (document) => Console.log(JSON.stringify(document)),
|
|
157
190
|
writeNoFilesMatched: (ruleTitle) => Console.log(JSON.stringify({ skipped: ruleTitle })),
|
|
158
|
-
writeLintOk: Console.log(JSON.stringify({ status: 'ok' })),
|
|
159
|
-
writeLintFailed: Console.log(JSON.stringify({ status: 'failed' })),
|
|
191
|
+
writeLintOk: () => Console.log(JSON.stringify({ status: 'ok' })),
|
|
192
|
+
writeLintFailed: () => Console.log(JSON.stringify({ status: 'failed' })),
|
|
160
193
|
})
|
|
161
194
|
|
|
162
195
|
await Effect.runPromise(
|
|
@@ -167,7 +200,7 @@ await Effect.runPromise(
|
|
|
167
200
|
JsonOutputLive,
|
|
168
201
|
),
|
|
169
202
|
)
|
|
170
|
-
|
|
203
|
+
```
|
|
171
204
|
|
|
172
205
|
The output service receives structured judgments and lint results, including probabilities even when `--verbose` is off. Your layer decides what to print, collect, or omit. Formatting does not change the cutoff or exit status. The example emits one JSON object per output event, not a single JSON document for the entire run.
|
|
173
206
|
|
|
@@ -177,7 +210,7 @@ This is an embedding API, not a `--format` flag or dynamically discovered output
|
|
|
177
210
|
|
|
178
211
|
With the default TypeSafe backend, `TYPESAFE_API_KEY` is required for lint and `ask`. Optional `TYPESAFE_BASE_URL` (default `https://api.typesafe.ai`) and `TYPESAFE_DEFAULT_MODEL` (default `jev-latest`).
|
|
179
212
|
|
|
180
|
-
`pnpm -w patdown`
|
|
213
|
+
`pnpm -w patdown` inherits `TYPESAFE_*` from your environment.
|
|
181
214
|
|
|
182
215
|
## Release
|
|
183
216
|
|
|
@@ -203,6 +236,8 @@ Inspired by [pi-warden](https://github.com/DevMortimer/pi-warden). Same idea, in
|
|
|
203
236
|
|
|
204
237
|
[Jev Review](https://github.com/devagrawal09/jev-review) is a Jev-backed diff/codebase reviewer with a local dashboard of those judgments. Patdown stays in the terminal; `--verbose` draws a P(yes) shade bar instead of a GUI.
|
|
205
238
|
|
|
239
|
+
[Dillon Mulroy's anti-slop](https://github.com/dmmulroy/anti-slop) is the Oxlint ruleset that [`packs/anti-slop`](packs/anti-slop/) rephrases for fuzzy judging. Keep the static rules for exact AST hits; use the pack for paraphrases and type-laundering that still looks clean to a linter.
|
|
240
|
+
|
|
206
241
|
Name inspired by It's Always Sunny in Philadelphia
|
|
207
242
|
|
|
208
243
|
<img width="511" height="415" alt="image" src="https://github.com/user-attachments/assets/f7c73138-3914-4fbd-9e6b-7a37d161334a" />
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,iBAAiB,EAKjB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAU,UAAU,
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,iBAAiB,EAKjB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAU,UAAU,EAAU,IAAI,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AAChE,OAAO,EAAY,OAAO,EAAQ,MAAM,qBAAqB,CAAA;AAE7D,OAAO,EAAE,YAAY,EAAuC,MAAM,oBAAoB,CAAA;AAGtF,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AA8CnD,KAAK,mBAAmB,GACrB,UAAU,CAAC,UAAU,GACrB,KAAK,CAAC,KAAK,GACX,YAAY,GACZ,IAAI,CAAC,IAAI,GACT,aAAa,GACb,iBAAiB,CAAA;AA+IpB,4EAA4E;AAC5E,wBAAgB,kBAAkB,CACjC,gBAAgB,GAAE,OAAc,GAC9B,OAAO,CAAC,OAAO,CAAC,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,CAAC,CA6BvE;AAED,uEAAuE;AACvE,eAAO,MAAM,cAAc,uEAAuB,CAAA"}
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { PatdownRuleSource, PatdownRulesFileMissing, PatdownRulesReadFailed, PatdownRulesLoadFailed, PatdownYesThresholdInvalid, } from '@patdown/rules';
|
|
2
|
-
import { Effect, FileSystem, Path, Stdio } from 'effect';
|
|
2
|
+
import { Effect, FileSystem, Option, Path, Stdio } from 'effect';
|
|
3
3
|
import { Argument, Command, Flag } from 'effect/unstable/cli';
|
|
4
4
|
import { PatdownJudge, PatdownJudgeFailed, askPatdownJudge } from '#src/patdown-judge';
|
|
5
5
|
import { runPatdownLint } from '#src/patdown-lint';
|
|
6
|
+
import { resolvePatdownLintFileSelection } from '#src/patdown-lint-files';
|
|
6
7
|
import { PatdownOutput } from '#src/patdown-output';
|
|
7
8
|
import { readPatdownQuestionInput } from '#src/patdown-question-input';
|
|
8
9
|
import { loadConfiguredPatdownRules } from '#src/patdown-rule-source-adapter';
|
|
@@ -11,10 +12,13 @@ const failPatdown = (message) => Effect.sync(() => {
|
|
|
11
12
|
process.exitCode = 1;
|
|
12
13
|
process.stderr.write(`${message}\n`);
|
|
13
14
|
});
|
|
14
|
-
const rulesFileFlag = Flag.optional(Flag.string('rules')).pipe(Flag.withDescription('
|
|
15
|
+
const rulesFileFlag = Flag.optional(Flag.string('rules')).pipe(Flag.withDescription('Rules markdown file or directory of rule files. Skips the AGENTS.PATDOWN.md walk'));
|
|
15
16
|
const verboseFlag = Flag.boolean('verbose').pipe(Flag.withDefault(false), Flag.withDescription('Show estimated yes probabilities, the decision cutoff, and elapsed judge time'));
|
|
16
|
-
const adapterFlag = Flag.optional(Flag.string('adapter')).pipe(Flag.withDescription('Module exporting PatdownRuleSourceLive,
|
|
17
|
+
const adapterFlag = Flag.optional(Flag.string('adapter')).pipe(Flag.withDescription('Module exporting PatdownRuleSourceLive, replacing markdown rule parsing'));
|
|
17
18
|
const yesThresholdFlag = Flag.optional(Flag.float('yes-threshold')).pipe(Flag.withDescription('Minimum exclusive P(yes) for yes; default 0.85, overridable per rule'));
|
|
19
|
+
const filesFlag = Flag.string('files').pipe(Flag.between(0, 10_000), Flag.withDescription('Restrict lint to these paths; intersects each rule glob'));
|
|
20
|
+
const filesFromFlag = Flag.optional(Flag.string('files-from')).pipe(Flag.withDescription('Newline-separated paths to lint; intersects each rule glob'));
|
|
21
|
+
const noGitHubFlag = Flag.boolean('no-github').pipe(Flag.withDefault(false), Flag.withDescription('Disable GitHub Actions summary and annotations'));
|
|
18
22
|
function finishPatdownLint(failed, verbose, elapsedMs) {
|
|
19
23
|
return Effect.gen(function* () {
|
|
20
24
|
const output = yield* PatdownOutput;
|
|
@@ -28,9 +32,8 @@ function finishPatdownLint(failed, verbose, elapsedMs) {
|
|
|
28
32
|
yield* output.writeLintOk(verbose ? elapsedMs : undefined);
|
|
29
33
|
});
|
|
30
34
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const rulesCommand = Command.make('rules', { adapter: adapterFlag, rules: rulesFileFlag }, ({ rules, adapter, }) => Effect.gen(function* () {
|
|
35
|
+
function makePatdownRulesCommand(discoverAdapters) {
|
|
36
|
+
return Command.make('rules', { adapter: adapterFlag, rules: rulesFileFlag }, ({ rules, adapter, }) => Effect.gen(function* () {
|
|
34
37
|
const patdownRuleSource = yield* PatdownRuleSource;
|
|
35
38
|
const output = yield* PatdownOutput;
|
|
36
39
|
const document = yield* discoverAdapters
|
|
@@ -43,7 +46,9 @@ export function makePatdownCommand(discoverAdapters = true) {
|
|
|
43
46
|
PatdownRulesLoadFailed: (error) => failPatdown(error.message),
|
|
44
47
|
PatdownYesThresholdInvalid: (error) => failPatdown(error.message),
|
|
45
48
|
}))).pipe(Command.withDescription('Load and print patdown rules'));
|
|
46
|
-
|
|
49
|
+
}
|
|
50
|
+
function makePatdownAskCommand() {
|
|
51
|
+
return Command.make('ask', {
|
|
47
52
|
question: Argument.string('question'),
|
|
48
53
|
verbose: verboseFlag,
|
|
49
54
|
yesThreshold: yesThresholdFlag,
|
|
@@ -59,27 +64,47 @@ export function makePatdownCommand(discoverAdapters = true) {
|
|
|
59
64
|
PatdownJudgeFailed: (error) => failPatdown(error.message),
|
|
60
65
|
PatdownYesThresholdInvalid: (error) => failPatdown(error.message),
|
|
61
66
|
}))).pipe(Command.withDescription('Ask a yes/no question about text'));
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
rules: rulesFileFlag,
|
|
66
|
-
verbose: verboseFlag,
|
|
67
|
-
yesThreshold: yesThresholdFlag,
|
|
68
|
-
}, ({ rules, adapter, verbose, yesThreshold }) => Effect.gen(function* () {
|
|
67
|
+
}
|
|
68
|
+
function runPatdownRootLint(discoverAdapters, options) {
|
|
69
|
+
return Effect.gen(function* () {
|
|
69
70
|
const patdownRuleSource = yield* PatdownRuleSource;
|
|
70
|
-
const
|
|
71
|
+
const path = yield* Path.Path;
|
|
72
|
+
const cutoff = yield* resolvePatdownYesThreshold(options.yesThreshold);
|
|
73
|
+
const cwd = path.resolve('.');
|
|
74
|
+
const selection = yield* resolvePatdownLintFileSelection(cwd, options.files, options.filesFrom);
|
|
71
75
|
const document = yield* discoverAdapters
|
|
72
|
-
? loadConfiguredPatdownRules(adapter, rules)
|
|
73
|
-
: patdownRuleSource.loadPatdownRules(rules);
|
|
74
|
-
const linted = yield* runPatdownLint(document, verbose, cutoff);
|
|
75
|
-
yield* finishPatdownLint(linted.failed, verbose, linted.elapsedMs);
|
|
76
|
+
? loadConfiguredPatdownRules(options.adapter, options.rules)
|
|
77
|
+
: patdownRuleSource.loadPatdownRules(options.rules);
|
|
78
|
+
const linted = yield* runPatdownLint(document, options.verbose, cutoff, selection);
|
|
79
|
+
yield* finishPatdownLint(linted.failed, options.verbose, linted.elapsedMs);
|
|
76
80
|
}).pipe(Effect.catchTags({
|
|
77
81
|
PatdownRulesLoadFailed: (error) => failPatdown(error.message),
|
|
78
82
|
PatdownJudgeFailed: (error) => failPatdown(error.message),
|
|
79
83
|
PatdownYesThresholdInvalid: (error) => failPatdown(error.message),
|
|
80
84
|
PatdownRulesFileMissing: (error) => failPatdown(error.message),
|
|
81
85
|
PatdownRulesReadFailed: (error) => failPatdown(error.message),
|
|
82
|
-
}))
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
/** Builds commands with optional adapter discovery for embedded callers. */
|
|
89
|
+
export function makePatdownCommand(discoverAdapters = true) {
|
|
90
|
+
const rulesCommand = makePatdownRulesCommand(discoverAdapters);
|
|
91
|
+
const askCommand = makePatdownAskCommand();
|
|
92
|
+
return Command.make('patdown', {
|
|
93
|
+
adapter: adapterFlag,
|
|
94
|
+
files: filesFlag,
|
|
95
|
+
filesFrom: filesFromFlag,
|
|
96
|
+
noGitHub: noGitHubFlag,
|
|
97
|
+
rules: rulesFileFlag,
|
|
98
|
+
verbose: verboseFlag,
|
|
99
|
+
yesThreshold: yesThresholdFlag,
|
|
100
|
+
}, ({ rules, adapter, verbose, yesThreshold, files, filesFrom, noGitHub: _noGitHub }) => runPatdownRootLint(discoverAdapters, {
|
|
101
|
+
rules,
|
|
102
|
+
adapter,
|
|
103
|
+
verbose,
|
|
104
|
+
yesThreshold,
|
|
105
|
+
files,
|
|
106
|
+
filesFrom,
|
|
107
|
+
})).pipe(Command.withDescription('Lint a tree against fuzzy markdown rules'), Command.withShortDescription('Patdown CLI'), Command.withSubcommands([askCommand, rulesCommand]));
|
|
83
108
|
}
|
|
84
109
|
/** Default commands use explicit or package.json adapter discovery. */
|
|
85
110
|
export const patdownCommand = makePatdownCommand();
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
export { patdownCommand, makePatdownCommand } from '#src/cli';
|
|
2
2
|
export { PatdownOutput, PatdownOutputLive, type PatdownLintResult } from '#src/patdown-output';
|
|
3
|
+
export { PatdownGitHubActionsOutputLive } from '#src/patdown-github-actions-output';
|
|
4
|
+
export { formatPatdownGitHubActionsAnnotations, formatPatdownGitHubActionsSummary, formatPatdownRuleGuidance, patdownLintResultIsNearMiss, } from '#src/patdown-github-actions-summary';
|
|
5
|
+
export { patdownGitHubActionsIsEnabled, readPatdownGitHubStepSummaryPath, } from '#src/patdown-github-actions-env';
|
|
6
|
+
export type { PatdownLintFileSelection } from '#src/patdown-lint-files';
|
|
3
7
|
export type { PatdownRuleSourceLayer } from '#src/patdown-rule-source-adapter';
|
|
4
8
|
export { runPatdownCli } from '#src/run-patdown-cli';
|
|
5
|
-
export { PatdownJudge, PatdownJudgeFailed, PatdownJudgmentSchema, askPatdownJudge, patdownJudgmentIsYes, patdownYesThreshold, type PatdownJudgment, type PatdownTimedJudgment, } from '#src/patdown-judge';
|
|
9
|
+
export { PatdownJudge, PatdownJudgeFailed, PatdownJudgmentSchema, askPatdownJudge, locatePatdownEvidence, patdownJudgmentIsYes, patdownYesThreshold, type PatdownEvidenceChoice, type PatdownEvidenceLocation, type PatdownJudgment, type PatdownTimedJudgment, } from '#src/patdown-judge';
|
|
6
10
|
export { defaultPatdownYesThreshold, decodePatdownYesThreshold, PatdownYesThresholdInvalid, type PatdownYesThreshold, } from '@patdown/rules';
|
|
7
11
|
export { TypeSafeJudgeLive } from '#src/typesafe-judge';
|
|
8
12
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAE7D,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,KAAK,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAE9F,YAAY,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAA;AAE9E,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAEpD,OAAO,EACN,YAAY,EACZ,kBAAkB,EAClB,qBAAqB,EACrB,eAAe,EACf,oBAAoB,EACpB,mBAAmB,EACnB,KAAK,eAAe,EACpB,KAAK,oBAAoB,GACzB,MAAM,oBAAoB,CAAA;AAE3B,OAAO,EACN,0BAA0B,EAC1B,yBAAyB,EACzB,0BAA0B,EAC1B,KAAK,mBAAmB,GACxB,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAE7D,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,KAAK,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAE9F,OAAO,EAAE,8BAA8B,EAAE,MAAM,oCAAoC,CAAA;AAEnF,OAAO,EACN,qCAAqC,EACrC,iCAAiC,EACjC,yBAAyB,EACzB,2BAA2B,GAC3B,MAAM,qCAAqC,CAAA;AAE5C,OAAO,EACN,6BAA6B,EAC7B,gCAAgC,GAChC,MAAM,iCAAiC,CAAA;AAExC,YAAY,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAA;AAEvE,YAAY,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAA;AAE9E,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAEpD,OAAO,EACN,YAAY,EACZ,kBAAkB,EAClB,qBAAqB,EACrB,eAAe,EACf,qBAAqB,EACrB,oBAAoB,EACpB,mBAAmB,EACnB,KAAK,qBAAqB,EAC1B,KAAK,uBAAuB,EAC5B,KAAK,eAAe,EACpB,KAAK,oBAAoB,GACzB,MAAM,oBAAoB,CAAA;AAE3B,OAAO,EACN,0BAA0B,EAC1B,yBAAyB,EACzB,0BAA0B,EAC1B,KAAK,mBAAmB,GACxB,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export { patdownCommand, makePatdownCommand } from '#src/cli';
|
|
2
2
|
export { PatdownOutput, PatdownOutputLive } from '#src/patdown-output';
|
|
3
|
+
export { PatdownGitHubActionsOutputLive } from '#src/patdown-github-actions-output';
|
|
4
|
+
export { formatPatdownGitHubActionsAnnotations, formatPatdownGitHubActionsSummary, formatPatdownRuleGuidance, patdownLintResultIsNearMiss, } from '#src/patdown-github-actions-summary';
|
|
5
|
+
export { patdownGitHubActionsIsEnabled, readPatdownGitHubStepSummaryPath, } from '#src/patdown-github-actions-env';
|
|
3
6
|
export { runPatdownCli } from '#src/run-patdown-cli';
|
|
4
|
-
export { PatdownJudge, PatdownJudgeFailed, PatdownJudgmentSchema, askPatdownJudge, patdownJudgmentIsYes, patdownYesThreshold, } from '#src/patdown-judge';
|
|
7
|
+
export { PatdownJudge, PatdownJudgeFailed, PatdownJudgmentSchema, askPatdownJudge, locatePatdownEvidence, patdownJudgmentIsYes, patdownYesThreshold, } from '#src/patdown-judge';
|
|
5
8
|
export { defaultPatdownYesThreshold, decodePatdownYesThreshold, PatdownYesThresholdInvalid, } from '@patdown/rules';
|
|
6
9
|
export { TypeSafeJudgeLive } from '#src/typesafe-judge';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { PatdownLintResult } from '#src/patdown-output';
|
|
2
|
+
/** Soft cap so extreme directory labels do not dominate the header line. */
|
|
3
|
+
export declare const patdownConsolePathMaxWidth = 48;
|
|
4
|
+
/** Keep the basename side of long paths: `…/src/patdown-github-actions-summary.ts`. */
|
|
5
|
+
export declare function formatPatdownConsolePath(filePath: string, maxWidth?: number): string;
|
|
6
|
+
/** Quiet one-line lint result. */
|
|
7
|
+
export declare function formatPatdownLintResultLine(result: PatdownLintResult): string;
|
|
8
|
+
/**
|
|
9
|
+
* Verbose local console: one box per rule, files grouped by directory with ├─ headers. Empty
|
|
10
|
+
* results mean the rule matched no files.
|
|
11
|
+
*/
|
|
12
|
+
export declare function formatPatdownRuleBlock(ruleTitle: string, results: ReadonlyArray<PatdownLintResult>): string;
|
|
13
|
+
//# sourceMappingURL=patdown-console-rule-blocks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patdown-console-rule-blocks.d.ts","sourceRoot":"","sources":["../src/patdown-console-rule-blocks.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAK5D,4EAA4E;AAC5E,eAAO,MAAM,0BAA0B,KAAK,CAAA;AAc5C,uFAAuF;AACvF,wBAAgB,wBAAwB,CACvC,QAAQ,EAAE,MAAM,EAChB,QAAQ,GAAE,MAAmC,GAC3C,MAAM,CAaR;AA0BD,kCAAkC;AAClC,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAI7E;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACrC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,aAAa,CAAC,iBAAiB,CAAC,GACvC,MAAM,CA4CR"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { dirname, basename } from 'node:path';
|
|
2
|
+
import { formatPatdownProbabilityBar } from '#src/patdown-probability-bar';
|
|
3
|
+
const patdownRuleBlockWidth = 64;
|
|
4
|
+
/** Soft cap so extreme directory labels do not dominate the header line. */
|
|
5
|
+
export const patdownConsolePathMaxWidth = 48;
|
|
6
|
+
function formatPatdownConsoleElapsed(elapsedMs) {
|
|
7
|
+
return `${String(elapsedMs)}ms`;
|
|
8
|
+
}
|
|
9
|
+
function padPatdownRuleBlockTitle(title, failCount, judgedCount) {
|
|
10
|
+
const suffix = ` ${String(failCount)}✗ / ${String(judgedCount)}`;
|
|
11
|
+
const prefix = `┌ ${title} `;
|
|
12
|
+
const fill = Math.max(2, patdownRuleBlockWidth - prefix.length - suffix.length);
|
|
13
|
+
return `${prefix}${'─'.repeat(fill)}${suffix}`;
|
|
14
|
+
}
|
|
15
|
+
/** Keep the basename side of long paths: `…/src/patdown-github-actions-summary.ts`. */
|
|
16
|
+
export function formatPatdownConsolePath(filePath, maxWidth = patdownConsolePathMaxWidth) {
|
|
17
|
+
if (filePath.length <= maxWidth)
|
|
18
|
+
return filePath;
|
|
19
|
+
const ellipsis = '…';
|
|
20
|
+
const keep = Math.max(1, maxWidth - ellipsis.length);
|
|
21
|
+
const tail = filePath.slice(-keep);
|
|
22
|
+
const slash = tail.indexOf('/');
|
|
23
|
+
if (slash > 0 && slash < tail.length - 1) {
|
|
24
|
+
return `${ellipsis}${tail.slice(slash)}`;
|
|
25
|
+
}
|
|
26
|
+
return `${ellipsis}${tail}`;
|
|
27
|
+
}
|
|
28
|
+
function patdownConsoleDirectoryLabel(filePath) {
|
|
29
|
+
const directory = dirname(filePath);
|
|
30
|
+
return directory === '.' ? '.' : directory;
|
|
31
|
+
}
|
|
32
|
+
function formatPatdownDirectoryHeader(directory) {
|
|
33
|
+
return `├─ ${formatPatdownConsolePath(directory)}`;
|
|
34
|
+
}
|
|
35
|
+
function formatPatdownRuleBlockRow(result, timeWidth, nameWidth) {
|
|
36
|
+
const mark = result.violated ? '✗' : '✓';
|
|
37
|
+
const bar = formatPatdownProbabilityBar(result.violationProbability);
|
|
38
|
+
const score = result.violationProbability.toFixed(2).padStart(4, ' ');
|
|
39
|
+
const time = formatPatdownConsoleElapsed(result.elapsedMs).padStart(timeWidth, ' ');
|
|
40
|
+
const name = formatPatdownConsolePath(basename(result.filePath)).padEnd(nameWidth, ' ');
|
|
41
|
+
return `│ ${mark} ${bar} ${score} ${time} ${name}`.trimEnd();
|
|
42
|
+
}
|
|
43
|
+
/** Quiet one-line lint result. */
|
|
44
|
+
export function formatPatdownLintResultLine(result) {
|
|
45
|
+
const verdict = result.violated ? 'FAIL' : 'PASS';
|
|
46
|
+
return `${verdict} ${result.filePath}: ${result.ruleTitle}`;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Verbose local console: one box per rule, files grouped by directory with ├─ headers. Empty
|
|
50
|
+
* results mean the rule matched no files.
|
|
51
|
+
*/
|
|
52
|
+
export function formatPatdownRuleBlock(ruleTitle, results) {
|
|
53
|
+
const failCount = results.filter((result) => result.violated).length;
|
|
54
|
+
const lines = [padPatdownRuleBlockTitle(ruleTitle, failCount, results.length)];
|
|
55
|
+
lines.push('│');
|
|
56
|
+
if (results.length === 0) {
|
|
57
|
+
lines.push('│ (no files matched)');
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const ordered = [...results].toSorted((left, right) => left.filePath.localeCompare(right.filePath));
|
|
61
|
+
const timeWidth = Math.max(...ordered.map((result) => formatPatdownConsoleElapsed(result.elapsedMs).length));
|
|
62
|
+
const nameWidth = Math.max(...ordered.map((result) => formatPatdownConsolePath(basename(result.filePath)).length));
|
|
63
|
+
let currentDirectory = null;
|
|
64
|
+
for (const result of ordered) {
|
|
65
|
+
const directory = patdownConsoleDirectoryLabel(result.filePath);
|
|
66
|
+
const startingNewDirectory = directory !== currentDirectory;
|
|
67
|
+
if (startingNewDirectory && currentDirectory !== null) {
|
|
68
|
+
lines.push('│');
|
|
69
|
+
}
|
|
70
|
+
if (startingNewDirectory) {
|
|
71
|
+
lines.push(formatPatdownDirectoryHeader(directory));
|
|
72
|
+
lines.push('│');
|
|
73
|
+
currentDirectory = directory;
|
|
74
|
+
}
|
|
75
|
+
lines.push(formatPatdownRuleBlockRow(result, timeWidth, nameWidth));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
lines.push('└');
|
|
79
|
+
return lines.join('\n');
|
|
80
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** One Choice candidate with a known 1-based line span. */
|
|
2
|
+
export type PatdownEvidenceCandidate = {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly startLine: number;
|
|
5
|
+
readonly endLine: number;
|
|
6
|
+
readonly content: string;
|
|
7
|
+
};
|
|
8
|
+
/** Prefer one Choice label per source line while the file stays under this size. */
|
|
9
|
+
export declare const patdownEvidencePerLineMaxCount = 500;
|
|
10
|
+
/** Chunk height when the file is too large for per-line Choice. */
|
|
11
|
+
export declare const patdownEvidenceChunkLineCount = 80;
|
|
12
|
+
/** Cap chunked candidates so the second pass stays bounded. */
|
|
13
|
+
export declare const patdownEvidenceChunkMaxCount = 24;
|
|
14
|
+
/** Truncate long line text used as Choice descriptions. */
|
|
15
|
+
export declare const patdownEvidenceDescriptionMaxChars = 160;
|
|
16
|
+
/** Minimum Choice confidence before a FAIL annotation uses the selected span. */
|
|
17
|
+
export declare const patdownEvidenceMinConfidence = 0.55;
|
|
18
|
+
/** Label returned when no candidate is strong enough. */
|
|
19
|
+
export declare function patdownEvidenceNoMatchChoice(): string;
|
|
20
|
+
/**
|
|
21
|
+
* Build FAIL-only Choice candidates. Prefer one label per line. Past
|
|
22
|
+
* {@link patdownEvidencePerLineMaxCount}, fall back to chunks. Smarter units (functions, headings,
|
|
23
|
+
* hunks) can replace this later without changing the Choice pass.
|
|
24
|
+
*/
|
|
25
|
+
export declare function splitPatdownEvidenceCandidates(contents: string, perLineMaxCount?: number, chunkLineCount?: number, chunkMaxCount?: number): ReadonlyArray<PatdownEvidenceCandidate>;
|
|
26
|
+
/** Choice criteria: candidate ids plus noMatch. Descriptions stay short. */
|
|
27
|
+
export type PatdownEvidenceChoiceCriteria = {
|
|
28
|
+
readonly [label: string]: string;
|
|
29
|
+
};
|
|
30
|
+
export declare function patdownEvidenceChoiceCriteria(candidates: ReadonlyArray<PatdownEvidenceCandidate>): PatdownEvidenceChoiceCriteria;
|
|
31
|
+
/** Inputs for FAIL-only evidence Choice state. */
|
|
32
|
+
export type PatdownEvidenceChoiceStateInput = {
|
|
33
|
+
readonly relativePath: string;
|
|
34
|
+
readonly ruleTitle: string;
|
|
35
|
+
readonly ruleBody: string;
|
|
36
|
+
readonly violationProbability: number;
|
|
37
|
+
readonly contents: string;
|
|
38
|
+
readonly candidates: ReadonlyArray<PatdownEvidenceCandidate>;
|
|
39
|
+
};
|
|
40
|
+
/** State for the evidence Choice: original FAIL, full file, and candidate index. */
|
|
41
|
+
export declare function formatPatdownEvidenceChoiceState(input: PatdownEvidenceChoiceStateInput): string;
|
|
42
|
+
/** Instructions for selecting the strongest violating candidate. */
|
|
43
|
+
export declare function patdownEvidenceChoiceInstructions(): string;
|
|
44
|
+
//# sourceMappingURL=patdown-evidence-regions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patdown-evidence-regions.d.ts","sourceRoot":"","sources":["../src/patdown-evidence-regions.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,MAAM,MAAM,wBAAwB,GAAG;IACtC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACxB,CAAA;AAED,oFAAoF;AACpF,eAAO,MAAM,8BAA8B,MAAM,CAAA;AAEjD,mEAAmE;AACnE,eAAO,MAAM,6BAA6B,KAAK,CAAA;AAE/C,+DAA+D;AAC/D,eAAO,MAAM,4BAA4B,KAAK,CAAA;AAE9C,2DAA2D;AAC3D,eAAO,MAAM,kCAAkC,MAAM,CAAA;AAErD,iFAAiF;AACjF,eAAO,MAAM,4BAA4B,OAAO,CAAA;AAIhD,yDAAyD;AACzD,wBAAgB,4BAA4B,IAAI,MAAM,CAErD;AAiED;;;;GAIG;AACH,wBAAgB,8BAA8B,CAC7C,QAAQ,EAAE,MAAM,EAChB,eAAe,GAAE,MAAuC,EACxD,cAAc,GAAE,MAAsC,EACtD,aAAa,GAAE,MAAqC,GAClD,aAAa,CAAC,wBAAwB,CAAC,CAQzC;AAED,4EAA4E;AAC5E,MAAM,MAAM,6BAA6B,GAAG;IAC3C,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAAA;CAChC,CAAA;AAED,wBAAgB,6BAA6B,CAC5C,UAAU,EAAE,aAAa,CAAC,wBAAwB,CAAC,GACjD,6BAA6B,CAkB/B;AAED,kDAAkD;AAClD,MAAM,MAAM,+BAA+B,GAAG;IAC7C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAA;IACrC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC,wBAAwB,CAAC,CAAA;CAC5D,CAAA;AAED,oFAAoF;AACpF,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,+BAA+B,GAAG,MAAM,CA+B/F;AAED,oEAAoE;AACpE,wBAAgB,iCAAiC,IAAI,MAAM,CAO1D"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/** Prefer one Choice label per source line while the file stays under this size. */
|
|
2
|
+
export const patdownEvidencePerLineMaxCount = 500;
|
|
3
|
+
/** Chunk height when the file is too large for per-line Choice. */
|
|
4
|
+
export const patdownEvidenceChunkLineCount = 80;
|
|
5
|
+
/** Cap chunked candidates so the second pass stays bounded. */
|
|
6
|
+
export const patdownEvidenceChunkMaxCount = 24;
|
|
7
|
+
/** Truncate long line text used as Choice descriptions. */
|
|
8
|
+
export const patdownEvidenceDescriptionMaxChars = 160;
|
|
9
|
+
/** Minimum Choice confidence before a FAIL annotation uses the selected span. */
|
|
10
|
+
export const patdownEvidenceMinConfidence = 0.55;
|
|
11
|
+
const patdownEvidenceNoMatchLabel = 'noMatch';
|
|
12
|
+
/** Label returned when no candidate is strong enough. */
|
|
13
|
+
export function patdownEvidenceNoMatchChoice() {
|
|
14
|
+
return patdownEvidenceNoMatchLabel;
|
|
15
|
+
}
|
|
16
|
+
function normalizePatdownEvidenceLines(contents) {
|
|
17
|
+
const normalized = contents.replace(/\r\n/gu, '\n');
|
|
18
|
+
if (normalized.length === 0)
|
|
19
|
+
return [''];
|
|
20
|
+
// Trailing newline must not create a phantom empty line / endLine past EOF.
|
|
21
|
+
const withoutTrailingNewline = normalized.endsWith('\n') ? normalized.slice(0, -1) : normalized;
|
|
22
|
+
return withoutTrailingNewline.length === 0 ? [''] : withoutTrailingNewline.split('\n');
|
|
23
|
+
}
|
|
24
|
+
function truncatePatdownEvidenceDescription(text) {
|
|
25
|
+
const collapsed = text.replace(/\s+/gu, ' ').trim();
|
|
26
|
+
if (collapsed.length === 0)
|
|
27
|
+
return '(empty line)';
|
|
28
|
+
if (collapsed.length <= patdownEvidenceDescriptionMaxChars)
|
|
29
|
+
return collapsed;
|
|
30
|
+
return `${collapsed.slice(0, patdownEvidenceDescriptionMaxChars - 1)}…`;
|
|
31
|
+
}
|
|
32
|
+
function splitPatdownEvidenceChunks(lines, lineCount, maxChunks) {
|
|
33
|
+
const candidates = [];
|
|
34
|
+
const step = Math.max(1, lineCount);
|
|
35
|
+
for (let startIndex = 0; startIndex < lines.length; startIndex += step) {
|
|
36
|
+
if (candidates.length >= maxChunks)
|
|
37
|
+
break;
|
|
38
|
+
const endIndex = Math.min(lines.length, startIndex + step);
|
|
39
|
+
const startLine = startIndex + 1;
|
|
40
|
+
const endLine = endIndex;
|
|
41
|
+
const id = `c${String(candidates.length + 1)}`;
|
|
42
|
+
candidates.push({
|
|
43
|
+
id,
|
|
44
|
+
startLine,
|
|
45
|
+
endLine,
|
|
46
|
+
content: lines.slice(startIndex, endIndex).join('\n'),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return candidates;
|
|
50
|
+
}
|
|
51
|
+
function splitPatdownEvidenceLines(lines) {
|
|
52
|
+
return lines.map((content, index) => {
|
|
53
|
+
const startLine = index + 1;
|
|
54
|
+
return {
|
|
55
|
+
id: `L${String(startLine)}`,
|
|
56
|
+
startLine,
|
|
57
|
+
endLine: startLine,
|
|
58
|
+
content,
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build FAIL-only Choice candidates. Prefer one label per line. Past
|
|
64
|
+
* {@link patdownEvidencePerLineMaxCount}, fall back to chunks. Smarter units (functions, headings,
|
|
65
|
+
* hunks) can replace this later without changing the Choice pass.
|
|
66
|
+
*/
|
|
67
|
+
export function splitPatdownEvidenceCandidates(contents, perLineMaxCount = patdownEvidencePerLineMaxCount, chunkLineCount = patdownEvidenceChunkLineCount, chunkMaxCount = patdownEvidenceChunkMaxCount) {
|
|
68
|
+
const lines = normalizePatdownEvidenceLines(contents);
|
|
69
|
+
if (lines.length <= perLineMaxCount) {
|
|
70
|
+
return splitPatdownEvidenceLines(lines);
|
|
71
|
+
}
|
|
72
|
+
return splitPatdownEvidenceChunks(lines, chunkLineCount, chunkMaxCount);
|
|
73
|
+
}
|
|
74
|
+
export function patdownEvidenceChoiceCriteria(candidates) {
|
|
75
|
+
const entries = [
|
|
76
|
+
[patdownEvidenceNoMatchLabel, 'No candidate provides clear, direct evidence of the violation'],
|
|
77
|
+
];
|
|
78
|
+
for (const candidate of candidates) {
|
|
79
|
+
const span = candidate.startLine === candidate.endLine
|
|
80
|
+
? `Line ${String(candidate.startLine)}`
|
|
81
|
+
: `Lines ${String(candidate.startLine)}-${String(candidate.endLine)}`;
|
|
82
|
+
entries.push([
|
|
83
|
+
candidate.id,
|
|
84
|
+
`${span}: ${truncatePatdownEvidenceDescription(candidate.content)}`,
|
|
85
|
+
]);
|
|
86
|
+
}
|
|
87
|
+
return Object.fromEntries(entries);
|
|
88
|
+
}
|
|
89
|
+
/** State for the evidence Choice: original FAIL, full file, and candidate index. */
|
|
90
|
+
export function formatPatdownEvidenceChoiceState(input) {
|
|
91
|
+
const mode = input.candidates.length > 0 && input.candidates[0]?.startLine === input.candidates[0]?.endLine
|
|
92
|
+
? 'per-line'
|
|
93
|
+
: 'chunked';
|
|
94
|
+
const parts = [
|
|
95
|
+
`path: ${input.relativePath}`,
|
|
96
|
+
`noul P(yes): ${String(input.violationProbability)}`,
|
|
97
|
+
`candidate mode: ${mode}`,
|
|
98
|
+
'',
|
|
99
|
+
`# ${input.ruleTitle}`,
|
|
100
|
+
'',
|
|
101
|
+
input.ruleBody,
|
|
102
|
+
'',
|
|
103
|
+
'Full file:',
|
|
104
|
+
input.contents,
|
|
105
|
+
'',
|
|
106
|
+
'Candidates (choose the strongest direct evidence of the violation):',
|
|
107
|
+
];
|
|
108
|
+
for (const candidate of input.candidates) {
|
|
109
|
+
const span = candidate.startLine === candidate.endLine
|
|
110
|
+
? `line ${String(candidate.startLine)}`
|
|
111
|
+
: `lines ${String(candidate.startLine)}-${String(candidate.endLine)}`;
|
|
112
|
+
parts.push(`- ${candidate.id}: ${span}`);
|
|
113
|
+
}
|
|
114
|
+
return parts.join('\n');
|
|
115
|
+
}
|
|
116
|
+
/** Instructions for selecting the strongest violating candidate. */
|
|
117
|
+
export function patdownEvidenceChoiceInstructions() {
|
|
118
|
+
return [
|
|
119
|
+
'Which candidate provides the strongest direct evidence that this file violates the rule?',
|
|
120
|
+
'When candidates are single lines, pick the specific line that contains the violation.',
|
|
121
|
+
'Select noMatch when no candidate provides sufficient evidence.',
|
|
122
|
+
'Prefer the clearest violation, not merely related text.',
|
|
123
|
+
].join(' ');
|
|
124
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
/** True when GitHub Actions exposes a step summary file. */
|
|
3
|
+
export declare const patdownGitHubActionsIsEnabled: Effect.Effect<boolean>;
|
|
4
|
+
/** Absolute path of the step summary file when Actions is active. */
|
|
5
|
+
export declare const readPatdownGitHubStepSummaryPath: Effect.Effect<string | null>;
|
|
6
|
+
//# sourceMappingURL=patdown-github-actions-env.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patdown-github-actions-env.d.ts","sourceRoot":"","sources":["../src/patdown-github-actions-env.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,MAAM,EAAU,MAAM,QAAQ,CAAA;AAE/C,4DAA4D;AAC5D,eAAO,MAAM,6BAA6B,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAiB/D,CAAA;AAEF,qEAAqE;AACrE,eAAO,MAAM,gCAAgC,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,CAWzE,CAAA"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Config, Effect, Option } from 'effect';
|
|
2
|
+
/** True when GitHub Actions exposes a step summary file. */
|
|
3
|
+
export const patdownGitHubActionsIsEnabled = Effect.gen(function* () {
|
|
4
|
+
const actions = yield* Config.string('GITHUB_ACTIONS').pipe(Config.option, Effect.orElseSucceed(() => Option.none()));
|
|
5
|
+
const summary = yield* Config.string('GITHUB_STEP_SUMMARY').pipe(Config.option, Effect.orElseSucceed(() => Option.none()));
|
|
6
|
+
return (Option.isSome(actions) &&
|
|
7
|
+
actions.value === 'true' &&
|
|
8
|
+
Option.isSome(summary) &&
|
|
9
|
+
summary.value.length > 0);
|
|
10
|
+
});
|
|
11
|
+
/** Absolute path of the step summary file when Actions is active. */
|
|
12
|
+
export const readPatdownGitHubStepSummaryPath = Effect.gen(function* () {
|
|
13
|
+
if (!(yield* patdownGitHubActionsIsEnabled))
|
|
14
|
+
return null;
|
|
15
|
+
const summary = yield* Config.string('GITHUB_STEP_SUMMARY').pipe(Config.option, Effect.orElseSucceed(() => Option.none()));
|
|
16
|
+
return Option.getOrNull(summary);
|
|
17
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { FileSystem, Layer } from 'effect';
|
|
2
|
+
import { PatdownOutput } from '#src/patdown-output';
|
|
3
|
+
/**
|
|
4
|
+
* Human stdout plus GitHub annotations and a step-summary heatmap. Intended when GITHUB_ACTIONS and
|
|
5
|
+
* GITHUB_STEP_SUMMARY are set.
|
|
6
|
+
*/
|
|
7
|
+
export declare const PatdownGitHubActionsOutputLive: Layer.Layer<PatdownOutput, never, FileSystem.FileSystem>;
|
|
8
|
+
//# sourceMappingURL=patdown-github-actions-output.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patdown-github-actions-output.d.ts","sourceRoot":"","sources":["../src/patdown-github-actions-output.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,UAAU,EAAE,KAAK,EAAO,MAAM,QAAQ,CAAA;AAOhE,OAAO,EACN,aAAa,EAIb,MAAM,qBAAqB,CAAA;AAM5B;;;GAGG;AACH,eAAO,MAAM,8BAA8B,EAAE,KAAK,CAAC,KAAK,CACvD,aAAa,EACb,KAAK,EACL,UAAU,CAAC,UAAU,CAsErB,CAAA"}
|