@decantr/core 1.0.1 → 1.0.3

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
@@ -1,9 +1,6 @@
1
- // src/pipeline.ts
2
- import { validateEssence, isV3 as isV32, migrateV2ToV3 } from "@decantr/essence-spec";
3
- import { createResolver } from "@decantr/registry";
4
-
5
1
  // src/resolve.ts
6
- import { isSimple, isSectioned, isV3, computeDensity } from "@decantr/essence-spec";
2
+ import { computeDensity, isSectioned, isSimple, isV3 } from "@decantr/essence-spec";
3
+ import { detectWirings, resolvePatternPreset } from "@decantr/registry";
7
4
 
8
5
  // src/utils.ts
9
6
  function pascalCase(str) {
@@ -11,7 +8,6 @@ function pascalCase(str) {
11
8
  }
12
9
 
13
10
  // src/resolve.ts
14
- import { resolvePatternPreset, detectWirings } from "@decantr/registry";
15
11
  var NAV_ICONS = {
16
12
  overview: "layout-dashboard",
17
13
  dashboard: "layout-dashboard",
@@ -58,7 +54,11 @@ function extractLayoutRefs(layout) {
58
54
  refs.push({ id: item.pattern, explicitPreset: item.preset, alias: item.as });
59
55
  } else if (isColumnLayout(item)) {
60
56
  for (const col of item.cols) {
61
- refs.push({ id: col });
57
+ if (typeof col === "string") {
58
+ refs.push({ id: col });
59
+ } else {
60
+ refs.push({ id: col.pattern, explicitPreset: col.preset, alias: col.as });
61
+ }
62
62
  }
63
63
  }
64
64
  }
@@ -249,10 +249,13 @@ async function resolveEssence(essence, resolver) {
249
249
  registryTheme = themeResult.item;
250
250
  }
251
251
  const themeSpatial = registryTheme?.spatial;
252
- const density = computeDensity(simpleEssence.personality, themeSpatial ? {
253
- density_bias: themeSpatial.density_bias,
254
- content_gap_shift: themeSpatial.content_gap_shift
255
- } : void 0);
252
+ const density = computeDensity(
253
+ simpleEssence.personality,
254
+ themeSpatial ? {
255
+ density_bias: themeSpatial.density_bias,
256
+ content_gap_shift: themeSpatial.content_gap_shift
257
+ } : void 0
258
+ );
256
259
  const themeId = simpleEssence.theme.id;
257
260
  const isAddon = themeId.startsWith("custom:") || !CORE_STYLES.has(themeId);
258
261
  const theme = buildTheme(simpleEssence, isAddon);
@@ -293,10 +296,13 @@ async function resolveV3Essence(essence, resolver) {
293
296
  registryTheme = themeResult.item;
294
297
  }
295
298
  const themeSpatial = registryTheme?.spatial;
296
- const density = computeDensity(dna.personality, themeSpatial ? {
297
- density_bias: themeSpatial.density_bias,
298
- content_gap_shift: themeSpatial.content_gap_shift
299
- } : void 0);
299
+ const density = computeDensity(
300
+ dna.personality,
301
+ themeSpatial ? {
302
+ density_bias: themeSpatial.density_bias,
303
+ content_gap_shift: themeSpatial.content_gap_shift
304
+ } : void 0
305
+ );
300
306
  const densityResult = {
301
307
  gap: dna.spacing.content_gap || density.content_gap,
302
308
  level: dna.spacing.density || density.level
@@ -305,10 +311,17 @@ async function resolveV3Essence(essence, resolver) {
305
311
  const isAddon = themeId.startsWith("custom:") || !CORE_STYLES.has(themeId);
306
312
  const theme = buildThemeFromV3(essence, isAddon);
307
313
  const blueprintPages = blueprint.pages ?? (blueprint.sections ? blueprint.sections.flatMap((s) => s.pages) : [{ id: "home", layout: ["hero"] }]);
308
- const defaultShell = blueprint.shell ?? (blueprint.sections?.[0]?.shell ?? "sidebar-main");
314
+ const defaultShell = blueprint.shell ?? blueprint.sections?.[0]?.shell ?? "sidebar-main";
309
315
  const structurePages = blueprint.pages ? blueprint.pages.map((page) => blueprintPageToStructurePage(page, defaultShell)) : blueprint.sections ? blueprint.sections.flatMap(
310
- (section) => section.pages.map((page) => blueprintPageToStructurePage(page, section.shell ?? defaultShell))
311
- ) : [blueprintPageToStructurePage({ id: "home", layout: ["hero"] }, defaultShell)];
316
+ (section) => section.pages.map(
317
+ (page) => blueprintPageToStructurePage(page, section.shell ?? defaultShell)
318
+ )
319
+ ) : [
320
+ blueprintPageToStructurePage(
321
+ { id: "home", layout: ["hero"] },
322
+ defaultShell
323
+ )
324
+ ];
312
325
  const resolvedPages = await resolvePages(structurePages, resolver, registryTheme);
313
326
  const shellType = defaultShell;
314
327
  const brand = pascalCase(meta.archetype);
@@ -390,15 +403,20 @@ function buildPatternNode(patternId, alias, resolved, wiring, theme, density, la
390
403
  const isStandalone = layout === "hero" || layout === "row";
391
404
  const contained = pattern && preset ? shouldWrapInCard(pattern, preset, theme) : false;
392
405
  const components = pattern?.components || [];
406
+ const presetName = preset?.preset || "default";
407
+ const presetDescription = pattern?.presets?.[presetName]?.description;
408
+ const interactions = Array.isArray(pattern?.interactions) ? pattern.interactions : void 0;
393
409
  const patternMeta = {
394
410
  patternId,
395
- preset: preset?.preset || "default",
411
+ preset: presetName,
396
412
  alias,
397
413
  layout,
398
414
  contained,
399
415
  standalone: isStandalone,
400
416
  code: preset?.code ? { imports: preset.code.imports, example: preset.code.example } : null,
401
- components
417
+ components,
418
+ ...presetDescription ? { presetDescription } : {},
419
+ ...interactions && interactions.length > 0 ? { interactions } : {}
402
420
  };
403
421
  const card = contained && !isStandalone && pattern ? buildCardWrapping(pattern, theme) : null;
404
422
  const wireProps = wiring?.props[alias] || wiring?.props[patternId] || null;
@@ -426,27 +444,32 @@ function buildPageIR(page, resolvedPatterns, wiring, theme, density, layer) {
426
444
  const resolved = resolvedPatterns.get(alias) || resolvedPatterns.get(item.pattern);
427
445
  children.push(buildPatternNode(item.pattern, alias, resolved, wiring, theme, density, layer));
428
446
  } else if (isColumnLayout2(item)) {
429
- const cols = item.cols;
447
+ const colEntries = item.cols.map((col) => ({
448
+ id: typeof col === "string" ? col : col.pattern,
449
+ alias: typeof col === "string" ? col : col.as ?? col.pattern
450
+ }));
430
451
  const breakpoint = item.at || null;
431
452
  const spans = item.span || null;
432
453
  let normalizedSpans = null;
433
454
  if (spans) {
434
455
  normalizedSpans = {};
435
- for (const col of cols) {
436
- normalizedSpans[col] = spans[col] || 1;
456
+ for (const entry of colEntries) {
457
+ normalizedSpans[entry.id] = spans[entry.id] || spans[entry.alias] || 1;
437
458
  }
438
459
  }
439
460
  const gridChildren = [];
440
- for (const col of cols) {
441
- const resolved = resolvedPatterns.get(col);
442
- gridChildren.push(buildPatternNode(col, col, resolved, wiring, theme, density, layer));
461
+ for (const entry of colEntries) {
462
+ const resolved = resolvedPatterns.get(entry.alias) || resolvedPatterns.get(entry.id);
463
+ gridChildren.push(
464
+ buildPatternNode(entry.id, entry.alias, resolved, wiring, theme, density, layer)
465
+ );
443
466
  }
444
- const totalCols = normalizedSpans ? Object.values(normalizedSpans).reduce((a, b) => a + b, 0) : cols.length;
467
+ const totalCols = normalizedSpans ? Object.values(normalizedSpans).reduce((a, b) => a + b, 0) : colEntries.length;
445
468
  const breakpoints = item.breakpoints?.map((bp) => ({ at: bp.at, cols: bp.cols })) || null;
446
469
  const responsive = item.responsive || null;
447
470
  const gridNode = {
448
471
  type: "grid",
449
- id: `grid-${cols.join("-")}`,
472
+ id: `grid-${colEntries.map((e) => e.id).join("-")}`,
450
473
  children: gridChildren,
451
474
  cols: totalCols,
452
475
  spans: normalizedSpans,
@@ -460,7 +483,7 @@ function buildPageIR(page, resolvedPatterns, wiring, theme, density, layer) {
460
483
  }
461
484
  }
462
485
  const gapAtom = density.gap.startsWith("_") ? density.gap : density.gap.startsWith("gap") ? `_${density.gap}` : `_gap${density.gap}`;
463
- const surface = page.surface || `_flex _col ${gapAtom} _p4 _overauto _flex1`;
486
+ const surface = page.surface || `_flex _col ${gapAtom}`;
464
487
  return {
465
488
  type: "page",
466
489
  id: page.id,
@@ -472,12 +495,63 @@ function buildPageIR(page, resolvedPatterns, wiring, theme, density, layer) {
472
495
  };
473
496
  }
474
497
 
498
+ // src/ir-helpers.ts
499
+ function walkIR(node, visitor, parent = null) {
500
+ visitor(node, parent);
501
+ for (const child of node.children) {
502
+ walkIR(child, visitor, node);
503
+ }
504
+ }
505
+ function findNodes(root, type) {
506
+ const results = [];
507
+ walkIR(root, (node) => {
508
+ if (node.type === type) {
509
+ results.push(node);
510
+ }
511
+ });
512
+ return results;
513
+ }
514
+ function countPatterns(root) {
515
+ return findNodes(root, "pattern").length;
516
+ }
517
+ function validateIR(root) {
518
+ const errors = [];
519
+ walkIR(root, (node, parent) => {
520
+ if (node.type === "grid") {
521
+ const grid = node;
522
+ if (grid.children.length === 0) {
523
+ errors.push(`Grid node "${grid.id}" has no children`);
524
+ }
525
+ }
526
+ if (node.type === "pattern") {
527
+ const pattern = node;
528
+ if (!pattern.pattern) {
529
+ errors.push(`Pattern node "${pattern.id}" is missing pattern meta`);
530
+ }
531
+ if (!pattern.pattern.patternId) {
532
+ errors.push(`Pattern node "${pattern.id}" has no patternId`);
533
+ }
534
+ }
535
+ if (node.type === "page") {
536
+ if (!node.id) {
537
+ errors.push("Page node is missing id");
538
+ }
539
+ }
540
+ });
541
+ return errors;
542
+ }
543
+
544
+ // src/packs.ts
545
+ import { isV3 as isV33, migrateV2ToV3 as migrateV2ToV32 } from "@decantr/essence-spec";
546
+
475
547
  // src/pipeline.ts
548
+ import { isV3 as isV32, migrateV2ToV3, validateEssence } from "@decantr/essence-spec";
549
+ import { createResolver } from "@decantr/registry";
476
550
  function extractRouting(essence) {
477
551
  if (isV32(essence)) {
478
- return essence.meta.platform.routing || "hash";
552
+ return essence.meta.platform.routing || "history";
479
553
  }
480
- return essence.platform?.routing || "hash";
554
+ return essence.platform?.routing || "history";
481
555
  }
482
556
  async function runPipeline(essence, options) {
483
557
  const validation = validateEssence(essence);
@@ -538,57 +612,10 @@ async function runPipeline(essence, options) {
538
612
  store: storeNode,
539
613
  features: resolved.features
540
614
  };
541
- return { ir: appNode };
542
- }
543
-
544
- // src/ir-helpers.ts
545
- function walkIR(node, visitor, parent = null) {
546
- visitor(node, parent);
547
- for (const child of node.children) {
548
- walkIR(child, visitor, node);
549
- }
550
- }
551
- function findNodes(root, type) {
552
- const results = [];
553
- walkIR(root, (node) => {
554
- if (node.type === type) {
555
- results.push(node);
556
- }
557
- });
558
- return results;
559
- }
560
- function countPatterns(root) {
561
- return findNodes(root, "pattern").length;
562
- }
563
- function validateIR(root) {
564
- const errors = [];
565
- walkIR(root, (node, parent) => {
566
- if (node.type === "grid") {
567
- const grid = node;
568
- if (grid.children.length === 0) {
569
- errors.push(`Grid node "${grid.id}" has no children`);
570
- }
571
- }
572
- if (node.type === "pattern") {
573
- const pattern = node;
574
- if (!pattern.pattern) {
575
- errors.push(`Pattern node "${pattern.id}" is missing pattern meta`);
576
- }
577
- if (!pattern.pattern.patternId) {
578
- errors.push(`Pattern node "${pattern.id}" has no patternId`);
579
- }
580
- }
581
- if (node.type === "page") {
582
- if (!node.id) {
583
- errors.push("Page node is missing id");
584
- }
585
- }
586
- });
587
- return errors;
615
+ return { ir: appNode, registryTheme: resolved.registryTheme };
588
616
  }
589
617
 
590
618
  // src/packs.ts
591
- import { isV3 as isV33, migrateV2ToV3 as migrateV2ToV32 } from "@decantr/essence-spec";
592
619
  var EXECUTION_PACK_SCHEMA_URLS = {
593
620
  scaffold: "https://decantr.ai/schemas/scaffold-pack.v1.json",
594
621
  section: "https://decantr.ai/schemas/section-pack.v1.json",
@@ -748,12 +775,14 @@ function summarizeRoutes(appNode) {
748
775
  return appNode.routes.flatMap((route) => {
749
776
  const pageNode = findPageNode(appNode, route.pageId);
750
777
  if (!pageNode) return [];
751
- return [{
752
- pageId: pageNode.pageId,
753
- path: route.path,
754
- patternIds: collectPatternIds(pageNode),
755
- shell: route.shell
756
- }];
778
+ return [
779
+ {
780
+ pageId: pageNode.pageId,
781
+ path: route.path,
782
+ patternIds: collectPatternIds(pageNode),
783
+ shell: route.shell
784
+ }
785
+ ];
757
786
  });
758
787
  }
759
788
  function summarizeSectionRoutes(appNode, input) {
@@ -775,7 +804,9 @@ function collectPagePatterns(page) {
775
804
  id: patternNode.pattern.patternId,
776
805
  alias: patternNode.pattern.alias,
777
806
  preset: patternNode.pattern.preset,
778
- layout: patternNode.pattern.layout
807
+ layout: patternNode.pattern.layout,
808
+ ...patternNode.pattern.presetDescription ? { presetDescription: patternNode.pattern.presetDescription } : {},
809
+ ...patternNode.pattern.interactions && patternNode.pattern.interactions.length > 0 ? { interactions: patternNode.pattern.interactions } : {}
779
810
  });
780
811
  });
781
812
  return patterns;
@@ -791,43 +822,108 @@ function renderList(title, entries) {
791
822
  if (entries.length === 0) return [];
792
823
  return [title, ...entries.map((entry) => `- ${entry}`), ""];
793
824
  }
825
+ function formatHotkeySemantics(semantics) {
826
+ if (!semantics) return "";
827
+ const parts = [];
828
+ if (typeof semantics.chord_window_ms === "number") {
829
+ parts.push(`chord window ${semantics.chord_window_ms}ms`);
830
+ }
831
+ if (semantics.input_guard === true) parts.push("suppress during text-input focus");
832
+ if (semantics.input_guard === false) parts.push("fire even while typing (explicit)");
833
+ if (semantics.modifier_suppression === true) parts.push("ignore when modifier held");
834
+ if (semantics.modifier_suppression === false) parts.push("allow modifier passthrough (explicit)");
835
+ if (semantics.match_case === true) parts.push("case-sensitive");
836
+ return parts.join("; ");
837
+ }
838
+ function routingImplementationHint(routing) {
839
+ switch (routing) {
840
+ case "hash":
841
+ return "HashRouter from react-router-dom; URLs prefixed with /# (e.g. /#/login). Only for static-only hosts without SPA fallback.";
842
+ case "history":
843
+ return "BrowserRouter from react-router-dom; regular URLs (e.g. /login). Works on Vite dev, Vercel, Netlify, Cloudflare Pages.";
844
+ case "pathname":
845
+ return "pathname-based routing (Next.js App Router file conventions).";
846
+ default:
847
+ return String(routing);
848
+ }
849
+ }
794
850
  function renderExecutionPackMarkdown(pack) {
795
851
  const lines = [];
796
852
  lines.push(`# ${pack.packType.charAt(0).toUpperCase()}${pack.packType.slice(1)} Pack`);
797
853
  lines.push("");
798
854
  lines.push(`**Objective:** ${pack.objective}`);
799
- lines.push(`**Target:** ${pack.target.adapter}${pack.target.framework ? ` (${pack.target.framework})` : ""}`);
800
- lines.push(`**Scope:** pages=${pack.scope.pageIds.join(", ") || "none"} | patterns=${pack.scope.patternIds.join(", ") || "none"}`);
855
+ lines.push(
856
+ `**Target:** ${pack.target.adapter}${pack.target.framework ? ` (${pack.target.framework})` : ""}`
857
+ );
858
+ lines.push(
859
+ `**Scope:** pages=${pack.scope.pageIds.join(", ") || "none"} | patterns=${pack.scope.patternIds.join(", ") || "none"}`
860
+ );
801
861
  lines.push("");
802
862
  if (pack.packType === "scaffold") {
803
863
  const scaffoldPack = pack;
804
- const scaffoldShells = [...new Set(scaffoldPack.data.routes.map((route) => route.shell).filter(Boolean))];
864
+ const scaffoldShells = [
865
+ ...new Set(scaffoldPack.data.routes.map((route) => route.shell).filter(Boolean))
866
+ ];
805
867
  lines.push("## Scaffold Contract");
806
868
  lines.push(`- Shell: ${scaffoldPack.data.shell}`);
807
869
  if (scaffoldShells.length > 1) {
808
870
  const secondaryShells = scaffoldShells.filter((shell) => shell !== scaffoldPack.data.shell);
809
- lines.push(`- Shells: ${[`${scaffoldPack.data.shell} (primary)`, ...secondaryShells].join(", ")}`);
871
+ lines.push(
872
+ `- Shells: ${[`${scaffoldPack.data.shell} (primary)`, ...secondaryShells].join(", ")}`
873
+ );
810
874
  }
811
875
  lines.push(`- Theme: ${scaffoldPack.data.theme.id} (${scaffoldPack.data.theme.mode})`);
812
- lines.push(`- Routing: ${scaffoldPack.data.routing}`);
876
+ lines.push(
877
+ `- Routing: ${scaffoldPack.data.routing} \u2192 ${routingImplementationHint(scaffoldPack.data.routing)}`
878
+ );
813
879
  if (scaffoldPack.data.features.length > 0) {
814
880
  lines.push(`- Features: ${scaffoldPack.data.features.join(", ")}`);
815
881
  }
816
882
  if (scaffoldPack.data.navigation?.commandPalette || scaffoldPack.data.navigation?.hotkeys.length) {
817
883
  lines.push("- Navigation:");
818
- if (scaffoldPack.data.navigation.commandPalette) {
819
- lines.push(" - command palette required");
884
+ const cp = scaffoldPack.data.navigation.commandPalette;
885
+ if (cp) {
886
+ if (typeof cp === "object") {
887
+ lines.push(" - Command palette:");
888
+ if (cp.trigger) lines.push(` - Trigger: ${cp.trigger}`);
889
+ if (cp.styling)
890
+ lines.push(` - Styling: ${cp.styling}${cp.width ? ` (width ${cp.width})` : ""}`);
891
+ else if (cp.width) lines.push(` - Width: ${cp.width}`);
892
+ if (cp.placeholder) lines.push(` - Placeholder: "${cp.placeholder}"`);
893
+ if (cp.commands && cp.commands.length > 0) {
894
+ lines.push(` - Commands (${cp.commands.length}):`);
895
+ for (const command of cp.commands) {
896
+ const parts = [];
897
+ if (command.section) parts.push(command.section);
898
+ parts.push(command.label);
899
+ if (command.hotkey) parts.push(`[${command.hotkey}]`);
900
+ if (command.route) parts.push(`\u2192 ${command.route}`);
901
+ lines.push(` - ${command.id}: ${parts.join(" \xB7 ")}`);
902
+ }
903
+ }
904
+ } else {
905
+ lines.push(" - command palette required");
906
+ }
820
907
  }
821
- for (const hotkey of scaffoldPack.data.navigation.hotkeys) {
822
- const target = [hotkey.label, hotkey.route].filter(Boolean).join(" \u2014 ");
823
- lines.push(` - ${hotkey.key}${target ? `: ${target}` : ""}`);
908
+ const globalSemantics = scaffoldPack.data.navigation.hotkeySemantics;
909
+ if (scaffoldPack.data.navigation.hotkeys.length > 0) {
910
+ const semanticsSummary = formatHotkeySemantics(globalSemantics);
911
+ lines.push(` - Hotkeys${semanticsSummary ? ` (${semanticsSummary})` : ""}:`);
912
+ for (const hotkey of scaffoldPack.data.navigation.hotkeys) {
913
+ const target = [hotkey.label, hotkey.route].filter(Boolean).join(" \u2014 ");
914
+ const perKeySemantics = formatHotkeySemantics(hotkey.semantics);
915
+ const suffix = perKeySemantics ? ` _(${perKeySemantics})_` : "";
916
+ lines.push(` - ${hotkey.key}${target ? `: ${target}` : ""}${suffix}`);
917
+ }
824
918
  }
825
919
  }
826
920
  lines.push("");
827
921
  lines.push("## Route Plan");
828
922
  for (const route of scaffoldPack.data.routes) {
829
923
  const patterns = route.patternIds.length > 0 ? route.patternIds.join(", ") : "none";
830
- lines.push(`- ${route.path} -> ${route.pageId}${route.shell ? ` @ ${route.shell}` : ""} [${patterns}]`);
924
+ lines.push(
925
+ `- ${route.path} -> ${route.pageId}${route.shell ? ` @ ${route.shell}` : ""} [${patterns}]`
926
+ );
831
927
  }
832
928
  lines.push("");
833
929
  }
@@ -848,9 +944,57 @@ function renderExecutionPackMarkdown(pack) {
848
944
  lines.push("## Section Routes");
849
945
  for (const route of sectionPack.data.routes) {
850
946
  const patterns = route.patternIds.length > 0 ? route.patternIds.join(", ") : "none";
851
- lines.push(`- ${route.path} -> ${route.pageId}${route.shell ? ` @ ${route.shell}` : ""} [${patterns}]`);
947
+ lines.push(
948
+ `- ${route.path} -> ${route.pageId}${route.shell ? ` @ ${route.shell}` : ""} [${patterns}]`
949
+ );
852
950
  }
853
951
  lines.push("");
952
+ if (sectionPack.data.navigationItems && sectionPack.data.navigationItems.length > 0) {
953
+ lines.push("## Section Navigation");
954
+ lines.push("");
955
+ lines.push(
956
+ "Render these items in the shell's primary navigation. Exact match on label, route, and icon."
957
+ );
958
+ lines.push("");
959
+ for (const item of sectionPack.data.navigationItems) {
960
+ const parts = [`${item.label} \u2192 ${item.route}`];
961
+ if (item.icon) parts.push(`icon: ${item.icon}`);
962
+ if (item.hotkey) parts.push(`hotkey: ${item.hotkey}`);
963
+ if (item.badge) parts.push(`badge: ${item.badge}`);
964
+ if (item.active_match) parts.push(`active match: \`${item.active_match}\``);
965
+ lines.push(`- ${parts.join(" \xB7 ")}`);
966
+ }
967
+ lines.push("");
968
+ }
969
+ if (sectionPack.data.directives && sectionPack.data.directives.length > 0) {
970
+ lines.push("## Section Directives");
971
+ lines.push("");
972
+ lines.push(
973
+ "Execution-level rules every page in this section must obey. Follow exactly \u2014 these live in the pack contract, not narrative prose."
974
+ );
975
+ lines.push("");
976
+ for (const directive of sectionPack.data.directives) {
977
+ lines.push(`- ${directive}`);
978
+ }
979
+ lines.push("");
980
+ }
981
+ if (sectionPack.data.themeDecorators && sectionPack.data.themeDecorators.length > 0) {
982
+ const escCell = (s) => s.replace(/\|/g, "\\|");
983
+ lines.push(`## Required Theme Decorators (${sectionPack.data.theme.id})`);
984
+ lines.push("");
985
+ lines.push(
986
+ `These classes carry the active theme's visual identity. Tokens alone give bones; decorators give personality. Generated source MUST apply these \u2014 without them, the page reads as "themed colors only" with no theme character.`
987
+ );
988
+ lines.push("");
989
+ lines.push("| Class | Intent | Apply to |");
990
+ lines.push("|-------|--------|----------|");
991
+ for (const entry of sectionPack.data.themeDecorators) {
992
+ lines.push(
993
+ `| \`.${entry.class}\` | ${escCell(entry.intent)} | ${escCell(entry.applyTo)} |`
994
+ );
995
+ }
996
+ lines.push("");
997
+ }
854
998
  }
855
999
  if (pack.packType === "page") {
856
1000
  const pagePack = pack;
@@ -872,7 +1016,18 @@ function renderExecutionPackMarkdown(pack) {
872
1016
  lines.push("");
873
1017
  lines.push("## Page Patterns");
874
1018
  for (const pattern of pagePack.data.patterns) {
875
- lines.push(`- ${pattern.alias} -> ${pattern.id} [${pattern.layout}${pattern.preset ? ` | ${pattern.preset}` : ""}]`);
1019
+ lines.push(
1020
+ `- ${pattern.alias} -> ${pattern.id} [${pattern.layout}${pattern.preset ? ` | ${pattern.preset}` : ""}]`
1021
+ );
1022
+ if (pattern.presetDescription) {
1023
+ lines.push(` > ${pattern.presetDescription}`);
1024
+ }
1025
+ if (pattern.interactions && pattern.interactions.length > 0) {
1026
+ lines.push(` **Interactions (MUST implement each \u2014 see DECANTR.md "Interaction Requirements"):**`);
1027
+ for (const interaction of pattern.interactions) {
1028
+ lines.push(` - [ ] ${interaction}`);
1029
+ }
1030
+ }
876
1031
  }
877
1032
  lines.push("");
878
1033
  if (pagePack.data.wiringSignals.length > 0) {
@@ -882,6 +1037,16 @@ function renderExecutionPackMarkdown(pack) {
882
1037
  }
883
1038
  lines.push("");
884
1039
  }
1040
+ if (pagePack.data.directives && pagePack.data.directives.length > 0) {
1041
+ lines.push("## Page Directives");
1042
+ lines.push("");
1043
+ lines.push("Execution-level rules for this route. Follow exactly.");
1044
+ lines.push("");
1045
+ for (const directive of pagePack.data.directives) {
1046
+ lines.push(`- ${directive}`);
1047
+ }
1048
+ lines.push("");
1049
+ }
885
1050
  }
886
1051
  if (pack.packType === "mutation") {
887
1052
  const mutationPack = pack;
@@ -889,7 +1054,9 @@ function renderExecutionPackMarkdown(pack) {
889
1054
  lines.push(`- Operation: ${mutationPack.data.mutationType}`);
890
1055
  lines.push(`- Shell: ${mutationPack.data.shell}`);
891
1056
  lines.push(`- Theme: ${mutationPack.data.theme.id} (${mutationPack.data.theme.mode})`);
892
- lines.push(`- Routing: ${mutationPack.data.routing}`);
1057
+ lines.push(
1058
+ `- Routing: ${mutationPack.data.routing} \u2192 ${routingImplementationHint(mutationPack.data.routing)}`
1059
+ );
893
1060
  if (mutationPack.data.features.length > 0) {
894
1061
  lines.push(`- Features: ${mutationPack.data.features.join(", ")}`);
895
1062
  }
@@ -897,7 +1064,9 @@ function renderExecutionPackMarkdown(pack) {
897
1064
  lines.push("## Route Topology");
898
1065
  for (const route of mutationPack.data.routes) {
899
1066
  const patterns = route.patternIds.length > 0 ? route.patternIds.join(", ") : "none";
900
- lines.push(`- ${route.path} -> ${route.pageId}${route.shell ? ` @ ${route.shell}` : ""} [${patterns}]`);
1067
+ lines.push(
1068
+ `- ${route.path} -> ${route.pageId}${route.shell ? ` @ ${route.shell}` : ""} [${patterns}]`
1069
+ );
901
1070
  }
902
1071
  lines.push("");
903
1072
  if (mutationPack.data.workflow.length > 0) {
@@ -922,7 +1091,9 @@ function renderExecutionPackMarkdown(pack) {
922
1091
  lines.push("## Review Topology");
923
1092
  for (const route of reviewPack.data.routes) {
924
1093
  const patterns = route.patternIds.length > 0 ? route.patternIds.join(", ") : "none";
925
- lines.push(`- ${route.path} -> ${route.pageId}${route.shell ? ` @ ${route.shell}` : ""} [${patterns}]`);
1094
+ lines.push(
1095
+ `- ${route.path} -> ${route.pageId}${route.shell ? ` @ ${route.shell}` : ""} [${patterns}]`
1096
+ );
926
1097
  }
927
1098
  lines.push("");
928
1099
  if (reviewPack.data.focusAreas.length > 0) {
@@ -940,6 +1111,14 @@ function renderExecutionPackMarkdown(pack) {
940
1111
  lines.push("");
941
1112
  }
942
1113
  }
1114
+ if (pack.packType === "page") {
1115
+ lines.push("## Shared Contract");
1116
+ lines.push(
1117
+ "Required setup, allowed vocabulary, success checks, anti-patterns, and token budget are shared across every page pack. The full list lives in the pack JSON sidecar (`page-<id>-pack.json`) and in the pack-manifest. Refer there instead of re-reading the same boilerplate 16 times."
1118
+ );
1119
+ lines.push("");
1120
+ return lines.join("\n").trimEnd() + "\n";
1121
+ }
943
1122
  lines.push("## Required Setup");
944
1123
  if (pack.requiredSetup.length === 0) {
945
1124
  lines.push("- None declared.");
@@ -954,9 +1133,24 @@ function renderExecutionPackMarkdown(pack) {
954
1133
  lines.push(...pack.allowedVocabulary.map((entry) => `- ${entry}`));
955
1134
  }
956
1135
  lines.push("");
957
- lines.push(...renderList("## Success Checks", pack.successChecks.map((entry) => `${entry.label} [${entry.severity}]`)));
958
- lines.push(...renderList("## Anti-Patterns", pack.antiPatterns.map((entry) => `${entry.summary}: ${entry.guidance}`)));
959
- lines.push(...renderList("## Examples", pack.examples.map((entry) => `${entry.label} (${entry.language})`)));
1136
+ lines.push(
1137
+ ...renderList(
1138
+ "## Success Checks",
1139
+ pack.successChecks.map((entry) => `${entry.label} [${entry.severity}]`)
1140
+ )
1141
+ );
1142
+ lines.push(
1143
+ ...renderList(
1144
+ "## Anti-Patterns",
1145
+ pack.antiPatterns.map((entry) => `${entry.summary}: ${entry.guidance}`)
1146
+ )
1147
+ );
1148
+ lines.push(
1149
+ ...renderList(
1150
+ "## Examples",
1151
+ pack.examples.map((entry) => `${entry.label} (${entry.language})`)
1152
+ )
1153
+ );
960
1154
  lines.push("## Token Budget");
961
1155
  lines.push(`- Target: ${pack.tokenBudget.target}`);
962
1156
  lines.push(`- Max: ${pack.tokenBudget.max}`);
@@ -976,35 +1170,45 @@ function listPackSections(essence) {
976
1170
  const declaredSections = essence.blueprint.sections;
977
1171
  if (declaredSections && declaredSections.length > 0) {
978
1172
  const routedSectionPages = new Set(
979
- Object.values(essence.blueprint.routes ?? {}).map((entry) => `${entry.section}:${entry.page}`)
1173
+ Object.values(essence.blueprint.routes ?? {}).map(
1174
+ (entry) => `${entry.section}:${entry.page}`
1175
+ )
980
1176
  );
981
1177
  return declaredSections.map((section) => {
982
- const pageIds = section.pages.filter((page) => routedSectionPages.size === 0 || routedSectionPages.has(`${section.id}:${page.id}`)).map((page) => page.id);
1178
+ const pageIds = section.pages.filter(
1179
+ (page) => routedSectionPages.size === 0 || routedSectionPages.has(`${section.id}:${page.id}`)
1180
+ ).map((page) => page.id);
983
1181
  return {
984
1182
  id: section.id,
985
1183
  role: section.role,
986
1184
  shell: section.shell,
987
1185
  description: section.description,
988
1186
  features: section.features,
989
- pageIds
1187
+ pageIds,
1188
+ ...Array.isArray(section.navigation_items) && section.navigation_items.length > 0 ? { navigationItems: section.navigation_items } : {},
1189
+ ...Array.isArray(section.directives) && section.directives.length > 0 ? { directives: section.directives } : {}
990
1190
  };
991
1191
  }).filter((section) => section.pageIds.length > 0);
992
1192
  }
993
1193
  const pages = essence.blueprint.pages ?? [{ id: "home", layout: ["hero"] }];
994
- return [{
995
- id: essence.meta.archetype || "default",
996
- role: "primary",
997
- shell: essence.blueprint.shell ?? "sidebar-main",
998
- description: `${essence.meta.archetype || "Application"} section`,
999
- features: essence.blueprint.features || [],
1000
- pageIds: pages.map((page) => page.id)
1001
- }];
1194
+ return [
1195
+ {
1196
+ id: essence.meta.archetype || "default",
1197
+ role: "primary",
1198
+ shell: essence.blueprint.shell ?? "sidebar-main",
1199
+ description: `${essence.meta.archetype || "Application"} section`,
1200
+ features: essence.blueprint.features || [],
1201
+ pageIds: pages.map((page) => page.id)
1202
+ }
1203
+ ];
1002
1204
  }
1003
1205
  function listPackPages(essence) {
1004
1206
  const declaredSections = essence.blueprint.sections;
1005
1207
  if (declaredSections && declaredSections.length > 0) {
1006
1208
  const routedSectionPages = new Set(
1007
- Object.values(essence.blueprint.routes ?? {}).map((entry) => `${entry.section}:${entry.page}`)
1209
+ Object.values(essence.blueprint.routes ?? {}).map(
1210
+ (entry) => `${entry.section}:${entry.page}`
1211
+ )
1008
1212
  );
1009
1213
  return declaredSections.flatMap(
1010
1214
  (section) => section.pages.map((page) => ({
@@ -1012,8 +1216,11 @@ function listPackPages(essence) {
1012
1216
  shell: page.shell_override ?? section.shell,
1013
1217
  sectionId: section.id,
1014
1218
  sectionRole: section.role,
1015
- features: section.features
1016
- })).filter((page) => routedSectionPages.size === 0 || routedSectionPages.has(`${page.sectionId}:${page.pageId}`))
1219
+ features: section.features,
1220
+ ...Array.isArray(page.directives) && page.directives.length > 0 ? { directives: page.directives } : {}
1221
+ })).filter(
1222
+ (page) => routedSectionPages.size === 0 || routedSectionPages.has(`${page.sectionId}:${page.pageId}`)
1223
+ )
1017
1224
  );
1018
1225
  }
1019
1226
  const pages = essence.blueprint.pages ?? [{ id: "home", layout: ["hero"] }];
@@ -1048,13 +1255,15 @@ function buildScaffoldPack(appNode, options = {}) {
1048
1255
  "Treat the declared routes as the topology source of truth.",
1049
1256
  "Preserve the resolved theme and shell contract unless the task explicitly mutates them."
1050
1257
  ],
1051
- allowedVocabulary: [.../* @__PURE__ */ new Set([
1052
- appNode.shell.config.type,
1053
- appNode.theme.id,
1054
- appNode.theme.mode,
1055
- ...appNode.features,
1056
- ...scopePatternIds
1057
- ])],
1258
+ allowedVocabulary: [
1259
+ .../* @__PURE__ */ new Set([
1260
+ appNode.shell.config.type,
1261
+ appNode.theme.id,
1262
+ appNode.theme.mode,
1263
+ ...appNode.features,
1264
+ ...scopePatternIds
1265
+ ])
1266
+ ],
1058
1267
  examples: options.examples ?? [],
1059
1268
  antiPatterns: options.antiPatterns ?? [],
1060
1269
  successChecks: options.successChecks ?? DEFAULT_SUCCESS_CHECKS,
@@ -1098,15 +1307,17 @@ function buildSectionPack(appNode, input, options = {}) {
1098
1307
  "Use the declared section routes as the source of truth for this slice of the app.",
1099
1308
  "Keep the section shell consistent unless the task explicitly changes the shell contract."
1100
1309
  ],
1101
- allowedVocabulary: [.../* @__PURE__ */ new Set([
1102
- input.id,
1103
- input.role,
1104
- input.shell,
1105
- appNode.theme.id,
1106
- appNode.theme.mode,
1107
- ...input.features,
1108
- ...scopePatternIds
1109
- ])],
1310
+ allowedVocabulary: [
1311
+ .../* @__PURE__ */ new Set([
1312
+ input.id,
1313
+ input.role,
1314
+ input.shell,
1315
+ appNode.theme.id,
1316
+ appNode.theme.mode,
1317
+ ...input.features,
1318
+ ...scopePatternIds
1319
+ ])
1320
+ ],
1110
1321
  examples: options.examples ?? [],
1111
1322
  antiPatterns: options.antiPatterns ?? [],
1112
1323
  successChecks: options.successChecks ?? DEFAULT_SECTION_SUCCESS_CHECKS,
@@ -1122,7 +1333,10 @@ function buildSectionPack(appNode, input, options = {}) {
1122
1333
  mode: appNode.theme.mode,
1123
1334
  shape: appNode.theme.shape
1124
1335
  },
1125
- routes
1336
+ routes,
1337
+ ...input.navigationItems && input.navigationItems.length > 0 ? { navigationItems: input.navigationItems } : {},
1338
+ ...input.directives && input.directives.length > 0 ? { directives: input.directives } : {},
1339
+ ...options.themeDecorators && options.themeDecorators.length > 0 ? { themeDecorators: options.themeDecorators } : {}
1126
1340
  },
1127
1341
  renderedMarkdown: ""
1128
1342
  };
@@ -1155,16 +1369,20 @@ function buildPagePack(appNode, input, options = {}) {
1155
1369
  "Keep the compiled route and shell contract stable for this page.",
1156
1370
  "Treat the listed page patterns as the primary structure for this route."
1157
1371
  ],
1158
- allowedVocabulary: [...new Set([
1159
- input.pageId,
1160
- input.shell,
1161
- input.sectionId ?? "",
1162
- input.sectionRole ?? "",
1163
- appNode.theme.id,
1164
- appNode.theme.mode,
1165
- ...input.features,
1166
- ...patterns.flatMap((pattern) => [pattern.id, pattern.alias, pattern.layout])
1167
- ].filter(Boolean))],
1372
+ allowedVocabulary: [
1373
+ ...new Set(
1374
+ [
1375
+ input.pageId,
1376
+ input.shell,
1377
+ input.sectionId ?? "",
1378
+ input.sectionRole ?? "",
1379
+ appNode.theme.id,
1380
+ appNode.theme.mode,
1381
+ ...input.features,
1382
+ ...patterns.flatMap((pattern) => [pattern.id, pattern.alias, pattern.layout])
1383
+ ].filter(Boolean)
1384
+ )
1385
+ ],
1168
1386
  examples: options.examples ?? [],
1169
1387
  antiPatterns: options.antiPatterns ?? [],
1170
1388
  successChecks: options.successChecks ?? DEFAULT_PAGE_SUCCESS_CHECKS,
@@ -1183,7 +1401,8 @@ function buildPagePack(appNode, input, options = {}) {
1183
1401
  shape: appNode.theme.shape
1184
1402
  },
1185
1403
  wiringSignals: pageNode.wiring?.signals.map((signal) => signal.name) ?? [],
1186
- patterns
1404
+ patterns,
1405
+ ...input.directives && input.directives.length > 0 ? { directives: input.directives } : {}
1187
1406
  },
1188
1407
  renderedMarkdown: ""
1189
1408
  };
@@ -1221,14 +1440,16 @@ function buildMutationPack(appNode, options) {
1221
1440
  "Treat the compiled topology as the source of truth until the essence changes.",
1222
1441
  "Refresh Decantr context after structural mutations so downstream tasks read current packs."
1223
1442
  ],
1224
- allowedVocabulary: [.../* @__PURE__ */ new Set([
1225
- options.mutationType,
1226
- appNode.shell.config.type,
1227
- appNode.theme.id,
1228
- appNode.theme.mode,
1229
- ...appNode.features,
1230
- ...scopePatternIds
1231
- ])],
1443
+ allowedVocabulary: [
1444
+ .../* @__PURE__ */ new Set([
1445
+ options.mutationType,
1446
+ appNode.shell.config.type,
1447
+ appNode.theme.id,
1448
+ appNode.theme.mode,
1449
+ ...appNode.features,
1450
+ ...scopePatternIds
1451
+ ])
1452
+ ],
1232
1453
  examples: options.examples ?? [],
1233
1454
  antiPatterns: options.antiPatterns ?? [],
1234
1455
  successChecks: options.successChecks ?? DEFAULT_MUTATION_SUCCESS_CHECKS[options.mutationType],
@@ -1286,15 +1507,17 @@ function buildReviewPack(appNode, options = {}) {
1286
1507
  "Read the compiled scaffold and route packs before reviewing code.",
1287
1508
  "Use concrete evidence from the workspace instead of purely stylistic intuition."
1288
1509
  ],
1289
- allowedVocabulary: [.../* @__PURE__ */ new Set([
1290
- reviewType,
1291
- appNode.shell.config.type,
1292
- appNode.theme.id,
1293
- appNode.theme.mode,
1294
- ...appNode.features,
1295
- ...scopePatternIds,
1296
- ...focusAreas
1297
- ])],
1510
+ allowedVocabulary: [
1511
+ .../* @__PURE__ */ new Set([
1512
+ reviewType,
1513
+ appNode.shell.config.type,
1514
+ appNode.theme.id,
1515
+ appNode.theme.mode,
1516
+ ...appNode.features,
1517
+ ...scopePatternIds,
1518
+ ...focusAreas
1519
+ ])
1520
+ ],
1298
1521
  examples: options.examples ?? [],
1299
1522
  antiPatterns: options.antiPatterns ?? DEFAULT_REVIEW_ANTI_PATTERNS,
1300
1523
  successChecks: options.successChecks ?? DEFAULT_REVIEW_SUCCESS_CHECKS,
@@ -1331,32 +1554,57 @@ async function compileExecutionPackBundle(essence, options = {}) {
1331
1554
  overridePaths: options.overridePaths,
1332
1555
  resolver: options.resolver
1333
1556
  });
1557
+ const navMeta = effectiveEssence.meta.navigation;
1558
+ const commandPaletteValue = typeof navMeta?.command_palette === "object" && navMeta.command_palette !== null ? navMeta.command_palette : Boolean(navMeta?.command_palette);
1334
1559
  const scaffold = buildScaffoldPack(pipeline.ir, {
1335
1560
  target: sharedTarget,
1336
1561
  navigation: {
1337
- commandPalette: Boolean(effectiveEssence.meta.navigation?.command_palette),
1338
- hotkeys: Array.isArray(effectiveEssence.meta.navigation?.hotkeys) ? effectiveEssence.meta.navigation.hotkeys.filter((hotkey) => Boolean(
1339
- hotkey && typeof hotkey.key === "string" && typeof hotkey.label === "string"
1340
- )).map((hotkey) => ({
1562
+ commandPalette: commandPaletteValue,
1563
+ hotkeys: Array.isArray(navMeta?.hotkeys) ? navMeta.hotkeys.filter(
1564
+ (hotkey) => Boolean(
1565
+ hotkey && typeof hotkey.key === "string" && typeof hotkey.label === "string"
1566
+ )
1567
+ ).map((hotkey) => ({
1341
1568
  key: hotkey.key,
1342
1569
  route: hotkey.route,
1343
- label: hotkey.label
1344
- })) : []
1570
+ label: hotkey.label,
1571
+ ...hotkey.semantics ? { semantics: hotkey.semantics } : {}
1572
+ })) : [],
1573
+ ...navMeta?.hotkey_semantics ? { hotkeySemantics: navMeta.hotkey_semantics } : {}
1345
1574
  }
1346
1575
  });
1347
1576
  const review = buildReviewPack(pipeline.ir, {
1348
1577
  target: sharedTarget
1349
1578
  });
1350
- const sections = listPackSections(effectiveEssence).map((section) => buildSectionPack(pipeline.ir, section, {
1351
- target: sharedTarget
1352
- }));
1353
- const pages = listPackPages(effectiveEssence).map((page) => buildPagePack(pipeline.ir, page, {
1354
- target: sharedTarget
1355
- }));
1356
- const mutations = ["add-page", "modify"].map((mutationType) => buildMutationPack(pipeline.ir, {
1357
- mutationType,
1358
- target: sharedTarget
1359
- }));
1579
+ const themeDecorators = (() => {
1580
+ const defs = pipeline.registryTheme?.decorator_definitions;
1581
+ if (!defs) return void 0;
1582
+ const entries = [];
1583
+ for (const [name, def] of Object.entries(defs)) {
1584
+ const intent = def.intent || def.description || "";
1585
+ const applyTo = (def.usage || []).join(", ");
1586
+ if (!intent && !applyTo) continue;
1587
+ entries.push({ class: name, intent, applyTo });
1588
+ }
1589
+ return entries.length > 0 ? entries : void 0;
1590
+ })();
1591
+ const sections = listPackSections(effectiveEssence).map(
1592
+ (section) => buildSectionPack(pipeline.ir, section, {
1593
+ target: sharedTarget,
1594
+ ...themeDecorators ? { themeDecorators } : {}
1595
+ })
1596
+ );
1597
+ const pages = listPackPages(effectiveEssence).map(
1598
+ (page) => buildPagePack(pipeline.ir, page, {
1599
+ target: sharedTarget
1600
+ })
1601
+ );
1602
+ const mutations = ["add-page", "modify"].map(
1603
+ (mutationType) => buildMutationPack(pipeline.ir, {
1604
+ mutationType,
1605
+ target: sharedTarget
1606
+ })
1607
+ );
1360
1608
  const manifest = {
1361
1609
  $schema: PACK_MANIFEST_SCHEMA_URL,
1362
1610
  version: "1.0.0",