Skip to content

refactor: migrate to pnpm and enhance header component flexibility#38

Merged
moshloop merged 10 commits into
mainfrom
feat/streaming-render-security
May 12, 2026
Merged

refactor: migrate to pnpm and enhance header component flexibility#38
moshloop merged 10 commits into
mainfrom
feat/streaming-render-security

Conversation

@moshloop
Copy link
Copy Markdown
Member

@moshloop moshloop commented Apr 27, 2026

What

  • Migrate build tooling from npm to pnpm across documentation, tasks, and examples
  • Add logoClass and className props to FlanksourceHeader for flexible styling
  • Refactor Header component to support custom className and style props
  • Move hardcoded padding from CSS to component-level DEFAULT_CLASSES
  • Add Fira Code as primary monospace font in Tailwind config
  • Update .gitignore for .agent/ and .sandbox.yaml

Why

  • Improved dependency management with pnpm
  • Enable per-instance header customization without CSS modifications
  • Better component reusability

Summary by CodeRabbit

New Features

  • Facet Doctor Command: New facet doctor CLI command performs environment preflight checks with --json and -v/--verbose flags.
  • Badge Enhancements: Added 'label' variant for split label/value styling, 'xxs' size, wrapping support, and custom class overrides.
  • ListTable Grouping: Supports nested grouping, date bucketing, customizable date formats, and overflow handling.
  • StatCard Improvements: New 'summary' variant and optional shrink prop.
  • Avatar: Added 'xxs' size support.

Chores

  • Migrated package manager from npm to pnpm (8.13.1) across all workflows and scripts.
  • Updated tests to use Bun instead of Jest.
  • Removed deprecated CLI documentation files.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

Warning

Rate limit exceeded

@moshloop has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 53 minutes and 6 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f2e5e9cf-e6db-4946-a9ca-4edfc4f817e4

📥 Commits

Reviewing files that changed from the base of the PR and between 372c143 and 58da211.

⛔ Files ignored due to path filters (2)
  • cli/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • .github/workflows/release.yml
  • .github/workflows/test.yml
  • Dockerfile
  • cli/package.json
  • package.json
  • src/components/Document.tsx

Walkthrough

This PR migrates the project from npm to pnpm, introduces a facet doctor CLI command for environment validation, refactors dependency installation with robust retry handling, enhances the Badge and StatCard components with new variants and flexible sizing, updates component styling from gray to slate tokens, and refreshes documentation and examples.

Changes

pnpm Migration & Package Manager Infrastructure

Layer / File(s) Summary
Package Manager Pin
package.json, cli/package.json
Root and CLI packages now pin pnpm@8.13.1 as the package manager and update all scripts to use pnpm run/pnpm exec instead of npm.
Build, Test & TypeScript Config
cli/package.json, cli/tsconfig.json, cli/tsdown.config.ts
Migrate test runner from Jest to Bun, add gen-version script, update TypeScript types from jest to bun/DOM, mark external dependencies (bun, .css files).
CI/CD Workflows
.github/workflows/test.yml, .github/workflows/release.yml
Add pnpm setup steps, update caching to use pnpm lockfiles, switch dependency installation and build commands to pnpm across test, build-binaries, and publish jobs.
Docker Build
Dockerfile
Install pnpm globally in builder and runtime stages, copy pnpm lockfiles, use pnpm install --frozen-lockfile instead of npm installs.
npm Configuration & Documentation
.npmrc, cli/.npmrc, README.md, Taskfile.yml, examples/*/package.json, cli/README.md, .gitignore
Enable npm lifecycle scripts, update documentation and task examples to reference pnpm, remove obsolete CLI documentation files, add .agent/ and .sandbox.yaml to gitignore.

CLI Infrastructure & Doctor Command

Layer / File(s) Summary
Package Manager Resolver
cli/src/utils/package-manager.ts, cli/src/utils/package-manager.test.ts
New utility enforces pnpm usage, provides resolved PackageManager with version caching, searches upward for package.json pins, rejects non-pnpm managers with actionable errors.
Doctor Preflight Command
cli/src/cli.ts, cli/src/commands/doctor.ts
Implement facet doctor command with --json and --verbose flags that validate Node version, CPU architecture, pnpm availability, native bindings, Chromium resolution, FACET_PACKAGE_PATH, version alignment, and .npmrc auth leakage.

FacetDirectory & Dependency Installation

Layer / File(s) Summary
FacetDirectory Refactoring
cli/src/builders/facet-directory.ts, cli/src/builders/facet-directory.test.ts
Migrate from hash-based npm caching to pnpm-driven installs, add isInstallBroken() detection, implement nukeInstall() and cleanup APIs, merge consumer npmrc/overrides into build graph, remove stale pnpm-lock.yaml on package.json changes, generate .facet/.npmrc by merging settings.
Vite Builder & Retry Logic
cli/src/bundler/vite-builder.ts, cli/src/bundler/vite-builder.test.ts
Introduce installWithRetry with pre-clean/reinstall on failure, add isMissingDepError classification, wrap loader invocation with runLoaderWithRetry for automatic missing-dep recovery.
Remote Package Resolution
cli/src/utils/remote-resolver.ts, cli/src/utils/remote-resolver.test.ts
Switch npm package installation to pnpm add with custom .npmrc setup and retry-on-failure logic.
HTML Generation & Cache
cli/src/generators/html.ts, cli/src/server/routes.ts, cli/src/server/facet-cache.ts
Update Tailwind invocation to pnpm exec, remove legacy hash-based facet-cache module, retain .facet cleanup.

Component Library Enhancements

Layer / File(s) Summary
Exported Type Updates
cli/src/server/facet-types.ts
Expand Badge to support xxs and numeric pt sizes, add label variant, change label/value to ReactNode, add styling props; add shrink to StatCard.
Size Resolution Utility
src/components/utils/resolveSizeVariant.ts
Centralized helper for size/variant lookup with deduplicated warnings and component context.
Badge & Avatar Components
src/components/{Badge,Avatar,AvatarGroup}.tsx
Expand Badge with xxs/label variant/flexible sizing, add Avatar xxs size, implement ReactNode content and split styling for label variant.
Component Size Resolution
src/components/{BulletList,CalloutBox,ProgressBar,ScoreGauge,Shield}.tsx
Use resolveSizeVariant for graceful handling of unknown size/variant values.
StatCard Enhancements
src/components/StatCard.tsx
Add summary variant, introduce shrink prop, use resolveSizeVariant, add noBreakStyle control, migrate neutral colors to slate.
Other Components
src/components/{Header,Status,Finding,ListTable,Document}.tsx
Add className/style to Header, improve Status accessibility, narrow Finding types, refactor ListTable with grouping/Badge rendering/slate colors, sanitize CSS in Document.
Typography & Colors
src/components/SeverityStatCard.tsx, tailwind.config.js, src/styles.css
Update to slate colors and explicit sizing, add Fira Code font, remove print header padding.

Documentation & Examples

Layer / File(s) Summary
Storybook & Kitchen-Sink
src/stories/components/Badge.stories.tsx, examples/kitchen-sink/*.tsx, examples/quickstart/README.md, cli/vite-ssr-loader.ts
Add Badge label/wrap stories, introduce FindingsReport example, update header sizing variants, improve loader error formatting and bundle retention control.

Possibly Related PRs

  • flanksource/facet#25: Modifies Badge component with the same props (labelClassName, valueClassName, wrap, xxs/label variants) as this PR, suggesting direct collaboration on Badge enhancements.
  • flanksource/facet#24: Both PRs modify CLI build/version generation and Docker/workflow configurations (GIT_COMMIT propagation, prebuild/release automation), indicating related infrastructure work.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/streaming-render-security
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/streaming-render-security

…dge and Avatar

Introduce 'xxs' (extra extra small) as a new named size preset for Badge, Avatar, and AvatarGroup components. Refactor Badge sizing to use a named size map with consistent mm-based measurements for container padding, icon dimensions, and gaps. Add support for numeric font sizes (in pt) that interpolate layout dimensions from named presets, enabling custom typography while maintaining consistent spacing.

Update ListTable to use Badge component for tags instead of inline spans, improving consistency and maintainability. Replace gray color palette with slate throughout components for better visual hierarchy.

Add resolveSizeVariant utility to safely resolve variant lookups with fallback behavior and console warnings for unknown values, preventing SSR crashes from typos.

Update example documentation and package.json scripts to use pnpm instead of npm.

BREAKING CHANGE: Badge size type now includes 'xxs' and number; components using hardcoded 'xs' should be reviewed for appropriate sizing.
…eader component flexibility

- Replace npm with pnpm across all documentation, task definitions, and examples for improved dependency management
- Add logoClass and className props to FlanksourceHeader for flexible styling and sizing
- Refactor Header component to support custom className and style props with merged utility function
- Remove hardcoded padding from CSS and move to component-level DEFAULT_CLASSES for better control
- Update HeaderSolid example with detailed comments showing 30mm hero and 14mm compact header configurations
- Add Fira Code as primary monospace font in Tailwind config
- Update .gitignore to exclude .agent/ and .sandbox.yaml files

These changes improve component reusability and allow per-instance header customization without CSS modifications.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
cli/src/server/routes.ts (1)

497-501: ⚠️ Potential issue | 🟠 Major

Silent fallback masks pnpm missing at runtime.

Same pnpm exec concern as cli/src/generators/html.ts Line 114 (see comment there). Additionally, the catch on Line 499–501 only emits at debug level — if pnpm isn't on PATH on the server host, every render silently falls back to the (likely incomplete) Vite CSS without operators noticing. Consider logging at warn level here, or using a direct binary path as suggested in the root-cause comment.

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

In `@cli/src/server/routes.ts` around lines 497 - 501, The catch currently
swallows errors from the shell invocation of pnpm (`await $`cd ${facetRoot} &&
pnpm exec tailwindcss -i ${stylesInput} --content ${tempHtmlPath} -o
${outputCssPath}`.quiet()`) and only logs at debug level; change the catch to
accept an error (e) and log a warning with the error details via logger.warn
including which command failed and relevant variables (facetRoot, stylesInput,
tempHtmlPath, outputCssPath), so missing pnpm on PATH is visible at runtime;
additionally, consider using a direct binary path (or a resolved pnpm exec path)
instead of relying on plain `pnpm` in the shell to avoid PATH issues.
.github/workflows/test.yml (1)

22-53: ⚠️ Potential issue | 🟠 Major

Workflow still uses npm despite the PR's stated pnpm migration.

The PR objective is "migrate to pnpm", but this workflow continues to:

  • cache against cli/package-lock.json (line 27), which no longer exists (repository now uses cli/pnpm-lock.yaml),
  • run npm install and cd cli && npm install (lines 46–47),
  • run cd cli && npm run build:lib (line 53).

Since the repository has migrated to pnpm (evidenced by pnpm-lock.yaml at root and cli, and "packageManager": "pnpm@8.13.1" in package.json), this workflow will fail: the cache step will not find the referenced npm lockfile, and npm install will not respect the pnpm lockfiles.

Switch the setup, cache, install, and build steps to pnpm (e.g., pnpm/action-setup, cache: "pnpm", pnpm install --frozen-lockfile, pnpm --filter cli build:lib).

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

In @.github/workflows/test.yml around lines 22 - 53, The workflow still uses
npm-specific steps and cache keys (actions/setup-node with cache-dependency-path
referencing cli/package-lock.json, and the commands npm install, cd cli && npm
install, cd cli && npm run build:lib) but the repo uses pnpm/pnpm-lock.yaml;
update the job to use pnpm (replace actions/setup-node step to include pnpm via
pnpm/action-setup or install pnpm, change cache: "npm" to cache: "pnpm" and
cache-dependency-path to refer to pnpm-lock.yaml files, replace npm install
commands with pnpm install --frozen-lockfile (and run from root only or use pnpm
--filter cli install where appropriate), and replace cd cli && npm run build:lib
with pnpm --filter cli build:lib) so the CI respects pnpm lockfiles and caching.
examples/kitchen-sink/UberKitchenSink.tsx (1)

1-40: ⚠️ Potential issue | 🟡 Minor

Add AIModelCard to the CLI runtime types in facet-types.ts.

AIModelCard is properly exported from the package's public API (src/components/index.tsx), but it is missing from cli/src/server/facet-types.ts. Templates rendered through the CLI will lack TypeScript support for this component.

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

In `@examples/kitchen-sink/UberKitchenSink.tsx` around lines 1 - 40, The CLI
runtime type definitions are missing the AIModelCard component type; update the
facet runtime types by adding AIModelCard to the exported component/type list in
facet-types.ts (the same union/interface where other components like Badge,
StatCard, and Theme are declared), ensure its TypeScript type matches the public
export signature (as in src/components/index.tsx), and export it so templates
rendered through the CLI receive proper typings.
cli/src/utils/pdf-multipass.ts (1)

499-566: ⚠️ Potential issue | 🟡 Minor

Silent skip when placeholders are present but browser or html is missing.

When headerHasPlaceholders.get(key) is true and either browser or html is undefined, the placeholder branch is skipped, embedded remains undefined, and the header (or footer) is not drawn at all for that page. Since embeddedHeaders.set(key, …) was also skipped at line 507 specifically because placeholders were detected, there is no pre-embedded fallback either.

Both arguments are optional in the new signature, so a caller that passes only pageSizeMap (or only one of browser/html) silently loses headers/footers on pages that were supposed to receive page numbers. At minimum, log a warning when this combination occurs so the regression isn't invisible; consider falling back to the unsubstituted overlay (await embedOverlay(doc, buf)) so static portions still render.

🛡️ Suggested guard
   for (const [key, buf] of overlays.headers) {
     const has = htmlHasTokens || bufferHasPlaceholders(buf);
     headerHasPlaceholders.set(key, has);
-    if (!has) embeddedHeaders.set(key, await embedOverlay(doc, buf));
+    if (!has || !browser || !html) embeddedHeaders.set(key, await embedOverlay(doc, buf));
   }
   for (const [key, buf] of overlays.footers) {
     const has = htmlHasTokens || bufferHasPlaceholders(buf);
     footerHasPlaceholders.set(key, has);
-    if (!has) embeddedFooters.set(key, await embedOverlay(doc, buf));
+    if (!has || !browser || !html) embeddedFooters.set(key, await embedOverlay(doc, buf));
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/src/utils/pdf-multipass.ts` around lines 499 - 566, The code currently
skips rendering headers/footers when headerHasPlaceholders.get(key) or
footerHasPlaceholders.get(key) is true but browser or html are missing; update
the branches in the header and footer handling (the blocks using
headerHasPlaceholders/footerHasPlaceholders, embeddedHeaders/embeddedFooters,
overlays.headers/overlays.footers, renderElementPdf, and embedOverlay) to detect
this condition, emit a warning (so the regression is visible) and fall back to
embedding the raw overlay (await embedOverlay(doc, buf)) so static parts still
render; apply the same change for both the header and footer paths and ensure
the fallback assigns embedded so page.drawPage still runs.
🧹 Nitpick comments (26)
README.md (1)

402-405: Redundant explicit prepublishOnly invocation.

prepublishOnly is a lifecycle script that npm/pnpm run automatically before publish — invoking it manually duplicates the build. Either drop the manual step (just document npm publish / pnpm publish) or update the comment to clarify it's only for testing the prepublish step in isolation.

Minor — pre-existing nit surfaced by this diff.

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

In `@README.md` around lines 402 - 405, The README currently shows an explicit
pnpm run prepublishOnly followed by npm publish which duplicates npm/pnpm's
automatic running of the prepublishOnly lifecycle; update that snippet to either
remove the manual prepublishOnly call and show just "npm publish" / "pnpm
publish", or keep the manual command but change the comment to state it's only
for testing the prepublishOnly step in isolation (reference the prepublishOnly
lifecycle and the npm publish / pnpm publish commands in the change).
package.json (1)

62-62: source-map-support as a runtime dep — intentional for consumers of the library?

@flanksource/facet is published as a library (per main/exports). Listing source-map-support under dependencies means every downstream consumer of the React component library installs it, even though it's primarily useful at the Node CLI runtime. If it's only needed by the CLI, consider moving it to cli/package.json's dependencies (and out of root) so library consumers don't pull it transitively.

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

In `@package.json` at line 62, The package.json currently lists
"source-map-support" as a root dependency, which makes every consumer of the
published library install it; move "source-map-support" out of the root
"dependencies" and into the CLI package's dependencies instead (e.g., add it to
cli/package.json dependencies and remove it from the root package.json) so only
the CLI runtime pulls it; update any root code that imports it to either
lazy-require from the CLI entrypoint or ensure the CLI package provides the
import path.
src/components/PageNo.tsx (1)

9-18: Document the placeholder semantics — bare _PG_/_TL_ will render in non-PDF contexts.

When this component is rendered in a browser/Storybook (i.e. anywhere outside the PDF multipass that substitutes _PG_/_TL_), users will see literal _PG_ / _TL_ text. That's surprising without a doc comment. A short JSDoc on the component noting that the output is intended to be post-processed by cli/src/utils/pdf-multipass.ts would prevent confusion.

📝 Proposed JSDoc
+/**
+ * Renders a placeholder span whose text is later substituted with the actual
+ * page number / total during PDF post-processing (see cli/src/utils/pdf-multipass.ts).
+ * In non-PDF rendering contexts the raw `_PG_`/`_TL_` markers will be visible.
+ */
 export default function PageNo({
   format = '${page} / ${total}',
   className,
 }: PageNoProps) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/PageNo.tsx` around lines 9 - 18, Add a short JSDoc comment
above the PageNo component explaining that the format prop uses placeholders
PAGE_PLACEHOLDER (`_PG_`) and TOTAL_PLACEHOLDER (`_TL_`) which are intended to
be replaced later by the PDF multipass processor (see
cli/src/utils/pdf-multipass.ts), and note that in non-PDF contexts
(browser/Storybook) these raw placeholders will be rendered literally; reference
the component name PageNo and the symbols PAGE_PLACEHOLDER and TOTAL_PLACEHOLDER
so maintainers know where to look.
cli/src/utils/package-manager.test.ts (1)

59-59: Use the top-level fs/promises import instead of dynamic import().

fs/promises is already statically imported at line 2; pulling in mkdir again via await import('fs/promises') inside each test adds noise without benefit.

♻️ Diff
-import { mkdtemp, rm, writeFile } from 'fs/promises';
+import { mkdir, mkdtemp, rm, writeFile } from 'fs/promises';
@@
-    const nested = join(workDir, 'a', 'b');
-    const { mkdir } = await import('fs/promises');
-    await mkdir(nested, { recursive: true });
+    const nested = join(workDir, 'a', 'b');
+    await mkdir(nested, { recursive: true });
@@
-    const nested = join(workDir, 'child');
-    const { mkdir } = await import('fs/promises');
-    await mkdir(nested);
+    const nested = join(workDir, 'child');
+    await mkdir(nested);

Also applies to: 71-71

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

In `@cli/src/utils/package-manager.test.ts` at line 59, Replace the dynamic await
import of 'fs/promises' and its destructured mkdir with the top-level static
import already present in this test module: remove occurrences of "const { mkdir
} = await import('fs/promises')" and use the statically imported mkdir symbol
directly in the tests (both places where the dynamic import is used). This keeps
the code consistent with the existing import at the top and removes unnecessary
dynamic imports.
cli/src/utils/package-manager.ts (1)

79-79: Nit: ?? raw is unreachable — String.prototype.split always returns a non-empty array.

raw.split('@')[0] is string (never undefined), so the nullish fallback is dead. Either drop it, or simplify with pkg.packageManager.split('@', 1)[0]. Functionally fine either way.

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

In `@cli/src/utils/package-manager.ts` at line 79, Nit: the nullish fallback is
unreachable when computing the variable name from raw.split; remove the dead
fallback or simplify the split logic. Update the code that assigns name (the
variable computed from raw.split) to either drop the "?? raw" fallback entirely
or use a single-item split on the packageManager field (e.g., call split with a
limit of 1 on pkg.packageManager) so the result is clearly a string without
needing a fallback.
examples/kitchen-sink/package.json (1)

12-12: Consider link:../../ for the local facet dependency.

file:../../ works with pnpm but may copy the package at install time, requiring reinstallation to reflect changes in src/. With pnpm, link:../../ creates a direct symlink and provides live updates during development, which is typically better for in-repo examples.

If you set up a pnpm-workspace.yaml at the repository root, "@flanksource/facet": "workspace:*" would be another option, but that requires additional monorepo configuration.

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

In `@examples/kitchen-sink/package.json` at line 12, Update the local dependency
spec for "@flanksource/facet" in package.json to use a pnpm symlink instead of a
file copy by replacing the value "file:../../" with "link:../../" (or use
"workspace:*" if you add a pnpm-workspace.yaml); edit the dependency entry for
"@flanksource/facet" so installs create a live symlink to the repo source rather
than copying it.
src/components/Page.tsx (1)

14-24: product prop in PageProps is unused and should be removed or wired through to rendering.

The product prop is declared in the PageProps interface but never destructured or used in the Page component. However, multiple callers are already passing this prop (examples/kitchen-sink/TypographyTest.tsx, examples/kitchen-sink/UberKitchenSink.tsx, cli/src/server/playground-html.ts), suggesting it was intended to display in the page header. Either remove it from the public API to clarify the interface, or wire it through to the title/Header rendering logic.

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

In `@src/components/Page.tsx` around lines 14 - 24, The PageProps interface
declares a product prop that is never used by the Page component; either remove
it from PageProps or pass it into the header rendering—preferably wire it
through: update the Page component to destructure product from its props
(alongside title, titleClassName, etc.) and include it in the header/title
rendering logic (e.g., render product next to or below title in the Header/title
block, or pass product into the existing Header component if one is used) so
callers like TypographyTest and UberKitchenSink see the product string; if you
choose removal, delete product from PageProps and remove usages in callers.
src/components/CalloutBox.tsx (1)

60-60: Nit: helper name resolveSizeVariant reads oddly when applied to a non-size variant.

CalloutBox.variant is a semantic kind (info/warning/success/default), not a size. The lookup logic is identical, but naming the helper around "size" creates a small cognitive mismatch with every non-size caller (CalloutBox, and any others that key on color/theme/variant). Consider renaming to resolveVariant or resolveByKey in a follow-up; not worth blocking here.

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

In `@src/components/CalloutBox.tsx` at line 60, The helper name resolveSizeVariant
is misleading for semantic variants like CalloutBox.variant; rename the helper
(and its exported identifier) to something generic such as resolveVariant or
resolveByKey and update all usages (e.g., the call in CalloutBox that currently
uses resolveSizeVariant, the helper definition, any imports/exports, and tests)
so callers still pass the same args (variant, variantStyles, default,
'CalloutBox') but the function name reflects generic variant lookup; ensure
types/signatures remain unchanged and update any references in other components
to avoid breaking imports.
cli/package.json (2)

16-28: gen-version runs twice during pnpm run build; also consider pinning the package manager.

  1. prebuild runs gen-version, then prebuild:cli runs it again before build:cli. Not a correctness issue but redundant — drop one of them (typically prebuild:cli is sufficient since build:lib doesn't need the generated file, or rely solely on prebuild and remove prebuild:cli).
  2. Since the PR standardizes on pnpm, adding a top-level "packageManager": "pnpm@<version>" field makes the toolchain reproducible and lets Corepack pick it up automatically — otherwise contributors running npm install won't get a clear failure.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/package.json` around lines 16 - 28, Remove the redundant double
