Skip to content

chore(cli): retire SCAFFOLD_TSX_RANGE, a conditional statement nothing satisfies - #16951

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-16668-scaffold-tsx-range-disposition
Sep 8, 2026
Merged

chore(cli): retire SCAFFOLD_TSX_RANGE, a conditional statement nothing satisfies#16951
os-project-manager merged 1 commit into
mainfrom
claude/issue-16668-scaffold-tsx-range-disposition

Conversation

@claude

@claude claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Fixes #16668

Clause-②: no
SCAFFOLD_TSX_RANGE was a package-internal module export, not a published surface.
Re-declared here from the delivered diff rather than from card content: this
package's exports map is . / ./console / ./hook-body / ./package.json
with no subpath pattern, src/index.ts re-exports only the oclif command classes,
and there is no packages/cli/api-surface baseline. Removing the constant relaxes
no accept set and withdraws nothing anyone could import. The diff emits no new
scaffold script, so nothing about what the tool produces changes.

The direction question, answered first

Is there any surviving scaffold that should emit a script needing tsx? Name the
scaffold and the script and go direction 2; if you cannot name one, go direction 1 and
write "no taker" into the PR.

No — and the taker is: none. Direction 1, retire the constant.

The reading, re-taken on origin/main@9a89a0040d rather than relayed:

$ git grep -n "SCAFFOLD_TSX_RANGE"
packages/cli/src/commands/init.ts:321                        export const SCAFFOLD_TSX_RANGE = '^4.21.0';
packages/cli/test/scaffold-emission-policy.e2e.test.ts:207   // ...a COMMENT, not a consumer

$ git grep -n "tsx" -- packages/cli/src/commands/init.ts packages/cli/src/commands/create.ts
packages/cli/src/commands/init.ts:320   /** The `tsx` range a scaffolded project declares when its scripts need it. */

Control (the instrument fires): SCAFFOLD_PNPM_RANGE / SCAFFOLD_TYPESCRIPT_RANGE
hit 8 times in that same file. So the two hits above are a zero, not a broken search.

Why no surviving scaffold should declare it, which is the half a grep cannot answer.
tsx exists to execute a .ts entrypoint directly. Reading the scripts every surviving
emission actually writes:

emission scripts it emits
os init -t app objectstack dev · objectstack compile && objectstack serve · objectstack compile · objectstack validate · objectstack lint · tsc --noEmit
os init -t plugin objectstack compile · objectstack validate · objectstack lint · vitest run · tsc --noEmit
os init -t empty objectstack compile · objectstack validate · objectstack lint · tsc --noEmit
os create plugin tsc · tsc --watch · vitest · tsc --noEmit
create-objectstack blank template objectstack dev · objectstack start · objectstack build · objectstack validate · objectstack lint · tsc --noEmit

Not one of them runs a .ts file directly. Every one routes through objectstack, tsc
or vitest, each of which carries its own TypeScript handling. And the capability is not
missing from a scaffolded project either: @objectstack/cli — which every emission above
declares — lists tsx: ^4.23.12 among its own dependencies. A scaffold re-declaring
tsx would have been announcing a second, lower floor (^4.21.0) for a binary the
project never invokes by name.

That also settles the character of the alternative: direction 2 would have been adding
a capability, not restoring a lost one — so there was no gap to close.

Before / after

-/** The `tsx` range a scaffolded project declares when its scripts need it. */
-export const SCAFFOLD_TSX_RANGE = '^4.21.0';
-

Constant and docblock removed together. Not rewritten to "reserved for future use":
that is a promise nobody made.

The reason: removing a conditional statement whose antecedent nothing satisfies

The card called the docblock false. Strictly it is not:

The tsx range a scaffolded project declares when its scripts need it.

That is a conditional, and its antecedent holds nowhere on the tree — so the sentence
is vacuously true, not false. That is a worse class than a lie, not a lesser one: no
review looking for false statements catches it, because it tells none. What it misleads is
the reader's default assumption that some scaffold satisfies the antecedent. So the fix is
described as removing a conditional statement nothing satisfies, never as "fixing a false
comment".

The :207-211 comment, updated rather than left describing the intermediate state

No assertion is added for the retired range — PR #16654 already wrote down why, and that
reasoning is kept intact rather than inverted: a row is owed by a range some emission
really declares, and by nothing else.
The comment used to describe the halfway state
("the constant is still here but reaches nothing"); it now describes the retirement, and
states the condition under which a tsx row would legitimately return.

Changeset: measured, not assumed — and skip-changeset is not defensible here

packages/cli builds with plain tsc -p tsconfig.build.json, no bundler and no
tree-shaking, so a package-internal export still reaches the tarball. Measured with a
before/after build of packages/cli in one run (mutate the two files back to the base
revision, rebuild, snapshot; restore, rebuild, snapshot), hashing every file under the
published files[] entry dist:

snapshot before: 500 files
snapshot after:  500 files
diff  ->  4 files move, 496 byte-identical:
    dist/commands/init.js      dist/commands/init.js.map
    dist/commands/init.d.ts    dist/commands/init.d.ts.map

Symbol search inside those shipped paths at the base state, with its positive control:

target   SCAFFOLD_TSX_RANGE        -> 2 shipped files
           dist/commands/init.js:303    export const SCAFFOLD_TSX_RANGE = '^4.21.0';
           dist/commands/init.d.ts:155  export declare const SCAFFOLD_TSX_RANGE = "^4.21.0";
control  SCAFFOLD_TYPES_NODE_RANGE -> 3 shipped files   (a sibling constant of identical
                                                         kind, still present: the search fires)
value    4.21.0                    -> 2 shipped files

Restore leg proven rather than assumed: both files restored to their HEAD blob hashes
byte-for-byte (bdc57a5b8c… and 9c2201fa1b…), git diff HEAD empty, and the rebuilt
dist snapshot identical to the first after snapshot — so the rebuild really re-ran and
the reading is not a cache artifact.

Reading: something inside files[] moves, so this diff publishes. A patch changeset
for @objectstack/cli is added. It is patch and not breaking because the exports map
never exposed the symbol — nothing importable is withdrawn.

Docs drift: re-derived from a clean worktree, plus a hand sweep with a live control

scripts/docs-audit/affected-docs.mjs --json, re-derived on a clean tree:

computedOn: { diffBase: 9a89a0040d…, dirty: false }
anchors:    symbol SCAFFOLD_TSX_RANGE   |   command "os init"
docs:       6

The tool's number is not zero, and reading it correctly matters: all six pages are
reached through the os init command anchor — they mention the command whose file this
diff touches. The SCAFFOLD_TSX_RANGE symbol anchor matched zero pages.

Because the tool's zero on the symbol is not a clean bill, content/ was swept by hand for
this change's tokens:

