@decantr/essence-spec 1.0.7 → 2.0.0

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/dist/index.js CHANGED
@@ -112,8 +112,11 @@ function getColumnPreset(col) {
112
112
  if (typeof col === "string") return void 0;
113
113
  return col.preset;
114
114
  }
115
- function isV3(essence) {
116
- return (essence.version === "3.0.0" || essence.version === "3.1.0") && "dna" in essence && "blueprint" in essence;
115
+ function isV4(essence) {
116
+ return typeof essence === "object" && essence !== null && essence.version === "4.0.0" && "dna" in essence && "blueprint" in essence;
117
+ }
118
+ function isLegacyV3(essence) {
119
+ return typeof essence === "object" && essence !== null && (essence.version === "3.0.0" || essence.version === "3.1.0") && "dna" in essence && "blueprint" in essence;
117
120
  }
118
121
  function flattenPages(blueprint) {
119
122
  if (blueprint.sections && blueprint.sections.length > 0) {
@@ -127,26 +130,19 @@ function flattenPages(blueprint) {
127
130
  return blueprint.pages ?? [];
128
131
  }
129
132
  function isSectioned(essence) {
130
- if (isV3(essence)) return false;
131
- return "sections" in essence && Array.isArray(essence.sections);
133
+ if (isV4(essence)) return false;
134
+ return typeof essence === "object" && essence !== null && "sections" in essence && Array.isArray(essence.sections);
132
135
  }
133
136
  function isSimple(essence) {
134
- if (isV3(essence)) return false;
135
- return "archetype" in essence && !("sections" in essence);
137
+ if (isV4(essence)) return false;
138
+ return typeof essence === "object" && essence !== null && "archetype" in essence && !("sections" in essence);
136
139
  }
137
140
 
138
141
  // src/guard.ts
139
142
  function checkThemeModeCompatibility(essence, context) {
140
143
  if (!context.themeRegistry) return null;
141
- let themeId;
142
- let mode;
143
- if (isV3(essence)) {
144
- themeId = essence.dna.theme?.id ?? null;
145
- mode = essence.dna.theme?.mode ?? null;
146
- } else {
147
- themeId = isSimple(essence) ? essence.theme?.id : null;
148
- mode = isSimple(essence) ? essence.theme?.mode : null;
149
- }
144
+ const themeId = essence.dna.theme?.id ?? null;
145
+ const mode = essence.dna.theme?.mode ?? null;
150
146
  if (!themeId || !mode) return null;
151
147
  const theme = context.themeRegistry.get(themeId);
152
148
  if (!theme) {
@@ -160,7 +156,8 @@ function checkThemeModeCompatibility(essence, context) {
160
156
  severity: "error",
161
157
  message: `Theme "${themeId}" does not support "${mode}" mode. Supported modes: ${supportedModes}.`,
162
158
  suggestion,
163
- ...isV3(essence) ? { layer: "dna", autoFixable: false } : {}
159
+ layer: "dna",
160
+ autoFixable: false
164
161
  };
165
162
  }
166
163
  return null;
@@ -213,17 +210,18 @@ function checkPatternExistence(essence, context) {
213
210
  const suggestion = similar.length > 0 ? `Similar patterns: ${similar.join(", ")}` : `Run "decantr search ${patternId}" to find alternatives.`;
214
211
  violations.push({
215
212
  rule: "pattern-exists",
216
- severity: isV3(essence) ? "warning" : "error",
213
+ severity: "warning",
217
214
  message: `Pattern "${patternId}" is referenced but does not exist in the registry.`,
218
215
  suggestion,
219
- ...isV3(essence) ? { layer: "blueprint", autoFixable: false } : {}
216
+ layer: "blueprint",
217
+ autoFixable: false
220
218
  });
221
219
  }
222
220
  }
223
221
  return violations;
224
222
  }
225
223
  function checkAccessibility(essence, context) {
226
- const accessibility = isV3(essence) ? essence.dna.accessibility : "accessibility" in essence ? essence.accessibility : void 0;
224
+ const accessibility = essence.dna.accessibility;
227
225
  if (!accessibility?.wcag_level || accessibility.wcag_level === "none") {
228
226
  return null;
229
227
  }
@@ -235,7 +233,8 @@ function checkAccessibility(essence, context) {
235
233
  severity: "error",
236
234
  message: `WCAG ${accessibility.wcag_level} compliance required. Issues found: ${issueList}${moreCount}`,
237
235
  suggestion: "Fix accessibility issues before proceeding. Run an accessibility audit for details.",
238
- ...isV3(essence) ? { layer: "dna", autoFixable: false } : {}
236
+ layer: "dna",
237
+ autoFixable: false
239
238
  };
240
239
  }
241
240
  return null;
@@ -244,8 +243,7 @@ function checkInteractions(essence, context) {
244
243
  if (!context.interaction_issues || context.interaction_issues.length === 0) {
245
244
  return null;
246
245
  }
247
- const guard = isV3(essence) ? essence.meta.guard : null;
248
- if (!guard) return null;
246
+ const guard = essence.meta.guard;
249
247
  let enforcement;
250
248
  if (guard.interactions_enforcement) {
251
249
  enforcement = guard.interactions_enforcement;
@@ -269,7 +267,7 @@ function checkInteractions(essence, context) {
269
267
  };
270
268
  }
271
269
  function evaluateGuard(essence, context = {}) {
272
- const guard = isV3(essence) ? essence.meta.guard : essence.guard;
270
+ const guard = essence.meta.guard;
273
271
  if (guard.mode === "creative") {
274
272
  return [];
275
273
  }
@@ -277,19 +275,14 @@ function evaluateGuard(essence, context = {}) {
277
275
  const isStrict = guard.mode === "strict";
278
276
  const requestedTheme = context.theme ?? context.style;
279
277
  if (requestedTheme) {
280
- let essenceThemeId;
281
- if (isV3(essence)) {
282
- essenceThemeId = essence.dna.theme.id;
283
- } else {
284
- essenceThemeId = isSimple(essence) ? essence.theme.id : null;
285
- }
286
- const enforceStyle = isV3(essence) ? true : guard.enforce_style !== false;
287
- if (essenceThemeId && requestedTheme !== essenceThemeId && enforceStyle) {
278
+ const essenceThemeId = essence.dna.theme.id;
279
+ if (essenceThemeId && requestedTheme !== essenceThemeId) {
288
280
  violations.push({
289
281
  rule: "theme",
290
282
  severity: "error",
291
283
  message: `Theme "${requestedTheme}" does not match essence theme "${essenceThemeId}". Change the essence theme first.`,
292
- ...isV3(essence) ? { layer: "dna", autoFixable: false } : {}
284
+ layer: "dna",
285
+ autoFixable: false
293
286
  });
294
287
  }
295
288
  }
@@ -299,13 +292,11 @@ function evaluateGuard(essence, context = {}) {
299
292
  if (!pageExists) {
300
293
  violations.push({
301
294
  rule: "structure",
302
- severity: isV3(essence) ? "warning" : "error",
295
+ severity: "warning",
303
296
  message: `Page "${context.pageId}" does not exist in essence structure. Add it to the essence first.`,
304
- ...isV3(essence) ? {
305
- layer: "blueprint",
306
- autoFixable: true,
307
- autoFix: { type: "add_page", patch: { id: context.pageId } }
308
- } : {}
297
+ layer: "blueprint",
298
+ autoFixable: true,
299
+ autoFix: { type: "add_page", patch: { id: context.pageId } }
309
300
  });
310
301
  }
311
302
  }
@@ -321,34 +312,28 @@ function evaluateGuard(essence, context = {}) {
321
312
  if (!matches) {
322
313
  violations.push({
323
314
  rule: "layout",
324
- severity: isV3(essence) ? "warning" : "error",
315
+ severity: "warning",
325
316
  message: `Layout for page "${context.pageId}" deviates from essence. Expected: [${essenceLayout.join(", ")}].`,
326
- ...isV3(essence) ? {
327
- layer: "blueprint",
328
- autoFixable: true,
329
- autoFix: {
330
- type: "update_layout",
331
- patch: { page: context.pageId, layout: context.layout }
332
- }
333
- } : {}
317
+ layer: "blueprint",
318
+ autoFixable: true,
319
+ autoFix: {
320
+ type: "update_layout",
321
+ patch: { page: context.pageId, layout: context.layout }
322
+ }
334
323
  });
335
324
  }
336
325
  }
337
326
  }
338
327
  if (isStrict && context.density_gap) {
339
- let expectedGap;
340
- if (isV3(essence)) {
341
- const overriddenDensity = getPageDensityOverride(essence, context.pageId);
342
- expectedGap = overriddenDensity ?? essence.dna.spacing.content_gap;
343
- } else {
344
- expectedGap = essence.density.content_gap;
345
- }
328
+ const overriddenDensity = getPageDensityOverride(essence, context.pageId);
329
+ const expectedGap = overriddenDensity ?? essence.dna.spacing.content_gap;
346
330
  if (context.density_gap !== expectedGap) {
347
331
  violations.push({
348
332
  rule: "density",
349
333
  severity: "warning",
350
334
  message: `Content gap "${context.density_gap}" does not match essence density "${expectedGap}".`,
351
- ...isV3(essence) ? { layer: "dna", autoFixable: false } : {}
335
+ layer: "dna",
336
+ autoFixable: false
352
337
  });
353
338
  }
354
339
  }
@@ -366,37 +351,30 @@ function evaluateGuard(essence, context = {}) {
366
351
  if (interactionsViolation) {
367
352
  violations.push(interactionsViolation);
368
353
  }
369
- if (isV3(essence)) {
370
- const v3Guard = guard;
371
- if (v3Guard.dna_enforcement === "off") {
372
- return violations.filter((v) => v.layer !== "dna");
373
- }
374
- if (v3Guard.dna_enforcement === "warn") {
375
- for (const v of violations) {
376
- if (v.layer === "dna") {
377
- v.severity = "warning";
378
- }
354
+ const v4Guard = guard;
355
+ if (v4Guard.dna_enforcement === "off") {
356
+ return violations.filter((v) => v.layer !== "dna");
357
+ }
358
+ if (v4Guard.dna_enforcement === "warn") {
359
+ for (const v of violations) {
360
+ if (v.layer === "dna") {
361
+ v.severity = "warning";
379
362
  }
380
363
  }
381
- if (v3Guard.blueprint_enforcement === "off") {
382
- return violations.filter((v) => v.layer !== "blueprint");
383
- }
364
+ }
365
+ if (v4Guard.blueprint_enforcement === "off") {
366
+ return violations.filter((v) => v.layer !== "blueprint");
384
367
  }
385
368
  return violations;
386
369
  }
387
370
  function getAllPages(essence) {
388
- if (isV3(essence)) {
389
- const pages = flattenPages(essence.blueprint);
390
- return pages.map((page) => ({
391
- id: page.id,
392
- shell: page.shell_override ?? essence.blueprint.shell ?? "",
393
- layout: page.layout,
394
- ...page.surface ? { surface: page.surface } : {}
395
- }));
396
- }
397
- if (isSimple(essence)) return essence.structure;
398
- if (isSectioned(essence)) return essence.sections.flatMap((s) => s.structure);
399
- throw new Error("Unknown EssenceFile type");
371
+ const pages = flattenPages(essence.blueprint);
372
+ return pages.map((page) => ({
373
+ id: page.id,
374
+ shell: page.shell_override ?? essence.blueprint.shell ?? "",
375
+ layout: page.layout,
376
+ ...page.surface ? { surface: page.surface } : {}
377
+ }));
400
378
  }
401
379
  function getPageDensityOverride(essence, pageId) {
402
380
  if (!pageId) return void 0;
@@ -412,72 +390,135 @@ function getPageDensityOverride(essence, pageId) {
412
390
  }
413
391
 
414
392
  // src/migrate.ts
415
- function migrateV2ToV3(essence) {
416
- if (isV3(essence)) return essence;
417
- if (isSectioned(essence)) {
418
- return migrateSectionedToV3(essence);
419
- }
420
- if (isSimple(essence)) {
421
- return migrateSimpleToV3(essence);
422
- }
423
- throw new Error("Unknown EssenceFile type \u2014 cannot migrate");
424
- }
425
- function migrateSimpleToV3(essence) {
393
+ function migrateToV4(input) {
394
+ if (isV4(input)) return normalizeV4(input);
395
+ if (isLegacyV3(input)) return migrateLegacyV3ToV4(input);
396
+ if (isSectioned(input)) return migrateSectionedToV4(input);
397
+ if (isSimple(input)) return migrateSimpleToV4(input);
398
+ throw new Error("Unknown essence format. Only Essence v2, v3.0, v3.1, and v4 can migrate to v4.");
399
+ }
400
+ function migrateSimpleToV4(essence) {
426
401
  const dna = buildDNA(essence);
427
- const blueprint = buildBlueprintFromSimple(essence);
428
- const meta = buildMeta(essence);
402
+ const defaultShell = essence.structure[0]?.shell ?? "top-nav-main";
403
+ const pages = essence.structure.map((page, index) => ({
404
+ id: page.id,
405
+ route: page.id === "home" || index === 0 ? "/" : `/${page.id}`,
406
+ ...page.shell !== defaultShell ? { shell_override: page.shell } : {},
407
+ layout: page.layout,
408
+ ...page.surface ? { surface: page.surface } : {}
409
+ }));
410
+ const section = {
411
+ id: essence.archetype,
412
+ role: "primary",
413
+ shell: defaultShell,
414
+ features: essence.features ?? [],
415
+ description: `${essence.archetype} primary section`,
416
+ pages
417
+ };
429
418
  return {
430
- version: "3.0.0",
419
+ version: "4.0.0",
431
420
  dna,
432
- blueprint,
433
- meta,
421
+ blueprint: buildSectionedBlueprint([section], essence.features ?? []),
422
+ meta: buildMeta(essence),
434
423
  ...essence._impression ? { _impression: essence._impression } : {}
435
424
  };
436
425
  }
437
- function migrateSectionedToV3(essence) {
426
+ function migrateSectionedToV4(essence) {
438
427
  const firstSection = essence.sections[0];
428
+ if (!firstSection) {
429
+ throw new Error("Cannot migrate a sectioned essence with no sections.");
430
+ }
439
431
  const syntheticSimple = {
440
432
  theme: firstSection.theme,
441
433
  density: essence.density,
442
434
  guard: essence.guard,
443
- accessibility: essence.accessibility
435
+ accessibility: essence.accessibility,
436
+ personality: essence.personality
444
437
  };
445
- const dna = buildDNA(syntheticSimple);
446
- dna.personality = essence.personality;
447
- const pages = essence.sections.flatMap(
448
- (section) => section.structure.map((page) => ({
449
- id: page.id,
450
- shell_override: page.shell,
451
- layout: page.layout,
452
- ...page.surface ? { surface: page.surface } : {}
453
- }))
454
- );
455
- const allFeatures = [
438
+ const sections = essence.sections.map((section) => {
439
+ const defaultShell = section.structure[0]?.shell ?? "top-nav-main";
440
+ return {
441
+ id: section.id,
442
+ role: "primary",
443
+ shell: defaultShell,
444
+ features: section.features ?? [],
445
+ description: `${section.archetype} section`,
446
+ pages: section.structure.map((page, index) => ({
447
+ id: page.id,
448
+ route: index === 0 && section.path ? section.path : `${section.path}/${page.id}`.replace(/\/+/g, "/"),
449
+ ...page.shell !== defaultShell ? { shell_override: page.shell } : {},
450
+ layout: page.layout,
451
+ ...page.surface ? { surface: page.surface } : {}
452
+ }))
453
+ };
454
+ });
455
+ const features = [
456
456
  ...essence.shared_features ?? [],
457
- ...essence.sections.flatMap((s) => s.features ?? [])
457
+ ...essence.sections.flatMap((section) => section.features ?? [])
458
458
  ];
459
- const blueprint = {
460
- shell: firstSection.structure[0]?.shell ?? "top-nav-main",
461
- pages,
462
- features: [...new Set(allFeatures)]
459
+ return {
460
+ version: "4.0.0",
461
+ dna: buildDNA(syntheticSimple),
462
+ blueprint: buildSectionedBlueprint(sections, [...new Set(features)]),
463
+ meta: {
464
+ archetype: firstSection.archetype,
465
+ target: essence.target,
466
+ platform: essence.platform,
467
+ guard: migrateGuard(essence.guard.mode)
468
+ },
469
+ ...essence._impression ? { _impression: essence._impression } : {}
463
470
  };
464
- const meta = {
465
- archetype: firstSection.archetype,
466
- target: essence.target,
467
- platform: essence.platform,
468
- guard: migrateGuard(essence.guard.mode)
471
+ }
472
+ function migrateLegacyV3ToV4(essence) {
473
+ const sections = essence.blueprint.sections && essence.blueprint.sections.length > 0 ? essence.blueprint.sections : [
474
+ {
475
+ id: essence.meta.archetype,
476
+ role: "primary",
477
+ shell: essence.blueprint.shell ?? "top-nav-main",
478
+ features: essence.blueprint.features,
479
+ description: `${essence.meta.archetype} primary section`,
480
+ pages: essence.blueprint.pages ?? []
481
+ }
482
+ ];
483
+ return normalizeV4({
484
+ ...essence,
485
+ version: "4.0.0",
486
+ blueprint: buildSectionedBlueprint(
487
+ sections,
488
+ essence.blueprint.features,
489
+ essence.blueprint.routes
490
+ )
491
+ });
492
+ }
493
+ function normalizeV4(essence) {
494
+ return {
495
+ ...essence,
496
+ version: "4.0.0",
497
+ blueprint: buildSectionedBlueprint(
498
+ essence.blueprint.sections,
499
+ essence.blueprint.features,
500
+ essence.blueprint.routes,
501
+ essence.blueprint.shell
502
+ )
469
503
  };
504
+ }
505
+ function buildSectionedBlueprint(sections, features, existingRoutes, shell) {
506
+ const routes = { ...existingRoutes ?? {} };
507
+ for (const section of sections) {
508
+ for (const page of section.pages) {
509
+ if (!page.route || routes[page.route]) continue;
510
+ routes[page.route] = { section: section.id, page: page.id };
511
+ }
512
+ }
470
513
  return {
471
- version: "3.0.0",
472
- dna,
473
- blueprint,
474
- meta,
475
- ...essence._impression ? { _impression: essence._impression } : {}
514
+ ...shell ? { shell } : {},
515
+ sections,
516
+ features: [...new Set(features)],
517
+ routes
476
518
  };
477
519
  }
478
520
  function buildDNA(essence) {
479
521
  const shape = essence.theme.shape ?? "rounded";
480
- const radiusBase = inferRadiusBase(shape);
481
522
  return {
482
523
  theme: {
483
524
  id: essence.theme.id,
@@ -488,7 +529,7 @@ function buildDNA(essence) {
488
529
  base_unit: 4,
489
530
  scale: "linear",
490
531
  density: essence.density?.level ?? "comfortable",
491
- content_gap: essence.density?.content_gap ?? "4"
532
+ content_gap: essence.density?.content_gap ?? "_gap4"
492
533
  },
493
534
  typography: {
494
535
  scale: "modular",
@@ -502,7 +543,7 @@ function buildDNA(essence) {
502
543
  },
503
544
  radius: {
504
545
  philosophy: shape,
505
- base: radiusBase
546
+ base: inferRadiusBase(shape)
506
547
  },
507
548
  elevation: {
508
549
  system: "layered",
@@ -521,19 +562,6 @@ function buildDNA(essence) {
521
562
  personality: essence.personality ?? ["professional"]
522
563
  };
523
564
  }
524
- function buildBlueprintFromSimple(essence) {
525
- const defaultShell = essence.structure[0]?.shell ?? "top-nav-main";
526
- return {
527
- shell: defaultShell,
528
- pages: essence.structure.map((page) => ({
529
- id: page.id,
530
- ...page.shell !== defaultShell ? { shell_override: page.shell } : {},
531
- layout: page.layout,
532
- ...page.surface ? { surface: page.surface } : {}
533
- })),
534
- features: essence.features ?? []
535
- };
536
- }
537
565
  function buildMeta(essence) {
538
566
  return {
539
567
  archetype: essence.archetype,
@@ -565,135 +593,14 @@ function inferRadiusBase(shape) {
565
593
  return 8;
566
594
  }
567
595
  }
568
- function migrateV30ToV31(essence) {
569
- if (essence.version === "3.1.0" || essence.blueprint.sections) {
570
- return essence;
571
- }
572
- const section = {
573
- id: essence.meta.archetype,
574
- role: "primary",
575
- shell: essence.blueprint.shell ?? "top-nav-main",
576
- features: essence.blueprint.features,
577
- description: `${essence.meta.archetype} primary section`,
578
- pages: essence.blueprint.pages ?? []
579
- };
580
- return {
581
- ...essence,
582
- version: "3.1.0",
583
- blueprint: {
584
- ...essence.blueprint,
585
- sections: [section],
586
- pages: void 0,
587
- routes: {}
588
- }
589
- };
590
- }
591
596
 
592
597
  // src/normalize.ts
598
+ var V4_REQUIRED_MESSAGE = "Active Decantr V2 workflows require Essence v4.0.0. Run `decantr migrate --to v4` for older essence files.";
593
599
  function normalizeEssence(input) {
594
- if (input.version === "3.0.0" || input.version === "3.1.0") {
595
- return input;
596
- }
597
- if (input.theme && input.platform) {
600
+ if (isV4(input)) {
598
601
  return input;
599
602
  }
600
- if (input.sections) {
601
- return normalizeSectioned(input);
602
- }
603
- return normalizeSimple(input);
604
- }
605
- function normalizeSimple(v1) {
606
- const vintage = v1.vintage;
607
- const vessel = v1.vessel;
608
- const clarity = v1.clarity;
609
- const cork = v1.cork;
610
- const structure = v1.structure;
611
- return {
612
- version: "2.0.0",
613
- archetype: v1.terroir ?? v1.vignette ?? v1.archetype,
614
- theme: {
615
- id: vintage?.style ?? "",
616
- mode: vintage?.mode ?? "dark",
617
- ...vintage?.shape ? { shape: vintage.shape } : {}
618
- },
619
- // Accept both old 'character' and new 'personality' field names
620
- personality: v1.character ?? v1.personality,
621
- platform: {
622
- type: vessel?.type ?? "spa",
623
- // Modern-SPA default. See packages/cli/src/scaffold.ts getPlatformMeta for rationale.
624
- routing: vessel?.routing ?? "history"
625
- },
626
- structure: (structure ?? []).map(normalizeStructurePage),
627
- features: v1.tannins ?? v1.features ?? [],
628
- density: {
629
- level: clarity?.density ?? "comfortable",
630
- content_gap: stripGapPrefix(clarity?.content_gap ?? "4")
631
- },
632
- guard: normalizeGuard(cork),
633
- target: v1.target ?? "decantr",
634
- ...v1._impression ? { _impression: v1._impression } : {}
635
- };
636
- }
637
- function normalizeSectioned(v1) {
638
- const vessel = v1.vessel;
639
- const clarity = v1.clarity;
640
- const cork = v1.cork;
641
- const sections = v1.sections;
642
- return {
643
- version: "2.0.0",
644
- platform: {
645
- type: vessel?.type ?? "spa",
646
- // Modern-SPA default. See packages/cli/src/scaffold.ts getPlatformMeta for rationale.
647
- routing: vessel?.routing ?? "history"
648
- },
649
- // Accept both old 'character' and new 'personality' field names
650
- personality: v1.character ?? v1.personality,
651
- sections: sections.map((section) => ({
652
- id: section.id,
653
- path: section.path,
654
- archetype: section.terroir ?? section.vignette ?? section.archetype,
655
- theme: {
656
- id: section.vintage?.style ?? "",
657
- mode: section.vintage?.mode ?? "dark"
658
- },
659
- structure: (section.structure ?? []).map(
660
- normalizeStructurePage
661
- ),
662
- ...section.tannins ? { features: section.tannins } : {}
663
- })),
664
- ...v1.shared_tannins ? { shared_features: v1.shared_tannins } : {},
665
- density: {
666
- level: clarity?.density ?? "comfortable",
667
- content_gap: stripGapPrefix(clarity?.content_gap ?? "4")
668
- },
669
- guard: normalizeGuard(cork),
670
- target: v1.target ?? "decantr"
671
- };
672
- }
673
- function normalizeStructurePage(page) {
674
- return {
675
- id: page.id,
676
- shell: page.carafe ?? page.shell,
677
- layout: page.blend ?? page.layout,
678
- ...page.surface ? { surface: page.surface } : {}
679
- };
680
- }
681
- function normalizeGuard(cork) {
682
- if (!cork) return { mode: "creative" };
683
- const modeMap = {
684
- creative: "creative",
685
- maintenance: "strict",
686
- strict: "strict",
687
- guided: "guided"
688
- };
689
- return {
690
- ...cork.enforce_style !== void 0 ? { enforce_style: cork.enforce_style } : {},
691
- mode: modeMap[cork.mode ?? "creative"] ?? "creative"
692
- };
693
- }
694
- function stripGapPrefix(gap) {
695
- const match = gap.match(/^_gap(\d+)$/);
696
- return match ? match[1] : gap;
603
+ throw new Error(V4_REQUIRED_MESSAGE);
697
604
  }
698
605
 
699
606
  // src/validate.ts
@@ -704,6 +611,7 @@ import Ajv from "ajv";
704
611
  var __dirname = dirname(fileURLToPath(import.meta.url));
705
612
  var v2SchemaPath = join(__dirname, "..", "schema", "essence.v2.json");
706
613
  var v3SchemaPath = join(__dirname, "..", "schema", "essence.v3.json");
614
+ var v4SchemaPath = join(__dirname, "..", "schema", "essence.v4.json");
707
615
  var cachedAjv = null;
708
616
  var validatorCache = /* @__PURE__ */ new Map();
709
617
  function getAjv() {
@@ -714,7 +622,7 @@ function getAjv() {
714
622
  }
715
623
  function getValidator(version) {
716
624
  if (validatorCache.has(version)) return validatorCache.get(version);
717
- const schemaPath = version === "v3" ? v3SchemaPath : v2SchemaPath;
625
+ const schemaPath = version === "v4" ? v4SchemaPath : version === "v3" ? v3SchemaPath : v2SchemaPath;
718
626
  if (!existsSync(schemaPath)) return null;
719
627
  const schema = JSON.parse(readFileSync(schemaPath, "utf-8"));
720
628
  const ajv = getAjv();
@@ -722,7 +630,7 @@ function getValidator(version) {
722
630
  validatorCache.set(version, validate);
723
631
  return validate;
724
632
  }
725
- function detectVersion(data) {
633
+ function detectLegacyVersion(data) {
726
634
  if (typeof data === "object" && data !== null && "version" in data) {
727
635
  const v = data.version;
728
636
  if (v === "3.0.0" || v === "3.1.0") return "v3";
@@ -730,7 +638,32 @@ function detectVersion(data) {
730
638
  return "v2";
731
639
  }
732
640
  function validateEssence(data) {
733
- const version = detectVersion(data);
641
+ const version = typeof data === "object" && data !== null ? data.version : void 0;
642
+ if (version !== "4.0.0") {
643
+ return {
644
+ valid: false,
645
+ errors: [
646
+ "Active Decantr V2 workflows require Essence v4.0.0. Run `decantr migrate --to v4` for older essence files."
647
+ ]
648
+ };
649
+ }
650
+ const validate = getValidator("v4");
651
+ if (!validate) {
652
+ return { valid: false, errors: ["No schema available for v4 documents"] };
653
+ }
654
+ const valid = validate(data);
655
+ if (valid) {
656
+ return { valid: true, errors: [] };
657
+ }
658
+ const errors = (validate.errors ?? []).map((err) => {
659
+ const path = err.instancePath || "/";
660
+ const msg = err.message ?? "unknown error";
661
+ return `${path}: ${msg}`;
662
+ });
663
+ return { valid: false, errors };
664
+ }
665
+ function validateLegacyEssenceForMigration(data) {
666
+ const version = typeof data === "object" && data !== null && data.version === "4.0.0" ? "v4" : detectLegacyVersion(data);
734
667
  const validate = getValidator(version);
735
668
  if (!validate) {
736
669
  return { valid: false, errors: [`No schema available for ${version} documents`] };
@@ -754,12 +687,13 @@ export {
754
687
  getColumnAlias,
755
688
  getColumnId,
756
689
  getColumnPreset,
690
+ isLegacyV3,
757
691
  isSectioned,
758
692
  isSimple,
759
- isV3,
760
- migrateV2ToV3,
761
- migrateV30ToV31,
693
+ isV4,
694
+ migrateToV4,
762
695
  normalizeEssence,
763
- validateEssence
696
+ validateEssence,
697
+ validateLegacyEssenceForMigration
764
698
  };
765
699
  //# sourceMappingURL=index.js.map