@decantr/core 1.0.2 → 1.0.4

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,45 +822,127 @@ 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("");
929
+ if (scaffoldPack.data.themeDecorators && scaffoldPack.data.themeDecorators.length > 0) {
930
+ const escScaffoldCell = (s) => s.replace(/\|/g, "\\|");
931
+ lines.push(`## Required Theme Decorators (${scaffoldPack.data.theme.id})`);
932
+ lines.push("");
933
+ lines.push(
934
+ `These classes carry the active theme's visual identity. Tokens alone give bones; decorators give personality. Generated source MUST apply these across all sections \u2014 without them, every page reads as "themed colors only" with no theme character. Section packs reference this table; the contract is project-wide.`
935
+ );
936
+ lines.push("");
937
+ lines.push("| Class | Intent | Apply to |");
938
+ lines.push("|-------|--------|----------|");
939
+ for (const entry of scaffoldPack.data.themeDecorators) {
940
+ lines.push(
941
+ `| \`.${entry.class}\` | ${escScaffoldCell(entry.intent)} | ${escScaffoldCell(entry.applyTo)} |`
942
+ );
943
+ }
944
+ lines.push("");
945
+ }
833
946
  }
834
947
  if (pack.packType === "section") {
835
948
  const sectionPack = pack;
@@ -848,9 +961,64 @@ function renderExecutionPackMarkdown(pack) {
848
961
  lines.push("## Section Routes");
849
962
  for (const route of sectionPack.data.routes) {
850
963
  const patterns = route.patternIds.length > 0 ? route.patternIds.join(", ") : "none";
851
- lines.push(`- ${route.path} -> ${route.pageId}${route.shell ? ` @ ${route.shell}` : ""} [${patterns}]`);
964
+ lines.push(
965
+ `- ${route.path} -> ${route.pageId}${route.shell ? ` @ ${route.shell}` : ""} [${patterns}]`
966
+ );
852
967
  }
853
968
  lines.push("");
969
+ if (sectionPack.data.navigationItems && sectionPack.data.navigationItems.length > 0) {
970
+ lines.push("## Section Navigation");
971
+ lines.push("");
972
+ lines.push(
973
+ "Render these items in the shell's primary navigation. Exact match on label, route, and icon."
974
+ );
975
+ lines.push("");
976
+ for (const item of sectionPack.data.navigationItems) {
977
+ const parts = [`${item.label} \u2192 ${item.route}`];
978
+ if (item.icon) parts.push(`icon: ${item.icon}`);
979
+ if (item.hotkey) parts.push(`hotkey: ${item.hotkey}`);
980
+ if (item.badge) parts.push(`badge: ${item.badge}`);
981
+ if (item.active_match) parts.push(`active match: \`${item.active_match}\``);
982
+ lines.push(`- ${parts.join(" \xB7 ")}`);
983
+ }
984
+ lines.push("");
985
+ }
986
+ if (sectionPack.data.directives && sectionPack.data.directives.length > 0) {
987
+ lines.push("## Section Directives");
988
+ lines.push("");
989
+ lines.push(
990
+ "Execution-level rules every page in this section must obey. Follow exactly \u2014 these live in the pack contract, not narrative prose."
991
+ );
992
+ lines.push("");
993
+ for (const directive of sectionPack.data.directives) {
994
+ lines.push(`- ${directive}`);
995
+ }
996
+ lines.push("");
997
+ }
998
+ if (sectionPack.data.themeDecorators && sectionPack.data.themeDecorators.length > 0) {
999
+ const escCell = (s) => s.replace(/\|/g, "\\|");
1000
+ lines.push(`## Required Theme Decorators (${sectionPack.data.theme.id})`);
1001
+ lines.push("");
1002
+ lines.push(
1003
+ `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.`
1004
+ );
1005
+ lines.push("");
1006
+ lines.push("| Class | Intent | Apply to |");
1007
+ lines.push("|-------|--------|----------|");
1008
+ for (const entry of sectionPack.data.themeDecorators) {
1009
+ lines.push(
1010
+ `| \`.${entry.class}\` | ${escCell(entry.intent)} | ${escCell(entry.applyTo)} |`
1011
+ );
1012
+ }
1013
+ lines.push("");
1014
+ } else {
1015
+ lines.push("## Theme Decorators");
1016
+ lines.push("");
1017
+ lines.push(
1018
+ `Theme \`${sectionPack.data.theme.id}\` decorators are documented ONCE in \`scaffold-pack.md\` under "Required Theme Decorators". Apply them across this section's pages \u2014 the contract is the same project-wide. See also DECANTR.md "Decorator Quick Reference" for the same table.`
1019
+ );
1020
+ lines.push("");
1021
+ }
854
1022
  }
855
1023
  if (pack.packType === "page") {
856
1024
  const pagePack = pack;
@@ -872,7 +1040,18 @@ function renderExecutionPackMarkdown(pack) {
872
1040
  lines.push("");
873
1041
  lines.push("## Page Patterns");
874
1042
  for (const pattern of pagePack.data.patterns) {
875
- lines.push(`- ${pattern.alias} -> ${pattern.id} [${pattern.layout}${pattern.preset ? ` | ${pattern.preset}` : ""}]`);
1043
+ lines.push(
1044
+ `- ${pattern.alias} -> ${pattern.id} [${pattern.layout}${pattern.preset ? ` | ${pattern.preset}` : ""}]`
1045
+ );
1046
+ if (pattern.presetDescription) {
1047
+ lines.push(` > ${pattern.presetDescription}`);
1048
+ }
1049
+ if (pattern.interactions && pattern.interactions.length > 0) {
1050
+ lines.push(` **Interactions (MUST implement each \u2014 see DECANTR.md "Interaction Requirements"):**`);
1051
+ for (const interaction of pattern.interactions) {
1052
+ lines.push(` - [ ] ${interaction}`);
1053
+ }
1054
+ }
876
1055
  }
877
1056
  lines.push("");
878
1057
  if (pagePack.data.wiringSignals.length > 0) {
@@ -882,6 +1061,16 @@ function renderExecutionPackMarkdown(pack) {
882
1061
  }
883
1062
  lines.push("");
884
1063
  }
1064
+ if (pagePack.data.directives && pagePack.data.directives.length > 0) {
1065
+ lines.push("## Page Directives");
1066
+ lines.push("");
1067
+ lines.push("Execution-level rules for this route. Follow exactly.");
1068
+ lines.push("");
1069
+ for (const directive of pagePack.data.directives) {
1070
+ lines.push(`- ${directive}`);
1071
+ }
1072
+ lines.push("");
1073
+ }
885
1074
  }
886
1075
  if (pack.packType === "mutation") {
887
1076
  const mutationPack = pack;
@@ -889,7 +1078,9 @@ function renderExecutionPackMarkdown(pack) {
889
1078
  lines.push(`- Operation: ${mutationPack.data.mutationType}`);
890
1079
  lines.push(`- Shell: ${mutationPack.data.shell}`);
891
1080
  lines.push(`- Theme: ${mutationPack.data.theme.id} (${mutationPack.data.theme.mode})`);
892
- lines.push(`- Routing: ${mutationPack.data.routing}`);
1081
+ lines.push(
1082
+ `- Routing: ${mutationPack.data.routing} \u2192 ${routingImplementationHint(mutationPack.data.routing)}`
1083
+ );
893
1084
  if (mutationPack.data.features.length > 0) {
894
1085
  lines.push(`- Features: ${mutationPack.data.features.join(", ")}`);
895
1086
  }
@@ -897,7 +1088,9 @@ function renderExecutionPackMarkdown(pack) {
897
1088
  lines.push("## Route Topology");
898
1089
  for (const route of mutationPack.data.routes) {
899
1090
  const patterns = route.patternIds.length > 0 ? route.patternIds.join(", ") : "none";
900
- lines.push(`- ${route.path} -> ${route.pageId}${route.shell ? ` @ ${route.shell}` : ""} [${patterns}]`);
1091
+ lines.push(
1092
+ `- ${route.path} -> ${route.pageId}${route.shell ? ` @ ${route.shell}` : ""} [${patterns}]`
1093
+ );
901
1094
  }
902
1095
  lines.push("");
903
1096
  if (mutationPack.data.workflow.length > 0) {
@@ -922,7 +1115,9 @@ function renderExecutionPackMarkdown(pack) {
922
1115
  lines.push("## Review Topology");
923
1116
  for (const route of reviewPack.data.routes) {
924
1117
  const patterns = route.patternIds.length > 0 ? route.patternIds.join(", ") : "none";
925
- lines.push(`- ${route.path} -> ${route.pageId}${route.shell ? ` @ ${route.shell}` : ""} [${patterns}]`);
1118
+ lines.push(
1119
+ `- ${route.path} -> ${route.pageId}${route.shell ? ` @ ${route.shell}` : ""} [${patterns}]`
1120
+ );
926
1121
  }
927
1122
  lines.push("");
928
1123
  if (reviewPack.data.focusAreas.length > 0) {
@@ -940,6 +1135,14 @@ function renderExecutionPackMarkdown(pack) {
940
1135
  lines.push("");
941
1136
  }
942
1137
  }
1138
+ if (pack.packType === "page") {
1139
+ lines.push("## Shared Contract");
1140
+ lines.push(
1141
+ "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."
1142
+ );
1143
+ lines.push("");
1144
+ return lines.join("\n").trimEnd() + "\n";
1145
+ }
943
1146
  lines.push("## Required Setup");
944
1147
  if (pack.requiredSetup.length === 0) {
945
1148
  lines.push("- None declared.");
@@ -954,9 +1157,24 @@ function renderExecutionPackMarkdown(pack) {
954
1157
  lines.push(...pack.allowedVocabulary.map((entry) => `- ${entry}`));
955
1158
  }
956
1159
  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})`)));
1160
+ lines.push(
1161
+ ...renderList(
1162
+ "## Success Checks",
1163
+ pack.successChecks.map((entry) => `${entry.label} [${entry.severity}]`)
1164
+ )
1165
+ );
1166
+ lines.push(
1167
+ ...renderList(
1168
+ "## Anti-Patterns",
1169
+ pack.antiPatterns.map((entry) => `${entry.summary}: ${entry.guidance}`)
1170
+ )
1171
+ );
1172
+ lines.push(
1173
+ ...renderList(
1174
+ "## Examples",
1175
+ pack.examples.map((entry) => `${entry.label} (${entry.language})`)
1176
+ )
1177
+ );
960
1178
  lines.push("## Token Budget");
961
1179
  lines.push(`- Target: ${pack.tokenBudget.target}`);
962
1180
  lines.push(`- Max: ${pack.tokenBudget.max}`);
@@ -976,35 +1194,45 @@ function listPackSections(essence) {
976
1194
  const declaredSections = essence.blueprint.sections;
977
1195
  if (declaredSections && declaredSections.length > 0) {
978
1196
  const routedSectionPages = new Set(
979
- Object.values(essence.blueprint.routes ?? {}).map((entry) => `${entry.section}:${entry.page}`)
1197
+ Object.values(essence.blueprint.routes ?? {}).map(
1198
+ (entry) => `${entry.section}:${entry.page}`
1199
+ )
980
1200
  );
981
1201
  return declaredSections.map((section) => {
982
- const pageIds = section.pages.filter((page) => routedSectionPages.size === 0 || routedSectionPages.has(`${section.id}:${page.id}`)).map((page) => page.id);
1202
+ const pageIds = section.pages.filter(
1203
+ (page) => routedSectionPages.size === 0 || routedSectionPages.has(`${section.id}:${page.id}`)
1204
+ ).map((page) => page.id);
983
1205
  return {
984
1206
  id: section.id,
985
1207
  role: section.role,
986
1208
  shell: section.shell,
987
1209
  description: section.description,
988
1210
  features: section.features,
989
- pageIds
1211
+ pageIds,
1212
+ ...Array.isArray(section.navigation_items) && section.navigation_items.length > 0 ? { navigationItems: section.navigation_items } : {},
1213
+ ...Array.isArray(section.directives) && section.directives.length > 0 ? { directives: section.directives } : {}
990
1214
  };
991
1215
  }).filter((section) => section.pageIds.length > 0);
992
1216
  }
993
1217
  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
- }];
1218
+ return [
1219
+ {
1220
+ id: essence.meta.archetype || "default",
1221
+ role: "primary",
1222
+ shell: essence.blueprint.shell ?? "sidebar-main",
1223
+ description: `${essence.meta.archetype || "Application"} section`,
1224
+ features: essence.blueprint.features || [],
1225
+ pageIds: pages.map((page) => page.id)
1226
+ }
1227
+ ];
1002
1228
  }
1003
1229
  function listPackPages(essence) {
1004
1230
  const declaredSections = essence.blueprint.sections;
1005
1231
  if (declaredSections && declaredSections.length > 0) {
1006
1232
  const routedSectionPages = new Set(
1007
- Object.values(essence.blueprint.routes ?? {}).map((entry) => `${entry.section}:${entry.page}`)
1233
+ Object.values(essence.blueprint.routes ?? {}).map(
1234
+ (entry) => `${entry.section}:${entry.page}`
1235
+ )
1008
1236
  );
1009
1237
  return declaredSections.flatMap(
1010
1238
  (section) => section.pages.map((page) => ({
@@ -1012,8 +1240,11 @@ function listPackPages(essence) {
1012
1240
  shell: page.shell_override ?? section.shell,
1013
1241
  sectionId: section.id,
1014
1242
  sectionRole: section.role,
1015
- features: section.features
1016
- })).filter((page) => routedSectionPages.size === 0 || routedSectionPages.has(`${page.sectionId}:${page.pageId}`))
1243
+ features: section.features,
1244
+ ...Array.isArray(page.directives) && page.directives.length > 0 ? { directives: page.directives } : {}
1245
+ })).filter(
1246
+ (page) => routedSectionPages.size === 0 || routedSectionPages.has(`${page.sectionId}:${page.pageId}`)
1247
+ )
1017
1248
  );
1018
1249
  }
1019
1250
  const pages = essence.blueprint.pages ?? [{ id: "home", layout: ["hero"] }];
@@ -1048,13 +1279,15 @@ function buildScaffoldPack(appNode, options = {}) {
1048
1279
  "Treat the declared routes as the topology source of truth.",
1049
1280
  "Preserve the resolved theme and shell contract unless the task explicitly mutates them."
1050
1281
  ],
1051
- allowedVocabulary: [.../* @__PURE__ */ new Set([
1052
- appNode.shell.config.type,
1053
- appNode.theme.id,
1054
- appNode.theme.mode,
1055
- ...appNode.features,
1056
- ...scopePatternIds
1057
- ])],
1282
+ allowedVocabulary: [
1283
+ .../* @__PURE__ */ new Set([
1284
+ appNode.shell.config.type,
1285
+ appNode.theme.id,
1286
+ appNode.theme.mode,
1287
+ ...appNode.features,
1288
+ ...scopePatternIds
1289
+ ])
1290
+ ],
1058
1291
  examples: options.examples ?? [],
1059
1292
  antiPatterns: options.antiPatterns ?? [],
1060
1293
  successChecks: options.successChecks ?? DEFAULT_SUCCESS_CHECKS,
@@ -1069,7 +1302,8 @@ function buildScaffoldPack(appNode, options = {}) {
1069
1302
  routing: appNode.routing,
1070
1303
  features: appNode.features,
1071
1304
  navigation: options.navigation,
1072
- routes
1305
+ routes,
1306
+ ...options.themeDecorators && options.themeDecorators.length > 0 ? { themeDecorators: options.themeDecorators } : {}
1073
1307
  },
1074
1308
  renderedMarkdown: ""
1075
1309
  };
@@ -1098,15 +1332,17 @@ function buildSectionPack(appNode, input, options = {}) {
1098
1332
  "Use the declared section routes as the source of truth for this slice of the app.",
1099
1333
  "Keep the section shell consistent unless the task explicitly changes the shell contract."
1100
1334
  ],
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
- ])],
1335
+ allowedVocabulary: [
1336
+ .../* @__PURE__ */ new Set([
1337
+ input.id,
1338
+ input.role,
1339
+ input.shell,
1340
+ appNode.theme.id,
1341
+ appNode.theme.mode,
1342
+ ...input.features,
1343
+ ...scopePatternIds
1344
+ ])
1345
+ ],
1110
1346
  examples: options.examples ?? [],
1111
1347
  antiPatterns: options.antiPatterns ?? [],
1112
1348
  successChecks: options.successChecks ?? DEFAULT_SECTION_SUCCESS_CHECKS,
@@ -1122,7 +1358,10 @@ function buildSectionPack(appNode, input, options = {}) {
1122
1358
  mode: appNode.theme.mode,
1123
1359
  shape: appNode.theme.shape
1124
1360
  },
1125
- routes
1361
+ routes,
1362
+ ...input.navigationItems && input.navigationItems.length > 0 ? { navigationItems: input.navigationItems } : {},
1363
+ ...input.directives && input.directives.length > 0 ? { directives: input.directives } : {},
1364
+ ...options.themeDecorators && options.themeDecorators.length > 0 ? { themeDecorators: options.themeDecorators } : {}
1126
1365
  },
1127
1366
  renderedMarkdown: ""
1128
1367
  };
@@ -1155,16 +1394,20 @@ function buildPagePack(appNode, input, options = {}) {
1155
1394
  "Keep the compiled route and shell contract stable for this page.",
1156
1395
  "Treat the listed page patterns as the primary structure for this route."
1157
1396
  ],
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))],
1397
+ allowedVocabulary: [
1398
+ ...new Set(
1399
+ [
1400
+ input.pageId,
1401
+ input.shell,
1402
+ input.sectionId ?? "",
1403
+ input.sectionRole ?? "",
1404
+ appNode.theme.id,
1405
+ appNode.theme.mode,
1406
+ ...input.features,
1407
+ ...patterns.flatMap((pattern) => [pattern.id, pattern.alias, pattern.layout])
1408
+ ].filter(Boolean)
1409
+ )
1410
+ ],
1168
1411
  examples: options.examples ?? [],
1169
1412
  antiPatterns: options.antiPatterns ?? [],
1170
1413
  successChecks: options.successChecks ?? DEFAULT_PAGE_SUCCESS_CHECKS,
@@ -1183,7 +1426,8 @@ function buildPagePack(appNode, input, options = {}) {
1183
1426
  shape: appNode.theme.shape
1184
1427
  },
1185
1428
  wiringSignals: pageNode.wiring?.signals.map((signal) => signal.name) ?? [],
1186
- patterns
1429
+ patterns,
1430
+ ...input.directives && input.directives.length > 0 ? { directives: input.directives } : {}
1187
1431
  },
1188
1432
  renderedMarkdown: ""
1189
1433
  };
@@ -1221,14 +1465,16 @@ function buildMutationPack(appNode, options) {
1221
1465
  "Treat the compiled topology as the source of truth until the essence changes.",
1222
1466
  "Refresh Decantr context after structural mutations so downstream tasks read current packs."
1223
1467
  ],
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
- ])],
1468
+ allowedVocabulary: [
1469
+ .../* @__PURE__ */ new Set([
1470
+ options.mutationType,
1471
+ appNode.shell.config.type,
1472
+ appNode.theme.id,
1473
+ appNode.theme.mode,
1474
+ ...appNode.features,
1475
+ ...scopePatternIds
1476
+ ])
1477
+ ],
1232
1478
  examples: options.examples ?? [],
1233
1479
  antiPatterns: options.antiPatterns ?? [],
1234
1480
  successChecks: options.successChecks ?? DEFAULT_MUTATION_SUCCESS_CHECKS[options.mutationType],
@@ -1286,15 +1532,17 @@ function buildReviewPack(appNode, options = {}) {
1286
1532
  "Read the compiled scaffold and route packs before reviewing code.",
1287
1533
  "Use concrete evidence from the workspace instead of purely stylistic intuition."
1288
1534
  ],
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
- ])],
1535
+ allowedVocabulary: [
1536
+ .../* @__PURE__ */ new Set([
1537
+ reviewType,
1538
+ appNode.shell.config.type,
1539
+ appNode.theme.id,
1540
+ appNode.theme.mode,
1541
+ ...appNode.features,
1542
+ ...scopePatternIds,
1543
+ ...focusAreas
1544
+ ])
1545
+ ],
1298
1546
  examples: options.examples ?? [],
1299
1547
  antiPatterns: options.antiPatterns ?? DEFAULT_REVIEW_ANTI_PATTERNS,
1300
1548
  successChecks: options.successChecks ?? DEFAULT_REVIEW_SUCCESS_CHECKS,
@@ -1331,32 +1579,63 @@ async function compileExecutionPackBundle(essence, options = {}) {
1331
1579
  overridePaths: options.overridePaths,
1332
1580
  resolver: options.resolver
1333
1581
  });
1582
+ const navMeta = effectiveEssence.meta.navigation;
1583
+ const commandPaletteValue = typeof navMeta?.command_palette === "object" && navMeta.command_palette !== null ? navMeta.command_palette : Boolean(navMeta?.command_palette);
1584
+ const themeDecorators = (() => {
1585
+ const defs = pipeline.registryTheme?.decorator_definitions;
1586
+ if (!defs) return void 0;
1587
+ const entries = [];
1588
+ for (const [name, def] of Object.entries(defs)) {
1589
+ const intent = def.intent || def.description || "";
1590
+ const applyTo = (def.usage || []).join(", ");
1591
+ if (!intent && !applyTo) continue;
1592
+ entries.push({ class: name, intent, applyTo });
1593
+ }
1594
+ return entries.length > 0 ? entries : void 0;
1595
+ })();
1334
1596
  const scaffold = buildScaffoldPack(pipeline.ir, {
1335
1597
  target: sharedTarget,
1336
1598
  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) => ({
1599
+ commandPalette: commandPaletteValue,
1600
+ hotkeys: Array.isArray(navMeta?.hotkeys) ? navMeta.hotkeys.filter(
1601
+ (hotkey) => Boolean(
1602
+ hotkey && typeof hotkey.key === "string" && typeof hotkey.label === "string"
1603
+ )
1604
+ ).map((hotkey) => ({
1341
1605
  key: hotkey.key,
1342
1606
  route: hotkey.route,
1343
- label: hotkey.label
1344
- })) : []
1345
- }
1607
+ label: hotkey.label,
1608
+ ...hotkey.semantics ? { semantics: hotkey.semantics } : {}
1609
+ })) : [],
1610
+ ...navMeta?.hotkey_semantics ? { hotkeySemantics: navMeta.hotkey_semantics } : {}
1611
+ },
1612
+ ...themeDecorators ? { themeDecorators } : {}
1346
1613
  });
1347
1614
  const review = buildReviewPack(pipeline.ir, {
1348
1615
  target: sharedTarget
1349
1616
  });
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
- }));
1617
+ const sections = listPackSections(effectiveEssence).map(
1618
+ (section) => buildSectionPack(pipeline.ir, section, {
1619
+ target: sharedTarget
1620
+ // themeDecorators intentionally NOT passed to section packs as of 1.7.22.
1621
+ // Section packs render a one-line pointer to scaffold-pack instead of
1622
+ // duplicating the full decorator table — F2 Phase 1 reports flagged
1623
+ // 7-10× duplication across context files. The decorators are still
1624
+ // available on each section pack as section-pack.theme.id allows the
1625
+ // renderer to compose the pointer with the correct theme name.
1626
+ })
1627
+ );
1628
+ const pages = listPackPages(effectiveEssence).map(
1629
+ (page) => buildPagePack(pipeline.ir, page, {
1630
+ target: sharedTarget
1631
+ })
1632
+ );
1633
+ const mutations = ["add-page", "modify"].map(
1634
+ (mutationType) => buildMutationPack(pipeline.ir, {
1635
+ mutationType,
1636
+ target: sharedTarget
1637
+ })
1638
+ );
1360
1639
  const manifest = {
1361
1640
  $schema: PACK_MANIFEST_SCHEMA_URL,
1362
1641
  version: "1.0.0",