@brandon_m_behring/book-scaffold-astro 4.13.0 → 4.14.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/components/ChapterHeader.astro +8 -15
- package/components/Sidebar.astro +7 -16
- package/dist/index.d.ts +56 -11
- package/dist/index.mjs +27 -22
- package/dist/schemas.mjs +15 -13
- package/package.json +1 -1
- package/pages/convergence.astro +28 -1
- package/src/lib/academic-parts.ts +87 -0
- package/src/lib/chapter-sort.ts +7 -17
- package/src/lib/patterns.ts +15 -3
- package/src/profiles/renderers/academic-chapters.ts +7 -40
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import type { CollectionEntry } from 'astro:content';
|
|
23
23
|
import { getFreshness, freshnessLabel } from '../src/lib/freshness';
|
|
24
|
+
import { academicPartHeading } from '../src/lib/academic-parts';
|
|
24
25
|
import StatusBadge from './StatusBadge.astro';
|
|
25
26
|
import CodeRef from './CodeRef.astro';
|
|
26
27
|
|
|
@@ -60,23 +61,15 @@ const freshnessText = freshness
|
|
|
60
61
|
: 'Stale'
|
|
61
62
|
: null;
|
|
62
63
|
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
// the
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
foundations: 'Part I · Foundations',
|
|
69
|
-
'ssm-core': 'Part II · SSM Core',
|
|
70
|
-
'beyond-ssm': 'Part III · Beyond SSMs',
|
|
71
|
-
integration: 'Part IV · Integration',
|
|
72
|
-
synthesis: 'Part V · Synthesis',
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
// Display strings, profile-tagged for clarity in markup.
|
|
64
|
+
// Display strings, profile-tagged for clarity in markup. Academic part
|
|
65
|
+
// labels ("Part {roman} · {name}") come from the shared academic-parts
|
|
66
|
+
// module — the single source of truth across the /chapters index, Sidebar,
|
|
67
|
+
// and this header (#95). Unknown/custom academic parts fall back to the
|
|
68
|
+
// title-cased name (no ordinal), consistent with the other two surfaces.
|
|
76
69
|
const partLabel = (() => {
|
|
77
70
|
const p = d.part;
|
|
78
|
-
if (hasAcademicMeta && typeof p === 'string' && p
|
|
79
|
-
return
|
|
71
|
+
if (hasAcademicMeta && typeof p === 'string' && p.length > 0) {
|
|
72
|
+
return academicPartHeading(p);
|
|
80
73
|
}
|
|
81
74
|
if (typeof p === 'number') return `Part ${p}`;
|
|
82
75
|
if (typeof p === 'string' && p.length > 0) return `Part: ${p}`;
|
package/components/Sidebar.astro
CHANGED
|
@@ -18,26 +18,17 @@
|
|
|
18
18
|
* them in as Astro.props from Base.astro if you want per-page overrides.
|
|
19
19
|
*/
|
|
20
20
|
import { getCollection } from 'astro:content';
|
|
21
|
+
import { academicParts } from '../src/schemas';
|
|
22
|
+
import { academicPartHeading } from '../src/lib/academic-parts';
|
|
21
23
|
|
|
22
24
|
const profile = import.meta.env.BOOK_PROFILE ?? 'minimal';
|
|
23
25
|
const siteTitle = 'Book';
|
|
24
26
|
const siteSubtitle = 'A scaffold-astro book';
|
|
25
27
|
|
|
26
|
-
// Academic profile: part is a string enum.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
'beyond-ssm',
|
|
31
|
-
'integration',
|
|
32
|
-
'synthesis',
|
|
33
|
-
] as const;
|
|
34
|
-
const ACADEMIC_PART_LABEL: Record<string, string> = {
|
|
35
|
-
foundations: 'Part I · Foundations',
|
|
36
|
-
'ssm-core': 'Part II · SSM Core',
|
|
37
|
-
'beyond-ssm': 'Part III · Beyond SSMs',
|
|
38
|
-
integration: 'Part IV · Integration',
|
|
39
|
-
synthesis: 'Part V · Synthesis',
|
|
40
|
-
};
|
|
28
|
+
// Academic profile: part is a string enum. `academicParts` (schemas.ts) is
|
|
29
|
+
// the canonical order, shared with the renderer and ChapterHeader (#95); the
|
|
30
|
+
// "Part {roman} · {name}" labels come from the same academic-parts module.
|
|
31
|
+
const ACADEMIC_PART_ORDER = academicParts;
|
|
41
32
|
|
|
42
33
|
const rawChapters = await getCollection('chapters', (entry) => !entry.data.draft);
|
|
43
34
|
|
|
@@ -101,7 +92,7 @@ function isCurrent(id: string): boolean {
|
|
|
101
92
|
}
|
|
102
93
|
|
|
103
94
|
function partLabel(key: string): string {
|
|
104
|
-
if (profile === 'academic') return
|
|
95
|
+
if (profile === 'academic') return academicPartHeading(key);
|
|
105
96
|
return `Part ${key}`;
|
|
106
97
|
}
|
|
107
98
|
---
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AstroUserConfig, AstroIntegration } from 'astro';
|
|
2
|
-
import { c as BookConfigOptions, f as BookScaffoldIntegrationOptions, Y as volatilityLevels, h as ChaptersRenderer, o as Style } from './types-CULHImU4.js';
|
|
3
|
-
export { A as AcademicChapter, B as BOOK_PRESETS, a as BOOK_PROFILES, b as BookConfigError, d as BookPreset, e as BookProfile, g as BookSchemasOptions, C as ChapterFor, i as CourseNotesChapter, F as FreshnessAffordance, j as FrontmatterRouteConfig, M as MinimalChapter, P as PartKey, k as PartialRouteToggles, l as ProfileDefinition, m as Provenance, R as ResearchPortfolioChapter, n as RouteToggles, S as StatusBadge, p as StyleInput, T as ToolsChapter, V as VolatilityBadge, q as academicChapterSchema,
|
|
2
|
+
import { c as BookConfigOptions, f as BookScaffoldIntegrationOptions, Y as volatilityLevels, h as ChaptersRenderer, r as academicParts, o as Style } from './types-CULHImU4.js';
|
|
3
|
+
export { A as AcademicChapter, B as BOOK_PRESETS, a as BOOK_PROFILES, b as BookConfigError, d as BookPreset, e as BookProfile, g as BookSchemasOptions, C as ChapterFor, i as CourseNotesChapter, F as FreshnessAffordance, j as FrontmatterRouteConfig, M as MinimalChapter, P as PartKey, k as PartialRouteToggles, l as ProfileDefinition, m as Provenance, R as ResearchPortfolioChapter, n as RouteToggles, S as StatusBadge, p as StyleInput, T as ToolsChapter, V as VolatilityBadge, q as academicChapterSchema, s as changeKinds, t as changelogSchema, u as chapterStatus, v as citationBackstops, w as composeStyles, x as courseNotesChapterSchema, y as defineProfile, z as defineStyle, D as minimalChapterSchema, E as normalizeFrontmatterConfig, G as patternCategories, H as patternsSchema, I as provenanceObject, J as provenanceSchema, K as researchPortfolioChapterSchema, L as resolvePreset, N as resolveProfile, O as sourceTiers, Q as sourceTiersResearch, U as sourcesSchema, W as toolSlugs, X as toolsChapterSchema } from './types-CULHImU4.js';
|
|
4
4
|
import 'astro/zod';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -95,14 +95,6 @@ declare function getFreshness(lastVerified: Date | undefined, volatility: Volati
|
|
|
95
95
|
* affordance without a separate branch. */
|
|
96
96
|
declare function freshnessLabel(f: Freshness | null): string;
|
|
97
97
|
|
|
98
|
-
/**
|
|
99
|
-
* src/lib/chapter-sort.ts — pure sort-key helper for chapter frontmatter.
|
|
100
|
-
*
|
|
101
|
-
* Lives in its own file (no `astro:content` imports) so tsup can include it
|
|
102
|
-
* in the toolkit's DTS bundle without trying to resolve Astro's virtual
|
|
103
|
-
* modules at build time. The Astro-context wrappers (sortKey, getAllChapters,
|
|
104
|
-
* getNeighbors) live in src/lib/chapters.ts and import from here.
|
|
105
|
-
*/
|
|
106
98
|
/**
|
|
107
99
|
* Pure-function sort key from a chapter frontmatter object. Spans both
|
|
108
100
|
* tools and academic schemas:
|
|
@@ -159,6 +151,59 @@ declare const academicChaptersRenderer: ChaptersRenderer;
|
|
|
159
151
|
|
|
160
152
|
declare const fallbackChaptersRenderer: ChaptersRenderer;
|
|
161
153
|
|
|
154
|
+
/**
|
|
155
|
+
* src/lib/academic-parts.ts — single source of truth for academic-profile
|
|
156
|
+
* part labels (#95).
|
|
157
|
+
*
|
|
158
|
+
* The academic `part` enum (src/schemas.ts: academicParts) is rendered on
|
|
159
|
+
* three surfaces — the `/chapters` index, the Sidebar nav, and the chapter
|
|
160
|
+
* header. Before v4.14.0 each surface carried its own copy of the
|
|
161
|
+
* enum→label map, and they had silently diverged: `beyond-ssm` rendered
|
|
162
|
+
* "Beyond SSM" on the index but "Beyond SSMs" in the sidebar/header. This
|
|
163
|
+
* module is the one place that owns:
|
|
164
|
+
*
|
|
165
|
+
* - the base display name per part ("SSM Core", "Beyond SSMs", …), and
|
|
166
|
+
* - the unknown/custom-part fallback (titleCase, consistently).
|
|
167
|
+
*
|
|
168
|
+
* The renderer uses the bare name; the Sidebar/ChapterHeader compose
|
|
169
|
+
* "Part {roman} · {name}". Both the Roman heading prefix AND the on-page
|
|
170
|
+
* sort order derive from the part's position in `academicParts` (the
|
|
171
|
+
* canonical order) via `academicPartOrdinal()` below — so labels and
|
|
172
|
+
* ordering share one source and cannot drift apart (#95 + #99).
|
|
173
|
+
*/
|
|
174
|
+
|
|
175
|
+
type AcademicPart = (typeof academicParts)[number];
|
|
176
|
+
/**
|
|
177
|
+
* Base display names for the academic `part` enum. An explicit map (not
|
|
178
|
+
* naive title-casing) so acronyms render correctly: `ssm-core` → "SSM Core",
|
|
179
|
+
* not "Ssm Core" (#91). Keys mirror `academicParts` in src/schemas.ts.
|
|
180
|
+
* Canonical spelling for `beyond-ssm` is the plural "Beyond SSMs" (#95).
|
|
181
|
+
*/
|
|
182
|
+
declare const ACADEMIC_PART_NAMES: Record<AcademicPart, string>;
|
|
183
|
+
/**
|
|
184
|
+
* Bare part name for the `/chapters` index group heading. Known parts use
|
|
185
|
+
* the explicit map; unknown/custom parts fall back to titleCase.
|
|
186
|
+
*/
|
|
187
|
+
declare function academicPartName(part: string): string;
|
|
188
|
+
/** Ordinal returned for any `part` outside `academicParts` — it sorts after
|
|
189
|
+
* all known parts and renders with no Roman prefix. */
|
|
190
|
+
declare const UNKNOWN_PART_ORDINAL = 99;
|
|
191
|
+
/**
|
|
192
|
+
* 1-based ordinal of an academic `part`, from its position in `academicParts`
|
|
193
|
+
* (the canonical order); `UNKNOWN_PART_ORDINAL` for anything outside the enum.
|
|
194
|
+
* The single source for BOTH the on-page sort key (academicChaptersRenderer
|
|
195
|
+
* and chapterSortKey) and the Roman heading prefix below — so a reorder of
|
|
196
|
+
* `academicParts` moves sort order and labels together, never apart (#99).
|
|
197
|
+
*/
|
|
198
|
+
declare function academicPartOrdinal(part: string): number;
|
|
199
|
+
/**
|
|
200
|
+
* "Part {roman} · {name}" heading for the Sidebar and ChapterHeader. Known
|
|
201
|
+
* parts get the Roman-ordinal prefix; unknown/custom parts (no ordinal)
|
|
202
|
+
* fall back to the bare title-cased name — one consistent rule across both
|
|
203
|
+
* surfaces, replacing the old divergent fallbacks (raw key vs "Part: key").
|
|
204
|
+
*/
|
|
205
|
+
declare function academicPartHeading(part: string): string;
|
|
206
|
+
|
|
162
207
|
/**
|
|
163
208
|
* src/styles/built-in.ts — toolkit-shipped Styles, one per BookPreset (v4.0.0).
|
|
164
209
|
*
|
|
@@ -260,4 +305,4 @@ type TipsConfigInput = Omit<TipsConfig, typeof TipsConfigBrand | '__tipsConfigVe
|
|
|
260
305
|
*/
|
|
261
306
|
declare function defineTips(opts: TipsConfigInput): TipsConfig;
|
|
262
307
|
|
|
263
|
-
export { BRANDON_PORTFOLIO_DEFAULT, BUILTIN_STYLES, BookConfigOptions, BookScaffoldIntegrationOptions, ChaptersRenderer, type Freshness, type FreshnessStatus, Style, type TipsConfig, type TipsConfigInput, type VolatilityLevel, academicChaptersRenderer, academicStyle, bookScaffoldIntegration, chapterSortKey, courseNotesStyle, defineBookConfig, defineMdxComponents, defineTips, fallbackChaptersRenderer, freshnessLabel, getFreshness, minimalStyle, researchPortfolioStyle, toolsChaptersRenderer, toolsStyle, volatilityLevels };
|
|
308
|
+
export { ACADEMIC_PART_NAMES, BRANDON_PORTFOLIO_DEFAULT, BUILTIN_STYLES, BookConfigOptions, BookScaffoldIntegrationOptions, ChaptersRenderer, type Freshness, type FreshnessStatus, Style, type TipsConfig, type TipsConfigInput, UNKNOWN_PART_ORDINAL, type VolatilityLevel, academicChaptersRenderer, academicPartHeading, academicPartName, academicPartOrdinal, academicParts, academicStyle, bookScaffoldIntegration, chapterSortKey, courseNotesStyle, defineBookConfig, defineMdxComponents, defineTips, fallbackChaptersRenderer, freshnessLabel, getFreshness, minimalStyle, researchPortfolioStyle, toolsChaptersRenderer, toolsStyle, volatilityLevels };
|
package/dist/index.mjs
CHANGED
|
@@ -359,32 +359,40 @@ var patternsSchema = z.object({
|
|
|
359
359
|
convergence_date: z.date().nullable().optional()
|
|
360
360
|
});
|
|
361
361
|
|
|
362
|
-
// src/
|
|
363
|
-
var
|
|
364
|
-
foundations: 1,
|
|
365
|
-
"ssm-core": 2,
|
|
366
|
-
"beyond-ssm": 3,
|
|
367
|
-
integration: 4,
|
|
368
|
-
synthesis: 5
|
|
369
|
-
};
|
|
370
|
-
var UNKNOWN_PART_ORDINAL = 99;
|
|
371
|
-
var ACADEMIC_PART_LABEL = {
|
|
362
|
+
// src/lib/academic-parts.ts
|
|
363
|
+
var ACADEMIC_PART_NAMES = {
|
|
372
364
|
foundations: "Foundations",
|
|
373
365
|
"ssm-core": "SSM Core",
|
|
374
|
-
"beyond-ssm": "Beyond
|
|
366
|
+
"beyond-ssm": "Beyond SSMs",
|
|
375
367
|
integration: "Integration",
|
|
376
368
|
synthesis: "Synthesis"
|
|
377
369
|
};
|
|
370
|
+
var ROMAN = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII"];
|
|
378
371
|
function titleCase(part) {
|
|
379
372
|
return part.split("-").map((w) => w.length > 0 ? w.charAt(0).toUpperCase() + w.slice(1) : "").join(" ");
|
|
380
373
|
}
|
|
374
|
+
function academicPartName(part) {
|
|
375
|
+
return ACADEMIC_PART_NAMES[part] ?? titleCase(part);
|
|
376
|
+
}
|
|
377
|
+
var UNKNOWN_PART_ORDINAL = 99;
|
|
378
|
+
function academicPartOrdinal(part) {
|
|
379
|
+
const i = academicParts.indexOf(part);
|
|
380
|
+
return i >= 0 ? i + 1 : UNKNOWN_PART_ORDINAL;
|
|
381
|
+
}
|
|
382
|
+
function academicPartHeading(part) {
|
|
383
|
+
const ord = academicPartOrdinal(part);
|
|
384
|
+
const name = academicPartName(part);
|
|
385
|
+
return ord === UNKNOWN_PART_ORDINAL ? name : `Part ${ROMAN[ord - 1]} \xB7 ${name}`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// src/profiles/renderers/academic-chapters.ts
|
|
381
389
|
var academicChaptersRenderer = {
|
|
382
390
|
partKey(data) {
|
|
383
391
|
return data.part ?? "";
|
|
384
392
|
},
|
|
385
393
|
formatPartLabel(part) {
|
|
386
394
|
if (typeof part === "string" && part.length > 0) {
|
|
387
|
-
return
|
|
395
|
+
return academicPartName(part);
|
|
388
396
|
}
|
|
389
397
|
return String(part);
|
|
390
398
|
},
|
|
@@ -417,7 +425,7 @@ var academicChaptersRenderer = {
|
|
|
417
425
|
},
|
|
418
426
|
sortKey(data) {
|
|
419
427
|
const partRaw = data.part;
|
|
420
|
-
const partOrdinal = typeof partRaw === "string" ?
|
|
428
|
+
const partOrdinal = typeof partRaw === "string" ? academicPartOrdinal(partRaw) : typeof partRaw === "number" ? partRaw : UNKNOWN_PART_ORDINAL;
|
|
421
429
|
const week = typeof data.week === "number" ? data.week : 0;
|
|
422
430
|
return partOrdinal * 1e3 + week;
|
|
423
431
|
}
|
|
@@ -1226,17 +1234,9 @@ async function defineBookConfig(opts) {
|
|
|
1226
1234
|
}
|
|
1227
1235
|
|
|
1228
1236
|
// src/lib/chapter-sort.ts
|
|
1229
|
-
var ACADEMIC_PART_ORDINAL2 = {
|
|
1230
|
-
foundations: 1,
|
|
1231
|
-
"ssm-core": 2,
|
|
1232
|
-
"beyond-ssm": 3,
|
|
1233
|
-
integration: 4,
|
|
1234
|
-
synthesis: 5
|
|
1235
|
-
};
|
|
1236
|
-
var UNKNOWN_PART_ORDINAL2 = 99;
|
|
1237
1237
|
function chapterSortKey(data) {
|
|
1238
1238
|
const partRaw = data.part;
|
|
1239
|
-
const partOrdinal = typeof partRaw === "number" ? partRaw : typeof partRaw === "string" ?
|
|
1239
|
+
const partOrdinal = typeof partRaw === "number" ? partRaw : typeof partRaw === "string" ? academicPartOrdinal(partRaw) : UNKNOWN_PART_ORDINAL;
|
|
1240
1240
|
const within = typeof data.chapter === "number" ? data.chapter : typeof data.week === "number" ? data.week : 0;
|
|
1241
1241
|
return partOrdinal * 1e3 + within;
|
|
1242
1242
|
}
|
|
@@ -1281,13 +1281,18 @@ function defineTips(opts) {
|
|
|
1281
1281
|
return { __tipsConfigVersion: 1, ...opts };
|
|
1282
1282
|
}
|
|
1283
1283
|
export {
|
|
1284
|
+
ACADEMIC_PART_NAMES,
|
|
1284
1285
|
BOOK_PRESETS,
|
|
1285
1286
|
BOOK_PROFILES,
|
|
1286
1287
|
BRANDON_PORTFOLIO_DEFAULT,
|
|
1287
1288
|
BUILTIN_STYLES,
|
|
1288
1289
|
BookConfigError,
|
|
1290
|
+
UNKNOWN_PART_ORDINAL,
|
|
1289
1291
|
academicChapterSchema,
|
|
1290
1292
|
academicChaptersRenderer,
|
|
1293
|
+
academicPartHeading,
|
|
1294
|
+
academicPartName,
|
|
1295
|
+
academicPartOrdinal,
|
|
1291
1296
|
academicParts,
|
|
1292
1297
|
academicStyle,
|
|
1293
1298
|
bookScaffoldIntegration,
|
package/dist/schemas.mjs
CHANGED
|
@@ -243,32 +243,34 @@ var patternsSchema = z.object({
|
|
|
243
243
|
convergence_date: z.date().nullable().optional()
|
|
244
244
|
});
|
|
245
245
|
|
|
246
|
-
// src/
|
|
247
|
-
var
|
|
248
|
-
foundations: 1,
|
|
249
|
-
"ssm-core": 2,
|
|
250
|
-
"beyond-ssm": 3,
|
|
251
|
-
integration: 4,
|
|
252
|
-
synthesis: 5
|
|
253
|
-
};
|
|
254
|
-
var UNKNOWN_PART_ORDINAL = 99;
|
|
255
|
-
var ACADEMIC_PART_LABEL = {
|
|
246
|
+
// src/lib/academic-parts.ts
|
|
247
|
+
var ACADEMIC_PART_NAMES = {
|
|
256
248
|
foundations: "Foundations",
|
|
257
249
|
"ssm-core": "SSM Core",
|
|
258
|
-
"beyond-ssm": "Beyond
|
|
250
|
+
"beyond-ssm": "Beyond SSMs",
|
|
259
251
|
integration: "Integration",
|
|
260
252
|
synthesis: "Synthesis"
|
|
261
253
|
};
|
|
262
254
|
function titleCase(part) {
|
|
263
255
|
return part.split("-").map((w) => w.length > 0 ? w.charAt(0).toUpperCase() + w.slice(1) : "").join(" ");
|
|
264
256
|
}
|
|
257
|
+
function academicPartName(part) {
|
|
258
|
+
return ACADEMIC_PART_NAMES[part] ?? titleCase(part);
|
|
259
|
+
}
|
|
260
|
+
var UNKNOWN_PART_ORDINAL = 99;
|
|
261
|
+
function academicPartOrdinal(part) {
|
|
262
|
+
const i = academicParts.indexOf(part);
|
|
263
|
+
return i >= 0 ? i + 1 : UNKNOWN_PART_ORDINAL;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/profiles/renderers/academic-chapters.ts
|
|
265
267
|
var academicChaptersRenderer = {
|
|
266
268
|
partKey(data) {
|
|
267
269
|
return data.part ?? "";
|
|
268
270
|
},
|
|
269
271
|
formatPartLabel(part) {
|
|
270
272
|
if (typeof part === "string" && part.length > 0) {
|
|
271
|
-
return
|
|
273
|
+
return academicPartName(part);
|
|
272
274
|
}
|
|
273
275
|
return String(part);
|
|
274
276
|
},
|
|
@@ -301,7 +303,7 @@ var academicChaptersRenderer = {
|
|
|
301
303
|
},
|
|
302
304
|
sortKey(data) {
|
|
303
305
|
const partRaw = data.part;
|
|
304
|
-
const partOrdinal = typeof partRaw === "string" ?
|
|
306
|
+
const partOrdinal = typeof partRaw === "string" ? academicPartOrdinal(partRaw) : typeof partRaw === "number" ? partRaw : UNKNOWN_PART_ORDINAL;
|
|
305
307
|
const week = typeof data.week === "number" ? data.week : 0;
|
|
306
308
|
return partOrdinal * 1e3 + week;
|
|
307
309
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brandon_m_behring/book-scaffold-astro",
|
|
3
3
|
"description": "Astro 6 + MDX toolkit for long-form technical books. Profile-aware (academic / tools / minimal); ships Tufte typography, KaTeX, BibTeX citations, Pagefind, Cloudflare Workers deploy. See PACKAGE_DESIGN.md for the API contract.",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.14.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Brandon Behring",
|
package/pages/convergence.astro
CHANGED
|
@@ -14,15 +14,39 @@ import Base from '../layouts/Base.astro';
|
|
|
14
14
|
import PatternTimeline from '../components/PatternTimeline.astro';
|
|
15
15
|
import {
|
|
16
16
|
getPatternsByCategory,
|
|
17
|
+
emptyPatternsByCategory,
|
|
17
18
|
CATEGORY_LABELS,
|
|
18
19
|
} from '../src/lib/patterns';
|
|
19
20
|
import { patternCategories } from '@brandon_m_behring/book-scaffold-astro';
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
// A tools-profile book may use the inline <Convergence> component yet define
|
|
23
|
+
// no `changelog/patterns.yaml` — then the `patterns` collection is never
|
|
24
|
+
// registered and getCollection('patterns') errors with "The collection
|
|
25
|
+
// 'patterns' does not exist or is empty" (#86). Gate on the manifest's
|
|
26
|
+
// presence and never touch the collection when it is absent — the same
|
|
27
|
+
// presence-gate references.astro uses for the optional `sources` collection.
|
|
28
|
+
// When absent, render the dashboard's honest empty-state from a pure
|
|
29
|
+
// all-empty map (no content-collection access).
|
|
30
|
+
const patternsManifest = import.meta.glob('/changelog/patterns.yaml', {
|
|
31
|
+
query: '?raw',
|
|
32
|
+
import: 'default',
|
|
33
|
+
eager: true,
|
|
34
|
+
});
|
|
35
|
+
const hasPatterns = '/changelog/patterns.yaml' in patternsManifest;
|
|
36
|
+
const grouped = hasPatterns
|
|
37
|
+
? await getPatternsByCategory()
|
|
38
|
+
: emptyPatternsByCategory();
|
|
22
39
|
const totalPatterns = Object.values(grouped).reduce(
|
|
23
40
|
(n, arr) => n + arr.length,
|
|
24
41
|
0,
|
|
25
42
|
);
|
|
43
|
+
// Distinguish a missing/misnamed manifest from a legitimately empty registry:
|
|
44
|
+
// a tools book that uses <Convergence> but never created changelog/patterns.yaml
|
|
45
|
+
// would otherwise see a blank dashboard indistinguishable from "no patterns yet".
|
|
46
|
+
// Surface an actionable hint instead — matching tips.astro / references.astro.
|
|
47
|
+
const noManifestHint = hasPatterns
|
|
48
|
+
? null
|
|
49
|
+
: 'No changelog/patterns.yaml found — create it to populate this dashboard, or set routes: { convergence: false } to drop this page.';
|
|
26
50
|
---
|
|
27
51
|
<Base
|
|
28
52
|
title="Convergence — Agentic Coding"
|
|
@@ -43,6 +67,9 @@ const totalPatterns = Object.values(grouped).reduce(
|
|
|
43
67
|
converge or as historical patterns get backfilled; expect the
|
|
44
68
|
count to drift upward, not the existing entries.
|
|
45
69
|
</p>
|
|
70
|
+
{noManifestHint && (
|
|
71
|
+
<p class="convergence-no-manifest" role="note">{noManifestHint}</p>
|
|
72
|
+
)}
|
|
46
73
|
</header>
|
|
47
74
|
|
|
48
75
|
{patternCategories.map((cat) => {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/lib/academic-parts.ts — single source of truth for academic-profile
|
|
3
|
+
* part labels (#95).
|
|
4
|
+
*
|
|
5
|
+
* The academic `part` enum (src/schemas.ts: academicParts) is rendered on
|
|
6
|
+
* three surfaces — the `/chapters` index, the Sidebar nav, and the chapter
|
|
7
|
+
* header. Before v4.14.0 each surface carried its own copy of the
|
|
8
|
+
* enum→label map, and they had silently diverged: `beyond-ssm` rendered
|
|
9
|
+
* "Beyond SSM" on the index but "Beyond SSMs" in the sidebar/header. This
|
|
10
|
+
* module is the one place that owns:
|
|
11
|
+
*
|
|
12
|
+
* - the base display name per part ("SSM Core", "Beyond SSMs", …), and
|
|
13
|
+
* - the unknown/custom-part fallback (titleCase, consistently).
|
|
14
|
+
*
|
|
15
|
+
* The renderer uses the bare name; the Sidebar/ChapterHeader compose
|
|
16
|
+
* "Part {roman} · {name}". Both the Roman heading prefix AND the on-page
|
|
17
|
+
* sort order derive from the part's position in `academicParts` (the
|
|
18
|
+
* canonical order) via `academicPartOrdinal()` below — so labels and
|
|
19
|
+
* ordering share one source and cannot drift apart (#95 + #99).
|
|
20
|
+
*/
|
|
21
|
+
import { academicParts } from '../schemas.js';
|
|
22
|
+
|
|
23
|
+
type AcademicPart = (typeof academicParts)[number];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Base display names for the academic `part` enum. An explicit map (not
|
|
27
|
+
* naive title-casing) so acronyms render correctly: `ssm-core` → "SSM Core",
|
|
28
|
+
* not "Ssm Core" (#91). Keys mirror `academicParts` in src/schemas.ts.
|
|
29
|
+
* Canonical spelling for `beyond-ssm` is the plural "Beyond SSMs" (#95).
|
|
30
|
+
*/
|
|
31
|
+
export const ACADEMIC_PART_NAMES: Record<AcademicPart, string> = {
|
|
32
|
+
foundations: 'Foundations',
|
|
33
|
+
'ssm-core': 'SSM Core',
|
|
34
|
+
'beyond-ssm': 'Beyond SSMs',
|
|
35
|
+
integration: 'Integration',
|
|
36
|
+
synthesis: 'Synthesis',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Roman ordinals for the "Part {roman}" heading prefix, indexed by the
|
|
40
|
+
* part's position in `academicParts`. Sized past the 5 known parts so a
|
|
41
|
+
* future enum addition still resolves a numeral. */
|
|
42
|
+
const ROMAN = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII'];
|
|
43
|
+
|
|
44
|
+
/** Title-case an enum string: "consumer-custom" → "Consumer Custom".
|
|
45
|
+
* Fallback for parts outside the known ACADEMIC_PART_NAMES map. */
|
|
46
|
+
function titleCase(part: string): string {
|
|
47
|
+
return part
|
|
48
|
+
.split('-')
|
|
49
|
+
.map((w) => (w.length > 0 ? w.charAt(0).toUpperCase() + w.slice(1) : ''))
|
|
50
|
+
.join(' ');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Bare part name for the `/chapters` index group heading. Known parts use
|
|
55
|
+
* the explicit map; unknown/custom parts fall back to titleCase.
|
|
56
|
+
*/
|
|
57
|
+
export function academicPartName(part: string): string {
|
|
58
|
+
return ACADEMIC_PART_NAMES[part as AcademicPart] ?? titleCase(part);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Ordinal returned for any `part` outside `academicParts` — it sorts after
|
|
62
|
+
* all known parts and renders with no Roman prefix. */
|
|
63
|
+
export const UNKNOWN_PART_ORDINAL = 99;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 1-based ordinal of an academic `part`, from its position in `academicParts`
|
|
67
|
+
* (the canonical order); `UNKNOWN_PART_ORDINAL` for anything outside the enum.
|
|
68
|
+
* The single source for BOTH the on-page sort key (academicChaptersRenderer
|
|
69
|
+
* and chapterSortKey) and the Roman heading prefix below — so a reorder of
|
|
70
|
+
* `academicParts` moves sort order and labels together, never apart (#99).
|
|
71
|
+
*/
|
|
72
|
+
export function academicPartOrdinal(part: string): number {
|
|
73
|
+
const i = academicParts.indexOf(part as AcademicPart);
|
|
74
|
+
return i >= 0 ? i + 1 : UNKNOWN_PART_ORDINAL;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* "Part {roman} · {name}" heading for the Sidebar and ChapterHeader. Known
|
|
79
|
+
* parts get the Roman-ordinal prefix; unknown/custom parts (no ordinal)
|
|
80
|
+
* fall back to the bare title-cased name — one consistent rule across both
|
|
81
|
+
* surfaces, replacing the old divergent fallbacks (raw key vs "Part: key").
|
|
82
|
+
*/
|
|
83
|
+
export function academicPartHeading(part: string): string {
|
|
84
|
+
const ord = academicPartOrdinal(part);
|
|
85
|
+
const name = academicPartName(part);
|
|
86
|
+
return ord === UNKNOWN_PART_ORDINAL ? name : `Part ${ROMAN[ord - 1]} · ${name}`;
|
|
87
|
+
}
|
package/src/lib/chapter-sort.ts
CHANGED
|
@@ -5,23 +5,13 @@
|
|
|
5
5
|
* in the toolkit's DTS bundle without trying to resolve Astro's virtual
|
|
6
6
|
* modules at build time. The Astro-context wrappers (sortKey, getAllChapters,
|
|
7
7
|
* getNeighbors) live in src/lib/chapters.ts and import from here.
|
|
8
|
+
*
|
|
9
|
+
* The academic `part`→ordinal mapping is shared with the label surfaces via
|
|
10
|
+
* `academicPartOrdinal` (src/lib/academic-parts.ts, also astro:content-free),
|
|
11
|
+
* so on-page sort order and the Roman heading prefix derive from one canonical
|
|
12
|
+
* order (`academicParts`) and cannot drift apart (#99).
|
|
8
13
|
*/
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Ordinal positions for the academic-profile `part` enum (src/schemas.ts:
|
|
12
|
-
* academicParts). Order here is load-bearing: it determines the on-page
|
|
13
|
-
* grouping order of academic books. Anything outside the enum (e.g., a
|
|
14
|
-
* consumer-extended part name) sorts to the end.
|
|
15
|
-
*/
|
|
16
|
-
const ACADEMIC_PART_ORDINAL: Record<string, number> = {
|
|
17
|
-
foundations: 1,
|
|
18
|
-
'ssm-core': 2,
|
|
19
|
-
'beyond-ssm': 3,
|
|
20
|
-
integration: 4,
|
|
21
|
-
synthesis: 5,
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const UNKNOWN_PART_ORDINAL = 99;
|
|
14
|
+
import { academicPartOrdinal, UNKNOWN_PART_ORDINAL } from './academic-parts.js';
|
|
25
15
|
|
|
26
16
|
/**
|
|
27
17
|
* Pure-function sort key from a chapter frontmatter object. Spans both
|
|
@@ -43,7 +33,7 @@ export function chapterSortKey(data: Record<string, unknown>): number {
|
|
|
43
33
|
typeof partRaw === 'number'
|
|
44
34
|
? partRaw
|
|
45
35
|
: typeof partRaw === 'string'
|
|
46
|
-
? (
|
|
36
|
+
? academicPartOrdinal(partRaw)
|
|
47
37
|
: UNKNOWN_PART_ORDINAL;
|
|
48
38
|
const within =
|
|
49
39
|
typeof data.chapter === 'number'
|
package/src/lib/patterns.ts
CHANGED
|
@@ -79,6 +79,20 @@ export async function getAllPatterns(): Promise<PatternEntry[]> {
|
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* An all-empty category map (every category → []), in the exact shape
|
|
84
|
+
* getPatternsByCategory returns. Pure — no content-collection access — so it
|
|
85
|
+
* is safe to call when a book defines no `patterns` collection at all: the
|
|
86
|
+
* convergence dashboard renders its honest "no patterns yet" placeholders
|
|
87
|
+
* from it without ever calling getCollection('patterns') (#86, cf. how
|
|
88
|
+
* references.astro gates the absent `sources` collection).
|
|
89
|
+
*/
|
|
90
|
+
export function emptyPatternsByCategory(): Record<PatternCategory, PatternEntry[]> {
|
|
91
|
+
return Object.fromEntries(
|
|
92
|
+
patternCategories.map((c) => [c, [] as PatternEntry[]]),
|
|
93
|
+
) as Record<PatternCategory, PatternEntry[]>;
|
|
94
|
+
}
|
|
95
|
+
|
|
82
96
|
/**
|
|
83
97
|
* Patterns grouped by category; categories that have no patterns are
|
|
84
98
|
* still emitted with an empty array so the dashboard can render
|
|
@@ -88,9 +102,7 @@ export async function getPatternsByCategory(): Promise<
|
|
|
88
102
|
Record<PatternCategory, PatternEntry[]>
|
|
89
103
|
> {
|
|
90
104
|
const all = await getAllPatterns();
|
|
91
|
-
const grouped =
|
|
92
|
-
patternCategories.map((c) => [c, [] as PatternEntry[]]),
|
|
93
|
-
) as Record<PatternCategory, PatternEntry[]>;
|
|
105
|
+
const grouped = emptyPatternsByCategory();
|
|
94
106
|
for (const p of all) {
|
|
95
107
|
const cat = p.data.category ?? 'other';
|
|
96
108
|
grouped[cat].push(p);
|
|
@@ -12,44 +12,11 @@ import type {
|
|
|
12
12
|
PartKey,
|
|
13
13
|
StatusBadge,
|
|
14
14
|
} from '../../lib/chapters-renderer.js';
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
*/
|
|
21
|
-
const ACADEMIC_PART_ORDINAL: Record<string, number> = {
|
|
22
|
-
foundations: 1,
|
|
23
|
-
'ssm-core': 2,
|
|
24
|
-
'beyond-ssm': 3,
|
|
25
|
-
integration: 4,
|
|
26
|
-
synthesis: 5,
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const UNKNOWN_PART_ORDINAL = 99;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Display labels for the academic-profile `part` enum. An explicit map (not
|
|
33
|
-
* naive title-casing) so acronyms render correctly: `ssm-core` → "SSM Core",
|
|
34
|
-
* not "Ssm Core" (#91). Keys mirror ACADEMIC_PART_ORDINAL; unknown/custom
|
|
35
|
-
* parts fall back to titleCase() in formatPartLabel.
|
|
36
|
-
*/
|
|
37
|
-
const ACADEMIC_PART_LABEL: Record<string, string> = {
|
|
38
|
-
foundations: 'Foundations',
|
|
39
|
-
'ssm-core': 'SSM Core',
|
|
40
|
-
'beyond-ssm': 'Beyond SSM',
|
|
41
|
-
integration: 'Integration',
|
|
42
|
-
synthesis: 'Synthesis',
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
/** Title-case an enum string: "ssm-core" → "Ssm Core". Fallback for parts
|
|
46
|
-
* outside the known ACADEMIC_PART_LABEL map (e.g. consumer-custom parts). */
|
|
47
|
-
function titleCase(part: string): string {
|
|
48
|
-
return part
|
|
49
|
-
.split('-')
|
|
50
|
-
.map((w) => (w.length > 0 ? w.charAt(0).toUpperCase() + w.slice(1) : ''))
|
|
51
|
-
.join(' ');
|
|
52
|
-
}
|
|
15
|
+
import {
|
|
16
|
+
academicPartName,
|
|
17
|
+
academicPartOrdinal,
|
|
18
|
+
UNKNOWN_PART_ORDINAL,
|
|
19
|
+
} from '../../lib/academic-parts.js';
|
|
53
20
|
|
|
54
21
|
export const academicChaptersRenderer: ChaptersRenderer = {
|
|
55
22
|
partKey(data) {
|
|
@@ -58,7 +25,7 @@ export const academicChaptersRenderer: ChaptersRenderer = {
|
|
|
58
25
|
|
|
59
26
|
formatPartLabel(part) {
|
|
60
27
|
if (typeof part === 'string' && part.length > 0) {
|
|
61
|
-
return
|
|
28
|
+
return academicPartName(part);
|
|
62
29
|
}
|
|
63
30
|
return String(part);
|
|
64
31
|
},
|
|
@@ -107,7 +74,7 @@ export const academicChaptersRenderer: ChaptersRenderer = {
|
|
|
107
74
|
const partRaw = data.part;
|
|
108
75
|
const partOrdinal =
|
|
109
76
|
typeof partRaw === 'string'
|
|
110
|
-
? (
|
|
77
|
+
? academicPartOrdinal(partRaw)
|
|
111
78
|
: typeof partRaw === 'number'
|
|
112
79
|
? partRaw
|
|
113
80
|
: UNKNOWN_PART_ORDINAL;
|