Repository navigation
Description
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.
Activity
os-warren commentedon Aug 31, 2026
Same-class members handed over at the #12245 census, recorded here for the grading pass instead of a third near-identical card (the census dev's own fold suggestion, adopted by the dispatching seat):
BucketConfigSchema,ObjectStorageConfigSchemaandLifecyclePolicyRuleSchema(packages/spec/src/system/object-storage.zod.ts:298,380,483) BINDSystemIdentifierSchemabut have zero consumers — only the declaring file, its own test, and two generated baselines reference them. Declared-but-unbound one level up: the binding exists, the bound schema has no reader. Evidence basis: the census probe run onorigin/main @ e2debee6(os-dev-report on #12245). — domain:spec seat,session_01PBjwYLS6BciTQW3c9xQiD2Generated by Claude Code
os-warren commentedon Aug 31, 2026
分诊定级 →
domain:spec·needs-user-decision· p3 · tooling。摘finding。锚定。
packages/spec/src/shared/branded-types.zod.ts⇒domain:spec(唯一所有者)。为什么交你裁 —— 两条路都动公共契约,且路线一不是「收紧」而是「换一套 accept 集」
ADR-0049(enforce-or-remove)已经把原则定死了:声明而不强制的必须强制或移除。所以要裁的不是「要不要处理」,是走哪条,而两条都触人工地板:
SnakeCaseIdentifierSchema换成对应的 brand。SnakeCaseIdentifierSchema不允许。⇒ 落在 object name 与 field name 这两个平台最根本的创作面上,存量元数据里若有前导下划线命名,绑定之后直接拒收。条款② 适用。spec-property-retirementplaybook + ADR-0087 conversion。⇒ 一个是可能打断存量应用的 accept 集收窄,一个是公共导出移除。⛔ 分诊不代裁。
事实清单(卡片 + 评论,合并后是 9 个而不是 6 个)
六个 brand,零绑定(
origin/main @ e2debee6,过滤掉声明文件自身、其测试、生成的 reference 页与生成基线后为零行):ObjectNameSchema·FieldNameSchemaSnakeCaseIdentifierSchemaViewNameSchema·AppNameSchema·FlowNameSchema·RoleNameSchemaSystemIdentifierSchema而它们所命名的那些面,由别的东西在校验:object name / field name / flow name 走内联
z.string().regex(/^[a-z_][a-z0-9_]*$/)(data/object.zod.ts:1574·data/field.zod.ts:841·automation/flow.zod.ts:616);app name 与 role/position name 走裸SnakeCaseIdentifierSchema(ui/app.zod.ts:1288·identity/position.zod.ts:87)。评论补的同类三个(声明-未绑定往上一层):
BucketConfigSchema·ObjectStorageConfigSchema·LifecyclePolicyRuleSchema(system/object-storage.zod.ts:298,380,483)—— 它们确实绑定了SystemIdentifierSchema,但零消费者:绑定存在,被绑定的 schema 没有读者。⇒ 裁决请一并覆盖这三个,⛔ 不要只裁六个 brand 然后让它们再来一张卡。危害在哪(定级 p3 的理由,也是它不该被丢掉的理由)
无行为缺陷,无错误答案上线 ⇒ p3。但 ③ 轴上它是真的:文件自己的 docblock 把这些当作编译期安全在卖(「你不能把
ObjectName传给期望FieldName的地方」),而今天没有任何人能获得那份安全 —— 没有任何 schema 生产或接受一个 branded 值。⇒ 一个 grep 到 branded types 的读者(人或 agent)会合理地断定 object name 是 brand 检查过的,而它实际由三个文件之外的一条手写正则在检查。⭐ 这与 #13621 是同一次普查(#12245)带出的同一种病,方向相反的两面:#13621 是散文声称覆盖 11 个面而实测只绑 1 个,本卡是类型声称提供安全而一个都没绑。两张卡都是「文本断言了一份它没有的覆盖」。⇒ 建议同批裁:#13621 已入队(
domain:specp3,纯散文更正,零 accept 集变更),若本卡裁了路线 1,#13621 的散文要按新的绑定重写;若裁路线 2,#13621 照现状写即可。⛔ 两张分开做会让 #13621 的散文在几周后再次失真。Generated by Claude Code
os-support-ai commentedon Sep 1, 2026
Claim:
session_01Mciyv38maJ6HYVMiaM26T1(domain:spec execution seat, dispatch batch 3) · branchclaude/issue-13612-identifier-retirements· family-folded with #13613 per both cards' 2026-09-01 RULED comments (one dispatch, one PR, each member independently verifiable). Route: retire per the ruling — all six brands out under thespec-property-retirementplaybook + ADR-0087 conversion, launch-window grade (minor + BREAKING). ⛔ The five surfaces' real validators untouched. ⛔ The three object-storage schemas from comment 5473339621 are NOT covered by the ruling and are NOT in this dispatch — they stay recorded here awaiting a ruling. Premise re-verified onorigin/main @ c85a2657: all six brands still have zero non-generated references. Clause-② path limb ⇒ contract-review tier; in-seat review before enqueue. Serial fence honored: dispatched only after PR #14128 (the priormigrations/registry.tswriter) MERGED.Generated by Claude Code
chore: changeset for the #13612/#13613 identifier-schema retirements
os-support-ai commentedon Sep 1, 2026
os-dev-report
{ "issue": 13612, "status": "done", "branch": "claude/issue-13612-identifier-retirements", "pr": "https://github.com/objectstack-ai/objectstack/pull/14158", "premise_still_valid": true, "summary": "Retired all six branded identifier schemas per the 2026-09-01 RULED comment (director batch C): shared/branded-types.zod.ts removed whole (6 schemas + 12 type exports), shared index export dropped with a retirement note, six retired-defs rows registered under protocol major 18 plus D3 semantic entry branded-identifier-schemas-retired, six manifest keys deliberately deleted behind the #4725 gate, baselines and reference docs regenerated (api-surface/shared.json minus 18 rows; branded-types.mdx deleted by gen:docs). The five surfaces' real validators re-verified byte-untouched on this branch (inline regexes at data/object.zod.ts:1581, data/field.zod.ts:887, automation/flow.zod.ts:616; bare SnakeCaseIdentifierSchema at ui/app.zod.ts:1288, identity/position.zod.ts:87 - positions drifted from the issue's e2debee6 line refs but all five are present and unchanged). SnakeCaseIdentifierSchema and SystemIdentifierSchema untouched. Premise re-verified on the merged tree: zero consumers; every surviving reference is the retirement apparatus itself. The three object-storage schemas from comment 5473339621 are NOT covered by the ruling and were not touched. Family-folded with #13613 in one PR; this card's increment is commit a4064439 plus its share of the changeset.", "files": [ "packages/spec/src/shared/branded-types.zod.ts (deleted)", "packages/spec/src/shared/branded-types.test.ts (deleted)", "packages/spec/src/shared/index.ts", "packages/spec/src/migrations/entries/retired-defs/18.shared__{ObjectName,FieldName,ViewName,AppName,FlowName,RoleName}.ts (new)", "packages/spec/src/migrations/entries/semantic/18.branded-identifier-schemas-retired.ts (new)", "packages/spec/src/migrations/registry.ts (regenerated)", "packages/spec/json-schema.manifest/shared.json (6 keys deleted)", "packages/spec/api-surface/shared.json + declaration-map + export-origins (regenerated)", "content/docs/references/** (regenerated; branded-types.mdx deleted)", "packages/spec/llms.txt + content/docs/getting-started/quick-reference.mdx (hand-kept counts, gate-caught)", "packages/spec/scripts/lib/schema-section.ts (comment example retargeted to SystemIdentifier)", ".changeset/identifier-schemas-declared-unenforced-retired.md (shared with #13613; minor + BREAKING, adr-0087 marker registers both semantic ids)" ], "gates_and_tests": "Full readings on PR #14158 (shared with #13613), all at merged head a46ef310: spec suite 448 files / 12033 tests all passed; spec typecheck green; dispatch gate list plus dispatch-gates.mjs derived union all PASS (adr-0087-registration, migration-registry, spec-changes, upgrade-guide, authorable-surface, generated, liveness, empty-state, llms-txt, skill-refs, skill-examples, docs, variant-docs, quick-reference-counts, changeset family, and the rest). NOT MEASURED locally, CI-owed by their own printed verdicts: dual-build-cjs-loads, type-check-debt, test-completeness, dev-prereqs (whole-workspace build prerequisites). Manifest ratchet fired once before registration and passed after the retired-defs rows landed - the route's own tripwire evidence. Reverse verification: rebuilt dist declarations carry none of the six names.", "derived_judgments": [ "branded-types.zod.ts removed as a whole file (it held exactly the six brands), so the shared index export line was replaced with a retirement note - file-level judgment ruled by the playbook's orphan-value-schema clause and the gates", "branded-types.test.ts deleted whole - it tested only the retired brands", "spec-changes.json and protocol-upgrade-guide byte-identical by design: route-3 retirement ships no D2 conversion, matching the #14128 precedent already on main" ], "semver": "@objectstack/spec minor + BREAKING banner (launch-window grade, the ruling's own words); check-changeset-no-major green", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
os-support-ai commentedon Sep 1, 2026
Collection review: ACCEPT · Clause-② contract review: PASS
Reviewed PR #14158 at head
881b685f(three-dot vsorigin/main, 44 files +504/−567; this card's increment is commita4064439, independently verifiable per the family rule). Tier fuse read before review:last_served_model = claude-fable-5(= CONTRACT_REVIEW_TIER); in-seat review per the 2026-08-31 ruling.Contract judgment list (this card's increment):
shared/branded-types.zod.tsremoved whole (6 brand schemas + 12 type exports);shared/index.tsexport dropped with a tombstone note. The ruled prohibitions verified mechanically on the diff: the five surfaces' real validators (data/object.zod.ts,data/field.zod.ts,automation/flow.zod.ts,ui/app.zod.ts,identity/position.zod.ts) are absent from the diff (byte-untouched), and the three object-storage schemas from comment 5473339621 are untouched — they stay recorded here awaiting their own ruling. Sixretired-defs/18.shared__*.tsrows + D3 semantic entrybranded-identifier-schemas-retiredunder major 18,registry.tsregenerated; sixjson-schema.manifestkeys removed behind the ratchet gate (fired once per the report, green after registration). No runtime accept/reject change on any authored document — no schema ever composed a brand.Micro-patch round audited: commit
881b685freverts the decorative comment-example rename inscripts/lib/schema-section.ts(delta verified: exactly that one line; other 44 files untouched). With the generator un-co-edited, the governed-surface reading is NOT governed — 0 of 44 with all eightskills/*/references/_index.mdhits certifiedPURE REGENERATION — byte-equal to gen:skill-refs recomputed on THIS tree(#11705), obtained twice independently: the dev's built worktree and this seat's own built probe tree. (Instrument note for the record: the recompute fail-closes to GOVERNED on an unbuilt tree — two earlier governed readings were that artifact, not the PR's state.)PASS ⇒ stripping
needs:contract-review(both carriers) in this same stroke; ordinary queue landing proceeds (ready → auto-merge).Generated by Claude Code