Skip to content

feat(genui): add TypeDoc-driven A2UI catalog extractor#2521

Closed
PupilTong wants to merge 5 commits intolynx-family:mainfrom
PupilTong:codex/a2ui-catalog-extractor
Closed

feat(genui): add TypeDoc-driven A2UI catalog extractor#2521
PupilTong wants to merge 5 commits intolynx-family:mainfrom
PupilTong:codex/a2ui-catalog-extractor

Conversation

@PupilTong
Copy link
Copy Markdown
Collaborator

@PupilTong PupilTong commented Apr 24, 2026

Summary by CodeRabbit

  • New Features

    • New catalog extractor tool with CLI commands to generate and validate A2UI catalog JSON.
  • Documentation

    • Added full extractor guides, README, skill guide, and schema/tsdoc mapping and compatibility references.
  • Chores

    • Integrated catalog generation into the A2UI build.
    • Added comprehensive tests and fixtures for extraction validation.
  • Bug Fixes / Minor API

    • Added JSDoc for many catalog components and small prop updates (e.g., added 'stretch' for layout justify; clarified Button action typing).

Summary

  • add a new private @lynx-js/a2ui-catalog-extractor workspace package that can be used as an API, CLI, and installable Codex skill
  • replace the old checker-driven A2UI catalog generator with a TypeDoc plus explicit-syntax extractor and wire A2UI builds to use it
  • annotate the current A2UI catalog components with English TSDoc and add golden rstest coverage to keep generated legacy catalog shards exactly aligned with the previous output

Testing

  • Tests updated (or not required).
  • Documentation updated (or not required).
  • Changeset added, and when a BREAKING CHANGE occurs, it needs to be clearly marked (or not required).

Validation

  • fnm exec --using v24.15.0 -- pnpm --filter @lynx-js/a2ui-catalog-extractor build
  • fnm exec --using v24.15.0 -- pnpm --filter @lynx-js/a2ui-catalog-extractor test
  • fnm exec --using v24.15.0 -- pnpm --filter @lynx-js/a2ui-reactlynx build

Notes

  • no changeset was added because this work only affects private internal packages and generated assets in this repository
  • the new extractor keeps .tsx as the primary authoring path, supports .jsx as a best-effort path, and uses @a2uiSchema as the only custom escape hatch

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 24, 2026

⚠️ No Changeset found

Latest commit: a5186af

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

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

Click here to learn what changesets are, and how to add one.

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 24, 2026

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

Adds a new @lynx-js/a2ui-catalog-extractor package (implementation, CLI, docs, tests, fixtures) and integrates it into the a2ui build, removing the legacy local generator and adding TypeDoc/JSDoc-backed schema extraction with legacy-shard compatibility.

Changes

