@brandon_m_behring/book-scaffold-astro 3.0.0-alpha.0 → 3.0.0-alpha.2
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.mjs +1 -1
- package/dist/schemas.d.ts +1 -6
- package/dist/schemas.mjs +22 -14
- package/package.json +2 -1
- package/src/lib/chapters.ts +37 -0
- package/src/lib/freshness.ts +84 -0
- package/src/lib/katex-macros.ts +110 -0
- package/src/lib/patterns.ts +127 -0
- package/src/lib/repo-url.ts +32 -0
- package/src/lib/sources.ts +62 -0
package/dist/index.mjs
CHANGED
|
@@ -152,12 +152,12 @@ var ALWAYS_ON_STYLES = [
|
|
|
152
152
|
];
|
|
153
153
|
var TOOLS_ONLY_STYLES = ["convergence.css", "tool-filter.css"];
|
|
154
154
|
var DEFAULT_ROUTES_ALL = [
|
|
155
|
-
{ pattern: "/chapters", file: "chapters.astro" },
|
|
156
155
|
{ pattern: "/references", file: "references.astro" },
|
|
157
156
|
{ pattern: "/print", file: "print.astro" },
|
|
158
157
|
{ pattern: "/search", file: "search.astro" }
|
|
159
158
|
];
|
|
160
159
|
var DEFAULT_ROUTES_TOOLS = [
|
|
160
|
+
{ pattern: "/chapters", file: "chapters.astro" },
|
|
161
161
|
{ pattern: "/convergence", file: "convergence.astro" }
|
|
162
162
|
];
|
|
163
163
|
function resolvePage(file) {
|
package/dist/schemas.d.ts
CHANGED
|
@@ -6,12 +6,7 @@ import 'astro';
|
|
|
6
6
|
* consumer extends via object spread and Zod `.extend()` (see PACKAGE_DESIGN.md §5).
|
|
7
7
|
*/
|
|
8
8
|
declare function defineBookSchemas(opts?: BookSchemasOptions): {
|
|
9
|
-
collections:
|
|
10
|
-
chapters: unknown;
|
|
11
|
-
sources: unknown;
|
|
12
|
-
changelog: unknown;
|
|
13
|
-
patterns: unknown;
|
|
14
|
-
};
|
|
9
|
+
collections: Record<string, unknown>;
|
|
15
10
|
};
|
|
16
11
|
|
|
17
12
|
export { defineBookSchemas };
|
package/dist/schemas.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/schemas-entry.ts
|
|
2
|
+
import { existsSync } from "fs";
|
|
2
3
|
import { defineCollection } from "astro:content";
|
|
3
4
|
import { glob, file } from "astro/loaders";
|
|
4
5
|
|
|
@@ -139,21 +140,28 @@ function defineBookSchemas(opts = {}) {
|
|
|
139
140
|
}),
|
|
140
141
|
schema: profile === "academic" ? academicChapterSchema : toolsChapterSchema
|
|
141
142
|
});
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
schema: sourcesSchema
|
|
145
|
-
});
|
|
146
|
-
const changelog = defineCollection({
|
|
147
|
-
loader: glob({ pattern: "*.yaml", base: "./changelog/tools" }),
|
|
148
|
-
schema: changelogSchema
|
|
149
|
-
});
|
|
150
|
-
const patterns = defineCollection({
|
|
151
|
-
loader: file("changelog/patterns.yaml"),
|
|
152
|
-
schema: patternsSchema
|
|
153
|
-
});
|
|
154
|
-
return {
|
|
155
|
-
collections: { chapters, sources, changelog, patterns }
|
|
143
|
+
const collections = {
|
|
144
|
+
chapters
|
|
156
145
|
};
|
|
146
|
+
if (existsSync("./sources/manifest.yaml")) {
|
|
147
|
+
collections.sources = defineCollection({
|
|
148
|
+
loader: file("sources/manifest.yaml"),
|
|
149
|
+
schema: sourcesSchema
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (existsSync("./changelog/tools")) {
|
|
153
|
+
collections.changelog = defineCollection({
|
|
154
|
+
loader: glob({ pattern: "*.yaml", base: "./changelog/tools" }),
|
|
155
|
+
schema: changelogSchema
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (existsSync("./changelog/patterns.yaml")) {
|
|
159
|
+
collections.patterns = defineCollection({
|
|
160
|
+
loader: file("changelog/patterns.yaml"),
|
|
161
|
+
schema: patternsSchema
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return { collections };
|
|
157
165
|
}
|
|
158
166
|
export {
|
|
159
167
|
defineBookSchemas
|
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": "3.0.0-alpha.
|
|
4
|
+
"version": "3.0.0-alpha.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Brandon Behring",
|
|
@@ -95,6 +95,7 @@
|
|
|
95
95
|
},
|
|
96
96
|
"files": [
|
|
97
97
|
"dist",
|
|
98
|
+
"src/lib",
|
|
98
99
|
"components",
|
|
99
100
|
"styles",
|
|
100
101
|
"layouts",
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/lib/chapters.ts — ordering + nav helpers for the chapters collection.
|
|
3
|
+
*
|
|
4
|
+
* Centralizes the sort key (part × 100 + chapter) and draft filtering so
|
|
5
|
+
* components and static-path generation use the same logic.
|
|
6
|
+
*/
|
|
7
|
+
import { getCollection, type CollectionEntry } from 'astro:content';
|
|
8
|
+
|
|
9
|
+
export type Chapter = CollectionEntry<'chapters'>;
|
|
10
|
+
|
|
11
|
+
/** Numeric sort key; chapters within a part come before a higher part. */
|
|
12
|
+
export function sortKey(c: Chapter): number {
|
|
13
|
+
return c.data.part * 1000 + c.data.chapter;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** All non-draft chapters, ordered by part+chapter ascending. */
|
|
17
|
+
export async function getAllChapters(): Promise<Chapter[]> {
|
|
18
|
+
const all = await getCollection('chapters', (entry) => !entry.data.draft);
|
|
19
|
+
return all.sort((a, b) => sortKey(a) - sortKey(b));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Given a chapter id, return its ordered neighbors.
|
|
24
|
+
* Either may be null at the edges of the book.
|
|
25
|
+
*/
|
|
26
|
+
export async function getNeighbors(id: string): Promise<{
|
|
27
|
+
prev: Chapter | null;
|
|
28
|
+
next: Chapter | null;
|
|
29
|
+
}> {
|
|
30
|
+
const all = await getAllChapters();
|
|
31
|
+
const idx = all.findIndex((c) => c.id === id);
|
|
32
|
+
if (idx === -1) return { prev: null, next: null };
|
|
33
|
+
return {
|
|
34
|
+
prev: idx > 0 ? all[idx - 1] : null,
|
|
35
|
+
next: idx < all.length - 1 ? all[idx + 1] : null,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/lib/freshness.ts — volatility-aware staleness computation.
|
|
3
|
+
*
|
|
4
|
+
* Each chapter carries a `last_verified` date and a `volatility` class.
|
|
5
|
+
* This module maps those to a freshness status the reader can trust.
|
|
6
|
+
*
|
|
7
|
+
* Thresholds chosen to align with Ch 15's source-tier audit cadences:
|
|
8
|
+
* stable-principle → 365 days (principles drift annually at most)
|
|
9
|
+
* architectural-pattern → 180 days (between annual and quarterly; "on
|
|
10
|
+
* major release" isn't derivable from
|
|
11
|
+
* frontmatter)
|
|
12
|
+
* feature-surface → 90 days (quarterly)
|
|
13
|
+
*
|
|
14
|
+
* Status bands (fraction of threshold):
|
|
15
|
+
* fresh (<75%) — green dot, unobtrusive
|
|
16
|
+
* verify-soon (75-100%) — yellow, mild warning
|
|
17
|
+
* stale (>100%) — amber/red, "verify before trusting"
|
|
18
|
+
*
|
|
19
|
+
* Example assertions (verified by visual inspection during Stage 3.1):
|
|
20
|
+
* getFreshness(today, 'feature-surface').status === 'fresh'
|
|
21
|
+
* getFreshness(today-70d, 'feature-surface').status === 'verify-soon'
|
|
22
|
+
* getFreshness(today-100d, 'feature-surface').status === 'stale'
|
|
23
|
+
* getFreshness(today-200d, 'stable-principle').status === 'fresh'
|
|
24
|
+
* getFreshness(today-300d, 'stable-principle').status === 'verify-soon'
|
|
25
|
+
*/
|
|
26
|
+
import type { volatilityLevels } from '../schemas.js';
|
|
27
|
+
|
|
28
|
+
export type VolatilityLevel = (typeof volatilityLevels)[number];
|
|
29
|
+
|
|
30
|
+
export type FreshnessStatus = 'fresh' | 'verify-soon' | 'stale';
|
|
31
|
+
|
|
32
|
+
export interface Freshness {
|
|
33
|
+
status: FreshnessStatus;
|
|
34
|
+
daysOld: number;
|
|
35
|
+
thresholdDays: number;
|
|
36
|
+
/** Days until stale; negative when already stale. */
|
|
37
|
+
daysUntil: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const THRESHOLDS: Record<VolatilityLevel, number> = {
|
|
41
|
+
'stable-principle': 365,
|
|
42
|
+
'architectural-pattern': 180,
|
|
43
|
+
'feature-surface': 90,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const MS_PER_DAY = 1000 * 60 * 60 * 24;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Compute freshness for a chapter given its last_verified date + volatility.
|
|
50
|
+
*
|
|
51
|
+
* Pure function; caller supplies `now` only in tests. Production callers omit.
|
|
52
|
+
*/
|
|
53
|
+
export function getFreshness(
|
|
54
|
+
lastVerified: Date,
|
|
55
|
+
volatility: VolatilityLevel,
|
|
56
|
+
now: Date = new Date(),
|
|
57
|
+
): Freshness {
|
|
58
|
+
const thresholdDays = THRESHOLDS[volatility];
|
|
59
|
+
const daysOld = Math.floor((now.getTime() - lastVerified.getTime()) / MS_PER_DAY);
|
|
60
|
+
const daysUntil = thresholdDays - daysOld;
|
|
61
|
+
|
|
62
|
+
let status: FreshnessStatus;
|
|
63
|
+
if (daysOld < thresholdDays * 0.75) {
|
|
64
|
+
status = 'fresh';
|
|
65
|
+
} else if (daysOld < thresholdDays) {
|
|
66
|
+
status = 'verify-soon';
|
|
67
|
+
} else {
|
|
68
|
+
status = 'stale';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { status, daysOld, thresholdDays, daysUntil };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Human-readable label for each status; used for ARIA + tooltips. */
|
|
75
|
+
export function freshnessLabel(f: Freshness): string {
|
|
76
|
+
switch (f.status) {
|
|
77
|
+
case 'fresh':
|
|
78
|
+
return `Fresh (${f.daysOld}d old; verify within ${f.daysUntil}d)`;
|
|
79
|
+
case 'verify-soon':
|
|
80
|
+
return `Verify soon (${f.daysOld}d old; ${f.daysUntil}d until stale)`;
|
|
81
|
+
case 'stale':
|
|
82
|
+
return `Stale (${f.daysOld}d old; ${Math.abs(f.daysUntil)}d past threshold)`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/lib/katex-macros.ts — KaTeX macro definitions ported from the LaTeX guide.
|
|
3
|
+
*
|
|
4
|
+
* Source files in the LaTeX tree:
|
|
5
|
+
* - guides/shared/ssm-notation.tex (20 SSM-specific macros)
|
|
6
|
+
* - guides/shared/ssm-guide-preamble.sty:387-413 (16 general math macros)
|
|
7
|
+
*
|
|
8
|
+
* Wired into astro.config.mjs via:
|
|
9
|
+
* rehypeKatex({ strict: 'error', macros: ssmMacros, trust: true })
|
|
10
|
+
*
|
|
11
|
+
* KaTeX-specific adaptations:
|
|
12
|
+
* - \bm is not part of KaTeX; aliased to \boldsymbol (visually identical
|
|
13
|
+
* in stix-two / Computer Modern math fonts).
|
|
14
|
+
* - \DeclareMathOperator and \DeclareMathOperator* are translated to
|
|
15
|
+
* \operatorname / \operatorname* respectively.
|
|
16
|
+
* - \psmallmatrix is an environment in amsmath, not a macro. KaTeX
|
|
17
|
+
* macros operate at command level; substitution happens in the
|
|
18
|
+
* converter script (latex-to-mdx.mjs in Phase 2.7) — search for
|
|
19
|
+
* \begin{psmallmatrix} and replace with \begin{pmatrix}.
|
|
20
|
+
*
|
|
21
|
+
* Equation numbering is handled by a separate remark plugin (deferred to
|
|
22
|
+
* a follow-up session per plan Phase 2.1 Task #9).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export const ssmMacros: Record<string, string> = {
|
|
26
|
+
// -----------------------------------------------------------------
|
|
27
|
+
// Compatibility alias: \bm{x} -> \boldsymbol{x}
|
|
28
|
+
// KaTeX does not ship \bm. The LaTeX source uses \bm extensively in
|
|
29
|
+
// ssm-notation.tex for vectors and matrices.
|
|
30
|
+
// -----------------------------------------------------------------
|
|
31
|
+
'\\bm': '\\boldsymbol{#1}',
|
|
32
|
+
|
|
33
|
+
// -----------------------------------------------------------------
|
|
34
|
+
// SSM state space variables (ssm-notation.tex:5-13)
|
|
35
|
+
// -----------------------------------------------------------------
|
|
36
|
+
'\\statevec': '\\boldsymbol{h}', // state vector h_t
|
|
37
|
+
'\\statemat': '\\boldsymbol{A}', // dynamics matrix A
|
|
38
|
+
'\\inputmat': '\\boldsymbol{B}', // input matrix B
|
|
39
|
+
'\\outputmat': '\\boldsymbol{C}', // output matrix C
|
|
40
|
+
'\\feedmat': '\\boldsymbol{D}', // feedthrough matrix D
|
|
41
|
+
'\\stepsize': '\\Delta', // discretization step
|
|
42
|
+
'\\discA': '\\bar{\\boldsymbol{A}}', // discretized A
|
|
43
|
+
'\\discB': '\\bar{\\boldsymbol{B}}', // discretized B
|
|
44
|
+
|
|
45
|
+
// -----------------------------------------------------------------
|
|
46
|
+
// Dimensions (ssm-notation.tex:16-18)
|
|
47
|
+
// -----------------------------------------------------------------
|
|
48
|
+
'\\seqlen': 'L', // sequence length
|
|
49
|
+
'\\statedim': 'N', // state dimension
|
|
50
|
+
'\\inputdim': 'D', // input / model dimension
|
|
51
|
+
|
|
52
|
+
// -----------------------------------------------------------------
|
|
53
|
+
// Scan operator (ssm-notation.tex:23-24)
|
|
54
|
+
// -----------------------------------------------------------------
|
|
55
|
+
'\\scanop': '\\oplus', // associative binary operator for diagonal SSMs
|
|
56
|
+
'\\elemwise': '\\odot', // element-wise product
|
|
57
|
+
|
|
58
|
+
// -----------------------------------------------------------------
|
|
59
|
+
// Dynamical systems (ssm-notation.tex:27-30)
|
|
60
|
+
// -----------------------------------------------------------------
|
|
61
|
+
'\\monodromy': '\\boldsymbol{Z}', // monodromy matrix Z(T)
|
|
62
|
+
'\\floquet': '\\mu', // Floquet multiplier
|
|
63
|
+
'\\lyapexp': '\\lambda', // Lyapunov exponent
|
|
64
|
+
'\\jacobian': '\\boldsymbol{J}', // Jacobian matrix
|
|
65
|
+
|
|
66
|
+
// -----------------------------------------------------------------
|
|
67
|
+
// Calculus shortcuts (ssm-notation.tex:33-35)
|
|
68
|
+
// -----------------------------------------------------------------
|
|
69
|
+
'\\ddt': '\\frac{d}{dt}',
|
|
70
|
+
'\\pderiv': '\\frac{\\partial #1}{\\partial #2}', // 2 args
|
|
71
|
+
'\\spectralradius': '\\rho',
|
|
72
|
+
|
|
73
|
+
// -----------------------------------------------------------------
|
|
74
|
+
// Common sets (preamble:390-393)
|
|
75
|
+
// -----------------------------------------------------------------
|
|
76
|
+
'\\R': '\\mathbb{R}',
|
|
77
|
+
'\\C': '\\mathbb{C}',
|
|
78
|
+
'\\N': '\\mathbb{N}',
|
|
79
|
+
'\\Z': '\\mathbb{Z}',
|
|
80
|
+
|
|
81
|
+
// -----------------------------------------------------------------
|
|
82
|
+
// Probability / statistics (preamble:396-397)
|
|
83
|
+
// -----------------------------------------------------------------
|
|
84
|
+
'\\E': '\\mathbb{E}',
|
|
85
|
+
'\\Prob': '\\mathbb{P}',
|
|
86
|
+
|
|
87
|
+
// -----------------------------------------------------------------
|
|
88
|
+
// Norms and inner products (preamble:400-402)
|
|
89
|
+
// -----------------------------------------------------------------
|
|
90
|
+
'\\norm': '\\lVert #1 \\rVert',
|
|
91
|
+
'\\ip': '\\langle #1, #2 \\rangle',
|
|
92
|
+
'\\abs': '\\lvert #1 \\rvert',
|
|
93
|
+
|
|
94
|
+
// -----------------------------------------------------------------
|
|
95
|
+
// Operators (preamble:405-410)
|
|
96
|
+
// \DeclareMathOperator* -> \operatorname* (with limits below in display)
|
|
97
|
+
// \DeclareMathOperator -> \operatorname
|
|
98
|
+
// -----------------------------------------------------------------
|
|
99
|
+
'\\argmax': '\\operatorname*{arg\\,max}',
|
|
100
|
+
'\\argmin': '\\operatorname*{arg\\,min}',
|
|
101
|
+
'\\diag': '\\operatorname{diag}',
|
|
102
|
+
'\\tr': '\\operatorname{tr}',
|
|
103
|
+
'\\spec': '\\operatorname{spec}',
|
|
104
|
+
'\\rank': '\\operatorname{rank}',
|
|
105
|
+
|
|
106
|
+
// -----------------------------------------------------------------
|
|
107
|
+
// Complexity (preamble:413)
|
|
108
|
+
// -----------------------------------------------------------------
|
|
109
|
+
'\\bigO': '\\mathcal{O}(#1)',
|
|
110
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/lib/patterns.ts — helpers for the convergence dashboard.
|
|
3
|
+
*
|
|
4
|
+
* Joins the patterns collection (registry of agentic-coding patterns)
|
|
5
|
+
* with the changelog collection (per-tool version timelines) to answer
|
|
6
|
+
* the two questions the dashboard surfaces:
|
|
7
|
+
*
|
|
8
|
+
* 1. For a given pattern, which tools adopted it and when?
|
|
9
|
+
* 2. What is the set of tracked patterns, in a stable display order?
|
|
10
|
+
*
|
|
11
|
+
* Neither file format stores the join directly; these helpers compute
|
|
12
|
+
* it lazily at build time so new tool/version rows require no code
|
|
13
|
+
* changes.
|
|
14
|
+
*/
|
|
15
|
+
import { getCollection, type CollectionEntry } from 'astro:content';
|
|
16
|
+
import {
|
|
17
|
+
toolSlugs,
|
|
18
|
+
patternCategories,
|
|
19
|
+
changeKinds,
|
|
20
|
+
} from '../content.config';
|
|
21
|
+
|
|
22
|
+
export type PatternEntry = CollectionEntry<'patterns'>;
|
|
23
|
+
export type ToolSlug = (typeof toolSlugs)[number];
|
|
24
|
+
export type PatternCategory = (typeof patternCategories)[number];
|
|
25
|
+
export type ChangeKind = (typeof changeKinds)[number];
|
|
26
|
+
|
|
27
|
+
export interface TimelineEntry {
|
|
28
|
+
tool: ToolSlug;
|
|
29
|
+
version: string;
|
|
30
|
+
date: Date;
|
|
31
|
+
kind: ChangeKind;
|
|
32
|
+
note: string;
|
|
33
|
+
sourceKey?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Per-pattern timeline: every (tool, version, change) triple where
|
|
38
|
+
* change.pattern === slug, sorted by date ascending. A pattern that no
|
|
39
|
+
* tool has adopted yet returns an empty array.
|
|
40
|
+
*/
|
|
41
|
+
export async function getPatternTimeline(slug: string): Promise<TimelineEntry[]> {
|
|
42
|
+
const tools = await getCollection('changelog');
|
|
43
|
+
const out: TimelineEntry[] = [];
|
|
44
|
+
for (const entry of tools) {
|
|
45
|
+
const tool = entry.data.tool;
|
|
46
|
+
for (const v of entry.data.versions) {
|
|
47
|
+
for (const change of v.changes) {
|
|
48
|
+
if (change.pattern !== slug) continue;
|
|
49
|
+
out.push({
|
|
50
|
+
tool,
|
|
51
|
+
version: v.version,
|
|
52
|
+
date: v.date,
|
|
53
|
+
kind: change.kind,
|
|
54
|
+
note: change.note,
|
|
55
|
+
sourceKey: change.source_key,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
out.sort((a, b) => a.date.getTime() - b.date.getTime());
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* All patterns in stable display order: converged patterns first (by
|
|
66
|
+
* convergence_date ascending — oldest convergence first), then
|
|
67
|
+
* unconverged patterns (by slug). Within each bucket, preserves
|
|
68
|
+
* manifest order.
|
|
69
|
+
*/
|
|
70
|
+
export async function getAllPatterns(): Promise<PatternEntry[]> {
|
|
71
|
+
const all = await getCollection('patterns');
|
|
72
|
+
return all.sort((a, b) => {
|
|
73
|
+
const aDate = a.data.convergence_date?.getTime();
|
|
74
|
+
const bDate = b.data.convergence_date?.getTime();
|
|
75
|
+
if (aDate != null && bDate != null) return aDate - bDate;
|
|
76
|
+
if (aDate != null) return -1;
|
|
77
|
+
if (bDate != null) return 1;
|
|
78
|
+
return a.id.localeCompare(b.id);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Patterns grouped by category; categories that have no patterns are
|
|
84
|
+
* still emitted with an empty array so the dashboard can render
|
|
85
|
+
* honest "no patterns yet" placeholders without checking undefined.
|
|
86
|
+
*/
|
|
87
|
+
export async function getPatternsByCategory(): Promise<
|
|
88
|
+
Record<PatternCategory, PatternEntry[]>
|
|
89
|
+
> {
|
|
90
|
+
const all = await getAllPatterns();
|
|
91
|
+
const grouped = Object.fromEntries(
|
|
92
|
+
patternCategories.map((c) => [c, [] as PatternEntry[]]),
|
|
93
|
+
) as Record<PatternCategory, PatternEntry[]>;
|
|
94
|
+
for (const p of all) {
|
|
95
|
+
const cat = p.data.category ?? 'other';
|
|
96
|
+
grouped[cat].push(p);
|
|
97
|
+
}
|
|
98
|
+
return grouped;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Human-readable labels per category. */
|
|
102
|
+
export const CATEGORY_LABELS: Record<PatternCategory, string> = {
|
|
103
|
+
safety: 'Safety',
|
|
104
|
+
scale: 'Scale',
|
|
105
|
+
context: 'Context',
|
|
106
|
+
interaction: 'Interaction',
|
|
107
|
+
extension: 'Extension',
|
|
108
|
+
other: 'Other',
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Which tools have adopted a given pattern (deduplicated). Helpful
|
|
113
|
+
* for computing "adopted by N of 3 tools" summary counts.
|
|
114
|
+
*/
|
|
115
|
+
export function toolsInTimeline(timeline: TimelineEntry[]): ToolSlug[] {
|
|
116
|
+
const seen = new Set<ToolSlug>();
|
|
117
|
+
for (const e of timeline) seen.add(e.tool);
|
|
118
|
+
return Array.from(seen);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Fixed display order for tool rows on the timeline. 'cross-tool' is
|
|
122
|
+
* excluded because a pattern is something tools adopt individually. */
|
|
123
|
+
export const TIMELINE_TOOL_ORDER: ToolSlug[] = [
|
|
124
|
+
'claude-code',
|
|
125
|
+
'gemini-cli',
|
|
126
|
+
'codex-cli',
|
|
127
|
+
];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repository URL constants used by CodeRef and CodeBlock components.
|
|
3
|
+
*
|
|
4
|
+
* Centralized here so renaming the repo or moving branches is a one-file
|
|
5
|
+
* change rather than a hunt across components.
|
|
6
|
+
*
|
|
7
|
+
* If the repo is later mirrored to a different host, update GITHUB_BASE
|
|
8
|
+
* accordingly; the components do not assume GitHub-specific anchor syntax
|
|
9
|
+
* beyond #L<N>-L<M>, which all major hosts (GitHub, GitLab, Codeberg)
|
|
10
|
+
* support.
|
|
11
|
+
*/
|
|
12
|
+
export const GITHUB_REPO = 'brandon-behring/post_transformers';
|
|
13
|
+
export const GITHUB_BRANCH = 'main';
|
|
14
|
+
export const GITHUB_BASE = `https://github.com/${GITHUB_REPO}/blob/${GITHUB_BRANCH}`;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build a GitHub line-anchor URL.
|
|
18
|
+
* buildGithubUrl('experiments/jax/week04/s4.py', 42) -> .../s4.py#L42
|
|
19
|
+
* buildGithubUrl('experiments/jax/week04/s4.py', 42, 58) -> .../s4.py#L42-L58
|
|
20
|
+
* buildGithubUrl('experiments/jax/week04/s4.py') -> .../s4.py
|
|
21
|
+
*/
|
|
22
|
+
export function buildGithubUrl(path: string, line?: number, lineEnd?: number): string {
|
|
23
|
+
const cleanPath = path.replace(/^\/+/, '');
|
|
24
|
+
let url = `${GITHUB_BASE}/${cleanPath}`;
|
|
25
|
+
if (line !== undefined) {
|
|
26
|
+
url += `#L${line}`;
|
|
27
|
+
if (lineEnd !== undefined && lineEnd !== line) {
|
|
28
|
+
url += `-L${lineEnd}`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return url;
|
|
32
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/lib/sources.ts — helpers for the sources collection.
|
|
3
|
+
*
|
|
4
|
+
* `sources/manifest.yaml` drives every citation in the book. This
|
|
5
|
+
* module centralizes tier grouping + ordering so the SourceArchive
|
|
6
|
+
* component and any future dashboards share a single source of truth.
|
|
7
|
+
*/
|
|
8
|
+
import { getCollection, type CollectionEntry } from 'astro:content';
|
|
9
|
+
import { sourceTiers } from '../content.config';
|
|
10
|
+
|
|
11
|
+
export type SourceEntry = CollectionEntry<'sources'>;
|
|
12
|
+
export type SourceTier = (typeof sourceTiers)[number];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Grouped source entries keyed by tier, in descending authority
|
|
16
|
+
* (T1-official → T4-conjecture). Empty tiers map to an empty array
|
|
17
|
+
* so callers can render "no sources yet" placeholders without
|
|
18
|
+
* checking for undefined.
|
|
19
|
+
*
|
|
20
|
+
* Within each tier, entries sort by publish_date descending (newest
|
|
21
|
+
* first); entries without a publish_date sort after those with one.
|
|
22
|
+
*/
|
|
23
|
+
export async function getSourcesByTier(): Promise<Record<SourceTier, SourceEntry[]>> {
|
|
24
|
+
const all = await getCollection('sources');
|
|
25
|
+
const grouped: Record<SourceTier, SourceEntry[]> = {
|
|
26
|
+
'T1-official': [],
|
|
27
|
+
'T2-release-notes': [],
|
|
28
|
+
'T3-practitioner': [],
|
|
29
|
+
'T4-conjecture': [],
|
|
30
|
+
};
|
|
31
|
+
for (const entry of all) {
|
|
32
|
+
grouped[entry.data.tier].push(entry);
|
|
33
|
+
}
|
|
34
|
+
for (const tier of sourceTiers) {
|
|
35
|
+
grouped[tier].sort((a, b) => {
|
|
36
|
+
const ad = a.data.publish_date?.getTime() ?? -Infinity;
|
|
37
|
+
const bd = b.data.publish_date?.getTime() ?? -Infinity;
|
|
38
|
+
return bd - ad;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return grouped;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Human-readable description for each tier (used as empty-state label). */
|
|
45
|
+
export const TIER_LABELS: Record<SourceTier, string> = {
|
|
46
|
+
'T1-official': 'T1 · Official',
|
|
47
|
+
'T2-release-notes': 'T2 · Release notes',
|
|
48
|
+
'T3-practitioner': 'T3 · Practitioner',
|
|
49
|
+
'T4-conjecture': 'T4 · Conjecture',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** One-line explanation per tier (shown under the heading). */
|
|
53
|
+
export const TIER_DESCRIPTIONS: Record<SourceTier, string> = {
|
|
54
|
+
'T1-official':
|
|
55
|
+
'Vendor-official documentation or release notes. Highest trust for factual claims about the vendor\u2019s own tool.',
|
|
56
|
+
'T2-release-notes':
|
|
57
|
+
'Release blog posts, changelogs, conference talks. Trustworthy for intent and availability claims.',
|
|
58
|
+
'T3-practitioner':
|
|
59
|
+
'Respected community writing with a durable argument the author has defended over time.',
|
|
60
|
+
'T4-conjecture':
|
|
61
|
+
'Blog posts, tweets, or unverified claims. Pointers to investigate, not authorities.',
|
|
62
|
+
};
|