patdown 0.3.1

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.
Files changed (54) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +201 -0
  3. package/dist/cli.d.ts +12 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +85 -0
  6. package/dist/index.d.ts +8 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +6 -0
  9. package/dist/markdown-squint-rule-parser.d.ts +7 -0
  10. package/dist/markdown-squint-rule-parser.d.ts.map +1 -0
  11. package/dist/markdown-squint-rule-parser.js +85 -0
  12. package/dist/patdown-cli-bin.d.ts +3 -0
  13. package/dist/patdown-cli-bin.d.ts.map +1 -0
  14. package/dist/patdown-cli-bin.js +4 -0
  15. package/dist/patdown-cli-version.d.ts +3 -0
  16. package/dist/patdown-cli-version.d.ts.map +1 -0
  17. package/dist/patdown-cli-version.js +18 -0
  18. package/dist/patdown-judge.d.ts +30 -0
  19. package/dist/patdown-judge.d.ts.map +1 -0
  20. package/dist/patdown-judge.js +26 -0
  21. package/dist/patdown-lint.d.ts +7 -0
  22. package/dist/patdown-lint.d.ts.map +1 -0
  23. package/dist/patdown-lint.js +89 -0
  24. package/dist/patdown-output.d.ts +27 -0
  25. package/dist/patdown-output.d.ts.map +1 -0
  26. package/dist/patdown-output.js +45 -0
  27. package/dist/patdown-package-config.d.ts +15 -0
  28. package/dist/patdown-package-config.d.ts.map +1 -0
  29. package/dist/patdown-package-config.js +123 -0
  30. package/dist/patdown-question-input.d.ts +5 -0
  31. package/dist/patdown-question-input.d.ts.map +1 -0
  32. package/dist/patdown-question-input.js +21 -0
  33. package/dist/patdown-rule-source-adapter.d.ts +10 -0
  34. package/dist/patdown-rule-source-adapter.d.ts.map +1 -0
  35. package/dist/patdown-rule-source-adapter.js +83 -0
  36. package/dist/patdown-yes-threshold-config.d.ts +7 -0
  37. package/dist/patdown-yes-threshold-config.d.ts.map +1 -0
  38. package/dist/patdown-yes-threshold-config.js +20 -0
  39. package/dist/run-patdown-cli.d.ts +12 -0
  40. package/dist/run-patdown-cli.d.ts.map +1 -0
  41. package/dist/run-patdown-cli.js +17 -0
  42. package/dist/squint-lint.d.ts +7 -0
  43. package/dist/squint-lint.d.ts.map +1 -0
  44. package/dist/squint-lint.js +80 -0
  45. package/dist/squint-rule-source.d.ts +34 -0
  46. package/dist/squint-rule-source.d.ts.map +1 -0
  47. package/dist/squint-rule-source.js +78 -0
  48. package/dist/squint-rule.d.ts +17 -0
  49. package/dist/squint-rule.d.ts.map +1 -0
  50. package/dist/squint-rule.js +2 -0
  51. package/dist/typesafe-judge.d.ts +8 -0
  52. package/dist/typesafe-judge.d.ts.map +1 -0
  53. package/dist/typesafe-judge.js +14 -0
  54. package/package.json +83 -0
