Skip to content

finding(spec): all six branded identifier schemas in branded-types.zod.ts are declared but unbound — no schema anywhere composes them #13612

Description

@os-warren

Observed while running the per-surface identifier census for #12245. Filed unassigned as a finding, not a defect claim.

What was observed

packages/spec/src/shared/branded-types.zod.ts declares six branded identifier schemas:

Schema Base Composed into any other schema?
ObjectNameSchema SnakeCaseIdentifierSchema no
FieldNameSchema SnakeCaseIdentifierSchema no
ViewNameSchema SystemIdentifierSchema no
AppNameSchema SystemIdentifierSchema no
FlowNameSchema SystemIdentifierSchema no
RoleNameSchema SystemIdentifierSchema no

Every reference outside the declaring file is either the file's own test
(branded-types.test.ts), the generated reference page
(content/docs/references/shared/branded-types.mdx), or a generated baseline
(api-surface/shared.json, export-origins/shared.json,
type-alias-convention.pin.test.ts). Reproduced on origin/main at e2debee6:

git grep -n "\bObjectNameSchema\b\|\bFieldNameSchema\b" -- 'packages/**/*.ts' \
  | grep -v branded-types | grep -v "\.test\.ts"
# zero lines

and, for the four SystemIdentifierSchema-derived ones, the same filter leaves
only branded-types.test.ts and the docs page.

Meanwhile the surfaces those brands are named for are validated by something
else entirely:

  • object name — inline z.string().regex(/^[a-z_][a-z0-9_]*$/), data/object.zod.ts:1574
  • field name — inline z.string().regex(/^[a-z_][a-z0-9_]*$/), data/field.zod.ts:841
  • app name — SnakeCaseIdentifierSchema, ui/app.zod.ts:1288
  • flow name — inline z.string().regex(/^[a-z_][a-z0-9_]*$/), automation/flow.zod.ts:616
  • role/position name — SnakeCaseIdentifierSchema, identity/position.zod.ts:87

Why it may matter

The file's own docblock sells these as compile-time safety ("you cannot pass an
ObjectName where a FieldName is expected"). Nothing can currently obtain
that safety, because no schema in the repo produces or accepts a branded value —
the brands exist only for a caller who parses through them by hand. That is the
ADR-0049 declared-but-unenforced shape, and it is also a live hazard for AI-authored
metadata: a reader who greps the branded types reasonably concludes object names are
brand-checked when they are checked by a hand-written regex three files away.

Two honest routes, both maintainer-ruled rather than obvious:

  1. Bind them — replace the inline regexes and the bare SnakeCaseIdentifierSchema
    uses at the five surfaces above with the matching brand. This is an accept-set change
    at each surface (the inline regexes allow a leading underscore, SnakeCaseIdentifierSchema
    does not), so it needs its own census.
  2. Retire them under ADR-0049 enforce-or-remove — the spec-property-retirement
    playbook route, with the ADR-0087 conversion for a published export removal.

Not sized here, and no route recommended: recording the measurement only.

Related: #12245 (the census that surfaced this) · #12194 · ADR-0049.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions