Skip to content

fix(create-objectstack): a project name that starts with the template namespace scaffolds instead of being refused - #22921

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-22911-blank-prefixed-project-name
Oct 12, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-22911-blank-prefixed-project-name

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22911
Clause-②: no

What this changes

create-objectstack blank-probe exited 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_note became blank_probe_note). The self-check after it, findStaleNamespacePrefixes, tested each name against the template prefix alone, and blank_probe_note starts with blank_ because the new namespace blank_probe does.

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-prefixed name: literal on a line is checked against the same namePrefixRe built from the new namespace. One predicate for every template namespace, with no exemption for blank. 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 the namespace it has just written. Nothing outside the package imports the function. The only other rewrite-identity mentions are comments, and the package's exports map publishes ./created-summary only.
  • Error text and rewrite logic are unchanged.

Measured at the public door

Repo-built CLI (tsx packages/create-objectstack/src/index.ts blank-probe --skip-install --skip-skills):

tree exit result
base eab7236ff7 1 Scaffolding 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',
this PR 8679418942 0 src/objects/note.object.ts:6 name: 'blank_probe_note', and objectstack.config.ts:27 namespace: 'blank_probe',

Tests

  • rewrite-identity.test.ts: the two existing findStaleNamespacePrefixes cases now pass the new namespace. A new describe.each runs over blank → blank_probe and todo → todo_list, so the same pins run under two template namespaces. It has three cases:
    • after the rewrite, nothing is stale;
    • CONTROL: without the rewrite, the genuinely stale OLD_note is still reported;
    • a line carrying a correct name and a stale one is still reported.
  • scaffold-namespace-extends-template.test.ts (new) drives the real CLI through tsx, the pattern scaffold-description.test.ts uses. There is one case per bundled template in TEMPLATES. Each probe project is named from that template's own namespace (blank gives blank-probe). The case asserts:
    • exit 0;
    • the scaffolded namespace is blank_probe;
    • every template-prefixed name moved to blank_probe_*;
    • each name passes the spec's own validateObjectNamespacePrefix.
  • Runs at 8679418942, through the shared verify lock:
    • pnpm --filter create-objectstack exec vitest run --maxWorkers=2 followed by pnpm --filter create-objectstack run typecheck: Test Files 18 passed (18), Tests 261 passed (261), tsc --noEmit clean, VERDICT command-exit 0.
    • tsc --noEmit --listFiles includes both touched test files (18 test files in the program).

Ablation (committed fix, two legs, scripts/ablation-replace.mjs WRAP 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.

  • Leg A: 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.
  • Leg B: 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.
  • Restore, after each leg and again in the trap: blob == HEAD (35eae3f4e754) and git diff HEAD is empty, porcelain empty.

No dist/ is involved: the unit tests import ./rewrite-identity.js from src/, and the scaffold test runs src/index.ts through tsx.

Edge answers (measured, one-off probe on 8679418942)

case old → new name on disk stale
forward, rewritten blank → blank_probe blank_probe_note 0
forward, no rewrite blank → blank_probe blank_note 1
reverse, rewritten blank_probe → blank blank_note 0
reverse, no rewrite blank_probe → blank blank_probe_note 0
equal blank → blank blank_note 0
disjoint, no rewrite blank → my_app blank_note 1

The "reverse, no rewrite" row reads 0 because blank_probe_note is legal under namespace blank (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 !== templateNamespace guard).

Gates (at 8679418942)

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, with no paths, derived 61 commands from merge base eab7236ff (5 paths, +153 / -8). That matches the PM's dispatch-time count. All 61 ran with the exit code captured before any pipe:

  • 59 exit 0. Examples:
    • 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-repo pnpm build. create-objectstack ships 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"). After pnpm --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".
  • Reconciliation: dispatch-gates --repo objectstack-ai/objectstack --ran FILE, with each line recorded as COMMAND :: exit CODE, printed "61 derived famil(ies) accounted for — 60 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)".
  • Also run: the four artifact-roster families whose roster sits under a directory this diff touches, plus the scaffold one. All exit 0:
    • 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 lint is CI's)

  • Population (from eslint.config.mjs): files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'] minus NEVER_LINTED. eslint --print-config returns a config for each of the 4 touched .ts files (5 to 6 rules each), so none is ignored.
  • Count (from --format json): 4 files, 0 errors, 0 warnings, run with --no-inline-config as pnpm lint does.
  • Invariance: the config never enables type-aware linting. Every parserOptions is { ecmaVersion, sourceType }, with no project or projectService. Its only disk reads are its own baselines, so this diff cannot move a verdict on any untouched file.

Acceptance notes

  • The "second template" pin does not apply today. TEMPLATES in template-registry.ts holds only blank, and src/templates/ holds only blank/ and AGENTS.md. The scaffold-level pin iterates TEMPLATES and names each probe from that template's own namespace, so a second template is pinned on registration.
  • The reverse case (a template namespace X_Y with project namespace X) cannot be reached with the bundled template, because blank contains 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/main moved two commits after this branch was cut (3caae97942, 018b077fbf). Both touch packages/objectql only, and git diff --stat eab7236ff7 origin/main -- packages/create-objectstack is empty. They were not merged in; the merge queue rebuilds on main.
  • Changeset .changeset/22911-blank-prefixed-project-name.md is create-objectstack: patch, with Clause-②: no.

Generated by Claude Code

…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
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 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
  • the SDK route bridge reached 54 of 206 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 — 11 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 018b077fbf8876c0fd5f46e66dcc551a4e74c895 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 9c9f68e6feb41c1cdcc4176c9c227553ebd0f572 — the merge of head 8679418942c91827751803a3eda0ff92c724165b into base 018b077fbf8876c0fd5f46e66dcc551a4e74c895, 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 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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants