diff --git a/src/rendering/orchestrator/css-candidate-manifest.test.ts b/src/rendering/orchestrator/css-candidate-manifest.test.ts
index 30e0221c62..b4e7c96a0a 100644
--- a/src/rendering/orchestrator/css-candidate-manifest.test.ts
+++ b/src/rendering/orchestrator/css-candidate-manifest.test.ts
@@ -2,6 +2,11 @@ import "#veryfront/schemas/_test-setup.ts";
import { assertEquals } from "#veryfront/testing/assert.ts";
import { describe, it } from "#veryfront/testing/bdd.ts";
import { createStyleScopeProfile } from "#veryfront/html/styles-builder/style-scope-profile.ts";
+import {
+ __registerLogRecordEmitter,
+ __resetLogRecordEmitterForTests,
+ type LogEntry,
+} from "#veryfront/utils/logger/logger.ts";
import {
getCandidateManifestCacheStats,
getProjectCandidates,
@@ -9,6 +14,17 @@ import {
invalidateProjectCandidateManifests,
} from "./css-candidate-manifest.ts";
+function captureLogs(): { entries: LogEntry[]; restore: () => void } {
+ const entries: LogEntry[] = [];
+ __registerLogRecordEmitter((entry) => entries.push(entry));
+ return {
+ entries,
+ restore: () => {
+ __resetLogRecordEmitterForTests();
+ },
+ };
+}
+
describe("rendering/orchestrator/css-candidate-manifest", () => {
describe("invalidateProjectCandidateManifests", () => {
it("should clear all caches when no scope provided", () => {
@@ -229,6 +245,109 @@ describe("rendering/orchestrator/css-candidate-manifest", () => {
assertEquals(result.has("text-blue-500"), false);
});
+ it("degrades a file that exceeds the candidate-count admission cap instead of throwing", () => {
+ invalidateProjectCandidateManifests();
+ // >MAX_CSS_SELECTOR_TOKENS (100_000) distinct candidates in one file —
+ // the shape of a large minified vendor bundle in project sources.
+ const poisonContent = Array.from({ length: 100_001 }, (_, i) => `tok-${i}`).join(" ");
+ const options = {
+ projectScope: "project-poison-count",
+ projectVersion: "v1",
+ projectDir: "/project",
+ files: [
+ { path: "/project/vendor/minified.js", content: poisonContent },
+ {
+ path: "/project/pages/index.tsx",
+ content: '
Home
',
+ },
+ ],
+ developmentMode: false,
+ };
+
+ const result = getProjectCandidates(options);
+
+ assertEquals(result.has("text-red-500"), true);
+ assertEquals(result.has("tok-0"), false);
+
+ // The completed manifest must be cached so the pathological file is not
+ // re-scanned (and cannot re-fail) on every request.
+ const statsAfterFirst = getCandidateManifestCacheStats();
+ assertEquals(statsAfterFirst.manifests.entries, 1);
+ const second = getProjectCandidates(options);
+ assertEquals(second.has("text-red-500"), true);
+ });
+
+ it("logs rejected source files without exposing absolute project paths", () => {
+ invalidateProjectCandidateManifests();
+ const captured = captureLogs();
+ try {
+ const projectDir = "/Users/someone/private/path/my-project";
+ const absoluteSourcePath = `${projectDir}/vendor/minified.js`;
+ const outsideSourcePath = "/Users/someone/other-parent/minified.js";
+ const poisonContent = Array.from({ length: 100_001 }, (_, i) => `tok-${i}`).join(" ");
+
+ getProjectCandidates({
+ projectScope: "project-poison-log-redaction",
+ projectVersion: "v1",
+ projectDir,
+ files: [
+ { path: absoluteSourcePath, content: poisonContent },
+ { path: outsideSourcePath, content: poisonContent },
+ ],
+ developmentMode: false,
+ });
+
+ const projectWarning = captured.entries.find((entry) =>
+ entry.message === "Skipping file rejected by candidate extraction" &&
+ entry.context?.path === "vendor/minified.js"
+ );
+ assertEquals(projectWarning !== undefined, true, "the in-project warning must be emitted");
+ assertEquals(JSON.stringify(projectWarning!.context).includes(projectDir), false);
+ assertEquals(JSON.stringify(projectWarning!.context).includes(absoluteSourcePath), false);
+
+ const outsideWarning = captured.entries.find((entry) =>
+ entry.message === "Skipping file rejected by candidate extraction" &&
+ entry.context?.path === "[outside-project]/minified.js"
+ );
+ assertEquals(
+ outsideWarning !== undefined,
+ true,
+ "the outside-project warning must be emitted",
+ );
+ assertEquals(outsideWarning!.context?.path, "[outside-project]/minified.js");
+ const outsideContext = JSON.stringify(outsideWarning!.context);
+ assertEquals(outsideContext.includes(outsideSourcePath), false);
+ assertEquals(outsideContext.includes("/Users/someone"), false);
+ assertEquals(outsideContext.includes("other-parent"), false);
+ } finally {
+ captured.restore();
+ }
+ });
+
+ it("degrades a file that exceeds the byte-size admission cap instead of throwing", () => {
+ invalidateProjectCandidateManifests();
+ // >MAX_CSS_FILE_BYTES (16MB) — e.g. a giant generated asset in sources.
+ const oversized = "text-blue-500 ".repeat(
+ Math.ceil((16 * 1024 * 1024 + 1) / "text-blue-500 ".length),
+ );
+ const result = getProjectCandidates({
+ projectScope: "project-poison-bytes",
+ projectVersion: "v1",
+ projectDir: "/project",
+ files: [
+ { path: "/project/generated/blob.js", content: oversized },
+ {
+ path: "/project/pages/index.tsx",
+ content: 'Home
',
+ },
+ ],
+ developmentMode: false,
+ });
+
+ assertEquals(result.has("text-red-500"), true);
+ assertEquals(result.has("text-blue-500"), false);
+ });
+
it("keeps configured runtime roots in the candidate graph", () => {
invalidateProjectCandidateManifests();
const result = getProjectCandidates({
diff --git a/src/rendering/orchestrator/css-candidate-manifest.ts b/src/rendering/orchestrator/css-candidate-manifest.ts
index d2194e4ac6..05ad9e4f26 100644
--- a/src/rendering/orchestrator/css-candidate-manifest.ts
+++ b/src/rendering/orchestrator/css-candidate-manifest.ts
@@ -85,6 +85,19 @@ function toRelativeProjectPath(path: string, projectDir: string): string {
return normalized.replace(/^\/+/, "");
}
+function toDiagnosticProjectPath(path: string, projectDir: string): string {
+ const normalized = normalizePath(path);
+ const normalizedProjectDir = normalizePath(projectDir).replace(/\/+$/, "");
+ if (normalized === normalizedProjectDir) return ".";
+ if (normalized.startsWith(`${normalizedProjectDir}/`)) {
+ return normalized.slice(normalizedProjectDir.length + 1);
+ }
+ if (/^(?:[A-Za-z]:)?\//.test(normalized)) {
+ return `[outside-project]/${normalized.split("/").pop() ?? "unknown"}`;
+ }
+ return normalized.replace(/^\/+/, "");
+}
+
function buildManifestCacheKey(
projectScope: string,
projectVersion: string,
@@ -123,7 +136,23 @@ function buildCandidateManifest(files: SourceFileLike[], projectDir: string): Ca
if (!file.content) continue;
if (!SOURCE_EXTENSIONS.some((ext) => file.path.endsWith(ext))) continue;
- const candidates = new Set(extractCandidates(file.content));
+ // A file the tokenizer refuses to admit (over the byte or candidate-count
+ // cap) must degrade to "contributes no candidates", not abort the manifest:
+ // an escaping throw here propagates to the SSR boundary and, because it
+ // happens before manifestCache.set, is rebuilt and re-thrown on every
+ // request to the project (VERYFRONT-SERVER-F).
+ let extracted: string[];
+ try {
+ extracted = extractCandidates(file.content);
+ } catch (error) {
+ logger.warn("Skipping file rejected by candidate extraction", {
+ path: toDiagnosticProjectPath(file.path, projectDir),
+ error: error instanceof Error ? error.message : String(error),
+ });
+ continue;
+ }
+
+ const candidates = new Set(extracted);
const relativePath = toRelativeProjectPath(file.path, projectDir);
const absolutePath = normalizePath(file.path);