/** * ComponentIndexBuilder — discover Eddie components and parse their metadata. * * The write side of what used to be one `ComponentIndex` class: it scans the * monorepo, reads TypeScript sources and Storybook stories, and produces a * populated `ComponentIndex`. * * It is a separate module because it imports the TypeScript compiler (for * `extractCompositionContract`, which reads the `composition` export from the * AST rather than by regex — #1685). The serverless MCP bundle is produced by * walking the static import graph, so anything reachable from the MCP entry * gets inlined; with the parser inside `ComponentIndex`, every consumer of * `getComponent()` pulled in 12.26 MB of compiler and #1732 followed. * * **Nothing on the MCP path may import this module.** The hosted Brain reads a * pre-built graph; only `cli/brain.ts` builds one from source. The bundle-size * and bundle-content assertions in `test/serverless-bundle.test.ts` are what * keep that true. */ import { ComponentIndex } from './component-index.js'; export declare class ComponentIndexBuilder { /** * An `ed-*` tag in markup: group 1 is the closing slash, group 2 the tag. * Shared so the flat scan and the nesting walk cannot drift apart. */ private static readonly TAG_PATTERN; /** * Ancestor|descendant pairs harvested from every file scanned this build. * Applied in `build()`, once every component exists to attach them to. */ private nestingPairs; private components; /** Cached `@property` declarations of the EdFormElement base class. */ private formElementProperties; /** * Build the component registry by scanning the Eddie codebase. * * Scans four packages: * * eddie-web-components — core Lit components (depth 1: /.ts) * eddie-recipes — recipe components (depth 2: //.ts) * eddie-pages — full-page templates (depth 2: //.ts) * eddie-charts — chart recipe components (depth 2: //.ts) * * All four are walked by scanPackage, which handles depth-agnostic traversal * and enforces the basename-matches-containing-dir convention. eddie-charts * is a category recipe library: its components share the `ed-r-*` tag prefix * and `ed-r-*` CSS class prefix with eddie-recipes, but live in a separate * package so that the Chart.js runtime is opt-in for consumers who don't * need chart surfaces. */ build(rootDir: string): Promise; /** * Close `composesWith` under the reverse direction, once every entry exists. * * The Component Documentation Standard's gate 2 asks for both halves of the * relationship — *containers list what they contain; children list what wraps * them* — but only the container half can be read from a component's own * source. Nothing in `link-list-item.ts` or its stories mentions `ed-link-list` * or `ed-menu`; the relationship is only visible from the other side. Before * #2013 the reverse half came from hand-written `@related` tags, which is why * it was simultaneously incomplete and full of alternatives. * * So: after the scan, every A that names B gives B back A. That makes the * field symmetric by construction, which is also what makes it checkable — an * asymmetry now means a dropped entry rather than an unwritten JSDoc line. * * The cost is that a ubiquitous atom carries a long list: `ed-icon` is * rendered by dozens of components, and it now says so. That is the honest * answer to "what composes with ed-icon", and a shorter wrong one was what * #2013 filed. * * Tags naming something outside the index (a component in a package that was * not scanned) are dropped rather than invented, so every entry resolves. */ /** * Fold the build-wide nesting pairs into the components they name. * * Deferred to here because a pair is harvested from whichever file happened * to demonstrate it, which is usually neither of its two components — so * there is nothing to attach it to until every entry exists. */ private applyNestingPairs; private invertComposesWith; /** * Scan a package directory for components. * * Walks any depth under the package root and picks up files that follow * the Eddie component convention: every component file lives at * `/.ts` where the file's basename matches * its containing directory's basename. That gives us: * * eddie-web-components: components/button/button.ts * components/card/card.ts * → depth 1 under pkgDir * * eddie-recipes: recipes/common/project-card/project-card.ts * recipes/we-are-here/wah-logo/wah-logo.ts * → depth 2 under pkgDir (extra project-name level) * * Historic bug: the previous glob was a two-level pattern (star slash star * dot ts) which only matched depth 1, so the entire eddie-recipes catalog * was invisible to the indexer. `ed-r-project-card` and 11 other real, * shipping recipe components never made it into components.json. Fix: walk * any depth via a double-star glob, then enforce the basename-matches- * containing-directory convention to filter helpers, types, and barrel * files that happen to sit next to real components. */ private scanPackage; /** * Parse a single component file and extract metadata. * * @param componentName The component's base name (e.g. "button", "project-card") * @param filePath Absolute path to the component's .ts file * @param componentDir Absolute path to the component's own directory * (contains the .ts, .scss, .stories.ts, and test/) * @param relativeDir Path from the package root to the component dir, * e.g. "button" (eddie-web-components) or * "common/project-card" (eddie-recipes) * @param pkgName Which package this component lives in * @param rootDir Repo root (unused today but passed for future use) */ private parseComponent; /** * What a page must load before this component's markup does anything (#1635). * * Derived from the package rather than declared per component, because that * is where the requirement actually lives — and because 48 hand-written * copies would drift the first time a component changed packages. * * Eddie ships one autoloader per package, and only two packages have one. * A page that loads the components autoloader and writes `ed-r-*` markup gets * an element that never upgrades: it renders its light-DOM children bare and * says nothing. That is #1634 (primary nav at 1:1 contrast) and #1616 * (missing banner landmark) — both of which first read as component bugs and * neither of which is. * * Recipes therefore need BOTH loaders, and the string says so, because the * half-loaded case is the one people actually hit. */ private static runtimeFor; /** * Every `ed-*` tag named in a chunk of markup, opening or closing. * * Deliberately direction-blind at this level: `composesWith` is one * "composes with" list rather than two directed ones, and the nesting walk * below is what recovers structure where it matters. */ private static tagsInMarkup; /** * Ancestor → descendant pairs implied by a chunk of markup. * * This is what a flat tag list cannot see. `ed-p-grid`'s story contains both * `ed-grid-item` and `ed-card`, but only their NESTING says a card goes * inside a grid item — the composition `CLAUDE.md` calls the one that breaks * most often. Same for `ed-table-row` → `ed-table-cell`, `ed-page` → * `ed-header`, and `ed-toolbar` → `ed-menu`: every one of those is * demonstrated in some story, and none of them is derivable from the two * components' own files. (`ed-toolbar` → `ed-button` used to be the example * here and is no longer true: the toolbar's only button sits inside the * overflow `ed-menu`, so the nearest-ancestor rule gives it to the menu.) * * Only the NEAREST `ed-*` ancestor counts, not every one on the stack. Plain * HTML between two Eddie tags is skipped, so a card inside a `
` inside a * grid item still pairs with the grid item — but a page that wraps everything * in an `ed-layout` does not thereby "compose with" the card four levels * down. Every relationship this is meant to recover (grid→grid-item, * grid-item→card, table-row→table-cell, page→header, toolbar→button) is a * nearest-ancestor one; transitive pairs add reach without adding meaning. * * Markup inside a Lit template is not guaranteed to balance — a conditional * branch can open a tag one arm closes — so a closing tag that does not match * the stack top pops to the nearest match and is otherwise ignored. That * loses pairs rather than inventing them. */ private static nestingPairsInMarkup; /** * The text of every Lit `html` tagged template in a source file. * * Parsed with the TypeScript compiler rather than scanned by hand. A previous * hand-rolled scan counted `${` but not a bare `{`, so the `}` closing an * arrow-function body inside an interpolation ended the template early — and * it did so silently, in one real component (`ed-select-field`, whose trailing * `` sits after a `.map((item) => { … })`). A parser that reports a * wrong answer with no error is the exact failure mode #2013 was filed about, * so this uses the compiler that is already a dependency of this file. * * Template text includes `${…}` spans verbatim, which is what we want: a tag * rendered in a conditional branch composes exactly as much as an * unconditional one. */ private static htmlTemplates; /** * Record the composition a file demonstrates: its tags, and their nesting. * * Nesting is recorded on the BUILDER rather than on the component, because * the file that demonstrates a relationship is usually neither of the two * components in it. `ed-card` inside `ed-grid-item` is shown by a page's * story; neither `card.stories.ts` nor `grid-item.ts` knows about it. */ private harvestMarkup; /** * Harvest the NESTING a component's stories demonstrate. Contributes no tags * of its own (#2052). * * A container rarely imports what it holds and rarely renders it either — the * children arrive through a slot, so the only place they are written down is * the stories file. `ed-menu` imports none of `ed-link-list`, * `ed-link-list-item` or `ed-menu-separator`, and renders none of them; its * stories show all three. That gap is why this source exists. * * It used to close the gap by adding EVERY `ed-*` tag in the file to the * component's own `composesWith`, which pairs it with everything its stories * mention regardless of structure — and structure is the whole question. * Two ways that went wrong, both found while writing #2049's menu stories: * * - **Sharing an ancestor read as composition.** `ed-kbd`'s knockout story * puts an `ed-heading` and an `ed-text-passage` inside one `ed-card`, and * the `` inside the passage. The flat scan paired `ed-kbd` with * `ed-heading` — its UNCLE. The same shape at the same level pairs true * siblings: an `ed-r-icon-card` beside an `ed-r-stat-card` is a * side-by-side comparison, not one holding the other. * - **Descendants several levels down read as composition.** `ed-toolbar` * picked up `ed-link-list-item` and `ed-menu-separator`: rows inside the * menu's panel, which a toolbar does not contain in any useful sense. * `ed-table` picked the same pair up SIX levels down, through * `ed-table-body`, `ed-table-row`, `ed-table-cell`, `ed-menu` and * `ed-link-list`. * * It also paired a base recipe with its own subclasses — `ed-r-timeline-node` * with all six `ed-r-timeline-node-*` variants, which share a stories file. * A variant is an alternative, not a content: `@related` is where that lives. * * The nesting walk `harvestMarkup` already performs answers the original * worry better than the flat scan did, because it pairs each tag with its * NEAREST `ed-*` ancestor: `ed-menu` → `ed-button` + `ed-link-list`, and * `ed-link-list` → `ed-link-list-item` + `ed-menu-separator`. A separator * genuinely sits inside the list, not inside the menu. So the precise source * subsumes the imprecise one on the very case the imprecise one was added * for, and only the nesting pairs are kept. * * Imports, the component's own templates and `@anatomy` are untouched. */ private harvestStoryNesting; /** * The `ed-*` tags an `@anatomy` tag claims this component is made of. * * `@anatomy` is the authored answer to "what is this thing made of" — the * containment question — where `@related` answers "what else should I know * about", which is mostly alternatives (#2013). The two were conflated; only * one of them belongs here. * * It is also the one authored source that is already policed: `npm run * check:doc-parity` resolves every `ed-*` an `@anatomy` names against the * template AND against `composesWith`, and fails the build on a claim neither * supports. So an `@anatomy` entry cannot drift into wishful thinking the way * a `@related` entry could, and dropping it from this derivation is what * broke that gate on the first attempt at this fix. */ private static anatomyTags; /** * Extract the list of Eddie component tag names referenced via `import` * statements in the source. * * Recognizes the canonical Eddie import paths: * * import '@brad-frost-web/eddie-web-components/components/button/button.js'; * → ed-button * import '@brad-frost-web/eddie-recipes/recipes/common/site-header/site-header.js'; * → ed-r-site-header * import '@brad-frost-web/eddie-pages/pages/common/homepage/homepage.js'; * → ed-p-homepage * * The `.js` extension is optional here: cross-package specifiers carry one so * Node ESM can resolve them (#1349), while same-package relative imports and * older call sites may not. * * The trailing `//` segment is the convention enforced by * scanPackage (component basename matches its containing directory). We * map the basename to a tag name using the same package → prefix table * the indexer uses for cssClassName. * * Imports that don't match this shape (e.g. `import { html } from 'lit'`, * `import styles from './foo.scss?inline'`, EdElement base-class imports) * are ignored. Side-effect imports without a `from` (the most common form * for Eddie component registration) and named/default imports are both * matched. * * The resulting list is deduplicated and sorted for stable output across * builds (eliminates a class of dirty-diff in the catalog snapshot). */ private extractComposesWith; /** * Map a `@brad-frost-web//` import specifier to its Eddie tag * name, or return null if the specifier doesn't resolve to a component. * * Handles the three component-bearing packages: * * eddie-web-components/components// → ed- * eddie-recipes/recipes/// → ed-r- * eddie-pages/pages/// → ed-p- * * The specifier may carry an explicit `.js` extension — cross-package * imports need one so Node ESM can resolve them (#1349) — so the extension * is stripped before the `/` convention is checked. * * Returns null for non-component imports such as the `EdElement` base * class, theme/token CSS, asset paths, or any other shape we don't * recognize as a registered Eddie tag. */ private importPathToTagName; /** * Extract `use` / `dontUse` / `accessibility` guidelines from the class-level * JSDoc. Recognizes three custom tag forms: * * @use → adds one entry to guidelines.use * @dontuse → adds one entry to guidelines.dontUse * @a11y → adds one entry to guidelines.accessibility * * Tags can appear multiple times in the same JSDoc block, with each * occurrence becoming a separate bullet in the corresponding array. The * tag text runs from after the tag name through every wrapped continuation * line — see `collectTagBodies`. * * This is the counterpart to authoring-side guidelines in the component * source files. Today the Eddie catalog is sparse on guidelines; this * parser is the mechanism that lets new guidelines actually land in * eddie-brain's output. */ /** * Return the CLASS-level JSDoc block — the `/** ... *\/` immediately * preceding `export ... class`. * * Historic bug (#1242): every extractor used * `content.match(/\/\*\*[\s\S]*?\*\/\s*export class/)`, which starts at the * FIRST `/**` in the file and spans to the class. When a `const`/type with * its own JSDoc precedes the class (e.g. `ed-alert` / `ed-toast`'s * "Default icon names…" doc on an internal icon map), that unrelated block * was swallowed and its first paragraph became the component's `intent` — * so genuinely rich class docs never reached the brain. This helper scopes * to the text before `export class` and returns the LAST JSDoc block there, * i.e. the one that actually documents the class. Returned in the legacy * `RegExpMatchArray` shape (`[0]` = the block) so callers are unchanged. */ /** * Strip JSDoc comment furniture from a single raw line — the opening `/**`, * the leading ` * `, and a trailing `* /` — leaving just the prose. */ private static stripJsDocFurniture; /** * Collect the FULL body of every occurrence of a JSDoc tag, joining wrapped * continuation lines into one string. * * Historic bug: every guidance-tag extractor used * `new RegExp('@' + tag + '\\s+([^\\n]+)', 'gi')`, which stops at the first * newline. Any tag whose body wrapped across JSDoc lines — which is most of * the well-documented ones, since the doc standard asks for full sentences — * was silently truncated mid-clause in `components.json`, and that truncated * text is exactly what `eddie_get_component` hands to agents. `ed-r-visualzzz` * shipped `"As the \`background\` slot of \`ed-hero\` or \`ed-band\` — a * full-bleed"` and the sentence just stopped. * * A tag body starts at the tag and runs through every following interior * comment line, ending at whichever comes first: * * - the next `@tag` at the start of a line, * - a blank ` *` line (so a trailing prose paragraph after a tag block * never bleeds into the last tag's body), * - the end of the comment (`* /`), * - any line that isn't comment interior (this method is also run over * whole source files for `@slot`, so code after a comment must not be * mistaken for a continuation). * * Lines are joined with single spaces and whitespace is collapsed, so the * hanging indent authors use to align continuations under the tag text * (`@dontuse Many times on one page — each embed…`) disappears cleanly. * * Tag matching is case-insensitive and anchored to the start of the line, so * an `@` inside prose can't open a spurious body. */ private collectTagBodies; private matchClassJsDoc; private extractGuidelines; /** * Extract `@antipattern` tags from the class-level JSDoc (#1888): * * @antipattern — — fix: * @antipattern (warning) — — fix: * * The selector must fit the grammar in `analyze/anti-pattern-selector.ts`; * a tag that does not parse fails `init` with the component named, because * a dataset carrying a selector the matcher cannot evaluate is worse than * no dataset. Em-dash separators, continuation lines as for `@use`. */ private extractAntiPatterns; /** * Extract `@overridableSlot` and `@overridableProp` tags from class-level * JSDoc. These mark the specific slots/props a consumer is expected to use * to customize the component — distinct from `slots` (which lists every * slot that exists) and `properties` (which lists everything the class * declares). See #642 for why we need both. * * Tag shapes: * `@overridableSlot name — purpose` * `@overridableSlot `name` purpose` * `@overridableProp name — purpose` * (any of em-dash, en-dash, or hyphen between name and purpose; optional) */ private extractOverridableSurface; /** * Extract the recipe classification from a `@recipe` JSDoc tag on the class. * * `@recipe composition` — an assembly of `ed-*` components acting as * chrome; adapt the canonicalUsage skeleton. * `@recipe declarative` — a single-purpose, prop-driven component. * `@recipe exception — ` — a composition intentionally locked. * * Only the three canonical values are recognized; anything else (or an absent * tag) yields `undefined`. See #1259 and * `docs/RECIPES-COMPOSITION-VS-DECLARATIVE.md`. */ /** * Spacing doctrine metadata (docs/SPACING.md, #1647), derived rather than * declared so it cannot drift from the stylesheet: * * - `rhythmOwner` + `rolesConsumed` come from the component's SCSS: a * `gap`/`row-gap`/`column-gap` declaration consuming one of the six * spacing ROLE tokens marks the component as owning its children's * rhythm. Internal `size()` gaps (icon lockups, control internals) do * NOT count — the flag means "don't add your own spacing in here", * which is only true of the doctrine's official owners. * - `overhang` comes from an `@overhang` JSDoc tag on the class — the * declared "this intentionally renders outside its box" marker the * adjacency gate (#1648) and reviewers can trust. */ private extractSpacing; private extractRecipeKind; /** * Extract the canonical invocation of the component. * * Sourced from two places: * - Optional `@canonicalUsage` JSDoc tag on the class → prose `note` * - The `Default` export in the component's stories file → `default` * markup (only when the story uses `html\`...\`` — imperative * `document.createElement` stories are skipped because they have no * static markup to extract) * * The goal (per #642): when a consumer asks "use the default site header", * an agent can call `eddie_get_component` and get back the exact markup to * paste. No interpretation, no guessing. */ private extractCanonicalUsage; /** * Extract a `@contentApi { ... }` JSDoc tag from the class-level comment. * * The tag value is a JSON-ish object literal (single quotes or double quotes * accepted, trailing commas tolerated). Three keys are recognized: * * contentVia — string description of the primary content path * labelVia — "prop" | "slot" | "none" * labelVisibility — "visible" | "hidden" | "accessible-only" * * Components that don't declare the tag get `undefined`. See #627: this is * the structural answer to "where does this component's content come from?", * letting consumers and validators detect prop-vs-slot mismatches before * they render as silent drops. */ private extractContentApi; /** * Parse the `Default` story template literal out of a stories file. * * Recognizes the common Eddie story shape: * * export const Default = () => html``; * export const Default = (args: Args) => html`...`; * export const Default = () => html` * * ... * * `; * * Returns undefined for imperative stories (`document.createElement(...)`) * which have no extractable markup — consumers of those recipes interact * via JS properties rather than markup and don't benefit from a canonical * markup string. */ private extractDefaultStoryMarkup; /** * Extract @property decorated properties */ private extractProperties; /** * Extract (and cache) the reactive `@property` declarations of the * EdFormElement base class, so form-associated components can report the * members they inherit — chiefly the controlled `value` accessor. * * EdFormElement.ts sits one level up from a core component's own directory * (`components//` → `components/EdFormElement.ts`). If it can't be found * (unexpected layout), inherited props are simply omitted rather than failing * the whole parse. */ private getFormElementProperties; private extractJsDocBefore; /** * Extract enum options from a string-literal union type annotation. * * Returns the list of literal values when the annotation is composed * ENTIRELY of quoted string literals joined by `|` (e.g. `'sm' | 'md' | 'lg'` * → `['sm', 'md', 'lg']`), otherwise undefined. A single literal counts. * * The previous implementation matched `'x' | 'y'` pairs globally and then * stripped quotes across each whole match, so a three-member union like * `'a' | 'b' | 'c'` collapsed to the single mangled entry `['a | b']` and * dropped `'c'` entirely (#1015). Matching each quoted literal individually * fixes both the mangling and the dropped tail. */ /** * Collapse a captured type annotation to a single canonical line: internal * whitespace (including the newlines of a one-member-per-line union) becomes * a single space, and TypeScript's optional leading `|` is dropped. This is * what lets `extractEnumOptions` treat a Prettier-wrapped union exactly like * an inline one, and keeps the emitted `type` string free of raw newlines. */ private normalizeTypeAnnotation; /** * Resolve a bare type-alias annotation to its string-literal members when the * alias is defined in the same source file. Two idioms are supported: * * type Variant = 'a' | 'b' | 'c'; (direct union alias) * const VARIANTS = ['a', 'b'] as const; * type Variant = typeof VARIANTS[number]; (derived from a const array) * * The second is how components share a value list between runtime validation * and typing (e.g. ed-timeline-node's `TimelineNodeVariant`). Without this, * such a prop published NO `options` and agents had no way to learn its * valid values. Returns the members, or undefined when the alias can't be * resolved locally (imported aliases stay unresolved — better no options * than guessed ones). */ private resolveLocalTypeAlias; private extractEnumOptions; /** * Extract slot definitions from JSDoc comments. * * Parses the Eddie JSDoc slot convention, which comes in these forms: * * @slot - The default slot description. (default slot, leading dash) * @slot header - Optional header content (named slot, name before dash) * @slot `header` Optional header content (named slot, backticked name) * @slot Just a default description (default slot, no dash, bare prose) * * Also looks for `` in the render template as a secondary * source for slots that weren't explicitly documented in JSDoc. * * Historic bug: the previous regex `/@slot\s*(?:-\s*)?([^\n]*)/` consumed * the optional leading `- ` and then an inner regex `/^`?(\w+)`?/` grabbed * the first word of the remaining text as the slot name. For a default slot * like `@slot - The grid items`, this produced slot name "The" (the first * word of "The grid items") instead of "default". Bug fix: distinguish the * four cases above explicitly. */ /** * Read the `composition` export — which parts of a template are shell and * which are fillable regions (#1685). * * Parsed from the TypeScript AST rather than by regex or `eval`: the value is * a nested literal, so brace-matching is fragile and evaluating source at * index time would run first-party code for no benefit. The AST walk reads * only literals and ignores anything computed, so a non-literal entry is * dropped rather than guessed at. */ private extractCompositionContract; private extractSlots; /** * Returns true if the given string has balanced parens, braces, and * brackets. Used to reject truncated expressions (like multi-line method * calls that span past the regex capture boundary) from being emitted as * property default values. */ private hasBalancedDelimiters; /** * Extract custom events from dispatch calls */ private extractEvents; /** * Extract CSS custom properties referenced */ /** * The `--ed-*` custom properties a component CONSUMES, read from its SCSS. * * This is the component→token consumption index: it backs * `findComponentsUsingToken()` (the `usedBy` field on `eddie_get_token`) and * `findTokensUsedByComponent()`. It is NOT a list of knobs the component * exposes — `@cssprop` declarations are a separate concern and deliberately * not merged here, because mixing declared knobs into a consumption index * would make `usedBy` claim a component consumes a token it only defines. * * Two bugs made this return `[]` for 162 of 163 components until #1803: * * 1. It was handed the component's `.ts` source, not its `.scss` — despite * the comment that said otherwise. Almost nothing writes `var(--ed-*)` * in TypeScript, so it found nothing. The lone populated component, * `ed-p-authentication`, was a page emitting inline styles in its * template, which is why the bug read as "one component uses tokens". * 2. `var\((--ed[^)]+)\)` stopped at the first `)`. Any fallback carrying * parens — `var(--a, var(--b))`, `var(--a, #{size(2)})`, which is most * real usage — captured a malformed name AND swallowed the inner token. * * The regex now matches only the property name and stops there, so the * fallback is left to the global scan, which finds any nested token on its * own pass. * * The `.ts` file is still scanned as well: a handful of components (and most * pages) set custom properties in inline styles inside their templates, and * those are genuine consumption too. */ private extractCssCustomProperties; /** * Extract intent/purpose from JSDoc comment. * * Parses the class-level JSDoc preceding `export class` and returns the * first real description paragraph. A "paragraph" is a sequence of * consecutive non-blank, non-@tag lines joined with spaces. * * Heading detection: Eddie components sometimes begin their JSDoc with a * one-word title line (e.g. "Grid", "Button") on its own, separated from * the actual description by a blank line. In that case the first paragraph * is just the single heading word, which is not a useful intent. If the * first paragraph is a short heading-like line AND a second paragraph * exists, return the second paragraph instead. * * Historic bug: the previous implementation used `.slice(0, 1)` on the * filtered lines, which returned only the first non-blank non-@ line * regardless of context — so `ed-grid`'s intent came back as literally the * single word "Grid" while its real description lived on the next * paragraph. Bug fix: paragraph-aware extraction. */ private extractIntent; /** * Get atomic level from the component's own stories file. * * `componentDir` is the component's actual directory (e.g. * `packages/eddie-web-components/components/button` or * `packages/eddie-recipes/recipes/common/project-card`). The stories file * lives directly inside it as `.stories.ts`. */ private getAtomicLevel; /** * Extract the functional group from the Storybook title's middle segment. * * "Recipes/Global/Site Header" → "Global" * "Molecules/Layout & Containers/Layout" → "Layout & Containers" * "Atoms/Text/Heading" → "Text" * * Case is preserved (unlike getAtomicLevel, which lowercases for matching). * Titles with no middle segment (e.g. "Recipes/Foo") yield `undefined`. This * is the grouping axis shared with the component catalog, so the graph can * answer "which card / block recipes exist?". See #1259 sweep. */ private extractStoryCategory; /** * Derive the owning project from an asset's relative directory. Recipes, * pages, and charts live under `//` (e.g. `common/site-header`, * `we-are-here/wah-logo`), so the first path segment is the project scope. * Core components are flat (`button/`) — no scope, returns `undefined`. This * is the ownership axis, orthogonal to `category`. */ private deriveProjectScope; /** * Determine parent/child relationships for compound components */ private determineCompoundRelationship; /** * Convert class name to display name */ private classNameToDisplayName; } //# sourceMappingURL=component-index-builder.d.ts.map