patdown 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,7 +28,7 @@ pnpm -w patdown -- ask "Is this urgent?" --input-text "ASAP" --verbose
28
28
  pnpm -w patdown -- --yes-threshold 0.9
29
29
  ```
30
30
 
31
- 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 the estimated probability of yes and the cutoff. The old `--noul` and `--state` flags have been replaced by a positional question and `--input-text`.
31
+ 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
32
 
33
33
  To judge piped output, use `--stdin`. For example, after building with `pnpm -w build` or running any `pnpm -w patdown` command:
34
34
 
@@ -119,7 +119,12 @@ FAIL README.md: No title case
119
119
  patdown: failed
120
120
  ```
121
121
 
122
- Exit 1 on a violation, a missing rules file, a read error, an invalid cutoff, or a judge error. Add `--verbose` to show probabilities. 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.
122
+ Exit 1 on a violation, a missing rules file, a read error, an invalid cutoff, or a judge error. Add `--verbose` to show a P(yes) shade bar, the numeric probability, the cutoff, and elapsed judge time. Lint totals that time across every rule/file pair.
123
+
124
+ ````
125
+ FAIL service.ts: explicit-actor (▓▓▓▓▓▓▓▓▒░ estimated P(yes): 0.86; cutoff: >0.85; elapsed: 312ms)
126
+ patdown: failed (elapsed: 1840ms)
127
+ ``` 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.
123
128
 
124
129
  See [judge providers](docs/judge-providers.md) for custom layers and the planned Effect Decision integration.
125
130
 
@@ -162,7 +167,7 @@ await Effect.runPromise(
162
167
  JsonOutputLive,
163
168
  ),
164
169
  )
165
- ```
170
+ ````
166
171
 
167
172
  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.
168
173
 
