Skip to content

fix(create-objectstack): refuse a project name npm would refuse, before writing anything - #26796

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-26642-npm-name-check
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-26642-npm-name-check

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #26642

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么: create-objectstack 在写任何文件之前,先用 npm 自己的包名规则检查项目名(参数,或无参数时的当前目录名);不合法就退出 1,打印 npm 的拒绝理由、规则和一个建议名(My App! 与 MyApp 都建议 my-app)。名字永不被悄悄改写。
  • 为什么改: 以前 "My App!" 原样写进 package.json 和 manifest,validate、build 都通过,直到 npm publish 才被拒,离原因很远。
  • 风险与回滚: 新增一个运行时依赖 validate-npm-package-name(npm 自己用的判定模块,零依赖,锁文件里本来就有 7.0.2);以前能"成功"的非法名(包括 ./my-app 这种路径写法)现在会被拒。回滚即 revert 本 PR。
  • 先看: ① packages/create-objectstack/src/index.ts 中 checkProjectName 的接线(必须在第一次写盘之前);② src/package-name.ts(判定与建议名);③ package.json 与 pnpm-lock.yaml 的新依赖。
  • 两分钟验证: 在空目录跑 node packages/create-objectstack/bin/create-objectstack.js "My App!" --skip-install --skip-skills; echo $?,预期看到规则与 Suggested name: create-objectstack my-app,退出码 1,目录里没有任何新文件。
  • 需要你做的: 无,席位落地。

What changes

  • New src/package-name.ts. checkProjectName(name) returns npm's own verdict for a NEW package (validate-npm-package-name's validForNewPackages), with npm's reasons verbatim. suggestPackageName(name) derives a slug (My App! and MyApp give my-app, @Acme/My App gives @acme/my-app, combining marks are dropped) and returns nothing when the slug is empty or would itself be refused (HTTP slugs to the core module name http), so a refusal never recommends a name it would refuse next. describeInvalidProjectName renders the refusal: the headline, npm's reasons, the rule in one sentence, and the re-run line.
  • src/index.ts. The check runs on the resolved projectName (so the no-argument, current-directory branch is covered too) right after it is resolved and before the first write. A refused name exits 1. In the current-directory case the remedy says the name came from the directory: rename it, or scaffold into a new directory with an explicit name.
  • Why a dependency rather than a local rule set. The defect is "npm refuses this name", so the predicate is npm's own module rather than a restated copy that can drift from it. It is small, has no dependencies, and was already in the lockfile at 7.0.2 (through shadcn), so no new package enters the install graph. @types/validate-npm-package-name is a dev dependency for the typecheck. The lockfile diff is only the two importer entries: pnpm add also re-picked an unrelated jsdom peer in two snapshot keys, and that churn was reverted by hand (pnpm install --frozen-lockfile exits 0 on the result).
  • No prompt path exists. create-objectstack has no interactive prompt, so the refusal is the only path. Nothing is rewritten: the written value is always what the user typed.
  • One patch changeset, .changeset/26642-npm-name-check.md.

Tests

New src/package-name.test.ts (32 tests):

  • predicate: accepts my-app, @scope/my-app, my.app, a_b, app2, x; refuses My App!, MyApp, HTTP, http, .hidden, _private, a leading space, node_modules, apps/my-app and a 215-character name, each naming npm's reason;
  • slug: the expected slug per input, none for HTTP and !!!, and every slug it does return passes validForNewPackages;
  • the real CLI under tsx: My App! and MyApp exit 1, print the rule and Suggested name: create-objectstack my-app, and leave the temp directory empty; a current directory named My App exits 1 and stays empty; control legs: my-app and @scope/my-app exit 0 and write the name verbatim to package.json and objectstack.manifest.json.

