Skip to content

feat(cli): a component doc can name a differently named export of the file beside it - #7659

Merged
josephfarina merged 1 commit into
mainfrom
josephfarina/doc-names-another-export
Oct 18, 2026
Merged

josephfarina merged 1 commit into
mainfrom
josephfarina/doc-names-another-export

Conversation

@josephfarina

@josephfarina josephfarina commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

User need

An integration package documents a component the CLI cannot find by name when the doc's file is not named after the component.

  • A doc beside a file named otherwise. The CLI pairs a component doc with the source file that shares its stem and treats the file name as the component's name. Slides.doc.mjs beside a Slides.tsx that exports AcmeCarousel lists as Slides, component AcmeCarousel finds nothing, search never shows AcmeCarousel, and integration verify fails with component_export_missing because the module does not export Slides. Setting the doc's name to the real export changes nothing.
  • Kebab-case file names. Every package that names its files in kebab case hits the same wall: acme-widget.doc.mjs beside an acme-widget.tsx that exports AcmeWidget lists as acme-widget, component AcmeWidget fails with ERR_UNKNOWN_COMPONENT, and integration verify fails once per component. Today the only way out is to rename the package's source files.

Problem and solution fit

A component doc's name is the export it documents (ComponentDoc.name already says so). After this change, discovery reads it that way: when the same-stem source has no runtime export named after its stem, the doc's name may name another runtime export of that source, and discovery records the component under that name. A source that exports its stem keeps the stem, and the file name still opens the doc.

The list, component <Name>, search, swizzle, gap-report, the doctor integration checks and integration verify all know the component by that name.

One owner rule keeps every lookup that resolves today resolving the same way, on every surface. Each owner of a name holds it as core (Core documents it), file (a doc named after its file), or doc (only a doc records it: the export name, or the file name of a doc recorded under another export). On an exact unscoped lookup, doc owners give way whenever a core or file owner exists; --package still reaches them. Every surface applies it in the same order: an active replacement first, then the rule over every owner (whether its doc loads or not), then the surface's own check of which docs load.

Current specification

spec:AST-039 FR4 owns how discovery pairs a descriptor with its source. #7658, merged as c992783 on the maintainer's approval, states this rule there and in the current record architecture:cli-surface INV24.

Public delta and caller burden

  • Before: astryx component AcmeWidget exits 1 with ERR_UNKNOWN_COMPONENT, and astryx integration verify reports component_export_missing for acme-widget. After: component AcmeWidget prints the doc with import: @acme/widgets, component acme-widget still opens it, and verify passes.
  • A name Core, a legacy docs package (astryx.docs in package.json), or another package's file already has stays theirs. With Overlays.doc.mjs naming Dialog, component Dialog, --props, --source, swizzle and gap-report still answer with Core's Dialog, search shows the package's entry with astryx component Dialog --package @acme/widgets, and Doctor reports the shared name. With Badge.doc.mjs naming AcmeBadge, component Badge returns Core's Badge on every surface (today it is ambiguous).
  • A name is taken, and the doc keeps its file name, when another doc's file has it (hidden docs included), when a doc in the same package replaces that Core name (the replacement keeps answering it, bare and with --package), or when an earlier doc records it. Docs are ordered by path in code-unit order, the same in every locale.
  • An exact name, including one a doc records, beats Core's keyword fallback: with no Core component named Sheet, component Sheet returns a package's documented Sheet instead of guessing Core's BottomSheet. component BottomSheet is unchanged.
  • An author writes the doc's name as the export, which the authoring type already asks for. Nothing else changes for authors or apps. JSON shapes are unchanged; Core component discovery is untouched.
  • Changeset: patch, [feat].

Design and alternatives

Discovery reads docs and sources without running them. A quick text scan picks the docs whose top-level name differs from their file name (or may be set by a spread); only those are parsed.

  • foundation/discovery/component-discovery.mjs:
    • discoverIntegrationComponents records a doc that names another export of its file under that export, unless the name is taken.
    • findIntegrationComponent(integration, name) returns the doc, source and hold exactly as discovery records the name: the same-stem doc first, hidden or not, then a doc whose export is the name. findIntegrationComponentDoc and findIntegrationComponentSource answer through it.
    • unscopedComponentOwners(owners) applies the owner rule.
  • foundation/discovery/module-exports.mjs: staticExportObject reads an exported object literal as plain data; moduleExportSource names the file that declares an export, following local re-exports; moduleExportsName answers through it. A module whose text writes neither the name nor export * is not parsed for it; otherwise the parsed tree decides, so a re-export inside a comment counts for nothing. Parsed modules and docs are reused while their file keeps its size, mtime, ctime and inode.
  • api/component, api/swizzle, api/gap-report: build owners from findIntegrationComponent or discovery records and apply the rule in the order above. Swizzle counts Core as owning every name it documents, then leaves a Core name with no folder of its own unknown, as before. gap-report answers to a renamed doc's file name, and its owner rule counts another package's hidden file-named doc and a legacy docs package the way detail does, for every spelling its case-blind match finds (both still route the report to Core, as on main).
  • api/search/search.mjs: applies the rule from the records it already gathered, counting Core for every name its doc lookup finds, as detail does. A candidate that component <name> would not open gets a command with --package and ranks after the owner; a file-name alias that opens another owner is dropped.
  • api/component/_adapter.mjs (legacyExternalKeeps), component.mjs and api/search/search.mjs: when every owner holds the name only as an export a doc records, a legacy docs package's exact doc keeps the name, as on main, and search names the integration's result with --package. A renamed doc's file name still answers before a legacy package, as it always did.
  • Docs: ComponentDoc.name (type and self-doc) and the "Resolve the import" guide.

Rejected:

  • requiring the source file to carry the export's name (forces packages with kebab-case or barrel-style files to rename public source paths);
  • a manifest map from file to name (a central catalog, which spec:AST-039 FR11 forbids);
  • running docs or sources to read their exports (discovery never runs package code);
  • letting a doc-recorded name share an unscoped lookup with Core or a file-named doc (it would turn answers that work today into ERR_AMBIGUOUS_COMPONENT).

Evidence

  • Kebab-case package (three components named like artifact-viewer.tsx, one in a subfolder):
    • main: the list shows artifact-viewer, artifact-toolbar and viewer-header; component ArtifactViewer and the other two fail with ERR_UNKNOWN_COMPONENT; integration verify fails three times with component_export_missing;
    • this branch: the list shows ArtifactViewer, ArtifactToolbar and ViewerHeader; each opens; component artifact-viewer still opens its doc; search "artifact viewer" ranks ArtifactViewer first; integration verify is packable with no issues.
  • A real 32-doc integration package (measured on the first revision of this change): the three components documented beside files named otherwise list, open, search and verify under their export names; the other 29 entries are byte-identical.
  • Cross-surface matrix: 20 consumer projects covering a file-named doc, an export name, kebab case, hidden docs, replaces, a Core name, a Core keyword-fallback name, a name another package's file has, two packages recording one name, re-export chains, folder indexes, a re-export in a comment, .doc.ts, legacy export const docs, and a doc the loader rejects. Every surface (component and its flags, batch, swizzle, gap-report, search, the list, doctor integration validate and components, integration verify, doctor) ran on main and on this branch, plus the JS API twice in one process with files edited between. Result: no exact lookup that answers on main answers differently, every surface agrees on the owner of each name, and nothing crashes or repeats an issue.
  • Unchanged paths: the Core list at every detail level, Core component detail and search, and every doc named after its file give identical output.
  • Cost on a 300-doc package (median of 5 runs, with the stacked family-members PR applied): component Button, component --list, doctor, validate and search are 11% to 20% slower than main, almost all of it the first parse of the docs that name another export or list members.

Overlaps

Scope

  • One capability is added: a component doc can name a differently named export of the file beside it, under one owner rule for every lookup.
  • Unrelated fixes, cleanup, and policy changes were removed or split.
  • Consumer docs, focused tests, and migration evidence ship with the feature.
  • Public text and artifacts contain no internal Meta context.

Testing

  • Size: 18 files, 1,987 authored lines.
  • Every new test fails on the previous revision:
    • component-discovery.export-names.test.mjs: the owner rule; hidden-doc and same-package replacement names taken; code-unit path order; a name a spread may set; an unreadable doc reported under its file name; a kebab-case doc found by its export and by its file name.
    • integration-export-name.test.mjs: another package's invalid or hidden file-named doc keeps the name on every surface, gap-report included; a legacy docs package keeps an export name it documents while a renamed doc's file name answers first, and search names the integration's result with --package; a Core name only the sub-component lookup finds (CheckboxIndicator) stays Core's in search and gap-report too, while a doc named after its file (BaseTable) still routes reports to its package, as on main; gap-report by a renamed doc's file name; the search command and order for a Core name; a Core name with no folder stays unknown to swizzle; another package's invalid file-named doc keeps the name on every surface; a file name Core has stays Core's in search; a replacement recorded under its export swizzles into its file-name folder.
    • module-exports.test.mjs: the static readers, and a file npm replaced with one of the same size and mtime is read again.
    • pack-check.test.mjs: verify checks the export a doc names.
  • Static gates pass: copyright, sync-exports, README check, API types sync and verify, the JSON-API and authoring typechecks, token docs, changesets, check:repo, knowledge, CLI structure, eslint and prettier. The strict typecheck passes once @astryxdesign/theme-neutral is built; unbuilt, it reports only that package's two built errors.
  • Targeted suites (discovery, component, integration, integrations, search, swizzle, gap-report, doc compiler, theme): 96 files, 1,937 tests pass. Full CLI suite on the previous revision: 6,346 passed; 5 files hit 30-second timeouts at 24 workers and pass when run alone.

