@brandon_m_behring/book-scaffold-astro 3.6.5 → 3.7.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/dist/index.d.ts +43 -3
- package/dist/index.mjs +222 -42
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.mjs +216 -5
- package/package.json +4 -1
- package/pages/chapters.astro +126 -125
- package/src/lib/chapters-renderer.ts +99 -0
- package/src/profile-kit.ts +100 -0
- package/src/profiles/academic.ts +30 -0
- package/src/profiles/course-notes.ts +32 -0
- package/src/profiles/index.ts +54 -0
- package/src/profiles/minimal.ts +27 -0
- package/src/profiles/renderers/academic-chapters.ts +102 -0
- package/src/profiles/renderers/fallback-chapters.ts +87 -0
- package/src/profiles/renderers/tools-chapters.ts +102 -0
- package/src/profiles/research-portfolio.ts +44 -0
- package/src/profiles/tools.ts +29 -0
- package/src/schemas.ts +291 -0
|
@@ -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
|
+
});
|
package/src/schemas.ts
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schemas + enum constants for book content collections.
|
|
3
|
+
*
|
|
4
|
+
* All Zod schemas live in this single file (single `astro/zod` import) so
|
|
5
|
+
* tsup's DTS bundler doesn't traverse Zod's dual CJS/ESM package multiple
|
|
6
|
+
* times — rollup-plugin-dts can't resolve Zod v4's `default` export when
|
|
7
|
+
* the same Zod import appears in multiple entry-graph files.
|
|
8
|
+
*
|
|
9
|
+
* Per-profile organization lives at src/profiles/<name>.ts which imports
|
|
10
|
+
* these schemas as values + declares the inferred chapter type + the
|
|
11
|
+
* route/style defaults. See ~/.claude/plans/address-and-finish-moonlit-shell.md.
|
|
12
|
+
*
|
|
13
|
+
* Imports `z` from `astro/zod` (a real module re-export) so schemas can
|
|
14
|
+
* be constructed at package-load time outside an Astro runtime context.
|
|
15
|
+
* `defineBookSchemas` in schemas-entry.ts wraps these into Astro
|
|
16
|
+
* `defineCollection` calls at the consumer's content-config load time.
|
|
17
|
+
*/
|
|
18
|
+
import { z } from 'astro/zod';
|
|
19
|
+
|
|
20
|
+
// ===== Tools-profile enums =====
|
|
21
|
+
|
|
22
|
+
export const toolSlugs = [
|
|
23
|
+
'claude-code',
|
|
24
|
+
'gemini-cli',
|
|
25
|
+
'codex-cli',
|
|
26
|
+
'cross-tool',
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
29
|
+
export const volatilityLevels = [
|
|
30
|
+
'stable-principle',
|
|
31
|
+
'architectural-pattern',
|
|
32
|
+
'feature-surface',
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
export const sourceTiers = [
|
|
36
|
+
'T1-official',
|
|
37
|
+
'T2-release-notes',
|
|
38
|
+
'T3-practitioner',
|
|
39
|
+
'T4-conjecture',
|
|
40
|
+
] as const;
|
|
41
|
+
|
|
42
|
+
export const changeKinds = ['added', 'removed', 'changed', 'deprecated'] as const;
|
|
43
|
+
|
|
44
|
+
export const patternCategories = [
|
|
45
|
+
'safety',
|
|
46
|
+
'scale',
|
|
47
|
+
'context',
|
|
48
|
+
'interaction',
|
|
49
|
+
'extension',
|
|
50
|
+
'other',
|
|
51
|
+
] as const;
|
|
52
|
+
|
|
53
|
+
// ===== Academic-profile enums =====
|
|
54
|
+
|
|
55
|
+
export const academicParts = [
|
|
56
|
+
'foundations',
|
|
57
|
+
'ssm-core',
|
|
58
|
+
'beyond-ssm',
|
|
59
|
+
'integration',
|
|
60
|
+
'synthesis',
|
|
61
|
+
] as const;
|
|
62
|
+
|
|
63
|
+
export const chapterStatus = [
|
|
64
|
+
'implemented',
|
|
65
|
+
'chapter_only',
|
|
66
|
+
'reading_only',
|
|
67
|
+
'prose_only',
|
|
68
|
+
'code_only',
|
|
69
|
+
'scaffolded',
|
|
70
|
+
'planned',
|
|
71
|
+
] as const;
|
|
72
|
+
|
|
73
|
+
// ===== Chapter schemas — one per profile =====
|
|
74
|
+
|
|
75
|
+
export const academicChapterSchema = z.object({
|
|
76
|
+
week: z.number().int().min(1).max(99),
|
|
77
|
+
part: z.enum(academicParts),
|
|
78
|
+
title: z.string().min(1),
|
|
79
|
+
status: z.enum(chapterStatus),
|
|
80
|
+
roadmap_lines: z.tuple([z.number().int(), z.number().int()]).optional(),
|
|
81
|
+
code_path: z.string().optional(),
|
|
82
|
+
tests_path: z.string().optional(),
|
|
83
|
+
notebook_path: z.string().optional(),
|
|
84
|
+
description: z.string().optional(),
|
|
85
|
+
draft: z.boolean().default(false),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export const toolsChapterSchema = z.object({
|
|
89
|
+
title: z.string().min(1),
|
|
90
|
+
part: z.number().int().min(0).max(10),
|
|
91
|
+
chapter: z.number().int().min(0).max(99),
|
|
92
|
+
volatility: z.enum(volatilityLevels),
|
|
93
|
+
tools_compared: z.array(z.enum(toolSlugs)).min(1),
|
|
94
|
+
last_verified: z.date(),
|
|
95
|
+
sources: z.array(z.string()).default([]),
|
|
96
|
+
description: z.string().optional(),
|
|
97
|
+
draft: z.boolean().default(false),
|
|
98
|
+
updated: z.date().optional(),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
/** Minimal profile currently aliases the tools schema. */
|
|
102
|
+
export const minimalChapterSchema = toolsChapterSchema;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Research-portfolio source tiers (v3.5.0, closes issue #6).
|
|
106
|
+
*
|
|
107
|
+
* Lighter shape than the tools-profile `sourceTiers` enum (`'T1-official'` etc.)
|
|
108
|
+
* — research portfolios cite primary sources inline per-chapter, so short
|
|
109
|
+
* `'T1'`/`'T2'` is more compact and readable. Semantics overlap (T1 = official
|
|
110
|
+
* primary, T2 = secondary, T3 = practitioner / community, T4 = conjecture).
|
|
111
|
+
*/
|
|
112
|
+
export const sourceTiersResearch = ['T1', 'T2', 'T3', 'T4'] as const;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Course-notes profile schema (v3.3.0, closes issue #4). Designed for
|
|
116
|
+
* course-derived study notes (DLAI, Coursera, Manning, ...). Key fields:
|
|
117
|
+
* - `course`/`instructor`/`source_url` — attribution
|
|
118
|
+
* - `learning_outcomes` — structured Bloom-tag-ready outcomes
|
|
119
|
+
* - `tags` — freeform string array (NOT tools_compared enum)
|
|
120
|
+
*/
|
|
121
|
+
export const courseNotesChapterSchema = z.object({
|
|
122
|
+
// Identity
|
|
123
|
+
title: z.string().min(1),
|
|
124
|
+
chapter: z.number().int().min(0).max(99),
|
|
125
|
+
part: z.number().int().min(0).max(20).default(1),
|
|
126
|
+
description: z.string().optional(),
|
|
127
|
+
|
|
128
|
+
// Source attribution
|
|
129
|
+
course: z.string().optional(),
|
|
130
|
+
instructor: z.string().optional(),
|
|
131
|
+
source_url: z.string().url().optional(),
|
|
132
|
+
|
|
133
|
+
// Pedagogy
|
|
134
|
+
learning_outcomes: z
|
|
135
|
+
.array(
|
|
136
|
+
z.object({
|
|
137
|
+
id: z.string(),
|
|
138
|
+
verb: z.string(),
|
|
139
|
+
text: z.string(),
|
|
140
|
+
}),
|
|
141
|
+
)
|
|
142
|
+
.default([]),
|
|
143
|
+
tags: z.array(z.string()).default([]),
|
|
144
|
+
|
|
145
|
+
// Provenance + status (shared shape with tools profile)
|
|
146
|
+
last_verified: z.date(),
|
|
147
|
+
volatility: z.enum(volatilityLevels).default('architectural-pattern'),
|
|
148
|
+
sources: z.array(z.string()).default([]),
|
|
149
|
+
draft: z.boolean().default(false),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Research-portfolio profile schema (v3.5.0, closes issue #6).
|
|
154
|
+
*
|
|
155
|
+
* Union of academic + tools field shapes, modernized: uses `tags` (freeform
|
|
156
|
+
* string array) instead of `tools_compared` (CLI-enum, doesn't fit research
|
|
157
|
+
* content). Designed for research-portfolio books that need BOTH academic-
|
|
158
|
+
* style structure (week/part/status, math/BibTeX/Theorem support via the
|
|
159
|
+
* `katex: true` profile flag) AND tools-style provenance (volatility class,
|
|
160
|
+
* tier-tagged sources, last_verified freshness signal).
|
|
161
|
+
*
|
|
162
|
+
* Reference (forthcoming) consumer: prompt-injection-portfolio.
|
|
163
|
+
*
|
|
164
|
+
* Hierarchy fields are all optional — chapters can use academic-style
|
|
165
|
+
* (`week` + part-enum string) OR tools-style (`chapter` + part-number) OR
|
|
166
|
+
* minimal (just title). The route templates dispatch on which is set.
|
|
167
|
+
*
|
|
168
|
+
* Sources are STRUCTURED INLINE (each chapter cites primary sources directly)
|
|
169
|
+
* rather than referencing a sources collection — saves cross-file lookup +
|
|
170
|
+
* matches research-paper citation conventions. Tier shorthand T1/T2/T3/T4
|
|
171
|
+
* (per sourceTiersResearch) over the tools-profile long form.
|
|
172
|
+
*/
|
|
173
|
+
export const researchPortfolioChapterSchema = z.object({
|
|
174
|
+
// Identity
|
|
175
|
+
title: z.string().min(1),
|
|
176
|
+
slug: z.string().optional(), // explicit slug override (otherwise filename)
|
|
177
|
+
description: z.string().optional(),
|
|
178
|
+
|
|
179
|
+
// Hierarchy — accept either academic-style or tools-style; all optional.
|
|
180
|
+
// The academic 'part' field is a string enum; tools 'part' is a number.
|
|
181
|
+
// Use z.union to permit either type.
|
|
182
|
+
part: z.union([z.number().int().min(0).max(20), z.string()]).optional(),
|
|
183
|
+
week: z.number().int().min(0).max(99).optional(),
|
|
184
|
+
chapter: z.number().int().min(0).max(99).optional(),
|
|
185
|
+
|
|
186
|
+
// Academic-style status (optional for research-portfolio — books may track
|
|
187
|
+
// chapters as 'prose_only' / 'experimental-result' / etc.).
|
|
188
|
+
status: z
|
|
189
|
+
.enum([
|
|
190
|
+
'implemented',
|
|
191
|
+
'chapter_only',
|
|
192
|
+
'reading_only',
|
|
193
|
+
'prose_only',
|
|
194
|
+
'code_only',
|
|
195
|
+
'scaffolded',
|
|
196
|
+
'planned',
|
|
197
|
+
])
|
|
198
|
+
.optional(),
|
|
199
|
+
|
|
200
|
+
// Research-portfolio specific: nature of the chapter's content.
|
|
201
|
+
// Distinct from academic's 'status' (which tracks authoring state) — this
|
|
202
|
+
// describes the EVIDENCE TYPE the chapter rests on.
|
|
203
|
+
freshness: z
|
|
204
|
+
.enum([
|
|
205
|
+
'experimental-result', // primary data the author produced
|
|
206
|
+
'literature-survey', // synthesis of others' work
|
|
207
|
+
'theoretical', // analytical / mathematical argument
|
|
208
|
+
'reference', // canonical material (definitions, taxonomy)
|
|
209
|
+
])
|
|
210
|
+
.optional(),
|
|
211
|
+
|
|
212
|
+
// Provenance (tools-style — overlap with tools/course-notes profiles).
|
|
213
|
+
volatility: z.enum(volatilityLevels).optional(),
|
|
214
|
+
tags: z.array(z.string()).default([]), // freeform; replaces tools_compared
|
|
215
|
+
|
|
216
|
+
// Structured inline sources with T1-T4 tiers.
|
|
217
|
+
sources: z
|
|
218
|
+
.array(
|
|
219
|
+
z.object({
|
|
220
|
+
tier: z.enum(sourceTiersResearch),
|
|
221
|
+
url: z.string().url(),
|
|
222
|
+
label: z.string().min(1),
|
|
223
|
+
}),
|
|
224
|
+
)
|
|
225
|
+
.default([]),
|
|
226
|
+
|
|
227
|
+
// Status + dates.
|
|
228
|
+
last_verified: z.date(),
|
|
229
|
+
updated: z.date().optional(),
|
|
230
|
+
draft: z.boolean().default(false),
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// ===== Inferred chapter types — one per schema =====
|
|
234
|
+
//
|
|
235
|
+
// Exported here so per-profile modules can re-export under a common name
|
|
236
|
+
// (AcademicChapter, ToolsChapter, etc.) without each touching `z.infer`
|
|
237
|
+
// in its own file (which would multiply the Zod import points and trip
|
|
238
|
+
// rollup-plugin-dts).
|
|
239
|
+
|
|
240
|
+
export type AcademicChapter = z.infer<typeof academicChapterSchema>;
|
|
241
|
+
export type ToolsChapter = z.infer<typeof toolsChapterSchema>;
|
|
242
|
+
export type MinimalChapter = z.infer<typeof minimalChapterSchema>;
|
|
243
|
+
export type CourseNotesChapter = z.infer<typeof courseNotesChapterSchema>;
|
|
244
|
+
export type ResearchPortfolioChapter = z.infer<typeof researchPortfolioChapterSchema>;
|
|
245
|
+
|
|
246
|
+
// ===== Collateral collection schemas (tools-profile; always-defined) =====
|
|
247
|
+
|
|
248
|
+
export const sourcesSchema = z.object({
|
|
249
|
+
url: z.string().url(),
|
|
250
|
+
title: z.string().min(1),
|
|
251
|
+
author: z.string().optional(),
|
|
252
|
+
publish_date: z.date().optional(),
|
|
253
|
+
captured_at: z.date(),
|
|
254
|
+
content_hash: z
|
|
255
|
+
.string()
|
|
256
|
+
.regex(/^sha256:[a-f0-9]+$/)
|
|
257
|
+
.optional(),
|
|
258
|
+
tier: z.enum(sourceTiers),
|
|
259
|
+
tool: z.enum(toolSlugs),
|
|
260
|
+
perma_cc: z.string().url().nullable().optional(),
|
|
261
|
+
local_cache: z.string().nullable().optional(),
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
export const changelogSchema = z.object({
|
|
265
|
+
tool: z.enum(toolSlugs),
|
|
266
|
+
versions: z
|
|
267
|
+
.array(
|
|
268
|
+
z.object({
|
|
269
|
+
version: z.string().min(1),
|
|
270
|
+
date: z.date(),
|
|
271
|
+
changes: z
|
|
272
|
+
.array(
|
|
273
|
+
z.object({
|
|
274
|
+
pattern: z.string(),
|
|
275
|
+
kind: z.enum(changeKinds),
|
|
276
|
+
note: z.string().min(1),
|
|
277
|
+
source_key: z.string().optional(),
|
|
278
|
+
}),
|
|
279
|
+
)
|
|
280
|
+
.default([]),
|
|
281
|
+
}),
|
|
282
|
+
)
|
|
283
|
+
.default([]),
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
export const patternsSchema = z.object({
|
|
287
|
+
name: z.string().min(1),
|
|
288
|
+
description: z.string().optional(),
|
|
289
|
+
category: z.enum(patternCategories).optional(),
|
|
290
|
+
convergence_date: z.date().nullable().optional(),
|
|
291
|
+
});
|