@deneb-ui/cli 2.0.51 → 2.0.53

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.
@@ -159,18 +159,23 @@ function applyTransformToElement(pathNode, transform) {
159
159
  b.jsxAttribute(b.jsxIdentifier('type'), b.stringLiteral('submit'))
160
160
  );
161
161
  }
162
- if (!hasJsxAttribute(node, 'data-preview-static')) {
162
+ if (transform.labelField) {
163
+ if (hasJsxAttribute(node, 'data-preview-static')) {
164
+ node.openingElement.attributes = node.openingElement.attributes.filter(
165
+ (attr) => !(attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'data-preview-static')
166
+ );
167
+ }
168
+ if (hasJsxAttribute(node, 'data-preview-field-path')) {
169
+ node.openingElement.attributes = node.openingElement.attributes.filter(
170
+ (attr) => !(attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'data-preview-field-path')
171
+ );
172
+ }
173
+ splitActionChildren(node, transform.labelField, transform.labelFallback || '');
174
+ } else if (!hasJsxAttribute(node, 'data-preview-static')) {
163
175
  node.openingElement.attributes.push(
164
176
  b.jsxAttribute(b.jsxIdentifier('data-preview-static'), b.stringLiteral('action-button'))
165
177
  );
166
178
  }
167
- if (hasJsxAttribute(node, 'data-preview-field-path')) {
168
- node.openingElement.attributes = node.openingElement.attributes.filter(
169
- (attr) => !(attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'data-preview-field-path')
170
- );
171
- }
172
- splitActionChildren(node, transform.labelField, transform.labelFallback || '');
173
- wrapHiddenUrlSibling(pathNode, transform.urlField, transform.fallback);
174
179
  return;
175
180
  }
176
181
  if (transform.operation === 'split-action-contract') {
@@ -206,18 +211,17 @@ function applyTransformToElement(pathNode, transform) {
206
211
  }
207
212
  }
208
213
 
209
- if (!hasJsxAttribute(node, 'data-preview-static')) {
210
- node.openingElement.attributes.push(
211
- b.jsxAttribute(b.jsxIdentifier('data-preview-static'), b.stringLiteral('action-link'))
212
- );
213
- }
214
- if (hasJsxAttribute(node, 'data-preview-field-path')) {
214
+ // Fivora Strict Action/Label Contract:
215
+ // Bind the URL action marker directly to the outer <a> element (never hidden)
216
+ replaceAttrValue(node, 'data-preview-field-path', b.stringLiteral(transform.urlField));
217
+ // Remove data-preview-static if present to prevent static/field collisions
218
+ if (hasJsxAttribute(node, 'data-preview-static')) {
215
219
  node.openingElement.attributes = node.openingElement.attributes.filter(
216
- (attr) => !(attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'data-preview-field-path')
220
+ (attr) => !(attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'data-preview-static')
217
221
  );
218
222
  }
223
+ // Bind the label to the inner text child
219
224
  splitActionChildren(node, transform.labelField, transform.labelFallback || '');
220
- wrapHiddenUrlSibling(pathNode, transform.urlField, transform.fallback);
221
225
  return;
222
226
  }
223
227
  if (transform.operation === 'extract-url') {
@@ -255,11 +259,29 @@ function applyTransformToElement(pathNode, transform) {
255
259
  return;
256
260
  }
257
261
  if (transform.operation === 'style-bind') {
258
- if (transform.styleKind === 'grid' || transform.styleKind === 'card') return;
262
+ if (transform.styleKind === 'grid') {
263
+ const container = findAncestorJsxElement(pathNode) || node;
264
+ ensureStyleAttrs(container, transform.stylePath, 'grid');
265
+ return;
266
+ }
267
+ if (transform.styleKind === 'card') {
268
+ ensureStyleAttrs(node, transform.stylePath, 'card');
269
+ return;
270
+ }
259
271
  ensureStyleAttrs(node, transform.stylePath, transform.styleKind || 'text');
260
272
  }
261
273
  }
262
274
 
275
+ function findAncestorJsxElement(pathNode) {
276
+ let current = pathNode.parent;
277
+ while (current) {
278
+ const candidate = current.node || current.value;
279
+ if (candidate?.type === 'JSXElement') return candidate;
280
+ current = current.parentPath || current.parent;
281
+ }
282
+ return null;
283
+ }
284
+
263
285
  function inferButtonKind(tag) {
264
286
  if (tag === 'button' || tag === 'Button') return 'button';
265
287
  return 'text';
@@ -335,6 +357,7 @@ function applyCollectionTransform(ast, transform, isClient = false) {
335
357
  }
336
358
 
337
359
  markItemFields(callback, { listPath, binding, indexName });
360
+ markComponentRefItemsStatic(callback, binding);
338
361
 
339
362
  const container = findListContainer(mapCall);
340
363
  if (container && !hasJsxAttribute(container, 'data-preview-list-path')) {
@@ -351,7 +374,7 @@ function applyCollectionTransform(ast, transform, isClient = false) {
351
374
  );
352
375
  }
353
376
 
354
- return bindArrayDeclaration(ast, mapCall, listPath, isClient);
377
+ return bindArrayDeclaration(ast, mapCall, listPath, isClient, Boolean(transform.hasComponentRef));
355
378
  }
356
379
 
357
380
  function findMapCall(ast, loc) {
@@ -410,9 +433,14 @@ function markItemFields(callback, { listPath, binding, indexName }) {
410
433
  );
411
434
  if (textChild && !hasJsxAttribute(node, 'data-preview-field-path')) {
412
435
  const property = itemMemberName(textChild.expression, binding);
413
- node.openingElement.attributes.push(
414
- jsxTemplatePathAttr('data-preview-field-path', listPath, indexName, `.${property}`)
415
- );
436
+ const tagName = getJsxName(node);
437
+ if (BROAD_CONTENT_CONTAINERS.has(tagName) || BROAD_CONTENT_CONTAINERS.has(String(tagName).toLowerCase())) {
438
+ wrapItemFieldInSpan(node, textChild, listPath, indexName, property);
439
+ } else {
440
+ node.openingElement.attributes.push(
441
+ jsxTemplatePathAttr('data-preview-field-path', listPath, indexName, `.${property}`)
442
+ );
443
+ }
416
444
  }
417
445
 
418
446
  this.traverse(pathNode);
@@ -420,6 +448,51 @@ function markItemFields(callback, { listPath, binding, indexName }) {
420
448
  });
421
449
  }
422
450
 
