Skip to content

fix(spec)!: refuse multiple: true outside the multi-capable types, and derive driver-sql's storage from the spec predicate - #18187

Merged
os-warren merged 8 commits into
mainfrom
claude/issue-17469-multiple-non-capable-refused
Sep 14, 2026
Merged

os-warren merged 8 commits into
mainfrom
claude/issue-17469-multiple-non-capable-refused

Conversation

@os-warren

@os-warren os-warren commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

Fixes #17469

Maintainer ruling of 2026-09-13 (director seat decision batch #128 item 5, option 1′), implemented in full: one definition of "multi-valued". FieldSchema refuses multiple: true at the authoring entrance on every type outside MULTI_CAPABLE_TYPESMULTI_OPTION_TYPES, and @objectstack/driver-sql derives its storage decisions from isMultiValueField rather than reading the flag raw. MULTI_CAPABLE_TYPES and isMultiValueField are untouched, exactly as the ruling requires.

One PR across two domains, on the cross-domain exception path — ⛔ deliberately not split into a driver PR that could land without the refusal.

1 — Entrance refusal (packages/spec)

packages/spec/src/data/field.zod.ts, in the same superRefine and immediately after the #11437 radio check it generalises. The message names the field, its type, the declarable set and a type-aware remedy (lookup for a reference type, file/image for a media type, the option types otherwise).

The declarable set is derived from MULTI_CAPABLE_TYPESMULTI_OPTION_TYPES rather than re-listed (#12017 two-copies shape), and the remedy list filters radio out mechanically so the author is never sent from one refusal into another. Both are computed on first use rather than at module top level: field-value.zod reaches back into field.zod through shared/strict-objectshared/suggestions.zod, and spreading the sets at module scope threw TypeError: MULTI_CAPABLE_TYPES is not iterable in six spec suites — measured, then fixed.

radio is inside the set, so the two refusals never double-fire; a pin asserts exactly one issue on the multiple path for radio.

2 — Storage alignment (packages/drivers/driver-sql) — seventeen expressions, not one

The ruling named isJsonField as the instance it had measured, not as an exhaustive list. Measurement found sixteen more expressions asking the same question, pinned equal to one another by the package's own tests. Aligning only the named one re-opens #11535 in reverse: the DDL writer builds a JSON column that the read-side deserializer no longer recognises.

Every site below was read individually against the premise "this expression asks whether the value is multi-valued". The evidence column is the site's own contract text or its neighbours':

# site expression (before) why it means "is this multi-valued"
1 sql-driver.ts crossFieldComparisonClass if (decl.multiple) return null; its own docblock: refuses "every JSON-stored shape (multiple: true and the JSON_COLUMN_TYPES classes — element-wise semantics SQL comparison operators do not have)"
2 createColumn if (field.multiple) { jsonColumn; return; } the multi-value short-circuit above the type switch; JSON_COLUMN_TYPES' header calls itself the single source for "both the DDL column-type switch and isJsonField so the two can't drift"
3 isJsonField limb 1 if (!field.multiple && FILE_REFERENCE_TYPES.has(type)) selects the SINGLE-VALUE media arm; multiple is the multi/single discriminator
4 isJsonField limb 2 JSON_COLUMN_TYPES.has(type) || !!field.multiple the ruled site
5 varcharColumnChars if (field?.multiple) return null; its own comment: "multiple is decided before the type switch in createColumn — a JSON column, whatever the element type would have been". ⚠️ It is a width function, so it was read twice: it does not compute a width from multiple, it asks "is this a JSON column" and returns "no varchar width" as the consequence. Its pin (sql-driver-11565-row-byte-budget.test.ts) compares it against live columnInfo(), i.e. against createColumn
6–9 registerObjectMetadata fill !field.multiple on mediaCols / booleanCols / numericCols / numericValueCols mediaFields is the SINGLE-VALUE media registry; the boolean line's own comment says "SCALAR only … a multi-valued (JSON) column collapses the parsed array to true"; isNonTextColumn's docblock quotes the numeric one as "NUMERIC_SCALAR_TYPES.has(type) && !field.multiple, so the condition never reaches this predicate" — the condition being a multi-valued column is excluded
10–13 registerExternalObject fill the same four same registries, the other fill; #17586 measured that a repair to one leaves the other live
14–15 legacyMysqlTimestampColumns / legacyMysqlTimeColumns && !field?.multiple candidate sets for widening a legacy MySQL TIMESTAMP/TIME column. A multi-valued temporal field is a JSON column, not a temporal column, so it is not a candidate — the exclusion is the multi/single question
16 schema-drift.ts fieldHasColumn if (field?.multiple) return true; its docblock: "Mirrors SqlDriver.createColumn exactly … everything else — including multiple (a JSON column) — gets one"
17 schema-drift.ts declaresJsonColumn || field.multiple === true #15771 bound the differ's JSON-class predicate to the writer's; schema-drift.json-column-parity.test.ts pins the two equal over the whole vocabulary × multiple
18 schema-drift.ts declaresArray field.multiple === true || MULTI_OPTION_TYPES.has(declaredType) "Is the declared VALUE an array?" — character for character isMultiValueField's own definition

All of them now go through one module-local helper, isMultiValuedColumn(type, field), which is isMultiValueField({ type, multiple: field?.multiple === true }). It takes the resolved type because every caller had already applied its own field.type || 'string' default, and two spellings of that default is how this drift started.

⛔ Three field.multiple reads deliberately NOT aligned

sql-driver.ts lines ~15565 / ~15572 / ~15630 interpolate ', multiple' into an uncompilableFieldReferenceError message. They echo what the author declared back to them; they do not ask whether the value is multi-valued. The verdict on those paths comes from crossFieldComparisonClass (site 1), which is aligned. Premise tested per site, and it fails for exactly these three.

Consequence, stated plainly

multiple: true on boolean / toggle / number / currency / percent / date / datetime / time ceases to be a supported shape end to end — a consequence of ruled item 1, which refuses those declarations at the entrance, not something the driver half invents. Such a column is no longer a JSON column, so it is no longer excluded from the scalar read-coercion registries and the declared-type text-operator gate applies to it.

avatar / video / audio split from file / image for the same reason: only the latter two are MULTI_CAPABLE_TYPES members, so only they keep a deployment-independent JSON column under multiple: true. The other three follow the ADR-0104 arm like any single-value media column.

3 — Migration + changeset

packages/spec/src/migrations/entries/semantic/18.field-multiple-non-capable-type-refused.ts, registered into migrations/registry.ts by gen:migration-registry (⛔ the generated table was never hand-edited). No lossless conversion exists — the column was physically built as a JSON array — so the entry emits the structured TODO naming the surface, the replacement, the reason and how the author proves the hand-migration. It states the full narrowing, including that a stored field in the retired shape also leaves the JSON read path.

.changeset/17469-multiple-non-capable-type-refused.md: minor on both packages under the launch-window convention, BREAKING banner, FROM → TO mapping, and adr-0087: registered field-multiple-non-capable-type-refused. Clause-②: no — the diff narrows the accept set and widens nothing.

4 — Consumers

objectui#8886's pin ("multiple: true is INERT on a type outside MULTI_CAPABLE_TYPES") stays true and becomes enforced rather than tolerated. objectui#8937's "owed and not filed" is discharged. The objectui seat is to be notified when the spec version lands.

Test re-targeting — per test, before → after, and why it is the post-ruling contract

⛔ No test was deleted to make anything pass; no assertion was relaxed to "any answer is fine".

file before after why this is the contract, not a relaxation
packages/spec/src/data/field.test.ts +9 new tests: refusal over 14 non-declarable types on the multiple path; the three remedy shapes; the remedy list derived from the sets and never offering radio; every declarable type still accepted; multiple: false / absent still accepted; parse(parse(x)) stable; fires through ObjectSchema; radio keeps exactly one issue; MULTI_CAPABLE_TYPES / isMultiValueField untouched pure addition
value-roundtrip-conformance.ts v_multi: { type: 'string', multiple: true } { type: 'lookup', multiple: true } the route under test ("multi-value decides the column type before the type switch") is unchanged — lookup is not a JSON-class type, so it still reaches JSON storage by the multiple route alone. string is a driver alias, not a FieldType: the old spelling reached that route by a door the protocol had closed. All five driver cells re-run green
schema-drift.base-type-mismatch.test.ts ×4 { type: 'string'/'integer', multiple: true } { type: 'lookup', multiple: true } same assertions, byte for byte; only the type carrying the multi-value route changed, to the spelling the file already uses in its other 14 rows
schema-drift.json-column-parity.test.ts media loop asserted multiple: true media is JSON on both arms for all 5 asserts file/image on both arms and avatar/video/audio moving with the arm, with the two halves derived from MULTI_CAPABLE_TYPES and pinned by name (['file','image'] / ['audio','avatar','video']) strictly more assertions than before; the split is the ruled change made observable
same file non-vacuity control differReports({type:'string',multiple:true}) === true {type:'lookup',multiple:true} === true plus {type:'string',multiple:true} === false the old single control became a two-sided one: the positive moved to a shape that still exists, and the retired shape is now the negative control
schema-drift.unbounded-text-column.test.ts { type: 'signature', multiple: true } { type: 'image', multiple: true } the row asserts a multi-value field has no varchar bound to compare; image is multi-capable so it still is one
sql-driver-11223-updatemany-write-coercion.test.ts tags: { type: 'string', multiple: true } { type: 'select', multiple: true } identical assertions; the column is still the JSON column the bulk door must stringify into
sql-driver-target-field-provenance.test.ts ×10 JSON column made by { type: 'text', multiple: true } { type: 'select', multiple: true } all ten #8197 refusal/disclosure assertions unchanged; only the declaration that produces the JSON column moved
sql-driver-17590-json-column-membership.test.ts nums: { type:'number', multiple:true }; population sweep over string/boolean/datetime + multiple nums: { type:'select', multiple:true } (option values are numbers, so the members are still JSON numbers — the property the row is named for); sweep over select/lookup/user + multiple same rows, same expected ids, same construct regexes. The sweep's population is the post-ruling one; pinning the old one would pin a branch the writer no longer has
sql-driver-15683-temporal-text-operator-gate.test.ts "a MULTI-VALUED temporal column keeps $contains" over {type:'datetime',multiple:true} the carve-out asserted over a JSON column that still exists (select + multiple), plus a new assertion that {type:'datetime',multiple:true} now compiles 1 = 0 like the scalar beside it one assertion added. The carve-out is still asserted; the ruled change is asserted next to it
sql-driver-17343-…test.ts (rewritten) $contains over boolean/toggle/number + multiple answers by member; sweep asserting the gate never fires on a multi-valued non-text class same invariant over select/lookup/tags; the sweep now runs in both directions — the retired declarations are gated (they are scalars now) and every shape that still produces a JSON column is not; plus a new entrance-half block asserting FieldSchema refuses all of NON_TEXT_STORED_VALUE_TYPES + multiple, with a negative control that the multi-capable ones still parse the file's own fourth reading was "boolean + multiple: true is authorable — FieldSchema.multiple refuses exactly one type (radio)". That premise is what the ruling retires, so the file says so and pins both halves of the new contract. Test count went up (11 → 12 in the compiled block, + 2 entrance pins)
sql-driver-17586-…test.ts (rewritten) a stored [false] must not present as true; booleanFields must exclude multi-valued columns; four readers the array-survives-the-read assertions over the JSON columns that still exist; the registry rule restated as a partition (for (const f of jsonRegistry()) expect(booleanRegistry()).not.toContain(f)) with non-vacuity on both sides; the same four readers; plus two new pins — FieldSchema refuses boolean/toggle + multiple, and the retired declaration reads back as the scalar boolean it now is, in booleanFields and not in jsonFields the partition is stronger than the original per-column assertion: it quantifies over the whole JSON registry instead of naming two columns. The collapse the card is filed for has no reachable input any more, and the file states that rather than asserting it over a shape that cannot exist
sql-driver-json-column-operator-refusal.test.ts 9 rows refusing the #7398 operator family on "the normalised JSON column" ({type:'datetime',multiple:true} on an external object) the premise row still asserts the column reaches the normalised lowering family; one new row asserts it is no longer a JSON column and that the operators are therefore compiled rather than refused ⚠️ This is a coverage loss and it is named, not hidden. The normalised family is reached only through needsLegacyDatetimeRepair / needsLegacyTimeRepair, both of which require a declared temporal field — and no declared temporal field can be multi-valued any more. The intersection "JSON column × normalised lowering" is empty, so #7398's gate covers that family as defence only. Re-declaring the column as a multi-capable type would make it JSON again but drop it out of the normalised family, turning the block into a silent copy of the one above — the exact failure its own header names

Verification

run result
pnpm --filter @objectstack/spec test 477 files / 13606 tests passed, 0 failed
pnpm --filter @objectstack/spec typecheck pass (incl. check:scripts-typecheck, check:test-typecheck)
pnpm --filter @objectstack/spec check:generated All 15 generated artifacts up to date
pnpm --filter @objectstack/driver-sql test 177 files passed / 11 skipped, 2618 tests passed, 0 failed
pnpm --filter @objectstack/driver-sql typecheck pass
driver-memory / driver-sqlite-wasm / driver-turso / driver-mongodb test 1245 / 518 / 1249 / 562 passed, 0 failed — the four other consumers of the shared round-trip fixture
objectql / rest / lint / metadata test 4990 / 3196 / 3822 / 788 passed, 0 failed — the spec-contract consumers
derived gate sweep node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack114 families, all 114 run, exit codes captured to disk before any output was read; reconciled with --ran: 114 derived, 114 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero — every entry carries its code). Seven first exited 3 / PREREQUISITE NOT MET on an unbuilt package; after pnpm build (73 tasks) all seven are 0

Population re-measure, on origin/main 689d606f and again after the change: 0 fields carrying multiple: true on a non-capable type, by TypeScript-AST sweep over all tracked non-test .ts (2744 files), pairing every object literal that declares multiple: true with the type: in the same literal or the enclosing Field.TYPE() builder. Positive control fired both times (4 file, 4 select, 3 user, 2→3 lookup), so the zero is a reading. The one pre-change non-capable hit was value-roundtrip-conformance.ts's driver-alias fixture, re-spelled above.

Acceptance notes

  • The multiple key's .describe() now states the enforced set. packages/spec's files[] ships src/**/*.zod.ts literally, so that sentence goes to npm verbatim — it names FieldSchema parse as the door, which is the door the tests exercise.
  • packages/lint moved on main while this branch was open; the merge touched no packages/spec or packages/drivers file, so the generated spec artifacts are unaffected by it.
  • noted, not filed: the sql-driver.ts docblocks that quoted the old isJsonField body verbatim are updated in place (the jsonFields registry note and the JSON_COLUMN_TYPES media note). Successor: this PR.

Patch round — the four red CI jobs at 72a1f589, root-caused and fixed

The varcharColumnChars / row-byte-budget hypothesis is FALSIFIED. sql-driver-11565-row-byte-budget.test.ts PASSED in that same job (✓ … (10 tests) 421ms), and the InnoDB os11565_narrow record-size line in the MySQL container log is that suite's OWN negative-path fixture — the table it builds so the driver's prescriptive refusal has something to refuse. It is not a symptom.

red job real failing test root cause, tied to a diff line
Temporal Conformance (live PG + MySQL) sql-driver-17639-distinct-fault-envelope.test.ts › "the JSON-column refusal the card measured (live postgres)" — insert … invalid input syntax for type boolean: "{"false"}" createColumn's multi-value short-circuit (if (field.multiple)if (isMultiValuedColumn(…))). The PG-only fixture declared toggles: toggle, flags: boolean, nums: number + multiple: true and wrote arrays into them. Those are the retired shapes: the DDL now emits real scalar columns, so the array write is refused before the card's distinct() assertion is ever reached
Test Core (5/6) cli/generate-multiple-json-column.pin.test.ts › "driver-sql fieldHasColumn still answers the flag before the type" a SOURCE-TEXT pin on schema-drift.ts matching the literal if (field?.multiple) return true;
Test Core (6/6) cli/generate-field-type-vocabulary.pin.test.ts › the formula row it slices 200 chars after export function fieldHasColumn(; the new isMultiValueField({ … }) line is ~3× the width of the read it replaced and pushed !== 'formula' outside that window
Test Core (6/6) cli/generate-declared-column-default.pin.test.ts ×2 c_multiple: { type: 'text', multiple: true, defaultValue: 'x' } is no longer a JSON column, so createColumn reaches the default question and emits DEFAULT 'x'

⚠️ Why nothing local caught them, stated so the gap is closed and not just patched: (a) the population sweep excluded test files; (b) the live-PG/MySQL cells are SKIPPED without a server, and the local run reported 11 skipped files; (c) packages/cli was never in the local scope. All three are fixed below.

Patch-round test re-targeting — before → after

file before after why it is the contract, not a relaxation
sql-driver-17639-distinct-fault-envelope.test.ts JSON columns built from toggle/boolean/number + multiple; loop over ['toggles','flags','nums','tags_'] the same loop over ['picks','refs','people','tags_'] — multi-valued select/lookup/user plus tagsplus a new retired_flags control asserting that a boolean + multiple: true column ANSWERS distinct() because it is a scalar column now the file's own comment already says the class is "the json STORAGE, not the type"; the members of that class moved. One assertion added, and it is the one that reddens if the storage half of the ruling is reverted
cli/generate-multiple-json-column.pin.test.ts source pin /if \(field\.multiple\)/ on createColumn; source pin /if \(field\?\.multiple\) return true;/ on fieldHasColumn in a 300-char window /if \(isMultiValuedColumn\(/ and /if \(isMultiValueField\(/ in a 600-char window, plus a new assertion that !== 'formula' is still inside that window the pins exist to catch the driver moving its rule; they now pin the rule it moved to. The window assertion is new coverage, not a loosening
cli/generate-field-type-vocabulary.pin.test.ts slice(at, at + 200) slice(at, at + 600) the assertion (toContain("!== 'formula'")) is byte-identical; only the read window was resized to still contain the thing it asserts
cli/generate-declared-column-default.pin.test.ts c_multiple: { type: 'text', multiple: true, … } { type: 'lookup', multiple: true, … } the row's job is "the multi-value short-circuit returns before the default question"; lookup is multi-capable so it still does, and c_multiple stays in UNDEFAULTED unchanged
sql-driver-17586-…test.ts I had WEAKENED this file and did not disclose it. The PG named-divergence pin (distinct() over a JSON column → DATABASE_ERROR/500/cause 42883) was deleted rather than re-targeted, and the else branch went with it, so "the SCALAR boolean answers normally at the same door" and "reader 1 — the #11635 cast" ended up gated to non-PG cells only the named-divergence pin is restored, re-aimed at picks / tags_ (still JSON columns, still refused by PG's json-equality gap) and asserted on the CLASS so a one-column regression stops matching; the two controls are moved OUT of the distinctExecutes branch entirely, since PG is precisely the cell each of them is about declared here rather than left in the diff. Restoring is the only acceptable outcome for a weakened gate

Five shipping docblocks corrected

driver-sql builds with declaration: true, so this JSDoc reaches dist/index.d.ts. Corrected: mediaFields ("already covered by jsonFields through !!field.multiple"), booleanFields ("isJsonField reduces to !!field.multiple for these two types"), isNonTextColumn's numeric-limb quote, its claim of a pin in sql-driver-17586-… that no longer exists, and schema-drift.ts's JSON_COLUMN_FIELD_TYPES header still quoting … || !!field.multiple as the writer's predicate.

Patch-round verification

run result
driver-sql against a live PostgreSQL 16.13 — server timezone=Asia/Shanghai, process TZ=America/New_York, the shape CI uses 185 files passed / 3 skipped, 3299 tests passed, 0 failed. This is the run that could see the 11 cells the earlier local run skipped
@objectstack/cli vitest run --project unit 206 files / 2958 tests passed, 0 failed
@objectstack/spec test 477 files / 13606 tests passed
spec / driver-sql / cli typecheck all exit 0
derived gate sweep, re-derived at the patched head 117 families (3 more than before: check:i18n, check:i18n-coverage, check:i18n-walk-parity), all 117 run, exit codes to disk before any read, reconciled --ran: 117 derived, 117 run, 0 NOT-MEASURED, 0 UNRUN, every one exit 0
scripts/pm/check-clause2-carriers.mjs --pair 18187 was exit 4 on a T2 false positive at the multi-line import specifier list in field.zod.ts; the import is spelled on one line and it now exits 0

⛔ Deliberately NOT done in this PR

field.multiple is still read raw outside driver-sql: packages/cli/src/commands/generate.ts (:2617, :2443, :1706), driver-turso/src/remote-transport.ts:2435, objectql/src/engine.ts:4741 / :13584, runtime/src/action-execution.ts:1279. The ruling's scope was the driver, and packages/cli is another lane. ⚠️ The consequence is recorded rather than hidden: os generate migration and the driver now disagree for the retired shapes — the generator emits JSONB for a text field flagged multiple: true where the driver emits a varchar (#14829 in reverse). It is bounded, because that declaration is refused at the authoring entrance and can only reach the generators through the unvalidated door they explicitly serve. The CLI pin file's header now says so in place of its old "the column authority is the driver's flag-first rule" thesis.


Authored in Claude Code — session session_01KB5PFtxuy1x3dcR5gxudx6.


Generated by Claude Code

… driver-sql derives JSON storage from the spec predicate

Maintainer ruling 2026-09-13 (decision batch #128 item 5, option 1'): one
definition of "multi-valued". `FieldSchema` refuses an authored `multiple: true`
on any type outside MULTI_CAPABLE_TYPES union MULTI_OPTION_TYPES, and
driver-sql's `isJsonField` asks `isMultiValueField` instead of reading
`field.multiple` raw. `MULTI_CAPABLE_TYPES` and `isMultiValueField` are
untouched.

Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6
Co-authored-by: Claude <noreply@anthropic.com>
…nit cycle

`field-value.zod` reaches back into `field.zod` through
`shared/strict-object` -> `shared/suggestions.zod`, so spreading the two sets at
module top level threw `MULTI_CAPABLE_TYPES is not iterable` on the import
orders that enter `field-value.zod` first (six spec suites).

Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6
Co-authored-by: Claude <noreply@anthropic.com>
…pec predicate, and retriage the fixtures it moves

Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6
Co-authored-by: Claude <noreply@anthropic.com>
… the ADR-0087 entry

Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation protocol:data tests tooling labels Sep 14, 2026
@os-warren os-warren changed the title fix(spec)!: refuse outside the multi-capable types, and derive driver-sql's storage from the spec predicate fix(spec)!: refuse multiple: true outside the multi-capable types, and derive driver-sql's storage from the spec predicate Sep 14, 2026
@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/driver-sql, @objectstack/spec, touching 19 documentable anchor(s).

19 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json f0b2db30dabf08af0b543fee6642f315449bd5a5.

2 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json f0b2db30dabf08af0b543fee6642f315449bd5a5packageMentionDocs.

Which tree this was computed on

This run read content/docs from c5550294433c007ba970db550a3f41df7b09ab51 — the merge of head 3b5b8498f2c9f764cc5d9d39ed2ae89cd4034eb3 into base f0b2db30dabf08af0b543fee6642f315449bd5a5, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c5550294433c007ba970db550a3f41df7b09ab51 && git checkout c5550294433c007ba970db550a3f41df7b09ab51
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f0b2db30dabf08af0b543fee6642f315449bd5a5 3b5b8498f2c9f764cc5d9d39ed2ae89cd4034eb3 && git checkout -B drift-repro f0b2db30dabf08af0b543fee6642f315449bd5a5 && git merge --no-ff 3b5b8498f2c9f764cc5d9d39ed2ae89cd4034eb3

node scripts/docs-audit/affected-docs.mjs --json f0b2db30dabf08af0b543fee6642f315449bd5a5

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs f0b2db30dabf08af0b543fee6642f315449bd5a5 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…ue narrowing moves, restore the PG named-divergence pin, and correct five shipping docblocks

Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/xl tests tooling

Projects

None yet

2 participants