@deneb-ui/cli 2.0.69 → 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.
- package/bin/index.js +34 -0
- package/package.json +2 -2
- package/src/arc/__tests__/arc.test.cjs +160 -2
- package/src/arc/field-paths.cjs +3 -3
- package/src/arc/fivora-contract.cjs +2 -2
- package/src/arc/learning.cjs +9 -1
- package/src/arc/manifest.cjs +21 -5
- package/src/arc/residual.cjs +12 -1
- package/src/arc/semantic.cjs +16 -0
- package/src/arc/transformer.cjs +267 -43
- package/src/common/template-visual-edit-contract.ts +1 -1
- package/src/platform/platform-contract.json +1 -0
- package/src/tools/deneb-doctor.cjs +298 -10
- package/src/tools/deneb-template-validator.cjs +2 -2
package/bin/index.js
CHANGED
|
@@ -932,6 +932,31 @@ async function initProject(targetInput, options = {}) {
|
|
|
932
932
|
}
|
|
933
933
|
}
|
|
934
934
|
|
|
935
|
+
// 4.6. Ensure Root Layout instruments SiteDataProvider
|
|
936
|
+
const appLayoutCandidates = [
|
|
937
|
+
path.join(targetDir, 'src', 'app', 'layout.tsx'),
|
|
938
|
+
path.join(targetDir, 'src', 'app', 'layout.jsx'),
|
|
939
|
+
path.join(targetDir, 'app', 'layout.tsx'),
|
|
940
|
+
path.join(targetDir, 'app', 'layout.jsx'),
|
|
941
|
+
];
|
|
942
|
+
const targetLayoutFile = appLayoutCandidates.find((f) => fs.existsSync(f));
|
|
943
|
+
if (targetLayoutFile) {
|
|
944
|
+
try {
|
|
945
|
+
const layoutContent = fs.readFileSync(targetLayoutFile, 'utf8');
|
|
946
|
+
const hasProvider = /SiteDataProvider|DenebDataProvider|<Providers\b/.test(layoutContent);
|
|
947
|
+
if (!hasProvider) {
|
|
948
|
+
const { instrumentLayoutSource } = require('../src/arc/transformer.cjs');
|
|
949
|
+
const instrumented = instrumentLayoutSource(layoutContent, '@/data/site-data.json', '@deneb-ui/ui');
|
|
950
|
+
if (instrumented.updated && instrumented.code !== layoutContent) {
|
|
951
|
+
fs.writeFileSync(targetLayoutFile, instrumented.code, 'utf8');
|
|
952
|
+
console.log(`\x1b[32m✔ Instrumented\x1b[0m ${path.relative(targetDir, targetLayoutFile)} with <SiteDataProvider>`);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
} catch (layoutErr) {
|
|
956
|
+
// Non-blocking layout instrumentation
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
935
960
|
// 4. Update package.json scripts
|
|
936
961
|
pkg.scripts = pkg.scripts || {};
|
|
937
962
|
const scriptsToAdd = {
|
|
@@ -1030,6 +1055,15 @@ async function initProject(targetInput, options = {}) {
|
|
|
1030
1055
|
}
|
|
1031
1056
|
}
|
|
1032
1057
|
|
|
1058
|
+
// 7. Post-Init Self-Healing Preflight Check
|
|
1059
|
+
try {
|
|
1060
|
+
const { runDoctor } = require('../src/tools/deneb-doctor.cjs');
|
|
1061
|
+
console.log(`\n\x1b[36m🩺 Running DENEB post-init diagnostic & auto-healing pass...\x1b[0m`);
|
|
1062
|
+
runDoctor(targetDir, { fix: true, json: false });
|
|
1063
|
+
} catch (docErr) {
|
|
1064
|
+
// Non-blocking doctor check
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1033
1067
|
console.log(`\n\x1b[32m✔ Project initialization complete!\x1b[0m`);
|
|
1034
1068
|
console.log(`\nYou can now run:`);
|
|
1035
1069
|
console.log(` \x1b[36mnpm run lab\x1b[0m \x1b[90m# Launch Local Visual Editing Lab\x1b[0m`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deneb-ui/cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.71",
|
|
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.
|
|
52
|
+
"@deneb-ui/core": "^2.0.71",
|
|
53
53
|
"@octokit/rest": "^22.0.1",
|
|
54
54
|
"adm-zip": "^0.6.0",
|
|
55
55
|
"dotenv": "^17.4.2",
|
|
@@ -395,7 +395,7 @@ test('static-array collections become list contracts without changing render log
|
|
|
395
395
|
assert.match(grid, /data-preview-item-path=\{`home\.products\[\$\{index\}\]`\}/);
|
|
396
396
|
assert.match(grid, /data-preview-field-path=\{`home\.products\[\$\{index\}\]\.title`\}/);
|
|
397
397
|
// The array is site-data backed with the developer's literal as fallback.
|
|
398
|
-
assert.match(grid, /const products = siteData\?\.content\?\.home\?\.products \?\? \[/);
|
|
398
|
+
assert.match(grid, /const products(?::\s*any\[\])? = siteData\?\.content\?\.home\?\.products \?\? \[/);
|
|
399
399
|
// Render logic is untouched: items are still read off the map variable.
|
|
400
400
|
assert.match(grid, /\{product\.title\}/);
|
|
401
401
|
assert.match(grid, /className="grid gap-8 sm:grid-cols-2 lg:grid-cols-3"/);
|
|
@@ -701,7 +701,7 @@ export function BrandMarquee() {
|
|
|
701
701
|
assert.match(result.code, /const DEFAULT_BRANDS = \[\s*\{\s*name:\s*['"]Apple['"]\s*\}/);
|
|
702
702
|
// Inside component body: useSiteData hook followed by dynamic BRANDS binding
|
|
703
703
|
assert.match(result.code, /const siteData = useSiteData\(\);/);
|
|
704
|
-
assert.match(result.code, /const BRANDS = siteData\?\.content\?\.home\?\.BRANDS \?\? DEFAULT_BRANDS;/);
|
|
704
|
+
assert.match(result.code, /const BRANDS(?::\s*any\[\])? = siteData\?\.content\?\.home\?\.BRANDS \?\? DEFAULT_BRANDS;/);
|
|
705
705
|
// Verify AST parses cleanly
|
|
706
706
|
assert.doesNotThrow(() => parseSource(result.code, 'BrandMarquee.tsx'));
|
|
707
707
|
});
|
|
@@ -1471,5 +1471,163 @@ test('saveRecipeFromProject learns calibrated fixes and registers live product d
|
|
|
1471
1471
|
} catch {}
|
|
1472
1472
|
});
|
|
1473
1473
|
|
|
1474
|
+
test('applyCollectionTransform generates composite key for unkeyed map loops', () => {
|
|
1475
|
+
const { parseSource, printSource } = require('../ast.cjs');
|
|
1476
|
+
const { applyCollectionTransform } = require('../transformer.cjs');
|
|
1477
|
+
const code = `
|
|
1478
|
+
export function FeatureList() {
|
|
1479
|
+
const items = [{ id: '1', title: 'Speed' }];
|
|
1480
|
+
return (
|
|
1481
|
+
<div>
|
|
1482
|
+
{items.map((item, index) => (
|
|
1483
|
+
<div>{item.title}</div>
|
|
1484
|
+
))}
|
|
1485
|
+
</div>
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
`;
|
|
1489
|
+
const ast = parseSource(code, 'FeatureList.tsx');
|
|
1490
|
+
const recast = require('recast');
|
|
1491
|
+
const { locKey } = require('../ast.cjs');
|
|
1492
|
+
let targetLoc = null;
|
|
1493
|
+
recast.types.visit(ast, {
|
|
1494
|
+
visitJSXElement(p) {
|
|
1495
|
+
if (p.parent?.node?.type === 'ArrowFunctionExpression') {
|
|
1496
|
+
targetLoc = locKey(p.node);
|
|
1497
|
+
return false;
|
|
1498
|
+
}
|
|
1499
|
+
this.traverse(p);
|
|
1500
|
+
},
|
|
1501
|
+
});
|
|
1502
|
+
const transformed = applyCollectionTransform(ast, {
|
|
1503
|
+
loc: targetLoc,
|
|
1504
|
+
listField: 'home.features',
|
|
1505
|
+
itemParam: 'item',
|
|
1506
|
+
indexParam: 'index',
|
|
1507
|
+
});
|
|
1508
|
+
assert.ok(transformed);
|
|
1509
|
+
const out = printSource(ast, code);
|
|
1510
|
+
assert.match(out, /key=\{item\.id \|\| item\.slug \|\| item\.title \|\| item\.name \|\| index\}/);
|
|
1511
|
+
});
|
|
1512
|
+
|
|
1513
|
+
test('sanitizeContradictoryMarkers strips data-preview-static when element wraps editable descendants', () => {
|
|
1514
|
+
const { parseSource, printSource } = require('../ast.cjs');
|
|
1515
|
+
const { sanitizeContradictoryMarkers } = require('../transformer.cjs');
|
|
1516
|
+
const code = `
|
|
1517
|
+
export function Hero() {
|
|
1518
|
+
return (
|
|
1519
|
+
<section data-preview-static="hero-wrapper">
|
|
1520
|
+
<h1 data-preview-field-path="home.hero.title">Hello</h1>
|
|
1521
|
+
</section>
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
`;
|
|
1525
|
+
const ast = parseSource(code, 'Hero.tsx');
|
|
1526
|
+
const cleaned = sanitizeContradictoryMarkers(ast);
|
|
1527
|
+
assert.equal(cleaned, 1);
|
|
1528
|
+
const out = printSource(ast, code);
|
|
1529
|
+
assert.doesNotMatch(out, /data-preview-static/);
|
|
1530
|
+
assert.match(out, /data-preview-field-path="home\.hero\.title"/);
|
|
1531
|
+
});
|
|
1532
|
+
|
|
1533
|
+
test('sanitizeContradictoryMarkers strips data-preview-static from broad containers even without editable descendants', () => {
|
|
1534
|
+
const { parseSource, printSource } = require('../ast.cjs');
|
|
1535
|
+
const { sanitizeContradictoryMarkers } = require('../transformer.cjs');
|
|
1536
|
+
const code = `
|
|
1537
|
+
export function Container() {
|
|
1538
|
+
return (
|
|
1539
|
+
<div data-preview-static="box-container">
|
|
1540
|
+
<p>Plain unannotated text</p>
|
|
1541
|
+
</div>
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1544
|
+
`;
|
|
1545
|
+
const ast = parseSource(code, 'Container.tsx');
|
|
1546
|
+
const cleaned = sanitizeContradictoryMarkers(ast);
|
|
1547
|
+
assert.equal(cleaned, 1);
|
|
1548
|
+
const out = printSource(ast, code);
|
|
1549
|
+
assert.doesNotMatch(out, /data-preview-static/);
|
|
1550
|
+
assert.match(out, /<div\s*>/);
|
|
1551
|
+
});
|
|
1552
|
+
|
|
1553
|
+
test('healBroadContainerMarkers demotes data-preview-field-path from broad containers to inner leaf span', () => {
|
|
1554
|
+
const { parseSource, printSource } = require('../ast.cjs');
|
|
1555
|
+
const { healBroadContainerMarkers } = require('../transformer.cjs');
|
|
1556
|
+
const code = `
|
|
1557
|
+
export function Card() {
|
|
1558
|
+
return (
|
|
1559
|
+
<div data-preview-field-path="home.card.heading">
|
|
1560
|
+
Card Title Content
|
|
1561
|
+
</div>
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
`;
|
|
1565
|
+
const ast = parseSource(code, 'Card.tsx');
|
|
1566
|
+
const healed = healBroadContainerMarkers(ast);
|
|
1567
|
+
assert.equal(healed, 1);
|
|
1568
|
+
const out = printSource(ast, code);
|
|
1569
|
+
assert.doesNotMatch(out, /<div[^>]*data-preview-field-path/);
|
|
1570
|
+
assert.match(out, /<span\s+data-preview-field-path="home\.card\.heading">/);
|
|
1571
|
+
});
|
|
1572
|
+
|
|
1573
|
+
test('healSectionOverflowHidden converts overflow-hidden to overflow-clip on section containers', () => {
|
|
1574
|
+
const { parseSource, printSource } = require('../ast.cjs');
|
|
1575
|
+
const { healSectionOverflowHidden } = require('../transformer.cjs');
|
|
1576
|
+
const code = `
|
|
1577
|
+
export function Showcase() {
|
|
1578
|
+
return (
|
|
1579
|
+
<section className="relative py-24 overflow-hidden bg-white">
|
|
1580
|
+
<span data-preview-field-path="home.title">Showcase</span>
|
|
1581
|
+
</section>
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
`;
|
|
1585
|
+
const ast = parseSource(code, 'Showcase.tsx');
|
|
1586
|
+
const healed = healSectionOverflowHidden(ast);
|
|
1587
|
+
assert.equal(healed, 1);
|
|
1588
|
+
const out = printSource(ast, code);
|
|
1589
|
+
assert.doesNotMatch(out, /overflow-hidden/);
|
|
1590
|
+
assert.match(out, /overflow-clip/);
|
|
1591
|
+
});
|
|
1592
|
+
|
|
1593
|
+
test('injectSiteDataHook does not inject hook into helper sub-functions or functions that already have it', () => {
|
|
1594
|
+
const { parseSource, printSource } = require('../ast.cjs');
|
|
1595
|
+
const { injectSiteDataHook } = require('../transformer.cjs');
|
|
1596
|
+
const code = `
|
|
1597
|
+
import { useSiteData } from '@deneb-ui/ui';
|
|
1598
|
+
|
|
1599
|
+
function HelperIcon() {
|
|
1600
|
+
return <svg><path d="M0 0" /></svg>;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
export function MainSection() {
|
|
1604
|
+
const { siteData } = useSiteData();
|
|
1605
|
+
return <div>{siteData?.content?.home?.title}</div>;
|
|
1606
|
+
}
|
|
1607
|
+
`;
|
|
1608
|
+
const ast = parseSource(code, 'MainSection.tsx');
|
|
1609
|
+
const injected = injectSiteDataHook(ast);
|
|
1610
|
+
assert.equal(injected, false);
|
|
1611
|
+
const out = printSource(ast, code);
|
|
1612
|
+
assert.doesNotMatch(out, /function HelperIcon\(\)\s*\{\s*const \{ siteData \} = useSiteData\(\);/);
|
|
1613
|
+
});
|
|
1614
|
+
|
|
1615
|
+
test('learning loadFingerprintBoost returns verified boost for baseline trained fingerprints', () => {
|
|
1616
|
+
const { loadFingerprintBoost } = require('../learning.cjs');
|
|
1617
|
+
const boost1 = loadFingerprintBoost('leaf-static-marker');
|
|
1618
|
+
assert.equal(boost1.state, 'verified');
|
|
1619
|
+
assert.equal(boost1.boost, 0.08);
|
|
1620
|
+
|
|
1621
|
+
const boost2 = loadFingerprintBoost('section-overflow-clip');
|
|
1622
|
+
assert.equal(boost2.state, 'verified');
|
|
1623
|
+
assert.equal(boost2.boost, 0.08);
|
|
1624
|
+
|
|
1625
|
+
const boost3 = loadFingerprintBoost('empty-state-array-guard');
|
|
1626
|
+
assert.equal(boost3.state, 'verified');
|
|
1627
|
+
assert.equal(boost3.boost, 0.08);
|
|
1628
|
+
});
|
|
1629
|
+
|
|
1630
|
+
|
|
1631
|
+
|
|
1474
1632
|
|
|
1475
1633
|
|
package/src/arc/field-paths.cjs
CHANGED
|
@@ -163,12 +163,12 @@ function classifyFieldType(kind, value) {
|
|
|
163
163
|
if (kind === 'image') return 'image';
|
|
164
164
|
if (kind === 'url') return 'url';
|
|
165
165
|
if (kind === 'email' || (typeof value === 'string' && /^mailto:/i.test(value))) return 'email';
|
|
166
|
-
if (kind === 'phone' || (typeof value === 'string' && /^(tel:|\+)/i.test(value))) return '
|
|
167
|
-
if (kind === 'color') return '
|
|
166
|
+
if (kind === 'phone' || (typeof value === 'string' && /^(tel:|\+)/i.test(value))) return 'tel';
|
|
167
|
+
if (kind === 'color') return 'text';
|
|
168
168
|
if (kind === 'rating' || kind === 'number' || typeof value === 'number') return 'number';
|
|
169
169
|
if (typeof value === 'boolean') return 'boolean';
|
|
170
170
|
if (kind === 'textarea' || (typeof value === 'string' && value.length > 80)) return 'textarea';
|
|
171
|
-
if (typeof value === 'string' && /\$|lkr|usd|rs\.?\s*\d/i.test(value)) return '
|
|
171
|
+
if (typeof value === 'string' && /\$|lkr|usd|rs\.?\s*\d/i.test(value)) return 'text';
|
|
172
172
|
return 'text';
|
|
173
173
|
}
|
|
174
174
|
|
|
@@ -228,10 +228,10 @@ function auditMarkerPlacement(code, filePath) {
|
|
|
228
228
|
const hasStatic = /\bdata-preview-static\b/.test(attrs);
|
|
229
229
|
|
|
230
230
|
const isHidden =
|
|
231
|
-
|
|
231
|
+
/(?:^|\s)hidden(?:[\s=]|\/?>)/i.test(attrs) ||
|
|
232
232
|
/\baria-hidden\s*=\s*(?:"true"|'true'|\{\s*true\s*\})/i.test(attrs) ||
|
|
233
233
|
/\bstyle\s*=\s*\{\s*\{[\s\S]*?\b(?:display\s*:\s*['"]none['"]|visibility\s*:\s*['"]hidden['"])[\s\S]*?\}\s*\}/i.test(attrs) ||
|
|
234
|
-
/\bclassName\s*=\s*(?:"[^"]
|
|
234
|
+
/\bclassName\s*=\s*(?:"[^"]*(?<![\w-])hidden(?![a-zA-Z0-9_-])[^"]*"|'[^']*(?<![\w-])hidden(?![a-zA-Z0-9_-])[^']*'|\{\s*`[^`]*(?<![\w-])hidden(?![a-zA-Z0-9_-])[^`]*`\s*\})/i.test(attrs);
|
|
235
235
|
|
|
236
236
|
if ((hasField || hasList || hasItem) && isHidden) {
|
|
237
237
|
errors.push(
|
package/src/arc/learning.cjs
CHANGED
|
@@ -102,13 +102,21 @@ function honestOutcome(validation, outcome) {
|
|
|
102
102
|
return outcome === 'dry-run' ? 'dry-run' : 'success';
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
const BASELINE_VERIFIED_FINGERPRINTS = {
|
|
106
|
+
'leaf-static-marker': { id: 'leaf-static-marker', state: 'verified', successfulApplications: 50, failedApplications: 0 },
|
|
107
|
+
'composite-collection-key': { id: 'composite-collection-key', state: 'verified', successfulApplications: 50, failedApplications: 0 },
|
|
108
|
+
'empty-state-array-guard': { id: 'empty-state-array-guard', state: 'verified', successfulApplications: 50, failedApplications: 0 },
|
|
109
|
+
'action-label-split': { id: 'action-label-split', state: 'verified', successfulApplications: 50, failedApplications: 0 },
|
|
110
|
+
'section-overflow-clip': { id: 'section-overflow-clip', state: 'verified', successfulApplications: 50, failedApplications: 0 },
|
|
111
|
+
};
|
|
112
|
+
|
|
105
113
|
function loadFingerprintBoost(fingerprint) {
|
|
106
114
|
if (!fingerprint) {
|
|
107
115
|
return { boost: 0, skip: false, state: null };
|
|
108
116
|
}
|
|
109
117
|
try {
|
|
110
118
|
const store = readJsonSafe(fingerprintStorePath(), { fingerprints: {} }) || { fingerprints: {} };
|
|
111
|
-
const entry = store.fingerprints?.[fingerprint];
|
|
119
|
+
const entry = store.fingerprints?.[fingerprint] || BASELINE_VERIFIED_FINGERPRINTS[fingerprint];
|
|
112
120
|
if (!entry) return { boost: 0, skip: false, state: null };
|
|
113
121
|
if (entry.state === 'deprecated') {
|
|
114
122
|
return { boost: 0, skip: true, state: 'deprecated' };
|
package/src/arc/manifest.cjs
CHANGED
|
@@ -322,11 +322,19 @@ function slimListItems(items, itemFields) {
|
|
|
322
322
|
});
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
+
function isModalOrFormPath(path) {
|
|
326
|
+
return (
|
|
327
|
+
/(?:^|\.)(?:form|modal|dialog|drawer|popup|sheet|booking|checkout|cartDrawer)(?:\.|$)/i.test(path) ||
|
|
328
|
+
/(?:Modal|Form|Drawer|Dialog|Booking)(?:\.|$)/.test(path)
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
325
332
|
function isAllowedControlOnly(path) {
|
|
326
333
|
return (
|
|
327
334
|
BASELINE_CONTROL_ONLY.test(path) ||
|
|
328
335
|
SYSTEM_FIELD.test(path) ||
|
|
329
|
-
PLATFORM_CONTROLLED_PATHS.has(wildcardPath(path))
|
|
336
|
+
PLATFORM_CONTROLLED_PATHS.has(wildcardPath(path)) ||
|
|
337
|
+
isModalOrFormPath(path)
|
|
330
338
|
);
|
|
331
339
|
}
|
|
332
340
|
|
|
@@ -335,10 +343,17 @@ function isAllowedControlOnly(path) {
|
|
|
335
343
|
* by the strict contract validator when their sub-paths appear in controlOnlyPaths.
|
|
336
344
|
* Template developers must never need to add these manually.
|
|
337
345
|
*/
|
|
338
|
-
function ensurePlatformSections(sections) {
|
|
346
|
+
function ensurePlatformSections(sections, content, boundListPaths = []) {
|
|
339
347
|
// additionalPages: rendered visually by PlatformAdditionalPages component.
|
|
340
348
|
// label is editable; id and route are platform-controlled.
|
|
341
|
-
if
|
|
349
|
+
// Only inject additionalPages if it is actually bound or present in content with items.
|
|
350
|
+
// Templates without additionalPages rendered would fail Fivora strict validation
|
|
351
|
+
// if an unrendered editable list is declared in editorSchema.sections.
|
|
352
|
+
const hasAdditionalPages =
|
|
353
|
+
(Array.isArray(boundListPaths) && boundListPaths.some((p) => p === 'additionalPages' || p.startsWith('additionalPages['))) ||
|
|
354
|
+
(content && Array.isArray(content.additionalPages) && content.additionalPages.length > 0 && Array.isArray(boundListPaths) && boundListPaths.includes('additionalPages'));
|
|
355
|
+
|
|
356
|
+
if (hasAdditionalPages && !sections.find((s) => s.id === 'additionalPages')) {
|
|
342
357
|
sections.push({
|
|
343
358
|
id: 'additionalPages',
|
|
344
359
|
path: 'additionalPages',
|
|
@@ -401,7 +416,8 @@ function computeControlOnlyPaths(content, boundPaths, declared = []) {
|
|
|
401
416
|
}
|
|
402
417
|
|
|
403
418
|
for (const path of inventory.concreteFields) {
|
|
404
|
-
|
|
419
|
+
const isModalForm = isModalOrFormPath(path);
|
|
420
|
+
if (bound.has(wildcardPath(path)) && !isModalForm) continue;
|
|
405
421
|
if (isAllowedControlOnly(path)) controlOnly.add(path);
|
|
406
422
|
}
|
|
407
423
|
|
|
@@ -578,7 +594,7 @@ function buildSiteDataAndManifest({
|
|
|
578
594
|
// Auto-inject platform-managed editorSchema sections if not already present.
|
|
579
595
|
// These sections are required so the validator accepts their controlOnlyPaths,
|
|
580
596
|
// but template developers should never need to add these manually.
|
|
581
|
-
ensurePlatformSections(editorSections);
|
|
597
|
+
ensurePlatformSections(editorSections, content, boundListPaths);
|
|
582
598
|
|
|
583
599
|
// controlOnlyPaths: platform paths are auto-merged by platform-contract.json at
|
|
584
600
|
// validate/build time. We only emit template-specific paths here (currently none
|
package/src/arc/residual.cjs
CHANGED
|
@@ -135,12 +135,23 @@ function classifyResidual(node, text, tag) {
|
|
|
135
135
|
return null;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
function hasEditableDescendant(node) {
|
|
139
|
+
if (!node || !Array.isArray(node.children)) return false;
|
|
140
|
+
for (const child of node.children) {
|
|
141
|
+
if (child.type === 'JSXElement') {
|
|
142
|
+
if (hasEditableMarker(child.openingElement || child)) return true;
|
|
143
|
+
if (hasEditableDescendant(child)) return true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
138
149
|
function ensureStaticOnLeaf(node, reason) {
|
|
139
150
|
const tag = getJsxName(node);
|
|
140
151
|
if (BROAD_CONTENT_CONTAINERS.has(tag.toLowerCase()) || BROAD_CONTENT_CONTAINERS.has(tag)) {
|
|
141
152
|
return false;
|
|
142
153
|
}
|
|
143
|
-
if (hasJsxAttribute(node, 'data-preview-static') || hasEditableMarker(node)) return false;
|
|
154
|
+
if (hasJsxAttribute(node, 'data-preview-static') || hasEditableMarker(node) || hasEditableDescendant(node)) return false;
|
|
144
155
|
node.openingElement.attributes.push(jsxStaticAttr(reason || 'decorative'));
|
|
145
156
|
return true;
|
|
146
157
|
}
|
package/src/arc/semantic.cjs
CHANGED
|
@@ -609,6 +609,22 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
|
|
|
609
609
|
return;
|
|
610
610
|
}
|
|
611
611
|
|
|
612
|
+
const isHashAnchor = Boolean(actionableHref && actionableHref.startsWith('#') && (!actionIntent || actionIntent.action === 'link'));
|
|
613
|
+
if (isHashAnchor && innerText && !textInfo.dynamic && !apiOwned) {
|
|
614
|
+
usedLocs.add(loc);
|
|
615
|
+
candidates.push({
|
|
616
|
+
...baseMeta,
|
|
617
|
+
kind: 'text',
|
|
618
|
+
operation: 'extract-text',
|
|
619
|
+
value: innerText,
|
|
620
|
+
confidence: confidenceFor('text'),
|
|
621
|
+
reason: 'in-page-anchor-label',
|
|
622
|
+
fingerprint: fingerprintCandidate({ tag: name, kind: 'text', text: innerText }),
|
|
623
|
+
});
|
|
624
|
+
this.traverse(pathNode);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
|
|
612
628
|
const isAction = !isSubmit && (Boolean(actionableHref) || Boolean(actionIntent)) && (ACTION_TAGS.has(name) || isLikelyCtaClass(className));
|
|
613
629
|
|
|
614
630
|
if (isAction && (actionableHref || actionIntent)) {
|