kitfly 0.2.1 → 0.2.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.
Files changed (108) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +25 -10
  3. package/VERSION +1 -1
  4. package/dist/_raw/content/guide/branding.md +146 -0
  5. package/dist/_raw/content/guide/data-driven-content.md +204 -0
  6. package/dist/_raw/content/reference/configuration.md +145 -7
  7. package/dist/_raw/content/reference/environment-variables.md +26 -1
  8. package/dist/_raw/content/reference/glossary.md +25 -1
  9. package/dist/_raw/content/reference/key-concepts.md +30 -2
  10. package/dist/_raw/content/reference/plugins.md +14 -0
  11. package/dist/_raw/docs/decisions/ADR-0006-data-driven-content.md +350 -0
  12. package/dist/content/deployment/preflight.html +10 -6
  13. package/dist/content/deployment/recipes/aws-s3.html +10 -6
  14. package/dist/content/deployment/recipes/cloudflare-pages.html +10 -6
  15. package/dist/content/deployment/recipes/cloudflare-r2.html +10 -6
  16. package/dist/content/deployment/recipes/fly-io.html +10 -6
  17. package/dist/content/deployment/recipes/github-pages.html +10 -6
  18. package/dist/content/deployment/recipes/netlify.html +10 -6
  19. package/dist/content/deployment/recipes/vercel.html +10 -6
  20. package/dist/content/deployment/secrets-and-env-vars.html +10 -6
  21. package/dist/content/deployment.html +10 -6
  22. package/dist/content/guide/approaches.html +10 -6
  23. package/dist/content/guide/branding.html +510 -0
  24. package/dist/content/guide/data-driven-content.html +543 -0
  25. package/dist/content/guide/features.html +10 -6
  26. package/dist/content/guide/getting-started.html +10 -6
  27. package/dist/content/guide/kitfly-overview.html +10 -6
  28. package/dist/content/reference/configuration.html +135 -9
  29. package/dist/content/reference/design-catalog.html +10 -6
  30. package/dist/content/reference/environment-variables.html +50 -8
  31. package/dist/content/reference/glossary.html +24 -8
  32. package/dist/content/reference/key-concepts.html +33 -9
  33. package/dist/content/reference/plugins.html +22 -7
  34. package/dist/content/reference/slides-authoring-guidelines.html +10 -6
  35. package/dist/content/reference/structure.html +10 -6
  36. package/dist/content/reference.html +10 -6
  37. package/dist/content/templates/crucible.html +10 -6
  38. package/dist/content/templates/handbook.html +10 -6
  39. package/dist/content/templates/minimal.html +10 -6
  40. package/dist/content/templates/overview.html +10 -6
  41. package/dist/content/templates/pipeline.html +10 -6
  42. package/dist/content/templates/productbook.html +10 -6
  43. package/dist/content/templates/runbook.html +10 -6
  44. package/dist/content/templates/servicebook.html +10 -6
  45. package/dist/content-index.json +29 -2
  46. package/dist/docs/decisions/ADR-0001-minimalist-site-code.html +10 -6
  47. package/dist/docs/decisions/ADR-0002-ai-accessibility.html +10 -6
  48. package/dist/docs/decisions/ADR-0003-single-file-bundle.html +10 -6
  49. package/dist/docs/decisions/ADR-0004-bun-runtime.html +10 -6
  50. package/dist/docs/decisions/ADR-0005-plugin-contract-and-distribution.html +10 -6
  51. package/dist/docs/decisions/ADR-0006-data-driven-content.html +752 -0
  52. package/dist/docs/decisions/DDR-0001-viewport-locked-layout.html +10 -6
  53. package/dist/docs/decisions/DDR-0002-theme-system.html +10 -6
  54. package/dist/docs/decisions/DDR-0003-bounded-logo-slot.html +10 -6
  55. package/dist/docs/decisions/DDR-0004-slides-rendering-model.html +10 -6
  56. package/dist/docs/decisions/DDR-0005-deterministic-layout-boundary.html +10 -6
  57. package/dist/docs/userguide/cli/build.html +10 -6
  58. package/dist/docs/userguide/cli/bundle.html +10 -6
  59. package/dist/docs/userguide/cli/dev.html +10 -6
  60. package/dist/docs/userguide/cli/init.html +10 -6
  61. package/dist/docs/userguide/cli/servers.html +10 -6
  62. package/dist/docs/userguide/cli/stop.html +10 -6
  63. package/dist/docs/userguide/cli/update.html +10 -6
  64. package/dist/docs/userguide/cli/version.html +10 -6
  65. package/dist/docs/userguide/cli.html +10 -6
  66. package/dist/docs/userguide/sharing.html +10 -6
  67. package/dist/index.html +10 -6
  68. package/dist/llms.txt +3 -3
  69. package/dist/provenance.json +4 -4
  70. package/dist/schemas/plugin-registry.schema.html +10 -6
  71. package/dist/schemas/plugin-schemas-notes.html +10 -6
  72. package/dist/schemas/plugin.schema.html +10 -6
  73. package/dist/schemas/plugins.schema.html +10 -6
  74. package/dist/schemas/v0/common.schema.html +14 -10
  75. package/dist/schemas/v0/plugin-registry.schema.html +13 -9
  76. package/dist/schemas/v0/plugin.schema.html +13 -9
  77. package/dist/schemas/v0/plugins.schema.html +13 -9
  78. package/dist/schemas/v0/site.schema.html +67 -7
  79. package/dist/schemas/v0/theme.schema.html +21 -17
  80. package/dist/schemas.html +10 -6
  81. package/dist/styles.css +39 -4
  82. package/package.json +1 -1
  83. package/plugins-dist/latex-runtime.js +140 -0
  84. package/plugins-dist/latex.js +178 -0
  85. package/plugins-dist/slides-charts-lite-runtime.js +179 -0
  86. package/plugins-dist/slides-charts-lite.js +198 -0
  87. package/registry/plugins.yaml +25 -0
  88. package/schemas/v0/site.schema.json +56 -0
  89. package/scripts/build.ts +191 -69
  90. package/scripts/bundle.ts +118 -10
  91. package/scripts/dev.ts +245 -166
  92. package/src/__tests__/brief.test.ts +151 -0
  93. package/src/__tests__/build.test.ts +169 -1
  94. package/src/__tests__/bundle.test.ts +134 -0
  95. package/src/__tests__/init.test.ts +51 -2
  96. package/src/__tests__/latex-runtime.bun.test.ts +35 -0
  97. package/src/__tests__/shared.test.ts +598 -1
  98. package/src/__tests__/slides-charts-lite-runtime.bun.test.ts +45 -0
  99. package/src/cli.ts +11 -4
  100. package/src/commands/init.ts +1 -1
  101. package/src/shared.ts +725 -18
  102. package/src/site/styles.css +39 -4
  103. package/src/site/template.html +5 -2
  104. package/src/templates/brief.ts +486 -0
  105. package/src/templates/deck.ts +59 -0
  106. package/src/templates/driver.ts +46 -13
  107. package/src/templates/handbook.ts +32 -0
  108. package/src/templates/runbook.ts +32 -0
