fix(spec): describe format by the readers that exist, not by email/phone - #19763
Conversation
…phone
`FieldSchema.format` was described as `Format string (e.g. email, phone)`,
published verbatim to `content/docs/references/data/field.mdx`. Nothing
honours `email` or `phone` for this key. `email`, `url` and `phone` are
field TYPES; the `email | url | phone | json` vocabulary belongs to a
`format` VALIDATION RULE, one schema over.
What actually reads the key, measured:
- `resolveAutonumberFormat` (`data/autonumber-format.ts`), called by the
ObjectQL engine's `applyAutonumbers` and twice by driver-sql: on an
`autonumber` field this is the record-number pattern, the shorthand
that predates `autonumberFormat`. So an author following the old
sentence and writing `format: 'email'` gets a pattern with no `{...}`
token, which renders as literal text plus the bare counter: `email1`.
- objectui's `DateCellRenderer` / `DateTimeCellRenderer` at the pinned
`.objectui-sha`: a display style, with a different vocabulary and a
different default per type.
The new description names both readings, says the spec declares no
vocabulary and validates nothing, and redirects value constraints to the
field `type` and to the `format` validation rule. Scope is the sentence:
no key is split, narrowed or retired, and no consumer moves.
Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr
Co-authored-by: Claude <noreply@anthropic.com>
`pnpm --filter @objectstack/spec gen:docs`, from a `dist` this branch built. Three projections of one describe string move: `data/field.mdx`, `data/object.mdx` (twice) and `system/migration.mdx` (twice). `check:authorable-surface` stayed green across the edit, so `authorable-surface/data.json` and its `.base.json` anchor do not move — a description is not an authorable key. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude <noreply@anthropic.com>
…ion repair Measured rather than assumed: the changed bytes reach three entries of this package's own `files[]`. `packages/spec/src/data/field.zod.ts` is itself shipped (`src/**/*.zod.ts`), and the new text greps out of 22 `dist/` files and 13 `json-schema/` files — the same counts a positive control from the same schema (`required`'s published describe) returns over the same two trees. `skip-changeset` would have been wrong. `Clause-②: no`: the key stays `z.string().optional()` and no accept set moves in either direction. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude <noreply@anthropic.com>
…rmat-describe-vocabulary
📓 Docs Drift CheckThis PR changes 1 package(s): 12 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b3dc8c511f8f5d6d1aa2b349240f5caeac3a6ed3 && git checkout b3dc8c511f8f5d6d1aa2b349240f5caeac3a6ed3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ed4b655e5b2f2e274d9c8e71fa7f11fe2a8ae608 a8c7f177510a6ab895aec57ecac218d7226c65ee && git checkout -B drift-repro ed4b655e5b2f2e274d9c8e71fa7f11fe2a8ae608 && git merge --no-ff a8c7f177510a6ab895aec57ecac218d7226c65ee
node scripts/docs-audit/affected-docs.mjs --json ed4b655e5b2f2e274d9c8e71fa7f11fe2a8ae608
|
Contract reviewVERDICT: FAIL — one blocking finding, adopted by the seat verbatim and sent back for a second round. ⛔ Not softened. Head reviewed: Reading moment: this record was written at 2026-09-23T01:23Z. A contract-review record binds ONE head and one reading moment. Reviewed-by: isolated at-tier subagent dispatched by Tier — measured by the SEAT from the reviewer's transcript
B1 (BLOCKING) — the new sentence says 「nothing else reads this key」 and 「
|
…hing else reads it The round-1 description said two consumers read the key and nothing else does, and that email/url/phone are field types rather than formats. At the pinned objectui the shared cell-renderer resolver promotes a plain-text field by a small word set, so both absolutes were false. The description now makes positive statements only: the autonumber pattern arm and its precedence, the server acting on the key for that arm alone, the Studio UI reading it as a display hint whose words its renderers own (the date/datetime style and the plain-text renderer hint as examples, no word list copied), and value checks keyed on `type`. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude <noreply@anthropic.com>
…escription Produced by `gen:schema` then `gen:docs` from the committed `describe`; nothing under content/docs/references/ is hand-edited. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude <noreply@anthropic.com>
The round-1 body said neither `email` nor `phone` is read by anything on any field type, and that nothing but two consumers reads the key. At the pinned objectui a plain-text field's cell renderer is promoted by those words, so the body now states what each reader does and drops the absolutes. `patch` and `Clause-②: no` are unchanged. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Round 2 of card #19679. Base Tier, measured by the SEAT from the reviewer's transcript (⛔ not its self-report): ① Derived judgmentsEvery clause of the new
② Semver level
③ Boundary flagsNone tripped. No governed surface. CI at this head, the reviewer's two readings 02:03Z and 02:17Z (identical): 31 success, 4 skipped ( Two non-blocking PR-body errors, both re-verified by this seat at source and corrected in the body (the head does not move):
Filed from this card's rounds, ⛔ not fixed here: #19764 (six hand-written doc rows carrying the old meaning); #19772 ( Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #19679
Clause-②: no
One published sentence described
FieldSchema.formatasFormat string (e.g. email, phone): two example words, with no field type and no reader named. It shipped verbatim intopackages/spec/json-schema/, intodist, into the publishedsrc/**/*.zod.ts, and to a customer incontent/docs/references/data/field.mdx. On anautonumberfield the same key is the record-number pattern, so following the documented example there mintedemail1.This round fixes the sentence and nothing else. The key is still
z.string().optional(): nothing is split, narrowed, retired or gated by type, and no consumer is touched.What reads the key, measured and stated positively
Round 2 rewrote this section. Round 1 listed two readers and a
nothing elserow. The at-tier review found a third reader at the pinned objectui, so this table lists what each measured reader does and makes no claim that the list is complete.resolveAutonumberFormat(packages/spec/src/data/autonumber-format.ts:196), minted through bypackages/objectql/src/engine.tsapplyAutonumbers(:5051) and twice bypackages/drivers/driver-sql/src/sql-driver.ts(:10639,:10744)autonumberautonumberFormatfirst, then this key, then the default{0000}lintAutonumberFormats(packages/lint/src/lint-autonumber-formats.ts:60-62), wired intoos lint,os compileandos validatethroughpackages/lint/src/authoring-rules.tsautonumber{...}tokens and bad{field}referencesDateCellRendererandDateTimeCellRenderer(packages/fields/src/index.tsx:1127,:1176) at the pinned.objectui-sha87af769e9a3ee28ace099fdd653d3ebd79fe82e2date,datetimeresolveCellRendererType(packages/fields/src/index.tsx:2915-2947), same pin; called from the grid (plugin-grid/src/cellRendererResolution.ts:114), the detail views (DetailSection.tsx:411,DetailView.tsx:1298,HeaderHighlight.tsx:137,RelatedList.tsx:1282), and the kanban, gallery, report and dashboard viewstext,textareaand the rest ofTEXTUAL_BASE_TYPES)packages/plugin-grid/src/__tests__/formatHintedColumnRenderer-8920.test.tsx, where{ type: 'text', format: 'phone' }renders atel:anchorrenderFieldValue(packages/plugin-dashboard/src/recordFields.tsx:382-427), same pin, fed the object field'sformatat:334%, or any of the lettersY,M,D,H,m,sselects a formatter, and anything else falls through toresolveCellRendererTypeRun against this branch's build of
@objectstack/spec(dist/data/index.mjs), seq 1:So an author who followed the key's own documentation onto an
autonumberfield gotemail1as a business identifier. It parsed, it stored, and neither the runtime nor the build-time lint reported it:lintAutonumberFormatsreturns no finding forformat: 'email', while a{nope}{000}control drawsautonumber-references-unknown-field.The negatives the description keeps, each with its radius:
autonumberfield the server does not act on the key. Radius: this repo'spackages/**non-test sources ata8c7f17751. Instrument:git grepover every non-call property read offormat(107 hits). Triaged, the only field-definition reads areautonumber-format.ts:199andlint-autonumber-formats.ts:62, both on the autonumber arm. A word census overpackages/driversandpackages/objectql/srcfinds every field-reading line gated on the autonumber type (sql-driver.ts:10638,:10743;engine.ts:5044). Firing control: the same instrument returns the two known readers. Named blind spots: multi-line destructuring, computed keys, and whole-object forwarding. Corroboration:driver-sql's ownFIELD_KEY_STORAGE_CLASSclassifiesformataspresentation(packages/drivers/driver-sql/src/builtin-column-collision.ts:97).packages/objectql/src/validation/record-validator.ts.def.formathas 0 reads, againstdef.type7 anddef.maxLength2 as lit controls.const t = def.typeis at:628, and the email, url and phone checks at:746,:749and:752testt.FieldTypemembers times 6 values (email,phone,url,relative, an arbitrary string,INV-{0000}) gives 294 cells, run through bothFieldSchemaandObjectSchema: 0 issues on aformatpath. Firing control:format: 123draws 1 issue on each.formatDate(packages/core/src/utils/date-display.ts:198), which honoursshortandrelativeand otherwise paints its default face. The datetime cell handlesrelative,shortandcompactand hands any other word toformatDateTime, which paints its default face (index.tsx:1243-1282).resolveCellRendererTypereturns the base type's renderer for a word outside its map (:2943-2946). None of the three warns or throws.Where a value check lives, which the description now states: the record validator's email, url and phone checks key on the field type (
type: 'email'), and the closedemail | url | phone | jsonvocabulary belongs to aformatvalidation rule (FormatValidationSchema,packages/spec/src/data/validation.zod.ts:204, itsformatkey at:214). The description no longer saysemail,urlandphoneare "not formats". On a plain-text field objectui reads them as renderer hints, and whether that reading should become a declared vocabulary is the decision this card leaves open.Write surface
packages/spec/src/data/field.zod.ts— the onedescribestring. Everything else in the diff is the repo's own generators:pnpm --filter @objectstack/spec gen:docsrewrotecontent/docs/references/data/field.mdx,data/object.mdxandsystem/migration.mdx(three projections of one string). Nothing was hand-edited undercontent/docs/references/.The conditional fence did not trip.
packages/spec/authorable-surface/data.jsonis held by open PR #19618.check:authorable-surfaceran green across the edit and the file did not move — a description is not an authorable key — so no hunk under another claim was touched.Changeset — measured, not assumed
patchon@objectstack/spec.skip-changesetwould have been wrong, and the measurement is the reason rather than a rule of thumb:packages/spec/src/data/field.zod.tsis itself a published file — it matchessrc/**/*.zod.tsin this package'sfiles[].record-number PATTERN) over eachfiles[]entry: 22dist/files and 13json-schema/files carry it.required's existing published describe (Write-time contract (ADR-0113)) — returns 22 and 13 over the same two trees. Same counts, so the route is measuring what it claims to measure.Is a regression test owed? No, and here is the reasoning rather than a silence
emailorphone— goes red on the correction, because the new sentence deliberately names both words in order to redirect the author to the fieldtypeand the validation rule. The defect was a false sentence, not the presence of two words.z.string()with no declared vocabulary and no type gating — narrowing it is exactly the contract-shape decision triage reserved.packages/spec/src/data/autonumber-format.test.ts: canonical-over-shorthand precedence, the{0000}default rendering as0001, and the no-slot branch. The two objectui arms are pinned in objectui at the pin:packages/fields/src/__tests__/datetimeCell.formatVocabulary-8853.test.tsxfor the date and datetime styles, andpackages/plugin-grid/src/__tests__/formatHintedColumnRenderer-8920.test.tsxfor the plain-text hint. The description names those arms without copying their words, so an objectui change to a word list cannot make the spec sentence false. An objectui change that removes an arm could, and nothing in this repository would go red. That residual risk is why the arms are introduced as examples.describeoffers is honoured by some reader. Nothing like it exists, building it is well outside a one-sentence repair, and it belongs with the decision below.Verification
Final commit
a8c7f17751. No merge oforigin/mainthis round:origin/mainis one commit ahead (ed4b655e5b,scripts/pm/**only) and touches none of this PR's five files, soos-regen-merge.shwas not needed. The branch still carries round 1's merge ofdc1b98680b.a8c7f17751pnpm --filter @objectstack/spec build(underscripts/pm/os-verify-lock.sh)VERDICT command-exit 0pnpm --filter @objectstack/spec typecheck, thentest, in one locked runVERDICT command-exit 0; 516 test files, 15056 tests passed, 1 todogen:schema, thengen:docsdata/field.mdx(1 row),data/object.mdx(2 rows) andsystem/migration.mdx(2 rows)pnpm --filter @objectstack/spec check:docs225 generated files in sync with packages/specpnpm --filter @objectstack/spec check:generatedAll 15 generated artifacts are up to datefield.mdx1 time,object.mdx2 times andmigration.mdx2 times; the round-1 sentence and the original sentence appear 0 times in each.git diff dc1b98680b HEAD -- content/docs/referencesis the fiveformatrows and nothing elsescripts/pm/dispatch-gates.mjsderives for this path set, run from its own--commandslist with each exit recorded--ran: 101 derived, 98 run at exit 0, 3 NOT MEASURED, 0 unrunfield.zod.ts): 0 errors, 0 warnings, read from--format json. The.mdxpages and the.changeset/*.mdanswerFile ignored because no matching configuration was supplied.eslint.config.mjs:327-329records that type-aware linting is enabled for no file, so this diff cannot move the verdict on an untouched fileNOT MEASURED, declared to CI:
check:skill-examples,check:dual-build-cjs-loadsandcheck:lean-entry-closureexited 3 (PREREQUISITE NOT MET). Each reads built output of packages this diff does not touch:client-react, the whole workspace, andobjectql. Not a pass and not a finding.The red at
66cb68d947was an intermediate head. That push carried the newdescribebefore its regenerated pages.875c198d2cadded them, but its run was superseded by the next push before the docs-sync step ran (Type Check · source gatescancelled, step 26Check generated reference docs are in sync with the specskipped), so that head is not measured. At the current heada8c7f17751the same step readssuccess.Acceptance notes
1. The key-vocabulary decision card triage said was owed does not exist yet. Triage graded this card "scoped to the
describe" and said the wider problem "belongs in the decision box as its own card" — nothing has been filed, and this round deliberately does not file it. The readings below are recorded so whoever files it starts from measurements instead of from scratch.The shape of the decision: one
z.string()key is read by at least four readers with four different vocabularies (the autonumber pattern, the date and datetime style, the textual renderer hint, and the dashboard display pattern), with no declared value set and nothing that checks the readers agree.resolveAutonumberFormat(packages/spec/src/data/autonumber-format.ts:196);AutonumberFormatSourcedeclaresformatas the shorthand predatingautonumberFormat; call sitespackages/objectql/src/engine.tsapplyAutonumbersandpackages/drivers/driver-sql/src/sql-driver.ts(two). A pattern-less value renders as literal text plus the bare counter — measuredemail1above — and nothing refuses it..objectui-sha87af769e9a3ee28ace099fdd653d3ebd79fe82e2):packages/fields/src/index.tsx:1127const style = dateField.format || 'relative';for thedatecell, and:1176const authoredFormat = (field as DateTimeFieldMetadata | undefined)?.format || 'compact';for thedatetimecell. Two different defaults for one key. The vocabularies differ too:formatDate(packages/core/src/utils/date-display.ts) honoursshortandrelativeand silently paints its default locale face for anything else, while the datetime cell mapsrelativeandshortonto its own faces and falls through for everything else,compactand date patterns such asYYYY-MM-DDincluded.resolveCellRendererTypepromotes a textual field by a word set (packages/fields/src/index.tsx:2915-2946), andplugin-dashboard'srenderFieldValuereads the same key as a display pattern (recordFields.tsx:382-427). The two disagree on at least one word.format: 'email'on atextfield is amailto:hint to the resolver, but the dashboard's pattern test matches itsmand hands the value toformatDate, which answers an em dash. That defect is reported separately in the round-2 report.format— measured overpackages/*/src/objectsandexamples/*/src/data.skills/corpus is different: it teaches the key on the autonumber arm.skills/objectstack-data/rules/field-types.mdspells{ type: 'autonumber', format: 'CASE-{0000}' }under its Autonumber heading and usesformat:in bothorder_noexamples. That is the shorthand this describe now tells a new field to replace withautonumberFormat, so the skill and the describe disagree on which spelling to teach. That disagreement is input for the key-vocabulary decision below, ⛔ not a change made here. What the false sentence reached was the reference docs and the JSON Schema, i.e. exactly what an AI authoring agent reads before writing a field.displayFormat, or promote the style arm), retire one arm under ADR-0049 enforce-or-remove, narrow the type to a declared vocabulary, or declare the two-arm shape and make each arm refuse the other's words loudly. Each widens or removes a published surface.format,autonumberFormat,displayFormat,field vocabulary,date display style.2.
packages/spec/liveness/field.json'sformatrow understates the key. It is a bare{ "status": "live", "evidence": "packages/objectql/src/engine.ts" }— noverifiedAt, no function anchor and no note, and it records only the autonumber reader. Its own siblings are richer on exactly the two axes it is missing:autonumberFormatnames the consuming function, androwscarriesevidenceScope: "cross-repo"for a key objectui reads. The objectui display-style arm is invisible in this row. Not touched here — a ledger row is not thedescribethis card scopes — and it is work the decision card above has to redo anyway, so it is recorded rather than filed.The same false meaning on six hand-written doc rows — filed as #19764, ⛔ not fixed here. This PR repairs the key's own
describe. Six hand-written rows incontent/docs/data-modeling/field-types.mdx(:24undertext,:71underphone) andcontent/docs/data-modeling/validation-rules.mdx(:46text,:64email,:72url,:80phone) creditformatwith validation it never performs. The reader, measured at source:packages/objectql/src/validation/record-validator.ts:628bindsconst t = def.type, and the three shape checks at:746/:749/:752aret === 'email'/'url'/'phone'; that file readsdef.format0 times against a same-filedef.typelit control of 7. ⇒ on validation, this PR'sdescribe, which sends an author totypeand to theformatvalidation rule, is the correct side. On display it is not the whole story: thetextrow (field-types.mdx:24) describes a live renderer hint at the pinned objectui (see the reader table), which the seat recorded on #19764. Three of them also declare aDefaultofemail/url/phonethat does not exist:FieldSchema.parse({ name: 'x', type: 'email' })returns noformatkey at all. Out of this PR's scope — triage scoped the card to thedescribe, and hand-written docs are outside its claimed file surface — so they ride their own card, where the routing question (hand-writtencontent/docs/**versus theFieldSchemasurface it describes) is triage's.Generated by Claude Code
Generated by Claude Code