@brandon_m_behring/book-scaffold-astro 3.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +179 -0
- package/bin/book-scaffold.mjs +61 -0
- package/components/CaseStudy.astro +36 -0
- package/components/ChapterHeader.astro +61 -0
- package/components/ChapterNav.astro +29 -0
- package/components/ChapterTOC.astro +33 -0
- package/components/Citation.astro +94 -0
- package/components/Cite.astro +71 -0
- package/components/CodeBlock.astro +115 -0
- package/components/CodeRef.astro +49 -0
- package/components/ConceptBox.astro +26 -0
- package/components/Convergence.astro +41 -0
- package/components/CounterBox.astro +15 -0
- package/components/Divergence.astro +32 -0
- package/components/DynConnect.astro +15 -0
- package/components/ExampleBox.astro +15 -0
- package/components/Figure.astro +35 -0
- package/components/InsightBox.astro +15 -0
- package/components/KeyIdea.astro +21 -0
- package/components/MarginNote.astro +37 -0
- package/components/NoteBox.astro +15 -0
- package/components/OpenQuestion.astro +15 -0
- package/components/PaperBox.astro +15 -0
- package/components/PatternTimeline.astro +133 -0
- package/components/Recovery.astro +34 -0
- package/components/ResultBox.astro +15 -0
- package/components/Sidebar.astro +268 -0
- package/components/Sidenote.astro +26 -0
- package/components/SkillBox.astro +24 -0
- package/components/SourceArchive.astro +285 -0
- package/components/StatusBadge.astro +51 -0
- package/components/Tag.astro +60 -0
- package/components/Theorem.astro +65 -0
- package/components/TipBox.astro +15 -0
- package/components/ToolFilter.tsx +160 -0
- package/components/TryThis.astro +23 -0
- package/components/VersionSelector.tsx +85 -0
- package/components/WarnBox.astro +15 -0
- package/components/WeekRef.astro +51 -0
- package/components/XRef.astro +40 -0
- package/dist/index.d.ts +135 -0
- package/dist/index.mjs +369 -0
- package/dist/lib/katex-macros.d.ts +26 -0
- package/dist/lib/katex-macros.mjs +98 -0
- package/dist/schemas.d.ts +17 -0
- package/dist/schemas.mjs +160 -0
- package/dist/types-Cz-pwE1N.d.ts +61 -0
- package/examples/chapter-template-academic.mdx +100 -0
- package/examples/chapter-template-tools.mdx +90 -0
- package/layouts/Base.astro +250 -0
- package/layouts/Chapter.astro +37 -0
- package/package.json +137 -0
- package/pages/chapters.astro +371 -0
- package/pages/convergence.astro +96 -0
- package/pages/print.astro +39 -0
- package/pages/references.astro +160 -0
- package/pages/search.astro +87 -0
- package/pedagogy/kf-chapter-shape.md +96 -0
- package/pedagogy/source-tiers.md +121 -0
- package/pedagogy/volatility-classes.md +110 -0
- package/recipes/00-getting-started.md +77 -0
- package/recipes/01-add-math.md +71 -0
- package/recipes/02-bibliography-pipeline.md +82 -0
- package/recipes/03-asset-pipelines.md +84 -0
- package/recipes/04-component-library.md +118 -0
- package/recipes/05-deploy-cloudflare.md +74 -0
- package/recipes/06-mobile-first-layout.md +73 -0
- package/recipes/07-chapter-shapes.md +84 -0
- package/recipes/08-decisions-ledger.md +110 -0
- package/recipes/09-validation.md +106 -0
- package/recipes/10-custom-domain.md +72 -0
- package/recipes/README.md +43 -0
- package/scripts/build-bib.mjs +99 -0
- package/scripts/build-figures.mjs +179 -0
- package/scripts/render-notebooks.mjs +223 -0
- package/scripts/validate.mjs +179 -0
- package/styles/callouts.css +303 -0
- package/styles/chapter.css +209 -0
- package/styles/convergence.css +349 -0
- package/styles/layout.css +156 -0
- package/styles/print.css +203 -0
- package/styles/tokens.css +194 -0
- package/styles/tool-filter.css +135 -0
- package/styles/typography.css +147 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* /chapters — book index page.
|
|
4
|
+
*
|
|
5
|
+
* Groups non-draft chapters by Part in ascending part+chapter order.
|
|
6
|
+
* Each card exposes `data-tools="<slug> <slug>..."` so future tool-filter
|
|
7
|
+
* plumbing (Stage 3.2 commit 3) can hide cards via a single attribute
|
|
8
|
+
* selector without touching the card rendering.
|
|
9
|
+
*/
|
|
10
|
+
import Base from '../layouts/Base.astro';
|
|
11
|
+
import { getAllChapters, type Chapter } from '../src/lib/chapters';
|
|
12
|
+
import { getFreshness, freshnessLabel } from '../src/lib/freshness';
|
|
13
|
+
|
|
14
|
+
const chapters = await getAllChapters();
|
|
15
|
+
|
|
16
|
+
// Stable insertion-order grouping: iterate sorted chapters once.
|
|
17
|
+
const byPart = new Map<number, Chapter[]>();
|
|
18
|
+
for (const c of chapters) {
|
|
19
|
+
const list = byPart.get(c.data.part);
|
|
20
|
+
if (list) list.push(c);
|
|
21
|
+
else byPart.set(c.data.part, [c]);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function formatDate(d: Date): string {
|
|
25
|
+
return d.toISOString().slice(0, 10);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function partLabel(part: number, appendix: boolean): string {
|
|
29
|
+
return appendix ? 'Appendices' : `Part ${part}`;
|
|
30
|
+
}
|
|
31
|
+
---
|
|
32
|
+
<Base
|
|
33
|
+
title="Chapters — Agentic Coding"
|
|
34
|
+
description="All chapters grouped by Part, with volatility, freshness, and tools-compared metadata at a glance."
|
|
35
|
+
>
|
|
36
|
+
<article class="prose chapters-index">
|
|
37
|
+
<header class="chapters-index-header">
|
|
38
|
+
<h1>Chapters</h1>
|
|
39
|
+
<p class="chapters-index-lede">
|
|
40
|
+
Every chapter, grouped by Part. Each card shows its volatility
|
|
41
|
+
class, freshness status, and the tools it compares. Use a chapter
|
|
42
|
+
card's metadata to calibrate how much trust to place in its
|
|
43
|
+
specific claims — stable principles age slowly; feature surfaces
|
|
44
|
+
age fast.
|
|
45
|
+
</p>
|
|
46
|
+
<p class="chapters-index-cross-ref">
|
|
47
|
+
See also: <a href="/convergence/">convergence dashboard</a> —
|
|
48
|
+
which patterns have landed in which tools, when.
|
|
49
|
+
</p>
|
|
50
|
+
</header>
|
|
51
|
+
|
|
52
|
+
<p class="chapters-filter-hint" id="filter-hint" aria-live="polite"></p>
|
|
53
|
+
|
|
54
|
+
{Array.from(byPart.entries()).map(([part, list]) => {
|
|
55
|
+
const appendix = part >= 6;
|
|
56
|
+
return (
|
|
57
|
+
<section class="part-group">
|
|
58
|
+
<h2 class="part-heading">
|
|
59
|
+
<span class="part-label">{partLabel(part, appendix)}</span>
|
|
60
|
+
</h2>
|
|
61
|
+
<ol class="chapter-list">
|
|
62
|
+
{list.map((c) => {
|
|
63
|
+
const freshness = getFreshness(c.data.last_verified, c.data.volatility);
|
|
64
|
+
const freshnessText =
|
|
65
|
+
freshness.status === 'fresh'
|
|
66
|
+
? 'Fresh'
|
|
67
|
+
: freshness.status === 'verify-soon'
|
|
68
|
+
? 'Verify soon'
|
|
69
|
+
: 'Stale';
|
|
70
|
+
const toolsAttr = c.data.tools_compared.join(' ');
|
|
71
|
+
return (
|
|
72
|
+
<li
|
|
73
|
+
class="chapter-card"
|
|
74
|
+
data-tools={toolsAttr}
|
|
75
|
+
>
|
|
76
|
+
<a href={`/${c.id}/`} class="chapter-card-link">
|
|
77
|
+
<div class="chapter-card-meta">
|
|
78
|
+
<span class="chapter-card-number">
|
|
79
|
+
{appendix ? `Appendix ${String.fromCharCode(64 + c.data.chapter).toLowerCase()}` : `Chapter ${c.data.chapter}`}
|
|
80
|
+
</span>
|
|
81
|
+
<span
|
|
82
|
+
class={`volatility-badge volatility-${c.data.volatility}`}
|
|
83
|
+
title={`Volatility: ${c.data.volatility}`}
|
|
84
|
+
>{c.data.volatility}</span>
|
|
85
|
+
<span
|
|
86
|
+
class="freshness-badge"
|
|
87
|
+
data-status={freshness.status}
|
|
88
|
+
aria-label={freshnessLabel(freshness)}
|
|
89
|
+
title={freshnessLabel(freshness)}
|
|
90
|
+
>{freshnessText}</span>
|
|
91
|
+
<span class="chapter-card-verified">
|
|
92
|
+
verified {formatDate(c.data.last_verified)}
|
|
93
|
+
</span>
|
|
94
|
+
</div>
|
|
95
|
+
<h3 class="chapter-card-title">{c.data.title}</h3>
|
|
96
|
+
{c.data.description && (
|
|
97
|
+
<p class="chapter-card-description">{c.data.description}</p>
|
|
98
|
+
)}
|
|
99
|
+
<div class="chapter-card-tools">
|
|
100
|
+
{c.data.tools_compared.map((t) => (
|
|
101
|
+
<span class="tool-badge">{t}</span>
|
|
102
|
+
))}
|
|
103
|
+
</div>
|
|
104
|
+
</a>
|
|
105
|
+
</li>
|
|
106
|
+
);
|
|
107
|
+
})}
|
|
108
|
+
</ol>
|
|
109
|
+
</section>
|
|
110
|
+
);
|
|
111
|
+
})}
|
|
112
|
+
</article>
|
|
113
|
+
</Base>
|
|
114
|
+
|
|
115
|
+
{/* Inline non-island script: applies tool filter from localStorage on
|
|
116
|
+
load, then reacts to ToolFilter island's "book:tool-filter:change"
|
|
117
|
+
CustomEvent. Handled as plain DOM so /chapters/ doesn't need its
|
|
118
|
+
own hydration island. */}
|
|
119
|
+
<script is:inline>
|
|
120
|
+
(function () {
|
|
121
|
+
var STORAGE_KEY = 'book:tool-filter';
|
|
122
|
+
var EVENT_NAME = 'book:tool-filter:change';
|
|
123
|
+
|
|
124
|
+
function readSelected() {
|
|
125
|
+
try {
|
|
126
|
+
var raw = localStorage.getItem(STORAGE_KEY);
|
|
127
|
+
if (!raw) return [];
|
|
128
|
+
var parsed = JSON.parse(raw);
|
|
129
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
130
|
+
} catch (e) {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function applyFilter(selected) {
|
|
136
|
+
var cards = document.querySelectorAll('.chapter-card[data-tools]');
|
|
137
|
+
var shownCount = 0;
|
|
138
|
+
var totalCount = cards.length;
|
|
139
|
+
var active = Array.isArray(selected) && selected.length > 0;
|
|
140
|
+
|
|
141
|
+
cards.forEach(function (card) {
|
|
142
|
+
var attr = card.getAttribute('data-tools') || '';
|
|
143
|
+
var tools = attr.split(/\s+/).filter(Boolean);
|
|
144
|
+
var isCrossTool = tools.indexOf('cross-tool') !== -1;
|
|
145
|
+
var overlaps = false;
|
|
146
|
+
for (var i = 0; i < tools.length; i++) {
|
|
147
|
+
if (selected.indexOf(tools[i]) !== -1) {
|
|
148
|
+
overlaps = true;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
var hidden = active && !overlaps && !isCrossTool;
|
|
154
|
+
if (hidden) {
|
|
155
|
+
card.setAttribute('data-hidden-by-filter', 'true');
|
|
156
|
+
} else {
|
|
157
|
+
card.removeAttribute('data-hidden-by-filter');
|
|
158
|
+
shownCount++;
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Hide entire part sections that become empty under the filter.
|
|
163
|
+
document.querySelectorAll('.part-group').forEach(function (group) {
|
|
164
|
+
var visible = group.querySelectorAll(
|
|
165
|
+
'.chapter-card:not([data-hidden-by-filter="true"])',
|
|
166
|
+
);
|
|
167
|
+
if (visible.length === 0) {
|
|
168
|
+
group.setAttribute('data-hidden-by-filter', 'true');
|
|
169
|
+
} else {
|
|
170
|
+
group.removeAttribute('data-hidden-by-filter');
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Update the hint line.
|
|
175
|
+
var hint = document.getElementById('filter-hint');
|
|
176
|
+
if (hint) {
|
|
177
|
+
if (!active) {
|
|
178
|
+
hint.textContent = '';
|
|
179
|
+
} else {
|
|
180
|
+
hint.textContent =
|
|
181
|
+
'Showing ' + shownCount + ' of ' + totalCount +
|
|
182
|
+
' chapters (filter: ' + selected.join(', ') + ').' +
|
|
183
|
+
' Cross-tool chapters stay visible.';
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Apply saved filter on first load, before the island hydrates.
|
|
189
|
+
applyFilter(readSelected());
|
|
190
|
+
|
|
191
|
+
// React to subsequent changes from the ToolFilter island.
|
|
192
|
+
window.addEventListener(EVENT_NAME, function (e) {
|
|
193
|
+
var detail = e && e.detail ? e.detail : {};
|
|
194
|
+
applyFilter(Array.isArray(detail.selected) ? detail.selected : []);
|
|
195
|
+
});
|
|
196
|
+
})();
|
|
197
|
+
</script>
|
|
198
|
+
|
|
199
|
+
<style>
|
|
200
|
+
.chapters-index-header {
|
|
201
|
+
margin-bottom: var(--space-8);
|
|
202
|
+
border-bottom: 1px solid var(--color-border);
|
|
203
|
+
padding-bottom: var(--space-6);
|
|
204
|
+
}
|
|
205
|
+
.chapters-index-lede {
|
|
206
|
+
font-size: var(--text-lg);
|
|
207
|
+
color: var(--color-text-muted);
|
|
208
|
+
max-width: 65ch;
|
|
209
|
+
}
|
|
210
|
+
.chapters-index-cross-ref {
|
|
211
|
+
font-size: var(--text-sm);
|
|
212
|
+
color: var(--color-text-muted);
|
|
213
|
+
margin-top: var(--space-3);
|
|
214
|
+
text-indent: 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.part-group {
|
|
218
|
+
margin: var(--space-8) 0;
|
|
219
|
+
}
|
|
220
|
+
.part-heading {
|
|
221
|
+
font-size: var(--text-sm);
|
|
222
|
+
font-family: var(--font-code);
|
|
223
|
+
text-transform: uppercase;
|
|
224
|
+
letter-spacing: 0.08em;
|
|
225
|
+
color: var(--color-text-muted);
|
|
226
|
+
border-bottom: 1px solid var(--color-border);
|
|
227
|
+
padding-bottom: var(--space-2);
|
|
228
|
+
margin-bottom: var(--space-4);
|
|
229
|
+
}
|
|
230
|
+
.part-label {
|
|
231
|
+
font-weight: 500;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.chapter-list {
|
|
235
|
+
list-style: none;
|
|
236
|
+
padding: 0;
|
|
237
|
+
margin: 0;
|
|
238
|
+
display: grid;
|
|
239
|
+
gap: var(--space-4);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.chapter-card {
|
|
243
|
+
border: 1px solid var(--color-border);
|
|
244
|
+
border-radius: var(--radius-md);
|
|
245
|
+
background: var(--color-bg);
|
|
246
|
+
transition:
|
|
247
|
+
border-color 120ms ease,
|
|
248
|
+
background 120ms ease,
|
|
249
|
+
opacity 180ms ease,
|
|
250
|
+
max-height 220ms ease;
|
|
251
|
+
max-height: 40rem; /* ceiling for the transition; actual height is auto */
|
|
252
|
+
overflow: hidden;
|
|
253
|
+
}
|
|
254
|
+
.chapter-card:hover {
|
|
255
|
+
border-color: var(--color-link);
|
|
256
|
+
background: var(--color-bg-subtle);
|
|
257
|
+
}
|
|
258
|
+
.chapter-card-link {
|
|
259
|
+
display: block;
|
|
260
|
+
padding: var(--space-4);
|
|
261
|
+
text-decoration: none;
|
|
262
|
+
color: inherit;
|
|
263
|
+
}
|
|
264
|
+
.chapter-card-link:hover {
|
|
265
|
+
text-decoration: none;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.chapter-card-meta {
|
|
269
|
+
display: flex;
|
|
270
|
+
flex-wrap: wrap;
|
|
271
|
+
gap: var(--space-2);
|
|
272
|
+
align-items: center;
|
|
273
|
+
font-size: var(--text-sm);
|
|
274
|
+
color: var(--color-text-muted);
|
|
275
|
+
margin-bottom: var(--space-2);
|
|
276
|
+
}
|
|
277
|
+
.chapter-card-number {
|
|
278
|
+
font-family: var(--font-code);
|
|
279
|
+
font-weight: 500;
|
|
280
|
+
color: var(--color-text);
|
|
281
|
+
text-transform: uppercase;
|
|
282
|
+
letter-spacing: 0.04em;
|
|
283
|
+
font-size: var(--text-xs);
|
|
284
|
+
}
|
|
285
|
+
.chapter-card-verified {
|
|
286
|
+
font-family: var(--font-code);
|
|
287
|
+
font-size: var(--text-xs);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.chapter-card-title {
|
|
291
|
+
font-size: var(--text-lg);
|
|
292
|
+
font-weight: 500;
|
|
293
|
+
margin: 0 0 var(--space-2) 0;
|
|
294
|
+
color: var(--color-link);
|
|
295
|
+
}
|
|
296
|
+
.chapter-card-description {
|
|
297
|
+
color: var(--color-text);
|
|
298
|
+
margin: 0 0 var(--space-3) 0;
|
|
299
|
+
text-indent: 0;
|
|
300
|
+
line-height: 1.5;
|
|
301
|
+
}
|
|
302
|
+
.chapter-card-tools {
|
|
303
|
+
display: flex;
|
|
304
|
+
flex-wrap: wrap;
|
|
305
|
+
gap: var(--space-2);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/* Filter target: cards fade + collapse smoothly; full display:none
|
|
309
|
+
* kicks in after the transition so we don't keep a focusable node
|
|
310
|
+
* in the list. Part-groups collapse immediately because they're
|
|
311
|
+
* structural, not reader-facing. */
|
|
312
|
+
.chapter-card[data-hidden-by-filter='true'] {
|
|
313
|
+
opacity: 0;
|
|
314
|
+
max-height: 0;
|
|
315
|
+
margin: 0;
|
|
316
|
+
padding: 0;
|
|
317
|
+
border-width: 0;
|
|
318
|
+
pointer-events: none;
|
|
319
|
+
overflow: hidden;
|
|
320
|
+
}
|
|
321
|
+
.chapter-card[data-hidden-by-filter='true'] .chapter-card-link {
|
|
322
|
+
visibility: hidden;
|
|
323
|
+
}
|
|
324
|
+
.part-group[data-hidden-by-filter='true'] {
|
|
325
|
+
display: none;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.chapters-filter-hint {
|
|
329
|
+
font-size: var(--text-sm);
|
|
330
|
+
color: var(--color-text-muted);
|
|
331
|
+
font-style: italic;
|
|
332
|
+
margin: var(--space-2) 0 var(--space-4) 0;
|
|
333
|
+
min-height: 1.4em;
|
|
334
|
+
}
|
|
335
|
+
.chapters-filter-hint:empty {
|
|
336
|
+
margin: 0;
|
|
337
|
+
min-height: 0;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/* Print override: the filter UI doesn't exist in print (chrome is
|
|
341
|
+
* hidden), so all chapters must render regardless of any filter
|
|
342
|
+
* state left over in the viewer. */
|
|
343
|
+
@media print {
|
|
344
|
+
.chapter-card[data-hidden-by-filter='true'] {
|
|
345
|
+
opacity: 1;
|
|
346
|
+
max-height: none;
|
|
347
|
+
margin: unset;
|
|
348
|
+
padding: unset;
|
|
349
|
+
border-width: 1px;
|
|
350
|
+
pointer-events: auto;
|
|
351
|
+
overflow: visible;
|
|
352
|
+
}
|
|
353
|
+
.chapter-card[data-hidden-by-filter='true'] .chapter-card-link {
|
|
354
|
+
visibility: visible;
|
|
355
|
+
}
|
|
356
|
+
.part-group[data-hidden-by-filter='true'] {
|
|
357
|
+
display: block;
|
|
358
|
+
}
|
|
359
|
+
.chapters-filter-hint {
|
|
360
|
+
display: none;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/* Reduced-motion: skip the opacity/height transition for anyone who
|
|
365
|
+
* prefers less motion. The display change is instant either way. */
|
|
366
|
+
@media (prefers-reduced-motion: reduce) {
|
|
367
|
+
.chapter-card {
|
|
368
|
+
transition: border-color 120ms ease, background 120ms ease;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
</style>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* /convergence — dashboard of agentic-coding patterns across tools.
|
|
4
|
+
*
|
|
5
|
+
* For each category, renders every pattern in that category as a
|
|
6
|
+
* PatternTimeline card. Categories with no patterns render an honest
|
|
7
|
+
* placeholder so readers see the coverage gaps rather than inferring
|
|
8
|
+
* the registry is exhaustive.
|
|
9
|
+
*
|
|
10
|
+
* Driven entirely from changelog/patterns.yaml + changelog/tools/*.yaml.
|
|
11
|
+
* No code change is needed to grow the dashboard — add manifest rows.
|
|
12
|
+
*/
|
|
13
|
+
import Base from '../layouts/Base.astro';
|
|
14
|
+
import PatternTimeline from '../components/PatternTimeline.astro';
|
|
15
|
+
import {
|
|
16
|
+
getPatternsByCategory,
|
|
17
|
+
CATEGORY_LABELS,
|
|
18
|
+
} from '../src/lib/patterns';
|
|
19
|
+
import { patternCategories } from '../content.config';
|
|
20
|
+
|
|
21
|
+
const grouped = await getPatternsByCategory();
|
|
22
|
+
const totalPatterns = Object.values(grouped).reduce(
|
|
23
|
+
(n, arr) => n + arr.length,
|
|
24
|
+
0,
|
|
25
|
+
);
|
|
26
|
+
---
|
|
27
|
+
<Base
|
|
28
|
+
title="Convergence — Agentic Coding"
|
|
29
|
+
description="Which agentic-coding patterns have converged across Claude Code, Gemini CLI, and Codex CLI — and when? A live timeline driven from the changelog manifest."
|
|
30
|
+
>
|
|
31
|
+
<article class="prose convergence-dashboard">
|
|
32
|
+
<header class="convergence-header">
|
|
33
|
+
<h1>Convergence dashboard</h1>
|
|
34
|
+
<p class="convergence-lede">
|
|
35
|
+
Each card below tracks one agentic-coding pattern across the
|
|
36
|
+
three primary tools. A pattern is <em>converged</em> when all
|
|
37
|
+
three tools have shipped it. The timeline shows the sequence of
|
|
38
|
+
adoptions — who first, who followed, who has not yet.
|
|
39
|
+
</p>
|
|
40
|
+
<p class="convergence-metric">
|
|
41
|
+
<strong>{totalPatterns}</strong> pattern{totalPatterns === 1 ? '' : 's'}
|
|
42
|
+
{' '}currently tracked. The registry grows as new patterns
|
|
43
|
+
converge or as historical patterns get backfilled; expect the
|
|
44
|
+
count to drift upward, not the existing entries.
|
|
45
|
+
</p>
|
|
46
|
+
</header>
|
|
47
|
+
|
|
48
|
+
{patternCategories.map((cat) => {
|
|
49
|
+
const patterns = grouped[cat];
|
|
50
|
+
return (
|
|
51
|
+
<section class="convergence-category" data-category={cat} id={`category-${cat}`}>
|
|
52
|
+
<h2 class="convergence-category-heading">
|
|
53
|
+
<span class="convergence-category-label">{CATEGORY_LABELS[cat]}</span>
|
|
54
|
+
<span class="convergence-category-count">
|
|
55
|
+
{patterns.length === 0
|
|
56
|
+
? 'no patterns'
|
|
57
|
+
: `${patterns.length} pattern${patterns.length === 1 ? '' : 's'}`}
|
|
58
|
+
</span>
|
|
59
|
+
</h2>
|
|
60
|
+
{patterns.length === 0 ? (
|
|
61
|
+
<p class="convergence-category-empty">
|
|
62
|
+
No tracked patterns in this category yet. This is a
|
|
63
|
+
coverage gap, not an assertion that nothing belongs here.
|
|
64
|
+
</p>
|
|
65
|
+
) : (
|
|
66
|
+
<div class="convergence-card-list">
|
|
67
|
+
{patterns.map((p) => <PatternTimeline pattern={p} />)}
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
</section>
|
|
71
|
+
);
|
|
72
|
+
})}
|
|
73
|
+
|
|
74
|
+
<section class="convergence-method">
|
|
75
|
+
<h2>How to read this</h2>
|
|
76
|
+
<p>
|
|
77
|
+
The registry lives at <code>changelog/patterns.yaml</code>.
|
|
78
|
+
Each tool's adoption timeline lives at
|
|
79
|
+
<code>changelog/tools/<tool>.yaml</code>. The dashboard
|
|
80
|
+
joins them at build time — no code edits needed to add new
|
|
81
|
+
patterns or new adoption events.
|
|
82
|
+
</p>
|
|
83
|
+
<p>
|
|
84
|
+
A pattern with <em>convergence_date: null</em> is either a
|
|
85
|
+
partial convergence (1 or 2 of 3 tools) or an open design
|
|
86
|
+
space where the right shape is still being argued. Either
|
|
87
|
+
way, it is information worth surfacing, not hiding.
|
|
88
|
+
</p>
|
|
89
|
+
<p>
|
|
90
|
+
Coverage is intentionally sparse in the early book — the
|
|
91
|
+
registry grows organically through quarterly audit cycles.
|
|
92
|
+
Pattern nominations are welcome via Issues.
|
|
93
|
+
</p>
|
|
94
|
+
</section>
|
|
95
|
+
</article>
|
|
96
|
+
</Base>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* print.astro — concatenated book rendered as one long document, the
|
|
4
|
+
* input for pagedjs-cli.
|
|
5
|
+
*
|
|
6
|
+
* Walks the chapters collection in reading order, renders each chapter
|
|
7
|
+
* body, and wraps in a <section.chapter-print> so print.css can force
|
|
8
|
+
* page breaks between chapters.
|
|
9
|
+
*
|
|
10
|
+
* Build pipeline:
|
|
11
|
+
* npm run build → Astro emits dist/print/index.html
|
|
12
|
+
* npm run pdf → pagedjs-cli fetches dist/print/ via preview
|
|
13
|
+
* server, runs Paged.js in headless Chrome,
|
|
14
|
+
* outputs dist-pdf/book.pdf
|
|
15
|
+
*/
|
|
16
|
+
import Base from '../layouts/Base.astro';
|
|
17
|
+
import { render } from 'astro:content';
|
|
18
|
+
import { getAllChapters } from '../src/lib/chapters';
|
|
19
|
+
import ChapterHeader from '../components/ChapterHeader.astro';
|
|
20
|
+
|
|
21
|
+
const chapters = await getAllChapters();
|
|
22
|
+
const rendered = await Promise.all(
|
|
23
|
+
chapters.map(async (entry) => {
|
|
24
|
+
const { Content } = await render(entry);
|
|
25
|
+
return { entry, Content };
|
|
26
|
+
})
|
|
27
|
+
);
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
<Base title="Book (print edition)" description="Full book rendered as a single paginated document for PDF export.">
|
|
31
|
+
<main class="prose print-edition">
|
|
32
|
+
{rendered.map(({ entry, Content }) => (
|
|
33
|
+
<section class="chapter-print">
|
|
34
|
+
<ChapterHeader data={entry.data} />
|
|
35
|
+
<Content />
|
|
36
|
+
</section>
|
|
37
|
+
))}
|
|
38
|
+
</main>
|
|
39
|
+
</Base>
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* /references — the book's bibliography page.
|
|
4
|
+
*
|
|
5
|
+
* Renders every entry in src/data/references.json (produced by
|
|
6
|
+
* scripts/build-bib.mjs from guides/shared/references.bib). Each
|
|
7
|
+
* entry gets an anchor ID matching its bibkey, so `<Cite key="gu2024mamba" />`
|
|
8
|
+
* elsewhere on the site links to /references#gu2024mamba.
|
|
9
|
+
*
|
|
10
|
+
* Sorted alphabetically by first-author surname, then by year.
|
|
11
|
+
* arXiv-style notes are surfaced as direct links when present.
|
|
12
|
+
*/
|
|
13
|
+
import Base from '../layouts/Base.astro';
|
|
14
|
+
|
|
15
|
+
type CslAuthor = { family?: string; given?: string; literal?: string };
|
|
16
|
+
type CslEntry = {
|
|
17
|
+
id: string;
|
|
18
|
+
type?: string;
|
|
19
|
+
author?: CslAuthor[];
|
|
20
|
+
issued?: { 'date-parts'?: number[][] };
|
|
21
|
+
title?: string;
|
|
22
|
+
'container-title'?: string;
|
|
23
|
+
publisher?: string;
|
|
24
|
+
volume?: string;
|
|
25
|
+
issue?: string;
|
|
26
|
+
page?: string;
|
|
27
|
+
URL?: string;
|
|
28
|
+
DOI?: string;
|
|
29
|
+
note?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Resolve references.json from consumer's project root. Missing file -> empty
|
|
33
|
+
// list (page renders an "empty bibliography" notice rather than crashing).
|
|
34
|
+
const refsModules = import.meta.glob<{ default: Record<string, CslEntry> }>(
|
|
35
|
+
'/src/data/references.json',
|
|
36
|
+
{ eager: true },
|
|
37
|
+
);
|
|
38
|
+
const refsModule = refsModules['/src/data/references.json'];
|
|
39
|
+
const map = (refsModule?.default ?? {}) as Record<string, CslEntry>;
|
|
40
|
+
const entries = Object.values(map);
|
|
41
|
+
|
|
42
|
+
const surname = (a: CslAuthor): string =>
|
|
43
|
+
(a.family ?? a.literal ?? '').toLowerCase();
|
|
44
|
+
|
|
45
|
+
const firstAuthor = (e: CslEntry): string =>
|
|
46
|
+
e.author && e.author.length > 0 ? surname(e.author[0]) : '~';
|
|
47
|
+
|
|
48
|
+
const year = (e: CslEntry): number =>
|
|
49
|
+
e.issued?.['date-parts']?.[0]?.[0] ?? 0;
|
|
50
|
+
|
|
51
|
+
entries.sort((a, b) => {
|
|
52
|
+
const sa = firstAuthor(a);
|
|
53
|
+
const sb = firstAuthor(b);
|
|
54
|
+
if (sa !== sb) return sa < sb ? -1 : 1;
|
|
55
|
+
return year(a) - year(b);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
function formatAuthors(authors: CslAuthor[] | undefined): string {
|
|
59
|
+
if (!authors || authors.length === 0) return '(anon)';
|
|
60
|
+
const names = authors.map((a) => {
|
|
61
|
+
const family = a.family ?? a.literal ?? '';
|
|
62
|
+
const given = a.given ? `${a.given} ` : '';
|
|
63
|
+
return `${given}${family}`.trim();
|
|
64
|
+
});
|
|
65
|
+
if (names.length === 1) return names[0];
|
|
66
|
+
if (names.length === 2) return `${names[0]} and ${names[1]}`;
|
|
67
|
+
return `${names.slice(0, -1).join(', ')}, and ${names[names.length - 1]}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// arXiv URL extraction from `note` field (existing .bib uses `note = {arXiv:2111.00396}`).
|
|
71
|
+
function arxivUrl(note?: string): string | null {
|
|
72
|
+
if (!note) return null;
|
|
73
|
+
const m = note.match(/arXiv:\s*(\S+)/i);
|
|
74
|
+
return m ? `https://arxiv.org/abs/${m[1]}` : null;
|
|
75
|
+
}
|
|
76
|
+
---
|
|
77
|
+
<Base
|
|
78
|
+
title="References — Post-Transformers"
|
|
79
|
+
description="Bibliography for the post_transformers guide, generated from guides/shared/references.bib."
|
|
80
|
+
>
|
|
81
|
+
<article class="prose">
|
|
82
|
+
<header>
|
|
83
|
+
<h1>References</h1>
|
|
84
|
+
<p class="lede">
|
|
85
|
+
Every paper, book, and software citation in this guide, sorted
|
|
86
|
+
alphabetically by first-author surname. Click an entry's
|
|
87
|
+
anchor to share a deep link, or follow the arXiv / DOI / URL
|
|
88
|
+
for the source.
|
|
89
|
+
</p>
|
|
90
|
+
<p>
|
|
91
|
+
<small>
|
|
92
|
+
{entries.length} entries. Generated from
|
|
93
|
+
<code>guides/shared/references.bib</code> at build time via
|
|
94
|
+
<code>scripts/build-bib.mjs</code>.
|
|
95
|
+
</small>
|
|
96
|
+
</p>
|
|
97
|
+
</header>
|
|
98
|
+
|
|
99
|
+
<ol class="references-list">
|
|
100
|
+
{entries.map((e) => {
|
|
101
|
+
const y = year(e);
|
|
102
|
+
const arxiv = arxivUrl(e.note);
|
|
103
|
+
const primaryUrl = arxiv ?? e.URL ?? (e.DOI ? `https://doi.org/${e.DOI}` : null);
|
|
104
|
+
return (
|
|
105
|
+
<li id={e.id} class="reference-entry">
|
|
106
|
+
<span class="reference-key" aria-label="bibkey">[{e.id}]</span>
|
|
107
|
+
<span class="reference-text">
|
|
108
|
+
{formatAuthors(e.author)}
|
|
109
|
+
{y > 0 && <> ({y})</>}.
|
|
110
|
+
{' '}
|
|
111
|
+
<em>{e.title}</em>.
|
|
112
|
+
{e['container-title'] && <> {e['container-title']}.</>}
|
|
113
|
+
{e.publisher && !e['container-title'] && <> {e.publisher}.</>}
|
|
114
|
+
{e.volume && <> Vol. {e.volume}{e.issue && <>, no. {e.issue}</>}.</>}
|
|
115
|
+
{e.page && <> pp. {e.page}.</>}
|
|
116
|
+
{primaryUrl && (
|
|
117
|
+
<>
|
|
118
|
+
{' '}
|
|
119
|
+
<a href={primaryUrl} rel="external noopener">link</a>.
|
|
120
|
+
</>
|
|
121
|
+
)}
|
|
122
|
+
</span>
|
|
123
|
+
</li>
|
|
124
|
+
);
|
|
125
|
+
})}
|
|
126
|
+
</ol>
|
|
127
|
+
</article>
|
|
128
|
+
</Base>
|
|
129
|
+
|
|
130
|
+
<style>
|
|
131
|
+
.references-list {
|
|
132
|
+
list-style: none;
|
|
133
|
+
padding: 0;
|
|
134
|
+
counter-reset: ref;
|
|
135
|
+
}
|
|
136
|
+
.reference-entry {
|
|
137
|
+
margin: var(--space-3) 0;
|
|
138
|
+
padding: var(--space-2) 0 var(--space-2) var(--space-4);
|
|
139
|
+
border-left: 2px solid transparent;
|
|
140
|
+
transition: border-color 200ms ease;
|
|
141
|
+
}
|
|
142
|
+
.reference-entry:target {
|
|
143
|
+
border-left-color: var(--callout-info);
|
|
144
|
+
background: var(--warm-blue-tint);
|
|
145
|
+
}
|
|
146
|
+
.reference-key {
|
|
147
|
+
display: inline-block;
|
|
148
|
+
font-family: var(--font-code);
|
|
149
|
+
font-size: var(--text-xs);
|
|
150
|
+
color: var(--color-text-muted);
|
|
151
|
+
margin-right: var(--space-2);
|
|
152
|
+
vertical-align: baseline;
|
|
153
|
+
}
|
|
154
|
+
.reference-text {
|
|
155
|
+
line-height: var(--leading-normal);
|
|
156
|
+
}
|
|
157
|
+
.reference-text em {
|
|
158
|
+
font-style: italic;
|
|
159
|
+
}
|
|
160
|
+
</style>
|