Files
projectbrain-Public/scripts/check-visual-polish.mjs
T

158 lines
11 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
const root = process.cwd();
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
const exists = (file) => fs.existsSync(path.join(root, file));
const checks = [];
const pass = (name, ok, detail = '') => checks.push({ name, ok, detail });
const blueprint = read('src/features/blueprint/pages/BlueprintEngine.tsx');
pass('Blueprint ListChecks import is present', blueprint.includes('ListChecks'));
pass('Blueprint route should not trigger missing ListChecks ReferenceError', /import\s*\{[^}]*ListChecks[^}]*\}\s*from\s*["']lucide-react["']/.test(blueprint));
const appLayout = read('src/components/layout/AppLayout.tsx');
pass('Main shell has compact responsive header offset', appLayout.includes('pt-6') && appLayout.includes('lg:pt-10'));
pass('Main shell prevents page-level horizontal overflow', appLayout.includes('overflow-x-hidden') && appLayout.includes('min-w-0'));
const pageHeader = read('src/components/ui/PageHeader.tsx');
pass('PageHeader wraps actions on mobile', pageHeader.includes('flex-wrap') && pageHeader.includes('md:w-auto') && pageHeader.includes('break-words'));
const routes = read('src/app/routes.tsx');
pass('Route scroll reset is enabled', routes.includes('ScrollToTop') && routes.includes('data-projectbrain-main'));
pass('Route loading fallback explains project loading', routes.includes('Project laden…') && routes.includes('aria-label="ProjectBrain-pagina laden"'));
const sidebar = read('src/components/layout/Sidebar.tsx');
pass('Sidebar descriptions are hidden on smaller desktop widths', sidebar.includes('xl:block'));
const prompt = read('src/features/prompts/pages/PromptGenerator.tsx');
pass('Prompt Builder starts with task input', prompt.includes('What should AI/Codex do?'));
pass('Prompt Builder uses multiline output area', prompt.includes('min-h-40 font-mono text-xs'));
const knowledge = read('src/features/knowledge/pages/KnowledgeVault.tsx');
pass('Knowledge Vault avoids preview wording in primary actions', !knowledge.includes('preview'));
const audit = read('src/features/audit/pages/ProjectAuditCenter.tsx');
pass('Audit Center treats old priority labels as historical', audit.includes('was {finding.severity}'));
const header = read('src/components/layout/Header.tsx');
pass('Header New Project menu navigates to unified Start', header.includes('navigate("/start")'));
pass('Header exposes runtime status popover', header.includes('Runtimestatus') && header.includes('useBackendSystemReadinessData'));
pass('Header runtime status links to Runtime page', header.includes('Runtimepagina') && header.includes('/runtime'));
pass('Header labels unavailable backend as local mode', header.includes('Lokale modus') && !header.includes('degraded: "Degraded"'));
const commandMenu = read('src/components/layout/CommandMenu.tsx');
const commandMenuSearch = read('src/components/layout/commandMenuSearch.ts');
pass('Command palette shortcut listens in capture phase', commandMenu.includes('window.addEventListener("keydown", down, { capture: true })'));
pass(
'Command palette prioritizes best-matching navigation while searching',
commandMenu.includes('rankNavigationItems') &&
commandMenu.includes('<CommandGroup heading="Beste overeenkomsten">') &&
commandMenu.includes('{navigationSearchGroup}') &&
commandMenuSearch.includes('getNavigationScore') &&
commandMenuSearch.includes('label.startsWith(normalizedQuery)'),
);
pass('Command palette teaches route-oriented examples', commandMenu.includes('Zoek op repo, review, contextpack, historiek, bron of skill'));
const commandUi = read('src/components/ui/command.tsx');
pass('Command dialog has screenreader title and description', commandUi.includes('DialogTitle className="sr-only"') && commandUi.includes('DialogDescription className="sr-only"'));
const projectsPage = read('src/features/projects/pages/Projects.tsx');
pass(
'Projects mobile hero actions are full-width and stacked',
projectsPage.includes('flex w-full flex-col gap-2 sm:w-auto sm:flex-row') &&
projectsPage.includes('className="w-full sm:w-auto"') &&
projectsPage.includes('{refreshing ? "Bijwerken…" : "Bijwerken"}') &&
projectsPage.includes('disabled={refreshing}'),
);
pass('Projects progressively reveal large portfolios', projectsPage.includes('PROJECT_PAGE_SIZE') && projectsPage.includes('remainingProjectCount'));
const runtimeReadiness = read('src/features/projects/pages/RuntimeReadiness.tsx');
pass(
'System status prevents page-level mobile overflow',
runtimeReadiness.includes('min-w-0 space-y-6 overflow-x-hidden') &&
runtimeReadiness.includes('grid min-w-0 gap-3 sm:grid-cols-2 xl:grid-cols-3'),
);
pass(
'System status capability cards wrap long runtime evidence safely',
runtimeReadiness.includes('className="min-w-0 overflow-hidden"') &&
runtimeReadiness.includes('[overflow-wrap:anywhere]') &&
runtimeReadiness.includes('max-w-[45%] shrink-0 whitespace-normal text-center'),
);
pass(
'System status release/build proof avoids mobile overflow',
runtimeReadiness.includes('min-w-0 overflow-hidden scroll-mt-24 border-primary/25') &&
runtimeReadiness.includes('break-all font-semibold') &&
runtimeReadiness.includes('break-all font-mono text-xs'),
);
const review = read('src/features/operations/pages/ReviewQueue.tsx');
pass('Review Queue bulk accept requires confirmation', review.includes('Confirm bulk accept'));
pass('V84 documentation exists', exists('docs/STABILIZATION_PASS_V84_VISUAL_RC_POLISH_2.md'));
pass('V85 documentation exists', exists('docs/STABILIZATION_PASS_V85_RUNTIME_STATUS_POLISH.md'));
pass('V87 correction documentation exists', exists('docs/STABILIZATION_PASS_V87_REAL_PRODUCT_IMPROVEMENT_CORRECTION.md'));
pass('Audit Center no longer exposes V86 catalog tab', !audit.includes('V86 1000') && !audit.includes('megaImprovementItems'));
const relationshipGraph = read('src/features/relationships/pages/RelationshipGraph.tsx');
pass('Relationship Graph shows real suggestions instead of only empty state', relationshipGraph.includes('buildRelationshipSuggestions') && relationshipGraph.includes('suggestions.map'));
const repoBuilder = read('src/features/repo-builder/pages/RepoBuilder.tsx');
pass('Repo Builder mobile lifecycle has swipe affordance', repoBuilder.includes('Veeg zijwaarts om de latere fasen te bekijken.') && repoBuilder.includes('overflow-x-auto'));
pass(
'Repo Builder archive actions are confirmation-gated',
repoBuilder.includes('ConfirmActionButton') &&
repoBuilder.includes('title="Geselecteerde repository-intake archiveren?"') &&
repoBuilder.includes('onConfirm={archiveSelectedRepositoryIntake}') &&
repoBuilder.includes('title="Deze repositorybasis archiveren?"') &&
repoBuilder.includes('onConfirm={archiveBlueprint}') &&
repoBuilder.includes('title="Deze uitbreidingsbrief archiveren?"') &&
repoBuilder.includes('onConfirm={archiveExpansion}') &&
repoBuilder.includes('title="Dit deliverypakket archiveren?"') &&
repoBuilder.includes('onConfirm={archiveDeliveryPackage}') &&
repoBuilder.includes('title="Dit repositorywerkplan archiveren?"') &&
repoBuilder.includes('onConfirm={archiveWorkPlan}') &&
repoBuilder.includes('title="Deze repositorystatus archiveren?"') &&
repoBuilder.includes('onConfirm={archiveStateSnapshot}'),
);
const sessionPacks = read('src/features/session-packs/pages/SessionPacks.tsx');
pass('Session Packs exposes top next action for empty state', sessionPacks.includes('Create a Codex handoff before packaging a session.'));
pass('Session Packs archive action is confirmation-gated', sessionPacks.includes('Archive this session pack?') && sessionPacks.includes('AlertDialogAction onClick={archiveSelected}'));
const browserBridge = read('src/features/browser-bridge/pages/BrowserBridge.tsx');
pass('Browser Bridge exposes top session-pack-required action', browserBridge.includes('Session pack required') && browserBridge.includes('Open Session Packs'));
pass('Browser Bridge archive action is confirmation-gated', browserBridge.includes('Archive this browser bridge session?') && browserBridge.includes('AlertDialogAction onClick={archiveSession}'));
const codexHandoffs = read('src/features/handoffs/pages/CodexHandoffs.tsx');
pass('Codex handoff archive actions are confirmation-gated', codexHandoffs.includes('Archive this Codex handoff?') && codexHandoffs.includes('Archive this impact review?'));
const runtimeStateManager = read('src/features/operations/pages/RuntimeStateManager.tsx');
pass('Runtime reset uses accessible AlertDialog instead of native confirm', runtimeStateManager.includes('AlertDialogTitle') && !runtimeStateManager.includes('window.confirm'));
pass('Compact runtime state manager hides import-only control', runtimeStateManager.includes('!compact ? (') && runtimeStateManager.includes('Import JSON'));
const confirmActionButton = read('src/shared/components/ConfirmActionButton.tsx');
pass('Shared confirm action button uses accessible AlertDialog', confirmActionButton.includes('AlertDialogTitle') && confirmActionButton.includes('confirmLabel'));
const executionFeedback = read('src/features/execution-feedback/pages/ExecutionFeedback.tsx');
pass('Execution Feedback archive action is confirmation-gated', executionFeedback.includes('Archive this execution feedback?'));
const sourceLibrary = read('src/features/source-library/pages/SourceLibrary.tsx');
pass(
'Source Library archive action is confirmation-gated',
sourceLibrary.includes('ConfirmActionButton') &&
sourceLibrary.includes('title="Deze bron archiveren?"') &&
sourceLibrary.includes('confirmLabel="Bron archiveren"') &&
sourceLibrary.includes('onConfirm={() => archive(source.id)}'),
);
const projectHistory = read('src/features/history/pages/ProjectHistory.tsx');
pass('Project History remove action is confirmation-gated', projectHistory.includes('Dit geschiedenisitem verbergen?'));
const runtimeGateEvidence = read('src/features/operations/pages/RuntimeGateEvidence.tsx');
pass('Runtime evidence reset is confirmation-gated', runtimeGateEvidence.includes('Reset P2 evidence history?'));
const runtimeGateChecklist = read('src/features/operations/pages/RuntimeGateChecklist.tsx');
pass('Runtime checklist reset is confirmation-gated', runtimeGateChecklist.includes('Reset P2 checklist?'));
const runtimeTodoBoard = read('src/features/operations/pages/RuntimeTodoBoard.tsx');
pass('Runtime todo removal is confirmation-gated', runtimeTodoBoard.includes('Remove this runtime todo?'));
const packageJson = read('package.json');
pass('Mega audit command is removed from package scripts', !packageJson.includes('audit:mega'));
const failed = checks.filter((check) => !check.ok);
if (failed.length > 0) {
console.error('ProjectBrain visual polish audit: FAIL');
for (const check of failed) console.error(`- ${check.name}${check.detail ? `: ${check.detail}` : ''}`);
process.exit(1);
}
console.log('ProjectBrain visual polish audit: PASS');
for (const check of checks) console.log(`- ${check.name}`);