Cohort / File(s) Summary
Package config & build
packages/genui/a2ui-catalog-extractor/package.json, .../eslint.config.js, .../rslib.config.ts, .../rstest.config.ts, .../tsconfig.json, .../tsconfig.build.json, .../turbo.json
New package manifests and build/test/lint/turbo configs for the extractor package; CLI entrypoint configured; Node runtime constraint set.
Documentation & references
.github/a2ui-catalog-extractor.instructions.md, packages/genui/a2ui-catalog-extractor/README.md, .../SKILL.md, references/a2ui-v0.9-schema.md, references/tsdoc-mapping.md
Comprehensive docs describing extraction rules, authoring model, TypeDoc/TSDoc mapping, compatibility requirements, and update workflow.
Core source & API
packages/genui/a2ui-catalog-extractor/src/index.ts, .../types.ts, .../docs.ts, .../extractor.ts, .../cli.ts
Extractor implementation: TypeScript types, TypeDoc/JSDoc parsing/indexing, type→schema conversion, CLI generate/check, file render/write/diff utilities and public exports.
Tests & fixtures
packages/genui/a2ui-catalog-extractor/test/*, .../test/fixtures/tsx/*, .../test/fixtures/jsx/*, .../test/fixtures/jsx-loose/*, .../test/fixtures/tsx-*/*, .../test/fixtures/legacy-baseline/*/catalog.json
New unit/integration tests and extensive fixtures validating TSX/JSX extraction, error cases, golden legacy-shard parity and CLI behavior.
A2UI integration
packages/genui/a2ui/README.md, packages/genui/a2ui/package.json, packages/genui/a2ui/turbo.json, packages/genui/tsconfig.json
Switched a2ui build to invoke the new extractor CLI, added workspace dependency, updated turbo inputs and TS project references.
A2UI catalog components updates
packages/genui/a2ui/src/catalog/Button/index.tsx, .../Card/..., .../CheckBox/..., .../Column/..., .../Divider/..., .../Image/..., .../List/..., .../RadioGroup/..., .../Row/..., .../Text/...
Mostly JSDoc additions; Button action types extracted to named types; Column/Row justify unions include 'stretch'; small union order changes.
Removed legacy generator
packages/genui/a2ui/tools/catalog_generator.ts
Deleted old local catalog generator script (functionality replaced by new package).
Agent config
packages/genui/a2ui-catalog-extractor/agents/openai.yaml
New OpenAI agent config for the extractor with implicit invocation enabled.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • HuJean
  • Sherry-hue
  • gaoachao
  • fzx2666-fz

Poem

🐰 A tiny rabbit hopped the repo tree,
nibbling docs, TypeDoc, and a CLI spree,
old script retired, new extractor springs,
catalogs bloom from typed and doc-string things,
hop—golden fixtures pass, and builds dance with glee.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.19% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the primary change: introducing a TypeDoc-driven catalog extractor for A2UI components, which is the main purpose of this PR.
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.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 24, 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!

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: 11

🧹 Nitpick comments (8)
packages/genui/a2ui/package.json (1)

96-96: Prefer the extractor's workspace bin over invoking raw TS source.

The build directly invokes ../a2ui-catalog-extractor/src/cli.ts with node --experimental-strip-types, bypassing the package's declared bin entry (a2ui-catalog-extractor./dist/cli.js). While the current approach works—because the extractor source happens to conform to Node's type-stripping constraints and turbo's ^build ordering ensures the extractor is compiled first—this pattern:

  • Tightly couples the build to the sibling package's source-code layout, undermining the purpose of workspace dependencies and public entry points.
  • Obscures the actual dependency chain: future refactors to the extractor's TypeScript syntax or import structure could silently break this build, even though they pass the extractor's own build.

Since @lynx-js/a2ui-catalog-extractor is already declared as a workspace:* devDep and its bin entry is configured, use the built CLI:

Suggested change
-    "build": "node --experimental-strip-types ../a2ui-catalog-extractor/src/cli.ts generate --source ./src/catalog --out ./dist/catalog --tsconfig ./tsconfig.json --format legacy-shards && tsc -b"
+    "build": "a2ui-catalog-extractor generate --source ./src/catalog --out ./dist/catalog --tsconfig ./tsconfig.json --format legacy-shards && tsc -b"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/package.json` at line 96, The build script in the
package.json currently calls the extractor's source via "node
--experimental-strip-types ../a2ui-catalog-extractor/src/cli.ts", coupling to
the sibling package layout; change the "build" npm script to invoke the
extractor's declared bin (a2ui-catalog-extractor) instead so the
workspace-provided CLI is used (e.g. replace the node invocation with
"a2ui-catalog-extractor generate --source ./src/catalog --out ./dist/catalog
--tsconfig ./tsconfig.json --format legacy-shards && tsc -b"), keeping the rest
of the arguments and the trailing "tsc -b" intact.
packages/genui/a2ui-catalog-extractor/test/cli.test.ts (1)

33-116: Temp directories are never removed.

All three tests fs.mkdtemp under os.tmpdir() and never unlink, so each CI run leaks directories (and, on the drift test, also a modified Chip/catalog.json). Use a try { ... } finally { await fs.rm(outputDir, { recursive: true, force: true }); } around each test body, or extract a small helper.

♻️ Proposed cleanup pattern
   test('generate writes catalog shards to disk', async () => {
     const outputDir = await fs.mkdtemp(
       path.join(os.tmpdir(), 'a2ui-catalog-cli-generate-'),
     );
-
-    const { stdout } = await runCli([...]);
-    ...
-    expect(JSON.parse(generated)).toHaveProperty('Chip');
+    try {
+      const { stdout } = await runCli([...]);
+      ...
+      expect(JSON.parse(generated)).toHaveProperty('Chip');
+    } finally {
+      await fs.rm(outputDir, { recursive: true, force: true });
+    }
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui-catalog-extractor/test/cli.test.ts` around lines 33 -
116, Each test creates a temporary outputDir via fs.mkdtemp and never removes
it; wrap each test body that calls runCli (including the "generate", "check",
and "drift" cases) in a try { ... } finally { await fs.rm(outputDir, {
recursive: true, force: true }) } (or factor out a helper that creates the temp
dir, yields it to the test, and always cleans up) so that outputDir (and any
modified Chip/catalog.json) is removed after the test; refer to the existing
fs.mkdtemp usage, the outputDir variable, and runCli invocation points to locate
where to add the try/finally cleanup.
packages/genui/a2ui-catalog-extractor/test/extract-catalog.test.ts (2)

173-197: Temp directory is never cleaned up.

fs.mkdtemp creates a directory under os.tmpdir() that is never removed after the test. It's harmless but accumulates over repeated runs. Consider a try { ... } finally { await fs.rm(outputDir, { recursive: true, force: true }); } wrapper.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui-catalog-extractor/test/extract-catalog.test.ts` around
lines 173 - 197, The test 'writeCatalogFiles and checkCatalogFiles round-trip
generated shards' creates a temp dir with fs.mkdtemp (assigned to outputDir) but
never removes it; wrap the test's runtime usage of outputDir in a try { ... }
finally { await fs.rm(outputDir, { recursive: true, force: true }); } so the
directory is always cleaned up after using extractCatalog, writeCatalogFiles and
checkCatalogFiles; ensure the finally runs even on assertion failures.

106-131: Golden-baseline test couples to repo's real dist/ path via outDir.

renderCatalogFiles only uses outDir to compute paths (no I/O), so this works today. Just be aware that the outDir value here is effectively ignored by the assertion (you only read via path.basename(path.dirname(...))); you could pass any placeholder outDir and get the same result, which would decouple the test from packages/genui/a2ui/dist/catalog. Not a defect, just more robust.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui-catalog-extractor/test/extract-catalog.test.ts` around
lines 106 - 131, The test for legacy A2UI catalog fixtures passes a real
repository dist path into renderCatalogFiles via the outDir option, coupling the
test to packages/genui/a2ui/dist/catalog even though renderCatalogFiles only
uses outDir to compute file paths (no I/O); change the call that constructs
renderedFiles to pass a neutral placeholder outDir (e.g., a tmp or dummy path)
instead of path.join(workspaceRoot, 'packages/genui/a2ui/dist/catalog') so the
assertions that use path.basename(path.dirname(renderedFile.path)) remain valid
while decoupling the test from the repo-specific dist location; keep the rest of
the test (extractCatalog, fixturePath lookup and content assertions) unchanged.
packages/genui/a2ui/src/catalog/Button/index.tsx (1)

9-75: Override appears to duplicate what the extractor can infer from the TS type.

The action field's TS shape ({ event: { name: string; context?: Record<string, string | number | boolean | { path: string }> } }) already encodes everything the @a2uiSchema block asserts — including required: ['event'], required: ['name'], the oneOf for context values, and { path: string } branches. Per the extractor instructions, @a2uiSchema should be reserved for "node-local JSON Schema fragments that cannot be represented clearly with normal declarations." If the inferred schema already matches the fixture, dropping the override would keep this file leaner and reduce drift risk between the TS type and the hand-written JSON.

Worth verifying by extracting without the override and diffing against the golden fixture; if they match, prefer deletion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/catalog/Button/index.tsx` around lines 9 - 75, The
`@a2uiSchema` JSON schema block on ButtonProps duplicates the TypeScript shape of
the action property (action -> event -> name, context) and should be removed to
avoid drift: delete the `@a2uiSchema` comment block that annotates
ButtonProps.action (the JSON schema starting at the comment before action) so
the extractor will infer the schema from the TypeScript declaration
(ButtonProps.action / event.name / event.context); after removal, run the
extractor and diff the generated schema against the golden fixture to verify
they match, and only keep the override if there is a concrete mismatch.
packages/genui/a2ui-catalog-extractor/src/cli.ts (1)

71-105: Consider catching top-level rejections for a friendlier CLI UX.

await main() at the top level will surface a raw unhandled-rejection stack (and still exit non-zero on Node 24) for expected failures like missing --source, invalid format, extractor errors, etc. Wrapping in a small handler gives users a single-line message and a clean exit code.

🛠️ Suggested change
-await main();
+try {
+  await main();
+} catch (error) {
+  console.error(error instanceof Error ? error.message : String(error));
+  process.exitCode = 1;
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui-catalog-extractor/src/cli.ts` around lines 71 - 105, The
top-level await on main() can surface raw unhandled-rejection stacks; replace
the bare await main() with a top-level error handler that calls main() and
catches any rejection, prints a single-line friendly error (e.g., use err
instanceof Error ? err.message : String(err) with console.error), and sets
process.exitCode = 1; locate the current top-level call to main() and change it
to an invocation with .catch(...) (or a try/catch wrapper) so failures from
parseCatalogOptions, extractCatalog, writeCatalogFiles, or checkCatalogFiles
produce a clean, single-line CLI error and exit code.
packages/genui/a2ui-catalog-extractor/src/extractor.ts (2)

383-401: Dead branch and duplicated schema.required assignment.

required is populated only inside the for (const property of properties) loop at lines 354-367, and that loop is the sole source of property-name collection. So when propertyNames.length === 0, required is guaranteed to be [], making the else if (required.length > 0) branch unreachable. The two branches can collapse to a single check.

♻️ Proposed simplification
-  if (propertyNames.length > 0) {
-    schema.required = required;
-  } else if (required.length > 0) {
-    schema.required = required;
-  }
-
-  if (additionalProperties === undefined) {
-    if (propertyNames.length > 0) {
-      schema.additionalProperties = false;
-    }
-  } else {
+  if (required.length > 0) {
+    schema.required = required;
+  }
+
+  if (additionalProperties === undefined) {
+    if (propertyNames.length > 0) {
+      schema.additionalProperties = false;
+    }
+  } else {
     schema.additionalProperties = additionalProperties;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui-catalog-extractor/src/extractor.ts` around lines 383 -
401, The post-loop logic duplicates an impossible branch: since required is only
filled inside the for (const property of properties) that also populates
propertyNames, the else if (required.length > 0) branch can never run when
propertyNames.length === 0; consolidate the required assignment by setting
schema.required = required when propertyNames.length > 0 (remove the separate
else-if) and simplify the additionalProperties handling to set
schema.additionalProperties = false when additionalProperties === undefined &&
propertyNames.length > 0, otherwise assign the provided additionalProperties
value; update the code paths referencing propertyNames, required, schema, and
additionalProperties accordingly.

964-964: Use a fixed locale for deterministic output.

Array.prototype.sort with default localeCompare depends on the runtime ICU locale, which can reorder components like z vs accented variants across machines/CI runners. Since the output is a golden-tested catalog, prefer a stable ordering.

♻️ Suggested fix
-  components.sort((left, right) => left.name.localeCompare(right.name));
+  components.sort((left, right) =>
+    left.name.localeCompare(right.name, 'en-US')
+  );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui-catalog-extractor/src/extractor.ts` at line 964, The sort
is nondeterministic across locales because it calls
left.name.localeCompare(right.name) without a fixed locale; update the
comparison used by components.sort to call localeCompare with an explicit, fixed
locale (e.g., "en" or "en-US") and optional stable options (e.g., sensitivity)
so component ordering is stable across environments; locate the
components.sort(...) call and replace the comparator to use
left.name.localeCompare(right.name, "en-US", {sensitivity: "base"}) (or similar
fixed-locale/options) to ensure deterministic output.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/genui/a2ui-catalog-extractor/references/tsdoc-mapping.md`:
- Around line 16-27: The markdown table row containing the literal text "A | B"
is malformed because the pipe is unescaped; update that table row so the left
cell reads the union text with the pipe escaped (e.g., "A \| B") and set the
right cell to indicate the generated schema mapping to oneOf (use the schema
token `oneOf`) so the row reads: left cell "A \| B" and right cell "`oneOf`".

In `@packages/genui/a2ui-catalog-extractor/SKILL.md`:
- Around line 30-32: The SKILL.md Implementation Note incorrectly claims
`@a2uiSchema` payloads are read from the TypeScript AST only; update the wording
to state the real dual-source behavior: TypeDoc is the primary source (see
src/docs.ts:buildPropertyDoc which uses
reflection.comment?.getTag('@a2uiSchema') and src/docs.ts:buildTypeDocIndex
which registers the blockTag), and src/extractor.ts:getAstPropertyDoc provides
an AST fallback for inline property declarations; rephrase to explicitly
instruct contributors to keep both paths (TypeDoc-first, AST-as-fallback) so
future edits won't remove the TypeDoc handling.

In `@packages/genui/a2ui-catalog-extractor/src/cli.ts`:
- Around line 54-57: Validate the format passed in values['format'] before
casting: ensure the value is one of the CatalogFormat string literals (e.g., the
allowed enum/union members) and if undefined default to 'legacy-shards'; if the
string is not a valid CatalogFormat, throw or exit with a clear error message.
Update the construction of extractOptions (and any code that reads
values['format']) to perform this check rather than an unchecked cast so
renderCatalogFiles won't silently treat unknown formats as legacy; reference the
ExtractCatalogOptions type and the CatalogFormat symbol when implementing the
guard and error path.

In `@packages/genui/a2ui-catalog-extractor/src/docs.ts`:
- Around line 69-95: parseScalarToken currently returns undefined for both empty
input and the literal token "undefined", causing buildPropertyDoc (which checks
defaultValue !== undefined) to silently drop `@default` undefined; also JSON.parse
errors are unwrapped. Fix by changing parseScalarToken to treat the literal
'undefined' as an explicit error (throw a clear, descriptive Error mentioning
the offending token and the caller parseScalarToken) instead of returning
undefined, and wrap the JSON.parse call (the branch that tests
startsWith('"')/startsWith('[')/startsWith('{')) in a try/catch that rethrows a
new Error including the original trimmed content and that it occurred while
parsing a default/@a2uiSchema token so callers can surface actionable messages;
update any unit tests to expect the new error behavior for the 'undefined' token
and for malformed JSON.
- Around line 187-192: The switch handling override keys (the block with cases
'const', 'default', 'deprecated', 'description', 'type' that currently just
continues) must validate the 'type' override against the allowed JsonSchema
types; instead of falling through for 'type', check that the provided value is
one of: 'array', 'boolean', 'number', 'object', 'string' and throw or report a
validation error if not. Update the branch in the same switch (the case 'type')
to perform this membership check on the override value and reject/raise an error
with a clear message referencing the invalid type value so typos like "String"
are caught before the catalog is emitted.

In `@packages/genui/a2ui-catalog-extractor/src/extractor.ts`:
- Around line 744-747: parseTypeNode currently returns a silent permissive
fallback ({ type: 'string' }) for unknown/unhandled TypeScript nodes which can
hide schema errors; change it to surface diagnostics by throwing or recording a
warning that includes the source file path and the node text (use
typeNode.getText(sourceFile)) when no rule matches, and only allow the
permissive { type: 'string' } fallback when the source file's ScriptKind
indicates a permissive JSX/JS mode; update parseTypeNode (and any callers) to
either propagate the thrown diagnostic or consume/aggregate the warning into a
collected warnings list so unrecognized types are visible to the caller.
- Around line 1011-1046: The function checkCatalogFiles performs a direct string
comparison between file.content (from renderCatalogFiles) and the on-disk
content read with fs.readFile, which produces false mismatches on CRLF vs LF
platforms; normalize line endings on both sides before comparing (e.g., convert
both fileContent and file.content to use '\n' only) so mismatched, missing,
actual and expected are set from the normalized values; update the comparison in
checkCatalogFiles to compare normalizedFileContent !== normalizedExpectedContent
and assign actual/expected from the normalized strings.
- Around line 90-138: getComponentDeclarations currently only inspects
FunctionDeclaration and VariableStatement and will silently miss re-exports and
default export assignments; update getComponentDeclarations to detect
ts.isExportAssignment (export default Foo) and ts.isExportDeclaration with
namedBindings/specifiers (export { Foo }) within sourceFile.statements and, when
encountered, throw a clear, descriptive error (e.g. referencing the unsupported
export kind and the symbol name/text) so the extractor fails loudly; reference
the function name getComponentDeclarations and the AST kinds ts.ExportAssignment
and ts.ExportDeclaration in your change so the extractor explicitly handles or
rejects these patterns.

In `@packages/genui/a2ui-catalog-extractor/src/types.ts`:
- Line 39: The JsonSchema.enum field is typed too narrowly as string[] while
validateSchemaValue in docs.ts allows enum entries of null | boolean | number |
string; update the JsonSchema type to reflect the full set of allowed literal
types (e.g., enum?: Array<string | number | boolean | null>) so the TypeScript
type matches runtime validation and downstream uses like mergeSchema; locate the
JsonSchema definition in types.ts and adjust the enum member accordingly (or
alternatively, if you prefer stricter runtime behavior, change
validateSchemaValue in docs.ts to only accept strings and keep JsonSchema.enum
as string[] — pick one consistent approach and apply it across JsonSchema and
validateSchemaValue/mergeSchema).

In `@packages/genui/a2ui/src/catalog/Row/index.tsx`:
- Around line 15-23: The union type for justify includes 'stretch' but there's
no .distribution-stretch rule in Row/style.css and Column/style.css; either
remove 'stretch' from the justify union in Row/index.tsx (and any corresponding
Column type) to keep types in sync with available CSS classes, or add a
.distribution-stretch CSS rule to both Row/style.css and Column/style.css
implementing the same main-axis distribution behavior as the other
.distribution-* classes (mirror how .distribution-start/.distribution-center
etc. set justify-content) so the emitted class name distribution-stretch has a
backing style.

In `@packages/genui/a2ui/turbo.json`:
- Around line 11-12: Replace the relative cross-package inputs in
packages/genui/a2ui/turbo.json with the Turborepo root token: change the entries
referencing "../a2ui-catalog-extractor/src/**" and
"../a2ui-catalog-extractor/tsconfig*.json" to use "$TURBO_ROOT$" and the package
path (i.e., point to the packages/genui/a2ui-catalog-extractor/src/** and
packages/genui/a2ui-catalog-extractor/tsconfig*.json via $TURBO_ROOT$) so the
Turbo cache and artifact resolution follow the repository convention used
elsewhere.

---

Nitpick comments:
In `@packages/genui/a2ui-catalog-extractor/src/cli.ts`:
- Around line 71-105: The top-level await on main() can surface raw
unhandled-rejection stacks; replace the bare await main() with a top-level error
handler that calls main() and catches any rejection, prints a single-line
friendly error (e.g., use err instanceof Error ? err.message : String(err) with
console.error), and sets process.exitCode = 1; locate the current top-level call
to main() and change it to an invocation with .catch(...) (or a try/catch
wrapper) so failures from parseCatalogOptions, extractCatalog,
writeCatalogFiles, or checkCatalogFiles produce a clean, single-line CLI error
and exit code.

In `@packages/genui/a2ui-catalog-extractor/src/extractor.ts`:
- Around line 383-401: The post-loop logic duplicates an impossible branch:
since required is only filled inside the for (const property of properties) that
also populates propertyNames, the else if (required.length > 0) branch can never
run when propertyNames.length === 0; consolidate the required assignment by
setting schema.required = required when propertyNames.length > 0 (remove the
separate else-if) and simplify the additionalProperties handling to set
schema.additionalProperties = false when additionalProperties === undefined &&
propertyNames.length > 0, otherwise assign the provided additionalProperties
value; update the code paths referencing propertyNames, required, schema, and
additionalProperties accordingly.
- Line 964: The sort is nondeterministic across locales because it calls
left.name.localeCompare(right.name) without a fixed locale; update the
comparison used by components.sort to call localeCompare with an explicit, fixed
locale (e.g., "en" or "en-US") and optional stable options (e.g., sensitivity)
so component ordering is stable across environments; locate the
components.sort(...) call and replace the comparator to use
left.name.localeCompare(right.name, "en-US", {sensitivity: "base"}) (or similar
fixed-locale/options) to ensure deterministic output.

In `@packages/genui/a2ui-catalog-extractor/test/cli.test.ts`:
- Around line 33-116: Each test creates a temporary outputDir via fs.mkdtemp and
never removes it; wrap each test body that calls runCli (including the
"generate", "check", and "drift" cases) in a try { ... } finally { await
fs.rm(outputDir, { recursive: true, force: true }) } (or factor out a helper
that creates the temp dir, yields it to the test, and always cleans up) so that
outputDir (and any modified Chip/catalog.json) is removed after the test; refer
to the existing fs.mkdtemp usage, the outputDir variable, and runCli invocation
points to locate where to add the try/finally cleanup.

In `@packages/genui/a2ui-catalog-extractor/test/extract-catalog.test.ts`:
- Around line 173-197: The test 'writeCatalogFiles and checkCatalogFiles
round-trip generated shards' creates a temp dir with fs.mkdtemp (assigned to
outputDir) but never removes it; wrap the test's runtime usage of outputDir in a
try { ... } finally { await fs.rm(outputDir, { recursive: true, force: true });
} so the directory is always cleaned up after using extractCatalog,
writeCatalogFiles and checkCatalogFiles; ensure the finally runs even on
assertion failures.
- Around line 106-131: The test for legacy A2UI catalog fixtures passes a real
repository dist path into renderCatalogFiles via the outDir option, coupling the
test to packages/genui/a2ui/dist/catalog even though renderCatalogFiles only
uses outDir to compute file paths (no I/O); change the call that constructs
renderedFiles to pass a neutral placeholder outDir (e.g., a tmp or dummy path)
instead of path.join(workspaceRoot, 'packages/genui/a2ui/dist/catalog') so the
assertions that use path.basename(path.dirname(renderedFile.path)) remain valid
while decoupling the test from the repo-specific dist location; keep the rest of
the test (extractCatalog, fixturePath lookup and content assertions) unchanged.

In `@packages/genui/a2ui/package.json`:
- Line 96: The build script in the package.json currently calls the extractor's
source via "node --experimental-strip-types
../a2ui-catalog-extractor/src/cli.ts", coupling to the sibling package layout;
change the "build" npm script to invoke the extractor's declared bin
(a2ui-catalog-extractor) instead so the workspace-provided CLI is used (e.g.
replace the node invocation with "a2ui-catalog-extractor generate --source
./src/catalog --out ./dist/catalog --tsconfig ./tsconfig.json --format
legacy-shards && tsc -b"), keeping the rest of the arguments and the trailing
"tsc -b" intact.

In `@packages/genui/a2ui/src/catalog/Button/index.tsx`:
- Around line 9-75: The `@a2uiSchema` JSON schema block on ButtonProps duplicates
the TypeScript shape of the action property (action -> event -> name, context)
and should be removed to avoid drift: delete the `@a2uiSchema` comment block that
annotates ButtonProps.action (the JSON schema starting at the comment before
action) so the extractor will infer the schema from the TypeScript declaration
(ButtonProps.action / event.name / event.context); after removal, run the
extractor and diff the generated schema against the golden fixture to verify
they match, and only keep the override if there is a concrete mismatch.
🪄 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: 42b5ecc4-6e73-4ccb-8277-ca098b42e897

📥 Commits

Reviewing files that changed from the base of the PR and between 57c7fa3 and 6c5eef3.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (52)
  • .github/a2ui-catalog-extractor.instructions.md
  • packages/genui/a2ui-catalog-extractor/README.md
  • packages/genui/a2ui-catalog-extractor/SKILL.md
  • packages/genui/a2ui-catalog-extractor/agents/openai.yaml
  • packages/genui/a2ui-catalog-extractor/eslint.config.js
  • packages/genui/a2ui-catalog-extractor/package.json
  • packages/genui/a2ui-catalog-extractor/references/a2ui-v0.9-schema.md
  • packages/genui/a2ui-catalog-extractor/references/tsdoc-mapping.md
  • packages/genui/a2ui-catalog-extractor/rslib.config.ts
  • packages/genui/a2ui-catalog-extractor/rstest.config.ts
  • packages/genui/a2ui-catalog-extractor/src/cli.ts
  • packages/genui/a2ui-catalog-extractor/src/docs.ts
  • packages/genui/a2ui-catalog-extractor/src/extractor.ts
  • packages/genui/a2ui-catalog-extractor/src/index.ts
  • packages/genui/a2ui-catalog-extractor/src/types.ts
  • packages/genui/a2ui-catalog-extractor/test/cli.test.ts
  • packages/genui/a2ui-catalog-extractor/test/extract-catalog.test.ts
  • packages/genui/a2ui-catalog-extractor/test/fixtures/jsx/catalog/Badge/index.jsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/jsx/tsconfig.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/Button/catalog.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/Card/catalog.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/CheckBox/catalog.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/Column/catalog.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/Divider/catalog.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/Image/catalog.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/List/catalog.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/RadioGroup/catalog.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/Row/catalog.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/Text/catalog.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid/catalog/Broken/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid/tsconfig.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx/catalog/Chip/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx/tsconfig.json
  • packages/genui/a2ui-catalog-extractor/tsconfig.build.json
  • packages/genui/a2ui-catalog-extractor/tsconfig.json
  • packages/genui/a2ui-catalog-extractor/tsdoc.json
  • packages/genui/a2ui-catalog-extractor/turbo.json
  • packages/genui/a2ui/README.md
  • packages/genui/a2ui/package.json
  • packages/genui/a2ui/src/catalog/Button/index.tsx
  • packages/genui/a2ui/src/catalog/Card/index.tsx
  • packages/genui/a2ui/src/catalog/CheckBox/index.tsx
  • packages/genui/a2ui/src/catalog/Column/index.tsx
  • packages/genui/a2ui/src/catalog/Divider/index.tsx
  • packages/genui/a2ui/src/catalog/Image/index.tsx
  • packages/genui/a2ui/src/catalog/List/index.tsx
  • packages/genui/a2ui/src/catalog/RadioGroup/index.tsx
  • packages/genui/a2ui/src/catalog/Row/index.tsx
  • packages/genui/a2ui/src/catalog/Text/index.tsx
  • packages/genui/a2ui/tools/catalog_generator.ts
  • packages/genui/a2ui/turbo.json
  • packages/genui/tsconfig.json
💤 Files with no reviewable changes (1)
  • packages/genui/a2ui/tools/catalog_generator.ts

Comment thread packages/genui/a2ui-catalog-extractor/references/tsdoc-mapping.md Outdated
Comment thread packages/genui/a2ui-catalog-extractor/SKILL.md Outdated
Comment thread packages/genui/a2ui-catalog-extractor/src/cli.ts
Comment thread packages/genui/a2ui-catalog-extractor/src/docs.ts Outdated
Comment thread packages/genui/a2ui-catalog-extractor/src/docs.ts Outdated
Comment thread packages/genui/a2ui-catalog-extractor/src/extractor.ts
Comment thread packages/genui/a2ui-catalog-extractor/src/extractor.ts
Comment thread packages/genui/a2ui-catalog-extractor/src/types.ts Outdated
Comment on lines 15 to 23
justify?:
| 'start'
| 'center'
| 'end'
| 'stretch'
| 'spaceBetween'
| 'spaceAround'
| 'spaceEvenly'
| 'stretch';
| 'spaceEvenly';
align?: 'start' | 'center' | 'end' | 'stretch';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify both Row and Column styles define distribution-stretch (and related) classes.
fd -a 'style.css' packages/genui/a2ui/src/catalog | xargs -I {} sh -c 'echo "=== {} ==="; cat "{}"'
rg -nP '\bdistribution-(stretch|spaceEvenly|spaceBetween|spaceAround|start|center|end)\b' packages/genui/a2ui

Repository: lynx-family/lynx-stack

Length of output: 9863


Add distribution-stretch CSS rule to Row and Column styles, or remove 'stretch' from the justify union.

The code adds 'stretch' to the justify union type, but packages/genui/a2ui/src/catalog/Row/style.css and packages/genui/a2ui/src/catalog/Column/style.css do not define a corresponding .distribution-stretch CSS class. When justify: 'stretch' is used, the renderer will emit the class name distribution-stretch with no CSS rule backing it, causing the element to render without any main-axis distribution styling.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/catalog/Row/index.tsx` around lines 15 - 23, The
union type for justify includes 'stretch' but there's no .distribution-stretch
rule in Row/style.css and Column/style.css; either remove 'stretch' from the
justify union in Row/index.tsx (and any corresponding Column type) to keep types
in sync with available CSS classes, or add a .distribution-stretch CSS rule to
both Row/style.css and Column/style.css implementing the same main-axis
distribution behavior as the other .distribution-* classes (mirror how
.distribution-start/.distribution-center etc. set justify-content) so the
emitted class name distribution-stretch has a backing style.

Comment thread packages/genui/a2ui/turbo.json 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: 2

🧹 Nitpick comments (4)
packages/genui/a2ui-catalog-extractor/src/extractor.ts (2)

1041-1079: checkCatalogFiles conflates all read errors with "missing".

The bare catch { missing.push(file.path) } lumps every fs.readFile failure — including permission errors, EISDIR, EIO, etc. — into the missing bucket. Debugging a CI catalog:check failure on a wonky filesystem/permissions setup will be confusing because the tool will say the file is missing when it isn't. Consider narrowing the catch to ENOENT and rethrowing other errors.

🔧 Narrow to ENOENT
-    } catch {
-      missing.push(file.path);
-    }
+    } catch (error) {
+      if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') {
+        missing.push(file.path);
+        continue;
+      }
+      throw error;
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui-catalog-extractor/src/extractor.ts` around lines 1041 -
1079, The catch in checkCatalogFiles currently treats any fs.readFile failure as
"missing"; change the error handling inside the for loop around await
fs.readFile(file.path, ...) so that you only push file.path into the missing
array when the error code is ENOENT, and rethrow (or propagate) any other errors
(e.g., permission/EISDIR/EIO) so they surface instead of being misclassified;
keep all other logic (mismatched, actual/expected assignment, and returned
CheckCatalogFilesResult) unchanged.

1008-1025: renderCatalogFiles trusts result.catalog to be populated.

result.catalog is typed as optional but is dereferenced unconditionally for the a2ui-catalog format. If a caller (e.g., a library consumer) constructs an ExtractCatalogResult with format: 'a2ui-catalog' but omits catalog, JSON.stringify(undefined, null, 2) returns the primitive value undefined, and the template literal writes a literal "undefined\n" file with no error. A narrow guard would fail fast with a useful message instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui-catalog-extractor/src/extractor.ts` around lines 1008 -
1025, The function renderCatalogFiles currently assumes result.catalog exists
when result.format === 'a2ui-catalog' and will emit the literal "undefined" if
it's missing; update renderCatalogFiles to validate that result.catalog is
defined when format is 'a2ui-catalog' and throw a clear, fast-failing Error (or
return a rejected Promise if async) otherwise; reference the symbols result,
renderCatalogFiles, ExtractCatalogResult and catalog in the error message so
callers know they passed an invalid ExtractCatalogResult; keep behavior for
non-'a2ui-catalog' paths unchanged.
packages/genui/a2ui-catalog-extractor/src/docs.ts (1)

316-336: Multiple @a2uiSchema tags silently take the last one.

getAstPropertyDoc iterates all @a2uiSchema tags and lets the last one overwrite schemaOverride without warning. If a developer accidentally adds two of these tags, the earlier override is dropped silently. Consider either breaking after the first tag or throwing on duplicates for better debuggability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui-catalog-extractor/src/docs.ts` around lines 316 - 336, In
getAstPropertyDoc, the loop over comment tags for `@a2uiSchema` currently lets
later tags overwrite schemaOverride silently; update getAstPropertyDoc so it
detects multiple `@a2uiSchema` tags and either (a) stops after the first tag by
breaking once schemaOverride is set, or (b) throws a clear error when a second
`@a2uiSchema` is encountered; locate the tag iteration in getAstPropertyDoc (where
schemaOverride is assigned) and add the duplicate-detection logic and an early
break or throw to prevent silent overwrites.
packages/genui/a2ui-catalog-extractor/src/cli.ts (1)

115-120: Consider printing usage on parse errors.

When parseCatalogOptions throws (missing --source/--out, unknown option under strict: true, unsupported --format), only error.message is printed and the CLI exits with 1. Invoking printUsage() in the catch for argument-shaped errors would make the CLI more discoverable, but this is non-blocking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui-catalog-extractor/src/cli.ts` around lines 115 - 120,
Catch block should call printUsage() for argument/parse errors so users see
help; modify the catch around await main() to detect errors coming from CLI
parsing (thrown by parseCatalogOptions) — e.g., check error originates from
parseCatalogOptions (by error.name, a custom property you add when throwing, or
by matching known parse error messages like missing --source/--out/unsupported
--format/unknown option) and invoke printUsage() before logging the error and
setting process.exitCode = 1; ensure functions parseCatalogOptions and
printUsage are referenced so you locate where to add the detection and call.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/genui/a2ui-catalog-extractor/src/extractor.ts`:
- Around line 114-120: The current blanket rejection of ts.isExportDeclaration
blocks type-only and star re-exports; change the condition so you only throw for
export declarations that introduce value bindings: check
ts.isExportDeclaration(statement) AND that statement.isTypeOnly is false AND
that statement.exportClause is present (meaning it's a named re-export of
values), then throw the Error using the existing message (reference symbols:
ts.isExportDeclaration, ExportDeclaration, statement.isTypeOnly,
statement.exportClause, statement.getText(sourceFile), sourceFile.fileName).
- Around line 515-532: collectInterfaceProperties currently only uses
declaration.members and thus omits inherited properties from heritage clauses;
update it to resolve each heritage clause type via context.localDeclarations and
for each resolved base declaration call parseNamedDeclaration (or recursively
call collectInterfaceProperties) to collect and merge inherited members into the
CollectedTypeElements before calling collectTypeElementProperties, honoring the
parseState.seen set to avoid cycles; if a heritage clause cannot be resolved
through context.localDeclarations, throw a clear error naming the unresolved
base type to prompt an `@a2uiSchema` override or simplification.

---

Nitpick comments:
In `@packages/genui/a2ui-catalog-extractor/src/cli.ts`:
- Around line 115-120: Catch block should call printUsage() for argument/parse
errors so users see help; modify the catch around await main() to detect errors
coming from CLI parsing (thrown by parseCatalogOptions) — e.g., check error
originates from parseCatalogOptions (by error.name, a custom property you add
when throwing, or by matching known parse error messages like missing
--source/--out/unsupported --format/unknown option) and invoke printUsage()
before logging the error and setting process.exitCode = 1; ensure functions
parseCatalogOptions and printUsage are referenced so you locate where to add the
detection and call.

In `@packages/genui/a2ui-catalog-extractor/src/docs.ts`:
- Around line 316-336: In getAstPropertyDoc, the loop over comment tags for
`@a2uiSchema` currently lets later tags overwrite schemaOverride silently; update
getAstPropertyDoc so it detects multiple `@a2uiSchema` tags and either (a) stops
after the first tag by breaking once schemaOverride is set, or (b) throws a
clear error when a second `@a2uiSchema` is encountered; locate the tag iteration
in getAstPropertyDoc (where schemaOverride is assigned) and add the
duplicate-detection logic and an early break or throw to prevent silent
overwrites.

In `@packages/genui/a2ui-catalog-extractor/src/extractor.ts`:
- Around line 1041-1079: The catch in checkCatalogFiles currently treats any
fs.readFile failure as "missing"; change the error handling inside the for loop
around await fs.readFile(file.path, ...) so that you only push file.path into
the missing array when the error code is ENOENT, and rethrow (or propagate) any
other errors (e.g., permission/EISDIR/EIO) so they surface instead of being
misclassified; keep all other logic (mismatched, actual/expected assignment, and
returned CheckCatalogFilesResult) unchanged.
- Around line 1008-1025: The function renderCatalogFiles currently assumes
result.catalog exists when result.format === 'a2ui-catalog' and will emit the
literal "undefined" if it's missing; update renderCatalogFiles to validate that
result.catalog is defined when format is 'a2ui-catalog' and throw a clear,
fast-failing Error (or return a rejected Promise if async) otherwise; reference
the symbols result, renderCatalogFiles, ExtractCatalogResult and catalog in the
error message so callers know they passed an invalid ExtractCatalogResult; keep
behavior for non-'a2ui-catalog' paths unchanged.
🪄 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: 5983757f-cda3-4a44-bad8-95c2a3d1f268

📥 Commits

Reviewing files that changed from the base of the PR and between 6c5eef3 and d46a6b6.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (24)
  • .github/a2ui-catalog-extractor.instructions.md
  • packages/genui/a2ui-catalog-extractor/SKILL.md
  • packages/genui/a2ui-catalog-extractor/references/tsdoc-mapping.md
  • packages/genui/a2ui-catalog-extractor/src/cli.ts
  • packages/genui/a2ui-catalog-extractor/src/docs.ts
  • packages/genui/a2ui-catalog-extractor/src/extractor.ts
  • packages/genui/a2ui-catalog-extractor/src/types.ts
  • packages/genui/a2ui-catalog-extractor/test/cli.test.ts
  • packages/genui/a2ui-catalog-extractor/test/extract-catalog.test.ts
  • packages/genui/a2ui-catalog-extractor/test/fixtures/jsx-loose/catalog/Loose/index.jsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/jsx-loose/tsconfig.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-default/catalog/UndefinedDefault/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-default/tsconfig.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-export-default/catalog/DefaultExport/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-export-default/tsconfig.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-export-named/catalog/NamedExport/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-export-named/tsconfig.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-json/catalog/BrokenJson/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-json/tsconfig.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-schema-type/catalog/BrokenSchemaType/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-schema-type/tsconfig.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-type/catalog/Fancy/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-type/tsconfig.json
  • packages/genui/a2ui/turbo.json
✅ Files skipped from review due to trivial changes (17)
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-type/catalog/Fancy/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-default/catalog/UndefinedDefault/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-export-named/tsconfig.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/jsx-loose/catalog/Loose/index.jsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-export-default/tsconfig.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-json/tsconfig.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-export-named/catalog/NamedExport/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-schema-type/tsconfig.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-schema-type/catalog/BrokenSchemaType/index.tsx
  • packages/genui/a2ui/turbo.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-export-default/catalog/DefaultExport/index.tsx
  • packages/genui/a2ui-catalog-extractor/SKILL.md
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-json/catalog/BrokenJson/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-type/tsconfig.json
  • .github/a2ui-catalog-extractor.instructions.md
  • packages/genui/a2ui-catalog-extractor/references/tsdoc-mapping.md
  • packages/genui/a2ui-catalog-extractor/test/fixtures/jsx-loose/tsconfig.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/genui/a2ui-catalog-extractor/test/cli.test.ts
  • packages/genui/a2ui-catalog-extractor/test/extract-catalog.test.ts

Comment on lines +114 to +120
if (ts.isExportDeclaration(statement)) {
throw new Error(
`Unsupported component export in ${sourceFile.fileName}: re-exports like "${
statement.getText(sourceFile)
}" are not supported. Use a direct exported function or const declaration instead.`,
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Blanket ExportDeclaration rejection also blocks type-only re-exports.

ts.isExportDeclaration matches every export { ... } [from '...'] and export * from '...' — including type-only re-exports such as export type { ButtonProps } from './Button.tsx';. If an index.tsx just happens to re-export a type alongside the component definition, extraction will throw before the component is even considered. Consider narrowing the rejection to export declarations that contribute value bindings (skip isTypeOnly exports and pure export * side-effect statements that don't introduce component names).

🔧 Suggested narrowing
-    if (ts.isExportDeclaration(statement)) {
+    if (ts.isExportDeclaration(statement) && !statement.isTypeOnly) {
       throw new Error(
         `Unsupported component export in ${sourceFile.fileName}: re-exports like "${
           statement.getText(sourceFile)
         }" are not supported. Use a direct exported function or const declaration instead.`,
       );
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui-catalog-extractor/src/extractor.ts` around lines 114 -
120, The current blanket rejection of ts.isExportDeclaration blocks type-only
and star re-exports; change the condition so you only throw for export
declarations that introduce value bindings: check
ts.isExportDeclaration(statement) AND that statement.isTypeOnly is false AND
that statement.exportClause is present (meaning it's a named re-export of
values), then throw the Error using the existing message (reference symbols:
ts.isExportDeclaration, ExportDeclaration, statement.isTypeOnly,
statement.exportClause, statement.getText(sourceFile), sourceFile.fileName).

Comment on lines +515 to +532
function collectInterfaceProperties(
declaration: ts.InterfaceDeclaration,
context: SourceContext,
parseState: ParseState,
): CollectedTypeElements {
const typeDocRecord = getTypeDocRecord(
context.typeDocIndex,
context.filePath,
declaration.name.text,
);

return collectTypeElementProperties(
declaration.members,
context,
parseState,
typeDocRecord,
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether any catalog component props type uses `extends` so the impact can be assessed.
rg -nP --type=ts -C2 '\binterface\s+\w+Props\b[^{]*\bextends\b' packages/genui/a2ui/src

Repository: lynx-family/lynx-stack

Length of output: 4514


🏁 Script executed:

#!/bin/bash
# First, let's look at the function in question and surrounding context
head -n 600 packages/genui/a2ui-catalog-extractor/src/extractor.ts | tail -n 150

Repository: lynx-family/lynx-stack

Length of output: 4005


🏁 Script executed:

#!/bin/bash
# Search for how heritage clauses might be handled
rg -n "heritage|extends|baseTypes" packages/genui/a2ui-catalog-extractor/src/extractor.ts

Repository: lynx-family/lynx-stack

Length of output: 48


🏁 Script executed:

#!/bin/bash
# Find GenericComponentProps definition
rg -n "GenericComponentProps" packages/genui/a2ui/src --type=ts

Repository: lynx-family/lynx-stack

Length of output: 2448


🏁 Script executed:

#!/bin/bash
# Check if there's any special handling for interface inheritance
rg -n "heritageClause|getBaseTypes" packages/genui/a2ui-catalog-extractor/src/

Repository: lynx-family/lynx-stack

Length of output: 48


🏁 Script executed:

#!/bin/bash
# Read GenericComponentProps definition to see what's being missed
cat -n packages/genui/a2ui/src/core/types.ts | sed -n '77,120p'

Repository: lynx-family/lynx-stack

Length of output: 364


🏁 Script executed:

#!/bin/bash
# Verify that declaration.members is the only source being used - check collectTypeElementProperties signature
rg -n "function collectTypeElementProperties|const collectTypeElementProperties" packages/genui/a2ui-catalog-extractor/src/extractor.ts -A 5

Repository: lynx-family/lynx-stack

Length of output: 279


🏁 Script executed:

#!/bin/bash
# Check the full collectTypeElementProperties function to see what it does
rg -n "function collectTypeElementProperties" packages/genui/a2ui-catalog-extractor/src/extractor.ts -A 40

Repository: lynx-family/lynx-stack

Length of output: 1349


🏁 Script executed:

#!/bin/bash
# Check if there's any handling of heritageClause in collectInterfaceProperties or nearby
rg -n "heritageClause|heritage" packages/genui/a2ui-catalog-extractor/src/extractor.ts

Repository: lynx-family/lynx-stack

Length of output: 48


🏁 Script executed:

#!/bin/bash
# Check if InterfaceDeclaration.members includes inherited members or if heritageClause is separate
rg -n "declaration.members|heritageClause" packages/genui/a2ui-catalog-extractor/src/extractor.ts

Repository: lynx-family/lynx-stack

Length of output: 95


🏁 Script executed:

#!/bin/bash
# Confirm TypeScript InterfaceDeclaration structure - check if members includes heritage
# Let's look for documentation or understanding of how heritageClauses work
rg -n "InterfaceDeclaration" packages/genui/a2ui-catalog-extractor/src/extractor.ts -B 2 -A 2

Repository: lynx-family/lynx-stack

Length of output: 504


🏁 Script executed:

#!/bin/bash
# Check if any of the 10 affected components have their own properties defined
# to understand what's being captured vs what's missing
cat -n packages/genui/a2ui/src/catalog/Text/index.tsx | sed -n '8,20p'

Repository: lynx-family/lynx-stack

Length of output: 479


🏁 Script executed:

#!/bin/bash
# Check TypeScript documentation behavior - verify declaration.members doesn't include heritage
# Let's see if there are any test files or examples showing how heritage is handled
find packages/genui/a2ui-catalog-extractor -name "*.test.ts" -o -name "*.spec.ts" | head -5

Repository: lynx-family/lynx-stack

Length of output: 188


🏁 Script executed:

#!/bin/bash
# Check if there are any tests for collectInterfaceProperties that show expected behavior
rg -n "collectInterfaceProperties" packages/genui/a2ui-catalog-extractor --type=ts -B 2 -A 2

Repository: lynx-family/lynx-stack

Length of output: 915


🏁 Script executed:

#!/bin/bash
# Let's verify: does the generated schema for TextProps include 'surface' from GenericComponentProps?
# Find any generated schemas or test outputs
find packages/genui/a2ui-catalog-extractor -name "*.json" -o -name "*schema*" | head -10

Repository: lynx-family/lynx-stack

Length of output: 860


🏁 Script executed:

#!/bin/bash
# Check one of the legacy baseline catalogs to see if base properties are captured
cat packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/Text/catalog.json 2>/dev/null || cat packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/Button/catalog.json

Repository: lynx-family/lynx-stack

Length of output: 787


🏁 Script executed:

#!/bin/bash
# Check the test file to see if it validates that base properties are present
rg -n "surface|id|setValue|GenericComponentProps" packages/genui/a2ui-catalog-extractor/test/extract-catalog.test.ts -B 2 -A 2

Repository: lynx-family/lynx-stack

Length of output: 2482


🏁 Script executed:

#!/bin/bash
# Verify the issue by checking another baseline catalog
cat packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/Button/catalog.json

Repository: lynx-family/lynx-stack

Length of output: 1811


🏁 Script executed:

#!/bin/bash
# Search for any heritage clause processing in the codebase
rg -n "heritageClause" packages/genui/a2ui-catalog-extractor/src/ -A 3

Repository: lynx-family/lynx-stack

Length of output: 48


🏁 Script executed:

#!/bin/bash
# Check if there's any handling in parseNamedDeclaration for resolving base interfaces
rg -n "getBaseTypeOfLiteralType|getResolvedType|resolveTypeReference" packages/genui/a2ui-catalog-extractor/src/extractor.ts

Repository: lynx-family/lynx-stack

Length of output: 48


🏁 Script executed:

#!/bin/bash
# Let's check the test file to see if there's any test specifically for inherited interfaces
rg -n "extends|inherit" packages/genui/a2ui-catalog-extractor/test/extract-catalog.test.ts -B 2 -A 2

Repository: lynx-family/lynx-stack

Length of output: 48


🏁 Script executed:

#!/bin/bash
# Check if GenericComponentProps is defined as an interface and is it in localDeclarations
rg -n "GenericComponentProps" packages/genui/a2ui-catalog-extractor/test/ -l

Repository: lynx-family/lynx-stack

Length of output: 145


🏁 Script executed:

#!/bin/bash
# Check the full TextProps baseline to confirm what's included
cat packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/Text/catalog.json

Repository: lynx-family/lynx-stack

Length of output: 787


🏁 Script executed:

#!/bin/bash
# Verify that GenericComponentProps is NOT being resolved - check if there's a test fixture with it
find packages/genui/a2ui-catalog-extractor/test/fixtures -name "*.ts" -o -name "*.tsx" | xargs grep -l "extends.*Props" | head -3

Repository: lynx-family/lynx-stack

Length of output: 145


🏁 Script executed:

#!/bin/bash
# Check the Chip component in test fixtures to see if it extends something
cat packages/genui/a2ui-catalog-extractor/test/fixtures/tsx/catalog/Chip/index.tsx

Repository: lynx-family/lynx-stack

Length of output: 773


🏁 Script executed:

#!/antml:
# Check if there's a corresponding baseline for Chip
cat packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/Chip/catalog.json 2>/dev/null || echo "No Chip baseline found"

Repository: lynx-family/lynx-stack

Length of output: 89


🏁 Script executed:

#!/bin/bash
# Search for any test case that validates inheritance or base types
rg -n "extends|base|inherit" packages/genui/a2ui-catalog-extractor/test/extract-catalog.test.ts -C 3

Repository: lynx-family/lynx-stack

Length of output: 393


🏁 Script executed:

#!/bin/bash
# Check the test to see if it validates schemas against the baselines
cat packages/genui/a2ui-catalog-extractor/test/extract-catalog.test.ts | sed -n '190,220p'

Repository: lynx-family/lynx-stack

Length of output: 1016


🏁 Script executed:

#!/bin/bash
# Verify that GenericComponentProps properties (id, surface, sendAction) are NOT in any baseline
rg -n '"id"|"surface"|"sendAction"' packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/*/catalog.json