@@ -0,0 +1,27 @@
1
+ import type { PatdownRulesDocument } from '@patdown/rules';
2
+ import { type PatdownYesThreshold } from '@patdown/rules';
3
+ import { Context, Effect, Layer } from 'effect';
4
+ import { type PatdownJudgment } from '#src/patdown-judge';
5
+ /** One file/rule result; probability estimates a violation, not correctness of the verdict. */
6
+ export type PatdownLintResult = {
7
+ readonly violated: boolean;
8
+ readonly ruleTitle: string;
9
+ readonly filePath: string;
10
+ readonly violationProbability: number;
11
+ readonly yesThreshold: PatdownYesThreshold;
12
+ };
13
+ declare const PatdownOutput_base: Context.ServiceClass<PatdownOutput, "@patdown/cli/PatdownOutput", {
14
+ readonly writeLintFailed: Effect.Effect<void>;
15
+ readonly writeLintOk: Effect.Effect<void>;
16
+ readonly writeLintResult: (result: PatdownLintResult, verbose: boolean) => Effect.Effect<void>;
17
+ readonly writeNoFilesMatched: (ruleTitle: string) => Effect.Effect<void>;
18
+ readonly writeAnswer: (judgment: PatdownJudgment, verbose: boolean, yesThreshold?: PatdownYesThreshold) => Effect.Effect<void>;
19
+ readonly writeRulesDocument: (document: PatdownRulesDocument) => Effect.Effect<void>;
20
+ }>;
21
+ /** Swappable output service. Probability details are opt-in. */
22
+ export declare class PatdownOutput extends PatdownOutput_base {
23
+ }
24
+ /** Default human-readable output, without provider-specific vocabulary. */
25
+ export declare const PatdownOutputLive: Layer.Layer<PatdownOutput, never, never>;
26
+ export {};
27
+ //# sourceMappingURL=patdown-output.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,45 @@
1
+ import { defaultPatdownYesThreshold } from '@patdown/rules';
2
+ import { Console, Context, Effect, Layer } from 'effect';
3
+ import { patdownJudgmentIsYes } from '#src/patdown-judge';
4
+ function formatPatdownProbability(probability, yesThreshold) {
5
+ return `estimated P(yes): ${String(probability)}; cutoff: >${String(yesThreshold)}`;
6
+ }
7
+ function formatPatdownAnswer(judgment, verbose, yesThreshold) {
8
+ const answer = patdownJudgmentIsYes(judgment, yesThreshold) ? 'yes' : 'no';
9
+ return verbose
10
+ ? `${answer} (${formatPatdownProbability(judgment.yesProbability, yesThreshold)})`
11
+ : answer;
12
+ }
13
+ function formatPatdownLintResult(result, verbose) {
14
+ const verdict = result.violated ? 'FAIL' : 'PASS';
15
+ const line = `${verdict} ${result.filePath}: ${result.ruleTitle}`;
16
+ return verbose
17
+ ? `${line} (${formatPatdownProbability(result.violationProbability, result.yesThreshold)})`
18
+ : line;
19
+ }
20
+ function formatPatdownRulesDocument(document) {
21
+ const lines = [
22
+ `patdown: ${String(document.patdownRules.length)} rules from ${document.patdownRulesFilePath}`,
23
+ ];
24
+ for (const rule of document.patdownRules) {
25
+ lines.push('');
26
+ lines.push(rule.patdownRuleTitle);
27
+ lines.push(`globs: ${rule.patdownRuleGlobs.length === 0 ? '*' : rule.patdownRuleGlobs.join(' ')}`);
28
+ if (rule.patdownRuleYesThreshold !== undefined) {
29
+ lines.push(`yes-threshold: ${String(rule.patdownRuleYesThreshold)}`);
30
+ }
31
+ }
32
+ return lines.join('\n');
33
+ }
34
+ /** Swappable output service. Probability details are opt-in. */
35
+ export class PatdownOutput extends Context.Service()('@patdown/cli/PatdownOutput') {
36
+ }
37
+ /** Default human-readable output, without provider-specific vocabulary. */
38
+ export const PatdownOutputLive = Layer.succeed(PatdownOutput, {
39
+ writeLintFailed: Console.log('patdown: failed'),
40
+ writeLintOk: Console.log('patdown: ok'),
41
+ writeLintResult: (result, verbose) => Console.log(formatPatdownLintResult(result, verbose)),
42
+ writeNoFilesMatched: (title) => Console.log(`patdown: no files matched ${title}`),
43
+ writeAnswer: (judgment, verbose, yesThreshold = defaultPatdownYesThreshold) => Console.log(formatPatdownAnswer(judgment, verbose, yesThreshold)),
44
+ writeRulesDocument: (document) => Console.log(formatPatdownRulesDocument(document)),
45
+ });
@@ -0,0 +1,15 @@
1
+ import { PatdownRulesLoadFailed, PatdownYesThresholdInvalid, type PatdownYesThreshold } from '@patdown/rules';
2
+ import { Effect, FileSystem } from 'effect';
3
+ export type PatdownPackageConfig = {
4
+ readonly adapter?: string;
5
+ readonly fromDirectory: string;
6
+ readonly yesThreshold?: PatdownYesThreshold;
7
+ };
8
+ /**
9
+ * Walks from cwd for package.json `patdown` settings. A nearer file without adapter/yesThreshold
10
+ * does not hide a parent that has them. Invalid JSON fails immediately.
11
+ */
12
+ export declare function discoverPatdownPackageConfig(startDirectory?: string): Effect.Effect<PatdownPackageConfig | null, PatdownRulesLoadFailed | PatdownYesThresholdInvalid, FileSystem.FileSystem>;
13
+ /** Reads only a cutoff from package.json. Broken adapter config is skipped for `ask`. */
14
+ export declare function discoverPatdownYesThresholdConfig(startDirectory?: string): Effect.Effect<PatdownYesThreshold | null, PatdownYesThresholdInvalid, FileSystem.FileSystem>;
15
+ //# sourceMappingURL=patdown-package-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patdown-package-config.d.ts","sourceRoot":"","sources":["../src/patdown-package-config.ts"],"names":[],"mappings":"AAEA,OAAO,EAEN,sBAAsB,EACtB,0BAA0B,EAC1B,KAAK,mBAAmB,EACxB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,MAAM,EAAE,UAAU,EAAU,MAAM,QAAQ,CAAA;AAanD,MAAM,MAAM,oBAAoB,GAAG;IAClC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,YAAY,CAAC,EAAE,mBAAmB,CAAA;CAC3C,CAAA;AAoID;;;GAGG;AACH,wBAAgB,4BAA4B,CAC3C,cAAc,GAAE,MAAsB,GACpC,MAAM,CAAC,MAAM,CACf,oBAAoB,GAAG,IAAI,EAC3B,sBAAsB,GAAG,0BAA0B,EACnD,UAAU,CAAC,UAAU,CACrB,CAgCA;AAED,yFAAyF;AACzF,wBAAgB,iCAAiC,CAChD,cAAc,GAAE,MAAsB,GACpC,MAAM,CAAC,MAAM,CAAC,mBAAmB,GAAG,IAAI,EAAE,0BAA0B,EAAE,UAAU,CAAC,UAAU,CAAC,CAmB9F"}
@@ -0,0 +1,123 @@
1
+ import { dirname, join, resolve } from 'node:path';
2
+ import { decodePatdownYesThreshold, PatdownRulesLoadFailed, PatdownYesThresholdInvalid, } from '@patdown/rules';
3
+ import { Effect, FileSystem, Schema } from 'effect';
4
+ const PatdownPackageJsonSchema = Schema.fromJsonString(Schema.Struct({
5
+ patdown: Schema.optionalKey(Schema.Struct({
6
+ adapter: Schema.optionalKey(Schema.NonEmptyString),
7
+ yesThreshold: Schema.optionalKey(Schema.Finite),
8
+ })),
9
+ }));
10
+ function decodeConfiguredYesThreshold(value, filename) {
11
+ const decoded = decodePatdownYesThreshold(value, `package.json ${filename} patdown.yesThreshold`);
12
+ return decoded instanceof PatdownYesThresholdInvalid
13
+ ? Effect.fail(decoded)
14
+ : Effect.succeed(decoded);
15
+ }
16
+ function failedPackageJson(filename) {
17
+ return new PatdownRulesLoadFailed({
18
+ message: `patdown: failed to read ${filename}`,
19
+ });
20
+ }
21
+ function decodedPackageJson(adapter, yesThreshold) {
22
+ const decoded = {};
23
+ return {
24
+ ...(adapter === undefined ? decoded : { ...decoded, adapter }),
25
+ ...(yesThreshold === undefined ? decoded : { ...decoded, yesThreshold }),
26
+ };
27
+ }
28
+ function readPatdownPackageJson(filename) {
29
+ return Effect.gen(function* () {
30
+ const fs = yield* FileSystem.FileSystem;
31
+ const exists = yield* fs
32
+ .exists(filename)
33
+ .pipe(Effect.mapError(() => failedPackageJson(filename)));
34
+ if (!exists)
35
+ return null;
36
+ const text = yield* fs
37
+ .readFileString(filename)
38
+ .pipe(Effect.mapError(() => failedPackageJson(filename)));
39
+ const decoded = yield* Schema.decodeEffect(PatdownPackageJsonSchema)(text).pipe(Effect.mapError((cause) => new PatdownRulesLoadFailed({
40
+ message: `patdown: invalid adapter config ${filename}: ${String(cause)}`,
41
+ })));
42
+ if (decoded.patdown === undefined)
43
+ return {};
44
+ return decodedPackageJson(decoded.patdown.adapter, decoded.patdown.yesThreshold);
45
+ });
46
+ }
47
+ function collectPackageConfig(directory, adapter, yesThresholdValue, decoded) {
48
+ const nextAdapter = adapter ??
49
+ (decoded.adapter === undefined
50
+ ? undefined
51
+ : { moduleSpecifier: decoded.adapter, fromDirectory: directory });
52
+ const nextYesThreshold = yesThresholdValue ??
53
+ (decoded.yesThreshold === undefined
54
+ ? undefined
55
+ : { value: decoded.yesThreshold, filename: join(directory, 'package.json') });
56
+ const collected = {
57
+ adapter: nextAdapter,
58
+ yesThresholdValue: nextYesThreshold,
59
+ };
60
+ return collected;
61
+ }
62
+ function packageConfigFromDiscovery(directory, adapter, yesThreshold) {
63
+ const config = {
64
+ fromDirectory: adapter?.fromDirectory ?? directory,
65
+ };
66
+ if (adapter !== undefined && yesThreshold !== undefined) {
67
+ return { ...config, adapter: adapter.moduleSpecifier, yesThreshold };
68
+ }
69
+ if (adapter !== undefined) {
70
+ return { ...config, adapter: adapter.moduleSpecifier };
71
+ }
72
+ if (yesThreshold !== undefined) {
73
+ return { ...config, yesThreshold };
74
+ }
75
+ return config;
76
+ }
77
+ /**
78
+ * Walks from cwd for package.json `patdown` settings. A nearer file without adapter/yesThreshold
79
+ * does not hide a parent that has them. Invalid JSON fails immediately.
80
+ */
81
+ export function discoverPatdownPackageConfig(startDirectory = process.cwd()) {
82
+ return Effect.gen(function* () {
83
+ let directory = resolve(startDirectory);
84
+ let adapter;
85
+ let yesThresholdValue;
86
+ while (adapter === undefined || yesThresholdValue === undefined) {
87
+ const filename = join(directory, 'package.json');
88
+ const decoded = yield* readPatdownPackageJson(filename);
89
+ if (decoded !== null) {
90
+ const collected = collectPackageConfig(directory, adapter, yesThresholdValue, decoded);
91
+ adapter = collected.adapter;
92
+ yesThresholdValue = collected.yesThresholdValue;
93
+ }
94
+ const parent = dirname(directory);
95
+ if (parent === directory)
96
+ break;
97
+ directory = parent;
98
+ }
99
+ if (adapter === undefined && yesThresholdValue === undefined)
100
+ return null;
101
+ const yesThreshold = yesThresholdValue === undefined
102
+ ? undefined
103
+ : yield* decodeConfiguredYesThreshold(yesThresholdValue.value, yesThresholdValue.filename);
104
+ return packageConfigFromDiscovery(directory, adapter, yesThreshold);
105
+ });
106
+ }
107
+ /** Reads only a cutoff from package.json. Broken adapter config is skipped for `ask`. */
108
+ export function discoverPatdownYesThresholdConfig(startDirectory = process.cwd()) {
109
+ return Effect.gen(function* () {
110
+ let directory = resolve(startDirectory);
111
+ while (true) {
112
+ const filename = join(directory, 'package.json');
113
+ const decoded = yield* readPatdownPackageJson(filename).pipe(Effect.orElseSucceed(() => null));
114
+ if (decoded?.yesThreshold !== undefined) {
115
+ return yield* decodeConfiguredYesThreshold(decoded.yesThreshold, filename);
116
+ }
117
+ const parent = dirname(directory);
118
+ if (parent === directory)
119
+ return null;
120
+ directory = parent;
121
+ }
122
+ });
123
+ }
@@ -0,0 +1,5 @@
1
+ import { Effect, Option, Stdio } from 'effect';
2
+ import { PatdownJudgeFailed } from '#src/patdown-judge';
3
+ /** Reads piped UTF-8 input only when requested; explicit text and stdin are mutually exclusive. */
4
+ export declare function readPatdownQuestionInput(inputText: Option.Option<string>, stdin: boolean): Effect.Effect<string, PatdownJudgeFailed, Stdio.Stdio>;
5
+ //# sourceMappingURL=patdown-question-input.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patdown-question-input.d.ts","sourceRoot":"","sources":["../src/patdown-question-input.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAU,MAAM,QAAQ,CAAA;AAEtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAEvD,mGAAmG;AACnG,wBAAgB,wBAAwB,CACvC,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAChC,KAAK,EAAE,OAAO,GACZ,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,kBAAkB,EAAE,KAAK,CAAC,KAAK,CAAC,CAwBxD"}
@@ -0,0 +1,21 @@
1
+ import { Effect, Option, Stdio, Stream } from 'effect';
2
+ import { PatdownJudgeFailed } from '#src/patdown-judge';
3
+ /** Reads piped UTF-8 input only when requested; explicit text and stdin are mutually exclusive. */
4
+ export function readPatdownQuestionInput(inputText, stdin) {
5
+ return Effect.gen(function* () {
6
+ if (stdin && Option.isSome(inputText)) {
7
+ return yield* new PatdownJudgeFailed({
8
+ message: 'patdown: choose either --input-text or --stdin, not both',
9
+ });
10
+ }
11
+ if (!stdin)
12
+ return Option.getOrElse(inputText, () => '');
13
+ const stdio = yield* Stdio.Stdio;
14
+ if (yield* stdio.stdinIsTerminal) {
15
+ return yield* new PatdownJudgeFailed({
16
+ message: 'patdown: --stdin requires piped or redirected input',
17
+ });
18
+ }
19
+ return yield* stdio.stdin.pipe(Stream.decodeText(), Stream.mkString, Effect.mapError(() => new PatdownJudgeFailed({ message: 'patdown: failed to read stdin' })));
20
+ });
21
+ }
@@ -0,0 +1,10 @@
1
+ import { PatdownRuleSource, PatdownRulesLoadFailed, type PatdownRulesDocument } from '@patdown/rules';
2
+ import { Effect, FileSystem, Layer, Option, Path } from 'effect';
3
+ /**
4
+ * Adapter layers may acquire resources using FileSystem and Path; provide other dependencies
5
+ * internally.
6
+ */
7
+ export type PatdownRuleSourceLayer = Layer.Layer<PatdownRuleSource, PatdownRulesLoadFailed, FileSystem.FileSystem | Path.Path>;
8
+ /** Loads configured rules only for lint/rules. Imported adapter resources close after loading. */
9
+ export declare function loadConfiguredPatdownRules(adapter: Option.Option<string>, rules: Option.Option<string>): Effect.Effect<PatdownRulesDocument, PatdownRulesLoadFailed, PatdownRuleSource | FileSystem.FileSystem | Path.Path>;
10
+ //# sourceMappingURL=patdown-rule-source-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patdown-rule-source-adapter.d.ts","sourceRoot":"","sources":["../src/patdown-rule-source-adapter.ts"],"names":[],"mappings":"AAGA,OAAO,EACN,iBAAiB,EACjB,sBAAsB,EACtB,KAAK,oBAAoB,EACzB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAkB,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAU,MAAM,QAAQ,CAAA;AAmBxF;;;GAGG;AACH,MAAM,MAAM,sBAAsB,GAAG,KAAK,CAAC,KAAK,CAC/C,iBAAiB,EACjB,sBAAsB,EACtB,UAAU,CAAC,UAAU,GAAG,IAAI,CAAC,IAAI,CACjC,CAAA;AA2DD,kGAAkG;AAClG,wBAAgB,0BAA0B,CACzC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAC9B,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,GAC1B,MAAM,CAAC,MAAM,CACf,oBAAoB,EACpB,sBAAsB,EACtB,iBAAiB,GAAG,UAAU,CAAC,UAAU,GAAG,IAAI,CAAC,IAAI,CACrD,CA4CA"}
@@ -0,0 +1,83 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { isAbsolute, join, resolve } from 'node:path';
10
+ import { pathToFileURL } from 'node:url';
11
+ import { PatdownRuleSource, PatdownRulesLoadFailed, } from '@patdown/rules';
12
+ import { Cause, Context, Effect, FileSystem, Layer, Option, Path, Schema } from 'effect';
13
+ import { resolve as resolveModule } from 'import-meta-resolve';
14
+ import { discoverPatdownPackageConfig } from '#src/patdown-package-config';
15
+ const AdapterModuleSchema = Schema.Struct({ PatdownRuleSourceLive: Schema.Unknown });
16
+ const RulesDocumentSchema = Schema.Struct({
17
+ patdownRulesFilePath: Schema.String,
18
+ patdownRules: Schema.Array(Schema.Struct({
19
+ patdownRuleTitle: Schema.String,
20
+ patdownRuleBody: Schema.String,
21
+ patdownRuleGlobs: Schema.Array(Schema.String),
22
+ patdownRuleYesThreshold: Schema.optionalKey(Schema.Finite),
23
+ })),
24
+ });
25
+ function importPatdownAdapter(moduleSpecifier, fromDirectory) {
26
+ return Effect.gen(function* () {
27
+ const imported = yield* Effect.tryPromise({
28
+ // Raw module import is the untyped I/O boundary. The next Effect step decodes it,
29
+ // preserving schema failures in the error channel instead of decoding synchronously here.
30
+ // oxlint-disable-next-line anti-slop/no-unknown-returns
31
+ try: async () => {
32
+ const importUrl = moduleSpecifier.startsWith('.') || isAbsolute(moduleSpecifier)
33
+ ? pathToFileURL(resolve(fromDirectory, moduleSpecifier)).href
34
+ : resolveModule(moduleSpecifier, pathToFileURL(join(fromDirectory, 'package.json')).href);
35
+ const exports = await import(__rewriteRelativeImportExtension(importUrl));
36
+ return exports;
37
+ },
38
+ catch: (cause) => new PatdownRulesLoadFailed({
39
+ message: cause instanceof Error ? cause.message : String(cause),
40
+ }),
41
+ });
42
+ const decoded = yield* Schema.decodeUnknownEffect(AdapterModuleSchema)(imported);
43
+ if (!Layer.isLayer(decoded.PatdownRuleSourceLive)) {
44
+ return yield* new PatdownRulesLoadFailed({
45
+ message: 'patdown: adapter must export a PatdownRuleSourceLive Layer',
46
+ });
47
+ }
48
+ // SAFETY: The boundary retains unknown failures/output. Host requirements alone are asserted;
49
+ // missing dependencies are caught at build, and service presence and returned rules are checked.
50
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
51
+ return decoded.PatdownRuleSourceLive;
52
+ }).pipe(Effect.mapError((cause) => new PatdownRulesLoadFailed({
53
+ message: `patdown: cannot import adapter ${moduleSpecifier}: ${cause instanceof Error ? cause.message : String(cause)}`,
54
+ })));
55
+ }
56
+ /** Loads configured rules only for lint/rules. Imported adapter resources close after loading. */
57
+ export function loadConfiguredPatdownRules(adapter, rules) {
58
+ return Effect.scoped(Effect.gen(function* () {
59
+ const selected = Option.isSome(adapter)
60
+ ? { moduleSpecifier: adapter.value, fromDirectory: process.cwd() }
61
+ : yield* discoverPatdownPackageConfig().pipe(Effect.map((config) => config?.adapter === undefined
62
+ ? null
63
+ : { moduleSpecifier: config.adapter, fromDirectory: config.fromDirectory }));
64
+ if (selected === null) {
65
+ const source = yield* PatdownRuleSource;
66
+ return yield* source.loadPatdownRules(rules);
67
+ }
68
+ const layer = yield* importPatdownAdapter(selected.moduleSpecifier, selected.fromDirectory);
69
+ const context = yield* Layer.build(layer);
70
+ const source = Context.getOption(context, PatdownRuleSource);
71
+ if (Option.isNone(source)) {
72
+ return yield* new PatdownRulesLoadFailed({
73
+ message: 'patdown: adapter Layer did not provide PatdownRuleSource',
74
+ });
75
+ }
76
+ const document = yield* source.value.loadPatdownRules(rules);
77
+ return yield* Schema.decodeUnknownEffect(RulesDocumentSchema)(document);
78
+ })).pipe(Effect.catchCause((cause) => Effect.fail(new PatdownRulesLoadFailed({
79
+ message: `patdown: failed to load rules: ${Cause.prettyErrors(cause)
80
+ .map((error) => error.message)
81
+ .join('; ')}`,
82
+ }))));
83
+ }
@@ -0,0 +1,7 @@
1
+ import { PatdownYesThresholdInvalid, type PatdownYesThreshold } from '@patdown/rules';
2
+ import { Effect, FileSystem, Option } from 'effect';
3
+ /** CLI flag wins, then package.json, then the default 0.85 cutoff. */
4
+ export declare function resolvePatdownYesThreshold(flag: Option.Option<number>): Effect.Effect<PatdownYesThreshold, PatdownYesThresholdInvalid, FileSystem.FileSystem>;
5
+ /** Decode a markdown or adapter-supplied per-rule cutoff. */
6
+ export declare function decodePatdownRuleYesThreshold(value: number, ruleTitle: string): Effect.Effect<PatdownYesThreshold, PatdownYesThresholdInvalid>;
7
+ //# sourceMappingURL=patdown-yes-threshold-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patdown-yes-threshold-config.d.ts","sourceRoot":"","sources":["../src/patdown-yes-threshold-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAGN,0BAA0B,EAC1B,KAAK,mBAAmB,EACxB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAInD,sEAAsE;AACtE,wBAAgB,0BAA0B,CACzC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,GACzB,MAAM,CAAC,MAAM,CAAC,mBAAmB,EAAE,0BAA0B,EAAE,UAAU,CAAC,UAAU,CAAC,CAYvF;AAED,6DAA6D;AAC7D,wBAAgB,6BAA6B,CAC5C,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GACf,MAAM,CAAC,MAAM,CAAC,mBAAmB,EAAE,0BAA0B,CAAC,CAShE"}
@@ -0,0 +1,20 @@
1
+ import { decodePatdownYesThreshold, defaultPatdownYesThreshold, PatdownYesThresholdInvalid, } from '@patdown/rules';
2
+ import { Effect, FileSystem, Option } from 'effect';
3
+ import { discoverPatdownYesThresholdConfig } from '#src/patdown-package-config';
4
+ /** CLI flag wins, then package.json, then the default 0.85 cutoff. */
5
+ export function resolvePatdownYesThreshold(flag) {
6
+ if (Option.isSome(flag)) {
7
+ const decoded = decodePatdownYesThreshold(flag.value, '--yes-threshold');
8
+ return decoded instanceof PatdownYesThresholdInvalid
9
+ ? Effect.fail(decoded)
10
+ : Effect.succeed(decoded);
11
+ }
12
+ return discoverPatdownYesThresholdConfig().pipe(Effect.map((configured) => configured ?? defaultPatdownYesThreshold));
13
+ }
14
+ /** Decode a markdown or adapter-supplied per-rule cutoff. */
15
+ export function decodePatdownRuleYesThreshold(value, ruleTitle) {
16
+ const decoded = decodePatdownYesThreshold(value, `rule ${JSON.stringify(ruleTitle)} yes-threshold`);
17
+ return decoded instanceof PatdownYesThresholdInvalid
18
+ ? Effect.fail(decoded)
19
+ : Effect.succeed(decoded);
20
+ }
@@ -0,0 +1,12 @@
1
+ import { type PatdownRulesLoadFailed } from '@patdown/rules';
2
+ import { Effect, Layer } from 'effect';
3
+ import type { CliError } from 'effect/unstable/cli/CliError';
4
+ import type { PatdownJudge, PatdownJudgeFailed } from '#src/patdown-judge';
5
+ import { type PatdownOutput } from '#src/patdown-output';
6
+ import type { PatdownRuleSourceLayer } from '#src/patdown-rule-source-adapter';
7
+ /**
8
+ * Returns a runnable CLI Effect. A supplied rule source disables discovery; argv excludes node and
9
+ * script. The third and fourth arguments replace the judge and output layers independently.
10
+ */
11
+ export declare function runPatdownCli(ruleSourceLayer?: PatdownRuleSourceLayer, argv?: readonly string[], judgeLayer?: Layer.Layer<PatdownJudge, PatdownJudgeFailed>, outputLayer?: Layer.Layer<PatdownOutput>): Effect.Effect<void, PatdownRulesLoadFailed | PatdownJudgeFailed | CliError>;
12
+ //# sourceMappingURL=run-patdown-cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-patdown-cli.d.ts","sourceRoot":"","sources":["../src/run-patdown-cli.ts"],"names":[],"mappings":"AACA,OAAO,EAAiC,KAAK,sBAAsB,EAAE,MAAM,gBAAgB,CAAA;AAC3F,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AAEtC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAA;AAI5D,OAAO,KAAK,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAC1E,OAAO,EAAqB,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAC3E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAA;AAG9E;;;GAGG;AACH,wBAAgB,aAAa,CAC5B,eAAe,CAAC,EAAE,sBAAsB,EACxC,IAAI,GAAE,SAAS,MAAM,EAA0B,EAC/C,UAAU,GAAE,KAAK,CAAC,KAAK,CAAC,YAAY,EAAE,kBAAkB,CAAqB,EAC7E,WAAW,GAAE,KAAK,CAAC,KAAK,CAAC,aAAa,CAAqB,GACzD,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,sBAAsB,GAAG,kBAAkB,GAAG,QAAQ,CAAC,CAS7E"}
@@ -0,0 +1,17 @@
1
+ import { NodeServices } from '@effect/platform-node';
2
+ import { MarkdownPatdownRuleSourceLive } from '@patdown/rules';
3
+ import { Effect, Layer } from 'effect';
4
+ import { Command } from 'effect/unstable/cli';
5
+ import { makePatdownCommand } from '#src/cli';
6
+ import { patdownCliVersion } from '#src/patdown-cli-version';
7
+ import { PatdownOutputLive } from '#src/patdown-output';
8
+ import { TypeSafeJudgeLive } from '#src/typesafe-judge';
9
+ /**
10
+ * Returns a runnable CLI Effect. A supplied rule source disables discovery; argv excludes node and
11
+ * script. The third and fourth arguments replace the judge and output layers independently.
12
+ */
13
+ export function runPatdownCli(ruleSourceLayer, argv = process.argv.slice(2), judgeLayer = TypeSafeJudgeLive, outputLayer = PatdownOutputLive) {
14
+ const command = makePatdownCommand(ruleSourceLayer === undefined);
15
+ const source = ruleSourceLayer ?? MarkdownPatdownRuleSourceLive;
16
+ return Command.runWith(command, { version: patdownCliVersion })(argv).pipe(Effect.provide(Layer.mergeAll(source, judgeLayer, outputLayer).pipe(Layer.provideMerge(NodeServices.layer))));
17
+ }
@@ -0,0 +1,7 @@
1
+ import { JevRequestFailed, JevSystemOne } from '@squint/jev';
2
+ import type { SquintRulesDocument } from '@squint/rules';
3
+ import { Effect, FileSystem, Path } from 'effect';
4
+ import type { HttpClient } from 'effect/unstable/http';
5
+ /** Lint files matched by each rule's globs. Yes on the noul means a violation. */
6
+ export declare function runSquintLint(document: SquintRulesDocument): Effect.Effect<boolean, JevRequestFailed, FileSystem.FileSystem | HttpClient.HttpClient | JevSystemOne | Path.Path>;
7
+ //# sourceMappingURL=squint-lint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"squint-lint.d.ts","sourceRoot":"","sources":["../src/squint-lint.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAqC,MAAM,aAAa,CAAA;AAC/F,OAAO,KAAK,EAAc,mBAAmB,EAAE,MAAM,eAAe,CAAA;AACpE,OAAO,EAAW,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAC1D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAiItD,kFAAkF;AAClF,wBAAgB,aAAa,CAC5B,QAAQ,EAAE,mBAAmB,GAC3B,MAAM,CAAC,MAAM,CACf,OAAO,EACP,gBAAgB,EAChB,UAAU,CAAC,UAAU,GAAG,UAAU,CAAC,UAAU,GAAG,YAAY,GAAG,IAAI,CAAC,IAAI,CACxE,CAaA"}
@@ -0,0 +1,80 @@
1
+ import { JevRequestFailed, JevSystemOne, jevNoulIsYes, jevNoulYesThreshold } from '@squint/jev';
2
+ import { Console, Effect, FileSystem, Path } from 'effect';
3
+ const squintGlobExcludes = [
4
+ '**/.git/**',
5
+ '**/.turbo/**',
6
+ '**/coverage/**',
7
+ '**/dist/**',
8
+ '**/node_modules/**',
9
+ ];
10
+ function squintGlobPatterns(globs) {
11
+ return globs.length === 0 ? ['**/*'] : globs;
12
+ }
13
+ function squintViolationInstructions(rule) {
14
+ return [
15
+ 'Does this file violate the following squint rule? Answer yes only if there is a clear violation.',
16
+ '',
17
+ `# ${rule.squintRuleTitle}`,
18
+ '',
19
+ rule.squintRuleBody,
20
+ ].join('\n');
21
+ }
22
+ function squintFileState(relativePath, contents) {
23
+ return `path: ${relativePath}\n\n${contents}`;
24
+ }
25
+ function renderSquintLintLine(failed, ruleTitle, relativePath, noul) {
26
+ const verdict = failed ? 'fail' : 'pass';
27
+ return `squint: ${verdict} ${ruleTitle} ${relativePath} (${String(noul)}, threshold ${String(jevNoulYesThreshold)})`;
28
+ }
29
+ function globSquintRuleFiles(cwd, globs) {
30
+ return Effect.gen(function* () {
31
+ const fileSystem = yield* FileSystem.FileSystem;
32
+ const patterns = squintGlobPatterns(globs);
33
+ const matches = [];
34
+ for (const pattern of patterns) {
35
+ const found = yield* fileSystem
36
+ .glob(pattern, {
37
+ exclude: squintGlobExcludes,
38
+ root: cwd,
39
+ })
40
+ .pipe(Effect.orElseSucceed(() => []));
41
+ matches.push(...found);
42
+ }
43
+ return [...new Set(matches)].toSorted();
44
+ });
45
+ }
46
+ function lintSquintRuleFile(rule, cwd, filePath) {
47
+ return Effect.gen(function* () {
48
+ const fileSystem = yield* FileSystem.FileSystem;
49
+ const path = yield* Path.Path;
50
+ const jev = yield* JevSystemOne;
51
+ const relativePath = path.relative(cwd, filePath);
52
+ const contents = yield* fileSystem.readFileString(filePath).pipe(Effect.mapError(() => new JevRequestFailed({
53
+ message: `squint: failed to read ${relativePath}`,
54
+ })));
55
+ const answer = yield* jev.askNoul(squintViolationInstructions(rule), squintFileState(relativePath, contents));
56
+ const failed = jevNoulIsYes(answer.noul);
57
+ yield* Console.log(renderSquintLintLine(failed, rule.squintRuleTitle, relativePath, answer.noul));
58
+ return failed;
59
+ });
60
+ }
61
+ function lintSquintRule(rule, cwd) {
62
+ return Effect.gen(function* () {
63
+ const files = yield* globSquintRuleFiles(cwd, rule.squintRuleGlobs);
64
+ if (files.length === 0) {
65
+ yield* Console.log(`squint: no files matched ${rule.squintRuleTitle}`);
66
+ return false;
67
+ }
68
+ const failures = yield* Effect.forEach(files, (filePath) => lintSquintRuleFile(rule, cwd, filePath), { concurrency: 1 });
69
+ return failures.some((failed) => failed);
70
+ });
71
+ }
72
+ /** Lint files matched by each rule's globs. Yes on the noul means a violation. */
73
+ export function runSquintLint(document) {
74
+ return Effect.gen(function* () {
75
+ const path = yield* Path.Path;
76
+ const cwd = path.resolve('.');
77
+ const failures = yield* Effect.forEach(document.squintRules, (rule) => lintSquintRule(rule, cwd), { concurrency: 1 });
78
+ return failures.some((failed) => failed);
79
+ });
80
+ }
@@ -0,0 +1,34 @@
1
+ import { Context, Effect, FileSystem, Layer, Option, Path } from 'effect';
2
+ import { type SquintRulesDocument } from '#/squint-rule';
3
+ declare const SquintRulesFileMissing_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 & {
4
+ readonly _tag: "SquintRulesFileMissing";
5
+ } & Readonly<A>;
6
+ /** No AGENTS.SQUINT.md (or override path) existed walking up from the start directory. */
7
+ export declare class SquintRulesFileMissing extends SquintRulesFileMissing_base<{
8
+ readonly squintRulesFileName: string;
9
+ readonly startDirectory: string;
10
+ }> {
11
+ get message(): string;
12
+ }
13
+ declare const SquintRulesReadFailed_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 & {
14
+ readonly _tag: "SquintRulesReadFailed";
15
+ } & Readonly<A>;
16
+ /** The rules file existed but could not be read. */
17
+ export declare class SquintRulesReadFailed extends SquintRulesReadFailed_base<{
18
+ readonly squintRulesFilePath: string;
19
+ }> {
20
+ get message(): string;
21
+ }
22
+ declare const SquintRuleSource_base: Context.ServiceClass<SquintRuleSource, "@squint/cli/SquintRuleSource", {
23
+ readonly loadSquintRules: (rulesFilePathOverride: Option.Option<string>) => Effect.Effect<SquintRulesDocument, SquintRulesFileMissing | SquintRulesReadFailed, FileSystem.FileSystem | Path.Path>;
24
+ }>;
25
+ /**
26
+ * Loads fuzzy squint rules for a run. Swap the live layer to change file format without touching
27
+ * the CLI command.
28
+ */
29
+ export declare class SquintRuleSource extends SquintRuleSource_base {
30
+ }
31
+ /** Live squint rule source that reads markdown (`AGENTS.SQUINT.md` by default). */
32
+ export declare const MarkdownSquintRuleSourceLive: Layer.Layer<SquintRuleSource, never, never>;
33
+ export {};
34
+ //# sourceMappingURL=squint-rule-source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"squint-rule-source.d.ts","sourceRoot":"","sources":["../src/squint-rule-source.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAQ,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAG/E,OAAO,EAA8B,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAA;;;;AAEpF,0FAA0F;AAC1F,qBAAa,sBAAuB,SAAQ,4BAA2C;IACtF,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAA;IACpC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;CAC/B,CAAC;IACD,IAAa,OAAO,IAAI,MAAM,CAE7B;CACD;;;;AAED,oDAAoD;AACpD,qBAAa,qBAAsB,SAAQ,2BAA0C;IACpF,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAA;CACpC,CAAC;IACD,IAAa,OAAO,IAAI,MAAM,CAE7B;CACD;;8BAS2B,CACzB,qBAAqB,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,KACxC,MAAM,CAAC,MAAM,CACjB,mBAAmB,EACnB,sBAAsB,GAAG,qBAAqB,EAC9C,UAAU,CAAC,UAAU,GAAG,IAAI,CAAC,IAAI,CACjC;;AAbH;;;GAGG;AACH,qBAAa,gBAAiB,SAAQ,qBAWH;CAAG;AA2FtC,mFAAmF;AACnF,eAAO,MAAM,4BAA4B,6CAEvC,CAAA"}
@@ -0,0 +1,78 @@
1
+ import { Context, Data, Effect, FileSystem, Layer, Option, Path } from 'effect';
2
+ import { parseMarkdownSquintRules } from '#/markdown-squint-rule-parser';
3
+ import { defaultSquintRulesFileName } from '#/squint-rule';
4
+ /** No AGENTS.SQUINT.md (or override path) existed walking up from the start directory. */
5
+ export class SquintRulesFileMissing extends Data.TaggedError('SquintRulesFileMissing') {
6
+ get message() {
7
+ return `squint: no ${this.squintRulesFileName} found walking up from ${this.startDirectory}`;
8
+ }
9
+ }
10
+ /** The rules file existed but could not be read. */
11
+ export class SquintRulesReadFailed extends Data.TaggedError('SquintRulesReadFailed') {
12
+ get message() {
13
+ return `squint: failed to read ${this.squintRulesFilePath}`;
14
+ }
15
+ }
16
+ /**
17
+ * Loads fuzzy squint rules for a run. Swap the live layer to change file format without touching
18
+ * the CLI command.
19
+ */
20
+ export class SquintRuleSource extends Context.Service()('@squint/cli/SquintRuleSource') {
21
+ }
22
+ function pathExists(filePath) {
23
+ return FileSystem.FileSystem.use((fileSystem) => fileSystem
24
+ .exists(filePath)
25
+ .pipe(Effect.mapError(() => new SquintRulesReadFailed({ squintRulesFilePath: filePath }))));
26
+ }
27
+ function findSquintRulesFilePath(startDirectory, fileName) {
28
+ return Effect.gen(function* () {
29
+ const path = yield* Path.Path;
30
+ let directory = startDirectory;
31
+ while (true) {
32
+ const candidate = path.join(directory, fileName);
33
+ if (yield* pathExists(candidate))
34
+ return candidate;
35
+ const parent = path.dirname(directory);
36
+ if (parent === directory) {
37
+ return yield* new SquintRulesFileMissing({
38
+ squintRulesFileName: fileName,
39
+ startDirectory,
40
+ });
41
+ }
42
+ directory = parent;
43
+ }
44
+ });
45
+ }
46
+ function resolveSquintRulesFilePath(rulesFilePathOverride) {
47
+ return Effect.gen(function* () {
48
+ const path = yield* Path.Path;
49
+ const startDirectory = path.resolve('.');
50
+ if (Option.isNone(rulesFilePathOverride)) {
51
+ return yield* findSquintRulesFilePath(startDirectory, defaultSquintRulesFileName);
52
+ }
53
+ const candidate = path.resolve(startDirectory, rulesFilePathOverride.value);
54
+ if (yield* pathExists(candidate))
55
+ return candidate;
56
+ return yield* new SquintRulesFileMissing({
57
+ squintRulesFileName: rulesFilePathOverride.value,
58
+ startDirectory,
59
+ });
60
+ });
61
+ }
62
+ function loadMarkdownSquintRules(rulesFilePathOverride) {
63
+ return Effect.gen(function* () {
64
+ const squintRulesFilePath = yield* resolveSquintRulesFilePath(rulesFilePathOverride);
65
+ const fileSystem = yield* FileSystem.FileSystem;
66
+ const markdown = yield* fileSystem
67
+ .readFileString(squintRulesFilePath)
68
+ .pipe(Effect.mapError(() => new SquintRulesReadFailed({ squintRulesFilePath })));
69
+ return {
70
+ squintRules: parseMarkdownSquintRules(markdown),
71
+ squintRulesFilePath,
72
+ };
73
+ });
74
+ }
75
+ /** Live squint rule source that reads markdown (`AGENTS.SQUINT.md` by default). */
76
+ export const MarkdownSquintRuleSourceLive = Layer.succeed(SquintRuleSource, {
77
+ loadSquintRules: loadMarkdownSquintRules,
78
+ });