From d32227f56707bb649b2c18abebff428862852647 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 16:12:48 +0000 Subject: [PATCH] fix(lint): validate-expressions script tombstone uses house `os migrate meta` sentence (#7030) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The script-node retired-dispatch-key lint diagnostic (`packages/lint/src/validate-expressions.ts`) closed with `Run \`os migrate meta --from 16\` to rewrite it automatically.` — the false-antecedent phrasing #6856 (route D, maintainer-ruled) already swept out of every `packages/spec/src` tombstone. This is the one live site that sweep's scan surface (spec-only) could not see: the branch can DELETE the key outright (`template`/`recipients`/`variables`/`script`), so "rewrite it" reads two ways. Sentence now reads "to rewrite existing sources automatically." — a property of the tool, not the key's fate (which the message body already states per key). Message copy only, no behaviour change. `retired-key-migrate-sentence.test.ts` (the #6856 class pin) is widened to scan `packages/lint/src` alongside `packages/spec/src` so the two corpora share one pin instead of drifting apart under two. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01F8q5J1MQyocgtNspb15fSn --- .../lint-script-retired-key-house-sentence.md | 29 +++++++ .../lint/src/validate-expressions.test.ts | 10 +++ packages/lint/src/validate-expressions.ts | 9 +- .../retired-key-migrate-sentence.test.ts | 85 +++++++++++++++---- 4 files changed, 114 insertions(+), 19 deletions(-) create mode 100644 .changeset/lint-script-retired-key-house-sentence.md diff --git a/.changeset/lint-script-retired-key-house-sentence.md b/.changeset/lint-script-retired-key-house-sentence.md new file mode 100644 index 0000000000..1a02aa6c56 --- /dev/null +++ b/.changeset/lint-script-retired-key-house-sentence.md @@ -0,0 +1,29 @@ +--- +"@objectstack/lint": patch +"@objectstack/spec": patch +--- + +fix(lint): script-node retired-key diagnostic no longer says "rewrite it" (#7030) + +`validate-expressions`'s lint diagnostic for a `script` node carrying a retired +dispatch key (`config.actionType` / `template` / `recipients` / `variables` / +`script`, retired in `@objectstack/spec` 17, #4343) closed with `Run \`os +migrate meta --from 16\` to rewrite it automatically.` For the +`template`/`recipients`/`variables`/`script` branches the value is **deleted**, +not rewritten into anything, so "rewrite **it**" named the wrong antecedent — +the same false-antecedent shape #6856 (route D, maintainer-ruled) already swept +out of every `packages/spec/src` tombstone. This was the one live site the +sweep's scan surface (`packages/spec/src` only) could not see. + +The sentence now reads `Run \`os migrate meta --from 16\` to rewrite existing +sources automatically.` — naming a property of the TOOL (it rewrites your +source files), never the retired key's fate, which the message body already +states per branch. Message copy only: the diagnostic still fires on the same +inputs, at the same severity, with the same `#4343` / per-key / replacement +guidance untouched. + +`packages/spec/src/shared/retired-key-migrate-sentence.test.ts` — the #6856 +class pin — is widened to scan `packages/lint/src` alongside `packages/spec/src` +so this sentence cannot drift from the house form again, in either package. +That widening is test-only (no `@objectstack/spec` runtime code changed); +listed here only because the pin's own file lives inside `packages/spec`. diff --git a/packages/lint/src/validate-expressions.test.ts b/packages/lint/src/validate-expressions.test.ts index 8bde2dc508..496fdc4a73 100644 --- a/packages/lint/src/validate-expressions.test.ts +++ b/packages/lint/src/validate-expressions.test.ts @@ -144,6 +144,16 @@ describe('validateStackExpressions (ADR-0032 build-time)', () => { expect(issues[0].message).toMatch(/config\.template/); expect(issues[0].message).toMatch(/`notify` node/); expect(issues[0].message).toMatch(/os migrate meta --from 16/); + // #7030 — house sentence (#6856 route D): names the TOOL's behaviour, never + // the retired key's fate. This branch can DELETE the key outright + // (`template`/`recipients`/`variables`/`script`), so "rewrite it" reads two + // ways ("it" = the key vs. "it" = your sources) while "rewrite existing + // sources" has one antecedent. Pinned here AND class-wide in + // `retired-key-migrate-sentence.test.ts` (widened to `packages/lint/src`). + expect(issues[0].message).toMatch( + /Run `os migrate meta --from 16` to rewrite existing sources automatically\.$/, + ); + expect(issues[0].message).not.toMatch(/rewrite it automatically/); }); it('tells a shorthand actionType exactly where its name belongs', () => { diff --git a/packages/lint/src/validate-expressions.ts b/packages/lint/src/validate-expressions.ts index fcf28353cd..c3f045f0c0 100644 --- a/packages/lint/src/validate-expressions.ts +++ b/packages/lint/src/validate-expressions.ts @@ -768,7 +768,14 @@ export function validateStackExpressions(stack: AnyRec): ExprIssue[] { ? `\`actionType: '${action}'\` named a registered function — move it to \`function: '${action}'\`. ` : `Use a \`notify\` node for mail, a \`connector_action\` (Slack connector) or \`http\` node ` + `for Slack, and a registered function for logic. `) + - `Run \`os migrate meta --from 16\` to rewrite it automatically.`, + // #6856 route D (maintainer-ruled): the house sentence names the TOOL's + // behaviour, never the retired key's fate — "rewrite it" reads two ways + // over a branch that DELETES the key (template/recipients/variables/script), + // "rewrite existing sources" only one. Plain-quoted (not a template literal) + // so this site is a member of `retired-key-migrate-sentence.test.ts`'s + // widened scan (#7030) on the same textual shape as the spec corpus — no + // interpolation lives in this clause, so nothing is lost switching quote style. + 'Run `os migrate meta --from 16` to rewrite existing sources automatically.', source: JSON.stringify({ id: node.id, type: node.type, config: cfg }), }); } else if (!fn) { diff --git a/packages/spec/src/shared/retired-key-migrate-sentence.test.ts b/packages/spec/src/shared/retired-key-migrate-sentence.test.ts index 48a33d892c..98007fd46e 100644 --- a/packages/spec/src/shared/retired-key-migrate-sentence.test.ts +++ b/packages/spec/src/shared/retired-key-migrate-sentence.test.ts @@ -21,14 +21,25 @@ * rest>." (`ui/dashboard.zod.ts` `compareTo.offset` is the model; the script * node's `config.actionType` is the other member.) * + * [#7030] Widened, not duplicated: `packages/lint/src/validate-expressions.ts` + * carries one live occurrence of the identical sentence (the lint diagnostic + * for a script node's retired dispatch keys — same #6856 ruling, same + * false-antecedent risk, since that branch too can DELETE the key rather than + * rewrite it into anything). `judgeMigrateSentences` is a plain text scan with + * no dependency on `retiredKey()` or on anything `packages/spec`-specific, so + * this pin's INPUT (the CORPORA it walks) widens for free — the matching + * mechanism below is unchanged. A second, standalone pin over that one lint + * site could only drift from this one the moment either wording changes; + * one pin covering both corpora cannot. + * * Mechanism: a SOURCE scan over string literals (this pin pins textual facts — * the sentences ARE text in source). Comment lines are skipped: descriptive * prose about the tool ("`os migrate meta` rewrites sources") is not a * prescription. `migrations/registry.ts` is out of scope structurally — it is * the migration LEDGER, whose `notes` are release prose over whole migrations, * not tombstone prescriptions an author meets in a parse error. That is a - * scope bound on the corpus, not a per-site exemption: every prescription - * string in every schema file is judged, with no allowlist. + * scope bound on the spec corpus, not a per-site exemption: every + * prescription string in every scanned file is judged, with no allowlist. * * What this pin deliberately does NOT check: a tombstone whose prescription * carries no `os migrate meta` sentence at all (#6914's worklist) — absence of @@ -43,10 +54,33 @@ import url from 'node:url'; import { describe, expect, it } from 'vitest'; const HERE = path.dirname(url.fileURLToPath(import.meta.url)); -const SRC_ROOT = path.resolve(HERE, '..'); +const SPEC_SRC_ROOT = path.resolve(HERE, '..'); +/** #7030: `packages/lint/src`, the one other corpus carrying this sentence. */ +const LINT_SRC_ROOT = path.resolve(HERE, '../../../lint/src'); + +/** One scanned corpus: a root directory, plus its own out-of-scope exemptions. */ +interface Corpus { + /** Short label, used as the `file` prefix on judged sites (e.g. `spec:`, `lint:`). */ + name: string; + root: string; + /** Paths relative to `root` that are structurally out of scope (see module doc). */ + outOfScope: Set; +} -/** The migration ledger — release prose, not tombstone prescriptions (see module doc). */ -const OUT_OF_SCOPE = new Set([path.join('migrations', 'registry.ts')]); +const CORPORA: Corpus[] = [ + { + name: 'spec', + root: SPEC_SRC_ROOT, + // The migration ledger — release prose, not tombstone prescriptions (see module doc). + outOfScope: new Set([path.join('migrations', 'registry.ts')]), + }, + { + // #7030: `validate-expressions.ts`'s script-node lint diagnostic is the only site. + name: 'lint', + root: LINT_SRC_ROOT, + outOfScope: new Set(), + }, +]; const MARKER = /(?:Run )?`os migrate meta --from \d+`/g; @@ -66,7 +100,7 @@ const MIXED_AT_MARKER = /^Run `os migrate meta --from \d+` to rewrite the [^;'"]+ case[^;'"]* automatically; [^;'"]+\.['"]/; interface JudgedSite { - /** Path relative to `packages/spec/src`. */ + /** Corpus-prefixed path, e.g. `spec:data/object.zod.ts` or `lint:validate-expressions.ts`. */ file: string; /** 1-based line of the sentence's marker (best effort across concatenation). */ line: number; @@ -129,16 +163,18 @@ function* walk(dir: string): Generator { function judgeTree(): JudgedSite[] { const all: JudgedSite[] = []; - for (const file of walk(SRC_ROOT)) { - const rel = path.relative(SRC_ROOT, file); - if (OUT_OF_SCOPE.has(rel)) continue; - all.push(...judgeMigrateSentences(fs.readFileSync(file, 'utf8'), rel)); + for (const corpus of CORPORA) { + for (const file of walk(corpus.root)) { + const rel = path.relative(corpus.root, file); + if (corpus.outOfScope.has(rel)) continue; + all.push(...judgeMigrateSentences(fs.readFileSync(file, 'utf8'), `${corpus.name}:${rel}`)); + } } return all; } -describe('retiredKey() `os migrate meta` sentences are the house sentence (#6856 route D)', () => { - it('every prescription sentence in packages/spec/src is house-form or MIXED two-clause', () => { +describe('`os migrate meta` sentences are the house sentence, across corpora (#6856 route D, widened #7030)', () => { + it('every prescription sentence in packages/spec/src and packages/lint/src is house-form or MIXED two-clause', () => { const judged = judgeTree(); const violations = judged.filter((j) => !j.ok); expect( @@ -149,17 +185,30 @@ describe('retiredKey() `os migrate meta` sentences are the house sentence (#6856 ).toEqual([]); }); - it('anti-vacuity: the scanner actually judges the corpus (floor, not a census)', () => { - // 54 prescription sentences at the time of the sweep. The floor guards - // against the SCANNER going blind (a regex or comment-filter regression - // reporting an empty corpus as green), not against tombstones aging out — - // lower it deliberately, with the removal that shrinks the corpus, when - // that day comes. #6914's 35 pending sentences will only raise the count. + it('anti-vacuity: the scanner actually judges every corpus (floor, not a census)', () => { + // 54 prescription sentences in packages/spec/src at the time of the #6856 + // sweep; packages/lint/src contributes one more under #7030's widened + // scan. The floor guards against a SCANNER going blind on either corpus (a + // regex or comment-filter regression reporting an empty tree as green), + // not against tombstones aging out — lower it deliberately, with the + // removal that shrinks a corpus, when that day comes. #6914's 35 pending + // sentences will only raise the count further. const judged = judgeTree(); expect(judged.length).toBeGreaterThanOrEqual(50); expect(judged.every((j) => j.ok)).toBe(true); }); + it('anti-vacuity: the lint corpus specifically is reached, not just outnumbered by spec', () => { + // The combined floor above (>=50) is already satisfied by packages/spec/src + // alone, so a broken LINT_SRC_ROOT (wrong relative path, corpus silently + // walking zero files) would NOT fail it — this assertion is the one thing + // that actually exercises #7030's widening rather than merely declaring it. + const judged = judgeTree(); + const lintSites = judged.filter((j) => j.file.startsWith('lint:')); + expect(lintSites.length).toBeGreaterThanOrEqual(1); + expect(lintSites.every((j) => j.ok)).toBe(true); + }); + it('goes RED on the retired "rewrite it" spelling, naming the site', () => { const planted = [ "const X = retiredKey(",