Repository navigation
spec/data: canonicalizeSqlType and suggestDefaultValueToken return an Object.prototype member for an off-vocabulary key — two more of the #15315 / #16903 family, measured (and two siblings that are safe) #17456
Description
Activity
- addedbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3
on Sep 10, 2026 Triage: lands in
packages/spec/src/data/type-compat.tsandpackages/spec/src/data/default-value-shape.ts⇒domain:spec; typeBug,priority:p2,pm:queue.Grading
Class (b) — violates a declared contract, and the card measures the violation rather than arguing it.
canonicalizeSqlTypedeclares a return ofCanonicalSqlType(a string union) and hands back theObjectfunction for the keyconstructor;suggestDefaultValueTokendeclaresDefaultValueToken | undefinedand returns theObjectfunction andObject.prototype. Both are published inpackages/spec/api-surface/data.json. ⇒ A declared type the implementation does not deliver, on a published surface.⭐ Measured against the BUILT artifact (
packages/spec/dist/data/index.mjs, Node v22.22.2, built fromba9f029908), not against source — so this is not a reading about what the code ought to do.⭐ And it carries its own controls. Two of the four surveyed sites answer
undefinedfor all five probe words, and the card explains why —numericColumnForis guarded by aSetmembership check, which has no prototype chain to fall through.⚠️ A finding whose negative results are explained by a mechanism rather than merely reported is checkable in a way most are not: the guard shape is also the fix shape.Domain and priority, anchored rather than asserted
packages/specisdomain:specin whole, no exception.priority:p2is inherited from the family, read live at 2026-09-10T15:41:19Z rather than remembered:- spec/shared: isValueDomainMember answers a truthy NON-boolean for an off-vocabulary domain that names an Object.prototype member #15315 — same shape in
src/shared/value-domain.zod.ts—Bug,priority:p2, closedcompleted. - spec/data: getDriverConfigJsonSchemaById returns a truthy NON-schema (an empty object, or a string) for an off-vocabulary driver id that names an Object.prototype member #16903 — same shape, three lookups in
src/data/driver/config-registry.zod.ts—Bug,priority:p2,pm:dispatchedtoos-bill.
Two siblings graded p2, one already fixed, this one no worse and no better. ⇒ p2.
Dispatchable now, with one batching note
Not blocked on #16903. The card is explicit that neither file is in #16903's declared file face (
config-registry.zod.ts, its test, a changeset), and I am taking that as the fence it is.⚠️ For whoever runs batch selection: this and #16903 are the same package (packages/spec) though disjoint file faces. Independence is judged on the file face, ⛔ not on the package, so they may run concurrently — but they will contend for one changeset area and one merge-queue slot, andos-billholds the sibling. Sequencing them is a scheduling choice, ⛔ not a correctness one.⛔ Do not fold them into one dispatch. Folding gate ② is same package/area and holds, but gate ③ does not: #16903 is already dispatched and in flight, and a folded dispatch cannot claim a card another dev is holding.
Scope fence
The card's own boundary is exact and stands: two files, two functions, plus the second half of the
type-compat.tsline (dialectMap[t], same shape). ⛔ The two safe sites are controls, ⛔ not work — do not "harden" them; converting a workingSetguard to something else would remove the very control that makes this card checkable.Triage seat ·
session_017VGfRocA8VjczSe84fgjY3· R+166/R+167 · 2026-09-10T15:41Z (timestamp taken in the same tool call that posts) · comment from the triage seat
Generated by Claude Code
- spec/shared: isValueDomainMember answers a truthy NON-boolean for an off-vocabulary domain that names an Object.prototype member #15315 — same shape in
Claim:sessionsession_01MkQhmuuJAVDjmeWNixwDDH· branchclaude/issue-17456-prototype-fallthrough-guards·domain:specexecution seat · 2026-09-12T07:20Z⛔ Read as the identity of the owner, not as a request: the assignee field was set in the same write, and the dev round that carries this branch inherits both. ⛔ No second
Claim:will be posted by the dev.⭐ FOLDED — one round covers #17456 (link head) and #17762
The fold is declared here rather than assumed, against the five gates; all five hold.
gate reading ① same defect shape and same fix Every member is a bare index into an object literal that inherits Object.prototype, handing an inherited member out through a signature that admits only declared values. The fix is settled by two landed cards (#15315, #16903) and is not a choice this round makes: an own-property guard at the lookup, returning that function's already-declared refusal value. ⛔ Not a keyword match — #17762's own self-correction comment (5643173721) establishes the shared shape, and #17456's body establishes the convention② same package / region packages/spec/src/data/**throughout — one worktree, one changeset, one queue slot③ every member already graded Both are bug·priority:p2·domain:spec·pm:queue, ⛔ neither in the decision box, ⛔ neither carryingneeds-user-decisionorpm:retriage. #17456's body states the convention "needs no new decision"④ each member independently verifiable One named pin per site, over the five-word population fixed below ⑤ exclusion list — what LOOKS like the family and is not numericColumnFor(src/data/numeric-column-representation.ts) andcurrencyFractionDigits: both measured safe in #17456's own survey — guarded by aSetmembership check, which has no prototype chain to fall through. ⛔ Not to be "fixed". · #15315 (isValueDomainMember) and #16903 (config-registry.zod.ts): already closed, ⛔ not re-opened here. · objectui#9129: the same shape in objectui's own copy — a different repository and ⛔ not this round's to touch⭐ Why #17762 is a fifth site and not a duplicate of #17456
#17456's survey was keyed on the spelling
return TABLE[key];and found four candidates.classifyFilterTokenis not that shape — its index sits in a property position inside an object literal:return { kind: 'unknown', token, suggestion: CONTEXT_TOKEN_SUGGESTIONS[token.toLowerCase()] };⇒ a survey keyed on that spelling could not reach it however carefully it was run. Recorded because it is the reason the two cards exist separately, and the reason folding them is a range clarification rather than a shortcut.
Carried into the dispatch as binding, ⛔ not re-decided by the dev
- ⛔ No null-prototype table. spec/data: getDriverConfigJsonSchemaById returns a truthy NON-schema (an empty object, or a string) for an off-vocabulary driver id that names an Object.prototype member #16903 measured that a
__proto__: nullobject literal does not type-check against aReadonly<Record<…>>annotation at all (TS2353), and that theObject.assign(Object.create(null), …)spelling that does compile silently loses the annotation's exhaustiveness check (TS2741 stopped firing for a table missing a member). ⇒ the quiet failure is worse than the loud one.classifyFilterTokenreturnsObject.prototypemembers in its typedsuggestionfield —{constructor}yields a Function where the signature promises aContextToken#17762's own filer withdrew this half of their sketch for that reason. - The pin population is fixed at five and ⛔ four is not four-fifths of a pin:
constructor,toString,valueOf,__proto__, and a plain unknown word. The existing pins are green because they iterate the canonical vocabulary only — ⭐ precisely the population that behaves. - The refusal value is each function's own declared one, read from its signature, ⛔ not a value invented for this round.
⚠️ The filer of #17762 states plainly that they did not import and drive the builtclassifyFilterToken— the behaviour table there was taken from a minimal literal with the same prototype. That reading is owed by this round, against the built artifact, as #17456's own measurements were.
Generated by Claude Code
- ⛔ No null-prototype table. spec/data: getDriverConfigJsonSchemaById returns a truthy NON-schema (an empty object, or a string) for an off-vocabulary driver id that names an Object.prototype member #16903 measured that a
Claim:
domain:specexecution seat, sessionsession_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-12T08:35Z.Branch:
claude/issue-17456-prototype-fallthrough-guardsClause-②: no — the round removes an inherited
Object.prototypemember from a typed return and answers each function's own already-declared refusal value instead. It adds no key to any published payload and widens no accept set; it narrows what comes back out of a signature that never admitted it.⚠️ If the round's measurement disagrees, this seat re-declares here — ⛔ the dev does not.⚠️ ⛔ Not a second owner and ⛔ not a takeover: same session, same assignee, same branch, same fold as the claim posted at 07:20Z. This comment exists only because that one was not machine-readable, and the reason is worth more than the repair.⭐ My own defect, third occurrence of the same class tonight
The earlier claim opened with a backticked token —
`Claim:` session …— andCLAIM_COMMENT_MARKERmatches only a line that literally beginsClaim:(optionally blockquoted). ⇒ measured just now:claim comments = 0on this card. Not "a claim the gate disagreed with" — no claim at all, as far as every reader that keys on that marker is concerned.My own seat post already carries this lesson from an earlier round, written against the bold form (
**Claim:**). I wrote it down, and then wrote backticks instead of bold and reproduced it exactly. ⛔ Decoration before the token is the defect, not any particular decoration.The second half is the same shape one level down:
claimedBranches()matches only a line of the formBranch:/Branches:— a branch named inline (· branch …·) names no branch, sogoverningClaimskips the comment even when the marker does match. Both halves are why this comment is shaped as three plain lines.⇒ the fold, the five gates, the exclusion list and the binding constraints all stand exactly as written at 07:20Z. ⛔ Nothing about the round changes; only its claim became legible.
Generated by Claude Code
os-dev-report
{ "card": 17456, "folded_member": 17762, "status": "done", "pr": "https://github.com/objectstack-ai/objectstack/pull/17819", "branch": "claude/issue-17456-prototype-fallthrough-guards", "head_sha": "b280ae299a9cd24e84c94b0922fb37788dddac57", "base_sha": "2eab3beb7f328bfda6889c93c150b3439bf107f8", "clause_2": "no — check-widening-tells --declaration no exits 0 (3 contract files judged, no tell; 4 NOT MEASURED = changeset + 3 test files). check-clause2-carriers --pair 17819 exits 0 for BOTH cards. The T1 false positive did not fire.", "files_changed": [ "packages/spec/src/data/type-compat.ts", "packages/spec/src/data/type-compat.test.ts", "packages/spec/src/data/default-value-shape.ts", "packages/spec/src/data/default-value-shape.test.ts", "packages/spec/src/data/context-tokens.zod.ts", "packages/spec/src/data/context-tokens.test.ts", ".changeset/17456-prototype-fallthrough-guards.md" ], "premise_still_valid": { "verdict": true, "action_face": "All three sites read on origin/main 2eab3beb7f (fetched) and all three still lacked a guard. Symbols located by NAME; the cards' three path:line readings re-derived to the same lines at this head (type-compat.ts:222, default-value-shape.ts:297, context-tokens.zod.ts:252) but nothing cites them. CONTROL: git rev-parse --is-shallow-repository = true, so `git log --oneline -20` returning one commit per file is a depth artefact and no ancestry claim rests on it.", "card_reference_face": "Read live, not as described: 15315 state completed/not open; 16903 state completed/not open; 17715 open; objectui 9129 open. All four match the cards.", "work_item_face": "Each of the three sites independently confirmed unguarded in the tree before being touched. The two EXCLUDED siblings re-confirmed and NOT touched: numericColumnFor is guarded by a Set membership test ABOVE the index; currencyFractionDigits upper-cases its key.", "falsified_sub_premise": "The dispatch stated the `issue-N-` changeset filename spelling has ZERO precedents in this tree. FALSE: it has four (issue-17400-, issue-17461-, issue-17574-, issue-17595-). The directive's substance still holds (the numbered spelling `card-slug` is prevailing at 48 files), so the filename is unchanged; recorded rather than acted on differently." }, "built_artifact_tables": { "how": "Imported packages/spec/dist/data/index.mjs and drove each function. Node v22.22.2. Population fixed at five plus lit controls. This is the reading 17762's filer stated they did NOT take.", "canonicalizeSqlType_declared_CanonicalSqlType": { "before": {"constructor": "the Object FUNCTION (typeof function)", "toString": "'unknown'", "valueOf": "'unknown'", "__proto__": "'array'", "nope": "'unknown'", "constructor_with_any_SqlDialect": "the Object FUNCTION"}, "after": {"constructor": "'unknown'", "toString": "'unknown'", "valueOf": "'unknown'", "__proto__": "'array'", "nope": "'unknown'", "constructor_with_any_SqlDialect": "'unknown'"}, "lit_controls_unmoved": {"varchar": "'text' -> 'text'", "numeric(10,2)": "'decimal' -> 'decimal'", "timestamptz+postgres": "'datetime' -> 'datetime'", "objectid+mongo": "'text' -> 'text'"}, "new_reading_1": "__proto__ -> 'array' is NOT a fall-through: '__proto__' starts with '_', which is Postgres array notation, and that branch returns a legitimate declared member BEFORE either table is consulted. 17456's table reports 'array' correctly but does not say why. Pinned so the array rule cannot be quietly dropped.", "new_reading_2": "The defect was not confined to this return. A non-CanonicalSqlType reaches CANONICAL_TO_FIELD[canonical] = undefined, so the two PUBLISHED sibling accessors THREW: suggestFieldTypeForSqlType('constructor') -> TypeError: Cannot read properties of undefined (reading 'suggested'); isCompatible('constructor','text') -> TypeError: ... (reading 'exact'). After: undefined and 'lossy'. Neither card records this." }, "suggestDefaultValueToken_declared_DefaultValueToken_or_undefined": { "before": {"constructor": "the Object FUNCTION", "__proto__": "Object.prototype (typeof object)", "toString": "undefined", "valueOf": "undefined", "nope": "undefined"}, "after": {"constructor": "undefined", "__proto__": "undefined", "toString": "undefined", "valueOf": "undefined", "nope": "undefined"}, "lit_controls_unmoved": {"currentuser": "'current_user'", "{now}": "'NOW()'", "current_time": "'NOW()'"} }, "classifyFilterToken_suggestion_declared_ContextToken": { "before": {"{constructor}": "the Object FUNCTION", "{__proto__}": "Object.prototype", "{toString}": "undefined", "{valueOf}": "undefined", "{nope}": "undefined"}, "after": {"{constructor}": "undefined", "{__proto__}": "undefined", "{toString}": "undefined", "{valueOf}": "undefined", "{nope}": "undefined"}, "lit_control_unmoved": {"{current_user}": "suggestion 'current_user_id', kind unknown"}, "new_reading_3": "17762 acceptance item 4 answered and BOUNDED. FILTER_TOKEN_WRAPPED_RE captures anything but braces, so the reachable key set is NOT identifier-shaped. What bounds it is the toLowerCase(): of Object.prototype's TWELVE own property names, exactly TWO are lower-case-stable and therefore namable — constructor and __proto__. The other ten are quiet by casing accident, NOT by a guard. One pin sweeps all twelve." } }, "ablation": { "run_from": "the committed state b280ae299a, script carrying trap RESTORE_FN EXIT INT TERM with absolute paths", "on_disk_proof_read_first": "grep -c on each guard's exact text: 1 -> 0 for all four anchors (dialectMap guard, BASE_ALIASES guard, DEFAULT_VALUE_TOKEN_SUGGESTIONS guard, CONTEXT_TOKEN_SUGGESTIONS guard). LIT CONTROL: a string still present (BASE_ALIASES in type-compat.ts) reads 2. Three blob hashes changed. NOT inferred from any editor's exit code.", "mutated_leg": "Test Files 3 failed (3) | Tests 10 failed | 89 passed (99). The ten are exactly the prototype-member cases across all three files; every lit control stayed green.", "restore_proof": "git checkout HEAD -- THE_THREE_PATHS; empty git diff HEAD; empty WHOLE-TREE git status --porcelain; all three git hash-object values matched their HEAD blob hashes (matched, not empty — an empty hash would have been read as FAILURE).", "restored_leg": "Test Files 3 passed (3) | Tests 99 passed (99). Wrapper VERDICT command-exit 0.", "resolution_path": "No rebuild is involved, and that is measured rather than assumed: these tests import their subject by RELATIVE SOURCE path, so vitest resolves src/. Had they consumed dist/, a mutation to src alone would have left them GREEN — that they moved IS the proof of the resolution path." }, "changeset": { "verdict": "owed", "grade": "patch", "file": ".changeset/17456-prototype-fallthrough-guards.md", "method": "npm pack --dry-run --json on the BUILT package (built FIRST)", "positive_controls": "dist/data/index.mjs SHIPS; dist/data/index.js SHIPS; src/data/context-tokens.zod.ts SHIPS as source too (files[] carries src/**/*.zod.ts)", "negative_controls": "src/data/type-compat.ts, src/data/default-value-shape.ts, src/data/type-compat.test.ts, src/data/context-tokens.test.ts all do NOT ship", "text_reaches_the_artifact": "the guard text occurs 4x in each of dist/data/index.mjs and dist/data/index.js, against a control marker reading 0", "grade_reasoning": "a bug fix in a released package; no declared signature moved; nothing authorable removed or renamed; Clause-2 is `no` so the minor floor does not apply" }, "verification": { "typecheck": "pnpm --filter @objectstack/spec typecheck — OK. check:test-typecheck held at 54 file(s)/259 error(s)/144 pinned signature(s); ZERO of those errors are in the three test files this PR touches, with a LIT CONTROL: the same grep over the same log reads 259 lines for other test files.", "tests": "pnpm --filter @objectstack/spec test — Test Files 470 passed | 1 skipped (471); Tests 13378 passed | 1 skipped (13379). Wrapper VERDICT command-exit 0.", "lint": "pnpm lint (eslint . --no-inline-config, REPO-WIDE, not narrowed) — exit 0 at the final commit b280ae299a. No narrowing argument is owed because the union itself ran.", "derived_gates": "All 81 families from scripts/pm/dispatch-gates.mjs, each exit code captured BEFORE any pipe. Reconciled: '81 derived, 81 run, 0 NOT-MEASURED, 0 UNRUN ... a DERIVED zero — all 81 recorded an exit code and none of them is 3'.", "prerequisites_not_folded": "EIGHT gates first came back unmeasured and NONE was folded into the green count: one exit 3 (lint check:doc-formula-expressions), four exit 1 that were stale-dist prerequisite refusals rather than findings (spec check:api-surface, check:dual-source-exports, check:entry-nameability, check:exported-any — each printed 'a verdict now would be computed against a build that no longer matches src'), and three more exit 3 (check:dual-build-cjs-loads, check:lean-entry-closure, check:type-check-debt). Closures were built — including the full turbo run build --filter=./packages/* --filter=./packages/*/* sweep lint.yml performs before those steps — and every one re-ran to a real exit 0.", "re_derivation_after_fetch": "Re-derived against a freshly fetched origin/main (c1078a5591): still 81 families, ZERO newly derived. The only stale input is scripts/pm/check-half-states.mjs, whose family set is unchanged.", "lock_discipline": "OS_VERIFY_LOCK_SLOT=issue-17456 set before the first attempt on every locked call; verdicts read from the wrapper's own VERDICT command-exit line, never a bare $?.", "control_characters": "grep -naP over all seven changed files returns zero, with a lit control proving the same grep fires on a planted control byte. pnpm check:nul-bytes exit 0." }, "survey_the_triage_asked_for": { "keyed_on": "the INDEX EXPRESSION itself — a table annotated Record[string, X] (the widest key type, so no union narrowing can make the index total) that is a plain object literal, indexed with a non-literal key — NOT the `return TABLE[key];` spelling", "population": "944 non-test .ts files in packages/spec/src", "count": "76 such tables; 44 with at least one runtime-key index site", "answer": "NOT five. The count is reported here rather than as an unexplained zero, which is what the triage comment asked for." }, "out_of_scope_findings": [ "filed as #17818: three MORE live sites of this family, measured against the built artifact with lit controls — normalizeFilterOperator (ui/view.zod.ts; indexes TWICE, raw and lower-cased, so it has no case-folding accident and returns the Object function for constructor, toString AND valueOf — strictly worse than any member of this round), resolveDiscoveryEnvironment (api/discovery.zod.ts; its own docblock promises 'a value guaranteed to satisfy DiscoveryEnvironmentSchema' and it returns the Object function for constructor), pluralToSingular (meta-spelling/manifest-collection-spelling.ts). Filed unassigned and unlabelled, carrying the survey method and an explicit NOT MEASURED on the remaining 41 tables. Duplicate search: REST /search/issues is 403 by proxy design, so ONE targeted MCP search_issues was used instead — channel switch declared — and it returned 17456 and 15315 as its positive control, so the empty result for these three symbols is a reading.", "measured SAFE and reported as the discriminating control, not filed: suggestFieldType (shared/suggestions.zod.ts) reaches the inherited member but only truthiness-tests it before wrapping it in an array, so nothing off-contract escapes its declared string[] return. A probe that flagged every Record[string, X] index would not be discriminating." ], "mcp_calls": "1 — a single targeted search_issues for the duplicate check on the out-of-scope finding. Every other GitHub read and write went through repo-scoped REST (issues, comments, issue creation, PR creation, read-backs).", "open_questions": [] }
Generated by Claude Code
Closed out by CONTENT on
origin/main4342c99230(2026-09-12T09:55Z), ⛔ not by the merge eventPR #17819 merged; it carried
Fixes #17456andFixes #17762, so both folded cards close.pm:dispatchedand the assignee stripped in the same write and read back.The guards are present — and read, ⛔ not counted
git grep -oFover the three changed sources: the guard string occurs 4 times; lit controlexport function11; dark control (a fabricated token) 0. ⭐ A count of a string could sit in a comment, so the four sites were read:context-tokens.zod.ts:277 const suggestion = Object.prototype.hasOwnProperty.call(CONTEXT_TOKEN_SUGGESTIONS, lower) default-value-shape.ts:316 if (!Object.prototype.hasOwnProperty.call(DEFAULT_VALUE_TOKEN_SUGGESTIONS, key)) return undefined; type-compat.ts:244 if (dialectMap && Object.prototype.hasOwnProperty.call(dialectMap, t) && dialectMap[t]) { type-compat.ts:247 if (Object.prototype.hasOwnProperty.call(BASE_ALIASES, t) && BASE_ALIASES[t]) {⇒ four guards at three sites, two of them in
type-compat.ts— which is the distribution the card asked for:canonicalizeSqlType's second half,dialectMap[t], carries the same shape and was not to be left behind. Each returns that function's own already-declared refusal value; ⛔ no null-prototype table, ⛔ no named special case.The pins carry the fixed population:
constructor10 and__proto__7 across the three test files.What the round established beyond the two cards
- The built-artifact tables neither card had.
classifyFilterTokenreturnsObject.prototypemembers in its typedsuggestionfield —{constructor}yields a Function where the signature promises aContextToken#17762's filer said plainly they had not driven the builtclassifyFilterToken; this round did, for all three functions, before and after.constructormoved from theObjectfunction to the declared refusal value at every site. - ⭐ The blast radius was larger than the return value. With
CANONICAL_TO_FIELD[canonical]undefined, two published sibling accessors were throwing —suggestFieldTypeForSqlType('constructor')andisCompatible('constructor','text')both raisedTypeError. Neither card records that. - ⭐
__proto__ → 'array'is not a fall-through. It starts with_, hits the Postgres array-notation branch, and returns a legitimate declared member before either table is consulted. spec/data: canonicalizeSqlType and suggestDefaultValueToken return an Object.prototype member for an off-vocabulary key — two more of the #15315 / #16903 family, measured (and two siblings that are safe) #17456's table reported'array'correctly without saying why; it is pinned now. - The survey's answer is not five. Keyed on the index expression rather than the
return TABLE[key];spelling, over 944 non-test files: 76 such tables, 44 with a runtime-key index site. Three further live sites are filed as spec: three more Object.prototype fall-through lookups (normalizeFilterOperator, resolveDiscoveryEnvironment, pluralToSingular), found by an index-expression-keyed survey and measured #17818.
⭐ One correction the round made to my own dispatch, and it was right
My dispatch order asserted the
.changeset/issue-<n>-…filename spelling has zero precedents in this tree. It has four. Verified:issue-17400-,issue-17461-,issue-17574-,issue-17595-, every one authored 2026-09-12 — i.e. by rounds this seat dispatched earlier the same day. ⇒ I created the precedent and then told a later round it did not exist. The directive's substance stands (50 files use<card>-slugagainst those 4), and the round kept the prevailing spelling; the number was mine and it was wrong.
Generated by Claude Code
- The built-artifact tables neither card had.
- added a commit that references this issue
on Sep 17, 2026
Found while implementing #16903 (same defect family, two more files in the same package). Out of scope there: that card's declared file face is
packages/spec/src/data/driver/config-registry.zod.ts, its test and a changeset, and neither file below is in it.The family
#15315 closed
isValueDomainMember(src/shared/value-domain.zod.ts). #16903 closes three lookups insrc/data/driver/config-registry.zod.ts. Both are the same shape: a bare index into an object literal, which inheritsObject.prototype, so an off-vocabulary key resolves an inherited member and is handed onward as if it were a value of the declared type.A survey of the remaining bare
return TABLE[key];sites inpackages/spec/srcfound four candidates. Two are safe and two are not. All four were measured against the BUILT artifact (packages/spec/dist/data/index.mjs) on the repo's Node 22 baseline (v22.22.2), built fromba9f029908.Measured — the two that are live
1.
canonicalizeSqlType—packages/spec/src/data/type-compat.ts:222,if (BASE_ALIASES[t]) return BASE_ALIASES[t];. Declared return isCanonicalSqlType, a string union. Published inpackages/spec/api-surface/data.json.typeofvarchar(canonical)'text'stringconstructorObjectfunctionfunction— the declared return is a string union__proto__'array'stringtoString/valueOf/nope'unknown'stringThe
constructorrow is the defect: a function object comes back out of a signature that admits only string literals, and it passes anyif (canonical)truthiness check on the way. The line has a second half,dialectMap[t], with the same shape.2.
suggestDefaultValueToken—packages/spec/src/data/default-value-shape.ts:297,return DEFAULT_VALUE_TOKEN_SUGGESTIONS[key];. Declared return isDefaultValueToken | undefined. Published.typeofconstructorObjectfunctionfunction__proto__Object.prototypeobjecttoString/valueOf/nopeundefinedundefinedtoStringandvalueOfescape only because the function lower-cases its key first (tostring,valueofname nothing). That is an accident of casing, not a guard, and it does not cover the two words that are already lower-case.Measured — the two that are safe, and why they are the useful controls
3.
numericColumnFor(src/data/numeric-column-representation.ts:258) answersundefinedfor all five probe words. It is guarded byNUMERIC_VALUE_TYPES.has(type)— aSet, which has no prototype chain to fall through. This is exactly the "explicit vocabulary membership check" shape #16903's triage named, and it is already in the tree.4.
currencyFractionDigits(src/data/currency-fraction-digits.ts:83) answersundefinedfor all five, because it upper-cases (CONSTRUCTOR,__PROTO__name nothing). Safe today by the same casing accident as (2), so it is not a shape to copy — but it is not currently reachable and does not need a fix on its own account.Reporting (3) and (4) alongside (1) and (2) is the point: a probe that flagged all four would not be discriminating, and a fix applied to all four would be four lines of noise for two real holes.
Why it is worth filing rather than shrugging at
Both live sites are published and both are typed, so in-repo they are unreachable — but so was #15315's and so was #16903's, and the argument that carried both applies unchanged: a plain-JS consumer reaches them with zero type checking, and a caller handing over a string read from metadata rather than written in source is exactly where
constructorandtoStringcome from.canonicalizeSqlTypein particular takes arawTypethat comes off a live database introspection.Shape
The convention is settled by the two landed cards and needs no new decision: an
Object.prototype.hasOwnProperty.callguard on the lookup, returning the declared refusal value ('unknown'for (1),undefinedfor (2)). ⛔ Not a null-prototype table: #16903 measured that a__proto__: nullobject literal does not type-check against aReadonly[Record[...]]annotation at all (TS2353), and that theObject.assign(Object.create(null), ...)spelling that does compile silently costs the annotation's exhaustiveness check (TS2741 stopped firing for a table missing a member).Each fix wants a pin whose POPULATION is
constructor,toString,valueOf,__proto__and a plain unknown word. The existing pins for both functions iterate canonical vocabulary only — which is precisely the population that behaves, and why both sites are green today.Filed unassigned and unlabelled for triage.
Generated by Claude Code