invocation of the gen-version script by deleting either the "prebuild:cli"
script or the "prebuild" script so "gen-version" runs only once during the build
(keep "prebuild:cli" if you want generation only before build:cli, or keep
"prebuild" to run it before the whole build chain); and add a top-level
"packageManager": "pnpm@<pin-version>" field to package.json to pin pnpm
(replace <pin-version> with the project’s chosen pnpm version) so Corepack and
contributors use a consistent package manager.

70-84: Remove stale Jest tooling now that tests run on Bun.

test and test:watch have been migrated to bun test, and the test files use bun:test imports, but jest, @types/jest, and ts-jest are still listed in devDependencies. They're unreachable from any script and only add install weight and supply-chain surface. Drop them unless something outside this PR still depends on them.

♻️ Proposed cleanup
   "devDependencies": {
     "@types/bun": "^1.3.0",
-    "@types/jest": "^29.5.0",
     "@types/node": "^20.11.0",
     "@types/node-forge": "^1.3.14",
     "@types/semver": "^7.5.0",
     "@types/react": "^18.3.12",
     "@types/react-dom": "^18.3.1",
-    "jest": "^29.7.0",
     "node-forge": "^1.3.3",
     "pdfassembler": "^0.1.0-beta.8",
-    "ts-jest": "^29.1.0",
     "tsdown": "^0.16.0",
     "typescript": "^5.6.3"
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/package.json` around lines 70 - 84, The devDependencies still include
stale Jest packages ("jest", "@types/jest", "ts-jest") even though tests were
migrated to Bun; remove those three entries from the "devDependencies" object in
package.json to eliminate unused install weight and supply-chain surface, and
then verify no scripts or other files reference Jest symbols before committing
(look for occurrences of "jest", "@types/jest", and "ts-jest" to confirm safe
removal).
cli/src/utils/remote-resolver.test.ts (1)

229-242: Opt-in network test depends on a third-party package; consider pinning to a more reliable fixture.

leftpad@0.0.1 is fetched live from the public registry. Even though this is gated behind FACET_TEST_NPM=1, using an unmaintained third-party package as a fixture leaves the test exposed to registry/availability flakiness, unpublishes, or supply-chain incidents. A tiny first-party fixture package (or a well-known stable package like is-number / @flanksource/*) would be a safer long-term choice.

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

In `@cli/src/utils/remote-resolver.test.ts` around lines 229 - 242, Replace the
external leftpad fixture in the itNetwork test by pointing npmRef to a small,
pinned test package maintained by us (or a highly stable public package); update
the npmRef object (repoUrl and ref fields) inside the itNetwork('resolveNpmRef
installs a real package via pnpm' ...) test and adjust the expected templateFile
and resolvedSha assertions to match the chosen fixture, then run the test to
confirm resolveRemoteRef still writes the expected file under
result.consumerRoot; specifically modify the npmRef declaration and the
expect(...) checks that reference result.templateFile and result.resolvedSha so
they align with the new fixture package and its file layout.
src/stories/components/AIModelCard.stories.tsx (1)

74-74: Minor: type MultipleCards as Story for consistency.

All other stories use Story (i.e. StoryObj<typeof meta>). Bare StoryObj here loses the meta linkage and the controls/argTypes inference, which is a small consistency wart even though MultipleCards only uses render.

♻️ Proposed change
-export const MultipleCards: StoryObj = {
+export const MultipleCards: Story = {
   render: () => (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/stories/components/AIModelCard.stories.tsx` at line 74, The story export
MultipleCards is currently typed as StoryObj which breaks linkage to meta and
argTypes; change its type to Story (i.e. StoryObj<typeof meta> or the project's
alias Story) to restore proper meta inference and consistency with other
stories—locate the MultipleCards export in AIModelCard.stories.tsx and update
its type annotation from StoryObj to Story (or StoryObj<typeof meta> if using
the generic form).
src/components/utils/resolveSizeVariant.ts (1)

1-31: Optional: cap warnedKeys to avoid unbounded growth in long-running processes.

warnedKeys is a module-singleton Set that lives for the process lifetime. Under normal use this is fine (small, finite valid+invalid key space), but in long-running SSR/dev servers a misconfigured caller passing dynamically generated size values would accumulate entries indefinitely. A trivial size cap (or LRU) prevents that without changing the “warn once” UX.

♻️ Possible bound
 const warnedKeys = new Set<string>();
+const MAX_WARN_KEYS = 1024;
@@
-  if (!warnedKeys.has(key) && size != null) {
+  if (!warnedKeys.has(key) && size != null) {
+    if (warnedKeys.size >= MAX_WARN_KEYS) warnedKeys.clear();
     warnedKeys.add(key);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/utils/resolveSizeVariant.ts` around lines 1 - 31, warnedKeys
is an unbounded module-level Set which can grow indefinitely; replace it with a
bounded, insertion-ordered store (e.g., a Map used as an LRU FIFO) and evict the
oldest entry when capacity is exceeded. Concretely, change the top-level
warnedKeys to a Map<string, boolean> (or similar) with a MAX_WARNED_KEYS
constant, update checks in resolveSizeVariant to use warnedKeys.has(key) and
warnedKeys.set(key, true), and after adding a new key, if warnedKeys.size >
MAX_WARNED_KEYS remove the oldest entry by iterating
warnedKeys.keys().next().value and deleting it so the “warn once” behavior is
preserved without unbounded memory growth.
src/components/Header.tsx (1)

17-21: Tailwind class merging: user className won't reliably override DEFAULT_CLASSES for overlapping utilities.

With Tailwind's JIT, utilities at the same layer have equal CSS specificity, so the last rule emitted in the generated stylesheet wins — not the last token in the className string. A consumer passing className="px-[10mm]" to override px-[5mm] from DEFAULT_CLASSES may or may not actually take effect depending on Tailwind's class ordering. Since the whole point of this refactor is per-instance customization, this can silently bite consumers.

Consider using tailwind-merge so the right-most caller-supplied utility wins deterministically:

♻️ Proposed fix using tailwind-merge
 import React from 'react';
+import { twMerge } from 'tailwind-merge';

 export type PageType = 'first' | 'default' | 'last';
 ...
 const DEFAULT_CLASSES = 'py-[1mm] px-[5mm]';

-function merged(...parts: Array<string | undefined>): string {
-  return parts.filter(Boolean).join(' ');
-}
+function merged(...parts: Array<string | undefined>): string {
+  return twMerge(parts.filter(Boolean).join(' '));
+}

Also applies to: 41-41

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

In `@src/components/Header.tsx` around lines 17 - 21, DEFAULT_CLASSES and the
merged(...) helper use naive string concat so consumer className like
"px-[10mm]" may not override "px-[5mm]" reliably; replace the simple join with
tailwind-merge to deterministically resolve conflicts. Import the twMerge (or
merge from 'tailwind-merge') and change merged to call twMerge(DEFAULT_CLASSES,
...parts) (and update any other usage at the other occurrence mentioned around
the merged call), ensuring the caller-supplied utilities win when appropriate.
src/stories/components/DatasheetTemplate.stories.tsx (1)

26-53: Optional: extract the repeated margins literal.

The same { top: 10, bottom: 10, left: 10, right: 10 } is repeated four times. Lifting it to a constant (e.g., const MARGINS = { top: 10, bottom: 10, left: 10, right: 10 } as const;) would make future tweaks a single-line change.

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

In `@src/stories/components/DatasheetTemplate.stories.tsx` around lines 26 - 53,
Extract the repeated margins object into a single constant (e.g., MARGINS) at
the top of the story and replace every inline `{ top: 10, bottom: 10, left: 10,
right: 10 }` with that constant in the Page usages; specifically update the Page
props used in the default story and the MultiPage story so all four Page
components reference MARGINS (you can declare it as `const MARGINS = { top: 10,
bottom: 10, left: 10, right: 10 } as const`), ensuring all references to Page's
margins prop use the new symbol.
cli/src/bundler/vite-builder.ts (2)

97-108: Regex classification looks correct, but consider unmatched-export cases.

The patterns correctly anchor on a non-./non-/ first character to exclude relative/absolute path failures. One pattern that occasionally surfaces from broken installs is SyntaxError: The requested module '<pkg>' does not provide an export named ..., which is also typically resolved by reinstalling (e.g., when a dep gets bumped under your feet). Worth adding if you see it in practice.

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

In `@cli/src/bundler/vite-builder.ts` around lines 97 - 108, Add detection for
"does not provide an export named" errors to RUNTIME_REINSTALL_PATTERNS used by
isMissingDepError: update the RUNTIME_REINSTALL_PATTERNS array to include a
regex that matches messages like "SyntaxError: The requested module '<pkg>' does
not provide an export named" (case-insensitive, allow single/double quotes and
variable export names), so isMissingDepError(stderr) returns true for these
broken-install export errors; ensure the new pattern follows the same
non-relative/module-name anchoring style as the other entries and is pushed into
the existing array constant RUNTIME_REINSTALL_PATTERNS referenced by
isMissingDepError.

118-150: Loader stdout is discarded on failure.

runLoaderWithRetry only inspects firstErr?.stderr when classifying and surfacing errors. Bun's $ populates both stdout and stderr on a non-zero exit, and Vite SSR sometimes prints the most useful frames (e.g., transformed source location) to stdout. If a non-missing-dep failure occurs, users will see only the stderr fragment in the wrapped Vite SSR build failed: message and may lose the original stack context.

♻️ Suggested change to include stdout in the surfaced error
-  } catch (firstErr: any) {
-    const stderr = firstErr?.stderr?.toString?.() || '';
-    if (!isMissingDepError(stderr)) {
-      const raw = stderr || (firstErr instanceof Error ? firstErr.message : String(firstErr));
-      throw new Error(`Vite SSR build failed:\n${deduplicateOutput(raw)}`);
-    }
+  } catch (firstErr: any) {
+    const stderr = firstErr?.stderr?.toString?.() || '';
+    const stdout = firstErr?.stdout?.toString?.() || '';
+    if (!isMissingDepError(stderr) && !isMissingDepError(stdout)) {
+      const raw = [stderr, stdout].filter(Boolean).join('\n')
+        || (firstErr instanceof Error ? firstErr.message : String(firstErr));
+      throw new Error(`Vite SSR build failed:\n${deduplicateOutput(raw)}`);
+    }

Apply the same to the second-attempt catch block.

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

In `@cli/src/bundler/vite-builder.ts` around lines 118 - 150, The error handling
in runLoaderWithRetry only inspects firstErr.stderr (and secondErr.stderr) so
useful output printed to stdout by runLoaderOnce (via bun's $) is lost; change
the classification and error construction to include both stdout and stderr
(e.g., combine firstErr.stdout + firstErr.stderr into a single string before
calling isMissingDepError and before passing to deduplicateOutput), and apply
the same combination in the second catch block for secondErr so both attempts
surface full output from runLoaderOnce.
cli/src/commands/doctor.ts (3)

352-355: .npmrc secret regex has redundant alternations and may false-positive on comments.

_auth matches inside _authToken and _password (well, no — _password contains no _auth, but _authToken does), making the first three alternations partially overlapping. More importantly, this is unanchored, so a comment like # remember to set _authToken later triggers the path. Anchor to ^ after optional whitespace and split on =/: to reduce false positives:

♻️ Suggested tighter detection
-  const hasSecret = /_authToken|_password|_auth|certfile|keyfile/.test(raw);
+  const hasSecret = raw.split(/\r?\n/).some(line => {
+    const t = line.replace(/^\s*[#;].*/, '').trim();
+    return /^([@\w./:-]+:)?(_authToken|_password|_auth|certfile|keyfile)\s*=/.test(t);
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/src/commands/doctor.ts` around lines 352 - 355, The current unanchored
regex in the hasSecret check (/ _authToken|_password|_auth|certfile|keyfile/)
can false-positive on comments and has overlapping alternations; update the test
to match credential keys only at the start of a line (allowing leading
whitespace) and require a separator (= or :) so comments won't trigger it.
Replace the /_authToken|_password|_auth|certfile|keyfile/.test(raw) usage with a
multiline-anchored pattern like
/^\s*(?:_authToken|_password|_auth|certfile|keyfile)\s*(?:=|:)/m.test(raw) (put
longer tokens before shorter to avoid accidental partial matches) so hasSecret
only returns true for actual key assignments in doctor.ts.

356-365: .gitignore parsing is line-exact and misses common patterns.

The check accepts only .facet, .facet/, /.facet, or /.facet/. A user who ignores via **/.facet/ or who relies on a parent .gitignore will be flagged as leaking secrets even when the directory is ignored. Consider using a small gitignore parser (e.g., ignore from npm) or git check-ignore:

+import { execSync } from 'child_process';
+...
+  try {
+    execSync(`git -C ${JSON.stringify(consumerRoot)} check-ignore -q .facet/`, { stdio: 'ignore' });
+    ignored = true;
+  } catch { ignored = false; }

This delegates the check to git itself, which is the source of truth.

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

In `@cli/src/commands/doctor.ts` around lines 356 - 365, The current .gitignore
check in doctor.ts is too strict and only matches exact lines like '.facet' or
'/.facet/'; replace this ad-hoc parsing by delegating to git or a proper
gitignore parser: use 'git check-ignore --no-index --quiet --path' (or spawn git
check-ignore) on the path join(consumerRoot, '.facet') to determine ignore
status, or install and use the 'ignore' npm package to load the .gitignore
contents and test the relative path; update the logic around gitignore,
gitignore variable and ignored flag so ignored accurately reflects whether the
directory is ignored (including parent .gitignore and patterns like
'**/.facet/') and handle errors/fallbacks when git is not available.

196-203: Native bindings check may pass when an unrelated, wrong-arch binding is co-installed.

The loop iterates CRITICAL_NATIVE_PACKAGES, finds matching entries, then asserts some(m => m.includes(expectedSuffix)). If the package has multiple arch-specific subpackages installed (e.g., both @rollup+rollup-darwin-arm64@… and @rollup+rollup-linux-x64@… got hoisted in a transferred node_modules), some(...) returns true and the check passes — but the runtime may still load the foreign binary first and fail. Consider asserting that all matching - arch suffixes equal expectedSuffix, or that no foreign-arch suffix is present:

♻️ Tighter check
-    const archMatch = matches.some(m => m.includes(expectedSuffix));
-    if (!archMatch) missing.push(prefix);
+    const archSpecific = matches.filter(m => /-(darwin|linux|win32|freebsd)-(x64|arm64|ia32|arm)\b/.test(m));
+    const goodArch = archSpecific.some(m => m.includes(expectedSuffix));
+    const foreignArch = archSpecific.some(m => !m.includes(expectedSuffix));
+    if (!goodArch || foreignArch) missing.push(prefix);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/src/commands/doctor.ts` around lines 196 - 203, The current arch-check
uses archMatch = matches.some(m => m.includes(expectedSuffix)) which can pass
when a wrong-arch build is present alongside a correct one; change the logic
around CRITICAL_NATIVE_PACKAGES/entries/matches/expectedSuffix so that you first
collect arch-specific matches (e.g., matches.filter(m =>
/-\w+-\w+(@|$)/.test(m))) and then consider the package OK only if either there
are no arch-specific matches or every arch-specific match ends with the
expectedSuffix (i.e., archSpecific.every(m => m.includes(expectedSuffix))); if
any arch-specific match has a different suffix, treat the package as missing and
push to missing.
cli/src/builders/facet-directory.test.ts (1)

13-20: Minor: silence info/warn in tests for cleaner output.

new Logger(false) still emits info/warn per the comment. With ~15 tests exercising broken/foreign-install paths that intentionally trigger warning logs, output noise can swamp real failures. Consider a quieter test logger or capturing logs via mock.module if your test runner supports it. Not a blocker.

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

In `@cli/src/builders/facet-directory.test.ts` around lines 13 - 20, The tests
create a Logger in beforeEach with new Logger(false) which still prints
info/warn; replace this with a test-silent logger by either (A) providing a
dedicated test logger that implements the same API but no-ops info/warn (e.g.,
replace new Logger(false) with new QuietTestLogger() or createQuietLogger()) or
(B) if your test framework supports mocking, stub Logger.prototype.info and
Logger.prototype.warn to no-op before tests run (or spy and restore) so that the
logger variable used in beforeEach emits only errors; update the beforeEach to
instantiate or install that quiet logger so tests no longer print info/warn.
src/components/StatCard.tsx (1)

698-718: summary variant: long values can overflow the bordered card.

label is constrained with whitespace-nowrap overflow-hidden text-ellipsis, but the value <div> has no overflow handling. With a long formattedValue (e.g., "$1,234,567,890.12" or a long string), the parent's min-w-* will be exceeded and the card grows past its intended report-band width — defeating the "compact KPI" purpose. Consider mirroring the label clamping on the value too:

♻️ Optional clamp
-        <div className={`font-bold ${colorClasses.text} ${sizeClasses.summaryValueText} ${conditionalClasses || ''} ${valueClassName || ''}`.trim()}>
+        <div className={`font-bold ${colorClasses.text} ${sizeClasses.summaryValueText} truncate ${conditionalClasses || ''} ${valueClassName || ''}`.trim()}>
           {formattedValue}
         </div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/StatCard.tsx` around lines 698 - 718, The summary variant's
value div (the element that renders formattedValue in the 'summary' branch of
StatCard) lacks overflow/clamping and can force the card wider; update that
div's class string to include the same truncation/overflow utilities used on the
label (e.g., whitespace-nowrap overflow-hidden text-ellipsis or the Tailwind
shorthand truncate) so long values are clipped instead of expanding the card,
and ensure this change composes with valueClassName/conditionalClasses (avoid
allowing those props to override the overflow behavior).
src/components/ListTable.tsx (1)

167-172: diffDays math is sensitive to DST transitions.

(startOfToday.getTime() - dateStart.getTime()) / 86400000 doesn't account for the 23-/25-hour days at DST boundaries — Math.floor can land off-by-one for dates that straddle a transition, putting (e.g.) "exactly 7 days ago" into the 30-day week bucket on some days of the year. Consider computing the difference in calendar days directly:

const msPerDay = 86_400_000;
const utcToday = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate());
const utcThen  = Date.UTC(d.getFullYear(),    d.getMonth(),    d.getDate());
const diffDays = Math.round((utcToday - utcThen) / msPerDay);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ListTable.tsx` around lines 167 - 172, The diffDays
calculation in getTimeBucket is DST-sensitive and can produce off-by-one
results; replace the current local-ms math using startOfToday/getTime with a
UTC-based day difference: compute utcToday = Date.UTC(now.getFullYear(),
now.getMonth(), now.getDate()) and utcThen = Date.UTC(d.getFullYear(),
d.getMonth(), d.getDate()), set msPerDay = 86400000 and compute diffDays =
Math.round((utcToday - utcThen) / msPerDay); update the code around the diffDays
variable in getTimeBucket to use these UTC values to ensure correct calendar-day
differences across DST transitions.
examples/kitchen-sink/FindingsReport.tsx (1)

421-432: Default metadata is a fallback for missing data.metadata, not for an empty object.

data.metadata ? data.metadata as ReportMetadata : {…defaults} substitutes the defaults only when data.metadata is falsy. If a caller passes an explicit metadata: {} (empty but truthy) the report will render with no reporting window, classification, etc., and the footer/header strings will collapse to bare defaults like "Internal" and the current date. Probably fine for an example, but worth being aware of if this file is the template consumers copy from.

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

In `@examples/kitchen-sink/FindingsReport.tsx` around lines 421 - 432, The
metadata fallback currently treats any truthy data.metadata (including an empty
object) as authoritative; update the logic in FindingsReport to merge defaults
with any provided metadata so missing fields are filled and an empty object
doesn't wipe defaults. Locate the metadata variable in FindingsReport and
replace the ternary check on data.metadata with a merge pattern that defines a
defaults object (reportingWindow, environment, classification, audience,
preparedBy, scope) and then spreads ...(data.metadata as Partial<ReportMetadata>
|| {}) on top, so callers can override specific fields but empty or partial
metadata still inherits sensible defaults.
cli/src/utils/pdf-multipass.ts (2)

434-477: replaceInPdfStreams is unused and should be removed.

This function is defined but never called anywhere in the codebase. The compositing path (lines 528-565) uses a different approach (regenerating the overlay via Puppeteer rather than rewriting PDF streams), making this dead code from an earlier design. Retaining it creates maintenance burden and divergent placeholder logic between this and replaceInBuffer.

Remove the function to clean up the codebase.

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

In `@cli/src/utils/pdf-multipass.ts` around lines 434 - 477, The function
replaceInPdfStreams is dead code (defined but never used) and should be removed:
delete the entire async function replaceInPdfStreams(buf: Buffer, replacements:
[string, string][]) and any related unused imports or constants that only
support it (e.g., PDFDocument/PDFRawStream/PDFName/zlib references if they
become unused after removal), keeping replaceInBuffer and the existing
Puppeteer-based compositing logic as the canonical implementation.

405-419: bufferHasPlaceholders may inflate on every literal 'x' byte.

Buffer.from([0x78]) matches the byte 0x78, which is also the ASCII code for 'x' and appears throughout typical PDF content streams (text, names, hex strings). For each occurrence the function attempts zlib.inflateSync(buf.subarray(offset)), which throws and is caught — but the throw path through Node's zlib still does meaningful work and can dominate runtime on multi-MB overlay PDFs.

Two cheaper options:

  1. Match the actual zlib magic more strictly (e.g., 0x78 0x01, 0x78 0x5E, 0x78 0x9C, 0x78 0xDA) before calling inflateSync.
  2. Iterate doc.context.enumerateIndirectObjects() and only inflate PDFRawStream entries whose Filter is /FlateDecode (the same approach replaceInPdfStreams already uses at lines 437-444).

Optional given that overlays are typically small, but worth noting if this ends up running on large content buffers.

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

In `@cli/src/utils/pdf-multipass.ts` around lines 405 - 419, bufferHasPlaceholders
currently tries to inflate on every 0x78 byte which causes expensive throw
paths; change the zlib detection to first check two-byte sequences (0x78
followed by one of 0x01,0x5E,0x9C,0xDA) before calling zlib.inflateSync on
buf.subarray(offset), so only likely zlib streams are inflated, and keep checks
for PAGE_MARKER, TOTAL_MARKER, PAGE_MARKER_HEX, TOTAL_MARKER_HEX as before;
alternatively (if you prefer stream-aware scanning) restrict inflation to actual
PDF stream objects by iterating doc.context.enumerateIndirectObjects() and
inflating only PDFRawStream entries with Filter /FlateDecode (same approach used
in replaceInPdfStreams).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 43dc156a-5928-4282-abaa-23e8ef677731

📥 Commits

Reviewing files that changed from the base of the PR and between 16d82b4 and 5b11709.

⛔ Files ignored due to path filters (4)
  • cli/package-lock.json is excluded by !**/package-lock.json
  • cli/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • package-lock.json is excluded by !**/package-lock.json
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (70)
  • .github/workflows/test.yml
  • .gitignore
  • .storybook/main.ts
  • README.md
  • Taskfile.yml
  • cli/ARCHITECTURE.md
  • cli/README.md
  • cli/STATUS.md
  • cli/package.json
  • cli/scripts/gen-version.cjs
  • cli/src/builders/facet-directory.test.ts
  • cli/src/builders/facet-directory.ts
  • cli/src/bundler/vite-builder.test.ts
  • cli/src/bundler/vite-builder.ts
  • cli/src/cli.ts
  • cli/src/commands/doctor.ts
  • cli/src/generators/html.ts
  • cli/src/server/facet-cache.ts
  • cli/src/server/facet-types.ts
  • cli/src/server/routes.ts
  • cli/src/utils/package-manager.test.ts
  • cli/src/utils/package-manager.ts
  • cli/src/utils/pdf-generator.ts
  • cli/src/utils/pdf-multipass.ts
  • cli/src/utils/remote-resolver.test.ts
  • cli/src/utils/remote-resolver.ts
  • cli/src/version-generated.ts
  • cli/test/page-number.test.ts
  • cli/tsconfig.json
  • cli/tsdown.config.ts
  • cli/vite-ssr-loader.ts
  • examples/kitchen-sink/BadgeExamples.tsx
  • examples/kitchen-sink/FindingsReport.tsx
  • examples/kitchen-sink/FlanksourceHeader.tsx
  • examples/kitchen-sink/HeaderSolid.tsx
  • examples/kitchen-sink/ListTableExample.tsx
  • examples/kitchen-sink/UberKitchenSink.tsx
  • examples/kitchen-sink/package.json
  • examples/kitchen-sink/sample-findings.json
  • examples/quickstart/README.md
  • package.json
  • src/components/AIModelCard.tsx
  • src/components/Avatar.tsx
  • src/components/AvatarGroup.tsx
  • src/components/Badge.tsx
  • src/components/BulletList.tsx
  • src/components/CalloutBox.tsx
  • src/components/DatasheetTemplate.tsx
  • src/components/Document.tsx
  • src/components/Finding.tsx
  • src/components/Header.tsx
  • src/components/Heatmap.tsx
  • src/components/ListTable.tsx
  • src/components/Page.tsx
  • src/components/PageNo.tsx
  • src/components/ProgressBar.tsx
  • src/components/ScoreGauge.tsx
  • src/components/SeverityStatCard.tsx
  • src/components/Shield/Shield.tsx
  • src/components/StatCard.test.tsx
  • src/components/StatCard.tsx
  • src/components/Status.tsx
  • src/components/index.tsx
  • src/components/utils/resolveSizeVariant.ts
  • src/stories/components/AIModelCard.stories.tsx
  • src/stories/components/Badge.stories.tsx
  • src/stories/components/DatasheetTemplate.stories.tsx
  • src/stories/components/StatCard.stories.tsx
  • src/styles.css
  • tailwind.config.js
💤 Files with no reviewable changes (6)
  • cli/src/version-generated.ts
  • cli/STATUS.md
  • src/components/Heatmap.tsx
  • src/styles.css
  • cli/src/server/facet-cache.ts
  • cli/ARCHITECTURE.md

Comment thread cli/scripts/gen-version.cjs Outdated
Comment thread cli/src/builders/facet-directory.ts Outdated
Comment thread cli/src/builders/facet-directory.ts Outdated
Comment thread cli/src/cli.ts
try {
// Run tailwindcss CLI: -i input.css --content "output.html" -o output.css
const tailwindCmd = $`cd ${facetRoot} && npx tailwindcss -i ${stylesInput} --content ${tempHtmlPath} -o ${outputCssPath}`;
const tailwindCmd = $`cd ${facetRoot} && pnpm exec tailwindcss -i ${stylesInput} --content ${tempHtmlPath} -o ${outputCssPath}`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm tailwindcss is installed into .facet/node_modules during setup, so a direct binary path is viable
rg -nP --type=ts -C3 'tailwindcss' cli/src
fd -t f 'package.json' cli | head -20 | xargs -I{} sh -c 'echo "=== {} ==="; cat "{}"'

Repository: flanksource/facet

Length of output: 4671


🏁 Script executed:

# Search for where .facet directory is created and used
rg -n '\.facet' cli/src --type ts -B 2 -A 2

Repository: flanksource/facet

Length of output: 32778


🏁 Script executed:

# Find where dependencies (including tailwindcss) are installed during facet setup
rg -n 'pnpm.*install|npm.*install' cli/src --type ts -B 3 -A 3 | head -60

Repository: flanksource/facet

Length of output: 4374


🏁 Script executed:

# Check the facet-directory.ts file more thoroughly for the full context of the dependency list
sed -n '400,430p' cli/src/builders/facet-directory.ts

Repository: flanksource/facet

Length of output: 922


🏁 Script executed:

# Check README for pnpm documentation
find . -name "README*" -o -name "readme*" | head -5 | xargs cat 2>/dev/null | grep -i pnpm -B 3 -A 3 | head -40

Repository: flanksource/facet

Length of output: 656


🏁 Script executed:

# Check if pnpm is validated/checked before execution
rg -n 'resolvePackageManager|checkPnpm|pnpm.*--version' cli/src --type ts -B 2 -A 5 | head -80

Repository: flanksource/facet

Length of output: 5400


🏁 Script executed:

# Verify the exact locations and context of the pnpm exec calls
sed -n '110,120p' cli/src/generators/html.ts
sed -n '490,505p' cli/src/server/routes.ts

Repository: flanksource/facet

Length of output: 1064


Document pnpm as a runtime prerequisite and consider using the resolved tailwindcss binary instead of pnpm exec.

The README offers npm install -g @flanksource/facet`` as an alternative installation method without noting that pnpm must also be available at runtime. While the code enforces pnpm as a hard requirement during `.facet/` setup (resolved in `vite-builder.ts`), users installing via npm won't be aware of this runtime dependency before attempting HTML/PDF generation.

Additionally, since tailwindcss is already installed into .facet/node_modules/.bin/ during the pnpm install step, you could invoke the binary directly instead of relying on pnpm exec:

const tailwindBin = join(facetRoot, 'node_modules/.bin/tailwindcss');
const tailwindCmd = $`${tailwindBin} -i ${stylesInput} --content ${tempHtmlPath} -o ${outputCssPath}`;

This eliminates the implicit assumption that pnpm is available in PATH during CSS generation and would work regardless of which package manager was used to install the CLI.

Add a note to the README install section clarifying that pnpm must be present at runtime, or apply the direct binary path approach to both cli/src/generators/html.ts (line 114) and cli/src/server/routes.ts (line 497).

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

In `@cli/src/generators/html.ts` at line 114, The current CSS generation uses
`pnpm exec` which forces a runtime pnpm requirement; update
`cli/src/generators/html.ts` to resolve and call the installed tailwind binary
directly by constructing `tailwindBin` from `facetRoot` (e.g., join `facetRoot`
with `node_modules/.bin/tailwindcss`) and replace the `tailwindCmd` invocation
that currently references `stylesInput`, `tempHtmlPath`, and `outputCssPath` to
call that binary; apply the same change in `cli/src/server/routes.ts` (the
analogous tailwind invocation around line ~497) and also add a short note in the
README install section documenting that pnpm is required at runtime if you
choose not to call the local binary.

Comment thread src/components/ListTable.tsx
Comment thread src/components/StatCard.tsx
Comment thread src/components/Status.tsx Outdated
Comment thread src/components/Status.tsx Outdated
Comment thread tailwind.config.js
@moshloop moshloop force-pushed the feat/streaming-render-security branch from 5b11709 to a7a45a7 Compare April 27, 2026 09:43
@socket-security
Copy link
Copy Markdown

socket-security Bot commented Apr 27, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​types/​bun@​1.3.131001004992100
Updatednpm/​puppeteer@​24.26.1 ⏵ 22.15.093 +110089 +250 -46100
Updatednpm/​@​storybook/​react-vite@​8.6.14 ⏵ 8.6.189910066100100
Addednpm/​pdfassembler@​0.1.0-beta.8661009876100
Addednpm/​node-forge@​1.4.0951001008770
Updatednpm/​lightningcss@​1.30.2 ⏵ 1.32.099 +1100100 +183 +170
Updatednpm/​@​types/​dagre@​0.7.53 ⏵ 0.7.541001007181100
Addednpm/​@​signpdf/​signer-p12@​3.3.0901007186100
Addednpm/​pdf-rfc3161@​0.1.4781007392100
Updatednpm/​@​storybook/​react@​8.6.14 ⏵ 8.6.189310073100100
Addednpm/​@​types/​semver@​7.7.11001007581100
Addednpm/​@​pdfsmaller/​pdf-encrypt@​1.0.27610010088100
Addednpm/​@​types/​node-forge@​1.3.141001007682100
Addednpm/​@​signpdf/​utils@​3.3.0931007786100
Updatednpm/​@​types/​react@​18.3.26 ⏵ 18.3.281001007988100
Addednpm/​@​signpdf/​signpdf@​3.3.0921008087100
Updatednpm/​@​types/​node@​20.19.22 ⏵ 20.19.23100 +110081 +196 +3100
Updatednpm/​@​types/​node@​20.19.22 ⏵ 20.19.40100 +110081 +196 +3100
Updatednpm/​tsx@​4.20.6 ⏵ 4.21.01001008184100
Updatednpm/​@​flanksource/​icons@​1.0.53 ⏵ 1.0.5881 +1100100 +194 +1100
Addednpm/​@​signpdf/​placeholder-plain@​3.3.0831008287100
Updatednpm/​postcss@​8.5.8 ⏵ 8.5.1499 +1100 +28294100
Updatednpm/​chalk@​5.6.2 ⏵ 4.1.210010010082100
Updatednpm/​ora@​8.2.0 ⏵ 5.4.199 +110010087100
Updatednpm/​@​tailwindcss/​vite@​4.1.17 ⏵ 4.3.0100 +110090 +298100
Updatednpm/​ajv@​8.18.0 ⏵ 8.20.099 +110010090100
Addednpm/​typescript@​5.9.3100100909890
Updatednpm/​@​xyflow/​react@​12.8.6 ⏵ 12.10.297100100 +190100
Updatednpm/​semver@​6.3.1 ⏵ 7.8.0100 +1100100 +191100
Addednpm/​tsdown@​0.16.8981009295100
Addednpm/​@​aws-sdk/​client-s3@​3.1045.09810010098100
Addednpm/​@​aws-sdk/​s3-request-presigner@​3.1045.010010010098100
See 2 more rows in the dashboard

View full report

@socket-security
Copy link
Copy Markdown

socket-security Bot commented Apr 27, 2026

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn Critical
Critical CVE: npm crypto-js PBKDF2 1,000 times weaker than specified in 1993 and 1.3M times weaker than current standard

CVE: GHSA-xwcq-pm8m-c4vf crypto-js PBKDF2 1,000 times weaker than specified in 1993 and 1.3M times weaker than current standard (CRITICAL)

Affected versions: < 4.2.0

Patched version: 4.2.0

From: cli/pnpm-lock.yamlnpm/@signpdf/placeholder-plain@3.3.0npm/crypto-js@3.3.0

ℹ Read more on: This package | This alert | What is a critical CVE?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Remove or replace dependencies that include known critical CVEs. Consumers can use dependency overrides or npm audit fix --force to remove vulnerable dependencies.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/crypto-js@3.3.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm pdfkit is 94.0% likely obfuscated

Confidence: 0.94

Location: Package overview

From: cli/pnpm-lock.yamlnpm/@signpdf/placeholder-plain@3.3.0npm/pdfkit@0.10.0

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/pdfkit@0.10.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm pdfkit is 94.0% likely obfuscated

Confidence: 0.94

Location: Package overview

From: cli/pnpm-lock.yamlnpm/@signpdf/placeholder-plain@3.3.0npm/pdfkit@0.10.0

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/pdfkit@0.10.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

moshloop and others added 4 commits April 27, 2026 16:54
The pnpm migration in a7a45a7 left npm references in CI/build:
- test.yml setup-node cached cli/package-lock.json (no longer exists)
- Dockerfile builder ran npm run build, but prebuild calls pnpm
- Dockerfile final stage copied package-lock.json (no longer exists)
- release.yml installed and built via npm and tried to git-add stale
  package-lock files

Switch all of them to pnpm@8.13.1 via pnpm/action-setup, replace
npm install/run with pnpm equivalents, and update lockfile paths.
pnpm 8 disables pre/post lifecycle scripts by default for security.
cli/package.json relies on prebuild → gen-version to emit
src/version-generated.ts; without it, the Docker build fails with
"Could not resolve '../version-generated.js'".

Add .npmrc with enable-pre-post-scripts=true at the root and in cli/.
facet shells out to pnpm when materialising .facet/node_modules for a
template. The final image only had pnpm in the builder stage, so the
in-image warmup (and any runtime PDF render) failed with:
"facet requires pnpm, but it was not found on PATH".

Install pnpm@8.13.1 globally in the runtime stage too.
CLI:
- Complete bun test migration (remove jest deps, switch test imports to bun:test)
- Add DOM to tsconfig lib (was provided transitively via jest types)
- Per-pin cache in package-manager to prevent foreign-manager bypass
- Reject workspace: protocol in facet overrides with descriptive error
- Broaden env-var regex to case-insensitive in .npmrc parsing
- Wrap doctor command in try/catch matching other CLI commands
- Add shrink prop to StatCardProps
- Suppress git stderr in gen-version; fix header comment filename
- Drop misleading CI hint from SSR bundle preservation message

Components:
- Sanitize CSS inputs in Document to prevent </style> breakouts
- Drop DEFAULT_CLASSES from Header children render path
- Use inline styles for numeric Badge sizes (Tailwind JIT can't compile
  runtime arbitrary values)
- Narrow Finding icon type; drop unused dot field
- Add near-future/week/month bucketing to ListTable parallel to past
- Fix Status aria-label fallback; move className to outer wrapper
- Import Fira Code font for headless Chromium rendering

Examples:
- Fix FindingsReport anchor ID mismatch

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Comment thread src/components/Document.tsx Fixed
Comment thread src/components/Document.tsx Fixed
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
cli/src/server/facet-types.ts (1)

1574-1574: ⚡ Quick win

Add JSDoc comment for the shrink property.

The new shrink?: boolean property lacks documentation. For consistency with the Badge prop additions (which include JSDoc for labelClassName, valueClassName, and wrap), add a brief JSDoc comment explaining when and why consumers should use shrink.

📝 Suggested JSDoc addition
       sublabel?: React.ReactNode;
       sublabelClassName?: string;
+      /**
+       * Reduce the card size/spacing for compact layouts.
+       */
       shrink?: boolean;
       conditionalStyles?: ConditionalStyle[];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/server/facet-types.ts` at line 1574, Add a JSDoc comment for the
shrink?: boolean property in the facet-types.ts type definition explaining its
purpose and when to use it (e.g., "When true, reduces the component's horizontal
spacing/width to fit tight layouts or inline contexts; defaults to false").
Place the comment directly above the shrink property alongside existing JSDoc
for labelClassName/valueClassName/wrap so it remains consistent with the Badge
prop docs and mentions expected behavior and intended use.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Dockerfile`:
- Around line 32-33: The Docker build fails because the CLI layer uses "RUN cd
cli && pnpm install --frozen-lockfile" but the cli pnpm-lock.yaml is
missing/out-of-sync or incompatible with pnpm 8.13.1; go into the CLI package
(look for the cli directory and its pnpm-lock.yaml), verify its presence and
lockfileVersion, regenerate it by removing cli/pnpm-lock.yaml and running pnpm
install inside the CLI (with the same pnpm version used in CI), then add/commit
the new pnpm-lock.yaml so the frozen lockfile install succeeds; alternatively,
if the lockfile update is not desired, remove the --frozen-lockfile flag from
the Dockerfile RUN lines to avoid failing the build.

In `@src/components/Document.tsx`:
- Around line 41-47: The regex in sanitizeCssValue and sanitizeCssBlock
(/</?\s*style/gi) misses variants with spaces between '<' and '/' or before the
tag name, allowing injection; update both functions (sanitizeCssValue and
sanitizeCssBlock) to first strip any literal angle brackets (remove all '<' and
'>') and then remove any occurrences of the word "style" as a tag/token
(case-insensitive), and ensure sanitizeCssBlock gets the same second-pass
removal of residual characters as sanitizeCssValue; verify the sanitized output
used with dangerouslySetInnerHTML (the <style> insertion) cannot contain any
angle brackets or closing style tokens.

---

Nitpick comments:
In `@cli/src/server/facet-types.ts`:
- Line 1574: Add a JSDoc comment for the shrink?: boolean property in the
facet-types.ts type definition explaining its purpose and when to use it (e.g.,
"When true, reduces the component's horizontal spacing/width to fit tight
layouts or inline contexts; defaults to false"). Place the comment directly
above the shrink property alongside existing JSDoc for
labelClassName/valueClassName/wrap so it remains consistent with the Badge prop
docs and mentions expected behavior and intended use.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0cd6cc84-0c81-46f5-8f7b-7ef3353932f9

📥 Commits

Reviewing files that changed from the base of the PR and between 5b11709 and 372c143.

⛔ Files ignored due to path filters (4)
  • cli/package-lock.json is excluded by !**/package-lock.json
  • cli/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • package-lock.json is excluded by !**/package-lock.json
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (60)
  • .github/workflows/release.yml
  • .github/workflows/test.yml
  • .gitignore
  • .npmrc
  • Dockerfile
  • README.md
  • Taskfile.yml
  • cli/.npmrc
  • cli/ARCHITECTURE.md
  • cli/README.md
  • cli/STATUS.md
  • cli/package.json
  • cli/scripts/gen-version.cjs
  • cli/src/builders/facet-directory.test.ts
  • cli/src/builders/facet-directory.ts
  • cli/src/bundler/vite-builder.test.ts
  • cli/src/bundler/vite-builder.ts
  • cli/src/cli.ts
  • cli/src/commands/doctor.ts
  • cli/src/generators/html.ts
  • cli/src/server/facet-cache.ts
  • cli/src/server/facet-types.ts
  • cli/src/server/routes.ts
  • cli/src/utils/package-manager.test.ts
  • cli/src/utils/package-manager.ts
  • cli/src/utils/pdf-generator.ts
  • cli/src/utils/remote-resolver.test.ts
  • cli/src/utils/remote-resolver.ts
  • cli/test/jsx-parser.test.ts
  • cli/test/lint-rules.test.ts
  • cli/test/resolve-output.test.ts
  • cli/tsconfig.json
  • cli/tsdown.config.ts
  • cli/vite-ssr-loader.ts
  • examples/kitchen-sink/BadgeExamples.tsx
  • examples/kitchen-sink/FindingsReport.tsx
  • examples/kitchen-sink/FlanksourceHeader.tsx
  • examples/kitchen-sink/HeaderSolid.tsx
  • examples/kitchen-sink/package.json
  • examples/quickstart/README.md
  • package.json
  • src/components/Avatar.tsx
  • src/components/AvatarGroup.tsx
  • src/components/Badge.tsx
  • src/components/BulletList.tsx
  • src/components/CalloutBox.tsx
  • src/components/Document.tsx
  • src/components/Finding.tsx
  • src/components/Header.tsx
  • src/components/ListTable.tsx
  • src/components/ProgressBar.tsx
  • src/components/ScoreGauge.tsx
  • src/components/SeverityStatCard.tsx
  • src/components/Shield/Shield.tsx
  • src/components/StatCard.tsx
  • src/components/Status.tsx
  • src/components/utils/resolveSizeVariant.ts
  • src/stories/components/Badge.stories.tsx
  • src/styles.css
  • tailwind.config.js
💤 Files with no reviewable changes (3)
  • cli/STATUS.md
  • cli/ARCHITECTURE.md
  • cli/src/server/facet-cache.ts
✅ Files skipped from review due to trivial changes (11)
  • cli/test/jsx-parser.test.ts
  • .npmrc
  • cli/.npmrc
  • .gitignore
  • src/components/AvatarGroup.tsx
  • examples/quickstart/README.md
  • cli/src/bundler/vite-builder.test.ts
  • cli/README.md
  • examples/kitchen-sink/package.json
  • tailwind.config.js
  • README.md
🚧 Files skipped from review as they are similar to previous changes (33)
  • src/components/CalloutBox.tsx
  • src/components/Shield/Shield.tsx
  • cli/tsdown.config.ts
  • src/components/Avatar.tsx
  • src/components/BulletList.tsx
  • examples/kitchen-sink/FlanksourceHeader.tsx
  • cli/src/cli.ts
  • src/components/SeverityStatCard.tsx
  • src/components/ProgressBar.tsx
  • cli/scripts/gen-version.cjs
  • package.json
  • cli/src/builders/facet-directory.test.ts
  • src/components/ScoreGauge.tsx
  • cli/src/utils/package-manager.test.ts
  • cli/src/utils/remote-resolver.test.ts
  • cli/package.json
  • examples/kitchen-sink/FindingsReport.tsx
  • cli/src/utils/package-manager.ts
  • src/components/utils/resolveSizeVariant.ts
  • cli/src/utils/remote-resolver.ts
  • cli/src/commands/doctor.ts
  • cli/vite-ssr-loader.ts
  • cli/src/generators/html.ts
  • src/components/Header.tsx
  • examples/kitchen-sink/HeaderSolid.tsx
  • cli/src/server/routes.ts
  • examples/kitchen-sink/BadgeExamples.tsx
  • src/components/Badge.tsx
  • cli/src/builders/facet-directory.ts
  • src/stories/components/Badge.stories.tsx
  • cli/src/bundler/vite-builder.ts
  • src/components/StatCard.tsx
  • src/components/ListTable.tsx

Comment thread Dockerfile
Comment thread src/components/Document.tsx
…ease, harden Document.tsx sanitization

- Bump pnpm 8.13.1 -> 9.15.9 across Dockerfile, workflows, and packageManager pins to match lockfileVersion 9.0 (Docker/Helm CI was failing with ERR_PNPM_LOCKFILE_BREAKING_CHANGE).
- Pin unrun to 0.2.37 via pnpm.overrides; unrun@0.2.38 was published without its dist/ directory, breaking tsdown builds on CI.
- Regenerate root + cli pnpm-lock.yaml at lockfileVersion 9.0.
- Document.tsx: replace incomplete /<\/?\s*style/gi sanitization (bypassable via spaced "< / style >" variants flagged by CodeQL and CodeRabbit) with simple [<>{}] / [<>] stripping.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@moshloop moshloop merged commit 1e8f631 into main May 12, 2026
15 checks passed
@moshloop moshloop deleted the feat/streaming-render-security branch May 12, 2026 10:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants