Skip to content

fix(genui): fix flaky api-extractor dist-rewrite race (TS2307/TS7016)#2780

Merged
upupming merged 13 commits into
mainfrom
fix/genui-api-extractor-build-order
Jun 4, 2026
Merged

fix(genui): fix flaky api-extractor dist-rewrite race (TS2307/TS7016)#2780
upupming merged 13 commits into
mainfrom
fix/genui-api-extractor-build-order

Conversation

@upupming
Copy link
Copy Markdown
Collaborator

@upupming upupming commented Jun 3, 2026

Summary

Fixes the flaky test-api / code-style-check failure where @lynx-js/genui-cli#build (and genui#api-extractor) fail with:

error TS2307 / TS7016: Cannot find module '@lynx-js/genui-a2ui-prompt'
  ('.../a2ui-prompt/dist/index.js' implicitly has an 'any' type)

Root cause

packages/genui/scripts/run-api-extractor.mjs ran pnpm run build inside the api-extractor task. For a2ui-prompt that is rslib build, which cleans and rewrites dist/ (emitting index.js before index.d.ts).

genui-cli#build is pulled into the api-extractor graph (via a2ui#api-extractor, whose build:catalog needs the genui CLI) and reads a2ui-prompt/dist. Turbo places no edge between that in-script rebuild and genui-cli#build, so the consumer's tsc can observe dist/ mid-rewrite — index.js present, index.d.ts momentarily gone → TS2307/TS7016. (Only the rslib-built subpackage manifests it; tsc-based subpackages don't clean dist.)

The earlier theory — a TOCTOU in the file lock — was a real but secondary bug; it was not what broke CI.