Repository: lynx-family/lynx-stack

Length of output: 48


🏁 Script executed:

#!/bin/bash
# Check what files are in the test fixtures that are being extracted
ls -la packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/

Repository: lynx-family/lynx-stack

Length of output: 734


🏁 Script executed:

#!/bin/bash
# Final confirmation: Check TypeScript's InterfaceDeclaration structure
# to confirm declaration.members does NOT include inherited properties
python3 << 'EOF'
# TypeScript fact: InterfaceDeclaration.members contains only direct members,
# not inherited members. Heritage clauses are in declaration.heritageClauses
# Let's verify by examining the code flow

import re

# Read extractor.ts to trace the flow
with open('packages/genui/a2ui-catalog-extractor/src/extractor.ts', 'r') as f:
    content = f.read()
    
# Find collectInterfaceProperties function
match = re.search(r'function collectInterfaceProperties\([^)]*\)[^{]*\{[^}]*\}', content, re.DOTALL)
if match:
    func = match.group(0)
    print("collectInterfaceProperties implementation:")
    print(func[:500])
    print("\n--- KEY OBSERVATION ---")
    if 'declaration.members' in func and 'heritageClause' not in func:
        print("✓ CONFIRMED: Uses declaration.members WITHOUT heritageClause handling")
        print("✓ This means inherited properties from 'extends' clauses are NOT processed")
    if 'declaration.heritageClauses' not in func:
        print("✓ CONFIRMED: No access to heritageClauses at all")
