|
144 | 144 | import { execSync } from 'node:child_process'; |
145 | 145 | import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs'; |
146 | 146 | import { join, relative } from 'node:path'; |
| 147 | +// The one answer to "is this span a comment, or code?" (#9367). Dependency-free and |
| 148 | +// side-effect-free on import, so the no-install contract this script runs under holds. |
| 149 | +import { maskComments } from '../js-comment-mask.mjs'; |
147 | 150 |
|
148 | 151 | const repoRoot = execSync('git rev-parse --show-toplevel').toString().trim(); |
149 | 152 | const args = process.argv.slice(2); |
@@ -944,9 +947,40 @@ function symbolAnchorsFromSource(text, changed) { |
944 | 947 | * mentions. This is the mechanical half of the SDK bridge: a changed protocol method |
945 | 948 | * appears in the handler of the route it serves, which the ledger then binds to a client |
946 | 949 | * method the docs actually name. |
| 950 | + * |
| 951 | + * ⛔ READ OFF CODE, NEVER OFF PROSE (#9432). Everything this function claims is a claim |
| 952 | + * about what a handler DOES: the premise the bridge tests is "this symbol IS this route's |
| 953 | + * implementation, so the handler that implements the route mentions it". A name that a |
| 954 | + * handler mentions only in an English sentence satisfies a bare token scan without |
| 955 | + * satisfying that premise — and an implementation comment naming a neighbouring symbol is |
| 956 | + * ordinary, careful writing, not a smell. Measured on `40d5b2d4c` (#9405, a |
| 957 | + * `metadata-protocol` batch-publish change): BOTH route anchors the run produced came from |
| 958 | + * prose and nothing else — `promoteDraftForPublish` from two comment lines in the publish |
| 959 | + * handler, and `publishPackageDrafts` from two more that put the state-machine route (and |
| 960 | + * `meta.getLegalNextStates` through the ledger) on the advisory for a diff that never |
| 961 | + * touched a state machine. Comments contribute 4329 of the 7649 identifier slots this scan |
| 962 | + * sees on today's tree, so the surface is more than half prose. |
| 963 | + * |
| 964 | + * The mask covers the `path:` scan too, not just the identifier scan, and that is the same |
| 965 | + * rule rather than a second one: a route registration written inside a JSDoc `@example` is |
| 966 | + * an illustration, not a registration (`route-manager.ts` has exactly one, minting a |
| 967 | + * phantom `/api/users/:id`), and a `path:` line inside a comment must not truncate the |
| 968 | + * previous handler's window either. `maskComments` is the projection — this scan reasons in |
| 969 | + * LINE POSITIONS (a window is 150 lines past a site) and in what precedes `path:` on a |
| 970 | + * line, and blanking keeps both while deleting would move columns. The #9367 lazy-matcher |
| 971 | + * hazard does not apply: the one lazy quantifier here is `(.*?)` inside a per-LINE regex, |
| 972 | + * bounded by a line, never dragged across the file. Cost measured: 115 ms to mask the 19 |
| 973 | + * registrar files (888 KB), taking a whole run from 387 ms to 480 ms. |
| 974 | + * |
| 975 | + * Measured cost in RECALL, which is the question the card said to answer before assuming: |
| 976 | + * ZERO, over the last 60 commits touching `packages/`. Three runs changed at all, and every |
| 977 | + * row that moved is a wrong row — including the one that looked most like a real loss, |
| 978 | + * `api/client-sdk.mdx` on `40d5b2d4c`: the hunks that made `promoteDraftForPublish` an |
| 979 | + * anchor there are DOC-COMMENT-only, so `publishItem`'s behaviour never moved, and the page |
| 980 | + * names none of the three symbols that did. |
947 | 981 | */ |
948 | 982 | function parseRegistrarSource(text) { |
949 | | - const lines = text.split('\n'); |
| 983 | + const lines = maskComments(text).split('\n'); |
950 | 984 | const sites = []; |
951 | 985 | for (let i = 0; i < lines.length; i++) { |
952 | 986 | const m = lines[i].match(/(?:^|[\s{,(])path\s*:\s*([`'"])(.*?)\1/); |
@@ -1404,6 +1438,84 @@ function selfTest() { |
1404 | 1438 | check('symbolAnchorsFromSource.bridgeable', label, `${name} @ line ${line}`, want, bridgeableAt(src, line).has(name)); |
1405 | 1439 | } |
1406 | 1440 |
|
| 1441 | + // ---- the handler-window scan reads CODE, never PROSE (#9432) --------------- |
| 1442 | + // #9294 removed CONTAINER names from the bridge, which closed the specimen it measured. |
| 1443 | + // The residue is one layer down and is what this block pins: a LEAF symbol named only in |
| 1444 | + // an English comment inside a handler window bridged just the same. |
| 1445 | + // |
| 1446 | + // MEASURED FAILURE (40d5b2d4c, #9405 — a `metadata-protocol` batch-publish change): BOTH |
| 1447 | + // route anchors that run produced were prose and nothing else. |
| 1448 | + // |
| 1449 | + // promoteDraftForPublish → /:type/:name/publish rest-server.ts:5324,5376 (comments) |
| 1450 | + // publishPackageDrafts → /:name/state/:field rest-server.ts:5694,5722 (comments) |
| 1451 | + // |
| 1452 | + // The second one carried `content/docs/protocol/objectql/state-machine.mdx` onto the |
| 1453 | + // advisory (and `meta.getLegalNextStates` through the ledger) for a diff that went |
| 1454 | + // nowhere near a state machine. Neither name is called by the handler that named it; |
| 1455 | + // both sentences are ordinary implementation commentary about a neighbouring door. |
| 1456 | + // |
| 1457 | + // ⚠️ PINNED IN BOTH DIRECTIONS, for the reason #9294's block states: an exclusion-only |
| 1458 | + // test passes just as happily on a fix that breaks the bridge outright, which would trade |
| 1459 | + // a precision bug for the recall hole the bridge exists to fill. So the comment-named leaf |
| 1460 | + // must NOT bridge, the code-named leaf MUST, and the raw scan must still SEE the prose |
| 1461 | + // name — that last one is the counterfactual, and it is what stops this block going green |
| 1462 | + // because the fixture drifted into carrying no comment at all. |
| 1463 | + const commentaryRegistrar = [ |
| 1464 | + 'export class RestServer {', |
| 1465 | + ' private registerPublishRoutes() {', |
| 1466 | + ' this.routeManager.register({', |
| 1467 | + " method: 'POST',", |
| 1468 | + ' path: `${metaPath}/:type/:name/publish`,', |
| 1469 | + ' handler: async (req, res) => {', |
| 1470 | + ' // Promotion is authoring. `promoteDraftForPublish` flips the', |
| 1471 | + " // `sys_metadata` row state: 'draft' → 'active', and ADR-0027", |
| 1472 | + ' // (E)(5) defines sealing a publish as exactly that.', |
| 1473 | + " const base = 'https://acme.test//v1'; // a URL is not a comment opener", |
| 1474 | + ' return this.publishMetaItem(base, req.params); // seals the draft', |
| 1475 | + ' },', |
| 1476 | + ' });', |
| 1477 | + ' }', |
| 1478 | + '', |
| 1479 | + ' /**', |
| 1480 | + ' * @example', |
| 1481 | + ' * manager.register({', |
| 1482 | + " * method: 'GET',", |
| 1483 | + " * path: '/api/users/:id',", |
| 1484 | + ' * handler: getUserHandler,', |
| 1485 | + ' * });', |
| 1486 | + ' */', |
| 1487 | + ' private describeRegistration() {}', |
| 1488 | + '}', |
| 1489 | + ].join('\n'); |
| 1490 | + const commentary = parseRegistrarSource(commentaryRegistrar); |
| 1491 | + const publishIds = commentary.get('/:type/:name/publish'); |
| 1492 | + check('parseRegistrarSource', 'a leaf the handler actually CALLS is still seen — the bridge is untouched where its premise holds', 'publishMetaItem', true, !!publishIds?.has('publishMetaItem')); |
| 1493 | + check('parseRegistrarSource', 'a leaf named only in an English comment is NOT this route\'s implementation', 'promoteDraftForPublish', false, !!publishIds?.has('promoteDraftForPublish')); |
| 1494 | + check('parseRegistrarSource', 'a `//` inside a STRING opens no comment — this scan rides the shared scanner, not a private regex', 'base + publishMetaItem', true, !!publishIds?.has('base') && !!publishIds?.has('publishMetaItem')); |
| 1495 | + check('parseRegistrarSource', 'a `path:` inside a JSDoc @example is an illustration, not a registration', 'tails', JSON.stringify(['/:type/:name/publish']), JSON.stringify([...commentary.keys()])); |
| 1496 | + |
| 1497 | + // The counterfactual, twice: the PRE-FIX scan, verbatim, over the same fixture. Both |
| 1498 | + // halves must still be there to be excluded, or this block proves nothing. |
| 1499 | + const rawWindowIds = (src, siteFragment) => { |
| 1500 | + const ls = src.split('\n'); |
| 1501 | + const start = ls.findIndex((l) => l.includes(siteFragment)); |
| 1502 | + const ids = new Set(); |
| 1503 | + for (let j = start; j >= 0 && j < Math.min(ls.length, start + REGISTRAR_HANDLER_WINDOW); j++) { |
| 1504 | + for (const m of ls[j].matchAll(/[A-Za-z_$][\w$]*/g)) ids.add(m[0]); |
| 1505 | + } |
| 1506 | + return ids; |
| 1507 | + }; |
| 1508 | + check('parseRegistrarSource', 'counterfactual: the bare-token scan DID read the prose name out of that window', 'promoteDraftForPublish', true, rawWindowIds(commentaryRegistrar, ':type/:name/publish').has('promoteDraftForPublish')); |
| 1509 | + check('parseRegistrarSource', 'counterfactual: that @example line DID match the registration-site regex', '/api/users/:id', '/api/users/:id', routeTailOf(commentaryRegistrar.split('\n').find((l) => l.includes('/api/users/:id')).match(/(?:^|[\s{,(])path\s*:\s*([`'"])(.*?)\1/)?.[2] ?? '')); |
| 1510 | + |
| 1511 | + // End to end through the same selection step the bridge runs: a diff confined to the |
| 1512 | + // commented-about symbol selects no route, and one on the called symbol still selects its |
| 1513 | + // own. The commented-about name is a doc anchor either way — it enters `symbolAnchors` |
| 1514 | + // from its OWN declaration, which this hop never touched — so nothing is lost but the |
| 1515 | + // false bridge. |
| 1516 | + check('bridge', 'a diff touching only the symbol the comment MENTIONS selects no route', 'tails', JSON.stringify([]), JSON.stringify(tailsSelectedBy(new Set(['promoteDraftForPublish']), commentary))); |
| 1517 | + check('bridge', 'a diff touching the symbol the handler CALLS still selects the publish route', 'tails', JSON.stringify(['/:type/:name/publish']), JSON.stringify(tailsSelectedBy(new Set(['publishMetaItem']), commentary))); |
| 1518 | + |
1407 | 1519 | // ---- the CLI command anchor kind (#9230) ---------------------------------- |
1408 | 1520 | // The recall class: a CLI-surface change derives `MetaResync` / a lowercase `resync`, |
1409 | 1521 | // and neither reaches the page that documents the command. The phrase does. Both halves |
@@ -1762,9 +1874,10 @@ for (const name of [...symbolAnchors].sort()) { |
1762 | 1874 | // TWO KINDS OF SYMBOL ARE DOC ANCHORS BUT NOT BRIDGE SYMBOLS. The bridge's premise is |
1763 | 1875 | // "this name IS some route's implementation, so the handler that implements that route |
1764 | 1876 | // mentions it" — and `parseRegistrarSource` tests that premise by scanning a handler |
1765 | | - // window for the BARE IDENTIFIER. Any name a handler mentions for some OTHER reason |
1766 | | - // satisfies the scan without satisfying the premise, and mints a route (and, through |
1767 | | - // the ledger, an sdk) anchor from a diff that never came near that route. |
| 1877 | + // window for the BARE IDENTIFIER (over comment-masked source since #9432, so a name a |
| 1878 | + // handler only WRITES ABOUT never satisfies it). Any name a handler mentions for some |
| 1879 | + // OTHER reason satisfies the scan without satisfying the premise, and mints a route (and, |
| 1880 | + // through the ledger, an sdk) anchor from a diff that never came near that route. |
1768 | 1881 | // |
1769 | 1882 | // 1. A SCREAMING_SNAKE constant is a data table, not a route's implementation: it is |
1770 | 1883 | // referenced by handlers that merely consult it. `ERROR_CODE_LEDGER` names a real |
|
0 commit comments