@@ -196,6 +201,8 @@ Inspired by [pi-warden](https://github.com/DevMortimer/pi-warden). Same idea, in
196
201
 
197
202
  [Abide](https://github.com/coldteadotai/abide) is a similar Jev-backed checker. It hooks into coding agents, reads project instruction files, and asks Jev whether each edit or turn broke a rule.
198
203
 
204
+ [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
+
199
206
  Name inspired by It's Always Sunny in Philadelphia
200
207
 
201
208
  <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,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AACxD,OAAO,EAAY,OAAO,EAAQ,MAAM,qBAAqB,CAAA;AAE7D,OAAO,EAAE,YAAY,EAAuC,MAAM,oBAAoB,CAAA;AAEtF,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AA4BnD,KAAK,mBAAmB,GACrB,UAAU,CAAC,UAAU,GACrB,KAAK,CAAC,KAAK,GACX,YAAY,GACZ,IAAI,CAAC,IAAI,GACT,aAAa,GACb,iBAAiB,CAAA;AAmBpB,4EAA4E;AAC5E,wBAAgB,kBAAkB,CACjC,gBAAgB,GAAE,OAAc,GAC9B,OAAO,CAAC,OAAO,CAAC,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,CAAC,CA6GvE;AAED,uEAAuE;AACvE,eAAO,MAAM,cAAc,uEAAuB,CAAA"}
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,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AACxD,OAAO,EAAY,OAAO,EAAQ,MAAM,qBAAqB,CAAA;AAE7D,OAAO,EAAE,YAAY,EAAuC,MAAM,oBAAoB,CAAA;AAEtF,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AA8BnD,KAAK,mBAAmB,GACrB,UAAU,CAAC,UAAU,GACrB,KAAK,CAAC,KAAK,GACX,YAAY,GACZ,IAAI,CAAC,IAAI,GACT,aAAa,GACb,iBAAiB,CAAA;AAuBpB,4EAA4E;AAC5E,wBAAgB,kBAAkB,CACjC,gBAAgB,GAAE,OAAc,GAC9B,OAAO,CAAC,OAAO,CAAC,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,CAAC,CA6GvE;AAED,uEAAuE;AACvE,eAAO,MAAM,cAAc,uEAAuB,CAAA"}
package/dist/cli.js CHANGED
@@ -12,20 +12,20 @@ const failPatdown = (message) => Effect.sync(() => {
12
12
  process.stderr.write(`${message}\n`);
13
13
  });
14
14
  const rulesFileFlag = Flag.optional(Flag.string('rules')).pipe(Flag.withDescription('Path passed to the rule source. Markdown skips the AGENTS.PATDOWN.md walk'));
15
- const verboseFlag = Flag.boolean('verbose').pipe(Flag.withDefault(false), Flag.withDescription('Show estimated yes probabilities and the decision cutoff'));
15
+ const verboseFlag = Flag.boolean('verbose').pipe(Flag.withDefault(false), Flag.withDescription('Show estimated yes probabilities, the decision cutoff, and elapsed judge time'));
16
16
  const adapterFlag = Flag.optional(Flag.string('adapter')).pipe(Flag.withDescription('Module exporting PatdownRuleSourceLive, replacingkdown rule parsing'));
17
17
  const yesThresholdFlag = Flag.optional(Flag.float('yes-threshold')).pipe(Flag.withDescription('Minimum exclusive P(yes) for yes; default 0.85, overridable per rule'));
18
- function finishPatdownLint(failed) {
18
+ function finishPatdownLint(failed, verbose, elapsedMs) {
19
19
  return Effect.gen(function* () {
20
20
  const output = yield* PatdownOutput;
21
21
  if (failed) {
22
- yield* output.writeLintFailed;
22
+ yield* output.writeLintFailed(verbose ? elapsedMs : undefined);
23
23
  yield* Effect.sync(() => {
24
24
  process.exitCode = 1;
25
25
  });
26
26
  return;
27
27
  }
28
- yield* output.writeLintOk;
28
+ yield* output.writeLintOk(verbose ? elapsedMs : undefined);
29
29
  });
30
30
  }
31
31
  /** Builds commands with optional adapter discovery for embedded callers. */
@@ -53,8 +53,8 @@ export function makePatdownCommand(discoverAdapters = true) {
53
53
  const output = yield* PatdownOutput;
54
54
  const text = yield* readPatdownQuestionInput(inputText, stdin);
55
55
  const cutoff = yield* resolvePatdownYesThreshold(yesThreshold);
56
- const answer = yield* askPatdownJudge(question, text);
57
- yield* output.writeAnswer(answer, verbose, cutoff);
56
+ const timed = yield* askPatdownJudge(question, text);
57
+ yield* output.writeAnswer(timed.judgment, verbose, cutoff, timed.elapsedMs);
58
58
  }).pipe(Effect.catchTags({
59
59
  PatdownJudgeFailed: (error) => failPatdown(error.message),
60
60
  PatdownYesThresholdInvalid: (error) => failPatdown(error.message),
@@ -71,8 +71,8 @@ export function makePatdownCommand(discoverAdapters = true) {
71
71
  const document = yield* discoverAdapters
72
72
  ? loadConfiguredPatdownRules(adapter, rules)
73
73
  : patdownRuleSource.loadPatdownRules(rules);
74
- const failed = yield* runPatdownLint(document, verbose, cutoff);
75
- yield* finishPatdownLint(failed);
74
+ const linted = yield* runPatdownLint(document, verbose, cutoff);
75
+ yield* finishPatdownLint(linted.failed, verbose, linted.elapsedMs);
76
76
  }).pipe(Effect.catchTags({
77
77
  PatdownRulesLoadFailed: (error) => failPatdown(error.message),
78
78
  PatdownJudgeFailed: (error) => failPatdown(error.message),
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export { patdownCommand, makePatdownCommand } from '#src/cli';
2
2
  export { PatdownOutput, PatdownOutputLive, type PatdownLintResult } from '#src/patdown-output';
3
3
  export type { PatdownRuleSourceLayer } from '#src/patdown-rule-source-adapter';
4
4
  export { runPatdownCli } from '#src/run-patdown-cli';
5
- export { PatdownJudge, PatdownJudgeFailed, PatdownJudgmentSchema, askPatdownJudge, patdownJudgmentIsYes, patdownYesThreshold, type PatdownJudgment, } from '#src/patdown-judge';
5
+ export { PatdownJudge, PatdownJudgeFailed, PatdownJudgmentSchema, askPatdownJudge, patdownJudgmentIsYes, patdownYesThreshold, type PatdownJudgment, type PatdownTimedJudgment, } from '#src/patdown-judge';
6
6
  export { defaultPatdownYesThreshold, decodePatdownYesThreshold, PatdownYesThresholdInvalid, type PatdownYesThreshold, } from '@patdown/rules';
7
7
  export { TypeSafeJudgeLive } from '#src/typesafe-judge';
8
8
  //# sourceMappingURL=index.d.ts.map
@@ -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,GACpB,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,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"}
@@ -6,6 +6,11 @@ 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
+ };
9
14
  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
15
  readonly _tag: "PatdownJudgeFailed";
11
16
  } & Readonly<A>;
@@ -25,6 +30,6 @@ export declare const patdownYesThreshold = 0.85;
25
30
  /** Applies a cutoff to a validated probability. Equality is not yes. */
26
31
  export declare function patdownJudgmentIsYes(judgment: PatdownJudgment, yesThreshold?: PatdownYesThreshold): boolean;
27
32
  /** Validates custom judge responses at the service boundary before applying policy or printing. */
28
- export declare function askPatdownJudge(question: string, inputText: string): Effect.Effect<PatdownJudgment, PatdownJudgeFailed, PatdownJudge>;
33
+ export declare function askPatdownJudge(question: string, inputText: string): Effect.Effect<PatdownTimedJudgment, PatdownJudgeFailed, PatdownJudge>;
29
34
  export {};
30
35
  //# 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,EAAE,OAAO,EAAQ,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAEtD,0FAA0F;AAC1F,eAAO,MAAM,qBAAqB;;EAEhC,CAAA;AAEF,wFAAwF;AACxF,MAAM,MAAM,eAAe,GAAG,OAAO,qBAAqB,CAAC,IAAI,CAAA;;;;AAE/D,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;;AAPzD,6FAA6F;AAC7F,qBAAa,YAAa,SAAQ,iBAQF;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,eAAe,EAAE,kBAAkB,EAAE,YAAY,CAAC,CAYlE"}
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;;AAPzD,6FAA6F;AAC7F,qBAAa,YAAa,SAAQ,iBAQF;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"}
@@ -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,13 @@ 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
- return yield* Schema.decodeEffect(PatdownJudgmentSchema)(answer).pipe(Effect.mapError(() => new PatdownJudgeFailed({ message: 'patdown: judge returned an invalid yes probability' })));
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
+ };
25
31
  });
26
32
  }
@@ -3,5 +3,8 @@ import { Effect, FileSystem, Path } from 'effect';
3
3
  import { PatdownJudge, PatdownJudgeFailed } from '#src/patdown-judge';
4
4
  import { PatdownOutput } from '#src/patdown-output';
5
5
  /** 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<boolean, PatdownJudgeFailed | PatdownYesThresholdInvalid, FileSystem.FileSystem | PatdownJudge | Path.Path | PatdownOutput>;
6
+ export declare function runPatdownLint(document: PatdownRulesDocument, verbose?: boolean, yesThreshold?: PatdownYesThreshold): Effect.Effect<{
7
+ readonly failed: boolean;
8
+ readonly elapsedMs: number;
9
+ }, PatdownJudgeFailed | PatdownYesThresholdInvalid, FileSystem.FileSystem | PatdownJudge | Path.Path | PatdownOutput>;
7
10
  //# 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,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAEjD,OAAO,EACN,YAAY,EACZ,kBAAkB,EAGlB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AA0InD,iFAAiF;AACjF,wBAAgB,cAAc,CAC7B,QAAQ,EAAE,oBAAoB,EAC9B,OAAO,GAAE,OAAe,EACxB,YAAY,GAAE,mBAAgD,GAC5D,MAAM,CAAC,MAAM,CACf,OAAO,EACP,kBAAkB,GAAG,0BAA0B,EAC/C,UAAU,CAAC,UAAU,GAAG,YAAY,GAAG,IAAI,CAAC,IAAI,GAAG,aAAa,CAChE,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;AAExD,OAAO,EACN,YAAY,EACZ,kBAAkB,EAGlB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AA2InD,iFAAiF;AACjF,wBAAgB,cAAc,CAC7B,QAAQ,EAAE,oBAAoB,EAC9B,OAAO,GAAE,OAAe,EACxB,YAAY,GAAE,mBAAgD,GAC5D,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,CAmBA"}
@@ -1,5 +1,5 @@
1
1
  import { defaultPatdownYesThreshold, PatdownYesThresholdInvalid, } from '@patdown/rules';
2
- import { Effect, FileSystem, Path } from 'effect';
2
+ import { Clock, Effect, FileSystem, Path } from 'effect';
3
3
  import { PatdownJudge, PatdownJudgeFailed, askPatdownJudge, patdownJudgmentIsYes, } from '#src/patdown-judge';
4
4
  import { PatdownOutput } from '#src/patdown-output';
5
5
  import { decodePatdownRuleYesThreshold } from '#src/patdown-yes-threshold-config';
@@ -51,14 +51,15 @@ function lintPatdownRuleFile(rule, filePath, options) {
51
51
  const contents = yield* fileSystem.readFileString(filePath).pipe(Effect.mapError(() => new PatdownJudgeFailed({
52
52
  message: `patdown: failed to read ${relativePath}`,
53
53
  })));
54
- const answer = yield* askPatdownJudge(patdownViolationInstructions(rule), patdownFileState(relativePath, contents));
55
- const failed = patdownJudgmentIsYes(answer, options.yesThreshold);
54
+ const timed = yield* askPatdownJudge(patdownViolationInstructions(rule), patdownFileState(relativePath, contents));
55
+ const failed = patdownJudgmentIsYes(timed.judgment, options.yesThreshold);
56
56
  yield* output.writeLintResult({
57
57
  violated: failed,
58
58
  ruleTitle: rule.patdownRuleTitle,
59
59
  filePath: relativePath,
60
- violationProbability: answer.yesProbability,
60
+ violationProbability: timed.judgment.yesProbability,
61
61
  yesThreshold: options.yesThreshold,
62
+ elapsedMs: timed.elapsedMs,
62
63
  }, options.verbose);
63
64
  return failed;
64
65
  });
@@ -83,7 +84,12 @@ export function runPatdownLint(document, verbose = false, yesThreshold = default
83
84
  return Effect.gen(function* () {
84
85
  const path = yield* Path.Path;
85
86
  const cwd = path.resolve('.');
87
+ const startedAt = yield* Clock.currentTimeMillis;
86
88
  const failures = yield* Effect.forEach(document.patdownRules, (rule) => lintPatdownRule(rule, cwd, verbose, yesThreshold), { concurrency: 1 });
87
- return failures.some((failed) => failed);
89
+ const finishedAt = yield* Clock.currentTimeMillis;
90
+ return {
91
+ failed: failures.some((failed) => failed),
92
+ elapsedMs: Math.max(0, finishedAt - startedAt),
93
+ };
88
94
  });
89
95
  }
@@ -9,13 +9,14 @@ export type PatdownLintResult = {
9
9
  readonly filePath: string;
10
10
  readonly violationProbability: number;
11
11
  readonly yesThreshold: PatdownYesThreshold;
12
+ readonly elapsedMs: number;
12
13
  };
13
14
  declare const PatdownOutput_base: Context.ServiceClass<PatdownOutput, "@patdown/cli/PatdownOutput", {
14
- readonly writeLintFailed: Effect.Effect<void>;
15
- readonly writeLintOk: Effect.Effect<void>;
15
+ readonly writeLintFailed: (elapsedMs?: number) => Effect.Effect<void>;
16
+ readonly writeLintOk: (elapsedMs?: number) => Effect.Effect<void>;
16
17
  readonly writeLintResult: (result: PatdownLintResult, verbose: boolean) => Effect.Effect<void>;
17
18
  readonly writeNoFilesMatched: (ruleTitle: string) => Effect.Effect<void>;
18
- readonly writeAnswer: (judgment: PatdownJudgment, verbose: boolean, yesThreshold?: PatdownYesThreshold) => Effect.Effect<void>;
19
+ readonly writeAnswer: (judgment: PatdownJudgment, verbose: boolean, yesThreshold?: PatdownYesThreshold, elapsedMs?: number) => Effect.Effect<void>;
19
20
  readonly writeRulesDocument: (document: PatdownRulesDocument) => Effect.Effect<void>;
20
21
  }>;
21
22
  /** Swappable output service. Probability details are opt-in. */
@@ -1 +1 @@
1
- {"version":3,"file":"patdown-output.d.ts","sourceRoot":"","sources":["../src/patdown-output.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAC1D,OAAO,EAA8B,KAAK,mBAAmB,EAAE,MAAM,gBAAgB,CAAA;AACrF,OAAO,EAAW,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AAExD,OAAO,EAAwB,KAAK,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAE/E,+FAA+F;AAC/F,MAAM,MAAM,iBAAiB,GAAG;IAC/B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAA;IACrC,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAA;CAC1C,CAAA;;8BAmD2B,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;0BACvB,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;8BACf,CAAC,MAAM,EAAE,iBAAiB,EAAE,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;kCAChE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;0BAClD,CACrB,QAAQ,EAAE,eAAe,EACzB,OAAO,EAAE,OAAO,EAChB,YAAY,CAAC,EAAE,mBAAmB,KAC9B,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;iCACK,CAAC,QAAQ,EAAE,oBAAoB,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;;AAbtF,gEAAgE;AAChE,qBAAa,aAAc,SAAQ,kBAcF;CAAG;AAEpC,2EAA2E;AAC3E,eAAO,MAAM,iBAAiB,0CAQ5B,CAAA"}
1
+ {"version":3,"file":"patdown-output.d.ts","sourceRoot":"","sources":["../src/patdown-output.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAC1D,OAAO,EAA8B,KAAK,mBAAmB,EAAE,MAAM,gBAAgB,CAAA;AACrF,OAAO,EAAW,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AAExD,OAAO,EAAwB,KAAK,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAG/E,+FAA+F;AAC/F,MAAM,MAAM,iBAAiB,GAAG;IAC/B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAA;IACrC,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAA;IAC1C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC1B,CAAA;;8BA4D2B,CAAC,SAAS,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;0BAC/C,CAAC,SAAS,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;8BACvC,CAAC,MAAM,EAAE,iBAAiB,EAAE,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;kCAChE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;0BAClD,CACrB,QAAQ,EAAE,eAAe,EACzB,OAAO,EAAE,OAAO,EAChB,YAAY,CAAC,EAAE,mBAAmB,EAClC,SAAS,CAAC,EAAE,MAAM,KACd,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;iCACK,CAAC,QAAQ,EAAE,oBAAoB,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;;AAdtF,gEAAgE;AAChE,qBAAa,aAAc,SAAQ,kBAeF;CAAG;AAEpC,2EAA2E;AAC3E,eAAO,MAAM,iBAAiB,0CAkB5B,CAAA"}
@@ -1,20 +1,24 @@
1
1
  import { defaultPatdownYesThreshold } from '@patdown/rules';
2
2
  import { Console, Context, Effect, Layer } from 'effect';
3
3
  import { patdownJudgmentIsYes } from '#src/patdown-judge';
4
- function formatPatdownProbability(probability, yesThreshold) {
5
- return `estimated P(yes): ${String(probability)}; cutoff: >${String(yesThreshold)}`;
4
+ import { formatPatdownProbabilityBar } from '#src/patdown-probability-bar';
5
+ function formatPatdownElapsedMs(elapsedMs) {
6
+ return `${String(elapsedMs)}ms`;
6
7
  }
7
- function formatPatdownAnswer(judgment, verbose, yesThreshold) {
8
+ function formatPatdownProbability(probability, yesThreshold, elapsedMs) {
9
+ return `${formatPatdownProbabilityBar(probability)} estimated P(yes): ${String(probability)}; cutoff: >${String(yesThreshold)}; elapsed: ${formatPatdownElapsedMs(elapsedMs)}`;
10
+ }
11
+ function formatPatdownAnswer(judgment, verbose, yesThreshold, elapsedMs) {
8
12
  const answer = patdownJudgmentIsYes(judgment, yesThreshold) ? 'yes' : 'no';
9
13
  return verbose
10
- ? `${answer} (${formatPatdownProbability(judgment.yesProbability, yesThreshold)})`
14
+ ? `${answer} (${formatPatdownProbability(judgment.yesProbability, yesThreshold, elapsedMs)})`
11
15
  : answer;
12
16
  }
13
17
  function formatPatdownLintResult(result, verbose) {
14
18
  const verdict = result.violated ? 'FAIL' : 'PASS';
15
19
  const line = `${verdict} ${result.filePath}: ${result.ruleTitle}`;
16
20
  return verbose
17
- ? `${line} (${formatPatdownProbability(result.violationProbability, result.yesThreshold)})`
21
+ ? `${line} (${formatPatdownProbability(result.violationProbability, result.yesThreshold, result.elapsedMs)})`
18
22
  : line;
19
23
  }
20
24
  function formatPatdownRulesDocument(document) {
@@ -36,10 +40,14 @@ export class PatdownOutput extends Context.Service()('@patdown/cli/PatdownOutput
36
40
  }
37
41
  /** Default human-readable output, without provider-specific vocabulary. */
38
42
  export const PatdownOutputLive = Layer.succeed(PatdownOutput, {
39
- writeLintFailed: Console.log('patdown: failed'),
40
- writeLintOk: Console.log('patdown: ok'),
43
+ writeLintFailed: (elapsedMs) => Console.log(elapsedMs === undefined
44
+ ? 'patdown: failed'
45
+ : `patdown: failed (elapsed: ${formatPatdownElapsedMs(elapsedMs)})`),
46
+ writeLintOk: (elapsedMs) => Console.log(elapsedMs === undefined
47
+ ? 'patdown: ok'
48
+ : `patdown: ok (elapsed: ${formatPatdownElapsedMs(elapsedMs)})`),
41
49
  writeLintResult: (result, verbose) => Console.log(formatPatdownLintResult(result, verbose)),
42
50
  writeNoFilesMatched: (title) => Console.log(`patdown: no files matched ${title}`),
43
- writeAnswer: (judgment, verbose, yesThreshold = defaultPatdownYesThreshold) => Console.log(formatPatdownAnswer(judgment, verbose, yesThreshold)),
51
+ writeAnswer: (judgment, verbose, yesThreshold = defaultPatdownYesThreshold, elapsedMs = 0) => Console.log(formatPatdownAnswer(judgment, verbose, yesThreshold, elapsedMs)),
44
52
  writeRulesDocument: (document) => Console.log(formatPatdownRulesDocument(document)),
45
53
  });
@@ -0,0 +1,3 @@
1
+ /** Ten-cell shade bar for estimated P(yes). Search for ▓ ▒ ░ to find the glyphs. */
2
+ export declare function formatPatdownProbabilityBar(probability: number): string;
3
+ //# sourceMappingURL=patdown-probability-bar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patdown-probability-bar.d.ts","sourceRoot":"","sources":["../src/patdown-probability-bar.ts"],"names":[],"mappings":"AAgBA,oFAAoF;AACpF,wBAAgB,2BAA2B,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAqBvE"}
@@ -0,0 +1,29 @@
1
+ const patdownProbabilityBarFilled = '▓';
2
+ const patdownProbabilityBarPartial = '▒';
3
+ const patdownProbabilityBarEmpty = '░';
4
+ const patdownProbabilityBarWidth = 10;
5
+ function clampPatdownProbability(probability) {
6
+ if (probability <= 0)
7
+ return 0;
8
+ if (probability >= 1)
9
+ return 1;
10
+ return probability;
11
+ }
12
+ /** Ten-cell shade bar for estimated P(yes). Search for ▓ ▒ ░ to find the glyphs. */
13
+ export function formatPatdownProbabilityBar(probability) {
14
+ const filledCells = clampPatdownProbability(probability) * patdownProbabilityBarWidth;
15
+ const cells = [];
16
+ for (let index = 0; index < patdownProbabilityBarWidth; index += 1) {
17
+ const cellFill = filledCells - index;
18
+ if (cellFill >= 1) {
19
+ cells.push(patdownProbabilityBarFilled);
20
+ continue;
21
+ }
22
+ if (cellFill >= 0.5) {
23
+ cells.push(patdownProbabilityBarPartial);
24
+ continue;
25
+ }
26
+ cells.push(patdownProbabilityBarEmpty);
27
+ }
28
+ return cells.join('');
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patdown",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Standalone CLI that lints a tree against fuzzy markdown rules",
5
5
  "homepage": "https://github.com/tyler-dot-earth/patdown",
6
6
  "bugs": {
@@ -48,8 +48,8 @@
48
48
  "@effect/platform-node": "4.0.0-rc.112",
49
49
  "effect": "4.0.0-rc.112",
50
50
  "import-meta-resolve": "4.2.0",
51
- "@patdown/jev": "0.3.1",
52
- "@patdown/rules": "0.3.1"
51
+ "@patdown/jev": "0.4.0",
52
+ "@patdown/rules": "0.4.0"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@effect/language-service": "0.87.2",
@@ -61,8 +61,8 @@
61
61
  "oxlint-tsgolint": "^7.0.2001",
62
62
  "typescript": "7.0.2",
63
63
  "vitest": "^4.1.11",
64
- "@patdown/oxlint-config": "0.0.0",
65
- "@patdown/tsconfig": "0.0.0"
64
+ "@patdown/tsconfig": "0.0.0",
65
+ "@patdown/oxlint-config": "0.0.0"
66
66
  },
67
67
  "engines": {
68
68
  "node": ">=22.22.2"