Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

invoked-as.mjs names scripts/check-entry-guard.mjs as its enforcement; that file does not exist, and 27 hand-typed entry guards still carry the spelling it documents as silently inert #6078

Closed

Description

@yinlianghui-tw

Filed unassigned by the dev seat implementing #5793 (branch claude/issue-5793-spec-range-floors, session session_019b5UBNMtTzKbVtZZGvFuxe). Out of that card's scope entirely — noticed while adopting isEntrypoint for a new gate. Recording it, not acting on it.

What was measured

scripts/invoked-as.mjs landed recently (#5984, ported from objectstack). Its header states, as the reason the module exists at all:

scripts/check-entry-guard.mjs enforces this: a process.argv[1] in an entry-guard position anywhere in scripts/** that is not this module is a failure. That gate is what stops a TWELFTH spelling, which is the whole reason this file exists rather than a one-time sweep.

Measured on a1c41c516:

  • scripts/check-entry-guard.mjs does not exist. The string check-entry-guard appears in exactly one place in the whole repository — that sentence in invoked-as.mjs itself. Nothing in package.json or .github/workflows/ runs such a gate.
  • 27 files under scripts/ still spell their entry guard by hand with process.argv[1] (every .mjs there except invoked-as.mjs that has one). Only two modules import isEntrypoint: scripts/pm/check-half-states.mjs, which arrived in the same PR, and the gate added by Packages whose published d.ts references spec-17.1.0-only symbols still declare "@objectstack/spec": "^17.0.0" — the range admits a spec that lacks the symbols #5793.
  • All 27 use the plain resolve(process.argv[1]) === fileURLToPath(import.meta.url) shape with no realpath leg — which is precisely the spelling invoked-as.mjs's own header documents as the failure that motivated it:

Node resolves symlinks for the module graph but leaves process.argv[1] as the caller typed it. Reach a script through a symlink and the two name different paths, the guard answers false, and the script does NOTHING — with exit 0 and no output.

Examples: check-published-dist-tooling.mjs:400, check-package-self-import.mjs:446, check-action-forward-parity.mjs:1061, check-doc-component-types.mjs:1006, check-doc-links.mjs:970, check-spec-symbol-derivation.mjs:1179, check-i18n-en-drift.mjs:553, check-changeset-fixed.mjs:232, sync-quick-reference-release.mjs:408, extract-mdx-demos.mjs:417.

Why it can bite

The silent direction is the one that matters, and invoked-as.mjs argues it better than this issue can: CI wrappers hold result.status only, so an inert child is a green gate. Several of the 27 are blocking checks — check-published-dist-tooling.mjs guards the publish path, check-doc-component-types.mjs and check-action-forward-parity.mjs gate pull requests.

No current invocation goes through a symlink, so nothing is failing today. What is broken now is the claim: a header states a gate enforces the single-spelling rule, that gate is absent, and the sweep it was supposed to protect never happened either. Both halves of the port landed as documentation. That is the same declared≠enforced shape check-spec-symbol-derivation.mjs's header calls "a planted premise for the next session" — an agent reading invoked-as.mjs reasonably concludes the tree is already converted and enforced, and neither is true.

The call to make

Probably two pieces, and they are separable:

  1. Port or write the missing gate, so a twenty-eighth hand-typed spelling cannot land. Without it, converting the 27 is a one-time sweep that starts rotting the day it merges — the explicit argument the header makes.
  2. Convert the 27 call sites to isEntrypoint(import.meta.url). Mechanical, but it is a lot of blocking gates to touch at once and wants its own review; it may be worth doing after (1), since the gate names its own worklist.

If the enforcement is genuinely not wanted here, the alternative is to correct invoked-as.mjs's header so it stops asserting a gate that does not exist — the claim is what makes this expensive, not the spellings.

Refs: #5984 (the port that introduced the module and its one adopter) · #5793 (the card whose seat found it).


Generated by Claude Code

Activity

  1. added
    domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo
    on Aug 24, 2026
  2. yinlianghui-tw commented on Aug 24, 2026

    @yinlianghui-tw
    ContributorAuthor
    {
      "issue": 6078,
      "status": "done",
      "branch": "claude/issue-6078-invoked-as-false-gate-claim",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6096",
      "premise_still_valid": true,
      "summary": "The card's premise holds: scripts/check-entry-guard.mjs does not exist here and the header asserted it as enforcement. Root cause measured beyond the card: #5984 ported invoked-as.mjs out of objectstack with its prose but without the gate or the sweep, so FOUR paths the header names resolve only in objectstack (check-entry-guard.mjs, pm/check-governed-merges.mjs, packages/cli/src/utils/invocation.ts, js-comment-mask.mjs) while all five exist there at 644ad5043. The correction therefore re-attributes four passages rather than deleting one sentence, and replaces the borrowed objectstack symlink example with the same measurement taken on a real objectui blocking gate. Two of the card's figures moved and are corrected: ONE module imports isEntrypoint (not two - the #5793 gate has not landed on main), and 29 hand-typed guards remain (not 27 - check-designer-field-key-parity.mjs landed since a1c41c516). One card claim was simply wrong: the guards are not all one shape, they are NINE textually distinct spellings, one of which (check-node-esm-load.mjs:847) is the percent-encoding failure the header itself warns about. Conversion and the gate port are out of scope per the PM ruling and are filed as #6092. One bounded in-place fix in the same file, named in the PR body: a self-test case reached for js-comment-mask.mjs (objectstack-only), so 'an unrelated existing file is not this module' had silently become a duplicate of the case below it; it now names a real neighbour with an existence assertion so it cannot degrade that way again.",
      "tests": "All runs on f726727cd (the final commit; clean tree, verified via git status --porcelain). Verdict lines quoted are the gates' own, not a bare $?.\n\nMEASUREMENTS (each with the command that produced it, on origin/main @ 7c96c9420):\n- Gate absent, with positive control: `ls scripts/check-entry-guard.mjs` -> 'No such file or directory'; `ls scripts/check-control-bytes.mjs` -> exists, 16244 bytes. `git ls-files | grep -c 'check-entry-guard'` -> 0; same for check-control-bytes -> 2. `git grep -n 'check-entry-guard'` -> exactly 1 hit, scripts/invoked-as.mjs:73 (the sentence itself); `git grep -l 'check-control-bytes' | wc -l` -> 16. The single hit is over ALL tracked files, so package.json and .github/workflows/ are covered - nothing runs such a gate.\n- Provenance: the same five paths checked in /home/user/objectstack @ 644ad5043 - all five EXIST there, four are MISSING in objectui. objectstack's check-entry-guard.mjs is 787 lines and wired as `check:entry-guard` in package.json.\n- Hand-typed guards: `git grep -l 'process\\.argv\\[1\\]' -- 'scripts/'` -> 30 files; minus invoked-as.mjs itself -> 29 guards (28 .mjs + scripts/shadcn-sync.js). Card said 27; reconciled by `comm -13` against the same grep at a1c41c516 -> exactly one file added (check-designer-field-key-parity.mjs), none removed.\n- No-realpath-leg shape: 28 of the 29. Only scripts/shadcn-sync.js:1048-1057 carries a realpath leg (`realpathSync(resolved) === __filename`).\n- Distinct spellings: 9 across the 28 .mjs, via `git grep -n ... | cut -d: -f3- | sort | uniq -c` (12x, 5x, 3x, 2x, 2x, 1x, 1x, 1x, 1x). Card implied one shape.\n- isEntrypoint importers: `git grep -l 'isEntrypoint' -- .` -> 2 files = invoked-as.mjs itself + ONE adopter, scripts/pm/check-half-states.mjs:777. Card said two adopters.\n\nSYMLINK INERTNESS - DEMONSTRATED, not inherited. One untracked guide file made deliberately wrong (removed by an EXIT/INT/TERM trap in the same command; `git status --porcelain` empty afterwards), then a real blocking gate run two ways against the identical defective tree:\n  node scripts/check-skills-paths.mjs          -> exit=1, 696 bytes naming the dead path\n  node <scratchpad>/link/check-skills-paths.mjs -> exit=0, 0 bytes\nSo a RED gate reports clean through a symlink. Separately measured, since the header asserts it: the ``import.meta.url === `file://${argv[1]}` `` spelling (check-node-esm-load.mjs:847) goes INERT in a directory named 'a#b c' with no symlink at all - file:///.../a%23b%20c/enc.mjs vs /.../a#b c/enc.mjs. Also checked before asserting and found NOT a bug: regenerate-known-schema-types.mjs:187 omits resolve() on argv[1], but node absolutizes argv[1] itself (measured by invoking a probe relatively).\n\nREVERSE VERIFICATION of the new existence assertion. Predicted direction: red. Mutation proven on disk BEFORE reading any result, by anchored greps in both directions: injected \"'js-comment-mask.mjs');\" count=1, removed \"'check-control-bytes.mjs');\" count=0, `git diff --stat` = 1 file changed, 1 insertion(+), 1 deletion(-). Run: `node scripts/invoked-as.mjs --self-test` -> exit=1, \"1 of 12 case(s) failed\", and the failing case is the new assertion only - the OLD case still passed with the dead path, which is exactly the vacuity being closed. Restored via `git checkout --` (the fix was committed first), restoration proven by the same greps inverted, re-run green 12/12, git status clean. No build is involved: this module is plain .mjs run directly, not resolved through any package dist, so there is no stale-artifact leg to rebuild.\n\nGATES (exit captured before any pipe; verdict lines are the gates' own):\n- node scripts/invoked-as.mjs --self-test -> exit 0, \"invoked-as self-test: 12 cases pass\" (was 11)\n- npx vitest run scripts/__tests__ --maxWorkers=2, from the repo ROOT (never package-scoped) -> exit 0, \"Test Files 65 passed (65)\", \"Tests 1789 passed (1789)\"\n- pnpm check:control-bytes -> exit 0, \"check-control-bytes: OK (scanned 5036 tracked text file(s); skipped 85 binary)\"\n- pnpm check:skills-paths -> exit 0, \"check-skills-paths: OK (93/94 stated path(s) resolve across 18 guide file(s); 1 baselined)\" - also confirms the temp probe file is gone\n- pnpm check:esm-specifiers -> exit 0, \"Specifier leg: no un-ledgered package emits an extensionless relative specifier.\"\n- pnpm check:node-esm-load -> exit 0\n- node scripts/check-changeset-presence.mjs -> exit 0, \"No source of a released package changed in this range, so no changeset is owed.\" Followed the gate's verdict: NO changeset added. objectui has no skip-changeset label mechanism and none was created or applied.\n- grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]' on the changed file -> clean\n\nTWO DECLARED NARROWINGS (both measured, not skipped):\n1. LINT. scripts/ is not a workspace package, so `pnpm lint` (turbo run lint, per-package fan-out) never reaches the changed file - measured, not assumed: pnpm-workspace.yaml globs packages/*, examples/*, apps/*, docs, and of the 47 workspace packages NONE has a directory containing scripts/. Linted directly instead: `npx eslint scripts/invoked-as.mjs --format json` -> exit 0, files linted = 1 (count read from the json), 0 errors, 0 warnings. Population read from eslint's own config: `--print-config` resolves a config for the file, so it is covered rather than merely unmatched. Invariance: eslint.config.js enables no type-aware linting (no projectService, no parserOptions.project, no recommendedTypeChecked), so a one-file diff with no config change cannot move any untouched file's verdict.\n2. pnpm check:published-dist NOT RUN LOCALLY. Its criterion is the file list of every published package's tarball after a real build; it exceeded this container's ~10 minute foreground cap (SIGTERM, exit 143). Sound to narrow for the same measured reason as lint: scripts/ is inside no workspace package, so this diff cannot appear in any package's dist/ or change any tarball's file list. CI runs it regardless.\n\nLIMIT OF THIS VERIFICATION, stated because it is the card's own point: this change is prose. No gate catches a false comment, and none will keep the corrected one true - the original header was false and every gate in this repository stayed green. The durable remedy is the gate tracked in #6092.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #6092: port objectstack's check-entry-guard.mjs (787 lines, two rules, wired as check:entry-guard there) and convert the 29 hand-typed entry guards to isEntrypoint - the conversion half the PM ruled out of scope for this PR, filed unassigned with all measured numbers, the nine-spelling breakdown, and the symlink and percent-encoding demonstrations",
        "recorded inside #6092 rather than filed separately, since they are the same worklist: (a) check-node-esm-load.mjs:847 still carries the exact percent-encoding spelling invoked-as.mjs's header warns about, measured inert in a directory named 'a#b c' with no symlink involved; (b) the worklist GROWS while the card sits open - one guard was added between a1c41c516 and 7c96c9420, which is the argument for porting the gate before doing the sweep; (c) scripts/shadcn-sync.js already hand-types the correct two-leg shape, so converting it is a simplification rather than a fix"
      ]
    }

    Generated by Claude Code

  3. yinlianghui-tw commented on Aug 24, 2026

    @yinlianghui-tw
    ContributorAuthor

    os-dev-report

    (Supersedes the previous comment on this issue: its leading HTML-comment marker was stripped by GitHub's body sanitizer after the comment was stored, so it is invisible to a marker scan. This one carries the marker as literal text. Content is identical apart from the marker and one path that the same sanitizer ate.)

    {
      "issue": 6078,
      "status": "done",
      "branch": "claude/issue-6078-invoked-as-false-gate-claim",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6096",
      "premise_still_valid": true,
      "summary": "The card's premise holds: scripts/check-entry-guard.mjs does not exist here and the header asserted it as enforcement. Root cause measured beyond the card: #5984 ported invoked-as.mjs out of objectstack with its prose but without the gate or the sweep, so FOUR paths the header names resolve only in objectstack (check-entry-guard.mjs, pm/check-governed-merges.mjs, packages/cli/src/utils/invocation.ts, js-comment-mask.mjs) while all five exist there at 644ad5043. The correction therefore re-attributes four passages rather than deleting one sentence, and replaces the borrowed objectstack symlink example with the same measurement taken on a real objectui blocking gate. Two of the card's figures moved and are corrected: ONE module imports isEntrypoint (not two - the #5793 gate has not landed on main), and 29 hand-typed guards remain (not 27 - check-designer-field-key-parity.mjs landed since a1c41c516). One card claim was simply wrong: the guards are not all one shape, they are NINE textually distinct spellings, one of which (check-node-esm-load.mjs:847) is the percent-encoding failure the header itself warns about. Conversion and the gate port are out of scope per the PM ruling and are filed as #6092. One bounded in-place fix in the same file, named in the PR body: a self-test case reached for js-comment-mask.mjs (objectstack-only), so 'an unrelated existing file is not this module' had silently become a duplicate of the case below it; it now names a real neighbour with an existence assertion so it cannot degrade that way again.",
      "tests": "All runs on f726727cd (the final commit; clean tree, verified via git status --porcelain). Verdict lines quoted are the gates' own, not a bare $?.\n\nMEASUREMENTS (each with the command that produced it, on origin/main @ 7c96c9420):\n- Gate absent, with positive control: `ls scripts/check-entry-guard.mjs` -> 'No such file or directory'; `ls scripts/check-control-bytes.mjs` -> exists, 16244 bytes. `git ls-files | grep -c 'check-entry-guard'` -> 0; same for check-control-bytes -> 2. `git grep -n 'check-entry-guard'` -> exactly 1 hit, scripts/invoked-as.mjs:73 (the sentence itself); `git grep -l 'check-control-bytes' | wc -l` -> 16. The single hit is over ALL tracked files, so package.json and .github/workflows/ are covered - nothing runs such a gate.\n- Provenance: the same five paths checked in /home/user/objectstack @ 644ad5043 - all five EXIST there, four are MISSING in objectui. objectstack's check-entry-guard.mjs is 787 lines and wired as `check:entry-guard` in package.json.\n- Hand-typed guards: `git grep -l 'process\\.argv\\[1\\]' -- 'scripts/'` -> 30 files; minus invoked-as.mjs itself -> 29 guards (28 .mjs + scripts/shadcn-sync.js). Card said 27; reconciled by `comm -13` against the same grep at a1c41c516 -> exactly one file added (check-designer-field-key-parity.mjs), none removed.\n- No-realpath-leg shape: 28 of the 29. Only scripts/shadcn-sync.js:1048-1057 carries a realpath leg (`realpathSync(resolved) === __filename`).\n- Distinct spellings: 9 across the 28 .mjs, via `git grep -n ... | cut -d: -f3- | sort | uniq -c` (12x, 5x, 3x, 2x, 2x, 1x, 1x, 1x, 1x). Card implied one shape.\n- isEntrypoint importers: `git grep -l 'isEntrypoint' -- .` -> 2 files = invoked-as.mjs itself + ONE adopter, scripts/pm/check-half-states.mjs:777. Card said two adopters.\n\nSYMLINK INERTNESS - DEMONSTRATED, not inherited. One untracked guide file made deliberately wrong (removed by an EXIT/INT/TERM trap in the same command; `git status --porcelain` empty afterwards), then a real blocking gate run two ways against the identical defective tree:\n  node scripts/check-skills-paths.mjs                  -> exit=1, 696 bytes naming the dead path\n  node SCRATCHPAD/link/check-skills-paths.mjs (symlink) -> exit=0, 0 bytes\nSo a RED gate reports clean through a symlink. Separately measured, since the header asserts it: the ``import.meta.url === `file://${argv[1]}` `` spelling (check-node-esm-load.mjs:847) goes INERT in a directory named 'a#b c' with no symlink at all - file:///.../a%23b%20c/enc.mjs vs /.../a#b c/enc.mjs. Also checked before asserting and found NOT a bug: regenerate-known-schema-types.mjs:187 omits resolve() on argv[1], but node absolutizes argv[1] itself (measured by invoking a probe relatively).\n\nREVERSE VERIFICATION of the new existence assertion. Predicted direction: red. Mutation proven on disk BEFORE reading any result, by anchored greps in both directions: injected \"'js-comment-mask.mjs');\" count=1, removed \"'check-control-bytes.mjs');\" count=0, `git diff --stat` = 1 file changed, 1 insertion(+), 1 deletion(-). Run: `node scripts/invoked-as.mjs --self-test` -> exit=1, \"1 of 12 case(s) failed\", and the failing case is the new assertion only - the OLD case still passed with the dead path, which is exactly the vacuity being closed. Restored via `git checkout --` (the fix was committed first), restoration proven by the same greps inverted, re-run green 12/12, git status clean. No build is involved: this module is plain .mjs run directly, not resolved through any package dist, so there is no stale-artifact leg to rebuild.\n\nGATES (exit captured before any pipe; verdict lines are the gates' own):\n- node scripts/invoked-as.mjs --self-test -> exit 0, \"invoked-as self-test: 12 cases pass\" (was 11)\n- npx vitest run scripts/__tests__ --maxWorkers=2, from the repo ROOT (never package-scoped) -> exit 0, \"Test Files 65 passed (65)\", \"Tests 1789 passed (1789)\"\n- pnpm check:control-bytes -> exit 0, \"check-control-bytes: OK (scanned 5036 tracked text file(s); skipped 85 binary)\"\n- pnpm check:skills-paths -> exit 0, \"check-skills-paths: OK (93/94 stated path(s) resolve across 18 guide file(s); 1 baselined)\" - also confirms the temp probe file is gone\n- pnpm check:esm-specifiers -> exit 0, \"Specifier leg: no un-ledgered package emits an extensionless relative specifier.\"\n- pnpm check:node-esm-load -> exit 0\n- node scripts/check-changeset-presence.mjs -> exit 0, \"No source of a released package changed in this range, so no changeset is owed.\" Followed the gate's verdict: NO changeset added. objectui has no skip-changeset label mechanism and none was created or applied.\n- grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]' on the changed file -> clean\n\nTWO DECLARED NARROWINGS (both measured, not skipped):\n1. LINT. scripts/ is not a workspace package, so `pnpm lint` (turbo run lint, per-package fan-out) never reaches the changed file - measured, not assumed: pnpm-workspace.yaml globs packages/*, examples/*, apps/*, docs, and of the 47 workspace packages NONE has a directory containing scripts/. Linted directly instead: `npx eslint scripts/invoked-as.mjs --format json` -> exit 0, files linted = 1 (count read from the json), 0 errors, 0 warnings. Population read from eslint's own config: `--print-config` resolves a config for the file, so it is covered rather than merely unmatched. Invariance: eslint.config.js enables no type-aware linting (no projectService, no parserOptions.project, no recommendedTypeChecked), so a one-file diff with no config change cannot move any untouched file's verdict.\n2. pnpm check:published-dist NOT RUN LOCALLY. Its criterion is the file list of every published package's tarball after a real build; it exceeded this container's ~10 minute foreground cap (SIGTERM, exit 143). Sound to narrow for the same measured reason as lint: scripts/ is inside no workspace package, so this diff cannot appear in any package's dist/ or change any tarball's file list. CI runs it regardless.\n\nLIMIT OF THIS VERIFICATION, stated because it is the card's own point: this change is prose. No gate catches a false comment, and none will keep the corrected one true - the original header was false and every gate in this repository stayed green. The durable remedy is the gate tracked in #6092.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #6092: port objectstack's check-entry-guard.mjs (787 lines, two rules, wired as check:entry-guard there) and convert the 29 hand-typed entry guards to isEntrypoint - the conversion half the PM ruled out of scope for this PR, filed unassigned with all measured numbers, the nine-spelling breakdown, and the symlink and percent-encoding demonstrations",
        "recorded inside #6092 rather than filed separately, since they are the same worklist: (a) check-node-esm-load.mjs:847 still carries the exact percent-encoding spelling invoked-as.mjs's header warns about, measured inert in a directory named 'a#b c' with no symlink involved; (b) the worklist GROWS while the card sits open - one guard was added between a1c41c516 and 7c96c9420, which is the argument for porting the gate before doing the sweep; (c) scripts/shadcn-sync.js already hand-types the correct two-leg shape, so converting it is a simplification rather than a fix"
      ]
    }

    Generated by Claude Code

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

Metadata

Metadata

Labels

domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repopm:dispatched

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions