Skip to content

assertLazyLinterStaysLazy ignores resolveSpecDistInjection's output — the counter-probe fails every console build made with OBJECTSTACK_SPEC_DIST set #5388

Description

@os-project-manager

Companion to objectstack-ai/objectstack#10136, which measured this from the consumer side. It blocks objectstack#10129 (bumping the objectui pin) and will turn objectstack's Console Pin Gate red on any pin at or after 6d5368ac8 (this repo's #5323).

What breaks

scripts/build-console.sh in objectstack sets OBJECTSTACK_SPEC_DIST so the console bundles that tree's spec instead of the last published one. apps/console/vite.config.ts honours it, and the console build then dies in generateBundle:

[plugin assert-lazy-linter-stays-lazy]
RolldownError: counter-probe failed: no eagerly loaded chunk contains an `@objectstack/spec` module.
(entry chunks: assets/index-mpVXni3j.js; eager chunks: 59/508)

The counter-probe is not wrong about what it saw — it is reading a module-id shape the injection deliberately abolishes, and it correctly refuses to return a verdict from a walk that sees nothing. The bug is upstream of its judgement.

The seam, verified on origin/main at apps/console/vite.config.ts

The file already computes an injection-aware spec test, and one consumer reads it while the other does not:

line what it does
321–322 resolveSpecDistInjection(process.env.OBJECTSTACK_SPEC_DIST, { vendorChunkTest: VENDOR_OBJECTSTACK_TEST })
345–347 const vendorObjectstackTest = specDistInjection ? specDistInjection.vendorChunkTest : VENDOR_OBJECTSTACK_TEST
442 chunk grouping uses vendorObjectstackTest ✅ reads the injection-aware test
76–78 assertLazyLinterStaysLazy() declares its own private const LINT = /@objectstack[\\/+]lint/ and const SPEC = /@objectstack[\\/+]spec/
366 assertLazyLinterStaysLazy() is registered with no argument — it cannot see vendorObjectstackTest ❌

Under the injection, resolveSpecDistInjection rewrites all 18 @objectstack/spec* specifiers to absolute paths in the framework tree (/…/objectstack/packages/spec/dist/<sub>/index.mjs). Those ids contain no @objectstack segment at all, so the private SPEC regex matches zero modules and the counter-probe fires.

This is not path-shape-specific to one machine: on GitHub's runner the framework path is /home/runner/work/objectstack/objectstack/packages/spec, which also has no @objectstack segment.

Fix — contract-first

Make assertLazyLinterStaysLazy() take the spec test as a parameter and pass the injection-aware value at line 366, the same way line 442 already does. resolveSpecDistInjection already publishes the correct answer (its widened vendorChunkTest); the defect is a consumer that does not read its producer's output. Widening the private regex by hand, or having objectstack drop the injection, would both paper over that.

Review the LINT half in the same pass. @objectstack/lint is not injected today, so its regex still matches and the probe's primary assertion is unaffected — but objectstack#9659 proposes injecting four more @objectstack/* packages the same way, and lint is a plausible member of that set. Decide deliberately whether LINT should become injection-aware now or is genuinely out of scope; say which in the PR either way.

Reproduction — the injection is the only variable

Same tree, same commit, same command:

OBJECTSTACK_SPEC_DIST result
set Build failed in 11.73s, counter-probe error above
unset EXIT=0, dist written

Acceptance

  • The console build succeeds with OBJECTSTACK_SPEC_DIST pointing at a framework spec dist, and the lazy-linter assertion still does its real job in that mode — i.e. the probe finds the injected spec in the eager closure rather than being skipped or defanged. A probe that passes because it stopped looking is a regression, not a fix.
  • The build still succeeds without the injection, with the assertion unchanged.
  • Reverse-verify: with the fix in place, deliberately make @objectstack/lint eager and confirm the linter assertion still goes red. A guard that can no longer fail is not a guard (perf(console): keep the lazily-imported @objectstack/lint out of the eager chunk #5323's own reasoning).
  • A changeset. This is build config for a private app, so state plainly whether anything publishes; empty frontmatter if not.

Non-goals

Activity

  1. os-project-manager commented on Aug 20, 2026

    @os-project-manager
    CollaboratorAuthor
    {
      "issue": 5388,
      "status": "done",
      "branch": "claude/issue-5388-lazy-linter-injection",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5390",
      "premise_still_valid": true,
      "summary": "Every line number in the issue was re-verified on origin/main (0fd11444a) before editing and all were accurate. resolveSpecDistInjection now publishes specModuleTest beside vendorChunkTest, widened by one shared widen() rule; assertLazyLinterStaysLazy takes the spec test as a parameter and the config hands it the injection-aware value, exactly as the vendor-objectstack group does at line 442. specModuleTest is deliberately NOT vendorChunkTest, because the latter is the whole vendor scope minus lint and an eager @objectstack/client would satisfy it without proving the walk can see the spec chunk. LINT decision: NOT made injection-aware (no producer publishes a lint location today, so it could only be speculative and untestable), but it gains its own counter-probe, because a blind LINT fails SILENT where a blind SPEC fails LOUD. One measurement in the issue and the dispatch is wrong and is corrected in the PR body: @object-ui/console is NOT a private app. It has no private:true, carries publishConfig.access public, is one of the 40 packages in the .changeset fixed group and ships its built dist/. Nothing publishes here for a narrower reason: vite.config.ts and repo tooling are not in its files list and the emitted bundle is unchanged. Empty-frontmatter changeset added; this repo has no skip-changeset label and none was minted.",
      "tests": "All at HEAD b7b2b3b8c (= the final commit; the gate union below was run on that tree). BASELINE FIRST, on unfixed origin/main: OBJECTSTACK_SPEC_DIST set => EXIT=1, 'counter-probe failed: no eagerly loaded chunk contains an @objectstack/spec module ... (entry chunks: assets/index-BInyohVQ.js; eager chunks: 58/507)'; unset => EXIT=0. WITH FIX: set => EXIT=0, 8592 modules transformed; unset => EXIT=0, 8592 modules transformed. The probe FOUND the injected spec rather than skipping: the injected build's own dist/stats.html carries 13 distinct module ids under the override directory and 0 ids from the installed @objectstack+spec@17.0.0. REVERSE VERIFICATION, direction predicted before running (plain RED, and on the LINT message not the counter-probe): negative lookaheads removed from VENDOR_OBJECTSTACK_TEST so the vendor group claims the linter => EXIT=1 both with and without the override, 'RolldownError: [assert-lazy-linter-stays-lazy] `@objectstack/lint` is in the EAGER closure (assets/vendor-objectstack-BXk7O0mY.js), so every console page load pays for it - ~89 KiB gzipped'. Ablation applied on top of the committed fix and reverted with git checkout HEAD --; tree verified clean afterwards. UNIT: vitest run --project unit over vite-objectstack-spec-dist + vitest-config-alias-targets-3944 + side-effects-declaration-consistency + check-type-check-coverage => 'Test Files 4 passed (4) / Tests 381 passed (381)'. GATES at b7b2b3b8c: type-check:scripts [0]; tsc -b apps/console/tsconfig.node.json --force [0] (the project that actually contains vite.config.ts and scripts/vite-*.ts); eslint on all 3 changed files [0] (5 pre-existing-style no-explicit-any warnings, 0 errors); check-changeset-presence [0]; check-changeset-no-major [0]; check-control-bytes [0]. NOT RUN and why: apps/console's full 'tsc --noEmit' half needs a built workspace dep closure that does not exist in a fresh worktree (no packages/*/dist anywhere) - it fails identically on unmodified origin/main with TS2307/TS2882 on @object-ui/* and covers src/, which this diff does not touch. The full gate farm runs once in CI.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #5391 (label: finding, unassigned): resolveSpecDistInjection validates the override's exports targets but not that the overriding package's own dependencies resolve from its location - a dep-less spec dist produces no error from the helper and fails ~80s later as a rolldown 'failed to resolve zod', naming zod rather than OBJECTSTACK_SPEC_DIST. Noted alongside it: while 5388 was unfixed that resolve error was completely invisible, because the counter-probe threw in generateBundle before rolldown escalated the accumulated resolve errors."
      ]
    }

    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions