diff --git a/packages/cloud-connection/src/cloud-connection-route-ledger.conformance.test.ts b/packages/cloud-connection/src/cloud-connection-route-ledger.conformance.test.ts index 564468f8e3..67ff461891 100644 --- a/packages/cloud-connection/src/cloud-connection-route-ledger.conformance.test.ts +++ b/packages/cloud-connection/src/cloud-connection-route-ledger.conformance.test.ts @@ -53,6 +53,13 @@ import { readdirSync, readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, it, expect } from 'vitest'; +// The repo's ONE answer to "is this span a comment, or code?" — its header +// carries the two private-stripper families that drifted apart and the +// parser-differential sweep that measured which way each fails. This package +// already imports the module next door in `canonical-expression-envelopes.test.ts`. +// `stripComments` (not `maskComments`) is the projection this file wants: every +// finding here reports a `file:line` or a bare file name, never an offset. +import { stripComments } from '../../../scripts/js-comment-mask.mjs'; import { CLOUD_CONNECTION_ROUTE_LEDGER } from './cloud-connection-route-ledger.js'; /** @@ -121,63 +128,28 @@ const DECLARED_COMPUTED_MOUNTS = [ // --------------------------------------------------------------------------- /** - * Strip comments before scanning. Prose cannot mount a route, and this - * package's headers quote every wire path they serve — so a raw-text scan would - * report a documented path as an unledgered mount, a false red on an accurate - * package. The three string forms are tracked so a literal CONTAINING comment - * punctuation (`'/api/v1/x/*'`, `'https://host'`) is never mistaken for a - * comment opener; `comment-stripper` below pins both directions, because a - * stripper that swallowed real code would make this scan silently blind, which - * is the failure that actually matters here. + * WHY COMMENTS ARE REMOVED BEFORE ANY SCAN HERE. Prose cannot mount a route and + * cannot reach for a host app, and this package's headers quote every wire path + * they serve — a raw-text scan reports a documented path as an unledgered mount + * and a documented `getRawApp()` as a second reacher: a false red on an + * accurate package. + * + * This file used to answer that question with its own character scanner. It was + * converted to `scripts/js-comment-mask.mjs` (#12398), the tree's one answer to + * it, and the swap was MEASURED rather than assumed: over this package's 13 + * scanned source files the two differ on exactly one, + * `marketplace-proxy-plugin.ts`, where the private scanner read the `//` inside + * the regex literal `/\/packages\/[^/]+\/versions\//` as a line-comment opener + * and deleted the 38 characters of REAL CODE that followed it to end of line — + * inside a declared MOUNT SOURCE, which is source this census reads. That is + * the naive-`//` family the shared module's header measures, live here. + * + * `stripComments` keeps line numbers (block-comment newlines survive — this + * package's headers run to eighty lines and every finding quotes `file:line`) + * and keeps string, template and regex literals INTACT, which is what lets the + * census resolve wire paths out of them at all. `scan machinery` at the foot of + * this file pins both directions. */ -export function stripComments(source: string): string { - let out = ''; - let i = 0; - while (i < source.length) { - const c = source[i]; - const next = source[i + 1]; - if (c === '/' && next === '/') { - while (i < source.length && source[i] !== '\n') i++; - continue; - } - if (c === '/' && next === '*') { - i += 2; - // Newlines inside the block are PRESERVED. Line numbers are what - // every finding below points a reader at, and this package's - // headers run to eighty lines — swallowing them would send someone - // to a line eighty short of the mount, which reads as a wrong - // report rather than as the accurate one it is. - while (i < source.length && !(source[i] === '*' && source[i + 1] === '/')) { - if (source[i] === '\n') out += '\n'; - i++; - } - i += 2; - continue; - } - if (c === '\'' || c === '"' || c === '`') { - const quote = c; - out += c; - i++; - while (i < source.length) { - if (source[i] === '\\') { - out += source.slice(i, i + 2); - i += 2; - continue; - } - out += source[i]; - if (source[i] === quote) { - i++; - break; - } - i++; - } - continue; - } - out += c; - i++; - } - return out; -} /** Module-scope `const NAME = '';` bindings, for resolving mount paths. */ export function constantBindings(code: string): Map { @@ -293,6 +265,30 @@ function packageSourceFiles(): string[] { /** The spellings by which a module in this package reaches the HOST app. */ const HOST_APP_REACH = /getRawApp|['"`]http-server['"`]|['"`]http\.server['"`]/; +/** + * Does `source` reach for the host app IN CODE? + * + * COMMENTS ARE REMOVED FIRST, and that half is the whole of #12398. A docblock + * explaining why a mount sits where it does — "the mount takes the + * framework-native handle through `IHttpServer.getRawApp()`" — is prose, and + * prose reaches for nothing. Scanned raw it scored as an extra reacher and + * failed an IDENTITY assertion by naming a file that reaches for nothing, whose + * own failure text then invites the wrong repair: widening the expected list, + * which retires the only property the assertion has. + * + * STRING, TEMPLATE AND REGEX LITERALS ARE LEFT INTACT, and that half is what + * keeps the fix from being a silent disarm. Two of the three spellings above + * ARE string literals — `ctx.getService('http.server')` reaches for the host + * app entirely inside quotes — so a probe that masked literals as well as + * comments would detect nothing and this identity would pass vacuously. Both + * directions are pinned in `scan machinery` at the foot of this file. + */ +const reachesHostApp = (source: string): boolean => HOST_APP_REACH.test(stripComments(source)); + +/** Which of `files` reach for the host app in code. Driveable for the pins. */ +const filesReachingHostApp = (files: readonly string[], read: (f: string) => string): string[] => + files.filter((f) => reachesHostApp(read(f))); + const ledgerRoutes = (): Set => new Set(CLOUD_CONNECTION_ROUTE_LEDGER.map((e) => e.route)); const liveCensus = (): Census => censusOf(MOUNT_SOURCES, readSource); @@ -369,7 +365,7 @@ describe('cloud-connection mount population', () => { // An IDENTITY, not a count: the day a FIFTH module in this package // resolves `http-server` or calls `getRawApp()`, this names it — and // the census above, which only reads MOUNT_SOURCES, would not have. - const reaching = packageSourceFiles().filter((f) => HOST_APP_REACH.test(readSource(f))); + const reaching = filesReachingHostApp(packageSourceFiles(), readSource); expect( reaching, 'files reaching for the host HTTP app. A registrar not listed in MOUNT_SOURCES is invisible ' @@ -475,6 +471,38 @@ describe('scan machinery, pinned in both directions', () => { expect(census.routes[0].line).toBe(4); }); + it('the host-app reach probe does not count PROSE — the #12398 false positive', () => { + // The exact docblock shape that fired it: a module explaining that the + // mount takes the framework-native handle, in a comment. + expect(reachesHostApp('// the mount takes the handle through `IHttpServer.getRawApp()`\n')).toBe(false); + expect(reachesHostApp("/*\n * resolves 'http-server' before mounting\n */\n")).toBe(false); + expect(reachesHostApp("/* the ctx.getService('http.server') seam, explained */\n")).toBe(false); + }); + + it('the host-app reach probe still counts a REACH THAT LIVES IN A STRING', () => { + // The direction that makes the fix a fix rather than a disarm: two of + // the three spellings are service keys, which are string literals. + expect(reachesHostApp("const s = ctx.getService('http.server');\n")).toBe(true); + expect(reachesHostApp('const s = ctx.getService("http-server");\n')).toBe(true); + expect(reachesHostApp('const s = ctx.getService(`http-server`);\n')).toBe(true); + expect(reachesHostApp('const app = server.getRawApp();\n')).toBe(true); + }); + + it('a genuine FIFTH reacher is still named — anti-vacuity on the identity limb', () => { + // LOAD-BEARING POSITIVE for #12398's fix, driven through the same + // function the live limb calls with source injected. Without it, a + // strip that quietly stopped matching anything would leave the identity + // green forever — the failure direction the whole family distrusts. + const fake: Record = { + 'cloud-connection-plugin.ts': 'const app = http.getRawApp();\n', + 'prose-only.ts': '// getRawApp() is reached in the four mount sources, never here\n', + 'zzz-fifth-reacher.ts': "const s = ctx.getService('http.server');\n", + }; + expect( + filesReachingHostApp(Object.keys(fake).sort(), (f) => fake[f]), + ).toEqual(['cloud-connection-plugin.ts', 'zzz-fifth-reacher.ts']); + }); + it('resolves the three argument spellings this package actually uses', () => { const constants = new Map([['ROUTE_BASE', '/api/v1/marketplace/install-local'], ['P', '/api/v1/cloud-connection']]); // bare identifier — marketplace-install-local-plugin.ts diff --git a/packages/metadata/src/metadata-route-ledger.conformance.test.ts b/packages/metadata/src/metadata-route-ledger.conformance.test.ts index d166c09a66..4cb31da4f8 100644 --- a/packages/metadata/src/metadata-route-ledger.conformance.test.ts +++ b/packages/metadata/src/metadata-route-ledger.conformance.test.ts @@ -31,6 +31,14 @@ import { readdirSync, readFileSync, statSync } from 'node:fs'; import { dirname, join, relative, sep } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, it, expect } from 'vitest'; +// The repo's ONE answer to "is this span a comment, or code?" — see its header +// for the two private-stripper families that drifted apart and why neither was +// safe. `stripComments` (not `maskComments`) is the projection this file wants: +// every finding here reports a `file:line` or a bare file name, never an +// offset, and the module's own guidance is to pick by what the caller reports. +// The `.mjs` specifier is deliberate; `scripts/js-comment-mask.d.mts` beside it +// is a hand-written declaration, so this import needs no `allowJs`. +import { stripComments } from '../../../scripts/js-comment-mask.mjs'; import { METADATA_ROUTE_LEDGER } from './metadata-route-ledger.js'; /** @@ -60,49 +68,26 @@ const NON_ROUTE_MEMBERS = new Set(['use', 'notFound', 'onError', 'fire', 'fetch' // --------------------------------------------------------------------------- /** - * Strip comments before scanning. Prose cannot mount a route, and this - * package's headers quote the wire paths they serve — a raw-text scan would - * report a documented path as an unledgered mount. Newlines inside block - * comments are PRESERVED so every finding's `file:line` points at the real - * line; `hmr-routes.ts` opens with a 27-line header, so swallowing them would - * report the mount 27 lines short of where it is. + * WHY COMMENTS ARE REMOVED BEFORE ANY SCAN HERE. Prose cannot mount a route and + * cannot reach for a host app, and this package's headers quote the wire paths + * and the handles they serve — a raw-text scan reports a documented path as an + * unledgered mount, and a documented `getRawApp()` as a second reacher. + * + * This file used to answer that question with its own character scanner. It was + * converted to `scripts/js-comment-mask.mjs` (#12398), which is the tree's one + * answer to it, and the swap was MEASURED rather than assumed: over this + * package's 29 scanned source files the two differ on exactly one, `plugin.ts`, + * where the private scanner read the `//` inside the regex literal + * `/^https?:\/\//i` as a line-comment opener and deleted the 40 characters of + * REAL CODE that followed it to end of line. That is the naive-`//` family the + * shared module's header measures, live in the very file this guard's identity + * limb pins. + * + * `stripComments` keeps line numbers (block-comment newlines survive) and keeps + * string, template and regex literals INTACT — both properties are load-bearing + * below: `hmr-routes.ts` opens with a 27-line header, and the host-app reach + * this file detects is partly a SERVICE KEY, which is a string literal. */ -export function stripComments(source: string): string { - let out = ''; - let i = 0; - while (i < source.length) { - const c = source[i]; - const next = source[i + 1]; - if (c === '/' && next === '/') { - while (i < source.length && source[i] !== '\n') i++; - continue; - } - if (c === '/' && next === '*') { - i += 2; - while (i < source.length && !(source[i] === '*' && source[i + 1] === '/')) { - if (source[i] === '\n') out += '\n'; - i++; - } - i += 2; - continue; - } - if (c === '\'' || c === '"' || c === '`') { - const quote = c; - out += c; - i++; - while (i < source.length) { - if (source[i] === '\\') { out += source.slice(i, i + 2); i += 2; continue; } - out += source[i]; - if (source[i] === quote) { i++; break; } - i++; - } - continue; - } - out += c; - i++; - } - return out; -} /** * Blank out string CONTENTS, preserving quotes, length and newlines. @@ -258,6 +243,30 @@ function packageSourceFiles(dir = SRC_DIR): string[] { /** The spellings by which a module reaches the HOST app. */ const HOST_APP_REACH = /getRawApp|['"`]http-server['"`]|['"`]http\.server['"`]/; +/** + * Does `source` reach for the host app IN CODE? + * + * COMMENTS ARE REMOVED FIRST, and that half is the whole of #12398. A docblock + * explaining why a mount sits outside the auth seam — "the mount takes the + * framework-native handle through `IHttpServer.getRawApp()`" — is prose, and + * prose reaches for nothing. Scanned raw it scored as a second reacher and + * failed an IDENTITY assertion by naming a file that reaches for nothing, whose + * own failure text then invites the wrong repair: widening the expected list, + * which retires the only property the assertion has. + * + * STRING, TEMPLATE AND REGEX LITERALS ARE LEFT INTACT, and that half is what + * keeps the fix from being a silent disarm. Two of the three spellings above + * ARE string literals — `ctx.getService('http.server')` reaches for the host + * app entirely inside quotes — so the sibling `maskStrings` below must never be + * applied here. Both directions are pinned in `scan machinery` at the foot of + * this file, including a genuine second reacher that lives in a string. + */ +const reachesHostApp = (source: string): boolean => HOST_APP_REACH.test(stripComments(source)); + +/** Which of `files` reach for the host app in code. Driveable for the pins. */ +const filesReachingHostApp = (files: readonly string[], read: (f: string) => string): string[] => + files.filter((f) => reachesHostApp(read(f))); + /** A mount-shaped call on a handle named `app` — how a SECOND registrar would look. */ const MOUNT_SHAPED = /\bapp\s*\.\s*(?:get|post|put|patch|delete|options|head|all)\s*\(/; @@ -315,7 +324,7 @@ describe('metadata mount population', () => { it('plugin.ts is the only file that reaches for the host app', () => { // An IDENTITY, not a count: the day a second module resolves // `http-server` or calls `getRawApp()`, this names it. - const reaching = packageSourceFiles().filter((f) => HOST_APP_REACH.test(readSource(f))); + const reaching = filesReachingHostApp(packageSourceFiles(), readSource); expect( reaching, 'files reaching for the host HTTP app. A second registrar is invisible to the census above — ' @@ -442,6 +451,40 @@ describe('scan machinery, pinned in both directions', () => { expect(masked).toContain('http.getRawApp()'); }); + it('the host-app reach probe does not count PROSE — the #12398 false positive', () => { + // The exact docblock that fired it: a module explaining that the mount + // takes the framework-native handle, in a comment. + expect(reachesHostApp('// the mount takes the handle through `IHttpServer.getRawApp()`\n')).toBe(false); + expect(reachesHostApp("/*\n * resolves 'http-server' before mounting\n */\n")).toBe(false); + expect(reachesHostApp("/* the ctx.getService('http.server') seam, explained */\n")).toBe(false); + }); + + it('the host-app reach probe still counts a REACH THAT LIVES IN A STRING', () => { + // The direction that makes the fix a fix rather than a disarm. Two of + // the three spellings are service keys — string literals — so a probe + // that masked literals as well as comments would detect nothing here + // and the identity limb would pass vacuously forever. + expect(reachesHostApp("const s = ctx.getService('http.server');\n")).toBe(true); + expect(reachesHostApp('const s = ctx.getService("http-server");\n')).toBe(true); + expect(reachesHostApp('const s = ctx.getService(`http-server`);\n')).toBe(true); + expect(reachesHostApp('const app = server.getRawApp();\n')).toBe(true); + }); + + it('a genuine SECOND reacher is still named — anti-vacuity on the identity limb', () => { + // LOAD-BEARING POSITIVE for #12398's fix: driven through the same + // function the live limb calls, with source injected. Without it, a + // strip that quietly stopped matching anything would leave the identity + // green forever — the failure direction the whole family distrusts. + const fake: Record = { + 'plugin.ts': 'const app = http.getRawApp();\n', + 'routes/prose-only.ts': '// getRawApp() is reached in plugin.ts, never here\n', + 'routes/second-reacher.ts': "const s = ctx.getService('http.server');\n", + }; + expect( + filesReachingHostApp(Object.keys(fake).sort(), (f) => fake[f]), + ).toEqual(['plugin.ts', 'routes/second-reacher.ts']); + }); + it('resolves both binding spellings this package uses, and refuses the rest', () => { const b = pathBindings("const routePath = options.path ?? '/api/v1/dev/metadata-events';\nconst FIXED = '/api/v1/fixed';\n"); expect(b.get('routePath')).toBe('/api/v1/dev/metadata-events'); diff --git a/packages/triggers/trigger-api/src/trigger-api-route-ledger.conformance.test.ts b/packages/triggers/trigger-api/src/trigger-api-route-ledger.conformance.test.ts index b068962aab..9fa74c32e1 100644 --- a/packages/triggers/trigger-api/src/trigger-api-route-ledger.conformance.test.ts +++ b/packages/triggers/trigger-api/src/trigger-api-route-ledger.conformance.test.ts @@ -58,6 +58,14 @@ import { readdirSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { describe, it, expect, vi } from 'vitest'; +// The repo's ONE answer to "is this span a comment, or code?" — its header +// carries the two private-stripper families that drifted apart and the +// parser-differential sweep that measured which way each fails. +// `stripComments` (not `maskComments`) is the projection this file wants: every +// finding here reports a `src/: ` or a bare file name, never an +// offset. The `.mjs` specifier is deliberate; `scripts/js-comment-mask.d.mts` +// beside it is a hand-written declaration, so this import needs no `allowJs`. +import { stripComments } from '../../../../scripts/js-comment-mask.mjs'; import { ApiTriggerPlugin } from './plugin.js'; import { TRIGGER_API_ROUTE_LEDGER } from './trigger-api-route-ledger.js'; @@ -204,55 +212,26 @@ function packageSourceFiles(): string[] { } /** - * Strip comments before scanning for path literals. Prose cannot mount a route, - * and this package's own doc comments quote wire paths — so a raw-text scan - * would report a documented path as an unledgered mount, which is a false red - * on an accurate package. The three string forms are tracked so that a literal - * CONTAINING comment punctuation (`'/api/v1/x/*'`, `'http://host'`) is never - * mistaken for a comment opener; `comment-stripper` below pins both directions, - * because a stripper that swallowed real code would make this scan silently - * blind, which is the failure that actually matters here. + * WHY COMMENTS ARE REMOVED BEFORE ANY SCAN HERE. Prose cannot mount a route and + * cannot reach for a host app, and this package's own doc comments quote wire + * paths and handles — a raw-text scan reports a documented path as an + * unledgered mount and a documented `getRawApp()` as a second reacher: a false + * red on an accurate package. + * + * This file used to answer that question with its own character scanner. It was + * converted to `scripts/js-comment-mask.mjs` (#12398), the tree's one answer to + * it. The swap was MEASURED rather than assumed: over this package's three + * scanned source files the two agree on every byte of live code and differ only + * where the private scanner DROPPED block-comment newlines — the shared module + * keeps them, so a `file:line` finding now points at the real line instead of + * one short by the length of the header above it. Nothing this file reports + * moves; the reads it feeds are literal collections, not offsets. + * + * String, template and regex literals are left INTACT, which is what lets the + * path scan below find a wire path at all — and, for the host-app reach probe, + * what keeps a service key from being masked away. `comment-stripper` below + * pins both directions. */ -function stripComments(source: string): string { - let out = ''; - let i = 0; - while (i < source.length) { - const c = source[i]; - const next = source[i + 1]; - if (c === '/' && next === '/') { - while (i < source.length && source[i] !== '\n') i++; - continue; - } - if (c === '/' && next === '*') { - i += 2; - while (i < source.length && !(source[i] === '*' && source[i + 1] === '/')) i++; - i += 2; - continue; - } - if (c === '\'' || c === '"' || c === '`') { - const quote = c; - out += c; - i++; - while (i < source.length) { - if (source[i] === '\\') { - out += source.slice(i, i + 2); - i += 2; - continue; - } - out += source[i]; - if (source[i] === quote) { - i++; - break; - } - i++; - } - continue; - } - out += c; - i++; - } - return out; -} /** Every absolute-path literal in one source file, comments removed. */ function pathLiteralsIn(file: string): string[] { @@ -266,6 +245,29 @@ const ABSOLUTE_PATH_LITERAL = /(['"`])(\/[A-Za-z0-9._~:@-][^'"`\s]*)\1/g; /** The two spellings by which a module in this package reaches the HOST app. */ const HOST_APP_REACH = /getRawApp|['"`]http-server['"`]/; +/** + * Does `source` reach for the host app IN CODE? + * + * COMMENTS ARE REMOVED FIRST, and that half is the whole of #12398. A docblock + * explaining that a mount takes the framework-native handle through + * `IHttpServer.getRawApp()` is prose, and prose reaches for nothing. Scanned + * raw it scored as a second reacher and failed an IDENTITY assertion by naming + * a file that reaches for nothing, whose own failure text then invites the + * wrong repair: widening the expected list, which retires the only property the + * assertion has. + * + * STRING AND TEMPLATE LITERALS ARE LEFT INTACT, and that half is what keeps the + * fix from being a silent disarm: `'http-server'` is a SERVICE KEY, so a probe + * that masked literals as well as comments would detect nothing here and this + * identity would pass vacuously. Both directions are pinned at the foot of this + * file. + */ +const reachesHostApp = (source: string): boolean => HOST_APP_REACH.test(stripComments(source)); + +/** Which of `files` reach for the host app in code. Driveable for the pins. */ +const filesReachingHostApp = (files: readonly string[], read: (f: string) => string): string[] => + files.filter((f) => reachesHostApp(read(f))); + // --------------------------------------------------------------------------- describe('trigger-api route ledger ↔ ApiTriggerPlugin enumeration', () => { @@ -354,8 +356,9 @@ describe('trigger-api mount population (source scan)', () => { // An IDENTITY, not a count: the day a second module resolves // `http-server` or calls `getRawApp()`, this names it, and limb 1 — // which only drives ApiTriggerPlugin — would not have. - const reaching = packageSourceFiles().filter((f) => - HOST_APP_REACH.test(readFileSync(join(SRC_DIR, f), 'utf8')), + const reaching = filesReachingHostApp( + packageSourceFiles(), + (f) => readFileSync(join(SRC_DIR, f), 'utf8'), ); expect( reaching, @@ -367,6 +370,33 @@ describe('trigger-api mount population (source scan)', () => { }); describe('comment-stripper (the scan machinery, pinned in both directions)', () => { + it('the host-app reach probe does not count PROSE — the #12398 false positive', () => { + expect(reachesHostApp('// the mount takes the handle through `IHttpServer.getRawApp()`\n')).toBe(false); + expect(reachesHostApp("/*\n * resolves 'http-server' before mounting\n */\n")).toBe(false); + }); + + it('the host-app reach probe still counts a REACH THAT LIVES IN A STRING', () => { + // The direction that makes the fix a fix rather than a disarm: + // `'http-server'` is a service key, and a service key is a string. + expect(reachesHostApp("const s = ctx.getService('http-server');\n")).toBe(true); + expect(reachesHostApp('const s = ctx.getService("http-server");\n')).toBe(true); + expect(reachesHostApp('const s = ctx.getService(`http-server`);\n')).toBe(true); + expect(reachesHostApp('const app = server.getRawApp();\n')).toBe(true); + }); + + it('a genuine second reacher is still named — anti-vacuity on the identity limb', () => { + // LOAD-BEARING POSITIVE for #12398's fix, driven through the same + // function the live limb calls with source injected. + const fake: Record = { + 'plugin.ts': 'const app = http.getRawApp();\n', + 'prose-only.ts': '// getRawApp() is reached in plugin.ts, never here\n', + 'zzz-second-reacher.ts': "const s = ctx.getService('http-server');\n", + }; + expect( + filesReachingHostApp(Object.keys(fake).sort(), (f) => fake[f]), + ).toEqual(['plugin.ts', 'zzz-second-reacher.ts']); + }); + it('drops paths that only appear in prose, and keeps every path in code', () => { const fixture = [ "// mounts '/api/v1/commented-out'", diff --git a/scripts/check-comment-mask-adoption.mjs b/scripts/check-comment-mask-adoption.mjs index 08bc293fae..501b274113 100644 --- a/scripts/check-comment-mask-adoption.mjs +++ b/scripts/check-comment-mask-adoption.mjs @@ -69,6 +69,21 @@ * a sweep, and is deliberately not this gate's call: a row whose verdict * changes under the shared mask is a finding to read. * + * FIRST SHRINK (#12398): the three route-ledger conformance guards + * (`metadata`, `cloud-connection`, `trigger-api`) were converted and their rows + * deleted here in the same PR, which is the half this gate's `stale` branch + * exists to demand. Their conversion was a measurement and it found something: + * on two of the three, the private scanner was reading the `//` inside a REGEX + * LITERAL as a line-comment opener and deleting real code to end of line -- + * `/^https?:\/\//i` in `packages/metadata/src/plugin.ts` (40 bytes) and + * `/\/packages\/[^/]+\/versions\//` in + * `packages/cloud-connection/src/marketplace-proxy-plugin.ts` (38 bytes, inside + * a declared MOUNT SOURCE). That is the naive-`//` family this gate exists for, + * found live rather than argued from the shape. `packages/cli/src/utils/ + * console-route-ledger.conformance.test.ts` is the fourth guard in that family + * and stays recorded: its population limb already stripped, so it was out of + * that card's scope and nobody has re-read its scanner. + * * A recorded row that the scan no longer finds FAILS as stale. That is the * property `check-self-test-wired.mjs` names as the difference between a rule * with a witness and a rule without one: it converts "no findings" into "the @@ -197,8 +212,6 @@ const LEDGER = new Map([ { shapes: ['regex-block', 'regex-line'], verdict: 'specimen', why: 'the private two-regex strip this file carried until its conversion, kept as a NEGATIVE CONTROL that the shared mask beats it; converting it would delete the evidence' }], ['packages/cli/src/utils/console-route-ledger.conformance.test.ts', { shapes: ['scanner-decl'], verdict: 'unconverted', why: 'predates this gate; hand-rolled scanner, preserves block-comment newlines' }], - ['packages/cloud-connection/src/cloud-connection-route-ledger.conformance.test.ts', - { shapes: ['scanner-decl'], verdict: 'unconverted', why: 'predates this gate; hand-rolled scanner carrying its own 80-line rationale for preserving newlines' }], ['packages/create-objectstack/src/template-registry.test.ts', { shapes: ['regex-line'], verdict: 'unconverted', why: 'predates this gate; line-comment strip over a template manifest, the spelling the filing card missed' }], ['packages/drivers/driver-sql/src/live-dialect-matrix.isolation.test.ts', @@ -215,8 +228,6 @@ const LEDGER = new Map([ { shapes: ['regex-block', 'regex-line'], verdict: 'unconverted', why: 'predates this gate; the full naive two-regex pair' }], ['packages/metadata-protocol/src/migrations/live-mysql-database.isolation.test.ts', { shapes: ['regex-block', 'regex-line'], verdict: 'unconverted', why: 'predates this gate; the full naive two-regex pair' }], - ['packages/metadata/src/metadata-route-ledger.conformance.test.ts', - { shapes: ['scanner-decl'], verdict: 'unconverted', why: 'predates this gate; hand-rolled scanner, preserves block-comment newlines' }], ['packages/plugins/plugin-approvals/src/admin-exemption-retired.test.ts', { shapes: ['scanner-decl'], verdict: 'unconverted', why: 'predates this gate; hand-rolled scanner that DROPS block-comment newlines while its gate reports a line number' }], ['packages/plugins/plugin-auth/src/rate-limit-storage-isolation.test.ts', @@ -227,8 +238,6 @@ const LEDGER = new Map([ { shapes: ['regex-block', 'regex-line'], verdict: 'unconverted', why: 'predates this gate; the full naive two-regex pair' }], ['packages/spec/scripts/lazify-schemas.ts', { shapes: ['regex-block', 'regex-line'], verdict: 'unconverted', why: 'predates this gate; import-block matcher consuming leading comments, and package-local tooling the card excluded by substring' }], - ['packages/triggers/trigger-api/src/trigger-api-route-ledger.conformance.test.ts', - { shapes: ['scanner-decl'], verdict: 'unconverted', why: 'predates this gate; hand-rolled scanner that DROPS block-comment newlines while its gate reports a line number' }], ]); function walk(dir, out = []) { diff --git a/scripts/cross-package-test-inputs.mjs b/scripts/cross-package-test-inputs.mjs index 6e4c0d3539..bf36177d13 100644 --- a/scripts/cross-package-test-inputs.mjs +++ b/scripts/cross-package-test-inputs.mjs @@ -473,6 +473,36 @@ export const CROSS_PACKAGE_TEST_INPUTS = { 'scripts/js-comment-mask.d.mts', ], }, + '@objectstack/metadata': { + // src/metadata-route-ledger.conformance.test.ts (#12398) imports + // `stripComments` from `js-comment-mask.mjs` to decide which text in this + // package's `src/` is prose and which is a mount or a host-app reach — the + // conversion off its own private scanner, whose row in + // `check-comment-mask-adoption.mjs` was deleted in the same PR. The + // coupling is real: every one of that guard's verdicts, including an + // IDENTITY over the package's whole source population, is a function of the + // module's stripping behaviour, so a change to it has to re-run this + // package's suite. The `.d.mts` sibling is declared alongside it because it + // is what gives `stripComments` its type, so this package's `tsc --noEmit` + // verdict is a function of it too — the reason the `@objectstack/cli` entry + // above declares the pair rather than the module alone. + globs: [ + 'scripts/js-comment-mask.mjs', + 'scripts/js-comment-mask.d.mts', + ], + }, + '@objectstack/trigger-api': { + // src/trigger-api-route-ledger.conformance.test.ts (#12398) imports + // `stripComments` from `js-comment-mask.mjs` for the same reason and in the + // same conversion as the `@objectstack/metadata` entry above: the guard's + // path-literal census and its host-app-reach IDENTITY are both functions of + // the module's stripping behaviour, and the `.d.mts` is what types the + // import for this package's typecheck. + globs: [ + 'scripts/js-comment-mask.mjs', + 'scripts/js-comment-mask.d.mts', + ], + }, '@objectstack/plugin-auth': { // src/managed-extension-fields.test.ts walks every `*.object.ts`, and pins // core's api-key source alongside it. diff --git a/turbo.json b/turbo.json index dbb3d093fe..6260ef7b6d 100644 --- a/turbo.json +++ b/turbo.json @@ -26,7 +26,14 @@ "@objectstack/metadata#test": { "dependsOn": ["build"], "outputs": [], - "inputs": ["$TURBO_DEFAULT$", "!dist/**", "!coverage/**", "!.turbo/**"] + "inputs": [ + "$TURBO_DEFAULT$", + "!dist/**", + "!coverage/**", + "!.turbo/**", + "$TURBO_ROOT$/scripts/js-comment-mask.mjs", + "$TURBO_ROOT$/scripts/js-comment-mask.d.mts" + ] }, "@objectstack/spec#test": { "dependsOn": ["^build"], @@ -191,6 +198,18 @@ "outputs": [], "inputs": ["$TURBO_DEFAULT$", "!dist/**", "!coverage/**", "!.turbo/**"] }, + "@objectstack/trigger-api#test": { + "dependsOn": ["^build"], + "outputs": [], + "inputs": [ + "$TURBO_DEFAULT$", + "!dist/**", + "!coverage/**", + "!.turbo/**", + "$TURBO_ROOT$/scripts/js-comment-mask.mjs", + "$TURBO_ROOT$/scripts/js-comment-mask.d.mts" + ] + }, "@objectstack/trigger-record-change#test": { "dependsOn": ["^build"], "inputs": [