EOF

Repository: lynx-family/lynx-stack

Length of output: 726


🏁 Script executed:

#!/bin/bash
# One final check: does the test fixture have GenericComponentProps defined in the same file?
# If so, the inheritance could be resolved locally
grep -n "interface GenericComponentProps" packages/genui/a2ui-catalog-extractor/test/fixtures/tsx/catalog/Chip/index.tsx

Repository: lynx-family/lynx-stack

Length of output: 102


Heritage clause properties from extended interfaces are silently omitted from extracted schemas.

collectInterfaceProperties uses only declaration.members, which excludes inherited properties from interface heritage clauses. All 10 catalog components extend GenericComponentProps (which defines id, surface, setValue, sendAction, dataContextPath), yet these base properties never appear in the generated catalog schemas. The extractor produces incomplete, silent failures with no diagnostic.

Options:

  • Resolve each heritage clause via context.localDeclarations, recursively collect inherited members through parseNamedDeclaration, and merge with direct members (respecting seen to prevent cycles).
  • If unresolved, throw a clear error naming the base type so authors either simplify or add an @a2uiSchema override.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui-catalog-extractor/src/extractor.ts` around lines 515 -
532, collectInterfaceProperties currently only uses declaration.members and thus
omits inherited properties from heritage clauses; update it to resolve each
heritage clause type via context.localDeclarations and for each resolved base
declaration call parseNamedDeclaration (or recursively call
collectInterfaceProperties) to collect and merge inherited members into the
CollectedTypeElements before calling collectTypeElementProperties, honoring the
parseState.seen set to avoid cycles; if a heritage clause cannot be resolved
through context.localDeclarations, throw a clear error naming the unresolved
base type to prompt an `@a2uiSchema` override or simplification.

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

♻️ Duplicate comments (2)
packages/genui/a2ui-catalog-extractor/src/extractor.ts (2)

516-533: ⚠️ Potential issue | 🟠 Major

Heritage clause properties from extended interfaces are still silently omitted.

collectInterfaceProperties only reads declaration.members, so inherited members from extends clauses are dropped. All 10 A2UI catalog components extend GenericComponentProps; the extractor silently produces schemas missing any props declared on the base interface (e.g. id, surface, setValue, sendAction, dataContextPath). Filtering GENERIC_PROPS in buildObjectSchema at Line 378 confirms inheritance was intended to be processed — but the walk never actually pulls in the base-type members. The golden-parity tests only cover what the baseline already contains, so this regresses quietly.

Resolve each declaration.heritageClauses[*].types through context.localDeclarations, recursively parse via parseNamedDeclaration (honoring parseState.seen to prevent cycles), and merge members with direct ones before collectTypeElementProperties. If a base type cannot be resolved locally, throw a clear error naming the unresolved base so authors can add an @a2uiSchema override or flatten the declaration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui-catalog-extractor/src/extractor.ts` around lines 516 -
533, collectInterfaceProperties currently only uses declaration.members so
inherited props from declaration.heritageClauses are dropped; update
collectInterfaceProperties to iterate declaration.heritageClauses[*].types,
resolve each type via context.localDeclarations, and for each resolved
declaration call parseNamedDeclaration (respecting parseState.seen to avoid
cycles) to obtain its members, then merge those inherited members with the
direct declaration.members before calling collectTypeElementProperties; if a
heritage type cannot be resolved locally, throw a clear error naming the
unresolved base type so callers can add an `@a2uiSchema` override or flatten the
declaration (this aligns with buildObjectSchema's GENERIC_PROPS filtering
expectations).

115-121: ⚠️ Potential issue | 🟡 Minor

Blanket ExportDeclaration rejection still blocks type-only and side-effect re-exports.

ts.isExportDeclaration matches every export { ... } [from '...'] and export * from '...', including type-only re-exports (e.g. export type { ButtonProps } from './Button.tsx';). An index.tsx that re-exports a prop type alongside its component definition will throw before the component is ever considered. Narrow the rejection to value re-exports only.

🔧 Suggested narrowing
-    if (ts.isExportDeclaration(statement)) {
+    if (ts.isExportDeclaration(statement) && !statement.isTypeOnly && statement.exportClause) {
       throw new Error(
         `Unsupported component export in ${sourceFile.fileName}: re-exports like "${
           statement.getText(sourceFile)
         }" are not supported. Use a direct exported function or const declaration instead.`,
       );
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui-catalog-extractor/src/extractor.ts` around lines 115 -
121, The current blanket ts.isExportDeclaration check rejects all export
declarations including type-only re-exports; change it to only throw for value
re-exports by checking statement.moduleSpecifier and that the export is not
type-only. Concretely, update the condition to something like: if
(ts.isExportDeclaration(statement) && statement.moduleSpecifier &&
!statement.isTypeOnly) { throw new Error(...) } so that type-only exports
(statement.isTypeOnly) and local export clauses are allowed, while cross-module
value re-exports still produce the error referencing sourceFile.fileName and
statement.getText(sourceFile).
🧹 Nitpick comments (2)
packages/genui/a2ui-catalog-extractor/src/extractor.ts (2)

399-414: Minor code-smell: unreachable branch and redundant allocations in buildObjectSchema.

A couple of small cleanups in this block:

  • The else if (required.length > 0) branch at Line 412 is unreachable. required is only appended inside the loop over properties, which also populates propertyMap; so propertyNames.length === 0 implies required.length === 0.
  • The schema object initialized at Line 372 is never used by the top-level return path (Line 399-404) other than as a staging area for schema.properties. For topLevel, you can return { properties: propertyMap, required } directly.
  • additionalProperties at Line 375 is just an alias for explicitAdditionalProperties; using the parameter directly removes the indirection.

Not behavior-affecting; purely readability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui-catalog-extractor/src/extractor.ts` around lines 399 -
414, In buildObjectSchema: remove the unreachable else-if and redundant
allocations by returning directly for the topLevel case using the constructed
propertyMap and required array (i.e., return { properties: propertyMap, required
}) instead of relying on the pre-initialized schema object; eliminate the
useless alias additionalProperties and use explicitAdditionalProperties
directly; also drop the unreachable branch that checks required.length > 0 after
propertyNames is empty and simplify the logic that assigns schema.required when
propertyNames.length > 0 to assign required onto schema (or the returned object)
only when there are properties.

805-832: getComponentEntryFiles does serial fs.stat per candidate; consider using readdir on each subdirectory instead.

For each subdirectory you probe up to 4 filenames via fs.stat inside a loop. A single fs.readdir(path.join(sourceDir, entry.name)) returns the directory listing in one syscall and avoids the try/catch-per-candidate dance. Given catalog sizes are small this isn't a hot path, but the current shape couples correctness to EXT permissions (silent catches swallow non-ENOENT errors like EACCES, which would then look like "component has no entry file" rather than a permission problem).

🔧 Sketch
   for (const entry of directoryEntries.sort((l, r) => l.name.localeCompare(r.name))) {
     if (!entry.isDirectory()) continue;
-    for (const candidate of indexCandidates) {
-      const candidatePath = path.join(sourceDir, entry.name, candidate);
-      try {
-        const stats = await fs.stat(candidatePath);
-        if (stats.isFile()) {
-          entryFiles.push(candidatePath);
-          break;
-        }
-      } catch {
-        // Ignore missing entry candidates.
-      }
-    }
+    const dirPath = path.join(sourceDir, entry.name);
+    const children = new Set(
+      (await fs.readdir(dirPath, { withFileTypes: true }))
+        .filter(c => c.isFile())
+        .map(c => c.name),
+    );
+    const match = indexCandidates.find(c => children.has(c));
+    if (match) entryFiles.push(path.join(dirPath, match));
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui-catalog-extractor/src/extractor.ts` around lines 805 -
832, The function getComponentEntryFiles currently stats up to 4 candidate
filenames per subdirectory and swallows all errors; replace that inner loop by
performing a single await fs.readdir(path.join(sourceDir, entry.name)) to get
the subdirectory listing, check that listing for any of the indexCandidates
(index.tsx, index.jsx, index.ts, index.js) and when found push the joined
candidate path to entryFiles (optionally verify it's a file with one fs.stat if
needed). When calling fs.readdir for a subdirectory, only ignore ENOENT
(directory missing) — rethrow other errors (e.g., EACCES) instead of silently
swallowing them so permission issues surface.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/genui/a2ui-catalog-extractor/src/extractor.ts`:
- Around line 685-719: The union handling branch for ts.isUnionTypeNode filters
out null/undefined into actualTypes but doesn't handle the case when all members
were null/undefined, which leads to returning an empty oneOf; add a guard right
after computing actualTypes (check actualTypes.length === 0) and return a valid
schema (e.g., { type: 'null' }) so consumers don't receive { oneOf: [] }; update
the block around actualTypes, parseTypeNode and dedupeSchemas to place this
early-return before the later checks that build enums/oneOf.

---

Duplicate comments:
In `@packages/genui/a2ui-catalog-extractor/src/extractor.ts`:
- Around line 516-533: collectInterfaceProperties currently only uses
declaration.members so inherited props from declaration.heritageClauses are
dropped; update collectInterfaceProperties to iterate
declaration.heritageClauses[*].types, resolve each type via
context.localDeclarations, and for each resolved declaration call
parseNamedDeclaration (respecting parseState.seen to avoid cycles) to obtain its
members, then merge those inherited members with the direct declaration.members
before calling collectTypeElementProperties; if a heritage type cannot be
resolved locally, throw a clear error naming the unresolved base type so callers
can add an `@a2uiSchema` override or flatten the declaration (this aligns with
buildObjectSchema's GENERIC_PROPS filtering expectations).
- Around line 115-121: The current blanket ts.isExportDeclaration check rejects
all export declarations including type-only re-exports; change it to only throw
for value re-exports by checking statement.moduleSpecifier and that the export
is not type-only. Concretely, update the condition to something like: if
(ts.isExportDeclaration(statement) && statement.moduleSpecifier &&
!statement.isTypeOnly) { throw new Error(...) } so that type-only exports
(statement.isTypeOnly) and local export clauses are allowed, while cross-module
value re-exports still produce the error referencing sourceFile.fileName and
statement.getText(sourceFile).

---

Nitpick comments:
In `@packages/genui/a2ui-catalog-extractor/src/extractor.ts`:
- Around line 399-414: In buildObjectSchema: remove the unreachable else-if and
redundant allocations by returning directly for the topLevel case using the
constructed propertyMap and required array (i.e., return { properties:
propertyMap, required }) instead of relying on the pre-initialized schema
object; eliminate the useless alias additionalProperties and use
explicitAdditionalProperties directly; also drop the unreachable branch that
checks required.length > 0 after propertyNames is empty and simplify the logic
that assigns schema.required when propertyNames.length > 0 to assign required
onto schema (or the returned object) only when there are properties.
- Around line 805-832: The function getComponentEntryFiles currently stats up to
4 candidate filenames per subdirectory and swallows all errors; replace that
inner loop by performing a single await fs.readdir(path.join(sourceDir,
entry.name)) to get the subdirectory listing, check that listing for any of the
indexCandidates (index.tsx, index.jsx, index.ts, index.js) and when found push
the joined candidate path to entryFiles (optionally verify it's a file with one
fs.stat if needed). When calling fs.readdir for a subdirectory, only ignore
ENOENT (directory missing) — rethrow other errors (e.g., EACCES) instead of
silently swallowing them so permission issues surface.
🪄 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: 92d2e2bd-0196-4511-8bba-cd381324c0a4

📥 Commits

Reviewing files that changed from the base of the PR and between d46a6b6 and d8ea753.

📒 Files selected for processing (4)
  • packages/genui/a2ui-catalog-extractor/rslib.config.ts
  • packages/genui/a2ui-catalog-extractor/src/cli.ts
  • packages/genui/a2ui-catalog-extractor/src/extractor.ts
  • packages/genui/a2ui-catalog-extractor/src/types.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/genui/a2ui-catalog-extractor/rslib.config.ts
  • packages/genui/a2ui-catalog-extractor/src/types.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/genui/a2ui-catalog-extractor/src/cli.ts

Comment on lines +685 to +719
if (ts.isUnionTypeNode(typeNode)) {
const actualTypes = typeNode.types.filter(type =>
!isNullLikeTypeNode(type) && !isUndefinedLikeTypeNode(type)
);

if (
actualTypes.length === 2
&& actualTypes.every(type => isBooleanLiteralTypeNode(type))
) {
return { type: 'boolean' };
}

if (actualTypes.length === 1) {
return parseTypeNode(actualTypes[0]!, context, parseState);
}

if (
actualTypes.length > 0
&& actualTypes.every(type => isStringLiteralTypeNode(type))
) {
return {
type: 'string',
enum: actualTypes.map(type => (type.literal as ts.StringLiteral).text),
};
}

const schemas = dedupeSchemas(
actualTypes.map(type => parseTypeNode(type, context, parseState)),
);
if (schemas.length === 1) {
return schemas[0]!;
}

return { oneOf: schemas };
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Pure null | undefined union yields malformed { oneOf: [] }.

After filtering null/undefined at Line 686-688, if every union member was null/undefined the code falls through all branches and returns { oneOf: [] } — an invalid JSON Schema that consumers will silently accept. Unlikely in practice for component props, but cheap to guard:

🔧 Proposed guard
     const schemas = dedupeSchemas(
       actualTypes.map(type => parseTypeNode(type, context, parseState)),
     );
     if (schemas.length === 1) {
       return schemas[0]!;
     }
+    if (schemas.length === 0) {
+      throw new Error(
+        `Union "${typeNode.getText(context.sourceFile)}" in ${context.filePath} has no representable members.`,
+      );
+    }
 
     return { oneOf: schemas };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui-catalog-extractor/src/extractor.ts` around lines 685 -
719, The union handling branch for ts.isUnionTypeNode filters out null/undefined
into actualTypes but doesn't handle the case when all members were
null/undefined, which leads to returning an empty oneOf; add a guard right after
computing actualTypes (check actualTypes.length === 0) and return a valid schema
(e.g., { type: 'null' }) so consumers don't receive { oneOf: [] }; update the
block around actualTypes, parseTypeNode and dedupeSchemas to place this
early-return before the later checks that build enums/oneOf.

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 24, 2026

Merging this PR will improve performance by 17.3%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

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

Performance Changes

Benchmark BASE HEAD Efficiency
transform 1000 view elements 46.8 ms 39.9 ms +17.3%

Comparing PupilTong:codex/a2ui-catalog-extractor (a5186af) with main (7e5f54c)2

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.

  2. No successful run was found on main (27d5872) during the generation of this report, so 7e5f54c was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@relativeci
Copy link
Copy Markdown

relativeci Bot commented Apr 24, 2026

React Example

#7637 Bundle Size — 225.38KiB (0%).

a5186af(current) vs 7e5f54c main#7632(baseline)

Bundle metrics  no changes
                 Current
#7637
     Baseline
#7632
No change  Initial JS 0B 0B
No change  Initial CSS 0B 0B
Change  Cache Invalidation 0% 35.31%
No change  Chunks 0 0
No change  Assets 4 4
No change  Modules 179 179
No change  Duplicate Modules 69 69
No change  Duplicate Code 44.57% 44.57%
No change  Packages 2 2
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#7637
     Baseline
#7632
No change  IMG 145.76KiB 145.76KiB
No change  Other 79.63KiB 79.63KiB

Bundle analysis reportBranch PupilTong:codex/a2ui-catalog-ext...Project dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci Bot commented Apr 24, 2026

Web Explorer

#9210 Bundle Size — 900.02KiB (0%).

a5186af(current) vs 7e5f54c main#9205(baseline)

Bundle metrics  Change 2 changes
                 Current
#9210
     Baseline
#9205
No change  Initial JS 44.46KiB 44.46KiB
No change  Initial CSS 2.22KiB 2.22KiB
No change  Cache Invalidation 0% 0%
No change  Chunks 9 9
No change  Assets 11 11
Change  Modules 230(+0.88%) 228
No change  Duplicate Modules 11 11
Change  Duplicate Code 27.28%(-0.04%) 27.29%
No change  Packages 10 10
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#9210
     Baseline
#9205
No change  JS 495.88KiB 495.88KiB
No change  Other 401.92KiB 401.92KiB
No change  CSS 2.22KiB 2.22KiB

Bundle analysis reportBranch PupilTong:codex/a2ui-catalog-ext...Project dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci Bot commented Apr 24, 2026

React MTF Example

#769 Bundle Size — 196.54KiB (0%).

a5186af(current) vs 7e5f54c main#764(baseline)

Bundle metrics  no changes
                 Current
#769
     Baseline
#764
No change  Initial JS 0B 0B
No change  Initial CSS 0B 0B
Change  Cache Invalidation 0% 43.38%
No change  Chunks 0 0
No change  Assets 3 3
No change  Modules 173 173
No change  Duplicate Modules 66 66
No change  Duplicate Code 44.08% 44.08%
No change  Packages 2 2
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#769
     Baseline
#764
No change  IMG 111.23KiB 111.23KiB
No change  Other 85.31KiB 85.31KiB

Bundle analysis reportBranch PupilTong:codex/a2ui-catalog-ext...Project dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci Bot commented Apr 24, 2026

React External

#754 Bundle Size — 680.27KiB (0%).

a5186af(current) vs 7e5f54c main#749(baseline)

Bundle metrics  no changes
                 Current
#754
     Baseline
#749
No change  Initial JS 0B 0B
No change  Initial CSS 0B 0B
Change  Cache Invalidation 0% 39.69%
No change  Chunks 0 0
No change  Assets 3 3
No change  Modules 17 17
No change  Duplicate Modules 5 5
No change  Duplicate Code 8.59% 8.59%
No change  Packages 0 0
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#754
     Baseline
#749
No change  Other 680.27KiB 680.27KiB

Bundle analysis reportBranch PupilTong:codex/a2ui-catalog-ext...Project dashboard


Generated by RelativeCIDocumentationReport issue

@PupilTong PupilTong self-assigned this Apr 24, 2026
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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/genui/a2ui-catalog-extractor/src/extractor.ts`:
- Around line 282-308: The GENERIC_PROPS filter in buildObjectSchema is applied
unconditionally and thus removes framework-injected names from nested object
shapes; change the loop so the check uses the topLevel flag (i.e., only skip
properties when topLevel is true and GENERIC_PROPS.has(property.name)), keeping
property processing identical otherwise (still building propertyMap, required,
etc.); update references to property.name, GENERIC_PROPS, and topLevel in the
for loop that iterates over properties to scope the filter to top-level
component schemas.

In `@packages/genui/a2ui/src/catalog/Button/index.tsx`:
- Around line 9-14: The TSDoc block currently above ButtonActionContextBinding
is describing ButtonProps and will be picked up for the wrong symbol; remove the
existing doc comment that precedes the ButtonActionContextBinding interface and
place that exact comment immediately above the exported ButtonProps interface
declaration so ButtonProps retains the correct summary; ensure the comment text
remains unchanged and that it is directly adjacent to the ButtonProps interface
(exported symbol name: ButtonProps) to be picked up by the TypeDoc extractor.
🪄 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: 128a719d-b180-4412-a731-1f2cc71c1737

📥 Commits

Reviewing files that changed from the base of the PR and between d8ea753 and aeb86cb.

📒 Files selected for processing (11)
  • .github/a2ui-catalog-extractor.instructions.md
  • packages/genui/a2ui-catalog-extractor/README.md
  • packages/genui/a2ui-catalog-extractor/SKILL.md
  • packages/genui/a2ui-catalog-extractor/references/tsdoc-mapping.md
  • packages/genui/a2ui-catalog-extractor/src/docs.ts
  • packages/genui/a2ui-catalog-extractor/src/extractor.ts
  • packages/genui/a2ui-catalog-extractor/src/types.ts
  • packages/genui/a2ui-catalog-extractor/test/extract-catalog.test.ts
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-complex/catalog/ActionButton/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-complex/tsconfig.json
  • packages/genui/a2ui/src/catalog/Button/index.tsx
✅ Files skipped from review due to trivial changes (4)
  • packages/genui/a2ui-catalog-extractor/references/tsdoc-mapping.md
  • packages/genui/a2ui-catalog-extractor/README.md
  • packages/genui/a2ui-catalog-extractor/SKILL.md
  • packages/genui/a2ui-catalog-extractor/src/types.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • .github/a2ui-catalog-extractor.instructions.md
  • packages/genui/a2ui-catalog-extractor/test/extract-catalog.test.ts

Comment on lines +282 to +308
function buildObjectSchema(
properties: PropertyDefinition[],
context: SourceContext,
parseState: ParseState,
topLevel: boolean,
typeDocRecord?: TypeDocRecord,
explicitAdditionalProperties?: JsonSchema | boolean,
): JsonSchema | ComponentSchema {
const schema: JsonSchema = topLevel ? {} : { type: 'object' };
const propertyMap: Record<string, JsonSchema> = {};
const required: string[] = [];
const additionalProperties = explicitAdditionalProperties;

for (const property of properties) {
if (GENERIC_PROPS.has(property.name)) continue;

const doc = property.doc ?? typeDocRecord?.properties.get(property.name);
const propertySchema = applyPropertyDoc(
parseTypeNode(property.typeNode, context, parseState),
doc,
);

propertyMap[property.name] = propertySchema;
if (!property.optional) {
required.push(property.name);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate GENERIC_PROPS definition and surrounding context to confirm which names are silently filtered.
rg -nP --type=ts -C3 '\bGENERIC_PROPS\b'

Repository: lynx-family/lynx-stack

Length of output: 1969


🏁 Script executed:

#!/bin/bash
# Get complete GENERIC_PROPS definition
cat -n packages/genui/a2ui-catalog-extractor/src/types.ts | head -20

Repository: lynx-family/lynx-stack

Length of output: 791


🏁 Script executed:

#!/bin/bash
# Find all call sites of buildObjectSchema with context
rg -n 'buildObjectSchema' packages/genui/a2ui-catalog-extractor/src/extractor.ts | head -20

Repository: lynx-family/lynx-stack

Length of output: 325


🏁 Script executed:

#!/bin/bash
# Check context around each buildObjectSchema call site to see topLevel parameter values
for line in 397 479 506 654 892; do
  echo "=== Call at line $line ==="
  sed -n "$((line-2)),$((line+2))p" packages/genui/a2ui-catalog-extractor/src/extractor.ts
done

Repository: lynx-family/lynx-stack

Length of output: 739


🏁 Script executed:

#!/bin/bash
# Get wider context around each buildObjectSchema call to see topLevel parameter
for line in 397 479 506 654 892; do
  echo "=== Call at line $line ==="
  sed -n "$((line-3)),$((line+5))p" packages/genui/a2ui-catalog-extractor/src/extractor.ts
  echo ""
done

Repository: lynx-family/lynx-stack

Length of output: 1252


🏁 Script executed:

#!/bin/bash
# Get function context for each call - look for 'function' or similar keywords above each line
for line in 397 479 506 654 892; do
  echo "=== Function containing call at line $line ==="
  awk -v start=1 -v end=$line 'NR<=end {buffer[NR%50]=$0} END {for (i=start; i<=end; i++) print buffer[i%50]}' packages/genui/a2ui-catalog-extractor/src/extractor.ts | grep -E '(^function |^type |^interface |^export |^  return buildObjectSchema)' | tail -3
  echo ""
done

Repository: lynx-family/lynx-stack

Length of output: 571


🏁 Script executed:

#!/bin/bash
# Better approach: find the function definition by searching backwards
sed -n '1,397p' packages/genui/a2ui-catalog-extractor/src/extractor.ts | tail -50 | head -40

Repository: lynx-family/lynx-stack

Length of output: 1241


🏁 Script executed:

#!/bin/bash
# Find where parseSyntheticTypedef is called
rg -n 'parseSyntheticTypedef' packages/genui/a2ui-catalog-extractor/src/extractor.ts -B2 -A2

Repository: lynx-family/lynx-stack

Length of output: 306


🏁 Script executed:

#!/bin/bash
# Check what functions call buildObjectSchema at lines 479, 506
# First, let's find function definitions around those lines
sed -n '450,510p' packages/genui/a2ui-catalog-extractor/src/extractor.ts | head -60

Repository: lynx-family/lynx-stack

Length of output: 1690


🏁 Script executed:

#!/bin/bash
# Find all calls to parseNamedDeclaration and their topLevel parameter
rg -n 'parseNamedDeclaration' packages/genui/a2ui-catalog-extractor/src/extractor.ts -B2 -A5

Repository: lynx-family/lynx-stack

Length of output: 728


🏁 Script executed:

#!/bin/bash
# Check what's at line 654 - which case is it?
sed -n '620,670p' packages/genui/a2ui-catalog-extractor/src/extractor.ts

Repository: lynx-family/lynx-stack

Length of output: 1293


Scope GENERIC_PROPS filter to the top-level component schema.

buildObjectSchema is invoked for both top-level component props and nested object type literals / referenced type aliases (e.g., line 654 explicitly passes topLevel: false). The unconditional if (GENERIC_PROPS.has(property.name)) continue; at line 296 therefore strips any of these framework-injected names (id, surface, setValue, sendAction, dataContextPath, __template, component) from any nested object, not just the top-level shape. This silently drops user-authored fields with those names, leaving no diagnostic.

Since the intent is to exclude framework-injected props from the component schema itself, gate the filter on topLevel:

   for (const property of properties) {
-    if (GENERIC_PROPS.has(property.name)) continue;
+    if (topLevel && GENERIC_PROPS.has(property.name)) continue;

     const doc = property.doc ?? typeDocRecord?.properties.get(property.name);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function buildObjectSchema(
properties: PropertyDefinition[],
context: SourceContext,
parseState: ParseState,
topLevel: boolean,
typeDocRecord?: TypeDocRecord,
explicitAdditionalProperties?: JsonSchema | boolean,
): JsonSchema | ComponentSchema {
const schema: JsonSchema = topLevel ? {} : { type: 'object' };
const propertyMap: Record<string, JsonSchema> = {};
const required: string[] = [];
const additionalProperties = explicitAdditionalProperties;
for (const property of properties) {
if (GENERIC_PROPS.has(property.name)) continue;
const doc = property.doc ?? typeDocRecord?.properties.get(property.name);
const propertySchema = applyPropertyDoc(
parseTypeNode(property.typeNode, context, parseState),
doc,
);
propertyMap[property.name] = propertySchema;
if (!property.optional) {
required.push(property.name);
}
}
function buildObjectSchema(
properties: PropertyDefinition[],
context: SourceContext,
parseState: ParseState,
topLevel: boolean,
typeDocRecord?: TypeDocRecord,
explicitAdditionalProperties?: JsonSchema | boolean,
): JsonSchema | ComponentSchema {
const schema: JsonSchema = topLevel ? {} : { type: 'object' };
const propertyMap: Record<string, JsonSchema> = {};
const required: string[] = [];
const additionalProperties = explicitAdditionalProperties;
for (const property of properties) {
if (topLevel && GENERIC_PROPS.has(property.name)) continue;
const doc = property.doc ?? typeDocRecord?.properties.get(property.name);
const propertySchema = applyPropertyDoc(
parseTypeNode(property.typeNode, context, parseState),
doc,
);
propertyMap[property.name] = propertySchema;
if (!property.optional) {
required.push(property.name);
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui-catalog-extractor/src/extractor.ts` around lines 282 -
308, The GENERIC_PROPS filter in buildObjectSchema is applied unconditionally
and thus removes framework-injected names from nested object shapes; change the
loop so the check uses the topLevel flag (i.e., only skip properties when
topLevel is true and GENERIC_PROPS.has(property.name)), keeping property
processing identical otherwise (still building propertyMap, required, etc.);
update references to property.name, GENERIC_PROPS, and topLevel in the for loop
that iterates over properties to scope the filter to top-level component
schemas.

Comment on lines +9 to +14
/**
* Props for the Button catalog component.
*/
export interface ButtonActionContextBinding {
path: string;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Misplaced TSDoc: this describes ButtonProps, not ButtonActionContextBinding.

The /** Props for the Button catalog component. */ block is attached to ButtonActionContextBinding because it immediately precedes that interface. Given this PR wires up a TypeDoc-driven catalog extractor that consumes TSDoc on exported interfaces, this description will be picked up for the wrong symbol and ButtonProps will lose its intended summary. Move the block down to ButtonProps at line 32.

📝 Proposed fix
-/**
- * Props for the Button catalog component.
- */
-export interface ButtonActionContextBinding {
+export interface ButtonActionContextBinding {
   path: string;
 }
@@
-export interface ButtonProps extends GenericComponentProps {
+/**
+ * Props for the Button catalog component.
+ */
+export interface ButtonProps extends GenericComponentProps {
   child: string;
   variant?: 'primary' | 'borderless';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui/src/catalog/Button/index.tsx` around lines 9 - 14, The
TSDoc block currently above ButtonActionContextBinding is describing ButtonProps
and will be picked up for the wrong symbol; remove the existing doc comment that
precedes the ButtonActionContextBinding interface and place that exact comment
immediately above the exported ButtonProps interface declaration so ButtonProps
retains the correct summary; ensure the comment text remains unchanged and that
it is directly adjacent to the ButtonProps interface (exported symbol name:
ButtonProps) to be picked up by the TypeDoc extractor.

@PupilTong PupilTong force-pushed the codex/a2ui-catalog-extractor branch from aeb86cb to 35a990a Compare April 24, 2026 10:18
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

🧹 Nitpick comments (3)
packages/genui/a2ui-catalog-extractor/src/extractor.ts (2)

999-1017: loadCatalogConfig casts dynamic-import results without runtime validation.

await import(...) may resolve to a module whose default (or top-level namespace) isn't an object — e.g., someone exports an array or a primitive — but the return type is asserted as Record<string, JsonValue>. Consider a lightweight runtime guard that rejects non-plain-object configs with a clear error message pointing at resolvedPath, so misconfigurations fail at load time instead of surfacing as undefined reads deeper in the CLI.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui-catalog-extractor/src/extractor.ts` around lines 999 -
1017, loadCatalogConfig currently casts dynamic-import results to Record<string,
JsonValue> without validating that the imported module (module.default or
module) is a plain object, which can hide misconfigurations; update
loadCatalogConfig to perform a runtime guard after the import that checks
whether the resolved value assigned to config is a non-null plain object (use
resolvedPath in the error message) and reject (throw) with a clear error if it's
not, so any array/primitive exports fail fast; reference loadCatalogConfig,
resolvedPath and the local variable config when adding the validation and error
throw.

310-340: Minor dead/redundant branches in buildObjectSchema.

  • Lines 311-315 set schema.properties for the topLevel path, but the topLevel return at Line 317-322 immediately re-derives it via schema.properties ?? {}, so that branch only matters for the non-top-level case.
  • At Line 328-332, required.length > 0 is only reachable when propertyNames.length > 0 (required is pushed in the same loop that fills propertyMap), so the else if is dead.

Consider simplifying to keep the control flow easier to reason about.

Proposed cleanup
-  const propertyNames = Object.keys(propertyMap);
-  if (propertyNames.length > 0) {
-    schema.properties = propertyMap;
-  } else if (topLevel) {
-    schema.properties = {};
-  }
-
-  if (topLevel) {
+  const propertyNames = Object.keys(propertyMap);
+  if (topLevel) {
     return {
-      properties: schema.properties ?? {},
+      properties: propertyMap,
       required,
     };
   }
 
+  if (propertyNames.length > 0) {
+    schema.properties = propertyMap;
+  }
+
   if (propertyNames.length === 0 && additionalProperties === undefined) {
     return { type: 'object' };
   }
 
-  if (propertyNames.length > 0) {
-    schema.required = required;
-  } else if (required.length > 0) {
+  if (propertyNames.length > 0) {
     schema.required = required;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui-catalog-extractor/src/extractor.ts` around lines 310 -
340, In buildObjectSchema, remove the redundant topLevel branch that pre-sets
schema.properties (the block that sets schema.properties when topLevel before
the early return) and instead let the return use propertyMap directly; also
remove the dead else-if that sets schema.required when propertyNames === 0
(since required is only populated when propertyNames > 0) and replace the
required logic with a single conditional that assigns schema.required = required
only when required.length > 0; finally collapse the additionalProperties logic
to: if additionalProperties is undefined and propertyNames > 0 set
schema.additionalProperties = false, otherwise set schema.additionalProperties =
additionalProperties (so you only have one assignment path).
packages/genui/a2ui-catalog-extractor/test/extract-catalog.test.ts (1)

219-244: Golden-fixture test is brittle but intentional.

The hard-coded toHaveLength(10) and per-file equality against legacy-baseline/<Component>/catalog.json will need updating whenever a new component is added or renamed in packages/genui/a2ui/src/catalog. That's arguably the point of a golden test, but consider adding a reciprocal assertion that every fixture directory under legacy-baseline corresponds to a rendered file, so stale fixtures (e.g., after a component removal) don't silently stay green.

Proposed extra assertion
     expect(renderedFiles).toHaveLength(10);
+
+    const fixtureDirs = (await fs.readdir(fixturePath('legacy-baseline'), {
+      withFileTypes: true,
+    }))
+      .filter((entry) => entry.isDirectory())
+      .map((entry) => entry.name)
+      .sort();
+    const renderedNames = renderedFiles
+      .map((file) => path.basename(path.dirname(file.path)))
+      .sort();
+    expect(renderedNames).toEqual(fixtureDirs);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui-catalog-extractor/test/extract-catalog.test.ts` around
lines 219 - 244, The golden-fixture test currently only asserts renderedFiles
length and that each rendered file matches its fixture, which allows stale
fixtures to persist; update the test (the 'matches the legacy A2UI catalog
fixtures exactly' case) to also read the fixture directories under
'legacy-baseline' (use fixturePath helper to locate the base), derive each
fixture's component name, and assert that a corresponding entry exists in
renderedFiles (match by path/componentName) so every fixture directory has a
rendered file; keep existing checks using extractCatalog, renderCatalogFiles,
renderedFiles, and the per-file content equality assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/genui/a2ui-catalog-extractor/src/docs.ts`:
- Around line 254-277: The `@typedef/`@property parsing fails on nested brace type
expressions because the regex uses a greedy capture for {type}; replace the
fragile regex approach by extracting the braced type with a small parser:
implement a helper (e.g., extractBracedContent) that scans from the opening '{'
and returns the balanced content accounting for nested braces, then use that to
populate typedefTypeExpression/property type before matching the rest of the
line (use the existing typedefLine and lines loops and property parsing logic),
and additionally add a warning log when a line starts with '@property ' but no
property was extracted (so authors aren't silently losing fields); keep
JsDocTypedef shape and property accumulation unchanged.

---

Nitpick comments:
In `@packages/genui/a2ui-catalog-extractor/src/extractor.ts`:
- Around line 999-1017: loadCatalogConfig currently casts dynamic-import results
to Record<string, JsonValue> without validating that the imported module
(module.default or module) is a plain object, which can hide misconfigurations;
update loadCatalogConfig to perform a runtime guard after the import that checks
whether the resolved value assigned to config is a non-null plain object (use
resolvedPath in the error message) and reject (throw) with a clear error if it's
not, so any array/primitive exports fail fast; reference loadCatalogConfig,
resolvedPath and the local variable config when adding the validation and error
throw.
- Around line 310-340: In buildObjectSchema, remove the redundant topLevel
branch that pre-sets schema.properties (the block that sets schema.properties
when topLevel before the early return) and instead let the return use
propertyMap directly; also remove the dead else-if that sets schema.required
when propertyNames === 0 (since required is only populated when propertyNames >
0) and replace the required logic with a single conditional that assigns
schema.required = required only when required.length > 0; finally collapse the
additionalProperties logic to: if additionalProperties is undefined and
propertyNames > 0 set schema.additionalProperties = false, otherwise set
schema.additionalProperties = additionalProperties (so you only have one
assignment path).

In `@packages/genui/a2ui-catalog-extractor/test/extract-catalog.test.ts`:
- Around line 219-244: The golden-fixture test currently only asserts
renderedFiles length and that each rendered file matches its fixture, which
allows stale fixtures to persist; update the test (the 'matches the legacy A2UI
catalog fixtures exactly' case) to also read the fixture directories under
'legacy-baseline' (use fixturePath helper to locate the base), derive each
fixture's component name, and assert that a corresponding entry exists in
renderedFiles (match by path/componentName) so every fixture directory has a
rendered file; keep existing checks using extractCatalog, renderCatalogFiles,
renderedFiles, and the per-file content equality assertions.
🪄 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: 2187b9b3-d280-41d8-9667-ee5be1ccc07e

📥 Commits

Reviewing files that changed from the base of the PR and between aeb86cb and 35a990a.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (63)
  • .github/a2ui-catalog-extractor.instructions.md
  • packages/genui/a2ui-catalog-extractor/README.md
  • packages/genui/a2ui-catalog-extractor/SKILL.md
  • packages/genui/a2ui-catalog-extractor/agents/openai.yaml
  • packages/genui/a2ui-catalog-extractor/eslint.config.js
  • packages/genui/a2ui-catalog-extractor/package.json
  • packages/genui/a2ui-catalog-extractor/references/a2ui-v0.9-schema.md
  • packages/genui/a2ui-catalog-extractor/references/tsdoc-mapping.md
  • packages/genui/a2ui-catalog-extractor/rslib.config.ts
  • packages/genui/a2ui-catalog-extractor/rstest.config.ts
  • packages/genui/a2ui-catalog-extractor/src/cli.ts
  • packages/genui/a2ui-catalog-extractor/src/docs.ts
  • packages/genui/a2ui-catalog-extractor/src/extractor.ts
  • packages/genui/a2ui-catalog-extractor/src/index.ts
  • packages/genui/a2ui-catalog-extractor/src/types.ts
  • packages/genui/a2ui-catalog-extractor/test/cli.test.ts
  • packages/genui/a2ui-catalog-extractor/test/extract-catalog.test.ts
  • packages/genui/a2ui-catalog-extractor/test/fixtures/jsx-loose/catalog/Loose/index.jsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/jsx-loose/tsconfig.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/jsx/catalog/Badge/index.jsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/jsx/tsconfig.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/Button/catalog.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/Card/catalog.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/CheckBox/catalog.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/Column/catalog.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/Divider/catalog.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/Image/catalog.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/List/catalog.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/RadioGroup/catalog.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/Row/catalog.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/Text/catalog.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-complex/catalog/ActionButton/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-complex/tsconfig.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-default/catalog/UndefinedDefault/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-default/tsconfig.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-export-default/catalog/DefaultExport/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-export-default/tsconfig.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-export-named/catalog/NamedExport/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-export-named/tsconfig.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-json/catalog/BrokenJson/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-json/tsconfig.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-type/catalog/Fancy/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-type/tsconfig.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx/catalog/Chip/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx/tsconfig.json
  • packages/genui/a2ui-catalog-extractor/tsconfig.build.json
  • packages/genui/a2ui-catalog-extractor/tsconfig.json
  • packages/genui/a2ui-catalog-extractor/turbo.json
  • packages/genui/a2ui/README.md
  • packages/genui/a2ui/package.json
  • packages/genui/a2ui/src/catalog/Button/index.tsx
  • packages/genui/a2ui/src/catalog/Card/index.tsx
  • packages/genui/a2ui/src/catalog/CheckBox/index.tsx
  • packages/genui/a2ui/src/catalog/Column/index.tsx
  • packages/genui/a2ui/src/catalog/Divider/index.tsx
  • packages/genui/a2ui/src/catalog/Image/index.tsx
  • packages/genui/a2ui/src/catalog/List/index.tsx
  • packages/genui/a2ui/src/catalog/RadioGroup/index.tsx
  • packages/genui/a2ui/src/catalog/Row/index.tsx
  • packages/genui/a2ui/src/catalog/Text/index.tsx
  • packages/genui/a2ui/tools/catalog_generator.ts
  • packages/genui/a2ui/turbo.json
  • packages/genui/tsconfig.json
💤 Files with no reviewable changes (1)
  • packages/genui/a2ui/tools/catalog_generator.ts
✅ Files skipped from review due to trivial changes (46)
  • packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/Card/catalog.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/jsx-loose/catalog/Loose/index.jsx
  • packages/genui/a2ui/src/catalog/Divider/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-export-named/tsconfig.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/Divider/catalog.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-default/tsconfig.json
  • packages/genui/tsconfig.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-json/catalog/BrokenJson/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-default/catalog/UndefinedDefault/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-json/tsconfig.json
  • packages/genui/a2ui/src/catalog/Text/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/List/catalog.json
  • packages/genui/a2ui-catalog-extractor/tsconfig.build.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx/tsconfig.json
  • packages/genui/a2ui-catalog-extractor/tsconfig.json
  • packages/genui/a2ui/src/catalog/CheckBox/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-type/tsconfig.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/RadioGroup/catalog.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-export-named/catalog/NamedExport/index.tsx
  • packages/genui/a2ui/src/catalog/Image/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/jsx-loose/tsconfig.json
  • packages/genui/a2ui/src/catalog/RadioGroup/index.tsx
  • packages/genui/a2ui/turbo.json
  • packages/genui/a2ui-catalog-extractor/rslib.config.ts
  • packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/Row/catalog.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-export-default/catalog/DefaultExport/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/Image/catalog.json
  • packages/genui/a2ui-catalog-extractor/agents/openai.yaml
  • .github/a2ui-catalog-extractor.instructions.md
  • packages/genui/a2ui-catalog-extractor/turbo.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-complex/tsconfig.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/CheckBox/catalog.json
  • packages/genui/a2ui/src/catalog/List/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-type/catalog/Fancy/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx/catalog/Chip/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/jsx/catalog/Badge/index.jsx
  • packages/genui/a2ui-catalog-extractor/src/index.ts
  • packages/genui/a2ui/README.md
  • packages/genui/a2ui/src/catalog/Card/index.tsx
  • packages/genui/a2ui-catalog-extractor/README.md
  • packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/Text/catalog.json
  • packages/genui/a2ui-catalog-extractor/SKILL.md
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-invalid-export-default/tsconfig.json
  • packages/genui/a2ui-catalog-extractor/src/types.ts
  • packages/genui/a2ui/src/catalog/Column/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/fixtures/tsx-complex/catalog/ActionButton/index.tsx
🚧 Files skipped from review as they are similar to previous changes (10)
  • packages/genui/a2ui-catalog-extractor/rstest.config.ts
  • packages/genui/a2ui-catalog-extractor/test/fixtures/jsx/tsconfig.json
  • packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/Column/catalog.json
  • packages/genui/a2ui-catalog-extractor/package.json
  • packages/genui/a2ui-catalog-extractor/references/tsdoc-mapping.md
  • packages/genui/a2ui-catalog-extractor/test/fixtures/legacy-baseline/Button/catalog.json
  • packages/genui/a2ui/package.json
  • packages/genui/a2ui-catalog-extractor/src/cli.ts
  • packages/genui/a2ui/src/catalog/Button/index.tsx
  • packages/genui/a2ui-catalog-extractor/test/cli.test.ts

Comment on lines +254 to +277
const typedefMatch = typedefLine.match(
/^@typedef\s+\{(?<type>.+)\}\s+(?<name>[A-Za-z_$][\w$]*)(?:\s+(?<description>.+))?$/u,
);
if (!typedefMatch?.groups) continue;

const typedef: JsDocTypedef = {
name: typedefMatch.groups['name']!,
properties: [],
};
const typedefDescription = typedefMatch.groups['description']?.trim();
const typedefTypeExpression = typedefMatch.groups['type']?.trim();
if (typedefDescription) {
typedef.description = typedefDescription;
}
if (typedefTypeExpression) {
typedef.typeExpression = typedefTypeExpression;
}

for (const line of lines) {
if (!line.startsWith('@property ')) continue;
const propertyMatch = line.match(
/^@property\s+\{(?<type>.+)\}\s+(?<name>\[[^\]]+\]|[A-Za-z_$][\w$]*)(?:\s+(?<description>.+))?$/u,
);
if (!propertyMatch?.groups) continue;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

@typedef / @property regexes don't tolerate nested braces in the type expression.

\{(?<type>.+)\} matches greedily up to the last } on the line, so it silently breaks on JSDoc types that embed braces, e.g. @property {{ path: string }} label or @property {Record<string, {a: number}>} ctx — the entire typedef/property line will fail to match and be dropped without diagnostic. For JSX best-effort mode that's a realistic authoring shape.

If supporting nested braces is out of scope, consider at least logging/throwing when an @property line fails to match, so authors don't silently lose fields.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/genui/a2ui-catalog-extractor/src/docs.ts` around lines 254 - 277,
The `@typedef/`@property parsing fails on nested brace type expressions because
the regex uses a greedy capture for {type}; replace the fragile regex approach
by extracting the braced type with a small parser: implement a helper (e.g.,
extractBracedContent) that scans from the opening '{' and returns the balanced
content accounting for nested braces, then use that to populate
typedefTypeExpression/property type before matching the rest of the line (use
the existing typedefLine and lines loops and property parsing logic), and
additionally add a warning log when a line starts with '@property ' but no
property was extracted (so authors aren't silently losing fields); keep
JsDocTypedef shape and property accumulation unchanged.

@PupilTong PupilTong force-pushed the codex/a2ui-catalog-extractor branch 2 times, most recently from 5aa880b to 9a62559 Compare April 24, 2026 15:17
@PupilTong PupilTong force-pushed the codex/a2ui-catalog-extractor branch from 9a62559 to a5186af Compare April 24, 2026 17:18
@PupilTong PupilTong closed this Apr 25, 2026
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.

1 participant