diff --git a/.changeset/schema-verify-docstrings.md b/.changeset/schema-verify-docstrings.md new file mode 100644 index 00000000..17e9ffbf --- /dev/null +++ b/.changeset/schema-verify-docstrings.md @@ -0,0 +1,11 @@ +--- +"@taskless/cli": patch +--- + +Corrected the published `@taskless/cli/schemas` docstrings for +`verifyOutputSchema` and `valeVerifyOutputSchema`, which named a command form +— `taskless rule verify --json` — that was removed when rule addressing +moved from id to path. No runtime behavior changes; the schemas themselves +are unchanged. A consumer reading these docstrings (e.g. via editor +tooltips or generated docs) would previously be pointed at a command that +does not exist. diff --git a/packages/cli/src/schemas/ast-grep-rule.ts b/packages/cli/src/schemas/ast-grep-rule.ts index bd0f58aa..5a86ed1d 100644 --- a/packages/cli/src/schemas/ast-grep-rule.ts +++ b/packages/cli/src/schemas/ast-grep-rule.ts @@ -7,8 +7,12 @@ import astGrepSchema from "../generated/ast-grep-rule-schema.json"; * ast-grep JSON Schema via `z.fromJSONSchema()`. This gives us full * validation coverage matching the upstream spec. * - * The raw JSON Schema is also embedded for agent consumption via - * `rule verify --schema`. + * The raw JSON Schema is also embedded in `getSchemaPayload()`'s + * `astGrepSchema` field, for agent-facing consumption. There is no `--schema` + * CLI flag any more — it was removed CLI-wide in favor of embedding schema + * content in recipes — so nothing today reaches this via a command a user + * types; the comment used to say `rule verify --schema`, which named both a + * flag and a command form that no longer exist. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument export const astGrepRuleSchema = z.fromJSONSchema(astGrepSchema as any); diff --git a/packages/cli/src/schemas/index.ts b/packages/cli/src/schemas/index.ts index 503f0c39..06938155 100644 --- a/packages/cli/src/schemas/index.ts +++ b/packages/cli/src/schemas/index.ts @@ -76,7 +76,20 @@ export { } from "./verify-test.js"; export { - /** `taskless rule verify --json`, for an ast-grep rule. */ + /** + * The layered detail behind one ast-grep rule's verification — schema + * validation, Taskless requirement checks, and `sg test`, kept separate + * rather than flattened into `verifyTestOutputSchema`'s single envelope. + * + * Not what `taskless verify --json` or `taskless test --json` print. Both + * commands compute this internally via `verifyRule()` and then flatten it + * before printing anything, so there is no current CLI invocation that + * emits this shape verbatim. A command that did — `taskless rule verify + * --json` — existed once and was removed when rule addressing moved + * from id to path, because an id can name a rule under two engines and a + * path cannot (see `resolve-path.ts`). This schema describes the pre- + * flattening detail that command used to print, unchanged since. + */ verifyOutputSchema, /** The same, for a Vale rule — a different shape, discriminated on `engine`. */ valeVerifyOutputSchema, diff --git a/packages/cli/src/schemas/rules-verify.ts b/packages/cli/src/schemas/rules-verify.ts index 0b5906cf..b560df41 100644 --- a/packages/cli/src/schemas/rules-verify.ts +++ b/packages/cli/src/schemas/rules-verify.ts @@ -29,7 +29,19 @@ export const schemaOutputSchema = z.object({ .describe("Curated annotated rule examples"), }); -// --- Verify mode output (rule verify --json) --- +// --- Verify layer detail --- +// +// The shape `verifyRule()` (ast-grep) and `verifyValeRule()` (Vale) return +// internally: schema/requirements/tests kept as separate layers rather than +// flattened. `verify`/`test`'s own implementation is the only caller of +// either function, and it flattens this into the single `errors`/ +// `violations` envelope `verifyTestOutputSchema` describes before printing +// anything — so this layered shape is not what `taskless verify --json` or +// `taskless test --json` put on stdout today. It once was: `taskless rule +// verify --json` printed exactly this (`engine: "sg"` plus `verifyRule`'s +// result), until rule addressing moved from id to path — an id can name a +// rule under two engines, a path cannot — and `rule verify` was removed with +// it. See `resolve-path.ts` for why that move happened. const layerResultSchema = z.object({ valid: z.boolean(), diff --git a/packages/cli/test/schemas-export.test.ts b/packages/cli/test/schemas-export.test.ts index 60ed4947..e4526b67 100644 --- a/packages/cli/test/schemas-export.test.ts +++ b/packages/cli/test/schemas-export.test.ts @@ -7,6 +7,10 @@ import { promisify } from "node:util"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { verifyRule } from "../src/rules/verify"; +import { findValeBinary } from "../src/rules/vale/binary"; +import { verifyValeRule } from "../src/rules/vale/verify"; + const execFileAsync = promisify(execFile); const distributionDirectory = resolve(import.meta.dirname, "../dist"); @@ -129,3 +133,104 @@ describe("the published schemas entry", () => { ).toBe(false); }); }); + +/** + * `verifyOutputSchema` and `valeVerifyOutputSchema` are the other half of the + * bug this file exists to catch (issue #283): no command spawns to produce + * their shape any more. `taskless rule verify --json` printed exactly + * this — `{ engine: "sg", ...verifyRule() }`, or the equivalent mapped + * envelope over `verifyValeRule()` for Vale — until rule addressing moved + * from id to path and `rule verify` was removed with it. See the docstrings + * on these exports in `src/schemas/index.ts` for the full history. + * + * That means the "spawn the CLI, parse its stdout" pattern above cannot pin + * these two: there is no invocation left that emits this shape. What CAN be + * pinned, absent a command to spawn, is that the published schema still + * parses the exact envelope the internal functions produce today — built the + * same way the removed command built it. A future change to `verifyRule()` or + * `verifyValeRule()` that drifts from what's published here fails a real + * test, rather than staying invisible the way the id/path mismatch did. + */ +describe("verifyOutputSchema and valeVerifyOutputSchema", () => { + it("verifyOutputSchema parses verifyRule()'s real return value for an sg rule", async () => { + const directory = join(cwd, ".taskless", "rules", "sg", "schema-probe"); + await mkdir(join(directory, ".tests"), { recursive: true }); + await writeFile( + join(directory, "schema-probe.yml"), + "id: schema-probe\nlanguage: TypeScript\nseverity: error\n" + + "message: no eval\nrule:\n pattern: eval($ARG)\n" + ); + await writeFile( + join(directory, ".tests", "schema-probe-test.yml"), + "id: schema-probe\nvalid:\n - const a = 1;\ninvalid:\n - eval(x);\n" + ); + + // The exact envelope the removed command built: `{ engine: "sg", ...result }`. + const result = await verifyRule(cwd, "schema-probe"); + + const built = await importBuiltSchemas(); + const schema = built.verifyOutputSchema as ParsedSchema; + const parsed = schema.parse({ engine: "sg", ...result }) as { + success: boolean; + ruleId: string; + tests: { passed: number; failed: number }; + }; + + expect(parsed.success).toBe(true); + expect(parsed.ruleId).toBe("schema-probe"); + expect(parsed.tests).toMatchObject({ passed: 1, failed: 0 }); + }); + + const withVale = findValeBinary().path === undefined ? it.skip : it; + + withVale( + "valeVerifyOutputSchema parses the envelope built over verifyValeRule()'s real return value", + async () => { + const directory = join( + cwd, + ".taskless", + "rules", + "vale", + "schema-probe-vale" + ); + await mkdir(join(directory, ".tests", "pass"), { recursive: true }); + await mkdir(join(directory, ".tests", "fail"), { recursive: true }); + await writeFile( + join(directory, "schema-probe-vale.yml"), + "extends: existence\nmessage: \"Avoid 'simply'\"\nlevel: warning\ntokens:\n - simply\n" + ); + await writeFile( + join(directory, ".tests", "pass", "clean.md"), + "Nothing objectionable.\n" + ); + await writeFile( + join(directory, ".tests", "fail", "dirty.md"), + "Just simply do it.\n" + ); + + const result = await verifyValeRule(cwd, "schema-probe-vale"); + if ("outcome" in result) { + throw new Error( + `Vale did not run: ${result.outcome.status} — ${result.outcome.message}` + ); + } + + // The exact mapping the removed command applied: `passed` -> `success`, + // everything else carried straight through. + const built = await importBuiltSchemas(); + const schema = built.valeVerifyOutputSchema as ParsedSchema; + const parsed = schema.parse({ + engine: "vale", + success: result.passed, + ruleId: result.ruleId, + fixtures: result.fixtures, + missingFailures: result.missingFailures, + unexpectedFindings: result.unexpectedFindings, + ...(result.notice === undefined ? {} : { notice: result.notice }), + }) as { success: boolean; ruleId: string }; + + expect(parsed.success).toBe(true); + expect(parsed.ruleId).toBe("schema-probe-vale"); + } + ); +});