Uh oh!
There was an error while loading. Please reload this page.
feat(devx): class-level keyed-text-bounds gate over every *.object.ts, superseding the three per-package pins - #12991
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CPrUz21stTFhJRUirdc4yw
… three per-package pins Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CPrUz21stTFhJRUirdc4yw
…yed-text-bounds-gate
…me it Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CPrUz21stTFhJRUirdc4yw
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
os-elon
commented
Aug 28, 2026
ACCEPT — armed ( Entry qualification: every check. 39 runs at ⭐ This gate found a live defect on its first run, in a package no pin ever covered. 15 unbounded keyed text columns in Triage's acceptance criteria, both met and both measuredPer-pin coverage, read off each pin's own runtime enumeration rather than argued:
The two the pin missed are ⭐ And the claim was decomposed before being made larger than it is: re-running each pin's enumeration under the emitter's 8-type family returns the same 95/5/8, so the family widening is future-proofing, ⛔ not currently-hidden defects. Resisting the bigger number is the harder call. All three pins retired on the rule half only, keeping the assertions the gate does not make — platform-objects' #11701 non-unique-ceiling describe, and the two plugins' referenced-column bound values plus the composite-key ceiling. Those are relations between columns that no presence check can see. The family is derived from the emitter, not duplicatedAll three pins hard-code a 4-type set; The ablation, including a prediction that was wrong and was reported as wrong
The count prediction was wrong and says so: the 15 ledgered rows are excused, so the report is exactly the one un-ledgered offender — "the delta is the ledger working, reported not smoothed over." ⭐ The green control is the best thing here. The unkeyed probe stays green while the text-family count moves 594 → 595, which proves the gate saw it and passed because it is not keyed — ⛔ not because it is blind. That is the difference between a control and a decoration. Import-free measured, not asserted: Five vacuity floors, and the gate refuses rather than passes on any shape it cannot classify that a declared index keys: One reading decomposed rather than accepted: platform-objects and plugin-security exclude
Generated by Claude Code |
os-elon
commented
Aug 28, 2026
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#12147
A class-level source-scanning gate for route A of #11374 — "a text-family column that a declared index keys on must declare a
maxLength" — replacing the duplicated rule in three per-package pins.All numbers below were measured on
f5668c004c, the head of this branch, and the gate union was re-run on that commit after the last one landed.Why a source scan, and why not a central pin
Not re-derived — adopted from the measurement on PR #12143. A central pin cannot import plugin objects: each plugin's
package.jsondeclares only the.export and the root barrel does not re-export./objects, and making it importable inverts the dependency graph, sinceplatform-objectsdepends only onmetadata-core+specwhile both plugins depend onplatform-objects.The gate found live members of the class — 15 of them, in a package no pin covers
The card allowed for a tree with zero offenders. It has 15, all in
packages/services/service-messaging, which never had a keyed-text-bounds pin:The sharpest is
sys_notification_delivery's(notification_id, recipient_id, channel)UNIQUE index — the outbox's dedup constraint, over three unbounded text columns. On MySQL that index does not exist at all, so duplicate deliveries are admitted.They are ledgered rather than repaired here, and filed as #12978, which stays open. Bounding them needs a NAMED producer per column (
topic,channelandprincipalare open vocabularies with no sibling to read a bound off), and declaring a bound moves the column TEXT tovarchar(n)— a drift op belonging to the services lane with a changeset, not to a devx gate PR. Repairing part of it would leave the composite UNIQUE index still inexpressible while reading as addressed.The ledger is enumerated, not a wildcard: a new unbounded keyed text column anywhere still fails, a row whose column gets bounded fails as stale, a row whose column stops being keyed text fails as stale, a row naming the wrong package fails, and a
pendingrow citing no issue fails.Per-pin coverage — the measurement, not the argument
Each pin's population was read off the pin's own enumeration (its runtime objects, driven through the same import or
init()path it uses) with a temporary probe, then set-compared against the gate's population for the same packages. The probes were deleted; they are not in this diff.platform-objectssys_metadata_commit.{package_id,parent_commit_id}plugin-auditplugin-securityThe two extra columns are the card's thesis reproducing itself.
platform-objects/src/metadata/index.tsis a hand-written back-compat re-export naming four objects;sys_metadata_commitmoved tometadata-corelater and was never added to it, so the pin enumeratingObject.values(PlatformObjects)could not see it. Both columns are bounded today, so nothing was broken — but nothing was watching them either, which is the same escape-by-boundary that widened this pin once before, whensys_import_job.created_byslipped past itsidentity/scoping.A count worth decomposing before it is read as more than it is. The pins hard-code
TEXT_FAMILY = {text, textarea, html, markdown}; the gate reads the family offdriver-sql's own switch and gets eight —richtext,code,signatureandqrcodejoined it in #11794 and #11875. On today's tree that widening changes nothing: re-running each pin's enumeration under the 8-type family gives the same 95 / 5 / 8. So the widening is future-proofing, not four types' worth of currently-hidden defects, and this PR does not claim otherwise.What was retired, and what deliberately stayed
Retired from all three pins: route A's rule itself, its vacuity control, its
UNBOUNDABLEallowlist, the allowlist-staleness test and the synthetic allowlist control. That is the duplication, and the gate carries every one of those as floors, a per-package ledger and self-test cases.Kept, because the gate does not assert it and should not:
platform-objects— the#11701describe. A different predicate with a different disposition: route A asks is there a bound, that asks is the declared bound small enough to be a key, and only for non-unique indexes, because a UNIQUE index over an unkeyable column is expressible after driver-sql (MySQL): full-value UNIQUE on >768-char token columns is inexpressible on utf8mb4 — hash-shadow-key route for the four ruled cases (C half of the #11374 ruling) #11627 while a non-unique one is not.sys_account.issuerat 2048 is the live illustration that the two rules are independent — it passes the gate and is out of that describe's scope. driver-sql (MySQL): full-value UNIQUE on >768-char token columns is inexpressible on utf8mb4 — hash-shadow-key route for the four ruled cases (C half of the #11374 ruling) #11627 is confirmed separate and is not folded in.plugin-audit— the ActivityPointer id columns carry 255, the width of the physicalidcolumn. The gate sees a positive integer and stops; it cannot see that a "tidy" to 100 would make the column unable to hold ids the id column accepts.plugin-security— the referenced-column bounds (255 / 100) and the composite-key ceiling. Same reason: a bound's correctness here is a RELATION to another column.Both remaining plugin files keep driving the real registration path, so an object added to the manifest is still covered with no second edit.
plugin-security's ceiling test gained an explicit non-vacuity assertion, because with no registration its sum was0 + 255— a ceiling check that passes while measuring nothing.The gate is import-free — measured, not asserted
git archive HEADinto a directory with 0node_modulesanywhere in it; production run and--self-testboth exit 0 there. The whole import closure is Node builtins plusscripts/invoked-as.mjsandscripts/js-comment-mask.mjs, the two shared modulescheck:entry-guardandcheck:comment-mask-adoptionrequire — no private comment stripper, no TypeScript parser.Ablation — predicted before running, then run
Predictions were written down first; the one delta is reported rather than smoothed over. Each leg proved its mutation on disk by occurrence count before the reading was taken, and proved its restore by
git diff HEADbeing empty and the blob hash matching the HEAD blob. No rebuild leg applies and that is a property of this gate, not an omission: it resolves nothing through a package'sexports— it reads.tsfiles as text and its own two imports as source.mjs— so nodist/can stand between the mutation and the reading.sys_webhook.os_ablation_probe [text] no maxLength--self-testduring both (control, must stay green)The delta. I predicted 16 reported offenders and observed 1, because I predicted the wrong quantity: the 15 ledgered columns are excused, so the reported list is exactly the un-ledgered new one. That is the ledger working, and it is the property that makes it safe to land with rows in it.
The unkeyed control is the half that makes this a real ablation rather than something that just breaks. Its counts decompose: text-family fields moved 594 to 595, so the gate saw the new column; keyed text columns stayed 151, so it stayed green because the column is not keyed — not because it is blind.
Refusals, floors, and the spellings it knows
A source scan sees only the spellings it knows, and an unrecognised one produces no finding, silently. Two mechanisms pay for that, both failing loud:
Fieldbuilder, a non-literaltype, a non-literalmaxLength, a spread insidefields:orindexes:, a non-literal index column, an index keying a column the object does not declare. Unclassified fields that nothing keys cost nothing and are counted in--list; there are currently 0 of them across the corpus, and 0 refusals.The text family is extracted from the emitter rather than retyped, with
EXPECTED_TEXT_FAMILYas a witness: when the two disagree the gate refuses and prints both sets, so a type joining or leaving the keyed-text arm is a decision someone makes rather than a silent change in what goes unpoliced.ROOT_DIR_WATCH_HINTSis declared so a dispatch brief can name this gate for a card that edits an object file. The walk stays repo-wide on purpose — narrowing it to the declared roots would rebuild the boundary-scoped hole one level up — and an object file discovered outside every hint is a refusal naming the constant, so the declaration cannot silently under-name the population.Verification
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackderived the family from the actual diff (STALE TREE on the first run; synced to2b4178aa5and re-derived). All 46 derived and convention-triggered gates run, exit code captured before any pipe, all 0.check:i18nfirst returnedPREREQUISITE NOT MET(the CLI was unbuilt) — read as NOT MEASURED, the CLI was built, and the real leg is 0.check:type-check-debtre-measured 31 ledger entries with none above its recorded number.One reading needed decomposing rather than accepting:
platform-objectsandplugin-securityexclude**/*.test.tsfrom their only tsc program, so their greentypechecksays nothing about the files this PR edits. Both were re-checked under a temporary config without that exclusion — the edited file is in the program in each case, with 0 errors attributable to it. The other errors that surfaced there (3 and 11, in files this PR does not touch) are the already-ledgered TEST_DEBT conditioncheck:type-check-coveragereports as 18 packages / 1188 frozen errors, so no new card is warranted.Changeset receipt: none,
skip-changesetinstead. Rootpackage.jsonisprivate: true, all three packages publish["dist","README.md","CHANGELOG.md"]so no edited test file ships, andscripts/and.github/are in no package. This PR publishes nothing.Generated by Claude Code