Repository navigation
fix(create-objectstack): a project name that starts with the template namespace scaffolds instead of being refused - #22921
Conversation
…the new one findStaleNamespacePrefixes tested each object name against the template's prefix alone. A project namespace that itself begins with the template namespace plus an underscore (blank-probe -> blank_probe) makes every correctly rewritten name (blank_probe_note) carry the old prefix too, so a valid project was refused as a scaffolder bug. The check now takes the new namespace and reports a literal only when it carries the old prefix and not the new one, judged per literal. One predicate for every template namespace. The one caller passes the namespace it just wrote. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6
📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 11 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 9c9f68e6feb41c1cdcc4176c9c227553ebd0f572 && git checkout 9c9f68e6feb41c1cdcc4176c9c227553ebd0f572
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 018b077fbf8876c0fd5f46e66dcc551a4e74c895 8679418942c91827751803a3eda0ff92c724165b && git checkout -B drift-repro 018b077fbf8876c0fd5f46e66dcc551a4e74c895 && git merge --no-ff 8679418942c91827751803a3eda0ff92c724165b
node scripts/docs-audit/affected-docs.mjs --json 018b077fbf8876c0fd5f46e66dcc551a4e74c895 |
Fixes #22911
Clause-②: no
What this changes
create-objectstack blank-probeexited 1 with "1 object name(s) still carry the template's 'blank_' prefix ... This is a bug in the scaffolder". The rewrite itself was right (blank_notebecameblank_probe_note). The self-check after it,findStaleNamespacePrefixes, tested each name against the template prefix alone, andblank_probe_notestarts withblank_because the new namespaceblank_probedoes.Per the triage direction (comment 6115833632), a name is now stale only when it starts with the old prefix and does not start with the new namespace's prefix:
packages/create-objectstack/src/rewrite-identity.ts:findStaleNamespacePrefixes(dir, oldNs, newNs)takes the new namespace as a required third argument. Each old-prefixedname:literal on a line is checked against the samenamePrefixRebuilt from the new namespace. One predicate for every template namespace, with no exemption forblank. The check runs per literal, so a correct name on a line cannot hide a stale one on the same line.packages/create-objectstack/src/index.ts: the one caller passes thenamespaceit has just written. Nothing outside the package imports the function. The only otherrewrite-identitymentions are comments, and the package'sexportsmap publishes./created-summaryonly.Measured at the public door
Repo-built CLI (
tsx packages/create-objectstack/src/index.ts blank-probe --skip-install --skip-skills):eab7236ff7Scaffolding rewrote the namespace to 'blank_probe' but 1 object name(s) still carry the template's 'blank_' prefix: src/objects/note.object.ts:6 name: 'blank_probe_note',8679418942src/objects/note.object.ts:6 name: 'blank_probe_note',andobjectstack.config.ts:27 namespace: 'blank_probe',Tests
rewrite-identity.test.ts: the two existingfindStaleNamespacePrefixescases now pass the new namespace. A newdescribe.eachruns overblank → blank_probeandtodo → todo_list, so the same pins run under two template namespaces. It has three cases:OLD_noteis still reported;scaffold-namespace-extends-template.test.ts(new) drives the real CLI throughtsx, the patternscaffold-description.test.tsuses. There is one case per bundled template inTEMPLATES. Each probe project is named from that template's own namespace (blankgivesblank-probe). The case asserts:blank_probe;blank_probe_*;validateObjectNamespacePrefix.8679418942, through the shared verify lock:pnpm --filter create-objectstack exec vitest run --maxWorkers=2followed bypnpm --filter create-objectstack run typecheck:Test Files 18 passed (18),Tests 261 passed (261),tsc --noEmitclean,VERDICT command-exit 0.tsc --noEmit --listFilesincludes both touched test files (18 test files in the program).Ablation (committed fix, two legs,
scripts/ablation-replace.mjsWRAP mode with a restore trap)The anchor was
if (literals.some((literal) => !newRe.test(literal))) {. For each leg the on-disk count went anchor 1 → 0 and replacement 0 → 1, and the blob changed.if (literals.length > 0) {, the pre-fix predicate (old prefix alone). Predicted red; observed red:Tests 3 failed | 26 passed (29). The failures were both "reports nothing once the rewrite has run" cases and the scaffold case, which printed the original✗ Scaffolding rewrote the namespace to 'blank_probe' but 1 object name(s) still carry the template's 'blank_' prefix.if (literals.length > 0 && !newRe.test(lines[i])) {, a line-level predicate. Predicted red; observed red:Tests 2 failed | 27 passed (29). The failures were both "judges each literal" cases.blob == HEAD (35eae3f4e754) and git diff HEAD is empty, porcelain empty.No
dist/is involved: the unit tests import./rewrite-identity.jsfromsrc/, and the scaffold test runssrc/index.tsthroughtsx.Edge answers (measured, one-off probe on
8679418942)blank→blank_probeblank_probe_noteblank→blank_probeblank_noteblank_probe→blankblank_noteblank_probe→blankblank_probe_noteblank→blankblank_noteblank→my_appblank_noteThe "reverse, no rewrite" row reads 0 because
blank_probe_noteis legal under namespaceblank(blank_+probe_note). The project would not be rejected, which is the question the check answers. The caller never calls the check for "equal" (namespace !== templateNamespaceguard).Gates (at
8679418942)node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, with no paths, derived 61 commands from merge baseeab7236ff(5 paths, +153 / -8). That matches the PM's dispatch-time count. All 61 ran with the exit code captured before any pipe:check-adr-0087-registration --base origin/main: "this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)"check:nul-bytes: "OK (scanned 10960 text file(s) ... no raw ASCII control bytes)"check:cross-package-test-inputs: "OK: 30 package(s) read outside themselves, all declared"check:test-source-alias: "OK — 73 packages with tests scanned"check:issue-citations: "no issue citations added against eab7236"check:published-files: "70 publishable package(s) ... declare a files whitelist"check:type-check-debt: "--re-measure: OK — 1 ledger entr(ies) re-measured ... none above its recorded number"pnpm check:dual-build-cjs-loads, exit 3, NOT MEASURED: "PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/ ... ⛔ This is NOT a pass: nothing was measured." It needs a whole-repopnpm build.create-objectstackships ESM only. CI's Build Core runs it.pnpm check:lean-entry-closure: exit 3 on the first pass ("PREREQUISITE NOT MET ... packages/objectql/dist/core.mjs"). Afterpnpm --filter '@objectstack/objectql...' build(under the lock,VERDICT command-exit 0), the re-run gave exit 0: "✓ check-lean-entry-closure: 2 published condition(s) measured from a real load".dispatch-gates --repo objectstack-ai/objectstack --ran FILE, with each line recorded asCOMMAND :: exit CODE, printed "61 derived famil(ies) accounted for — 60 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)".check-changeset-fixed: "fixed group is in sync with 70 public workspace packages"check:authz-resolver: "single shared authorization resolver intact"check:error-code-casing: "no unlisted lowercase error codes in 8216 scanned file(s)"check:filter-alias-parity: "OK (4 transport spelling(s) ...)"check:scaffold-emission-policy: "2 carrier(s) emit the shared policy"Lint (proven narrowing;
pnpm lintis CI's)eslint.config.mjs):files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']minusNEVER_LINTED.eslint --print-configreturns a config for each of the 4 touched.tsfiles (5 to 6 rules each), so none is ignored.--format json): 4 files, 0 errors, 0 warnings, run with--no-inline-configaspnpm lintdoes.parserOptionsis{ ecmaVersion, sourceType }, with noprojectorprojectService. Its only disk reads are its own baselines, so this diff cannot move a verdict on any untouched file.Acceptance notes
TEMPLATESintemplate-registry.tsholds onlyblank, andsrc/templates/holds onlyblank/andAGENTS.md. The scaffold-level pin iteratesTEMPLATESand names each probe from that template's own namespace, so a second template is pinned on registration.X_Ywith project namespaceX) cannot be reached with the bundled template, becauseblankcontains no underscore. If a rewrite were skipped there, the check would pass names that are already legal under the new namespace. That is consistent with the decided predicate, not a defect.origin/mainmoved two commits after this branch was cut (3caae97942,018b077fbf). Both touchpackages/objectqlonly, andgit diff --stat eab7236ff7 origin/main -- packages/create-objectstackis empty. They were not merged in; the merge queue rebuilds onmain..changeset/22911-blank-prefixed-project-name.mdiscreate-objectstack: patch, withClause-②: no.Generated by Claude Code