Ablation (the refusal pins can fail): node scripts/ablation-replace.mjs replaced if (!nameVerdict.valid) { with if (false && !nameVerdict.valid) { in index.ts (anchor 1 to 0, blob 625a2dc9 to 9824e843), and the three CLI refusal legs went red while the control legs stayed green. The tool then restored the file (blob equal to HEAD, git diff HEAD empty).

Ablation run (excerpt)
ablation-replace: ok mutation landed: anchor 1 -> 0, blob 625a2dc9c1cb -> 9824e843117f
     × "My App!": exit 1, the rule, the slug, and no directory 740ms
     × "MyApp": exit 1, the rule, the slug, and no directory 721ms
     × the current directory's name is checked too, and the directory stays empty 681ms
AssertionError: expected +0 to be 1 // Object.is equality
 Test Files  1 failed (1)
      Tests  3 failed | 29 passed (32)
ablation-replace: ok restored: blob == HEAD (625a2dc9c1cb) and `git diff HEAD` is empty
Package build, typecheck, full test suite at 0108d0c
pnpm --workspace-concurrency=2 --filter 'create-objectstack^...' build   -> VERDICT command-exit 0
pnpm --filter create-objectstack build && pnpm --filter create-objectstack typecheck && pnpm --filter create-objectstack exec vitest run --maxWorkers=2
ESM ⚡️ Build success in 34ms
DTS ⚡️ Build success in 856ms
> tsc --noEmit
 Test Files  19 passed (19)
      Tests  301 passed (301)
os-verify-lock: VERDICT command-exit 0

The built dist/index.js keeps validate-npm-package-name external (import validateNpmPackageName from "validate-npm-package-name"), and node bin/create-objectstack.js "My App!" exits 1 with the refusal, while my-app exits 0 with "name": "my-app".

Acceptance notes

  • Path-shaped names are now refused. create-objectstack ./my-app or apps/my-app used to write that string (an illegal package name) into package.json; it is now refused with the slug (my-app, apps-my-app). The [name] argument is documented as a name, not a path.
  • The sibling scaffolder keeps its own rule set. os init (packages/cli/src/commands/init.ts, validateProjectName) already refuses before writing, with a hand-written subset of npm's rules: it refuses scoped names and does not refuse core module names such as http, and it suggests no slug. The two scaffolders therefore answer differently for @scope/my-app and http. Recorded only; no wrong answer from os init was measured here.
  • test:repo (the vitest repo project) was not run locally; it belongs to CI's Test Core.

Verification

Gate families derived from the diff (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no paths, at 0108d0c2): 71 commands. 68 exit 0. 3 are NOT MEASURED (exit 3, PREREQUISITE NOT MET), and CI's Lint & Repo Gates runs them on a full build:

  • pnpm check:dual-build-cjs-loads and pnpm check:lean-entry-closure need every package's dist, and only create-objectstack and its closure were built;
  • node scripts/check-plugin-teardown-shape.mjs --self-test needs an unshallowed clone (checker health only).

--ran reconciliation with recorded exit codes: 71 derived famil(ies) accounted for — 68 run, 3 NOT-MEASURED (3 DERIVED from a recorded exit 3). pnpm exec eslint on the three changed source files exits 0. The control-byte scan of the new files is clean, and check:nul-bytes is OK.

Selected gate verdict lines
✓ check:published-files — 73 publishable package(s) of 85 workspace member(s) declare a `files` whitelist ...
✓ check:undeclared-dep-imports: 129 workspace packages ... 3 ledger row(s), all evidence intact.
✓ 65 published-manifest declaration(s) covered by pnpm-workspace.yaml overrides all resolve to their override targets.
✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).
✓ No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added).
✓ This diff introduces no `major` bump.
✓ .changeset/config.json "fixed" group is in sync with 109 public workspace packages and 1 declared private member(s).
check-nul-bytes: OK (scanned 21457 text file(s) ...; no raw ASCII control bytes).

