Skip to content

feat(spec): refuse malformed Field.scale/precision declarations at authoring time (#8321) - #8756

Merged
os-zhuang merged 6 commits into
mainfrom
claude/issue-8321-scale-precision-refusal
Aug 14, 2026
Merged

feat(spec): refuse malformed Field.scale/precision declarations at authoring time (#8321)#8756
os-zhuang merged 6 commits into
mainfrom
claude/issue-8321-scale-precision-refusal

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #8321

What

Field.scale ("Decimal places") and Field.precision ("Total digits") are digit counts, but both parsed as bare z.number() — admitting scale: 2.5 and scale: -1, neither of which has a defined meaning as a count. #7501 made scale enforced at write time with a deliberate Number.isInteger(def.scale) && def.scale >= 0 runtime guard that leaves a malformed declaration unenforced (inventing floor/round semantics in a consumer would be PD #12 guessing) — so a typo'd declaration silently got no enforcement at all: the declared-but-inert shape that hides AI-authored metadata errors.

Both keys now refuse non-integer and negative values at parse time — the house z.number().int().min(0) shape (ADR-0078 declared=enforced), per the triage-settled ruling on the issue.

Changes

  • packages/spec/src/data/field.zod.tsprecision/scale tightened to z.number().int().min(0). Refusals carry the real issue path + code (invalid_type "expected int" / too_small ">=0"). CurrencyConfigSchema.precision and its scale → precision alias table are a different surface (the A number field's declared scale is never enforced — values with more decimals are accepted and stored verbatim (min/max on the same field are enforced) #7501-thread trap) and are untouched — pinned by test.
  • ADR-0087 D2 conversion field-malformed-scale-precision-removed (toMajor: 18, retiredFromLoadPath: true): drops a malformed scale/precision from objects[].fields / objectExtensions[].fields. Behaviour-preserving — a malformed declaration enforced nothing, so removing it enforces the same nothing. Retired-from-load-path means a live author gets the loud schema refusal, while the stored-row rehydration seam (applyConversionsToStoredItem, which replays retired entries by design) and os migrate meta still apply the delete — an existing tenant with scale: 2.5 at rest in sys_metadata stays loadable, not hard-broken. Pinned in conversions/stored.test.ts.
  • Protocol-18 semantic entry field-scale-precision-integer-refused (entry FILE + gen:migration-registry — registry regenerated, not hand-edited) + hand-written step18 rationale/conversionIds update.
  • objectql runtime pin FLIPPED, not deleted (record-validator.test.ts): "a malformed declaration stays unenforced" → "is refused at AUTHORING time". The flipped pin asserts FieldSchema refuses all four malformed shapes, and keeps the defense-in-depth half: a raw malformed def bypassing the schema still gets no invented semantics from the runtime guard.
  • Spec pins in field.test.ts: all four refusal directions with issue code + message substance; every legal declaration (0, 2, positive integers, absent) accepted byte-identically; the currency-surface non-conflation pin.
  • Changeset: minor + BREAKING body annotation + ADR-0087 disposition marker (post-v17.0.0-cut convention; precedents PR feat(spec): refuse ${…} placeholder syntax in memory persistence.path / persistence.key at publish (#8495) #8666 / feat(spec): analytics authorable strictness + the data/ mixed(p) verdicts (#4001 data batch D) #8695 / feat(spec): retire the inert additionalTypes key from MetadataPluginConfig (#8586, ADR-0049) #8702).
  • content/docs/references/data/field.mdx — regenerated via check:generated --fix (only the artifact proved stale).

Fixture sweep

Swept packages/services/apps/examples for non-integer or negative scale/precision authorings (both spellings, ts/tsx/json/yaml). Only two hits: the objectql pin itself (flipped deliberately, above) and a currencyConfig.precision: -1 in field.test.ts — the different surface, already refused there. No other fixture authors a malformed value; all consumer fixtures use legal integers and parse unchanged.

Verification (union run at 2c408a8c2 — the branch tip, after merging current origin/main)

  • Reverse verification, direction predicted before the run (red expected on exactly the refusal pins): restored origin/main's field.zod.ts (tree-only, fix committed first), rebuilt spec, proved the ablation reached dist/ (rebuilt dist accepts scale: 2.5) — the 4 new spec pins + the flipped objectql pin went red, everything else green (spec 10597/10601, objectql 3590/3591). Restored the fix from HEAD, rebuilt, proved dist refuses again — all green.
  • @objectstack/spec: test 400 files / 10601 passed; typecheck green; check:generated 13/13 up to date.
  • @objectstack/objectql: test 205 files / 3617 passed (flipped pin green); typecheck green.
  • Derived gates (node scripts/pm/dispatch-gates.mjs over the actual changed paths) all green, including families the dispatch prompt did not name: check:changeset-gate-self-tests, check:cross-package-test-inputs, check:doc-formula-expressions, check:docs-audit-scope, check:durability-log-level, check:merge-driver, check:objectui-changeset, check:quick-reference-counts, check:role-word, check:spec-parsed-alias, check:type-source-resolution, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, check-dev-prereqs, check-engine-split-ratio, and the test-file convention trio check:query-options-erasure / check:type-check-coverage / check:type-check-debt (workspace closure built first), plus check:nul-bytes.
  • Landing interplay: merged origin/main at 8a9e7f4c1 before finalising — no spec files moved under this branch (spec: record:reference_rail has no ComponentPropsMap row — an entry filter parses, typechecks, validates, ships, and silently does nothing #8691's protocol-18 entry has not landed yet; entry files are id-named and disjoint, so whichever lands second takes the regen relay leg).

Generated by Claude Code


Generated by Claude Code

@vercel

vercel Bot commented Aug 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 14, 2026 6:44pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/permissions/system-context.mdx (via packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)

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.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:data tests tooling labels Aug 14, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 14, 2026 18:44
@os-zhuang
os-zhuang enabled auto-merge August 14, 2026 18:44
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 14, 2026
Merged via the queue into main with commit d575779 Aug 14, 2026
28 of 29 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-8321-scale-precision-refusal branch August 14, 2026 19:18
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/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec: Field.scale accepts meaningless declarations (2.5, -1) — now that scale is enforced, malformed declarations should be refused at authoring time

2 participants