@brandon_m_behring/book-scaffold-astro 3.6.5 → 3.7.1

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.
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Profile registry — single source of truth for the BOOK_PROFILE enum.
3
+ *
4
+ * Each profile is a self-contained module that declares its schema + routes
5
+ * + styles via defineProfile(). The PROFILES object below is the only place
6
+ * the toolkit looks up profile config; bookScaffoldIntegration consumes
7
+ * PROFILES[profile] to drive injectRoute / injectScript calls.
8
+ *
9
+ * To add a new profile: create src/profiles/<name>.ts, then add two lines
10
+ * here (one to PROFILES, one to ChapterFor). The discriminated union of
11
+ * BookProfile = keyof typeof PROFILES updates automatically.
12
+ */
13
+ import { academicProfile, type AcademicChapter } from './academic.js';
14
+ import { toolsProfile, type ToolsChapter } from './tools.js';
15
+ import { minimalProfile, type MinimalChapter } from './minimal.js';
16
+ import { courseNotesProfile, type CourseNotesChapter } from './course-notes.js';
17
+ import { researchPortfolioProfile, type ResearchPortfolioChapter } from './research-portfolio.js';
18
+
19
+ export const PROFILES = {
20
+ academic: academicProfile,
21
+ tools: toolsProfile,
22
+ minimal: minimalProfile,
23
+ 'course-notes': courseNotesProfile,
24
+ 'research-portfolio': researchPortfolioProfile,
25
+ } as const;
26
+
27
+ export type BookProfile = keyof typeof PROFILES;
28
+
29
+ export const BOOK_PROFILES = Object.keys(PROFILES) as readonly BookProfile[];
30
+
31
+ /**
32
+ * Generic chapter-shape lookup. Consumers writing helpers parametrized over
33
+ * a profile can use this for narrowed typing:
34
+ *
35
+ * function summarize<P extends BookProfile>(profile: P, chapter: ChapterFor<P>) {
36
+ * // chapter is narrowed to AcademicChapter / ToolsChapter / etc.
37
+ * }
38
+ */
39
+ export type ChapterFor<P extends BookProfile> =
40
+ P extends 'academic' ? AcademicChapter :
41
+ P extends 'tools' ? ToolsChapter :
42
+ P extends 'minimal' ? MinimalChapter :
43
+ P extends 'course-notes' ? CourseNotesChapter :
44
+ P extends 'research-portfolio' ? ResearchPortfolioChapter :
45
+ never;
46
+
47
+ // Re-export the inferred chapter types for consumer ergonomics.
48
+ export type {
49
+ AcademicChapter,
50
+ ToolsChapter,
51
+ MinimalChapter,
52
+ CourseNotesChapter,
53
+ ResearchPortfolioChapter,
54
+ };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Minimal profile — single-author essays / manifestos. Currently aliases
3
+ * the tools chapter schema (defined in src/schemas.ts as
4
+ * minimalChapterSchema). If minimal-specific fields emerge from a real
5
+ * consumer, this is where they land.
6
+ */
7
+ import { defineProfile } from '../profile-kit.js';
8
+ import { minimalChapterSchema } from '../schemas.js';
9
+ import { fallbackChaptersRenderer } from './renderers/fallback-chapters.js';
10
+
11
+ export type { MinimalChapter } from '../schemas.js';
12
+
13
+ export const minimalProfile = defineProfile({
14
+ name: 'minimal',
15
+ schema: minimalChapterSchema,
16
+ routes: {
17
+ references: true,
18
+ search: true,
19
+ print: true,
20
+ chapters: false,
21
+ convergence: false,
22
+ frontmatter: false, // opt-in per book; see #7
23
+ },
24
+ styles: ['tokens.css', 'layout.css', 'callouts.css', 'chapter.css', 'typography.css', 'print.css'],
25
+ // v3.7.0 (#35): minimal aliases tools schema; fallback renderer field-dispatches if a consumer opts into routes.chapters
26
+ chaptersRenderer: fallbackChaptersRenderer,
27
+ });
@@ -0,0 +1,102 @@
1
+ /**
2
+ * src/profiles/renderers/academic-chapters.ts — ChaptersRenderer implementation
3
+ * for the academic profile. Owns: string-enum part grouping (foundations,
4
+ * ssm-core, beyond-ssm, integration, synthesis), Week N numbering, status
5
+ * badge (7-state). No freshness/last_verified, no tools_compared.
6
+ *
7
+ * Mirrors the v3.5.2 academic branch of pages/chapters.astro — DOM output
8
+ * intended to match exactly so academic visual baselines stay stable.
9
+ */
10
+ import type {
11
+ ChaptersRenderer,
12
+ PartKey,
13
+ StatusBadge,
14
+ } from '../../lib/chapters-renderer.js';
15
+
16
+ /**
17
+ * Ordinal positions for the academic-profile `part` enum (src/schemas.ts:
18
+ * academicParts). Order is load-bearing — drives both grouping order and
19
+ * sort key. Must match academicParts in schemas.ts.
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
+ /** Title-case an enum string: "ssm-core" → "Ssm Core". */
32
+ function titleCase(part: string): string {
33
+ return part
34
+ .split('-')
35
+ .map((w) => (w.length > 0 ? w.charAt(0).toUpperCase() + w.slice(1) : ''))
36
+ .join(' ');
37
+ }
38
+
39
+ export const academicChaptersRenderer: ChaptersRenderer = {
40
+ partKey(data) {
41
+ return (data.part as PartKey) ?? '';
42
+ },
43
+
44
+ formatPartLabel(part) {
45
+ if (typeof part === 'string' && part.length > 0) {
46
+ return titleCase(part);
47
+ }
48
+ return String(part);
49
+ },
50
+
51
+ isAppendix(_part) {
52
+ // Academic profile doesn't have appendices in the tools sense.
53
+ return false;
54
+ },
55
+
56
+ formatChapterNumber(data, _appendix) {
57
+ const week = (data.week as number) ?? 0;
58
+ return `Week ${week}`;
59
+ },
60
+
61
+ getToolsAttr(_data) {
62
+ // Academic chapters opt out of the ToolFilter island by claiming
63
+ // "cross-tool" — they remain visible regardless of filter selection.
64
+ return 'cross-tool';
65
+ },
66
+
67
+ getVolatilityData(_data) {
68
+ // Academic profile uses status, not volatility.
69
+ return null;
70
+ },
71
+
72
+ getStatusData(data): StatusBadge | null {
73
+ const status = data.status as string | undefined;
74
+ if (!status) return null;
75
+ return { status, label: status };
76
+ },
77
+
78
+ getFreshnessData(_data, _now) {
79
+ // Academic profile doesn't track last_verified.
80
+ return null;
81
+ },
82
+
83
+ getVerifiedDateLabel(_data) {
84
+ return null;
85
+ },
86
+
87
+ getToolsCompared(_data) {
88
+ return [];
89
+ },
90
+
91
+ sortKey(data) {
92
+ const partRaw = data.part;
93
+ const partOrdinal =
94
+ typeof partRaw === 'string'
95
+ ? (ACADEMIC_PART_ORDINAL[partRaw] ?? UNKNOWN_PART_ORDINAL)
96
+ : typeof partRaw === 'number'
97
+ ? partRaw
98
+ : UNKNOWN_PART_ORDINAL;
99
+ const week = typeof data.week === 'number' ? data.week : 0;
100
+ return partOrdinal * 1000 + week;
101
+ },
102
+ };
@@ -0,0 +1,87 @@
1
+ /**
2
+ * src/profiles/renderers/fallback-chapters.ts — ChaptersRenderer used by
3
+ * profiles that don't ship a dedicated renderer (minimal, course-notes,
4
+ * research-portfolio). Dispatches by field presence — exactly the v3.5.2
5
+ * logic that lived inline in pages/chapters.astro before #35.
6
+ *
7
+ * Safety net for shapes we haven't designed for explicitly. If a consumer
8
+ * opts a course-notes or research-portfolio book into `routes.chapters: true`,
9
+ * the fallback renders reasonably without crashing. Custom output for those
10
+ * profiles is a v4+ extension point (consumer-overridable renderer).
11
+ */
12
+ import type {
13
+ ChaptersRenderer,
14
+ PartKey,
15
+ VolatilityBadge,
16
+ StatusBadge,
17
+ FreshnessAffordance,
18
+ } from '../../lib/chapters-renderer.js';
19
+ import { toolsChaptersRenderer } from './tools-chapters.js';
20
+ import { academicChaptersRenderer } from './academic-chapters.js';
21
+
22
+ function isToolsShape(data: Record<string, unknown>): boolean {
23
+ return 'volatility' in data && 'chapter' in data;
24
+ }
25
+
26
+ function isAcademicShape(data: Record<string, unknown>): boolean {
27
+ return 'week' in data && 'status' in data;
28
+ }
29
+
30
+ /** Per-chapter dispatch to whichever known renderer matches the data shape. */
31
+ function dispatch(data: Record<string, unknown>): ChaptersRenderer {
32
+ if (isToolsShape(data)) return toolsChaptersRenderer;
33
+ if (isAcademicShape(data)) return academicChaptersRenderer;
34
+ // Unknown shape — return tools as the most-feature-complete default. The
35
+ // individual method results below short-circuit to safe values when fields
36
+ // are missing.
37
+ return toolsChaptersRenderer;
38
+ }
39
+
40
+ export const fallbackChaptersRenderer: ChaptersRenderer = {
41
+ partKey(data) {
42
+ return (data.part as PartKey) ?? 0;
43
+ },
44
+
45
+ formatPartLabel(part) {
46
+ if (typeof part === 'number') {
47
+ return part >= 6 ? 'Appendices' : `Part ${part}`;
48
+ }
49
+ return dispatch({ part } as Record<string, unknown>).formatPartLabel(part);
50
+ },
51
+
52
+ isAppendix(part) {
53
+ return typeof part === 'number' && part >= 6;
54
+ },
55
+
56
+ formatChapterNumber(data, appendix) {
57
+ return dispatch(data).formatChapterNumber(data, appendix);
58
+ },
59
+
60
+ getToolsAttr(data) {
61
+ return dispatch(data).getToolsAttr(data);
62
+ },
63
+
64
+ getVolatilityData(data): VolatilityBadge | null {
65
+ return dispatch(data).getVolatilityData(data);
66
+ },
67
+
68
+ getStatusData(data): StatusBadge | null {
69
+ return dispatch(data).getStatusData(data);
70
+ },
71
+
72
+ getFreshnessData(data, now): FreshnessAffordance | null {
73
+ return dispatch(data).getFreshnessData(data, now);
74
+ },
75
+
76
+ getVerifiedDateLabel(data) {
77
+ return dispatch(data).getVerifiedDateLabel(data);
78
+ },
79
+
80
+ getToolsCompared(data) {
81
+ return dispatch(data).getToolsCompared(data);
82
+ },
83
+
84
+ sortKey(data) {
85
+ return dispatch(data).sortKey(data);
86
+ },
87
+ };
@@ -0,0 +1,102 @@
1
+ /**
2
+ * src/profiles/renderers/tools-chapters.ts — ChaptersRenderer implementation
3
+ * for the tools profile. Owns: numeric part grouping (with Appendix splitting
4
+ * at part >= 6), Chapter N numbering, volatility badge, freshness affordance
5
+ * from last_verified + volatility class, tools_compared tags.
6
+ *
7
+ * Mirrors the pre-v3.7.0 logic in pages/chapters.astro for tools-shape
8
+ * chapters — DOM output is intended to be byte-equivalent so the existing
9
+ * visual-regression baselines (package/tests/visual/fixture/) pass without
10
+ * recapture.
11
+ */
12
+ import type {
13
+ ChaptersRenderer,
14
+ PartKey,
15
+ VolatilityBadge,
16
+ StatusBadge,
17
+ FreshnessAffordance,
18
+ } from '../../lib/chapters-renderer.js';
19
+ import { getFreshness, freshnessLabel, type VolatilityLevel } from '../../lib/freshness.js';
20
+
21
+ function formatDate(d: Date): string {
22
+ return d.toISOString().slice(0, 10);
23
+ }
24
+
25
+ function freshnessText(status: 'fresh' | 'verify-soon' | 'stale'): string {
26
+ switch (status) {
27
+ case 'fresh':
28
+ return 'Fresh';
29
+ case 'verify-soon':
30
+ return 'Verify soon';
31
+ case 'stale':
32
+ return 'Stale';
33
+ }
34
+ }
35
+
36
+ export const toolsChaptersRenderer: ChaptersRenderer = {
37
+ partKey(data) {
38
+ return (data.part as number) ?? 0;
39
+ },
40
+
41
+ formatPartLabel(part) {
42
+ if (typeof part === 'number') {
43
+ return part >= 6 ? 'Appendices' : `Part ${part}`;
44
+ }
45
+ // Defensive: unknown shape passed in. Render as-is.
46
+ return String(part);
47
+ },
48
+
49
+ isAppendix(part) {
50
+ return typeof part === 'number' && part >= 6;
51
+ },
52
+
53
+ formatChapterNumber(data, appendix) {
54
+ const chapter = (data.chapter as number) ?? 0;
55
+ if (appendix) {
56
+ // 'a', 'b', 'c', ... — matches pre-v3.7.0 String.fromCharCode(64 + chapter).toLowerCase()
57
+ return `Appendix ${String.fromCharCode(64 + chapter).toLowerCase()}`;
58
+ }
59
+ return `Chapter ${chapter}`;
60
+ },
61
+
62
+ getToolsAttr(data) {
63
+ const tools = (data.tools_compared as string[]) ?? [];
64
+ return tools.join(' ');
65
+ },
66
+
67
+ getVolatilityData(data): VolatilityBadge | null {
68
+ const level = data.volatility as string | undefined;
69
+ if (!level) return null;
70
+ return { level, label: level };
71
+ },
72
+
73
+ getStatusData(_data): StatusBadge | null {
74
+ // Tools profile uses volatility, not status. Status is academic-only.
75
+ return null;
76
+ },
77
+
78
+ getFreshnessData(data, now): FreshnessAffordance | null {
79
+ const lastVerified = data.last_verified as Date | undefined;
80
+ const volatility = data.volatility as VolatilityLevel | undefined;
81
+ if (!lastVerified || !volatility) return null;
82
+ const f = getFreshness(lastVerified, volatility, now);
83
+ if (!f) return null;
84
+ return { status: f.status, label: freshnessLabel(f) };
85
+ },
86
+
87
+ getVerifiedDateLabel(data) {
88
+ const lastVerified = data.last_verified as Date | undefined;
89
+ if (!(lastVerified instanceof Date)) return null;
90
+ return `verified ${formatDate(lastVerified)}`;
91
+ },
92
+
93
+ getToolsCompared(data) {
94
+ return ((data.tools_compared as string[]) ?? []).slice();
95
+ },
96
+
97
+ sortKey(data) {
98
+ const part = (data.part as number) ?? 0;
99
+ const chapter = (data.chapter as number) ?? 0;
100
+ return part * 1000 + chapter;
101
+ },
102
+ };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Research-portfolio profile — books that combine academic structure (week/
3
+ * part/status + math + BibTeX + Theorem family) with tools-style provenance
4
+ * (volatility class, tier-tagged sources, last_verified freshness signal).
5
+ *
6
+ * Closes issue #6 (v3.5.0). Reference consumer (forthcoming):
7
+ * prompt-injection-portfolio.
8
+ *
9
+ * Schema + inferred type live in src/schemas.ts; this module composes with
10
+ * routes + styles + katex flag.
11
+ *
12
+ * Distinguishing features vs other profiles:
13
+ *
14
+ * - Routes: /references + /search + /print + /frontmatter all auto-injected
15
+ * by default (research portfolios universally need a title-page /
16
+ * ai-disclosure / pre-release-banner under /frontmatter). /chapters and
17
+ * /convergence stay off — portfolios typically have a custom landing
18
+ * page enumerating chapters by part.
19
+ * - Styles: same as academic (chapter.css/typography.css/etc.) — KaTeX
20
+ * math is on by default since most portfolio chapters reference equations.
21
+ * - katex: true — math typesetting wired in (same as academic).
22
+ */
23
+ import { defineProfile } from '../profile-kit.js';
24
+ import { researchPortfolioChapterSchema } from '../schemas.js';
25
+ import { fallbackChaptersRenderer } from './renderers/fallback-chapters.js';
26
+
27
+ export type { ResearchPortfolioChapter } from '../schemas.js';
28
+
29
+ export const researchPortfolioProfile = defineProfile({
30
+ name: 'research-portfolio',
31
+ schema: researchPortfolioChapterSchema,
32
+ routes: {
33
+ references: true,
34
+ search: true,
35
+ print: true,
36
+ chapters: false, // portfolio books ship their own landing/index
37
+ convergence: false, // tools-profile-specific
38
+ frontmatter: true, // portfolios universally need title/disclosure/banner pages
39
+ },
40
+ styles: ['tokens.css', 'layout.css', 'callouts.css', 'chapter.css', 'typography.css', 'print.css'],
41
+ katex: true, // math is common in research content
42
+ // v3.7.0 (#35): portfolio schema is a union of academic + tools shapes — fallback renderer dispatches per chapter via field presence
43
+ chaptersRenderer: fallbackChaptersRenderer,
44
+ });
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Tools profile — AI-CLI comparison content with volatility + sources.
3
+ *
4
+ * Reference consumer: book-template-astro. Schema + inferred type live in
5
+ * src/schemas.ts; this module composes with routes + styles.
6
+ */
7
+ import { defineProfile } from '../profile-kit.js';
8
+ import { toolsChapterSchema } from '../schemas.js';
9
+ import { toolsChaptersRenderer } from './renderers/tools-chapters.js';
10
+
11
+ export type { ToolsChapter } from '../schemas.js';
12
+
13
+ export const toolsProfile = defineProfile({
14
+ name: 'tools',
15
+ schema: toolsChapterSchema,
16
+ routes: {
17
+ references: true,
18
+ search: true,
19
+ print: true,
20
+ chapters: true, // tools profile ships a flat chapter index
21
+ convergence: true, // tools profile ships convergence dashboard
22
+ frontmatter: false, // opt-in per book; see #7
23
+ },
24
+ styles: [
25
+ 'tokens.css', 'layout.css', 'callouts.css', 'chapter.css',
26
+ 'typography.css', 'print.css', 'convergence.css', 'tool-filter.css',
27
+ ],
28
+ chaptersRenderer: toolsChaptersRenderer, // v3.7.0 (#35) — owns /chapters semantics for tools shape
29
+ });