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
5 changes: 5 additions & 0 deletions .changeset/vale-3-26-0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@taskless/cli": patch
---

Update the bundled Vale to 3.26.0. `.mjs`, `.cjs`, `.mts` and `.cts` files are now read as code, so a Vale rule over them sees comments only, as it already did for `.js` and `.ts`. A phrase in a rule now matches across a line wrap, and a `spelling` rule no longer flags common technical words such as `OAuth` and `npm`.
45 changes: 39 additions & 6 deletions .github/scripts/vale-upgrade-report.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@
* run. Listing the files upstream ADDED under `internal/lint/` is the source
* check, mechanically.
*
* AND THE ROUTING TABLE, BECAUSE A FORMAT CAN BE LEARNED WITHOUT A NEW FILE.
* 3.26.0 sent `.mjs`, `.cjs`, `.mts` and `.cts` to the existing JavaScript and
* TypeScript readers with a two-line edit to `FormatByExtension` in
* `internal/core/format.go`, while the four files it did add were build-tag
* splits for a WebAssembly build that the native CLI never reaches. The list
* above pointed a reviewer at four non-formats and said nothing about four
* real ones. So the report also prints that file's patch, whenever it changed.
*
* Both halves report and never throw. A failed fetch or a missing test report
* is said in the body rather than failing the run: the proposal is still worth
* opening, and "we could not check" must not read like "nothing to see".
Expand All @@ -43,6 +51,9 @@ const { readFileSync, writeFileSync } = require("node:fs");

const { baseVersion } = require("./vale-upgrade-detect.cjs");

/** Where upstream maps an extension to the reader that handles it. */
const ROUTING_FILE = "internal/core/format.go";

/** The compare API returns at most this many files, with no marker when cut. */
const COMPARE_FILE_CAP = 300;

