fix(cli): derive a parseable JS identifier for the emitted plugin symbol - #16536
Conversation
`os create plugin <name>` interpolated the project name straight into an
identifier position (`export const <name>Plugin`), while the shared
`validateProjectName` accepts exactly what npm accepts — a dot, an
underscore and a leading digit included. So `os create plugin foo.bar`
exited 0 having written `export const foo.barPlugin: Plugin = {`, a
property access where a binding name belongs.
Acceptance is unchanged: the emitted package name, its scope and the
emitted directory name stay byte-for-byte what the user typed. Only the
code identifier is normalised. `toCamelCase` folded `-x` into `X` and
passed everything else through; `sanitizeIdentifier` generalises that
fold to every run of non-identifier characters and prefixes a leading
digit with `a`, the rule `sanitizeNamespace()` already uses. `my-app`
still yields `myApp`.
The emitted README now names the derived identifier in prose, so the
mapping from package name to exported symbol is stated once where the
user reads it.
The pin drives TypeScript's own parser over the emitted bytes and
asserts zero syntactic diagnostics, with `my-app` as a control in both
directions and a canary that asserts the pre-fix bytes DO produce a
diagnostic.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
…ifier Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
📓 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:
What this run could not see
Coarse fallback — 22 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 42c24e952c3a3e00ae795c4acb689e99d38a58cc && git checkout 42c24e952c3a3e00ae795c4acb689e99d38a58cc
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 46e626513940806af0e8da5dbe1eeec12eae1c45 b96b0620df27108306f9dd687fdd0700e4175d86 && git checkout -B drift-repro 46e626513940806af0e8da5dbe1eeec12eae1c45 && git merge --no-ff b96b0620df27108306f9dd687fdd0700e4175d86
node scripts/docs-audit/affected-docs.mjs --json 46e626513940806af0e8da5dbe1eeec12eae1c45
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 34099758788 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Queue build 34099758788 — standing down. NEW SIGNATURE ⇒ ⛔ not re-queued on this seat's judgement.
The signature, in fullInitial judgement: this is not this PR's regression — and the ground is structural, not a hunch
⛔ Why it is NOT being re-queued anywayThe signature ledger is the sole authority here and it does not carry this file. Open anchors, read at 08:29Z: #16506 ( ⇒ New signature ⇒ ⛔ do not re-投. A blind re-queue rebuilds every PR behind this one, and "flake" is not a root cause. ⭐ ⛔ This seat also does not add a ledger row: a suspected new flaky is a proposal, never a self-added entry — only a human upgrades that table. The triage workflow's own checklist lands in the same place. Item 1 (failing test in a package this PR changed) is no; item 2 (known cross-PR signature) is no aggregate issue yet — "24h 窗口内只有本 PR 撞到过", and What this seat will do
⛔ No test will be skipped, quarantined or given a longer timeout to get green. ⛔ No empty commit, no close-and-reopen. The diff is unchanged and stays unchanged.
Generated by Claude Code |
The one sanctioned re-queue — spent now, unmodified. ⛔ There is no second.
The precondition I named at 08:31Z is metI said this seat would (1) wait for #16537 to land or leave the queue, then (2) re-queue this PR once, unmodified. Read just now: ⇒ The batch that triage's item 3 named — "可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次" — has resolved by landing, not by timing out. That is the condition, and it is the only one that was outstanding. Nothing about the diff movedHead is still PR-side CI, collapsed latest-per-name before tallying35 raw check runs ⇒ 33 distinct names: 30 success, 3 skipped, 0 failure, 0 pending. What a second red would mean, and what it would NOT licenseIf build N+1 reddens on
If it reddens on a different signature, that is a new signature again and the same standing-down rule applies to it — not an escalation of this one. If it goes green, the timeout reading is confirmed and the card's residue gets stripped on merge. Generated by Claude Code |
Fixes #15892
os create plugin foo.barexited 0 having writtenexport const foo.barPlugin: Plugin = {— a property access where a binding name belongs — so the project the scaffolder emitted did not parse.The ruling this implements
Comment
5564368535on the card (director seat, decision batch #64, 2026-09-07T02:52:30Z) records the maintainer's reply, verbatim 「同意」, adopting option 2:Acceptance is untouched:
validateProjectNamestill admits exactly what npm admits, and the emitted package name, its scope and the project directory stay byte-for-byte what the user typed. Only the code identifier is normalised. The emitted scope question raised on #15530 stays with the maintainer and is not addressed here.What changed
toCamelCaseinpackages/cli/src/commands/create.tsfolded-xintoXand passed every other character straight through.sanitizeIdentifiergeneralises that one fold: every run of characters illegal in a JS identifier is treated as the separator-already was, and a leading digit takes theaprefixsanitizeNamespace()has always used for the same rule. It is used at all four emission sites the ruling names —src/index.tstwice,README.mdtwice — and the emitted README now names the derived symbol in prose, so the mapping from package name to exported symbol is stated once where the user reads it.No reserved-word handling, deliberately: every emission site appends
Plugin, so the identifier that lands is never a bare keyword.Driven verification
Built, then driven through the published entry into a scratch directory, exit code read before any pipe, and each emitted file handed to TypeScript's own parser (
ts.createSourceFile+getSyntacticDiagnostics, no lib, no resolution):my-appis the control in both directions and still yieldsmyAppPlugin. The canary is what makes a zero a reading rather than an inert harness: the same instrument, over the bytes this change replaces, reports the defect — and it reproduces the count the card measured.Tests
packages/cli/test/create-plugin-identifier-parses.test.ts(unit tier) drives the same instrument over the rendered template for all four names and both placements, and additionally pins:a_bdoes not discriminate on parseability —a_bPluginwas always legal TypeScript, so it is asserted on the fold instead;validateProjectNamestill accepts every one of them, which is this PR's own re-derivation that npm acceptance did not move;A tilde is worth recording: the card lists
~as a shape that reaches the emission, and atorigin/mainit does not —validateProjectName's charset is^[a-z0-9][a-z0-9._-]*$, which refuses it before any write. The sanitiser folds it anyway; the test states which of the two refuses it.验收备注
Noted, not filed — the PM seat opens the card if it wants one:
os generatecarries the same class, and has no validator in front of it. Reproduced on this branch, driven:os generate object foo.barexits 0 and writesconst foo.bar: Data.ServiceObject = {insrc/objects/foo.bar.object.ts(1 syntactic diagnostic) andexport { default as foo.bar } from './foo.bar.object';insrc/objects/index.ts(1 syntactic diagnostic).generate.tsruns no name validation at all, so its accepted set is wider thancreate's. This PR is scoped tocreate.tsby the ruling and does not touch it.generate.tsandinit.ts) fold[-_]([a-z]), not-([a-z])— they are not byte-identical to the one this PR replaced, so a future consolidation is a behaviour decision aboutinitandgenerate, not a lift-and-drop.Generated by Claude Code