@vercel

vercel Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Oct 18, 2026 4:52am UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 17, 2026
@astracat-bot

astracat-bot Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Oct 18, 2026, 8:02 PM UTC

@github-actions

github-actions Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR

🧪 Sandbox Preview

View Sandbox for this PR

No new or modified components detected.

Bundle Size Summary

No component packages changed.

Accessibility Audit

Status: No accessibility violations detected.

Visual Regression

40 of 478 shot(s) changed.

A change here is a question, not a failure: check whether the after is the
picture you intended. Record that review on the PR. Baseline maintenance is an
explicit dispatch of CI; this report never rewrites the baseline or adds a release gate.

component story theme mode pixels
Selector Indicator Space Bottom Sheet Narrow End RTL neutral light 754,077
Selector Indicator Space Bottom Sheet Wide Start neutral light 740,188
Chat Full AI Chat probe light 230,823
Chat Full AI Chat probe dark 229,805
RichTextEditor Markdown parity: side by side probe dark 94,866
RichTextEditor Markdown parity: side by side probe light 89,762
Collapsible Dividers probe dark 66,445
Collapsible Dividers probe light 66,445
Chat Default neutral dark 36,304
Chat Default neutral light 35,767
Chat Empty State probe light 23,531
Chat Empty State probe dark 22,622
ChatAutoScroll Scroll Behavior Comparison neutral dark 15,378
ChatAutoScroll Scroll Behavior Comparison neutral light 14,107
Chat With Attachments probe light 13,406
Chat With Attachments probe dark 11,232
ProgressBar With Target Mark probe dark 7,895
ProgressBar With Target Mark probe light 7,895
Markdown Default neutral dark 7,573
Markdown Default neutral light 7,566

and 20 more.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This needs a maintainer decision before it can be approved: no current spec covers finding an integration component by its doc's name when the file is named otherwise, or treating a family doc's components entries as components (spec:AST-039 is still a draft, and its FR4 derives the export from the file stem), so a maintainer needs to provide or identify the governing spec, settling the identity rule proposed in #7658 and whether existing packages get these names without an opt-in when one matches a Core component. Independently of that decision, please split this into two pull requests (export names, then family members) so each stays under 2,000 changed lines, drop the reformatting of untouched code, and fix these two issues:

  • A doc whose name differs from its file stem, beside a file that exports both, is recorded twice when it lists its own name in components, so its single replaces counts as two declarations and a replacement that works today stops applying.
  • moduleExportSource follows export * from lines inside comments, so a commented-out re-export creates a component that integration verify then rejects with component_export_missing.

[Automated review]

