diff --git a/scripts/check-published-readme-exports.mjs b/scripts/check-published-readme-exports.mjs index b0ad6b156f..81e46614cc 100644 --- a/scripts/check-published-readme-exports.mjs +++ b/scripts/check-published-readme-exports.mjs @@ -117,6 +117,11 @@ // count(...)` on a locally-bound variable is pseudo-code and is never read. // Anything whose type is `any`, or which carries an index signature, is not // reported -- absence of a property there is not evidence. +// +// That fence is a CHARACTER CLASS, and #9610 measured what happens when it is +// spelled as a consuming alternation instead of a zero-width assertion: the +// receiver in `kernel.use(SomePlugin.configure(…))` became unreachable, because +// the outer call had already eaten the `(` in front of it. See extractMemberCalls. import { existsSync, readFileSync, readdirSync } from 'node:fs'; import { join, posix, resolve } from 'node:path'; @@ -337,14 +342,31 @@ export function extractMemberCalls(markdown, localNames) { for (const { n, text } of fence.lines) { // Skip the import statements themselves and single-line comments. if (/^\s*(import\b|\/\/|\*|\/\*)/.test(text)) continue; - const rx = /(^|[^\w$.'"`])([A-Za-z_$][\w$]*)\s*\.\s*([A-Za-z_$][\w$]*)\s*\(/g; + // ⛔ The leading boundary is ASSERTED, never consumed (#9610). The obvious + // spelling -- `(^|[^\w$.'"`])` -- eats the character in front of the receiver, + // and `rx` is global, so a receiver beginning at the very next character after + // a previous match has no boundary left to match against. A match always ends + // at its own `(`, which makes the swallowed position exactly `outer(Inner.m(` + // -- and `kernel.use(SomePlugin.configure({…}))` is the house spelling of every + // README this gate was built for, so the blind spot was the NORMAL position, + // not a corner. Measured on the published regex, one space apart: + // + // kernel.use(CacheServicePlugin.configure({…})) -> extracted: kernel.use + // kernel.use( CacheServicePlugin.configure({…})) -> extracted: both + // + // A negative lookbehind is zero-width, so nothing is consumed and the `^` arm + // folds in (a negative lookbehind is satisfied at position 0). The character + // class is byte-for-byte the old one, so the fence is unchanged: `a.b.c(` and + // `'str'.trim(` stay out -- now in the nested position too, which is the only + // position this change newly reaches. + const rx = /(? `${c.object}.${c.member}`), + ['CacheServicePlugin.configure'], + ); + + // The other direction, in that SAME position: reaching it must not widen the + // fence. Property access, all three quote styles, and the CORRECT `new X(` + // spelling stay silent when nested exactly as above. + const nestedRejected = [ + '```typescript', + "import { CacheServicePlugin } from '@objectstack/service-cache';", + 'await kernel.use(wrapper.CacheServicePlugin.configure({}));', + "console.log('CacheServicePlugin.configure(');", + 'console.log("CacheServicePlugin.configure(");', + 'console.log(`CacheServicePlugin.configure(`);', + 'await kernel.use(new CacheServicePlugin({ adapter: "memory" }));', + '```', + ].join('\n'); + eq( + 'extractMemberCalls — the nested position does not widen the fence', + extractMemberCalls(nestedRejected, ['CacheServicePlugin']), + [], + ); + // -- specifier splitting ------------------------------------------------------ eq('splitSpecifier — scoped root', splitSpecifier('@objectstack/spec'), { name: '@objectstack/spec', @@ -1065,6 +1125,25 @@ function selfTest() { ['@objectstack/kernel|packages/kernel/README.md|member|@objectstack/kernel|Kernel.configure'], ); + // ...and the same fabricated static written the way plugin registration is + // actually written: nested inside another call, no separator (#9610). This ran + // GREEN end to end on the real tree before the boundary became zero-width. + const fabricatedStaticNested = { + pkg: '@objectstack/kernel', + file: 'packages/kernel/README.md', + text: [ + '```typescript', + "import { Kernel } from '@objectstack/kernel';", + 'await app.use(Kernel.configure({}));', + '```', + ].join('\n'), + }; + eq( + 'analyzeDocument — a fabricated static nested inside another call is reported', + analyzeDocument(fabricatedStaticNested, resolveFake).map((f) => f.id), + ['@objectstack/kernel|packages/kernel/README.md|member|@objectstack/kernel|Kernel.configure'], + ); + // Undeclared subpath: the packaged surface says something the source does not. const badSubpath = { pkg: '@objectstack/service-analytics',