@deneb-ui/cli 2.0.104 → 2.0.106

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.
package/bin/index.js CHANGED
@@ -929,7 +929,13 @@ async function initProject(targetInput, options = {}) {
929
929
  conversionRes = pipelineRes.result || pipelineRes;
930
930
  if (pipelineRes.rolledBack) {
931
931
  console.log('\n\x1b[31m✖ DENEB CONVERSION BLOCKED — Original project files preserved intact.\x1b[0m');
932
- console.log(`\x1b[33m Reason:\x1b[0m ${pipelineRes.reasons?.[0] || '100% Fivora Contract / Acceptance Gate validation failed'}`);
932
+ const primaryReason = pipelineRes.reasons?.[0] || pipelineRes.error || '100% Fivora Contract / Acceptance Gate validation failed';
933
+ console.log(`\x1b[33m Reason:\x1b[0m ${primaryReason}`);
934
+ if (pipelineRes.reasons && pipelineRes.reasons.length > 1) {
935
+ for (let i = 1; i < pipelineRes.reasons.length; i++) {
936
+ console.log(`\x1b[90m - ${pipelineRes.reasons[i]}\x1b[0m`);
937
+ }
938
+ }
933
939
  console.log(`\n Run \x1b[36m\x1b[1mnpx @deneb-ui/cli explain --blocked\x1b[0m for exact file locations, line numbers, and fixes.\n`);
934
940
  process.exit(1);
935
941
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deneb-ui/cli",
3
- "version": "2.0.104",
3
+ "version": "2.0.106",
4
4
  "description": "Official DENEB CLI — scaffold, convert, validate, and package Fivora-ready storefront templates.",
5
5
  "bin": {
6
6
  "deneb": "bin/index.js",
@@ -49,7 +49,7 @@
49
49
  "license": "MIT",
50
50
  "dependencies": {
51
51
  "@babel/parser": "^7.28.0",
52
- "@deneb-ui/core": "^2.0.104",
52
+ "@deneb-ui/core": "^2.0.106",
53
53
  "@octokit/rest": "^22.0.1",
54
54
  "adm-zip": "^0.6.0",
55
55
  "dotenv": "^17.4.2",
@@ -83,7 +83,7 @@ function evaluateAcceptanceGates(metrics = {}) {
83
83
  },
84
84
  designPreservation: {
85
85
  name: ACCEPTANCE_GATES.DESIGN_PRESERVATION,
86
- passed: Boolean((metrics.designPreservationScore ?? 100) >= 95),
86
+ passed: Boolean((metrics.designPreservationScore ?? 100) >= 90),
87
87
  critical: false,
88
88
  },
89
89
  controlOnlyProtection: {
@@ -615,16 +615,26 @@ function findUncoveredVisibleTextRegex(code, filePath) {
615
615
  function auditEmptyStateSource(code, filePath) {
616
616
  const errors = [];
617
617
  const source = String(code);
618
+ const isInteractiveOrPageToggle = (condition) => {
619
+ return /(?:open|active|selected|modal|dialog|lightbox|drawer|popup|sheet|portal|loading|pending|tab|filter|step|enabled|ispageenabled|haspage|pagestatus|pageenabled)/i.test(
620
+ String(condition || '')
621
+ );
622
+ };
623
+
618
624
  const andPattern =
619
625
  /\{([^{}]{0,120}?)\s*&&\s*\(?\s*<\s*([a-zA-Z][\w.:-]*)([^>]*data-preview-(?:field-path|list-path|item-path)[^>]*)>/g;
620
626
  for (const match of source.matchAll(andPattern)) {
627
+ const condition = String(match[1]).trim();
628
+ if (isInteractiveOrPageToggle(condition)) continue;
621
629
  errors.push(
622
- `${filePath}:${lineNumberAt(source, match.index ?? 0)} editable marker is gated behind "${String(match[1]).trim()} &&". Keep the target mounted when its value is empty, false, or zero.`
630
+ `${filePath}:${lineNumberAt(source, match.index ?? 0)} editable marker is gated behind "${condition} &&". Keep the target mounted when its value is empty, false, or zero.`
623
631
  );
624
632
  }
625
633
  const ternaryPattern =
626
634
  /\{([^{}]{0,80}?)\s*\?\s*<\s*([a-zA-Z][\w.:-]*)([^>]*data-preview-(?:field-path|list-path|item-path)[^>]*)>[\s\S]{0,200}?\?\s*(?:null|false|undefined)/g;
627
635
  for (const match of source.matchAll(ternaryPattern)) {
636
+ const condition = String(match[1]).trim();
637
+ if (isInteractiveOrPageToggle(condition)) continue;
628
638
  errors.push(
629
639
  `${filePath}:${lineNumberAt(source, match.index ?? 0)} editable marker unmounts on a falsy ternary. Keep the target mounted when its value is empty.`
630
640
  );
package/src/arc/index.cjs CHANGED
@@ -21,7 +21,8 @@ const { planTransformations } = require('./planner.cjs');
21
21
  const { applyFilePlan, instrumentLayoutSource, instrumentPageKey, resolveSiteDataSpecifier, resolveSiteDataRuntimeSpecifier, rewriteRecursiveSiteDataContext, ensureJsonModule, sanitizeContradictoryMarkersInSource } = require('./transformer.cjs');
22
22
  const { applyResidualPass } = require('./residual.cjs');
23
23
  const { parseSource } = require('./ast.cjs');
24
- const { buildSiteDataAndManifest, writeDataBank, loadExistingData, countSchemaFields } = require('./manifest.cjs');
24
+ const { buildSiteDataAndManifest, writeDataBank, loadExistingData, countSchemaFields, upsertSchemaField, getDeep } = require('./manifest.cjs');
25
+ const { classifyFieldType, humanLabel } = require('./field-paths.cjs');
25
26
  const { collectFontIdsFromSiteData } = require('./font-plan.cjs');
26
27
  const { validateAstFiles, validateContracts, designPreservationScore, coverageMetrics } = require('./validator.cjs');
27
28
  const { recordExperience, registryArchitecture } = require('./learning.cjs');
@@ -751,7 +752,7 @@ function runArcTransformations(projectDir, projectName, opts, profile, graph, an
751
752
  .map((f) => ({ abs: path.join(projectDir, f), rel: f }))
752
753
  );
753
754
  const syntaxPassed = astResults.every((r) => r.passed);
754
- const contracts = validateContracts(projectDir, dataBundle.siteData, dataBundle.manifest);
755
+ let contracts = validateContracts(projectDir, dataBundle.siteData, dataBundle.manifest);
755
756
  const design = designPreservationScore(plan.files, afterFiles);
756
757
  const alreadyEditable = analyses.filter((a) => a.alreadyEditable).length;
757
758
  const skippedDynamic = plan.skipped.filter((s) => /dynamic|api/.test(s.reason || '')).length;
@@ -786,6 +787,26 @@ function runArcTransformations(projectDir, projectName, opts, profile, graph, an
786
787
  manifest: dataBundle.manifest,
787
788
  inventory: collectMarkerInventory(projectDir, profile, graph),
788
789
  });
790
+ contracts = validateContracts(projectDir, dataBundle.siteData, dataBundle.manifest);
791
+ }
792
+ }
793
+
794
+ // Auto-reconciliation: If contracts flagged missingSchema, register them in manifest.editorSchema
795
+ if (contracts.missingSchema.length > 0) {
796
+ let schemaModified = false;
797
+ dataBundle.manifest.editorSchema = dataBundle.manifest.editorSchema || { version: 1, sections: [] };
798
+ dataBundle.manifest.editorSchema.sections = dataBundle.manifest.editorSchema.sections || [];
799
+ for (const item of contracts.missingSchema) {
800
+ const fp = item.fieldPath;
801
+ const leaf = fp.split('.').pop();
802
+ const val = getDeep(dataBundle.siteData.content, fp);
803
+ const type = classifyFieldType('text', val, leaf);
804
+ upsertSchemaField(dataBundle.manifest.editorSchema.sections, fp, type, humanLabel(leaf));
805
+ schemaModified = true;
806
+ }
807
+ if (schemaModified) {
808
+ writeDataBank(projectDir, dataBundle.siteData, dataBundle.manifest);
809
+ contracts = validateContracts(projectDir, dataBundle.siteData, dataBundle.manifest);
789
810
  }
790
811
  }
791
812
 
@@ -882,7 +903,7 @@ function runArcTransformations(projectDir, projectName, opts, profile, graph, an
882
903
 
883
904
  const criticalFailure = !syntaxPassed || (contracts.actionCollisions > 0 && appliedCount === 0) || (buildResult.passed === false);
884
905
  const contractFailure = !fivoraAudit.passed || fivoraAudit.uncoveredVisibleText.length > 0;
885
- const designFailure = design.score < 95 && design.total > 0;
906
+ const designFailure = design.score < 90 && design.total > 0;
886
907
  const assetFailure = !assetAudit.passed && assetAudit.errors.length > 0;
887
908
  let outcome = 'success';
888
909
  if (criticalFailure) {
@@ -909,7 +930,7 @@ function runArcTransformations(projectDir, projectName, opts, profile, graph, an
909
930
  const reasons = [];
910
931
  if (!fivoraAudit.passed) reasons.push(`${fivoraAudit.errors.length} Fivora contract error(s)`);
911
932
  if (fivoraAudit.uncoveredVisibleText.length > 0) reasons.push(`${fivoraAudit.uncoveredVisibleText.length} uncovered visible text node(s)`);
912
- if (designFailure) reasons.push(`Design preservation score below threshold (${design.score} < 98)`);
933
+ if (designFailure) reasons.push(`Design preservation score below threshold (${design.score} < 90)`);
913
934
  if (assetFailure) reasons.push(`Asset integrity failed (${assetAudit.errors.length} missing asset(s))`);
914
935
  if (!acceptance.allCriticalPassed) reasons.push(`Critical acceptance gates failed: ${acceptance.status}`);
915
936
  printer.printRollback(`Strict Fivora contract failed: ${reasons.join(', ')}`);
@@ -1180,7 +1201,9 @@ function runTransactionalPipeline(targetDirInput = '.', options = {}) {
1180
1201
  const syntaxOk = result.validation?.syntaxPassed !== false;
1181
1202
  const assetOk = result.validation?.assetIntegrity?.passed !== false;
1182
1203
  const gatesOk = result.validation?.acceptance ? result.validation.acceptance.passed : true;
1183
- const isSuccess = result.outcome === 'success' && (!opts.strict || (contractOk && syntaxOk && assetOk && gatesOk));
1204
+ const isSuccess =
1205
+ result.outcome !== 'rolled-back' &&
1206
+ (!opts.strict || (result.outcome === 'success' && contractOk && syntaxOk && assetOk && gatesOk));
1184
1207
 
1185
1208
  if (isSuccess) {
1186
1209
  // Collect all files to commit from workspace to project
@@ -1219,7 +1242,7 @@ function runTransactionalPipeline(targetDirInput = '.', options = {}) {
1219
1242
  if (result.validation?.fivoraContractPassed === false) reasons.push('Fivora strict contract validation failed');
1220
1243
  if (result.validation?.uncoveredVisibleText > 0) reasons.push(`${result.validation.uncoveredVisibleText} uncovered visible text node(s)`);
1221
1244
  if (result.validation?.syntaxPassed === false) reasons.push('AST syntax validation failed');
1222
- if (result.validation?.designPreservation < 95 && result.validation?.designPreservation > 0) reasons.push(`Design preservation (${result.validation.designPreservation}%) below threshold`);
1245
+ if (result.validation?.designPreservation < 90 && result.validation?.designPreservation > 0) reasons.push(`Design preservation (${result.validation.designPreservation}%) below threshold`);
1223
1246
  if (result.validation?.assetIntegrity?.passed === false) reasons.push(`Asset integrity failed: ${result.validation.assetIntegrity.errors?.[0] || 'missing asset'}`);
1224
1247
  if (result.validation?.acceptance && !result.validation.acceptance.passed) reasons.push(`15-Gate Acceptance Matrix failed: ${result.validation.acceptance.status}`);
1225
1248
 
@@ -1251,6 +1274,7 @@ function runTransactionalPipeline(targetDirInput = '.', options = {}) {
1251
1274
  rolledBack: true,
1252
1275
  outcome: 'rolled-back',
1253
1276
  error: err.message,
1277
+ reasons: [err.message],
1254
1278
  };
1255
1279
  }
1256
1280
  }
@@ -180,7 +180,31 @@ function upsertSchemaList(sections, listPath, itemFields, items) {
180
180
  }
181
181
 
182
182
  const key = rest[rest.length - 1];
183
- if (fields.some((f) => f.key === key)) return;
183
+ const existingList = fields.find((f) => f.key === key);
184
+ if (existingList) {
185
+ existingList.fields = existingList.fields || [];
186
+ for (const field of itemFields || []) {
187
+ if (!existingList.fields.some((ef) => ef.key === field.key)) {
188
+ existingList.fields.push({
189
+ key: field.key,
190
+ type:
191
+ field.type === 'url'
192
+ ? 'url'
193
+ : field.type === 'image'
194
+ ? 'image'
195
+ : field.type === 'textarea'
196
+ ? 'textarea'
197
+ : field.type === 'tel' || field.type === 'phone'
198
+ ? 'tel'
199
+ : field.type === 'email'
200
+ ? 'email'
201
+ : field.type || 'text',
202
+ label: humanLabel(field.key),
203
+ });
204
+ }
205
+ }
206
+ return;
207
+ }
184
208
 
185
209
  const isPrimitiveList = (itemFields || []).length === 0;
186
210
  fields.push({
@@ -255,6 +279,9 @@ function enrichSchemasFromContent(content, sections) {
255
279
  if (Object.prototype.hasOwnProperty.call(node, labelKey)) {
256
280
  upsertSchemaField(sections, `${prefix}.${labelKey}`, 'text', humanLabel(labelKey));
257
281
  }
282
+ } else if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
283
+ const fieldType = classifyFieldType('text', value, key);
284
+ upsertSchemaField(sections, nextPath, fieldType, humanLabel(key));
258
285
  } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
259
286
  walk(value, nextPath);
260
287
  }
@@ -627,6 +654,35 @@ function buildSiteDataAndManifest({
627
654
  }
628
655
  }
629
656
 
657
+ // Ensure all bound field paths (e.g., from existing template code) are registered in editorSchema and content
658
+ for (const bPath of boundFieldPaths || []) {
659
+ if (!bPath || typeof bPath !== 'string') continue;
660
+ const listMatch = bPath.match(/^(.*?)\[(?:\d+|\*|[a-zA-Z0-9_]+)\]\.(.*)$/);
661
+ if (listMatch) {
662
+ const listPath = listMatch[1];
663
+ const itemKey = listMatch[2];
664
+ const listVal = getDeep(content, listPath) || [];
665
+ const sample = Array.isArray(listVal) && listVal[0] ? listVal[0] : {};
666
+ const itemVal = getDeep(sample, itemKey);
667
+ const itemType = classifyFieldType('text', itemVal, itemKey);
668
+ upsertSchemaList(editorSections, listPath, [{ key: itemKey, type: itemType }], listVal);
669
+ continue;
670
+ }
671
+ if (!isBound(bPath) && !isAllowedControlOnly(bPath)) continue;
672
+ let val = getDeep(content, bPath);
673
+ if (val === undefined && existingSiteData?.content) {
674
+ val = getDeep(existingSiteData.content, bPath);
675
+ if (val !== undefined) setDeep(content, bPath, val);
676
+ }
677
+ if (val === undefined) {
678
+ setDeep(content, bPath, '');
679
+ val = '';
680
+ }
681
+ const leafKey = bPath.split('.').pop();
682
+ const type = classifyFieldType('text', val, leafKey);
683
+ upsertSchemaField(editorSections, bPath, type, humanLabel(leafKey));
684
+ }
685
+
630
686
  pruneUnboundLeaves(content, isBound);
631
687
 
632
688
  const siteData = {
@@ -747,6 +803,7 @@ module.exports = {
747
803
  collectFieldsFromPlan,
748
804
  computeControlOnlyPaths,
749
805
  assignSectionPageKeys,
806
+ upsertSchemaField,
750
807
  upsertSchemaList,
751
808
  enrichSchemasFromContent,
752
809
  isListActionCtaKey,
@@ -415,7 +415,7 @@ function callbackRootElement(callback) {
415
415
  */
416
416
  function collectItemFieldUsage(callback, itemParam) {
417
417
  const usage = new Map();
418
- if (!callback || !itemParam) return usage;
418
+ if (!callback || !itemParam) return { usage, usesItemAsComponent: false, usesDirectItem: false };
419
419
 
420
420
  function record(property, role) {
421
421
  if (!property || usage.has(property)) return;
@@ -595,6 +595,21 @@ function analyzeFile(optionsOrFile, codeArg, profileArg) {
595
595
  return;
596
596
  }
597
597
 
598
+ if (hasStaticMarker(node)) {
599
+ candidates.push({
600
+ loc,
601
+ tag: name,
602
+ kind: 'static',
603
+ confidence: 1,
604
+ skip: true,
605
+ reason: 'already-static-marker',
606
+ file: relativeFile,
607
+ ownerScope,
608
+ });
609
+ this.traverse(pathNode);
610
+ return;
611
+ }
612
+
598
613
  if (isElementOrParentHidden(pathNode)) {
599
614
  candidates.push({
600
615
  loc,
@@ -31,47 +31,23 @@ function isUnsafeToStripCondition(leftExpr, rightExpr, pathNode) {
31
31
 
32
32
  if (leftIdentifiers.size === 0) return true;
33
33
 
34
- // 2. State & modal variable keywords (e.g., selectedDish, lightboxIndex, isOpen, activeTab)
35
- const stateKeywordRegex = /(?:open|active|selected|current|expanded|visible|show|modal|dialog|lightbox|drawer|loading|tab|filter|step|index)/i;
34
+ // 2. Interactive state & modal variable keywords (e.g. lightboxIndex, selectedDish, isOpen, activeTab, isModalOpen)
35
+ const interactiveStateKeywordRegex = /(?:modal|dialog|lightbox|drawer|popup|sheet|portal|isopen|activetab|selecteditem|selecteddish|lightboxindex|expanded|loading|pending)/i;
36
36
  for (const id of leftIdentifiers) {
37
- if (stateKeywordRegex.test(id)) {
38
- return true; // NEVER strip modal/state/selection guards
37
+ if (interactiveStateKeywordRegex.test(id)) {
38
+ return true; // NEVER strip modal/interactive state guards
39
39
  }
40
40
  }
41
41
 
42
- // 3. Null / undefined / equality / comparison checks
43
- let hasComparisonOrNullCheck = false;
44
- recast.types.visit(leftExpr, {
45
- visitBinaryExpression(bPath) {
46
- const op = bPath.node.operator;
47
- if (['!==', '!=', '===', '==', '>', '<', '>=', '<='].includes(op)) {
48
- hasComparisonOrNullCheck = true;
49
- }
50
- this.traverse(bPath);
51
- },
52
- visitUnaryExpression(uPath) {
53
- if (uPath.node.operator === '!') {
54
- hasComparisonOrNullCheck = true;
55
- }
56
- this.traverse(uPath);
57
- },
58
- });
59
- if (hasComparisonOrNullCheck) return true;
60
-
61
- // 4. Check if ANY identifier in leftExpr is referenced inside rightExpr
62
- let rightReferencesLeftId = false;
63
- recast.types.visit(rightExpr, {
64
- visitIdentifier(idPath) {
65
- if (leftIdentifiers.has(idPath.node.name)) {
66
- rightReferencesLeftId = true;
67
- return false;
68
- }
69
- this.traverse(idPath);
70
- },
71
- });
72
- if (rightReferencesLeftId) return true;
42
+ // 3. Page & feature toggles (e.g. inventoryEnabled, contactEnabled, isPageEnabled)
43
+ const pageToggleRegex = /(?:enabled|ispageenabled|haspage|pagestatus|pageenabled)/i;
44
+ for (const id of leftIdentifiers) {
45
+ if (pageToggleRegex.test(id)) {
46
+ return true; // NEVER strip page/route enablement guards
47
+ }
48
+ }
73
49
 
74
- // 5. Check if rightExpr or any of its ancestors is an interactive modal / dialog / overlay / AnimatePresence
50
+ // 4. Check if rightExpr or any of its ancestors is an interactive modal / dialog / overlay / AnimatePresence
75
51
  let curr = pathNode;
76
52
  while (curr && curr.parent) {
77
53
  const parentNode = curr.parent.node;
@@ -111,11 +87,6 @@ function isUnsafeToStripCondition(leftExpr, rightExpr, pathNode) {
111
87
  });
112
88
  if (isModalOrOverlay) return true;
113
89
 
114
- // 6. Only allow stripping if leftExpr is purely a direct siteData content member chain
115
- const leftCode = recast.print(leftExpr).code;
116
- const isDirectContentCheck = /siteData(?:\?\.|\.)content(?:\?\.|\.)/i.test(leftCode);
117
- if (!isDirectContentCheck) return true;
118
-
119
90
  return false;
120
91
  }
121
92
 
@@ -141,6 +112,15 @@ function healEmptyStateConditionals(ast) {
141
112
  ) {
142
113
  return true;
143
114
  }
115
+ const attrs = node.openingElement?.attributes || [];
116
+ for (const a of attrs) {
117
+ if (a.type === 'JSXSpreadAttribute') {
118
+ const code = recast.print(a).code;
119
+ if (/data-preview-(?:field-path|list-path|item-path)/.test(code)) {
120
+ return true;
121
+ }
122
+ }
123
+ }
144
124
  return (node.children || []).some((child) => jsxHasPreviewMarker(child));
145
125
  }
146
126
  if (node.type === 'JSXFragment') {
@@ -48,7 +48,7 @@ function validateContracts(projectDir, siteData, manifest) {
48
48
  if (siteData?.content && getDeep(siteData.content, fieldPath) === undefined) {
49
49
  orphans.push({ fieldPath, file: relative });
50
50
  }
51
- if (manifest && !isFieldInEditorSchema(manifest, fieldPath)) {
51
+ if (manifest && !isFieldInEditorSchema(manifest, fieldPath) && !isControlOnly(fieldPath, manifest.visualEditing?.controlOnlyPaths)) {
52
52
  missingSchema.push({ fieldPath, file: relative });
53
53
  }
54
54
  }
@@ -82,24 +82,49 @@ function validateContracts(projectDir, siteData, manifest) {
82
82
  }
83
83
 
84
84
  function isFieldInEditorSchema(manifest, fieldPath) {
85
- const parts = String(fieldPath).split('.');
86
- const sectionId = parts[0];
87
- const fieldKey = parts.slice(1).join('.');
88
- const section = (manifest.editorSchema?.sections || []).find((s) => s.id === sectionId || s.path === sectionId);
85
+ if (!manifest?.editorSchema?.sections) return false;
86
+ const cleanPath = String(fieldPath)
87
+ .replace(/\[(?:\d+|\*|[a-zA-Z0-9_]+)\]/g, '.[*]')
88
+ .replace(/\.\.+/g, '.');
89
+ const parts = cleanPath.split('.').filter(Boolean);
90
+ const rawSectionId = parts[0];
91
+ const sectionId = rawSectionId ? rawSectionId.replace(/\[\*\]/g, '') : '';
92
+
93
+ const section = (manifest.editorSchema?.sections || []).find(
94
+ (s) => s.id === sectionId || s.path === sectionId || s.id === rawSectionId
95
+ );
89
96
  if (!section) return false;
97
+ const fieldParts = parts.slice(1);
98
+ if (fieldParts.length === 0) return true;
99
+
100
+ function check(fields, remainingParts) {
101
+ if (!Array.isArray(fields) || remainingParts.length === 0) return false;
102
+ const current = remainingParts[0];
103
+ const rest = remainingParts.slice(1);
104
+
105
+ if (current === '[*]') {
106
+ if (rest.length === 0) return true;
107
+ return check(fields, rest);
108
+ }
90
109
 
91
- function checkFields(fields, targetKey) {
92
- if (!Array.isArray(fields)) return false;
93
110
  for (const f of fields) {
94
- if (f.key === targetKey) return true;
95
- if (f.fields && targetKey.startsWith(f.key + '.')) {
96
- const subKey = targetKey.substring(f.key.length + 1);
97
- if (checkFields(f.fields, subKey)) return true;
111
+ if (f.key === current) {
112
+ if (rest.length === 0) return true;
113
+ const subFields = f.fields || f.itemFields;
114
+ if (subFields) {
115
+ if (rest[0] === '[*]') {
116
+ if (rest.length === 1) return true;
117
+ if (check(subFields, rest.slice(1))) return true;
118
+ }
119
+ if (check(subFields, rest)) return true;
120
+ }
98
121
  }
122
+ if (f.key === remainingParts.join('.')) return true;
99
123
  }
100
124
  return false;
101
125
  }
102
- return checkFields(section.fields, fieldKey);
126
+
127
+ return check(section.fields || section.itemFields, fieldParts);
103
128
  }
104
129
 
105
130
  function designPreservationScore(filePlans, afterFiles) {