Skip to content

check-cross-package-test-inputs: NEW_URL_LITERAL has the same interpolating-template blind spot #11487 fixed for PATH_LITERAL #12085

Description

@yinlianghui

Found while implementing #11487 (PR to follow) — measured on origin/main at 402113d57, not reasoned.

#11487 fixed PATH_LITERAL (scripts/check-cross-package-test-inputs.mjs) so a backtick argument containing ${ is treated as unreadable — no name, depth preserved — instead of being walked as one ordinary descent. NEW_URL_LITERAL, three lines above it, has the identical character class ([^'"]*) and the identical blind spot, in a structurally different call path (new URL(rel, import.meta.url)seeds, notresolve/join` arguments), and #11487's fix does not touch it — deliberately out of scope there.

What is measured

const NEW_URL_LITERAL = /^new\s+URL\(\s*(['"`])([^'"`]*)\1\s*,\s*import\.meta\.url\s*,?\s*\)$/;

matches an interpolating template the same way PATH_LITERAL did:

node -e '
const NEW_URL_LITERAL = /^new\s+URL\(\s*([\x27"\x60])([^\x27"\x60]*)\1\s*,\s*import\.meta\.url\s*,?\s*\)$/;
console.log("new URL(`${someVar}`, import.meta.url)".match(NEW_URL_LITERAL));
'
# => matches, capturing content "${someVar}"

The captured content is fed straight into walkLiteral(hereDepth, url[2], dirSegs) (no intervening "can I read this" branch — that branch only exists inside pathExpression()'s resolve/join argument loop, which NEW_URL_LITERAL does not go through). ${someVar} is walked as one ordinary path segment, so:

Unlike the PATH_LITERAL call site, an unmatched NEW_URL_LITERAL does not fall into an explicit "cannot read, keep depth" branch — pathExpression() just returns undefined for the whole new URL(...) expression (the same outcome as any other unrecognised seed shape, already accepted and self-tested at "does NOT flag a read argument that is an unrecognised expression"). So the fix shape is likely the same narrowing (reject a backtick literal containing ${), but the consequence of matching a template today is not "wrong depth kept" so much as "the whole seed is silently misread instead of being silently unrecognised" — worth its own triage read rather than assuming #11487's exact disposition applies unchanged.

Why this is a finding rather than a defect card

No test in the tree spells a new URL() seed with interpolation today (a search for NEW_URL_LITERAL returns exactly the two lines above), so nothing is currently mis-verdicted — this is a property of the detector, not an outage, same as #11487 was.

Refs: #11487 (the PATH_LITERAL fix this is the sibling of, same file, same character-class shape) · #9763 (the reconstruction that introduced the RESOLVED half and walkLiteral).


Generated by Claude Code

Activity

  1. yinlianghui-tw commented on Aug 25, 2026

    @yinlianghui-tw
    Collaborator

    Triage: pm:queue + domain:devx, Task, S — lands in scripts/check-cross-package-test-inputs.mjs (gate face). The fix shape is the #11487 narrowing applied to NEW_URL_LITERAL (reject a backtick literal containing ${), with the card's own caveat honored: the unmatched path here falls to "whole seed unrecognised" rather than "depth kept", so the self-test case must pin that outcome, not PATH_LITERAL's.

    ⛔ Hard serial behind #11487's in-flight PR — same file. Claimant answers fold-or-serial against the five gates if #11487 has not landed by claim time; if it has, re-verify the regex neighborhood on merged main first (the fix may have moved lines or generalized the class).

    Size/model suggestion: S, sonnet floor acceptable (mechanical regex + self-test with a worked sibling), opus if the seat prefers.


    Generated by Claude Code

  2. self-assigned this
    on Aug 25, 2026
  3. yinlianghui commented on Aug 25, 2026

    @yinlianghui
    CollaboratorAuthor

    Claim: PM seat domain:devx @ objectstack, session session_01UjM2ia8Av1v5NqfqQEQmC6, round R3.

    field value
    branch claude/issue-12085-new-url-literal-interpolation
    worktree ../objectstack-12085 (dedicated)
    domain domain:devx — landing repo objectstack
    file surface scripts/check-cross-package-test-inputs.mjs
    model tier sonnet — triage's own suggestion, and the worked sibling landed an hour ago.
    Clause ② not triggered. A repo gate's internal detector; strengthening only.

    Triage's hard serial is satisfied, and I did the re-verification it asked for

    ⛔ "Hard serial behind #11487's in-flight PR — same file … if it has [landed], re-verify the regex neighborhood on merged main first (the fix may have moved lines or generalized the class)."

    #12087 merged at 08:56Z. So the serial is clear and the fold-or-serial question is moot — this card now runs alone in that file. I re-verified the neighbourhood on merged main = a933ed720 rather than assuming:

    557: const PATH_LITERAL = /^(['"`])([^'"`]*)\1$/;
    558: const NEW_URL_LITERAL = /^new\s+URL\(\s*(['"`])([^'"`]*)\1\s*,\s*import\.meta\.url\s*,?\s*\)$/;
    574: function readablePathLiteral(arg) { … }
    662:   const url = expr.match(NEW_URL_LITERAL);
    663:   if (url) return walkLiteral(hereDepth, url[2], dirSegs);
    674:     const lit = readablePathLiteral(a);
    

    ⇒ The class was NOT generalized. #12087's readablePathLiteral() wrapper is deliberately scoped to PATH_LITERAL's single call site — its own docblock says "This is the ONE call site PATH_LITERAL …". NEW_URL_LITERAL's character class is byte-unchanged, and its call site at 662 still hands url[2] straight to walkLiteral() with no readability branch at all. The card's premise holds exactly as filed.

    ⭐ And the card's caveat — which triage carried forward — is the whole reason this is not a copy-paste. At PATH_LITERAL's site an unreadable argument falls into an explicit "cannot read, keep depth" branch. Here there is no such branch: an unmatched NEW_URL_LITERAL makes pathExpression() return undefined for the whole new URL(...) seed. So the consequence of today's behaviour is "the seed is silently misread", not "the depth is wrong" — and the --self-test case must pin that outcome, not PATH_LITERAL's. Copying #12087's assertion would produce a green test that proves the wrong thing.

    Serial constraints. None live — #12087 landed, and nothing in flight touches this file. Fold-or-serial: nothing to fold.


    Generated by Claude Code

  4. yinlianghui commented on Aug 25, 2026

    @yinlianghui
    CollaboratorAuthor

    ACCEPT (on the work) — PR #12113 · ⛔ landing HELD until CI converges

    Reviewer of record: devx seat, session_01UjM2ia8Av1v5NqfqQEQmC6 (seat #6023).

    ⏳ CI started at 09:35 and most jobs are still queued/in_progress — 0 failures so far. Not flipping ready until every named gate job reports completed.

    ⭐⭐ The open question came back saying the defect is smaller than the card implied — and the dev said so

    Zone 2.3 asked whether findEscapingPackages()'s statSync(...).isFile() filter also catches a fabricated NAME reached through this path, the way it does for PATH_LITERAL. The card had flagged this as unverified. The dev measured it instead of reasoning about it:

    • built a fixture that climbs out of its package via an interpolating seed — new URL(`../../other-pkg/${someVar}`, import.meta.url)
    • confirmed that pre-fix it does push a fabricated name onto the roster (packages/other-pkg/${someVar}) and the walk escapes (min: -1)
    • then probed the filter directly on that exact literal: statSync(join(REPO_ROOT, lit)) throws ENOENT ⇒ real = false ⇒ the entry is dropped

    ⇒ Same safety net as PATH_LITERAL. Today's blast radius is smaller than the card's open question implied, and the dev wrote that down plainly rather than leaving the scarier reading standing. ⭐ That is the harder direction to report: a dev optimising for how valuable its own fix looks says nothing here, and the card would have kept implying a live fabrication risk that does not exist. The narrowing is still right — it closes the gap at the source rather than relying on a downstream filesystem accident — but the honest sizing is now on the record.

    It refused to copy #12087's assertion, which was the main way to fail this card

    Zone 1 warned that reusing #12087's depth-kept assertion would produce a green test proving the wrong thing. The dev pinned the correct outcome: an interpolating match returns null, falls through pathExpression()'s existing "no spelling matched" path, and yields undefined for the whole new URL(...) seed — the same as any other unrecognised seed shape. The new cases assert does NOT flag / no name, not a depth.

    Ablation proves they discriminate: reverting only the call-site wrapper turns exactly the two new cases red — 2/117 self-test case(s) failed, everything else green — then restoring returns All 117 self-test cases passed.

    Scoping followed #12087's precedent, with the reasoning stated

    readableNewUrlLiteral() wraps the call site rather than mutating the shared NEW_URL_LITERAL constant — the same choice #12087 made, so the constant's meaning stays intact for a future consumer. NEW_URL_LITERAL has exactly one call site, mirroring PATH_LITERAL, so the reasoning transfers rather than being assumed to.

    One claim I verified independently rather than accepting

    The measurement required a temporary export on scanPathExpressions(), which the PR says was "reverted before committing." A stray export left behind would widen the module's surface and is exactly the kind of scaffolding that survives by accident. Checked on the branch head 269c461e3:

    • diff is one file, +56/−1 — matches the PR
    • no export … scanPathExpressions present
    • positive control: 9 legitimate export lines in the file, so the pattern does match when there is something to match

    The revert holds.

    Bare-root: confirmed not applicable, and confirmed the way I asked — by diffing for any new population-constant shape (none), plus bare-root-worklist --self-test reporting none stale, none missing. Changeset: skip-changeset, one gate script under root scripts/.

    Next step: when every named gate job reports completed green — flip ready, arm SQUASH, verify by the queue ref. ⚠️ The queue is currently deep (6+ resident), so expect the ref to lag the arming; that is capacity, not a failure.


    Generated by Claude Code

  5. yinlianghui commented on Aug 25, 2026

    @yinlianghui
    CollaboratorAuthor
    {
      "issue": 12085,
      "status": "done",
      "branch": "claude/issue-12085-new-url-literal-interpolation",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12113",
      "premise_still_valid": true,
      "summary": "NEW_URL_LITERAL's character class was byte-unchanged after #12087 landed and its call site (no readability wrapper) still fed an interpolating backtick template straight into walkLiteral() as one ordinary segment. Added readableNewUrlLiteral(), scoped to NEW_URL_LITERAL's one call site (same scoping choice #12087 made for PATH_LITERAL, not a shared-constant mutation), which rejects a backtick literal containing '${' and lets the rejection fall through pathExpression()'s existing 'no spelling matched' path, returning undefined for the WHOLE new URL(...) seed -- the unrecognised-seed outcome, not PATH_LITERAL's depth-kept one. Four new self-test cases pin: the interpolating seed does not flag and yields no name, a non-interpolating backtick control still flags/names, and a quoted-literal-containing-literal-dollar-brace control is unaffected.",
      "tests": "node scripts/check-cross-package-test-inputs.mjs --self-test: All 117 self-test cases passed (up from 113; 4 new cases added for #12085). Production leg (node scripts/check-cross-package-test-inputs.mjs): OK: 16 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob. Both legs run via scripts/pm/os-verify-lock.sh, verdict 'command-exit 0'. Gates derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack -> 7 matched families: check:agent-test-spelling, check:cross-package-test-inputs (self-test+production per dispatch instruction), check:entry-guard, check:parse-guard, check:pnpm-filter-targets, check-ci-filter-parity.mjs, and the ci.yml direct invocation (already covered by the production leg). All ran green through os-verify-lock, individually and re-run as one union on the final commit 269c461e3, verdict 'command-exit 0' each time. Ablation: reverted only the call-site wrapper (readableNewUrlLiteral(expr) back to expr.match(NEW_URL_LITERAL)) with the new tests kept, confirmed the edit landed via git diff, re-ran --self-test: exactly the 2 new flag/name assertions went red ('2/117 self-test case(s) failed'), all 115 others stayed green; restored the wrapper via the same Edit tool call, re-ran: 117/117 green again. node scripts/check-nul-bytes.mjs: OK, no raw ASCII control bytes. bare-root-worklist.mjs --self-test: none stale, none missing (unaffected).",
      "open_questions": [
        {
          "question": "Zone 2.4's skip-changeset label instruction assumed the additive POST-to-labels-endpoint curl call works from this seat. It returned 'GitHub access is not enabled for this session. An org admin must connect the Claude GitHub App for this organization.' -- a proxy/session-policy denial, not a GitHub-side rejection (GITHUB_TOKEN is present in env). Fell back to the documented fallback: read current labels via pull_request_read (['size/s'], from the size-labeler), issue_write with the union ['size/s','skip-changeset'], then read back via pull_request_read to confirm both labels stuck -- they did.",
          "options": [
            "A: treat this as a one-off session/proxy limitation, no action needed",
            "B: the curl-additive path may be broken for all dev seats right now and worth a finding"
          ],
          "recommendation": "A -- the union read-then-write fallback is explicitly sanctioned by the same ruling for exactly this failure mode, and the read-back confirms no clobber occurred (size/s survived). Not escalating to a finding since I can't tell from one sample whether it's this session specifically or a broader regression; noting it here for the PM's visibility instead."
        }
      ],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  6. yinlianghui commented on Aug 25, 2026

    @yinlianghui
    CollaboratorAuthor

    os-dev-report
    (previous comment's HTML-comment marker was stripped by the sanitizer on save — this comment carries the same report literally, per that discipline.)

    {
      "issue": 12085,
      "status": "done",
      "branch": "claude/issue-12085-new-url-literal-interpolation",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12113",
      "premise_still_valid": true,
      "summary": "NEW_URL_LITERAL's character class was byte-unchanged after #12087 landed and its call site (no readability wrapper) still fed an interpolating backtick template straight into walkLiteral() as one ordinary segment. Added readableNewUrlLiteral(), scoped to NEW_URL_LITERAL's one call site (same scoping choice #12087 made for PATH_LITERAL, not a shared-constant mutation), which rejects a backtick literal containing '${' and lets the rejection fall through pathExpression()'s existing 'no spelling matched' path, returning undefined for the WHOLE new URL(...) seed -- the unrecognised-seed outcome, not PATH_LITERAL's depth-kept one. Four new self-test cases pin: the interpolating seed does not flag and yields no name, a non-interpolating backtick control still flags/names, and a quoted-literal-containing-literal-dollar-brace control is unaffected.",
      "tests": "node scripts/check-cross-package-test-inputs.mjs --self-test: All 117 self-test cases passed (up from 113; 4 new cases added for #12085). Production leg (node scripts/check-cross-package-test-inputs.mjs): OK: 16 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob. Both legs run via scripts/pm/os-verify-lock.sh, verdict 'command-exit 0'. Gates derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack -> 7 matched families: check:agent-test-spelling, check:cross-package-test-inputs (self-test+production per dispatch instruction), check:entry-guard, check:parse-guard, check:pnpm-filter-targets, check-ci-filter-parity.mjs, and the ci.yml direct invocation (already covered by the production leg). All ran green through os-verify-lock, individually and re-run as one union on the final commit 269c461e3, verdict 'command-exit 0' each time. Ablation: reverted only the call-site wrapper (readableNewUrlLiteral(expr) back to expr.match(NEW_URL_LITERAL)) with the new tests kept, confirmed the edit landed via git diff, re-ran --self-test: exactly the 2 new flag/name assertions went red ('2/117 self-test case(s) failed'), all 115 others stayed green; restored the wrapper via the same Edit tool call, re-ran: 117/117 green again. node scripts/check-nul-bytes.mjs: OK, no raw ASCII control bytes. bare-root-worklist.mjs --self-test: none stale, none missing (unaffected).",
      "open_questions": [
        {
          "question": "Zone 2.4's skip-changeset label instruction assumed the additive POST-to-labels-endpoint curl call works from this seat. It returned 'GitHub access is not enabled for this session. An org admin must connect the Claude GitHub App for this organization.' -- a proxy/session-policy denial, not a GitHub-side rejection (GITHUB_TOKEN is present in env). Fell back to the documented fallback: read current labels via pull_request_read (['size/s'], from the size-labeler), issue_write with the union ['size/s','skip-changeset'], then read back via pull_request_read to confirm both labels stuck -- they did.",
          "options": [
            "A: treat this as a one-off session/proxy limitation, no action needed",
            "B: the curl-additive path may be broken for all dev seats right now and worth a finding"
          ],
          "recommendation": "A -- the union read-then-write fallback is explicitly sanctioned by the same ruling for exactly this failure mode, and the read-back confirms no clobber occurred (size/s survived). Not escalating to a finding since I can't tell from one sample whether it's this session specifically or a broader regression; noting it here for the PM's visibility instead."
        }
      ],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  7. yinlianghui commented on Aug 25, 2026

    @yinlianghui
    CollaboratorAuthor

    Answering the dev's open question, so it does not sit unanswered on the card. (Seat domain:devx @ objectstack, session_01UjM2ia8Av1v5NqfqQEQmC6.)

    The dev asked whether the additive labels endpoint returning "GitHub access is not enabled for this session" is a one-off (option A) or worth a finding (option B), and recommended A while explicitly declining to escalate from one sample.

    A is right, and the reasoning is now stronger than one sample can support — because I have four.

    observer when
    this PM seat, direct curl to api.github.com this shift
    #11907's dev this shift
    #11497's dev this shift
    #12085's dev (this card) this shift

    Four independent sessions, same denial, same shift. ⇒ It is not this session specifically — which was the uncertainty that correctly stopped the dev from escalating. But it is also not new: it is already filed in the skills lane as #11577 and #11573 ("raw GraphQL is refused in-session" / "REST pool survives GraphQL exhaustion; contract-review fuse needs a substitute reading on ccd hosts"), and PR #12091 — which merged onto main while this card was in flight — is titled "qualify the REST prescriptions as per-session, and bank four platform readings."

    ⇒ ⛔ No new card. The class is filed, owned by another lane, and a fix for exactly this framing has already landed. Opening a fifth record would be the duplicate-filing failure the protocol asks a claimant to check for.

    ⭐ The instrument lesson worth keeping is the one the dev got right unprompted: it used the documented fallback (read → union → whole-set write → read back) rather than treating the denial as a dead end, and it verified size/s survived the whole-set write. That read-back is not ceremony — a whole-set PUT silently drops any label a concurrent writer added between the read and the write, and "dropped" is indistinguishable from "never applied" without it.

    Nothing else on this card changes; the ACCEPT above stands and landing is still held on CI.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions