@deneb-ui/cli 2.0.70 → 2.0.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,6 +13,7 @@ const fs = require('fs');
13
13
  const path = require('path');
14
14
  const { spawnSync } = require('child_process');
15
15
  const { matchRecipeForProject, loadAllRecipes } = require('./recipe-engine.cjs');
16
+ const { healMissingSiteDataHooks, healBroadContainerMarkers, healSectionOverflowHidden } = require('../arc/transformer.cjs');
16
17
 
17
18
  function createBox(lines, width = 60) {
18
19
  const horizontal = '═'.repeat(width - 2);
@@ -286,6 +287,34 @@ function runDoctor(targetDirInput = '.', options = {}) {
286
287
  } else {
287
288
  addCheck(suite2, 'err', 'Platform Engine Compatibility', 'Detected packages requiring Node >=22. Run "deneb doctor --fix" to inject Node 20 overrides.', { code: 'DNB-ENG-001' }) //, 'Detected packages requiring Node >=22. Run "deneb doctor --fix" to inject Node 20 overrides.');
288
289
  }
290
+
291
+ // Check TypeScript compilation preflight
292
+ const tsconfigPath = path.join(targetDir, 'tsconfig.json');
293
+ if (fs.existsSync(tsconfigPath)) {
294
+ const tscBin = process.platform === 'win32'
295
+ ? path.join(targetDir, 'node_modules', '.bin', 'tsc.cmd')
296
+ : path.join(targetDir, 'node_modules', '.bin', 'tsc');
297
+
298
+ let tscRes = null;
299
+ if (fs.existsSync(tscBin)) {
300
+ tscRes = spawnSync(tscBin, ['--noEmit'], { cwd: targetDir, encoding: 'utf-8', shell: process.platform === 'win32', maxBuffer: 10 * 1024 * 1024 });
301
+ } else {
302
+ const npxCmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';
303
+ tscRes = spawnSync(npxCmd, ['--no-install', 'tsc', '--noEmit'], { cwd: targetDir, encoding: 'utf-8', shell: process.platform === 'win32', maxBuffer: 10 * 1024 * 1024 });
304
+ }
305
+
306
+ if (tscRes && tscRes.status === 0) {
307
+ addCheck(suite2, 'pass', 'TypeScript Compilation Preflight', '0 type errors detected (tsc --noEmit clean)', { code: 'DNB-TYP-001' });
308
+ } else if (tscRes && tscRes.status !== null) {
309
+ const rawErr = (tscRes.stdout || tscRes.stderr || '').trim();
310
+ const lines = rawErr.split(/\r?\n/).filter(Boolean);
311
+ const errLines = lines.filter((l) => l.includes('error TS'));
312
+ const summary = errLines.length > 0 ? `${errLines.length} type error(s) (e.g. ${errLines[0].trim()})` : (lines[0] || 'Type errors found');
313
+ addCheck(suite2, 'err', 'TypeScript Compilation Preflight', summary, { code: 'DNB-TYP-001', action: 'Fix TypeScript compilation errors shown by tsc --noEmit' });
314
+ } else {
315
+ addCheck(suite2, 'warn', 'TypeScript Compilation Preflight', 'Could not execute tsc to verify types', { code: 'DNB-TYP-001' });
316
+ }
317
+ }
289
318
  } catch (e) {
290
319
  addCheck(suite2, 'err', 'package.json Syntax', e.message);
291
320
  }
@@ -419,6 +448,37 @@ function runDoctor(targetDirInput = '.', options = {}) {
419
448
  addCheck(suite5, 'warn', 'site-data.json', 'src/data/site-data.json not found');
420
449
  }
421
450
 
451
+ // Root Layout SiteDataProvider Instrumentation Check
452
+ const doctorLayoutFiles = [
453
+ path.join(targetDir, 'src', 'app', 'layout.tsx'),
454
+ path.join(targetDir, 'src', 'app', 'layout.jsx'),
455
+ path.join(targetDir, 'app', 'layout.tsx'),
456
+ path.join(targetDir, 'app', 'layout.jsx'),
457
+ ];
458
+ const docLayout = doctorLayoutFiles.find((f) => fs.existsSync(f));
459
+ if (docLayout) {
460
+ const layoutSrc = fs.readFileSync(docLayout, 'utf8');
461
+ const hasSiteProvider = /SiteDataProvider|DenebDataProvider|<Providers\b/.test(layoutSrc);
462
+ if (hasSiteProvider) {
463
+ addCheck(suite5, 'pass', 'Root Layout Provider', `<SiteDataProvider> mounted in ${path.relative(targetDir, docLayout)}`, { code: 'DNB-LAY-001' });
464
+ } else if (isFix) {
465
+ try {
466
+ const { instrumentLayoutSource } = require('../arc/transformer.cjs');
467
+ const instrumented = instrumentLayoutSource(layoutSrc, '@/data/site-data.json', '@deneb-ui/ui');
468
+ if (instrumented.updated && instrumented.code !== layoutSrc) {
469
+ fs.writeFileSync(docLayout, instrumented.code, 'utf8');
470
+ addCheck(suite5, 'fixed', 'Root Layout Provider', `Instrumented <SiteDataProvider> into ${path.relative(targetDir, docLayout)}`, { code: 'DNB-LAY-001' });
471
+ } else {
472
+ addCheck(suite5, 'warn', 'Root Layout Provider', `Missing <SiteDataProvider> in ${path.relative(targetDir, docLayout)}`, { code: 'DNB-LAY-001' });
473
+ }
474
+ } catch {
475
+ addCheck(suite5, 'warn', 'Root Layout Provider', `Missing <SiteDataProvider> in ${path.relative(targetDir, docLayout)}`, { code: 'DNB-LAY-001' });
476
+ }
477
+ } else {
478
+ addCheck(suite5, 'warn', 'Root Layout Provider', `Missing <SiteDataProvider> in ${path.relative(targetDir, docLayout)} (Run with --fix to instrument automatically)`, { code: 'DNB-LAY-001' });
479
+ }
480
+ }
481
+
422
482
  // Scan all source files for visual editing contract compliance
423
483
  const sourceFiles = findSourceFiles(path.join(targetDir, 'src'));
424
484
  const foundFieldPaths = new Set();
@@ -503,8 +563,8 @@ function runDoctor(targetDirInput = '.', options = {}) {
503
563
  }
504
564
  }
505
565
 
506
- // 4. Broad Static Container Detection
507
- // Fivora forbids data-preview-static on broad layout containers like <div>, <section>, <nav>, <main>, <header>
566
+ // 4. Broad Container Detection
567
+ // Fivora forbids data-preview-static and data-preview-field-path on broad layout containers like <div>, <section>, <nav>, <main>, <header>
508
568
  const broadStaticMatches = code.matchAll(/<(div|section|nav|main|header|article|aside)(\s+[^>]*data-preview-static="[^"]*"[^>]*)>/gi);
509
569
  let fileNeedsBroadStaticFix = false;
510
570
  let newCodeBroad = fs.readFileSync(file, 'utf-8');
@@ -513,7 +573,7 @@ function runDoctor(targetDirInput = '.', options = {}) {
513
573
  const tag = bsm[1].toLowerCase();
514
574
  // Only flag if it's not a pure leaf element
515
575
  if (['div', 'section', 'nav', 'main', 'header', 'article', 'aside'].includes(tag)) {
516
- broadStaticContainers.push({ tag, file: path.relative(targetDir, file) });
576
+ broadStaticContainers.push({ tag, file: path.relative(targetDir, file), type: 'static' });
517
577
  if (shouldFix) {
518
578
  const originalTag = `<${bsm[1]}${bsm[2]}>`;
519
579
  const cleanTag = originalTag.replace(/\s*data-preview-static="[^"]*"/g, '');
@@ -523,6 +583,26 @@ function runDoctor(targetDirInput = '.', options = {}) {
523
583
  }
524
584
  }
525
585
 
586
+ const broadFieldMatches = code.matchAll(/<(div|section|nav|main|header|article|aside)(\s+[^>]*data-preview-field-path="[^"]*"[^>]*)>/gi);
587
+ for (const bfm of broadFieldMatches) {
588
+ const tag = bfm[1].toLowerCase();
589
+ if (['div', 'section', 'nav', 'main', 'header', 'article', 'aside'].includes(tag)) {
590
+ broadStaticContainers.push({ tag, file: path.relative(targetDir, file), type: 'field' });
591
+ }
592
+ }
593
+
594
+ if (shouldFix && [...broadFieldMatches].length > 0) {
595
+ try {
596
+ const { parseSource, printSource } = require('../arc/ast.cjs');
597
+ const ast = parseSource(newCodeBroad, file);
598
+ const healed = healBroadContainerMarkers(ast);
599
+ if (healed > 0) {
600
+ newCodeBroad = printSource(ast, newCodeBroad);
601
+ fileNeedsBroadStaticFix = true;
602
+ }
603
+ } catch {}
604
+ }
605
+
526
606
  if (shouldFix && fileNeedsBroadStaticFix) {
527
607
  fs.writeFileSync(file, newCodeBroad, 'utf8');
528
608
  }
@@ -595,11 +675,11 @@ function runDoctor(targetDirInput = '.', options = {}) {
595
675
  }
596
676
 
597
677
  if (broadStaticContainers.length === 0) {
598
- addCheck(suite5, 'pass', 'Granular Static Markup', 'Zero broad layout containers (div/nav/section) marked static', { code: 'DNB-STC-006' }) //, 'Zero broad layout containers (div/nav/section) marked static');
678
+ addCheck(suite5, 'pass', 'Granular Static Markup', 'Zero broad layout containers (div/nav/section) marked static or field-bound', { code: 'DNB-STC-006' });
599
679
  } else if (shouldFix) {
600
- addCheck(suite5, 'fixed', 'Granular Static Markup', `Stripped data-preview-static from ${broadStaticContainers.length} broad container(s)`, { code: 'DNB-STC-006' }) //, `Stripped data-preview-static from ${broadStaticContainers.length} broad container(s)`);
680
+ addCheck(suite5, 'fixed', 'Granular Static Markup', `Repaired/demoted ${broadStaticContainers.length} broad container marker(s)`, { code: 'DNB-STC-006' });
601
681
  } else {
602
- addCheck(suite5, 'warn', 'Granular Static Markup', `${broadStaticContainers.length} broad container(s) marked with data-preview-static (Fivora requires marking only smallest leaf elements)`, { code: 'DNB-STC-006' }) //, `${broadStaticContainers.length} broad container(s) marked with data-preview-static (Fivora requires marking only smallest leaf elements)`);
682
+ addCheck(suite5, 'warn', 'Granular Static Markup', `${broadStaticContainers.length} broad container(s) marked with static or field paths (Fivora requires marking only smallest leaf elements). Run with --fix to repair.`, { code: 'DNB-STC-006' });
603
683
  }
604
684
 
605
685
  if (dynamicVariableMarkers.length === 0) {
@@ -608,6 +688,73 @@ function runDoctor(targetDirInput = '.', options = {}) {
608
688
  addCheck(suite5, 'warn', 'Literal Marker Standard', `${dynamicVariableMarkers.length} dynamic variable marker(s) detected`, { code: 'DNB-AST-002' }) //, `${dynamicVariableMarkers.length} dynamic variable marker(s) detected (e.g. ${dynamicVariableMarkers[0].expr})`);
609
689
  }
610
690
 
691
+ // Check: Control-Only & Platform Contract Path Integrity (DNB-CTL-001)
692
+ if (manifestData) {
693
+ manifestData.visualEditing = manifestData.visualEditing || {};
694
+ const existingControlOnly = new Set(manifestData.visualEditing.controlOnlyPaths || []);
695
+ const missingControlOnly = [];
696
+
697
+ // 1. Platform contract paths: if __fivoraIntake or additionalPages exist in editorSchema,
698
+ // ensure their paths are in controlOnlyPaths
699
+ const schemaSections = manifestData.editorSchema?.sections || [];
700
+ const hasIntake = schemaSections.some((s) => s.id === '__fivoraIntake');
701
+ const hasPages = schemaSections.some((s) => s.id === 'additionalPages');
702
+
703
+ if (hasIntake) {
704
+ const intakePaths = [
705
+ '__fivoraIntake.tone',
706
+ '__fivoraIntake.outputLanguage',
707
+ '__fivoraIntake.businessSummary',
708
+ '__fivoraIntake.additionalBusinessDetails',
709
+ '__fivoraIntake.referenceWebsiteUrl',
710
+ ];
711
+ for (const p of intakePaths) {
712
+ if (!existingControlOnly.has(p)) missingControlOnly.push(p);
713
+ }
714
+ }
715
+
716
+ if (hasPages) {
717
+ const pagesPaths = [
718
+ 'additionalPages[*].id',
719
+ 'additionalPages[*].route',
720
+ 'additionalPages[*].label',
721
+ ];
722
+ for (const p of pagesPaths) {
723
+ if (!existingControlOnly.has(p)) missingControlOnly.push(p);
724
+ }
725
+ }
726
+
727
+ // 2. Modal & unrendered form fields in siteData.content (e.g. *.form.*)
728
+ if (siteData && siteData.content) {
729
+ function collectModalFormPaths(obj, prefix = '') {
730
+ if (!obj || typeof obj !== 'object') return;
731
+ if (Array.isArray(obj)) return;
732
+ for (const [k, v] of Object.entries(obj)) {
733
+ const p = prefix ? `${prefix}.${k}` : k;
734
+ if (typeof v === 'object' && v !== null && !Array.isArray(v)) {
735
+ collectModalFormPaths(v, p);
736
+ } else {
737
+ if (/\.form\./i.test(p) || /^form\./i.test(p) || /\.modal\./i.test(p) || /Modal\./i.test(p)) {
738
+ if (!existingControlOnly.has(p)) missingControlOnly.push(p);
739
+ }
740
+ }
741
+ }
742
+ }
743
+ collectModalFormPaths(siteData.content);
744
+ }
745
+
746
+ if (missingControlOnly.length === 0) {
747
+ addCheck(suite5, 'pass', 'Control-Only Path Contract', 'All platform-managed & modal form paths declared in controlOnlyPaths', { code: 'DNB-CTL-001' });
748
+ } else if (shouldFix) {
749
+ for (const p of missingControlOnly) existingControlOnly.add(p);
750
+ manifestData.visualEditing.controlOnlyPaths = [...existingControlOnly].sort();
751
+ fs.writeFileSync(manifestPath, JSON.stringify(manifestData, null, 2) + '\n', 'utf8');
752
+ addCheck(suite5, 'fixed', 'Control-Only Path Contract', `Added ${missingControlOnly.length} platform & modal field(s) to controlOnlyPaths`, { code: 'DNB-CTL-001' });
753
+ } else {
754
+ addCheck(suite5, 'warn', 'Control-Only Path Contract', `${missingControlOnly.length} platform or modal path(s) missing from controlOnlyPaths (Run with --fix to register automatically)`, { code: 'DNB-CTL-001' });
755
+ }
756
+ }
757
+
611
758
  // =========================================================================
612
759
  // SUITE 6: Multi-Niche Storefront Architecture & Security Preflight
613
760
  // =========================================================================
@@ -647,17 +794,33 @@ function runDoctor(targetDirInput = '.', options = {}) {
647
794
  // Check: Empty-State Array & Out-of-Range Guards (DNB-ARR-003)
648
795
  let unguardedArrayCount = 0;
649
796
  for (const file of sourceFiles) {
650
- const c = fs.readFileSync(file, 'utf-8');
797
+ let c = fs.readFileSync(file, 'utf-8');
798
+ let fileModified = false;
651
799
  if (c.includes('data-preview-list-path')) {
652
800
  if (/\?\s*[A-Z0-9_]+\s*:\s*[A-Z0-9_]+/i.test(c) && !c.includes('Array.isArray')) {
653
801
  unguardedArrayCount++;
802
+ if (shouldFix) {
803
+ const repaired = c.replace(
804
+ /([a-zA-Z0-9_$]+)\s*(?:&&|\?\.)\s*length(?:\s*>\s*0)?\s*\?\s*\1\s*:\s*([a-zA-Z0-9_$]+)/g,
805
+ 'Array.isArray($1) ? $1 : ($1 || $2)'
806
+ );
807
+ if (repaired !== c) {
808
+ c = repaired;
809
+ fileModified = true;
810
+ }
811
+ }
654
812
  }
655
813
  }
814
+ if (shouldFix && fileModified) {
815
+ fs.writeFileSync(file, c, 'utf8');
816
+ }
656
817
  }
657
818
  if (unguardedArrayCount === 0) {
658
819
  addCheck(suite5, 'pass', 'Empty-State Array Guards', 'All list collections guarded against empty state ([]) out-of-range elements', { code: 'DNB-ARR-003' });
820
+ } else if (shouldFix) {
821
+ addCheck(suite5, 'fixed', 'Empty-State Array Guards', `Injected Array.isArray safe empty-state guard into ${unguardedArrayCount} component(s)`, { code: 'DNB-ARR-003' });
659
822
  } else {
660
- addCheck(suite5, 'warn', 'Empty-State Array Guards', `${unguardedArrayCount} list component(s) should verify Array.isArray(liveList) ? liveList : DEFAULT_LIST`, { code: 'DNB-ARR-003' });
823
+ addCheck(suite5, 'warn', 'Empty-State Array Guards', `${unguardedArrayCount} list component(s) should verify Array.isArray(liveList) ? liveList : (liveList || DEFAULT_LIST). Run with --fix to repair.`, { code: 'DNB-ARR-003' });
661
824
  }
662
825
 
663
826
  // Check: Empty-State Singleton Persistence Guard (DNB-EMP-002)
@@ -701,6 +864,120 @@ function runDoctor(targetDirInput = '.', options = {}) {
701
864
  addCheck(suite5, 'warn', 'WhatsApp Action Live Sync', `${disconnectedWhatsAppCount} WhatsApp trigger(s) using disconnected fallback instead of whatsappOrderUrl. Run with --fix to repair.`, { code: 'DNB-WHA-008' });
702
865
  }
703
866
 
867
+ // Check: Multi-Component Scope & useSiteData Injection (DNB-SCP-001)
868
+ let missingSiteDataHookCount = 0;
869
+ for (const file of sourceFiles) {
870
+ if (!/\.(tsx|jsx|ts|js)$/.test(file)) continue;
871
+ let c = fs.readFileSync(file, 'utf-8');
872
+ if (c.includes('siteData') && (c.includes('useSiteData') || c.includes('@deneb-ui/ui'))) {
873
+ const healed = healMissingSiteDataHooks(c, file);
874
+ const norm = (s) => s.replace(/\r\n/g, '\n');
875
+ if (healed && norm(healed) !== norm(c)) {
876
+ missingSiteDataHookCount++;
877
+ if (shouldFix) {
878
+ fs.writeFileSync(file, healed, 'utf8');
879
+ }
880
+ }
881
+ }
882
+ }
883
+ if (missingSiteDataHookCount === 0) {
884
+ addCheck(suite5, 'pass', 'Component Scope siteData Binding', 'All components referencing siteData have local useSiteData() hook binding', { code: 'DNB-SCP-001' });
885
+ } else if (shouldFix) {
886
+ addCheck(suite5, 'fixed', 'Component Scope siteData Binding', `Injected missing useSiteData() hook into ${missingSiteDataHookCount} component(s)`, { code: 'DNB-SCP-001' });
887
+ } else {
888
+ addCheck(suite5, 'err', 'Component Scope siteData Binding', `${missingSiteDataHookCount} component(s) reference siteData without calling useSiteData(). Run with --fix to inject.`, { code: 'DNB-SCP-001' });
889
+ }
890
+
891
+ // Check: Collection Callback TypeScript Typing & Collision Guard (DNB-TYP-002)
892
+ let collParamsFixed = 0;
893
+ for (const file of sourceFiles) {
894
+ if (!/\.(tsx|ts)$/.test(file)) continue;
895
+ let c = fs.readFileSync(file, 'utf-8');
896
+ let fileModified = false;
897
+
898
+ // 1. Fix 3-parameter collision: .map((stat, idx, index) =>
899
+ const trippleMatches = [...c.matchAll(/\.map\(\s*\(\s*([a-zA-Z0-9_$]+)\s*,\s*([a-zA-Z0-9_$]+)\s*,\s*index\s*\)\s*=>/g)];
900
+ if (trippleMatches.length > 0) {
901
+ collParamsFixed += trippleMatches.length;
902
+ if (shouldFix) {
903
+ for (const m of trippleMatches) {
904
+ const p1 = m[1];
905
+ const p2 = m[2];
906
+ c = c.replace(m[0], `.map((${p1}: any, ${p2}: number) =>`);
907
+ c = c.replace(/\[\$\{index\}\]/g, `[\${${p2}}]`);
908
+ }
909
+ fileModified = true;
910
+ }
911
+ }
912
+
913
+ // 2. Fix leftover [${index}] when callback uses idx
914
+ if (c.includes('[${index}]') && c.includes('idx') && !c.includes('index) =>') && !c.includes('index,') && !c.includes('index:')) {
915
+ c = c.replace(/\[\$\{index\}\]/g, '[${idx}]');
916
+ fileModified = true;
917
+ }
918
+
919
+ // 3. Fix untyped parameters in .map() on data-preview or siteData collections causing TS7006
920
+ if (c.includes('data-preview-') || c.includes('siteData')) {
921
+ const untypedMap2 = [...c.matchAll(/\.map\(\s*\(\s*([a-zA-Z0-9_$]+)\s*,\s*([a-zA-Z0-9_$]+)\s*\)\s*=>/g)];
922
+ for (const m of untypedMap2) {
923
+ const p1 = m[1];
924
+ const p2 = m[2];
925
+ if (!p1.includes(':') && !p2.includes(':')) {
926
+ collParamsFixed++;
927
+ if (shouldFix) {
928
+ c = c.replace(m[0], `.map((${p1}: any, ${p2}: number) =>`);
929
+ fileModified = true;
930
+ }
931
+ }
932
+ }
933
+
934
+ const untypedMap1 = [...c.matchAll(/\.map\(\s*\(\s*([a-zA-Z0-9_$]+)\s*\)\s*=>/g)];
935
+ for (const m of untypedMap1) {
936
+ const p1 = m[1];
937
+ if (!p1.includes(':')) {
938
+ collParamsFixed++;
939
+ if (shouldFix) {
940
+ c = c.replace(m[0], `.map((${p1}: any) =>`);
941
+ fileModified = true;
942
+ }
943
+ }
944
+ }
945
+ }
946
+
947
+ if (shouldFix && fileModified) {
948
+ fs.writeFileSync(file, c, 'utf8');
949
+ }
950
+ }
951
+
952
+ if (collParamsFixed === 0) {
953
+ addCheck(suite5, 'pass', 'Collection Callback TypeScript Contracts', 'Zero parameter collisions or untyped callback parameters in collection maps', { code: 'DNB-TYP-002' });
954
+ } else if (shouldFix) {
955
+ addCheck(suite5, 'fixed', 'Collection Callback TypeScript Contracts', `Repaired and type-annotated ${collParamsFixed} collection map callback(s)`, { code: 'DNB-TYP-002' });
956
+ } else {
957
+ addCheck(suite5, 'warn', 'Collection Callback TypeScript Contracts', `${collParamsFixed} collection callback(s) have untyped parameters or parameter collisions. Run with --fix to repair.`, { code: 'DNB-TYP-002' });
958
+ }
959
+
960
+ // Check: JSON Module Direct Import Type Narrowing Guard (DNB-TYP-003)
961
+ let staticJsonImportCount = 0;
962
+ for (const file of sourceFiles) {
963
+ if (!/\.(tsx|ts)$/.test(file)) continue;
964
+ let c = fs.readFileSync(file, 'utf-8');
965
+ if (c.includes('import siteData from') && c.includes('site-data.json')) {
966
+ staticJsonImportCount++;
967
+ if (shouldFix) {
968
+ c = c.replace(/import\s+siteData\s+from\s+['"]([^'"]*site-data\.json)['"];?/, 'import rawSiteData from "$1";\nconst siteData: any = rawSiteData;');
969
+ fs.writeFileSync(file, c, 'utf8');
970
+ }
971
+ }
972
+ }
973
+ if (staticJsonImportCount === 0) {
974
+ addCheck(suite5, 'pass', 'JSON Import Type Safety', 'No statically locked site-data.json imports causing TS2339 schema collisions', { code: 'DNB-TYP-003' });
975
+ } else if (shouldFix) {
976
+ addCheck(suite5, 'fixed', 'JSON Import Type Safety', `Safely typed ${staticJsonImportCount} static site-data.json import(s)`, { code: 'DNB-TYP-003' });
977
+ } else {
978
+ addCheck(suite5, 'warn', 'JSON Import Type Safety', `${staticJsonImportCount} file(s) statically import site-data.json without any-casting, which can trigger TS2339 under resolveJsonModule. Run with --fix to heal.`, { code: 'DNB-TYP-003' });
979
+ }
980
+
704
981
  // Check: Next.js Image Empty String Guard (DNB-IMG-002)
705
982
  let emptyImageSrcCount = 0;
706
983
  for (const file of sourceFiles) {
@@ -895,9 +1172,20 @@ function runDoctor(targetDirInput = '.', options = {}) {
895
1172
  fs.existsSync(path.join(targetDir, 'public', 'preview.png'));
896
1173
 
897
1174
  if (previewExists) {
898
- addCheck(suite6, 'pass', 'Storefront Preview Graphic', 'preview.png / thumbnail.png verified for Fivora gallery', { code: 'DNB-PRV-001' }) //, 'preview.png / thumbnail.png verified for Fivora gallery');
1175
+ addCheck(suite6, 'pass', 'Storefront Preview Graphic', 'preview.png / thumbnail.png verified for Fivora gallery', { code: 'DNB-PRV-001' });
1176
+ } else if (shouldFix) {
1177
+ try {
1178
+ const minimalPng = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', 'base64');
1179
+ fs.writeFileSync(path.join(targetDir, 'preview.png'), minimalPng);
1180
+ if (fs.existsSync(path.join(targetDir, 'public'))) {
1181
+ fs.writeFileSync(path.join(targetDir, 'public', 'preview.png'), minimalPng);
1182
+ }
1183
+ addCheck(suite6, 'fixed', 'Storefront Preview Graphic', 'Created placeholder preview.png for Fivora marketplace preflight', { code: 'DNB-PRV-001' });
1184
+ } catch {
1185
+ addCheck(suite6, 'warn', 'Storefront Preview Graphic', 'preview.png not found in root or public folder', { code: 'DNB-PRV-001' });
1186
+ }
899
1187
  } else {
900
- addCheck(suite6, 'warn', 'Storefront Preview Graphic', 'preview.png not found in root or public folder', { code: 'DNB-PRV-001' }) //, 'preview.png not found in root or public folder');
1188
+ addCheck(suite6, 'warn', 'Storefront Preview Graphic', 'preview.png not found in root or public folder (Run with --fix to scaffold placeholder)', { code: 'DNB-PRV-001' });
901
1189
  }
902
1190
 
903
1191
  // Large assets audit (> 4MB)