SCAFFOLD_TSX_RANGE in content/          -> 0 files
literal 4.21 in content/                -> 0 files
word-boundary tsx in content/           -> 8 files, every one of them either a
                                           ```tsx code-fence language tag (JSX syntax
                                           highlighting, an unrelated sense of the word)
                                           or automation/hook-bodies.mdx:338, which
                                           describes the CLI's OWN loader ("already loaded
                                           by tsx/esbuild") and is untouched by this diff
devDependencies printed in content/     -> 1 page (protocol/kernel/plugin-spec.mdx:706),
                                           listing @objectstack/cli and typescript only

Live positive control for the blind spot named on the card — a page stating a rule by
its inputs
, sharing no identifier with the emitter:

$ grep -rn "TypeScript 5\.3" content/
content/docs/getting-started/index.mdx:183      ObjectStack works with TypeScript 5.3+, …
content/docs/deployment/troubleshooting.mdx:359 TypeScript 5.3.0 or later …

Two hits. That is exactly the shape the tool cannot see and the sweep can, so the sweep is
live rather than silently empty. Reading: no content/ page states or implies that a
scaffolded project declares tsx. No docs change is owed.

Verification

Exit codes captured before any pipe, throughout.

  • Gate familiesnode scripts/pm/dispatch-gates.mjs --commands derived 59; all
    59 were run and reconciled with --ran:
    ✓ 59 derived famil(ies) accounted for — 59 run, 0 NOT-MEASURED, 0 UNRUN.
    Re-derived at the final commit: the family list is byte-identical.
  • 56 of 59 green. The other three are PREREQUISITE NOT MET (exit 3) and are recorded
    as NOT MEASURED, never as red: check:dual-build-cjs-loads, check:i18n-coverage and
    check:type-check-debt each refuse to measure until a whole-repo pnpm build exists
    (12 packages, among them @objectstack/studio and the connectors, have no dist). CI
    builds that closure on a fresh checkout. Each gate says so in its own verdict text.
  • pnpm check:scaffold-emission-policy — exit 0. This is the family the diff touches
    most directly: scripts/sync-scaffold-emission-policy.mjs reads SCAFFOLD_* constants
    out of init.ts, and its POLICY_STAMPS table names only SCAFFOLD_PNPM_RANGE and
    SCAFFOLD_TYPESCRIPT_RANGE, so the retired constant had no stamp row to remove.
  • Tests. The edited test file is a nightly-tier file by name (*.e2e.test.ts) and an
    integration-tier file by behaviour, so the default population excludes it; it was run
    explicitly: OS_TEST_TIERS=nightly vitest run test/scaffold-emission-policy.e2e.test.ts
    Test Files 1 passed (1) · Tests 10 passed (10). Plus the package's unit tier:
    vitest run --project unitTest Files 189 passed (189) · Tests 2612 passed | 6 expected fail.
  • The comment edit does not move the file between projects — measured, not reasoned:
    tierSignals over the comment-masked source reads childProcess, entryBasename, tsxBin
    and isIntegration = true at both the base revision and this commit, identically.
  • pnpm --filter @objectstack/cli typecheck — exit 0, tsc --noEmit plus
    check:test-typecheck, both clean. (A first run exited 2 with 33 TS7016 errors naming
    @objectstack/driver-sql and @objectstack/plugin-hono-server; that is the shared-turbo-cache
    stale-artifact class AGENTS.md §9 describes — six packages had dist/*.js but no
    dist/*.d.ts. Zero of those 33 errors named init.ts, SCAFFOLD_ or the test file. A
    forced rebuild of those six cleared all 33.)
  • Lint, full repo rather than a narrowed runeslint . --no-inline-config --format json,
    exit 0: 6383 files linted, 0 errors, 0 warnings, with both touched files present in
    the run at 0/0. No narrowing argument is needed because nothing was narrowed.

All readings above were taken on the tree of the final commit 89a49d0923; the gate sweep
ran against a byte-identical tree (both resolve to tree ac7db785fa).

验收备注

Out-of-scope observations, noted and deliberately not filed — none of them is a
reproducible defect, a declared-contract violation, or a metadata-authoring trap, which
are the only three filing categories here:

Fences honoured: packages/cli/src/index.ts and package.json's exports are untouched;
content/docs/releases/ is untouched; no assertion was added for the retired constant; the
diff stays inside the two declared files plus the changeset.


Generated by Claude Code

…g satisfies

The `os create example` template was the only emission that ever declared a
`tsx` range; once it was retired the constant reached no scaffold at all.

Its docblock -- "The `tsx` range a scaffolded project declares when its scripts
need it" -- is not a false statement but a conditional whose antecedent nothing
on the tree satisfies. Vacuously true, so a review hunting false statements
never catches it; what it misleads is the reader's default assumption that some
scaffold satisfies the antecedent. The constant and its docblock are removed
together.

No surviving emission should declare `tsx`: all four (`os init -t app` /
`-t plugin` / `-t empty`, `os create plugin`) plus the bundled
`create-objectstack` blank template run every script they emit through
`objectstack`, `tsc` or `vitest`, none of which executes a `.ts` entrypoint
directly. `@objectstack/cli`, which each of them declares, already carries `tsx`
as its own dependency.

No assertion is added for the retired range. The comment in
`test/scaffold-emission-policy.e2e.test.ts` is updated to describe the
retirement rather than the intermediate state, keeping its rule intact: a row is
owed by a range some emission really declares, and by nothing else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
@github-actions github-actions Bot added the size/s label Sep 8, 2026
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 2 documentable anchor(s).

5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx (via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/getting-started/examples.mdx (via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/getting-started/your-first-project.mdx (via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/plugins/index.mdx (via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/protocol/kernel/index.mdx (via os init (command, read off packages/cli/src/commands/init.ts))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx (via os init (command, read off packages/cli/src/commands/init.ts))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 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.

Coarse fallback — 22 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 f7da71eb7e9c734d21c738e211ceccc2c176d7c9packageMentionDocs.

Which tree this was computed on

This run read content/docs from 11e8350c72e1486ca8ac41e1fc6b01c40378213c — the merge of head 89a49d0923236385f5bfb0d4c3c8862a5df8b968 into base f7da71eb7e9c734d21c738e211ceccc2c176d7c9, 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 11e8350c72e1486ca8ac41e1fc6b01c40378213c && git checkout 11e8350c72e1486ca8ac41e1fc6b01c40378213c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f7da71eb7e9c734d21c738e211ceccc2c176d7c9 89a49d0923236385f5bfb0d4c3c8862a5df8b968 && git checkout -B drift-repro f7da71eb7e9c734d21c738e211ceccc2c176d7c9 && git merge --no-ff 89a49d0923236385f5bfb0d4c3c8862a5df8b968

node scripts/docs-audit/affected-docs.mjs --json f7da71eb7e9c734d21c738e211ceccc2c176d7c9

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs f7da71eb7e9c734d21c738e211ceccc2c176d7c9 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Collaborator

PM review — accepted, arming

CI, with the latest-per-name collapse stated even though it changes nothing. 34 raw check-run rows; every name on this head is distinct, so the collapse is 34 → 34. Terminal states: 30 success, 4 skipped (Console Pin Gate, Build Docs, Packed-tarball smoke (opt-in), and filter's downstream skips), 0 red. The last row to land was Lint & Repo Gates (102185699076, 18:27:06Z → success). mergeable_state: clean.

Clause-② re-run by me, not relayed. node scripts/pm/check-clause2-carriers.mjs --pair 16951exit 0: declaration readable in the fixed spelling, both carriers agree, no widening tell. Provenance line names the board (objectstack-ai/objectstack, source: default) and the read path (token: present, served 4 read(s)). ⚠️ Recording the standing caveat the tool prints itself: a tell is not a proof and its absence is not one either — which is why I re-took the declaration's own evidence below.

The clause-② no re-derived from the file rather than from the PR body. packages/cli/package.json on head 89a49d0923 carries exactly . / ./console / ./hook-body / ./package.json, no subpath pattern. So no consumer could ever have imported SCAFFOLD_TSX_RANGE, and removing it relaxes no accepted set. The declaration stands.

Removal safety — anchor finds, I judge. git grep SCAFFOLD_TSX_RANGE on the head returns 3 hits, read out individually rather than counted: two are changeset prose (title line 5, body line 7) and one is the rewritten // comment at test/scaffold-emission-policy.e2e.test.ts:207. Zero live consumers. Positive control SCAFFOLD_TYPES_NODE_RANGE fires with 5 hits including a real import at create.ts:125 and a real emission site at create.ts:323 — so the zero is a zero, not a broken search.

The direction answer's load-bearing claim, verified verbatim. The PR argues direction 2 would have been adding a capability rather than restoring one, because @objectstack/cli already carries tsx. Read from the file: "tsx": "^4.23.12" sits inside "dependencies", not devDependencies — so it really does reach every scaffolded project that declares the CLI. The claim holds, and with it the reading that a scaffold re-declaring ^4.21.0 would have announced a second, lower floor for a binary it never invokes.

What I am accepting on the delivering seat's measurement rather than re-measuring. The skip-changeset disposition: mutate-to-base / rebuild / snapshot / restore / rebuild, both legs proven (500 files before and after, exactly 4 move, restore verified against the HEAD blob hashes with an empty git diff HEAD). That is the right instrument for this question and it was run with both legs — I am not re-running a build to re-derive a result whose method is stated and whose restore leg is proven. The reading it produces — something inside files[] moves, so a patch changeset is owed and skip-changeset is not defensible — matches the two prior times this seat's cheap guess was measured wrong (#16801, #16668's own sibling reasoning).

Docs drift read correctly. The tool's 6 is not a clean bill in the direction it looks: all six pages come through the os init command anchor, and the SCAFFOLD_TSX_RANGE symbol anchor matched zero. The hand sweep carries a live positive control for exactly the blind spot that zero hides — a page stating a rule by its inputs ("TypeScript 5.3" → 2 hits) — so the sweep is live rather than silently empty.

Marking ready for review, then arming auto-merge — in that order, since a draft conversion after arming destroys both.


Generated by Claude Code

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/s tests tooling

Projects

None yet

2 participants