Durable attribution: written by an os-dev agent in session session_01QSeKYmQXM3rHJcPnrEPk4B, dispatched by the PM loop (seat domain:server#3).


Generated by Claude Code

…re writing anything

The resolved project name (argument or current directory basename) is
checked with validate-npm-package-name's validForNewPackages before the
first write. A refused name exits 1 with npm's reasons, the rule, and a
suggested slug; the name is never rewritten silently.

Claude-Session: https://claude.ai/code/session_01QSeKYmQXM3rHJcPnrEPk4B
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tests tooling labels Oct 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

10 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/create-objectstack/package.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/create-objectstack/package.json) — pages documenting those are invisible to this run
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 55 of 207 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 13 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 7bdff7ec5a8c0ed80c51f87d0c77ac2ddd7a57d5 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5f5435368635f193bbea83850b7ec3eac51ea679 — the merge of head 0108d0c2038c41fc3644f1006cfc53e87f28dd6d into base 7bdff7ec5a8c0ed80c51f87d0c77ac2ddd7a57d5, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 5f5435368635f193bbea83850b7ec3eac51ea679 && git checkout 5f5435368635f193bbea83850b7ec3eac51ea679
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7bdff7ec5a8c0ed80c51f87d0c77ac2ddd7a57d5 0108d0c2038c41fc3644f1006cfc53e87f28dd6d && git checkout -B drift-repro 7bdff7ec5a8c0ed80c51f87d0c77ac2ddd7a57d5 && git merge --no-ff 0108d0c2038c41fc3644f1006cfc53e87f28dd6d

node scripts/docs-audit/affected-docs.mjs --json 7bdff7ec5a8c0ed80c51f87d0c77ac2ddd7a57d5

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:server seat 3 · session_01QSeKYmQXM3rHJcPnrEPk4B · 2026-10-27T16:06Z

Head: 0108d0c2038c41fc3644f1006cfc53e87f28dd6d

  • 形态: draft → main, first line Fixes #26642 (the card is fully delivered; no other open card number sits beside a closing keyword). Clause-②: no and Metadata-protocol: none at line start in the PR body and in .changeset/26642-npm-name-check.md (patch, create-objectstack). 6 files, +342/−1; check-governed-merges --pr 26796: NOT governed, under the 3000-line threshold. PR assignee os-justin = the card's.
  • 前提实测: seat read on the PR tree: index.ts resolves projectName = name || path.basename(cwd) and the new checkProjectName gate sits before sanitizeNamespace / targetDir, i.e. before the first write; nothing writes the name before it. The no-argument (cwd basename) branch is covered by the same check.
  • 门禁 check-runs: at this head, Lint & Repo Gates lanes, Type Check lanes, Test Core 1–8, Build Core / Docs / UI Deps and "Scaffold with repo dist" are still in_progress; Check Changeset, Governed Surface Queue Guard, PR-body guards, Validate Package Dependencies, PR size and Dogfood Regression Gate are success; the opt-in lanes skipped. Landing waits on every check green.
  • 接受的偏差: (1) a runtime dependency on validate-npm-package-name instead of a local rule set: npm's own predicate, zero dependencies, already in the lockfile at 7.0.2; seat checked the lockfile carries both package entries (validate-npm-package-name@7.0.2, @types/validate-npm-package-name@4.0.2) and the importer diff is only the two lines. (2) Path-shaped names (./my-app, apps/my-app) and . are now refused too: they always wrote an illegal package.json name; the seat found no doc or skill that teaches the path form (grep over content/, docs/, skills/, packages/create-objectstack, examples/, apps/ → 0 hits).
  • 范围外去向: the os init validateProjectName divergence (scoped names, core module names) is a read-only observation with no wrong answer measured at a public door: Acceptance notes, not filed. Skills-docs: not owed (Metadata-protocol: none).
  • 抽查: refusal pins assert exit code 1 + the rule text + an empty target dir; ablation recorded in the PR body (if (false && !nameVerdict.valid) → 3 refusal legs red, 2 control legs green, restored to blob 625a2dc9).
  • 落地条款: ordinary queue landing by the seat once every check on this head is green or an expected skip (pr_ready + automerge_enable through the relay).

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 27, 2026 16:24
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 27, 2026 16:24
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit 0f0b9f5 Oct 27, 2026
64 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-26642-npm-name-check branch October 27, 2026 16:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

create-objectstack accepts a project name that is not a legal npm package name and writes it to package.json, where npm publish later refuses it

2 participants