Fix

  • Stop building inside the api-extractor script. Turbo's graph already builds each package; the in-script pnpm run build was a second, racing writer of the same dist/. Removed it (kept ensureMainEntryPoint as a last-resort single-package build for a genuinely missing entry).
  • Order api-extractor after the package's own build, scoped to the Rust-free genui packages (a2ui-prompt, a2ui-catalog-extractor) via an explicit api-extractor → build edge. Not applied to a2ui / openui / ui-judge: their ^build transitively pulls @lynx-js/reactreact-transform#build:wasm (cargo), which must not enter the api-extractor graph that code-style-check runs without a Rust toolchain.
  • Removed the file lock entirely. It only existed to serialize the in-script rslib builds (rspack cache collisions, fix(genui-a2ui-prompt): serialize build:api after build to avoid rspack cache race #2794). With no in-script build, concurrent api-extractor runs touch only their own per-package outputs and need no mutual exclusion — so the link/reap lock (and its stale-reaping TOCTOU) is gone.

Net effect: a2ui-prompt/dist now has a single writer (a2ui-prompt#build) with all readers ordered after it, so the race cannot occur regardless of timing.

Test plan

  • turbo api-extractor --dry=json graph contains zero react/cargo #build tasks (the regression that a too-broad root dependency introduced mid-review)
  • forced turbo api-extractor --filter='@lynx-js/genui*' -- --local → all api-extractor tasks succeed, 0 TS errors (reproduced the TS2307/TS7016 failure before the fix)
  • CI test-api and code-style-check pass

`@lynx-js/genui#api-extractor` runs the root `tsc` after Turbo has
scheduled `<sub>#build:api` for each subpackage. On CI the cached
artifacts have been observed to land `.js` ahead of `.d.ts`, so the
root `index.ts` import of `@lynx-js/genui/<sub>` would resolve to a
typeless `.js` and the build would fail with TS7016. Read the subpackage
`.d.ts` targets from the package.json `exports` and wait for each before
invoking `tsc`.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jun 3, 2026

🦋 Changeset detected

Latest commit: 5eeb6cd

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 3, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR stages lock JSON to a per-process temp file and atomically publishes it with link(); acquireLock now retries on read/parse failures and only deletes an existing lock when a parsed pid is confirmed dead. It also removes an inline pnpm run build and updates Turbo/task and package script references to use build. A changeset documents the TOCTOU fix.

Changes

Lock Acquisition & Pipeline Alignment

Layer / File(s) Summary
Atomic publish and acquireLock rewrite
packages/genui/scripts/run-api-extractor.mjs
Adds link import and rewrites acquireLock to stage lock JSON to a temp file and publish atomically via link(). On EEXIST it retries until timeout; it only deletes an existing lock when a parsed pid is dead or the existing lock cannot be read/parsed. Ensures temp-file cleanup in finally, still removes published lock on exit, and removes the inline pnpm run build call.
Changeset note
.changeset/fix-genui-api-extractor-lock-race.md
Documents the TOCTOU race fix: read/parse failures now cause the lock to be waited on (assumed mid-write) rather than immediately deleted; explains CI impact for @lynx-js/genui#api-extractor.
Turbo/task and package script alignment
packages/genui/a2ui-prompt/package.json, packages/genui/a2ui-prompt/turbo.json, packages/genui/turbo.json, turbo.json
Removes build:api script from a2ui-prompt, consolidates dist/** outputs under the build task in that package, updates @lynx-js/genui-a2ui-prompt dependency to #build in packages/genui/turbo.json, and adds explicit build dependency to the api-extractor task in the root turbo.json.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • lynx-family/lynx-stack#2741: Introduces/previously modified the lock-based API Extractor runner that this PR further adjusts to fix the TOCTOU/partial-write race.
  • lynx-family/lynx-stack#2794: Also adjusts a2ui-prompt Turbo/task dependency ordering around build:api vs build, affecting the same pipeline ordering.

Suggested reviewers

  • HuJean
  • PupilTong
  • Sherry-hue
  • gaoachao

Poem

🐰 I staged my lock in a cozy file,
I wrote my pid and timestamp with a smile.
I linked it quick so writes stay whole,
No mid-write deletions to trouble the goal.
Now builders wait, not race — calm builds compile.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and specifically identifies the main change: fixing a race condition in api-extractor's lock acquisition that caused flaky TS7016 errors, directly matching the primary bug fix in the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/genui-api-extractor-build-order

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 617d45874a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/genui/scripts/run-api-extractor.mjs Outdated
@upupming upupming marked this pull request as draft June 3, 2026 07:56
@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 3, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Jun 3, 2026

Merging this PR will improve performance by 17.03%

⚡ 1 improved benchmark
✅ 86 untouched benchmarks
⏩ 26 skipped benchmarks1

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation transform 1000 view elements 47.1 ms 40.2 ms +17.03%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing fix/genui-api-extractor-build-order (5eeb6cd) with main (5ea9473)

Open in CodSpeed

Footnotes

  1. 26 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 3, 2026

UI Judge

GEQI weighted score: 62 / 100 across 8 examples.
Average visual-correctness score: 0 / 5.
8 results have an error.

Dimension Weight Average Results Status
Usability & Interaction 30% 2.9 / 5 8 OK
Visual & Aesthetics 25% 3.3 / 5 8 OK
Consistency & Standards 15% 3.3 / 5 8 OK
Architecture & UX Writing 15% 3.1 / 5 8 OK
Accessibility & Performance 15% 3.1 / 5 8 OK
# Example Visual Correctness Usability & Interaction (30%) Visual & Aesthetics (25%) Consistency & Standards (15%) Architecture & UX Writing (15%) Accessibility & Performance (15%) GEQI Page Status
1 recs 0 / 5 2 / 5 3 / 5 2 / 5 2 / 5 3 / 5 48 / 100 preview Error
2 cast-grid 0 / 5 3 / 5 4 / 5 5 / 5 5 / 5 3 / 5 77 / 100 preview Error
3 citywalk-list 0 / 5 3 / 5 3 / 5 3 / 5 2 / 5 3 / 5 57 / 100 preview Error
4 fridge-search 0 / 5 3 / 5 3 / 5 4 / 5 3 / 5 3 / 5 63 / 100 preview Error
5 trip-planner 0 / 5 2 / 5 3 / 5 2 / 5 2 / 5 3 / 5 48 / 100 preview Error
6 weather-current 0 / 5 4 / 5 4 / 5 4 / 5 5 / 5 4 / 5 83 / 100 preview Error
7 product-card 0 / 5 4 / 5 4 / 5 4 / 5 4 / 5 4 / 5 80 / 100 preview Error
8 workout-plan 0 / 5 2 / 5 2 / 5 2 / 5 2 / 5 2 / 5 40 / 100 preview Error
Details

Result 1

  • Example: recs
  • Dimension: visual-correctness
  • Visual correctness: 0 / 5
  • GEQI dimensions:
    • Usability & Interaction: 2 / 5 (30%)
    • Visual & Aesthetics: 3 / 5 (25%)
    • Consistency & Standards: 2 / 5 (15%)
    • Architecture & UX Writing: 2 / 5 (15%)
    • Accessibility & Performance: 3 / 5 (15%)
  • Task: The A2UI playground preview should show date-night dining recommendations for Moonlight Terrace, Pinewood Bistro, and Sea Breeze Kitchen.
  • Error: visual-correctness judge did not run before GEQI scoring.

Result 2

  • Example: cast-grid
  • Dimension: visual-correctness
  • Visual correctness: 0 / 5
  • GEQI dimensions:
    • Usability & Interaction: 3 / 5 (30%)
    • Visual & Aesthetics: 4 / 5 (25%)
    • Consistency & Standards: 5 / 5 (15%)
    • Architecture & UX Writing: 5 / 5 (15%)
    • Accessibility & Performance: 3 / 5 (15%)
  • Task: The A2UI playground preview should show a cast grid for the short film Night Notes, including Lin Xia and Zhou Ning cast cards.
  • Error: visual-correctness judge did not run before GEQI scoring.

Result 3

  • Example: citywalk-list
  • Dimension: visual-correctness
  • Visual correctness: 0 / 5
  • GEQI dimensions:
    • Usability & Interaction: 3 / 5 (30%)
    • Visual & Aesthetics: 3 / 5 (25%)
    • Consistency & Standards: 3 / 5 (15%)
    • Architecture & UX Writing: 2 / 5 (15%)
    • Accessibility & Performance: 3 / 5 (15%)
  • Task: The A2UI playground preview should show weekend citywalk coffee picks with Rooftop Brew Room, Corner Canvas Lab, and Late Sun Roastery.
  • Error: visual-correctness judge did not run before GEQI scoring.

Result 4

  • Example: fridge-search
  • Dimension: visual-correctness
  • Visual correctness: 0 / 5
  • GEQI dimensions:
    • Usability & Interaction: 3 / 5 (30%)
    • Visual & Aesthetics: 3 / 5 (25%)
    • Consistency & Standards: 4 / 5 (15%)
    • Architecture & UX Writing: 3 / 5 (15%)
    • Accessibility & Performance: 3 / 5 (15%)
  • Task: The A2UI playground preview should show refrigerator search results with Siemens, Hualing, Haier, and Midea product cards.
  • Error: visual-correctness judge did not run before GEQI scoring.

Result 5

  • Example: trip-planner
  • Dimension: visual-correctness
  • Visual correctness: 0 / 5
  • GEQI dimensions:
    • Usability & Interaction: 2 / 5 (30%)
    • Visual & Aesthetics: 3 / 5 (25%)
    • Consistency & Standards: 2 / 5 (15%)
    • Architecture & UX Writing: 2 / 5 (15%)
    • Accessibility & Performance: 3 / 5 (15%)
  • Task: The A2UI playground preview should show a Kyoto 48-hour trip planner with Day 1 and Day 2 itinerary sections, including Monkey Park Viewpoint.
  • Error: visual-correctness judge did not run before GEQI scoring.

Result 6

  • Example: weather-current
  • Dimension: visual-correctness
  • Visual correctness: 0 / 5
  • GEQI dimensions:
    • Usability & Interaction: 4 / 5 (30%)
    • Visual & Aesthetics: 4 / 5 (25%)
    • Consistency & Standards: 4 / 5 (15%)
    • Architecture & UX Writing: 5 / 5 (15%)
    • Accessibility & Performance: 4 / 5 (15%)
  • Task: The A2UI playground preview should show the current weather for Austin, TX, including clear skies with light breeze.
  • Error: visual-correctness judge did not run before GEQI scoring.

Result 7

  • Example: product-card
  • Dimension: visual-correctness
  • Visual correctness: 0 / 5
  • GEQI dimensions:
    • Usability & Interaction: 4 / 5 (30%)
    • Visual & Aesthetics: 4 / 5 (25%)
    • Consistency & Standards: 4 / 5 (15%)
    • Architecture & UX Writing: 4 / 5 (15%)
    • Accessibility & Performance: 4 / 5 (15%)
  • Task: The A2UI playground preview should show a Wireless Headphones Pro product card with a visible Add to Cart action.
  • Error: visual-correctness judge did not run before GEQI scoring.

Result 8

  • Example: workout-plan
  • Dimension: visual-correctness
  • Visual correctness: 0 / 5
  • GEQI dimensions:
    • Usability & Interaction: 2 / 5 (30%)
    • Visual & Aesthetics: 2 / 5 (25%)
    • Consistency & Standards: 2 / 5 (15%)
    • Architecture & UX Writing: 2 / 5 (15%)
    • Accessibility & Performance: 2 / 5 (15%)
  • Task: The A2UI playground preview should show a weekly workout plan with five days from Monday Ramp-Up through Friday Conditioning.
  • Error: visual-correctness judge did not run before GEQI scoring.

Workflow run

upupming added 2 commits June 3, 2026 17:02
`acquireLock` deleted the lock file whenever it failed to read/parse its
contents. That window is normal: between `open(wx)` (atomically creating
an empty file) and `writeFile` (populating the JSON payload), the holder
hasn't written anything yet, so a contender reads `""` and `JSON.parse`
throws. The old catch then `rm`-d the lock, letting both processes
"acquire" it.

In CI, Turbo schedules `<sub>#api-extractor` and `genui#api-extractor`
concurrently (they share only `//#build` in their dependency graph), so
both invoke this script. With the bug, `<sub>`'s `rslib build` rewrites
`<sub>/dist/index.{js,d.ts}` while `genui`'s `tsc` reads it, and tsc
catches the window where `dist/index.js` exists but `dist/index.d.ts`
does not, failing with TS7016.

Wait on read/parse failures instead of deleting; only clear the lock if
we can prove the holder's PID is dead. A standalone concurrency repro
(5 acquirers, 50ms simulated write delay) goes from `maxConcurrent=5`
to `maxConcurrent=1` with this change.
@upupming upupming changed the title fix(genui): wait for subpackage .d.ts before api-extractor's tsc fix(genui): close TOCTOU in api-extractor lock Jun 3, 2026
@upupming
Copy link
Copy Markdown
Collaborator Author

upupming commented Jun 4, 2026

@upupming
Copy link
Copy Markdown
Collaborator Author

upupming commented Jun 4, 2026

upupming added a commit that referenced this pull request Jun 4, 2026
…ck cache race (#2794)

## Summary

`pnpm turbo api-extractor` can flake on `code-style-check` / `test-api`
with:

```
thread 'tokio-N' panicked at crates/rspack_storage/src/filesystem/db/transaction/mod.rs:53:7:
Transaction already in progress by process M:rslib-node in directory '.../packages/genui/a2ui-prompt/node_modules/.cache/rspack/<hash>/.temp'
Aborted (core dumped)
[ELIFECYCLE] Command failed with exit code 134.
```

The race: `@lynx-js/genui#api-extractor` (in
`packages/genui/turbo.json`) depends on both `//#build` *and*
`@lynx-js/genui-a2ui-prompt#build:api`. The build chain triggers
`genui-a2ui-prompt#build`, and at the same time turbo schedules
`genui-a2ui-prompt#build:api`. Both scripts are literally `rslib build`,
both produce the same rspack content hash, and both write to the same
`node_modules/.cache/rspack/<hash>/.temp` directory. rspack's own
filesystem-storage transaction lock catches the conflict and aborts the
second process with `SIGABRT` (exit 134).

Fix: add `dependsOn: ["build"]` to `build:api` in
`packages/genui/a2ui-prompt/turbo.json` so turbo serializes the two.
`build` runs first, releases the rspack transaction on exit; `build:api`
then runs against a warm content-addressed cache.

This is independent from the api-extractor lock TOCTOU fix in #2780 —
that one fixed the JSON lockfile race in `run-api-extractor.mjs`; this
one is rspack's internal cache directory, surfaced by turbo's task graph
rather than my lock script.

Scoped to just `genui-a2ui-prompt` because that's the only package where
I have a panic trace. `openui`/`a2ui`/`a2ui-catalog-extractor` share the
same `build`/`build:api` shape but use `tsc` not `rslib`, so they don't
hit the rspack lock. If they start flaking we can extend the same dep.

## Test plan

- [ ] `code-style-check` (which runs `pnpm turbo api-extractor`) passes
on this PR.
- [ ] No regression in `test-api` (which runs `pnpm turbo api-extractor
-- --local`).
- [ ] Local: `pnpm turbo api-extractor --filter=@lynx-js/genui` still
produces the genui api reports without panic.
@upupming upupming marked this pull request as ready for review June 4, 2026 08:08
@upupming upupming enabled auto-merge (squash) June 4, 2026 08:08
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 404dea2a84

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/genui/scripts/run-api-extractor.mjs Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/genui/scripts/run-api-extractor.mjs`:
- Around line 49-60: The staleHolder flag is computed from an earlier read and
can suffer a TOCTOU race before rm(lockPath); revalidate the lock immediately
before deleting by re-reading/parsing the lock file at lockPath (or checking its
current owner/pid via readFile and isProcessAlive) and only call rm(lockPath, {
force: true }) if the freshly-read pid is still a numeric dead process or the
file is unchanged; update the code around the existing readFile/staleHolder
logic (the variables/functions: readFile, lockPath, staleHolder, isProcessAlive,
rm) to perform this final check and skip deletion if the lock now belongs to a
live process or has changed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 45ff56cb-d828-401a-b11b-d1f3302daf78

📥 Commits

Reviewing files that changed from the base of the PR and between 617d458 and 404dea2.

📒 Files selected for processing (2)
  • .changeset/fix-genui-api-extractor-lock-race.md
  • packages/genui/scripts/run-api-extractor.mjs

Comment thread packages/genui/scripts/run-api-extractor.mjs Outdated
upupming added 2 commits June 4, 2026 16:14
Stage the fully-written lock in a per-process temp file and publish it
with link(2) instead of open(wx)+writeFile. Linking is atomic and fails
with EEXIST when the lock exists, so the lock always has complete
contents the moment it appears -- closing the window where a holder that
dies between open() and writeFile() leaves an empty lock that wedges
every later run until the 10-minute timeout. An unparsable lock can now
only be a corrupt file, so it is reaped instead of waited on.

Addresses Codex review on #2780.
… race

genui-a2ui-prompt's build:api was identical to build (both 'rslib build')
and ran right after it, rewriting the same dist/. With no turbo edge
between build:api and genui-cli#build (both only depend on #build), they
ran concurrently: rslib cleans dist and emits index.js before index.d.ts,
so genui-cli's tsc could read dist/ in the window where the .d.ts was
missing -> TS2307/TS7016 'Cannot find module @lynx-js/genui-a2ui-prompt'.

Point the genui api-extractor task at #build instead and remove the
duplicate build:api. dist is now written once, before its consumers, so
the race is gone. This also subsumes #2794: with no second 'rslib build'
there is no concurrent rspack cache transaction to conflict.

Verified locally: 'turbo api-extractor --filter=@lynx-js/genui*
--force -- --local' reproduced the TS error before, and passes 3/3 after.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ccb0845867

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/genui/scripts/run-api-extractor.mjs Outdated
… race

The real cause of the flaky 'Cannot find module @lynx-js/genui-a2ui-prompt'
(TS2307/TS7016) in test-api: run-api-extractor.mjs ran 'pnpm run build'
in-script. For a2ui-prompt that is 'rslib build', which cleans and
rewrites dist/ (emitting index.js before index.d.ts). genui-cli#build is
pulled into the api-extractor graph (via a2ui#api-extractor, whose
build:catalog needs the genui CLI) and reads a2ui-prompt/dist with no
turbo edge ordering it against that in-script rebuild -> its tsc could
observe dist/ mid-rewrite with the .d.ts missing.

Fix: stop building inside the script and make the api-extractor task
depend on 'build' so turbo builds each package exactly once, before both
api-extractor and every consumer build. dist/ now has a single writer
with all readers ordered after it, so the race cannot occur regardless of
timing. ensureMainEntryPoint stays as a last-resort build.

Verified: 'turbo build' then 4x forced 'turbo api-extractor -- --local'
over the genui graph -> 0 TS errors (reproduced reliably on CI before).
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 47c19ee6c1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/genui/scripts/run-api-extractor.mjs Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
packages/genui/scripts/run-api-extractor.mjs (1)

58-69: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Revalidate the lock immediately before deleting it.

staleHolder is derived from an earlier read, but Line 68 removes by path later. Two contenders can both classify the same dead lock as stale; one links a fresh lock, and the other then deletes that fresh lock, reopening concurrent entry into the critical section.

🔧 Proposed fix
         let staleHolder = false;
         try {
           const current = JSON.parse(await readFile(lockPath, 'utf8'));
           if (typeof current.pid === 'number' && !isProcessAlive(current.pid)) {
             staleHolder = true;
           }
-        } catch {
-          staleHolder = true;
+        } catch {
+          staleHolder = false;
         }
         if (staleHolder) {
-          await rm(lockPath, { force: true });
-          continue;
+          try {
+            const latest = JSON.parse(await readFile(lockPath, 'utf8'));
+            if (typeof latest.pid === 'number' && !isProcessAlive(latest.pid)) {
+              await rm(lockPath, { force: true });
+              continue;
+            }
+          } catch {
+            // Lock changed or disappeared; never delete blindly.
+          }
         }
         await sleep(retryDelayMs);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/genui/scripts/run-api-extractor.mjs` around lines 58 - 69,
staleHolder is determined from an earlier read but the code calls rm(lockPath)
later, allowing a race where another process creates a fresh lock; before
deleting, revalidate by re-reading lockPath (using readFile) and re-checking the
pid with isProcessAlive (or confirming contents unchanged) and only call
rm(lockPath, { force: true }) if the re-read still shows a dead/stale holder;
use the same variables (staleHolder, lockPath, readFile, isProcessAlive, rm) and
bail out/continue if the revalidation indicates the lock is no longer stale.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@packages/genui/scripts/run-api-extractor.mjs`:
- Around line 58-69: staleHolder is determined from an earlier read but the code
calls rm(lockPath) later, allowing a race where another process creates a fresh
lock; before deleting, revalidate by re-reading lockPath (using readFile) and
re-checking the pid with isProcessAlive (or confirming contents unchanged) and
only call rm(lockPath, { force: true }) if the re-read still shows a dead/stale
holder; use the same variables (staleHolder, lockPath, readFile, isProcessAlive,
rm) and bail out/continue if the revalidation indicates the lock is no longer
stale.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8400c3ec-ad0f-483e-80d2-283af6e7b647

📥 Commits

Reviewing files that changed from the base of the PR and between ccb0845 and 47c19ee.

📒 Files selected for processing (3)
  • packages/genui/scripts/run-api-extractor.mjs
  • packages/genui/turbo.json
  • turbo.json
✅ Files skipped from review due to trivial changes (1)
  • packages/genui/turbo.json

upupming added 2 commits June 4, 2026 17:44
The lock existed only to serialize the in-script 'pnpm run build' calls
(concurrent rslib builds corrupting rspack's cache, #2794). That in-script
build is gone — turbo now builds each package before its api-extractor
task — so the api-extractor script only runs the extractor binary, which
writes per-package outputs with no shared mutable state. Concurrent runs
need no mutual exclusion, so the link/reap lock (and its stale-reaping
TOCTOU flagged in review) is removed entirely.

Verified: forced 'turbo api-extractor -- --local' across the genui graph
-> 26/26 tasks, all 'API Extractor completed successfully', 0 TS errors.
packages/genui/a2ui/turbo.json defines its own api-extractor.dependsOn,
and turbo replaces (not merges) array fields, so the root-level 'build'
dep did not apply to @lynx-js/genui-a2ui#api-extractor. With the in-script
build removed, a forced a2ui#api-extractor could run against a stale
dist/index.d.ts. Add 'build' to a2ui's local api-extractor deps so its
own package is rebuilt first too.

Caught in review (Codex). Verified: every genui #api-extractor task now
depends on its own #build, and forced 'turbo api-extractor -- --local'
passes 26/26.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 45d7648df4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/genui/turbo.json Outdated
The previous commit added 'build' to the root api-extractor task, which
made every package's api-extractor build its package -- including react
packages whose '^build' pulls 'react-transform#build:wasm' /
'swc-plugin-reactlynx#build' (cargo build-wasi). Those failed in the
code-style-check / test-api jobs, which never built Rust before (the old
graph only depended on '//#build', i.e. tsc). That is why CI went from the
genui-cli TS2307 race to a cargo failure.

Revert the broad 'build' deps (root, genui meta, a2ui) and instead scope a
'build' dep to a2ui-prompt's api-extractor only -- a2ui-prompt is Rust-free
(no @lynx-js/react dep), so this orders a2ui-prompt#api-extractor after its
single rslib build without pulling any cargo task. The genui-cli race is
fixed by the already-removed in-script build (a2ui-prompt#build is now the
sole dist writer, and genui-cli#build depends on it). a2ui/openui/meta
dists build via the kept ensureMainEntryPoint fallback (single package, no
turbo chain) when absent.

Verified: full 'turbo api-extractor --dry=json' graph has zero cargo/react
#build tasks; forced genui api-extractor runs pass 3/3 with 0 TS errors,
0 cargo invocations.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8dedae8827

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/genui/scripts/run-api-extractor.mjs
Same fix as a2ui-prompt: a2ui-catalog-extractor is rust-free, so add an
explicit api-extractor -> build edge to extract from a freshly built dist
instead of relying on the removed in-script build. Not applied to ui-judge:
its build transitively pulls @lynx-js/react -> react-transform#build:wasm
(cargo), which must not enter the api-extractor graph used by
code-style-check; ui-judge relies on the ensureMainEntryPoint fallback.

Verified: api-extractor dry-run graph has zero react/cargo #build tasks.
@upupming upupming changed the title fix(genui): close TOCTOU in api-extractor lock fix(genui): fix flaky api-extractor dist-rewrite race (TS2307/TS7016) Jun 4, 2026
…race fix

Rewrite the stale lock-TOCTOU description to the actual root cause
(in-script rebuild racing consumer builds) and fix. Frontmatter stays
empty: build-tooling only, no @lynx-js/genui API/runtime change.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5eeb6cd3f8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/genui/scripts/run-api-extractor.mjs
@upupming upupming merged commit 91a2a16 into main Jun 4, 2026
44 checks passed
@upupming upupming deleted the fix/genui-api-extractor-build-order branch June 4, 2026 11:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants