Skip to content

Commit 5087ac6

Browse files
fix(lint): validate-expressions script tombstone uses house os migrate meta sentence (#7030) (#7108)
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. Claude-Session: https://claude.ai/code/session_01F8q5J1MQyocgtNspb15fSn Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3e8e669 commit 5087ac6

4 files changed

Lines changed: 114 additions & 19 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
"@objectstack/lint": patch
3+
"@objectstack/spec": patch
4+
---
5+
6+
fix(lint): script-node retired-key diagnostic no longer says "rewrite it" (#7030)
7+
8+
`validate-expressions`'s lint diagnostic for a `script` node carrying a retired
9+
dispatch key (`config.actionType` / `template` / `recipients` / `variables` /
10+
`script`, retired in `@objectstack/spec` 17, #4343) closed with `Run \`os
11+
migrate meta --from 16\` to rewrite it automatically.` For the
12+
`template`/`recipients`/`variables`/`script` branches the value is **deleted**,
13+
not rewritten into anything, so "rewrite **it**" named the wrong antecedent —
14+
the same false-antecedent shape #6856 (route D, maintainer-ruled) already swept
15+
out of every `packages/spec/src` tombstone. This was the one live site the
16+
sweep's scan surface (`packages/spec/src` only) could not see.
17+
18+
The sentence now reads `Run \`os migrate meta --from 16\` to rewrite existing
19+
sources automatically.` — naming a property of the TOOL (it rewrites your
20+
source files), never the retired key's fate, which the message body already
21+
states per branch. Message copy only: the diagnostic still fires on the same
22+
inputs, at the same severity, with the same `#4343` / per-key / replacement
23+
guidance untouched.
24+
25+
`packages/spec/src/shared/retired-key-migrate-sentence.test.ts` — the #6856
26+
class pin — is widened to scan `packages/lint/src` alongside `packages/spec/src`
27+
so this sentence cannot drift from the house form again, in either package.
28+
That widening is test-only (no `@objectstack/spec` runtime code changed);
29+
listed here only because the pin's own file lives inside `packages/spec`.

packages/lint/src/validate-expressions.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,16 @@ describe('validateStackExpressions (ADR-0032 build-time)', () => {
144144
expect(issues[0].message).toMatch(/config\.template/);
145145
expect(issues[0].message).toMatch(/`notify` node/);
146146
expect(issues[0].message).toMatch(/os migrate meta --from 16/);
147+
// #7030 — house sentence (#6856 route D): names the TOOL's behaviour, never
148+
// the retired key's fate. This branch can DELETE the key outright
149+
// (`template`/`recipients`/`variables`/`script`), so "rewrite it" reads two
150+
// ways ("it" = the key vs. "it" = your sources) while "rewrite existing
151+
// sources" has one antecedent. Pinned here AND class-wide in
152+
// `retired-key-migrate-sentence.test.ts` (widened to `packages/lint/src`).
153+
expect(issues[0].message).toMatch(
154+
/Run `os migrate meta --from 16` to rewrite existing sources automatically\.$/,
155+
);
156+
expect(issues[0].message).not.toMatch(/rewrite it automatically/);
147157
});
148158

149159
it('tells a shorthand actionType exactly where its name belongs', () => {

packages/lint/src/validate-expressions.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -768,7 +768,14 @@ export function validateStackExpressions(stack: AnyRec): ExprIssue[] {
768768
? `\`actionType: '${action}'\` named a registered function — move it to \`function: '${action}'\`. `
769769
: `Use a \`notify\` node for mail, a \`connector_action\` (Slack connector) or \`http\` node ` +
770770
`for Slack, and a registered function for logic. `) +
771-
`Run \`os migrate meta --from 16\` to rewrite it automatically.`,
771+
// #6856 route D (maintainer-ruled): the house sentence names the TOOL's
772+
// behaviour, never the retired key's fate — "rewrite it" reads two ways
773+
// over a branch that DELETES the key (template/recipients/variables/script),
774+
// "rewrite existing sources" only one. Plain-quoted (not a template literal)
775+
// so this site is a member of `retired-key-migrate-sentence.test.ts`'s
776+
// widened scan (#7030) on the same textual shape as the spec corpus — no
777+
// interpolation lives in this clause, so nothing is lost switching quote style.
778+
'Run `os migrate meta --from 16` to rewrite existing sources automatically.',
772779
source: JSON.stringify({ id: node.id, type: node.type, config: cfg }),
773780
});
774781
} else if (!fn) {

packages/spec/src/shared/retired-key-migrate-sentence.test.ts

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,25 @@
2121
* rest>." (`ui/dashboard.zod.ts` `compareTo.offset` is the model; the script
2222
* node's `config.actionType` is the other member.)
2323
*
24+
* [#7030] Widened, not duplicated: `packages/lint/src/validate-expressions.ts`
25+
* carries one live occurrence of the identical sentence (the lint diagnostic
26+
* for a script node's retired dispatch keys — same #6856 ruling, same
27+
* false-antecedent risk, since that branch too can DELETE the key rather than
28+
* rewrite it into anything). `judgeMigrateSentences` is a plain text scan with
29+
* no dependency on `retiredKey()` or on anything `packages/spec`-specific, so
30+
* this pin's INPUT (the CORPORA it walks) widens for free — the matching
31+
* mechanism below is unchanged. A second, standalone pin over that one lint
32+
* site could only drift from this one the moment either wording changes;
33+
* one pin covering both corpora cannot.
34+
*
2435
* Mechanism: a SOURCE scan over string literals (this pin pins textual facts —
2536
* the sentences ARE text in source). Comment lines are skipped: descriptive
2637
* prose about the tool ("`os migrate meta` rewrites sources") is not a
2738
* prescription. `migrations/registry.ts` is out of scope structurally — it is
2839
* the migration LEDGER, whose `notes` are release prose over whole migrations,
2940
* not tombstone prescriptions an author meets in a parse error. That is a
30-
* scope bound on the corpus, not a per-site exemption: every prescription
31-
* string in every schema file is judged, with no allowlist.
41+
* scope bound on the spec corpus, not a per-site exemption: every
42+
* prescription string in every scanned file is judged, with no allowlist.
3243
*
3344
* What this pin deliberately does NOT check: a tombstone whose prescription
3445
* carries no `os migrate meta` sentence at all (#6914's worklist) — absence of
@@ -43,10 +54,33 @@ import url from 'node:url';
4354
import { describe, expect, it } from 'vitest';
4455

4556
const HERE = path.dirname(url.fileURLToPath(import.meta.url));
46-
const SRC_ROOT = path.resolve(HERE, '..');
57+
const SPEC_SRC_ROOT = path.resolve(HERE, '..');
58+
/** #7030: `packages/lint/src`, the one other corpus carrying this sentence. */
59+
const LINT_SRC_ROOT = path.resolve(HERE, '../../../lint/src');
60+
61+
/** One scanned corpus: a root directory, plus its own out-of-scope exemptions. */
62+
interface Corpus {
63+
/** Short label, used as the `file` prefix on judged sites (e.g. `spec:`, `lint:`). */
64+
name: string;
65+
root: string;
66+
/** Paths relative to `root` that are structurally out of scope (see module doc). */
67+
outOfScope: Set<string>;
68+
}
4769

48-
/** The migration ledger — release prose, not tombstone prescriptions (see module doc). */
49-
const OUT_OF_SCOPE = new Set([path.join('migrations', 'registry.ts')]);
70+
const CORPORA: Corpus[] = [
71+
{
72+
name: 'spec',
73+
root: SPEC_SRC_ROOT,
74+
// The migration ledger — release prose, not tombstone prescriptions (see module doc).
75+
outOfScope: new Set([path.join('migrations', 'registry.ts')]),
76+
},
77+
{
78+
// #7030: `validate-expressions.ts`'s script-node lint diagnostic is the only site.
79+
name: 'lint',
80+
root: LINT_SRC_ROOT,
81+
outOfScope: new Set(),
82+
},
83+
];
5084

5185
const MARKER = /(?:Run )?`os migrate meta --from \d+`/g;
5286

@@ -66,7 +100,7 @@ const MIXED_AT_MARKER =
66100
/^Run `os migrate meta --from \d+` to rewrite the [^;'"]+ case[^;'"]* automatically; [^;'"]+\.['"]/;
67101

68102
interface JudgedSite {
69-
/** Path relative to `packages/spec/src`. */
103+
/** Corpus-prefixed path, e.g. `spec:data/object.zod.ts` or `lint:validate-expressions.ts`. */
70104
file: string;
71105
/** 1-based line of the sentence's marker (best effort across concatenation). */
72106
line: number;
@@ -129,16 +163,18 @@ function* walk(dir: string): Generator<string> {
129163

130164
function judgeTree(): JudgedSite[] {
131165
const all: JudgedSite[] = [];
132-
for (const file of walk(SRC_ROOT)) {
133-
const rel = path.relative(SRC_ROOT, file);
134-
if (OUT_OF_SCOPE.has(rel)) continue;
135-
all.push(...judgeMigrateSentences(fs.readFileSync(file, 'utf8'), rel));
166+
for (const corpus of CORPORA) {
167+
for (const file of walk(corpus.root)) {
168+
const rel = path.relative(corpus.root, file);
169+
if (corpus.outOfScope.has(rel)) continue;
170+
all.push(...judgeMigrateSentences(fs.readFileSync(file, 'utf8'), `${corpus.name}:${rel}`));
171+
}
136172
}
137173
return all;
138174
}
139175

140-
describe('retiredKey() `os migrate meta` sentences are the house sentence (#6856 route D)', () => {
141-
it('every prescription sentence in packages/spec/src is house-form or MIXED two-clause', () => {
176+
describe('`os migrate meta` sentences are the house sentence, across corpora (#6856 route D, widened #7030)', () => {
177+
it('every prescription sentence in packages/spec/src and packages/lint/src is house-form or MIXED two-clause', () => {
142178
const judged = judgeTree();
143179
const violations = judged.filter((j) => !j.ok);
144180
expect(
@@ -149,17 +185,30 @@ describe('retiredKey() `os migrate meta` sentences are the house sentence (#6856
149185
).toEqual([]);
150186
});
151187

152-
it('anti-vacuity: the scanner actually judges the corpus (floor, not a census)', () => {
153-
// 54 prescription sentences at the time of the sweep. The floor guards
154-
// against the SCANNER going blind (a regex or comment-filter regression
155-
// reporting an empty corpus as green), not against tombstones aging out —
156-
// lower it deliberately, with the removal that shrinks the corpus, when
157-
// that day comes. #6914's 35 pending sentences will only raise the count.
188+
it('anti-vacuity: the scanner actually judges every corpus (floor, not a census)', () => {
189+
// 54 prescription sentences in packages/spec/src at the time of the #6856
190+
// sweep; packages/lint/src contributes one more under #7030's widened
191+
// scan. The floor guards against a SCANNER going blind on either corpus (a
192+
// regex or comment-filter regression reporting an empty tree as green),
193+
// not against tombstones aging out — lower it deliberately, with the
194+
// removal that shrinks a corpus, when that day comes. #6914's 35 pending
195+
// sentences will only raise the count further.
158196
const judged = judgeTree();
159197
expect(judged.length).toBeGreaterThanOrEqual(50);
160198
expect(judged.every((j) => j.ok)).toBe(true);
161199
});
162200

201+
it('anti-vacuity: the lint corpus specifically is reached, not just outnumbered by spec', () => {
202+
// The combined floor above (>=50) is already satisfied by packages/spec/src
203+
// alone, so a broken LINT_SRC_ROOT (wrong relative path, corpus silently
204+
// walking zero files) would NOT fail it — this assertion is the one thing
205+
// that actually exercises #7030's widening rather than merely declaring it.
206+
const judged = judgeTree();
207+
const lintSites = judged.filter((j) => j.file.startsWith('lint:'));
208+
expect(lintSites.length).toBeGreaterThanOrEqual(1);
209+
expect(lintSites.every((j) => j.ok)).toBe(true);
210+
});
211+
163212
it('goes RED on the retired "rewrite it" spelling, naming the site', () => {
164213
const planted = [
165214
"const X = retiredKey(",

0 commit comments

Comments
 (0)