Expand All @@ -66,7 +77,9 @@ function summarizeContract(report) {
for (const assertion of file.assertionResults ?? []) {
if (assertion.status !== "failed") continue;
failed.push({
file: String(file.name ?? "").split("/").pop(),
file: String(file.name ?? "")
.split("/")
.pop(),
name: assertion.fullName,
message: String(assertion.failureMessages?.[0] ?? "").split("\n")[0],
});
Expand Down Expand Up @@ -105,24 +118,32 @@ async function fetchAddedReaders(repository, from, to) {
throw new Error(`GET ${url} responded ${response.status}`);
}
const files = (await response.json()).files ?? [];
const routing = files.find((file) => file.filename === ROUTING_FILE);
return {
added: files
.filter((file) => file.status === "added" && isReader(file.filename))
.map((file) => file.filename)
.toSorted(),
// The patch text, or `undefined` when the file did not change. The API
// omits `patch` for a diff too large to inline, which reads as a change
// with nothing to show rather than as no change.
routing:
routing === undefined
? undefined
: (routing.patch ?? "(patch too large to inline; read it in the diff)"),
complete: files.length < COMPARE_FILE_CAP,
url: `https://github.com/${repository}/compare/v${from}...v${to}`,
};
}

/** A fence no line of `text` can close early. */
function fence(text) {
function fence(text, info = "") {
const longest = Math.max(
2,
...[...text.matchAll(/`+/g)].map((match) => match[0].length)
);
const marker = "`".repeat(longest + 1);
return `${marker}\n${text}\n${marker}`;
return `${marker}${info}\n${text}\n${marker}`;
}

function formatContract(contract) {
Expand All @@ -147,11 +168,20 @@ function formatReaders(readers, error) {
const caveat = readers.complete
? ""
: `\n\nThe comparison touched ${COMPARE_FILE_CAP} or more files, where the API stops listing them, so this may be incomplete. Check [the full diff](${readers.url}).`;
const routing = formatRouting(readers.routing);
if (readers.added.length === 0) {
return `${heading}\n\nNone under \`${READER_PREFIX}\` ([diff](${readers.url})). No extension can have changed tier without a row that re-probes it.${caveat}\n`;
return `${heading}\n\nNone under \`${READER_PREFIX}\` ([diff](${readers.url})).${caveat}\n${routing}`;
}
const list = readers.added.map((file) => `- \`${file}\``).join("\n");
return `${heading}\n\n${list}\n\nEach can route an extension that \`VALE_FORMAT_TIERS\` in \`capabilities.ts\` has no row for, and an unlisted extension is never probed. Find where \`internal/core/format.go\` sends it, probe the extension against the new binary, and add a row. A reader that needs an external converter is the dangerous case: the missing row becomes a crash that aborts every Vale rule in the run. ([diff](${readers.url}))${caveat}\n`;
return `${heading}\n\n${list}\n\nEach can route an extension that \`VALE_FORMAT_TIERS\` in \`capabilities.ts\` has no row for, and an unlisted extension is never probed. Find where \`${ROUTING_FILE}\` sends it, probe the extension against the new binary, and add a row. A reader that needs an external converter is the dangerous case: the missing row becomes a crash that aborts every Vale rule in the run. A file whose first line is \`//go:build js\` belongs to the WebAssembly build only, and the CLI never reaches it. ([diff](${readers.url}))${caveat}\n${routing}`;
}

function formatRouting(patch) {
const heading = `### Extension routing (\`${ROUTING_FILE}\`)`;
if (patch === undefined) {
return `\n${heading}\n\nUnchanged. With no reader added either, no extension can have changed tier without a row that re-probes it.\n`;
}
return `\n${heading}\n\n**Changed.** An extension added to \`FormatByExtension\` is a format Vale learned, even when no reader file was added. Probe each one against the new binary and add a row to \`VALE_FORMAT_TIERS\`.\n\n${fence(patch, "diff")}\n`;
}

function formatReport({ from, to, contract, readers, readersError }) {
Expand All @@ -176,7 +206,9 @@ async function main({
const repository = readArgument(argv, "--repository");
const out = readArgument(argv, "--out");

const contract = summarizeContract(readReport(readArgument(argv, "--contract")));
const contract = summarizeContract(
readReport(readArgument(argv, "--contract"))
);
let readers;
let readersError;
try {
Expand All @@ -193,6 +225,7 @@ async function main({

module.exports = {
COMPARE_FILE_CAP,
ROUTING_FILE,
fetchAddedReaders,
formatReport,
isReader,
Expand Down
37 changes: 33 additions & 4 deletions .github/scripts/vale-upgrade-report.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,10 @@ test("contract: names each failure with its file and first message line", () =>
]);
const section = report({ contract });
assert.match(section, /\*\*1 of 204 failed\.\*\*/);
assert.match(section, /vale-schema-contract\.test\.ts > Vale schema contract/);
assert.match(
section,
/vale-schema-contract\.test\.ts > Vale schema contract/
);
assert.doesNotMatch(section, /at somewhere/);
});

Expand Down Expand Up @@ -108,7 +111,10 @@ test("readers: lists only added, non-test Go files under internal/lint/", () =>

test("readers: an added reader asks for a probe and a row", () => {
const section = report({
readers: readers(["internal/lint/code/kt.go", "internal/lint/code/toml.go"]),
readers: readers([
"internal/lint/code/kt.go",
"internal/lint/code/toml.go",
]),
});
assert.match(section, /- `internal\/lint\/code\/kt\.go`/);
assert.match(section, /VALE_FORMAT_TIERS/);
Expand All @@ -122,7 +128,10 @@ test("readers: none added is said, with the diff to check it by", () => {
});

test("readers: a failed fetch says it could not check, never that none were added", () => {
const section = report({ readers: undefined, readersError: "GET x responded 502" });
const section = report({
readers: undefined,
readersError: "GET x responded 502",
});
assert.match(section, /\*\*Could not check\*\* \(GET x responded 502\)/);
assert.doesNotMatch(section, /None under/);
});
Expand Down Expand Up @@ -188,8 +197,28 @@ test("main: a fetch that throws is reported, not raised", async () => {
},
readReport: () => vitestReport(),
});
assert.match(readFileSync(out, "utf8"), /Could not check\*\* \(GET x responded 403\)/);
assert.match(
readFileSync(out, "utf8"),
/Could not check\*\* \(GET x responded 403\)/
);
} finally {
rmSync(directory, { recursive: true, force: true });
}
});

test("routing: a changed format.go prints its patch, even with no reader added", () => {
// 3.26.0: four `.mjs`-style spellings learned in format.go, no reader added
// for them, and the added readers were all WebAssembly build splits.
const patch =
'- `\\.(?:js|jsx)$`: {".js", "code"},\n+ `\\.(?:js|jsx|mjs|cjs)$`: {".js", "code"},';
const section = report({ readers: { ...readers([]), routing: patch } });
assert.match(section, /\*\*Changed\.\*\*/);
assert.match(section, /^```diff$/m);
assert.ok(section.includes("mjs|cjs"), "the patch itself is in the body");
assert.doesNotMatch(section, /Unchanged\./);
});

test("routing: an unchanged format.go is said, so silence is not mistaken for a check", () => {
const section = report({});
assert.match(section, /internal\/core\/format\.go`\)\n\nUnchanged\./);
});
2 changes: 1 addition & 1 deletion .taskless/taskless.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"reconciledTo": "0.13.4",
"engines": {
"sg": "0.51.0",
"vale": "3.24.0"
"vale": "3.26.0"
}
}
}
12 changes: 6 additions & 6 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,11 @@
"@ast-grep/cli-win32-arm64-msvc": "0.51.0",
"@ast-grep/cli-win32-ia32-msvc": "0.51.0",
"@ast-grep/cli-win32-x64-msvc": "0.51.0",
"@taskless/vale-darwin-arm64": "3.24.0-20261005234241",
"@taskless/vale-darwin-x64": "3.24.0-20261005234241",
"@taskless/vale-linux-arm64": "3.24.0-20261005234241",
"@taskless/vale-linux-x64": "3.24.0-20261005234241",
"@taskless/vale-win32-arm64": "3.24.0-20261005234241",
"@taskless/vale-win32-x64": "3.24.0-20261005234241"
"@taskless/vale-darwin-arm64": "3.26.0-20261102164455",
"@taskless/vale-darwin-x64": "3.26.0-20261102164455",
"@taskless/vale-linux-arm64": "3.26.0-20261102164455",
"@taskless/vale-linux-x64": "3.26.0-20261102164455",
"@taskless/vale-win32-arm64": "3.26.0-20261102164455",
"@taskless/vale-win32-x64": "3.26.0-20261102164455"
}
}
23 changes: 23 additions & 0 deletions packages/cli/src/agent/update.md
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,29 @@ What needs you, after `init`:
with the reason, and `verify` fails the rule until it is gone. Move
those files by hand into case directories, then delete `.tests/`.

### Migrating to 0.13.5

Vale moves from 3.24.0 to 3.26.0. Nothing here needs work by hand.
Three things change which findings an existing rule reports, each
measured against both binaries on the same fixture.

**`.mjs`, `.cjs`, `.mts` and `.cts` are read as code.** Through 3.24.0
Vale linted them as one block of prose, so a rule matching `[*.mjs]`
fired on `const simply = 1` as readily as on a comment. They now go
through the JavaScript and TypeScript readers, which read comments
only, the way `.js` and `.ts` always were. Findings in code disappear;
findings in comments stay.

**A phrase matches across a line wrap.** A rule with the token
`click here` missed `click` at the end of one line and `here` at the
start of the next. It now reports it, so a wrapped Markdown paragraph
can produce findings it did not before.

**Vale's built-in dictionary knows common technical words.** A
`spelling` rule no longer flags words like `OAuth` and `npm`, so
findings disappear and a project vocabulary entry that existed only
for such a word is now redundant. Leave it; it does no harm.

## Errors

With `--json`, `--rules` failures emit `{ ok: false, code, message }`:
Expand Down
12 changes: 6 additions & 6 deletions packages/cli/src/generated/vale-vocabulary-report.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Vale 3.24.0 vocabulary: divergence report
# Vale 3.26.0 vocabulary: divergence report

GENERATED FILE — DO NOT EDIT. Produced by `pnpm generate:vale-schema`
alongside `vale-vocabulary.ts`.

Every value in the vocabulary is the recorded answer of the vendored Vale
3.24.0 binary. This file is what the binary said that its own
3.26.0 binary. This file is what the binary said that its own
documentation does not, in both directions. A generator that dropped these
would be quietly deciding which of the two to believe.

Expand All @@ -28,25 +28,25 @@ the worse failure.

### `scope: meta`

Vale 3.24.0 documents this operand and it never fired, on any fixture probed (.md).
Vale 3.26.0 documents this operand and it never fired, on any fixture probed (.md).

**Consequence.** It is omitted from the vocabulary, so `validate` rejects it. A rule written from the documentation would otherwise load, run, and match nothing, with no error reported anywhere.

### `scope: meta.class.title`

Vale 3.24.0 documents this operand and it never fired, on any fixture probed (.md).
Vale 3.26.0 documents this operand and it never fired, on any fixture probed (.md).

**Consequence.** It is omitted from the vocabulary, so `validate` rejects it. A rule written from the documentation would otherwise load, run, and match nothing, with no error reported anywhere.

### `scope: frontmatter`

This operand fired and Vale 3.24.0 documents it nowhere.
This operand fired and Vale 3.26.0 documents it nowhere.

**Consequence.** It is included in the vocabulary. It is also the standing counterexample to trusting the candidate list: a real operand nobody proposes is simply absent, and the schema then rejects a rule the binary honors.

### `scope: frontmatter.title`

This operand fired and Vale 3.24.0 documents it nowhere.
This operand fired and Vale 3.26.0 documents it nowhere.

**Consequence.** It is included in the vocabulary. It is also the standing counterexample to trusting the candidate list: a real operand nobody proposes is simply absent, and the schema then rejects a rule the binary honors.

Expand Down
14 changes: 7 additions & 7 deletions packages/cli/src/generated/vale-vocabulary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* `scripts/generate-vale-schema.ts`, and its header explains what each value
* below was measured with and what it is worth.
*
* Derived against Vale 3.24.0. Every value here is the recorded answer
* Derived against Vale 3.26.0. Every value here is the recorded answer
* of that binary to a rule the generator wrote and ran; nothing is transcribed
* from documentation. Where the binary and the documentation disagree, the
* disagreement is in `vale-vocabulary-report.md` rather than dropped.
Expand All @@ -15,7 +15,7 @@
*/

/** The binary this vocabulary was derived from. */
export const VALE_VOCABULARY_VERSION = "3.24.0";
export const VALE_VOCABULARY_VERSION = "3.26.0";

/**
* Vale's check types, self-enumerated: an unknown `extends` makes the binary
Expand Down Expand Up @@ -199,7 +199,7 @@ export const VALE_SCOPE_PREFIXES = [
] as const;

/**
* Where Vale 3.24.0 and its documentation disagree.
* Where Vale 3.26.0 and its documentation disagree.
*
* Carried in the artifact rather than only in the report, so that a consumer
* can render them and a reviewer cannot miss them in a diff.
Expand All @@ -208,26 +208,26 @@ export const VALE_DIVERGENCES = [
{
subject: "scope: meta",
finding:
"Vale 3.24.0 documents this operand and it never fired, on any fixture probed (.md).",
"Vale 3.26.0 documents this operand and it never fired, on any fixture probed (.md).",
consequence:
"It is omitted from the vocabulary, so `validate` rejects it. A rule written from the documentation would otherwise load, run, and match nothing, with no error reported anywhere.",
},
{
subject: "scope: meta.class.title",
finding:
"Vale 3.24.0 documents this operand and it never fired, on any fixture probed (.md).",
"Vale 3.26.0 documents this operand and it never fired, on any fixture probed (.md).",
consequence:
"It is omitted from the vocabulary, so `validate` rejects it. A rule written from the documentation would otherwise load, run, and match nothing, with no error reported anywhere.",
},
{
subject: "scope: frontmatter",
finding: "This operand fired and Vale 3.24.0 documents it nowhere.",
finding: "This operand fired and Vale 3.26.0 documents it nowhere.",
consequence:
"It is included in the vocabulary. It is also the standing counterexample to trusting the candidate list: a real operand nobody proposes is simply absent, and the schema then rejects a rule the binary honors.",
},
{
subject: "scope: frontmatter.title",
finding: "This operand fired and Vale 3.24.0 documents it nowhere.",
finding: "This operand fired and Vale 3.26.0 documents it nowhere.",
consequence:
"It is included in the vocabulary. It is also the standing counterexample to trusting the candidate list: a real operand nobody proposes is simply absent, and the schema then rejects a rule the binary honors.",
},
Expand Down
22 changes: 21 additions & 1 deletion packages/cli/src/rules/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ export const AST_GREP_TSX_SPLIT: Readonly<
* Pinned against the binary by `test/vale-vendor-contract.test.ts`
* ("engine capabilities" → "reports the pinned version").
*/
export const VALE_VERSION = "3.24.0";
export const VALE_VERSION = "3.26.0";

/**
* Which tier Vale routes an extension to.
Expand Down Expand Up @@ -278,6 +278,22 @@ const CONVERTER_TIER_PREFIX = "converter:";
* extensions, so each one still needs `internal/core/format.go` read and the
* extension probed before a row is added.
*
* 3.24.0 → 3.26.0 LEARNED FOUR SPELLINGS, AND THE UPGRADE REPORT POINTED AT
* THE WRONG FILES. It named four readers added under `internal/lint/`, and
* none of them is a format: `code_js.go`, `code_ts.go` and
* `native_stubs_js.go` split the existing code and converter handlers by
* build tag for the new WebAssembly build, and `built.go` lints a converted
* format from HTML a JavaScript host supplies, which only that build reaches.
* The native CLI routes exactly as before through all four. The learned
* formats came from a two-line edit to `internal/core/format.go` (upstream
* a07aa34c), which no added file announces: `.mjs` and `.cjs` now route to
* `.js`, and `.mts` and `.cts` to `.ts`, so all four left the unnamed
* plaintext fallback for the `comment` tier and are new rows below. The
* benign narrowing again: measured on both binaries, a rule matching
* `[*.mjs]` fired on `const simply = 1` through 3.24.0 and reads only the
* comment on 3.26.0. Read the `format.go` diff on every bump, not only the
* added files.
*
* 3.22.0 → 3.23.0 LEARNED ONE FORMAT, AND NO TEST NOTICED UNTIL IT WAS
* PROBED. Every existing row was re-probed against the 3.23.0 binary and none
* moved, so the contract suite was green. The source check is what shows the
Expand Down Expand Up @@ -406,10 +422,12 @@ export const VALE_FORMAT_TIERS: Readonly<Record<string, ValeFormatTier>> = {
".c++": "comment",
".bsh": "comment",
".cc": "comment",
".cjs": "comment",
".clj": "comment",
".cpp": "comment",
".cs": "comment",
".css": "comment",
".cts": "comment",
".csx": "comment",
".cxx": "comment",
".ex": "comment",
Expand All @@ -427,6 +445,8 @@ export const VALE_FORMAT_TIERS: Readonly<Record<string, ValeFormatTier>> = {
".kts": "comment",
".less": "comment",
".lua": "comment",
".mjs": "comment",
".mts": "comment",
".php": "comment",
".pl": "comment",
".pm": "comment",
Expand Down
Loading
Loading