const name = entry.name;
if (typeof name !== 'string' || Object.keys(entry).length < 2) continue;
if (!COMPONENT_NAME_RE.test(name) || hidden.has(name)) continue;
if (name === stem || name === record.name) continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This skips the stem and the recorded name but not the doc's own name. When name differs from the stem and the file exports both (Chip.doc.mjs with name: 'AcmeChip' beside a Chip.tsx that exports Chip and AcmeChip), listing AcmeChip as the first full entry, as the family guide suggests, records the same doc twice. Both records then carry the doc's replaces (line 1187), so one valid declaration becomes two, the resolver reports ambiguous_component_replacement, and a replacement that applies on main stops applying. Skipping entries equal to the doc's own name here, and deciding the doc's own record from its identity rather than member lookup, avoids it.

}
// A module whose text never writes the export's name as a word can provide
// it only through `export * from`: follow those without parsing the module.
if (exportName !== 'default' && !writesWord(text, exportName)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This shortcut follows every export * from the regex finds, including one inside a comment or string. With // export * from './legacy'; in index.ts and AcmeTab exported by legacy.tsx, it returns legacy.tsx, discovery records AcmeTab as a family member, and integration verify then fails with component_export_missing, where main reports the package packable. Taking the shortcut only when the text contains no export *, or reading the export * targets from the parsed tree as the slow path does, avoids it; the theme import checks share this helper.

@josephfarina
josephfarina force-pushed the josephfarina/doc-names-another-export branch from 2657cea to 6d49bf7 Compare October 17, 2026 20:30
@josephfarina

Copy link
Copy Markdown
Contributor Author

Addressed the automated review in 6d49bf7:

  • Size: 1,975 authored lines (18 files, +1,873 −102), down from 2,109. The Core family-doc member read is no longer touched (D8), the reformatting of untouched code is gone (D9), and assertions that other tests already make were cut. No coverage was lost.
  • A doc that lists its own name among its members: that entry is no longer recorded as a second component. A doc's own record is decided by its identity, so its replaces is declared once. The Chip/AcmeChip fixture is now a test; it fails without the fix with two ambiguous_component_replacement errors.
  • A re-export inside a comment: moduleExportSource skips parsing only when the text writes neither the name nor export *, and otherwise reads the parsed tree. The commented-out re-export is now a test; it fails without the fix.
  • Core names: a name discovery records from a doc never takes an unscoped lookup that Core answers. component Dialog (also --props, --source, swizzle and gap-report) returns Core as on main, --package reaches the package's component, and Doctor reports the shared name. Nothing that resolves today resolves differently, so packages need no opt-in.
  • Spec: spec(AST-039, cli-surface): integration docs are found by their documented names and checked in one command #7658 states the rule in spec:AST-039 FR4 and in architecture:cli-surface INV24, the current record, and waits on a maintainer.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This needs a maintainer decision before it can be approved. No current spec covers finding an integration component by its doc's name when its file is named otherwise, or treating a family doc's full components entries as components: spec:AST-039 is still a draft, and its FR4 derives the export from the file stem. A maintainer needs to provide or identify the governing spec, settling the rule proposed in #7658, including how these names resolve against existing components and whether existing packages get them in a patch without an opt-in. Independently of that decision, please fix these two issues:

  • When another installed integration has a file-named component with a documented name, unscoped astryx component, --props, swizzle and gap-report exit with ERR_AMBIGUOUS_COMPONENT where the current release returns that component, so unscopedComponentOwners should yield to every file-named owner, not only Core.
  • integration verify repeats a failing family doc's errors once per listed entry, and loading each doc once would report it once.

[Automated review]

Comment on lines +1391 to +1399
export function unscopedComponentOwners(owners, name) {
const core = owners.filter(owner => owner.package === CORE_PACKAGE);
const documented = owners.every(
owner =>
owner.package === CORE_PACKAGE ||
(owner.docPath != null &&
docStem(owner.docPath) !== (owner.name ?? name)),
);
return core.length > 0 && documented ? core : owners;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This only yields to Core. If no Core component has the name but another installed integration has a file-named component with it, every owner comes back. Unscoped component, --props, swizzle and gap-report then exit with ERR_AMBIGUOUS_COMPONENT where the current release returns that integration's component. For example, @acme/widgets lists a full AcmeTab entry and @beta/kit ships AcmeTab.doc.mjs.

Keeping only Core and file-named owners whenever at least one exists restores today's owner set for every name that already resolves:

const docRecorded = owner =>
  owner.docPath != null && docStem(owner.docPath) !== (owner.name ?? name);
const primary = owners.filter(
  owner => owner.package === CORE_PACKAGE || !docRecorded(owner),
);
return primary.length > 0 ? primary : owners;

Comment on lines +1178 to +1183
for (const record of byPath) {
if (!scans.get(record.name)?.hasMembers) continue;
for (const member of familyMembers(integration, record)) {
if (taken.has(member.name)) continue;
taken.add(member.name);
members.push({...record, ...member});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Members are recorded from the doc's static read even when the loader rejects the doc. integration verify then loads every record in two places: the inventory in foundation/integrations/contribution-inventory.mjs and validatePackedComponentExports in api/integration/pack-check.mjs. As a result, one broken family doc is reported once per listed entry. With three entries, local_discovery_failed, packed_discovery_failed and component_doc_unloadable each appear three times, where main reports each once. Loading each doc path once in those loops, or skipping the member records of a doc that fails to load, would report it once.

@josephfarina
josephfarina force-pushed the josephfarina/doc-names-another-export branch from 6d49bf7 to 9b990ad Compare October 17, 2026 21:10
@josephfarina

Copy link
Copy Markdown
Contributor Author

Addressed the second automated review in 9b990ad:

  • A file-named component in another integration: unscopedComponentOwners now keeps Core and every component named after its file whenever one exists, so a name a doc records never takes an unscoped lookup that resolves today. --package still reaches the integration that records it. A two-package case is now a test; it fails without the fix with ERR_AMBIGUOUS_COMPONENT. The existing Core-name test covers the call sites (detail, --props, swizzle, gap-report).
  • A failing family doc in integration verify: the pack check and the contribution inventory each load a doc once, so its errors are reported once, not once per listed entry. A pack-check case is now a test; without the fix it repeats each issue.
  • Size: 1,988 authored lines (20 files, +1,884 −104). Two overlapping tests were merged and some comments shortened to make room.
  • Spec: spec(AST-039, cli-surface): integration docs are found by their documented names and checked in one command #7658's FR4 and architecture:cli-surface INV24 now say the same: a name a doc records never takes an unscoped lookup that Core or a file-named component answers. It still waits on a maintainer.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This needs a maintainer decision before it can be approved. No current spec covers finding an integration component by a doc name that differs from its file name, or treating a family doc's full components entries as components: spec:AST-039 is still a draft, and its FR4 derives the export from the file stem. A maintainer needs to provide or identify the governing spec, settling the rule proposed in #7658, how these names yield to existing components (Core's keyword fallback is not kept: astryx component Sheet now returns an integration's Sheet instead of Core's BottomSheet), and whether existing packages get these names in a patch without an opt-in. Independently of that decision, please fix these two issues:

  • When a doc's file stem is a Core name and its name is another export (Badge.doc.mjs naming AcmeBadge), component Badge and swizzle Badge still exit with ERR_AMBIGUOUS_COMPONENT, while doctor integration components no longer reports the conflict and gap-report Badge routes to Core, so that stem needs one rule on every surface.
  • The derived discovery caches are keyed only on the first file's size and mtime, so a long-lived process keeps a re-exported member after its file stops exporting it.

[Automated review]

owner =>
owner.package === CORE_PACKAGE ||
owner.docPath == null ||
docStem(owner.docPath) === (owner.name ?? name),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This keeps the stem of a doc that is recorded under another export as a file-named owner. When that stem is a Core name (Badge.doc.mjs naming AcmeBadge, beside a Badge.tsx that exports only AcmeBadge), component Badge and swizzle Badge still exit with ERR_AMBIGUOUS_COMPONENT, but doctor integration components no longer reports the conflict, because it reads the discovered name AcmeBadge, and gap-report Badge routes to Core. Treating such a stem as doc-recorded here would let it yield to Core, as the export name already does, and bring detail and swizzle in line with gap-report and Doctor.

importedModule(integration, record.docPath, doc) ?? record.sourcePath;
if (module == null) return [];
return names.flatMap(name => {
const sourcePath = readOnce(`member\0${module}\0${name}`, [module], () =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This cache is keyed only on module's size and mtime, but moduleExportSource follows re-exports into other files. In one process, after slide.tsx stops exporting AcmeSlide while index.ts (export * from './slide') is unchanged, discovery still records AcmeSlide. The name cache above, keyed on the doc and source only, has the same gap. Parsed modules are already cached per file, so dropping these derived caches, or stamping every file the answer read, avoids it.

@josephfarina
josephfarina force-pushed the josephfarina/doc-names-another-export branch from 9b990ad to 1e5c825 Compare October 17, 2026 21:48
@josephfarina

Copy link
Copy Markdown
Contributor Author

Addressed the third automated review in 1e5c825:

  • The stem of a doc recorded under another export: it now yields like the export name does. With Badge.doc.mjs naming AcmeBadge, component Badge, --props, swizzle Badge and gap-report Badge all return Core's Badge, so every surface agrees and Doctor has no conflict to report. A test with that fixture fails without the fix.
  • Stale caches: the export-name and member caches are gone. Each answer goes through the per-file parse cache, so a long-lived process sees a re-exported file change. A test that changes the re-exported file mid-process fails without the fix.
  • Core's keyword fallback: this is intended. A keyword fallback is a guess, not an owner, and an exact name, including one a doc records, beats it: with no Core component named Sheet, component Sheet returns a package's documented Sheet, and component BottomSheet is unchanged. The guard protects what an exact lookup answers today. If it covered fallbacks too, a documented name would be unreachable whenever any Core keyword resembled it. spec(AST-039, cli-surface): integration docs are found by their documented names and checked in one command #7658's FR4 now says so explicitly.
  • Size: 1,992 authored lines (20 files, +1,888 −104).

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This needs a maintainer decision before it can be approved: no current spec covers finding an integration component by a doc name that differs from its file name, or treating a family doc's full components entries as components (spec:AST-039 is still a draft, and its FR4 derives the export from the file stem). A maintainer needs to provide or identify the governing spec, settling the rule proposed in #7658, how these names yield to existing components (Core's keyword fallback is not kept: astryx component Sheet now returns an integration's Sheet instead of Core's BottomSheet), and whether existing packages get these names in a patch without an opt-in. The earlier review findings are fixed, and independently of the spec decision, one regression remains to fix:

  • astryx gap-report Slides, for a Slides.doc.mjs that names AcmeCarousel (or a kebab-case artifact-viewer.doc.mjs), now routes the report to Core, while the current release routes it to the integration and component Slides and swizzle Slides still resolve to the integration, so gap-report should match a renamed doc's file name the way those commands do.

[Automated review]

...new Set(
records
.filter(
unscopedComponentOwners(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This owner set sees only discovered names, and a doc recorded under another export is now discovered only under that export. So gap-report Slides, for a Slides.doc.mjs that names AcmeCarousel, finds no owner and falls back to Core (line 204). Meanwhile component Slides and swizzle Slides still resolve to the integration, and the current release routes this report to the integration. Matching records whose doc stem equals bareComponent here as well, before unscopedComponentOwners, would keep Core for a Core-named stem (Badge) and route Slides to its package.

@josephfarina
josephfarina force-pushed the josephfarina/doc-names-another-export branch from 1e5c825 to 154b3a1 Compare October 18, 2026 00:22
@josephfarina josephfarina changed the title feat(cli): integration components are found by the names their docs document feat(cli): a component doc can name a differently named export of the file beside it Oct 18, 2026
@josephfarina
josephfarina force-pushed the josephfarina/doc-names-another-export branch from db4b024 to e75b79f Compare October 18, 2026 01:52
@josephfarina

Copy link
Copy Markdown
Contributor Author

Addressed the latest automated review in e75b79f:

  • A renamed doc's file name and a legacy package: the legacy guard now applies only to export names a doc records. With a legacy package documenting Slides, component Slides (with --props and --source too) answers from the integration's Slides.doc.mjs as on main, which matches swizzle Slides. Tested.
  • Search and legacy packages: when a legacy package documents a name the integration records as an export, the integration's search result carries astryx component <name> --package <pkg>, since the bare name opens the legacy doc. Tested.
  • Both checks fail without their fix. Size: 1,923 authored lines. spec(AST-039, cli-surface): integration docs are found by their documented names and checked in one command #7658's FR4 states the narrower legacy rule.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This needs a maintainer decision before it can be approved: no current spec lets a component doc's name bind a differently named export of its same-stem file (spec:AST-039 is still a draft whose FR4 derives the export from the file stem, and #7658 has no approval), so a maintainer needs to provide or identify the governing spec, settling that rule, how these names yield to existing answers (for example, astryx component Sheet now returns a package's Sheet instead of Core's BottomSheet), and whether existing packages get this in a patch without an opt-in. The earlier legacy-docs findings are fixed; independently of the spec decision, one small fix remains:

  • astryx gap-report <Name> still sends a name a doc records to that integration when a hidden file-named doc in another package, or a legacy docs package, owns the name in astryx component, so gap-report should apply the same owner rule as component and search (main sends both reports to Core).

[Automated review]

)
.map(r => ({...r, hold: 'core'})),
...project.loadedIntegrations.flatMap(integration =>
discoverIntegrationComponents(integration).flatMap(r =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This owner set misses two owners that component and search count. discoverIntegrationComponents skips hidden docs, so another package's hidden AcmeTab.doc.mjs never makes @acme/widgets' recorded AcmeTab yield, and no legacy check runs, so a legacy docs package documenting AcmeCarousel does not either. Both reports now go to @acme/widgets, while component AcmeTab answers with the hidden doc's package, component AcmeCarousel shows the legacy doc, and main sends both reports to Core. Counting every same-stem doc as a file owner here (through findIntegrationComponent, as detail and swizzle do) and dropping doc owners of an export name a legacy docs package documents would restore main's routing.

@josephfarina
josephfarina force-pushed the josephfarina/doc-names-another-export branch from 3fb88dc to ba786cc Compare October 18, 2026 02:43
@josephfarina

Copy link
Copy Markdown
Contributor Author

Addressed the latest automated review in ba786cc:

  • gap-report and the owner rule: gap-report now counts another package's hidden file-named doc (found the way the component lookup finds it) and a legacy docs package before it routes. In both cases the recorded name gives way and the report goes to Core, as on main. Tests: the hidden variant of "leaves a name to another package's file-named doc even when that doc is %s", and the gap-report step of the legacy-docs test. Both fail without the fix.
  • The notes at component.mjs and search.mjs: those are the legacy-guard items fixed in the previous push (legacyExternalKeeps in the adapter, and --package in search). Nothing open remains there.
  • Size: 1,962 authored lines.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This needs a maintainer decision before it can be approved: no current spec lets a component doc's name bind a differently named export of its same-stem file (spec:AST-039 is still a draft whose FR4 derives the export from the file stem, and #7658 has no approval), so a maintainer needs to provide or identify the governing spec, settling that rule, how these names yield to existing answers (for example, astryx component Sheet now returns a package's Sheet instead of Core's BottomSheet), and whether existing packages get this in a patch without an opt-in. The earlier gap-report findings are fixed for the exact spelling; independently of the spec decision, one small fix remains:

  • astryx gap-report matches names without case, but its new hidden-doc and legacy-package checks use the name as typed, so gap-report acmetab or gap-report acmecarousel still sends the report to the integration that only records the name, where gap-report AcmeTab and main send it to Core.

[Automated review]

Comment on lines +212 to +221
const found = findIntegrationComponent(integration, bareComponent);
return found?.hold === 'file'
? [{...found, package: integration.name}]
: [];
}),
]);
// A legacy docs package keeps an export name a doc records, as in detail.
const owners = [
...new Set(
records
(legacyExternalKeeps(project.cwd, bareComponent, ruled) ? [] : ruled)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

selectTarget matches owners without case (lines 197–205), but this lookup and the legacy check on line 221 run on the typed bareComponent: findIntegrationComponent checks the exact same-stem path, and legacyExternalDocuments compares legacy file names exactly. So gap-report acmetab (another package's hidden AcmeTab.doc.mjs, on a case-sensitive file system) and gap-report acmecarousel (a legacy package documenting AcmeCarousel) still route to @acme/widgets, while gap-report AcmeTab, gap-report AcmeCarousel and main route to Core. Running both checks on the matched records' names, or comparing without case, would make the spellings agree; a lower-case variant in the two gap-report assertions would cover it.

@josephfarina
josephfarina force-pushed the josephfarina/doc-names-another-export branch from ba786cc to 4e70a55 Compare October 18, 2026 03:22
@josephfarina

Copy link
Copy Markdown
Contributor Author

Addressed in 4e70a55: gap-report's hidden-doc and legacy-package checks now run on every spelling its case-blind match finds, as well as the one typed. gap-report acmetab and gap-report acmecarousel now route to Core like the exact spellings and main. Both tests gained the lower-case variant, and each fails without the fix. Size: 1,982 authored lines.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This needs a maintainer decision before it can be approved: no current spec lets a component doc's name bind a differently named export of its same-stem file (spec:AST-039 is still a draft whose FR4 derives the export from the file stem, and #7658 has no approval), so a maintainer needs to provide or identify the governing spec, settling that rule, how these names yield to existing answers (for example, astryx component Sheet now returns a package's Sheet instead of Core's BottomSheet), and whether existing packages get this in a patch without an opt-in. The case-variant gap-report fix works; independently of the spec decision, one small fix remains:

  • When a doc records a Core name that astryx component resolves only through Core's sub-component lookup (such as CheckboxIndicator or BaseTable), astryx component CheckboxIndicator returns Core's, but astryx search offers the package's result with that same bare command and astryx gap-report CheckboxIndicator files with the package, so search and gap-report should count Core the way component does (findComponentReadme).

[Automated review]

// The owner rule, as component detail applies it: a candidate `component
// <name>` does not open names its package and follows the owner, and a
// file-name alias that opens another owner is dropped.
const coreNames = new Set(core.map(candidate => candidate.name));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

coreNames holds only the names Core's component list includes (discoverComponents), but component detail also counts Core as an owner wherever findComponentReadme resolves, which covers more names, such as CheckboxIndicator and BaseTable. For a doc that records CheckboxIndicator, opens returns true, so this result keeps the bare astryx component CheckboxIndicator, which opens Core's component instead. Checking findComponentReadme(coreDir, name) here as well would give that hit --package.

const ruled = unscopedComponentOwners([
...records
.filter(
r => r.package === CORE_PACKAGE && r.name.toLowerCase() === wanted,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Core counts as an owner here only for names in project.components(), which come from discoverComponents. Component detail and swizzle also count Core wherever findComponentReadme resolves, so gap-report CheckboxIndicator, for a doc that records CheckboxIndicator, now files with the package, while component CheckboxIndicator answers with Core and main sends the report to Core. Adding a core owner when findComponentReadme(coreDir, spelling) resolves would keep the surfaces together.

@josephfarina
josephfarina force-pushed the josephfarina/doc-names-another-export branch from 4e70a55 to 61969eb Compare October 18, 2026 04:02
@josephfarina

Copy link
Copy Markdown
Contributor Author

Addressed in 61969eb: search and gap-report now count Core for every name Core's doc lookup finds (findComponentReadme), as component does. That covers sub-component names such as CheckboxIndicator, which now stays Core's for the bare name in search, whose package result carries --package, and in gap-report. The Core-sub-component test now uses CheckboxIndicator, and its gap-report and search steps each fail without the fix. Size: 1,992 authored lines.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This needs a maintainer decision before it can be approved: no current spec lets a component doc's name bind a differently named export of its same-stem file (spec:AST-039 is still a draft whose FR4 derives the export from the file stem, and #7658 has no approval), so a maintainer needs to provide or identify the governing spec, settling that rule, how these names yield to existing answers (for example, astryx component Sheet now returns a package's Sheet instead of Core's BottomSheet), and whether existing packages get this in a patch without an opt-in. The sub-component fix for doc-recorded names works; independently of the spec decision, one small fix remains:

  • For an integration doc named after its file, astryx gap-report CheckboxIndicator (or BaseTable, or another Core name only findComponentReadme finds) now exits with ERR_AMBIGUOUS_COMPONENT where main sends the report to that package, so the Core owners gap-report adds from findComponentReadme should only make doc-recorded names yield and should not route reports themselves.

[Automated review]

Comment on lines +220 to +223
...[...spellings].flatMap(spelling => {
const coreDir = findCoreDir(project.cwd);
const docPath = coreDir && findComponentReadme(coreDir, spelling);
return docPath ? [{package: CORE_PACKAGE, docPath, hold: 'core'}] : [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These Core owners also count against file-named owners. For a CheckboxIndicator.doc.mjs beside a CheckboxIndicator.tsx that exports CheckboxIndicator, astryx gap-report CheckboxIndicator now exits ERR_AMBIGUOUS_COMPONENT, while main routes the report to the integration, because main's owners come only from project.components(), which omits the Core names only findComponentReadme resolves (about 52, such as BaseTable). Marking these owners so they still make a doc owner yield, then dropping them in the final filter on lines 246–248 the way the hidden file-named docs drop out, keeps main's answer for file-named docs and Core's for doc-recorded ones; a file-named CheckboxIndicator case in integration-export-name.test.mjs would pin it.

… file beside it

A doc beside a source that exports no runtime name after its stem may set
`name` to another runtime export of that source (Slides.doc.mjs naming
AcmeCarousel, acme-widget.doc.mjs naming AcmeWidget). Discovery records the
component under that export unless the name is taken: every doc's file name,
hidden docs included; a Core name a doc in the package replaces; a name an
earlier doc records (paths ordered by code unit, the same in every locale).

One owner rule decides every exact unscoped lookup: an owner that holds a name
only because a doc records it (the export name, or the file name of a doc
recorded under another export) yields to Core and to every component named
after its file, valid or not. Component detail (--props, --source, batch),
swizzle (Core holds every name it documents), gap-report (which also answers
to a renamed doc's file name), and search (commands and aliases) apply it after
an active replacement and before any check of which docs load. --package still
reaches the doc-recorded owner.

integration verify reads exports with the shared module-exports reader, so a
component re-exported through a folder's index file passes. A doc that fails to
load is reported under its file name. Module and doc caches key on size, mtime,
ctime and inode, so a file npm rewrites with the same mtime is read again.
@josephfarina
josephfarina force-pushed the josephfarina/doc-names-another-export branch from 61969eb to 7db5b0f Compare October 18, 2026 04:47
@josephfarina

Copy link
Copy Markdown
Contributor Author

Addressed in 7db5b0f. In gap-report, a Core name that only Core's doc lookup finds now only makes a doc-recorded name yield; it no longer routes the report itself. A doc named after its file (BaseTable.doc.mjs naming BaseTable) routes reports to its package again, as on main, while a doc that records CheckboxIndicator under another file name still yields and the report goes to Core. The test covers both, and the BaseTable step fails without the fix with ERR_AMBIGUOUS_COMPONENT. Size: 1,987 authored lines.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This needs a maintainer decision before it can be approved: no current spec lets a component doc's name bind a differently named export of its same-stem file (spec:AST-039 is still a draft whose FR4 derives the export from the file stem, and #7658 has no approval), so a maintainer needs to provide or identify the governing spec. That decision should settle the identity rule, how these names yield to existing answers (for example, astryx component Sheet now returns a package's Sheet instead of Core's BottomSheet, and astryx component Badge returns Core's Badge where it was ambiguous), and whether existing packages get this in a patch without an opt-in. The earlier gap-report regression is fixed, and nothing else in the code needs to change while that decision is pending.

[Automated review]

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This looks ready to approve. The rule it implements — an integration component doc's name is the runtime export it documents, and a name recorded only that way never takes a lookup that Core, a legacy docs package or a file-named component answers — is now current in architecture:cli-surface INV24, with the precedence details spelled out in spec:AST-039 FR4, and the answers that change for existing packages (astryx component Sheet, astryx component Badge, and integration add component refusing a recorded name) follow that rule. Listing, lookup, search, swizzle, gap-report, Doctor and integration verify behave as described, the targeted CLI tests and CI pass, and the branch merges cleanly with the integration verify change now on main, so no changes are needed.

[Automated review]

@josephfarina
josephfarina merged commit ebcc404 into main Oct 18, 2026
53 checks passed
josephfarina added a commit that referenced this pull request Oct 18, 2026
…ion-guides-cleanup

ComponentDoc.name takes #7659's sentence about the export apps import in place of the file-name rule. The versioning list keeps this branch's published-version bullets and adds #7674's family-member bullet. hiddenComponents now says what #7674's discovery does in an integration: it hides full entries in the doc's own `components`, and a sub-component with its own doc still uses `hidden`.
@github-actions
github-actions Bot deleted the josephfarina/doc-names-another-export branch October 19, 2026 06:30

This branch was successfully deployed

1 active deployment
Preview — 7db5b0f6 Deployed Oct 18, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant