feat(cli)!: refuse a generate name outside the charset spec declares for an object name - #17408
Conversation
`os generate <type> <name>` accepted any name at all until #16724 taught it to refuse names whose emitted TypeScript does not parse. It now also refuses, ahead of that check and ahead of every derivation, any name the object-`name` declaration in `@objectstack/spec` rejects — maintainer ruling, decision batch #82, option A: a gate, no sanitiser, no third charset. The judge is the schema itself (`ObjectSchema.shape.name`), so the charset is asked rather than transcribed, and the refusal quotes the schema's own message so the rule the author is shown is the rule that judged them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DapQyvYrFb1MxSYe7BL2nt
The #16726 charset gate answers first for `foo.bar`, so #16541's pin now measures its own subject through `class` — inside the charset, refused by the compiler — and keeps every `foo.bar` assertion that is still about the command. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DapQyvYrFb1MxSYe7BL2nt
…me gate Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DapQyvYrFb1MxSYe7BL2nt
📓 Docs Drift CheckThis PR changes 1 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 7d382cde7fe9155bb3db49bbadbf7f5048dccba2 && git checkout 7d382cde7fe9155bb3db49bbadbf7f5048dccba2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cfdd0e95dece7d6ccde99ac1e5042134f63788a4 f8c8629ea5193c024adfe3e16698076f646306a5 && git checkout -B drift-repro cfdd0e95dece7d6ccde99ac1e5042134f63788a4 && git merge --no-ff f8c8629ea5193c024adfe3e16698076f646306a5
node scripts/docs-audit/affected-docs.mjs --json cfdd0e95dece7d6ccde99ac1e5042134f63788a4
|
Fixes #16726
os generate TYPE NAMEnow refuses any name outside the charsetpackages/specalready declares for an objectname, before it derives anything from that name. Maintainer ruling, decision batch #82 (2026-09-08) — 「同意」 on option A: gate only.Clause-②: no
Re-declared from the delivered diff. This narrows what a published command accepts, and narrows it back onto a charset
packages/specalready declares — it declares no new accepted spelling, adds no public flag or option, and ships no bypass. The only new exported-ish surface is a file-local function ingenerate.ts.Its title still says
[Decision]and its body still ends with 「请选:A、B,还是 C?」. Both are stale, and a later reader should not mistake this PR for pre-empting a live decision:needs:contract-reviewfrom it.⛔ Nothing here re-opens A/B/C, and the body's closing question is deliberately left unanswered — it was answered on the card.
What the gate is
ObjectSchema.shape.namefrom@objectstack/spec/data, reached lazily inside the check. Nothing ingenerate.tssays what the charset IS, so there is no second declaration to drift. Re-read as the ruling instructed:packages/spec/src/data/object.zod.tsdeclaresname: z.string().regex(/^[a-z_][a-z0-9_]*$/), which matches the ruling comment's transcription. No discrepancy on that point.Invalid string: must match pattern /^[a-z_][a-z0-9_]*$/), so the author is shown the pattern that judged them.metadataFileName(type, toSnakeCase(name)), which is the first line that derives anything from the name.os generaterefuses to write TypeScript that does not parse #16724's parse check is untouched and stays behind it. Only one line of its advice text moved: it used to offerorder-lineas an equally good spelling, which the gate now refuses one layer earlier.The two layers are distinct — driven, both directions
os g object order-lineos g object order_lineorder_line.object.tsbindingorderLineos g object classclassis inside the charset;'class' is not allowed as a variable declaration name.os g view classThe ruling comment closes with: 「
os generate view classis therefore refused at the door rather than emitting a barrel line that binds a reserved word.」That does not follow from the mechanism the same ruling specifies.
classis inside the charsetpackages/specdeclares for an objectname(^[a-z_][a-z0-9_]*$— every character is a lowercase letter), so a charset gate admits it; theviewgenerator emitsconst classViews:andexport { default as class } from './class.view';, both of which parse. Driven on this branch, after the gate: exit 0, both files written.⛔ Not settled here. Refusing reserved words is a third rule, and the ruling's other half is ⛔ no third charset — so this PR implements the mechanism as ruled and reports the gap rather than legislating over it. Recorded as a measured row in
generate-refuses-name-outside-charset.test.tsso that whichever way it is answered, the answer is a deliberate edit. Reported on #16726 for the maintainer.Acceptance notes
order-line), uppercase (Order), dotted (foo.bar) and digit-initial (2fast) names were accepted before and are refused now.order-linein particular used to generateorder_line.object.tsbindingorderLine— a silent fold, which is precisely what option A refuses.order-lineCONTROL ingenerate-refuses-unparseable-name.test.tsandos g skill lead-qualificationingenerate-skill.e2e.test.ts. Both moved to the underscore spelling. Both spellings derive the same written file, the same binding and the same barrel alias, so every assertion in those files is the one its author wrote, byte for byte — only the authored input moved.os generatehas NO name validation at all —os generate object foo.baremitsconst foo.bar: Data.ServiceObjectand a barrel re-export, both un-parseable TypeScript #16541's pin now measures its own subject throughclass.foo.baris outside the charset, so the gate answers for it first and it no longer demonstrates the parse check. Nothing was deleted: everyfoo.barassertion still about the COMMAND (exit code, no rewrite, nothing on disk) is unchanged, and a newclassspawn carries the three assertions that were about the compiler's verdict. One property genuinely stopped being reachable through the command — a name that breaks the BARREL line as well as the scaffold, since a reserved word is legal as anexport { default as ... }alias and every name that breaks both is now stopped one layer earlier. That half stays pinned where it still runs: the CANARY row ingenerate-emission-parses.test.ts.content/docs/deployment/cli.mdxcarriedos g skill lead-qualandos g flow lead-qualification— copy-and-fail examples the moment this lands. Both corrected, plus one paragraph documenting whatNAMEaccepts.generate.ts+ its tests) and is declared as a deviation in the dev report; the file is held by no open PR (re-derived across all 15 open PRs).packages/specis untouched, as the claim required — the charset is read, never redeclared.nameargument advertiseduse kebab-case, which the gate makes false; corrected in the same file.os create(npm charset) andos init(sanitizeNamespace) still answer differently — deliberately untouched, per the card's own scope note.Gates
node scripts/pm/dispatch-gates.mjs --commandsderived 92 families for this diff; all 92 ran, exit codes recorded to disk before any verdict was read, and reconciled with--ran: 90 green, 2 NOT MEASURED, 0 unrun.exit 3= PREREQUISITE NOT MET, neither a finding:pnpm check:dual-build-cjs-loads(wants a repo-widedist/, which is CI's Build Core job) andpnpm check:type-check-debt(its repo-wide re-measure OOMs at this container's 4 GB heap cap; the ledger-completeness half,pnpm check:type-check-coverage, ran green).pnpm lint— the whole-repoeslint . --no-inline-config, exit 0.check:changeset-no-majorgreen; the changeset isminorand carries an ADR-0087not-required (no-migration-prescription)disposition.pnpm --filter @objectstack/cli typecheckexit 0. Targeted tests exit 0:generate-refuses-name-outside-charset(new),generate-refuses-unparseable-name,generate-emission-parses— 59 tests — plusgenerate-skill.e2eunderOS_TEST_TIERS=nightly, 13 tests.Generated by Claude Code