Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/schema-verify-docstrings.md
Original file line number Diff line number Diff line change
@@ -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 <id> --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.
8 changes: 6 additions & 2 deletions packages/cli/src/schemas/ast-grep-rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
15 changes: 14 additions & 1 deletion packages/cli/src/schemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,20 @@ export {
} from "./verify-test.js";

export {
/** `taskless rule verify <id> --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
* <id> --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,
Expand Down
14 changes: 13 additions & 1 deletion packages/cli/src/schemas/rules-verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,19 @@ export const schemaOutputSchema = z.object({
.describe("Curated annotated rule examples"),
});

// --- Verify mode output (rule verify <id> --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 <id> --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(),
Expand Down
105 changes: 105 additions & 0 deletions packages/cli/test/schemas-export.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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 <id> --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");
}
);
});
Loading