@@ -53,6 +53,13 @@ import { readdirSync, readFileSync } from 'node:fs';
5353import { dirname , join } from 'node:path' ;
5454import { fileURLToPath } from 'node:url' ;
5555import { describe , it , expect } from 'vitest' ;
56+ // The repo's ONE answer to "is this span a comment, or code?" — its header
57+ // carries the two private-stripper families that drifted apart and the
58+ // parser-differential sweep that measured which way each fails. This package
59+ // already imports the module next door in `canonical-expression-envelopes.test.ts`.
60+ // `stripComments` (not `maskComments`) is the projection this file wants: every
61+ // finding here reports a `file:line` or a bare file name, never an offset.
62+ import { stripComments } from '../../../scripts/js-comment-mask.mjs' ;
5663import { CLOUD_CONNECTION_ROUTE_LEDGER } from './cloud-connection-route-ledger.js' ;
5764
5865/**
@@ -121,63 +128,28 @@ const DECLARED_COMPUTED_MOUNTS = [
121128// ---------------------------------------------------------------------------
122129
123130/**
124- * Strip comments before scanning. Prose cannot mount a route, and this
125- * package's headers quote every wire path they serve — so a raw-text scan would
126- * report a documented path as an unledgered mount, a false red on an accurate
127- * package. The three string forms are tracked so a literal CONTAINING comment
128- * punctuation (`'/api/v1/x/*'`, `'https://host'`) is never mistaken for a
129- * comment opener; `comment-stripper` below pins both directions, because a
130- * stripper that swallowed real code would make this scan silently blind, which
131- * is the failure that actually matters here.
131+ * WHY COMMENTS ARE REMOVED BEFORE ANY SCAN HERE. Prose cannot mount a route and
132+ * cannot reach for a host app, and this package's headers quote every wire path
133+ * they serve — a raw-text scan reports a documented path as an unledgered mount
134+ * and a documented `getRawApp()` as a second reacher: a false red on an
135+ * accurate package.
136+ *
137+ * This file used to answer that question with its own character scanner. It was
138+ * converted to `scripts/js-comment-mask.mjs` (#12398), the tree's one answer to
139+ * it, and the swap was MEASURED rather than assumed: over this package's 13
140+ * scanned source files the two differ on exactly one,
141+ * `marketplace-proxy-plugin.ts`, where the private scanner read the `//` inside
142+ * the regex literal `/\/packages\/[^/]+\/versions\//` as a line-comment opener
143+ * and deleted the 38 characters of REAL CODE that followed it to end of line —
144+ * inside a declared MOUNT SOURCE, which is source this census reads. That is
145+ * the naive-`//` family the shared module's header measures, live here.
146+ *
147+ * `stripComments` keeps line numbers (block-comment newlines survive — this
148+ * package's headers run to eighty lines and every finding quotes `file:line`)
149+ * and keeps string, template and regex literals INTACT, which is what lets the
150+ * census resolve wire paths out of them at all. `scan machinery` at the foot of
151+ * this file pins both directions.
132152 */
133- export function stripComments ( source : string ) : string {
134- let out = '' ;
135- let i = 0 ;
136- while ( i < source . length ) {
137- const c = source [ i ] ;
138- const next = source [ i + 1 ] ;
139- if ( c === '/' && next === '/' ) {
140- while ( i < source . length && source [ i ] !== '\n' ) i ++ ;
141- continue ;
142- }
143- if ( c === '/' && next === '*' ) {
144- i += 2 ;
145- // Newlines inside the block are PRESERVED. Line numbers are what
146- // every finding below points a reader at, and this package's
147- // headers run to eighty lines — swallowing them would send someone
148- // to a line eighty short of the mount, which reads as a wrong
149- // report rather than as the accurate one it is.
150- while ( i < source . length && ! ( source [ i ] === '*' && source [ i + 1 ] === '/' ) ) {
151- if ( source [ i ] === '\n' ) out += '\n' ;
152- i ++ ;
153- }
154- i += 2 ;
155- continue ;
156- }
157- if ( c === '\'' || c === '"' || c === '`' ) {
158- const quote = c ;
159- out += c ;
160- i ++ ;
161- while ( i < source . length ) {
162- if ( source [ i ] === '\\' ) {
163- out += source . slice ( i , i + 2 ) ;
164- i += 2 ;
165- continue ;
166- }
167- out += source [ i ] ;
168- if ( source [ i ] === quote ) {
169- i ++ ;
170- break ;
171- }
172- i ++ ;
173- }
174- continue ;
175- }
176- out += c ;
177- i ++ ;
178- }
179- return out ;
180- }
181153
182154/** Module-scope `const NAME = '<literal>';` bindings, for resolving mount paths. */
183155export function constantBindings ( code : string ) : Map < string , string > {
@@ -293,6 +265,30 @@ function packageSourceFiles(): string[] {
293265/** The spellings by which a module in this package reaches the HOST app. */
294266const HOST_APP_REACH = / g e t R a w A p p | [ ' " ` ] h t t p - s e r v e r [ ' " ` ] | [ ' " ` ] h t t p \. s e r v e r [ ' " ` ] / ;
295267
268+ /**
269+ * Does `source` reach for the host app IN CODE?
270+ *
271+ * COMMENTS ARE REMOVED FIRST, and that half is the whole of #12398. A docblock
272+ * explaining why a mount sits where it does — "the mount takes the
273+ * framework-native handle through `IHttpServer.getRawApp()`" — is prose, and
274+ * prose reaches for nothing. Scanned raw it scored as an extra reacher and
275+ * failed an IDENTITY assertion by naming a file that reaches for nothing, whose
276+ * own failure text then invites the wrong repair: widening the expected list,
277+ * which retires the only property the assertion has.
278+ *
279+ * STRING, TEMPLATE AND REGEX LITERALS ARE LEFT INTACT, and that half is what
280+ * keeps the fix from being a silent disarm. Two of the three spellings above
281+ * ARE string literals — `ctx.getService('http.server')` reaches for the host
282+ * app entirely inside quotes — so a probe that masked literals as well as
283+ * comments would detect nothing and this identity would pass vacuously. Both
284+ * directions are pinned in `scan machinery` at the foot of this file.
285+ */
286+ const reachesHostApp = ( source : string ) : boolean => HOST_APP_REACH . test ( stripComments ( source ) ) ;
287+
288+ /** Which of `files` reach for the host app in code. Driveable for the pins. */
289+ const filesReachingHostApp = ( files : readonly string [ ] , read : ( f : string ) => string ) : string [ ] =>
290+ files . filter ( ( f ) => reachesHostApp ( read ( f ) ) ) ;
291+
296292const ledgerRoutes = ( ) : Set < string > => new Set ( CLOUD_CONNECTION_ROUTE_LEDGER . map ( ( e ) => e . route ) ) ;
297293const liveCensus = ( ) : Census => censusOf ( MOUNT_SOURCES , readSource ) ;
298294
@@ -369,7 +365,7 @@ describe('cloud-connection mount population', () => {
369365 // An IDENTITY, not a count: the day a FIFTH module in this package
370366 // resolves `http-server` or calls `getRawApp()`, this names it — and
371367 // the census above, which only reads MOUNT_SOURCES, would not have.
372- const reaching = packageSourceFiles ( ) . filter ( ( f ) => HOST_APP_REACH . test ( readSource ( f ) ) ) ;
368+ const reaching = filesReachingHostApp ( packageSourceFiles ( ) , readSource ) ;
373369 expect (
374370 reaching ,
375371 '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', () => {
475471 expect ( census . routes [ 0 ] . line ) . toBe ( 4 ) ;
476472 } ) ;
477473
474+ it ( 'the host-app reach probe does not count PROSE — the #12398 false positive' , ( ) => {
475+ // The exact docblock shape that fired it: a module explaining that the
476+ // mount takes the framework-native handle, in a comment.
477+ expect ( reachesHostApp ( '// the mount takes the handle through `IHttpServer.getRawApp()`\n' ) ) . toBe ( false ) ;
478+ expect ( reachesHostApp ( "/*\n * resolves 'http-server' before mounting\n */\n" ) ) . toBe ( false ) ;
479+ expect ( reachesHostApp ( "/* the ctx.getService('http.server') seam, explained */\n" ) ) . toBe ( false ) ;
480+ } ) ;
481+
482+ it ( 'the host-app reach probe still counts a REACH THAT LIVES IN A STRING' , ( ) => {
483+ // The direction that makes the fix a fix rather than a disarm: two of
484+ // the three spellings are service keys, which are string literals.
485+ expect ( reachesHostApp ( "const s = ctx.getService('http.server');\n" ) ) . toBe ( true ) ;
486+ expect ( reachesHostApp ( 'const s = ctx.getService("http-server");\n' ) ) . toBe ( true ) ;
487+ expect ( reachesHostApp ( 'const s = ctx.getService(`http-server`);\n' ) ) . toBe ( true ) ;
488+ expect ( reachesHostApp ( 'const app = server.getRawApp();\n' ) ) . toBe ( true ) ;
489+ } ) ;
490+
491+ it ( 'a genuine FIFTH reacher is still named — anti-vacuity on the identity limb' , ( ) => {
492+ // LOAD-BEARING POSITIVE for #12398's fix, driven through the same
493+ // function the live limb calls with source injected. Without it, a
494+ // strip that quietly stopped matching anything would leave the identity
495+ // green forever — the failure direction the whole family distrusts.
496+ const fake : Record < string , string > = {
497+ 'cloud-connection-plugin.ts' : 'const app = http.getRawApp();\n' ,
498+ 'prose-only.ts' : '// getRawApp() is reached in the four mount sources, never here\n' ,
499+ 'zzz-fifth-reacher.ts' : "const s = ctx.getService('http.server');\n" ,
500+ } ;
501+ expect (
502+ filesReachingHostApp ( Object . keys ( fake ) . sort ( ) , ( f ) => fake [ f ] ) ,
503+ ) . toEqual ( [ 'cloud-connection-plugin.ts' , 'zzz-fifth-reacher.ts' ] ) ;
504+ } ) ;
505+
478506 it ( 'resolves the three argument spellings this package actually uses' , ( ) => {
479507 const constants = new Map ( [ [ 'ROUTE_BASE' , '/api/v1/marketplace/install-local' ] , [ 'P' , '/api/v1/cloud-connection' ] ] ) ;
480508 // bare identifier — marketplace-install-local-plugin.ts
0 commit comments