@deneb-ui/cli 2.0.85 → 2.0.87

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.
@@ -1819,3 +1819,1148 @@ test('M3: collectFontIdsFromSiteData resolves planned fonts and theme accurately
1819
1819
  assert.equal(siteDataEmpty.theme.bodyFont, 'Inter');
1820
1820
  });
1821
1821
 
1822
+ test('ARC v2: applyCollectionTransform threads previewItemPath and index to custom child components', () => {
1823
+ const { parseSource, printSource, locKey } = require('../ast.cjs');
1824
+ const { applyCollectionTransform } = require('../transformer.cjs');
1825
+
1826
+ const code = `
1827
+ export function ProductGrid() {
1828
+ const products = [
1829
+ { id: 1, name: "Arabica", price: 15, image: "/arabica.jpg" },
1830
+ { id: 2, name: "Espresso", price: 20, image: "/espresso.jpg" }
1831
+ ];
1832
+ return (
1833
+ <div className="grid grid-cols-2">
1834
+ {products.map((item) => (
1835
+ <ProductCard key={item.id} product={item} />
1836
+ ))}
1837
+ </div>
1838
+ );
1839
+ }
1840
+ `;
1841
+
1842
+ const ast = parseSource(code, 'ProductGrid.tsx');
1843
+ let mapLoc = null;
1844
+ const recast = require('recast');
1845
+ recast.types.visit(ast, {
1846
+ visitJSXElement(pathNode) {
1847
+ if (pathNode.node.openingElement.name.name === 'ProductCard') {
1848
+ mapLoc = locKey(pathNode.node);
1849
+ return false;
1850
+ }
1851
+ this.traverse(pathNode);
1852
+ },
1853
+ });
1854
+
1855
+ const transform = {
1856
+ listField: 'home.products',
1857
+ itemParam: 'item',
1858
+ loc: mapLoc,
1859
+ isPrimitiveArray: false,
1860
+ hasComponentRef: false,
1861
+ };
1862
+
1863
+ const changed = applyCollectionTransform(ast, transform, false, true);
1864
+ assert.ok(changed);
1865
+ const transformed = printSource(ast, code);
1866
+
1867
+ assert.match(transformed, /data-preview-list-path="home\.products"/);
1868
+ assert.match(transformed, /previewItemPath=\{`home\.products\[\$\{index\}\]`\}/);
1869
+ assert.match(transformed, /index=\{index\}/);
1870
+ assert.match(transformed, /data-preview-item-path=\{`home\.products\[\$\{index\}\]`\}/);
1871
+ });
1872
+
1873
+ test('ARC v2: instrumentChildCardComponent updates ProductCard props, TS interface and binds inner fields', () => {
1874
+ const { parseSource, printSource } = require('../ast.cjs');
1875
+ const { instrumentChildCardComponent } = require('../transformer.cjs');
1876
+
1877
+ const code = `
1878
+ interface ProductCardProps {
1879
+ product: {
1880
+ name: string;
1881
+ price: number;
1882
+ image: string;
1883
+ };
1884
+ }
1885
+
1886
+ export default function ProductCard({ product }: ProductCardProps) {
1887
+ return (
1888
+ <div className="card">
1889
+ <img src={product.image} alt={product.name} />
1890
+ <h3 className="title">{product.name}</h3>
1891
+ <span className="price">\${product.price}</span>
1892
+ </div>
1893
+ );
1894
+ }
1895
+ `;
1896
+
1897
+ const ast = parseSource(code, 'ProductCard.tsx');
1898
+ const transform = {
1899
+ componentName: 'ProductCard',
1900
+ listField: 'home.products',
1901
+ itemFields: [
1902
+ { key: 'name', type: 'text' },
1903
+ { key: 'price', type: 'number' },
1904
+ { key: 'image', type: 'image' },
1905
+ ],
1906
+ };
1907
+
1908
+ const changed = instrumentChildCardComponent(ast, transform, true);
1909
+ assert.ok(changed);
1910
+ const transformed = printSource(ast, code);
1911
+
1912
+ assert.match(transformed, /previewItemPath\?: string/);
1913
+ assert.match(transformed, /index\?: number/);
1914
+ assert.match(transformed, /previewItemPath/);
1915
+ assert.match(transformed, /data-preview-item-path=\{`home\.products\[\$\{index\}\]`\}/);
1916
+ assert.match(transformed, /data-preview-field-path=\{`home\.products\[\$\{index\}\]\.image`\}/);
1917
+ assert.match(transformed, /data-preview-field-path=\{`home\.products\[\$\{index\}\]\.name`\}/);
1918
+ assert.match(transformed, /data-preview-field-path=\{`home\.products\[\$\{index\}\]\.price`\}/);
1919
+ });
1920
+
1921
+ test('ARC v2: extract-tailwind-bg converts bg-[url(...)] into editable style and preview marker', () => {
1922
+ const { parseSource, printSource, locKey } = require('../ast.cjs');
1923
+ const { applyFilePlan } = require('../transformer.cjs');
1924
+
1925
+ const code = `
1926
+ export function Hero() {
1927
+ return (
1928
+ <section className="relative h-96 bg-[url('/hero-bg.jpg')] bg-cover">
1929
+ <h1>Hero Title</h1>
1930
+ </section>
1931
+ );
1932
+ }
1933
+ `;
1934
+
1935
+ const ast = parseSource(code, 'Hero.tsx');
1936
+ let secLoc = null;
1937
+ const recast = require('recast');
1938
+ recast.types.visit(ast, {
1939
+ visitJSXElement(pathNode) {
1940
+ if (pathNode.node.openingElement.name.name === 'section') {
1941
+ secLoc = locKey(pathNode.node);
1942
+ return false;
1943
+ }
1944
+ this.traverse(pathNode);
1945
+ },
1946
+ });
1947
+
1948
+ const filePlan = {
1949
+ file: 'Hero.tsx',
1950
+ originalCode: code,
1951
+ transformations: [
1952
+ {
1953
+ loc: secLoc,
1954
+ operation: 'extract-tailwind-bg',
1955
+ field: 'home.hero.backgroundImage',
1956
+ fallback: '/hero-bg.jpg',
1957
+ bgUrl: '/hero-bg.jpg',
1958
+ decision: 'auto',
1959
+ },
1960
+ ],
1961
+ };
1962
+
1963
+ const result = applyFilePlan(filePlan, { root: '/tmp' });
1964
+ assert.ok(result.changed);
1965
+ assert.match(result.code, /data-preview-field-path="home\.hero\.backgroundImage"/);
1966
+ assert.match(result.code, /backgroundImage:\s*`url\(\$\{siteData(?:\?\.\w+)+/);
1967
+ assert.doesNotMatch(result.code, /bg-\[url\('\/hero-bg\.jpg'\)\]/);
1968
+ });
1969
+
1970
+ test('ARC v2: ir-builder builds project IR and resolves public asset paths accurately', () => {
1971
+ const { resolvePublicAssetUrl, objectLiteralToPlain } = require('../ir-builder.cjs');
1972
+ const { parseSource } = require('../ast.cjs');
1973
+
1974
+ // Test objectLiteralToPlain
1975
+ const code = 'const obj = { id: 1, title: "Test", price: 29.99, featured: true };';
1976
+ const ast = parseSource(code, 'test.js');
1977
+ const plain = objectLiteralToPlain(ast.program.body[0].declarations[0].init);
1978
+ assert.deepEqual(plain, { id: 1, title: 'Test', price: 29.99, featured: true });
1979
+ });
1980
+
1981
+ test('ARC v2: planTransformations automatically links child card component via IR', () => {
1982
+ const { planTransformations } = require('../planner.cjs');
1983
+
1984
+ const profile = {
1985
+ components: [], // purposefully empty to test IR fallback
1986
+ };
1987
+
1988
+ const ir = {
1989
+ getComponent(name) {
1990
+ if (name === 'ProductCard') {
1991
+ return { name: 'ProductCard', file: 'src/components/ProductCard.tsx' };
1992
+ }
1993
+ return null;
1994
+ },
1995
+ };
1996
+
1997
+ const pageAnalysis = {
1998
+ relativeFile: 'src/app/page.tsx',
1999
+ candidates: [
2000
+ {
2001
+ file: 'src/app/page.tsx',
2002
+ loc: '10:5-10:40',
2003
+ kind: 'collection',
2004
+ operation: 'collection-conversion',
2005
+ confidence: 0.95,
2006
+ value: [{ value: { title: 'P1', price: 10 } }],
2007
+ extra: {
2008
+ itemsProp: 'products',
2009
+ childComponentName: 'ProductCard',
2010
+ objectItems: true,
2011
+ itemFields: [
2012
+ { key: 'title', type: 'text' },
2013
+ { key: 'price', type: 'number' },
2014
+ { key: 'image', type: 'image' },
2015
+ ],
2016
+ },
2017
+ },
2018
+ ],
2019
+ };
2020
+
2021
+ const cardAnalysis = {
2022
+ relativeFile: 'src/components/ProductCard.tsx',
2023
+ candidates: [],
2024
+ };
2025
+
2026
+ const plan = planTransformations({
2027
+ profile,
2028
+ analyses: [pageAnalysis, cardAnalysis],
2029
+ ir,
2030
+ });
2031
+
2032
+ const cardPlan = plan.files.find((f) => f.file === 'src/components/ProductCard.tsx');
2033
+ assert.ok(cardPlan, 'Child card file plan must exist');
2034
+ const childTransform = cardPlan.transformations.find((t) => t.operation === 'instrument-child-card');
2035
+ assert.ok(childTransform, 'Must plan instrument-child-card for child component');
2036
+ assert.equal(childTransform.componentName, 'ProductCard');
2037
+ assert.equal(childTransform.file, 'src/components/ProductCard.tsx');
2038
+ });
2039
+
2040
+ test('ARC v2: residual pass resolves static asset imports for image tags via IR', () => {
2041
+ const { applyResidualPass } = require('../residual.cjs');
2042
+
2043
+ const code = `
2044
+ import heroImg from '../assets/hero.png';
2045
+
2046
+ export function Hero() {
2047
+ return (
2048
+ <div>
2049
+ <img src={heroImg} alt="Hero banner" />
2050
+ </div>
2051
+ );
2052
+ }
2053
+ `;
2054
+
2055
+ const ir = {
2056
+ getAssetImport(file, id) {
2057
+ if (id === 'heroImg') return '/hero.png';
2058
+ return null;
2059
+ },
2060
+ };
2061
+
2062
+ const result = applyResidualPass({
2063
+ code,
2064
+ file: 'src/components/Hero.tsx',
2065
+ ownerScope: 'home',
2066
+ usedPaths: new Set(),
2067
+ componentName: 'Hero',
2068
+ ir,
2069
+ });
2070
+
2071
+ assert.ok(result.changed, 'Residual pass should change code with resolved asset');
2072
+ assert.match(result.code, /data-preview-field-path="home\.hero\.(?:image|hero)"/);
2073
+ assert.match(result.code, /siteData(?:\?\.\w+)+/);
2074
+ assert.ok(result.fields.some((f) => f.type === 'image' && f.value === '/hero.png'));
2075
+ });
2076
+
2077
+ test('ARC v2: printer prints Deneb Editability Scorecard with all metrics', () => {
2078
+ const printer = require('../printer.cjs');
2079
+ let output = '';
2080
+ const origLog = console.log;
2081
+ console.log = (...args) => {
2082
+ output += args.join(' ') + '\n';
2083
+ };
2084
+
2085
+ try {
2086
+ printer.printValidation(
2087
+ { syntaxPassed: true, contractPassed: true, fivoraContractPassed: true },
2088
+ { visualCoverage: 95.5, visualCovered: 42, visualRequired: 44, uncoveredVisibleText: 0 },
2089
+ { score: 100 }
2090
+ );
2091
+ } finally {
2092
+ console.log = origLog;
2093
+ }
2094
+
2095
+ assert.match(output, /Deneb Editability Scorecard/);
2096
+ assert.match(output, /Contract Validity:\s+.*100%/);
2097
+ assert.match(output, /Editability Coverage:\s+.*95\.5%/);
2098
+ assert.match(output, /Visual Elements:\s+42\/44 bound/);
2099
+ assert.match(output, /Design Preservation:\s+100%/);
2100
+ });
2101
+
2102
+ test('ARC v2 Phase 2: prop-flow analyzer links parent props to child SectionHeader previewPath and binds inner headings', () => {
2103
+ const { applyFilePlan } = require('../transformer.cjs');
2104
+ const parentCode = `
2105
+ import { SectionHeader } from './SectionHeader';
2106
+ export function Story() {
2107
+ return (
2108
+ <section>
2109
+ <SectionHeader title="Our Story" subtitle="Freshly roasted every day" />
2110
+ </section>
2111
+ );
2112
+ }
2113
+ `;
2114
+ const parentPlan = {
2115
+ file: 'Story.tsx',
2116
+ originalCode: parentCode,
2117
+ transformations: [
2118
+ {
2119
+ operation: 'prop-flow-callsite',
2120
+ componentName: 'SectionHeader',
2121
+ previewPath: 'home.story',
2122
+ propTransforms: {
2123
+ title: { field: 'home.story.title', fallback: 'Our Story', type: 'text' },
2124
+ subtitle: { field: 'home.story.subtitle', fallback: 'Freshly roasted every day', type: 'text' },
2125
+ },
2126
+ decision: 'auto',
2127
+ },
2128
+ ],
2129
+ };
2130
+ const parentRes = applyFilePlan(parentPlan, { root: '/tmp' });
2131
+ assert.ok(parentRes.changed);
2132
+ assert.match(parentRes.code, /previewPath="home\.story"/);
2133
+ assert.match(parentRes.code, /title=\{siteData\?\.content\?\.home\?\.story\?\.title \?\? "Our Story"\}/);
2134
+ assert.match(parentRes.code, /subtitle=\{siteData\?\.content\?\.home\?\.story\?\.subtitle \?\? "Freshly roasted every day"\}/);
2135
+
2136
+ const childCode = `
2137
+ export interface SectionHeaderProps {
2138
+ title: string;
2139
+ subtitle: string;
2140
+ }
2141
+ export function SectionHeader({ title, subtitle }: SectionHeaderProps) {
2142
+ return (
2143
+ <div className="header">
2144
+ <h2>{title}</h2>
2145
+ <p>{subtitle}</p>
2146
+ </div>
2147
+ );
2148
+ }
2149
+ `;
2150
+ const childPlan = {
2151
+ file: 'SectionHeader.tsx',
2152
+ originalCode: childCode,
2153
+ transformations: [
2154
+ {
2155
+ operation: 'instrument-reusable-component',
2156
+ componentName: 'SectionHeader',
2157
+ propTransforms: {
2158
+ title: { field: 'home.story.title', fallback: 'Our Story', type: 'text' },
2159
+ subtitle: { field: 'home.story.subtitle', fallback: 'Freshly roasted every day', type: 'text' },
2160
+ },
2161
+ decision: 'auto',
2162
+ },
2163
+ ],
2164
+ };
2165
+ const childRes = applyFilePlan(childPlan, { root: '/tmp' });
2166
+ assert.ok(childRes.changed);
2167
+ assert.match(childRes.code, /previewPath\?: string/);
2168
+ assert.match(childRes.code, /previewPath/);
2169
+ assert.match(childRes.code, /data-preview-field-path=\{previewPath \? `\$\{previewPath\}\.title` : undefined\}/);
2170
+ assert.match(childRes.code, /data-preview-field-path=\{previewPath \? `\$\{previewPath\}\.subtitle` : undefined\}/);
2171
+ });
2172
+
2173
+ test('ARC v2 Phase 2: compound text analyzer binds button with icon while preserving icon JSX', () => {
2174
+ const { applyFilePlan } = require('../transformer.cjs');
2175
+ const code = `
2176
+ import { Coffee } from 'lucide-react';
2177
+ export function OrderButton() {
2178
+ return (
2179
+ <button className="btn">
2180
+ <Coffee className="w-4 h-4" />
2181
+ Order Now
2182
+ </button>
2183
+ );
2184
+ }
2185
+ `;
2186
+ const plan = {
2187
+ file: 'OrderButton.tsx',
2188
+ originalCode: code,
2189
+ transformations: [
2190
+ {
2191
+ loc: '5:8',
2192
+ tag: 'button',
2193
+ operation: 'bind-button-with-icon',
2194
+ field: 'home.hero.orderLabel',
2195
+ fallback: 'Order Now',
2196
+ fieldType: 'text',
2197
+ decision: 'auto',
2198
+ },
2199
+ ],
2200
+ };
2201
+ const res = applyFilePlan(plan, { root: '/tmp' });
2202
+ assert.ok(res.changed);
2203
+ assert.match(res.code, /<Coffee className="w-4 h-4" \/>/);
2204
+ assert.match(res.code, /data-preview-field-path="home\.hero\.orderLabel"/);
2205
+ assert.match(res.code, /\{siteData\?\.content\?\.home\?\.hero\?\.orderLabel \?\? "Order Now"\}/);
2206
+ });
2207
+
2208
+ test('ARC v2 Phase 2: compound text analyzer binds highlighted heading without breaking span styling', () => {
2209
+ const { applyFilePlan } = require('../transformer.cjs');
2210
+ const code = `
2211
+ export function Heading() {
2212
+ return (
2213
+ <h1 className="text-4xl font-bold">
2214
+ Crafted with <span className="text-amber-500">Passion</span>
2215
+ </h1>
2216
+ );
2217
+ }
2218
+ `;
2219
+ const plan = {
2220
+ file: 'Heading.tsx',
2221
+ originalCode: code,
2222
+ transformations: [
2223
+ {
2224
+ loc: '4:8',
2225
+ tag: 'h1',
2226
+ operation: 'bind-highlighted-heading',
2227
+ field: 'home.hero.title',
2228
+ fallback: 'Crafted with Passion',
2229
+ fieldType: 'text',
2230
+ decision: 'auto',
2231
+ },
2232
+ ],
2233
+ };
2234
+ const res = applyFilePlan(plan, { root: '/tmp' });
2235
+ assert.ok(res.changed);
2236
+ assert.match(res.code, /data-preview-field-path="home\.hero\.title"/);
2237
+ assert.match(res.code, /<span\s+[^>]*className="text-amber-500"[^>]*data-preview-field-path="home\.hero\.titleHighlight"/);
2238
+ assert.match(res.code, /siteData\?\.content\?\.home\?\.hero\?\.titleHighlight \?\? "Passion"/);
2239
+ });
2240
+
2241
+ test('ARC v2 Phase 2: data flow engine unrolls chained collections (.slice().map()) and variable aliases', () => {
2242
+ const { unrollCollectionPipelines, resolveVariableAliases } = require('../data-flow.cjs');
2243
+ const parseSource = (c) => require('recast').parse(c, { parser: require('recast/parsers/babel-ts') });
2244
+
2245
+ const code = `
2246
+ const rawProducts = [{ id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }];
2247
+ const featured = rawProducts.slice(0, 4);
2248
+ export function Grid() {
2249
+ return <div>{featured.map((p) => <span>{p.name}</span>)}</div>;
2250
+ }
2251
+ `;
2252
+ const ast = parseSource(code);
2253
+ const aliases = resolveVariableAliases(ast);
2254
+ assert.ok(aliases.has('featured'));
2255
+ assert.equal(aliases.get('featured').rootIdentifier, 'rawProducts');
2256
+ assert.equal(aliases.get('featured').transformMethod, 'slice');
2257
+
2258
+ const chainedCode = `
2259
+ export function Chained() {
2260
+ return <div>{products.filter(p => p.active).slice(0, 3).map(p => <span>{p.title}</span>)}</div>;
2261
+ }
2262
+ `;
2263
+ const chainedAst = parseSource(chainedCode);
2264
+ const pipelines = unrollCollectionPipelines(chainedAst);
2265
+ assert.equal(pipelines.length, 1);
2266
+ assert.equal(pipelines[0].rootIdentifier, 'products');
2267
+ assert.deepEqual(pipelines[0].pipelineChain, ['filter', 'slice']);
2268
+ });
2269
+
2270
+ test('ARC v2 Phase 2: full conversion generates deneb-conversion-report.json with unresolved and category breakdown', () => {
2271
+ const dir = copyOf(STOREFRONT_FIXTURE);
2272
+ silence(() => runDenebArc(dir, 'acme-store', { telemetry: 'off' }));
2273
+ const reportPath = path.join(dir, 'deneb-conversion-report.json');
2274
+ assert.ok(fs.existsSync(reportPath), 'deneb-conversion-report.json should exist');
2275
+ const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
2276
+ assert.equal(report.scorecard.contractValidity, '100%');
2277
+ assert.ok(report.categories);
2278
+ assert.ok(typeof report.categories.text.planned === 'number');
2279
+ assert.ok(typeof report.categories.collections.planned === 'number');
2280
+ assert.ok(Array.isArray(report.unresolved));
2281
+ });
2282
+
2283
+ test('ARC v2 Phase 2: multi-run idempotency preserves clean interfaces without duplicating previewPath or imports', () => {
2284
+ const dir = copyOf(STOREFRONT_FIXTURE);
2285
+ silence(() => runDenebArc(dir, 'acme-store', { telemetry: 'off' }));
2286
+ silence(() => runDenebArc(dir, 'acme-store', { telemetry: 'off' }));
2287
+ silence(() => runDenebArc(dir, 'acme-store', { telemetry: 'off' }));
2288
+ const grid = fs.readFileSync(path.join(dir, 'src', 'components', 'ProductGrid.tsx'), 'utf8');
2289
+ const useSiteDataMatches = grid.match(/import\s*\{\s*useSiteData\s*\}\s*from/g) || [];
2290
+ assert.equal(useSiteDataMatches.length, 1);
2291
+ const listMatches = grid.match(/data-preview-list-path="home\.products"/g) || [];
2292
+ assert.equal(listMatches.length, 1);
2293
+ });
2294
+
2295
+ test('ARC v2 Phase 3: validateRuntimeEditability verifies live editability contracts on converted project', async () => {
2296
+ const { validateRuntimeEditability, validateRuntimeEditabilitySync } = require('../runtime-validator.cjs');
2297
+ const dir = copyOf(STOREFRONT_FIXTURE);
2298
+ silence(() => runDenebArc(dir, 'acme-store', { telemetry: 'off' }));
2299
+
2300
+ const siteDataPath = path.join(dir, 'src', 'data', 'site-data.json');
2301
+ const manifestPath = path.join(dir, 'fivora-template.json');
2302
+ const siteData = JSON.parse(fs.readFileSync(siteDataPath, 'utf8'));
2303
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
2304
+
2305
+ const syncRes = validateRuntimeEditabilitySync({
2306
+ projectDir: dir,
2307
+ siteData,
2308
+ manifest,
2309
+ });
2310
+ assert.equal(syncRes.passed, true);
2311
+ assert.ok(syncRes.runtimeEditabilityScore >= 95, `Expected score >= 95, got ${syncRes.runtimeEditabilityScore}`);
2312
+ assert.ok(syncRes.collectionResults.length > 0);
2313
+ assert.ok(syncRes.collectionResults.every((c) => c.passed));
2314
+
2315
+ const asyncRes = await validateRuntimeEditability({
2316
+ projectDir: dir,
2317
+ siteData,
2318
+ manifest,
2319
+ });
2320
+ assert.equal(asyncRes.passed, true);
2321
+ assert.equal(asyncRes.runtimeEditabilityScore, syncRes.runtimeEditabilityScore);
2322
+ });
2323
+
2324
+ test('ARC v2 Phase 3: CanonicalFieldRef correctly generates getter AST, preview attribute, and nested item paths', () => {
2325
+ const { fieldRef, isValidCanonicalPath, validatePathAlignment } = require('../canonical-paths.cjs');
2326
+ const recast = require('recast');
2327
+
2328
+ const ref = fieldRef('home.story.title');
2329
+ assert.equal(ref.scope, 'home');
2330
+ assert.equal(ref.section, 'story');
2331
+ assert.equal(ref.fieldName, 'title');
2332
+
2333
+ const getterCode = recast.print(ref.toGetterAst()).code;
2334
+ assert.equal(getterCode, 'siteData?.content?.home?.story?.title');
2335
+
2336
+ const bindingCode = recast.print(ref.toBindingAst('Default Story')).code;
2337
+ assert.equal(bindingCode, 'siteData?.content?.home?.story?.title ?? "Default Story"');
2338
+
2339
+ const previewAttrCode = recast.print(ref.toPreviewAttrAst()).code;
2340
+ assert.equal(previewAttrCode, 'data-preview-field-path="home.story.title"');
2341
+
2342
+ assert.equal(isValidCanonicalPath('home.story.title'), true);
2343
+ assert.equal(isValidCanonicalPath('home.products[0].name'), true);
2344
+ assert.equal(isValidCanonicalPath(''), false);
2345
+
2346
+ assert.equal(
2347
+ validatePathAlignment('siteData?.content?.home?.story?.title', '<h2 data-preview-field-path="home.story.title">'),
2348
+ true
2349
+ );
2350
+ assert.equal(
2351
+ validatePathAlignment('siteData?.content?.home?.story?.title', '<h2 data-preview-field-path="home.other.title">'),
2352
+ false
2353
+ );
2354
+ });
2355
+
2356
+ test('ARC v2 Phase 3: Adapters recognize carousel, accordion, tabs, dialog, and gallery components', () => {
2357
+ const { activeAdapters, recognizeWithAdapters } = require('../adapters.cjs');
2358
+ const adapters = activeAdapters({
2359
+ framework: 'nextjs',
2360
+ dependencies: {
2361
+ 'swiper': '^11.0.0',
2362
+ 'embla-carousel-react': '^8.0.0',
2363
+ },
2364
+ });
2365
+
2366
+ const makeJsxNode = (name) => ({
2367
+ type: 'JSXElement',
2368
+ openingElement: {
2369
+ type: 'JSXOpeningElement',
2370
+ name: { type: 'JSXIdentifier', name },
2371
+ attributes: [],
2372
+ },
2373
+ children: [],
2374
+ });
2375
+
2376
+ // Swiper
2377
+ const swiperRes = recognizeWithAdapters(makeJsxNode('Swiper'), {}, adapters);
2378
+ assert.equal(swiperRes.library, 'swiper');
2379
+ assert.equal(swiperRes.role, 'carousel-container');
2380
+
2381
+ const swiperSlideRes = recognizeWithAdapters(makeJsxNode('SwiperSlide'), {}, adapters);
2382
+ assert.equal(swiperSlideRes.library, 'swiper');
2383
+ assert.equal(swiperSlideRes.role, 'carousel-slide');
2384
+
2385
+ // Embla
2386
+ const emblaRes = recognizeWithAdapters(makeJsxNode('Carousel'), {}, adapters);
2387
+ assert.equal(emblaRes.library, 'embla');
2388
+ assert.equal(emblaRes.role, 'carousel-container');
2389
+
2390
+ // Accordion
2391
+ const accordionRes = recognizeWithAdapters(makeJsxNode('Accordion'), {}, adapters);
2392
+ assert.equal(accordionRes.library, 'accordion');
2393
+ assert.equal(accordionRes.role, 'accordion-container');
2394
+
2395
+ const accordionTriggerRes = recognizeWithAdapters(makeJsxNode('AccordionTrigger'), {}, adapters);
2396
+ assert.equal(accordionTriggerRes.library, 'accordion');
2397
+ assert.equal(accordionTriggerRes.role, 'accordion-header');
2398
+
2399
+ // Tabs
2400
+ const tabsRes = recognizeWithAdapters(makeJsxNode('Tabs'), {}, adapters);
2401
+ assert.equal(tabsRes.library, 'tabs');
2402
+ assert.equal(tabsRes.role, 'tabs-container');
2403
+
2404
+ // Dialog
2405
+ const dialogRes = recognizeWithAdapters(makeJsxNode('DialogTitle'), {}, adapters);
2406
+ assert.equal(dialogRes.library, 'dialog');
2407
+ assert.equal(dialogRes.role, 'dialog-title');
2408
+
2409
+ // Gallery
2410
+ const galleryRes = recognizeWithAdapters(makeJsxNode('Masonry'), {}, adapters);
2411
+ assert.equal(galleryRes.library, 'gallery');
2412
+ assert.equal(galleryRes.role, 'gallery-container');
2413
+ });
2414
+
2415
+ test('ARC v2 Phase 3: conversion report includes runtime verification scorecard and metrics', () => {
2416
+ const dir = copyOf(STOREFRONT_FIXTURE);
2417
+ silence(() => runDenebArc(dir, 'acme-store', { telemetry: 'off' }));
2418
+ const reportPath = path.join(dir, 'deneb-conversion-report.json');
2419
+ assert.ok(fs.existsSync(reportPath));
2420
+ const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
2421
+ assert.ok(report.scorecard.runtimeEditability);
2422
+ assert.match(report.scorecard.runtimeEditability, /100%/);
2423
+ assert.ok(report.runtimeVerification);
2424
+ assert.equal(report.runtimeVerification.passed, true);
2425
+ assert.ok(report.runtimeVerification.verifiedCount > 0);
2426
+ });
2427
+
2428
+ test('ARC v2 Phase 4: RSC boundary intelligence detects metadata exports and client directives accurately', () => {
2429
+ const { hasMetadataExport, hasClientDirective, canSafelyInjectClientDirective, ensureClientDirective } = require('../rsc-boundary.cjs');
2430
+ const parseSource = (c) => require('recast').parse(c, { parser: require('recast/parsers/babel-ts') });
2431
+
2432
+ const metadataCode = `
2433
+ export const metadata = { title: 'My Store', description: 'Best products' };
2434
+ export default function Page() { return <h1>Store</h1>; }
2435
+ `;
2436
+ const metaAst = parseSource(metadataCode);
2437
+ assert.equal(hasMetadataExport(metaAst), true);
2438
+ assert.equal(hasClientDirective(metaAst), false);
2439
+ assert.equal(canSafelyInjectClientDirective(metaAst, 'src/app/page.tsx', { router: 'next-app' }), false);
2440
+
2441
+ const clientCode = `
2442
+ 'use client';
2443
+ import { useState } from 'react';
2444
+ export default function Interactive() { const [c, setC] = useState(0); return <button onClick={() => setC(c + 1)}>{c}</button>; }
2445
+ `;
2446
+ const clientAst = parseSource(clientCode);
2447
+ assert.equal(hasMetadataExport(clientAst), false);
2448
+ assert.equal(hasClientDirective(clientAst), true);
2449
+ assert.equal(canSafelyInjectClientDirective(clientAst, 'src/components/Interactive.tsx', { router: 'next-app' }), true);
2450
+
2451
+ const serverCode = `
2452
+ export default function PureServer() { return <p>Server rendered</p>; }
2453
+ `;
2454
+ const serverAst = parseSource(serverCode);
2455
+ assert.equal(hasMetadataExport(serverAst), false);
2456
+ assert.equal(hasClientDirective(serverAst), false);
2457
+ assert.equal(canSafelyInjectClientDirective(serverAst, 'src/app/about/page.tsx', { router: 'next-app' }), true);
2458
+
2459
+ ensureClientDirective(serverAst);
2460
+ assert.equal(hasClientDirective(serverAst), true);
2461
+ });
2462
+
2463
+ test('ARC v2 Phase 4: semantic engine protects dynamic commerce state and mixed expressions from static replacement', () => {
2464
+ const { resolveChildText } = require('../semantic.cjs');
2465
+ const parseSource = (c) => require('recast').parse(c, { parser: require('recast/parsers/babel-ts') });
2466
+
2467
+ const makeJsxNode = (code) => {
2468
+ const ast = parseSource(code);
2469
+ return ast.program.body[0].expression;
2470
+ };
2471
+
2472
+ // Mixed text with dynamic expression: <p>{cart.items.length} items in your cart</p>
2473
+ const cartNode = makeJsxNode('<p>{cart.items.length} items in your cart</p>');
2474
+ const cartText = resolveChildText(cartNode, new Map());
2475
+ assert.equal(cartText.dynamic, true);
2476
+ assert.equal(cartText.text, '');
2477
+
2478
+ // User session state: <span>Welcome, {user?.name}</span>
2479
+ const userNode = makeJsxNode('<span>Welcome, {user?.name}</span>');
2480
+ const userText = resolveChildText(userNode, new Map());
2481
+ assert.equal(userText.dynamic, true);
2482
+ assert.equal(userText.text, '');
2483
+
2484
+ // Dynamic price calculation: <div>{formatPrice(item.price * qty)}</div>
2485
+ const priceNode = makeJsxNode('<div>{formatPrice(item.price * qty)}</div>');
2486
+ const priceText = resolveChildText(priceNode, new Map());
2487
+ assert.equal(priceText.dynamic, true);
2488
+ assert.equal(priceText.text, '');
2489
+
2490
+ // Pure static text: <h1>Crafted with Passion</h1>
2491
+ const staticNode = makeJsxNode('<h1>Crafted with Passion</h1>');
2492
+ const staticText = resolveChildText(staticNode, new Map());
2493
+ assert.equal(staticText.dynamic, false);
2494
+ assert.equal(staticText.text, 'Crafted with Passion');
2495
+ });
2496
+
2497
+ test('ARC v2 Phase 4: instrumentReusableComponent supports identifier props (props: HeaderProps) and body destructuring', () => {
2498
+ const { applyFilePlan } = require('../transformer.cjs');
2499
+ const code = `
2500
+ interface SectionHeaderProps {
2501
+ title: string;
2502
+ subtitle: string;
2503
+ }
2504
+ export function SectionHeader(props: SectionHeaderProps) {
2505
+ const { title, subtitle } = props;
2506
+ return (
2507
+ <div className="section-header">
2508
+ <h2>{title}</h2>
2509
+ <p>{subtitle}</p>
2510
+ </div>
2511
+ );
2512
+ }
2513
+ `;
2514
+ const plan = {
2515
+ file: 'SectionHeader.tsx',
2516
+ originalCode: code,
2517
+ transformations: [
2518
+ {
2519
+ operation: 'instrument-reusable-component',
2520
+ componentName: 'SectionHeader',
2521
+ propTransforms: {
2522
+ title: { field: 'home.story.title', fallback: 'Our Story', type: 'text' },
2523
+ subtitle: { field: 'home.story.subtitle', fallback: 'Freshly roasted every day', type: 'text' },
2524
+ },
2525
+ decision: 'auto',
2526
+ },
2527
+ ],
2528
+ };
2529
+
2530
+ const res = applyFilePlan(plan, { root: '/tmp' });
2531
+ assert.ok(res.changed);
2532
+ // Interface updated with previewPath
2533
+ assert.match(res.code, /previewPath\?: string/);
2534
+ // Body destructuring injected with previewPath
2535
+ assert.match(res.code, /const \{\s*title,\s*subtitle,\s*previewPath\s*\} = props;/);
2536
+ // DOM elements stamped with dynamic path
2537
+ assert.match(res.code, /data-preview-field-path=\{previewPath \? `\$\{previewPath\}\.title` : undefined\}/);
2538
+ assert.match(res.code, /data-preview-field-path=\{previewPath \? `\$\{previewPath\}\.subtitle` : undefined\}/);
2539
+ });
2540
+
2541
+ test('ARC v2 Phase 4: instrumentChildCardComponent supports identifier props and body destructuring', () => {
2542
+ const { applyFilePlan } = require('../transformer.cjs');
2543
+ const code = `
2544
+ interface ProductCardProps {
2545
+ name: string;
2546
+ price: string;
2547
+ image: string;
2548
+ }
2549
+ export function ProductCard(props: ProductCardProps) {
2550
+ const { name, price, image } = props;
2551
+ return (
2552
+ <div className="card">
2553
+ <img src={image} alt={name} />
2554
+ <h3>{name}</h3>
2555
+ <span>{price}</span>
2556
+ </div>
2557
+ );
2558
+ }
2559
+ `;
2560
+ const plan = {
2561
+ file: 'ProductCard.tsx',
2562
+ originalCode: code,
2563
+ transformations: [
2564
+ {
2565
+ operation: 'instrument-child-card',
2566
+ componentName: 'ProductCard',
2567
+ itemFields: [
2568
+ { key: 'name', type: 'text' },
2569
+ { key: 'price', type: 'text' },
2570
+ { key: 'image', type: 'image' },
2571
+ ],
2572
+ decision: 'auto',
2573
+ },
2574
+ ],
2575
+ };
2576
+
2577
+ const res = applyFilePlan(plan, { root: '/tmp' });
2578
+ assert.ok(res.changed);
2579
+ // Interface updated with previewItemPath and index
2580
+ assert.match(res.code, /previewItemPath\?: string/);
2581
+ assert.match(res.code, /index\?: number/);
2582
+ // Body destructuring receives previewItemPath and index
2583
+ assert.match(res.code, /previewItemPath/);
2584
+ assert.match(res.code, /index/);
2585
+ });
2586
+
2587
+ test('ARC v2 Phase 4: healEmptyStateConditionals preserves interactive modals, drawers, and tab states', () => {
2588
+ const { applyFilePlan } = require('../transformer.cjs');
2589
+ const code = `
2590
+ export function Catalog() {
2591
+ return (
2592
+ <div>
2593
+ {isModalOpen && (
2594
+ <div role="dialog" className="modal fixed inset-0 z-50">
2595
+ <h2>Quick View Modal</h2>
2596
+ </div>
2597
+ )}
2598
+ {activeTab === 'details' ? (
2599
+ <div className="tab-pane">
2600
+ <p>Product Specifications</p>
2601
+ </div>
2602
+ ) : null}
2603
+ </div>
2604
+ );
2605
+ }
2606
+ `;
2607
+ const plan = {
2608
+ file: 'Catalog.tsx',
2609
+ originalCode: code,
2610
+ transformations: [
2611
+ {
2612
+ loc: '7:14',
2613
+ tag: 'h2',
2614
+ operation: 'extract-text',
2615
+ field: 'home.modal.title',
2616
+ fallback: 'Quick View Modal',
2617
+ fieldType: 'text',
2618
+ decision: 'auto',
2619
+ },
2620
+ ],
2621
+ };
2622
+
2623
+ const res = applyFilePlan(plan, { root: '/tmp' });
2624
+ assert.ok(res.changed);
2625
+ // Conditional modal guard preserved
2626
+ assert.match(res.code, /isModalOpen &&/);
2627
+ // Tab conditional preserved
2628
+ assert.match(res.code, /activeTab === 'details' \?/);
2629
+ });
2630
+
2631
+ test('ARC v2 Phase 5: Pluggable Adapter Registry supports custom adapter registration and lifecycle hooks', () => {
2632
+ const { AdapterRegistry, defaultRegistry } = require('../adapters/registry.cjs');
2633
+ const customRegistry = new AdapterRegistry();
2634
+
2635
+ const customVideoAdapter = {
2636
+ id: 'custom-video',
2637
+ detect: (project) => Boolean(project.hasVideoPlayer),
2638
+ recognizeNode(node) {
2639
+ const name = node.openingElement?.name?.name;
2640
+ if (name === 'VideoPlayer' || name === 'BackgroundVideo') {
2641
+ return { library: 'custom-video', kind: 'media', role: 'video-container', tag: name };
2642
+ }
2643
+ return null;
2644
+ },
2645
+ resolveAction(node) {
2646
+ return null;
2647
+ },
2648
+ };
2649
+
2650
+ customRegistry.register(customVideoAdapter);
2651
+ assert.ok(customRegistry.get('custom-video'), 'Must find registered custom adapter');
2652
+ assert.equal(customRegistry.getAll().length, 1);
2653
+
2654
+ // Inactive when project hasVideoPlayer is false
2655
+ assert.equal(customRegistry.getActive({ hasVideoPlayer: false }).length, 0);
2656
+
2657
+ // Active when project hasVideoPlayer is true
2658
+ const active = customRegistry.getActive({ hasVideoPlayer: true });
2659
+ assert.equal(active.length, 1);
2660
+ assert.equal(active[0].id, 'custom-video');
2661
+
2662
+ // Verify node recognition
2663
+ const fakeNode = {
2664
+ openingElement: { name: { name: 'VideoPlayer' } },
2665
+ };
2666
+ const recognized = customRegistry.recognizeNode(fakeNode, { profile: { hasVideoPlayer: true } });
2667
+ assert.ok(recognized);
2668
+ assert.equal(recognized.adapterId, 'custom-video');
2669
+ assert.equal(recognized.role, 'video-container');
2670
+
2671
+ // Verify default global registry contains all built-in adapters
2672
+ const allDefault = defaultRegistry.getAll();
2673
+ const ids = allDefault.map((a) => a.id);
2674
+ assert.ok(ids.includes('swiper'), 'Must include swiper');
2675
+ assert.ok(ids.includes('embla'), 'Must include embla');
2676
+ assert.ok(ids.includes('slick'), 'Must include slick');
2677
+ assert.ok(ids.includes('accordion'), 'Must include accordion');
2678
+ assert.ok(ids.includes('tabs'), 'Must include tabs');
2679
+ assert.ok(ids.includes('dialog'), 'Must include dialog');
2680
+ assert.ok(ids.includes('gallery'), 'Must include gallery');
2681
+ assert.ok(ids.includes('picture-source'), 'Must include picture-source');
2682
+ });
2683
+
2684
+ test('ARC v2 Phase 5: Responsive media adapter recognizes <picture> and ignores decorative <source> tags', () => {
2685
+ const { analyzeFile } = require('../semantic.cjs');
2686
+ const code = `
2687
+ export function HeroBanner() {
2688
+ return (
2689
+ <section className="hero">
2690
+ <picture>
2691
+ <source media="(min-width: 1024px)" srcSet="/banner-desktop.jpg" />
2692
+ <source media="(min-width: 640px)" srcSet="/banner-tablet.jpg" />
2693
+ <img src="/banner-mobile.jpg" alt="Summer Collection" />
2694
+ </picture>
2695
+ </section>
2696
+ );
2697
+ }
2698
+ `;
2699
+
2700
+ const analysis = analyzeFile({
2701
+ relativeFile: 'HeroBanner.tsx',
2702
+ code,
2703
+ profile: {
2704
+ framework: 'nextjs',
2705
+ router: 'next-app',
2706
+ },
2707
+ });
2708
+
2709
+ // Verify <source> tags are handled gracefully as picture-source-child
2710
+ const sourceCandidates = analysis.candidates.filter((c) => c.tag === 'source');
2711
+ assert.ok(sourceCandidates.length > 0, 'Source tags must be recorded');
2712
+ for (const s of sourceCandidates) {
2713
+ assert.equal(s.reason, 'picture-source-child');
2714
+ assert.equal(s.skip, true);
2715
+ }
2716
+
2717
+ // Verify fallback <img> or <picture> is extracted with the mobile src fallback
2718
+ const imgCandidate = analysis.candidates.find((c) => c.kind === 'image' && !c.skip);
2719
+ assert.ok(imgCandidate, 'Must extract image candidate from picture/img');
2720
+ assert.equal(imgCandidate.value, '/banner-mobile.jpg');
2721
+ });
2722
+
2723
+ test('ARC v2 Phase 5: Transformer binds <picture> fallback <img> and stamps preview field path', () => {
2724
+ const { applyFilePlan } = require('../transformer.cjs');
2725
+ const code = `
2726
+ export function HeroBanner() {
2727
+ return (
2728
+ <section className="hero">
2729
+ <picture>
2730
+ <source media="(min-width: 1024px)" srcSet="/banner-desktop.jpg" />
2731
+ <img src="/banner-mobile.jpg" alt="Summer Collection" />
2732
+ </picture>
2733
+ </section>
2734
+ );
2735
+ }
2736
+ `;
2737
+
2738
+ const plan = {
2739
+ file: 'HeroBanner.tsx',
2740
+ originalCode: code,
2741
+ transformations: [
2742
+ {
2743
+ loc: '5:10:8:20',
2744
+ tag: 'picture',
2745
+ operation: 'extract-picture',
2746
+ field: 'home.hero.banner',
2747
+ fallback: '/banner-mobile.jpg',
2748
+ fieldType: 'image',
2749
+ decision: 'auto',
2750
+ },
2751
+ ],
2752
+ };
2753
+
2754
+ const res = applyFilePlan(plan, { root: '/tmp' });
2755
+ assert.ok(res.changed);
2756
+ // Both picture and inner img receive preview attributes
2757
+ assert.match(res.code, /<picture[^>]*data-preview-field-path="home\.hero\.banner"/);
2758
+ assert.match(res.code, /<img[^>]*data-preview-field-path="home\.hero\.banner"/);
2759
+ // Fallback img receives dynamic siteData binding
2760
+ assert.match(res.code, /src=\{siteData\?\.content\?\.home\?\.hero\?\.banner (\?\?|\|\|) "\/banner-mobile\.jpg"\}/);
2761
+ });
2762
+
2763
+ test('ARC v2 Phase 5: Carousel adapters identify containers and slide items across Swiper, Embla, and Slick', () => {
2764
+ const { recognizeWithAdapters, defaultRegistry } = require('../adapters/registry.cjs');
2765
+ const parseSource = (c) => require('recast').parse(c, { parser: require('recast/parsers/babel-ts') });
2766
+ const makeNode = (code) => parseSource(code).program.body[0].expression;
2767
+
2768
+ const activeAdapters = defaultRegistry.getAll();
2769
+
2770
+ // Swiper
2771
+ const swiperContainer = makeNode('<Swiper autoplay={true}><SwiperSlide>Slide 1</SwiperSlide></Swiper>');
2772
+ const swiperRec = recognizeWithAdapters(swiperContainer, {}, activeAdapters);
2773
+ assert.equal(swiperRec?.role, 'carousel-container');
2774
+ assert.equal(swiperRec?.adapterId, 'swiper');
2775
+
2776
+ const swiperSlide = swiperContainer.children.find((c) => c.openingElement?.name?.name === 'SwiperSlide');
2777
+ const slideRec = recognizeWithAdapters(swiperSlide, {}, activeAdapters);
2778
+ assert.equal(slideRec?.role, 'carousel-slide');
2779
+ assert.equal(slideRec?.adapterId, 'swiper');
2780
+
2781
+ // Embla
2782
+ const emblaContainer = makeNode('<Carousel><CarouselContent><CarouselItem>Item 1</CarouselItem></CarouselContent></Carousel>');
2783
+ const emblaRec = recognizeWithAdapters(emblaContainer, {}, activeAdapters);
2784
+ assert.equal(emblaRec?.role, 'carousel-container');
2785
+ assert.equal(emblaRec?.adapterId, 'embla');
2786
+
2787
+ // Slick
2788
+ const slickContainer = makeNode('<Slider dots={true}><div>Slide</div></Slider>');
2789
+ const slickRec = recognizeWithAdapters(slickContainer, {}, activeAdapters);
2790
+ assert.equal(slickRec?.role, 'carousel-container');
2791
+ assert.equal(slickRec?.adapterId, 'slick');
2792
+ });
2793
+
2794
+ test('ARC v2 Phase 5: Accordion and Tabs adapters recognize component hierarchies and trigger/content roles', () => {
2795
+ const { recognizeWithAdapters, defaultRegistry } = require('../adapters/registry.cjs');
2796
+ const parseSource = (c) => require('recast').parse(c, { parser: require('recast/parsers/babel-ts') });
2797
+ const makeNode = (code) => parseSource(code).program.body[0].expression;
2798
+
2799
+ const activeAdapters = defaultRegistry.getAll();
2800
+
2801
+ // Accordion Trigger & Content
2802
+ const accTrigger = makeNode('<AccordionTrigger>What is your return policy?</AccordionTrigger>');
2803
+ const accTriggerRec = recognizeWithAdapters(accTrigger, {}, activeAdapters);
2804
+ assert.equal(accTriggerRec?.role, 'accordion-header');
2805
+ assert.equal(accTriggerRec?.adapterId, 'accordion');
2806
+
2807
+ const accContent = makeNode('<AccordionContent>We offer 30-day refunds.</AccordionContent>');
2808
+ const accContentRec = recognizeWithAdapters(accContent, {}, activeAdapters);
2809
+ assert.equal(accContentRec?.role, 'accordion-body');
2810
+ assert.equal(accContentRec?.adapterId, 'accordion');
2811
+
2812
+ // Tabs Trigger & Content
2813
+ const tabTrigger = makeNode('<TabsTrigger value="overview">Overview</TabsTrigger>');
2814
+ const tabTriggerRec = recognizeWithAdapters(tabTrigger, {}, activeAdapters);
2815
+ assert.equal(tabTriggerRec?.role, 'tab-button');
2816
+ assert.equal(tabTriggerRec?.adapterId, 'tabs');
2817
+
2818
+ const tabContent = makeNode('<TabsContent value="overview"><p>Details here</p></TabsContent>');
2819
+ const tabContentRec = recognizeWithAdapters(tabContent, {}, activeAdapters);
2820
+ assert.equal(tabContentRec?.role, 'tab-panel');
2821
+ assert.equal(tabContentRec?.adapterId, 'tabs');
2822
+ });
2823
+
2824
+ test('ARC v2 Phase 6: explainFile analyzes component AST and outputs structured explanation', () => {
2825
+ const fs = require('fs');
2826
+ const path = require('path');
2827
+ const os = require('os');
2828
+ const { explainFile } = require('../explain.cjs');
2829
+
2830
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'deneb-explain-test-'));
2831
+ const compFile = path.join(tmpDir, 'Hero.tsx');
2832
+ fs.writeFileSync(
2833
+ compFile,
2834
+ `
2835
+ export function Hero() {
2836
+ return (
2837
+ <section className="hero">
2838
+ <h1>Welcome to Luxury Watches</h1>
2839
+ <p>Handcrafted precision timepieces</p>
2840
+ <img src="/watches/hero.jpg" alt="Hero Watch" />
2841
+ </section>
2842
+ );
2843
+ }
2844
+ `,
2845
+ 'utf8'
2846
+ );
2847
+
2848
+ const explanation = explainFile('Hero.tsx', { root: tmpDir });
2849
+ assert.equal(explanation.file, 'Hero.tsx');
2850
+ assert.equal(explanation.isTypeScript, true);
2851
+ assert.ok(explanation.totalCandidates >= 3, 'Must discover heading, paragraph, and image');
2852
+ assert.ok(explanation.editableCount >= 3, 'All 3 content items must be editable');
2853
+
2854
+ const h1El = explanation.editableElements.find((e) => e.tag === 'h1');
2855
+ assert.ok(h1El, 'Must include h1');
2856
+ assert.equal(h1El.kind, 'text');
2857
+ assert.equal(h1El.value, 'Welcome to Luxury Watches');
2858
+
2859
+ const imgEl = explanation.editableElements.find((e) => e.tag === 'img');
2860
+ assert.ok(imgEl, 'Must include img');
2861
+ assert.equal(imgEl.kind, 'image');
2862
+ assert.equal(imgEl.value, '/watches/hero.jpg');
2863
+
2864
+ fs.rmSync(tmpDir, { recursive: true, force: true });
2865
+ });
2866
+
2867
+ test('ARC v2 Phase 6: createUnifiedDiff generates unified diff representation cleanly', () => {
2868
+ const { createUnifiedDiff } = require('../diff.cjs');
2869
+
2870
+ const oldCode = `export function Banner() {\n return <h1>Old Headline</h1>;\n}`;
2871
+ const newCode = `export function Banner() {\n return <h1 data-preview-field-path="home.title">New Headline</h1>;\n}`;
2872
+
2873
+ const diffResult = createUnifiedDiff('Banner.tsx', oldCode, newCode);
2874
+ assert.equal(diffResult.filename, 'Banner.tsx');
2875
+ assert.ok(diffResult.additions > 0);
2876
+ assert.ok(diffResult.deletions > 0);
2877
+ assert.match(diffResult.diff, /--- a\/Banner\.tsx/);
2878
+ assert.match(diffResult.diff, /\+\+\+ b\/Banner\.tsx/);
2879
+ assert.match(diffResult.diff, /- return <h1>Old Headline<\/h1>;/);
2880
+ assert.match(diffResult.diff, /\+ return <h1 data-preview-field-path="home\.title">New Headline<\/h1>;/);
2881
+ });
2882
+
2883
+ test('ARC v2 Phase 6: conversion report includes severity-graded diagnostics (INFO, WARNING, ERROR, BLOCKING)', () => {
2884
+ const fs = require('fs');
2885
+ const path = require('path');
2886
+ const os = require('os');
2887
+ const { runDenebArc } = require('../index.cjs');
2888
+
2889
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'deneb-severity-test-'));
2890
+ fs.mkdirSync(path.join(tmpDir, 'src', 'components'), { recursive: true });
2891
+ fs.writeFileSync(
2892
+ path.join(tmpDir, 'package.json'),
2893
+ JSON.stringify({ name: 'severity-test', dependencies: { next: '14.0.0', react: '18.2.0' } }),
2894
+ 'utf8'
2895
+ );
2896
+ fs.writeFileSync(
2897
+ path.join(tmpDir, 'src', 'components', 'Banner.tsx'),
2898
+ `
2899
+ export function Banner() {
2900
+ return (
2901
+ <div>
2902
+ <svg className="decorative-icon"><path d="M0 0" /></svg>
2903
+ <h2>Exclusive Deals</h2>
2904
+ </div>
2905
+ );
2906
+ }
2907
+ `,
2908
+ 'utf8'
2909
+ );
2910
+
2911
+ runDenebArc(tmpDir, { dryRun: false });
2912
+
2913
+ const reportPath = path.join(tmpDir, 'deneb-conversion-report.json');
2914
+ assert.ok(fs.existsSync(reportPath), 'deneb-conversion-report.json must exist');
2915
+ const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
2916
+
2917
+ assert.ok(report.diagnosticsBySeverity, 'Must contain diagnosticsBySeverity');
2918
+ assert.equal(typeof report.diagnosticsBySeverity.info, 'number');
2919
+ assert.equal(typeof report.diagnosticsBySeverity.warning, 'number');
2920
+ assert.equal(typeof report.diagnosticsBySeverity.error, 'number');
2921
+ assert.equal(typeof report.diagnosticsBySeverity.blocking, 'number');
2922
+
2923
+ // Decorative SVG is categorized as INFO
2924
+ const svgDiagnostic = (report.unresolved || []).find((u) => u.type === 'icon' || u.type === 'decoration');
2925
+ if (svgDiagnostic) {
2926
+ assert.equal(svgDiagnostic.severity, 'INFO');
2927
+ }
2928
+
2929
+ fs.rmSync(tmpDir, { recursive: true, force: true });
2930
+ });
2931
+
2932
+ test('ARC v2 Phase 6: runTransactionalPipeline executes atomic rollback on critical gate failure', () => {
2933
+ const fs = require('fs');
2934
+ const path = require('path');
2935
+ const os = require('os');
2936
+ const { runTransactionalPipeline } = require('../index.cjs');
2937
+
2938
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'deneb-rollback-test-'));
2939
+ fs.mkdirSync(path.join(tmpDir, 'src', 'components'), { recursive: true });
2940
+ fs.writeFileSync(
2941
+ path.join(tmpDir, 'package.json'),
2942
+ JSON.stringify({ name: 'rollback-test', dependencies: { next: '14.0.0', react: '18.2.0' } }),
2943
+ 'utf8'
2944
+ );
2945
+ const originalCode = `export function Card() { return <h3>Original Card</h3>; }`;
2946
+ const cardPath = path.join(tmpDir, 'src', 'components', 'Card.tsx');
2947
+ fs.writeFileSync(cardPath, originalCode, 'utf8');
2948
+
2949
+ // Run transactional pipeline with strict mode where contract failures trigger rollback
2950
+ const result = runTransactionalPipeline(tmpDir, { strict: true });
2951
+ // If it rolled back or succeeded, original code is preserved or successfully updated
2952
+ if (result.rolledBack) {
2953
+ const afterCode = fs.readFileSync(cardPath, 'utf8');
2954
+ assert.equal(afterCode, originalCode, 'Original code must be restored upon rollback');
2955
+ } else {
2956
+ assert.ok(result.success, 'Valid pipeline must report success');
2957
+ }
2958
+
2959
+ fs.rmSync(tmpDir, { recursive: true, force: true });
2960
+ });
2961
+
2962
+
2963
+
2964
+
2965
+
2966
+