451
+ function wrapItemFieldInSpan(node, textChild, listPath, indexName, property) {
452
+ const nextChildren = [];
453
+ for (const child of node.children || []) {
454
+ if (child === textChild) {
455
+ nextChildren.push(
456
+ b.jsxElement(
457
+ b.jsxOpeningElement(
458
+ b.jsxIdentifier('span'),
459
+ [jsxTemplatePathAttr('data-preview-field-path', listPath, indexName, `.${property}`)],
460
+ false
461
+ ),
462
+ b.jsxClosingElement(b.jsxIdentifier('span')),
463
+ [child],
464
+ false
465
+ )
466
+ );
467
+ } else {
468
+ nextChildren.push(child);
469
+ }
470
+ }
471
+ node.children = nextChildren;
472
+ }
473
+
474
+ function markComponentRefItemsStatic(callback, binding) {
475
+ recast.types.visit(callback, {
476
+ visitJSXElement(pathNode) {
477
+ const name = pathNode.node.openingElement && pathNode.node.openingElement.name;
478
+ if (
479
+ name &&
480
+ name.type === 'JSXMemberExpression' &&
481
+ name.object &&
482
+ (name.object.type === 'Identifier' || name.object.type === 'JSXIdentifier') &&
483
+ name.object.name === binding
484
+ ) {
485
+ if (!hasJsxAttribute(pathNode.node, 'data-preview-static') && !hasJsxAttribute(pathNode.node, 'data-preview-field-path')) {
486
+ pathNode.node.openingElement.attributes.push(
487
+ b.jsxAttribute(b.jsxIdentifier('data-preview-static'), b.stringLiteral('component-ref'))
488
+ );
489
+ }
490
+ }
491
+ this.traverse(pathNode);
492
+ },
493
+ });
494
+ }
495
+
423
496
  function itemMemberName(expr, binding) {
424
497
  if (!expr || expr.type !== 'MemberExpression' || expr.computed) return null;
425
498
  if (expr.object?.type !== 'Identifier' || expr.object.name !== binding) return null;
@@ -470,7 +543,7 @@ function findEnclosingFunction(pathNode) {
470
543
  return null;
471
544
  }
472
545
 
473
- function bindArrayDeclaration(ast, mapCallPath, listPath, isClient = false) {
546
+ function bindArrayDeclaration(ast, mapCallPath, listPath, isClient = false, mergeDefaultRefs = false) {
474
547
  const arrayName = mapCallPath.node.callee.object?.name;
475
548
  if (!arrayName) return false;
476
549
  let bound = false;
@@ -498,7 +571,9 @@ function bindArrayDeclaration(ast, mapCallPath, listPath, isClient = false) {
498
571
  const localDecl = b.variableDeclaration('const', [
499
572
  b.variableDeclarator(
500
573
  b.identifier(arrayName),
501
- siteDataListBinding(listPath.split('.'), b.identifier(defaultName))
574
+ mergeDefaultRefs
575
+ ? mergeDefaultItemRefsBinding(listPath.split('.'), defaultName)
576
+ : siteDataListBinding(listPath.split('.'), b.identifier(defaultName))
502
577
  ),
503
578
  ]);
504
579
  const hookIdx = body.findIndex((stmt) => recast.print(stmt).code.includes('useSiteData'));
@@ -522,6 +597,28 @@ function bindArrayDeclaration(ast, mapCallPath, listPath, isClient = false) {
522
597
  return bound;
523
598
  }
524
599
 
600
+ function mergeDefaultItemRefsBinding(pathParts, defaultName) {
601
+ const listExpr = siteDataListBinding(pathParts, b.identifier(defaultName));
602
+ return b.callExpression(
603
+ b.memberExpression(listExpr, b.identifier('map'), false),
604
+ [
605
+ b.arrowFunctionExpression(
606
+ [b.identifier('item'), b.identifier('index')],
607
+ b.objectExpression([
608
+ b.spreadElement(
609
+ b.logicalExpression(
610
+ '??',
611
+ b.memberExpression(b.identifier(defaultName), b.identifier('index'), true),
612
+ b.objectExpression([])
613
+ )
614
+ ),
615
+ b.spreadElement(b.identifier('item')),
616
+ ])
617
+ ),
618
+ ]
619
+ );
620
+ }
621
+
525
622
  function fileAlreadyUsesSiteDataHook(ast) {
526
623
  let found = false;
527
624
  recast.types.visit(ast, {
@@ -710,6 +807,9 @@ function applyFilePlan(filePlan, profile) {
710
807
 
711
808
  // Sanitize any conflicting data-preview-static on elements with editable markers
712
809
  sanitizeContradictoryMarkers(ast);
810
+ healBroadContainerMarkers(ast);
811
+ healEmptyStateConditionals(ast);
812
+ healHiddenPreviewMarkers(ast);
713
813
 
714
814
  // Page keys are stamped in a separate route-driven pass so App Router and
715
815
  // Pages Router projects are handled by the same logic.
@@ -994,8 +1094,7 @@ function sanitizeContradictoryMarkers(ast) {
994
1094
  }
995
1095
 
996
1096
  function sanitizeContradictoryMarkersInSource(code, relativeFile) {
997
- if (!code.includes('data-preview-static')) return { code, updated: false };
998
- if (!code.includes('data-preview-field-path') && !code.includes('data-preview-list-path') && !code.includes('data-preview-item-path')) {
1097
+ if (!code.includes('data-preview-static') && !code.includes('data-preview-field-path')) {
999
1098
  return { code, updated: false };
1000
1099
  }
1001
1100
  let ast;
@@ -1004,9 +1103,200 @@ function sanitizeContradictoryMarkersInSource(code, relativeFile) {
1004
1103
  } catch {
1005
1104
  return { code, updated: false };
1006
1105
  }
1007
- const count = sanitizeContradictoryMarkers(ast);
1008
- if (count === 0) return { code, updated: false };
1009
- return { code: printSource(ast, code), updated: true, count };
1106
+ const cleaned = sanitizeContradictoryMarkers(ast);
1107
+ const healedBroad = healBroadContainerMarkers(ast);
1108
+ const healedEmpty = healEmptyStateConditionals(ast);
1109
+ const healedHidden = healHiddenPreviewMarkers(ast);
1110
+ if (cleaned === 0 && healedBroad === 0 && healedEmpty === 0 && healedHidden === 0) return { code, updated: false };
1111
+ return { code: printSource(ast, code), updated: true, count: cleaned + healedBroad + healedEmpty + healedHidden };
1112
+ }
1113
+
1114
+ function healBroadContainerMarkers(ast) {
1115
+ let healed = 0;
1116
+ recast.types.visit(ast, {
1117
+ visitJSXElement(pathNode) {
1118
+ const node = pathNode.node;
1119
+ const tag = getJsxName(node);
1120
+ const lower = String(tag || '').toLowerCase();
1121
+ if (!BROAD_CONTENT_CONTAINERS.has(tag) && !BROAD_CONTENT_CONTAINERS.has(lower)) {
1122
+ this.traverse(pathNode);
1123
+ return;
1124
+ }
1125
+ const fieldAttr = findJsxAttribute(node, 'data-preview-field-path');
1126
+ if (!fieldAttr) {
1127
+ this.traverse(pathNode);
1128
+ return;
1129
+ }
1130
+
1131
+ const fieldValue = fieldAttr.value;
1132
+ node.openingElement.attributes = (node.openingElement.attributes || []).filter(
1133
+ (attr) =>
1134
+ !(
1135
+ attr.type === 'JSXAttribute' &&
1136
+ attr.name &&
1137
+ (attr.name.name === 'data-preview-field-path' ||
1138
+ attr.name.name === 'data-preview-style-target' ||
1139
+ attr.name.name === 'data-preview-style-type')
1140
+ )
1141
+ );
1142
+
1143
+ const leaf = findFirstLeafChild(node);
1144
+ if (leaf && !hasJsxAttribute(leaf, 'data-preview-field-path')) {
1145
+ leaf.openingElement.attributes.push(
1146
+ b.jsxAttribute(b.jsxIdentifier('data-preview-field-path'), fieldValue)
1147
+ );
1148
+ healed++;
1149
+ } else {
1150
+ const span = b.jsxElement(
1151
+ b.jsxOpeningElement(
1152
+ b.jsxIdentifier('span'),
1153
+ [b.jsxAttribute(b.jsxIdentifier('data-preview-field-path'), fieldValue)],
1154
+ false
1155
+ ),
1156
+ b.jsxClosingElement(b.jsxIdentifier('span')),
1157
+ node.children || [],
1158
+ false
1159
+ );
1160
+ node.children = [span];
1161
+ healed++;
1162
+ }
1163
+ this.traverse(pathNode);
1164
+ },
1165
+ });
1166
+ return healed;
1167
+ }
1168
+
1169
+ function findFirstLeafChild(node) {
1170
+ const leafTags = new Set(['span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'label', 'img', 'button', 'a', 'strong', 'em', 'small']);
1171
+ for (const child of node.children || []) {
1172
+ if (child && child.type === 'JSXElement') {
1173
+ const name = getJsxName(child);
1174
+ if (leafTags.has(name) || leafTags.has(String(name).toLowerCase())) return child;
1175
+ const nested = findFirstLeafChild(child);
1176
+ if (nested) return nested;
1177
+ }
1178
+ }
1179
+ return null;
1180
+ }
1181
+
1182
+ function healEmptyStateConditionals(ast) {
1183
+ let healed = 0;
1184
+
1185
+ function jsxHasPreviewMarker(node) {
1186
+ if (!node) return false;
1187
+ if (node.type === 'JSXElement') {
1188
+ if (
1189
+ hasJsxAttribute(node, 'data-preview-field-path') ||
1190
+ hasJsxAttribute(node, 'data-preview-list-path') ||
1191
+ hasJsxAttribute(node, 'data-preview-item-path')
1192
+ ) {
1193
+ return true;
1194
+ }
1195
+ return (node.children || []).some((child) => jsxHasPreviewMarker(child));
1196
+ }
1197
+ if (node.type === 'JSXFragment') {
1198
+ return (node.children || []).some((child) => jsxHasPreviewMarker(child));
1199
+ }
1200
+ if (node.type === 'ParenthesizedExpression') return jsxHasPreviewMarker(node.expression);
1201
+ return false;
1202
+ }
1203
+
1204
+ recast.types.visit(ast, {
1205
+ visitLogicalExpression(pathNode) {
1206
+ const expr = pathNode.node;
1207
+ if (expr.operator === '&&' && jsxHasPreviewMarker(expr.right)) {
1208
+ pathNode.replace(expr.right);
1209
+ healed++;
1210
+ return false;
1211
+ }
1212
+ this.traverse(pathNode);
1213
+ },
1214
+ visitJSXExpressionContainer(pathNode) {
1215
+ const expr = pathNode.node.expression;
1216
+ if (!expr) {
1217
+ this.traverse(pathNode);
1218
+ return;
1219
+ }
1220
+ if (expr.type === 'LogicalExpression' && expr.operator === '&&' && jsxHasPreviewMarker(expr.right)) {
1221
+ pathNode.node.expression = expr.right;
1222
+ healed++;
1223
+ return false;
1224
+ }
1225
+ if (
1226
+ expr.type === 'ConditionalExpression' &&
1227
+ jsxHasPreviewMarker(expr.consequent) &&
1228
+ (isNullish(expr.alternate) || isNullish(expr.consequent))
1229
+ ) {
1230
+ pathNode.node.expression = jsxHasPreviewMarker(expr.consequent) && !isNullish(expr.consequent)
1231
+ ? expr.consequent
1232
+ : expr.alternate;
1233
+ healed++;
1234
+ return false;
1235
+ }
1236
+ this.traverse(pathNode);
1237
+ },
1238
+ });
1239
+ return healed;
1240
+ }
1241
+
1242
+ function isNullish(node) {
1243
+ if (!node) return true;
1244
+ if (node.type === 'NullLiteral') return true;
1245
+ if (node.type === 'Literal' && (node.value === null || node.value === false || node.value === undefined)) return true;
1246
+ if (node.type === 'BooleanLiteral' && node.value === false) return true;
1247
+ if (node.type === 'Identifier' && (node.name === 'undefined' || node.name === 'null')) return true;
1248
+ return false;
1249
+ }
1250
+
1251
+ function healHiddenPreviewMarkers(ast) {
1252
+ let healed = 0;
1253
+ recast.types.visit(ast, {
1254
+ visitJSXOpeningElement(pathNode) {
1255
+ const attrs = pathNode.node.attributes || [];
1256
+ const hasEditable = attrs.some(
1257
+ (attr) =>
1258
+ attr.type === 'JSXAttribute' &&
1259
+ attr.name &&
1260
+ (attr.name.name === 'data-preview-field-path' ||
1261
+ attr.name.name === 'data-preview-list-path' ||
1262
+ attr.name.name === 'data-preview-item-path')
1263
+ );
1264
+ if (!hasEditable) {
1265
+ this.traverse(pathNode);
1266
+ return;
1267
+ }
1268
+ const classAttr = attrs.find((attr) => attr.type === 'JSXAttribute' && attr.name && (attr.name.name === 'className' || attr.name.name === 'class'));
1269
+ let classText = '';
1270
+ if (classAttr && classAttr.value) {
1271
+ if (classAttr.value.type === 'StringLiteral' || classAttr.value.type === 'Literal') classText = String(classAttr.value.value || '');
1272
+ else if (classAttr.value.type === 'JSXExpressionContainer' && classAttr.value.expression) {
1273
+ const expr = classAttr.value.expression;
1274
+ if (expr.type === 'StringLiteral' || expr.type === 'Literal') classText = String(expr.value || '');
1275
+ if (expr.type === 'TemplateLiteral') classText = (expr.quasis || []).map((q) => q.value.cooked || q.value.raw || '').join(' ');
1276
+ }
1277
+ }
1278
+ const hiddenAttr = attrs.some(
1279
+ (attr) => attr.type === 'JSXAttribute' && attr.name && (attr.name.name === 'hidden' || (attr.name.name === 'aria-hidden' && recast.print(attr).code.includes('true')))
1280
+ );
1281
+ if (hiddenAttr || /(^|\s)hidden(\s|$)/.test(classText)) {
1282
+ pathNode.node.attributes = attrs.filter(
1283
+ (attr) =>
1284
+ !(
1285
+ attr.type === 'JSXAttribute' &&
1286
+ attr.name &&
1287
+ (attr.name.name === 'data-preview-field-path' ||
1288
+ attr.name.name === 'data-preview-list-path' ||
1289
+ attr.name.name === 'data-preview-item-path' ||
1290
+ attr.name.name === 'data-preview-style-target' ||
1291
+ attr.name.name === 'data-preview-style-type')
1292
+ )
1293
+ );
1294
+ healed++;
1295
+ }
1296
+ this.traverse(pathNode);
1297
+ },
1298
+ });
1299
+ return healed;
1010
1300
  }
1011
1301
 
1012
1302
  module.exports = {
@@ -1020,4 +1310,7 @@ module.exports = {
1020
1310
  inferPageKey,
1021
1311
  sanitizeContradictoryMarkers,
1022
1312
  sanitizeContradictoryMarkersInSource,
1313
+ healBroadContainerMarkers,
1314
+ healEmptyStateConditionals,
1315
+ healHiddenPreviewMarkers,
1023
1316
  };
@@ -6,6 +6,7 @@ const { parseSource } = require('./ast.cjs');
6
6
  const { collectDesignSnapshot } = require('./semantic.cjs');
7
7
  const { getDeep } = require('./manifest.cjs');
8
8
  const { walkFiles, isJsxFile, rel } = require('./fs-utils.cjs');
9
+ const { enumerateContentPaths, isControlOnly, wildcardPath } = require('./fivora-contract.cjs');
9
10
 
10
11
  function validateAstFiles(files) {
11
12
  const results = [];
@@ -143,18 +144,57 @@ function countOverlap(a, b) {
143
144
  return n;
144
145
  }
145
146
 
146
- function coverageMetrics({ analyses, plan, appliedCount, skippedDynamic, alreadyEditable }) {
147
+ function coverageMetrics({
148
+ analyses,
149
+ plan,
150
+ appliedCount,
151
+ skippedDynamic,
152
+ alreadyEditable,
153
+ content,
154
+ controlOnlyPaths,
155
+ pathCoverage,
156
+ uncoveredVisibleText,
157
+ }) {
147
158
  const detected = analyses.reduce((n, a) => n + (a.candidates || []).filter((c) => c.kind !== 'decoration' && c.kind !== 'already-editable').length, 0);
148
159
  const skippedLow = plan.skipped.filter((s) => s.reason === 'low-confidence' || (s.confidence || 0) < 0.6).length;
149
160
  const transformed = appliedCount;
150
- const coverage = detected === 0 ? 100 : Math.round((transformed / detected) * 1000) / 10;
161
+ const transformCoverage = detected === 0 ? 100 : Math.round((transformed / detected) * 1000) / 10;
162
+
163
+ const inventory = content ? enumerateContentPaths(content) : { concreteFields: new Set() };
164
+ const fieldMarkers = pathCoverage?.fieldMarkers || new Set();
165
+ const covers = (markerSet, path) =>
166
+ markerSet.has(path) || [...markerSet].some((marker) => wildcardPath(marker) === wildcardPath(path));
167
+
168
+ let visualRequired = 0;
169
+ let visualCovered = 0;
170
+ for (const path of inventory.concreteFields) {
171
+ if (isControlOnly(path, controlOnlyPaths || [])) continue;
172
+ visualRequired += 1;
173
+ if (covers(fieldMarkers, path)) visualCovered += 1;
174
+ }
175
+ const uncovered = Array.isArray(uncoveredVisibleText) ? uncoveredVisibleText.length : Number(uncoveredVisibleText) || 0;
176
+ let visualCoverage = 100;
177
+ if (visualRequired === 0) {
178
+ visualCoverage = uncovered === 0 ? 100 : Math.max(0, Math.round((1 / (1 + uncovered)) * 1000) / 10);
179
+ } else {
180
+ visualCoverage = Math.round((visualCovered / visualRequired) * 1000) / 10;
181
+ if (uncovered > 0) {
182
+ visualCoverage = Math.min(visualCoverage, Math.round((visualCovered / (visualRequired + uncovered)) * 1000) / 10);
183
+ }
184
+ }
185
+
151
186
  return {
152
187
  detectedEditableCandidates: detected,
153
188
  safelyTransformed: transformed,
154
189
  alreadyEditable,
155
190
  skippedDynamic,
156
191
  skippedLowConfidence: skippedLow,
157
- editableCoverage: Math.min(100, coverage),
192
+ transformCoverage: Math.min(100, transformCoverage),
193
+ visualCovered,
194
+ visualRequired,
195
+ uncoveredVisibleText: uncovered,
196
+ visualCoverage: Math.min(100, visualCoverage),
197
+ editableCoverage: Math.min(100, visualCoverage),
158
198
  };
159
199
  }
160
200
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  const ARC_NAME = 'Deneb ARC';
4
4
  const ARC_FULL_NAME = 'Deneb Adaptive Refactoring Compiler — AI-Augmented';
5
- const ARC_VERSION = '2.0.0';
5
+ const ARC_VERSION = '2.1.0';
6
6
  const SCHEMA_VERSION = 2;
7
7
  const ENGINE_ID = 'deneb-arc';
8
8