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:
- 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.
- 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.
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.tsdeclares six branded identifier schemas:ObjectNameSchemaSnakeCaseIdentifierSchemaFieldNameSchemaSnakeCaseIdentifierSchemaViewNameSchemaSystemIdentifierSchemaAppNameSchemaSystemIdentifierSchemaFlowNameSchemaSystemIdentifierSchemaRoleNameSchemaSystemIdentifierSchemaEvery 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 onorigin/mainate2debee6:and, for the four
SystemIdentifierSchema-derived ones, the same filter leavesonly
branded-types.test.tsand the docs page.Meanwhile the surfaces those brands are named for are validated by something
else entirely:
z.string().regex(/^[a-z_][a-z0-9_]*$/),data/object.zod.ts:1574z.string().regex(/^[a-z_][a-z0-9_]*$/),data/field.zod.ts:841SnakeCaseIdentifierSchema,ui/app.zod.ts:1288z.string().regex(/^[a-z_][a-z0-9_]*$/),automation/flow.zod.ts:616SnakeCaseIdentifierSchema,identity/position.zod.ts:87Why it may matter
The file's own docblock sells these as compile-time safety ("you cannot pass an
ObjectNamewhere aFieldNameis expected"). Nothing can currently obtainthat 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:
SnakeCaseIdentifierSchemauses 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,
SnakeCaseIdentifierSchemadoes not), so it needs its own census.
spec-property-retirementplaybook 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.