@deneb-ui/cli 2.0.53 → 2.0.55

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.
@@ -133,6 +133,8 @@ function runDoctor(targetDirInput = '.', options = {}) {
133
133
  };
134
134
 
135
135
  function addCheck(suiteName, type, title, detail, meta = {}) {
136
+ const code = meta.code || 'DNB-GEN-001';
137
+ const action = meta.action || detail || '';
136
138
  if (type === 'pass') reportData.passed++;
137
139
  else if (type === 'warn') reportData.warnings++;
138
140
  else if (type === 'err') reportData.errors++;
@@ -146,17 +148,19 @@ function runDoctor(targetDirInput = '.', options = {}) {
146
148
  suite = { name: suiteName, checks: [] };
147
149
  reportData.suites.push(suite);
148
150
  }
149
- suite.checks.push({ type, title, detail, ...meta });
151
+ const checkObj = { code, type, title, detail, action, ...meta };
152
+ suite.checks.push(checkObj);
150
153
 
151
154
  if (!isJson) {
155
+ const codeTag = `\x1b[36m[${code}]\x1b[0m `;
152
156
  if (type === 'pass') {
153
- console.log(` \x1b[32m✔\x1b[0m \x1b[1m${title}\x1b[0m${detail ? ` \x1b[90m(${detail})\x1b[0m` : ''}`);
157
+ console.log(` \x1b[32m✔\x1b[0m ${codeTag}\x1b[1m${title}\x1b[0m${detail ? ` \x1b[90m(${detail})\x1b[0m` : ''}`);
154
158
  } else if (type === 'fixed') {
155
- console.log(` \x1b[35m⚡ FIXED:\x1b[0m \x1b[1m${title}\x1b[0m${detail ? ` \x1b[32m- ${detail}\x1b[0m` : ''}`);
159
+ console.log(` \x1b[35m⚡ FIXED:\x1b[0m ${codeTag}\x1b[1m${title}\x1b[0m${detail ? ` \x1b[32m- ${detail}\x1b[0m` : ''}`);
156
160
  } else if (type === 'warn') {
157
- console.log(` \x1b[33m⚠\x1b[0m \x1b[33m${title}\x1b[0m${detail ? ` \x1b[90m- ${detail}\x1b[0m` : ''}`);
161
+ console.log(` \x1b[33m⚠\x1b[0m ${codeTag}\x1b[33m${title}\x1b[0m${detail ? ` \x1b[90m- ${detail}\x1b[0m` : ''}`);
158
162
  } else {
159
- console.log(` \x1b[31m✖\x1b[0m \x1b[31m${title}\x1b[0m${detail ? ` \x1b[90m- ${detail}\x1b[0m` : ''}`);
163
+ console.log(` \x1b[31m✖\x1b[0m ${codeTag}\x1b[31m${title}\x1b[0m${detail ? ` \x1b[90m- ${detail}\x1b[0m` : ''}`);
160
164
  }
161
165
  }
162
166
  }
@@ -178,17 +182,17 @@ function runDoctor(targetDirInput = '.', options = {}) {
178
182
  const nodeVersion = process.version;
179
183
  const majorNode = parseInt(nodeVersion.replace(/^v/, '').split('.')[0], 10);
180
184
  if (majorNode >= 18) {
181
- addCheck(suite1, 'pass', 'Node.js Runtime', `${nodeVersion} (Supported)`);
185
+ addCheck(suite1, 'pass', 'Node.js Runtime', `${nodeVersion} (Supported)`, { code: 'DNB-SYS-001' }) //, `${nodeVersion} (Supported)`);
182
186
  } else {
183
- addCheck(suite1, 'err', 'Node.js Runtime', `${nodeVersion} (Requires Node.js >= 18.0.0)`);
187
+ addCheck(suite1, 'err', 'Node.js Runtime', `${nodeVersion} (Requires Node.js >= 18.0.0)`, { code: 'DNB-SYS-001' }) //, `${nodeVersion} (Requires Node.js >= 18.0.0)`);
184
188
  }
185
189
 
186
190
  const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm';
187
191
  const npmCheck = spawnSync(npmBin, ['--version'], { encoding: 'utf-8', shell: process.platform === 'win32' });
188
192
  if (!npmCheck.error && npmCheck.status === 0) {
189
- addCheck(suite1, 'pass', 'Package Manager', `npm v${npmCheck.stdout.trim()}`);
193
+ addCheck(suite1, 'pass', 'Package Manager', `npm v${npmCheck.stdout.trim()}`, { code: 'DNB-SYS-002' }) //, `npm v${npmCheck.stdout.trim()}`);
190
194
  } else {
191
- addCheck(suite1, 'warn', 'Package Manager', 'npm not found in system PATH');
195
+ addCheck(suite1, 'warn', 'Package Manager', 'npm not found in system PATH', { code: 'DNB-SYS-002' }) //, 'npm not found in system PATH');
192
196
  }
193
197
 
194
198
  // =========================================================================
@@ -202,26 +206,26 @@ function runDoctor(targetDirInput = '.', options = {}) {
202
206
  if (fs.existsSync(pkgPath)) {
203
207
  try {
204
208
  pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
205
- addCheck(suite2, 'pass', 'package.json', `Found "${pkg.name || 'unnamed'}"`);
209
+ addCheck(suite2, 'pass', 'package.json', `Found "${pkg.name || 'unnamed'}"`, { code: 'DNB-PKG-001' }) //, `Found "${pkg.name || 'unnamed'}"`);
206
210
 
207
211
  const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
208
212
 
209
213
  if (allDeps['next']) {
210
- addCheck(suite2, 'pass', 'Next.js Framework', allDeps['next']);
214
+ addCheck(suite2, 'pass', 'Next.js Framework', allDeps['next'], { code: 'DNB-PKG-002' }) //, allDeps['next']);
211
215
  } else {
212
- addCheck(suite2, 'err', 'Next.js Framework', 'next dependency missing in package.json');
216
+ addCheck(suite2, 'err', 'Next.js Framework', 'next dependency missing in package.json', { code: 'DNB-PKG-002' }) //, 'next dependency missing in package.json');
213
217
  }
214
218
 
215
219
  if (allDeps['@deneb-ui/ui'] || allDeps['@deneb/ui']) {
216
- addCheck(suite2, 'pass', '@deneb-ui/ui Library', allDeps['@deneb-ui/ui'] || allDeps['@deneb/ui']);
220
+ addCheck(suite2, 'pass', '@deneb-ui/ui Library', allDeps['@deneb-ui/ui'] || allDeps['@deneb/ui'], { code: 'DNB-PKG-003' }) //, allDeps['@deneb-ui/ui'] || allDeps['@deneb/ui']);
217
221
  } else {
218
- addCheck(suite2, 'warn', '@deneb-ui/ui Library', 'Not installed (run "npm i @deneb-ui/ui")');
222
+ addCheck(suite2, 'warn', '@deneb-ui/ui Library', 'Not installed (run "npm i @deneb-ui/ui")', { code: 'DNB-PKG-003' }) //, 'Not installed (run "npm i @deneb-ui/ui")');
219
223
  }
220
224
 
221
225
  if (allDeps['@deneb-ui/cli']) {
222
- addCheck(suite2, 'pass', '@deneb-ui/cli Tooling', allDeps['@deneb-ui/cli']);
226
+ addCheck(suite2, 'pass', '@deneb-ui/cli Tooling', allDeps['@deneb-ui/cli'], { code: 'DNB-PKG-004' }) //, allDeps['@deneb-ui/cli']);
223
227
  } else {
224
- addCheck(suite2, 'warn', '@deneb-ui/cli Tooling', 'Recommended for local CLI scripts');
228
+ addCheck(suite2, 'warn', '@deneb-ui/cli Tooling', 'Recommended for local CLI scripts', { code: 'DNB-PKG-004' }) //, 'Recommended for local CLI scripts');
225
229
  }
226
230
 
227
231
  // Check required scripts
@@ -230,16 +234,57 @@ function runDoctor(targetDirInput = '.', options = {}) {
230
234
  const missingScripts = requiredScripts.filter((s) => !pkg.scripts[s]);
231
235
 
232
236
  if (missingScripts.length === 0) {
233
- addCheck(suite2, 'pass', 'DENEB Package Scripts', 'lab, validate, zip, validate-and-zip verified');
237
+ addCheck(suite2, 'pass', 'DENEB Package Scripts', 'lab, validate, zip, validate-and-zip verified', { code: 'DNB-PKG-005' }) //, 'lab, validate, zip, validate-and-zip verified');
234
238
  } else if (shouldFix) {
235
239
  pkg.scripts['lab'] = pkg.scripts['lab'] || 'deneb lab .';
236
240
  pkg.scripts['validate'] = pkg.scripts['validate'] || 'deneb validate .';
237
241
  pkg.scripts['zip'] = pkg.scripts['zip'] || 'deneb zip .';
238
242
  pkg.scripts['validate-and-zip'] = pkg.scripts['validate-and-zip'] || 'deneb validate-and-zip .';
239
243
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
240
- addCheck(suite2, 'fixed', 'DENEB Package Scripts', `Injected missing scripts: ${missingScripts.join(', ')}`);
244
+ addCheck(suite2, 'fixed', 'DENEB Package Scripts', `Injected missing scripts: ${missingScripts.join(', ')}`, { code: 'DNB-PKG-005' }) //, `Injected missing scripts: ${missingScripts.join(', ')}`);
241
245
  } else {
242
- addCheck(suite2, 'warn', 'DENEB Package Scripts', `Missing scripts: ${missingScripts.join(', ')} (Run with --fix to repair)`);
246
+ addCheck(suite2, 'warn', 'DENEB Package Scripts', `Missing scripts: ${missingScripts.join(', ')} (Run with --fix to repair)`, { code: 'DNB-PKG-005' }) //, `Missing scripts: ${missingScripts.join(', ')} (Run with --fix to repair)`);
247
+ }
248
+
249
+ // Check Node 20 LTS platform engine compatibility
250
+ const lockPath = path.join(targetDir, 'package-lock.json');
251
+ let hasNode22EngineConflict = false;
252
+ const checkNode20 = (e) => {
253
+ if (!e || e === '*' || e === 'latest') return true;
254
+ return e.split('||').map(n => n.trim()).some(n => {
255
+ let r = n.match(/>=\s*(\d+)/);
256
+ if (r) return parseInt(r[1], 10) <= 20;
257
+ let i = n.match(/\^\s*(\d+)/);
258
+ if (i) return parseInt(i[1], 10) <= 20;
259
+ let s = n.match(/~(\d+)/);
260
+ if (s) return parseInt(s[1], 10) <= 20;
261
+ let o = n.match(/^\s*(\d+)/);
262
+ if (o) return parseInt(o[1], 10) <= 20;
263
+ return false;
264
+ });
265
+ };
266
+ if (fs.existsSync(lockPath)) {
267
+ try {
268
+ const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
269
+ for (const [pkgKey, pkgVal] of Object.entries(lock.packages || {})) {
270
+ const engineNode = pkgVal?.engines?.node;
271
+ if (engineNode && typeof engineNode === 'string' && !checkNode20(engineNode)) {
272
+ hasNode22EngineConflict = true;
273
+ break;
274
+ }
275
+ }
276
+ } catch {}
277
+ }
278
+ if (!hasNode22EngineConflict) {
279
+ addCheck(suite2, 'pass', 'Platform Engine Compatibility', 'All dependencies compatible with Fivora Node 20 LTS runtime', { code: 'DNB-ENG-001' }) //, 'All dependencies compatible with Fivora Node 20 LTS runtime');
280
+ } else if (shouldFix) {
281
+ pkg.overrides = pkg.overrides || {};
282
+ pkg.overrides['content-type'] = '2.1.0';
283
+ pkg.overrides['@octokit/request'] = { 'content-type': '2.1.0' };
284
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
285
+ addCheck(suite2, 'fixed', 'Platform Engine Compatibility', 'Injected Node 20 overrides for content-type: 2.1.0', { code: 'DNB-ENG-001' }) //, 'Injected Node 20 overrides for content-type: 2.1.0');
286
+ } else {
287
+ 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.');
243
288
  }
244
289
  } catch (e) {
245
290
  addCheck(suite2, 'err', 'package.json Syntax', e.message);
@@ -263,21 +308,21 @@ function runDoctor(targetDirInput = '.', options = {}) {
263
308
  const hasUnoptimized = /unoptimized\s*:\s*true/.test(content);
264
309
 
265
310
  if (hasExport) {
266
- addCheck(suite3, 'pass', 'Next.js Static Export', `output: 'export' verified in ${path.basename(nextConfigPath)}`);
311
+ addCheck(suite3, 'pass', 'Next.js Static Export', `output: 'export' verified in ${path.basename(nextConfigPath)}`, { code: 'DNB-EXP-001' }) //, `output: 'export' verified in ${path.basename(nextConfigPath)}`);
267
312
  } else if (shouldFix) {
268
313
  if (content.includes('nextConfig')) {
269
314
  content = content.replace(/(const\s+nextConfig\s*=\s*{)/, `$1\n output: 'export',`);
270
315
  fs.writeFileSync(nextConfigPath, content, 'utf8');
271
- addCheck(suite3, 'fixed', 'Next.js Static Export', `Added output: 'export' to ${path.basename(nextConfigPath)}`);
316
+ addCheck(suite3, 'fixed', 'Next.js Static Export', `Added output: 'export' to ${path.basename(nextConfigPath)}`, { code: 'DNB-EXP-001' }) //, `Added output: 'export' to ${path.basename(nextConfigPath)}`);
272
317
  } else {
273
- addCheck(suite3, 'err', 'Next.js Static Export', `Missing output: 'export' in ${path.basename(nextConfigPath)}`);
318
+ addCheck(suite3, 'err', 'Next.js Static Export', `Missing output: 'export' in ${path.basename(nextConfigPath)} (Required by Fivora)`, { code: 'DNB-EXP-001' }) //, `Missing output: 'export' in ${path.basename(nextConfigPath)}`);
274
319
  }
275
320
  } else {
276
321
  addCheck(suite3, 'err', 'Next.js Static Export', `Missing output: 'export' in ${path.basename(nextConfigPath)} (Required by Fivora)`);
277
322
  }
278
323
 
279
324
  if (hasUnoptimized) {
280
- addCheck(suite3, 'pass', 'Image Optimization Preflight', 'images.unoptimized = true verified');
325
+ addCheck(suite3, 'pass', 'Image Optimization Preflight', 'images.unoptimized = true verified', { code: 'DNB-IMG-001' }) //, 'images.unoptimized = true verified');
281
326
  } else if (shouldFix) {
282
327
  if (content.includes('images:')) {
283
328
  content = content.replace(/images:\s*{/, `images: { unoptimized: true, `);
@@ -285,9 +330,9 @@ function runDoctor(targetDirInput = '.', options = {}) {
285
330
  content = content.replace(/(const\s+nextConfig\s*=\s*{)/, `$1\n images: { unoptimized: true },`);
286
331
  }
287
332
  fs.writeFileSync(nextConfigPath, content, 'utf8');
288
- addCheck(suite3, 'fixed', 'Image Optimization Preflight', `Added images.unoptimized = true to ${path.basename(nextConfigPath)}`);
333
+ addCheck(suite3, 'fixed', 'Image Optimization Preflight', `Added images.unoptimized = true to ${path.basename(nextConfigPath)}`, { code: 'DNB-IMG-001' }) //, `Added images.unoptimized = true to ${path.basename(nextConfigPath)}`);
289
334
  } else {
290
- addCheck(suite3, 'warn', 'Image Optimization Preflight', 'Missing images.unoptimized = true (Next.js Image export requires unoptimized: true)');
335
+ addCheck(suite3, 'warn', 'Image Optimization Preflight', 'Missing images.unoptimized = true (Next.js Image export requires unoptimized: true)', { code: 'DNB-IMG-001' }) //, 'Missing images.unoptimized = true (Next.js Image export requires unoptimized: true)');
291
336
  }
292
337
  } else {
293
338
  addCheck(suite3, 'err', 'Next.js Config', 'No next.config.ts, next.config.mjs, or next.config.js found');
@@ -305,21 +350,21 @@ function runDoctor(targetDirInput = '.', options = {}) {
305
350
  if (fs.existsSync(manifestPath)) {
306
351
  try {
307
352
  manifestData = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
308
- addCheck(suite4, 'pass', 'fivora-template.json', `Valid JSON (strict=${manifestData.strict !== false})`);
353
+ addCheck(suite4, 'pass', 'fivora-template.json', `Valid JSON (strict=${manifestData.strict !== false})`, { code: 'DNB-MNF-001' }) //, `Valid JSON (strict=${manifestData.strict !== false})`);
309
354
 
310
355
  if (manifestData.version === 2 || manifestData.version === '2') {
311
- addCheck(suite4, 'pass', 'Manifest Contract Version', 'Version 2 (Current standard)');
356
+ addCheck(suite4, 'pass', 'Manifest Contract Version', 'Version 2 (Current standard)', { code: 'DNB-MNF-002' }) //, 'Version 2 (Current standard)');
312
357
  } else {
313
- addCheck(suite4, 'warn', 'Manifest Contract Version', `Version ${manifestData.version} detected (Recommend version 2)`);
358
+ addCheck(suite4, 'warn', 'Manifest Contract Version', `Version ${manifestData.version} detected (Recommend version 2)`, { code: 'DNB-MNF-002' }) //, `Version ${manifestData.version} detected (Recommend version 2)`);
314
359
  }
315
360
 
316
361
  // Check home route
317
362
  const pages = Array.isArray(manifestData.pages) ? manifestData.pages : [];
318
363
  const hasHome = pages.some((p) => p.route === '/' || p.slug === '/' || p.path === '/' || p.id === 'home');
319
364
  if (hasHome) {
320
- addCheck(suite4, 'pass', 'Home Route Entry', 'Home page ("/") declared in manifest');
365
+ addCheck(suite4, 'pass', 'Home Route Entry', 'Home page ("/") declared in manifest', { code: 'DNB-MNF-003' }) //, 'Home page ("/") declared in manifest');
321
366
  } else {
322
- addCheck(suite4, 'err', 'Home Route Entry', 'Manifest pages array missing root route: "/"');
367
+ addCheck(suite4, 'err', 'Home Route Entry', 'Manifest pages array missing root route: "/"', { code: 'DNB-MNF-003' }) //, 'Manifest pages array missing root route: "/"');
323
368
  }
324
369
 
325
370
  // Route coherence check: verify declared manifest routes exist on filesystem
@@ -344,9 +389,9 @@ function runDoctor(targetDirInput = '.', options = {}) {
344
389
  }
345
390
 
346
391
  if (missingDiskRoutes.length === 0) {
347
- addCheck(suite4, 'pass', 'Route Coherence', `All ${pages.length} declared routes verified against filesystem`);
392
+ addCheck(suite4, 'pass', 'Route Coherence', `All ${pages.length} declared routes verified against filesystem`, { code: 'DNB-RTE-001' }) //, `All ${pages.length} declared routes verified against filesystem`);
348
393
  } else {
349
- addCheck(suite4, 'warn', 'Route Coherence', `Declared routes missing corresponding files on disk: ${missingDiskRoutes.join(', ')}`);
394
+ addCheck(suite4, 'warn', 'Route Coherence', `Declared routes missing corresponding files on disk: ${missingDiskRoutes.join(', ')}`, { code: 'DNB-RTE-001' }) //, `Declared routes missing corresponding files on disk: ${missingDiskRoutes.join(', ')}`);
350
395
  }
351
396
  } catch (e) {
352
397
  addCheck(suite4, 'err', 'fivora-template.json Syntax', e.message);
@@ -366,7 +411,7 @@ function runDoctor(targetDirInput = '.', options = {}) {
366
411
  if (fs.existsSync(siteDataPath)) {
367
412
  try {
368
413
  siteData = JSON.parse(fs.readFileSync(siteDataPath, 'utf-8'));
369
- addCheck(suite5, 'pass', 'site-data.json', 'src/data/site-data.json exists & valid');
414
+ addCheck(suite5, 'pass', 'site-data.json', 'src/data/site-data.json exists & valid', { code: 'DNB-SYN-001' }) //, 'src/data/site-data.json exists & valid');
370
415
  } catch (e) {
371
416
  addCheck(suite5, 'err', 'site-data.json Syntax', e.message);
372
417
  }
@@ -408,15 +453,30 @@ function runDoctor(targetDirInput = '.', options = {}) {
408
453
  }
409
454
  }
410
455
 
411
- // 2. Action URL vs Visible Text Collision
412
- // Detect <a ... data-preview-field-path="...Url" ...>Visible Text</a> without inner span
413
- const anchorCollisions = code.matchAll(/<a\s+[^>]*data-preview-field-path="[^"]*(?:Url|Link|Action)"[^>]*>([^<>{}\n]+)<\/a>/gi);
414
- for (const ac of anchorCollisions) {
415
- const innerText = ac[1].trim();
416
- if (innerText.length > 1) {
417
- actionTextCollisions++;
456
+ // 2. Action URL vs Visible Text Collision (DNB-ACT-004)
457
+ // Only flag if element carries an action URL and has child text WITHOUT a dedicated child label marker
458
+ const actionCollisions = code.matchAll(/<(a|button)(\s+[^>]*data-preview-field-path="[^"]*(?:Url|Link|Action)"[^>]*)>([\s\S]*?)<\/\1>/gi);
459
+ let fileNeedsActionFix = false;
460
+ let newCodeAction = code;
461
+ for (const ac of actionCollisions) {
462
+ const innerContent = ac[3].trim();
463
+ // If innerContent has its own data-preview-field-path, it is decoupled!
464
+ if (innerContent.length > 0 && !/\bdata-preview-field-path\s*=/.test(innerContent)) {
465
+ const textOnly = innerContent.replace(/<[^>]*>/g, '').trim();
466
+ if (textOnly.length > 1) {
467
+ actionTextCollisions++;
468
+ if (shouldFix) {
469
+ const originalOpening = `<${ac[1]}${ac[2]}>`;
470
+ const cleanOpening = originalOpening.replace(/\s*data-preview-field-path="[^"]*"/g, '');
471
+ newCodeAction = newCodeAction.replace(originalOpening, cleanOpening);
472
+ fileNeedsActionFix = true;
473
+ }
474
+ }
418
475
  }
419
476
  }
477
+ if (shouldFix && fileNeedsActionFix) {
478
+ fs.writeFileSync(file, newCodeAction, 'utf8');
479
+ }
420
480
 
421
481
  // 3. Static Ancestor Collision
422
482
  // Detect element with data-preview-static wrapping element with data-preview-field-path
@@ -475,16 +535,16 @@ function runDoctor(targetDirInput = '.', options = {}) {
475
535
  }
476
536
  }
477
537
 
478
- addCheck(suite5, 'pass', 'Field Path Scan', `${foundFieldPaths.size} visual editing field markers scanned across ${sourceFiles.length} files`);
538
+ addCheck(suite5, 'pass', 'Field Path Scan', `${foundFieldPaths.size} visual editing field markers scanned across ${sourceFiles.length} files`, { code: 'DNB-AST-001' }) //, `${foundFieldPaths.size} visual editing field markers scanned across ${sourceFiles.length} files`);
479
539
 
480
540
  if (orphanPaths.length === 0) {
481
- addCheck(suite5, 'pass', 'Content Synchronization', 'All source field paths exist in site-data.json');
541
+ addCheck(suite5, 'pass', 'Content Synchronization', 'All source field paths exist in site-data.json', { code: 'DNB-SYN-002' }) //, 'All source field paths exist in site-data.json');
482
542
  } else {
483
- addCheck(suite5, 'warn', 'Content Synchronization', `${orphanPaths.length} field paths not found in site-data.json (e.g. ${orphanPaths[0].fieldPath})`);
543
+ addCheck(suite5, 'warn', 'Content Synchronization', `${orphanPaths.length} field paths not found in site-data.json`, { code: 'DNB-SYN-002' }) //, `${orphanPaths.length} field paths not found in site-data.json (e.g. ${orphanPaths[0].fieldPath})`);
484
544
  }
485
545
 
486
546
  if (missingInSchema.length === 0) {
487
- addCheck(suite5, 'pass', 'Schema Synchronization', 'All source field paths declared in fivora-template.json editorSchema');
547
+ addCheck(suite5, 'pass', 'Schema Synchronization', 'All source field paths declared in fivora-template.json editorSchema', { code: 'DNB-SCH-001' }) //, 'All source field paths declared in fivora-template.json editorSchema');
488
548
  } else if (shouldFix && manifestData) {
489
549
  // Auto-fix missing schema entries
490
550
  let fixedSchemaFields = 0;
@@ -513,49 +573,114 @@ function runDoctor(targetDirInput = '.', options = {}) {
513
573
  }
514
574
 
515
575
  fs.writeFileSync(manifestPath, JSON.stringify(manifestData, null, 2) + '\n', 'utf8');
516
- addCheck(suite5, 'fixed', 'Schema Synchronization', `Added ${fixedSchemaFields} missing field definitions to editorSchema`);
576
+ addCheck(suite5, 'fixed', 'Schema Synchronization', `Added ${fixedSchemaFields} missing field definitions to editorSchema`, { code: 'DNB-SCH-001' }) //, `Added ${fixedSchemaFields} missing field definitions to editorSchema`);
517
577
  } else {
518
- addCheck(suite5, 'warn', 'Schema Synchronization', `${missingInSchema.length} field paths missing in fivora-template.json (Run with --fix to register automatically)`);
578
+ addCheck(suite5, 'warn', 'Schema Synchronization', `${missingInSchema.length} field paths missing in fivora-template.json (Run with --fix to register automatically)`, { code: 'DNB-SCH-001' }) //, `${missingInSchema.length} field paths missing in fivora-template.json (Run with --fix to register automatically)`);
519
579
  }
520
580
 
521
581
  if (actionTextCollisions === 0) {
522
- addCheck(suite5, 'pass', 'Action vs Text Contracts', 'Zero URL vs text label collisions detected on interactive links');
582
+ addCheck(suite5, 'pass', 'Action vs Text Contracts', 'Zero URL vs text label collisions detected on interactive links', { code: 'DNB-ACT-004' }) //, 'Zero URL vs text label collisions detected on interactive links');
583
+ } else if (shouldFix) {
584
+ addCheck(suite5, 'fixed', 'Action vs Text Contracts', `Stripped ${actionTextCollisions} action URL marker(s) from container elements to protect child labels`, { code: 'DNB-ACT-004' }) //, `Stripped ${actionTextCollisions} action URL marker(s) from container elements to protect child labels`);
523
585
  } else {
524
- addCheck(suite5, 'warn', 'Action vs Text Contracts', `${actionTextCollisions} potential action URL/label conflict(s) (Split URL marker on <a> and text on <span>)`);
586
+ addCheck(suite5, 'warn', 'Action vs Text Contracts', `${actionTextCollisions} potential action URL/label conflict(s) (Split URL marker on <a>/<button> and text on <span>)`, { code: 'DNB-ACT-004' }) //, `${actionTextCollisions} potential action URL/label conflict(s) (Split URL marker on <a>/<button> and text on <span>)`);
525
587
  }
526
588
 
527
589
  if (staticAncestorCollisions === 0) {
528
- addCheck(suite5, 'pass', 'Ancestor Delegation', 'Zero static ancestor collisions (clickable visual focus intact)');
590
+ addCheck(suite5, 'pass', 'Ancestor Delegation', 'Zero static ancestor collisions (clickable visual focus intact)', { code: 'DNB-ANC-001' }) //, 'Zero static ancestor collisions (clickable visual focus intact)');
529
591
  } else if (shouldFix) {
530
- addCheck(suite5, 'fixed', 'Ancestor Delegation', `Stripped ${staticAncestorCollisions} static ancestor attribute(s) that shadowed editable children`);
592
+ addCheck(suite5, 'fixed', 'Ancestor Delegation', `Stripped ${staticAncestorCollisions} static ancestor attribute(s) that shadowed editable children`, { code: 'DNB-ANC-001' }) //, `Stripped ${staticAncestorCollisions} static ancestor attribute(s) that shadowed editable children`);
531
593
  } else {
532
- addCheck(suite5, 'err', 'Ancestor Delegation', `${staticAncestorCollisions} static ancestor wrapper(s) covering editable children (Run with --fix to strip automatically)`);
594
+ addCheck(suite5, 'err', 'Ancestor Delegation', `${staticAncestorCollisions} static ancestor wrapper(s) covering editable children (Run with --fix to strip automatically)`, { code: 'DNB-ANC-001' }) //, `${staticAncestorCollisions} static ancestor wrapper(s) covering editable children (Run with --fix to strip automatically)`);
533
595
  }
534
596
 
535
597
  if (broadStaticContainers.length === 0) {
536
- addCheck(suite5, 'pass', 'Granular Static Markup', 'Zero broad layout containers (div/nav/section) marked static');
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');
537
599
  } else if (shouldFix) {
538
- addCheck(suite5, 'fixed', 'Granular Static Markup', `Stripped data-preview-static from ${broadStaticContainers.length} broad container(s)`);
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)`);
539
601
  } else {
540
- addCheck(suite5, 'warn', 'Granular Static Markup', `${broadStaticContainers.length} broad container(s) marked with data-preview-static (Fivora requires marking only smallest leaf elements)`);
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)`);
541
603
  }
542
604
 
543
605
  if (dynamicVariableMarkers.length === 0) {
544
- addCheck(suite5, 'pass', 'Literal Marker Standard', 'All data-preview-field-path annotations use literal strings or JSX templates');
606
+ addCheck(suite5, 'pass', 'Literal Marker Standard', 'All data-preview-field-path annotations use literal strings or JSX templates', { code: 'DNB-AST-002' }) //, 'All data-preview-field-path annotations use literal strings or JSX templates');
545
607
  } else {
546
- addCheck(suite5, 'warn', 'Literal Marker Standard', `${dynamicVariableMarkers.length} dynamic variable marker(s) detected (e.g. ${dynamicVariableMarkers[0].expr})`);
608
+ 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})`);
547
609
  }
548
610
 
549
611
  // =========================================================================
550
612
  // SUITE 6: Multi-Niche Storefront Architecture & Security Preflight
551
613
  // =========================================================================
614
+
615
+ // Check: Hidden contract markers & regex token collisions (DNB-HID-005)
616
+ let hiddenContractCount = 0;
617
+ let overflowHiddenCount = 0;
618
+ for (const file of sourceFiles) {
619
+ let c = fs.readFileSync(file, 'utf-8');
620
+ let modified = false;
621
+ const hiddenMatches = c.matchAll(/<([a-zA-Z0-9_-]+)\s+[^>]*(?:hidden|display:\s*['"]none['"])[^>]*data-preview-(?:field-path|list-path|item-path)[^>]*>/gi);
622
+ for (const hm of hiddenMatches) hiddenContractCount++;
623
+
624
+ if (c.includes('overflow-hidden') && (c.includes('data-preview-style-type="card"') || c.includes('GlassCard') || c.includes('data-preview-item-path'))) {
625
+ overflowHiddenCount++;
626
+ if (shouldFix) {
627
+ c = c.replace(/\boverflow-hidden\b/g, 'overflow-clip');
628
+ modified = true;
629
+ }
630
+ }
631
+ if (/\bhidden\s+(?:md|sm|lg|xl):/.test(c) && c.includes('data-preview-')) {
632
+ if (shouldFix) {
633
+ c = c.replace(/\bhidden(\s+(?:md|sm|lg|xl):)/g, '[display:none]$1');
634
+ modified = true;
635
+ }
636
+ }
637
+ if (shouldFix && modified) fs.writeFileSync(file, c, 'utf8');
638
+ }
639
+ if (hiddenContractCount === 0 && overflowHiddenCount === 0) {
640
+ addCheck(suite5, 'pass', 'Contract Visibility & Regex Safety', 'Zero hidden preview contract markers or token collisions', { code: 'DNB-HID-005' });
641
+ } else if (shouldFix) {
642
+ addCheck(suite5, 'fixed', 'Contract Visibility & Regex Safety', `Sanitized ${overflowHiddenCount} class token(s) to overflow-clip / [display:none]`, { code: 'DNB-HID-005' });
643
+ } else {
644
+ addCheck(suite5, 'warn', 'Contract Visibility & Regex Safety', `${hiddenContractCount} hidden marker(s) or ${overflowHiddenCount} class collision(s) detected (Run with --fix to sanitize)`, { code: 'DNB-HID-005' });
645
+ }
646
+
647
+ // Check: Empty-State Array & Out-of-Range Guards (DNB-ARR-003)
648
+ let unguardedArrayCount = 0;
649
+ for (const file of sourceFiles) {
650
+ const c = fs.readFileSync(file, 'utf-8');
651
+ if (c.includes('data-preview-list-path')) {
652
+ if (/\?\s*[A-Z0-9_]+\s*:\s*[A-Z0-9_]+/i.test(c) && !c.includes('Array.isArray')) {
653
+ unguardedArrayCount++;
654
+ }
655
+ }
656
+ }
657
+ if (unguardedArrayCount === 0) {
658
+ addCheck(suite5, 'pass', 'Empty-State Array Guards', 'All list collections guarded against empty state ([]) out-of-range elements', { code: 'DNB-ARR-003' });
659
+ } 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' });
661
+ }
662
+
663
+ // Check: Empty-State Singleton Persistence Guard (DNB-EMP-002)
664
+ let unmountedSingletonCount = 0;
665
+ for (const file of sourceFiles) {
666
+ const c = fs.readFileSync(file, 'utf-8');
667
+ if (/\{[^{}]{0,100}length\s*>\s*0\s*&&[^{}]{0,300}data-preview-field-path="home\.[a-zA-Z0-9]+Label"/.test(c)) {
668
+ if (!c.includes('length === 0')) unmountedSingletonCount++;
669
+ }
670
+ }
671
+ if (unmountedSingletonCount === 0) {
672
+ addCheck(suite5, 'pass', 'Empty-State Singleton Persistence', 'Section singleton labels remain mounted during empty-state fixture tests', { code: 'DNB-EMP-002' });
673
+ } else {
674
+ addCheck(suite5, 'warn', 'Empty-State Singleton Persistence', `${unmountedSingletonCount} singleton label(s) unmount when list is empty. Provide an empty fallback container.`, { code: 'DNB-EMP-002' });
675
+ }
676
+
552
677
  if (!isJson) console.log('\n\x1b[1m[6/6] Multi-Niche Architecture & Asset Security:\x1b[0m');
553
678
  const suite6 = 'Niche Architecture & Security';
554
679
 
555
680
  // Niche match analysis
556
681
  const matchedRecipe = matchRecipeForProject(targetDir, pkg || {}, sourceFiles);
557
682
  if (matchedRecipe) {
558
- addCheck(suite6, 'pass', 'Storefront Niche Match', `${matchedRecipe.label} (${matchedRecipe.name})`);
683
+ addCheck(suite6, 'pass', 'Storefront Niche Match', `${matchedRecipe.label} (${matchedRecipe.name})`, { code: 'DNB-NIC-001' }) //, `${matchedRecipe.label} (${matchedRecipe.name})`);
559
684
 
560
685
  // Audit niche-specific essential features
561
686
  const allFileNames = sourceFiles.map((f) => path.basename(f).toLowerCase()).join(' ');
@@ -582,9 +707,9 @@ function runDoctor(targetDirInput = '.', options = {}) {
582
707
  const envFiles = ['.env', '.env.local', '.env.production', '.env.development'];
583
708
  const foundEnv = envFiles.filter((f) => fs.existsSync(path.join(targetDir, f)));
584
709
  if (foundEnv.length === 0) {
585
- addCheck(suite6, 'pass', 'Secrets Isolation', 'No raw .env files in root directory');
710
+ addCheck(suite6, 'pass', 'Secrets Isolation', 'No raw .env files in root directory', { code: 'DNB-SEC-001' }) //, 'No raw .env files in root directory');
586
711
  } else {
587
- addCheck(suite6, 'warn', 'Secrets Isolation', `Active env files: ${foundEnv.join(', ')} (Will be excluded from upload ZIP)`);
712
+ addCheck(suite6, 'warn', 'Secrets Isolation', `Active env files: ${foundEnv.join(', ')}`, { code: 'DNB-SEC-001' }) //, `Active env files: ${foundEnv.join(', ')} (Will be excluded from upload ZIP)`);
588
713
  }
589
714
 
590
715
  // Storefront preview image
@@ -593,9 +718,9 @@ function runDoctor(targetDirInput = '.', options = {}) {
593
718
  fs.existsSync(path.join(targetDir, 'public', 'preview.png'));
594
719
 
595
720
  if (previewExists) {
596
- addCheck(suite6, 'pass', 'Storefront Preview Graphic', 'preview.png / thumbnail.png verified for Fivora gallery');
721
+ 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');
597
722
  } else {
598
- addCheck(suite6, 'warn', 'Storefront Preview Graphic', 'preview.png not found in root or public folder');
723
+ 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');
599
724
  }
600
725
 
601
726
  // Large assets audit (> 4MB)
@@ -609,9 +734,9 @@ function runDoctor(targetDirInput = '.', options = {}) {
609
734
  }
610
735
 
611
736
  if (largeAssets.length === 0) {
612
- addCheck(suite6, 'pass', 'Asset Optimization Preflight', `All ${assets.length} public asset(s) within optimal static export bounds (< 4MB)`);
737
+ addCheck(suite6, 'pass', 'Asset Optimization Preflight', `All ${assets.length} public asset(s) within optimal static export bounds (< 4MB)`, { code: 'DNB-AST-003' }) //, `All ${assets.length} public asset(s) within optimal static export bounds (< 4MB)`);
613
738
  } else {
614
- addCheck(suite6, 'warn', 'Asset Optimization Preflight', `${largeAssets.length} large asset(s) detected (> 4MB): ${largeAssets.map((a) => `${a.file} (${a.sizeMB}MB)`).join(', ')}`);
739
+ addCheck(suite6, 'warn', 'Asset Optimization Preflight', `${largeAssets.length} large asset(s) detected (> 4MB)`, { code: 'DNB-AST-003' }) //, `${largeAssets.length} large asset(s) detected (> 4MB): ${largeAssets.map((a) => `${a.file} (${a.sizeMB}MB)`).join(', ')}`);
615
740
  }
616
741
 
617
742
  // =========================================================================
@@ -626,6 +751,28 @@ function runDoctor(targetDirInput = '.', options = {}) {
626
751
  if (isJson) {
627
752
  console.log(JSON.stringify(reportData, null, 2));
628
753
  } else {
754
+ if (reportData.errors > 0 || reportData.warnings > 0 || reportData.fixedCount > 0) {
755
+ console.log('\n \x1b[1m\x1b[36mSTANDARDIZED DEVELOPER ACTION MATRIX:\x1b[0m');
756
+ console.log(' \x1b[90m┌──────────────┬──────────┬─────────────────────────────────────┬──────────────────────────────────────────────────────┐\x1b[0m');
757
+ console.log(' \x1b[90m│\x1b[0m \x1b[1mCode\x1b[0m \x1b[90m│\x1b[0m \x1b[1mSeverity\x1b[0m \x1b[90m│\x1b[0m \x1b[1mDiagnostic Title\x1b[0m \x1b[90m│\x1b[0m \x1b[1mActionable Remediation / Standard\x1b[0m \x1b[90m│\x1b[0m');
758
+ console.log(' \x1b[90m├──────────────┼──────────┼─────────────────────────────────────┼──────────────────────────────────────────────────────┤\x1b[0m');
759
+
760
+ const actionChecks = reportData.suites
761
+ .flatMap(s => s.checks)
762
+ .filter(c => c.type === 'err' || c.type === 'warn' || c.type === 'fixed');
763
+
764
+ for (const item of actionChecks) {
765
+ const sevColor = item.type === 'err' ? '\x1b[31mBLOCKING\x1b[0m' : item.type === 'warn' ? '\x1b[33mADVISORY\x1b[0m' : '\x1b[32mREPAIRED\x1b[0m';
766
+ const rawSev = item.type === 'err' ? 'BLOCKING' : item.type === 'warn' ? 'ADVISORY' : 'REPAIRED';
767
+ const codePad = item.code.padEnd(12);
768
+ const titlePad = item.title.slice(0, 35).padEnd(35);
769
+ const actionText = (item.action || item.detail || 'Follow Fivora v2 Visual Editing Standard').slice(0, 52).padEnd(52);
770
+ console.log(` \x1b[90m│\x1b[0m \x1b[36m${codePad}\x1b[0m \x1b[90m│\x1b[0m ${sevColor}${' '.repeat(8 - rawSev.length)} \x1b[90m│\x1b[0m ${titlePad} \x1b[90m│\x1b[0m \x1b[90m${actionText}\x1b[0m \x1b[90m│\x1b[0m`);
771
+ }
772
+ console.log(' \x1b[90m└──────────────┴──────────┴─────────────────────────────────────┴──────────────────────────────────────────────────────┘\x1b[0m');
773
+ console.log(' \x1b[90mRun \x1b[32mdeneb doctor --fix\x1b[90m to automatically remediate auto-repairable items.\x1b[0m\n');
774
+ }
775
+
629
776
  console.log('\n' + createBox([
630
777
  '\x1b[1mDOCTOR DIAGNOSTIC SUMMARY\x1b[0m',
631
778
  `\x1b[32m✔ Passed:\x1b[0m ${reportData.passed}`,