Skip to content

fix(spec): describe format by the readers that exist, not by email/phone - #19763

Merged
os-support-ai merged 7 commits into
mainfrom
claude/issue-19679-format-describe-vocabulary
Sep 23, 2026
Merged

os-support-ai merged 7 commits into
mainfrom
claude/issue-19679-format-describe-vocabulary

Conversation

@os-support-ai

@os-support-ai os-support-ai commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator

Fixes #19679

Clause-②: no

One published sentence described FieldSchema.format as Format string (e.g. email, phone): two example words, with no field type and no reader named. It shipped verbatim into packages/spec/json-schema/, into dist, into the published src/**/*.zod.ts, and to a customer in content/docs/references/data/field.mdx. On an autonumber field the same key is the record-number pattern, so following the documented example there minted email1.

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 else row. 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.

reader field types what the value means there
resolveAutonumberFormat (packages/spec/src/data/autonumber-format.ts:196), minted through by packages/objectql/src/engine.ts applyAutonumbers (:5051) and twice by packages/drivers/driver-sql/src/sql-driver.ts (:10639, :10744) autonumber the record-number pattern: canonical autonumberFormat first, then this key, then the default {0000}
lintAutonumberFormats (packages/lint/src/lint-autonumber-formats.ts:60-62), wired into os lint, os compile and os validate through packages/lint/src/authoring-rules.ts autonumber the same pattern, linted at build time for unrecognised {...} tokens and bad {field} references
objectui DateCellRenderer and DateTimeCellRenderer (packages/fields/src/index.tsx:1127, :1176) at the pinned .objectui-sha 87af769e9a3ee28ace099fdd653d3ebd79fe82e2 date, datetime a display style, with its own words and a different default per type
objectui resolveCellRendererType (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 views textual base types (text, textarea and the rest of TEXTUAL_BASE_TYPES) a cell-renderer hint: a small word set promotes the cell to a richer renderer. Pinned end to end by packages/plugin-grid/src/__tests__/formatHintedColumnRenderer-8920.test.tsx, where { type: 'text', format: 'phone' } renders a tel: anchor
objectui renderFieldValue (packages/plugin-dashboard/src/recordFields.tsx:382-427), same pin, fed the object field's format at :334 any a display pattern: a leading currency symbol, a %, or any of the letters Y, M, D, H, m, s selects a formatter, and anything else falls through to resolveCellRendererType

Run against this branch's build of @objectstack/spec (dist/data/index.mjs), seq 1:

format: 'INV-{0000}'                             -> INV-0001
format: 'email'                                  -> email1
{ autonumberFormat: 'A-{000}', format: 'email' } -> A-001
{}                                               -> 0001

So an author who followed the key's own documentation onto an autonumber field got email1 as a business identifier. It parsed, it stored, and neither the runtime nor the build-time lint reported it: lintAutonumberFormats returns no finding for format: 'email', while a {nope}{000} control draws autonumber-references-unknown-field.

The negatives the description keeps, each with its radius:

  • On any non-autonumber field the server does not act on the key. Radius: this repo's packages/** non-test sources at a8c7f17751. Instrument: git grep over every non-call property read of format (107 hits). Triaged, the only field-definition reads are autonumber-format.ts:199 and lint-autonumber-formats.ts:62, both on the autonumber arm. A word census over packages/drivers and packages/objectql/src finds 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 own FIELD_KEY_STORAGE_CLASS classifies format as presentation (packages/drivers/driver-sql/src/builtin-column-collision.ts:97).
  • The write-time record validator never reads it. Radius: packages/objectql/src/validation/record-validator.ts. def.format has 0 reads, against def.type 7 and def.maxLength 2 as lit controls. const t = def.type is at :628, and the email, url and phone checks at :746, :749 and :752 test t.
  • The spec checks nothing but that it is a string. 49 FieldType members times 6 values (email, phone, url, relative, an arbitrary string, INV-{0000}) gives 294 cells, run through both FieldSchema and ObjectSchema: 0 issues on a format path. Firing control: format: 123 draws 1 issue on each.
  • The two named objectui arms fall back silently. Read at the pin: the date cell hands the word to formatDate (packages/core/src/utils/date-display.ts:198), which honours short and relative and otherwise paints its default face. The datetime cell handles relative, short and compact and hands any other word to formatDateTime, which paints its default face (index.tsx:1243-1282). resolveCellRendererType returns 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 closed email | url | phone | json vocabulary belongs to a format validation rule (FormatValidationSchema, packages/spec/src/data/validation.zod.ts:204, its format key at :214). The description no longer says email, url and phone are "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 one describe string. Everything else in the diff is the repo's own generators: pnpm --filter @objectstack/spec gen:docs rewrote content/docs/references/data/field.mdx, data/object.mdx and system/migration.mdx (three projections of one string). Nothing was hand-edited under content/docs/references/.

The conditional fence did not trip. packages/spec/authorable-surface/data.json is held by open PR #19618. check:authorable-surface ran 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

patch on @objectstack/spec. skip-changeset would have been wrong, and the measurement is the reason rather than a rule of thumb:

  • packages/spec/src/data/field.zod.ts is itself a published file — it matches src/**/*.zod.ts in this package's files[].
  • Greping a distinctive fragment of the new text (record-number PATTERN) over each files[] entry: 22 dist/ files and 13 json-schema/ files carry it.
  • Positive control from the same source — 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

  • A negative pin is impossible here. The obvious pin — assert the description never says email or phone — goes red on the correction, because the new sentence deliberately names both words in order to redirect the author to the field type and the validation rule. The defect was a false sentence, not the presence of two words.
  • A positive pin on this prose would rot by design. The next honest sharpening of the sentence breaks it, so the next author edits or deletes the pin — a check nobody trusts is worse than none, and this lane's rule prefers deleting the construct that permits the error over adding a check.
  • The construct that permits the error is out of scope this round. It is z.string() with no declared vocabulary and no type gating — narrowing it is exactly the contract-shape decision triage reserved.
  • Every behaviour claim the new sentence makes is pinned, but not all of it in this repository. The autonumber claims are pinned here by packages/spec/src/data/autonumber-format.test.ts: canonical-over-shorthand precedence, the {0000} default rendering as 0001, and the no-slot branch. The two objectui arms are pinned in objectui at the pin: packages/fields/src/__tests__/datetimeCell.formatVocabulary-8853.test.tsx for the date and datetime styles, and packages/plugin-grid/src/__tests__/formatHintedColumnRenderer-8920.test.tsx for 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.
  • What would earn its place is a gate asserting that every example value a describe offers 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 of origin/main this round: origin/main is one commit ahead (ed4b655e5b, scripts/pm/** only) and touches none of this PR's five files, so os-regen-merge.sh was not needed. The branch still carries round 1's merge of dc1b98680b.

check result at a8c7f17751
pnpm --filter @objectstack/spec build (under scripts/pm/os-verify-lock.sh) VERDICT command-exit 0
pnpm --filter @objectstack/spec typecheck, then test, in one locked run VERDICT command-exit 0; 516 test files, 15056 tests passed, 1 todo
gen:schema, then gen:docs rewrote exactly data/field.mdx (1 row), data/object.mdx (2 rows) and system/migration.mdx (2 rows)
pnpm --filter @objectstack/spec check:docs exit 0: 225 generated files in sync with packages/spec
pnpm --filter @objectstack/spec check:generated exit 0: All 15 generated artifacts are up to date
sentence census the new sentence appears in field.mdx 1 time, object.mdx 2 times and migration.mdx 2 times; the round-1 sentence and the original sentence appear 0 times in each. git diff dc1b98680b HEAD -- content/docs/references is the five format rows and nothing else
every family scripts/pm/dispatch-gates.mjs derives for this path set, run from its own --commands list with each exit recorded --ran: 101 derived, 98 run at exit 0, 3 NOT MEASURED, 0 unrun
eslint, narrowed 1 file linted (field.zod.ts): 0 errors, 0 warnings, read from --format json. The .mdx pages and the .changeset/*.md answer File ignored because no matching configuration was supplied. eslint.config.mjs:327-329 records that type-aware linting is enabled for no file, so this diff cannot move the verdict on an untouched file

NOT MEASURED, declared to CI: check:skill-examples, check:dual-build-cjs-loads and check:lean-entry-closure exited 3 (PREREQUISITE NOT MET). Each reads built output of packages this diff does not touch: client-react, the whole workspace, and objectql. Not a pass and not a finding.

The red at 66cb68d947 was an intermediate head. That push carried the new describe before its regenerated pages. 875c198d2c added them, but its run was superseded by the next push before the docs-sync step ran (Type Check · source gates cancelled, step 26 Check generated reference docs are in sync with the spec skipped), so that head is not measured. At the current head a8c7f17751 the same step reads success.

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.

  • Autonumber arm. resolveAutonumberFormat (packages/spec/src/data/autonumber-format.ts:196); AutonumberFormatSource declares format as the shorthand predating autonumberFormat; call sites packages/objectql/src/engine.ts applyAutonumbers and packages/drivers/driver-sql/src/sql-driver.ts (two). A pattern-less value renders as literal text plus the bare counter — measured email1 above — and nothing refuses it.
  • Display-style arm (objectui, pinned .objectui-sha 87af769e9a3ee28ace099fdd653d3ebd79fe82e2): packages/fields/src/index.tsx:1127 const style = dateField.format || 'relative'; for the date cell, and :1176 const authoredFormat = (field as DateTimeFieldMetadata | undefined)?.format || 'compact'; for the datetime cell. Two different defaults for one key. The vocabularies differ too: formatDate (packages/core/src/utils/date-display.ts) honours short and relative and silently paints its default locale face for anything else, while the datetime cell maps relative and short onto its own faces and falls through for everything else, compact and date patterns such as YYYY-MM-DD included.
  • Renderer-hint and display-pattern arms (objectui, same pin), found by round 2's at-tier review and census. resolveCellRendererType promotes a textual field by a word set (packages/fields/src/index.tsx:2915-2946), and plugin-dashboard's renderFieldValue reads the same key as a display pattern (recordFields.tsx:382-427). The two disagree on at least one word. format: 'email' on a text field is a mailto: hint to the resolver, but the dashboard's pattern test matches its m and hands the value to formatDate, which answers an em dash. That defect is reported separately in the round-2 report.
  • Blast radius today is documentation, not data. No field anywhere in this repository authors format — measured over packages/*/src/objects and examples/*/src/data. ⚠️ The published skills/ corpus is different: it teaches the key on the autonumber arm. skills/objectstack-data/rules/field-types.md spells { type: 'autonumber', format: 'CASE-{0000}' } under its Autonumber heading and uses format: in both order_no examples. That is the shorthand this describe now tells a new field to replace with autonumberFormat, 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.
  • The options the card will have to weigh are all surface-moving and all reserved: split the key (add a 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.
  • Dedupe words for whoever files it: format, autonumberFormat, displayFormat, field vocabulary, date display style.

2. packages/spec/liveness/field.json's format row understates the key. It is a bare { "status": "live", "evidence": "packages/objectql/src/engine.ts" } — no verifiedAt, 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: autonumberFormat names the consuming function, and rows carries evidenceScope: "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 the describe this 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 in content/docs/data-modeling/field-types.mdx (:24 under text, :71 under phone) and content/docs/data-modeling/validation-rules.mdx (:46 text, :64 email, :72 url, :80 phone) credit format with validation it never performs. The reader, measured at source: packages/objectql/src/validation/record-validator.ts:628 binds const t = def.type, and the three shape checks at :746 / :749 / :752 are t === 'email' / 'url' / 'phone'; that file reads def.format 0 times against a same-file def.type lit control of 7. ⇒ on validation, this PR's describe, which sends an author to type and to the format validation rule, is the correct side. On display it is not the whole story: the text row (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 a Default of email / url / phone that does not exist: FieldSchema.parse({ name: 'x', type: 'email' }) returns no format key at all. Out of this PR's scope — triage scoped the card to the describe, and hand-written docs are outside its claimed file surface — so they ride their own card, where the routing question (hand-written content/docs/** versus the FieldSchema surface it describes) is triage's.


Generated by Claude Code


Generated by Claude Code

…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>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation protocol:data tooling labels Sep 23, 2026
@github-actions

github-actions Bot commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 1 documentable anchor(s).

12 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-driven.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/data-modeling/external-datasources.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/data-modeling/field-types.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/data-modeling/validation-rules.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/deployment/troubleshooting.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/deployment/validating-metadata.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/getting-started/quick-reference.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/kernel/contracts/data-engine.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/protocol/backward-compatibility.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/protocol/objectql/types.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/protocol/objectui/concept.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/ui/forms.mdx (via FieldSchema (symbol, a top-level const))

2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/releases/v17/17-1.mdx (via FieldSchema (symbol, a top-level const))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 136 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 ed4b655e5b2f2e274d9c8e71fa7f11fe2a8ae608packageMentionDocs.

Which tree this was computed on

This run read content/docs from b3dc8c511f8f5d6d1aa2b349240f5caeac3a6ed3 — the merge of head a8c7f177510a6ab895aec57ecac218d7226c65ee into base ed4b655e5b2f2e274d9c8e71fa7f11fe2a8ae608, 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 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

⚠️ 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 ed4b655e5b2f2e274d9c8e71fa7f11fe2a8ae608 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Collaborator Author

Contract review

VERDICT: FAIL — one blocking finding, adopted by the seat verbatim and sent back for a second round. ⛔ Not softened. Head reviewed: 5a837c9ed081169bc4adf88f48b2ce73a550bb3f. Base main.

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 domain:spec execution seat 1 (session_013RDBh5DqXd2xnLwvHLgLFr), seat post #6017
Implemented-by: claude/issue-19679-format-describe-vocabulary

Tier — measured by the SEAT from the reviewer's transcript

CONTRACT_REVIEW_TIER on origin/main is claude-fable-5-1. Governing assistant-request stamps 123 / 123 claude-fable-5-1; dark control (any non-claude-fable-5-1 stamp) empty; the fallback/overload sweep read in context is repo source and prose (formatRelativeDate's English fallback, the autonumber fallback docblocks) — ⛔ none a notice. ⇒ AT TIER.

B1 (BLOCKING) — the new sentence says 「nothing else reads this key」 and 「email, url and phone are field TYPES, not formats」. At the pinned objectui, both are false.

Re-verified by the seat at source, ⛔ not adopted on the reviewer's word — objectui at the pinned .objectui-sha 87af769e9a3ee28ace099fdd653d3ebd79fe82e2, packages/fields/src/index.tsx:

  • :2915const FORMAT_TO_RENDERER: Record<string, string> = { (the reviewer reads it as phone | tel | telephone → phone, email → email, url | uri | link → url, currency | money → currency, percent | percentage → percent);
  • :2929const TEXTUAL_BASE_TYPES = new Set(['text', 'textarea', 'string', 'longtext', '']);
  • :2941const formatRaw = fieldOrType.format;
  • :2943-2944if (format && FORMAT_TO_RENDERER[format] && TEXTUAL_BASE_TYPES.has(baseType)) { return FORMAT_TO_RENDERER[format]; }

And it is pinned end-to-end by packages/plugin-grid/src/__tests__/formatHintedColumnRenderer-8920.test.tsx: the schema field work_phone: { type: 'text', format: 'phone', … } renders a[href="tel:<hinted>"], while an unhinted control column of the same type renders none; its header states the promotion 「is driven by the DECLARED format」.

on a text field, format: 'phone' / 'email' / 'url' IS honoured today, as a cell-renderer promotion. The old sentence 「Format string (e.g. email, phone)」 was describing a live vocabulary on the UI side. The card's premise (「nothing honours either value」), the PR's reader table, the new describe and the changeset body (「neither email nor phone is read by anything, on any field type」) are all wrong on this point — and the new sentence ships to json-schema/ (13 files), dist/ (22), src/data/field.zod.ts (in files[]), three reference pages and, through the changeset, CHANGELOG.md.

Also within this repo: 「Nothing else reads this key」 is literally false — packages/lint/src/lint-autonumber-formats.ts reads f.format (a compile-time lint over the same autonumber pattern, so no new vocabulary, but a third reader).

What would pass, in the reviewer's words: a describe and changeset body that name the third reader — textual-type cell-renderer promotion at the pinned objectui — or drop the 「two consumers / nothing else / not formats」 claims, without deciding the vocabulary question triage reserved.

Everything else — verified true, with readings

  • Autonumber arm: resolveAutonumberFormat precedence autonumberFormatformat{0000}, callers in engine.ts and sql-driver.ts (×2), driven through the real engine fallback and a real SQLite driver: 'INV-{0000}' → 'INV-0001', 'email' → 'email1', { autonumberFormat: 'A-{000}', format: 'email' } → 'A-001', {} → '0001'.
  • Date/datetime arm: defaults 'relative' (:1127) and 'compact' (:1176) at the pin. 「Studio UI grid cell」 is narrower than reality (related lists and detail panels reach the same renderers) but not false.
  • Never affects storage, coercion or write-time validation: true; validation is type-driven: true (record-validator.ts:628, checks at :746/749/752, 0 def.format reads against a def.maxLength control of 2).
  • The spec declares no vocabulary: true — 49 FieldType members swept with format: 'email' and an arbitrary string, 0 issues blame format.
  • Generators: gen:schema and gen:docs leave the tree unchanged at head; a firing control (swap the base field.zod.ts in, regenerate) moves exactly the three pages. The os-regen merge dropped nothing — main never touched those pages, and the whole-tree diff against dc1b98680b is exactly this PR's five files.
  • Scope: five files; autonumber-format.ts, engine.ts, sql-driver.ts, record-validator.ts untouched; authorable-surface/ unchanged.
  • Changeset: required (npm pack --dry-run lists src/data/field.zod.ts; controls listed and unlisted as expected), patch right, Clause-②: no right — ⚠️ but its body carries the B1 claim and must change with the describe.
  • The no-test decision is acceptable: 「no test can pin "nothing else reads it"; only a complete census can, and this one missed a pinned reader in the very tree it cited.」

CI at this head — converged

42 check runs, all completed; 0 failure, 0 in_progress, 0 queued, 0 cancelled. Four designed skips: Auto Label and Check PR Size on the edited event (their own if:; the push-event runs succeeded), Console Pin Gate (path filter not hit), Packed-tarball smoke (opt-in label absent). ⇒ the head is green; it is the sentence that fails.


Generated by Claude Code

…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>

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a8c7f177510a6ab895aec57ecac218d7226c65ee

Round 2 of card #19679. Base main. This record replaces the round-1 FAIL at 5a837c9ed0 for this head only. Reading moment: 2026-09-23T02:21Z. A record binds ONE head and one reading moment; it is ⛔ not a standing statement about CI.

Tier, measured by the SEAT from the reviewer's transcript (⛔ not its self-report): CONTRACT_REVIEW_TIER on origin/main is claude-fable-5-1. Governing assistant-request stamps 101 / 101 claude-fable-5-1; superset "model" + "advisorModel" 219 / 219 the same value; dark control (any other stamp) empty; the fallback/overload sweep, read in context, is tool-schema text and repo source (the autonumber fallback docblocks) — ⛔ none a notice. ⇒ AT TIER. This seat is off-tier and ⛔ did not 自审.

① Derived judgments

Every clause of the new describe (packages/spec/src/data/field.zod.ts, FieldSchema.format) was measured true at this head, and in objectui at the pinned .objectui-sha 87af769e9a3ee28ace099fdd653d3ebd79fe82e2:

  • No vocabulary. 49 FieldType members × 6 values = 294 cells through both FieldSchema and ObjectSchema: 0 issues on a format path. Control: format: 123 draws 1 issue on each.
  • Autonumber arm. resolveAutonumberFormat takes autonumberFormat first, then format. The reviewer also closed a trap the round did not name: the autonumberFormat default is a JSON-Schema annotation, not a parse-time write, so a parsed field carrying only format: 'INV-{0000}' still resolves to it. The engine and the SQL driver both mint INV-0001; format: 'email' mints email1.
  • The server does not act on it on any other type. Radius: this repo's packages/**, four instruments (property reads, bracket reads, destructuring, a word census). The only reads of a field definition's format are the resolver, lint-autonumber-formats.ts and the SQL driver's resolved autonumber config — all on the autonumber arm, all firing controls. The record validator reads def.format 0 times (lit controls: def.type 7, def.maxLength 2); its email, url and phone checks key on type.
  • The Studio arms. The date cell (dateField.format || 'relative'), the datetime cell (|| 'compact') and the shared resolver's textual promotion (FORMAT_TO_RENDERER × TEXTUAL_BASE_TYPES, pinned end-to-end by formatHintedColumnRenderer-8920.test.tsx) are as described. None of the three warns or throws on an unrecognised word.
  • No fifth reader, in either repo, under an instrument that returns all four known readers (objectui: 92 lines in 32 files triaged; each other hit reads a different format — report columns, the SDUI date-picker block, dataset measures, validation rules, chart axes).
  • The round's blocking finding is answered. The sentence names the readers it measured, makes no claim that the list is complete, and copies no word list from objectui. It decides no vocabulary: 「prefer autonumberFormat」 restates the precedence the spec already declares.

② Semver level

patch on @objectstack/spec, and the changeset is required: after build and generation the new sentence is in 22 dist/ and 13 json-schema/ files, identical to a positive control from the same source. The key's type is unchanged. check:docs (225 files in sync) and check:generated (15 artifacts up to date) exit 0 in the reviewer's tree; git status is clean after generation. Scope is 5 files, +28 / −6.

③ Boundary flags

None tripped. No governed surface. content/docs/references/** is on the merge=os-regen roster, and open PR #19618 holds the same two pages on disjoint rows (organizationField), so its merge needs os-regen-merge.sh plus a survival assertion. No consumer touched, nothing retired, no vocabulary chosen.

CI at this head, the reviewer's two readings 02:03Z and 02:17Z (identical): 31 success, 4 skipped (Auto Label, Check PR Size, Console Pin Gate, Packed-tarball smoke), 0 in progress, 0 failure. check:docs is step 26 of Type Check · source gates, success at this head.

Two non-blocking PR-body errors, both re-verified by this seat at source and corrected in the body (the head does not move):

  • F1 — the body said the published skills/ corpus 「never teaches the key」. It does, on the autonumber arm: skills/objectstack-data/rules/field-types.md spells format: 'CASE-{0000}' under its Autonumber heading and uses format: in both order_no examples. The skill and the new describe now disagree on which spelling to teach — input for the key-vocabulary decision, ⛔ not a defect of this PR.
  • F2 — the body said check:docs was green 「from 875c198d2c on」. At that head the job was cancelled and step 26 skipped: not measured. Green at a8c7f17751 is true.

Filed from this card's rounds, ⛔ not fixed here: #19764 (six hand-written doc rows carrying the old meaning); #19772 (lint-autonumber-formats keeps its own precedence rule and goes silent on an empty autonumberFormat); objectstack-ai/objectui#10220 (the dashboard's [YMDHms] date-pattern test catches format: 'email' on a text field and hides the value).

Implemented-by: claude/issue-19679-format-describe-vocabulary
Reviewed-by: session_013RDBh5DqXd2xnLwvHLgLFr

VERDICT: PASS


Generated by Claude Code

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/s tooling

Projects

None yet

2 participants