/data-assets/companies and /data-assets/people shell-injected; DeckStatsPanel lifts the homegrown chroma stats tile into shared shell ground
Chroma's home page already had a reviewer audit panel — the four-tile row above the matrix counting variants, slide files, people, companies — but the People and Companies tiles pointed at /data/team/ and /data/investors/ which were never actual routes. The audit pages themselves existed in calmstorm-decks (as /data-assets/companies and /data-assets/people) but had never been ported to the shell. This entry closes both gaps. Two new shell-injected routes — /data-assets/companies and /data-assets/people — discover .md files via consumer-root Vite globs that catch both calmstorm's single-firm data layout (data/firms/{firm}/portfolio|team/) and chroma's multi-investor layout (data/investors/{firm}/portfolio/, flat data/team/) without configuration. A new <DeckStatsPanel> shell component replaces chroma's local AssetsDataPanel, doing its own count math from registries + globs so the figures always match the audit pages. The work also surfaced three Astro-pipeline traps worth documenting: JSDoc terminators inside path strings, esbuild misclassifying inline block comments + slash-heavy regex literals as unterminated regex, and Astro's <Image> taking down a 373-row page over one corrupted SVG in the consumer's data tree.
/data-assets/companies and /data-assets/people shell-injected; DeckStatsPanel lifts the homegrown chroma stats tile into shared shell ground
Why care?
The goal stated explicitly this evening: move everything but the underlying content and data to the shell. The shell should provide the UI; consumer sites should provide the deck content and the firm/people/company .md records. After today's lift, every UI surface for the reviewer audit — the stats tile row on the home page, the per-firm grouped companies table, the role-grouped people table — lives in @dididecks/shell. Chroma's pages/index.astro is now 100 lines lighter and stops carrying logic that belongs at the shell tier.
The four-tile row was working visually but broken functionally: People and Companies tiles linked to /data/team/ and /data/investors/, neither of which was a route in chroma — they were never wired up after the calmstorm-style page was sketched. Today's work moves the destinations into existence (as shell-injected routes) and points the tiles at them.
What's new
/data-assets/companies — shell-injected reviewer audit
apps/deck-shell/src/routes/data-assets/companies.astro— full-page audit of every portfolio company .md the consumer ships underdata/**/portfolio/. Grouped by firm with sticky section headers per investor; one row per .md regardless of cross-firm duplicates (chroma's portfolio overlap shows up as the same company appearing in 2-4 sections, which is the reality we want surfaced).9-column table matches calmstorm's audit shape: Logo / Favicon / Name / Homepage / Sector / Description / og / Strategy / Status.
Image colocation: each .md's logo/favicon resolves by dirname-prefixed lookup (
<mdDir>/<filename>) so cross-firm filename collisions (favicon__Anduril.pngexists in bothquiet-capital/portfolio/andaix-ventures/portfolio/) don't conflate.Flagged rows highlighted: amber background + chip when
flagged: trueis set in frontmatter.Summary stats at top:
373 entries across 4 firms, plus logo/favicon/og-url/linked-CEO/flagged tallies. Sticky table headers track scroll within each firm section.
/data-assets/people — shell-injected people audit
apps/deck-shell/src/routes/data-assets/people.astro— companion to companies. Globsdata/**/{team,portfolio}/*.md, filters to entries with arole_classfrontmatter field. (Portfolio .md files for companies don't carryrole_class; portfolio .md files for portco-CEOs in calmstorm's pattern do. The filter is the right structural test for "is this a person.")Grouped by role_class in a known-first order (VC Team → Supporting Partners → Advisors → LPAC → Portfolio CEOs → Portfolio CPOs → Founders → Investors → Operators → Team) so reviewers scan "all advisors", "all VC team" coherently. Unknown role_classes follow, alphabetically.
8-column table: Photo / Name+slug / Title+Org+Deck-role / Links (LinkedIn/Twitter/GitHub/website pills) / Affiliation (firm + company slugs) / Bio short / Confidence pill / Status pill.
<DeckStatsPanel> shell component — replaces chroma's local AssetsDataPanel
apps/deck-shell/src/components/DeckStatsPanel.astro— embeddable 4-tile stats row for any consumer's landing page. Props:deckSlug(the only one needed). Computes counts itself from the same registries the routes use:Variants: from
loadDecksRegistry.Slide files / registered slots / static-port count: from
loadSlotsRegistry+perSlideFileExistsper (variant, slot).People count:
data/**/{team,portfolio}/*.mdfiltered byrole_class.Headshot count: filtered people whose colocated headshot file exists.
Companies:
data/**/portfolio/*.mdlisting count + distinct slug count + firm count via path extraction.
Tile CTAs point at the audit routes that now exist:
Design Variants →
/scroll/{deck}/Slide Files →
/toc/{deck}/People →
/data-assets/people← fix (was the broken/data/team/)Companies →
/data-assets/companies← fix (was the broken/data/investors/)
Themed via
--ddd-stats-*tokens with fallback chain through consumer color-system tokens (--color-background,--color-surface,--color-border, etc.) and finallycolor-mix(in srgb, currentColor)neutral defaults. Inherits the surrounding palette without explicit theming when a consumer has any of those token tiers in place.
<DididecksNav> extension — Data link added
New nav row entry:
Data→/data-assets/companies. Active state highlights when on either/data-assets/companiesor/data-assets/people(theisActive("/data-assets/")prefix check catches both). Inserted betweenPlayandChangelogin nav order — workflow-tier and reviewer-tier separated visually.
Chroma's pages/index.astro swapped to consume shell components
<AssetsDataPanel overview={overview} />→<DeckStatsPanel deckSlug={overview.deckSlug} />The local
AssetsDataPanel.astro+loadDeckOverviewdata path remain in tree but are now unreferenced by the swapped index. (Earlier today we left them feeding the variant cards above the matrix; those cards still consumeoverviewso the local lib stays load-bearing until the variant-card surface migrates too.)
Astro-knots reminder published
astro-knots/context-v/reminders/Astro-Frontmatter-Avoid-Inline-Block-Comments-and-Risky-Regex.md— written up the parser trap that bit twice during the lift. Includes the diagnostic signature (Unterminated regular expressionwith column position past EOF), three concrete workarounds (line comments over inline block comments; string methods over regex literals; hoisting regex to top-of-file const declarations), and an explicit "where this does NOT apply" section so the reminder doesn't over-apply. Companion to the existing[[YAML-Frontmatter-Parsing-Must-Be-Lenient]]reminder.
The story
Two layouts, one shell
Calmstorm-decks puts a single firm's data at data/firms/calm-storm-ventures/{portfolio,team}/*.md. Chroma-decks puts four investor firms' portfolios at data/investors/{firm}/portfolio/*.md and people flat at data/team/*.md. Same conceptual data, different file layouts. The first instinct was to add a dataAssets config option to the shell integration so each consumer could declare its own glob patterns. Rejected after one minute's thought: import.meta.glob requires literal string arguments at Vite's parse stage, not runtime config. Glob patterns can't be configured per consumer; they have to be hard-coded.
The fix that worked: pick glob patterns that cover both layouts simultaneously.
Companies:
/data/**/portfolio/*.mdcatchesdata/firms/.../portfolio/ANDdata/investors/.../portfolio/AND any future shape that puts portfolio underdata/.People:
/data/**/{team,portfolio}/*.mdwith arole_classfrontmatter filter catches the cross-layout reality — calmstorm puts portco-CEOs underportfolio/, chroma puts everyone underteam/. The role_class filter is the structural test, not the path.Firm grouping: path-based extraction.
data/firms/{firm}/portfolio/anddata/investors/{firm}/portfolio/both surface the firm slug at the same path position; a tinyfirmSlugFromPathregex (later string-method'd, see traps below) handles both.
The result: zero config knobs on the shell side. The two consumers shipping today and any third consumer that follows the same data/(any)/(portfolio|team)/ convention gets the audit pages for free.
The three Astro-pipeline traps
Trap 1 — JSDoc terminator inside path documentation. The people route's header comment originally read:
* One row per person .md found by the glob. "Person" = any .md under
* /data/**/team/ OR /data/**/portfolio/ that carries a `role_class`
* frontmatter field.The /data/**/team/ contains the sequence **/ — and */ ends a JSDoc block. The comment terminated early on line 6; the rest was parsed as code; build failed with "Expected ; but found portfolio" at line 6 col 30. Fix: rewrite path examples in prose inside JSDoc, or wrap them in backticks.
Trap 2 — Inline /* */ block comment misparsed as regex literal. DeckStatsPanel.astro originally had:
const staticPorted = authoredSlotFiles; /* every per-slide file is by definition a static-play port */esbuild's TS pre-parser saw ; and re-entered expression position. The /* that followed got classified as the start of a regex literal rather than a block comment. Build failed with "Unterminated regular expression" at a column position past the end of the line — the parser was reporting where the regex should have terminated, not where it started. The diagnostic signature was confusing because the highlighted line was usually a one-character }.
A second variant of the same trap: complex multi-slash regex literals (e.g. p.match(/^\/data\/(?:firms|investors)\/([^/]+)\/portfolio\//)) in expression-adjacent positions can trip the same regex-vs-divide ambiguity, especially when chained with ??. The patterns themselves are valid; the parser's classification of the surrounding context is the issue.
Fix: line comments over inline block comments; string methods (split, startsWith, endsWith, lastIndexOf) over slash-heavy regex literals when the logic is amenable; if regex is genuinely the right tool, hoist it to a top-of-file const.
Both lessons captured in the new astro-knots/context-v/reminders/Astro-Frontmatter-Avoid-Inline-Block-Comments-and-Risky-Regex.md reminder.
Trap 3 — Astro <Image> over consumer data is fragile. The first companies-page build failed on [NoImageMetadata] Could not load .../trademark__V7.svg. One corrupted SVG in chroma's 373-row data tree was enough to take down the page render. Astro's <Image> component runs metadata parsing on every asset; a malformed file in any asset blows the whole page. For audit pages over raw consumer-dropped data — where the whole point is to surface inconsistencies — <Image> is exactly the wrong choice.
Fix: use import.meta.glob with { query: "?url", import: "default" } to get plain URL strings, and render via raw <img src>. Skips the metadata pipeline entirely; one corrupted SVG renders as a broken image icon instead of killing the page. Worth a follow-up reminder if this recurs.
What the build flow looks like now
chroma's / chroma's /data-assets/companies chroma's /data-assets/people
│ │ │
▼ ▼ ▼
<DeckStatsPanel> [shell-injected route] [shell-injected route]
├ DECKS reg ├ glob /data/**/portfolio/*.md ├ glob /data/**/{team,portfolio}/*.md
├ SLOTS reg ├ glob /data/**/portfolio/*.{png,…} ├ glob /data/**/{team,portfolio}/*.{png,…}
├ /data/**/team/* ├ frontmatter parser (in-file) ├ frontmatter parser (in-file)
├ /data/**/portfolio/* ├ firm grouping by path ├ role_class filter
└ tile counts └ table render └ role_class grouping → table renderEverything to the right of "chroma's data" is shell code. The consumer side is just the data tree and the page that mounts <DeckStatsPanel> / <DeckMatrix> / etc.
How it works (or "Under the hood")
Layout-agnostic globs via ** zero-or-more matching
Vite's import.meta.glob (under fast-glob) supports ** as "match zero or more path components." That property is what lets one glob pattern cover both layouts:
// Catches:
// /data/firms/calm-storm-ventures/portfolio/foo.md (calmstorm — depth 4)
// /data/investors/quiet-capital/portfolio/anduril.md (chroma — depth 4)
// /data/portfolio/foo.md (hypothetical — depth 2)
const mdGlob = import.meta.glob<string>(
"/data/**/portfolio/*.md",
{ eager: true, query: "?raw", import: "default" },
);For people, the union pattern {team,portfolio} plus the role_class content filter means a single glob covers calmstorm's per-firm team dir, chroma's flat team dir, and calmstorm's portco-CEO .md files that live under portfolio/. The filter happens in-component, not in the glob, so the structural test for personhood is data-driven (presence of role_class) rather than path-driven (which dir it's in).
Why <DeckStatsPanel> does its own counting
The local AssetsDataPanel in chroma was fed by a loadDeckOverview library that wrapped the same registry/glob calls. Two reasons not to take that library into the shell:
The shell already has direct access to
loadDecksRegistry,loadSlotsRegistry, andperSlideFileExists. Wrapping those in a separate overview library is one layer of abstraction the shell doesn't need — the panel can do its own counting in 30 lines.The panel's data should always match the audit pages exactly. Having the panel and the audit page read from the same globs guarantees consistency; routing both through an intermediate library is a place where the two views could quietly diverge.
The chroma-side loadDeckOverview still exists because the variant cards above the matrix consume it. When those variant cards migrate to a shell component (likely a future iteration), the local library can retire.
Resilient image rendering for audit pages
// Note: NOT { eager: true } returning ImageMetadata. Plain URL imports:
const imageGlob = import.meta.glob<string>(
"/data/**/portfolio/*.{png,jpg,jpeg,webp,avif,svg}",
{ eager: true, query: "?url", import: "default" },
);
// In the template, plain <img> instead of Astro's <Image>:
<img src={logo} alt={`${c.name} logo`} loading="lazy" />Astro's <Image> component is the right choice for end-user-facing pages where optimization matters and the asset set is curated. For reviewer audit pages over consumer-dropped data, the audit is exactly the surface that should be able to render a broken thumbnail rather than 500 the whole page — that's how the reviewer learns there's a broken file to investigate.
What's next
Commit this batch. Two new routes, two new shell components, nav extension, chroma index swap, astro-knots reminder. Bundle.
Delete chroma's local
AssetsDataPanel.astro+ theloadDeckOverviewlib after the variant cards above the matrix also migrate to a shell component (one more iteration). Don't delete yet — the variant cards still consume the library.Variant cards migration. Chroma's home page still carries a row of variant cards (between the stats panel and the matrix) with Scroll / TOC / Play / Status chips per variant. Same pattern as the stats panel: would benefit from being a shell component. Maps cleanly to a future
<DeckVariantCards>companion to<DeckStatsPanel>.Calmstorm migration to the shell is the bigger lift waiting. Calmstorm-decks doesn't yet consume
@dididecks/shellat all — it's the reference implementation we're porting from. Once chroma has consumed all the shell's UI surfaces and the API is stable, calmstorm can switch over.Image-resilience pattern → reminder. If
<Image>over consumer data takes us down a third time, write up the "use?urlfor audit surfaces over uncurated assets" lesson as a sibling to today's frontmatter-parser reminder.Layout-agnostic glob discipline → skill reference. The
**zero-or-more pattern + content filter combo is a generalizable shape worth a paragraph in theastro-knotsskill once it recurs.
Related
[[2026-05-17_01]] — morning bulk shell entry (A++.2, audits v2, DeckMatrix initial). Today's data-assets routes complete the audit surface that DeckMatrix started.
[[2026-05-17_04]] — afternoon DeckMatrix iteration (vacuous-Ready fix, merged rollup pill, chip filter, OPEN retired). Today's
<DeckStatsPanel>is the sibling chrome above that matrix.[[../apps/deck-shell/src/routes/data-assets/companies.astro]] — companies audit route.
[[../apps/deck-shell/src/routes/data-assets/people.astro]] — people audit route.
[[../apps/deck-shell/src/components/DeckStatsPanel.astro]] — landing-page stats tile component.
astro-knots/context-v/reminders/Astro-Frontmatter-Avoid-Inline-Block-Comments-and-Risky-Regex.md— new reminder; companion to the existingYAML-Frontmatter-Parsing-Must-Be-Lenient.
