@taskless/cli 0.11.0 → 0.11.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.
- package/assets/reference.json +213 -0
- package/dist/binary-DSACXL6n.js +107 -0
- package/dist/index.js +5650 -4550
- package/dist/layout/index.d.ts +31 -0
- package/dist/layout.js +43 -0
- package/dist/node/runtimes/index.d.ts +102 -0
- package/dist/node/runtimes.js +10 -0
- package/dist/prompts/index.d.ts +25 -11
- package/dist/prompts/recipes.d.ts +37 -12
- package/dist/prompts.js +12 -12
- package/dist/recipes-w68swLg4.js +2662 -0
- package/dist/rules/ast-grep-binary.d.ts +26 -0
- package/dist/rules/capabilities.d.ts +46 -6
- package/dist/rules/constraints.d.ts +132 -0
- package/dist/rules/layout.d.ts +152 -0
- package/dist/rules/platform-binary.d.ts +113 -0
- package/dist/rules/vale/binary.d.ts +35 -0
- package/dist/schemas/index.d.ts +93 -0
- package/dist/schemas/rules-create.d.ts +1 -0
- package/dist/schemas/rules-improve.d.ts +1 -0
- package/dist/schemas/rules-verify.d.ts +67 -0
- package/dist/schemas/verify-test.d.ts +22 -0
- package/dist/schemas-OhdGAhu_.js +4512 -0
- package/dist/schemas.js +65 -0
- package/dist/types/errors.d.ts +20 -0
- package/dist/verify-test-BaZPwJqC.js +34 -0
- package/package.json +31 -12
- package/dist/recipes-C_i98mTO.js +0 -6975
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PlatformBinarySpec } from "./platform-binary";
|
|
2
|
+
/**
|
|
3
|
+
* ast-grep's per-platform packaging, as the shared resolver understands it.
|
|
4
|
+
*
|
|
5
|
+
* `toolchainSuffix: true` is what produces `@ast-grep/cli-linux-x64-gnu` and
|
|
6
|
+
* `-win32-x64-msvc`. The Vale packages set it false; see
|
|
7
|
+
* {@link PlatformBinarySpec} for why that distinction is load-bearing.
|
|
8
|
+
*
|
|
9
|
+
* Both `ast-grep` and `sg` are listed because the wrapper declares them as bin
|
|
10
|
+
* entries for the same target, so either may be what got linked. `ast-grep`
|
|
11
|
+
* leads because it is the name the platform package ships the binary under; the
|
|
12
|
+
* resolver reverses the list at the link-based tiers, so `sg` is still tried
|
|
13
|
+
* first there and named in the PATH advice, as it was before the shared
|
|
14
|
+
* resolver existed.
|
|
15
|
+
*
|
|
16
|
+
* ## Why this sits in its own module rather than in `scan.ts`
|
|
17
|
+
*
|
|
18
|
+
* The spec is data: five fields describing how a package is named and how the
|
|
19
|
+
* binary identifies itself. `scan.ts` is the scanner — it reaches
|
|
20
|
+
* `rules/engines.ts`, and through it `node:fs/promises` and the CLI's error
|
|
21
|
+
* type. `@taskless/cli/node/runtimes` publishes this spec so a consumer can
|
|
22
|
+
* resolve the same binary the CLI executes, and a consumer that wants a path
|
|
23
|
+
* has no business loading a scanner to get one. Vale's spec is already a leaf
|
|
24
|
+
* for the same reason: `rules/vale/binary.ts` imports nothing but the resolver.
|
|
25
|
+
*/
|
|
26
|
+
export declare const AST_GREP_BINARY: PlatformBinarySpec;
|
|
@@ -123,13 +123,21 @@ export declare function resolveAstGrepLanguage(spelling: string): AstGrepLanguag
|
|
|
123
123
|
export declare const AST_GREP_TSX_SPLIT: Readonly<Partial<Record<AstGrepLanguage, string>>>;
|
|
124
124
|
/**
|
|
125
125
|
* The Vale release carried by the `@taskless/vale-<platform>` packages pinned
|
|
126
|
-
* in `packages/cli/package.json`. Their npm versions append a build stamp
|
|
127
|
-
*
|
|
126
|
+
* in `packages/cli/package.json`. Their npm versions append a build stamp,
|
|
127
|
+
* `<valeVersion>-<yyyymmddhhmmss>`, minted per publish by
|
|
128
|
+
* `.github/workflows/release-vale.yml`; this is the version Vale itself
|
|
129
|
+
* reports.
|
|
130
|
+
*
|
|
131
|
+
* The SHAPE rather than an example, deliberately. A literal stamp here has to
|
|
132
|
+
* be hand-carried on every bump, and it has gone one release out of date twice
|
|
133
|
+
* now, which is worse than no example: it reads as the current pin and is not
|
|
134
|
+
* one. The real value lives in `packages/cli/package.json`, where a reader can
|
|
135
|
+
* see all six at once and cannot be told a stale one.
|
|
128
136
|
*
|
|
129
137
|
* Pinned against the binary by `test/vale-vendor-contract.test.ts`
|
|
130
138
|
* ("engine capabilities" → "reports the pinned version").
|
|
131
139
|
*/
|
|
132
|
-
export declare const VALE_VERSION = "3.
|
|
140
|
+
export declare const VALE_VERSION = "3.20.0";
|
|
133
141
|
/**
|
|
134
142
|
* Which tier Vale routes an extension to.
|
|
135
143
|
*
|
|
@@ -186,9 +194,41 @@ export type ValeFormatTier =
|
|
|
186
194
|
* plain text today, but the moment Vale routes it to a converter the same
|
|
187
195
|
* omission is a crash that takes down every Vale rule in the run.
|
|
188
196
|
*
|
|
189
|
-
*
|
|
190
|
-
* row below was re-probed against the
|
|
191
|
-
*
|
|
197
|
+
* NOTHING MOVED ACROSS 3.19.0 → 3.20.0, AND THE SECOND HALF OF THAT CLAIM IS
|
|
198
|
+
* THE ONE THAT COST SOMETHING. Every row below was re-probed against the
|
|
199
|
+
* 3.20.0 binary and none of them moved, which this table can report. It cannot
|
|
200
|
+
* report a format Vale *learned*, so that direction was checked at the source
|
|
201
|
+
* instead: the v3.19.0...v3.20.0 tree adds no `internal/lint/<format>.go` and
|
|
202
|
+
* no `internal/lint/code/<lang>.go`, and every existing one of those files
|
|
203
|
+
* changes by a single import line from the org rename. No format was learned,
|
|
204
|
+
* so there is no new row to add. Do that check on the next bump too — a green
|
|
205
|
+
* table is silence about the dangerous direction, not evidence against it.
|
|
206
|
+
*
|
|
207
|
+
* THE TABLE ALSO HELD ACROSS 3.18.0 → 3.19.0, AND THAT IS NOT THE SAME AS THE
|
|
208
|
+
* BUMP BEING FREE. Every row below was re-probed against the 3.19.0 binary and
|
|
209
|
+
* not one of them moved. What moved was a format Vale *learned*, which is
|
|
210
|
+
* precisely the case a table of existing rows cannot report: `.ex` and `.exs`
|
|
211
|
+
* gained comment and doc-attribute extraction, so they left the unnamed
|
|
212
|
+
* plaintext fallback for the `comment` tier and are new rows below.
|
|
213
|
+
*
|
|
214
|
+
* Read that direction carefully, because it is a narrowing rather than a gain.
|
|
215
|
+
* On 3.18.0 an Elixir file was linted as one block of prose, so a rule matching
|
|
216
|
+
* `[*.ex]` fired on identifiers and string literals as readily as on comments.
|
|
217
|
+
* On 3.19.0 the code body is invisible and only comments and `@doc` attributes
|
|
218
|
+
* are read. Findings disappear, no error is raised, and nothing but a re-probe
|
|
219
|
+
* would have told us. A format Vale learns is never a no-op: the benign version
|
|
220
|
+
* of it is this one, and the dangerous version is a converter (see `.typ`).
|
|
221
|
+
*
|
|
222
|
+
* `.mdx` stayed `markup` and changed underneath the tier: a JSX element's
|
|
223
|
+
* children are now read as the Markdown they are, so a Vale rule covers prose
|
|
224
|
+
* inside `<Steps>` or `<Aside>` that it previously skipped, and those children
|
|
225
|
+
* carry the element name as a `text.class.<name>` scope. The tier is the wrong
|
|
226
|
+
* instrument for that kind of change — it says a parser exists, not what the
|
|
227
|
+
* parser sees — which is the second reason a bump needs more than this table.
|
|
228
|
+
*
|
|
229
|
+
* The 3.17.1 → 3.18.0 bump is what the dangerous case looks like in practice.
|
|
230
|
+
* Every row was re-probed against the 3.18.0 binary then too, and eight moved,
|
|
231
|
+
* in three different directions — which is why "re-measure" is not boilerplate:
|
|
192
232
|
*
|
|
193
233
|
* - `.mdx` gained a native parser: `converter:mdx2vast` → `markup`. It is
|
|
194
234
|
* supported now, and `[*.{md,mdx}]` is a legitimate matcher again.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { EngineName } from "./layout";
|
|
2
|
+
/**
|
|
3
|
+
* What `verify` enforces beyond the engine's own schema.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this exists
|
|
6
|
+
*
|
|
7
|
+
* A rule the engine executes correctly can still be refused. Those refusals are
|
|
8
|
+
* deliberate, but they are OURS, and a generator that never reads our recipes
|
|
9
|
+
* cannot know them. Published in the conformance corpus so an external eval can
|
|
10
|
+
* tell "your rule is wrong about the subject" from "your rule broke a house
|
|
11
|
+
* rule it was never told about" — two findings that want completely different
|
|
12
|
+
* responses.
|
|
13
|
+
*
|
|
14
|
+
* ## Why every entry has a test that triggers it
|
|
15
|
+
*
|
|
16
|
+
* A hand-maintained list of what code does goes stale, and this is not
|
|
17
|
+
* hypothetical: `create-sg-rule.md` told agents for months that
|
|
18
|
+
* "`verify` never reads `language`", which stopped being true when
|
|
19
|
+
* `validateLanguage` landed. Nothing failed, because prose has no test.
|
|
20
|
+
*
|
|
21
|
+
* `test/constraints.test.ts` builds a rule that violates each entry and
|
|
22
|
+
* asserts `verify` rejects it, keyed on `id`. An entry describing a check that
|
|
23
|
+
* no longer fires fails the suite; a check with no entry is invisible to that
|
|
24
|
+
* test and is the gap this list is trying to close, so add one when you add a
|
|
25
|
+
* check.
|
|
26
|
+
*/
|
|
27
|
+
export interface RuleConstraint {
|
|
28
|
+
/** Stable key. Consumers branch on this; renaming is breaking. */
|
|
29
|
+
id: string;
|
|
30
|
+
engine: EngineName;
|
|
31
|
+
/**
|
|
32
|
+
* Which command refuses the rule.
|
|
33
|
+
*
|
|
34
|
+
* Load-bearing for a consumer's eval ORDER, not a detail. A `verify`
|
|
35
|
+
* constraint is decided from the files alone and can be checked before
|
|
36
|
+
* anything runs; a `test` constraint needs the fixtures to execute. Running
|
|
37
|
+
* the cross-comparison before `verify` passes measures the wrong thing, and
|
|
38
|
+
* treating a `test`-time refusal as a `verify` gap sends someone to the wrong
|
|
39
|
+
* layer.
|
|
40
|
+
*
|
|
41
|
+
* The split is not always where it looks. `verify` requires that a test FILE
|
|
42
|
+
* exists, by filename; whether that file is attributed to this rule is
|
|
43
|
+
* decided later, from the `id:` inside it.
|
|
44
|
+
*/
|
|
45
|
+
enforcedBy: "verify" | "test";
|
|
46
|
+
/** One line, for a report that lists several. */
|
|
47
|
+
summary: string;
|
|
48
|
+
/** Why it exists, so a reader can tell a house rule from a bug. */
|
|
49
|
+
rationale: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* One constraint a rule broke, paired with the message that reports it.
|
|
53
|
+
*
|
|
54
|
+
* Emitted alongside `errors` rather than replacing it. A consumer mapping a
|
|
55
|
+
* rejection back to the rationale we already wrote had only our wording to
|
|
56
|
+
* match on, and wording is not a contract: rephrasing an error message is not
|
|
57
|
+
* a breaking change, so a text match rots without anything reporting it.
|
|
58
|
+
*
|
|
59
|
+
* The message is repeated rather than joined to `errors` by index. An index
|
|
60
|
+
* join is a contract nobody can see, and it breaks the first time either side
|
|
61
|
+
* filters or reorders. Repeating the string lets a consumer ignore `errors`
|
|
62
|
+
* entirely.
|
|
63
|
+
*/
|
|
64
|
+
export interface RuleViolation {
|
|
65
|
+
constraintId: RuleConstraintId;
|
|
66
|
+
message: string;
|
|
67
|
+
}
|
|
68
|
+
export declare const RULE_CONSTRAINTS: readonly [{
|
|
69
|
+
readonly id: "sg-id-matches-directory";
|
|
70
|
+
readonly engine: "sg";
|
|
71
|
+
readonly enforcedBy: "verify";
|
|
72
|
+
readonly summary: "A rule's `id:` must equal the directory it lives in.";
|
|
73
|
+
readonly rationale: "The directory name is the rule id: it is what `check` and `test` address, and what a person types to delete a rule. ast-grep registers the rule under the id in its body. With the two apart, `test` cannot find the rule at all, and `check` does run it but reports findings under a name no directory has, so nobody can locate what produced them.";
|
|
74
|
+
}, {
|
|
75
|
+
readonly id: "sg-regex-needs-kind";
|
|
76
|
+
readonly engine: "sg";
|
|
77
|
+
readonly enforcedBy: "verify";
|
|
78
|
+
readonly summary: "A `regex` needs a sibling `kind`, in `rule`, `constraints` and `utils`.";
|
|
79
|
+
readonly rationale: "A regex match with no kind to anchor it is ambiguous and slow: it is applied to every node rather than to the one shape the author meant. ast-grep accepts it, so the engine is not the thing that will tell you.";
|
|
80
|
+
}, {
|
|
81
|
+
readonly id: "sg-language-accepted";
|
|
82
|
+
readonly engine: "sg";
|
|
83
|
+
readonly enforcedBy: "verify";
|
|
84
|
+
readonly summary: "`language:` must be a spelling ast-grep itself uses; a resolvable but non-canonical one is a notice.";
|
|
85
|
+
readonly rationale: "An unrecognized name aborts config parsing, which takes every other sg rule in the project down with it and reports nothing. That is the loudest possible failure with the quietest possible symptom: a clean report.";
|
|
86
|
+
}, {
|
|
87
|
+
readonly id: "sg-files-globs-parse";
|
|
88
|
+
readonly engine: "sg";
|
|
89
|
+
readonly enforcedBy: "verify";
|
|
90
|
+
readonly summary: "`files:` globs must not name `.tsx` under TypeScript, or `.ts` under Tsx.";
|
|
91
|
+
readonly rationale: "A glob naming an extension the language cannot parse matches nothing, so the rule reports a clean codebase rather than an error. Only the TypeScript/Tsx pair is checked, and deliberately so: they are separate parsers rather than aliases, which is the one language/extension mismatch decidable from the rule file alone. No other extension is compared against `language`, so this is narrower than it first reads.";
|
|
92
|
+
}, {
|
|
93
|
+
readonly id: "sg-required-fields";
|
|
94
|
+
readonly engine: "sg";
|
|
95
|
+
readonly enforcedBy: "verify";
|
|
96
|
+
readonly summary: "`id`, `language`, `severity`, `message` and `rule` are required.";
|
|
97
|
+
readonly rationale: "ast-grep needs fewer of these than we do. The extras are what make a finding actionable and a rule addressable once it is on disk.";
|
|
98
|
+
}, {
|
|
99
|
+
readonly id: "sg-test-file-required";
|
|
100
|
+
readonly engine: "sg";
|
|
101
|
+
readonly enforcedBy: "verify";
|
|
102
|
+
readonly summary: "A rule must ship at least one test file under `.tests/`.";
|
|
103
|
+
readonly rationale: "A rule with no fixtures has shown neither that it fires nor that it stays quiet. `verify` requires the file; `test` requires the cases inside it to cover both.";
|
|
104
|
+
}, {
|
|
105
|
+
readonly id: "sg-fixture-id-matches-rule";
|
|
106
|
+
readonly engine: "sg";
|
|
107
|
+
readonly enforcedBy: "test";
|
|
108
|
+
readonly summary: "A test file's own `id:` must equal the rule id.";
|
|
109
|
+
readonly rationale: "Fixtures are attributed by the id inside the file, not by its name. A fixture carrying another rule's id is silently not counted, so a rule that ships one reads as a rule that shipped none. `verify` passes, because the FILE is there; `test` is where it bites.";
|
|
110
|
+
}];
|
|
111
|
+
/**
|
|
112
|
+
* The id of a constraint this CLI publishes.
|
|
113
|
+
*
|
|
114
|
+
* Derived from the list rather than declared beside it, so a violation can only
|
|
115
|
+
* name a constraint that is actually published. Attributing a rejection to an
|
|
116
|
+
* id no consumer can look up would be worse than attributing nothing.
|
|
117
|
+
*/
|
|
118
|
+
export type RuleConstraintId = (typeof RULE_CONSTRAINTS)[number]["id"];
|
|
119
|
+
/**
|
|
120
|
+
* Record an attributable failure in both places at once.
|
|
121
|
+
*
|
|
122
|
+
* `errors` stays the complete list and `violations` the attributable subset, so
|
|
123
|
+
* a consumer reading only `errors` sees exactly what it saw before this
|
|
124
|
+
* existed. Written through one call because two arrays maintained separately is
|
|
125
|
+
* how the message in one comes to differ from the message in the other.
|
|
126
|
+
*/
|
|
127
|
+
export declare function violate(target: {
|
|
128
|
+
errors: string[];
|
|
129
|
+
violations: RuleViolation[];
|
|
130
|
+
}, constraintId: RuleConstraintId, message: string): void;
|
|
131
|
+
/** Constraints for one engine. */
|
|
132
|
+
export declare function constraintsFor(engine: EngineName): readonly RuleConstraint[];
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rule layout table — the single description of what a rule is made of and
|
|
3
|
+
* where its files go, for every engine.
|
|
4
|
+
*
|
|
5
|
+
* SPLIT OUT OF `engines.ts` SO IT CAN BE PUBLISHED. This module holds data and
|
|
6
|
+
* nothing else: no filesystem, no network, no command tree. `engines.ts` keeps
|
|
7
|
+
* the helpers that join these values to a `cwd`, because those import
|
|
8
|
+
* `node:fs/promises` and a Worker cannot load them.
|
|
9
|
+
*
|
|
10
|
+
* The split exists because the Cloud Generator builds rule payloads against
|
|
11
|
+
* this table. While it lived only in prose, the same layout was described in
|
|
12
|
+
* seven stale code comments and in `cli-runtime-rule-execution`'s own spec text,
|
|
13
|
+
* all naming a path two migrations had already moved — and one of those stale
|
|
14
|
+
* comments put the wrong layout into a cross-team design document. A shape
|
|
15
|
+
* described in prose drifts in every place it is described, including the one
|
|
16
|
+
* that is supposed to be authoritative. Published as data, it cannot.
|
|
17
|
+
*
|
|
18
|
+
* Re-exported for consumers as `@taskless/cli/layout` (see `src/layout/`).
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Engines this CLI knows. The directory name under `.taskless/rules/` **is**
|
|
22
|
+
* the engine: dispatch reads the path and never parses a rule file to decide
|
|
23
|
+
* who owns it.
|
|
24
|
+
*/
|
|
25
|
+
export declare const ENGINES: readonly ["sg", "vale", "runtime"];
|
|
26
|
+
export type EngineName = (typeof ENGINES)[number];
|
|
27
|
+
/**
|
|
28
|
+
* How a rule's fixtures group into cases.
|
|
29
|
+
*
|
|
30
|
+
* Stated here rather than left implicit in each engine's fixture reader, which
|
|
31
|
+
* is where it lived: `runtime/fixtures.ts` rejects a loose file because a case
|
|
32
|
+
* is a directory, `vale/verify.ts` rejects a nested directory because a case is
|
|
33
|
+
* a document, and `verify.ts` counts ast-grep's own `valid:`/`invalid:` keys.
|
|
34
|
+
* The fact was recoverable only by reading three rejections, so every consumer
|
|
35
|
+
* transcribed it — which is what published this table in the first place.
|
|
36
|
+
*
|
|
37
|
+
* Each value is pinned beside the rejection that implements it, rather than in
|
|
38
|
+
* a test of its own: `runtime-fixtures.test.ts` asserts `case-directories`
|
|
39
|
+
* where it proves a loose file is refused, `vale-verify.test.ts` asserts
|
|
40
|
+
* `case-documents` where it proves a nested directory is refused, and
|
|
41
|
+
* `reference.test.ts` asserts `ast-grep-test` publishes no cases. A reader
|
|
42
|
+
* changing a reader's mind about its layout meets the declaration in the same
|
|
43
|
+
* test, which a separate file would not have achieved.
|
|
44
|
+
*/
|
|
45
|
+
export type FixtureLayout =
|
|
46
|
+
/** A case is a directory under `pass/` or `fail/`, handed to the check as its root. */
|
|
47
|
+
"case-directories"
|
|
48
|
+
/** A case is one document under `pass/` or `fail/`. */
|
|
49
|
+
| "case-documents"
|
|
50
|
+
/** Not a directory layout: `valid:`/`invalid:` keys inside one ast-grep test file. */
|
|
51
|
+
| "ast-grep-test";
|
|
52
|
+
/** How a rule reaches execution, or `null` when this CLI has no executor yet. */
|
|
53
|
+
export type EngineExecutor = "ast-grep" | "vale-runner" | "runtime-harness" | null;
|
|
54
|
+
export interface EngineLayout {
|
|
55
|
+
engine: EngineName;
|
|
56
|
+
/**
|
|
57
|
+
* The file inside a rule directory that *is* the rule, as a function of the
|
|
58
|
+
* rule id. `sg` and `vale` name it after the rule; `runtime` always calls it
|
|
59
|
+
* `check.ts`, because the rule is a program rather than a document.
|
|
60
|
+
*/
|
|
61
|
+
ruleFile: (ruleId: string) => string;
|
|
62
|
+
/**
|
|
63
|
+
* The engine's per-rule config file, or `undefined` where the engine has
|
|
64
|
+
* nothing to put in one.
|
|
65
|
+
*
|
|
66
|
+
* Only Vale has one, and not for symmetry: Vale cannot express a rule's scope
|
|
67
|
+
* inside the style file — measured, it rejects unknown keys with `E201` — so
|
|
68
|
+
* scope needs somewhere else to live. ast-grep carries `files`/`ignores`
|
|
69
|
+
* inside the rule itself, so an `sg` per-rule config would be a file every
|
|
70
|
+
* author creates, no author fills, and every reader learns to ignore.
|
|
71
|
+
*/
|
|
72
|
+
ruleConfigFile: string | undefined;
|
|
73
|
+
/** Subdirectory holding ast-grep capture rules, for engines that use them. */
|
|
74
|
+
capturesDirectory: string | undefined;
|
|
75
|
+
/** How this engine's fixtures group into cases. See {@link FixtureLayout}. */
|
|
76
|
+
fixtureLayout: FixtureLayout;
|
|
77
|
+
executor: EngineExecutor;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Everything defining a rule lives in one directory,
|
|
81
|
+
* `.taskless/rules/<engine>/<id>/`, the same shape for every engine. A rule is
|
|
82
|
+
* therefore one path — which is what lets `verify` and `test` take a path
|
|
83
|
+
* instead of an id, and what makes deleting a rule an `rm -rf` of one thing.
|
|
84
|
+
*/
|
|
85
|
+
export declare const RULES_DIRECTORY = "rules";
|
|
86
|
+
/**
|
|
87
|
+
* The directory the whole tree hangs off, relative to the project root.
|
|
88
|
+
*
|
|
89
|
+
* Here rather than beside its callers because the conformance corpus publishes
|
|
90
|
+
* `.taskless/rules/<engine>/<id>/` as a path a consumer can act on, and a
|
|
91
|
+
* published path needs one value to be generated from.
|
|
92
|
+
*
|
|
93
|
+
* It is not yet the only copy. The literal appears in about fifteen places
|
|
94
|
+
* behind four constants that do not know about each other — `CANONICAL_DIR` in
|
|
95
|
+
* `install/install.ts` and `install/canonical.ts`, `TASKLESS_DIR` in
|
|
96
|
+
* `install/state.ts`, `TASKLESS_DIRECTORY` in `rules/scan.ts` and
|
|
97
|
+
* `rules/vale/formats.ts`. `rulesRoot` reads this one, so what the corpus
|
|
98
|
+
* publishes is what the rule commands resolve. Converging the rest is a
|
|
99
|
+
* tidy-up, and doing it inside a contract change would hide the contract
|
|
100
|
+
* change inside a rename.
|
|
101
|
+
*/
|
|
102
|
+
export declare const TASKLESS_DIRECTORY = ".taskless";
|
|
103
|
+
/**
|
|
104
|
+
* A rule's tests, relative to its rule directory. **The dot is load-bearing.**
|
|
105
|
+
*
|
|
106
|
+
* ast-grep's `ruleDirs` recurses and parses every `.yml` beneath it as a rule,
|
|
107
|
+
* so a plain `tests/` directory inside a rule directory fails the entire scan
|
|
108
|
+
* with `Fail to parse yaml as RuleConfig: missing field 'language'`. Measured
|
|
109
|
+
* against ast-grep 0.41.0: `tests/` and `__tests__/` both hard-fail,
|
|
110
|
+
* a dot-directory is skipped by rule discovery, and `sg test` still reads it
|
|
111
|
+
* when `testDir` names it.
|
|
112
|
+
*
|
|
113
|
+
* That is undocumented behavior, and three things make depending on it
|
|
114
|
+
* acceptable. The failure is loud — a parse error naming the file, never a test
|
|
115
|
+
* silently reinterpreted as a rule. `ast-grep-vendor-contract.test.ts` pins it
|
|
116
|
+
* alongside the rest of ast-grep's observed behavior, so it is
|
|
117
|
+
* checked on every run rather than remembered. And the binary is pinned to an
|
|
118
|
+
* exact version, so it cannot change without a deliberate bump, which is
|
|
119
|
+
* exactly where that test fires.
|
|
120
|
+
*
|
|
121
|
+
* If it ever does break, the recorded fallback is to materialize a rules-only
|
|
122
|
+
* tree for ast-grep and point `ruleDirs` at that (design D2).
|
|
123
|
+
*/
|
|
124
|
+
export declare const RULE_TESTS_DIRECTORY = ".tests";
|
|
125
|
+
export declare const ENGINE_LAYOUTS: {
|
|
126
|
+
sg: {
|
|
127
|
+
engine: "sg";
|
|
128
|
+
ruleFile: (ruleId: string) => string;
|
|
129
|
+
ruleConfigFile: undefined;
|
|
130
|
+
capturesDirectory: undefined;
|
|
131
|
+
fixtureLayout: "ast-grep-test";
|
|
132
|
+
executor: "ast-grep";
|
|
133
|
+
};
|
|
134
|
+
vale: {
|
|
135
|
+
engine: "vale";
|
|
136
|
+
ruleFile: (ruleId: string) => string;
|
|
137
|
+
ruleConfigFile: string;
|
|
138
|
+
capturesDirectory: undefined;
|
|
139
|
+
fixtureLayout: "case-documents";
|
|
140
|
+
executor: "vale-runner";
|
|
141
|
+
};
|
|
142
|
+
runtime: {
|
|
143
|
+
engine: "runtime";
|
|
144
|
+
ruleFile: () => string;
|
|
145
|
+
ruleConfigFile: undefined;
|
|
146
|
+
capturesDirectory: string;
|
|
147
|
+
fixtureLayout: "case-directories";
|
|
148
|
+
executor: "runtime-harness";
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
/** Whether `value` names an engine this CLI knows. */
|
|
152
|
+
export declare function isKnownEngine(value: string): value is EngineName;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolving a prebuilt binary that ships as a per-platform npm package.
|
|
3
|
+
*
|
|
4
|
+
* This was ast-grep's resolver in `scan.ts`, generalized when Vale became a
|
|
5
|
+
* second engine that needs the same treatment. Both depend on per-platform
|
|
6
|
+
* packages directly and exec by path, because neither can rely on an
|
|
7
|
+
* install-time step: `@ast-grep/cli`'s postinstall hardlink fails under pnpm
|
|
8
|
+
* dlx's strict isolation, and the `@taskless/vale-*` packages deliberately ship
|
|
9
|
+
* no `bin` and no scripts at all.
|
|
10
|
+
*
|
|
11
|
+
* WHAT IS PARAMETERIZED, and why each field exists rather than being derived:
|
|
12
|
+
*
|
|
13
|
+
* - `toolchainSuffix` is the one that bites. ast-grep publishes
|
|
14
|
+
* `@ast-grep/cli-linux-x64-gnu` and `-win32-x64-msvc`; the Vale packages are
|
|
15
|
+
* `@taskless/vale-linux-x64` with **no libc suffix at all**. Reusing
|
|
16
|
+
* ast-grep's naming for Vale would look up `@taskless/vale-linux-x64-gnu` and
|
|
17
|
+
* miss on every Linux host — a resolution failure that reads as "Vale is not
|
|
18
|
+
* installed" rather than as a naming bug.
|
|
19
|
+
* - `identity` exists because existence is not proof. A file can sit exactly
|
|
20
|
+
* where the binary belongs and not be the binary: ast-grep's failed hardlink
|
|
21
|
+
* leaves a placeholder text file there. Asking a candidate to identify itself
|
|
22
|
+
* is the only check that tells the two apart.
|
|
23
|
+
*/
|
|
24
|
+
export interface PlatformBinarySpec {
|
|
25
|
+
/** Name used in error messages, e.g. `ast-grep`. */
|
|
26
|
+
label: string;
|
|
27
|
+
/** Package name up to the platform suffix, e.g. `@ast-grep/cli`. */
|
|
28
|
+
packagePrefix: string;
|
|
29
|
+
/**
|
|
30
|
+
* Append the platform's toolchain suffix (`-gnu` on Linux, `-msvc` on
|
|
31
|
+
* Windows). True for ast-grep, false for the Vale packages.
|
|
32
|
+
*/
|
|
33
|
+
toolchainSuffix: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Executable names to try, in confidence order, spelled for unix. `.exe` is
|
|
36
|
+
* appended on Windows. More than one because ast-grep declares both
|
|
37
|
+
* `ast-grep` and `sg` for the same target.
|
|
38
|
+
*
|
|
39
|
+
* The first name is the canonical one: it is the only spelling probed inside
|
|
40
|
+
* the platform package, and it is the name error messages tell the user to put
|
|
41
|
+
* on PATH. The rest are alternative link names, tried *first* at the
|
|
42
|
+
* link-based tiers — see {@link resolvePlatformBinary}.
|
|
43
|
+
*/
|
|
44
|
+
binaryNames: string[];
|
|
45
|
+
/** Pattern the candidate's own `--version` output must match. */
|
|
46
|
+
identity: RegExp;
|
|
47
|
+
}
|
|
48
|
+
/** The npm package carrying this host's prebuilt binary. */
|
|
49
|
+
export declare function platformPackageName(spec: PlatformBinarySpec): string;
|
|
50
|
+
/**
|
|
51
|
+
* The command name to tell a user to put on PATH, spelled for this platform
|
|
52
|
+
* (`sg.exe` on Windows). It is the first spelling the PATH search itself tries,
|
|
53
|
+
* so the advice and the search cannot drift apart.
|
|
54
|
+
*/
|
|
55
|
+
export declare function pathCommandName(spec: PlatformBinarySpec): string;
|
|
56
|
+
/** First entry on PATH that holds a file named `command`. */
|
|
57
|
+
export declare function findOnPath(command: string): string | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* Whether `path` is really this binary, established by running it.
|
|
60
|
+
*
|
|
61
|
+
* See {@link PlatformBinarySpec.identity} — existence is not enough, because a
|
|
62
|
+
* placeholder file left by a failed install sits at exactly the right path and
|
|
63
|
+
* satisfies `existsSync` happily.
|
|
64
|
+
*/
|
|
65
|
+
export declare function isPlatformBinary(spec: PlatformBinarySpec, path: string): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Which tier produced a resolution.
|
|
68
|
+
*
|
|
69
|
+
* `platform-package` is the version this CLI pins and installs. The other two
|
|
70
|
+
* are whatever the host happens to carry, and can be any version.
|
|
71
|
+
*/
|
|
72
|
+
export type PlatformBinarySource = "platform-package" | "node_modules/.bin" | "PATH";
|
|
73
|
+
export interface PlatformBinaryResolution {
|
|
74
|
+
/** Absolute path to the verified binary, or `undefined` when none resolved. */
|
|
75
|
+
path: string | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* Which tier the path came from, or `undefined` when nothing resolved.
|
|
78
|
+
*
|
|
79
|
+
* **Identity is not provenance.** Resolution proves the file exists and
|
|
80
|
+
* answered `--version` as the right tool; it proves nothing about the
|
|
81
|
+
* version. Only `platform-package` is the pinned install, so only that tier
|
|
82
|
+
* entitles a caller to {@link AST_GREP_VERSION}-style constants without
|
|
83
|
+
* running the binary.
|
|
84
|
+
*
|
|
85
|
+
* Reported rather than left to be re-derived. A caller that needs the pinned
|
|
86
|
+
* engine would otherwise compare the returned path against a package root it
|
|
87
|
+
* reconstructs itself, which is this resolver's own search order copied into
|
|
88
|
+
* a consumer, and copies drift.
|
|
89
|
+
*/
|
|
90
|
+
source: PlatformBinarySource | undefined;
|
|
91
|
+
/** Locations searched, in order, for an actionable failure message. */
|
|
92
|
+
tried: string[];
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Search every place the binary could reasonably live, verifying each.
|
|
96
|
+
*
|
|
97
|
+
* Candidates are ordered by confidence rather than by convenience — the
|
|
98
|
+
* platform package first because it is the version we pinned, then a locally
|
|
99
|
+
* linked binary, then whatever the host provides on PATH.
|
|
100
|
+
*
|
|
101
|
+
* The platform package is probed under one name only. Its contents are ours to
|
|
102
|
+
* predict — the package ships the binary under its canonical name — so trying
|
|
103
|
+
* the alternative spellings there buys nothing but an extra `require.resolve`
|
|
104
|
+
* and a duplicate entry in `tried`, which reads to the user as the same
|
|
105
|
+
* location searched twice. The link-based tiers do try every spelling, because
|
|
106
|
+
* which one exists there is the installer's choice, not ours.
|
|
107
|
+
*
|
|
108
|
+
* Returns rather than throws. The two callers want different things from a
|
|
109
|
+
* miss: ast-grep cannot run at all without it, while a missing Vale binary
|
|
110
|
+
* makes one engine unavailable and must not abort the others (D6b). Encoding
|
|
111
|
+
* "not found" as a value rather than an exception is what lets each decide.
|
|
112
|
+
*/
|
|
113
|
+
export declare function resolvePlatformBinary(spec: PlatformBinarySpec): PlatformBinaryResolution;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type PlatformBinaryResolution, type PlatformBinarySpec } from "../platform-binary";
|
|
2
|
+
/**
|
|
3
|
+
* Vale's per-platform packaging.
|
|
4
|
+
*
|
|
5
|
+
* `toolchainSuffix: false` is the whole reason this spec exists separately from
|
|
6
|
+
* ast-grep's. `add-vale-binary-packages` publishes `@taskless/vale-<os>-<cpu>`
|
|
7
|
+
* — `@taskless/vale-linux-x64`, not `-linux-x64-gnu`. ast-grep's resolver
|
|
8
|
+
* appends `-gnu` on every Linux, so reusing its naming here would resolve
|
|
9
|
+
* nothing on Linux while reporting the ordinary "Vale is unavailable" message,
|
|
10
|
+
* making a naming bug indistinguishable from a host without the binary.
|
|
11
|
+
*
|
|
12
|
+
* One binary name, unlike ast-grep's two: these packages ship `vale` (or
|
|
13
|
+
* `vale.exe`) as pure payload, with no `bin` entry and no lifecycle script, so
|
|
14
|
+
* there is no wrapper spelling to also try.
|
|
15
|
+
*/
|
|
16
|
+
export declare const VALE_BINARY: PlatformBinarySpec;
|
|
17
|
+
/**
|
|
18
|
+
* Locate the Vale binary, or report that it is unavailable.
|
|
19
|
+
*
|
|
20
|
+
* Returns `undefined` rather than throwing, per D6b: a missing Vale binary
|
|
21
|
+
* makes the Vale engine unavailable and must not abort the other engines. The
|
|
22
|
+
* caller turns that into a reported-but-not-fatal outcome; ast-grep's resolver
|
|
23
|
+
* throws instead, because it has no degraded mode.
|
|
24
|
+
*/
|
|
25
|
+
export declare function findValeBinary(): PlatformBinaryResolution;
|
|
26
|
+
/** Reset the process cache. Tests only. */
|
|
27
|
+
export declare function resetValeBinaryCache(): void;
|
|
28
|
+
/**
|
|
29
|
+
* An actionable message naming where we looked.
|
|
30
|
+
*
|
|
31
|
+
* The PATH advice is spelled for the platform — `vale.exe` on Windows — rather
|
|
32
|
+
* than hardcoded, so a Windows user is not told to install a name that the
|
|
33
|
+
* resolver would not find there.
|
|
34
|
+
*/
|
|
35
|
+
export declare function valeUnavailableMessage(tried: string[]): string;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entry for `@taskless/cli/schemas`.
|
|
3
|
+
*
|
|
4
|
+
* The shapes this CLI's `--json` output is produced from, as data. A consumer
|
|
5
|
+
* parsing `taskless verify --json` validates against the schema the CLI emits
|
|
6
|
+
* from, rather than against an interface it hand-wrote by reading our output
|
|
7
|
+
* once.
|
|
8
|
+
*
|
|
9
|
+
* ## Why this is published
|
|
10
|
+
*
|
|
11
|
+
* This is `@taskless/cli/layout`'s argument applied to the other half of the
|
|
12
|
+
* contract. A hand-written interface is a copy that nothing checks: the CLI can
|
|
13
|
+
* add a field, change what one means, or extend an enum, and the copy stays
|
|
14
|
+
* confidently wrong until something downstream misbehaves. The Cloud eval team
|
|
15
|
+
* is doing exactly that today against the `verify` envelope.
|
|
16
|
+
*
|
|
17
|
+
* ## What this is NOT
|
|
18
|
+
*
|
|
19
|
+
* Not an SDK. Nothing here performs verification, and no `verify()` or `test()`
|
|
20
|
+
* is exported. Both spawn a vendored platform binary — ast-grep or Vale — so
|
|
21
|
+
* anywhere a function call could run them, `npx @taskless/cli verify --json`
|
|
22
|
+
* runs too: the export would buy a call site, not a capability, while making
|
|
23
|
+
* internal signatures public. The CLI stays the execution surface; this
|
|
24
|
+
* describes what it says.
|
|
25
|
+
*
|
|
26
|
+
* Nothing here reaches the filesystem, a process, the network or the command
|
|
27
|
+
* tree, so a Worker can import it — and the build fails rather than emitting an
|
|
28
|
+
* entry whose graph reaches a host capability. `writeJsonError` is deliberately
|
|
29
|
+
* absent for that reason: it writes to stdout, and a consumer asking what shape
|
|
30
|
+
* an error takes should not thereby acquire something that emits one.
|
|
31
|
+
*
|
|
32
|
+
* ## Zod is bundled, and that is the point
|
|
33
|
+
*
|
|
34
|
+
* This entry ships its own copy of zod rather than resolving the consumer's.
|
|
35
|
+
* That follows from the build's standing rule for library entries — everything
|
|
36
|
+
* but node builtins is bundled — but it is also the behaviour we want, so do
|
|
37
|
+
* not "fix" it by making zod external.
|
|
38
|
+
*
|
|
39
|
+
* **A consumer's zod is not ours.** Marking it external would make validation
|
|
40
|
+
* depend on whichever version resolved on the other side, so the same payload
|
|
41
|
+
* could parse there and not here, and the schema would stop being a statement
|
|
42
|
+
* about what this CLI emits. Bundling makes `parse()` answer with our zod's
|
|
43
|
+
* semantics wherever it runs.
|
|
44
|
+
*
|
|
45
|
+
* **And `parse()` is stronger than a JSON Schema of the same shape.** It
|
|
46
|
+
* strips: `parse({ ok: true, rules: [], surprise: 1 })` returns
|
|
47
|
+
* `{ ok: true, rules: [] }`, where a JSON Schema validator hands back the
|
|
48
|
+
* object it was given, unknown keys included. Every schema here is plain today
|
|
49
|
+
* — objects, enums, arrays, optionals — so a JSON Schema rendering would lose
|
|
50
|
+
* little beyond that. It is the moment we add a refinement that the difference
|
|
51
|
+
* bites, and it would bite silently: the rendering would go on validating a
|
|
52
|
+
* weaker shape while claiming to describe this one.
|
|
53
|
+
*
|
|
54
|
+
* `z.toJSONSchema()` therefore stays unexported, and the criterion is the
|
|
55
|
+
* TRANSPORT rather than the consumer's language. JSON Schema is what you reach
|
|
56
|
+
* for when the schema itself has to travel — when the far side can receive JSON
|
|
57
|
+
* and nothing else, so a description of the shape is the most that fits. That
|
|
58
|
+
* is not this boundary. A consumer here takes a dependency on our schema files,
|
|
59
|
+
* which means it can assert the payload rather than assert against a
|
|
60
|
+
* description of the payload, and rendering ours down to what a JSON transport
|
|
61
|
+
* could carry would give that up in exchange for nothing.
|
|
62
|
+
*/
|
|
63
|
+
export {
|
|
64
|
+
/**
|
|
65
|
+
* `taskless verify --json` and `taskless test --json`.
|
|
66
|
+
*
|
|
67
|
+
* One envelope for both, because the commands share an implementation and
|
|
68
|
+
* differ only in what they run against each rule.
|
|
69
|
+
*/
|
|
70
|
+
outputSchema as verifyTestOutputSchema, } from "./verify-test.js";
|
|
71
|
+
export {
|
|
72
|
+
/** `taskless rule verify <id> --json`, for an ast-grep rule. */
|
|
73
|
+
verifyOutputSchema,
|
|
74
|
+
/** The same, for a Vale rule — a different shape, discriminated on `engine`. */
|
|
75
|
+
valeVerifyOutputSchema, } from "./rules-verify.js";
|
|
76
|
+
export type {
|
|
77
|
+
/**
|
|
78
|
+
* A constraint `verify` or `test` enforces beyond an engine's own schema.
|
|
79
|
+
*
|
|
80
|
+
* The type of the entries in `@taskless/cli/reference.json`'s
|
|
81
|
+
* `constraints[]`, so a consumer reading the corpus and a consumer reading a
|
|
82
|
+
* rejection are working from one definition.
|
|
83
|
+
*/
|
|
84
|
+
RuleConstraint,
|
|
85
|
+
/** One constraint a rule broke, as `violations[]` carries it. */
|
|
86
|
+
RuleViolation,
|
|
87
|
+
/** The id of a constraint this CLI publishes. */
|
|
88
|
+
RuleConstraintId, } from "../rules/constraints.js";
|
|
89
|
+
export type {
|
|
90
|
+
/** The stable code an error envelope carries under `--json`. */
|
|
91
|
+
CLIErrorCode,
|
|
92
|
+
/** The envelope itself. Not every `--json` failure is a rule result. */
|
|
93
|
+
CLIErrorEnvelope, } from "../types/errors.js";
|
|
@@ -11,6 +11,7 @@ export declare const outputSchema: z.ZodObject<{
|
|
|
11
11
|
ruleId: z.ZodString;
|
|
12
12
|
rules: z.ZodArray<z.ZodString>;
|
|
13
13
|
files: z.ZodArray<z.ZodString>;
|
|
14
|
+
notices: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
14
15
|
}, z.core.$strip>;
|
|
15
16
|
/** Error schema for `taskless rule create --json` on failure */
|
|
16
17
|
export declare const errorSchema: z.ZodObject<{
|
|
@@ -14,6 +14,7 @@ export declare const outputSchema: z.ZodObject<{
|
|
|
14
14
|
requestId: z.ZodString;
|
|
15
15
|
rules: z.ZodArray<z.ZodString>;
|
|
16
16
|
files: z.ZodArray<z.ZodString>;
|
|
17
|
+
notices: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
17
18
|
}, z.core.$strip>;
|
|
18
19
|
/** Error schema for `taskless rule improve --json` on failure */
|
|
19
20
|
export declare const errorSchema: z.ZodObject<{
|