|
58 | 58 | * failure is known to mean "the bytes did not land". Adding an entry is a |
59 | 59 | * deliberate, reviewable act. |
60 | 60 | * |
61 | | - * Two honest limitations, stated up front rather than discovered later: |
| 61 | + * Three honest limitations, stated up front rather than discovered later: |
62 | 62 | * |
63 | 63 | * 1. It cannot FIND a durability seam whose operation is not in the |
64 | 64 | * vocabulary. It guarantees the seams already paid for cannot regress to |
|
70 | 70 | * path does end loudly — but it does mean an unrelated nested |
71 | 71 | * `logger.error` would satisfy the gate. Narrowing this would fail the |
72 | 72 | * legitimate recovering catch, which is the worse trade. |
| 73 | + * 3. A catch that reports through a receiver spelled anything other than |
| 74 | + * `logger` / `log` / `console` reads as SILENT to both rules — the |
| 75 | + * dangling-reference audit's `port.warn?.(…)` is the live example (#8897). |
| 76 | + * This is a DECIDED narrowness, not an undiscovered one: see the decision |
| 77 | + * recorded at `LOGGER_RECEIVERS` for what was weighed and why widening it |
| 78 | + * was declined. |
73 | 79 | * |
74 | 80 | * ## Why AST, not regex |
75 | 81 | * |
@@ -653,7 +659,9 @@ const FAILURE_PROPAGATION_SITES = new Map([ |
653 | 659 | // (66 seams to 67) and reported as `no invented answer`, gate green. Surveyed, |
654 | 660 | // cleared, harmful — the #6116 shape a third time. |
655 | 661 | // |
656 | | -// SECOND — the census, narrowing one criterion at a time: 66 read seams; 46 have |
| 662 | +// SECOND — the census, narrowing one criterion at a time (⚠️ the 66 is the |
| 663 | +// PRE-#11921 recognizer's count — 2 of it were not read seams; see "RECOGNIZER |
| 664 | +// CORRECTION" below before quoting any figure in this ladder): 66 read seams; 46 have |
657 | 665 | // no `return` anywhere in the catch; 41 have a valueless exit; 31 of those are |
658 | 666 | // silent; 25 are silent AND undiscriminated; 15 also have an empty accumulator |
659 | 667 | // declared above the `try`, written inside it, and read below. |
@@ -703,7 +711,9 @@ const FAILURE_PROPAGATION_SITES = new Map([ |
703 | 711 | // arithmetic standing in for a census — the count is not the argument, and a |
704 | 712 | // number nobody can re-run is worth less than a smaller one anchored to a |
705 | 713 | // commit. The anchor is checkable: run this gate at 8664a2c and it still |
706 | | -// answers `66 read seam(s)`; today's main answers 67, a seam the scan roots |
| 714 | +// answers `66 read seam(s)` — with the recognizer of that day; #11921 corrected |
| 715 | +// it to 64 over the same tree, see "RECOGNIZER CORRECTION" — today's main |
| 716 | +// answered 67, a seam the scan roots |
707 | 717 | // gained after that commit and NOT from #8895 (which moves nothing — below). |
708 | 718 | // |
709 | 719 | // - `cascadeDeleteRelations` — REPAIRED by #8895. Its dependents probe now |
@@ -791,7 +801,9 @@ const FAILURE_PROPAGATION_SITES = new Map([ |
791 | 801 | // ## Measured before it was added — 收窄先行, the #6451 discipline again |
792 | 802 | // |
793 | 803 | // Census over the three scan roots at `origin/main` @ 945ffbea8 (2026-08-24), |
794 | | -// 66 read seams, narrowing one criterion at a time: |
| 804 | +// 66 read seams (⚠️ pre-#11921 recognizer; 64 corrected — the ladder BELOW is |
| 805 | +// unaffected, measured, see "RECOGNIZER CORRECTION"), narrowing one criterion at |
| 806 | +// a time: |
795 | 807 | // |
796 | 808 | // | narrowing | sites | |
797 | 809 | // |-------------------------------------------------------|------:| |
@@ -903,6 +915,58 @@ const FAILURE_PROPAGATION_SITES = new Map([ |
903 | 915 | // writes nothing at all, and the `push` / `++` accumulation shapes #8845 |
904 | 916 | // measured, which are unchanged. |
905 | 917 |
|
| 918 | +// ── RECOGNIZER CORRECTION (#11921) — what "66" actually counted ───────────── |
| 919 | +// |
| 920 | +// Every census figure above is quoted against a 66-seam read population. Two of |
| 921 | +// those 66 were never read seams. `isReadCall` matched the callee NAME with no |
| 922 | +// further test, so `Array.prototype.find` on a plain local array counted as a |
| 923 | +// storage read — and, through the wrapper recursion, attributed that "read" to |
| 924 | +// the wrapper's CALLER. The corrected recognizer is `contradictsDriverReadShape` |
| 925 | +// below; this block restates the affected figures rather than leaving them to be |
| 926 | +// silently invalidated. |
| 927 | +// |
| 928 | +// RE-MEASURED, not derived by arithmetic — the corrected recognizer was run over |
| 929 | +// the SAME anchor tree, so the correction is a census and not a subtraction: |
| 930 | +// |
| 931 | +// | tree | old recognizer | corrected | |
| 932 | +// |-------------------------------|---------------:|----------:| |
| 933 | +// | `origin/main` @ 8664a2c (#8845 anchor) | 66 | 64 | |
| 934 | +// | `origin/main` @ 8619f9513 (#11921) | 66 | 64 | |
| 935 | +// |
| 936 | +// The old recognizer reproducing 66 at 8664a2c is the calibration: it is the |
| 937 | +// number this header already told the reader to re-run the gate and check, so |
| 938 | +// the instrument is verified against the record before it is trusted. |
| 939 | +// |
| 940 | +// THE TWO NON-MEMBERS, the same two functions on both trees: |
| 941 | +// |
| 942 | +// - `getMetaItems` (`metadata-protocol/src/protocol.ts`) — its try guards |
| 943 | +// `mergePackageAwareOverlay`, a PURE helper whose `list.find((c) => …)` runs |
| 944 | +// on a plain local array. The `try` holds exactly one `await`, |
| 945 | +// `metadataService.list(request.type)`, and no find/findOne/count of its own. |
| 946 | +// Catch shape: empty, returnless, silent, undiscriminated — so it was a |
| 947 | +// member of the 46 / 41 / 31 / 25 rungs, and it carries the shape the 15 rung |
| 948 | +// tests for (`let items: unknown[] = []` above the `try`, written inside, |
| 949 | +// read below). |
| 950 | +// - `resolveDeferredUpdates` (`metadata-protocol/src/seed-loader.ts`) — NOT |
| 951 | +// found when #11921 was filed, and the worse of the two: its try guards |
| 952 | +// `writeDeferredReference`, a WRITE. It is in this rule's population at all |
| 953 | +// only because `allResults.find(r => …)` updates an in-memory stats row. Its |
| 954 | +// catch reports at `error`, so it left the ladder at the "silent" rung and |
| 955 | +// sits in the 46 / 41 rungs only. |
| 956 | +// |
| 957 | +// ⛔ The #8845 and #9165 ladder rungs are NOT re-derived here, deliberately. |
| 958 | +// Re-running those narrowings is the census re-run #8901's restart conjunct (b) |
| 959 | +// reserves, and #8901's own ⛔ forbids re-opening the #8845 decision; #11921's |
| 960 | +// filing forbids this fix riding along with any other change. What is restated |
| 961 | +// is the DENOMINATOR every one of those figures is quoted against, plus enough |
| 962 | +// of each departing seam's shape that any rung's owner can place it exactly. |
| 963 | +// |
| 964 | +// Directly measured and unaffected: the #9165 assignment ladder (3 / 1 / 1 / 0) |
| 965 | +// and every parenthetical in the verdict line — 8 type-discriminated, 1 |
| 966 | +// pass-through, 1 answer-by-assignment, 1 baselined — are byte-identical before |
| 967 | +// and after. Only the seam count moves, and ⛔ no baseline entry was added: an |
| 968 | +// entry there says a human read the seam, not that a rule guessed wrong. |
| 969 | + |
906 | 970 | /** |
907 | 971 | * Where the read-seam rule looks. Narrowed on purpose — see above. |
908 | 972 | * |
@@ -953,8 +1017,55 @@ const DRIVER_READ_CALLEES = new Map([ |
953 | 1017 | * Two hops is what the real chain needs (`_find` → `engine.find`) plus one. |
954 | 1018 | * Measured on the scan scope: following wrappers grew the SEAM census from 45 |
955 | 1019 | * to 66 and the VIOLATION set not at all — it buys robustness at zero |
956 | | - * false-positive cost today. |
| 1020 | + * false-positive cost today. (#11921: that 66 included 2 non-reads the wrapper |
| 1021 | + * recursion manufactured from `Array.prototype.find`; corrected, 45 to 64. The |
| 1022 | + * conclusion is unchanged — the VIOLATION set is still untouched.) |
957 | 1023 | */ |
| 1024 | +/** |
| 1025 | + * The one thing the READ vocabulary above cannot say by NAME, said by SHAPE. |
| 1026 | + * |
| 1027 | + * `find` is `IDataDriver.find` and it is also `Array.prototype.find`, and a |
| 1028 | + * callee NAME cannot tell them apart. Until #11921 nothing tried: `isReadCall` |
| 1029 | + * matched the name with no further test, so `list.find((c) => c.pkg === id)` on |
| 1030 | + * a plain local array read as a storage read — and, through the wrapper |
| 1031 | + * recursion below, attributed that "read" to the wrapper's CALLER, pulling a |
| 1032 | + * try/catch that guards no storage read at all into the census. |
| 1033 | + * |
| 1034 | + * A receiver ALLOWLIST is the wrong answer here, and measurably so. Measured |
| 1035 | + * over the whole census (`--list`, 90 matched reads across 66 seams): the |
| 1036 | + * genuine driver reads arrive through `this`, `this.engine`, `this.ql`, |
| 1037 | + * `driver`, `engine`, `engineAny`, `ledger` and `port`; the array reads through |
| 1038 | + * `list`, `allResults` and a `.map(...)` chain. Nothing in the SHAPE of a |
| 1039 | + * receiver separates those two lists — only what the name is BOUND to does, and |
| 1040 | + * that is a type-checker. Requiring a `this`-rooted receiver, the closest |
| 1041 | + * syntactic approximation, drops 14 of the 66 seams and 12 of the 14 are real |
| 1042 | + * driver reads (history-cleanup, build-probes, the dangling-reference audit, |
| 1043 | + * `checkGovernance`). That is the UNSAFE direction: a population that shrinks |
| 1044 | + * silently. Declaring the receiver names instead is a second vocabulary with |
| 1045 | + * its own staleness obligation — the failure mode this file's staleness checks |
| 1046 | + * exist for — and it still fails the same way, because the next driver binding |
| 1047 | + * gets a different name and its seam leaves the census without a word. |
| 1048 | + * |
| 1049 | + * What CAN be said is said from the contract already anchored above, not from a |
| 1050 | + * new list of names: every read on `IDataDriver` takes the object NAME first — |
| 1051 | + * `find(objectName, query)`, `findOne(objectName, …)`, `count(objectName, |
| 1052 | + * query?)`. A predicate is never its first argument. `Array.prototype.find`'s |
| 1053 | + * first argument is ALWAYS one. So a call whose first argument is a function |
| 1054 | + * literal is not a call to this contract, however it is spelled. |
| 1055 | + * |
| 1056 | + * Direction of the error, stated up front: this predicate can only DECLINE to |
| 1057 | + * exclude. It fires on a shape the declared contract cannot produce, so it |
| 1058 | + * cannot drop a real driver read; and a spurious match it does not recognise — |
| 1059 | + * `list.find(isTarget)`, a named predicate rather than a literal — stays in the |
| 1060 | + * census, over-counting rather than under-counting. That is the same safe |
| 1061 | + * direction every other narrowing in this file takes. |
| 1062 | + */ |
| 1063 | +function contradictsDriverReadShape(node) { |
| 1064 | + const first = node.arguments[0]; |
| 1065 | + if (!first) return false; |
| 1066 | + return ts.isArrowFunction(first) || ts.isFunctionExpression(first); |
| 1067 | +} |
| 1068 | + |
958 | 1069 | const MAX_READ_WRAPPER_DEPTH = 2; |
959 | 1070 |
|
960 | 1071 | /** |
@@ -1084,7 +1195,54 @@ function calleeName(node) { |
1084 | 1195 | return undefined; |
1085 | 1196 | } |
1086 | 1197 |
|
1087 | | -/** The receiver names that make a `<recv>.<level>(…)` call a LOG. */ |
| 1198 | +/** |
| 1199 | + * The receiver names that make a `<recv>.<level>(…)` call a LOG. |
| 1200 | + * |
| 1201 | + * ## DECIDED, 2026-08-25 (#8897, option 1) — narrow on purpose, and recorded |
| 1202 | + * |
| 1203 | + * #8897's `Restart-when:` is *"any PR touches |
| 1204 | + * scripts/check-durability-degradation-log-level.mjs (decide options 1/2/3 in |
| 1205 | + * that change)"*. #11921 touched it, so the three-way is settled here rather |
| 1206 | + * than passed by. The options were: (1) leave it and note the narrowness beside |
| 1207 | + * the other honest limitations; (2) follow a same-file injected-logger |
| 1208 | + * PARAMETER the way this file already follows same-file helper FUNCTIONS; |
| 1209 | + * (3) declare additional receiver names explicitly. |
| 1210 | + * |
| 1211 | + * **Taken: option 1.** The filer's own weak preference, but the reason recorded |
| 1212 | + * here is not the filer's — it is the measurement #11921 ran on the mirror-image |
| 1213 | + * question, which none of the three options had. |
| 1214 | + * |
| 1215 | + * #11921's whole subject is that matching a NAME with no provenance manufactures |
| 1216 | + * false members: `find` is `IDataDriver.find` and also `Array.prototype.find`. |
| 1217 | + * Asking the same question of the receiver side was measured over the full |
| 1218 | + * read-seam census — and receiver PROVENANCE is not syntactically available |
| 1219 | + * either. Requiring a `this`-rooted receiver drops 14 of 66 seams, 12 of them |
| 1220 | + * real driver reads; declaring the receiver names instead only postpones that, |
| 1221 | + * because the next binding gets a name the list does not have. |
| 1222 | + * |
| 1223 | + * Options 2 and 3 are both that instrument, pointed at `loggerLevel`. And the |
| 1224 | + * direction of their error is the UNSAFE one here, which the filer's analysis |
| 1225 | + * did not price: for the read-seam rule `collectLoggedLevels` powers an |
| 1226 | + * EXEMPTION — "the catch said something, so this is the other rule's question". |
| 1227 | + * Widening what counts as a logger widens an exemption, so a seam that invents |
| 1228 | + * an answer is excused because something that is not a logger happens to own a |
| 1229 | + * `.warn`. Option 1 keeps the error in the direction this file takes everywhere |
| 1230 | + * else: a genuinely loud catch may be over-counted as silent, and no invention |
| 1231 | + * is ever excused for a reason nobody declared. |
| 1232 | + * |
| 1233 | + * ⚠️ What option 1 costs, stated rather than hidden: the misclassification is |
| 1234 | + * still real and still latent — the read-seam rule consults the log exemption |
| 1235 | + * only after finding an invented answer, and the live injected-receiver seam |
| 1236 | + * (`packages/objectql/src/integrity/dangling-reference-audit.ts`, receiver |
| 1237 | + * `port`) invents none, so it never reaches a verdict. #8897's SECOND restart |
| 1238 | + * condition is unchanged and is the promote-immediately one: a seam reporting |
| 1239 | + * through an injected receiver going red with a "silent" message. If that fires, |
| 1240 | + * this decision was wrong and the evidence says so out loud. |
| 1241 | + * |
| 1242 | + * ⛔ Not decided here, and deliberately: the CALL-SHAPE half of the same |
| 1243 | + * function's narrowness (#9657, closed) — a different defect in `loggerLevel`, |
| 1244 | + * already answered by #9609 with a named same-file helper. |
| 1245 | + */ |
1088 | 1246 | const LOGGER_RECEIVERS = /^(logger|log|console)$/i; |
1089 | 1247 |
|
1090 | 1248 | /** Every level name the two vocabularies above know. */ |
@@ -1880,7 +2038,11 @@ function unwrapExpression(expr) { |
1880 | 2038 | function isReadCall(node, functionBodies, seen = new Set(), depth = 0) { |
1881 | 2039 | const name = calleeName(node); |
1882 | 2040 | if (!name) return false; |
1883 | | - if (DRIVER_READ_CALLEES.has(name)) return true; |
| 2041 | + if (DRIVER_READ_CALLEES.has(name) && !contradictsDriverReadShape(node)) return true; |
| 2042 | + // A vocabulary NAME whose argument shape contradicts the contract is not |
| 2043 | + // treated as a dead end: it falls through to the wrapper recursion, so a |
| 2044 | + // same-file helper that happens to be called `find` is still followed. The |
| 2045 | + // shape test subtracts a false positive; it must not subtract a real read. |
1884 | 2046 | if (depth >= MAX_READ_WRAPPER_DEPTH || seen.has(name)) return false; |
1885 | 2047 | const body = functionBodies.get(name); |
1886 | 2048 | if (!body) return false; |
@@ -4813,6 +4975,115 @@ function selfTestReadSeams() { |
4813 | 4975 | expectViolation: false, |
4814 | 4976 | expectInvents: [], |
4815 | 4977 | }, |
| 4978 | + |
| 4979 | + // ── #11921: the callee NAME is not enough — the shape test, both ways ── |
| 4980 | + // |
| 4981 | + // `find` is `IDataDriver.find` and it is also `Array.prototype.find`. |
| 4982 | + // These five pin the line `contradictsDriverReadShape` draws, including |
| 4983 | + // the two places it deliberately does NOT draw it. `expectSeams` is the |
| 4984 | + // load-bearing assertion in the passing cases: `expectViolation: false` |
| 4985 | + // would hold vacuously if the seam were merely graded green, and what |
| 4986 | + // is being asserted here is that it is not in the POPULATION at all. |
| 4987 | + { |
| 4988 | + name: 'passes: #11921 — `Array.prototype.find` on a local array is not a storage read', |
| 4989 | + code: ` |
| 4990 | + class L { |
| 4991 | + async pick(id: string) { |
| 4992 | + const list = [{ id: 'a' }]; |
| 4993 | + try { |
| 4994 | + const hit = list.find((c) => c.id === id); |
| 4995 | + return hit ?? null; |
| 4996 | + } catch { return null; } |
| 4997 | + } |
| 4998 | + }`, |
| 4999 | + expectViolation: false, |
| 5000 | + expectSeams: 0, |
| 5001 | + }, |
| 5002 | + { |
| 5003 | + // The live instance, reduced: a PURE helper matched on `find`, and |
| 5004 | + // the wrapper recursion then attributed the "read" to its caller — |
| 5005 | + // whose try guards a MetadataService call and no storage read at all. |
| 5006 | + name: 'passes: #11921 — an array `find` inside a same-file helper does not make its CALLER a read seam', |
| 5007 | + code: ` |
| 5008 | + function mergeOverlay(base: unknown[], records: any[]) { |
| 5009 | + const list = base as any[]; |
| 5010 | + const out: unknown[] = []; |
| 5011 | + // SAME-TICK on purpose: the wrapper recursion does not descend |
| 5012 | + // into a nested function body, so spelling this as |
| 5013 | + // \`records.map((r) => list.find(...))\` makes the fixture pass |
| 5014 | + // for a reason that has nothing to do with the shape test — |
| 5015 | + // measured, it stayed green under ablation. |
| 5016 | + for (const r of records) { |
| 5017 | + const prev = list.find((c) => c.pkg === r.pkg); |
| 5018 | + out.push(prev ?? r); |
| 5019 | + } |
| 5020 | + return out; |
| 5021 | + } |
| 5022 | + class P { |
| 5023 | + async getMetaItems(type: string) { |
| 5024 | + let items: unknown[] = []; |
| 5025 | + try { |
| 5026 | + const runtime = await this.service.list(type); |
| 5027 | + items = mergeOverlay(runtime, items); |
| 5028 | + } catch { |
| 5029 | + // MetadataService not available |
| 5030 | + } |
| 5031 | + return items; |
| 5032 | + } |
| 5033 | + }`, |
| 5034 | + expectViolation: false, |
| 5035 | + expectSeams: 0, |
| 5036 | + }, |
| 5037 | + { |
| 5038 | + // The other direction, and the one that keeps the shape test honest: |
| 5039 | + // it must SUBTRACT a false positive without ever subtracting a read. |
| 5040 | + name: 'flags: #11921 — a vocabulary name taking a callback is still followed as a same-file wrapper', |
| 5041 | + code: ` |
| 5042 | + class L { |
| 5043 | + private async find(pred: (r: any) => boolean) { |
| 5044 | + const rows = await this.driver.find(this.tableName, { where: {} }); |
| 5045 | + return rows.filter(pred); |
| 5046 | + } |
| 5047 | + async loadMany(type: string) { |
| 5048 | + try { return await this.find((r) => r.type === type); } |
| 5049 | + catch { return []; } |
| 5050 | + } |
| 5051 | + }`, |
| 5052 | + expectViolation: true, |
| 5053 | + expectSeams: 1, |
| 5054 | + }, |
| 5055 | + { |
| 5056 | + name: 'flags: #11921 — the shape test reads the FIRST argument, so a function in a query option changes nothing', |
| 5057 | + code: ` |
| 5058 | + class L { |
| 5059 | + async loadMany(type: string) { |
| 5060 | + try { return await this.driver.find(this.tableName, { where: { type }, map: (r: any) => r }); } |
| 5061 | + catch { return []; } |
| 5062 | + } |
| 5063 | + }`, |
| 5064 | + expectViolation: true, |
| 5065 | + expectSeams: 1, |
| 5066 | + }, |
| 5067 | + { |
| 5068 | + // The documented limitation, pinned rather than described. The test |
| 5069 | + // reads function LITERALS, so a named predicate still counts — the |
| 5070 | + // safe direction (over-count, never under-count), and the next |
| 5071 | + // reader sees where the line is instead of assuming it is elsewhere. |
| 5072 | + name: 'flags (documented limitation): #11921 — an array `find` with a NAMED predicate is still counted', |
| 5073 | + code: ` |
| 5074 | + function matches(c: { id: string }) { return c.id === 'a'; } |
| 5075 | + class L { |
| 5076 | + async pick(id: string) { |
| 5077 | + const list = [{ id: 'a' }]; |
| 5078 | + try { |
| 5079 | + const hit = list.find(matches); |
| 5080 | + return hit ?? null; |
| 5081 | + } catch { return null; } |
| 5082 | + } |
| 5083 | + }`, |
| 5084 | + expectViolation: true, |
| 5085 | + expectSeams: 1, |
| 5086 | + }, |
4816 | 5087 | ]; |
4817 | 5088 |
|
4818 | 5089 | let failures = 0; |
|
0 commit comments