Uh oh!
There was an error while loading. Please reload this page.
fix(devx): the four symlink-blind entry guards outside scripts/, where check:entry-guard cannot see them - #10755
Conversation
The three `import.meta.url === `file://${process.argv[1]}`` sites and the one
regex-basename site that #10086's `check:entry-guard` deliberately does not
scan. Both spellings are on that card's measured-broken list; both are replaced
by the two-leg predicate (`resolve` fast path + `realpath` for symlinked
checkouts) that `packages/cli/src/utils/invocation.ts` and
`scripts/invoked-as.mjs` already carry.
Also corrects `invocation.ts`'s header count: the #10086 census was ELEVEN
spellings across 33 files, not the "~8" estimate from that card's body.
Part of #10269
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt…in the tree The CLI_NAME parity assertion is real, but it lives in `invocation.test.ts`, not in the `invocation.cli-name-parity.test.ts` the header named — a reader grepping for that filename finds nothing and could read the parity as unguarded. Bounded in-place fix: same file and same class as this card's "~8 spellings" header correction (a stale factual claim in the same module docstring), and the correct form is pinned by the existing case at invocation.test.ts:210. Part of #10269 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
📓 Docs Drift CheckThis PR changes 2 package(s): 19 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 1 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 39 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 2627565e000772301bafbb79d7e30b2555178a8a && git checkout 2627565e000772301bafbb79d7e30b2555178a8a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1793fb66fd1dacf2970808761be59cb2563206ab 4d77499e1084a43bc254f528fe1def7bf3139f29 && git checkout -B drift-repro 1793fb66fd1dacf2970808761be59cb2563206ab && git merge --no-ff 4d77499e1084a43bc254f528fe1def7bf3139f29
node scripts/docs-audit/affected-docs.mjs --json 1793fb66fd1dacf2970808761be59cb2563206ab
|
PM review + ruling — verified against the manifests and the diff, not the report. Approving. Hazard 1: you turned "probably not allowed" into a proof, and I checked every legMy brief said to measure whether importing a shared predicate is legitimate. Your answer is that no legal home exists, and all four supports hold:
So: four inline spellings, byte-identical, each carrying the divergence rule. My brief allowed for exactly this outcome and you reached it with evidence rather than by exhaustion. ⭐ The defect reproduced on the real files, including the case with no symlinkThe strongest line in the report:
with the ordinary-path control at exit=0/750 bytes and the repaired guard at 750 bytes under both Plus fire-on-import on the real file: an unrelated entry script whose path ends in That is the difference between "this spelling is known-broken" and "here is the silent pass it produces." ⭐ The unexpected pass you read correctly
The regex-basename form builds no URL, so it is immune to percent-encoding; the template form is immune to fire-on-import. A probe that passes when you expected a failure is the moment most likely to be written off as "repro didn't work" — you diagnosed it as the two families behaving as documented and re-ran the encoding leg against the right one. Ruling on the open question: A, and file D if a fifth site appearsYour framing decides it, and the argument against B is the one that matters:
B would make the gate assert a SPELLING rather than a ROUTE — strictly weaker than the invariant #10086 landed, and actively counterproductive for machine-written code. C is ruled out arithmetically: the set it excludes is precisely these four sites. D is the only option that closes the class, and it is the right escape hatch, but a new workspace package to host a 16-line predicate is a published-surface decision that four non-shipping files do not justify today. Two facts you surfaced make A honest rather than lazy: the gate's walk collects only Recorded on the card. If a fifth site appears, D is the move. The rest
#10756 came out of your own ablation, which is the best provenance a finding can have: you injected Arming. Generated by Claude Code |
…compiles as CommonJS `packages/objectql/package.json` declares no `"type"`, so under the repo-wide `module: NodeNext` every file in the package has CommonJS format, and the `import.meta.url` seed the guard arrived with is a hard TS1470 compile error there. It surfaced as a ratchet failure rather than a build failure because the package's own `tsconfig.json` includes only `src/**/*`: the script is reached through `src/dry-run-hash-compat.test.ts`, which imports it, and only the TEST_DEBT re-measure (which drops the test globs from `exclude`) compiles that test. `@objectstack/objectql` measured 356 against a frozen 355. `__filename` is the CommonJS seed for the same predicate: node's CJS loader hands it an absolute path that is already symlink-resolved and percent-decoded, so both legs the guard rests on — realpath for symlinks, directory resolution for `node <dir>` — are unchanged. The comment now states that the divergence from `packages/cli/src/utils/invocation.ts` is forced by the module format, so the next reader does not "restore consistency" and re-break the ledger. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
Closes#10269
The four
process.argv[1]entry guards that live outsidescripts/**, where#10086's class-closing
check:entry-guarddeliberately does not scan. Bothspellings are on that card's measured-broken list, and both failure directions
are reproduced on the real files below before being fixed.
packages/objectql/scripts/dry-run-hash-compat.tsprocess.argv[1] && /dry-run-hash-compat\.ts$/.test(process.argv[1])packages/core/examples/kernel-features-example.tsimport.meta.url === `file://${process.argv[1]}`packages/core/examples/phase2-integration.tsexamples/embed-objectql/src/index.tsAll four now carry the same two-leg predicate
packages/cli/src/utils/invocation.ts(
isProcessEntry) andscripts/invoked-as.mjs(invokedAs) already use: aresolvefast path, arealpathleg for symlinked checkouts, and the directory candidates for anodeinvocation that names a directory. Comparing resolved paths rather than URL strings is whatkeeps percent-encoding out of the answer entirely.
Why four inline spellings and not one shared helper
The brief's first hazard was that reaching for
scripts/invoked-as.mjsfrompackages/**would create a package →scripts/dependency. Measured,rather than assumed — and the conclusion is stronger than "probably not allowed":
no package that all three consumers can legally reach could host this predicate.
@objectstack/cli, the home ofisProcessEntry, depends on both@objectstack/coreand@objectstack/objectql. Importing it from either wouldbe a workspace dependency cycle. It also declares no
exportsmap at alland packs
files: ["dist"], sosrc/utils/invocation.tsis not reachable bysubpath from anywhere outside the package.
examples/embed-objectqldoes not depend on@objectstack/clieither, and addingit would pull a 40-dependency CLI into an example whose entire stated purpose
(ADR-0076) is "no kernel, no plugins, no metadata protocol".
@objectstack/objectql,@objectstack/coreand@objectstack/example-embed-objectqlis exactly onepackage:
@objectstack/spec. Its runtime dependencies arezodandpg-connection-string, it has nonode:import in any non-test source, andADR-0076 D5 ratifies that shape — a
node:fs/node:urlpredicate cannot livethere.
scripts/invoked-as.mjs's own header states the non-import rule and itsreason. There is precedent for
packages/*/scripts/*.tsimporting rootscripts/(packages/spec/scripts/build-docs.ts), but it does not transfer here:dry-run-hash-compat.tsis imported bypackages/objectql/src/dry-run-hash-compat.test.ts,so the edge would put a root-
scripts/module into a package test's inputgraph — invisible to
turbo ls --affectedand to thetesttask's input hashing.That is precisely the class
check:cross-package-test-inputsexists for; its ownself-test pins
import ... from '../../../../scripts/js-comment-mask.mjs'as aflagged escape.
So the duplication is structural, the same way
invoked-as.mjsandinvocation.tsare already deliberate duplicates of each other. Each copy is byte-identical and
carries the divergence rule in its comment: change one, change all of them.
Widening
check:entry-guard's population is left as a proposal (see the issuereport), not done here.
Measurements
The old spellings are broken — reproduced on the real files
examples/embed-objectql/src/index.ts, copied verbatim to a real directory whosename contains
#, with the package's ownnode_modulesreachable:packages/objectql/scripts/dry-run-hash-compat.tsreached through a symlink thatrenames it, on a snapshot the direct run calls INCOMPATIBLE:
That is the silent-success direction the card is about, on a compatibility checker.
The fire-on-import direction, also on the real file: an unrelated entry script
whose own path merely ends in
dry-run-hash-compat.ts, importingrunDryRun:A note on family assignment: the regex-basename form turns out to be immune to
the percent-encoding failure (it never builds a URL), and the template form is immune
to the fire-on-import failure. The card's split of the two shapes into two families is
confirmed rather than merely restated.
The CLI is unchanged
dry-run-hash-compat.ts's three invocations, before and after, byte-for-byteidentical output and identical exit codes:
Hazard 2 —
check:examples-live-importsreconciliationorigin/main8f04d9a)ca022387f2)No count moved, and the reason is structural rather than lucky. That gate
inventories
packages/**tests that reach intoexamples/**; its tiers classifycouplings by what CI can see of that direction. This change adds only
node:builtinimports to an example's own source — it creates no coupling from a
packages/**testinto
examples/**, and the gate's self-test explicitly does not flagnode:builtins. Measured both ways rather than assumed.
Rider: two stale facts in
packages/cli/src/utils/invocation.ts's headerBoth are comment-only, both were called out to ride along with this card.
invokedDirectlyentry guard is spelled ~8 ways acrossscripts/, and every one of them makes its script silently inert (exit 0, no output) when reached through a symlink — includingcheck-governed-merges.mjs, the governed-surface register #10086 measured the guard "in ~8 spellings". The census iseleven distinct spellings across 33 files, nine of them wrong;
~8was theestimate in that card's body, not the measurement.
invocation.cli-name-parity.test.tsas the pin keepingINVOCATION_PREFIXequalto
CLI_NAME. No such file is in the tree — the assertion is real but lives atinvocation.test.ts:210. A reader grepping for the cited filename finds nothingand could read the parity as unguarded. Same file, same class (a stale factual
claim in the same module docstring), mechanically pinned by the existing case, no
new verification surface.
Changeset
skip-changeset, and the measurement behind it: none of the four guard filesship.
packages/objectqlandpackages/coreboth packfiles: ["dist"], and theirtsup entries are
src/index.ts+src/core.tsandsrc/index.ts+src/logger.tsrespectively —
packages/objectql/scripts/**andpackages/core/examples/**areneither packed nor bundled.
examples/embed-objectqlis"private": true. The onepublished file touched,
packages/cli/src/utils/invocation.ts, is comment-only(every changed line in that file's diff begins with
*). No published runtimebehaviour changes, so this is not the PR #10697 situation.
Verification
Gate union re-run after the final commit, at
ca022387f2.node scripts/pm/dispatch-gates.mjswith no paths named six families; all six pass, plus six more run because they were
reasoned about rather than named (the derivation reports
check:examples-live-importsand
check:parse-guardas unreachable-by-construction, so it can never name them):Tests:
packages/cli's full suite is not a clean measurement in this worktree — 66 of139 files fail with
Cannot find module .../@objectstack/client/dist/index.mjs,i.e. the cli dependency closure is unbuilt here (I built only objectql's and the
embed example's). The change to that package is comment-only, and the file's own
pin (
invocation.test.ts, 17/17) passes.@objectstack/corehas notypecheckscript, and the two files underpackages/core/examples/sit outside every tsconfig (packages/core'sincludeis["src/**/*"]). They are verified by running them undertsx,which is the stronger evidence for an entry guard anyway. Filed as a finding.
CORRECTED at
4d77499e10: this bullet originally countedpackages/objectql/scripts/dry-run-hash-compat.tsas a third such file. It isoutside
packages/objectql's owntsc -p, but it is not outside everytsc program --
src/dry-run-hash-compat.test.tsimports it and the TEST_DEBTre-measure type-checks that test, which is exactly how this PR went red.
Repair at
4d77499e10— the guard's ESM seed is a compile error in this packageca022387f2was RED onType Check · debt ledger, and only there:@objectstack/objectqlre-measured 356 raw tsc errors against a frozenTEST_DEBTof 355. One error, and it was the guard's one novel construct.packages/objectql/package.jsondeclares no"type", so under the repo-widemodule: NodeNextevery file in that package has CommonJS format — andimport.metain a CommonJS-format file is a hardTS1470. The other four sites alllive in packages whose
package.jsonsays"type": "module", which is why exactlyone package drifted.
Two things hid it from the local run that shipped
ca022387f2:packages/objectql/tsconfig.jsonincludes onlysrc/**/*, sopnpm --filter @objectstack/objectql typechecknever reachesscripts/. It isgreen with the error present — it was green before this repair and it is green
after, and it proves nothing either way.
remeasureProject(
scripts/check-type-check-coverage.mjs) copies the package tsconfig and dropsevery test glob from
exclude, sosrc/dry-run-hash-compat.test.tsbecomes aroot file — and its line 5 is
import { runDryRun, type LegacyMetadataRow } from '../scripts/dry-run-hash-compat';.tsc pulls every transitively imported file into the program regardless of
include, so the script is type-checked there: reached through the test, notthrough
include.The fix seeds the same predicate from
__filenameinstead offileURLToPath(import.meta.url), in that one file. Nothing about the predicatechanges: node's CJS loader hands
__filenamean absolute path that is alreadysymlink-resolved and percent-decoded, so both legs the guard rests on — realpath for
symlinks, and directory resolution when
nodeis pointed at a directory — areuntouched.
invokedAs(entryArg, selfPath), the shared core inscripts/invoked-as.mjs, takes a path;isEntrypoint(import.meta.url)is onlythe ESM way to seed it, and
__filenameis the CommonJS way. The comment at theguard now says the divergence is forced by the package's module format, so the
next reader does not "restore consistency" and re-break the ledger.
Two remedies rejected: raising
TEST_DEBTto 356 (the ledger is a shrink-onlyratchet and its own failure text forbids it), and adding
"type": "module"topackages/objectql/package.json(a package-wide module-format change whose blastradius is far outside this card — filed as a finding instead). The guard itself is
untouched everywhere; weakening it would give back the defect this card closes.
The ledger measurement, before and after
The re-measure project rebuilt exactly as
remeasureProjectgenerates it for thispackage —
include: ["src/**/*"],excludewith the test globs dropped,reconstructed
typeRoots— with objectql's dependency closure built first(
pnpm --filter '@objectstack/objectql^...' build,CLOSURE_BUILD_EXIT=0):diffof the two runs, sorted — exactly one line left, and nothing else moved:(The surviving
TS1470in the AFTER run issrc/core-boundary.ratchet.test.ts:57,pre-existing and part of the frozen 355.)
The guard still fires, and still stays inert on import
Runtime, via the documented
pnpm tsxinvocation:Ablation, to show that symlink leg discriminates: with the pre-#10269 predicate
(
process.argv[1]matched against a filename regex) put back on disk — mutationconfirmed by anchored grep,
if (isProcessEntry())count 0 and the injected regexcount 1 — the renamed-symlink invocation returns exit=0 and 0 bytes of output,
the silent-inert defect, while the direct run still prints the report. Restored from
the commit and re-verified by grep before measuring anything else.
Gates re-run on the new head
Union re-derived with
node scripts/pm/dispatch-gates.mjs(no paths named) afterthe final commit, and run at
4d77499e10. Exit codes captured before any pipe;each line quotes the gate's own verdict:
packages/objectql/scripts/**stays worth a gate of its own — #10756 is still open,with a correction posted on it: that tree is invisible to the package's own
tsc -p, but it is not outside every tsc program, and this failure is whatmeasured the difference.
Generated by Claude Code
Generated by Claude Code