@deneb-ui/cli 2.0.87 → 2.0.89
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 +31 -5
- package/package.json +2 -2
- package/src/arc/__tests__/acceptance-gates.test.cjs +62 -0
- package/src/arc/__tests__/arc.test.cjs +922 -0
- package/src/arc/__tests__/canonical.test.cjs +57 -0
- package/src/arc/__tests__/classification.test.cjs +54 -0
- package/src/arc/__tests__/corpus.test.cjs +144 -0
- package/src/arc/__tests__/explain-blocked.test.cjs +192 -0
- package/src/arc/__tests__/fuzz.test.cjs +106 -0
- package/src/arc/__tests__/interaction.test.cjs +85 -0
- package/src/arc/__tests__/visual-regression.test.cjs +99 -0
- package/src/arc/acceptance-gates.cjs +119 -0
- package/src/arc/canonical-paths.cjs +142 -23
- package/src/arc/corpus-verifier.cjs +295 -0
- package/src/arc/data-classification.cjs +182 -0
- package/src/arc/data-flow.cjs +62 -2
- package/src/arc/explain-blocked.cjs +388 -0
- package/src/arc/fuzz-engine.cjs +604 -0
- package/src/arc/index.cjs +148 -2
- package/src/arc/interaction-verifier.cjs +185 -0
- package/src/arc/ir-builder.cjs +170 -12
- package/src/arc/planner.cjs +16 -1
- package/src/arc/residual.cjs +22 -0
- package/src/arc/rsc-boundary.cjs +105 -4
- package/src/arc/runtime-validator.cjs +65 -0
- package/src/arc/semantic.cjs +12 -1
- package/src/arc/transformer.cjs +14 -2745
- package/src/arc/transforms/assets/tailwind-bg.cjs +38 -0
- package/src/arc/transforms/collections/list.cjs +474 -0
- package/src/arc/transforms/components/child-card.cjs +268 -0
- package/src/arc/transforms/components/compound-content.cjs +49 -0
- package/src/arc/transforms/components/reusable-component.cjs +173 -0
- package/src/arc/transforms/element-transform.cjs +192 -0
- package/src/arc/transforms/healing/conditionals.cjs +295 -0
- package/src/arc/transforms/healing/sanitizer.cjs +401 -0
- package/src/arc/transforms/index.cjs +195 -0
- package/src/arc/transforms/primitives/action.cjs +76 -0
- package/src/arc/transforms/primitives/text.cjs +65 -0
- package/src/arc/transforms/routing/page-key.cjs +84 -0
- package/src/arc/transforms/routing/shared-layout.cjs +294 -0
- package/src/arc/transforms/runtime/provider.cjs +230 -0
- package/src/arc/transforms/transform-context.cjs +118 -0
- package/src/arc/types/acceptance.d.ts +50 -0
- package/src/arc/types/canonical.d.ts +50 -0
- package/src/arc/types/classification.d.ts +42 -0
- package/src/arc/types/corpus.d.ts +52 -0
- package/src/arc/types/explain-blocked.d.ts +41 -0
- package/src/arc/types/fuzz.d.ts +74 -0
- package/src/arc/types/interaction.d.ts +43 -0
- package/src/arc/types/ir.d.ts +114 -0
- package/src/arc/types/rsc.d.ts +39 -0
- package/src/arc/types/visual-regression.d.ts +49 -0
- package/src/arc/visual-regression.cjs +195 -0
- package/src/arc/workspace.cjs +233 -0
- package/src/tools/deneb-template-validator.cjs +17 -17
|
@@ -2959,6 +2959,928 @@ test('ARC v2 Phase 6: runTransactionalPipeline executes atomic rollback on criti
|
|
|
2959
2959
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
2960
2960
|
});
|
|
2961
2961
|
|
|
2962
|
+
test('ARC v3 Phase 7: createRunWorkspace creates isolated run structure and commits files cleanly', () => {
|
|
2963
|
+
const fs = require('fs');
|
|
2964
|
+
const path = require('path');
|
|
2965
|
+
const os = require('os');
|
|
2966
|
+
const {
|
|
2967
|
+
createRunWorkspace,
|
|
2968
|
+
copyProjectToWorkspace,
|
|
2969
|
+
commitWorkspaceToProject,
|
|
2970
|
+
cleanupWorkspace,
|
|
2971
|
+
saveRunArtifacts,
|
|
2972
|
+
formatFailureExplanation,
|
|
2973
|
+
} = require('../workspace.cjs');
|
|
2974
|
+
|
|
2975
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'deneb-workspace-test-'));
|
|
2976
|
+
fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true });
|
|
2977
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify({ name: 'ws-test' }), 'utf8');
|
|
2978
|
+
fs.writeFileSync(path.join(tmpDir, 'src', 'App.tsx'), 'export default function App() {}', 'utf8');
|
|
2979
|
+
|
|
2980
|
+
const runId = 'arc-test-phase7';
|
|
2981
|
+
const runDirs = createRunWorkspace(tmpDir, runId);
|
|
2982
|
+
assert.ok(fs.existsSync(runDirs.workspaceDir), 'workspaceDir should exist');
|
|
2983
|
+
assert.ok(fs.existsSync(runDirs.snapshotDir), 'snapshotDir should exist');
|
|
2984
|
+
assert.ok(fs.existsSync(runDirs.reportsDir), 'reportsDir should exist');
|
|
2985
|
+
|
|
2986
|
+
// Copy project to workspace
|
|
2987
|
+
copyProjectToWorkspace(tmpDir, runDirs.workspaceDir, runDirs.snapshotDir);
|
|
2988
|
+
assert.ok(fs.existsSync(path.join(runDirs.workspaceDir, 'src', 'App.tsx')), 'App.tsx copied to workspace');
|
|
2989
|
+
assert.ok(fs.existsSync(path.join(runDirs.snapshotDir, 'src', 'App.tsx')), 'App.tsx snapshotted');
|
|
2990
|
+
|
|
2991
|
+
// Modify in workspace
|
|
2992
|
+
fs.writeFileSync(path.join(runDirs.workspaceDir, 'src', 'App.tsx'), 'export default function App() { return <h1>Updated</h1>; }', 'utf8');
|
|
2993
|
+
// Original is unchanged before commit
|
|
2994
|
+
assert.equal(fs.readFileSync(path.join(tmpDir, 'src', 'App.tsx'), 'utf8'), 'export default function App() {}');
|
|
2995
|
+
|
|
2996
|
+
// Save artifacts
|
|
2997
|
+
saveRunArtifacts(runDirs, {
|
|
2998
|
+
report: { outcome: 'success', engine: 'ARC v3' },
|
|
2999
|
+
runtimeResults: { passed: true, score: 100 },
|
|
3000
|
+
});
|
|
3001
|
+
assert.ok(fs.existsSync(path.join(runDirs.reportsDir, 'conversion-report.json')));
|
|
3002
|
+
|
|
3003
|
+
// Commit to project
|
|
3004
|
+
const committed = commitWorkspaceToProject(runDirs.workspaceDir, tmpDir, ['src/App.tsx']);
|
|
3005
|
+
assert.ok(committed.includes('src/App.tsx'));
|
|
3006
|
+
assert.equal(fs.readFileSync(path.join(tmpDir, 'src', 'App.tsx'), 'utf8'), 'export default function App() { return <h1>Updated</h1>; }');
|
|
3007
|
+
|
|
3008
|
+
// Format failure explanation
|
|
3009
|
+
const explanation = formatFailureExplanation({ fivoraContractErrors: ['Missing key'], uncoveredVisibleText: 2 }, ['Contract failed']);
|
|
3010
|
+
assert.ok(explanation.includes('DENEB CONVERSION BLOCKED'));
|
|
3011
|
+
assert.ok(explanation.includes('Contract failed'));
|
|
3012
|
+
|
|
3013
|
+
cleanupWorkspace(runDirs.workspaceDir);
|
|
3014
|
+
assert.ok(!fs.existsSync(runDirs.workspaceDir), 'workspaceDir cleaned up');
|
|
3015
|
+
|
|
3016
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
3017
|
+
});
|
|
3018
|
+
|
|
3019
|
+
test('ARC v3 Phase 7: blocking Fivora contract enforcement prevents commit when contract checks fail', () => {
|
|
3020
|
+
const fs = require('fs');
|
|
3021
|
+
const path = require('path');
|
|
3022
|
+
const os = require('os');
|
|
3023
|
+
const { runTransactionalPipeline } = require('../index.cjs');
|
|
3024
|
+
|
|
3025
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'deneb-blocking-contract-test-'));
|
|
3026
|
+
fs.mkdirSync(path.join(tmpDir, 'src', 'components'), { recursive: true });
|
|
3027
|
+
fs.writeFileSync(
|
|
3028
|
+
path.join(tmpDir, 'package.json'),
|
|
3029
|
+
JSON.stringify({ name: 'blocking-contract-test', dependencies: { next: '14.0.0', react: '18.2.0' } }),
|
|
3030
|
+
'utf8'
|
|
3031
|
+
);
|
|
3032
|
+
// An invalid component that violates Fivora contract (uncovered text or invalid syntax)
|
|
3033
|
+
const initialCode = `export function Broken() { return <div>Unbound static text</div>; }`;
|
|
3034
|
+
const compPath = path.join(tmpDir, 'src', 'components', 'Broken.tsx');
|
|
3035
|
+
fs.writeFileSync(compPath, initialCode, 'utf8');
|
|
3036
|
+
|
|
3037
|
+
// Strict mode is true by default in ARC v3
|
|
3038
|
+
const pipelineRes = runTransactionalPipeline(tmpDir);
|
|
3039
|
+
assert.ok(pipelineRes !== null);
|
|
3040
|
+
|
|
3041
|
+
// If pipeline rolls back or fails contract, original file must remain untouched
|
|
3042
|
+
if (pipelineRes.rolledBack) {
|
|
3043
|
+
const afterCode = fs.readFileSync(compPath, 'utf8');
|
|
3044
|
+
assert.equal(afterCode, initialCode, 'Developer project file must remain untouched upon rollback');
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
3048
|
+
});
|
|
3049
|
+
|
|
3050
|
+
test('ARC v3 Phase 8: modular transforms architecture exports all sub-modules with 100% facade compatibility', () => {
|
|
3051
|
+
const facade = require('../transformer.cjs');
|
|
3052
|
+
const modular = require('../transforms/index.cjs');
|
|
3053
|
+
|
|
3054
|
+
assert.equal(typeof facade.applyFilePlan, 'function');
|
|
3055
|
+
assert.equal(typeof facade.applyCollectionTransform, 'function');
|
|
3056
|
+
assert.equal(typeof facade.instrumentChildCardComponent, 'function');
|
|
3057
|
+
assert.equal(typeof facade.instrumentReusableComponent, 'function');
|
|
3058
|
+
assert.equal(typeof facade.instrumentLayoutSource, 'function');
|
|
3059
|
+
assert.equal(typeof facade.instrumentPageKey, 'function');
|
|
3060
|
+
assert.equal(typeof facade.resolveSiteDataSpecifier, 'function');
|
|
3061
|
+
assert.equal(typeof facade.rewriteRecursiveSiteDataContext, 'function');
|
|
3062
|
+
|
|
3063
|
+
assert.strictEqual(facade.applyFilePlan, modular.applyFilePlan);
|
|
3064
|
+
assert.strictEqual(facade.applyCollectionTransform, modular.applyCollectionTransform);
|
|
3065
|
+
assert.strictEqual(facade.instrumentChildCardComponent, modular.instrumentChildCardComponent);
|
|
3066
|
+
assert.strictEqual(facade.instrumentLayoutSource, modular.instrumentLayoutSource);
|
|
3067
|
+
});
|
|
3068
|
+
|
|
3069
|
+
test('ARC v3 Phase 8: direct sub-module transformation works for primitives, compound-content, and background assets', () => {
|
|
3070
|
+
const { replaceTextChildren } = require('../transforms/primitives/text.cjs');
|
|
3071
|
+
const { extractTailwindBg } = require('../transforms/assets/tailwind-bg.cjs');
|
|
3072
|
+
const { bindButtonWithIcon } = require('../transforms/components/compound-content.cjs');
|
|
3073
|
+
const { parseSource, printSource } = require('../ast.cjs');
|
|
3074
|
+
|
|
3075
|
+
// 1. Primitive text transform
|
|
3076
|
+
const textCode = `export function Heading() { return <h1>Original Title</h1>; }`;
|
|
3077
|
+
const textAst = parseSource(textCode);
|
|
3078
|
+
const h1Node = textAst.program.body[0].declaration.body.body[0].argument;
|
|
3079
|
+
replaceTextChildren(h1Node, 'home.heroTitle', 'Original Title', 'text');
|
|
3080
|
+
const updatedText = printSource(textAst, textCode);
|
|
3081
|
+
assert.ok(updatedText.includes('siteData.content.home.heroTitle') || updatedText.includes('siteData?.content?.home?.heroTitle') || updatedText.includes('siteData.home.heroTitle'));
|
|
3082
|
+
|
|
3083
|
+
// 2. Asset tailwind background image extraction
|
|
3084
|
+
const bgCode = `export function Banner() { return <div className="h-64 bg-[url('/hero.png')] text-white" />; }`;
|
|
3085
|
+
const bgAst = parseSource(bgCode);
|
|
3086
|
+
const divNode = bgAst.program.body[0].declaration.body.body[0].argument;
|
|
3087
|
+
extractTailwindBg(divNode, { field: 'home.heroBanner', fallback: '/hero.png' });
|
|
3088
|
+
const updatedBg = printSource(bgAst, bgCode);
|
|
3089
|
+
assert.ok(updatedBg.includes('backgroundImage: `url(${'));
|
|
3090
|
+
assert.ok(!updatedBg.includes("bg-[url('/hero.png')]"));
|
|
3091
|
+
|
|
3092
|
+
// 3. Compound content button with icon
|
|
3093
|
+
const btnCode = `export function Action() { return <button className="btn"><svg /><span>Click Me</span></button>; }`;
|
|
3094
|
+
const btnAst = parseSource(btnCode);
|
|
3095
|
+
const btnNode = btnAst.program.body[0].declaration.body.body[0].argument;
|
|
3096
|
+
bindButtonWithIcon(btnNode, { field: 'home.ctaLabel' });
|
|
3097
|
+
const updatedBtn = printSource(btnAst, btnCode);
|
|
3098
|
+
assert.ok(updatedBtn.includes('data-preview-field-path="home.ctaLabel"'));
|
|
3099
|
+
});
|
|
3100
|
+
|
|
3101
|
+
test('ARC v3 Phase 9: canonicalField engine provides single source of truth for runtime, markers, schema, and setters', () => {
|
|
3102
|
+
const { canonicalField, fieldRef, isValidCanonicalPath, validatePathAlignment } = require('../canonical-paths.cjs');
|
|
3103
|
+
|
|
3104
|
+
// 1. Core derivation from canonical string
|
|
3105
|
+
const field = canonicalField('home.hero.title', { provenance: { file: 'Hero.tsx', loc: '12:4' } });
|
|
3106
|
+
assert.equal(field.path, 'home.hero.title');
|
|
3107
|
+
assert.equal(field.scope, 'home');
|
|
3108
|
+
assert.equal(field.section, 'hero');
|
|
3109
|
+
assert.equal(field.fieldName, 'title');
|
|
3110
|
+
assert.equal(field.previewPath, 'home.hero.title');
|
|
3111
|
+
assert.equal(field.previewMarker, 'data-preview-field-path="home.hero.title"');
|
|
3112
|
+
assert.equal(field.runtimePath, 'siteData.content.home.hero.title');
|
|
3113
|
+
assert.equal(field.manifestSchemaPath, 'home.hero.title');
|
|
3114
|
+
assert.equal(field.diagnosticsId, 'field:home.hero.title');
|
|
3115
|
+
assert.equal(field.toSetterCode('newVal'), 'siteData.content.home.hero.title = newVal;');
|
|
3116
|
+
assert.equal(field.validate().valid, true);
|
|
3117
|
+
|
|
3118
|
+
// 2. Collection and child derivation
|
|
3119
|
+
const col = canonicalField('home.categories');
|
|
3120
|
+
const item0 = col.toCollectionItem(0);
|
|
3121
|
+
assert.equal(item0.path, 'home.categories[0]');
|
|
3122
|
+
assert.equal(item0.isCollection, true);
|
|
3123
|
+
|
|
3124
|
+
const nestedChild = item0.toCollectionChild('products', 2);
|
|
3125
|
+
assert.equal(nestedChild.path, 'home.categories[0].products[2]');
|
|
3126
|
+
|
|
3127
|
+
// 3. Backward compatible alias
|
|
3128
|
+
const aliasRef = fieldRef('about.story.heading');
|
|
3129
|
+
assert.equal(aliasRef.fieldName, 'heading');
|
|
3130
|
+
assert.equal(aliasRef.scope, 'about');
|
|
3131
|
+
|
|
3132
|
+
// 4. AST generation
|
|
3133
|
+
const getterAst = field.toGetterAst();
|
|
3134
|
+
assert.equal(getterAst.type, 'OptionalMemberExpression');
|
|
3135
|
+
|
|
3136
|
+
const bindingAst = field.toBindingAst('Fallback');
|
|
3137
|
+
assert.equal(bindingAst.type, 'LogicalExpression');
|
|
3138
|
+
|
|
3139
|
+
const attrAst = field.toPreviewAttrAst();
|
|
3140
|
+
assert.equal(attrAst.name.name, 'data-preview-field-path');
|
|
3141
|
+
assert.equal(attrAst.value.value, 'home.hero.title');
|
|
3142
|
+
|
|
3143
|
+
const styleAttrAst = field.toPreviewStyleTargetAst();
|
|
3144
|
+
assert.equal(styleAttrAst.name.name, 'data-preview-style-target');
|
|
3145
|
+
|
|
3146
|
+
// 5. Validation and alignment
|
|
3147
|
+
assert.ok(isValidCanonicalPath('home.hero.title'));
|
|
3148
|
+
assert.ok(isValidCanonicalPath('home.categories[0].products[${index}].title'));
|
|
3149
|
+
assert.ok(!isValidCanonicalPath(''));
|
|
3150
|
+
assert.ok(!isValidCanonicalPath('invalid path with spaces'));
|
|
3151
|
+
|
|
3152
|
+
assert.ok(validatePathAlignment('siteData?.content?.home?.hero?.title', 'data-preview-field-path="home.hero.title"'));
|
|
3153
|
+
});
|
|
3154
|
+
|
|
3155
|
+
test('ARC v3 Phase 9: formalized typed IR models and validateProjectIR verify project structure integrity', () => {
|
|
3156
|
+
const {
|
|
3157
|
+
createDenebComponent,
|
|
3158
|
+
createDataSource,
|
|
3159
|
+
createCollectionDefinition,
|
|
3160
|
+
validateProjectIR,
|
|
3161
|
+
} = require('../ir-builder.cjs');
|
|
3162
|
+
|
|
3163
|
+
// 1. Factory validation
|
|
3164
|
+
const comp = createDenebComponent({
|
|
3165
|
+
name: 'ProductCard',
|
|
3166
|
+
file: 'components/ProductCard.tsx',
|
|
3167
|
+
propsSignature: { kind: 'destructured', names: ['product'], paramName: null, typeAnnotationName: 'CardProps' },
|
|
3168
|
+
memberAccesses: { product: ['name', 'price', 'image'] },
|
|
3169
|
+
});
|
|
3170
|
+
assert.equal(comp.id, 'components/ProductCard.tsx:ProductCard');
|
|
3171
|
+
assert.equal(comp.clientBoundary, 'server');
|
|
3172
|
+
|
|
3173
|
+
const ds = createDataSource({
|
|
3174
|
+
name: 'featuredItems',
|
|
3175
|
+
file: 'data/products.json',
|
|
3176
|
+
items: [{ id: 1, name: 'Item 1' }],
|
|
3177
|
+
fieldKeys: ['id', 'name'],
|
|
3178
|
+
});
|
|
3179
|
+
assert.equal(ds.editable, true);
|
|
3180
|
+
assert.equal(ds.kind, 'inline-array');
|
|
3181
|
+
|
|
3182
|
+
const col = createCollectionDefinition({
|
|
3183
|
+
file: 'app/shop/page.tsx',
|
|
3184
|
+
arrayName: 'featuredItems',
|
|
3185
|
+
itemParam: 'item',
|
|
3186
|
+
rootTagName: 'ProductCard',
|
|
3187
|
+
childComponent: comp,
|
|
3188
|
+
dataSource: ds,
|
|
3189
|
+
});
|
|
3190
|
+
assert.equal(col.arrayName, 'featuredItems');
|
|
3191
|
+
assert.equal(col.childComponent.name, 'ProductCard');
|
|
3192
|
+
|
|
3193
|
+
// 2. Validate valid IR
|
|
3194
|
+
const mockIR = {
|
|
3195
|
+
components: new Map([['ProductCard', comp]]),
|
|
3196
|
+
dataSources: new Map([['featuredItems', ds]]),
|
|
3197
|
+
collections: [col],
|
|
3198
|
+
assetImports: new Map(),
|
|
3199
|
+
backgroundImages: [],
|
|
3200
|
+
componentUsages: [],
|
|
3201
|
+
};
|
|
3202
|
+
|
|
3203
|
+
const validation = validateProjectIR(mockIR);
|
|
3204
|
+
assert.equal(validation.valid, true);
|
|
3205
|
+
assert.equal(validation.errors.length, 0);
|
|
3206
|
+
assert.equal(validation.stats.componentCount, 1);
|
|
3207
|
+
assert.equal(validation.stats.dataSourceCount, 1);
|
|
3208
|
+
assert.equal(validation.stats.collectionCount, 1);
|
|
3209
|
+
|
|
3210
|
+
// 3. Catch corrupted IR
|
|
3211
|
+
const brokenIR = {
|
|
3212
|
+
components: new Map([['BrokenComp', { name: 'MismatchedName', file: '' }]]),
|
|
3213
|
+
dataSources: new Map([['BrokenDS', { name: 'BrokenDS', file: '' }]]),
|
|
3214
|
+
collections: [{ file: '' }],
|
|
3215
|
+
};
|
|
3216
|
+
const brokenValidation = validateProjectIR(brokenIR);
|
|
3217
|
+
assert.equal(brokenValidation.valid, false);
|
|
3218
|
+
assert.ok(brokenValidation.errors.length >= 3);
|
|
3219
|
+
});
|
|
3220
|
+
|
|
3221
|
+
test('ARC v3 Phase 10: data-classification engine accurately categorizes 7 data tiers and protects platform/runtime data', () => {
|
|
3222
|
+
const { DATA_CLASSIFICATION, classifyDataCandidate, isEditableClassification } = require('../data-classification.cjs');
|
|
3223
|
+
|
|
3224
|
+
// 1. CONTENT -> editable
|
|
3225
|
+
const contentRes = classifyDataCandidate({ tag: 'h1', kind: 'text', value: 'Special Summer Blend' });
|
|
3226
|
+
assert.equal(contentRes.classification, DATA_CLASSIFICATION.CONTENT);
|
|
3227
|
+
assert.equal(contentRes.editable, true);
|
|
3228
|
+
assert.equal(isEditableClassification(contentRes.classification), true);
|
|
3229
|
+
|
|
3230
|
+
// 2. COMMERCE_CONTENT -> editable
|
|
3231
|
+
const commerceRes = classifyDataCandidate({ tag: 'h3', kind: 'text', value: 'Arabica Dark Roast', propName: 'productTitle' });
|
|
3232
|
+
assert.equal(commerceRes.classification, DATA_CLASSIFICATION.COMMERCE_CONTENT);
|
|
3233
|
+
assert.equal(commerceRes.editable, true);
|
|
3234
|
+
assert.equal(isEditableClassification(commerceRes.classification), true);
|
|
3235
|
+
|
|
3236
|
+
// 3. PLATFORM_CONTROLLED -> not editable (no marker)
|
|
3237
|
+
const platformRes = classifyDataCandidate({ tag: 'span', kind: 'text', value: 'LKR', field: 'common.currency' });
|
|
3238
|
+
assert.equal(platformRes.classification, DATA_CLASSIFICATION.PLATFORM_CONTROLLED);
|
|
3239
|
+
assert.equal(platformRes.editable, false);
|
|
3240
|
+
assert.equal(isEditableClassification(platformRes.classification), false);
|
|
3241
|
+
|
|
3242
|
+
const orderIdRes = classifyDataCandidate({ tag: 'span', kind: 'text', value: '#10492', propName: 'orderId' });
|
|
3243
|
+
assert.equal(orderIdRes.classification, DATA_CLASSIFICATION.PLATFORM_CONTROLLED);
|
|
3244
|
+
assert.equal(orderIdRes.editable, false);
|
|
3245
|
+
|
|
3246
|
+
// 4. INTERACTION_STATE -> not editable
|
|
3247
|
+
const modalRes = classifyDataCandidate({ tag: 'div', kind: 'text', propName: 'isOpen', value: 'true' });
|
|
3248
|
+
assert.equal(modalRes.classification, DATA_CLASSIFICATION.INTERACTION_STATE);
|
|
3249
|
+
assert.equal(modalRes.editable, false);
|
|
3250
|
+
|
|
3251
|
+
// 5. COMPUTED_DATA -> not editable
|
|
3252
|
+
const computedRes = classifyDataCandidate({ tag: 'span', kind: 'text', value: 'items.reduce((a, b) => a + b)' });
|
|
3253
|
+
assert.equal(computedRes.classification, DATA_CLASSIFICATION.COMPUTED_DATA);
|
|
3254
|
+
assert.equal(computedRes.editable, false);
|
|
3255
|
+
|
|
3256
|
+
// 6. RUNTIME_DATA -> not editable
|
|
3257
|
+
const runtimeRes = classifyDataCandidate({ tag: 'span', kind: 'text', value: 'user.email' });
|
|
3258
|
+
assert.equal(runtimeRes.classification, DATA_CLASSIFICATION.RUNTIME_DATA);
|
|
3259
|
+
assert.equal(runtimeRes.editable, false);
|
|
3260
|
+
|
|
3261
|
+
// 7. DECORATIVE -> not editable
|
|
3262
|
+
const decorRes = classifyDataCandidate({ tag: 'span', kind: 'text', value: '•' });
|
|
3263
|
+
assert.equal(decorRes.classification, DATA_CLASSIFICATION.DECORATIVE);
|
|
3264
|
+
assert.equal(decorRes.editable, false);
|
|
3265
|
+
});
|
|
3266
|
+
|
|
3267
|
+
test('ARC v3 Phase 10: planner enforces non-editable skip on platform-controlled, interaction, and computed candidates', () => {
|
|
3268
|
+
const { planTransformations } = require('../planner.cjs');
|
|
3269
|
+
|
|
3270
|
+
const analyses = [
|
|
3271
|
+
{
|
|
3272
|
+
relativeFile: 'components/OrderSummary.tsx',
|
|
3273
|
+
candidates: [
|
|
3274
|
+
{
|
|
3275
|
+
loc: '5:4',
|
|
3276
|
+
tag: 'h2',
|
|
3277
|
+
kind: 'text',
|
|
3278
|
+
value: 'Your Order Summary',
|
|
3279
|
+
confidence: 0.95,
|
|
3280
|
+
},
|
|
3281
|
+
{
|
|
3282
|
+
loc: '12:6',
|
|
3283
|
+
tag: 'span',
|
|
3284
|
+
kind: 'text',
|
|
3285
|
+
value: 'user.email',
|
|
3286
|
+
confidence: 0.95,
|
|
3287
|
+
},
|
|
3288
|
+
{
|
|
3289
|
+
loc: '18:6',
|
|
3290
|
+
tag: 'span',
|
|
3291
|
+
kind: 'text',
|
|
3292
|
+
value: 'LKR',
|
|
3293
|
+
field: 'common.currency',
|
|
3294
|
+
confidence: 0.95,
|
|
3295
|
+
},
|
|
3296
|
+
{
|
|
3297
|
+
loc: '24:6',
|
|
3298
|
+
tag: 'div',
|
|
3299
|
+
kind: 'text',
|
|
3300
|
+
propName: 'isOpen',
|
|
3301
|
+
value: 'true',
|
|
3302
|
+
confidence: 0.95,
|
|
3303
|
+
},
|
|
3304
|
+
],
|
|
3305
|
+
code: 'export function OrderSummary() {}',
|
|
3306
|
+
},
|
|
3307
|
+
];
|
|
3308
|
+
|
|
3309
|
+
const plan = planTransformations({ profile: { jsxFiles: ['components/OrderSummary.tsx'] }, analyses });
|
|
3310
|
+
const filePlan = plan.files[0];
|
|
3311
|
+
|
|
3312
|
+
// Only the content headline should be planned for transformation
|
|
3313
|
+
assert.equal(filePlan.transformations.length, 1);
|
|
3314
|
+
assert.equal(filePlan.transformations[0].fallback, 'Your Order Summary');
|
|
3315
|
+
assert.equal(filePlan.transformations[0].dataClassification, 'CONTENT');
|
|
3316
|
+
|
|
3317
|
+
// The 3 runtime/platform/interaction candidates must be skipped
|
|
3318
|
+
assert.equal(plan.skipped.length, 3);
|
|
3319
|
+
const skippedClassifications = plan.skipped.map((s) => s.classification);
|
|
3320
|
+
assert.ok(skippedClassifications.includes('RUNTIME_DATA'));
|
|
3321
|
+
assert.ok(skippedClassifications.includes('PLATFORM_CONTROLLED'));
|
|
3322
|
+
assert.ok(skippedClassifications.includes('INTERACTION_STATE'));
|
|
3323
|
+
});
|
|
3324
|
+
|
|
3325
|
+
test('ARC v3 Phase 11: data-flow engine unrolls nested member collections (category.products.map) and spread props', () => {
|
|
3326
|
+
const { unrollCollectionSource } = require('../data-flow.cjs');
|
|
3327
|
+
const parseSource = (c) => require('recast').parse(c, { parser: require('recast/parsers/babel-ts') });
|
|
3328
|
+
|
|
3329
|
+
// 1. Nested collection member: category.products.map(...)
|
|
3330
|
+
const code = `category.products.map((product) => <ProductCard key={product.id} {...product} />);`;
|
|
3331
|
+
const ast = parseSource(code);
|
|
3332
|
+
const mapExpr = ast.program.body[0].expression;
|
|
3333
|
+
const calleeObj = mapExpr.callee.object; // category.products
|
|
3334
|
+
|
|
3335
|
+
const unrolled = unrollCollectionSource(calleeObj);
|
|
3336
|
+
assert.equal(unrolled.kind, 'nested-member');
|
|
3337
|
+
assert.equal(unrolled.parentIdentifier, 'category');
|
|
3338
|
+
assert.equal(unrolled.childProp, 'products');
|
|
3339
|
+
assert.equal(unrolled.rootName, 'category.products');
|
|
3340
|
+
|
|
3341
|
+
// 2. Chained filter on nested member: category.products.filter(p => p.active)
|
|
3342
|
+
const chainedCode = `category.products.filter(p => p.active).slice(0, 5);`;
|
|
3343
|
+
const chainedAst = parseSource(chainedCode);
|
|
3344
|
+
const chainedExpr = chainedAst.program.body[0].expression;
|
|
3345
|
+
const chainedUnrolled = unrollCollectionSource(chainedExpr);
|
|
3346
|
+
assert.equal(chainedUnrolled.rootName, 'category.products');
|
|
3347
|
+
assert.ok(chainedUnrolled.chain.includes('slice'));
|
|
3348
|
+
});
|
|
3349
|
+
|
|
3350
|
+
test('ARC v3 Phase 11: data-flow engine resolves renamed destructuring and deep nested destructuring aliases', () => {
|
|
3351
|
+
const { collectVariableAliases } = require('../data-flow.cjs');
|
|
3352
|
+
const parseSource = (c) => require('recast').parse(c, { parser: require('recast/parsers/babel-ts') });
|
|
3353
|
+
|
|
3354
|
+
const code = `
|
|
3355
|
+
const { title: heading, image: heroImg } = hero;
|
|
3356
|
+
const { product: { name: productName, price: productPrice } } = props;
|
|
3357
|
+
`;
|
|
3358
|
+
const ast = parseSource(code);
|
|
3359
|
+
const aliases = collectVariableAliases(ast);
|
|
3360
|
+
|
|
3361
|
+
// 1. Renamed destructuring check
|
|
3362
|
+
const headingAlias = aliases.get('heading');
|
|
3363
|
+
assert.ok(headingAlias);
|
|
3364
|
+
assert.equal(headingAlias.rootName, 'hero');
|
|
3365
|
+
assert.equal(headingAlias.sourceProp, 'title');
|
|
3366
|
+
assert.equal(headingAlias.kind, 'destructured-alias');
|
|
3367
|
+
|
|
3368
|
+
const heroImgAlias = aliases.get('heroImg');
|
|
3369
|
+
assert.ok(heroImgAlias);
|
|
3370
|
+
assert.equal(heroImgAlias.rootName, 'hero');
|
|
3371
|
+
assert.equal(heroImgAlias.sourceProp, 'image');
|
|
3372
|
+
|
|
3373
|
+
// 2. Deep nested destructuring check
|
|
3374
|
+
const nameAlias = aliases.get('productName');
|
|
3375
|
+
assert.ok(nameAlias);
|
|
3376
|
+
assert.equal(nameAlias.rootName, 'props.product');
|
|
3377
|
+
assert.equal(nameAlias.sourceProp, 'name');
|
|
3378
|
+
assert.equal(nameAlias.kind, 'deep-destructured-alias');
|
|
3379
|
+
});
|
|
3380
|
+
|
|
3381
|
+
test('ARC v3 Phase 12: RSC boundary optimizer prevents converting async server components and metadata exporters', () => {
|
|
3382
|
+
const {
|
|
3383
|
+
isAsyncServerComponent,
|
|
3384
|
+
canSafelyInjectClientDirective,
|
|
3385
|
+
calculateMinimalClientBoundary,
|
|
3386
|
+
} = require('../rsc-boundary.cjs');
|
|
3387
|
+
const parseSource = (c) => require('recast').parse(c, { parser: require('recast/parsers/babel-ts') });
|
|
3388
|
+
|
|
3389
|
+
// 1. Async Server Component cannot be converted
|
|
3390
|
+
const asyncCode = `export default async function ProductPage() { return <div>Server Data</div>; }`;
|
|
3391
|
+
const asyncAst = parseSource(asyncCode);
|
|
3392
|
+
assert.equal(isAsyncServerComponent(asyncAst), true);
|
|
3393
|
+
assert.equal(canSafelyInjectClientDirective(asyncAst, 'app/product/[id]/page.tsx', { router: 'next-app', appDir: 'app' }), false);
|
|
3394
|
+
|
|
3395
|
+
// 2. Metadata exporter cannot be converted
|
|
3396
|
+
const metaCode = `
|
|
3397
|
+
export const metadata = { title: 'Store Catalog' };
|
|
3398
|
+
export default function Catalog() { return <div>Catalog</div>; }
|
|
3399
|
+
`;
|
|
3400
|
+
const metaAst = parseSource(metaCode);
|
|
3401
|
+
assert.equal(canSafelyInjectClientDirective(metaAst, 'app/shop/page.tsx', { router: 'next-app', appDir: 'app' }), false);
|
|
3402
|
+
|
|
3403
|
+
// 3. Minimal client boundary calculation
|
|
3404
|
+
assert.equal(calculateMinimalClientBoundary({ isAsync: true, hasHooks: true }), 'server');
|
|
3405
|
+
assert.equal(calculateMinimalClientBoundary({ hasMetadata: true, hasHooks: true }), 'server');
|
|
3406
|
+
assert.equal(calculateMinimalClientBoundary({ isLeaf: true, hasHooks: true }), 'client');
|
|
3407
|
+
assert.equal(calculateMinimalClientBoundary({ isLeaf: false, hasHooks: false }), 'server');
|
|
3408
|
+
});
|
|
3409
|
+
|
|
3410
|
+
test('ARC v3 Phase 12: RSC metrics tracking computes boundary counts and validates conversion threshold', () => {
|
|
3411
|
+
const { computeRscMetrics, validateRscBoundaryIntegrity } = require('../rsc-boundary.cjs');
|
|
3412
|
+
|
|
3413
|
+
const files = [
|
|
3414
|
+
{ file: 'app/layout.tsx', hadClientDirective: false, hasClientDirective: false },
|
|
3415
|
+
{ file: 'app/page.tsx', hadClientDirective: false, hasClientDirective: false },
|
|
3416
|
+
{ file: 'components/Hero.tsx', hadClientDirective: false, hasClientDirective: true },
|
|
3417
|
+
{ file: 'components/ProductCard.tsx', hadClientDirective: false, hasClientDirective: true },
|
|
3418
|
+
{ file: 'components/CartModal.tsx', hadClientDirective: true, hasClientDirective: true },
|
|
3419
|
+
];
|
|
3420
|
+
|
|
3421
|
+
const metrics = computeRscMetrics(files);
|
|
3422
|
+
assert.equal(metrics.clientBoundariesBefore, 1);
|
|
3423
|
+
assert.equal(metrics.clientBoundariesAfter, 3);
|
|
3424
|
+
assert.equal(metrics.serverComponentsConverted, 2);
|
|
3425
|
+
assert.equal(metrics.serverComponentsPreserved, 2);
|
|
3426
|
+
assert.equal(metrics.conversionExceededThreshold, false);
|
|
3427
|
+
|
|
3428
|
+
const validation = validateRscBoundaryIntegrity(metrics, 5);
|
|
3429
|
+
assert.equal(validation.valid, true);
|
|
3430
|
+
|
|
3431
|
+
// Exceed threshold
|
|
3432
|
+
const failValidation = validateRscBoundaryIntegrity(metrics, 1);
|
|
3433
|
+
assert.equal(failValidation.valid, false);
|
|
3434
|
+
assert.ok(failValidation.error.includes('Excessive RSC conversions'));
|
|
3435
|
+
});
|
|
3436
|
+
|
|
3437
|
+
test('ARC v3 Phase 13: validateCollectionOperations verifies runtime add, remove, reorder, and mutation operations', () => {
|
|
3438
|
+
const { validateCollectionOperations } = require('../runtime-validator.cjs');
|
|
3439
|
+
|
|
3440
|
+
const mockSiteData = {
|
|
3441
|
+
content: {
|
|
3442
|
+
home: {
|
|
3443
|
+
products: [
|
|
3444
|
+
{ id: '1', title: 'Product 1', price: 100 },
|
|
3445
|
+
{ id: '2', title: 'Product 2', price: 200 },
|
|
3446
|
+
],
|
|
3447
|
+
testimonials: [
|
|
3448
|
+
{ id: 't1', author: 'Jane Doe', quote: 'Great coffee' },
|
|
3449
|
+
],
|
|
3450
|
+
},
|
|
3451
|
+
},
|
|
3452
|
+
};
|
|
3453
|
+
|
|
3454
|
+
const report = validateCollectionOperations(mockSiteData);
|
|
3455
|
+
assert.equal(report.passed, true);
|
|
3456
|
+
assert.equal(report.totalCollections, 2);
|
|
3457
|
+
|
|
3458
|
+
const prodCol = report.collections.find((c) => c.path === 'home.products');
|
|
3459
|
+
assert.ok(prodCol);
|
|
3460
|
+
assert.equal(prodCol.operations.addItem, true);
|
|
3461
|
+
assert.equal(prodCol.operations.removeItem, true);
|
|
3462
|
+
assert.equal(prodCol.operations.reorderItems, true);
|
|
3463
|
+
assert.equal(prodCol.operations.mutateItemField, true);
|
|
3464
|
+
assert.equal(prodCol.passed, true);
|
|
3465
|
+
});
|
|
3466
|
+
|
|
3467
|
+
test('ARC v3 Phase 13: 12-Gate Acceptance Matrix evaluates critical gates, thresholds, and incomplete/failed statuses', () => {
|
|
3468
|
+
const { evaluateAcceptanceGates } = require('../acceptance-gates.cjs');
|
|
3469
|
+
|
|
3470
|
+
// 1. All Gates Passing
|
|
3471
|
+
const passingMetrics = {
|
|
3472
|
+
sourceAnalysis: { passed: true },
|
|
3473
|
+
astTransformation: { passed: true },
|
|
3474
|
+
typescriptValidation: { passed: true },
|
|
3475
|
+
buildSuccess: { passed: true },
|
|
3476
|
+
fivoraAudit: { passed: true },
|
|
3477
|
+
manifestValid: true,
|
|
3478
|
+
runtimeEditabilityScore: 99.5,
|
|
3479
|
+
collectionOpsPassed: true,
|
|
3480
|
+
imageEditingPassed: true,
|
|
3481
|
+
routeCoveragePassed: true,
|
|
3482
|
+
designPreservationScore: 99.1,
|
|
3483
|
+
controlOnlyValid: true,
|
|
3484
|
+
editabilityCoverage: 99.2,
|
|
3485
|
+
};
|
|
3486
|
+
const passRes = evaluateAcceptanceGates(passingMetrics);
|
|
3487
|
+
assert.equal(passRes.passed, true);
|
|
3488
|
+
assert.equal(passRes.status, 'CONVERSION_PASSED');
|
|
3489
|
+
assert.equal(passRes.allCriticalPassed, true);
|
|
3490
|
+
|
|
3491
|
+
// 2. Critical Gate Failure (Fivora Contract) -> CONVERSION_FAILED
|
|
3492
|
+
const failMetrics = {
|
|
3493
|
+
...passingMetrics,
|
|
3494
|
+
fivoraAudit: { passed: false },
|
|
3495
|
+
};
|
|
3496
|
+
const failRes = evaluateAcceptanceGates(failMetrics);
|
|
3497
|
+
assert.equal(failRes.passed, false);
|
|
3498
|
+
assert.equal(failRes.status, 'CONVERSION_FAILED');
|
|
3499
|
+
assert.equal(failRes.allCriticalPassed, false);
|
|
3500
|
+
|
|
3501
|
+
// 3. Sub-Threshold Coverage (e.g. 96%) -> CONVERSION_INCOMPLETE
|
|
3502
|
+
const incompleteMetrics = {
|
|
3503
|
+
...passingMetrics,
|
|
3504
|
+
editabilityCoverage: 96.0,
|
|
3505
|
+
};
|
|
3506
|
+
const incRes = evaluateAcceptanceGates(incompleteMetrics);
|
|
3507
|
+
assert.equal(incRes.passed, false);
|
|
3508
|
+
assert.equal(incRes.status, 'CONVERSION_INCOMPLETE');
|
|
3509
|
+
assert.equal(incRes.allCriticalPassed, true);
|
|
3510
|
+
});
|
|
3511
|
+
|
|
3512
|
+
test('ARC v3 Phase 15: calculateVisualPreservation computes >= 98% score across mobile, tablet, and desktop viewports', () => {
|
|
3513
|
+
const { calculateVisualPreservation } = require('../visual-regression.cjs');
|
|
3514
|
+
|
|
3515
|
+
const beforeCode = `
|
|
3516
|
+
export function Hero() {
|
|
3517
|
+
return (
|
|
3518
|
+
<section className="flex flex-col md:grid md:grid-cols-2 gap-8 p-12 bg-white">
|
|
3519
|
+
<h1 className="text-4xl font-bold">Original Hero Title</h1>
|
|
3520
|
+
<p className="text-lg text-gray-600">Subtitle copy text</p>
|
|
3521
|
+
<button className="btn flex items-center gap-2">
|
|
3522
|
+
<svg className="w-5 h-5" />
|
|
3523
|
+
<span>Discover Products</span>
|
|
3524
|
+
</button>
|
|
3525
|
+
</section>
|
|
3526
|
+
);
|
|
3527
|
+
}
|
|
3528
|
+
`;
|
|
3529
|
+
|
|
3530
|
+
const afterCode = `
|
|
3531
|
+
export function Hero() {
|
|
3532
|
+
return (
|
|
3533
|
+
<section className="flex flex-col md:grid md:grid-cols-2 gap-8 p-12 bg-white">
|
|
3534
|
+
<h1 data-preview-field-path="home.hero.title" className="text-4xl font-bold">
|
|
3535
|
+
{siteData?.content?.home?.hero?.title ?? "Original Hero Title"}
|
|
3536
|
+
</h1>
|
|
3537
|
+
<p data-preview-field-path="home.hero.subtitle" className="text-lg text-gray-600">
|
|
3538
|
+
{siteData?.content?.home?.hero?.subtitle ?? "Subtitle copy text"}
|
|
3539
|
+
</p>
|
|
3540
|
+
<button className="btn flex items-center gap-2">
|
|
3541
|
+
<svg className="w-5 h-5" />
|
|
3542
|
+
<span data-preview-field-path="home.hero.cta">
|
|
3543
|
+
{siteData?.content?.home?.hero?.cta ?? "Discover Products"}
|
|
3544
|
+
</span>
|
|
3545
|
+
</button>
|
|
3546
|
+
</section>
|
|
3547
|
+
);
|
|
3548
|
+
}
|
|
3549
|
+
`;
|
|
3550
|
+
|
|
3551
|
+
const report = calculateVisualPreservation(beforeCode, afterCode);
|
|
3552
|
+
assert.equal(report.passed, true);
|
|
3553
|
+
assert.ok(report.overallPreservationScore >= 98.0);
|
|
3554
|
+
assert.ok(report.viewports.mobile.preservationScore >= 98.0);
|
|
3555
|
+
assert.ok(report.viewports.tablet.preservationScore >= 98.0);
|
|
3556
|
+
assert.ok(report.viewports.desktop.preservationScore >= 98.0);
|
|
3557
|
+
assert.equal(report.blockingIssues.length, 0);
|
|
3558
|
+
});
|
|
3559
|
+
|
|
3560
|
+
test('ARC v3 Phase 15: calculateVisualPreservation detects dropped icons or layout shifts as blocking issues', () => {
|
|
3561
|
+
const { calculateVisualPreservation } = require('../visual-regression.cjs');
|
|
3562
|
+
|
|
3563
|
+
const beforeCode = `
|
|
3564
|
+
export function ButtonWithIcon() {
|
|
3565
|
+
return (
|
|
3566
|
+
<button className="btn">
|
|
3567
|
+
<svg className="icon" />
|
|
3568
|
+
<span>Add to Cart</span>
|
|
3569
|
+
</button>
|
|
3570
|
+
);
|
|
3571
|
+
}
|
|
3572
|
+
`;
|
|
3573
|
+
|
|
3574
|
+
const brokenAfterCode = `
|
|
3575
|
+
export function ButtonWithIcon() {
|
|
3576
|
+
return (
|
|
3577
|
+
<button className="btn">
|
|
3578
|
+
<span>Add to Cart</span>
|
|
3579
|
+
</button>
|
|
3580
|
+
);
|
|
3581
|
+
}
|
|
3582
|
+
`;
|
|
3583
|
+
|
|
3584
|
+
const report = calculateVisualPreservation(beforeCode, brokenAfterCode);
|
|
3585
|
+
assert.equal(report.passed, false);
|
|
3586
|
+
assert.ok(report.blockingIssues.length > 0);
|
|
3587
|
+
assert.ok(report.blockingIssues[0].includes('SVG or icon dropped'));
|
|
3588
|
+
});
|
|
3589
|
+
|
|
3590
|
+
test('ARC v3 Phase 15: verifyInteractionsSync validates mobile nav toggle, accordions, tabs, and carousels', () => {
|
|
3591
|
+
const { verifyInteractionsSync, INTERACTIVE_PATTERNS } = require('../interaction-verifier.cjs');
|
|
3592
|
+
|
|
3593
|
+
const files = [
|
|
3594
|
+
{
|
|
3595
|
+
file: 'components/Header.tsx',
|
|
3596
|
+
code: `
|
|
3597
|
+
export function Header() {
|
|
3598
|
+
const [open, setOpen] = useState(false);
|
|
3599
|
+
return (
|
|
3600
|
+
<header>
|
|
3601
|
+
<button aria-label="Toggle Menu" onClick={() => setOpen(!open)}>
|
|
3602
|
+
<span>Menu</span>
|
|
3603
|
+
</button>
|
|
3604
|
+
</header>
|
|
3605
|
+
);
|
|
3606
|
+
}
|
|
3607
|
+
`,
|
|
3608
|
+
},
|
|
3609
|
+
{
|
|
3610
|
+
file: 'components/FaqSection.tsx',
|
|
3611
|
+
code: `export function Faq() { return <AccordionTrigger>Item</AccordionTrigger>; }`,
|
|
3612
|
+
},
|
|
3613
|
+
{
|
|
3614
|
+
file: 'components/ProductTabs.tsx',
|
|
3615
|
+
code: `export function Tabs() { return <TabsTrigger value="one">Tab</TabsTrigger>; }`,
|
|
3616
|
+
},
|
|
3617
|
+
{
|
|
3618
|
+
file: 'components/Slider.tsx',
|
|
3619
|
+
code: `export function Slider() { return <Swiper><SwiperSlide>Slide 1</SwiperSlide></Swiper>; }`,
|
|
3620
|
+
},
|
|
3621
|
+
];
|
|
3622
|
+
|
|
3623
|
+
const report = verifyInteractionsSync({ files });
|
|
3624
|
+
assert.equal(report.passed, true);
|
|
3625
|
+
assert.equal(report.interactionScore, 100.0);
|
|
3626
|
+
assert.equal(report.totalTested, 4);
|
|
3627
|
+
assert.equal(report.failedTests, 0);
|
|
3628
|
+
|
|
3629
|
+
const patterns = report.patterns.map((p) => p.pattern);
|
|
3630
|
+
assert.ok(patterns.includes(INTERACTIVE_PATTERNS.NAVBAR_MOBILE_TOGGLE));
|
|
3631
|
+
assert.ok(patterns.includes(INTERACTIVE_PATTERNS.ACCORDION));
|
|
3632
|
+
assert.ok(patterns.includes(INTERACTIVE_PATTERNS.TABS));
|
|
3633
|
+
assert.ok(patterns.includes(INTERACTIVE_PATTERNS.CAROUSEL));
|
|
3634
|
+
});
|
|
3635
|
+
|
|
3636
|
+
function createMockCompliantProject() {
|
|
3637
|
+
const fs = require('fs');
|
|
3638
|
+
const os = require('os');
|
|
3639
|
+
const path = require('path');
|
|
3640
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'deneb-compliant-'));
|
|
3641
|
+
const appDir = path.join(tempDir, 'src', 'app');
|
|
3642
|
+
const dataDir = path.join(tempDir, 'src', 'data');
|
|
3643
|
+
fs.mkdirSync(appDir, { recursive: true });
|
|
3644
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
3645
|
+
fs.writeFileSync(path.join(tempDir, 'package.json'), JSON.stringify({ name: 'sample', dependencies: { next: '15.0.0' } }));
|
|
3646
|
+
fs.writeFileSync(path.join(tempDir, 'fivora-template.json'), JSON.stringify({
|
|
3647
|
+
framework: 'nextjs-static-export',
|
|
3648
|
+
version: 2,
|
|
3649
|
+
arcVersion: '2.1.0',
|
|
3650
|
+
schemaVersion: 2,
|
|
3651
|
+
siteDataFile: 'src/data/site-data.json',
|
|
3652
|
+
pages: [{ id: 'home', label: 'Home', route: '/', required: true }],
|
|
3653
|
+
editorSchema: {
|
|
3654
|
+
version: 1,
|
|
3655
|
+
sections: [
|
|
3656
|
+
{ id: 'home', path: 'home', type: 'object', label: 'Home', fields: [{ key: 'heroTitle', type: 'text' }] }
|
|
3657
|
+
]
|
|
3658
|
+
},
|
|
3659
|
+
visualEditing: {
|
|
3660
|
+
contractVersion: 1,
|
|
3661
|
+
mode: 'strict',
|
|
3662
|
+
controlOnlyPaths: []
|
|
3663
|
+
}
|
|
3664
|
+
}, null, 2));
|
|
3665
|
+
fs.writeFileSync(path.join(dataDir, 'site-data.json'), JSON.stringify({
|
|
3666
|
+
content: {
|
|
3667
|
+
home: { heroTitle: 'Welcome' }
|
|
3668
|
+
}
|
|
3669
|
+
}, null, 2));
|
|
3670
|
+
fs.writeFileSync(path.join(appDir, 'layout.tsx'), 'export default function RootLayout({ children }: any) { return <html><body>{children}</body></html>; }');
|
|
3671
|
+
fs.writeFileSync(path.join(appDir, 'page.tsx'), 'export default function HomePage() { return <main data-preview-page-id="home"><h1 data-preview-field-path="home.heroTitle">Welcome</h1></main>; }');
|
|
3672
|
+
|
|
3673
|
+
return {
|
|
3674
|
+
dir: tempDir,
|
|
3675
|
+
cleanup: () => fs.rmSync(tempDir, { recursive: true, force: true }),
|
|
3676
|
+
};
|
|
3677
|
+
}
|
|
3678
|
+
|
|
3679
|
+
test('ARC v3 Phase 16: auditStorefrontTemplate verifies real-world storefront against 12-Gate Acceptance Matrix', () => {
|
|
3680
|
+
const { auditStorefrontTemplate, resolveWorkspaceStorefrontsDir } = require('../corpus-verifier.cjs');
|
|
3681
|
+
const path = require('path');
|
|
3682
|
+
const fs = require('fs');
|
|
3683
|
+
const baseDir = resolveWorkspaceStorefrontsDir();
|
|
3684
|
+
const coffeeDir = path.join(baseDir, 'coffee');
|
|
3685
|
+
const hasRealStorefront = fs.existsSync(coffeeDir) && fs.existsSync(path.join(coffeeDir, 'fivora-template.json'));
|
|
3686
|
+
|
|
3687
|
+
let targetDir = coffeeDir;
|
|
3688
|
+
let cleanup = null;
|
|
3689
|
+
if (!hasRealStorefront) {
|
|
3690
|
+
const fixture = createMockCompliantProject();
|
|
3691
|
+
targetDir = fixture.dir;
|
|
3692
|
+
cleanup = fixture.cleanup;
|
|
3693
|
+
}
|
|
3694
|
+
|
|
3695
|
+
try {
|
|
3696
|
+
const result = auditStorefrontTemplate(targetDir, { templateId: 'coffee', templateName: 'Coffee Shop' });
|
|
3697
|
+
assert.equal(result.validStructure, true);
|
|
3698
|
+
assert.equal(result.fivoraContract.passed, true);
|
|
3699
|
+
assert.equal(result.fivoraContract.violationCount, 0);
|
|
3700
|
+
assert.equal(result.runtimeEditability.passed, true);
|
|
3701
|
+
assert.equal(result.collectionOperations.passed, true);
|
|
3702
|
+
assert.equal(result.acceptanceGates.passed, true);
|
|
3703
|
+
assert.equal(result.acceptanceGates.status, 'CONVERSION_PASSED');
|
|
3704
|
+
assert.equal(result.passed, true);
|
|
3705
|
+
} finally {
|
|
3706
|
+
if (cleanup) cleanup();
|
|
3707
|
+
}
|
|
3708
|
+
});
|
|
3709
|
+
|
|
3710
|
+
test('ARC v3 Phase 16: verifyStorefrontCorpus audits all real-world storefronts and blocks invalid conversions', () => {
|
|
3711
|
+
const { verifyStorefrontCorpus, resolveWorkspaceStorefrontsDir } = require('../corpus-verifier.cjs');
|
|
3712
|
+
const fs = require('fs');
|
|
3713
|
+
const path = require('path');
|
|
3714
|
+
|
|
3715
|
+
const baseDir = resolveWorkspaceStorefrontsDir();
|
|
3716
|
+
const hasWorkspaceCorpus = fs.existsSync(path.join(baseDir, 'coffee')) && fs.existsSync(path.join(baseDir, 'car-sale'));
|
|
3717
|
+
|
|
3718
|
+
const report = verifyStorefrontCorpus();
|
|
3719
|
+
assert.equal(report.totalTemplates, 6);
|
|
3720
|
+
|
|
3721
|
+
if (hasWorkspaceCorpus) {
|
|
3722
|
+
// When run in full workspace with all 6 storefronts available
|
|
3723
|
+
assert.equal(report.passedTemplates, 6);
|
|
3724
|
+
assert.equal(report.failedTemplates, 0);
|
|
3725
|
+
assert.equal(report.corpusSuccessRate, 100);
|
|
3726
|
+
assert.equal(report.allTemplatesPassed, true);
|
|
3727
|
+
|
|
3728
|
+
// All 6 real-world storefronts pass with 100% compliance
|
|
3729
|
+
for (const result of report.results) {
|
|
3730
|
+
assert.ok(result.passed, `Storefront ${result.templateId} must pass 100%`);
|
|
3731
|
+
assert.equal(result.acceptanceGates.status, 'CONVERSION_PASSED');
|
|
3732
|
+
}
|
|
3733
|
+
} else {
|
|
3734
|
+
// In standalone CI environment without sibling storefront repositories,
|
|
3735
|
+
// verify graceful non-crashing handling
|
|
3736
|
+
assert.ok(report.results.length === 6);
|
|
3737
|
+
assert.equal(report.failedTemplates, 6);
|
|
3738
|
+
assert.equal(report.allTemplatesPassed, false);
|
|
3739
|
+
for (const result of report.results) {
|
|
3740
|
+
assert.equal(result.passed, false);
|
|
3741
|
+
assert.equal(result.acceptanceGates.status, 'CONVERSION_FAILED');
|
|
3742
|
+
assert.ok(result.fivoraContract.errors.length > 0);
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3745
|
+
});
|
|
3746
|
+
|
|
3747
|
+
test('ARC v3 Phase 17: applyMutation generates AST mutations and testMutationResilience prevents unhandled compiler crashes', () => {
|
|
3748
|
+
const {
|
|
3749
|
+
applyMutation,
|
|
3750
|
+
testMutationResilience,
|
|
3751
|
+
DEFAULT_SAMPLE_COMPONENT,
|
|
3752
|
+
} = require('../fuzz-engine.cjs');
|
|
3753
|
+
|
|
3754
|
+
// Test spread mutation
|
|
3755
|
+
const spread = applyMutation(DEFAULT_SAMPLE_COMPONENT, 'SPREAD_PROPS');
|
|
3756
|
+
assert.equal(spread.applied, true);
|
|
3757
|
+
const spreadRes = testMutationResilience(spread.mutatedCode, 'SPREAD_PROPS');
|
|
3758
|
+
assert.equal(spreadRes.crashed, false);
|
|
3759
|
+
assert.equal(spreadRes.resilient, true);
|
|
3760
|
+
assert.equal(spreadRes.outputValidSyntax, true);
|
|
3761
|
+
|
|
3762
|
+
// Test corrupt syntax recovery (must catch cleanly without dying)
|
|
3763
|
+
const corrupt = applyMutation(DEFAULT_SAMPLE_COMPONENT, 'CORRUPT_SYNTAX');
|
|
3764
|
+
assert.equal(corrupt.applied, true);
|
|
3765
|
+
const corruptRes = testMutationResilience(corrupt.mutatedCode, 'CORRUPT_SYNTAX');
|
|
3766
|
+
assert.equal(corruptRes.crashed, false);
|
|
3767
|
+
assert.equal(corruptRes.status, 'PARSER_REJECTED_CLEANLY');
|
|
3768
|
+
assert.equal(corruptRes.resilient, true);
|
|
3769
|
+
});
|
|
3770
|
+
|
|
3771
|
+
test('ARC v3 Phase 17: runFuzzHarness achieves 100% resilience score across all mutation strategies', () => {
|
|
3772
|
+
const { runFuzzHarness, MUTATION_TYPES } = require('../fuzz-engine.cjs');
|
|
3773
|
+
|
|
3774
|
+
const report = runFuzzHarness();
|
|
3775
|
+
assert.equal(report.totalMutations, MUTATION_TYPES.length);
|
|
3776
|
+
assert.equal(report.crashedCount, 0, 'Must have zero unhandled crashes');
|
|
3777
|
+
assert.equal(report.syntaxErrorsInOutput, 0, 'Must have zero syntax errors in output');
|
|
3778
|
+
assert.equal(report.resilienceScore, 100.0, 'Resilience score must be 100%');
|
|
3779
|
+
assert.equal(report.allResilient, true);
|
|
3780
|
+
});
|
|
3781
|
+
|
|
3782
|
+
test('ARC v3 Phase 18: analyzeBlockedProject identifies exact unmapped fields, files, and line numbers for blocked conversions', () => {
|
|
3783
|
+
const { analyzeBlockedProject } = require('../explain-blocked.cjs');
|
|
3784
|
+
const fs = require('fs');
|
|
3785
|
+
const os = require('os');
|
|
3786
|
+
const path = require('path');
|
|
3787
|
+
|
|
3788
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'deneb-blocked-'));
|
|
3789
|
+
const compDir = path.join(tempDir, 'src', 'components');
|
|
3790
|
+
const dataDir = path.join(tempDir, 'src', 'data');
|
|
3791
|
+
fs.mkdirSync(compDir, { recursive: true });
|
|
3792
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
3793
|
+
fs.writeFileSync(path.join(dataDir, 'site-data.json'), JSON.stringify({ content: { home: { unmappedField: 'Hello' } } }));
|
|
3794
|
+
fs.writeFileSync(path.join(tempDir, 'fivora-template.json'), JSON.stringify({
|
|
3795
|
+
framework: 'nextjs-static-export',
|
|
3796
|
+
siteDataFile: 'src/data/site-data.json',
|
|
3797
|
+
editorSchema: {
|
|
3798
|
+
sections: [{ id: 'home', path: 'home', type: 'object', fields: [{ key: 'unmappedField', type: 'currency' }] }]
|
|
3799
|
+
}
|
|
3800
|
+
}));
|
|
3801
|
+
fs.writeFileSync(path.join(compDir, 'Showcase.tsx'), 'export function Showcase() {\n return <div><span>Hello</span></div>;\n}');
|
|
3802
|
+
|
|
3803
|
+
try {
|
|
3804
|
+
const report = analyzeBlockedProject(tempDir);
|
|
3805
|
+
assert.equal(report.blocked, true);
|
|
3806
|
+
assert.equal(report.totalBlockingIssues, 2);
|
|
3807
|
+
|
|
3808
|
+
const finding = report.findings.find((f) => f.ruleId === 'FIVORA_UNMAPPED_FIELD');
|
|
3809
|
+
assert.ok(finding);
|
|
3810
|
+
assert.ok(finding.file.includes('Showcase.tsx'));
|
|
3811
|
+
assert.ok(finding.line >= 1);
|
|
3812
|
+
assert.ok(finding.remediation.includes('data-preview-field-path') || finding.remediation.includes('controlOnlyPaths'));
|
|
3813
|
+
|
|
3814
|
+
const unsupported = report.findings.find((f) => f.ruleId === 'SCHEMA_UNSUPPORTED_TYPE');
|
|
3815
|
+
assert.ok(unsupported);
|
|
3816
|
+
assert.ok(unsupported.message.includes('currency'));
|
|
3817
|
+
} finally {
|
|
3818
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
3819
|
+
}
|
|
3820
|
+
});
|
|
3821
|
+
|
|
3822
|
+
test('ARC v3 Phase 18: formatBlockedExplanationTerminal renders actionable remediation guide with zero false positives', () => {
|
|
3823
|
+
const { analyzeBlockedProject, formatBlockedExplanationTerminal } = require('../explain-blocked.cjs');
|
|
3824
|
+
const { resolveWorkspaceStorefrontsDir } = require('../corpus-verifier.cjs');
|
|
3825
|
+
const fs = require('fs');
|
|
3826
|
+
const os = require('os');
|
|
3827
|
+
const path = require('path');
|
|
3828
|
+
|
|
3829
|
+
const baseDir = resolveWorkspaceStorefrontsDir();
|
|
3830
|
+
const coffeeDir = path.join(baseDir, 'coffee');
|
|
3831
|
+
const hasRealCoffee = fs.existsSync(coffeeDir) && fs.existsSync(path.join(coffeeDir, 'fivora-template.json'));
|
|
3832
|
+
|
|
3833
|
+
let compliantDir = coffeeDir;
|
|
3834
|
+
let compliantCleanup = null;
|
|
3835
|
+
if (!hasRealCoffee) {
|
|
3836
|
+
const fixture = createMockCompliantProject();
|
|
3837
|
+
compliantDir = fixture.dir;
|
|
3838
|
+
compliantCleanup = fixture.cleanup;
|
|
3839
|
+
}
|
|
3840
|
+
|
|
3841
|
+
try {
|
|
3842
|
+
// Compliant project formats clean success
|
|
3843
|
+
const coffeeReport = analyzeBlockedProject(compliantDir);
|
|
3844
|
+
assert.equal(coffeeReport.blocked, false);
|
|
3845
|
+
const coffeeText = formatBlockedExplanationTerminal(coffeeReport);
|
|
3846
|
+
assert.ok(coffeeText.includes('CONVERSION_PASSED'));
|
|
3847
|
+
assert.ok(coffeeText.includes('No blocking issues detected'));
|
|
3848
|
+
} finally {
|
|
3849
|
+
if (compliantCleanup) compliantCleanup();
|
|
3850
|
+
}
|
|
3851
|
+
|
|
3852
|
+
// Synthetic blocked project formats detailed remediation
|
|
3853
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'deneb-blocked-ui-'));
|
|
3854
|
+
const compDir = path.join(tempDir, 'src', 'components');
|
|
3855
|
+
const dataDir = path.join(tempDir, 'src', 'data');
|
|
3856
|
+
fs.mkdirSync(compDir, { recursive: true });
|
|
3857
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
3858
|
+
fs.writeFileSync(path.join(dataDir, 'site-data.json'), JSON.stringify({ content: { home: { testField: 'Text' } } }));
|
|
3859
|
+
fs.writeFileSync(path.join(tempDir, 'fivora-template.json'), JSON.stringify({
|
|
3860
|
+
framework: 'nextjs-static-export',
|
|
3861
|
+
siteDataFile: 'src/data/site-data.json',
|
|
3862
|
+
editorSchema: {
|
|
3863
|
+
sections: [{ id: 'home', path: 'home', type: 'object', fields: [{ key: 'testField', type: 'currency' }] }]
|
|
3864
|
+
}
|
|
3865
|
+
}));
|
|
3866
|
+
fs.writeFileSync(path.join(compDir, 'Comp.tsx'), 'export function Comp() { return <div>Text</div>; }');
|
|
3867
|
+
|
|
3868
|
+
try {
|
|
3869
|
+
const blockedReport = analyzeBlockedProject(tempDir);
|
|
3870
|
+
assert.equal(blockedReport.blocked, true);
|
|
3871
|
+
const blockedText = formatBlockedExplanationTerminal(blockedReport);
|
|
3872
|
+
assert.ok(blockedText.includes('DENEB CONVERSION BLOCKED'));
|
|
3873
|
+
assert.ok(blockedText.includes('SCHEMA_UNSUPPORTED_TYPE'));
|
|
3874
|
+
assert.ok(blockedText.includes('Fix:'));
|
|
3875
|
+
} finally {
|
|
3876
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
3877
|
+
}
|
|
3878
|
+
});
|
|
3879
|
+
|
|
3880
|
+
|
|
3881
|
+
|
|
3882
|
+
|
|
3883
|
+
|
|
2962
3884
|
|
|
2963
3885
|
|
|
2964
3886
|
|