@@ -0,0 +1,752 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ADR-0006: Data-Driven Content - Kitfly Docs</title>
7
+ <link rel="icon" type="image/png" sizes="32x32" href="../../assets/brand/kitfly-favicon-32.png">
8
+ <link rel="icon" type="image/png" sizes="64x64" href="../../assets/brand/kitfly-neon-256.png">
9
+ <link rel="stylesheet" href="../../styles.css">
10
+ <style id="kitfly-theme">
11
+ :root { --color-bg: #ffffff;
12
+ --color-bg-sidebar: #f5f7f8;
13
+ --color-text: #374151;
14
+ --color-text-muted: #6b7280;
15
+ --color-border: #e5e7eb;
16
+ --color-link: #007182;
17
+ --color-link-hover: #0a6172;
18
+ --color-accent: #152F46;
19
+ --color-code-bg: #f5f7f8;
20
+ --color-logo: #152F46;
21
+ --sidebar-width: 280px;
22
+ --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
23
+ --font-headings: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
24
+ --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; }
25
+ html { font-size: 16px; }
26
+ @media (prefers-color-scheme: dark) {
27
+ :root:not([data-theme="light"]) { --color-bg: #0d1117;
28
+ --color-bg-sidebar: #152F46;
29
+ --color-text: #e5e7eb;
30
+ --color-text-muted: #9ca3af;
31
+ --color-border: #374151;
32
+ --color-link: #709EA6;
33
+ --color-link-hover: #8fb5bc;
34
+ --color-accent: #f9fafb;
35
+ --color-code-bg: #152F46;
36
+ --color-logo: #f9fafb; }
37
+ }
38
+ [data-theme="dark"] { --color-bg: #0d1117;
39
+ --color-bg-sidebar: #152F46;
40
+ --color-text: #e5e7eb;
41
+ --color-text-muted: #9ca3af;
42
+ --color-border: #374151;
43
+ --color-link: #709EA6;
44
+ --color-link-hover: #8fb5bc;
45
+ --color-accent: #f9fafb;
46
+ --color-code-bg: #152F46;
47
+ --color-logo: #f9fafb; }
48
+ [data-theme="light"] { --color-bg: #ffffff;
49
+ --color-bg-sidebar: #f5f7f8;
50
+ --color-text: #374151;
51
+ --color-text-muted: #6b7280;
52
+ --color-border: #e5e7eb;
53
+ --color-link: #007182;
54
+ --color-link-hover: #0a6172;
55
+ --color-accent: #152F46;
56
+ --color-code-bg: #f5f7f8;
57
+ --color-logo: #152F46;
58
+ --sidebar-width: 280px;
59
+ --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
60
+ --font-headings: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
61
+ --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; }
62
+ </style>
63
+ <!-- Syntax highlighting - Prism.js -->
64
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism.min.css" id="prism-light">
65
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1/themes/prism-okaidia.min.css" id="prism-dark" disabled>
66
+
67
+ <script>
68
+ // Apply saved theme immediately to prevent flash
69
+ (function() {
70
+ const saved = localStorage.getItem('theme');
71
+ if (saved) {
72
+ document.documentElement.setAttribute('data-theme', saved);
73
+ }
74
+ // Set Prism theme based on saved or system preference
75
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
76
+ const isDark = saved === 'dark' || (!saved && prefersDark);
77
+ if (isDark) {
78
+ document.getElementById('prism-light')?.setAttribute('disabled', '');
79
+ document.getElementById('prism-dark')?.removeAttribute('disabled');
80
+ }
81
+ })();
82
+ </script>
83
+ </head>
84
+ <body class="mode-docs">
85
+ <div class="mobile-header">
86
+ <button class="nav-toggle" onclick="toggleNav()" aria-label="Toggle navigation">
87
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
88
+ <path d="M3 12h18M3 6h18M3 18h18"/>
89
+ </svg>
90
+ </button>
91
+ <a href="../../" class="mobile-logo" title="Home" data-initial="K">
92
+ <img src="../../assets/brand/kitfly-neon-256.png" alt="Kitfly" class="logo-img logo-icon" onerror="this.onerror=null;this.style.display='none';this.parentElement.classList.add('logo-fallback')">
93
+ </a>
94
+ <button class="mobile-theme-toggle" onclick="toggleTheme()" title="Toggle theme" aria-label="Toggle theme">
95
+ <svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
96
+ <circle cx="12" cy="12" r="5"/>
97
+ <path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
98
+ </svg>
99
+ <svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
100
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
101
+ </svg>
102
+ </button>
103
+ </div>
104
+ <div class="layout">
105
+ <nav class="sidebar">
106
+ <div class="sidebar-header">
107
+ <div class="logo logo-icon">
108
+ <a href="/" class="logo-icon" data-initial="K">
109
+ <img src="../../assets/brand/kitfly-neon-256.png" alt="Kitfly" class="logo-img" onerror="this.onerror=null;this.style.display='none';this.parentElement.classList.add('logo-fallback')">
110
+ </a>
111
+ <span class="logo-text">
112
+ <a href="/" class="brand">Kitfly</a>
113
+ <a href="../../" class="product">Kitfly Docs</a>
114
+ </span>
115
+ </div>
116
+ <div class="header-tools">
117
+ <button class="theme-toggle" onclick="toggleTheme()" title="Toggle theme" aria-label="Toggle theme">
118
+ <svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
119
+ <circle cx="12" cy="12" r="5"/>
120
+ <path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
121
+ </svg>
122
+ <svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
123
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
124
+ </svg>
125
+ </button>
126
+ <div class="sidebar-meta">
127
+ <span class="meta-version">v0.2.3</span>
128
+ <span class="meta-branch">HEAD</span>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ <div class="sidebar-nav">
133
+ <ul><li><a href="../../index.html" class="nav-home">Home</a></li><li><span class="nav-section">Guide</span><ul><li><a href="../../content/guide/approaches.html">approaches</a></li><li><a href="../../content/guide/branding.html">branding</a></li><li><a href="../../content/guide/data-driven-content.html">data-driven-content</a></li><li><a href="../../content/guide/features.html">features</a></li><li><a href="../../content/guide/getting-started.html">getting-started</a></li><li><a href="../../content/guide/kitfly-overview.html">kitfly-overview</a></li></ul></li><li><span class="nav-section">Templates</span><ul><li><a href="../../content/templates/crucible.html">crucible</a></li><li><a href="../../content/templates/handbook.html">handbook</a></li><li><a href="../../content/templates/minimal.html">minimal</a></li><li><a href="../../content/templates/overview.html">overview</a></li><li><a href="../../content/templates/pipeline.html">pipeline</a></li><li><a href="../../content/templates/productbook.html">productbook</a></li><li><a href="../../content/templates/runbook.html">runbook</a></li><li><a href="../../content/templates/servicebook.html">servicebook</a></li></ul></li><li><a href="../../content/reference.html" class="nav-section">Reference</a><ul><li><a href="../../content/reference/configuration.html">configuration</a></li><li><a href="../../content/reference/design-catalog.html">design-catalog</a></li><li><a href="../../content/reference/environment-variables.html">environment-variables</a></li><li><a href="../../content/reference/glossary.html">glossary</a></li><li><a href="../../content/reference/key-concepts.html">key-concepts</a></li><li><a href="../../content/reference/plugins.html">plugins</a></li><li><a href="../../content/reference/slides-authoring-guidelines.html">slides-authoring-guidelines</a></li><li><a href="../../content/reference/structure.html">structure</a></li></ul></li><li><a href="../../content/deployment.html" class="nav-section">Deployment</a><ul><li><a href="../../content/deployment/preflight.html">preflight</a></li><li><details><summary class="nav-group">recipes</summary><ul><li><a href="../../content/deployment/recipes/aws-s3.html">aws-s3</a></li><li><a href="../../content/deployment/recipes/cloudflare-pages.html">cloudflare-pages</a></li><li><a href="../../content/deployment/recipes/cloudflare-r2.html">cloudflare-r2</a></li><li><a href="../../content/deployment/recipes/fly-io.html">fly-io</a></li><li><a href="../../content/deployment/recipes/github-pages.html">github-pages</a></li><li><a href="../../content/deployment/recipes/netlify.html">netlify</a></li><li><a href="../../content/deployment/recipes/vercel.html">vercel</a></li></ul></details></li><li><a href="../../content/deployment/secrets-and-env-vars.html">secrets-and-env-vars</a></li></ul></li><li><span class="nav-section">User Guide</span><ul><li><details><summary class="nav-group"><a href="../../docs/userguide/cli.html">cli</a></summary><ul><li><a href="../../docs/userguide/cli/build.html">build</a></li><li><a href="../../docs/userguide/cli/bundle.html">bundle</a></li><li><a href="../../docs/userguide/cli/dev.html">dev</a></li><li><a href="../../docs/userguide/cli/init.html">init</a></li><li><a href="../../docs/userguide/cli/servers.html">servers</a></li><li><a href="../../docs/userguide/cli/stop.html">stop</a></li><li><a href="../../docs/userguide/cli/update.html">update</a></li><li><a href="../../docs/userguide/cli/version.html">version</a></li></ul></details></li><li><a href="../../docs/userguide/sharing.html">sharing</a></li></ul></li><li><span class="nav-section">Decisions</span><ul><li><a href="../../docs/decisions/ADR-0001-minimalist-site-code.html">ADR-0001-minimalist-site-code</a></li><li><a href="../../docs/decisions/ADR-0002-ai-accessibility.html">ADR-0002-ai-accessibility</a></li><li><a href="../../docs/decisions/ADR-0003-single-file-bundle.html">ADR-0003-single-file-bundle</a></li><li><a href="../../docs/decisions/ADR-0004-bun-runtime.html">ADR-0004-bun-runtime</a></li><li><a href="../../docs/decisions/ADR-0005-plugin-contract-and-distribution.html">ADR-0005-plugin-contract-and-distribution</a></li><li><a href="../../docs/decisions/ADR-0006-data-driven-content.html" class="active">ADR-0006-data-driven-content</a></li><li><a href="../../docs/decisions/DDR-0001-viewport-locked-layout.html">DDR-0001-viewport-locked-layout</a></li><li><a href="../../docs/decisions/DDR-0002-theme-system.html">DDR-0002-theme-system</a></li><li><a href="../../docs/decisions/DDR-0003-bounded-logo-slot.html">DDR-0003-bounded-logo-slot</a></li><li><a href="../../docs/decisions/DDR-0004-slides-rendering-model.html">DDR-0004-slides-rendering-model</a></li><li><a href="../../docs/decisions/DDR-0005-deterministic-layout-boundary.html">DDR-0005-deterministic-layout-boundary</a></li></ul></li><li><a href="../../schemas.html" class="nav-section">Schemas</a><ul><li><a href="../../schemas/plugin-registry.schema.html">plugin-registry.schema</a></li><li><a href="../../schemas/plugin-schemas-notes.html">plugin-schemas-notes</a></li><li><a href="../../schemas/plugin.schema.html">plugin.schema</a></li><li><a href="../../schemas/plugins.schema.html">plugins.schema</a></li><li><details><summary class="nav-group">v0</summary><ul><li><a href="../../schemas/v0/common.schema.html">common.schema</a></li><li><a href="../../schemas/v0/plugin-registry.schema.html">plugin-registry.schema</a></li><li><a href="../../schemas/v0/plugin.schema.html">plugin.schema</a></li><li><a href="../../schemas/v0/plugins.schema.html">plugins.schema</a></li><li><a href="../../schemas/v0/site.schema.html">site.schema</a></li><li><a href="../../schemas/v0/theme.schema.html">theme.schema</a></li></ul></details></li></ul></li></ul>
134
+ </div>
135
+ </nav>
136
+ <main class="content">
137
+ <article class="prose">
138
+ <nav class="breadcrumbs"><a href="../../docs/userguide/cli/build.html">Docs</a><span class="separator">›</span><a href="../../docs/decisions/ADR-0001-minimalist-site-code.html">Decisions</a><span class="separator">›</span><span>ADR-0006-data-driven-content</span></nav>
139
+
140
+ <h1 id="adr-0006-data-driven-content">ADR-0006: Data-Driven Content</h1>
141
+ <h2 id="status">Status</h2>
142
+ <p>Accepted (2026-02-17)</p>
143
+ <h2 id="context">Context</h2>
144
+ <p>Client engagements surfaced a real pattern: pages driven by structured data. The motivating case is a pricing page where ~15 scalar values appear in prose and ~10 computed tables/diagrams are generated from a 98-line JSON data file by a 368-line Python script.</p>
145
+ <p>Today&#39;s pipeline:</p>
146
+ <pre><code>pricing.json → generate-pricing.py → pricing.md (complete page) → kitfly → HTML
147
+ </code></pre>
148
+ <p>This works, but it conflates two concerns. The Python generator owns both computation (discount percentages, marginal bracket math, worked examples) and presentation (every paragraph of prose is embedded in string concatenation). Changing a word in a paragraph means editing Python. Changing a rate means regenerating the entire page.</p>
149
+ <p>The question is whether kitfly should provide any support for data-driven content, and if so, how much and with what constraints.</p>
150
+ <h3 id="what-we-are-not-building">What We Are Not Building</h3>
151
+ <p>This decision must be read against kitfly&#39;s identity. Kitfly is not a composable content framework. It is not Hugo, Eleventy, Astro, or Docusaurus. Those tools have mature data/template systems with loops, conditionals, expression evaluation, inheritance, and plugin hooks. If a project needs that power, it should use those tools.</p>
152
+ <p>Kitfly is a minimal markdown renderer. Data-driven content support must remain consistent with that identity: small, predictable, auditable, and bounded.</p>
153
+ <h2 id="terminology">Terminology</h2>
154
+ <p>A <strong>kitsite</strong> is the workspace that kitfly renders — the directory tree containing <code>site.yaml</code>, <code>content/</code>, <code>data/</code>, and optionally <code>scripts/</code>. A kitsite may be a documentation site, a slide deck, or any other kitfly output mode. It is typically a git repository but does not have to be. A kitsite created by <code>kitfly init</code> is standalone: it builds with <code>bun run build</code> and requires no kitfly CLI after setup.</p>
155
+ <p>A <strong>generator</strong> is any program that writes data files into a kitsite&#39;s data directory. Generators are user code, not kitfly code. They may live inside the kitsite (as a pre-build hook script) or outside it (as a separate service, CI step, or pipeline stage).</p>
156
+ <h2 id="decision">Decision</h2>
157
+ <h3 id="1-build-time-string-substitution-with-a-file-based-contract">1. Build-time string substitution with a file-based contract</h3>
158
+ <p>Kitfly will support resolving <code>{{ key }}</code> placeholders in markdown to string values from YAML or JSON data files, and <code>{{ snippet:name }}</code> placeholders to pre-rendered markdown blocks. Resolution happens before markdown rendering. Output is deterministic: same data + same markdown = same HTML.</p>
159
+ <p>The contract between kitfly and the outside world is the <strong>data file</strong> — not a code interface, not an import, not an API. A generator (in any language) writes a file into the kitsite. Kitfly reads and validates it. This is the entire integration surface.</p>
160
+ <h3 id="2-the-boundary-substitution-yes-logic-no">2. The boundary: substitution yes, logic no</h3>
161
+ <p>Kitfly handles:</p>
162
+ <table>
163
+ <thead>
164
+ <tr>
165
+ <th>Capability</th>
166
+ <th>Example</th>
167
+ <th>Nature</th>
168
+ </tr>
169
+ </thead>
170
+ <tbody><tr>
171
+ <td>Scalar value substitution</td>
172
+ <td><code>{{ baseline_rate | dollar }}</code></td>
173
+ <td>Lookup + format</td>
174
+ </tr>
175
+ <tr>
176
+ <td>Snippet injection</td>
177
+ <td><code>{{ snippet:pricing-table }}</code></td>
178
+ <td>Named block insertion</td>
179
+ </tr>
180
+ <tr>
181
+ <td>Built-in formatters</td>
182
+ <td><code>dollar</code>, <code>number</code>, <code>percent</code>, <code>round(n)</code>, <code>upper</code>, <code>lower</code></td>
183
+ <td>Pure display transforms</td>
184
+ </tr>
185
+ <tr>
186
+ <td>Formatter chaining</td>
187
+ <td><code>{{ key | round(0) | dollar }}</code></td>
188
+ <td>Left-to-right composition</td>
189
+ </tr>
190
+ <tr>
191
+ <td>Schema validation</td>
192
+ <td><code>data/pricing.schema.json</code></td>
193
+ <td>JSON Schema 2020-12</td>
194
+ </tr>
195
+ <tr>
196
+ <td>Error on missing values</td>
197
+ <td>Unresolved <code>{{ key }}</code> = build error</td>
198
+ <td>Fail loud, never silent</td>
199
+ </tr>
200
+ </tbody></table>
201
+ <p>Kitfly does not handle:</p>
202
+ <table>
203
+ <thead>
204
+ <tr>
205
+ <th>Excluded</th>
206
+ <th>Why</th>
207
+ </tr>
208
+ </thead>
209
+ <tbody><tr>
210
+ <td>Loops (<code>{% for %}</code>)</td>
211
+ <td>Template engine territory</td>
212
+ </tr>
213
+ <tr>
214
+ <td>Conditionals (<code>{% if %}</code>)</td>
215
+ <td>Template engine territory</td>
216
+ </tr>
217
+ <tr>
218
+ <td>Expression evaluation (<code>{{ x * y }}</code>)</td>
219
+ <td>Computation belongs in generators</td>
220
+ </tr>
221
+ <tr>
222
+ <td>User-defined formatters</td>
223
+ <td>Extensibility risk; closed set keeps contract stable</td>
224
+ </tr>
225
+ <tr>
226
+ <td>Object/array iteration</td>
227
+ <td>Pre-build hook territory</td>
228
+ </tr>
229
+ <tr>
230
+ <td>Data fetching (HTTP, DB)</td>
231
+ <td>Network I/O in builds violates determinism</td>
232
+ </tr>
233
+ </tbody></table>
234
+ <p>This boundary is the core architectural decision. Everything above the line is kitfly. Everything below the line is user code, triggered by pre-build hooks. The line does not move.</p>
235
+ <h3 id="3-formatters-are-deterministic-declarative-and-closed">3. Formatters are deterministic, declarative, and closed</h3>
236
+ <p>Every formatter is a pure function: <code>y = f(x)</code>. String in, string out. No side effects, no data access, no branching, no state.</p>
237
+ <p>Chaining composes left to right: <code>{{ key | round(2) | dollar }}</code> is <code>dollar(round(2, key))</code>. Each stage receives a string and returns a string. The pipeline is deterministic and testable in isolation.</p>
238
+ <p>The formatter set is closed. Adding a formatter requires a kitfly code change, review, and release. There are no user-defined formatters, no plugin hooks into the binding layer, no runtime extension points. This constraint is deliberate: it keeps the substitution layer auditable and prevents kitfly from becoming a template engine through accumulation.</p>
239
+ <h3 id="4-no-code-interface-for-generators">4. No code interface for generators</h3>
240
+ <p>Kitfly does not provide a TypeScript interface, base class, or SDK for generator scripts. The contract is:</p>
241
+ <table>
242
+ <thead>
243
+ <tr>
244
+ <th>Convention</th>
245
+ <th>Requirement</th>
246
+ </tr>
247
+ </thead>
248
+ <tbody><tr>
249
+ <td>Output location</td>
250
+ <td>Write to <code>data/</code> with a path matching the page&#39;s <code>data:</code> frontmatter</td>
251
+ </tr>
252
+ <tr>
253
+ <td>Output format</td>
254
+ <td>YAML or JSON, conforming to schema if one exists</td>
255
+ </tr>
256
+ <tr>
257
+ <td>Exit code</td>
258
+ <td>0 on success, non-zero on failure</td>
259
+ </tr>
260
+ <tr>
261
+ <td>Idempotency</td>
262
+ <td>Same input should produce same output</td>
263
+ </tr>
264
+ <tr>
265
+ <td>Language</td>
266
+ <td>Any (TypeScript recommended if kitfly-managed; Python, Go, shell all valid)</td>
267
+ </tr>
268
+ <tr>
269
+ <td>Check mode</td>
270
+ <td>Generators SHOULD support <code>--check</code> to verify output is current without regenerating</td>
271
+ </tr>
272
+ </tbody></table>
273
+ <p>This is a file-based contract. The generator writes a file; kitfly reads it. No imports, no extends, no implements. The schema is the type system. The file system is the integration bus.</p>
274
+ <p><strong>Rationale:</strong> A code interface would couple user code to kitfly internals, exclude non-TypeScript generators, and create a dependency direction that doesn&#39;t exist today. The Unix pattern — programs communicate through files and exit codes — is more aligned with kitfly&#39;s philosophy and more portable.</p>
275
+ <h3 id="5-pre-build-hooks-pass-environment-context">5. Pre-build hooks pass environment context</h3>
276
+ <p>When kitfly runs a pre-build hook, it sets environment variables so the generator can discover its context without hardcoding paths:</p>
277
+ <table>
278
+ <thead>
279
+ <tr>
280
+ <th>Variable</th>
281
+ <th>Value</th>
282
+ <th>Example</th>
283
+ </tr>
284
+ </thead>
285
+ <tbody><tr>
286
+ <td><code>KITFLY_SITE_ROOT</code></td>
287
+ <td>Absolute path to site root</td>
288
+ <td><code>/Users/me/my-docs</code></td>
289
+ </tr>
290
+ <tr>
291
+ <td><code>KITFLY_DATA_DIR</code></td>
292
+ <td>Data directory relative to root</td>
293
+ <td><code>data/</code></td>
294
+ </tr>
295
+ <tr>
296
+ <td><code>KITFLY_BUILD_MODE</code></td>
297
+ <td>Current build mode</td>
298
+ <td><code>dev</code>, <code>build</code>, or <code>bundle</code></td>
299
+ </tr>
300
+ <tr>
301
+ <td><code>KITFLY_PROFILE</code></td>
302
+ <td>Active content profile (if any)</td>
303
+ <td><code>internal</code></td>
304
+ </tr>
305
+ </tbody></table>
306
+ <p>Generators use these or ignore them. Most simple generators (like the pricing case) won&#39;t need them — they know their own paths. The env vars exist for the case where a generator serves multiple sites or adapts to build context.</p>
307
+ <h3 id="6-generators-are-a-first-class-concern-outside-kitfly-core">6. Generators are a first-class concern — outside kitfly core</h3>
308
+ <p>The generator — the code that transforms external business data into kitfly&#39;s data file contract — is where real-world friction lives. Kitfly&#39;s binding layer is ~90 lines of bounded substitution. The generator that turns &quot;pricing spreadsheet managed by a VP of Sales&quot; into <code>data/pricing.yaml</code> is the actual work.</p>
309
+ <p><strong>Data rarely belongs in the repo.</strong> In-repo JSON or YAML is a development convenience. In production, business data lives in:</p>
310
+ <ul>
311
+ <li>Spreadsheets (Excel, Google Sheets) maintained by business stakeholders</li>
312
+ <li>Headless CMS platforms (Directus, Strapi, Sanity)</li>
313
+ <li>SaaS APIs (Airtable, Notion databases, internal services)</li>
314
+ <li>Backend-as-a-Service or configuration stores</li>
315
+ <li>CI/CD secrets or environment-specific config</li>
316
+ </ul>
317
+ <p>The generator bridges that gap. Kitfly does not own generator code, but kitfly has a responsibility to make the generator&#39;s job clear and achievable.</p>
318
+ <p><strong>What kitfly provides for generators:</strong></p>
319
+ <table>
320
+ <thead>
321
+ <tr>
322
+ <th>Layer</th>
323
+ <th>Kitfly&#39;s contribution</th>
324
+ </tr>
325
+ </thead>
326
+ <tbody><tr>
327
+ <td>Contract</td>
328
+ <td>The data file schema — what shape the output must take</td>
329
+ </tr>
330
+ <tr>
331
+ <td>Validation</td>
332
+ <td>Schema enforcement at build time — generators get clear error messages when output is wrong</td>
333
+ </tr>
334
+ <tr>
335
+ <td>Environment</td>
336
+ <td><code>KITFLY_*</code> env vars so generators can discover site context</td>
337
+ </tr>
338
+ <tr>
339
+ <td>Conventions</td>
340
+ <td>Documented patterns for exit codes, idempotency, <code>--check</code> mode</td>
341
+ </tr>
342
+ <tr>
343
+ <td>Reference examples</td>
344
+ <td>Documented, tested generator patterns for common data sources (CSV, API, spreadsheet)</td>
345
+ </tr>
346
+ </tbody></table>
347
+ <p><strong>What kitfly does not provide:</strong></p>
348
+ <table>
349
+ <thead>
350
+ <tr>
351
+ <th>Layer</th>
352
+ <th>Why not</th>
353
+ </tr>
354
+ </thead>
355
+ <tbody><tr>
356
+ <td>Generator SDK / base class</td>
357
+ <td>Would couple user code to kitfly internals and exclude non-TypeScript generators</td>
358
+ </tr>
359
+ <tr>
360
+ <td>Data source connectors</td>
361
+ <td>Network I/O, auth, pagination — these are generator concerns, not build concerns</td>
362
+ </tr>
363
+ <tr>
364
+ <td>Generator hosting / registry</td>
365
+ <td>Generators are site-local scripts, not distributable packages</td>
366
+ </tr>
367
+ </tbody></table>
368
+ <p><strong>Future consideration: <code>kitflygen</code> tooling.</strong> As the generator pattern matures, there may be value in a separate tool or package that scaffolds generators for common data sources: <code>kitflygen init --source airtable</code>, <code>kitflygen init --source csv</code>. This is a v0.3+ concern and a separate domain — but the data file contract defined here is designed to be stable enough to support it. Naming it now prevents us from accidentally making architectural choices that foreclose it.</p>
369
+ <h3 id="7-the-control-plane-pattern-is-supported-not-owned">7. The control plane pattern is supported, not owned</h3>
370
+ <p>A common architecture uses site generators as build steps in a signal-driven pipeline. Kitfly supports this pattern at two levels of complexity, depending on where the generator lives.</p>
371
+ <p><strong>Simple: generator inside the kitsite (pre-build hook)</strong></p>
372
+ <pre><code>External signal → CI checks out kitsite → prebuild hook runs generator → data.yaml → kitfly build → deploy
373
+ </code></pre>
374
+ <p>The generator is a script in <code>scripts/</code>, declared as a <code>prebuild:</code> command in site.yaml. It runs in the kitsite&#39;s environment, fetches or transforms data, and writes to <code>data/</code>. This is the default pattern for teams whose data acquisition is straightforward (single API call, CSV download, config lookup).</p>
375
+ <p><strong>Advanced: generator outside the kitsite (separate pipeline stage)</strong></p>
376
+ <pre><code>External signal → data pipeline runs generator → writes data files into kitsite → kitfly build → deploy
377
+ </code></pre>
378
+ <p>The generator lives in its own repository, service, or CI job. It has its own dependencies, credentials, and error handling. Its only interaction with the kitsite is writing schema-conforming data files into the data directory. The kitsite doesn&#39;t need to know how the data arrived — it validates and builds.</p>
379
+ <p>This model is appropriate when:</p>
380
+ <ul>
381
+ <li>The generator has complex dependencies (database drivers, API clients, ML models) that don&#39;t belong in a kitsite</li>
382
+ <li>Multiple kitsites consume output from the same generator</li>
383
+ <li>Data acquisition requires credentials or network access that shouldn&#39;t be in the kitsite&#39;s environment</li>
384
+ <li>The generator runs on a schedule independent of site builds</li>
385
+ </ul>
386
+ <p><strong>What kitfly provides for both models:</strong></p>
387
+ <ul>
388
+ <li>Data files are validated against schemas (the contract holds regardless of data source)</li>
389
+ <li>Builds are deterministic (same data.yaml = same HTML, always)</li>
390
+ <li>Exit codes propagate (pre-build hook failure = build failure)</li>
391
+ <li>Missing or malformed data files are build errors, not silent omissions</li>
392
+ </ul>
393
+ <p><strong>What kitfly does not own:</strong> signal routing (that&#39;s CI/CD), data acquisition (that&#39;s the generator), deployment (that&#39;s infrastructure), and generator lifecycle (that&#39;s the team&#39;s engineering concern). Kitfly is a reliable, predictable build step in someone else&#39;s pipeline. This is the right scope.</p>
394
+ <p>The important implication: <strong>a kitsite can be &quot;live&quot; without being dynamic.</strong> A site that rebuilds on webhook from a headless CMS delivers fresh content without client-side JavaScript, without a runtime server, without a database connection. The content is always static HTML. The pipeline is what makes it responsive to change.</p>
395
+ <h3 id="8-data-lives-inside-the-kitsite-no-tree-escaping">8. Data lives inside the kitsite — no tree escaping</h3>
396
+ <p>Data files in their final, schema-conforming form must reside within the kitsite workspace. The data directory (default <code>data/</code>, configurable via <code>dataroot:</code> in site.yaml) is relative to site root and must resolve within the kitsite&#39;s directory tree.</p>
397
+ <p><strong>Containment rules:</strong></p>
398
+ <table>
399
+ <thead>
400
+ <tr>
401
+ <th>Rule</th>
402
+ <th>Rationale</th>
403
+ </tr>
404
+ </thead>
405
+ <tbody><tr>
406
+ <td>Data paths are relative to site root</td>
407
+ <td>Portability — the kitsite works on any machine</td>
408
+ </tr>
409
+ <tr>
410
+ <td>No <code>../</code> traversal out of the kitsite</td>
411
+ <td>Security and containment — builds are self-contained</td>
412
+ </tr>
413
+ <tr>
414
+ <td>No absolute paths</td>
415
+ <td>Portability — no machine-specific assumptions</td>
416
+ </tr>
417
+ <tr>
418
+ <td>No symlinks to external locations</td>
419
+ <td>The kitsite is the boundary; dependencies must be materialized inside it</td>
420
+ </tr>
421
+ <tr>
422
+ <td>No URI schemes (<code>s3://</code>, <code>https://</code>)</td>
423
+ <td>Data fetching belongs in generators, not in kitfly&#39;s path resolution</td>
424
+ </tr>
425
+ </tbody></table>
426
+ <p><strong>The source data can be anything, anywhere.</strong> An Excel sheet on SharePoint. An Airtable base. A Directus collection. A REST API. A database query. Kitfly doesn&#39;t know and doesn&#39;t care. The generator is the adapter that bridges external data into the kitsite&#39;s data directory in a form that conforms to the schema.</p>
427
+ <p><strong>The generator can live anywhere too.</strong> It may be a script inside the kitsite (the simple case: <code>scripts/generate-pricing-data.ts</code> run as a pre-build hook). Or it may live in a completely separate environment — a CI pipeline step, a scheduled job, a microservice that writes data files into a mounted volume or committed checkout. Kitfly&#39;s only requirement is that by build time, the data files exist inside the kitsite and pass schema validation.</p>
428
+ <p><strong><code>dataroot:</code> configuration.</strong> The data directory name defaults to <code>data/</code> but can be overridden in site.yaml for teams that prefer <code>_data/</code>, <code>src/data/</code>, or another convention. The same containment rules apply — the path must resolve within the kitsite.</p>
429
+ <p><strong>Typical layout — generator with external data source:</strong></p>
430
+ <pre><code>my-kitsite/
431
+ ├── site.yaml
432
+ ├── data/
433
+ │ ├── pricing.yaml # OUTPUT of generator (committed or .gitignored)
434
+ │ └── pricing.schema.json # validates generator output
435
+ ├── content/
436
+ │ └── product/
437
+ │ └── pricing.md # uses {{ key }} bindings
438
+ └── scripts/
439
+ └── generate-pricing-data.ts # pre-build hook — fetches from API/CSV/CMS
440
+ </code></pre>
441
+ <p><strong>Simpler layout — hand-authored data (DRY / small sites):</strong></p>
442
+ <pre><code>my-kitsite/
443
+ ├── site.yaml
444
+ ├── data/
445
+ │ └── team.yaml # hand-maintained, values reused across pages
446
+ └── content/
447
+ ├── about.md # {{ lead_name }}, {{ team_size }}
448
+ └── contact.md # {{ lead_email }}
449
+ </code></pre>
450
+ <p><strong>Automated layout — generator lives outside the kitsite:</strong></p>
451
+ <pre><code># The kitsite (deployed, built by CI)
452
+ my-kitsite/
453
+ ├── site.yaml
454
+ ├── data/
455
+ │ └── pricing.yaml # written by external pipeline before build
456
+ ├── content/
457
+ │ └── product/
458
+ │ └── pricing.md
459
+
460
+ # Separate repo / service (not part of the kitsite)
461
+ data-pipeline/
462
+ ├── generators/
463
+ │ └── pricing-generator.ts # fetches from Airtable, writes to kitsite/data/
464
+ └── config/
465
+ └── sources.yaml # data source credentials and endpoints
466
+ </code></pre>
467
+ <p>Whether <code>data/pricing.yaml</code> is committed (reproducible builds without generator) or <code>.gitignored</code> (always regenerated) is a team decision. Both patterns are valid. The schema validates the shape regardless of provenance.</p>
468
+ <h3 id="9-error-handling-fail-loud-never-silent">9. Error handling: fail loud, never silent</h3>
469
+ <table>
470
+ <thead>
471
+ <tr>
472
+ <th>Condition</th>
473
+ <th>Behavior</th>
474
+ </tr>
475
+ </thead>
476
+ <tbody><tr>
477
+ <td>Data file not found</td>
478
+ <td>Build error with path context</td>
479
+ </tr>
480
+ <tr>
481
+ <td>Unresolved <code>{{ key }}</code></td>
482
+ <td>Build error — never silently empty</td>
483
+ </tr>
484
+ <tr>
485
+ <td>Unknown snippet slot</td>
486
+ <td>Build error with file and slot name</td>
487
+ </tr>
488
+ <tr>
489
+ <td>Formatter parse failure</td>
490
+ <td>Build error (e.g., <code>dollar</code> on <code>&quot;hello&quot;</code>)</td>
491
+ </tr>
492
+ <tr>
493
+ <td>Unknown formatter</td>
494
+ <td>Build error with formatter name</td>
495
+ </tr>
496
+ <tr>
497
+ <td>Schema validation failure</td>
498
+ <td>Build error with field path and constraint</td>
499
+ </tr>
500
+ </tbody></table>
501
+ <p>This is non-negotiable for data-driven content. A pricing page with a missing value means publishing wrong numbers. A report with an empty <code>{{ revenue }}</code> is worse than a build failure. The system must refuse to produce output when data is incomplete.</p>
502
+ <h2 id="constraints">Constraints</h2>
503
+ <p>These constraints are guardrails, not limitations. They keep kitfly honest about what it is.</p>
504
+ <ol>
505
+ <li><p><strong>~90 lines of site code.</strong> The binding resolution layer (load data, resolve values, resolve snippets, formatters) must fit within kitfly&#39;s line budget. If it can&#39;t be implemented in ~90 lines, the design is too complex.</p>
506
+ </li>
507
+ <li><p><strong>No new dependencies.</strong> YAML parsing uses the existing parser in <code>shared.ts</code>. JSON Schema validation uses built-in <code>Bun</code> APIs or stays under 50 lines of hand-written validation. No ajv, no yaml library, no template engine.</p>
508
+ </li>
509
+ <li><p><strong>No syntax beyond <code>{{ key }}</code> and <code>{{ snippet:name }}</code>.</strong> No block tags, no comment directives, no frontmatter-driven conditionals. If you need logic, write a generator.</p>
510
+ </li>
511
+ <li><p><strong>Formatters are the only &quot;logic.&quot;</strong> And they are pure functions on single values. If a formatter needs to read data, branch on conditions, or access context — it doesn&#39;t belong in the formatter set.</p>
512
+ </li>
513
+ <li><p><strong>The file-based contract is the only integration surface.</strong> No TypeScript interfaces for generators. No kitfly SDK. No plugin hooks into the binding layer. The schema validates the shape. The file system carries the data.</p>
514
+ </li>
515
+ <li><p><strong>Data values are strings.</strong> Formatters handle display concerns. There is no type system, no coercion rules, no typed objects in the binding layer.</p>
516
+ </li>
517
+ </ol>
518
+ <h2 id="consequences">Consequences</h2>
519
+ <h3 id="positive">Positive</h3>
520
+ <ul>
521
+ <li><strong>Separation of concerns.</strong> Prose lives in markdown files that humans can read and edit. Computation lives in generator scripts. Data lives in validated YAML/JSON. Each layer does what it&#39;s good at.</li>
522
+ <li><strong>The pricing page becomes maintainable.</strong> Changing a word in a paragraph means editing markdown. Changing a rate means editing a data file. Changing computation logic means editing a TypeScript generator. No single file owns everything.</li>
523
+ <li><strong>Python dependency eliminated.</strong> The generator migrates to TypeScript (same ecosystem as kitfly). Not a hard requirement — the contract is language-agnostic — but a practical win for teams already using Bun.</li>
524
+ <li><strong>Hot reload path.</strong> Pre-build hooks + data watching means: edit data → hook runs → data.yaml updates → kitfly resolves bindings → page rebuilds. No manual generator step during development.</li>
525
+ <li><strong>CI/CD composability.</strong> Kitfly is a predictable build step. Signal-driven rebuild pipelines (webhook → generator → kitfly build → deploy) work without kitfly owning the orchestration.</li>
526
+ <li><strong>Data externalization is a documented path.</strong> Teams are guided toward keeping business data in business systems (spreadsheets, CMS, APIs) and using generators to bridge the gap — rather than embedding data in the repo as a default.</li>
527
+ </ul>
528
+ <h3 id="negative">Negative</h3>
529
+ <ul>
530
+ <li><strong>Two-stage debugging.</strong> When a value looks wrong, you debug across two layers: is the generator producing the right data? Or is the binding resolving correctly? Mitigation: clear error messages with file, line, and key context. The <code>--check</code> convention helps CI catch stale data.</li>
531
+ <li><strong>Snippet-heavy data files are awkward.</strong> A data file with 10 pre-rendered markdown tables is a serialized markdown artifact stored in YAML. It&#39;s generated output in a data format. This is structurally sound but aesthetically uncomfortable. Documentation should distinguish between values (simple key-value) and fragments (pre-rendered blocks).</li>
532
+ <li><strong>The 80/20 split favors generators.</strong> For a pricing page, ~80% of content is computed (tables, diagrams, worked examples). Data bindings handle the ~20% that&#39;s scalar values in prose. Teams must understand that data bindings complement generators — they don&#39;t replace them.</li>
533
+ <li><strong>Generator guidance is a commitment.</strong> Acknowledging generators as a first-class concern means maintaining reference patterns and documentation for common data sources. This is a content and support obligation, not a code obligation — but it&#39;s real work.</li>
534
+ </ul>
535
+ <h3 id="neutral">Neutral</h3>
536
+ <ul>
537
+ <li><strong>&quot;Can kitfly do X?&quot; has a clear answer.</strong> Lookup + format = yes. Logic + iteration = no, use a generator. The boundary is documented and stable.</li>
538
+ <li><strong>Migration path is unchanged.</strong> Teams that outgrow this pattern — needing loops, conditionals, component composition — should migrate to Astro, Eleventy, or Hugo. Their content is still markdown. Their data files are still YAML/JSON. The migration cost is in build tooling, not content.</li>
539
+ </ul>
540
+ <h2 id="alternatives-considered">Alternatives Considered</h2>
541
+ <h3 id="embed-a-template-engine-nunjucks-liquid-handlebars">Embed a template engine (Nunjucks, Liquid, Handlebars)</h3>
542
+ <p>Rejected. Any general-purpose template engine violates kitfly&#39;s minimalist philosophy, adds a dependency, and creates a second rendering pipeline alongside marked. The incremental value over <code>{{ key }}</code> substitution is loops and conditionals — which belong in generator scripts, not in kitfly&#39;s rendering path.</p>
543
+ <h3 id="dot-path-access-into-nested-objects-pricinguser_licenserate-">Dot-path access into nested objects (<code>{{ pricing.user_license.rate }}</code>)</h3>
544
+ <p>Deferred. The merged proposal uses flat strings with globals + page-level inject. Dot-path access adds syntax complexity (array indices, <code>.length</code>, edge cases with missing intermediate keys). If flat strings + generators cover the real-world cases, nested access isn&#39;t needed. Can revisit based on feedback.</p>
545
+ <h3 id="user-defined-formatters">User-defined formatters</h3>
546
+ <p>Rejected. Extensible formatters turn kitfly into a plugin-hosting platform for display logic. The closed set (dollar, number, percent, round, upper, lower) covers the documented use cases. Adding a formatter is a kitfly code change — intentional friction that prevents scope creep.</p>
547
+ <h3 id="typescript-interfacesdk-for-generators">TypeScript interface/SDK for generators</h3>
548
+ <p>Rejected (see Decision §4). The file-based contract is more portable, more Unix-aligned, and doesn&#39;t create coupling between user code and kitfly internals. The schema is the type system. The file system is the integration bus.</p>
549
+ <h3 id="data-fetching-in-the-build-pipeline-data-httpsapiexamplecompricing">Data fetching in the build pipeline (<code>data: https://api.example.com/pricing</code>)</h3>
550
+ <p>Rejected. Network I/O in builds violates determinism and introduces failure modes kitfly cannot control (timeouts, auth, rate limits, changed APIs). Generators handle data acquisition. Kitfly handles data consumption. The boundary is the file.</p>
551
+ <h3 id="symlinks-to-external-data-directories">Symlinks to external data directories</h3>
552
+ <p>Rejected. Symlinks that escape the kitsite break containment, create invisible dependencies on external paths, and make the kitsite non-portable. If data lives outside the kitsite, the generator materializes it inside the kitsite before build. The data directory contains real files, not references to files.</p>
553
+ <h3 id="arbitrary-absolute-data-paths">Arbitrary / absolute data paths</h3>
554
+ <p>Rejected. Allowing <code>data: /opt/shared/pricing.yaml</code> or <code>data: ../other-repo/data/pricing.yaml</code> breaks portability (the kitsite doesn&#39;t clone/deploy correctly on another machine) and containment (the build depends on state outside its control). The <code>dataroot:</code> config allows naming flexibility within the kitsite; generators handle externalization.</p>
555
+ <h2 id="compliance">Compliance</h2>
556
+ <p>All changes to the data binding layer in <code>src/shared.ts</code>, and all pre-build hook integration in <code>scripts/dev.ts</code>, <code>scripts/build.ts</code>, and <code>scripts/bundle.ts</code>, must conform to this ADR.</p>
557
+ <p>Feature requests that would expand the substitution syntax beyond <code>{{ key }}</code> and <code>{{ snippet:name }}</code>, or that would add logic to the binding layer, require a new ADR and explicit review.</p>
558
+ <h2 id="references">References</h2>
559
+ <ul>
560
+ <li><a href="ADR-0001-minimalist-site-code.md">ADR-0001: Minimalist Site Code</a> — Line budget and feature evaluation</li>
561
+ <li><a href="ADR-0005-plugin-contract-and-distribution.md">ADR-0005: Plugin Contract and Distribution</a> — Precedent for versioned contracts</li>
562
+ <li><code>.plans/active/v0.2.3/explorations/data-driven-merged-proposal.md</code> — Merged proposal (deliverylead + entarch)</li>
563
+ <li><code>.plans/active/v0.2.3/explorations/data-driven-content-schema.md</code> — Initial schema exploration</li>
564
+ <li><code>generate-pricing.py</code> (Epiphany client site) — Motivating real-world case</li>
565
+ </ul>
566
+
567
+ </article>
568
+ <aside class="toc"><span class="toc-title">On this page</span><ul><li><a href="#status">Status</a></li><li><a href="#context">Context</a></li><li class="toc-h3"><a href="#what-we-are-not-building">What We Are Not Building</a></li><li><a href="#terminology">Terminology</a></li><li><a href="#decision">Decision</a></li><li class="toc-h3"><a href="#1-build-time-string-substitution-with-a-file-based-contract">1. Build-time string substitution with a file-based contract</a></li><li class="toc-h3"><a href="#2-the-boundary-substitution-yes-logic-no">2. The boundary: substitution yes, logic no</a></li><li class="toc-h3"><a href="#3-formatters-are-deterministic-declarative-and-closed">3. Formatters are deterministic, declarative, and closed</a></li><li class="toc-h3"><a href="#4-no-code-interface-for-generators">4. No code interface for generators</a></li><li class="toc-h3"><a href="#5-pre-build-hooks-pass-environment-context">5. Pre-build hooks pass environment context</a></li><li class="toc-h3"><a href="#6-generators-are-a-first-class-concern-outside-kitfly-core">6. Generators are a first-class concern — outside kitfly core</a></li><li class="toc-h3"><a href="#7-the-control-plane-pattern-is-supported-not-owned">7. The control plane pattern is supported, not owned</a></li><li class="toc-h3"><a href="#8-data-lives-inside-the-kitsite-no-tree-escaping">8. Data lives inside the kitsite — no tree escaping</a></li><li class="toc-h3"><a href="#9-error-handling-fail-loud-never-silent">9. Error handling: fail loud, never silent</a></li><li><a href="#constraints">Constraints</a></li><li><a href="#consequences">Consequences</a></li><li class="toc-h3"><a href="#positive">Positive</a></li><li class="toc-h3"><a href="#negative">Negative</a></li><li class="toc-h3"><a href="#neutral">Neutral</a></li><li><a href="#alternatives-considered">Alternatives Considered</a></li><li class="toc-h3"><a href="#embed-a-template-engine-nunjucks-liquid-handlebars">Embed a template engine (Nunjucks, Liquid, Handlebars)</a></li><li class="toc-h3"><a href="#user-defined-formatters">User-defined formatters</a></li><li class="toc-h3"><a href="#typescript-interfacesdk-for-generators">TypeScript interface/SDK for generators</a></li><li class="toc-h3"><a href="#symlinks-to-external-data-directories">Symlinks to external data directories</a></li><li class="toc-h3"><a href="#arbitrary-absolute-data-paths">Arbitrary / absolute data paths</a></li><li><a href="#compliance">Compliance</a></li><li><a href="#references">References</a></li></ul></aside>
569
+ </main>
570
+ </div>
571
+
572
+ <footer class="site-footer">
573
+ <div class="footer-content">
574
+ <div class="footer-left">
575
+
576
+ <span class="footer-version">v0.2.3</span>
577
+ <span class="footer-separator">·</span>
578
+ <span class="footer-commit" title="Commit: 664328f">Published 2026-02-18</span>
579
+ </div>
580
+ <div class="footer-center">
581
+ <span class="footer-copyright"><a href="https://3leaps.net" class="footer-link">© 2026 3 Leaps, LLC</a></span>
582
+ <span class="footer-separator">·</span><a href="/" class="footer-link">Kitfly</a>
583
+ </div>
584
+ <div class="footer-right">
585
+ <a href="https://kitfly.dev" class="footer-link">Built with Kitfly</a>
586
+ </div>
587
+ </div>
588
+ </footer>
589
+ <!-- Syntax highlighting - Prism.js -->
590
+ <script src="https://cdn.jsdelivr.net/npm/prismjs@1/components/prism-core.min.js"></script>
591
+ <script src="https://cdn.jsdelivr.net/npm/prismjs@1/plugins/autoloader/prism-autoloader.min.js"></script>
592
+ <!-- Mermaid diagram support -->
593
+ <script type="module">
594
+ import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
595
+
596
+ function getMermaidTheme() {
597
+ const theme = document.documentElement.getAttribute('data-theme');
598
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
599
+ const isDark = theme === 'dark' || (!theme && prefersDark);
600
+ return isDark ? 'dark' : 'neutral';
601
+ }
602
+
603
+ mermaid.initialize({
604
+ startOnLoad: true,
605
+ theme: getMermaidTheme()
606
+ });
607
+
608
+ // Re-render mermaid diagrams when theme changes
609
+ window.reinitMermaid = async function() {
610
+ mermaid.initialize({ startOnLoad: false, theme: getMermaidTheme() });
611
+ const diagrams = document.querySelectorAll('.mermaid');
612
+ for (const el of diagrams) {
613
+ const code = el.getAttribute('data-mermaid-source');
614
+ if (code) {
615
+ el.innerHTML = code;
616
+ el.removeAttribute('data-processed');
617
+ }
618
+ }
619
+ await mermaid.run({ nodes: diagrams });
620
+ };
621
+ </script>
622
+
623
+ <script>
624
+ function toggleTheme() {
625
+ const html = document.documentElement;
626
+ const current = html.getAttribute('data-theme');
627
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
628
+
629
+ let next;
630
+ if (current === 'dark') {
631
+ next = 'light';
632
+ } else if (current === 'light') {
633
+ next = 'dark';
634
+ } else {
635
+ // No explicit theme set, toggle from system preference
636
+ next = prefersDark ? 'light' : 'dark';
637
+ }
638
+
639
+ html.setAttribute('data-theme', next);
640
+ localStorage.setItem('theme', next);
641
+
642
+ // Switch Prism theme
643
+ const prismLight = document.getElementById('prism-light');
644
+ const prismDark = document.getElementById('prism-dark');
645
+ if (next === 'dark') {
646
+ prismLight?.setAttribute('disabled', '');
647
+ prismDark?.removeAttribute('disabled');
648
+ } else {
649
+ prismLight?.removeAttribute('disabled');
650
+ prismDark?.setAttribute('disabled', '');
651
+ }
652
+
653
+ // Re-render mermaid diagrams with new theme
654
+ if (window.reinitMermaid) {
655
+ window.reinitMermaid();
656
+ }
657
+ if (window.reinitCharts) {
658
+ window.reinitCharts();
659
+ }
660
+ }
661
+
662
+ // Slides mode hash routing
663
+ (function initSlidesMode() {
664
+ const shell = document.querySelector('.slides-shell');
665
+ if (!shell) return;
666
+
667
+ const slides = Array.from(document.querySelectorAll('.slide'));
668
+ if (!slides.length) return;
669
+
670
+ const prevBtn = document.querySelector('.slide-prev');
671
+ const nextBtn = document.querySelector('.slide-next');
672
+ const counter = document.querySelector('.slide-counter');
673
+ const progressBar = document.querySelector('.slide-progress-bar');
674
+ const navLinks = Array.from(document.querySelectorAll('.sidebar-nav a[href^="#slide-"]'));
675
+ let current = 0;
676
+
677
+ function setActive(n) {
678
+ current = Math.max(0, Math.min(n, slides.length - 1));
679
+ slides.forEach((slide, idx) => slide.classList.toggle('active', idx === current));
680
+ navLinks.forEach((link) => {
681
+ const active = link.getAttribute('href') === '#' + slides[current].id;
682
+ link.classList.toggle('active', active);
683
+ });
684
+ if (counter) counter.textContent = (current + 1) + ' / ' + slides.length;
685
+ if (progressBar) progressBar.style.width = (((current + 1) / slides.length) * 100) + '%';
686
+ if (prevBtn) prevBtn.disabled = current === 0;
687
+ if (nextBtn) nextBtn.disabled = current === slides.length - 1;
688
+ history.replaceState(null, '', '#' + slides[current].id);
689
+ }
690
+
691
+ function setFromHash() {
692
+ const hash = window.location.hash || '';
693
+ const idx = slides.findIndex((s) => '#' + s.id === hash);
694
+ if (idx >= 0) setActive(idx);
695
+ else setActive(0);
696
+ }
697
+
698
+ prevBtn?.addEventListener('click', () => setActive(current - 1));
699
+ nextBtn?.addEventListener('click', () => setActive(current + 1));
700
+
701
+ document.addEventListener('keydown', (e) => {
702
+ if (e.key === 'ArrowRight' || e.key === ' ') {
703
+ e.preventDefault();
704
+ setActive(current + 1);
705
+ } else if (e.key === 'ArrowLeft') {
706
+ e.preventDefault();
707
+ setActive(current - 1);
708
+ } else if (e.key === 'Home') {
709
+ e.preventDefault();
710
+ setActive(0);
711
+ } else if (e.key === 'End') {
712
+ e.preventDefault();
713
+ setActive(slides.length - 1);
714
+ }
715
+ });
716
+
717
+ window.addEventListener('hashchange', setFromHash);
718
+ setFromHash();
719
+ })();
720
+
721
+ // Copy code button
722
+ document.querySelectorAll('.prose pre code').forEach(block => {
723
+ const button = document.createElement('button');
724
+ button.className = 'copy-button';
725
+ button.textContent = 'Copy';
726
+ button.onclick = async () => {
727
+ await navigator.clipboard.writeText(block.textContent);
728
+ button.textContent = 'Copied!';
729
+ setTimeout(() => button.textContent = 'Copy', 2000);
730
+ };
731
+ block.parentElement.appendChild(button);
732
+ });
733
+
734
+ // Mobile nav toggle
735
+ function toggleNav() {
736
+ document.querySelector('.sidebar').classList.toggle('open');
737
+ }
738
+
739
+ // Close nav when clicking outside on mobile
740
+ document.addEventListener('click', (e) => {
741
+ const sidebar = document.querySelector('.sidebar');
742
+ const toggle = document.querySelector('.nav-toggle');
743
+ if (sidebar.classList.contains('open') &&
744
+ !sidebar.contains(e.target) &&
745
+ !toggle.contains(e.target)) {
746
+ sidebar.classList.remove('open');
747
+ }
748
+ });
749
+ </script>
750
+
751
+ </body>
752
+ </html>